diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 068dd991..55a442ba 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -63,6 +63,7 @@ Implements the [stream.Duplex][1] class. * `options.text.dpi` **[number][14]** the resolution (size) at which to render the text. Does not take effect if `height` is specified. (optional, default `72`) * `options.text.rgba` **[boolean][15]** set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for pango markup features like `Red!`. (optional, default `false`) * `options.text.spacing` **[number][14]** text line height in points. Will use the font line height if none is specified. (optional, default `0`) + * `options.text.wrap` **[number][14]** word wrapping style when width is provided, one of: 'word', 'char', 'charWord' (prefer char, fallback to word) or 'none'. (optional, default `'word'`) ### Examples diff --git a/docs/api-output.md b/docs/api-output.md index 87e81130..6e10ff81 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -328,8 +328,8 @@ The palette of the input image will be re-used if possible. * `options` **[Object][6]?** output options - * `options.reoptimise` **[boolean][10]** always generate new palettes (slow), re-use existing by default (optional, default `false`) - * `options.reoptimize` **[boolean][10]** alternative spelling of `options.reoptimise` (optional, default `false`) + * `options.reuse` **[boolean][10]** re-use existing palette, otherwise generate new (slow) (optional, default `true`) + * `options.progressive` **[boolean][10]** use progressive (interlace) scan (optional, default `false`) * `options.colours` **[number][12]** maximum number of palette entries, including transparency, between 2 and 256 (optional, default `256`) * `options.colors` **[number][12]** alternative spelling of `options.colours` (optional, default `256`) * `options.effort` **[number][12]** CPU effort, between 1 (fastest) and 10 (slowest) (optional, default `7`) diff --git a/docs/changelog.md b/docs/changelog.md index 633758cf..00f040bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,28 @@ # Changelog +## v0.32 - *flow* + +Requires libvips v8.14.0 + +### v0.32.0 - TBD + +* Replace GIF output `optimise` / `optimize` option with `reuse`. + +* Add `progressive` option to GIF output for interlacing. + +* Add `wrap` option to text image creation. + +* Add `formatMagick` property to metadata of images loaded via *magick. + +* Allow use of GPS (IFD3) EXIF metadata. + [#2767](https://github.com/lovell/sharp/issues/2767) + +* Prebuilt binaries: ensure macOS 10.13+ support, as documented. + [#3438](https://github.com/lovell/sharp/issues/3438) + +* Prebuilt binaries: prevent use of glib slice allocator, improves QEMU support. + [#3448](https://github.com/lovell/sharp/issues/3448) + ## v0.31 - *eagle* Requires libvips v8.13.3 diff --git a/lib/constructor.js b/lib/constructor.js index 51947e5b..f0a06e46 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -158,6 +158,7 @@ const debuglog = util.debuglog('sharp'); * @param {number} [options.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified. * @param {boolean} [options.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for pango markup features like `Red!`. * @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified. + * @param {number} [options.text.wrap='word'] - word wrapping style when width is provided, one of: 'word', 'char', 'charWord' (prefer char, fallback to word) or 'none'. * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -291,7 +292,8 @@ const Sharp = function (input, options) { gifDither: 1, gifInterFrameMaxError: 0, gifInterPaletteMaxError: 3, - gifReoptimise: false, + gifReuse: true, + gifProgressive: false, tiffQuality: 80, tiffCompression: 'jpeg', tiffPredictor: 'horizontal', diff --git a/lib/input.js b/lib/input.js index 29b7c86e..e6c7aece 100644 --- a/lib/input.js +++ b/lib/input.js @@ -327,6 +327,13 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('text.spacing', 'number', inputOptions.text.spacing); } } + if (is.defined(inputOptions.text.wrap)) { + if (is.string(inputOptions.text.wrap) && is.inArray(inputOptions.text.wrap, ['word', 'char', 'wordChar', 'none'])) { + inputDescriptor.textWrap = inputOptions.text.wrap; + } else { + throw is.invalidParameterError('text.wrap', 'one of: word, char, wordChar, none', inputOptions.text.wrap); + } + } delete inputDescriptor.buffer; } else { throw new Error('Expected a valid string to create an image with text.'); diff --git a/lib/output.js b/lib/output.js index ee5514f8..d4445e51 100644 --- a/lib/output.js +++ b/lib/output.js @@ -559,8 +559,8 @@ function webp (options) { * .toFile('optim.gif'); * * @param {Object} [options] - output options - * @param {boolean} [options.reoptimise=false] - always generate new palettes (slow), re-use existing by default - * @param {boolean} [options.reoptimize=false] - alternative spelling of `options.reoptimise` + * @param {boolean} [options.reuse=true] - re-use existing palette, otherwise generate new (slow) + * @param {boolean} [options.progressive=false] - use progressive (interlace) scan * @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256 * @param {number} [options.colors=256] - alternative spelling of `options.colours` * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest) @@ -575,10 +575,11 @@ function webp (options) { */ function gif (options) { if (is.object(options)) { - if (is.defined(options.reoptimise)) { - this._setBooleanOption('gifReoptimise', options.reoptimise); - } else if (is.defined(options.reoptimize)) { - this._setBooleanOption('gifReoptimise', options.reoptimize); + if (is.defined(options.reuse)) { + this._setBooleanOption('gifReuse', options.reuse); + } + if (is.defined(options.progressive)) { + this._setBooleanOption('gifProgressive', options.progressive); } const colours = options.colours || options.colors; if (is.defined(colours)) { @@ -690,7 +691,7 @@ function jp2 (options) { } if (is.defined(options.chromaSubsampling)) { if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) { - this.options.heifChromaSubsampling = options.chromaSubsampling; + this.options.jp2ChromaSubsampling = options.chromaSubsampling; } else { throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling); } diff --git a/package.json b/package.json index 7f74474a..098ac62a 100644 --- a/package.json +++ b/package.json @@ -155,19 +155,19 @@ }, "license": "Apache-2.0", "config": { - "libvips": "8.13.3", + "libvips": "8.14.0-rc1", "integrity": { - "darwin-arm64v8": "sha512-xFgYt7CtQSZcWoyUdzPTDNHbioZIrZSEU+gkMxzH4Cgjhi4/N49UsonnIZhKQoTBGloAqEexHeMx4rYTQ2Kgvw==", - "darwin-x64": "sha512-6SivWKzu15aUMMohe0wg7sNYMPETVnOe40BuWsnKOgzl3o5FpQqNSgs+68Mi8Za3Qti9/DaR+H/fyD0x48Af2w==", - "linux-arm64v8": "sha512-b+iI9V/ehgDabXYRQcvqa5CEysh+1FQsgFmYc358StCrJCDahwNmsQdsiH1GOVd5WaWh5wHUGByPwMmFOO16Aw==", - "linux-armv6": "sha512-zRP2F+EiustLE4bXSH8AHCxwfemh9d+QuvmPjira/HL6uJOUuA7SyQgVV1TPwTQle2ioCNnKPm7FEB/MAiT+ug==", - "linux-armv7": "sha512-6OCChowE5lBXXXAZrnGdA9dVktg7UdODEBpE5qTroiAJYZv4yXRMgyDFYajok7du2NTgoklhxGk8d9+4vGv5hg==", - "linux-x64": "sha512-OTmlmP2r8ozGKdB96X+K5oQE1ojVZanqLqqKlwDpEnfixyIaDGYbVzcjWBNGU3ai/26bvkaCkjynnc2ecYcsuA==", - "linuxmusl-arm64v8": "sha512-Qh5Wi+bkKTohFYHzSPssfjMhIkD6z6EHbVmnwmWSsgY9zsUBStFp6+mKcNTQfP5YM5Mz06vJOkLHX2OzEr5TzA==", - "linuxmusl-x64": "sha512-DwB4Fs3+ISw9etaLCANkueZDdk758iOS+wNp4TKZkHdq0al6B/3Pk7OHLR8a9E3H6wYDD328u++dcJzip5tacA==", - "win32-arm64v8": "sha512-96r3W+O4BtX602B1MtxU5Ru4lKzRRTZqM4OQEBJ//TNL3fiCZdd9agD+RQBjaeR4KFIyBSt3F7IE425ZWmxz+w==", - "win32-ia32": "sha512-qfN1MsfQGek1QQd1UNW7JT+5K5Ne1suFQ2GpgpYm3JLSpIve/tz2vOGEGzvTVssOBADJvAkTDFt+yIi3PgU9pA==", - "win32-x64": "sha512-eb3aAmjbVVBVRbiYgebQwoxkAt69WI8nwmKlilSQ3kWqoc0pXfIe322rF2UR8ebbISCGvYRUfzD2r1k92RXISQ==" + "darwin-arm64v8": "sha512-WWVRbtTdBGx7l0mZ1RkMsNzVkiR3923knivO21RKyRm2q6PJZunAd9Da3wVBfGnofhirlsbRkQLwPbFsdq/lkA==", + "darwin-x64": "sha512-tyYkU/Mma1LKbg2PQELpoeECw/SOp5BqKwasIuEAlddC7g1JWLpIYrxuAbH1kaWy3soEFBJbN1CxRNL0YWy9Ug==", + "linux-arm64v8": "sha512-m7yr3ZSL3NlEpDOlUrScTYR4Rj1b5cSt8PmWvIkOtEGRJjtvFufurCDroOaaOIdKi1uB5n23ziB2PwTZX7tyAA==", + "linux-armv6": "sha512-dy2t/7uyqjKXCgiYDbrouFp3pNqpzsPNpgkk3uT/c3wAhNcRvpVHbYMMrau0o/o9YULLLQ5aBW9HgEvjHCaqZw==", + "linux-armv7": "sha512-5bJhPzDrX3IaWU9Z3qMtE/C20bFAynN9YhaPF8gqFBMRkyif1ZnAlZl6PlVg/c968ABB4vJcAQJf4ImZJvBZLQ==", + "linux-x64": "sha512-13nlg8oc0V/uxFa7r+QIJpIOVZstxM2CE1LSUI7SUmy4h6h+Xf0ztMi+6w+kL7LibtLWpjTEHQuCBNxSWdt2VA==", + "linuxmusl-arm64v8": "sha512-3RrraBIynOBDQuKwhiDrulRXZh1BfiDfy2SIyEgkPC301UdGw4FHQntgYc9E6K9CzYGv8jb1S9/8hcrn2/+LyA==", + "linuxmusl-x64": "sha512-K+5eujn+pgH/vDcAJrtbTfZxJn9W9pfvJlTrtF0AjZdOBMwt9fcCeCeNVvKzKYcemzFmBHsjqxkgqlxA4xEnXQ==", + "win32-arm64v8": "sha512-VliwICXWqGERjk2I3wsw6qwFVtHZZ9jljNW8Xev51M9Y/gdttzVk+fWqiikhJD5K2Gp5sg1ccbvEsu42ZVQXxA==", + "win32-ia32": "sha512-dNJoDRDkYzkNCz0gGtIahOf8n2g+dBg0iNVHvYDAJV7qLfjAqtfQJZ9TG2p4PzPCM2H4kazSNL/JOxFF4bpc1g==", + "win32-x64": "sha512-myvHzlVgjztJ//crm8daXyGUa7ZciRfj3+L/faqyZsX2FMr4SImw4OO2UkCvDLJz6Fxar4biau3kAeGDJLCIQw==" }, "runtime": "napi", "target": 7 diff --git a/src/common.cc b/src/common.cc index 99602f8e..6c47cc17 100644 --- a/src/common.cc +++ b/src/common.cc @@ -167,6 +167,9 @@ namespace sharp { if (HasAttr(input, "textSpacing")) { descriptor->textSpacing = AttrAsUint32(input, "textSpacing"); } + if (HasAttr(input, "textWrap")) { + descriptor->textWrap = AttrAsEnum(input, "textWrap", VIPS_TYPE_TEXT_WRAP); + } } // Limit input images to a given number of pixels, where pixels = width * height descriptor->limitInputPixels = static_cast(AttrAsInt64(input, "limitInputPixels")); @@ -454,6 +457,7 @@ namespace sharp { ->set("justify", descriptor->textJustify) ->set("rgba", descriptor->textRgba) ->set("spacing", descriptor->textSpacing) + ->set("wrap", descriptor->textWrap) ->set("autofit_dpi", &descriptor->textAutofitDpi); if (descriptor->textWidth > 0) { textOptions->set("width", descriptor->textWidth); @@ -612,6 +616,15 @@ namespace sharp { return copy; } + /* + Remove GIF palette from image. + */ + VImage RemoveGifPalette(VImage image) { + VImage copy = image.copy(); + copy.remove("gif-palette"); + return copy; + } + /* Does this image have a non-default density? */ diff --git a/src/common.h b/src/common.h index 5995f977..513e9ec5 100644 --- a/src/common.h +++ b/src/common.h @@ -25,9 +25,9 @@ // Verify platform and compiler compatibility #if (VIPS_MAJOR_VERSION < 8) || \ - (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 13) || \ - (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 13 && VIPS_MICRO_VERSION < 3) -#error "libvips version 8.13.3+ is required - please see https://sharp.pixelplumbing.com/install" + (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 14) || \ + (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 14 && VIPS_MICRO_VERSION < 0) +#error "libvips version 8.14.0+ is required - please see https://sharp.pixelplumbing.com/install" #endif #if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) @@ -81,6 +81,7 @@ namespace sharp { int textDpi; bool textRgba; int textSpacing; + VipsTextWrap textWrap; int textAutofitDpi; InputDescriptor(): @@ -114,6 +115,7 @@ namespace sharp { textDpi(72), textRgba(FALSE), textSpacing(0), + textWrap(VIPS_TEXT_WRAP_WORD), textAutofitDpi(0) {} }; @@ -255,6 +257,11 @@ namespace sharp { */ VImage RemoveAnimationProperties(VImage image); + /* + Remove GIF palette from image. + */ + VImage RemoveGifPalette(VImage image); + /* Does this image have a non-default density? */ diff --git a/src/libvips/cplusplus/vips-operators.cpp b/src/libvips/cplusplus/vips-operators.cpp index 9360bd89..56d46881 100644 --- a/src/libvips/cplusplus/vips-operators.cpp +++ b/src/libvips/cplusplus/vips-operators.cpp @@ -3679,6 +3679,13 @@ VipsBlob *VImage::webpsave_buffer( VOption *options ) const return( buffer ); } +void VImage::webpsave_mime( VOption *options ) const +{ + call( "webpsave_mime", + (options ? options : VImage::option())-> + set( "in", *this ) ); +} + void VImage::webpsave_target( VTarget target, VOption *options ) const { call( "webpsave_target", diff --git a/src/metadata.cc b/src/metadata.cc index 09e9c931..773e5bd1 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -80,6 +80,9 @@ class MetadataWorker : public Napi::AsyncWorker { if (image.get_typeof(VIPS_META_RESOLUTION_UNIT) == VIPS_TYPE_REF_STRING) { baton->resolutionUnit = image.get_string(VIPS_META_RESOLUTION_UNIT); } + if (image.get_typeof("magick-format") == VIPS_TYPE_REF_STRING) { + baton->formatMagick = image.get_string("magick-format"); + } if (image.get_typeof("openslide.level-count") == VIPS_TYPE_REF_STRING) { int const levels = std::stoi(image.get_string("openslide.level-count")); for (int l = 0; l < levels; l++) { @@ -204,6 +207,9 @@ class MetadataWorker : public Napi::AsyncWorker { if (!baton->resolutionUnit.empty()) { info.Set("resolutionUnit", baton->resolutionUnit == "in" ? "inch" : baton->resolutionUnit); } + if (!baton->formatMagick.empty()) { + info.Set("formatMagick", baton->formatMagick); + } if (!baton->levels.empty()) { int i = 0; Napi::Array levels = Napi::Array::New(env, static_cast(baton->levels.size())); diff --git a/src/metadata.h b/src/metadata.h index 593421d2..6228df5f 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -41,6 +41,7 @@ struct MetadataBaton { int pagePrimary; std::string compression; std::string resolutionUnit; + std::string formatMagick; std::vector> levels; int subifds; std::vector background; diff --git a/src/pipeline.cc b/src/pipeline.cc index 35068949..2e1fca6f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -409,6 +409,7 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.bandjoin(joinImage); } image = image.copy(VImage::option()->set("interpretation", baton->colourspace)); + image = sharp::RemoveGifPalette(image); } inputWidth = image.width(); @@ -660,6 +661,7 @@ class PipelineWorker : public Napi::AsyncWorker { ys.push_back(top); } image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys)); + image = sharp::RemoveGifPalette(image); } // Gamma decoding (brighten) @@ -689,6 +691,7 @@ class PipelineWorker : public Napi::AsyncWorker { std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean); booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput); image = sharp::Boolean(image, booleanImage, baton->booleanOp); + image = sharp::RemoveGifPalette(image); } // Apply per-channel Bandbool bitwise operations after all other operations @@ -870,7 +873,8 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) - ->set("reoptimise", baton->gifReoptimise) + ->set("reuse", baton->gifReuse) + ->set("interlace", baton->gifProgressive) ->set("interframe_maxerror", baton->gifInterFrameMaxError) ->set("interpalette_maxerror", baton->gifInterPaletteMaxError) ->set("dither", baton->gifDither))); @@ -1068,7 +1072,8 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) - ->set("reoptimise", baton->gifReoptimise) + ->set("reuse", baton->gifReuse) + ->set("interlace", baton->gifProgressive) ->set("dither", baton->gifDither)); baton->formatOut = "gif"; } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || @@ -1582,7 +1587,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->gifDither = sharp::AttrAsDouble(options, "gifDither"); baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError"); baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError"); - baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise"); + baton->gifReuse = sharp::AttrAsBool(options, "gifReuse"); + baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive"); baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality"); baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid"); baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth"); diff --git a/src/pipeline.h b/src/pipeline.h index 78e2b5e0..1e35f033 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -165,7 +165,8 @@ struct PipelineBaton { double gifDither; double gifInterFrameMaxError; double gifInterPaletteMaxError; - bool gifReoptimise; + bool gifReuse; + bool gifProgressive; int tiffQuality; VipsForeignTiffCompression tiffCompression; VipsForeignTiffPredictor tiffPredictor; @@ -322,7 +323,8 @@ struct PipelineBaton { gifDither(1.0), gifInterFrameMaxError(0.0), gifInterPaletteMaxError(3.0), - gifReoptimise(false), + gifReuse(true), + gifProgressive(false), tiffQuality(80), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), diff --git a/test/unit/gif.js b/test/unit/gif.js index 1c7b2172..e88b18c2 100644 --- a/test/unit/gif.js +++ b/test/unit/gif.js @@ -80,19 +80,36 @@ describe('GIF input', () => { assert.strictEqual(true, reduced.length < original.length); }); - it('valid optimise', () => { - assert.doesNotThrow(() => sharp().gif({ reoptimise: true })); - assert.doesNotThrow(() => sharp().gif({ reoptimize: true })); + it('valid reuse', () => { + assert.doesNotThrow(() => sharp().gif({ reuse: true })); + assert.doesNotThrow(() => sharp().gif({ reuse: false })); }); - it('invalid reoptimise throws', () => { + it('invalid reuse throws', () => { assert.throws( - () => sharp().gif({ reoptimise: -1 }), - /Expected boolean for gifReoptimise but received -1 of type number/ + () => sharp().gif({ reuse: -1 }), + /Expected boolean for gifReuse but received -1 of type number/ ); assert.throws( - () => sharp().gif({ reoptimize: 'fail' }), - /Expected boolean for gifReoptimise but received fail of type string/ + () => sharp().gif({ reuse: 'fail' }), + /Expected boolean for gifReuse but received fail of type string/ + ); + }); + + it('progressive changes file size', async () => { + const nonProgressive = await sharp(fixtures.inputGif).gif({ progressive: false }).toBuffer(); + const progressive = await sharp(fixtures.inputGif).gif({ progressive: true }).toBuffer(); + assert(nonProgressive.length !== progressive.length); + }); + + it('invalid progressive throws', () => { + assert.throws( + () => sharp().gif({ progressive: -1 }), + /Expected boolean for gifProgressive but received -1 of type number/ + ); + assert.throws( + () => sharp().gif({ progressive: 'fail' }), + /Expected boolean for gifProgressive but received fail of type string/ ); }); diff --git a/test/unit/text.js b/test/unit/text.js index 031b6075..e993c8a1 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -294,4 +294,24 @@ describe('Text to image', () => { }); }); }); + + it('valid wrap throws', () => { + assert.doesNotThrow(() => sharp({ text: { text: 'text', wrap: 'none' } })); + assert.doesNotThrow(() => sharp({ text: { text: 'text', wrap: 'wordChar' } })); + }); + + it('invalid wrap throws', () => { + assert.throws( + () => sharp({ text: { text: 'text', wrap: 1 } }), + /Expected one of: word, char, wordChar, none for text\.wrap but received 1 of type number/ + ); + assert.throws( + () => sharp({ text: { text: 'text', wrap: false } }), + /Expected one of: word, char, wordChar, none for text\.wrap but received false of type boolean/ + ); + assert.throws( + () => sharp({ text: { text: 'text', wrap: 'invalid' } }), + /Expected one of: word, char, wordChar, none for text\.wrap but received invalid of type string/ + ); + }); });