diff --git a/docs/changelog.md b/docs/changelog.md index 05fb485f..37fbfccd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,9 @@ Requires libvips v8.9.0. * Drop support for Node.js 8. [#1910](https://github.com/lovell/sharp/issues/1910) +* Expose `delay` and `loop` metadata for animated images. + [#1905](https://github.com/lovell/sharp/issues/1905) + * Ensure correct colour output for 16-bit, 2-channel PNG input with ICC profile. [#2013](https://github.com/lovell/sharp/issues/2013) diff --git a/src/metadata.cc b/src/metadata.cc index 6aaa2a31..66bc9a2d 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -77,6 +77,12 @@ class MetadataWorker : public Nan::AsyncWorker { if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) { baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT); } + if (image.get_typeof("loop") == G_TYPE_INT) { + baton->loop = image.get_int("loop"); + } + if (image.get_typeof("delay") == VIPS_TYPE_ARRAY_INT) { + baton->delay = image.get_array_int("delay"); + } if (image.get_typeof("heif-primary") == G_TYPE_INT) { baton->pagePrimary = image.get_int("heif-primary"); } @@ -169,6 +175,17 @@ class MetadataWorker : public Nan::AsyncWorker { if (baton->pageHeight > 0) { Set(info, New("pageHeight").ToLocalChecked(), New(baton->pageHeight)); } + if (baton->loop >= 0) { + Set(info, New("loop").ToLocalChecked(), New(baton->loop)); + } + if (!baton->delay.empty()) { + int i = 0; + v8::Local delay = New(baton->delay.size()); + for (int const d : baton->delay) { + Set(delay, i++, New(d)); + } + Set(info, New("delay").ToLocalChecked(), delay); + } if (baton->pagePrimary > -1) { Set(info, New("pagePrimary").ToLocalChecked(), New(baton->pagePrimary)); } diff --git a/src/metadata.h b/src/metadata.h index e16fda06..230fd690 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -36,6 +36,8 @@ struct MetadataBaton { int paletteBitDepth; int pages; int pageHeight; + int loop; + std::vector delay; int pagePrimary; bool hasProfile; bool hasAlpha; @@ -62,6 +64,7 @@ struct MetadataBaton { paletteBitDepth(0), pages(0), pageHeight(0), + loop(-1), pagePrimary(-1), hasProfile(false), hasAlpha(false), diff --git a/test/fixtures/animated-loop-3.gif b/test/fixtures/animated-loop-3.gif new file mode 100644 index 00000000..006d14aa Binary files /dev/null and b/test/fixtures/animated-loop-3.gif differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index affe8c3f..0e6a2c9a 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -102,6 +102,7 @@ module.exports = { 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 + inputGifAnimatedLoop3: getPath('animated-loop-3.gif'), // CC-BY-SA-4.0 Petrus3743 https://commons.wikimedia.org/wiki/File:01-Goldener_Schnitt_Formel-Animation.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/unit/metadata.js b/test/unit/metadata.js index 628cbe10..4a717918 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -232,6 +232,55 @@ describe('Image metadata', function () { done(); }); }); + + it('Animated GIF', () => + sharp(fixtures.inputGifAnimated) + .metadata() + .then(({ + format, width, height, space, channels, depth, + isProgressive, pages, pageHeight, loop, delay, + hasProfile, hasAlpha + }) => { + assert.strictEqual(format, 'gif'); + assert.strictEqual(width, 80); + assert.strictEqual(height, 80); + assert.strictEqual(space, 'srgb'); + assert.strictEqual(channels, 4); + assert.strictEqual(depth, 'uchar'); + assert.strictEqual(isProgressive, false); + assert.strictEqual(pages, 30); + assert.strictEqual(pageHeight, 80); + assert.strictEqual(loop, 0); + assert.deepStrictEqual(delay, Array(30).fill(30)); + assert.strictEqual(hasProfile, false); + assert.strictEqual(hasAlpha, true); + }) + ); + + it('Animated GIF with limited looping', () => + sharp(fixtures.inputGifAnimatedLoop3) + .metadata() + .then(({ + format, width, height, space, channels, depth, + isProgressive, pages, pageHeight, loop, delay, + hasProfile, hasAlpha + }) => { + assert.strictEqual(format, 'gif'); + assert.strictEqual(width, 370); + assert.strictEqual(height, 285); + assert.strictEqual(space, 'srgb'); + assert.strictEqual(channels, 4); + assert.strictEqual(depth, 'uchar'); + assert.strictEqual(isProgressive, false); + assert.strictEqual(pages, 10); + assert.strictEqual(pageHeight, 285); + assert.strictEqual(loop, 3); + assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]); + assert.strictEqual(hasProfile, false); + assert.strictEqual(hasAlpha, true); + }) + ); + it('vips', () => sharp(fixtures.inputV) .metadata()