From 9cc06c887ba9e715b0d3c21731e2eed688be8716 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sun, 17 Mar 2019 16:37:27 +0000 Subject: [PATCH] Add support for pages option for multi-page input #1566 --- docs/api-constructor.md | 3 +- docs/changelog.md | 3 ++ lib/constructor.js | 3 +- lib/input.js | 7 +++- src/common.cc | 11 +++-- src/common.h | 2 + test/fixtures/index.js | 1 + test/fixtures/rotating-squares.gif | Bin 0 -> 42245 bytes test/unit/gif.js | 64 +++++++++++++++++++++++++++++ test/unit/io.js | 31 -------------- 10 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 test/fixtures/rotating-squares.gif create mode 100644 test/unit/gif.js diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 6604edff..f59661e1 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -12,7 +12,8 @@ - `options.failOnError` **[Boolean][4]** by default halt processing and raise an error when loading invalid images. Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. (optional, default `true`) - `options.density` **[Number][5]** number representing the DPI for vector images. (optional, default `72`) - - `options.page` **[Number][5]** page number to extract for multi-page input (GIF, TIFF, PDF) (optional, default `0`) + - `options.pages` **[Number][5]** number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages. (optional, default `1`) + - `options.page` **[Number][5]** page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default `0`) - `options.raw` **[Object][3]?** describes raw pixel input image data. See `raw()` for pixel ordering. - `options.raw.width` **[Number][5]?** - `options.raw.height` **[Number][5]?** diff --git a/docs/changelog.md b/docs/changelog.md index 676f0bb4..c4363339 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,9 @@ Requires libvips v8.7.4. * Add `composite` operation supporting multiple images and blend modes; deprecate `overlayWith`. [#728](https://github.com/lovell/sharp/issues/728) +* Add support for `pages` input option for multi-page input. + [#1566](https://github.com/lovell/sharp/issues/1566) + * Add support for `page` input option to GIF and PDF. [#1595](https://github.com/lovell/sharp/pull/1595) [@ramiel](https://github.com/ramiel) diff --git a/lib/constructor.js b/lib/constructor.js index b156ccdd..5ac27481 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -64,7 +64,8 @@ const debuglog = util.debuglog('sharp'); * @param {Boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images. * Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. * @param {Number} [options.density=72] - number representing the DPI for vector images. - * @param {Number} [options.page=0] - page number to extract for multi-page input (GIF, TIFF, PDF) + * @param {Number} [options.pages=1] - number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages. + * @param {Number} [options.page=0] - page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {Number} [options.raw.width] * @param {Number} [options.raw.height] diff --git a/lib/input.js b/lib/input.js index ad898e6f..4e1a48bd 100644 --- a/lib/input.js +++ b/lib/input.js @@ -57,7 +57,12 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw new Error('Expected width, height and channels for raw pixel input'); } } - // Page input for multi-page images (GIF, TIFF, PDF) + // Multi-page input (GIF, TIFF, PDF) + if (is.defined(inputOptions.pages)) { + if (is.integer(inputOptions.pages) && is.inRange(inputOptions.pages, -1, 100000)) { + inputDescriptor.pages = inputOptions.pages; + } + } if (is.defined(inputOptions.page)) { if (is.integer(inputOptions.page) && is.inRange(inputOptions.page, 0, 100000)) { inputDescriptor.page = inputOptions.page; diff --git a/src/common.cc b/src/common.cc index 6e9b091e..7a43b466 100644 --- a/src/common.cc +++ b/src/common.cc @@ -71,7 +71,10 @@ namespace sharp { descriptor->rawWidth = AttrTo(input, "rawWidth"); descriptor->rawHeight = AttrTo(input, "rawHeight"); } - // Page input for multi-page TIFF, PDF + // Multi-page input (GIF, TIFF, PDF) + if (HasAttr(input, "pages")) { + descriptor->pages = AttrTo(input, "pages"); + } if (HasAttr(input, "page")) { descriptor->page = AttrTo(input, "page"); } @@ -254,7 +257,8 @@ namespace sharp { option->set("density", std::to_string(descriptor->density).data()); } if (ImageTypeSupportsPage(imageType)) { - option->set("page", descriptor->page); + option->set("n", descriptor->pages); + option->set("page", descriptor->page); } image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { @@ -299,7 +303,8 @@ namespace sharp { option->set("density", std::to_string(descriptor->density).data()); } if (ImageTypeSupportsPage(imageType)) { - option->set("page", descriptor->page); + option->set("n", descriptor->pages); + option->set("page", descriptor->page); } image = VImage::new_from_file(descriptor->file.data(), option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { diff --git a/src/common.h b/src/common.h index 4da7f361..40705af2 100644 --- a/src/common.h +++ b/src/common.h @@ -53,6 +53,7 @@ namespace sharp { int rawChannels; int rawWidth; int rawHeight; + int pages; int page; int createChannels; int createWidth; @@ -67,6 +68,7 @@ namespace sharp { rawChannels(0), rawWidth(0), rawHeight(0), + pages(1), page(0), createChannels(0), createWidth(0), diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 3b335065..c4f7c37f 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -100,6 +100,7 @@ module.exports = { inputTiff8BitDepth: getPath('8bit_depth.tiff'), inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif inputGifGreyPlusAlpha: getPath('grey-plus-alpha.gif'), // http://i.imgur.com/gZ5jlmE.gif + inputGifAnimated: getPath('rotating-squares.gif'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg diff --git a/test/fixtures/rotating-squares.gif b/test/fixtures/rotating-squares.gif new file mode 100644 index 0000000000000000000000000000000000000000..4d6202c7e3678fa22d7197cf0a0f415ef0f5dda9 GIT binary patch literal 42245 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 literal 0 HcmV?d00001 diff --git a/test/unit/gif.js b/test/unit/gif.js new file mode 100644 index 00000000..6c69d34c --- /dev/null +++ b/test/unit/gif.js @@ -0,0 +1,64 @@ +'use strict'; + +const fs = require('fs'); +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('GIF input', () => { + it('GIF Buffer to JPEG Buffer', () => + sharp(fs.readFileSync(fixtures.inputGif)) + .resize(8, 4) + .jpeg() + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + assert.strictEqual(true, data.length > 0); + assert.strictEqual(data.length, info.size); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(8, info.width); + assert.strictEqual(4, info.height); + }) + ); + + it('2 channel GIF file to PNG Buffer', () => + sharp(fixtures.inputGifGreyPlusAlpha) + .resize(8, 4) + .png() + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + assert.strictEqual(true, data.length > 0); + assert.strictEqual(data.length, info.size); + assert.strictEqual('png', info.format); + assert.strictEqual(8, info.width); + assert.strictEqual(4, info.height); + assert.strictEqual(4, info.channels); + }) + ); + + it('Animated GIF first page to PNG', () => + sharp(fixtures.inputGifAnimated) + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + assert.strictEqual(true, data.length > 0); + assert.strictEqual(data.length, info.size); + assert.strictEqual('png', info.format); + assert.strictEqual(80, info.width); + assert.strictEqual(80, info.height); + assert.strictEqual(4, info.channels); + }) + ); + + it('Animated GIF all pages to PNG "toilet roll"', () => + sharp(fixtures.inputGifAnimated, { pages: -1 }) + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + assert.strictEqual(true, data.length > 0); + assert.strictEqual(data.length, info.size); + assert.strictEqual('png', info.format); + assert.strictEqual(80, info.width); + assert.strictEqual(2400, info.height); + assert.strictEqual(4, info.channels); + }) + ); +}); diff --git a/test/unit/io.js b/test/unit/io.js index eb4d05dd..6161ddaf 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -517,37 +517,6 @@ describe('Input/output', function () { }); }); - it('Load GIF from Buffer', function (done) { - const inputGifBuffer = fs.readFileSync(fixtures.inputGif); - sharp(inputGifBuffer) - .resize(320, 240) - .jpeg() - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual(data.length, info.size); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(240, info.height); - done(); - }); - }); - - it('Load GIF grey+alpha from file, auto convert to PNG', function (done) { - sharp(fixtures.inputGifGreyPlusAlpha) - .resize(8, 4) - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual(data.length, info.size); - assert.strictEqual('png', info.format); - assert.strictEqual(8, info.width); - assert.strictEqual(4, info.height); - assert.strictEqual(4, info.channels); - done(); - }); - }); - it('Load Vips V file', function (done) { sharp(fixtures.inputV) .jpeg()