diff --git a/docs/api-output.md b/docs/api-output.md index 435af5484..4795930da 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -120,6 +120,7 @@ sRGB colour space and strip all metadata, including the removal of any ICC profi - `options.orientation` **[number][9]?** value between 1 and 8, used to update the EXIF `Orientation` tag. - `options.icc` **[string][2]?** filesystem path to output ICC profile, defaults to sRGB. - `options.exif` **[Object][6]<[Object][6]>** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default `{}`) + - `options.density` **[number][9]?** Number of pixels per inch (DPI). ### Examples @@ -132,7 +133,7 @@ sharp('input.jpg') ```javascript // Set "IFD0-Copyright" in output EXIF metadata -await sharp(input) +const data = await sharp(input) .withMetadata({ exif: { IFD0: { @@ -141,6 +142,12 @@ await sharp(input) } }) .toBuffer(); + + * @example +// Set output metadata to 96 DPI +const data = await sharp(input) + .withMetadata({ density: 96 }) + .toBuffer(); ``` - Throws **[Error][4]** Invalid parameters diff --git a/docs/changelog.md b/docs/changelog.md index acde494c7..b5696abd9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,9 @@ Requires libvips v8.10.6 ### v0.28.2 - TBD +* Allow `withMetadata` to set `density`. + [#967](https://github.com/lovell/sharp/issues/967) + * Skip shrink-on-load where one dimension <4px. [#2653](https://github.com/lovell/sharp/issues/2653) diff --git a/lib/constructor.js b/lib/constructor.js index ab992180f..618d1910b 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -231,6 +231,7 @@ const Sharp = function (input, options) { streamOut: false, withMetadata: false, withMetadataOrientation: -1, + withMetadataDensity: 0, withMetadataIcc: '', withMetadataStrs: {}, resolveWithObject: false, diff --git a/lib/output.js b/lib/output.js index 4ea73a470..98bbcf616 100644 --- a/lib/output.js +++ b/lib/output.js @@ -150,7 +150,7 @@ function toBuffer (options, callback) { * * @example * // Set "IFD0-Copyright" in output EXIF metadata - * await sharp(input) + * const data = await sharp(input) * .withMetadata({ * exif: { * IFD0: { @@ -160,10 +160,17 @@ function toBuffer (options, callback) { * }) * .toBuffer(); * + * * @example + * // Set output metadata to 96 DPI + * const data = await sharp(input) + * .withMetadata({ density: 96 }) + * .toBuffer(); + * * @param {Object} [options] * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. * @param {string} [options.icc] filesystem path to output ICC profile, defaults to sRGB. * @param {Object} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @param {number} [options.density] Number of pixels per inch (DPI). * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -177,6 +184,13 @@ function withMetadata (options) { throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation); } } + if (is.defined(options.density)) { + if (is.number(options.density) && options.density > 0) { + this.options.withMetadataDensity = options.density; + } else { + throw is.invalidParameterError('density', 'positive number', options.density); + } + } if (is.defined(options.icc)) { if (is.string(options.icc)) { this.options.withMetadataIcc = options.icc; diff --git a/package.json b/package.json index 0094524a9..fab9a3a85 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "async": "^3.2.0", "cc": "^3.0.1", "decompress-zip": "^0.3.3", - "documentation": "^13.2.0", + "documentation": "^13.2.1", "exif-reader": "^1.0.3", "icc": "^2.0.0", "license-checker": "^25.0.1", diff --git a/src/common.cc b/src/common.cc index ae25df028..353a64905 100644 --- a/src/common.cc +++ b/src/common.cc @@ -520,9 +520,8 @@ namespace sharp { VImage SetDensity(VImage image, const double density) { const double pixelsPerMm = density / 25.4; VImage copy = image.copy(); - copy.set("Xres", pixelsPerMm); - copy.set("Yres", pixelsPerMm); - copy.set(VIPS_META_RESOLUTION_UNIT, "in"); + copy.get_image()->Xres = pixelsPerMm; + copy.get_image()->Yres = pixelsPerMm; return copy; } diff --git a/src/pipeline.cc b/src/pipeline.cc index 30c334c1b..29a9664ea 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -722,6 +722,10 @@ class PipelineWorker : public Napi::AsyncWorker { if (baton->withMetadata && baton->withMetadataOrientation != -1) { image = sharp::SetExifOrientation(image, baton->withMetadataOrientation); } + // Override pixel density + if (baton->withMetadataDensity > 0) { + image = sharp::SetDensity(image, baton->withMetadataDensity); + } // Metadata key/value pairs, e.g. EXIF if (!baton->withMetadataStrs.empty()) { image = image.copy(); @@ -1385,6 +1389,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->fileOut = sharp::AttrAsStr(options, "fileOut"); baton->withMetadata = sharp::AttrAsBool(options, "withMetadata"); baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation"); + baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity"); baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc"); Napi::Object mdStrs = options.Get("withMetadataStrs").As(); Napi::Array mdStrKeys = mdStrs.GetPropertyNames(); diff --git a/src/pipeline.h b/src/pipeline.h index db6387a26..70d7ba9bf 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -168,6 +168,7 @@ struct PipelineBaton { std::string err; bool withMetadata; int withMetadataOrientation; + double withMetadataDensity; std::string withMetadataIcc; std::unordered_map withMetadataStrs; std::unique_ptr convKernel; @@ -290,6 +291,7 @@ struct PipelineBaton { heifLossless(false), withMetadata(false), withMetadataOrientation(-1), + withMetadataDensity(0.0), convKernelWidth(0), convKernelHeight(0), convKernelScale(0.0), diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 8474a56a5..111b1d59a 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -623,6 +623,44 @@ describe('Image metadata', function () { assert.strictEqual(parsedExif.exif.ExposureTime, 0.2); }); + it('Set density of JPEG', async () => { + const data = await sharp({ + create: { + width: 8, + height: 8, + channels: 3, + background: 'red' + } + }) + .withMetadata({ + density: 300 + }) + .jpeg() + .toBuffer(); + + const { density } = await sharp(data).metadata(); + assert.strictEqual(density, 300); + }); + + it('Set density of PNG', async () => { + const data = await sharp({ + create: { + width: 8, + height: 8, + channels: 3, + background: 'red' + } + }) + .withMetadata({ + density: 96 + }) + .png() + .toBuffer(); + + const { density } = await sharp(data).metadata(); + assert.strictEqual(density, 96); + }); + it('chromaSubsampling 4:4:4:4 CMYK JPEG', function () { return sharp(fixtures.inputJpgWithCmykProfile) .metadata() @@ -736,6 +774,16 @@ describe('Image metadata', function () { sharp().withMetadata({ orientation: 9 }); }); }); + it('Non-numeric density', function () { + assert.throws(function () { + sharp().withMetadata({ density: '1' }); + }); + }); + it('Negative density', function () { + assert.throws(function () { + sharp().withMetadata({ density: -1 }); + }); + }); it('Non string icc', function () { assert.throws(function () { sharp().withMetadata({ icc: true });