From 463da81fad4af5afd4cfcd6e48640db3157d5fb7 Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Mon, 8 Sep 2025 17:39:50 +0200 Subject: [PATCH] IFF: support for PCHG chunk Highlights: - Adds support for a new palette changer chunk. Some test cases attached to #38 . - Fixes the reading of ILBMs with the mask (test case: [cyclone.iff](/uploads/d8734d2155fd0d21f7b003b37e0d1259/cyclone.iff)). - Adds support for HAM5 encoding. - Adds more test cases created using [HAM Converter](http://mrsebe.bplaced.net/blog/wordpress/). - Adds support for Atari STE RAST chunk outside FORM one (test case: [fish.iff](/uploads/c461cf4b6a1423cec60fbce645d9fd07/fish.iff)). NOTE: I contacted Sebastiano Vigna, the author of the PCHG chunk specifications, and he provided me with: - Some images to test the code (but I can't include them in the test cases). - Permission to use [his code](https://vigna.di.unimi.it/amiga/PCHGLib.zip) without restrictions: Huffman decompression was achieved by converting `FastDecomp.a` via AI. Closes #38 --- autotests/read/iff/aga_pchg_amiga_16cl.iff | Bin 0 -> 10888 bytes autotests/read/iff/aga_pchg_amiga_16cl.png | Bin 0 -> 4400 bytes autotests/read/iff/aga_pchg_amiga_64cl.iff | Bin 0 -> 16644 bytes autotests/read/iff/aga_pchg_amiga_64cl.png | Bin 0 -> 5530 bytes autotests/read/iff/ham5.iff | Bin 0 -> 10562 bytes autotests/read/iff/ham5.png | Bin 0 -> 3442 bytes autotests/read/iff/ham8.iff | Bin 0 -> 17788 bytes autotests/read/iff/ham8.png | Bin 0 -> 5997 bytes autotests/read/iff/ocs_pchg_amiga_16cl.iff | Bin 0 -> 10130 bytes autotests/read/iff/ocs_pchg_amiga_16cl.png | Bin 0 -> 3517 bytes autotests/read/iff/ocs_pchg_amiga_64cl.iff | Bin 0 -> 11926 bytes autotests/read/iff/ocs_pchg_amiga_64cl.png | Bin 0 -> 3527 bytes ...gb16_sview5.iff => sv5_testcard_rgb16.iff} | Bin ...5.iff.json => sv5_testcard_rgb16.iff.json} | 0 ...a16_sview5.iff => sv5_testcard_rgba16.iff} | Bin ....iff.json => sv5_testcard_rgba16.iff.json} | 0 src/imageformats/chunks.cpp | 556 +++++++++++++++++- src/imageformats/chunks_p.h | 203 ++++++- src/imageformats/iff.cpp | 116 +++- 19 files changed, 808 insertions(+), 67 deletions(-) create mode 100644 autotests/read/iff/aga_pchg_amiga_16cl.iff create mode 100644 autotests/read/iff/aga_pchg_amiga_16cl.png create mode 100644 autotests/read/iff/aga_pchg_amiga_64cl.iff create mode 100644 autotests/read/iff/aga_pchg_amiga_64cl.png create mode 100644 autotests/read/iff/ham5.iff create mode 100644 autotests/read/iff/ham5.png create mode 100644 autotests/read/iff/ham8.iff create mode 100644 autotests/read/iff/ham8.png create mode 100644 autotests/read/iff/ocs_pchg_amiga_16cl.iff create mode 100644 autotests/read/iff/ocs_pchg_amiga_16cl.png create mode 100644 autotests/read/iff/ocs_pchg_amiga_64cl.iff create mode 100644 autotests/read/iff/ocs_pchg_amiga_64cl.png rename autotests/read/iff/{ps_testcard_rgb16_sview5.iff => sv5_testcard_rgb16.iff} (100%) rename autotests/read/iff/{ps_testcard_rgb16_sview5.iff.json => sv5_testcard_rgb16.iff.json} (100%) rename autotests/read/iff/{ps_testcard_rgba16_sview5.iff => sv5_testcard_rgba16.iff} (100%) rename autotests/read/iff/{ps_testcard_rgba16_sview5.iff.json => sv5_testcard_rgba16.iff.json} (100%) diff --git a/autotests/read/iff/aga_pchg_amiga_16cl.iff b/autotests/read/iff/aga_pchg_amiga_16cl.iff new file mode 100644 index 0000000000000000000000000000000000000000..235cf394fff76949446d6b9beb90c77d7ada1968 GIT binary patch literal 10888 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 SCUjcP#^1(4()=H4E&dB)vA<;i literal 0 HcmV?d00001 diff --git a/autotests/read/iff/aga_pchg_amiga_16cl.png b/autotests/read/iff/aga_pchg_amiga_16cl.png new file mode 100644 index 0000000000000000000000000000000000000000..ab426d23d9d0dd4d3147f6795845ea018a631d15 GIT binary patch literal 4400 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 murENgO$O>`8@N4|vYGuFR!{`om#`2PU!40s0s literal 0 HcmV?d00001 diff --git a/autotests/read/iff/aga_pchg_amiga_64cl.iff b/autotests/read/iff/aga_pchg_amiga_64cl.iff new file mode 100644 index 0000000000000000000000000000000000000000..2e6d001980153a1f150b6ffce03b9fa9af40b4d4 GIT binary patch literal 16644 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 Pe4q)59$NmdK&t-%jP;r= literal 0 HcmV?d00001 diff --git a/autotests/read/iff/aga_pchg_amiga_64cl.png b/autotests/read/iff/aga_pchg_amiga_64cl.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1e2585d7cf021dc06bf64dd4f9bd881af565da GIT binary patch literal 5530 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 literal 0 HcmV?d00001 diff --git a/autotests/read/iff/ham5.png b/autotests/read/iff/ham5.png new file mode 100644 index 0000000000000000000000000000000000000000..e19160f059599e1115bdc967856954a9e2199b42 GIT binary patch literal 3442 zcmZ`+c{tQ-`=8LvV60ONW#3Yk?-a_u45vo2L>&=k8bV_!&QD^P#@LM}a^#4JYEs$C zlAT{_50(yJok2ep8LL^`?>EVNBh$vLb5_25J&`l z#`-+(OWu2+VBWhV_t{k*-4JbU=@RZapPO>ykTJ}rkBMh!zu8}%FVz#{envP*N&8J) z#a*c$D^GER`mK7kGxIH5;wNr;O9-^>zjMUDGTniQ>2#Pfr}g?fjnIj{&>z()kCbu5 zqpwyrX(?@LEeILhv(`^}TbnByC?UA{uSG#krXX4N*KanUr4`f6b@7IS_1QGP5+7!M zGverRHmyVwjQ+6FyF&Ap2;__*ItX?}JTu7@PF=|^V#2cNmyqi=+&|5JdD<%-G4$Kc z+q()SrZX+=`n5_D3xkAUxusbL{9S~{z1e|;{+Bn1bUEXpd&6JsZ$N-rpSYeIh59eo zG>%BB{(z^JSMNT;_{jxS#8+~SKx!G?h{$dCO{cG?U}3kr4@Yj#0TbsDZczn>{aXtM zz(dK-c6u$tTtqwpM(6f;4Y=)kFz!C2MveI0spH^)toHFG?vdyZL}?(=Tz32g=RO0| zGa$v{9(mtMlz`2Kb27C57+ej!;Lc{hDDgMFIweEkrh8Xky6F9~&Rk?oL;dIEd}RLm z(~W0GHNWG*rGBZA+aeKFR0@{6{E20_8oboEw#9i;#Fkwn*admPP{oUtRp+~9*V(~> zWOR$2L!Q`v)UB()0FIe&+rkBaim7mKgo_b(fx0apV@zUrV*0}bdSe+o-9PKcoagnU zm0+43^xIncSnsbBU(QO7)&&%9ucR%bk}-Ov}ereOEb za%oAASLn48;AS=Lhj(=ThN!E7c6u9Pza7JNjj@&#L>nZb@c2?P19a*DSQEN03%Cq{ zZT2?BetVrSFzSRkBmP5_%-ai6eIOP~M8KeJ2glQmuwA5$i`FA|5~)qJ%<(CAmIR`} zMDq6K^n{DG6KP>%%71`}F--eERvSjg11YWQFq}qiW=6=&xJ}MVWBlZlOpVLA#ANJZ zIa*R-Fc!<{jcpABH$OLWIK5rJdc~W09S?k*tKkE4G!Jb??yUl9PfP^D;giM!|~!yla!9%O# zE`)FLO(fq=^_jZ7BkdP~XN)#@lmfYPUjF{?3pR4Q;IyV#4l<6d`7r&dMyj^GhNfTmw;L1 zf1+BpaR7(t9J|vleba?1gz_Bt1q>c9i0U^0p(28o$mRbnA&iRn>PWKW;#r$J^UlXD zSP_^+3J;6!&u2mW@vW|eqsRNXjn;FPpBJFF*6QmK&7mDmn`3uo4KSlQHI2Mne1GCG zP3ZetBiOWcOVL357qFaAm-lZ9vw>Fam53{3IaNq2dkkpo_e! z@nD(cN=Qahrg92-F6hHGV04xL8h{_lggBl<^xP<`SD8h2{B@R>PZ79yGp&}TIp7f$zE=cy!6;nlCmidHqEssJ1qM9`1x^{O_I@v z*5H`GG}c+1FKkzS?d8+>3bV74xFt!cc!P0pa5*E-FJVre@s4}SQifv;xDCj_VJ@4DW{wYxsh>A+_piiBo@2e>-kD61vk-AHFsyd*V-#;$oYRVki%Iyc1&9Hd>67K>ED+ zT2QP$R_U!d_L(2^x(M)Wq(7@D;O@SH6n7I4Jbs$H#^}0rBZbg7yBAiScf@mm>$_s4 z^I$$uQF@0BvO^SXpA;r%S$)%OpC(}qFY3->3Uul^8;1|Uoa7@hn4BgzQK6`goWDe0 zX^B~g#J_EfHm%#~s}{>ORzsqLX}et8Ae&AR((8aTy?&psqbRo zph`IUN)+H2f=nImc@8{A&$?3}`LQG<#Q`WA)%FlV@s0Ik{EA*Wd!*x=pS->J-m?Xa zJeI96ErosaHTRL)3;4c7%$dO7Aq`M*c>Lvy8hmBKFr)mO7B=}7NGFc8KiHpn_XST~ z9?ZGS*dc54J6m9u1{}@Z~yrU(lNO_aH#7u&`1|8 zTzUNYWT!>HDt|brIV`^NyKrdKXJ*#B%WV!&7q{srG-&te%yjmL1*I%tVvpe_2PMr5 zCDdbvvJx=NyLe&T&k!|p%f#XH*#w1f3kw4-_CDh+=m2{~|^2 zu43jB7>YKf{NHd0dVkgi{^P%8w@R)9ur0cRskmAE5GGQk1ixj zzWyBFQO?5^81eKaHB;nDg;j`BO3sTTx&dvKVWRqUc64fv?~y!tyR>q)hUC|$MepGm zfjm>hXgZ-LeDXjOw}myOb3cpg0eR%>iG_tY}`50g@-hAo zJ#ZyrguK`UJeJO&U~A16$D78Se-LX4KSQ3-1jQH^>hs4gyfGVA*jVCKT(-HV*-iAO zM!I~=v6x47#&(E^$xvF4)d&nWsT}p++ABHoU=*+TL;sfhU(FlxDhOrV{0myUi47VF zD{az!;BL22nyXM5%DZH?gG(n51HPHM+-o~?BQez<+e1Y%zhWJ8UqF5zpxTaAN%QWo zUG}v}MMgcOCk$}0KmFSB10*xWdr6Z%Et$wx)_`SB89+~pkZr|!=w)`&}3xVy4vAC*Tv=M&}Z4xYh)8Qjnb5w4}g0P2OKo^DuuqovkHI+1J~*uI_OkG+>4{ z(p?Gdx}O^yZOz~9?hL5~Me^>s?OF*o4~klCo`du?n4j5X&jkMffK zbkJv5p=o_vi$l dP=W38R|$n`UhSDV&YPt`Xd8R$5-Uv1zW_j9qt5^U literal 0 HcmV?d00001 diff --git a/autotests/read/iff/ham8.iff b/autotests/read/iff/ham8.iff new file mode 100644 index 0000000000000000000000000000000000000000..c750c3a26c02b2e248b539aba060f627225727a2 GIT binary patch literal 17788 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 Y!+OR96Z`wcAAk9mA|n4U|1FOH2Kr%bJpcdz literal 0 HcmV?d00001 diff --git a/autotests/read/iff/ham8.png b/autotests/read/iff/ham8.png new file mode 100644 index 0000000000000000000000000000000000000000..5853e7d7846faf59225489147bd80a1d0d52ac86 GIT binary patch literal 5997 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 MR`@q9{~v^Z0Wke{R{#J2 literal 0 HcmV?d00001 diff --git a/autotests/read/iff/ocs_pchg_amiga_16cl.png b/autotests/read/iff/ocs_pchg_amiga_16cl.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a98c723cdc6927333f76dfb9f49fb89f7e5b9c GIT binary patch literal 3517 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 WJ$MmnR1n;jZE-&2ih5}0oAh52CdUE* literal 0 HcmV?d00001 diff --git a/autotests/read/iff/ocs_pchg_amiga_64cl.iff b/autotests/read/iff/ocs_pchg_amiga_64cl.iff new file mode 100644 index 0000000000000000000000000000000000000000..611ee4a53c0fb4515d513d589cd557bf3caa1a57 GIT binary patch literal 11926 zcmd^_e~cXEdB>l3W_D)wZsYYDBRMp{ai)=R32}02!jCJrwolmnIB=p`O-e|vRdkq^ z7AuN!E_TcsjJ(+9NB)3#6%mmtZNlP8O_ZwG%Z~%o2H}JBk4jMz)?K9j;nHSrg$+A* zJ3sn;-gjnpW^a8yETmMb&u8zvJM)?E`@GNd%=5nQ+;qnmCy1_j?B?4xO>COjI!;8Z z=^^~!k4Xam4h-_~=8Y3uaA4rKv~BZNeAd5(&jLRW;zy%P$-?392Yvns+B_o2F7vNW zSI>@&)ayEJpj)cjaN!NLNkG)M$^SEA{p<~IPW<*yrhhqo^&1&+OZCp$UsU(J_1*oS z+#Q{#(U$KWtQvIl(Z8Onz4+^@pdClK?e0SseQj>9|5)>7|E;T7_`#FTQ|KAp{G)?EZXY-3+9xOKhflsRtI_q6K`S`IPy?g9Z^yb#L%MZU;hmw>0{crK<>iVPg+VTH-3O#;f zqW)qtL+h*8|M<4qgR}2eHM*v`0e8Nt67{6_e~Ydh|4iZb=9hL&ANbk!Q%y}=SGjHW z?O8#$)&$**>qhaJt#$*Bud8VExv86`G<+)dwWff*Gc+=C)6^FV_f)=xGykam-?*}C zcY{8E|E+i5^}@I7-PXNOeSh`A;qMgwe(GPRy6xSoR9m%b(Y>p8>5pCflvZ8+Q}LRn ziCL1M6$5!W;)#o=H4VAbn~KJk(!K2^VK_R>1+di2gm?m6`B`Y)Oqed>j~ zVEeB;vis=a=bpy6+*RMT`_bog`pTPsHG61>LGsM!9<5vSxcg+`@4om?8-H_j&g=Y* zZhXzz^X8!)D_*;O{J#IJL*w_~dAAP%8#a-WccK;jr|IXT;*~4JoUp)vb?7^q6Hu?Au zYU~BQ*6beZw07ikQ{Cv{b2m?H+=j@TESJlOHGHa8tN1WIJv}lqGBq`Yf1Azbc%@vO znyyqTHT-r(jHOK@^FwAzzn1rQgV1C`Ffff;HM@Z9g ziKa8&0^)QY220S0+g%`1&GJ&NE^`-~b^^b_f6s zsmSMB8xcp?2EH^qDLtJOqUck`M7n0gphP@FkV4OZBztu}25ulcW~}ToR+J=T-I5H- zxm_ioU{FCZx}DK{(xJreQcSd%swFhA)^1ttz>prIlNC7gq=~?XT^#!?J~rrmNO9>b z34vCOh=W^w-7{=c(_R$;PB17RI53$3r0#ndNMYRTQ&xkNSK(sZ6$`#Z8qN_upG9bZ z`C(j8y@OdEomTE=NOO^@M0PRSo88cfsCDc+v z?TCq*JZ1EuNnJ# zN9x?vDL>Zolbx(iXec>6nZd3zsG@-Mh`4P zFT`kR0i?h#8ll_EKBsKtP098HoPu~F4OWnBBxIy|UuK;pbuYnICXsB;<6yUz!Ctwf zxy3oneLA)YEyLXUULHv&KNFc7k2ZncVkFnZ<`(;SB-19@dk&LKCS>2J*zAOIDB&m} zdq>Qsjf>eY^<%F{xrzR=&$4$W&_e)O?bhQhc0e~UTneDtv7S#D`xMS`DI2+;daM?L z)&U4qz77BOP~TWSl6a`vLm0rfQQ>q|v&VA^4%s@_1AZUh2G)wun%+dm3e;5%$QAen zx@@!Ru8%}$er)qY!X-c6$O{F`o0x;k0(W_xp!uW%I-M)56jGHW_t#;dq^a>XR2DmFDy?D;3%nESV5R1Wf zyJ5AZ;01nu`0W)DQ}<*bSosEOcIjiLk3i(xSaSiYE18>h4^GzgGL@N8#D-qxBcJOG zF0?)$djp5_Lk*Oe8zL)6wWzZ!8jI9sQ2L-H_TRo_X$Pdm$bX|mVGk`PC+(07j3vN# zr@$vzav$)g6O#Q4mt0IqF0wH$K=PK9OCwZFCeJV-rT~nL zDU>xRn>mZ=n6qI9Wdn=`o+mFow8Avw8QifE>0fXyHD!;UQ z+%Un)rS+La1tYBwO&K^MrhI<+#jblVlbAb0JgEc~7D(C= zQSlUn84(=A(j_({k z?#J=)iE+}JN?gLc3G@9fk=-GVWMYWJL62I(VGgk7%U!3-eNKvaV&&W1b@QEfJoar1 zeu^~7eK&`%a0M?=Nmmj@G%+Ofthnn|&ZcbT=_7~zS@W%%^uS~Y)|*NE@isuTM`T@U z<+&;lg>f|5KfvNEp6wh$d)7H(jPHDj#YICoI_Zo_&g()ReQuQ zivTfiarj02XRv;`3_c;WQ+yrz5oeac!b17LbB{QoeOP>7`3(Dze4Wh$S!wHV%7FT` z^?f$QM~Uwm|HBz4ml|7E_0g)oAif+)y@gqz*d~2*p9iO;&^IrZzWJ>3%{+Us^3AJc zORs+EJ2?&~!c=LDQ@D(%0-}WrBN{{6c13{t40EEu(OYBI3|7T?;{gvnL`dM~XMzA1 zswNJ_7HWu5ZotYi1~)+g;VvkRW5;5^t4sb zw8$#+X*Nc=cR-nqA|6G78c>)QO{~C*22?ejk9|?d?io#HC#({&6IMY}FT2pyjq$~f z9J5O)n|wCZonT|~A$2F%Xu|Fmmo_wt8f2IaUf|8cd*9Q_TX88V|X;O z6B01d12o1Jg-b|FaQlaX8NLR`S%g1mbFyncV8tePgPwcJWH(0jhqg~pB+(>pXiQu>I;0R{jxRPg; z1+9G%FGN&Y6H6;y=&E}zYk!sDD?JU78ON&H5IUzfr^ z$nw+pBFn!oDgRaGPp0H^Qx9L6zem2t{GpWm67xST`PhwM^RNi4ys&v*kt3^``twuz zx!{zc)HRQv;D_c<@RjF;{Gp_MR9(rvJaz?pWm3Ov>ZgBz zx5qINxC}mzRZIY zyaa*^d3bph7b_DUS!k0bJa4+A&Es`%Y<{Ye_yg=RLv*+yKc76pw3Z-&@4z^@;TYl^ zX^7`oT8CL!)s0zf6=o?iA4S?wGxloZ zm1A}LsH56dZ8yw!jnekL@x>fsgPQ9>2UfS+R`g#9)+jtE|6wLgfrG~jB|#t@ICBOW8ya=n z`VyL`BiG_KLJR(${(vhPJl$w(ejaCr?uwfXor|?sv5s_E>{L*)10CjI<);;3cnp-- zAFvX)60D4-KOp7X>k`8D;!+56Vwl_~1MFJB_9@u4fRW3)C^1S4_^VVODePk-MyV+g z_Ql%dvo6@SOS19IVq;lsEDpqkv6QB>F~|rzm}`?^EFvKzsP0Kkwmq;IwGg8sO^nJV zj1~7U!Seg5>RKv74Fa`hF^XUKfz{OmJZ&oMeX;ctR+qz$Zrkt4(Sv^}K?as~pSKr) zd6|0$*Q6p(?Id+0Oouhq;CZxkcxOqx#I6M2-NQFN#r}oz!0G@NE;>Y#;6lv8#UV>g z#1T|zku3R|k%2Q_khgz{VTh9<&@@*og`4o!X{_7?L5MSAX%X&pgo6j5Dgf@^C2nu8 zHty*E@K>3bwmPC2i)g&VENN%?5({yd?kUrJ{tAdtn;Fq^vV20%{YcwjM00v+*j-|` zXIxMQZY0Jfy2{lUzV~ngX}OCcT&KjBNO7>7faQd{D&jdse7QO1(Ux!|ijh1AEt0o6 z;<+qgZKAW0JYMc{u)HMXS{I_3^zSkbsb^GR`x7e!?V$6|;34%O#i5101nw!KcV+JA z1g5)5x-|FKwsSe($eN~xH*9p6kOIF=XS|L*@6CIj8#H)50WVJ>rsFr0kRqd3e?msy zH*^3l^8vvX{Olg^u;_rd2au{jnVUR_A z>@~a{rfJ_$zzhSkGg!7WX=gtqP>BM z0u-eYM6f_ai^sCC)riao>uX*j&TW@?vtvd>bYJ%DbRu$6p;jo@XPp~>|f?yzOp zKxwpwx0q#+DR~NJ{1C9eRj>i>uPtU^vz;*vue6mQZU~l?;($ZfA+|qm%rGy1P~k91 z;8Dc9EE~RE$1lj~YcM^hXkLTMUdHKz*MGu* zFY=2Ea=m|@xgVDxV6)h`J#(hBpgUR5)A2w-=lShDdO{4V8)EWVjw_3q@yBLS_mOjX%<=-2m}$N1qlOU0y+a0 ziV8>(F@zR~16Uvu5RmFXsM6aD&cEJ!Yu&QeK6{_N&p!M6_BqM6)}}&&M+JF!c!bO@ z8DHW4341R;jQcGsD8X{&X=cWTc6Z!31t}5Vbww`v%Bej`tTZ@KnIVpg`@hE#e}R6` zG)u{IsdpgiU31oW8t{p5rAk+}c3VW~X3fpUf(vY+0tx@! zMQG{?GyIgttYqkR7Ogq?b^^6Rr)K^Y;l^N+kul_@#?m?+ybXVVVLp+&nc*i?~h7E~5ueX15 zc`Pj?V0T0@dbWd#t{5b-la@EbR^fb1DW|dtLUXnM)Rv_114H*eKHHFJ-78xuV~;W2 zaf^aIfx9SgksOJbT~SE2FVetLWs&5q8NPEvgC6-bJWLFI54t3k*y`idIiTUTW0WqE?&4O~9nDOLEm zPu>o)n&=N~7w;zZ==#cA8B9OP448a)v&+d-+9^8g!0hSVI3`FTdA{6fEW7o7A@a+3 zQPj8)BxT&Dd7+eyjX`U-->UvVz4?)8Jq?`RN`BRX7G(_XMxS(y&Z>ukDd$Lr8q#T; z{(KUQ|&662dt7jt9JDML6v6@7ecBx4OD){kVf!O$#DLMrxp3$vQ3K66iRd@ zWL`4cFnKl5c|y|0ETKiEkY^6QFRTsW+;hA5`J!I~Kc@RY`Y2++CmeR=_3vi3cNmA; zXs;jH79guoZDKgEpidq)wl;vt$?uc!kq11p1G{N>Q;A$7!x*hixEb=8=| z)X#x#HZViEvaC2MU+C>$zzcsn*p9r3x;du3rLV6KKC!f}BfepnhOZ-ItF8+j5_C>- z6JzQzxRnt~+cxo$y{esj2-ImM<<6AI!-LfRaBvk+4N_BTnb6l06sO>j_|$Trh44zG zr?fGtFsY?EC-7`N6G+s0YAH7vncHWfWhEOQ!;+VzjqU8ATeo z-ggp2OC92ZNBm8WDZ?YM1;VwkqN{Px&x)EVggGoK=xztKvHv^6O7KB@#sRJtwWjLr zU7*%u!A#w86FgiRbuVF05zVqubL4`#|U#6RK*V007fMnHx}rboc=`f+U;(>@{Vc&8TZLhZrbd7@Rb zWG#N@%DsjIr$e`mYPHSBJSx@{C7Ax|Q0M*bvJAgtQ5%FjywFAFtfVsPKav5za)%Goauqt%MV60j{tILpJLz z9}A~mY%ORTqKe3zqKBToL>ggXa8L~n8uK1rfERJIE4G&^Q<|}s-{#OY*hE?cWc=XSdc>Ifyyy~f!&RceV@PrZ-7`94QD>e_UqzdWZDr)#Pf z8ymYSg&=O`MmlYNe3kq>SyL^4zplO7G)pb_f(=qx?ET}IkcYO9qK_XV3-$A8oOr~o zqX~30snA6|lMC*jUPk{xJY^@W_@3t>3tD`?y>deomjK@k)XD7z zDH^SmG9Zq}iun7}a^Qs*7rQdew~wrnJ^QrgaFFC#sD$`-klSVFiD$XoL3Fg@;?f~W zneNY^)-AXu);PmFrJ_~9-$cLJ5_9>{*+B1(HDsK4_wM{Ns04d1(-23WQ$)!&_K7NKBf%UXNVNk0?=8}jJ>iFoclz<)P|{tOZ=iqzF@y)2XLQi|fHNy1W; zIcRb`%@^!FwXN5Ap`FTZ9xmmgT+3t+K1%ZryfFeo&o^QSq<p$x-~n?6 z37Ff7IB5Sb^^lOL?$1j@oDn~8&F$)(I5A#0eVfzNP!G@b(UD%3-iQ=WnG0byrlphv z$F2&b*FM@xX}zZ|@bks{Qzm;Xc7Im=tSAZSnBTp%u)1X%>aD|+>F~Ub)(6@K3cU6j z+>kWf=(acOi4RFh#2*<4G>9a9s!^sVh;>k-uf+3pc3X72%H;u{Y-RV!NHRt-B9YW% zcW^&i&w(BA3`1lrm8VOAP7y)z^Z66McJO>hsjEMpB&2!A>dn8O5i?2$ze$XRtV^2IhJ-)@nZnO35 z+$v4VB#GD$@y(XZtL?#-jehlrxt&SN^{dh`2lqG(E6cGT9i4z4Ch9bBR5J@Hy!RDv z7`dT{`tBP?4|~1=Py{dHdG^W~~rzjKJ85MoVs!x-r+Q&OPy9Kn6 zdZcC<)%(gQ3c-5+#9S$6jjlh>2u!gd8%F%V1i$$rrm7zy0EWNP&7Doql392PIqLr% zv7-nEL~VR?SJJ->t3P^uY~2YnJh`fOwVj$=oDw*@iCpv@;!DoY^~p4xe7=O$uS3kV zc8ajey3Q*=KPsnF%?mwTtQwCLaNfmExtAQNjY;Kn9e;f21Zx)TPdXCM`E%4_Bdo1t z$&2%N@1SzIqVz9!zIKZu(-?a|-$rszzi;R2rg)BJbrgEDTl;KT{gI0sVWa*VR@iE( z#7{lTDo&r=iDV8ANECX7w%yqd9ZrPtt2XnY5);4#gbiqETHs zpv65WyMPE?!;Y=ObAok;oDAv;n03 z)3FeM!P}=|PqkyY7nv1?F?oO8P0&qGjJA)VLTSqa3OHZrCrwLH;yUFFc!-ZSyzzvg o`9VuFK1UfP^Gk(sx?^5gb+b{?`>Y&}dqLwdGqE- #include +#include #ifdef QT_DEBUG Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtDebugMsg) @@ -306,6 +307,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new ICCPChunk()); } else if (cid == NAME_CHUNK) { chunk = QSharedPointer(new NAMEChunk()); + } else if (cid == PCHG_CHUNK) { + chunk = QSharedPointer(new PCHGChunk()); } else if (cid == RAST_CHUNK) { chunk = QSharedPointer(new RASTChunk()); } else if (cid == RGBA_CHUNK) { @@ -316,6 +319,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new TBHDChunk()); } else if (cid == VERS_CHUNK) { chunk = QSharedPointer(new VERSChunk()); + } else if (cid == XBMI_CHUNK) { + chunk = QSharedPointer(new XBMIChunk()); } else if (cid == XMP0_CHUNK) { chunk = QSharedPointer(new XMP0Chunk()); } else { // unknown chunk @@ -677,6 +682,51 @@ bool DPIChunk::innerReadStructure(QIODevice *d) return cacheData(d); } +/* ****************** + * *** XBMI Chunk *** + * ****************** */ + +XBMIChunk::~XBMIChunk() +{ + +} + +XBMIChunk::XBMIChunk() : DPIChunk() +{ +} + +bool XBMIChunk::isValid() const +{ + if (dpiX() == 0 || dpiY() == 0) { + return false; + } + return chunkId() == XBMIChunk::defaultChunkId(); +} + +quint16 XBMIChunk::dpiX() const +{ + if (bytes() < 6) { + return 0; + } + return i16(data().at(3), data().at(2)); +} + +quint16 XBMIChunk::dpiY() const +{ + if (bytes() < 6) { + return 0; + } + return i16(data().at(5), data().at(4)); +} + +XBMIChunk::PictureType XBMIChunk::pictureType() const +{ + if (bytes() < 6) { + return PictureType(-1); + } + return PictureType(i16(data().at(1), data().at(0))); +} + /* ****************** * *** BODY Chunk *** * ****************** */ @@ -884,7 +934,10 @@ quint32 BODYChunk::strideSize(const BMHDChunk *header, const QByteArray& formTyp } // ILBM - return header->rowLen() * header->bitplanes(); + auto sz = header->rowLen() * header->bitplanes(); + if (header->masking() == BMHDChunk::Masking::HasMask) + sz += header->rowLen(); + return sz; } QByteArray BODYChunk::pbm(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const @@ -959,11 +1012,18 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH ba = QByteArray(rowLen * 8 * 3, char()); auto pal = cmap->palette(); if (ipal) { - auto tmp = ipal->palette(y, header->height()); + auto tmp = ipal->palette(y); if (tmp.size() == pal.size()) pal = tmp; } - auto max = (1 << (bitplanes - 2)) - 1; + // HAM 6: 2 control bits+4 bits of data, 16-color palette + // + // HAM 8: 2 control bits+6 bits of data, 64-color palette + // + // HAM 5: 1 control bit (and 1 hardwired to zero)+4 bits of data + // (red and green modify operations are unavailable) + auto ctlbits = bitplanes > 5 ? 2 : 1; + auto max = (1 << (bitplanes - ctlbits)) - 1; quint8 prev[3] = {}; for (qint32 i = 0, cnt = 0; i < rowLen; ++i) { for (qint32 j = 0; j < 8; ++j, ++cnt) { @@ -971,11 +1031,14 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) { if ((planes.at(k * rowLen + i) & msk) == 0) continue; - if (k < bitplanes - 2) + if (k < bitplanes - ctlbits) idx |= 1 << k; else ctl |= 1 << (bitplanes - k - 1); } + if (ctl && ctlbits == 1) { + ctl <<= 1; // HAM 5 has only 1 control bit and the LSB is always 0 + } switch (ctl) { case 1: // red prev[0] = idx * 255 / max; @@ -1049,22 +1112,23 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH for (qint32 i = 0; i < rowLen; ++i) { for (qint32 k = 0, i8 = i * 8; k < bitplanes; ++k) { auto v = planes.at(k * rowLen + i); + auto msk = 1 << k; if (v & (1 << 7)) - ba[i8] |= 1 << k; + ba[i8] |= msk; if (v & (1 << 6)) - ba[i8 + 1] |= 1 << k; + ba[i8 + 1] |= msk; if (v & (1 << 5)) - ba[i8 + 2] |= 1 << k; + ba[i8 + 2] |= msk; if (v & (1 << 4)) - ba[i8 + 3] |= 1 << k; + ba[i8 + 3] |= msk; if (v & (1 << 3)) - ba[i8 + 4] |= 1 << k; + ba[i8 + 4] |= msk; if (v & (1 << 2)) - ba[i8 + 5] |= 1 << k; + ba[i8 + 5] |= msk; if (v & (1 << 1)) - ba[i8 + 6] |= 1 << k; + ba[i8 + 6] |= msk; if (v & 1) - ba[i8 + 7] |= 1 << k; + ba[i8 + 7] |= msk; } } } @@ -1257,8 +1321,9 @@ QImage::Format IFOR_Chunk::optionformat() const { auto fmt = this->format(); if (fmt == QImage::Format_Indexed8) { - if (searchIPal()) - fmt = FORMAT_RGB_8BIT; + if (auto ipal = searchIPal()) { + fmt = ipal->hasAlpha() ? FORMAT_RGBA_8BIT : FORMAT_RGB_8BIT; + } } return fmt; } @@ -1282,6 +1347,10 @@ const IPALChunk *IFOR_Chunk::searchIPal() const if (!rast.isEmpty()) { ipal = rast.first(); } + auto pchg = IFFChunk::searchT(this); + if (!pchg.isEmpty()) { + ipal = pchg.first(); + } if (ipal && ipal->isValid()) { return ipal; } @@ -1372,11 +1441,6 @@ QImage::Format FORMChunk::format() const return QImage::Format_RGBA64; } if (h->bitplanes() >= 1 && h->bitplanes() <= 8) { - if (!IFFChunk::search(PCHG_CHUNK, chunks()).isEmpty()) { - qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): PCHG chunk is not supported"; - return QImage::Format_Invalid; - } - if (h->bitplanes() >= BITPLANES_HAM_MIN && h->bitplanes() <= BITPLANES_HAM_MAX) { if (modeId & CAMGChunk::ModeId::Ham) return FORMAT_RGB_8BIT; @@ -2310,7 +2374,9 @@ BEAMChunk::~BEAMChunk() } -BEAMChunk::BEAMChunk() : IPALChunk() +BEAMChunk::BEAMChunk() + : IPALChunk() + , _height() { } @@ -2320,8 +2386,20 @@ bool BEAMChunk::isValid() const return chunkId() == BEAMChunk::defaultChunkId(); } -QList BEAMChunk::palette(qint32 y, qint32 height) const +IPALChunk *BEAMChunk::clone() const { + return new BEAMChunk(*this); +} + +bool BEAMChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + +QList BEAMChunk::palette(qint32 y) const +{ + auto &&height = _height; if (height < 1) { return {}; } @@ -2378,7 +2456,9 @@ SHAMChunk::~SHAMChunk() } -SHAMChunk::SHAMChunk() : IPALChunk() +SHAMChunk::SHAMChunk() + : IPALChunk() + , _height() { } @@ -2398,8 +2478,14 @@ bool SHAMChunk::isValid() const return chunkId() == SHAMChunk::defaultChunkId(); } -QList SHAMChunk::palette(qint32 y, qint32 height) const +IPALChunk *SHAMChunk::clone() const { + return new SHAMChunk(*this); +} + +QList SHAMChunk::palette(qint32 y) const +{ + auto && height = _height; if (height < 1) { return {}; } @@ -2426,6 +2512,12 @@ QList SHAMChunk::palette(qint32 y, qint32 height) const return pal; } +bool SHAMChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + bool SHAMChunk::innerReadStructure(QIODevice *d) { return cacheData(d); @@ -2440,7 +2532,9 @@ RASTChunk::~RASTChunk() } -RASTChunk::RASTChunk() : IPALChunk() +RASTChunk::RASTChunk() + : IPALChunk() + , _height() { } @@ -2450,8 +2544,14 @@ bool RASTChunk::isValid() const return chunkId() == RASTChunk::defaultChunkId(); } -QList RASTChunk::palette(qint32 y, qint32 height) const +IPALChunk *RASTChunk::clone() const { + return new RASTChunk(*this); +} + +QList RASTChunk::palette(qint32 y) const +{ + auto &&height = _height; if (height < 1) { return {}; } @@ -2478,7 +2578,413 @@ QList RASTChunk::palette(qint32 y, qint32 height) const return pal; } +bool RASTChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + bool RASTChunk::innerReadStructure(QIODevice *d) { return cacheData(d); } + +/* ****************** + * *** PCHG Chunk *** + * ****************** */ + +PCHGChunk::~PCHGChunk() +{ +} + +PCHGChunk::PCHGChunk() : IPALChunk() +{ + +} + +PCHGChunk::Compression PCHGChunk::compression() const +{ + if (!isValid()) { + return Compression::Uncompressed; + } + return Compression(ui16(data(), 0)); +} + +PCHGChunk::Flags PCHGChunk::flags() const +{ + if (!isValid()) { + return Flags(Flag::None); + } + return Flags(ui16(data(), 2)); +} + +qint16 PCHGChunk::startLine() const +{ + if (!isValid()) { + return 0; + } + return i16(data(), 4); +} + +quint16 PCHGChunk::lineCount() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 6); +} + +quint16 PCHGChunk::changedLines() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 8); +} + +quint16 PCHGChunk::minReg() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 10); +} + +quint16 PCHGChunk::maxReg() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 12); +} + +quint16 PCHGChunk::maxChanges() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 14); +} + +quint32 PCHGChunk::totalChanges() const +{ + if (!isValid()) { + return 0; + } + return ui32(data(), 16); +} + +bool PCHGChunk::hasAlpha() const +{ + return (flags() & PCHGChunk::Flag::UseAlpha) ? true : false; +} + +bool PCHGChunk::isValid() const +{ + if (bytes() < 20) { + return false; + } + return chunkId() == PCHGChunk::defaultChunkId(); +} + +IPALChunk *PCHGChunk::clone() const +{ + return new PCHGChunk(*this); +} + +QList PCHGChunk::palette(qint32 y) const +{ + return _palettes.value(y); +} + +// ---------------------------------------------------------------------------- +// PCHG_FastDecomp reimplementation (Amiga 68k -> portable C++/Qt) +// ---------------------------------------------------------------------------- +// This mirrors the original 68k routine semantics: +// - The Huffman tree is stored as a sequence of signed 16-bit words (big-endian) +// and TreeCode points to the *last word* of that sequence. +// - Bits are consumed MSB-first from 32-bit big-endian longwords of the source. +// - Navigation rules (matching the assembly): +// bit=1: read w = *(a3). If w < 0 then a3 += w (byte-wise) and continue; +// else emit (w & 0xFF) and reset a3 to TreeCode (last word). +// bit=0: predecrement a3 by 2; read w = *a3. If w < 0: continue; +// else if (w & 0x0100) emit (w & 0xFF) and reset a3; else continue. +// - Stop after writing exactly OriginalSize bytes. +// +// This function expects a single QByteArray laid out as: +// [ tree (treeSize bytes, even) | compressed bitstream (... bytes) ] +// +// On any error, logs with qCCritical(LOG_IFFPLUGIN) and returns {}. +// Comments are in English as requested. +// ---------------------------------------------------------------------------- +// +// NOTE: Sebastiano Vigna, the author of the PCHG specification and the ASM +// decompression code for the Motorola 68K, gave us permission to use his +// code and recommended that we convert it with AI. + +// Read a big-endian 16-bit signed word from a byte buffer +static inline qint16 read_be16(const char* base, int byteIndex, int size) +{ + if (byteIndex + 1 >= size) + return 0; // caller must bounds-check; we keep silent here + const quint8 b0 = static_cast(base[byteIndex]); + const quint8 b1 = static_cast(base[byteIndex + 1]); + return static_cast((b0 << 8) | b1); +} + +// Read a big-endian 32-bit unsigned long from a byte buffer +static inline quint32 read_be32(const char* base, int byteIndex, int size) +{ + if (byteIndex + 3 >= size) + return 0; // caller must bounds-check + const quint8 b0 = static_cast(base[byteIndex]); + const quint8 b1 = static_cast(base[byteIndex + 1]); + const quint8 b2 = static_cast(base[byteIndex + 2]); + const quint8 b3 = static_cast(base[byteIndex + 3]); + return (static_cast(b0) << 24) | + (static_cast(b1) << 16) | + (static_cast(b2) << 8) | + static_cast(b3); +} + +// Core decompressor (tree + compressed stream in one QByteArray) +static QByteArray pchgFastDecomp(const QByteArray& input, int treeSize, int originalSize) +{ + // Basic validation + if (treeSize <= 0 || (treeSize & 1)) { + qCCritical(LOG_IFFPLUGIN) << "Invalid treeSize (must be positive and even)" << treeSize; + return {}; + } + if (input.size() < treeSize) { + qCCritical(LOG_IFFPLUGIN) << "Input too small for treeSize" << input.size() << treeSize; + return {}; + } + if (originalSize < 0) { + qCCritical(LOG_IFFPLUGIN) << "Invalid originalSize" << originalSize; + return {}; + } + + const char* data = input.constData(); + const int totalSize = input.size(); + + // Tree view (big-endian words) + const int treeBytes = treeSize; + const int treeWords = treeBytes / 2; + if (treeWords <= 0) { + qCCritical(LOG_IFFPLUGIN) << "Tree has zero words"; + return {}; + } + + // Compressed stream + const int srcBase = treeBytes; // offset where bitstream starts + const int srcSize = totalSize - srcBase; + if (srcSize <= 0 && originalSize > 0) { + qCCritical(LOG_IFFPLUGIN) << "No compressed payload present"; + return {}; + } + + QByteArray out; + out.resize(originalSize); + char* outPtr = out.data(); + + // Emulate a3 pointer to words: + // a2 points to the *last word* => word index (0..treeWords-1) + auto resetA3 = [&]() { + return treeWords - 1; // last word index + }; + int a3_word = resetA3(); + + // Bit reader: loads 32b big-endian and shifts MSB-first + quint32 bitbuf = 0; + int bits = 0; // remaining bits in bitbuf + int srcPos = 0; // byte offset relative to srcBase + + auto refill = [&]() -> bool { + if (srcPos + 4 > srcSize) { + qCCritical(LOG_IFFPLUGIN) << "Compressed stream underflow while refilling bit buffer" + << "srcPos=" << srcPos << "srcSize=" << srcSize; + return false; + } + bitbuf = read_be32(data + srcBase, srcPos, srcSize); + bits = 32; + srcPos += 4; + return true; + }; + + int produced = 0; + + // Main decode loop: produce exactly originalSize bytes + while (produced < originalSize) { + if (bits == 0) { + if (!refill()) { + // Not enough bits to complete output + return {}; + } + } + + const bool bit1 = (bitbuf & 0x80000000u) != 0u; // MSB before shift + bitbuf <<= 1; + --bits; + + if (bit1) { + // Case bit == 1 --> w = *(a3) + if (a3_word < 0 || a3_word >= treeWords) { + qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds (bit=1)" << a3_word; + return {}; + } + const int byteIndex = a3_word * 2; + const qint16 w = read_be16(data, byteIndex, treeBytes); + + if (w < 0) { + // a3 += w (w is a signed byte offset, must be even) + if (w & 1) { + qCCritical(LOG_IFFPLUGIN) << "Misaligned tree offset (odd)" << w; + return {}; + } + const int deltaWords = w / 2; // arithmetic division, w is even in valid streams + const int next = a3_word + deltaWords; + if (next < 0 || next >= treeWords) { + qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds after offset" << next; + return {}; + } + a3_word = next; + } else { + // Leaf: emit low 8 bits, reset a3 + outPtr[produced++] = static_cast(w & 0xFF); + a3_word = resetA3(); + } + } else { + // Case bit == 0 --> w = *--a3 (predecrement) + --a3_word; + if (a3_word < 0) { + qCCritical(LOG_IFFPLUGIN) << "a3 underflow on predecrement"; + return {}; + } + const int byteIndex = a3_word * 2; + const qint16 w = read_be16(data, byteIndex, treeBytes); + + if (w < 0) { + // Internal node: continue with current a3 + continue; + } + + // Non-negative: check bit #8; if set -> leaf + if ((w & 0x0100) != 0) { + outPtr[produced++] = static_cast(w & 0xFF); + a3_word = resetA3(); + } else { + // Not a leaf: continue scanning + continue; + } + } + } + + return out; +} + +// !Huffman decompression + +bool PCHGChunk::initialize(const QList &cmapPalette, qint32 height) +{ + auto dt = data().mid(20); + if (compression() == PCHGChunk::Compression::Huffman) { + QDataStream ds(dt); + ds.setByteOrder(QDataStream::BigEndian); + + quint32 infoSize; + ds >> infoSize; + quint32 origSize; + ds >> origSize; + + dt = pchgFastDecomp(dt.mid(8), infoSize, origSize); + } + if (dt.isEmpty()) { + return false; + } + + QDataStream ds(dt); + ds.setByteOrder(QDataStream::BigEndian); + + // read the masks + auto lcnt = lineCount(); + auto nlw = (lcnt + 31) / 32; // number of LWORD containing the bit mask + QList masks; + for (auto i = 0; i < nlw; ++i) { + quint32 mask; + ds >> mask; + masks << mask; + } + if (ds.status() != QDataStream::Ok) { + return false; + } + + // read the palettes + auto changesLoaded = qint64(); + auto startY = startLine(); + auto last = cmapPalette; + auto flgs = flags(); + for (auto i = 0; i < lcnt; ++i) { + auto mask = masks.at(i / 32); + if (((mask >> (31 - i % 32)) & 1) == 0) { + _palettes.insert(i + startY, last); + continue; // no palette change for this line + } + + QHash hash; + if (flgs & PCHGChunk::Flag::F12Bit) { + quint8 c16; + ds >> c16; + quint8 c32; + ds >> c32; + for (auto j = 0; j < int(c16); ++j) { + quint16 tmp; + ds >> tmp; + hash.insert(((tmp >> 12) & 0xF), qRgb(((tmp >> 8) & 0xF) * 17, ((tmp >> 4) & 0xF) * 17, ((tmp & 0xF) * 17))); + } + for (auto j = 0; j < int(c32); ++j) { + quint16 tmp; + ds >> tmp; + hash.insert((((tmp >> 12) & 0xF) + 16), qRgb(((tmp >> 8) & 0xF) * 17, ((tmp >> 4) & 0xF) * 17, ((tmp & 0xF) * 17))); + } + } else if (flgs & PCHGChunk::Flag::F32Bit) { // NOTE: missing test case (not tested) + quint16 cnt; + ds >> cnt; + for (auto j = 0; j < int(cnt); ++j) { + quint16 reg; + ds >> reg; + quint8 alpha; + ds >> alpha; + quint8 red; + ds >> red; + quint8 blue; + ds >> blue; + quint8 green; + ds >> green; + hash.insert(reg, qRgba(red, green, blue, flgs & PCHGChunk::Flag::UseAlpha ? alpha : 0xFF)); + } + } + + if (ds.status() != QDataStream::Ok) { + return false; + } + + for (auto i = qsizetype(), n = last.size(); i < n; ++i) { + if (hash.contains(i)) + last[i] = hash.value(i); + } + + _palettes.insert(i + startY, last); + changesLoaded += hash.size(); + } + + if (changesLoaded != qint64(totalChanges())) { + qCDebug(LOG_IFFPLUGIN) << "PCHGChunk::innerReadStructure(): palette changes count mismatch!"; + } + + return true; +} + +bool PCHGChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index 6560ee0..eb80312 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -54,6 +55,7 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define CMAP_CHUNK QByteArray("CMAP") #define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK #define DPI__CHUNK QByteArray("DPI ") +#define XBMI_CHUNK QByteArray("XBMI") // Different palette for scanline #define BEAM_CHUNK QByteArray("BEAM") @@ -91,17 +93,20 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define CHUNKID_DEFINE(a) static QByteArray defaultChunkId() { return a; } -// The 8-bit RGB format must be one. If you change it here, you have also to use the same +// The 8-bit RGB format must be consistent. If you change it here, you have also to use the same // when converting an image with BEAM/CTBL/SHAM chunks otherwise the option(QImageIOHandler::ImageFormat) // could returns a wrong value. // Warning: Changing it requires changing the algorithms. Se, don't touch! :) -#define FORMAT_RGB_8BIT QImage::Format_RGB888 +#define FORMAT_RGB_8BIT QImage::Format_RGB888 // default one + +#define FORMAT_RGBA_8BIT QImage::Format_RGBA8888 // used by PCHG chunk /*! * \brief The IFFChunk class */ class IFFChunk { + friend class IFFHandlerPrivate; public: using ChunkList = QList>; @@ -318,18 +323,30 @@ protected: inline quint16 ui16(quint8 c1, quint8 c2) const { return (quint16(c2) << 8) | quint16(c1); } + inline quint16 ui16(const QByteArray &data, qint32 pos) const { + return ui16(data.at(pos + 1), data.at(pos)); + } inline qint16 i16(quint8 c1, quint8 c2) const { return qint32(ui16(c1, c2)); } + inline qint16 i16(const QByteArray &data, qint32 pos) const { + return i16(data.at(pos + 1), data.at(pos)); + } inline quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1); } + inline quint32 ui32(const QByteArray &data, qint32 pos) const { + return ui32(data.at(pos + 3), data.at(pos + 2), data.at(pos + 1), data.at(pos)); + } inline qint32 i32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { return qint32(ui32(c1, c2, c3, c4)); } + inline qint32 i32(const QByteArray &data, qint32 pos) const { + return i32(data.at(pos + 3), data.at(pos + 2), data.at(pos + 1), data.at(pos)); + } static ChunkList innerFromDevice(QIODevice *d, bool *ok, IFFChunk *parent = nullptr); @@ -358,7 +375,36 @@ class IPALChunk : public IFFChunk public: virtual ~IPALChunk() override {} IPALChunk() : IFFChunk() {} - virtual QList palette(qint32 y, qint32 height) const = 0; + IPALChunk(const IPALChunk& other) = default; + IPALChunk& operator =(const IPALChunk& other) = default; + + /*! + * \brief hasAlpha + * \return True it the palette supports the alpha channel. + */ + virtual bool hasAlpha() const { return false; } + + /*! + * \brief clone + * \return A new instance of the class with all data. + */ + virtual IPALChunk *clone() const = 0; + + /*! + * \brief palette + * \param y The scanline. + * \return The modified palette. + */ + virtual QList palette(qint32 y) const = 0; + + /*! + * \brief initialize + * Initialize the palette changer. + * \param cmapPalette The palette as stored in the CMAP chunk. + * \param height The image height. + * \return True on success, otherwise false. + */ + virtual bool initialize(const QList& cmapPalette, qint32 height) = 0; }; @@ -376,7 +422,17 @@ public: }; enum Masking { None = 0, /**< Designates an opaque rectangular image. */ - HasMask = 1, /**< A mask plane is interleaved with the bitplanes in the BODY chunk. */ + HasMask = 1, /**< A "mask" is an optional "plane" of data the same size (w, h) as a bitplane. + It tells how to "cut out" part of the image when painting it onto another + image. "One" bits in the mask mean "copy the corresponding pixel to the + destination". "Zero" mask bits mean "leave this destination pixel alone". In + other words, "zero" bits designate transparent pixels. + The rows of the different bitplanes and mask are interleaved in the file. + This localizes all the information pertinent to each scan line. It + makes it much easier to transform the data while reading it to adjust the + image size or depth. It also makes it possible to scroll a big image by + swapping rows directly from the file without the need for random-access to + all the bitplanes. */ HasTransparentColor = 2, /**< Pixels in the source planes matching transparentColor are to be considered “transparent”. (Actually, transparentColor isn’t a “color number” since it’s matched with numbers formed @@ -385,7 +441,7 @@ public: one of the color registers. */ Lasso = 3 /**< The reader may construct a mask by lassoing the image as in MacPaint. To do this, put a 1 pixel border of transparentColor around the image rectangle. - Then do a seed fill from this border. Filled pixels are to be transparent. */ + Then do a seed fill from this border. Filled pixels are to be transparent. */ }; virtual ~BMHDChunk() override; @@ -605,13 +661,13 @@ public: * \brief dpiX * \return The horizontal resolution in DPI. */ - quint16 dpiX() const; + virtual quint16 dpiX() const; /*! * \brief dpiY * \return The vertical resolution in DPI. */ - quint16 dpiY() const; + virtual quint16 dpiY() const; /*! * \brief dotsPerMeterX @@ -631,6 +687,50 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The XBMIChunk class + */ +class XBMIChunk : public DPIChunk +{ +public: + enum PictureType : quint16 { + Indexed = 0, + Grayscale = 1, + Rgb = 2, + RgbA = 3, + Cmyk = 4, + CmykA = 5, + Bitmap = 6 + }; + + virtual ~XBMIChunk() override; + XBMIChunk(); + XBMIChunk(const XBMIChunk& other) = default; + XBMIChunk& operator =(const XBMIChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief dpiX + * \return The horizontal resolution in DPI. + */ + virtual quint16 dpiX() const override; + + /*! + * \brief dpiY + * \return The vertical resolution in DPI. + */ + virtual quint16 dpiY() const override; + + /*! + * \brief pictureType + * \return The picture type + */ + PictureType pictureType() const; + + CHUNKID_DEFINE(XBMI_CHUNK) +}; + /*! * \brief The BODYChunk class @@ -1312,12 +1412,19 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(BEAM_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; /*! @@ -1349,12 +1456,19 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(SHAM_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; /*! @@ -1373,13 +1487,82 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(RAST_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; +/*! + * \brief The PCHGChunk class + */ +class PCHGChunk : public IPALChunk +{ +public: + enum Compression { + Uncompressed, + Huffman + }; + + enum Flag { + None = 0x00, + F12Bit = 0x01, + F32Bit = 0x02, + UseAlpha = 0x04 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + virtual ~PCHGChunk() override; + PCHGChunk(); + PCHGChunk(const PCHGChunk& other) = default; + PCHGChunk& operator =(const PCHGChunk& other) = default; + + Compression compression() const; + + Flags flags() const; + + qint16 startLine() const; + + quint16 lineCount() const; + + quint16 changedLines() const; + + quint16 minReg() const; + + quint16 maxReg() const; + + quint16 maxChanges() const; + + quint32 totalChanges() const; + + virtual bool hasAlpha() const override; + + virtual bool isValid() const override; + + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; + + CHUNKID_DEFINE(PCHG_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; + +private: + QHash> _paletteChanges; + + QHash> _palettes; +}; #endif // KIMG_CHUNKS_P_H diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 2c39d4f..2becc7b 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -27,6 +27,37 @@ public: } + /*! + * \brief atariSTERast + * On Atari STE images, the RAST chunk can be found outside + * the FORM one so, I check if this is the case. + * \param chunks The chunk list. + */ + void atariSTERast(QIODevice *d, IFFChunk::ChunkList &chunks) + { + if (chunks.size() != 1 || d->isSequential()) { + return; + } + auto &&c = chunks.first(); + if (c->chunkId() != FORMChunk::defaultChunkId()) { + return; + } + + // The RAST chunk is not aligned so I have to temporary change the + // position and the alignment to read it successfully. + auto pos = d->pos(); + auto align = c->alignBytes(); + c->setAlignBytes(1); + d->seek(c->nextChunkPos()); + c->setAlignBytes(align); + if (d->peek(4) == RAST_CHUNK) { + auto rast = QSharedPointer(new RASTChunk()); + if (rast->readStructure(d) && rast->isValid()) + chunks.first()->_chunks.append(rast); + } + d->seek(pos); + } + bool readStructure(QIODevice *d) { if (d == nullptr) { @@ -40,6 +71,7 @@ public: auto ok = false; auto chunks = IFFChunk::fromDevice(d, &ok); if (ok) { + atariSTERast(d, chunks); m_chunks = chunks; } return ok; @@ -101,7 +133,7 @@ bool IFFHandler::canRead() const bool IFFHandler::canRead(QIODevice *device) { if (!device) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() called with no device"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead(): called with no device"; return false; } @@ -124,7 +156,7 @@ bool IFFHandler::canRead(QIODevice *device) auto pos = device->pos(); auto chunks = IFFChunk::fromDevice(device, &ok); if (!device->seek(pos)) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() unable to reset device position"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead(): unable to reset device position"; } if (ok) { auto forms = IFFHandlerPrivate::searchForms(chunks, true); @@ -214,14 +246,18 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form) } // resolution -> leave after set of EXIF chunk + const DPIChunk *dpi = nullptr; auto dpis = IFFChunk::searchT(form); + auto xbmis = IFFChunk::searchT(form); if (!dpis.isEmpty()) { - auto &&dpi = dpis.first(); - if (dpi->isValid()) { - img.setDotsPerMeterX(dpi->dotsPerMeterX()); - img.setDotsPerMeterY(dpi->dotsPerMeterY()); - resChanged = true; - } + dpi = dpis.first(); + } else if (!xbmis.isEmpty()) { + dpi = xbmis.first(); // never seen + } + if (dpi && dpi->isValid()) { + img.setDotsPerMeterX(dpi->dotsPerMeterX()); + img.setDotsPerMeterY(dpi->dotsPerMeterY()); + resChanged = true; } // if no explicit resolution was found, apply the aspect ratio to the default one @@ -248,26 +284,30 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form) static QImage convertIPAL(const QImage& img, const IPALChunk *ipal) { if (img.format() != QImage::Format_Indexed8) { - qDebug(LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!"; + qCDebug(LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!"; return img; } - auto tmp = img.convertToFormat(FORMAT_RGB_8BIT); + auto tmp = img.convertToFormat(ipal->hasAlpha() ? FORMAT_RGBA_8BIT : FORMAT_RGB_8BIT); if (tmp.isNull()) { qCritical(LOG_IFFPLUGIN) << "convertIPAL(): error while converting the image!"; return img; } + auto mul = tmp.hasAlphaChannel() ? 4 : 3; for (auto y = 0, h = img.height(); y < h; ++y) { auto src = reinterpret_cast(img.constScanLine(y)); auto dst = tmp.scanLine(y); - auto lpal = ipal->palette(y, h); + auto lpal = ipal->palette(y); for (auto x = 0, w = img.width(); x < w; ++x) { if (src[x] < lpal.size()) { - auto x3 = x * 3; - dst[x3] = qRed(lpal.at(src[x])); - dst[x3 + 1] = qGreen(lpal.at(src[x])); - dst[x3 + 2] = qBlue(lpal.at(src[x])); + auto xmul = x * mul; + dst[xmul] = qRed(lpal.at(src[x])); + dst[xmul + 1] = qGreen(lpal.at(src[x])); + dst[xmul + 2] = qBlue(lpal.at(src[x])); + if (mul == 4) { + dst[xmul + 3] = qAlpha(lpal.at(src[x])); + } } } } @@ -287,7 +327,7 @@ bool IFFHandler::readStandardImage(QImage *image) // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); if (headers.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): no supported image found"; return false; } @@ -295,7 +335,7 @@ bool IFFHandler::readStandardImage(QImage *image) auto &&header = headers.first(); auto img = imageAlloc(header->size(), form->format()); if (img.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while allocating the image"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while allocating the image"; return false; } @@ -324,7 +364,19 @@ bool IFFHandler::readStandardImage(QImage *image) } // reading image data - auto ipal = form->searchIPal(); + std::unique_ptr ipal; + if (auto ptr = form->searchIPal()) { + ipal = std::unique_ptr(ptr->clone()); + } + if (ipal) { + auto pal = img.colorTable(); + if (pal.isEmpty()) + pal = cmap->palette(); + if (!ipal->initialize(pal, img.height())) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): unable to initialize palette changer"; + return false; + } + } auto bodies = IFFChunk::searchT(form); if (bodies.isEmpty()) { auto abits = IFFChunk::searchT(form); @@ -336,23 +388,23 @@ bool IFFHandler::readStandardImage(QImage *image) } else { auto &&body = bodies.first(); if (!body->resetStrideRead(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while reading image data"; return false; } for (auto y = 0, h = img.height(); y < h; ++y) { auto line = reinterpret_cast(img.scanLine(y)); - auto ba = body->strideRead(device(), y, header, camg, cmap, ipal, form->formType()); + auto ba = body->strideRead(device(), y, header, camg, cmap, ipal.get(), form->formType()); if (ba.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while reading image scanline"; return false; } memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size())); } } - // BEAM / CTBL conversion (if not already done) + // BEAM / CTBL, SHAM, RAST, PCHG conversion (if not already done) if (ipal && img.format() == QImage::Format_Indexed8) { - img = convertIPAL(img, ipal); + img = convertIPAL(img, ipal.get()); } // set metadata (including image resolution) @@ -374,7 +426,7 @@ bool IFFHandler::readMayaImage(QImage *image) // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); if (headers.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): no supported image found"; return false; } @@ -382,30 +434,30 @@ bool IFFHandler::readMayaImage(QImage *image) auto &&header = headers.first(); auto img = imageAlloc(header->size(), form->format()); if (img.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while allocating the image"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): error while allocating the image"; return false; } auto &&tiles = IFFChunk::searchT(form); if ((tiles.size() & 0xFFFF) != header->tiles()) { // Photoshop, on large images saves more than 65535 tiles - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() tile number mismatch: found" << tiles.size() << "while expected" << header->tiles(); + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): tile number mismatch: found" << tiles.size() << "while expected" << header->tiles(); return false; } for (auto &&tile : tiles) { auto tp = tile->pos(); auto ts = tile->size(); if (tp.x() < 0 || tp.x() + ts.width() > img.width()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): wrong tile position or size"; return false; } if (tp.y() < 0 || tp.y() + ts.height() > img.height()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): wrong tile position or size"; return false; } // For future releases: it might be a good idea not to use a QPainter auto ti = tile->tile(device(), header); if (ti.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while decoding the tile"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): error while decoding the tile"; return false; } QPainter painter(&img); @@ -426,7 +478,7 @@ bool IFFHandler::readMayaImage(QImage *image) bool IFFHandler::read(QImage *image) { if (!d->readStructure(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() invalid IFF structure"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): invalid IFF structure"; return false; } @@ -438,7 +490,7 @@ bool IFFHandler::read(QImage *image) return true; } - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found"; return false; } @@ -515,7 +567,7 @@ int IFFHandler::imageCount() const count = QImageIOHandler::imageCount(); if (!d->readStructure(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount() invalid IFF structure"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount(): invalid IFF structure"; return count; }