From 4a745f2d2ebb5727181466afc71cd5480199b4c2 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 9 Jan 2020 22:51:08 +0000 Subject: [PATCH] Expose delay/loop metadata for animated images #1905 --- docs/changelog.md | 3 ++ src/metadata.cc | 17 +++++++++++ src/metadata.h | 3 ++ test/fixtures/animated-loop-3.gif | Bin 0 -> 7757 bytes test/fixtures/index.js | 1 + test/unit/metadata.js | 49 ++++++++++++++++++++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 test/fixtures/animated-loop-3.gif 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 0000000000000000000000000000000000000000..006d14aa025b22cde7e294915b8de0c1554a5142 GIT binary patch literal 7757 zcmZvhgm0wN#{1E`27Au0`$L#K2O-67K5r6AHs8-S$5 zy136d&))akyXGHQ&#d?NeQ&6!DN9OOxj`5qCm;You7CItKmY(lv2h>}Y#sojhXt74 zz@-O(#{iZi9{|B4#07vVC19(FimnwGDiTQ20&^WYE?o!iW?)^1jMxT zGyoCEc#DAvev9GOLnel&q_@Rb*jR3Ja@^%$XQTspc)6`^+F&C9egQsKo+@EcAx=ro z>$d;_JU|Ouh#dqWloC=Bh4hSfW$uUyeUwGW7)wdZA!PZ4K|YCPUg;qLiER;ygL!%; z1toc59Wg1XOJ!AM6_p1vcTd$cR6%-5Q6#9Lr4BMO+9{ienTmmM4jnyhMI}(*P*2yy zP)`jszHhW3Ap!F9nVQ`Pg~U>{)r>74m_IZ#HqAp>@#z?X44eC5;S?`9%F8+*Ty zS3x0x-mgKeObyTp6&&HORG<FRe<tegxm^zHMyXD@5L^9pjosPK0snT5p#J4x~RrP;Z8SD4abFd?A~^S-R2w4kOC zOiQk)DhD&Nt81&md&5erK`<|`yrr_CslMrbV^v#CT_XsV6n$uG0n4izdKx>r+QFKd zwzkWb!PcI>F0iR#V5q-$0K9DP?3(Bq85>?69vzuj>Hjo1F*Obj^iNHJA7`dU7skP{ z;m==Y!O4&FU%>v&!NuhT@blcrx6z4jpXRpb*Ehd{%Zr=a8=ITp$Njn8?>peu*VX;6 zKX)&|o$nimn-@R#_I~{Ye;w|hem^=nJURw1PmcG`kIv3d!QW?B=ivF}Z}9T`3cLV8 zkogP><%IwM^A(wbo1K-roh{r0*o(U z0NxdFiO--~nbi@1N5O5@Uzyz%0%KKASFOtF38xWpn(wd5?TdyhgfXa9=MBVh8eq%@ zs`H1E1Z@V>)oKbx(j=el%n#HQj%6YO2pH9Cizaeamwv8soWht7)t7x~a9taGr_oTp@ZR&+&caYb#ZsFuHsLMJ#>$nh zPzoN4;l`@9J~XRFhGtXs`cSfn^Wtz*&E{B^LijDM=GyJ4B7+i(k>~>{97W6iMB-b^ z0Wf9Gl|V`($CV%&`=XU#h8J5aA@E?%)ljxX$JMu-1x2f2y!BhF;ex%KYZ0QK9M>Wx zH;UGx?jCQgMI&&zzM>VWQD0+JIf}o=YKm`vjnh@;T8}q0Lais5*cY!Sn!VUwPqGZ= z+DNudL~W!v78Gx!y3}uPq`CHTZKk__LT$eD+$i46@HyVz%=E=IO%S(*DzLGy6RrHcK%jL)~F89ocU1V(^$fg0D+$3L{HJIeaZLw`4_w2 z8s7#rUJE6t9wQ%6D(-Mk@IwijD9*M0NJ#eV4nI#tDi0@wWF@IpXd-8M9`AYRv0^#Qm{qk=vcZrAT;c$d%Jm-qb<#~1K|HCCm zC;`MQnZru2NXyYmo64eSQWM4G+c5zXMOnXAQ?HVlsrm%IuH z;(|i-<)ipHSmlu&XozjN9SoW0pc0FR3mJn2YRhWH&PI4}_giTu@K5G+dZ@d7&beFL zjQ|9!gNw9~2QY+MD*dM}8zfmfR$*{Mo<2;PY~Hgl$ZlE}Mp1U*wL;wDj?@rt(R~`8 zebeF>sa!{G4q*b?9~Xe2$~h?tk6#&du6Tb%mr~J5Lhn))MHcQ--N*l6me^d<*+jxpcLj07GG0#?@wbx701^NW zbo{L(c*pYwOInJ4f%J|L7%0PvwUH=fYB{b)MqQPQkYO$-E0qWU-hi>=NlFa?@7bG{u zNEM_w=X_2}MOD)lrge0j6{LGi7Jp57a72t;9m=#!$Uz8odV!D+cBAi;38%~`N zE$Gd_jCDg~fIa#a`zhkB;6m~mpk=U5p@!$CX)}r`iITo2k-&-0CN8OMPBj#!%ah+N zug7B7#2@)K$Wtt7j$K;Wg~hIe-vCeJ)%NbMa+Yj?KH#ak6=Wb^R2s~R1tBr{!d=gp zKsHd`3Cnt1Q+hn7C;LU49t*-aJjdT8qvyw4H@`?oK~pBl^Gy^AU{Mep6AW{!=(Xqh zb#7fvRjME?f$km}2cLZssjuixxm(E`)d;LCO4#2zwE+eIN8>c1|FD8g8Uv?hVgyhY;`W%VKSYp;*97{B zt3_FZ-p>}#;Br@gl{x?U1>QWJ=cBncj4_U~>+k4HGj|K7W-?W1c)OA81V*dh2u{rc zc6PjbV7$WNj&)P(v;hUT459$v=8Mtp&jjwa6WKPNJT(p6Xbub=&;Qcqqj_uB-nS=y z{9jJQtWbPSqKl6u7kND5Bm2q(ES7Qf$XaR&don|tadDcqK9O1Ig}puU*s@4+3n&y;SoS71ahi|$p;Dw zpak_*>1F@U1m3e2w@P`<8TC{UEMXNuC4l7=gmEP))~628?TJScYO8wL6Pp@zk3sYxt|wcvTII zTI51I=CJo(xm=w;yLayPY_;gQmAFr83aRIMF+L&EH$G6dQQ-?Y#5Cp3jxxNmg_e;Cqaqap-<2ox_U?pfKGe|^ zXRZDgh%{V!?C33_FwsgY8%OKI9QUAyieM<6!D?p%FSFP;YFtHd2RHvq@M{~A$zuBeS;g_`Y@v_$Yk50f)GmjsvAL2J2^BJD#jzQXCC7ZZZUCtl_7`_*Up1F4}qjouh)qXBX^67A_YHn--5x)ue94 zuMvkV$@Q%?%m;Mn6=%5Hn0mmU(WIW3d)g$1FnQZldu$Rr{uXC?eSFlE_S!@Jo~7>+ zv1EZCk?C1}`m7k|)*~fxFSck++|9fv2~OTzF)6?6w;#U?`@J2jmO*D${6110Risvi z8RRN$XyT?XsT)-1F020ho38xB&Tl7-4c2*TMG5VDy6Qr%mvl9GBk19@=PY67!pLd- z;jt@o(w|&;dPt=mTPU5{Xg2uDhmFSL6UW+Q}rpYGlRXi`_W7o3fF&=qG%#8VbYhC;8)R< z|KTKqf8Sy8J-@$v(#T~lJ=3#Z>D-82MkH*mx$M1g ze9BYr?E_-9XHLrn0S5N!HAk+i@g6wa>tUxtDw=O@|Ex>jFJBq(mr`+eFA&-q%weCj z%}a6HiMaEzZt)`3YFe!ZHDhG5L$v%k+k-x0P};+F1*(A>DWjJ{rbZ=8c<@)Q>u>bM zEb&D#iXHi7e7#a1Yo*UjQJyq^BjC9+e6+tu$_#)KL!XFaW8}{Tnh|p@q+BsTUuY5>a4)NKy!R4Uk4xQ&-^*YUR zJWK8{3ZoDyoM$j{9+-5|M}@DnH@C zQ8etnE(r>KSFC?}DZ-oupA25xPiY(=lRBU)W7Vsuqotd%z9Ao8dq67Zs~M*w)0x*f zk@{R&N>BFbhC+J|G!!BE89fmlAR)yYb+3V6{*P!0XM_ddqZ_Ay<3+cZ( z)RNwjyUNiO*2S>e9i^3c&^^;7J{#UqWJ?2BR4OPto-O9R;P2L9gKp{T@`T@==|Mc! zjfrt{No>*;J}81IAjPKRU;@y)A`|AeEbp24)=9bv;RGlJL@l`$HCwe<@Nzdo%vKGJ z`-xzh81h0jkiT_6ToE4*zTys<(TH$wQvfL{tTWSM?uM*^`;1|ZSv3BB1 zOy7XC^gkV!wxtOeWf4j?*#Cg{CX@t1k`<>bSjP73#{efiMwgbp&-TfL^6lEFwije0 zhb3HmBcl0k>M}>~k4_dmm$Z1V z3{CqkspnruvmejeV&ix&8xw0we!|M0Qn~q9Q)FT#RO9-9U&XyX;K{gJ8BR#2m_%J{uk1E^?^F+pa$%zB7B|icnT)+?3U$)odg=M=`t= z=G&(h7;8kd915Yu3B33(i$?mn(RaN$3@>IaoUZSHt?=tR03X#RGlPR_=gfUcPkC!} z@@DH!)!7|f!sTVc#L`qnsN@|=9iuG$rMR;h?5l?3$p^eyIB z6%p%-Lh|ZFW$J4sE=sH@6Kk(rVd`k0mRQ~siEZ4zMuX#DMpT!}u-Ax$zmZTqfvCp# z5BHxYW2-6acgIAVeckMpR8x0~b(QV3z&)8R>jF6^ZxOUM%35wmV5ky5w9hL%A=my~zRm`_*uy=%BBvnIN3B z>+1s=!lS?!f$XHv%0kHowGnwTIMk{G2ZHBU`C{3cL=g{i<5PQ>2T6REV<;yCkS=x` zMnWZ2q5OGO#8Itf*_^D3@`N8r#0*8TCTfZ|Km9*Via z**SkBYWq-UsR{%^t3MU(ZOZ`iD@->z!^cKdrSGKLdlRBgKy%?`pZjw}00Ys=`(@$^ z&pwUCgVd9juj>$8&-TUR=A;jIvLM9zd!(>Z6*ip4pUMQVe;CekfFAG%`v1CimU_-r z{;T@pHmtXWiNF1;`qIm3b#$M;@SdsqQ+-i{DL}C&o8qslFCWPYqXdbY4mN#>j^`fV%~)^7JMnY*Ty2Lj<|V%v zGTHm4tKX%?(!Ud&*xHeyEgQ`Adx`zTy-qQ~tRs|pR6$hf{%Rj-Q}G#L(?RD$4L8R6 z;WMW9at4jhZ(Rz^k?W?Z$yqR%iaAUbnHyV}1-<(=*hjY%d=WLul>eTh#57i<(bC1h ziB^R{hdATq5DwET+E-76*rG~ykHp#QkDK=Au9hKJ@A&e#riUtec(r97Jl05WdU(!1 zTybWb7SO5lVmyEPLJTL2ZXCz)BR`bR(~^dbOZ6lFJ<%<=0Zd(zS+7melR1d)v8rLv zt=F?wp?CdMS#)SkJ##c+)Qzi}Nqdmxa86=>yh!P~7{Vyw5d$TmyS<0nR6RIev68B_ z4s7b95skKLb>l*7akR{C_z4`_6utZ=iQU>{Ue5=}#5Dkp0_D1*ryMG8%$n?#fANxS zq>;ZB2?}$Ujt{3Z*eHhG8a%_FxTC?zRirRzxU!l}bbp{o;c3`0r@|{fX)bv*!fjjr zw zP0P?L90 zW{n*#`98EeeR;ajoAFG%!sFS8)2-fuSC6O`tLcU5XaN0#S%{S&$| zsZG}|teuvUg?u7j$|pYi;x2SUj2_(`SmeV?$dPZnrx85gKP_2Y*6%Ne|2SEGn5uJA z1m=oOW9h!qMK16;@nXVrfVjW7Ex9T4cC@}5J$lYNG#N%A<%O~(e1mt!4CON?Ye9H)pg`e<)WduHd z4=G4%%P0Z!le!f|A~@gfL%4;lKxqwxdQ-B&{|=)pkWByy@cfr0Wyzzo3GNCZ<>LBN zzahQEc(3EmJ`>#ZI_~hA2&^eTDp1B;G!!KuUi%iTm)z#P*Wnu8?+DcB+sej)2V^wr z2*pN!5%6sJa}8^LJk~Fq`*|TZ+X6Mx+`dYzIZX>w)i{0RR9|iPvE&M4d@w%SL3f6? z@r)cr0hfA8T5fk9{g4f-nV{Y@uyzO`VF>$Sm!_E!MuX!#ZO|XyF7iS1j3m#_3od}5 zs3U{BxK?ezv~fS79ds3}{TA?MJ4&12>{u#6+fuX4u@3Dwp3s!P`s7caze|FZ%B!n` zUs2hi`*a+lX}bCfZ!b2$m66tFi~6cZP-UJ)8jEC{i|7;g{j*5Bs5X`4<~8Y0zevdk z#$L$1DZ9FyG4Q=GSLpS_WAThf9`&zf1t9NkSz6$Hj#<90#Y5qI2WD$4Axy%?Xa*Ip06E`0JuYbtr+Pex!JCpiLCR_hcsNSMDplqV#8tWN3MEwHR?XP3j2j zAV+Adx~4QfyON2n$+hjdNLoiccu}mYhm9jL_r{jMmIH;a#JnVDuRM^MW=9S$R0?+; zVAb$8H6*=Z`|Zpji!Py~I|HO?j^XUu@844X%ln8cOLHlWBp*fJ<-bhYMx+NT2Xf~9 z$-f{<#HqMYI}f5qq^KvkR8mO}cSb@Wf6Jp4XaXJqH~&i>+|wS>=68esy_$4;D4A4B z+W)nh+;sAx`Lmi3Bvd8)U#}){NhUQo$rir8oUrGr##+EG|ZmB7ck(jIEwMSR;rvb4_)i`O>U%}CLzN%1^?G-qSYL>`$n*lc3Slx ztI3lk(ks3!#6`njt4ZNy!BiXDM2^NkRul90)o(ohY)IFYHDn&=y0R97e7r6iNv{8t zhU?1u;)Dn%%mMq)l(e@bEhyxV6ArOA?+P4g7krwPO_&^*mdvJ+95GN3PJr)YSX5@E z5f2V8u($>o%O*>LL3gnF(+8bRzD^Hq3wN^kapNo&J&h~sq=yDyY5cCBf~ebuIxY+* z^bZ7);1T5wh6B`v1g{j8nkRSkYO(8-Lc5Jn5k3Q++Y3WUGebcO{=BAz`t(%U_msFS hhE1mX!&fXPrrD`}C}ElNJ{N8f{0~q6X&e9m literal 0 HcmV?d00001 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()