From 05996c773981971c558357a6e9f97ef8392df44f Mon Sep 17 00:00:00 2001 From: mjohnsonengr Date: Thu, 8 Jan 2015 13:12:01 -0700 Subject: [PATCH 1/4] Added GaeVFS dependency; updated imageio.stream.FileCacehImageInputStream to use GaeVFS instead of standard File I/O for GAE compatibility --- pom.xml | 10 +++ .../stream/FileCacheImageInputStream.java | 56 ++++++++++------ .../imageio/stream/FileObjectUtils.java | 64 +++++++++++++++++++ 3 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/google/code/appengine/imageio/stream/FileObjectUtils.java diff --git a/pom.xml b/pom.xml index b0ccf2f..09b600f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,16 @@ sfntly 0.0.1-SNAPSHOT + + org.apache.commons + commons-vfs + 2.0-SNAPSHOT + + + com.newatlanta.commons + gaevfs + 0.3 + https://github.com/pascalleclercq/appengine-awt diff --git a/src/main/java/com/google/code/appengine/imageio/stream/FileCacheImageInputStream.java b/src/main/java/com/google/code/appengine/imageio/stream/FileCacheImageInputStream.java index 5b12fd0..92156f1 100644 --- a/src/main/java/com/google/code/appengine/imageio/stream/FileCacheImageInputStream.java +++ b/src/main/java/com/google/code/appengine/imageio/stream/FileCacheImageInputStream.java @@ -18,18 +18,24 @@ package com.google.code.appengine.imageio.stream; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import com.newatlanta.commons.vfs.provider.gae.GaeFileObject; +import com.newatlanta.commons.vfs.provider.gae.GaeFileSystemManager; +import com.newatlanta.commons.vfs.provider.gae.GaeRandomAccessContent; +import com.newatlanta.commons.vfs.provider.gae.GaeVFS; +import org.apache.commons.vfs.FileType; +import org.apache.commons.vfs.util.RandomAccessMode; import org.apache.harmony.x.imageio.internal.nls.Messages; -import com.google.code.appengine.imageio.stream.FileCacheImageOutputStream; -import com.google.code.appengine.imageio.stream.ImageInputStreamImpl; public class FileCacheImageInputStream extends ImageInputStreamImpl { private InputStream is; - private File file; - private RandomAccessFile raf; + private GaeFileObject file; + private GaeRandomAccessContent rac; public FileCacheImageInputStream(InputStream stream, File cacheDir) throws IOException { @@ -38,55 +44,65 @@ public FileCacheImageInputStream(InputStream stream, File cacheDir) throws IOExc } is = stream; - if (cacheDir == null || cacheDir.isDirectory()) { - file = File.createTempFile(FileCacheImageOutputStream.IIO_TEMP_FILE_PREFIX, null, cacheDir); - file.deleteOnExit(); + // Retrieve the file system manager + GaeFileSystemManager fsManager = GaeVFS.getManager(); + + // Convert cacheDir to a FileObject + GaeFileObject cacheDirObj = (cacheDir == null) ? null : + (GaeFileObject)fsManager.resolveFile(cacheDir.getPath()); + + // if cache dir is null, or if it is a directory + if (cacheDirObj == null || cacheDirObj.getType() == FileType.FOLDER) { + + // original code: + file = FileObjectUtils.createTempFileObject(fsManager, + FileCacheImageOutputStream.IIO_TEMP_FILE_PREFIX, null, cacheDirObj); } else { throw new IllegalArgumentException(Messages.getString("imageio.0B")); } - raf = new RandomAccessFile(file, "rw"); + rac = new GaeRandomAccessContent(file, RandomAccessMode.READWRITE); } @Override public int read() throws IOException { bitOffset = 0; - if (streamPos >= raf.length()) { + if (streamPos >= rac.length()) { int b = is.read(); if (b < 0) { return -1; } - raf.seek(streamPos++); - raf.write(b); + rac.seek(streamPos++); + rac.write(b); return b; } - raf.seek(streamPos++); - return raf.read(); + rac.seek(streamPos++); + return rac.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { bitOffset = 0; - if (streamPos >= raf.length()) { + if (streamPos >= rac.length()) { int nBytes = is.read(b, off, len); if (nBytes < 0) { return -1; } - raf.seek(streamPos); - raf.write(b, off, nBytes); + rac.seek(streamPos); + rac.write(b, off, nBytes); streamPos += nBytes; return nBytes; } - raf.seek(streamPos); - int nBytes = raf.read(b, off, len); + rac.seek(streamPos); + int nBytes = rac.read(b, off, len); streamPos += nBytes; return nBytes; } @@ -109,7 +125,7 @@ public boolean isCachedMemory() { @Override public void close() throws IOException { super.close(); - raf.close(); + rac.close(); file.delete(); } } diff --git a/src/main/java/com/google/code/appengine/imageio/stream/FileObjectUtils.java b/src/main/java/com/google/code/appengine/imageio/stream/FileObjectUtils.java new file mode 100644 index 0000000..d8bc09c --- /dev/null +++ b/src/main/java/com/google/code/appengine/imageio/stream/FileObjectUtils.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.code.appengine.imageio.stream; + +import com.newatlanta.commons.vfs.provider.gae.GaeFileObject; +import org.apache.commons.vfs.FileSystemException; +import org.apache.commons.vfs.FileSystemManager; +import org.apache.commons.vfs.FileType; + +import java.math.BigInteger; +import java.security.SecureRandom; + +/** + * Utility class for GaeFileObject. Contains methods for things such as creating a temporary file object. + */ +public class FileObjectUtils { + private FileObjectUtils(){} + + public final static class RandStringGenerator { + private static SecureRandom random = new SecureRandom(); + + public static String nextString() { + return new BigInteger(130, random).toString(32); + } + } + + public static GaeFileObject createTempFileObject(FileSystemManager fsManager, String prefix, String suffix, GaeFileObject directory) throws FileSystemException { + if (suffix == null) suffix = ".tmp"; + GaeFileObject file; + + if (directory == null || (directory.getType()== FileType.FOLDER)) { + String name = RandStringGenerator.nextString(); + String filename = prefix + name + suffix; + file = (GaeFileObject)fsManager.resolveFile(filename); + while (file.exists()) { + name = RandStringGenerator.nextString(); + filename = prefix + name + suffix; + file = (GaeFileObject)fsManager.resolveFile(filename); + } + // guaranteed file doesn't exist at this point + file.createFile(); + } else { + throw new IllegalArgumentException("directory must be a directory!"); + } + + return file; + } +} From 6c1c9b9d9183b592e0b95ee6d58de7e65e5b87ea Mon Sep 17 00:00:00 2001 From: mjohnsonengr Date: Thu, 8 Jan 2015 13:17:25 -0700 Subject: [PATCH 2/4] Added a way to install specific jars not available in Maven repositories to local Maven repository --- dep/commons-vfs-2.0-SNAPSHOT.jar | Bin 0 -> 398440 bytes dep/gaevfs-0.3-javadoc.jar | Bin 0 -> 67834 bytes dep/gaevfs-0.3.jar | Bin 0 -> 28215 bytes dep/install-jars-to-mvn.bat | 3 +++ dep/sfntly-0.0.1-SNAPSHOT.jar | Bin 0 -> 445034 bytes 5 files changed, 3 insertions(+) create mode 100644 dep/commons-vfs-2.0-SNAPSHOT.jar create mode 100644 dep/gaevfs-0.3-javadoc.jar create mode 100644 dep/gaevfs-0.3.jar create mode 100644 dep/install-jars-to-mvn.bat create mode 100644 dep/sfntly-0.0.1-SNAPSHOT.jar diff --git a/dep/commons-vfs-2.0-SNAPSHOT.jar b/dep/commons-vfs-2.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..f237fc06552ca2501c6775dd42f452ef4ce0fb59 GIT binary patch literal 398440 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

vSvTi6SzA=+Pw)D>lZu@Qa&K$iEr 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 literal 0 HcmV?d00001 diff --git a/dep/gaevfs-0.3.jar b/dep/gaevfs-0.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..ce90204cb4e96f7ab373a1417c23d3f2e130c881 GIT binary patch literal 28215 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 z�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%0ANG%$^u3$UqvdvWdkQF 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 literal 0 HcmV?d00001 diff --git a/dep/install-jars-to-mvn.bat b/dep/install-jars-to-mvn.bat new file mode 100644 index 0000000..2adfcfb --- /dev/null +++ b/dep/install-jars-to-mvn.bat @@ -0,0 +1,3 @@ +call mvn install:install-file -Dfile=commons-vfs-2.0-SNAPSHOT.jar -DgroupId=org.apache.commons -DartifactId=commons-vfs -Dversion=2.0-SNAPSHOT -Dpackaging=jar -DgeneratePom=true +call mvn install:install-file -Dfile=gaevfs-0.3.jar -DgroupId=com.newatlanta.commons -DartifactId=gaevfs -Dversion=0.3 -Dpackaging=jar -DgeneratePom=true +call mvn install:install-file -Dfile=sfntly-0.0.1-SNAPSHOT.jar -DgroupId=com.github.sfntly -DartifactId=sfntly -Dversion=0.0.1-SNAPSHOT -Dpackaging=jar -DgeneratePom=true \ No newline at end of file diff --git a/dep/sfntly-0.0.1-SNAPSHOT.jar b/dep/sfntly-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..36ff41791196279d0869f86ba55ecaf99645598d GIT binary patch literal 445034 zcmbrl1CVUpmM&VhZQHhY*}H6Wmu=g&ZQHhOyK0y1xBouf_jaHA-ha=DmoanZ%7`(( z9COB+Uk)vKDPRyNfWKZ#p$Q`Yaq*uM9AF-^kVrm%Bb1fyBHIK8Z%uEJAu2qd ztormJrnz>9{v;6|gqYtD7DghkpoIcm5f9R8xKu6=P1kys4Bm=%nM5=1G&=lUSlskO z!(UdicX)`r7fUtGZ`!Y&zA(Zv^0D0I@fV@rw3AI}uvs8l4Ez1i%#IxI$gE_hG|zN| zcP-JWXXze6+-rtwv^WJ0xn8c2Xd9o#X;+dU`Mo2pU6#A~*(Q{TA1pq4bdn?|;4CA( zKZ~pugAx-!Rx)@0Kjw5l*-u>ks1&6Ujd}?s7 zEbn?_xb@f4rg%%3>77qvatX9H-N-m>=MvkPIgVwa%fij2ni3@gYDp^P%<~(zD;Fov9Popjcnn+358{O{-ww*ZODR zl@HP&EQKThIWq+ld#|rYM=do6KleB$=$C_fTVF1?e|8s!P3wFk+zmk&g?1$^j~OxC zhzM*G0L=(8)k?rZeS@DPR}*Y(8g}UrXC8$FMuJU#Re`}jx46VabbMh5tzhT+;Nx$f zKZjgjQH0;tn?kt{sx5dU$VqO-^%uX?EXJ5h~eLXaWJto|J&_l`uEVR z9gUo z75o>8ZYv3q?``~ z_IgO?Bk3oo)xx_&L;d-cmkCe1-ZGR?72oM(HuK5viI$$OnK`j?@*n>}Uk?%{zLPmarh3!3h|0nBaBZ6vjBub1jN@VBe8>z%r1zc@c&ZjeD=fk$WJ1a@Rh2OlEdiVi;r z5FX@VOOtRkoLo@1g;CP@H*4Mm7IwyyY-c$v<}8T;@#05W z2RaK9hp^v!ZPVt&hR(+&%qH&D4pjPNT<^!yUYc|%sT)+Y^3}F<9i@&7P8JvIEjAYL zWe@TwIDy<{8HZXIy&C=#3FFddBalgm%#5$Q@A>7Y!4}fm($bf^A&tglCfdCAg`JDQ$^B> zGqFz+#M~`22(4X@Z-j|`q-rP}_MfRk)jm>RuFY^;f0}+Ox;d;C=kn)dI2w)Rbh#UZx3O^ZR-Mq)k7WzO5_tf#_2{L_ zRCUCV;7K|bM^5jlCs6Pk96t<3N~cVWIuy~rY7^8RycA}$DlX1|2(8Euh$?+VjweJE z@taO2rs0ex(i|~4*on<2e^l?E07Xe%A5|%rMV6f~g*|9%wM=iwif%KfvE8XuElB2= z&vq-3NzB0k|Ba$pr2Ht>cLK7iR_FW+nNghuh0-fXJQ z*6a%*^z_oZ74wG*Zc@%FG_v+?B_nb0Y>kjBY)jPjwp%Sen$(*NvJj1wf}SH9E>D&e zKZLre#f$>e;4>B_^9NLS@atRQK83{T;RJMwGQyhRVYzb-!`Grg@K5P)ZT1#H$5!mN$ zQv*dx(L-J+X+iF5cZ%5u_8=jzXxqGyQ_To9Vb-%sa~DLkDW6lqT+*ST7b-^IQ>iSb-QT6g-S#^UP5qa4jz+qvJB8ayMjeiyv2Pdv1>`$>^ zH=*c3Hv=iGQ{WneY1>MKMRm-H*w217gKj+-nYvq=UKOynZ}y?X25qJ6QLe*_(`w6` z!*4{YUEpN!jTFx`t2Z7t&_WhhT2V?-s~H-eDk}x$U-x=|;jMs1YgY~R8=@DQTDqk* zF3Pn#$MXRxgULf zlKuq4;Rf7*lhPfEuBQMd{rR-vyiLTd<6cb{HSZ;;s0JgW?Zco_% zSg2MKn*gy_>sT_PaNOFkZfDwb2`9yEK22MsO+wkoEl}H(KrLdbN)HI%#J+K9g-!7(U|5v zR@@CC;_S z@iyJ*r)Jo7hHft16Nl3+=XdL-FdAQU}`guXua+ zfEqWNzeewkUVoh3(oJL^*wy?QidFFYe(hjtcxzuFA#uFV@;%zuf?x`kQU^%N_(ACwoHZI`twY`S^x zJ*R6(yMPk{euTxpnKHegD;*$L_Ucl$_WX?XDOvmo@P8#Q`mK9-)nmN7&}h$$3zI3z z&wVDqAURe)%0Sg;D{;n`0aL(+sYFFu3>Cr=_#+->Y4I>soJ#$A=#%7CQUKdY06QtS zjYK(pS0nex8x@ecDFd@25DTX#=(}LWT&da~KxpsLli|d8{BeLNXdAml?BC zQKGE^%31*2m_}7`fPIFv9PpOaRmvHqKZBY=Q&eU+*b41dx-8(ZR-ayOwH#ghbza5I-#lmJ)_6P}RY z_ZMjWu-STyp}=Z}j;l{d8DV&{4ARHx}ZJ?s2=vp~kUUIyKj z!eC1gu@TW)5Cg17MC%~64IIWu%QpP-H-h`6qSdpu#VR>S)_!az08lvQT*U$;>cqE8 z0jTu>EcRLe)?)ZX)L``F-{gq8@0s>^&hMzck6ruIb&2(X6MjlGzzM~XlCN$#=&xWq zUKHP5!QG2jo_gc&)lHafFIYo6v7_5U=lFsOpLY*3;l|U{5ViTDVTlctAez;5B#8)V zDfXMzMd-mrq1SR<&GAg6O*E9?@RFn2dGASb zW4dmPiScXO5=8eC+bd0xStB(-gc%Bt9blk12v3OF%JV>JXA(5?h<;1t^g`Z}$cC5P z(oW5i4tCQ<;84o6&?)-S2HYzt9mM%XbK^Hh|w#TjNi6Fn+xQ;~S7g z9pMj<@(eb)rCj!u1jG2`e8;NKYQ7g`J$FX0eY(7BrqZEklPXu;B30f5VT zOKr31LvEm$3$80jnj_Ib?_~u-1%c$7=UWOzEuAB=5n`~V21dV3ao@mo0*=&aHbqBg zc{>mP^%Z=?AzPa(*R$?$;PuES`#$qN^TgQa`#l960O=HDPlPhiiU&rI)Yl$@(R1_| zF~*aG7>~$%a=$JT@|c{7<+y~2EG;;a@;EvYq=CpVy^Y#1*nB+ISjBjf(SqqX&4URo z)d@3V(hE~+a>j@x4HlE~SP&o?V>S$v|Gm4lO%e@~$eT5~v( zC*5a}Wp`>Sp%|s=sEvFsux{~JP}>B}Q3Fnno7$?ziR-f*L4q1RN{Lx;R54`29Oo~);ZlrSVZ+8- z`|^3)gxG-1HpR60fqzsY_lq*Sy zoGnau;3Uz7{O-5>I>io^)iri%+;i>soJ;XkUa#o=Eb^HSL(>((q_-LF@71Sq6fbFm zhqZ`Gm>X+=LCBQ!35gU%I%K*M?djG`C(aZ5?IZsQkrXMq1Uf}&;TJ6cr&#!`MtH%> zeLj-PI4r_DdZQn2klOTnui1%#PW9f5&H?)iqpi0Mr%2mZ6F0*?!PlgnJb4$Bfw1-A z$iXs3LmXSs^_d39c z=CyZ4D+He3z6L@;FI~7KQY{ww>XwGeryTjpdkJ;rz2UCWAEL7Ej#dc4m0I8A#@dt% zl3&Nht}Mn9*Fd}dxO6_Ojb&FBNeaS4d`n(GIK)D|YsPZU9|`O%c;aFzFYk#PFkApH z_}!FuYMc{QtMjxW|7hPUDoj7Y_V!ROS?vwR{C@pUpiQsM- ziPXX6D(5S>N=X3M;Me+V2y7xU$RC074IOxk`gyubgHH7Nd+ zK3M;KV)_RkWhwuZBUVQFvMzDV#Z7KPQ!!I@af)L)b#^gcQC8E^1I(z6s0)rVBU*6Q z0Gog@FZm@CX@m6%eGw2A)_o&HRLN+f%YG}S-s8aylZ5aC()OB@+@brL`()GO^*x6_ z2e38B6DPn`^Syb?8oswzyR8%Xz<5#lvoLuZ!c#P)Fn0k>*rw@m3)B+wVvd z^u$85ceJBgTeng@DtHS4L+jP{^7fsS-l)3q|4R*)=)2U+FNHRycMw8Z0 zvx3xyT=lW@*VOcDm3ECri*t)cTlmRao9NJ^MU_^Xa9dEa&CEn5rk|g`q7!Y8I{B>g z3_ycoS4pY^vjd4NN>{O_PK6Ryv(I7Gy7kfyQs@*{^}!6}=Jb=!!eP6B;~aHEZl3Z; z6F#d;xbr7*g_&Zt-Jd6n%EZUnq$gmU z4Zkid7Bk~%(=hl(K#ChqBp2!vIwj|V+Z`1o`Q(Rrb#BrhN z3aScVtgYNxGtbuQLX;vzT0^;Q@jzN5;~2(mDf=0Hs`xO*o4nTP3GsQ`Eg5@={(@QS z=k3{F&!;?SLe5BnH_SnsSGhvTo?i*6k)A&ZmMC{B(fAuuvRZ@bj`=MiZij)OOZrtj zXt}7rI2_6jw`gAyoLVUkR@n#`HKCCl!5mp=LH{mAE=gUTJ;G-XL7X=b?>+KcBk#RoxU3Bxfr$x6<*81oePFGx+!dL}BdN0YP;pE9Vsyx_ zktET)$}o8=lsF@?@eIF2rh6m)k$GG~_qV`My02-$*XV)oY}|Lhk*ksF046m17|>0? z0>XTtmAC@pXY5EQHz#0Fhsk6vMY0g*P(bTCCiF}A!RLMlB*mSN@)xcUdej@pSDeh~ zq!<3mPsLh=%Cjl}3$j)Yca40@Z0YZc3P)ZVZFot!TRK^Afmgltut=qNx}5wYw-p#< zLN%gHeWFx-kQU`*Tpiw<`ny{_&>zoY-=P1~uhD%Ck3OWaTMaB|a1m$aZI|_M40*9RLl@zWgO!2%q5acc4xJ2gHLE~&;k@;?|9ow{m z$4dRm5MY@Es9;j-F;G%#e;$s2OaMTVCOi(H-SK1^v^?TAHho5vF=W`lbf+8heb=?u zy~%I43cyanS+!cpU@Ns^UvWs5Lq}$d8c2}js`|G4 zn%H-)HaYBK{TOIRxZ<5<>tB!qk|a|?YuPOk?aBPCj>-aQ%_@$5%Z>Wy=o7ETU9Gva zC!@#`nwYdM%G6jq&Y9s|CW#B66nc1}~&??QPsLRX970Kr0k`={SnsSw9 z&B>{RA?*3SG0$Qb^N6SAGt6VcBJuqTvG(`yaB~7Q&@3|>8pp$#Dz-16GpbDPenFI= zwowllTIrHxZcR4k;cOZe6^rp2Kq1L1R$YyH+R3pp_DW64>w}sUFyx}{6(b`$!=}%#Zru&lXObtM^)x7HY=KXXhhk?RX)o~Z5kx)4rftL>HRrDBc zUiC`axrx>opyyzUlVS+Z03J}Cgd8?W5tv@XyT7@A9rBGE1kc8(L3Eu$=E2Ja&rYe} z3;@yu+u};`aG>?8!-Uvk83K9T!Puv#$w;HYdeGKvhz_Hw>_RMrZ!Ovz^O|U?(3GGy zSPrSca+{e-Pi;(1eyEIeXFu6p?|A@9+_VZ0Xtk;4{6=mTCUCL%N}Ix z&0v$^TSSY6pQ9M=AcI=2QT^bKKjo6hXL7`_fx>B7ohPb2fPJoaZThd-bDerpF7t z{94B|B|9p+!z`LUp1<@`;IfUJR3B_h7`OK9ClztHi-{tio)-$C=4QUAFC9S3 za*`&*l0%mzyotc>HiN(@qjUH^*moGm#y*Z@;1j4#94~NXbm8dO>C48GbW-7P4hrlI zL%C$M{6ge^CJ4SD$aX}W;ZSe#xb5GQ2A_pUt-@A<0{HIZT^;yS)$LD#Io%#dw`2v$h4JI<`Ipt_Z?JE z8LV{3_fp844lh_a2JHLw__mZ_sGEgDe!i@2K|5*l{nI6(`nO^9?@Big9LCN zkKOc(-np81V3(-=+nze{P9JnR>9G(a-<&u^}r(JkszFyt%dpYW{+-)htIp`+zHLOips!fPn z^9duRn3$-Esd!fW-mV3%w$C|kyb(Kb1w{!?7q$F^pOW1+SS&|UYub*|J9brQ94Q|;#n@=*%5;5%} z5Ts%i^hU=}HBFZ|Vm?b*|{GJkyAHO0P_www*`I)F2%ZE_2CUDqX18 zXC%3ih3Ptt$ioL_jNA>xk%i1uf~8F_@{gmeQoG>c&dG89Rh0CHLtug@eP9!SOutPB zK;B~7xq!T)Y)!VwmAth39XdOBI|l)&N}QYWJ*T&JFNYRzz}qqmd3x0so(Ucv;DGqc zHxF=P2uE+pgcuNqn6mfie$EUd1TWwx~?Yj@;8*>W1hYmMc zENGidos2m@({uMh$BpS@easiV*^ue|iO(Uqi@xD2=hmyM;1ha3c*6!3zj2BwyS*uj zKJxIja{30kci|EGK~*6IPwp~Tb}qGTfr6ogg%qosCo}D0b}=HpM>nk}bvAeL(wz+{ z7o%0AW-m}R+zQ2{`U{4HC}gqtHonAp+`>n#WNA(X{z`F(Tm~=DYnnl4_zVpmrY9uA zDET$uO}^8i0UX?gpR}ECQLTyXj#+1X#*MVcY#-~P4eVA@lUVn$4Nf_?N#({D)$=hB zYIs9_=A|@QwU2^2lwJ7-|LCE7*>k|YoAk^DbCDe1I6!0UHSA`Tol;|+sVrs? z{Q9B5;-Z@fCu{`%k}JdI>#i>Sjr1*yjGyEhj(&hn{V5$q<}IAeE?GBZAYA7eRpzIh zd>T)E#Fa{d5An>k9K)*?_BnR<7&>nP@!M<{u^#^I3%*P(B0ih{z15TU%~1FCT1u`6 zx!GJDt-n;jV*caR=a|i$F3PJ5@}I1mmWUAALrSNLo68$do5+mT-F;QWrW=FOzH zmB*&3sAY1PNn?&4tRSzxA9Yg!ekFj?eB3Z;;H<%$`S@=JVS)`NEtn5WW-EpiOP{kF zIF2rztwSzAchZXmZ1Bx)zb72d7l*ocOx3De3aS^v@@0294OSz#7_Tqkv$`lyWPRvS z`exUT{JF9190{1Qb=bp0n(}T`_EX`}F{WShZuVorpTYeJE!tSe$Sy)Uwh8M<^W7%A zDWg}n^YEX2C>sSPMJA45Jou3q9ASP{DX z%)(a>wt@#rtPe^VG@hJ5u?j?OmFj z_5|(VXftvuYHEuKnpwbYR=(i&MjSOZonV85e#$FY;xRi0L}qE_e-;y0N6)}B^$m6z zH{KzBO?1k206X3(>1MN!lr>2LosgPe6K&oZukzuaZ|pfncd^40Z2b*Y#k0VreT4<1 zW;9c7Uqeo5&S~CNJ}OlqUS)(L%o+3or|#}+-oaDGP>veJRK_pptnht=vIuM7ouG#U z%9>Lcom(s>zaW_xS#L60LRle{EIWhRncPn=9~sGna(a1U%c*Rl(%8+sA{D1GE|NL7MaGv}hcRF|-d_@@kTLaV%?#KSyZPBCm#A!Fbg z()mzBVEln>7r(_}9QOR%^fU&Ey5k#U*x=Q?n=nR0Sy4w}wz{~=JbWH?9`7ZwddO6` zLQt5^yr9|a{%S$P)7#)X6Hz9_JE>oXET$^3tNNUAO;_2;BOv~0X-f7TmQNT#?Lub3*J&m$<-!t(4nzJ%3 zD6~kql0I{}!$-DZ42ItgVd_3n-Iwz@2Eom=4x$pF+)>7A*`cox)kfQ~cUncPTCst)u1hQYk;i^hRS8KhaPh3BwEblO1sJG zx=u(FSLGm1Ir}{xlhJ_*`5QIF=mxp+xGVc`GLQu(*iH5B0$JcK>~wj;pT-(Ff$hB?5MZ}-5$KAomRP~H z={JF~f%*VHqk1U}IIJf}tX^7M(lXljNTts#iay!zNzA7S^;Js{+Y;oT3ci9JdpHgKQw zS3A)^;%B;kJ5UPW0*OzXfVlD8VGHFvVb1#|Q4srzuP8J$0nhfwINh3<&lZqvx(o;@9(w*}d}DBew#ndQBKF+ImTo z_Tbc>JRrT+1Sag4d6N5w*yEyR-UvAYqjF|*WxYboN!u9Uj)d=)X0wS`_QJMOu7;0d zA}xiAdqP`eJ42b4fZnqGNqpY}|L?>0SNrQT-EgidJOF?l@xSHuD;Vn={q<-3UuC3! z!jNb=CB3>g4T5XigU@>nWlu0KmOJK7?a?w@jSUvQ7?sCHd zLqzy0*)Ms&>X>-#_PBDH`OJP<$DYXol&>KolMTteF-G=5@CP>)S`B3uT%JyUk%S=2 zPDw#8e9c7hp(ywo9gG0=Uw$sD4vF)^nL19|@lkW?tZ$PxOiM`L z%gJ#W zRzoucyuES~4wJCDxZ_WR8zwEeoh7mzQi8%g!cKY9pL2Ly7zb zK$ZPv^|)I_ zO87J=rNbCE$!=kpF=d7)T1d;MIei;hb~uwLol~f zM}aYeY7do76)i0{(Q<2}xwWEUa)Ft5tL}43E3Q=rj^dLr?0hDd9%+`~)MHDb12rW7 z#@3;qaCJ>J<&sClQWlo@gTuhMG08#ydK^|QRs)+7Nlf~+eCzZO#&pr!MWLb9_A%~a zm*?lSBUzem*8TpqEPT{&@d$i2Au8<_D~=Ib<5jgVzo!1Qxan{*xplct8SwV1JS{5T zRMuN)P5@QxFBcPqwupY+NVlhFk`jW(6ule1DUC2`mz<6U81Yxcn|N;>n*DP!^5#o+ zJpHa~bvG_QoqXM9&UNJ4O=JWk)}7yL7Ez}Kh6k}T)sA#2$v={$;2}dV!@HbJoVH_> z6%oSxlQ6+mMp4;7``XQzV)RORc{9(N(-n!7=L|m8=`<^6ZIgC2EBzcz29>`Ut_HuU zD!WB*6V7_jhG}=_!ca;C<~dPBnYZbhREu0`BfkgSf`X6@AK@5zS{}{D#@pk{I^iNKh(YCZT_;_L1 z>WSU0xgX>yVa!(vjVI-o7TI+%NPeM3xEz#1-_@LoD~ftJ>^#x>LjBk6yRQr6@nX-Rno|! zEfPXQ*VT&R6?Owsr<~L2RjB~6H`CAYTh0k_E$56rNYXAR zj1MUURFZ%lnLlb^&Og=a(WUiv8ZvP6una1ik+#69W4<3A%?)q}Vcqai;wcstr<2W` z>GP1#qP-RFD*t33k}tuNb*(L+PuRB6om@G3tT5K>gYt^Uc9AA!?BtLzZw+>Hc21DN zZG1S|AX5D-LT~PdLjLh1S$wH}4D2HuCHGhTW?t?4vhI?0z9AXC{gb99pG|o)spXIo z@(&$CjW)6|X(Hs56ufjNx?(F{qV%jx(&z3H)lN!ko-3}KQ+>LwHLlp13@Unuq&08d zDjrl_8Yp&^7U|Ce!AJwXo-ejL>iGz=Irimok?|MH`1|S{o;y!EQ3R`1A;h)u7VS*a z(bENa?t=q{b=iT21{0iZWXnfA?1@-BJNA>yVdrJ4-@lJN*@?Un{HR_u6vEI6dU z3)Dj8&|2$8oebmHsvT*|w$w&-Bf9YD1omfA(H7wzIpHo1Gvr%*;gXbehO_Jvt;%Sh zjvYu2?>Bu9NWRPbWz=@v8+vcaj$iZ+y&xjsJ8yCDqwTqkkDnGOPT;g!ik8a~XE1&w z@+n8^o4nF=@Oi*Ej8m)Q|3IDKwm6V8wBBS6zBs^Ro5B7e&ED`XJjcct0FXUI#WRxN z+}?X|Sv|7BY9W2 zrdQYD%BVK(5xZM50Z-o!Cjv{XZf#*V%Dw1d5?PIUrqsnPV8;B8jDP5Uq^|&C!)tqM z0{f%RbrJm$nPASfW(A|`#*>E)I08PazrXc4aTQC%EK1$My4mfCgqP*)mattZcxQ3u z2h7KhD=v0Du8C{gMVGeYYrDl~J6vyH&KJVuUE{0Sm|8skVx5mb@Dt;!0l6{`{sF-c zoaY17sR=4Nci!ZE_EufKXd1y_R(?5QRsV?(2>XkgM3edX{+B@w=iSPNz+$8@ z98NSC5nyuPz%yz3v0R`eFEFrc)|IwE8IoO>FV#^W(2+>foqo_=L7K&C5zwS=!0JpY z;<|e)0KW3?++r)xnp~G(>*;mznoC2pYeAA9Xcn1bWo;o%xio9MM8xU#MHZN}j9^a| z`Q}+z9^%B%a}Ijla5<`7!Qm@)YGoN?4=~9!`QhbWsXb499K&h$JQq$M)^2&nBElBa zJj10`Wt{gg`z5vm6hL-EC1~dwV48Xsmu5z(3Lc&wS zTFMgE#|B~Tc+^Xs{?rxQ+UmUC86isMJH&cslg0$DnvMjnYhTuz7p@-+rsF@g8SVD1 zT@f7~0Nr7Z?iQW6{@g`@CliFpn!l!(vX{gTz!uH?qAyHglP^jxT|nCc!WXWwlu9*3 z$vl*`{ZJ|?AdVPd!05X+g~`_@W2OO7NvP>bG71oU^dlcaznB52#iY!zodb zTcXSk9<2-*s&1NZVry>>a&AU%ZC-b>YP!(yR9Nu_zqZr5IOrbrK~ie1Ub;hkJcl-Z zy^Sc+*}9aR9ip58o*pZm9ZShVAa#TzXevx54SH@5teh(mqeIcq(y%m^6HI*5ruH5Q zkB27*H)ZCH`^h|h!Gc6etW%bBe8W3bIsjVAthIQb z@n#Qbw_lhB{=*x+b<560=T&lJOB_$5kxu6(AItsW;SXL9_k^bWO3* zPqQL4*rbPjZ(;b1>s~&(J_1#_)V};n+XB9A0@JyE%*~*@Jr3RQlkkhrQwga|nR{Y~ zD>}HL4m@Ju8RO%8fkHJW&(^1zz>{7(9lzn0&?}*BPm|Q|VQ+%|MS<@!=wc~8N&@Yp zh??d_-=!~0R*$9*_&{ zTQl%M0Z^e=1gcdW-z9AOFwi<}`$F83M917dZt0rJc^ozK zJy2>TDc0K#v`~zk+*=0mHrNhypP!R+dfgy%o_d!jcqFT4!GwJO@K=_mq3QcugPu0y zab-le-+k)Q3yOEjryPKWf6n6R2o8M#bM-ynVdos9GU0jRepD{9t#F6Pqc~K@25xh)K1gMaz4?y^#N}2(;(9*NE!73X{=!l*GQ)u>d75CPQKs%qcUL?dYjtlSZtn)~Qv0++mfj)eM6{OBdAv zML4ry5c^4JGlGx|p9c0#A@P8V-(_3v+YM%-$q*ge{hZJZPhjSWfOtSS$;KBbG92>L z!V|=XkuRKec%*KQBi!WRXCuZ_4zu%Il}Kt0WCH!VN=og2j+1L@g?E3l$pPupK(a}> zBEB+6=(~WMi1b<=+Fla!(^%$D)?ey{h-k7h#i~(5x-T7c1*(So>BE>VHIVPu>XD=6 zj4U#6M!L_%&_?S5?N5<91^o@~#|}Lp|2ty;D?Mgw$P!%mr!fNS&$Fm9|9bjd(b&|= z*xJ$9=x_7rL&`f2$bb5+*gD3@^fe{T^Yg;|p_=npAXtfLlZqQzW913*R?)0l#>ikv z*3vBl8HlGj_)hV21Gox^={s+7nVCSHFmgVDJ#XZ0RX?v}#M8!MmT9M4EI^OtD{5w-Uo7sZ2 z&KjH5v}q7$Oli7$FuYltMw79m%>>=zc>Nc%N^ON~rA{f=kqM0rmkEs_Dz(Z|-5oRQ zd$@R9GnI2ynieb{Fv2kT4UcxX34`M^##Vxrk|!&J*yn{l-4>MuKyJq@n6C zqeIHVu||e1_;Mr!qFUNa7WIacRK8`EIhxp;CIC~B#io)Z)b@7`mlFu4$FK45@^@zE zg5-u`DgW{9ksz3eX~r9zr|`W_|E!EE5?o>au&NW-z}g7`5JBK~M0SQiVlcQ}CtL?G zy4}#|7MMxHzTSK_EtVU$eXecB+CXDqG8LG^SXGAm#8WDcmqLXGV6`65Ig_n;v$aYz zLvzK(Kn+Ue*D|Yg=Bai=!1Na z`ZmI-(X)>Rh#a=)SM(pk-5stY|%vM8EHby+9qw%^2=DwU;vu%$raMDVr3rf&S*Z(xRvF*x<8DF*b#uX%jjQs$FVXhHCwa8O&w!btr zQiW3>-+hAGCJl>{J=J}Cr${66#Xv8rgSq$oa zy<5GzuRj=inD1aCZhyX7qa_hWSJv&9)@nh8fPwG2AQfxp7iMCJ#ZfAQQ_g=W&mu{QKtCd1MvJr`A z5tr5x?`hV1ja!?6ggJ@PdQ3WjAX_L&I4YP(PM$QO0yR6i;Lh!rb=(g>50Pd4BxR~P z$rQDs38k739~Fr{i!2MhLN8d?e5juFs+Qgkm}*`}XxDWFt^JX?>DZze4mOPC{GA`y z=mYhlupQOye4X9uMlqAN+9n%Y;im|{yd1+bVMqJh2fxd_`t91tz|!8=wH+-cH1ECd zW}OTi@35@$wD$2=rk}8j5Up5C3Pt#4cr8_LV56On58}M7Udg~%S&7WWg;WnT1m4Fo z&~$!0&mQy##V8#u57+e1`oME*yC|!KM5e;3D{OFxozZg$1gwS*&(V^q-7uNv<*9|CGS3 zV^_g1{?lR`g!ykJa8>Qi|KkDk|Czx3Q|ot09K!sb+HO#fM_2_|15r`n86%4qfOJ9d z7er&rC-FtFXjt<~=&Vg?eHVpL`cw2>jzIIMYyP#AtV6T(i(Q3EwO z{AKxjpl2O@SpNO(ip_UB1pYn?9?z@{_SRQQb{lv7Z>^I#&CSlsu4-@BlbP06&Ix*l zduV=d%GpFJ42cO29fqnyv*Fd88TDt9s>0ssCs#3|q#5U!llueAf{pa-$r$wb1muM+ z!^YAMj^qZWg)MW9P3Q3Yc3czhPm%q)8kpG-*8QJbJ^! z;VMW#M0DFIq>tesgJ&7d;cX#8PMO>*W8u7>%*T zGrz;a#e9t7Gxd4B*45?fL;miD{nLchlFFzEh8(e@GF8g?gDF5xnrq|03s(uI(}d0{ zvnaR375?1K-XoWnZ&S>*(L_ST+b14MAc~jtxr1DN{{%HT4jE#ssgcI&c$hR&X;0>; z#NG&6w_}}LniC@#IAgUt%*c;S0jSB|)6telHd03`% zgr%cPq-J0r6mjJEt!otr`{Q*zJk1hV4Way+$h(zE(>qy|)Fx+e6I z=6F4|grar&iH0fqSh-$MZ-FS3NYk28&h4GgdK)$mzl*RlWGCqK|Do)i z+C+hxZN1gDZQHhO+qP}nwr$()UTybk+qOHsvrnF6pIqe2#ry&DX4ITjquyci==*Cm zej!MvM;Z)!1)_Tk&#EH$M7V)!o`|0yb~-?2PF@Tvs~$4)Np?Legry6Z=}9j8CqZR- zR8*Awp+Jf-EvF&EWF!1wq_9XS%4C|#d!g|$wn^L`5oQmgCin`R7kW#UeYhQumltx& z0Vlg#01wK5JZP|qCzxo0ME|SAO2H#A7-oMiIy&t3P%C5k19C@D-2zNF3t+jb_bk#T zG~`?|38{>Vku6Kzx7>{vJ5)7h78-#8LIjf1}cA*49-(5V?5TQ+fqw zkEN9K7^M$OMJ)54NGm8t&|Sx7CiOY~4BixXd|XM@n&%!!E2l_?;-pYezBfuQ$2#uf zsIui}S*CY7K5GA4a7p5GKIxD1=u3d+o)=QbTi=T`RuZIB;9<3a*h;!umaZ$ zVV~y)QY&og0DiffEboDwjlgh6QrqL2=K%WQU+rq%48pDWD`x|y9iH$myM#0>sfbhZ z4>oX6XH<$esZLyp)GFTXT+ zm#Umf>x2=Lgn&a~67vs z+r|E-=uX#Sm?!c>75D_*L9PWsEgs>*Jn%1mi+A$J*+pR;o#^oS&Y~%lozIAG&yoxTmdR%4#$i=-wPv7|NzbuIL9oJ;Fe{vJ<9F(8vbmdx1MC+4Ue>OaEe z4~Ouf(GRjdmmgQ}(ohOtl26|iyTx2wzgbuprn>nlrwO=r1OR{U_JZbN~}b%B0H3rd$$2aGB8AtCZ|1;dFbgD1JEWdr zC4L;^Vy+6_ir7o#cM75R5LdK9)?I2?z6dmJZl`2RJ?>pg=_P!wG| z-jdJv%z*DZh~4^Ct0$f;9eXmh>qu9t#vN|hF~+1iRd)pv?*im@iwV2+L>q<6r-3wriS`+p)i#j8T?mTzkybG^K!-AW`oM*L2Pu@T{@bis-RA*;+6L1fHs~$G$o#Macb&mMMRHf+Cew&&I+dw~AdO3$hzSm05vj zXhxo(=n~Ek8p6$rMdG?u?g&pah_%g8Y{3pOVLC*GY6r^iH6(>HAsb~nE1?&t#eg*p z=DERIaa{@l+`>y>*H~dTN)3KplbS;_tlKy$qewMuEV%{Bu@)&OOA?-I?l3B&SvE3t zLz%IcNdOd_3s+DK)oYmuN0Fjgb3)`(p7a+l8Vm#e@ZECasTgBB9>MG(D} z;IQIkR?UsGIl50kSq0Eaez%AT_wYIMBVf%7filfik*?Ix zm_a0(gXvVjCW6>t60;RE^4czWbOt*ubI!6-dnAr-#h9m?8&51<-IJ{yPY#Atwa?7d z)`LR%U7n>CpSc6W$=Tu6tcI5{9atdGkss4CJzYtZpMn3O|>$@AP4E;MSjd%T2U_%w8PsOPf>;g zp&a%g3}qHm)00UddorQmb_<4)>2|%3{dlLkUryct>qQU=p%IYwn>yM$#wkg_6-=A8 zRzC+IntEnZ8d6fl_9UHP7)Yueos1UHD}bGJjEpC<8jV8yj9TYpU8=>XS%KjbgH{T+ zC~Rx$gPXyaMp}%^q-Y$))W651PI6iM?Lu{LY5K>_{2N}z?Jn)g?amIN{^zxe=(|3c ze*rOPLVx}+|L4~hbg{5DHgWu4eg#bkb?xORzLTvC52hg~06+*NLqTcfLBw%M;-~;( zaEK^C)4B;`_x@A*ASNN;pKF&Ym(|WvTLq)#$_P+I3z5Ds@jG89T$m!O<1 zC;N=Z$(;5<7+G$KkrZ3;AxX9tHHXGXvFhUm%h`UZca@6 zqkL{R;7Dcq(q+pH`YGG@(B{ZXf>Q35ABj@#UV~XyzaE}-@`WHvb9BtWTG^GbRgXz? zTDS<6><&zV$f{>~$>#7)f?~J#sfuFf zHe}j81XFfE%Vs@H=X55^W_@HyvOQALwN;PfR^c8O#p+jw_1T^!cgib!ZHB?uKY=TY zdw58-c?VAh*VK+HF;ur-l+DbD=smS>Bzj=G31$kik(hknEXv+XcfCY@uyk#IC zR~O}0l#o-V%k#9^M3iM0rZsH^w8*myS6A}{$|bcmOz;Yzhk1Pi#S+e|i_=qTLvX;{ zqL>C9<}p=LS5k{T$ELQq+T8gewKo4}*gR|X7xUqTAgPqDCv#+J1UM~ou<}F<<3}Fu z-$ERy?^affyQjN__3Xktzaoy0^r+9gxls+~oEx(X>*VDW)<+|mqH4FKg#!LIQ|xg< zEJeO-vwXU}vNSw4nPLn6am2m7XlWV0Z{PgB@dVybY+X^f&geIXrxQTmr=L8S(S$qD zO1V_YB>ySUQ_$rUhMQU_FkT7u)*bM)U7vShT12zHSGJW_(k_3w7GY+Fe`s^W8oUW8 zpZ0W6L@#c#tjn5cX- zi49Z2;BngP)n(|#=Rq7R>T~Pui%814x=D#zFHJRvz+?2#-Vu_1MNRwB)NrP zv=^(;MDIEgn=5xyuHy7)RlHb09M7Mw7EhVd-m-K`tm&mCm`Dv!u0LaHDg8(Zeq_}2 zrRZTsWK4FdV51D@Wnppt;Ae>|GLnXH$rX)*T1rjaN~f@Bv$kGLBK4$fVL3V0wbXEV z*oBOjC`PobZSxN7HIPcV@a}+6Twaqn{FV2OI_hd4p&F_TOpxTv#Npe++Hu7MC)+gWX=Yx=i|mvUk)?rhZfYg) zS8V=7n6xZ~xR5!<~A4yO)Q7uOkJXX>?JqfBMlDAPgD=;?SRY_D6QCK1OG^^gxt z3*9mWli;&JT32;Mk_5kqCZJ33@trEQ1l3!Ug+XvSuH^!bzd+D()?)l!^_IB9`F)~Wf` zB}Qk&aRY6G=FZv684+#qJ`1I8-M$RvcH#aN_$rK3M_!F)^$8l|ph66W9$V?BcEaZ+ z;|YUhA8s_}wpa@FZC5HmTza?4kZaQN z>rFUyw1L~f@MGBruBYYSe00}=iZAj*iciO5dfh%lM>}S#JiMEyQQ#q)JZM)UV4I0{ zub)NH<*~cRv%$Yx*ypexkCuF5qO{kUlbhzZDDGNAtGl)^*ZYT)_GBJ2>_sfwzsreL z12pzj9Ud1IP4k}XBDc)J_g~d~ZwM9qZ#D(?yiF`=;|CYuQDddyQ=7UlW_#8!Q1-E) zFGDq$pFrOgdoQ~-hDj?&9uODvyB*x_0E3(JJe>E2a2B`hHR2NnD1B9XFMD>Fy#ajk zcQP=3;e3jBCNO@`be6BE3-3{*hq=tJtli=E-jz?bQ%76! zu0YTvdE!#ChcRIC8>B{$Mbnhu+uLOwgy&mgowq8Zibgp_)U6B5Wcsk7sCq|ZaP4t$ zH_|^vvo7t>t7$$GU=q)svM`i~KSMeT=HdJ&>nA_E<^U%~MtISL*EUeaPp>c|GDOD2>mHH3B0&qW}*l{(v zFcTp0L*@5R+6KtPK37 zl1fKLK>TEsCEE(gr=$(;gX4pzw?}c~=g-DBN4rio>>%^~kZAS&9`dy}Hu zg|W&#IMJ1}9tl8Zj1)byb&bujxFxRciPzF0?*;0~WJo~J!9)b{VsT~_y%g-%1rM-C zeXr)A;$0$r(I;cO(0b|?I1#+3iuFAxm#5a?^qIATY0YncRVt7)CdAiQHkPe{8TqQd zX*-^Mj%d0+6db}4`D-GB2vo&Rb$iCqc7PS%Gb6+;%&}vQjg1PQ$i%FAz8Ue2=81a9 z(6rcQ=RJyJVZIRzw&D*;?~a*|JXh^;dGmui_++m{ED>-?le`j_f}E#OJ>|LVkhMnj z;XOy3uSGKP;mOg)`=Kl&c+rx)qGm~BTtx&$6#uL<;b_suiJx&-O{ot+*sP(R4j^B~Ty9!3KpW%`G>rAPZ{^Ok;a zSXghIo|;ZzAX3H$CMC4U)^*WK_b>Ws%lx2~`K~p4uEA|BEVXV>|1#yU41ItKo?RIM zfrnj*JjcJvdB1Za`Na9{T{99W#PrPGyP%W!DJ@UF>HF+LEFW{zrzbEl%hW}GeME0; zq`3)x17vS*jabKU4x+$`fZwl1bq`fvG6M$ir|tE%hW0 z5+%!ma-nPtlv;-{mjE#$^Bq!{^{biW6$8Z$8ht)kQ!@04*TfC!N_w>T1;$YDyUah( z8~n-6YSjMPMngWfM{hhu#4S2Z{Y z9CHn?FzqK)7e!7RfXvt+&T}G&dA%g&4T`UIM#wX`B}Ygv5H^TifQd?O+7m7TOI7F< zJxlpSTiC>Cj`Fz$o(Ohtc2$It8}wb50HqG)JuOp#ldlq`8GQ|6{-&_Z03`H*m*g*} z&mUY>e&x)El;hfqH;&$=4slpR9<{Y>QqZ-AAc_i6P-m{eBW(Z|$NwimxMpSJK*??% zb+UjuTSTM#hvRQZb5B&hc*oj0?0hS$0`!i*SvVLikJKLpaC4=p>CslB=HXVNKZ{7} zvmthaPNWwX6-g$^41YMXz&VmGq9XoLjKpb)OPu%~G1^n4A$h5Zr{2eKkj8cxk1vBz zViT~Y6fftpsXZ)kZ%dn6w}c|5D%eWgJE|=@uJ#Kbtph_CYfhSIB91eY>=uT&8zeN} zLX|pF>tob2j-tlWo$kUOiFU-BLNlTXzM^8YKqM>9__%TkXx$&`KqOrqa}f0DXC~#9 zWrL`SB`u#2q$$_~Gjec&;}(xxwu-gi;7f~ewr~e`76#GBsB8;`7{%wa_T(m;j9%%Z z#a&{2hFl>)@hwW6=5zm1O5vfG@c>sPhfAX51|*}38`L~vxae1%8`TZGfj)nV~wB0z_X`+kX0IYFj=W*jJWRG8#jRJvA08IVhhTh+g82bKu8#cF!x+qI41#O_VJ6RtQCl!{n7{XM%8^kWFIW-;L0jrA-mLOx>D)@XAseZq; z{vPSILZamFA$d;4A&&n6+#m#KjPP(zO210yhzjRtbaOz5JD9I+r8d_Qb+d$nCrBhf z*;t81c+(5kxUz2+{)q(Yo&@QhG^U%dAUUa7QOKU5dzqg623^fapZ@trH9&e3MI9dG zMbzCNWEOh5;A2C>vP1l2@HKfvGt#Is@|(o!Zz0rOp7e_DJbe&T4e4pd495hJFO<*B z)NJs7i9PK&%$m9dslv~`MvZRqd)oEGPPG@bmT!v#3sK+nH zOTZo*+#NVrQ=}aau9qvW(Wz?XBWp8wVzlCoOND&_AG1m6Ky&z% zHuS2Mw2TDlnP4+Q5E&daty*(Z+*f!x>w;uLjeAC3_BW3RD zP&VLJ6W#paDZ#1RL0i^|qu;-RRo``0-`!-)16DkS(TiYC{v_`C#T7CfknfcBx$OcW zk=P_N5zQ1Ds-7hhTM@1V3RO$VYnP4GEA4Oa6By|dv?ddexTL$1l(yp`E(^4Jce93o zx?+O35B{Mp6Vg(Ut2N1}lB8}m3MnGL7NJs(v&7v=(PqT92tlIK#x=vsCWf;syAKyqrBKiX14q&K;u?-73A_I z?>2sL9`x?`GBsgqr^29yDJ|Y^7!OW#cgPM{!|}Y)f8;QA86+B41Si`8(`X?;V%iRW{#W}OvVt)8g-*U#hrls*Em&UUt=DWCBn5yz`4t?7v@9w=R<^y zn~YxbO>p zlvz_KrQfj4wE7m(s<60~N)(2L={N(dRzOp?L|uS#Ad8idD}WgQ{KJH@w2Ga9?nj6H z*=M}a(AGCMKw;_+n_m_S3{+T&wR&!G*V=pgPEB>ud#JmQ5zjK)$VC=bc`u3Xj;29T z=%G=C@0tQN)#0kQ&)j*#NT-cxn=Ubxh|k;# z*j4lx^uH_mPOkm5BvQwGeA-ZG2Z+O{$$V*TfRuenz%i9wq*Z~ic3i-H&{I@4fx1^j zZED-F$q}4ekp?JRM7P13ecE=h%TTakpWFC$yj*xVP10CnqPV0RxR(haAdyMoDkgM{Qm2jdTr7*8Y7 zHe3-UdBaJdT1PkxxSbd@U@}Wc?|l9ul(=y>3gn1=A{c_40pF1Ju@uEn<~j>_?twm} z85=p`QrTPfe>@vKKX$P#-}(q@A1Z}cDZUOn(h!+v7e97CW0E+mloLC6PxOwjR-2c-yOojp8(hd4hCE90F#|;0R%p zAiP0Iqs%B&KS--lX9v#q!L#Xf{i{Y4+C{vvT%*wT6z&i&QFamp?Fwu3WL~lb*PDQB z3CgB;rAvm`!J>+@a$(stZQmh&Q6&9_l?B#bioHC=LthInSh};|Mea)im?n=r`kjZ) zyxC_9mF9RrulgdAV3Vn)(ndf@V z=L8yke=FT4-j`Lss4I1bT1HqMJ2_l|Ub0B8ovc50HH{8k8XbESIs{a0r7t@ss_Sjp zCE4v0I}OCHso73%FgA1SEu+pPA@@}jlWr0sz4Mk1VALsn|6B}b)r)+FD<6y*Cv6j& zd_v@`(-r7-!_bB6X|bLtDCuC&0c@0X*0Dssm*DwF1+E{Ly>9z?=&&J?j~ z1w!ITx|?S72PlD6!Jo~sKA0a%O>5}m!t9K^nrkZla>e#CjQ~HSsOi19SG+j{#P3D; zKLhf=$O519AlCf|%zsk4-*?9Cf3^@uE!Y19tqSZ;H}!YT0op~8q@qG~(-7yI5xZA~ z1+*2r=kwwFjC4CwWtkUv7#NuW3}UO%SW6jUI)gww@k4exG?TS zvi<#Z?$$WoxMQ8UBg5V@W@i9?d)@w9@zi#>++hFw;s4L4 z5HbcvzmXGmPUa%EMs~&)wq_E>|2>f?N&Dx9Jccsbw_wduG+$mk-%A9X3M`5q6r=$H z7|ahiC=S@rCR&q`>e(qW3#~4TO*SBLNj)RTYSv_w9X}$G-w?3IK;ZDK_5WITmM!t%>JQ9LYl5dOqx+Afo=3HzR&(JhNv`Eg-Djpm%u*pSusp; zJWDLgV4|CrZt#vs`Vcxy{Qwhz{U8&;{ZJY_ z(oMoJ)=k4O``Hpa?JZT<@2xdx@GUxM(o59P-%aF@`=$jE=b=_O{+S~j@%DkfjS?=y z{t)Xmk02h0p)pC(G4iQPb}^$aYilfu&#}*5%4Uo;QmBEv#+vB@1;nvB{qLCH!eLj+~}H5Kz?+G4g_oFJ5EF}rLqE;ULdYHT>x6cuWbRz~WQfet-3fr&fH z5jw3$(1W*cDVvGANO*eOU{;gae8F~c0)tf#gAEe(1e@*3u~u3K3}%eM)Lh^HfuZ3 zER|jLFwh{)!XLe$ohPYFl3OsVaeM|)*mfz4_y$(fp6Bc5TZYr^LzB}TP- z+Oc{8HeeJCnTc_MO=3~XqpCbkWQIY~B#!9$JXhWh&&F?wTaWc}e_OpZ z8u-j@;V4Wig&9bU1A6v?V69ZByx#NFM;xftg#P4D9(jb>?Flh&OdqkrXb3UtoipO>K?rdy z{t!-DBKpY+@FejhL+Syw5;d8+>@NnCW7;wFhE(u(r&eRb#r)*l^CNXDV`$OIv{ksP>)mgIIY4Go zuEJN%&S#Fv+0RpQ?lymMi;d$I-3J$Tk*w9MKH~))KNc(-oE_sSQ16Tq~86Mz%I13-Ef7gm*UcUmrs0gS-Kow!Vn9CocxYC8!2ag$C4= zeFdDVfOqzNCr^RReUgw{44#Xo(FG zqNfrI5xru{!N)E@y}~YOJTm89k;<~bNb{kzCs&30lBRsC;+SvvJM>;c;V(Y&$YHg+ zmJ&mux-&%t$iA3cWgC<7ebhv@__;>8q9T(#2oP}Ca%=uc?F6ZCMndADzSgI4` z8be9hVzbbyF|xFoHo-lz3Juw1IyvQzY>yIi~8yi@j7e=dKuezWQ;>{1DP1!wLj?@}Rq zCDwwcM5Ube9H#uj#?17~l>aL&;$1$ayd-90N_kvF%S7VCY+Q=0V@WvIf(oc( zO*HJ|41XH3C{q47FC+ebN}&9vE+hVSN}~L6N=A-n1;N6*f?(-WO;+q z2V5{PW-3?|M-xnnw*fxQ>k6;w4UMSjb4l9#wkBl$(hxVdXT(>qI0hh?7N;->i5JGm zhF`nmh1VONvJ_27V)%$H`548{8U_3uAsG$z4jn~_ECQJ(%t8wlLRARLCQS1+n>gxK z2zwLd>5Y#mEY2L+Y>r4J(&Vn)98NG#$yg|3EYd|D3F{!?964DirHw!}3^Px&U6ODU zfqU1hPQnXcMJ2eO5XszQ`tt?Y9bIST3nca?BfA`Whsevh_W8eM)38U_l)n7JNzQ+Z zKGT0*@a0TRf1_^xyT+#|>!@L=Aa4mVPRKC!|1JZFTSXx!-ZCIawFII>%9|TmV2EQ~ ztkQ21D-B7n8Y_eL%|NaH@|`rPn*WIOTQukQ4H zLFnVQKs#?5lAGNz*p@kP#Bgb759@^51l&=}talrV=_h)q=|^@D#+&t$$?Nyh$s5-~ zN;CNp(hq&|>!-dc1#leC0#hH)f=Ev372lPFYF8bLp z$ySkQVOBR{VXHskm$Q|i;P6787d(_Zm@fnJHLewSnAa@62($Sr2#wm|U9x-0#Ll!7 zBX!2ujMYiOd@aNc3Td&HV1X2bT8|&N4pk}WGVQ0^oPq9ryJ)TVl!WuV zW_?<-Xzf^j^DcNfJ+!QsJAzK-(c8AgyPS}}!d@=6ZnMNxeK-o6F&eX6(A987_Vo2> zm~$#DaNOmbjzsd@!cmq;>~+HTxYmOxuOpd#DYU|r&5QSQYLYLwE#Ff(z}TqzkX?fP z$53b57<=(o)v8dL@^17|-6mkB_y#KyS<05FmN3C=Cxxh_dY(n=eEtoSCAB5U_4Kbx zoE!d7lasaOuFVvddsyCq2@ve_f<4RBb8>&b$|Y7i=)`S7fP*$8{RI6;ycsz?GDaze zDY_}fF*~Il z2y2LfK(qrw-6Ud=9_Tg%c9@8D@!yarU~Ujfmo~+kzbUulR_XHPVvc16=)NPDpiZ&p z+^F>v=1ysc5P9VvoSTEK8v=k6y9?QQp0UsNJAQq=9!CuPP5nIgopSRkM=XL)m+jS< z`*gMx0VpUgTgd7q)mwlxhBMV@exc6?e#xaqGXEf|4@BA2H%b(2`+nR`mkAQYxy6nt z8YPbsElQwE-d&hV4*e5@D3r)8DBm>Krzkm!4=D^VC1I)FIx`}l!WYV>o!RCwNM3GN4eEz|jHDA=rT>`B!?u~%xbv&$88rGUD`uX!e?Mn{GYuaZIGfrz+WcStg&gJo-q*5qP1*`3S!ymWmU#yvBCSBw(p1*8tZ#;w zq3CQz_gOF9a5iYtV#7-3%N5{d){Sx~`^A?=#*@~!m7D$@P zZraIp>U+p>y4l{z{rN=hN3K*DcjpQP(N>OEsYf2dr*&}35c|u(cw_%S8-{{H#;&fK zaQqq>3H?GvDcaswX_)-+q8#n;(+ij*py6W6d2JI$;JVEEDYI&o%!c0Fc#yswXv3Uk znzScfa?Bd(vRboYI=}PJ^&+>Sr$uSg8K-Y;)7jgrtX+8aUmu4lNSlfin{?=Dqe2t* zj*TsYxQS1Q;VO;UP>;-JLWt_38y57QR_Iotv1+Nk>y@C1O9HU0-;&Z*I6l#PQ-`-5 z^nGBwZ?z%Eaf+(Z)?+Qk1#6E2@Y7$?p%U$*P=`tK^-v|c^IkKKr+?mZBZw$%rfXKA zW_OqZ1pgQ?N#nk%z@OO6I8ym$>D|La9Xa|$C%HI4jpucEWGm#u*q1u zgOxB#8H)CRLp}1#38wQHH>2&D{!XKZV(f!`6b4XWx9^UKp)K%}$Tk*0up7)&@ zrJrs4-u2Ajx{|`BmH|!-#me~BFiWrv;cxL;QjB~6j+X)DL41nuiUIMB@*sLs^Y6gp zu^NCV@DkB?OP>;Zd`Gnm;1WDSFW?e{V*d*d-zxazh7|aQ7Tmu|@Es2!1Lq5xLJNka zPRFmhD%(Tprrd9aGv*jE5nqBfK_*>@L!dDB4Rq`qLMS7@<#6y3sOm%3mWXs1UHv8O zN3;|(hX8So!b#P;XEb_LywdCSK#BB%Y3x%g zqD`{t6z=0FI1ohg$_?NwNJ>LrKl#6NNx~(~@&#ySxO?hm&KBPoniDd)$jVL##le4J zCM0*iR19B#y?J3Z^vEt3tG7q_ddqqFrail*x!!^m7-kxNEzM&U3zq({Zx`#7PR7d) zaV#JaB3MjCIw;(IHQ_Tac=)5_`yd-KBU79`%+N?=6wQoWU>>6;0k&9r}K-M zNaVNkj}12R`vCU9LqH|Nm+M74<69g=w2FQzNn=eym&= z_19I|5I=|OzjlK)l8*jh>ZG`KcKxPF4({_ONDx57L`|S9`3C%-8e%nq^M~S>*sA&; zM}PdE&QBFvi{GY$$^WbMpsJ(v8~x$CeM#CS(Pjae%$jUxn8k1N2Y^biM8v!Zpc2Zw z8n&reD%17fNc_^?_Rp?+$$^YrcYXngD2)j6r&Mlq03eSnjh<=AXk6Q6FtFljF;C$WCRmwiHJn0`7J>w+@!`1-Ics}4NTeoBpn$={ml*j|q}2VKL(s^t4;=Y%eZrUR21yIsJ(ax@t4 z$ZXK_&%arz4E7LhFpqmqKoH#~_z}qgZVn^-NSbSNcKIoJ>*~!-r~2~~g-|q)h&aEi z@G;#79m4Gd8I>4goA*3izfKq0PrhFkV@ni4A0&|&&DGw$a{gU;h$M~dkZ*6G$j}?EQmI##bp;kjIKqQ6u|_Atzbdvcu`lG2Ux*% zD%l(^&S7{W%4>)tPsu4yMpc4!_8rbYcs>UyNKoS?d=*Lkak6yM>6D;7WVc64FUKmf}LN;=;t_elJG1{&aXp`yt=+n{gg- z&K4-1jV89z=r-mb|I*ylZv&QERvV&E8bJq_2P+Zo&x<%p!YnWqER$WQr2~-6yTt1+ z4wZMEMs$2F9v5aoYUTHve)26w9~KL#(Qzt%;!FnGF_DoMHvz|Dfj&X?Ar*p|PYDfY z>D^4Z=aOo0?+2zO?JWqBdLSbxgb3NnNMVHtSrC;7L&O|d5R%HIa$P8iCH#2|ouqPw z!ls6J+P#kS=VVyq^R=l872#>^`pEPQC=~^><7P_Cscc=5qKv5dc@8J2{rQ1NS412C z!g>)KUebHHQ^vM4w*lDWhA3{E13oWDoFv6Qx!mU4WPuVLZa##c{|Bw{zwGC|)s@|* zzamun|4)R}ENqSK+?@VjBBcB)LMrIKvW*kAf|;fK76k<;;PJp4>cBPL5>fg=0G4TR zi|;MLB-UM*hP>;0nm@s6oZFA3Xw1v4MMh<8+plogeNTm)2O((Zr!r4Ia8yXvblHhh z-Lt$W-FUNJ&sz^~L2);Lr(qa@foX%|2BISlC{6~=2iRF@9w*-E(KDvGXMv$b(x@!X zjCu=mBd?L6z(%@4YzH7PJke+@kBq&Q1QV~Bp~wdpl-uy~XxWpoUdssV^~M}j*UR_C zO7?2hW$8Ky;l?;))G}+L9oDHJp^hMKJW9@6g}=Y*EG*jwW9w=gQ&pX^M2b326fY(w zk=ckaMHndHr|Bn?RaA3zdumNKG}1?302#d|RjNe|oqoUT&tQMYQA*gvdXgauwVXnJ z{dFCtRJReBxpS1E+Dt$~sosc@#f}gONd|8RIYbj#OK0YcbtOWRKxfFyS&di@Egd~p zsfW=<8LG-^`Zh}XtSKGFT6ByJ`$&u)#lpG0C0$F_tRVCSk+^c2fqSvrd4%P0(j%d}I0>FVzB6wuGpWq?dG zi-xKDB&WG4A)9_gSE2var2rdFU|uSLJV=h{U;7P|qHw39ON}(=@>9YX9iJ%RV5b%h z$`nOZ7evDW#0=g*(q{o~+^!5dr8&DCk8K9H&TqI?E(lM_JlX(+F(LYhI0ucyD+KG7wddMnqeP=bfe?OYm&dy#Wa~xz~31*Q8QFs#uJwrNbw&4*!+&8%uPXW7Z#ovM?Q7Qtr#=`mrHhxFp8@9-|(VJzfBB@H*A$gg^an5?5tbu3GarxV869^?bySzv55ruxK9h&BGOuS@R z`((BgaReAF(kTn*cfgKD+8puU@aIoI;vXfln*CB_?xo$k8287D^F-KRyvj$I$b4pZ zt0r$PvdYe4%F2<5?z+d%BBLSP?GAKLUWd5&(pv$k7hPQEcvf+K{;j>w-vlKp7F{88 zUEr;hS~ssFEqynu@Z0MZ>x4jlLP)-1*nYd{JZ=Yc6^8!LndL=`#exNXU^8=Cr60+q zW`U2d;=cIU_Zswn5DP-t?C5EBD+m;`G;6opS>I5o5Jdi zK9PEj<;EXCk;V)Yu)sixERrk5pd?nQzq}yH6~cc7>hejC^~E&|EUj0Yq;Co7s8CY4 zaUr}VD115LxoVc-=WqNl3_N(%1q!oOEX)@kHKC|Hcts^cz|AY7{80{Hq{A^e0kTBW z&r+6ORGi6m5q?5SZ!|35ILCK9kgotHpW$s5p+7mbP2-Z;qe!0_@}wY5?5x}GaR2ie zWf=htqKEkB5AtvL`2R;-^#6(*{;y}$fA?}~)SeG&c^wvkaVdgE z6iTxigMA9KNIl52NV_Z|=gJ_(VzWvo!ct`yEkgY>FmsMeH5fB4N*7JIh1h41>9vZQ zxW|h)?j2hFxnqt!<@w(%+7r8RK|umhN*B2om?;;^)migy27oCM>idI0n&taB29+qA zMF=0X-<%;^*Mv-(g^OurjCxHRn~xgzxifSK)+7Or3i(4st2Ue0gIJU)8dtz)H8buk znq{k-wu3A+DU&OmD@!o}G%=SKKpAe{0MD5#U*v&o9_5K_5<8%PnIYDw^ELWI0BI5J z8_JtS`bqB3(CStA`T3*%2=Q0>vjSWJ1_HK1to`P2z;`{RzB=3X3rp z8OMvlV2CLMuIZ*iw`3^9905nF`U=oz$Bk&Ig9Rg`8L-c)6DeXRa8jb*MEr+>C3$Ul zKWZNjF)DvA8(R%S;MdK9col!61FU9aWi_SGP%B?tGrg-*regDO^N8j$5>!A~aJODa z$r$`ewNT&lp-=4mr9%m8Obk4fS!LqYz}j5`I>4>^43d z=(E>OkeK$6SpnmMrDkVgzFzOS1bc8!Wrc~Uwz~XbY1KMK79SUnYXvig6=P0~4$<^Z zGr&qTdScItCYvOG|I;m&xygUPrSjucw1cy)6qgB7Zjcy?R^Da;wUPMFXd zuYkO>g<%o(&b8r&)*?E1N2oy9+$C-!zl1NJGt$D04m+C0lz3uAn>VSpt}!qJ4-;rm zR9a>)8<_|ZmByN!Q_0yaClqvATy9O=OFblODJps(zJ7v&b?sivi+f}xlAgRK z1i?}0(>t<`pTh7^O=hlP@l5p8i-hQS2cXt)$-#fbTnTgbk|jqMZ~>Q@-w z?NjLR#pPb|;e642$=}sHnuWwPCJhe>+u46b;bdC6)FDIYc|ZRirj^%psP%jQz{Y+y z)S1e?0s#=AX6Yzu6TkFJ~=^VEM+qTSxgb&>Qm#ksq5UeOWZGsbru2?F_PEmWS%n18K1rq z_7)$IZdM!N^%fqedaLy7IdA%3xOi*mHcdLe1M1Yyqj2g(bUAlM_Ld{my>ni#o>{IJ zEwVJLm06u}3hq&uD8Wbyi)e4t7dAI(H`8BIewhJ`!$_Wn1Khg^_C# zt$rmsFTz(F^C>)tafb5K9kwxPlWkj2bLouf6=gqRLc#6U$qet#L4dOh5p(H|IpEYC zcBnPwH4-)CTEyYh9lGG$9=!OhhaL@b%yXAbzg{|{?bIssI@Qg&F6vt5O5~sf27i#H zDLH#|VOajsZ`jGVJ8#b8^XLO}_LRqdr^)zgwf6JBUOFYn!7UXF-c!iIEgEz2{N)_o z`P<_Z3n7Y7mx~#5hue|F4$*bjt@yq}nEi&Bh<5IGB&=+%UEAZksnKueQb``SQuQ29 z;L4q`3(8$Mr@xGGxzjlqn&gI-8`$43R;0ZKy% ztATu~_gew#qTM0^@S)r?0q~*SLIK!;yC-cgIN;Qtd^^pxv}meV+g(26hABhBC(>?_E8t)<0UenLEqwL9OA1u&S9wvgTd39g*R`qTTq8jZ31tzTu| z>l)yi0tuvoKikyV_-nyY*Rg5#+%%Mna;<4t>1}`Y7}i-_n$CIYP#$itfDnH%q~WW` z$T_JhWs)|r;70mua|QC|_M^wvva|mlpQ*ycFqKlq+aMiFPqKDpju;_U67$VCg>y; zamlKAoHj$3qQ)6fKH$^h>Q_B_8e1in8FK<%6}M+oZ&NiW$8auw#zAJ0xHL;wjShz) zq}k&bBo*Gk!Yl;nBQB#v?IJOuZk0Lg`Icci2+uMlxi(!dkZs+WCuJ(U5*X?X=%(dp|>lwW#&?B{M`qOoX zi%JE5GN)8*or_D}4}VdIZ*jK@inh{j)2WTOOqbBa@7wM5-i=CWnaFs!hy>aTxhDz< zds|o9o#X^WvnE~Y!3%Ml~hHK@7QrYt_l>jpz5R6sOFU5r{J z4F`PrnBL)RpSXy)k4|Rf*mVLwP*M~XG{)i?OQYW?(}YrxrGxo#_A0O8mtMHl4;K?r z`?WtAT|xf3-dDzThPg<_jr9`4<9^wWH8nkH4aOTZH8IcTApy*U>)Rp&%}po@soo2} zY-Dp-m_tmt9}ED^u7e%!rIw|76G8{&=P*I5^Mr%T)6H;eES9(1l>XWFsCvK(^|Xzd ziLH(uh_Dl>{ABAkrKOg;1}a^#MwCrUQKySMF<$txV#F>$0Hchbxf_|peaIjy3|D=6 zUp?+iZn5#-!wrSQYQ(9pmND-@tPa%r4?6+YA)Dw7M{LC`cWThF$oKDY8T_9N+OOcD zdAN#`dP@6QKsJ7f(_&al20Tj#F2yGq69C9-Fch+l#C~C5$OHCWI;%_~JQF(BZ;MvE zhTx7M@hQn%TPAf72`o^nyH`mJhIDi*dctHlXd>T{w>`d5W=hF*H-&J`JO%6uEF2=| zdeSGnAt?*#!~5msDT9k+H?JVuxx)4>GkohjX9jqBSHKXW#~vItHw*`+O&@}qO}|}E{5AcCaX$3sp@-t!EL1lSsD6*Cu3lb znI?CZs*)R6Ll~$b9C6!VF=yB0*%X)1-3f=MJ(=T7N)0HeorkA=3eG}Vj5}j-?Hhcb z47?EtZ-B%jMe)25%@00XL@qBaXNZW~FIilbSE3*kT9@vzSHfpq;B%QvygCLupX`Lf z4rp@z!?-Rvsr`$M$GD?uo)%e#1HAH3y&xYpf+@AvkseKcTexZK5^kZ*4tXSi zW7r_O4`#5vpPd$q$1_Bo)0}0X2*-Scmz<->-^T*mh4pp{W;zR*1^{_rdGvJu_bbPM z&;B1Ho(|9P5&ENntc;HV{xqE-V-V`~-IBnOh7axfWld&J!_q$*@vgiR~Os&!{m3sJ(fWoWJ(Ps9qpoyW}vr`g&H5eYc^@HN-J@tF*Cn=Wfei|8f-i z#m;^ZHe~X>Fn)K1tW5nXuyRMi->jgsrB88No0Nj;Qa2^~RPW|-E^ZL%DC}4qOU)tg zWCl-Lz{I>-n@0OWyRnbUA^$q-i}0yLxd|K4-bXvC9SWcfrKMQtG<>^Y zlF=kt_4h}ITZ5w%gShx$?|%0+3-#(nKM~3j!S9<$EluMahQ{jUnl5sNgLL6g1f&my z^TsG|UIO-HLqj}qY&R6R0c<`2f9ne}>IB=wYzMoVJ+D>bWuZMa+RPxT`)8E#IAA(% z9glLHX)e%tthRqK$L~~R?bjaXoN(IDNOM5AQOmTQz}~@9;@A1|4Z0Upk7s^7r(BWY z$6845Sn7gsd$XaeLpUExZPVO{6t(V>MsY{5%4k@f|-saYw5N4yM2c5-OPufd|tT2L~a5&UXGQ#qWI4yFU z?t23MM(juaJ2v5VccozoQ>Vy>9?7-zun$>#MKN*|CY9n39r>&1Ly|)a}t3 zqmO3CH7ZezzDD6-xCQH|vs=GD=1p9YLPGdxF{EWka!&N`i4+$At;GmRJEAlMqSYFq z85SJixJXj2>EZ5t~8?Mfy3tqxd=@X&=`POyIV%< zInj2Y{+gl!wsMz@sW_55VL8UBJW|mJCY99f9y8Pu9>A}B(;0X_KSICUxf3fiZoMP_ z2qpD`f>CpP-Ja40#gV4+s&D#$Z@zslf;15L4*#wPFQOv0dTpTN;wiuVA`h@t4fw+T zHiF8?d*RIR_>&I@o47yx_YS>6-Ya#6W=r}(nB||L59&5*lrsw*>K`hK$Tj{6^32|m7Nbi-P++*=r<*cu{2k( zO*7d~x4+T2x`9E)Rw}FsuPHOR5XgosbtBLbe#ZMNW3?5}nz`Oe&jsGk7&bwVR9Z z3tcEJWml>ZOKrj9GLK8s0`f5U#rK2$Yh14i2k=dNyBVaeAewcx+pI{`e|1~opYGCB_6PQNwKM|yCWgEK4^gzRFj4TVG)z8u`LuC6J3H$wyj+)09^UMTvswnH!&?4ZHyz&o z8#d%zOkEWBAU{68e?@hz`FVNRJQdQ?K6TT;Mobf@Wi}-8U)LT5qI#~IX$RTA`5MfEagA!>Ca2syG$-!q4#`?RQ-m68FlOV?9dDSj zBr{8J1ZjJvZt`YY1y}C}SL5~9Iw)LF7*XSr<}sa{wTDu3QdXa=PcveiG4vlIs{WNU z{f{!2LjXmwC~;}M4D%9~#~ye4KKffFt#|xvYy8>es?+S+8a2u6ywfaydChuu#2|x@ zKX__l6Yr0@ggx6(8_J22wzz#fqDc;i{^aefgqnOWWGjR}m4#H$2Q^&$E9#os=%;YT zpJ-#B(<12>f4S z7SOg*T(GXU7FZwbTlY7i{JBmOnr!b#!RQo{Hbf6dfC&r26%Y(hg)t-r3YHvpSe5VG2nNO_B?sK{+508 zMx(yuNe{%nY{tGmk@LJ$&iD)u%xt;g$c?gm2oBTVkwm^FMH%wY?x``Q>|v5UT0!;1 z-)aGyx$5ajE`cTebJLb-9u_Q1%0QyHI=$vAao@@~UP6c1V6Q<$xB?-q3U`x)8ES&f zQ`c;Y7rX))@}6_+$-t2mJrEYPE;iCEWiNIiC#I@;f|0&h#ac(R6t|jF7ZPJBszLxj z7JS~trO1|pke^tyW8T!sclPA{m{kgU4N0~iSVs&OtV;rC*0==vb+IDmZJ6NBwXP@Q zU8*GVPO9eS6C=V6u>~b4_^_j2t^Y|Hv^@Pq4uSoK9R8KIo(l_4`j(iN=f4-yEv#)N z{xsCI3J}7c+7L9LjmIIY9{Ma_u9m_jV~X@@0#~A#kVm*dz}PWO@jE22X)_9!4_blR zrus|h$s&*dU(}SFqAV0hZP{`-meh(`mzE9;XMpfup7Iiz z-AbdlX+5DL={i=#$e(w2s23a6?l~(*f>UI%-~5~z27|AZ1i?_kys7C}YSBy+IhiE{ zk3h8*l2qBA6H_@XyAx@^+3g?4N`8SF(7+V$vsT)ET?fVK;H5bq)*=(n2eF=)d=kK|2yJT{&hqD5cYE`YF@a8}CFQW`}*mNb`cQ*iba@ z9L(A>FhR=U#Z1`)7u>eB0I-cl1be#md7?cge~+D%6hRQxN2Fk5J>h@Mh1SQ5?W`kveba>3CC{$q+r7U7G`gUh@^6H2?1o&u)t384xO^cqkq&Y*;+wB|+1%JLON z4DTH2bVe|L$$}zbJen|X?2}OzG>BemQeIh{@aBf5*g>|Sa)z( zPg7Ql{C4J1`+}rKh4r7T(BOe)Q&gpb;rvHX*@I`h~5Wo>8ZQE(B zaDBEd%Af>j3dpwT(^#m;&YwrUZXW6c_tsOInxD7F%xNmbsmgUPO?$$1MIzpw;I>e| zWD*h+BCtv_DLcjP(7lqkCDzR|$4Oh8g*y#Ok%@U}qj}W?X=ixIQe)dpun)y}Z+z6O zD|OVkNWpkR#S=%RFZV?w7&sw^noo{4K6VM?=Pe8jiYk$>H;$#MJ@KpVoIwA4!8Q`0 z?zy<_!_qLq3Yc-((PVwB&OMr%mDa9_BNQH_G|H&J@KfshCbaHIEe;*T+6?WJo~S8 zVFmmD?e7zpCe#v3p%0|&)FU%yj`#jL#vAOM=E_*C4fGh<8Wg94k1N3yg~A)Zosxvn zDGHw!QPhrbU6`^DVe$<~|BYPCzAXOSAznqyVL=>gw-NhVcmUq=r~W2ntC^gf1F!fF z@s^)t{MJ0AXZh5>H_G(DyESUnhK*?faWWb3N)?YOL!#enA95bghiNVWC z?EOMEc$(;RMOlBh@c1Pif@ev?yD0BrfB|>^K<&h<&ZtRfU?%(4Iv>;!csb+L*#_uT z9j}efcLB*c37dHcIsy0aWdw~~=|54Cs4v{h7MyoW9$pdOBp!Tmmc$aHu)y)2C^Yz!VL0D9Jm|xTYj&y9FAd7( zt3NI9lDbiG#i^Qivre?Wc48xXDp_IvC{$fs_KRD2;HOK`&{5 zo~l+|IbXu9R#SSUjRih}DroR$z7btsR9j~J8)58X7|BwZPnsFssO1rf@kvoy;1f}Y zp4t4S;05C@TMqM=&UgFh-kx$ktp}InYf=e<;#{*)M@-pcgB1pdl>d?60 zV#>vsTEPfDr^Jj{s~3%fCt!gMj~VkUU4GJEXHujrEfo>;fnuFM?;$NYGwc{6TzofC z`2g1LoPZ0b#345ZE02^|J!3%?%JM+iTU6L=c(tTVjzO~n8(^ebjJn9VX?(vbZWUEI z==*D;6`%7HDCM$2m~k}=P9!UyP&9m~c<@q$KeR3-N^Mzl>+HnBg}Iv{uZBB}*});$ zJiD-lx}Wy#MdVy=)q$-tcc)mTL40J$4ZZxXH7>|g@5HS1jP75 z&9VKj+ISlSCv$pH3pW#EMm9#a|DGaJl(9onK;ey@@dwE!Dl*Yjuo?oU0i|i5!|0h0 z3R)C_jh+C8z+jq;Ghi4Bwmm_RlYrCiIrt2{dZM$ z6`Ut>kx_aK(|KA9qXIO*QbCnKfCC;`Pg44p>FY~x4?THnP0Nfl?{X;SC=5+_B ziNn`iZAV}$8!KsN>~h9s(@L|#19kG4s!W{_nTW;rb7~bv<%WvMe&um?OD5xLn8JE#c9kew^k$GloFAs0H(p24z$n3 zK!W_9BKw{@yT<12skh(*QCrQgZXU5x-6TF@o?w8cv}kcy&i#9a=U<|K_SHGXYk%OS zb4{#srN>b>^M~lxr`?yp%3Ts2K76m*J~n#(hqm;eqV!D`eQyE=0!sKPOxFL4qWmGD zWPYmiziU!n#%_TD!zZh7LKUr@9@>hG1tByBRCU50l#o!Se9>Qc?%H<02+smX5xtE2 zM((nCgHGE;aK9IR_ZuM8YgH1)9KZkUuE&qx-1GhR2Kxu!9+?j>XcczOK%G-Dn28?q zG)W{-?RSPH4TG%@W0T^dm}6ygaGk*Z8LAGt-d}#f2cQ`DvbPi#IaCh1v(+kB5@@Z zx9yRw2*NGexW5!mpcTklIXaCmkDp^X#Q8foE>_5BZ{yP53q6GCV(a|Lr#o~m z5cjGlb_634bOPEDw&mPt@<(OXONqhL*E_6-vsTV}0*G$91){llqzq)P$gJC_ENn-~h!Ipv^ zB{OOMD2kOPGTB*2iiX^aknl%*odmYnBIsb()B-z^0MynmVbDOrk8Nrh!F-(s%3|J0 zVqO3DwC{D4XhKyZS~cl)$3@%o$4S=rW<@Oew);ILQ1tl##Md@_@XgDej+b1-P3DWg z*LU9_yi6xmApiXVYp3ll{wKw7Y|+VIugh06Wd6GYy;~(@e&#zz{x{)C?3dtJ=C7q* z`p-0fJ&Bo~u?#(_haK-1_>U6&@8BJbmnwYJmm9+mcpvhiObFRGMs&MZ0JPn^MkjI8 z!lhE2po9WRa3s01t&saiX`id~(UBTvG1Jk-f$jKAv~pb@4L_X<>3o`DY1uqQvS9{3 zp~|rudg+-WSYaCBkTfM-((6%3`)KTvWlEW8=fXl(nq{0hfT=ILh>8%7cx%$cXn3t9sxdGgCS5kUtV17~TC582zwJ;+j| zCXX~0oAjdYdro$udjU|*2lqMF`8%@IG`x^YOnbuoV$5@HQQ0~d?t-CA($Naivdp9{ zVl+zxHZjRgoO`3uxO+a{+M?iFTk=&-oz9?$TKK$1rA^k)55m*U(ygh6+Q}e6uI@CL zj;e~FCG?`OG;1#21~nGR2kZ;0V+tr#Gjp<2SlN#t!R0!M>PRh#+KMPfdx>``PF&JA z0Fk4lqXXHcl*<_|;SD(Q49s>2KK)YXAQ9!W-{hi|>WT++%Gg z$6UxZB7c0WN}meJJxYM;iBm2mu6{1`MxUQfa<&Y!gn9dTlim!e^ zsmG_zZ*G;_FZ#Ya#?uFd4Q%AAu-L|=tjvvJ&3KGVfiecKhb9t9$BzY_g(=dWFHJw} zom%Gva!lBhvug>&zr)_KCCHIQM~3J}sjcMkS*_}{tZX)C*5j!{wzyk~hmdvgd(*@4 zI!%v%A&H2dVwEF^pOv?1Y-`krc;=WZ2iH;@Jmc+SxQ0elxE<+2dv2jJ(-A+G9DwJi zwZ@rsS8Ce=-0LysidBh1M5<#vGVFvh^VJN=#}X#Sk?pvLia}1uv%@KFpPFMfC`ac& z!3p=sd~$2MwR$se&xsJa{^hxOXO$e^AFEu%8=4e0*lolK;nRxuLE{Wbj>egor+=KU zDV+U%N%76^N)~30bbXXyROHiU+fmg@)@oFlF{_;a=%$NeJv8zWJY2^{`gK|p&jt;3 zyXFj3Q6b&F^sepC(#fgR_n zTbe5)deCij6p-N#?Rm|bme{%qzeFWe`@CFcz@CS)x^oxIF4Fr-RqN)}TFRTNYg8w0 z&pyzSq|uUG7*e8&=fi7_{|MQ%EW$ZQ2VqxLlVfX9RkqWjDjfDMon#8qm{gSJdC+bP+14&bQ(2a^E+Q#P zNyZ!chbs3J*(#nPZjH3gW?0i~6Qd_BT{PUe!RbnH_vIvIOVBw4Y`NIcqz~|8$qw!Z zdbCoeb%aWU;*hrznU_W7Z^&A~MUY_9{Wfmn*F_V1!P$qFxygB;ZBlDoCpXNP2|T1_ z&Hc4AMFSAt589&3_I+F@9TXyYKy?$5TYmcLdq`~&Vr^4$@X!jbWbyB4 zozu-3e62=_T*H$YVh$S>*07nwKW-p~THYxlUdnKS?fIo0*iv4{?|Vl)S~r0dHy^W#MZ;c}-n7r3k+Lx5fB4tefC(mMZb*E3jhS8NZi*}F)2i=$9rm0ao* zW+UE1*la57_E~c#NfYPE48Ll!3ViR!%$eW8vDRvdQb3B-ItSL!;Sbpuq0#G113{zD z`6Ij}41=`RicHK7sWeBha(t}Wif9kxE1lCHhax9=7l1;)U+{ZidXd6Y^p%TfCn*Nv zyZ9R0+}~bD=at2w?-y^g4OJ(hsK?V_mG{|V_ebK{<$FbczvIMyV($DMu=GgSWAFlP z(r&eN#lF!<>X=KcaUW0znOFE4yUD=aB&It{sF34Q(?K4_Bu9e7*1tbAJrgc%M<#2( zP`2?iNe>Qp@Qn1a7p5X(Wu@#v)Y;2QXp!A&t{O?1V}j!P6UNIWdZNAWlv~BxOCQhY zMmAT;7mRaEa>RL(_Y~+}d(^C97ZAu$M|VcBhsaJWFXOojFsa=ZqsGg_^(000I2J76 zIo>2Zt2)!1j66)1*4T<+#E%n3Ajw3=z_g+c9PubxQy=f(r(!#LE8+-F|{IF z!pPI-<2(3+y0Vr*f?i|RhKJ`DWZ_~%*+pHH-^EE`me^<3Gd=XFYRxkJGSePfDP&Lh zj%*@YoT=;vYo1Cgczlx)4U3#>3E`{#Tn*$zVG67?yzW)me|6&88UM0jo_V*OzdWxj zGSB1MU_^&{!pJQ&8^_Yl3I@&!q*;!OFZ!36UGiG1ue>8zyPzclR%fk-qf`Dp^N^IT5h zg|55*7P%@PXsXYDD-&9+%Gg{&jjlFmmAnYiue-(z=@`?w=JU^Ra7{PE*bYn^%gD1L ztxT`v4bzAnIPt*wB2II`8RBlc$Eb`UzQqNU5tqQP>2nt)^_i-tsK ztPaF;Jm&^{tWDHN96cKV-H#k$GlpeC4booz1wX)5hp=S@WO4;VgT9}<%H|)E@Q~S_ zfU&1`qN9&lMY9@)qH0rpkK+#|)t3!WB;PcCv_$F6$yt1kx8j%|T&pQAQ~(IqIUUpl z79}GT4YSUTT65p*rG0<<7p$AtdqgbfCNi<*rLAujPL-Y<06E9&yogR_I&qu5ueOLd zGKl?Q*Y$OLsW#&Xtb?;PyzU}hl6($;t!fQ?a78TN%Ib<- zzZqU?j4-baHec+EeBRT=NDFs&YgPb)U%14RCe(UkG$rMpy6Y3u{@9*1t%&?i zdEJhQ-w*08WgB|Jl8VKth4oVFAA75YShGM8S}_Z+ZLu3Gi%heV#`k4ZGf9iZ?iz)R zGB(6$#+kgI{yT<8>w&@)*61H|=_!FWy6ZBpKPNQ&{zK)h?c48T=*NMyXNqHZqdo+@ zI18B^2RWbpE5(VK#}v<}NvX$4Ta6a^b~HGHOcpc=qd5dN$pS^JWFhkuyW~e@*n&3# z%ktQQ5CI)6RraViFJEtsK$gz7VFon(fdt|bJ8lP$33C=zW;Q=O&G})FN2ckwQ*XvM ztAGr0`>eYs6yggz3?;*{XyM#MBm4n!^NOO!!S~YGP%HTR6J+5k6fx(&t2b)_pE*m- zW+8oyJVE?Z8#N#~Eee*qi4b3xHZ)APl&ahP*>?G-g)Y5$h_{itMW+`1hG0Dkfxcya zzH5@r^eb2Li5o>-Tc}LhgbS$m;9jVq0AHJ4_9$vWpHCeuDpQmf(4hhKW#eQm@>wlg z*n%^G%XmA-QenOcA*I6xA{G@sYM~VnWXUnsaz)J9szYChIZTc+dTj+M3 z;S3ZE{&}QdIX})~E{IKa{G9{N$W7Z*8VAadSv}cRh1j+E-SAmjSV5ncNl(1e6qmnl z@siE5{zQ_li^VR{U}DkAjZy2$tjsUC;1dP>!XR|ZY>jL3!Xt_~a%b-a>Ko8g1oIIR z1HghWdwnT%NjFg3eHC`qWQCFS*W|C9Swp8)U5YEMO3cjO!()abE3V$`n0_N{fCy%MVIwnW);osU}Hfe6p zKXw1yY(py4ePkbSkfHUvIfIKJa!OCq|I*A(8cshu+(cP{1|zPen2u z8&5!F2B#Z6gjF3x^_`j{x<(jM8)c>&w;o(L6k#V_iE|c~q%-r}&eMVAnTX<*%0>ww zaHV0tRY81Y_2P}y#STu>1^}>fVA@KuJ$U~DjiF}3@8zrmo!wf-T=9$hVU~sr0%&%; z&!0Y{^E;Y<8wA_BoSCj5oI5*K*xLT=7o>gTmt}BnM;AgoL%jHV7asJtD8 z<0}+0IH*sE)1=Vaa@DN!h&VOh)Q~8y?LWWsv>P^g2+^YqF5OU8cmm3nSdWgGS21sK z2AUvw22awNvv0vTcd-YLIqO#*7QexD1$2df?T}b`!Cc%T)qv4I)Zg*cMC>ecTpcT1 zygpz?%Z6Fu&4pDJgBc(t;;fc4#=ip!{d5ZB3aiJM$A0UI1oMxQ7+27nUpic$CPap| zAS`p0`yX*`>eB48=?uVaa*ux|G`mz)0{DOHiIm;kVvMZu?sKf5;)FWx5XbASKQKgq zURzXyhT7X3^x;A2xe|KC>UU3Go-}nb9KGEsO*?hpD6ZWw%J#@5`}k4c3T^M`k{*49 z34da-x@E_Gv;uaD@xW`ZY5klt$)xA$#JbCHZNP83cBuSRsN%UIc}h&P`<!pfQRc;QquP@xW zu5}Z4ye^$TT{mB3wq<2w)(R_YB`_NdMHjYq&YX8lbHHjk=#WgnABsQt8BojZoZB?> z>Ev(pbiO?Qzk;!Sfjq|54^W~c^uINk{C_kVTPI^@8vwnMsjZXn&%=M|GDDi~`pQd7 z{3p}K+nHl9LA?Y6M4bUr?sh-1&(&t2)#1dpJf2S#o0yw95rf8URby!m%TgbS90 zApRZ^A3u@V-OgQkHIJR7c~$rE-^cfzyuNsCn=?!#z<7BP>XAhH&1ONxx~H zOUZOgpHIl-5I>=krB1X{7da-66%>AjkEuy{M~^8Lb}t`Z6n1YPN+xO*x(A}LOV&w5 zOzCEMB#!Yv-A#E#ju8UZOCjJ+{*^~UKgx&>PCzdx%D5C%WRw6(@Xn| zAC}1Y3?G_x0exk~`i^yYfAI7W3`LUl92S(kszLNz79@fXj(u1q`Vu$3R9U-;i0VGg zx#`;7-gr4-)uisNbpFgs=xEx#yvX=SY1&EM#h~0s-sOzlNPT)zei01e7roF^^2O~7 zQSv41A83Cef3F?({G$8&0`ykn5G^P3mD%_A)g0HCuH<9u(0BBOU*;<{wrBTy`iTD3 z*Ws1=i!1eeYDc_oWY^Bog7}8@g&SXI^gYPuDz}12sZNdZs9*JnO*tl(?xY6xh%uS) zpg6CSuU#|5tDbK~IVI`@vph4_R(VS%8U8~V{tP9pyhQpRQ__xD%RO?d|2geS?>}O% zv@@e}zp^(F$R)Yvr_^Pjzg&v2EGo0gfrXsv`Zy+~go^O2iwsJz>?(O>qJEdO39g)oj~dO zrQv)Y3Ni_X^<~9rmgQx|>6Yzfrhn)brIn@(sY=6J1{Ig4;xo)!?OhJqk3&e1=}pdc zHs%*Q89M73naGioo4vBgPWK%K=Kpwk+w_vUbqy8(W)LEYt&2Q*-LPiIj;C z2TXaH%xY}Q3`Y`c8U3qnke<0rU4&UcWTLQ^ zUF*cprKPNk){P}gD!2W!;(`#@M^Q7cCvayIOqkL1};uk)d(W00&h z3szhKq2>;dJd|e(HD&#jjE;n)35Uu8fl-k zGa*)FG1<=`4gxA23f&TWnf+)&9Vwe|O*19|a>`pr`>ez?79(?hN=ym}x<%2IsE5_R zA!aSyXu46H?5vE&E0~wmdT?~KadQb#FyvhttXSm241C$3Bi(iLHPad~1GBq@=g2`t z5PGv>o%e=Y;7LLcjLy?!R$XA{-$LF)Ib%;*RSR%4DWkF*DaQI?f`hXLSkJr$!2M|h z>=q7)dFe)wa1Xap3SbyQ*j9{<*QQs@WVm=V_AE=R;pUE1po}RgsCHo0BLa0FaedWM z#ejzS^s_F*F;(EMbpX|*(8dH2N@or1AfZ+qygaZiZUcN{!OLmc=`B9)9n4I9LBsT1?@Y1jtd>Y<0W^l z`Z6CmsiQ!N!4{cE$n>-1nsVfrsnL4&YOXpPe_5AYqUFtmfVGIA%-RB1 zvzcFwS?@Gg<4ndQG^lkbl>kGpJA?6o!n?3^SY7MV16XH}eeJ=fUj~Tq*p_sHGQ#Q7 zt~g9$zQ1665hez+JV@RW(k4#Sb?f$|;n7vS;42=#-H?S;m{9SVrk<7(6~fuzMh;D; ztCzKGa7AZh#VN0JneE-y@(3FE!NKW8Tc@9zHz27+o%TlC6AW=Zxhdxod{SJ|Mg+{e#=kp3uMItF)ZqrQL)0aF)SijSf|8~g)KKcDW|}(FM3I3m!hd} zWW_JTbbE%ybxeLF1PL6!-D>p=pz=7Z?|!u{SWsD)C6^B? zHqA%m?#}Uu!htJzx*!%ZKV_$j1L-;v=^{&z5o!s9cT5BdCC;vW#LzydR!1xr(x{XW z@St1vlnCAI)+5BJ<3rc{k%Gyx1>0LFB)p4UM3NKN>ETOqPJD8JKIh?>>fvZRgPwKZ3+~5QGoqnxcgf zcRBbc$gfruNOv*85G?$u@vf=lHG?Ed>%z`G;c=M{2NI!1o z9s8BHQ_EgE+utQYDShTP3^+mr>S*ChgI3@)qRYRaqwikM&Ws-W#`JZFwv&Pt8quCP z48IEvHKiKs*b-`$10ysoleuu)lGsV?i-lnO@`(c)B~HOC2<^m`d93y(EgVa@FQLJ& zC>Sa7;%GsIy7JzTv>Y1#ajcq(k2k_4QO@OOHPJZ6R~Gf5Rar50VK|`ZCH|g{S`xa& z4u~{VkRK&9Lpy6=GYg~f5-&Ns=+yM=++NIZyEQjBP#<;Y6{aa3Aj`tDK!bq?qFxbz z!t(PL9;C~H4nanQL@+kTofIe}Q@o&w0$+)!6br`aZ6mBl`)Q#o@iDbMA5Ujb8I98E zZX>2Wy6)1Jo5@m3sU3b~E9u(J7XSj3lEe5s_oos#0mAie{z4>u)AU`Do7y_i3tzWl5HGr;cyZA!tQ5 z6}sgEienM99)Er8%9b9(MerBMLo|)bnk=(Le2e7v(oUTtx8wF{J#gKl3#`$)XpQV7 z|Lf+Y_Xvy=-^iD=PyE&^Z>D)pa~Eg%`n%7y3uuR69Kd!d2H5v~Nn9=h1=YPP1E8Oj zI>zt6WgYbe!<0ZeyJ7Q4?tcG5xh3RkT691660&Jr1V0xP(lIW>uZTpf*E~leJSsY` zmvtHg8%t@|E*1<$MbkQmb|%G1Zx{G0fo*g9fJ98i;6}#A_&T?UU8cQIAG*ripaW&I2m;i1Ww@vt zAi9M17RbOutDhuN+Fb6#`b{RR2J+g=@-(QYx!MoYV2K5FVl>ZzpK?1hTv*3g?jXG< z#j)+&!Xw<%*{>UKIBwH1=T`EHMhN=-Ims95b{@{I7-%fbmIC^PSO%is`877ur(Y1I zxJ7{0tH_TW)jQp{I=N2nx=R;I(9Fz4hQjDXxi_kvCEk~C=X@n~SE!k0NR_g`oJPSL zNz1CTa@860E9eQjjj6K4NzuiQhH58DTsH?$?ap4LK|`ifdnq1s=xY6hu`JbzlVYizjwI1YMA|0`Q~IvHZtj9U`s@@_u+ zfFf()r<`dIcavhSozpX>5+?L;wU3WLL{-;P|G}on*ET5vm^v%4U0+RTT(6i~#Kl}9bgmljC zd=Y-x8S!XSy9LrJaq&T#@B`ydKRA3>JF#eefN6bNl#A_!P2Y^%b8J}vO+2+wd|tIb zIJ`UR>y`j3G)7Tg>8vg_~z%CIUeefFPu`RP0dszFFYTt ze{>`l7J5IS`h1(ZtUP7Pex@k(TrJ*kJax!^<|y^Z^*}cApZ>$Y!yoIk^*x>RJ$&RZmUH@4 zdqZ*7bI+Z9I%$WCOZ}4l?)~5ocW_Az^-}*BIvA8{PRy7{u9gt#u+6g&sh$I`W^=GB zHY!Rl$RwSAJUz}G8B;-*+>3Zzj>-?*V$Z%Tf) zcbARd;l(0&xEIo*kmI^$J6Fv?9$lNdv39a2D9PpiW0${aD1F#+?}%}hC-(NXb-e&K z*}R`0Z9BZoMrD4qQI~6W0t;J3NYLvp9kY)p$>daaJKF$TUw6 zYoQw7D2o0#vT2I90_^&S8r@(f9#o`3ukvxA{y3o*&YWr{p4sMDMtf+}^Oj8R5-+Ih)8i2qqeNe!wshfy({`#S>_wIh z!poT>%>~^4e@J_$=v=!lTQpf&ak66Dwr$(CZQD*(Y}>YN+qUiG)!2Gg9vf*_4N)LT|yQPIit!Y?c>iEpX%lWj+0F9{6RkSSxF%>IBA zP3l6(7uL4FB(p-CiPXCYC-PY`$#zUyqmPbA)p_NwB972HqRV%Kj$ArYEH_S$1l#=S zFV1Td&+{^F7@HI8W{^C5&9@E>i8?v$FJ9({Jad~LNSpoJbDbSAS1KS}Ii3$WXJRm= z+z&PAf-xm}4<+Qe%ub9}D#NpXm}0YJN#^3u2-Pc+<{8cy-Iaw*o5Sbs%Tl#v%TI-z zkU9&RX2#E`-x}Mpe-yZg-$V>((as3vd&`e;^)O~ z-SsYjLz{xDmUXhVE;rv(vb8!l++iJGWgYoHR7&ArAzk<^x+pWtIaw$qF}XITFsWyf zX`t#?g_ur%T3)c0+!ovz)1PvU0LIPDhbu6SpJzNa^}kb6?3zlsjRvV#)Uw|KH+_po ztc>DEKx1%OlZiqB=`b4{d8+YV(7TsA2uWa1H6WGx3R~rH&z9% zxDpJS6nyBR@7QRerUrS~Iy1#bJa&Gs1@^58aIM?9xeP<#a$_KP!yE%LuvE=Ny^Wzr z?!cT+apBEEFtCJTO9s}7bB&gga6BRwXPqs~XSla1O!I2d-Us@tIm~)w823v}keTO0 zq@;#3vu~+6`72C2orxD$*`K*db8VN(2zE-fla*j1R*;OrjIWqZj0HXAA!teUacH2yy8Na1^(+Ar$DxaYKk3MNXRIBu?p@WtWds!|c|?l_YAnUkMlz)LY118~MBcMFa-a0)Qrf>@~{xyBG1?r%bRnceD;dK7qG+^(n03|xW(i9>hNxLMe{-r zEl`_fQUf>j**JwhvGd>lm~x{X>gjIYM8@eQ0`aK}z($4BVXZM$1b^xw0V?0}=|Vf{ zLZ726JNl>+0G2=C)&v{rLc8cvIqEvmP^%hAXveu0D*H5L)tz= z?Od%9)gHA=2*w=_<7(-xx9~LW`(E0C_BPE7DSHdeAypK8>PZ1qglYB9y$1HUH(RLI z{`>)0Q3~q!SS2tBho&llsrr@Y4VG01CQ2t8zV#ll3+0;yZkG)VF;%3^Npg>_yqV?n2z`H_81$|Gy9J87${Cd8AUILJX(YI^$xn=D67Gwp9`gsU!F zNW(NR2Q@$|89MPqK`k0KNkeXj$6!hp`;lV=S{ZzM%G#rxsW7@Tzr(J4CgA_{G=cZ& zn!r8ai;GD$gfJWxTy{FKkP>ivHB&0FiO^a`aL2n)Tcm46{NS3gL`AkGZKAxJ=9J6K zUcTYX0r*^1S#{^D!GZ0#;WD%H2N(Dpum@(kK6mE=ij>!MMNnZtD6<}6m*uhmhc2vs zP3)M?Dld1AJsWP=hLQ!4Quj(QF#0OU%25vEv!(Hqs;6XIa4XN(ghMM*ngBq&!2x^+ zbO(UODt|mHZ`=yTHOP?d&qdeVTut>tsZ>unpf?!mtdt@dZI!+5hhTrpTCklIa^x-= z>aueWfbTq$@YbcHmc={935yjzo@wvnpB@ax9t;!p zfz9>9pGuFWx9K4W(NeyGd~u#nqWszfuM*=|m9tH5@U&WJ3%Hu9kq5aVn|4Z6^QQQ_ zF8m`IJM`c7awb1%KA^S->Q+iRp1#+3eJ!>s{L3?~Rzc&?9j|%xcE5Jw_YfWB)^cxi z$?2G%xV_djI4ZV2JUWT9z~Te;%zv!6xf*tZs?WSvg_C(v*A%BH29+W9X8gp=P*BrXK~2fl060PNt%&yXp`7 z>_LPLys-4-VP{SgN#v}6?TSqM>&W@7^|!i5HsoayL5@UBv%I$(izC$O@3zlhWDL-@ zM4A{mNf?^PqR{EMWDZDi_xZ{ z6JQ;Uw}e+82zeq^?_}SQcco45SiDenC9&jh^|9Df*zASOc=+4w9ls)bN7)?G(A`sQ z7j1MXxxG3w1z#&3dM2k$R+_VISIT?FwEcQ)^vqt1yruf^ZHvH>f4(6oi^5TwzMEcv z{77*RTVJ?#4RMcfDRX%_J_GWW_Zn(nNdACqjLEideKAWOoNoE}sGt~`ZXJ6~t{UxF zXTorZcvY!N{HQCBP1gkX9&Ak0akAp|;y#mjaiUA+ga)~HKN~e@g_=EW9f5wnt)&gu z`5RWd59`nsQ=&K|=^g;lnlaqJk<{xE&Cj}Xh>Nez6~X#~nl%zno1O=s6-l}T`IT^s z*y0v2&6`Fig|F-V>r0p2v%zn{oATp9B_X7sTex^FiM&`i3%ELSe0$F82F5pULw1i% z6ENnBh^ue7KO_AE^er)}bo|$~Ob>hZxJ>?Tc6Xkp%HLoIqJu&Cchj&aNGS)TTh15F z82`WbVI2UdoRF_2!PkstP$f;GB3t>I^Dl%ie9bLE4H$e9bB--A%Y!#D)dIqsRS~Y> zG(vq{kr%kjMf>);Bz9}Zj)ojjcrwJ(+Ilx+p@GkHssS>a5+Xeom$~}q9O#y|sF^mT zCF>Nk)NjGQZ?XXvDc@3q{ZH~Xkxc|dljl${Ikb4U9lebpA`gH8v64}%0jm+98dB`Fh>V)d}1KWbm@=GIR_-D z((o?Ly1uC-5geM~96R1w138GCc&BbX&_HtQ!ZKq7nGCfO#C4KWGy`9K@l~U&vkc6$ zsAz8DTTod0+_LIxEj*jT(+Oxo-UNRY;Mb(5o0`765{K4Cb$B~%OPgU^JE9Cdp+0buX9Sqh>k$VS5(a>2Kz5n=wQVx-vVdu>wKdYBh^x< zR$r}PTneUG)#igS@#a8A>P`5$mY|w7!~@*D@+IPa0TF;NO{s@o(ZfN*l`#%qMUEHW z1J$ppy2n5wKWlsd*}HP?5LKIfSg~vuWD`sWBfB9gmDUWB@Hl37J&H`ty}xw>ct)fG zqrR_AoHb(NiZg_ay5T%;z!fK3ahqIQ8QDurQ z>J@iprQ|s&XJRI#JcTt+l8Yfz3V>}wkA+xe*+)uPp$fn(SRn>a@}b5Y2|U5X90}P_ z!?|QKpQnf+0=Oo@?HpKk(}kc|cGptP9DCR~RGflY(u~XGhe~DL2KF*i3{n+~g2zk@ z3?*u0)?w}uR2;hF*t>NfxU-`YTffHqmn;iFWR#Lvkxe|sl6q4WIe8lfFGb>bWL57p zI7+Z|ha0?I*xM|ue2g{NOKP{z7Ge|fLF8GG3cSr>p_K#7TG+#j%5xbdpvOBF9|M4- zPn+9%8NvLbK9yNG1WH%c-_R(EZ_QC*6YZp)M$nH>M?|VR6M%qd<@mOijkUe_)k8wv z5j*Mvw7n93o&={B66m}{WYQ$w)`kcDT+K75 zc*@bG8VnXl#zOVRsRuq2&lxlv$05+n+g|ROXn{;;VI9xA82gV;XBM(RE`AJQV^)u; z>yN2tw{>eu&dP#=Jk-bz~PJt}HyA=;o*SZWd29?9RA)^y{OY|be$I}lq(M;sS# z)azUOdtO&BzE&;={Z;JCRl$FeN+i_N%q?z-xYFk+{bWmv zBvFHnp-mP&wZ|bd!ki_ECkIv680Xcfj8W{3*W?{i`7L`HFI2sIYwCkiDv3)Na@Qlw zsgalG4OaQ79i;22xKep8#GWC42Tvvt&(M})ti2Wv1{zD<^ZFf$F_apcC`ueRnLrgN zY9O)J2E48^Fk77d876g#{Od;y_2(qm8U_MNFk7|IAXxSyQo0v)yHoZ8^CYgQs0rYb zl~hAmL6$x(Yii}!;%qdypRIQNFqZ@_x``7UN{QO6g0pZ=Qk%(USsWb|SCw2+vUEHYBc2-ypmGMF1M^H1oh%Q3m!4ZikXo zi>9tC3OuoGTVIZrs_MvnF9b4$Q-t+&fpNiWI7azHuuITu5>qGH@67Bi5>8LYm(37& zi(y_zeG*XdCwk_y;7YaNv_pe4@~7*Q(&SEZ4lVuHA6c+zP^oM05lJ?((5^XoA6?7S ziX-@|DY_05Jv;7V=5e-lW0%2ZeI1k7gV_&L|VS8_dRF zSt+KRL+k;Yk~geH6C8ZnZ8sby3&W~BKi)`!9~jZWhfM@9`VeK3e__U@J_DS_RO$IaI}rR zj@iwx@gya*L=L3I7)~D23KwIGi%D)EqTR{&5h=6EV-=rT$S==jjmKB-Omie2;WP}w zDq~ah|NM~OE2pxp29qz9&@?h5trAzPMOWImny}F{I{TMor<~`LFPVORk1w?ZT_V?d z;pus78gLLBSlpSkUyeToM^=grJrUJ)#6zja#{fN+*#_ivrYrQD+ICg6v zM_1H8_Mb9oG`b@zg^-)C!!ARt_Ly-EEFMkXXOk}rA}f`Ko)Bt3JD}HdBCV=@q)c){aiwJk2YiEUW?)TL_V!s>T5JP@f9gFm>O$ zHHd%s+eUryV7di}*#y!Zrjkx>lGv80YD(yC==tl>AM-8;s6w?Z*3VI33XIQAl?Fbo zJvwntGB*zMto(SK`(lq=?$%3oIh@|fL^6|VB;JcDjvv#z9rvBSCMO=g7l7yve~&DR zLC?TPI|V73JDIeU=C^&wl+FCKyIZ6Y%F$Ow$yj z3eyw=&l0_?Ta6zZFC}QVArW+BG#m?6ea?lNiI>dqAO1~a2J79~Wf?+?or1spifKcp zs=qe)FOs##OXqbniIwW5e&tnYfn}Gm+gGy_;U}~Ejt#JYmJRFQwLaR>mj?3K9(Lp0 zRM)OoT|VAxo(mD#*MnNcP*TGV=?QqB`7V8AkH)BO9MM>Ae((K8=3*mso7`;m0c`?f z3?$+2mhYtN^qnkme8y)6n%S6NUhMYK4;X5m6t9D3O3iROS4p#+O+l6yvYR`i3Z=tA}2D{iFA8DcJ- zt3Y$kYrF5+Pa4{y>lk-)N_ZL9n7u_T3O9j&0*TWeCXaTk$SunE#qahiGC*&3`aiB- zLRo|O5k$!2EmV3hcbX@VW$$`DQHbGQ3>=)^y9=+_EZ;i2@Jnyi3Lg+l)obnJMsj<% z*|HuQ;44@_8QGC4!*X`y{*5G5i~_6UCBUf}`gYXdCGRTXl4ED6jZiEkU_RM}Tbd|Ec`CC=YUIVvO5>R7j+iX1jJq7>?t#sEfHnYNZRA#C|$o_@}YGEmf zbNz`1D0Xy`oO=}$YeC}>`Xheo$(gweA^n;IT_$}zQumhoy^!lv(nV|EkMfRIuqDB#$r7n5%T^Z_#v8 zje4JHw&#as51VfdpUKH5abY)DV=%&E#KQO3$Bd z<&>M2FOR&;7ez|KEe{v0)*_-m28wu>Ns7y?))?1eGUD>WJRYA-=3#zuWmGA_6sqJtri75s6uZk5 zZj7Ib**sPHz$z-P?na&PH0Pbh+{vc31Op;8+YJwBAxcv2xBBIUAyuTcOef+cF-eE8 zq3fX;p&6ioQkAc4Z9yP@A=o~h*?TnOaM4|lGxPamcFh*8s^c<8Zu3Lbjeh_~k<4Jp zmM$!Oc#PQ8J?4$IQ31JkvAvP($OLy8f`VxlmxLl>@w(feaSWh@%cBxb3NNBL>LIg` zqO)H0No|CanmHrT{YE>}j%eiN@}FuQB`EkYst3#5o_Tx?OpO*k#?NxwSlt7s*ZW0; zC8HKz2t_0p89L{cB_CM`MIuLkn;=hVL>Lwqr6b3dM4u*j8kRj?t~#a)KNwkTIV?XE zPnx`^(tsc!w#!%G_AuKTlQ+)j4E#|wn(O0q^WTzBQ~@3wi+--8wI9bi(f`Ru{LfPG zZwtYH-2?`ew{0+lV851}J-h1HgOTNNf!7*%_js!d{hDeK)>aNs00}LDd)L=iSwWB6 z=iRgnT}@*F!bq@T;b}gALPHe&74zUb?nRU`wREBkNOo3YYaFv$Q!}sAGgCX;yKz2m zNqkvqtarkJgUdmx)I#^mf}oBh3x4>MTXmxC*1I6qW!-K-Za@)GqgFf>4x>FwsM0C| zy@jfF8og8U^LEXGBf5?a6KTg`H9L300*t6vQZ|a8ZCdxAX%v>L5FhFz0hvjimmYK0 zmL79fs~Vd2?*hHeu7gP92c&U$&CCqEC5m{?gRxH7D-!A_6VyWBhp6ztJ`%JUYqp%X z-u2pWdTPM4(##*^E-Y>*CGR6CvZJ&cnCw=;2t=dh*`31yU~`mgOfloeqHj9i4qSKj z1ilBzW-g;0`)qMV`g%K+o`~#qCKTz4X07`(frv51<3KDGG-$zLp8+!vSy26Jc8VRE zOHLRanxKwZV@TOqx$`DWX-GyC;H6RrDX#kVU9d*ogr+FerBEg#xi#wV6^IYuKhYdc zX!rfk@DaP zy>SXKns&T43@HN%ei~5M*l=|_%Rt?`BmJ^ACbqvw5}OkE0EK|A4QnDJ&?;~(bQAT= z9%dPNJKO0jd(o_;)J1~(=oYLRZZ|o_UAJcyclV=7gih>K+}4M2S-3{}-)c}s9`OWD zcP}8SiA(WE%W!5|O;T{YB~hy|otWWKP9X6Gw$UG)m4GDGA&8}CwV+H+u0#k$ftE;o z{N5^08grO>8hu)k7an0R_Ad^A2Apy5s~QKW*NJ2>W#+%l`an1*cK;yH_K|lDFc_wq zGQ@F~vn@LrLruqV2ft*P?Xk%lxMl$m+eK@VFOsY65zeo!f})ORy(tN4(sr4c+_Tmf-l7aAot?2z4L5dgk@o1|EFNQ3X9q zl>YITt5c=H;8ugOW8+Vh0yMl{D6;*Hxf-yxXb1nZ(gmuxfWaJ*yx8-p#gc-FP?bDFCj% zsp+oozP|99%Br6R=lTBnlmP&_5rXb$!Rkp5eTRG8mj+O|VNQ2ZbyFEy1lUF9a4~vd zIHLzaMW6>ki>D7pZ8m~pK&KZ*buo;jFEWgzyBb5S_6zF-NPSKb8&A=l7)dc~FqFK7 zO3$o_G#h$0nxH*0oS;AYv?Y9jiPYR`paSn&%*^cIOQ_RV6Q)jEx_2X>!9$l=OBEe! zQZOUfL#%TqFE(+3jKx~F$A85d7em31&cdbBV@Kn{XrBmgfC}r%t{?6^mzXgYj@m zp6C%m@`9@mKlepq=^H03#wTFo>hrexM78t8*Hw9uXp2TG*y^;w{Att@()o zoS=D%Zlked;Ve3EJ$EI`J4a-$Eiv45fmhj_-jh6nW#FP9Yiq%QSjGwEN!@)&_cBPR z^owksJ`;_-#^9-;4t}e^)?~Q=MlnB?Ov2^KA%l(((TzOSxt|M{JjugyUd5_qtWc^;$kA2G`l#G!2xk!;r1Gt5o{Zy*;V*F0NS`q^gw+ z!@#_o+Ez@DyvXIg98T6}l2n$LO1pe!_=b=7%TrKBOITE1w5g}7#X>*EJsmzD>K%f- zX)un|Uq1l*yiq_kF&*tj2K7)xG%=m*CI;SKduC?=X2MuG?~_&H5>-SO8# z(8x7l?j0lNga_TcaB~KVTmi=zcZ3_b=Ll;;Y2A@XE?=}|zctpuY^D_yc>WEIaISyx?FTg2h3M?c|TBhaYNA7T66b$-&07 z5W;Aq$l34?p*PHd6mIVvlg%yj^kCTh<;o%{aSQalG4HIqLEK$bDkowg-Eh}|8*9FS z6h~S>^Fdq0EfH=_s1b8DQG$TLo-#$&j5wp&p8~H^N|Z?LZ~ns2ni*p3KGHPrJjXo}NfjEsTOBgxBZv%6d&~Jv-kvkXn=%U>Ci(Y|jt=ht6 z-YL`4S@uQ%mxbS%ZbxIK5~0wPgIo-+L?vC$Y-&fIFBajjIQe@ZlK{6a$~U}Klo#U~ zH>cAXex|Dp{4Bgh5|$EQQdUQ>`Uz~VKVMm+53LT%@h#JInCKxdeT`Mn1fv&OD}|G3 zE7=jQD^KDk0sRw2-bYCBPM^E8ueN*Xo@B8Xo@+8$o3DLcV4$O;o) z`-GH*55Mf-MfrPne*2Lr3B|HIFHJS2Ygu%bp0N;m19tl@bIX=Wl_~Rc-uC$9ow4mZ z*|v(xMGlNlq&ggV9&<1pPjtLIUl#*R=`jU-zW9gSvv*DCBI*>v4KGuAZ~|!Fmb@rl4qHKMpRumUPMO=`5jCe*BgWoGJfg` z#Nm?vS{2qEpXoYtUO)fUowgXc-g5bJ0fh zxe5wXTSPoc3`}!{LCa6DJlVmGb<4fe=RiULATyLY8esT4+opMQ`gPZ%heqa4{3Yfr zMrlyp?R~fM$@3AL7G%a!BrqY&Rx(wu^A8kglKbv!M{jf@22pgPqxh zfC$S_k^8t31Lr&El6e>g_fC84I~pC!=R0aZEV74sLbzN?2lu2?$k3{+5}RH+I~UgA zOU&nTiBwQJhQE?S08V)nUj35IKwo-6-E%HjCfs|+kc4>r^u&~fAD*NdB;JE|l2KikX zLhoWDNxLz!c`p4YAo`Z-7DSKrFaA(%?K+Gb=-$_Mc;S^!{=7u=Ksib8>4A(LVU%Y$GnYv2 z&3F*_FK}TQuMjK7UqGMlRt0ybREDUgkgFZNcg(`3W5=c|eO)BgyFA!EECe5@+$#HQ zo`Mcp1)qZ4)rF_O_E~L9Z*czaxbqJhrvRIh)c-T);Qx4@3IDIg9WiTT8$!B&-{7P8 ztovwTg}!GRa`EKrcK|3=4H`jt!4%1nkszgaeh-TsxaO_k9-33S`(i^ZHndx(&Q6?I zbbY;dX?Fp1;6=d=i-L{%$#~^9HOd8^wmnr|a8JxAacJD-#hQq@x-|#ssc3wbdJYbw zBJ`E*D4QBvCEN%MwSo7)? zEb3>ol-&d#dSzH5lM;L^3Yh*z+NIk7lhNY|5%UkRCdc^Co$jAwGAoSMr22t5h<;!W zo&WtYDLCrcJN|1t6tJ7vaK-oVA4zi};tqty;H8AM;D)dpu7wa^PaM=g;y&JJ zjY-w%?*0a-2?!xhvYighWyS&qdQLp@^RogH0wM}7)S|gDjK@=;#G*f;vOA!!aPH{w zV#0dfxM8|}El@iL3waOWd`QS3bz*zK51y<#$mKkFCQxY_aP1SI7FxM;6BI7T`hBHo zE9SXwH?V>YMt7s7F-5EodpBxp{49ifS%M?#V=NJ5{mjXBgy2C#O!Kj7xbo*M&(^yF zBw%-dylpK+e~dS(;aI{7L}~#ot3XJel>=&ha4NKWziB$a&ua#wHQF*`X+Up#S#el; zHKEWtX96JD#Ee3*gk@pmqA)~H_0&DUcf8bm^Q42Zc<%nPTWVWMW0@G3xm_Qf{mMaR z^8U?#m;h<14?+?WNf9mW6klDn-zb>n`x2b?0L4N?EmMgTeZ0Xcf6s2%F26*!dbXoc zzPm*pgXt{`3Vq7xt=rMG?14}Vca&J%A;R*NqG5W#w8m5?JaKqr)_M}l7A`r5I3qk0 z(_3r{EzkH6gI-BpBpVr)dk9R!q53zU-FXZAx|Ahs@nJUn+2U`f?2bgAD$Q5$PVsZd zx`>|PZ-8TIUx$|9^IEPDsl5hH(&cpU8-qOcI0_Dq<*3$vaNQ&Tn^>-Z_1bUbtAPh< zu+*!HSu?D1#0wC)gXI7IrBdF=Rhsh1$xt=bZ6`v(+-WW>St@vp~A4XA3iGO zOZ8sWn$=mj=3Un3p~P3&Dpc&BQB|YyuGFGW*I_8ny835yY&Z7!*5llx5O|>K3>L?{ zgA(G-m88(opriV2iMwsMX`_m4IEF@F=moZ;wW?CRfAj_}{3w+N5!Q>O?bfTcP~SCI zNnG{XY>eX8lPu_kY1f$OU1~X_{+B9~ZMD>C>G;L!qgqq4sBVK*tKPwN5{EA~&dGRb zPSVD?#$Pq0dn2?3YAOlg}L^{(clFOmeQhS^%o#Q zQ7RL{g;B`m9?psCtM%woAt)Pd+l*L!s8WpSf%ZP6Bu^|WKUGk3pZ3=M-HEsLMoq10E zM@U$wb?rU5OpykP{ZLh=sJm-uoYi`f7U0yxEvN$F)e>}u?gEQB4m2(v_i-QBCdKht39LN@|bVhGxnFV^VI5 zdghNhW*>?%W!fI65SXW!UCcclg`nBqsx5M4EX^Oqin^?n&iZYMLm#n`NUE4>Z60)H zFu%dA^%qDpm_XNf4G;(wT*5Kv1utTjIs+vna@L6cR}asx9xJaaj)6Q8hi*J#tjSM^ zM~mE#UrzA>Q}fSMIJ*@KvC$m#P6y7%;BUvL2v~Mqgn(VqcyZ6*J^96hB0Omiyd5IN zZ+KW!&!r1EuxBBP^)V;HK9MT7;G4NJqgM=;dk&Vn5hn2KR~uPY?vQi(&0tqj}%G$_$d#+LBtIX2jHw}9vrY@@rP@XZp!}{$q zo}OaZIw;=Br&b5o27z%ZfPFJ1Tu}qt|8+9qG@+mS^V7&ce?VEo|LK*hVCHV5_z!dA z{{XXu{Qt(__&2cjp9Ux9X!LKF@4p(|q_Tv~KPZi~rHQ^t9D8Z}9DjfYzXit*I8lPg z+zfklTHBu;;=k0e@uN{(gITr4LrR*+xLcdPE4Y3GX;LUc_|R5FW8uIYU$C}Uu5s*U ztBh5Fl$*n?&)&xz+g!))Umx#NHUQGQo22lrOu={^$O7>3ohf~2X6Z?j=bflSYO5Iv zvP>;InaRO^(&OVE%CyDT_?4YLgmCw#H>`=Mc#^`vOzaNKoxXiB|2J zNNLLz>qa8gDrswNR&HtS)^3YMM`F)wWv7b|ZAr~b1HxoGjOZqXef4xFBwFezVhLI@Yr+CnHM>~j}#qjisCC49%z)|Ege@W3G z^f5f4>s1P2gX0s7>wFuJholB>RZ)Pchbf< z^GhTN#u2JNXDq}UfhRDckx?+Q3%Oq3Dl_19v3R`fy3u+ecJw=KqETmvwCRB*XP?~K z^&L)i7FoIJ&}yk+ezsEuVmpxB?JBsTV1HXdT;OQmu&8uAu8 zVeSi*67+t8;TgB;cI1;1JVg&a70A#{AH$uy6f0;YQ5y!l$OK@{eg)q2(dBh+&V}s~ zA@mF|iPcTAj`9HOCQu)V+~l`J*lch9ewfzD#ehP73d`7`p_mb26~!?KLsWZ% zDn4~MIDwp)Q&G)1=kC=bjy-&0wwufz{+J#>@q&v*7fbOQZOU5<+{2q1Z3_C4eOZ{+#z;G zxlQc#qk`I6hwIX)nsKp`twmP+$nazU`i&N#89_T6!19b~-GCMSo0FeUKVxXz`x_$b z;0tU$e`Hsbb!=4W;8jn$mQeoW4duV~qs@3gnYKRw0O(ZzL(14cgVq19AN_mZQH9jh zTulA;alu9jmT(1wqsJo-j|GS&96|-h6&nEpA_R_4DX?%rqwXKj3!Z|aQ(V6?UtH?i zZft5ar}8p%F-YU5t-NgGR&Tc0zFu4ETCm1>KeM)2e0aa=bVzNTP!u&ikHImS;(E^b ze)ZXY)jsBa>idKNP!(tglD%O=%1++)i9PBCa2B-*c|iQIz{BqApW}q@$$#;W^rG_} zzOkWyD#ZWbz)s#uE_@AV)Jb&HiQif-d=2LGR&d{rJ6fOf6dCHGu;?z{1O4?X2*CFt zn}#EHZ(x?CcpDFpv|Aj+xobjyeoXES*AwI_j-5r_$M90f_?^3_0=Tfh4EO=kP1WbK z*KOG6GY%pHIrlAk-%bC-VfH06_zhZvEHfIo4LTFfGjdaZ_abu2MKXC`cLI!5Y~+99 zj|*K3P$0U$3w{9oH|8U#K%UQ6krO+IA7Z2fKPi^+6scU1UzR0GI1fnJKgVfEUs&Hk zQHD5*aCbv0cAZYhI9w|cs*M88_ErRhEoza6RG30c2fpUIiAAA3&mLs4f~XjoUGj^j;F z@GA8M(}Q0{5XBLBs1vK+8v@knudmQoJQ9mIF{)u74}3_t0Ll|K2B6tSaH6FzTlU>I^pb_=0SpmfibD z7^8>S(XF6rTv^3U9bN3qu=Afk#8HU73e9=(um|2ITWnix(tNkWmnLVDB9snQqT-|y z0)~-p@ki5rDW@zPpd4CqRcXr)2W~2`;UfEC+6I61g7?Bg#gwEEYhrc(5RE2eiLT}* zsMv4N=>w%scg4aPyqLec-~ip-I-(Mk;yKs$mS9U;OF9psmf}mWNw1t4<{lH3-L<~s zBNI`Xb;qYnx{pTV8W5b4+OKq@|GYtq1>3u*bnLrl#xVOKVBuNe;w8f2|3uDl(nnNlEkP$Ov8lYCSqTNN!n+zS?yse=P{EGtz@ek+ z^1`0rO4}+X9Hu4%XGUsmr2EU&J?I{JR!d94Y81G-bu7T(@kLLrQmR-;-l);Aq6`zev(+rT4~1 zJn$YVs7@k{+z|fTNe~A$bf7P8esd2AvnigST%TtbGYhw4xbCKfdc`K!7-~xUmLXgi zeAIlRWvZ+P!3R33DEWsXgsC>Twjh8@Ha9XwPqHiPN5AFO6DJpyH7!MNA# z*Y#qGZ)h1c!lu$v%cht+#~)QzqTY|&M@}_I8mhsl zF3ChrHG9ITUN%c#c0`-I3(Jq5Iqh)z%gns6Ro0j$c}r++VRRqjbWvg+S1LcUvLHgq z9N%$5M11Ln4|Dd$yhFHuGBR4jC_)Jgt|}FWYVqh*y*E665#3ooS=izPekyDJsX z7t*0rG-=z86k}s;(}4{pa|3Hf;6{s=7Rn9LbjCGFB=k2UCmg2?G8vH|^eytcZop=< z2B9gRa@A!Xh-WKwt6?f~tx`D@r}BmTS?RSxJP3jOWIg_Vk{hu^ckuJ+1rjz2!$J*e zWRoM8u3n=MKz_482Rr;C5u(3py*2`yminq!C<OyWwo)_7XmfhT!p4v7e{XT0Tv2 zIQlrm_lGp} z6(La%hc?3>M4w#p+GwReEMqt$Hhnz<-mC2QLPz4$wpCxV8{uU2nl+uFh-6h(71FLRFtX66> zP`^)IUXt-Is`1?+R*vVR)HNWJe>fsmj!RylCwAt;)?Z#52);L~WNcnaI-Wtt3vP*; z({0DyJXT*xsr{P~L_pJJ>ogTqJu%!qaiDc|)X7Y8Ey$hVV4q(x9TqncBCnUH*e0iL zpI2~~OF0oWmxW-&h%?sOD#vc0hdC7@ZXS>J1bFuk#TlZR1 z8z*C6?`lZIEyN(I7kpu4OWWbv{tNiAPsR&uMrp=W1|-BjM1W1FG$p9RuEnsQXy19W z#|JY_+3QqZD`8t5+_?En%Ml!5xF0rIDO=SubH^s}Bkr^@wEiWTW{VB{c>78$&6#xu zKh~~u#&9OmK$YtDe2x7tXp-3$dk6}y6%OyW5a@a8y1;UP84+3D=e!i|@F2$M!UsCW z+2Pt?Zaw>w<5medf0v!iqt%MWRm2>)>%R6>B|7Rpj~nfGOF}ZW>#gIC_Dyag=&^fJ zmZ!qw2RQ@`bI;>zZCIj08Jjf=gotPL40jMR-Jb~q;IKgqfID_HTHGLLKg^95$>Z;Z`vv`G{- ztvF1aKEX9_NQ)UqBjZ*3d6F*@YflS|2dl2@;JZTL-?+(7!k}y_n+DrPGh~pYCFKt4 zE3hEAP1>)KEt>1z6jaBpJ;{cH?4n^fSqIw$`w3cVdL7`C8{OB;k+-AB-4t{f!X6b` zG<&y}-Aq3o7@pZCk{mt&xke|v{l(wVl4fXZxw!_Hz8EpS7)?PWCH$^kA0o&b@HC$Q1UWhfc>iHytF6~_&l5=8t(b(Dd{;slJUa&-d z7{+|ZY&t_?fJvW%UnQ{%iN zWREr&|J{@|(B$+bz8#W7jUBSQj?TEP^ag`%iN?cK`{IoFTj2wrHIsFjR$#`gM6z90 zPPWWdw#v0u{I*u?Wwxqhr4Vt?7rrM)J}@+oZEJZ7tQ5I5X$F=8=%P^#=o(_SzN!HKv2DDl! z@TEO&b|3gVZ_^s6{K3h*$3m?k0|vPfr7>HCTC5$T(CU}cNJ{olV3b>wmYZVOds8}` zE&TdmA_noyYMX0YUn6sgaBUqJWTLL{t=wUX91=|uM@vewDIeLT?mMU1HB)98>bv+e zi9g9*K^Ym{#N4lq7zO63Yzp}s!6Z59R9ftHE$)Env~HN2-Y~tF*6ncWrzxY2^WAUU zt{7??Jm;BTUyC=eN^`B3N(J&Hm!{QU2>*3QhK}(c{P-bGNn-pj9v}W=NB-AtT&W7~ zssb$yZAVM)NJ9|=;BCJ9eWDhv%P)`yINj3Agt+A+}~2||3oHvtL%L&t4jSGK5h z00_ICS&uN-H+XCMBP+|B8B(EAA> zjWXY1+B>P;us3eKu@{8Zd<1vMjBS0q`~R`_j@_AdZMta1wr$_BU9oN3wv&o&+cqn< zZKGn_td(bV_wM)IV}Dt_*BI9ixIUb7j(Hx8;n@toEKoH{bPG;{O zZuIha;i7lDyWqp`?i*lFXPazrJ>2*8o`~M+bnlI1in?JKAHc_dx*HChY^Enb*xlf{ zIF1hW;ONEKF-Q?}cTcj|aoK0o^`42e#64KspL`6$k@LJs!f?82f|$9vZtQ%`!kF0y zL(n}KlEUwt?5VwZs>JU+>U-vO-wI?;yD5R-zxfl&^BIrB@48Q6`=Q*s#e^6h($xBE z9zD|HcDXD=+#Je30fsBp+T22GZF8`L`evEv0JZ<**y#VuqlZNb1vbX+bvUga+BT`64S-57BPWS2Y!B*8 zfpb5NlZV*F@Yrr{Z%|Q~5tlth1mfMRm=1MS6Tsqod2zk+wye>k11j#TQ}u|` zbr76WhAqoNv2GZo9Crlfe^k+ORt`or7L~?D2^BeRu8AjB=9nVy4QQ?Z_jBROu58bSXOzAW~B_S}D>7<;#LKS|g37EWMQm(Vh0!pBIF*`N+qrAK7ux=z&nL+ zmLm)XuFr=#ONtU_3@D;%N5DZ(i$SRTnq=yGhzocw)aiq+Qm%v7vHg(jv%5(N*b#Ku zcZ2qg6#(TmM%4~!4XB3giVpp&-xYvxH*D~cx+mR-gsXCL6ijU0L~+5x;cgF|8W5TOS97=I z1RDk@U1e?Of^nT+&pj;sr%28XG;Z$x_{lKS=C=A+=sneM?JRZm9u*=oXw(P&E-h5* z0&!HOq7_xk&R~XRrNu>6$D@sQc>((KvM^-H*XCpJms z+|Por4Y9^`$pSAl?a&#;r!-97L6p_Sj;WsCZlQ&iYmBuM&Q~i?eFW4Vx;RgX;0BtH4*`{2jt_)`7rB` z>Z{c06eg?&+PU_YP!5bPF8|L&yrnS<=lkX)J$v4>;ChTK9#&T4B``c z`G@+a;{^xm6pXuJ7XZNGvTky~B);i&s-w!xyxg(a+hScxRdkLxHCt(^N`i7%;rMnX zxvHhs6nQkZrfhMIIf zNtP$bun#0_q$?|`;f~w`tQTFh?UEwjq2lSDk#ij#&H%T{T0N~Ng=QLxTz5V_#ZuK< zHWGL_u&*K88>YPu8_c%<-;1#$xXEIKS=Bf4rhQPrfa4pRbzw5xao-47_7Sc2LU_02 zyc$Yw;QNmPXJnD$#193J&YluiRJOl-s~w`d?muLE+<(;Q+|6QR`#8hi3;-CrlO*2;}#>P zAukOE*h|nQeOyqMnhD%G5*X*RBp$K7(cYx)AOth8nF3kF5iZDxT3{bolX%2f(y~LL zZ(^k=dQ>I(APAEy?{oOvDXO|0H&hUIsK{vepa_o&md9JYZ6rI`-?j!4yU2Sz2ij;L zJRQt;$e-yXmAiSVb`7-2M>G~zO-IaU!W0V^yf7&&8;gV=2y)VKkGEG*W;*6ePsB9v z(TpEyQx+qec#@9~S5YcFXiyUE9l2><5DeYYSWY|}hQL9{vjLPwuwR+16J{ic7@uk) zm9E<{8L{qU4}?mC4262g=8CFPJQbyEB{q#E9$qKJ9Qq^H1MMSr*YA*$V#ipVB!jG1 zh40u=Gy3>=!&{KBxDjU z`b&&&JzY7{Fdf1pZ?h$MfxS4HI6Bsfk439HO3fd{{3(*IS3}-ZA$=+Y^_=3UC!_l3 zd;Nx=UK%HmH^gb+F)<=}Jvc~kYjILpb*Q{kgYEWfSzwM`HS1mPIT)S;y7g2SuZE=k zGZ`bv8$rcR$J>#z6n!zEPuyHzT{+G}LSwqAhJh`(z=-U5sqI0|ZooiqL?r*ROVEA< z=?U=ZM_w5Fq45PFNKG9Tvt*iCw1|W>eKK^~dK*6xS?niE=nMfg+rkTl_=vp{yZQvE zyXErwG|m7noBpC-YbQ}`ORA&<>V=h^lOpTaKXpX{ogfzu9G`UTliNaQaB*;Og~SLF zQ#gvqq945dwMKKaB$jQ8t}lz;@hAtf)50%}x8N%z+>C&(`jBB2hI&%Z-eo;~pCznP z7s|QXHCmR3+Doi2>a3lSdtQHSEfNhHL;NXrQk$C;Xh2s8DjQ^d>Qqyo$?S4M&JqhB zI~-o>kG2Z>rdFaXp<_RU?CW}JWVSBV{qg$BAKWxw@s&+N#%r2@GnHF<$JyAv^gyt& zu51GKt*(lsPq@PFPe#_3zQWonc0$kS5(Lzggw&M!7Vs#mAJw7)=*p) zt;(y*J)9x8Ma8BHO=3b{KN!L(i=f-0{-nmMr4(Nc$aMpWj;a+7)vOOV94km|e zz26W$#;iB_gv3r7x1om~L8oU--}xp_@4!SAcIW~dhS}XfOZC)+j&kcE?hiKIOcNI4 zh_jPPvv~uY%(v3C-=`kEbrW%>Ubg+;uy=sR6b-KElVE5j#b$+zoYF~sKAfZN;x`Ed z?$wRm^s%EM#&Tl|A?0S1`NK8bp_kmT&GxV&PRytZatlRwJ?;)*lz-?I^?xA`ZBL+1 z)XHCZBn$4cI`&1jewW+4yOTV2(-(IzkfduTSU%eJAK!R5sw0_%oG(bGNJzYOL$;8# zE=J=>@35nKQ3hM5rJXc9I5RIGZ$+zmofJG6iC)Vj{FPFtUZ{%}tO|}g82OO;@SKbK z`!Asd|Hy5zIi@o3zC~y{-y$@H|8qwCAEnuM6uz^`zfj=+MT`H(Q@bl^#}-)tb+`v; zGZ?MNP^3~NQuR=75Bu8M3MIdsr~qSm(Vepw*eC_Bf!j*5AYAvI$#a1`i@bd*H}l_Zdrvfal-thPI%li-2$_%bd_o1} z8}LFcL_EhHt9D#pQrUxXR-@={&u#S1ULTD4p2YeLmHGA?F*9*BUOD?K+W4UGE z^>Ic6SWE(|QL1cDcLN`ql96@kq_2O4`mc%R8Z72l`+Pzcb51<9gZZ#-Pn#;!Z)>e- z2jguL%r3`f?KnFGujj_An!M~@v~onDQ|Rtso53cthnXTQw1=Sw7Qpa{QOH8hdT=HY z=~GTXZ6;#{{cY<=9}@FYKR^aFESF9_Bb97YXe!0@^(do=WQs*t?SFs>8B=QF6z}3r zi(#U0Y7>UVq&UPku(I)^;A9SWlU%&_7}CoaSQrhjjx5gaGw&e!=}-|6I|MQ3QrMGV zzc2zHi-n`un}X`D6Ky6l+6Fp0rzq6m_NN^M`PoN7jr1;w>*H1`e6ecBp5dq$aEZlB z_-Nb_hjWjTEB5@qq~E2VAeS-Uvv~Zi&`|onG>iXxKy6L_pW4*#X6J7aB<_f7R*8Q} zFuJ7Dn-22t{$GQkiwHmhv;tJTsE(#PQtHoWOq$V)=S zv~#s;9t}@-`7#}IS#jH!A%9gYS6)@ibHNcEues*2Wv7vS8LV*^9v&B1dV0E?MSI6D z8-immP-Zl50p7WlNT+)3MnUK0j==p|r&S9qQ9%f3KUQ}rMOzf=SUOQ}?v1T3I~vg) zi2zGiao8`}Y9LiP7@qcN+d(HsfB&$8Ui9|2RfOX{OJC5+-!PUAkzITWqB&OCc%tOX zp(j|awV7!AY;V@$w6D0;+@+!l827h-*pW*Ba~Oa^%UX-PJe$DlAj@_vfu8pg;I87* zsCS??u5*VLY@Pm{bU>rTNZ!dH z1Hv-uJj?Ms+%agkp9BUCQDMcv5i9KI9Tm)sIQOH`XF-33F2K zhg4|Xhy@^xN}o_Op^QVNL)j#k^>tQSGe%HveFO~}=uuPkbrwxK!+0)w*MVd%B~Dd> zWG;YYHYZN4fV0IuXXe&xn&3`NS6Sh#rE3d))6}9@1asd3KQbv>#6vd7+wnz>Tf_L} zN1IMc>djKce z`ol5VIqtc6>^zTZ6oo@9ZuXtFt7;8efF36 zPXw73?P{!p9R@{{Wn_MU{5R0?&sE%l+dDk_?R*%4{P9EL|IYaTC)%O1^Bo?8`jzq{ z(;6c}zBi$1Fald~!m_#PXL&H0zO^~6g+ddpc0#7HOJnAmcGJt%-#KgyZEbhYV zFNK)n?JYpan?9DQ*>#T7Ozm}+*UVba*Vi}KuU2oQk4u1qIna!1>Yzi%t~fX?VX3PK zlh96;DuVYr;brib|Au0Nu|8HyQq>9N=yiqm^1)MS0yQ>fzZuun3FWE!N*?_+r)@F> z_GH2FZLh0F$J7S4V#|*8EBYp6<{`=Aaxm2~}Oa5I&b^E(yfs@Y*d2TvuT+Do;z zkVdkReYfX~4jy;HWER-e4`?TLjX&ai>lbKnp?MfzV$wvkRXSoU8WW0b&3JvOXgx<) zzp-HsWPukL-1bn*5Lx-nnJU7FVD=bRUIq3h%MZZE15~XBrFeYtu;{0(4f0NQ=N11$Zk_c>VuJ^=_JGbhTL07LG4%N$gH%h|YZgXPjrM<&xQS>Ar&r3CBQ1m7a z@W&48qi%&26IYk@a|H?E?rTqrCtjv44zOn;ixY*?BaVeb)5<13Rb15BTVfymKik#5 zmgF1IJ}Fh(X;hzooQpEK;YzeixoQU1~KgU&OjB`?|VINY`yD?}Y0fgr6mc&_wDD&d+0ccv0 zI68YM6>~j7ml?u6(A9O+X%Ig2LVPeMa#bkm3ptcc8DEAA@vmN}{)org%g;mBFsl~^ zuAs{CHxlVtw}6M8BFikXh%?p@7KKJ9KCFr_6tckDK5xI0&W!>R6vA$Op1x)pCT*d z)CzMe_U2~G%IYvFoN99^yRIUUzv3=(MAt;I>bF zvAHj|O3v;MFt*7@X7gQBvC*wM8QicRo4w=&qIL34Oo)HR~h4?MvjyLpn78?s};I8==@;_^l`DNWsFF zEYpv6%Ff#zHg;k|HvdfvL~b--=heO`>*U#8vB-BkwDcB&;493m$U`;dOJezRt@j>! z7@GziL4*XlLeyaO!kjb*QWRM#V3+M%2#9NIXHy={28!lrr3SVFIqrWl_KCVs|tZk6+&ym>~zE%shoTsHCuIxV7$sCYY82i{f?3+{=Pzg zS(sSW+LZ${Rx9fSTM-JuX|N2i0YUs0_7(@nNRw8lDAv*C+^dYC-Z$)zSIn3OnL-~O z{Lt-@l{VmSvCmij;dG9J5@|wM4m^IHgJ)Be7*z%*QYB(jd0w5Sn;%C%ji!qn61br%rU(cbb_S-F@-IqAWAZ9fWHDuCcfTZJ#bd>c zc65qZ%j;pH>oVO!>$2Xet$ktZnq9Hog4eLzl5Z2kdkUx$ z)t2^fm}=mncpAU4e{A*gd)7;f2&35T(}G;za5Z1Qu-)QmSIBqS0gVsxAG&+^2*!oG zIh2R{Ni<^qWxFN#Sn2g5-yZoZ?fnZu#ycohCM$G7=Athd8lrR#GPL?gKkZSzKcl0i z&r;@uUJa%A2fYSKZq?p*Ax%M0TbHq*gqAQUKtfnbGqAo@rC?W!*q%et-7PbymdOE# zj+7#qjIDi9nNCxtO(waTr-Vs4rr#mZg#;xsX+@q-={1=O|Dhs3B;B$V+n`Ka>9E|} zKuLd(TH9tX^Ccx;lzMW5f0P*CvlL?c?rq!09fzhmeqH}9uRfvk(mjE`l|=$)1= zm&C>j{N=kvkzF!J0Gke`O;dlPmiXNGfqZIMF!7YDgOrX&y*L9Pd{WkTK@uWi;OS{= zLZ@Au^f?HRp4T<5NF91`ARm0aE}~=uBmC}CuJhgtZ5jiXZva6&4amDWorRFs%sUuw z+h{0QV6K)%o#1yM98KD$G=0NdanIs}TC=m?a^M2^w^b`4T%=zE*DyK{rp3BmF?vxS zgk6R{~ckT1JoJw2i<{`N#8c?Q$yU8-ZI9;%A^4TUFaKtI$k-*=|&evf>Yy>0V0CX4JQHBFZ+ zH>u`>*^2!!ZEtv(r05-RH$r|d8XmWjQMi@F3Go(e@)WBEvL|prpQ4OwnrYrcCS+zY zR@NpxNTDBiDJQHd>@NX4H|mI{EI9*vrrftIf7t`j@s@tC(iSy@M?$cQQg9qk3o?nV zP^G3V*~5`E4d^UerwPrU5;8G8SgXK0wo$tS9XLnuFR5)UeVe48GV_VA zbg)W)%r8x0cLI=9p44ml3!rpHm0H2sZ^-m^eNmNL-SiI{Y$&TnU1e@j+d3E|^I;0p z2cw7wG`a=1AKn^W+@uUHtfZN zdnh;3>F(Ti*G!Lo2XAjl;dS{Ky*0J;TzSzfJ)t$I`HWl zaz0bjEuwQ~Vs}NgYD!3Xv>8!Xvu3NF3}UWVA^9p+28+~iLf(I8I-78SrG7hzh4-Mf zC%-Y1$?zAtL(afE)k2|Kl=q&~RZn17;sP7q^rphi_~L55bQbHc>I1ubuEb7jc*f~< z=lvq91;lG{dowVe!YwcRFn=}^eHr#rC)lh*XSk|Ez3&jGqhVXr-or`9+Do0wA{O>j z9ZEB~qXv)-dw24(+S&029hMEw1epWtnCwL0K_&=9U!cIey|Ci+u-14`+ky}~S)m~Q zV8|YnF;7Gy5*WoWj?|vxVq;qh5e?=!QQ`9FFavJ+c7Wd|8|W2`d=L}XR?Mx0zwu>B zl03vsP+lMGrXY}A@tWNL(rVakE6L>~3LX&3I#J^=YEgF1GcU#V>~4smRwYj-E7`7-8;IbF>t6zZY#p18$0w?h&Sh3yDw0gsj{lxG*4CwKer82WM9 z{>RFFuR1OXvGR`cX)pKfZ0AWY#^A-&eIVi&0qP3_?HC>BimewU{60^f3YXbICpLqu zVI6q2LnMTN@k0)93?zSWAHR6fl+XtNu^b|Yf7(mNuntnL#HP*c9QNB(O#Q&v^|^PW zgPIIBTj(-ssMKg*369)cw1tB;jziQ5#xZ%TNdLYvjSuAPuKC?7+Hgq*V2(OZ_o2Y?Kfrq zR+ovxE%H44T}g);98lB_2I6GS8j%*O!VbsUvBFoBmvx8Fw{@Td5mG1jNEoOYDw;~@ zu9ZG}mtNkMhQjy!n5)VWU66q(UsZ>R;UqVeS?FG7So4_z#2H2up4h+}VCq|9lA(#F zzcl7X-u$U*kCC_7yq7MTmvF;R>`J=JqN`GaGrJYQ!#k%B{)q4^4%>MtrQEt~soWPF zdJ>0gb6rU@MX6hV%A8r?ygB!q3P>?dVGX@Bex2LsRv(x+aU|kex7VZUgkS9`DDBe7 zdzH~7?<#sREcHIaYUi7l**3T`r&Jm{&oG&WX?YW@zx;DcE95%fAa0#5hb3A0n(ml>$cRVP_nC0r6pR(W0uh(I8mL~zd=^nR5YrhDQs6F{CkTe zGi=S0ccv|@=JA^pCMz&qVCH6=q5YeEY0ed$I#&Bb2A}?vw{*mA_ho`P;_tuYcm1(t1r^nujHzc^dFr7)_0Q(6Vq)6Dk98-3e3F8|Z z)k>KugE*&>@-?fFb!$tRkYw*QOG?&>3apI1i)&3*t*z>78kGs>B-175D4!t(sRO{N ztk9MUQ*p$74i2$dm_$I8m8D4IeTIh9new=0sv)N#B2~@7ZziyiB?NZ)@!zG`w9{U^ z*d0(j22&O0us$AJi`y;OWK*snFJrfYnJOXG=DuQEN@zl?R+4@7E{!QsTR%_bx2Y>d z6`Eaa?NZUCoAVuZ^vKGelWfnXkq6{4MBB8h#J|9A4%mBT{q9_NzSXysG z6gTEUr5Mv2BcY+lF!r*Eaa=Oe@Q?boP5Qv!*6QSQuwq6S(}s;rg~_kp=wGS0LVJ6^ zHcL}3Ttn$l3K9ahz;S3I+?~j{(3TXE0a(ixNV^4je#Bf~_r>g$ev8zwk!oqKZX93c z8Fc7dB%Xi9`GJd%PtF&`^f-uk`^CWG;GJX=LLeVv?;`t25PKm-`|ML)QHoyWxV}4= zW~g{_Ek+nb=a1irQTb!ME?+=BwXLpy1ih%a%MslK^R$)!V*fUgHa zh|ddqSzLwkzP+VKMBu^{xC2%SVwG8ArduX_vf91_V0gWMS>(JdrykoR&E6IC?SmOzOXm1(A2jKeSIeVsVCL{{;%~PQYZ7wN6 z#>3A^{tDPQWqk7zH)7?zgZOU@^Pf{A8YAN1@_lNYzE2I`|BqAiA0R}rik8whTK|<2 zQbdc?q>F?|Uq(v^1WHxyNK=q*;4F%}{rj>dI*~k^)a1nPP5=)akKhC7i*lq<>Ljp` zne52mS-Zz+`ie6%x2O9%uOP}19gAm@Fj@>tyWgnoppjBvmCi|HazET33?#*01l*|f zywwGXA+6;&Ja6xg0X5&B4E)ok5Gf)o>^SCF7zftYx98}0b;&imj)6#5%nq!3=~5$h zH-?76K;&i)+ATGe9*k_=A<)G+&)zaOQ>w35(JGlhoBe2>%166C=7t;3ZOKMW#GkGv zgVD=()+_wG-gs}oVnGTDQxdpr{h6@McD=6Zd3*q+QZa2Z+rXX(Oz}$tV#wnl*@6vj zIH9w5EKxH3D(ryF?-dJ1>tmY@I_;)9yymV1#0D1rZW+ z3%1g-Gyg(ArH>LFm`}~aJ1(52cS$a8F*9oqxPTe`0k=gCD>COB&6o?59;_1?0x4{y zzglbZP^}8rMm+YvBn_`~8OgBJQVw8+DgkQ)>q(n(9ot0W|WqX7hF6F<|9)XmE zwmFX%8>{gR)@+{*ZX`}cu1 z-lf+N{YKZsiT{V_nzhmY2fEg!4x^v=jjqY+`Xzq&%>T&OSC~r%2LP*5TF|1xEZtT8 zV#qGCSqB$XOh7GLXbZL5I@(;!SE8`3+^|<7rwVhrDxO~1^lF`Io8sJBY|FoAG;^E& z{97cU)gn)AcusjW4lS$NZ;-N;W}nt8baS8A5{z4Dpz-%uv;m3WEJq&g~D~;7OQ)= zpWPvL@+#RN7SKaxcD+!i22Z9#$?pCP)6ciIaozvPLD&eP{lbRtyvyw_9&!zR;l<>w z7w4C^Yk*<8hqUIUY|5XoX2j&R8BTFi6bZ5Xhw4L}Ehi?ALiaFf3O*g3jGD?!tavOCOz3faPs9$9&-`x<4YZ328EP22-`|~Q^q38n^VrTd(zg68N|bw`uR_?)t%(`^BEz?vc86_-KWXS-Vt~;lT7f#%Fl~`+ff_Mw zGScE8ER0G9xm2)rvU_kagCDuBBM@kW06dyL$dp+YK@0GnlAtiOE_1P66$Ff^@c6ZQL9m} zs>)l6aGniwqx{=lbImRWwy(r0qaXtwN&04XUy%E{eWDb#C?%rlkKGGx$7}m$JN16~ zFc9MTMv*iJ#nam=+zx{a_vp(*+-nOJD`sJ@7}4puWucUTAxFsM6hbcxZ%ZO#F|C05 zq>)y27844?$bgN#v&7$1lblb(;8&nf`)LV~>)c6`;J5N}R^YR_jYk9YVCa_GT5l^E28nCpGaKV3Le(ufhS7`+ zQG2e-M#~;nJ(}8{`6N_-{~V}=tT&3UE+;8hZoWtpX=ORyrz(GaO@OqA!JA+kI*AN< z8CgQU3~vU*5lXlRJMB^$vXpRObO!v=8!@e#QwU{t{=kOd^Z_pvDozf9 z-0|RY)z8HB5*@L-(dq_v`GChbcSrA8xxqF2TvfKm^XnyW`GA@^e___0MH)trzcAeO zsR`*h3ELyX^9$A+*m3TRQgiO4j_aAY!T!oOV*HXwW7iZqHSZ_FJGZZiSqU~pXt8h? z8tDmI5@}T*$mH7Y-@vWm+V1Od>5Tk);w~r6+&fJAhb%g*&K-a(MFE48Q#BcCy%(@D zTC-2mDoi3nLD9zIcwRK&+6@avEDGxo4ISG{^_RPV6TZ<3oPBlRU84WAu+ye!9UNui zMF!jUWY3psb^2Lv2)u318ck+i`Z#!Adf1Fr7i+3&87+(+;Yxqk8tIWr#-IY2&z6@zv#T0 zGQ+G`h&f{}pK5CHhd-lz@^l9&VhH7^P?|N-R_Gw5rfg6j$SSq@vUZgBD1>zn8|~0J zy?miuX+cpA&b9x~Vt0wh&>dM&hITNPXpE2QLF+afxXFo&)tN(+>b;{BW3Bv!%%~ziq6$PVF)rWl*A56f@TR zXdKt-L8(tG{I6xTw>N_}v0d8Txjv3w0l=X{r~lB_kjAf!>}b<%49y96u?eoE<@?a$ z=B#I%`OWkuP5nc^pT5O*qWsp$WKUi8o0;Tld2D*d4RQh_QV|!*`#)3-nu#-OG2=4A z>(GbtOoYMLM`3l@usRb!0uQ|t?BSPh)G(EAC7&*mKSyya!&XoQwtg}|VZ&=qC^|l; zu$7DKo+LM6OiNlJ*SH7;duLO10kJ=NDZV!@{U04Yo9M6p1nRt z^-QbxJ%B>vrmQv)uDV?JK+_$b_=5JZJ63XpC8CW&R_Y1%0qQI4H{J*KAGj z&-CwpmKXA`jjX424{2psj^aB$4gvuS6B85U;?J9~*LOzW>|PP4%K1)V;}VD2vLx1L zePWr>n|@TKRQO;ylm)t$(ShEB7wSa&8@FWlA_A0@SA5YzI|TA=2s`A&v#KSwSetRW zNmF&K0iG7#w8@>C=wbG7s%{^Snqj^HEA}j>^5{449PFc9rh_?D*B6JhpNDCZN(yom z`QgVjWr7kqBLRzzB5<|RhTNj7>ul1!JifnQRGO+KL;q4U+uU=&wI}F*8SqnIe}&h!JPAI?b1- z4nhTo~BmB1ynNH)$ig|DPC zjWu9NWO+C0Kf>|Y;Viy$FfwINd9pWdPdG4D_eyJqQ)@=AEhf;lQ)Dt)82C&iyY98N zVXY-2WZvs=Xk0b8Dd!(kln_-MYOqN{HKrdnI&ygH>p*#x7PvOpH4rgk^iCwCb=SK( zZlR7fv9KhYEV;&Zts7J?gMa26PcggjP%}VfVYJzY|Nb5i-Rl#6ZMY?}DiTXn&Taba_1F}+==g1Sjz{^&$sC@tK*y+<8};vdmp}}A z9=>rR%^)G%8iIWd#w){PKiu3ubVr`>7Er;!bTaP_R2YO9D~VmLQMa3|xE(JuGn~F? zwj*df6US1!NNA$6vH{178dQ;uip4=@Y85%;xw6Wqh6XY)ubrf0EZyTZszdgC+k>n; zjZUF1kSi4rmB%nT2@*00IPmV6;yHd}IN^0oOHW`Q0S@OBPNfXLW=rDgCvps&zdf1YC=e)#0D7Ez zANd-o6nlq5qd*~Xv*nViNT;3hpq78EQrtktQj4b7O3TxHOUv@Is@F#MwRdCO7C?3J z;Mwczz4Ll=$D6r>-p~0D!!JdJOdyQy$-$Xh73gM6j@Y~n+-jckU2{Y~SbxT1j={pRJ(=$8bZkew8{LM0th3l*XTjPcT* z8S@)4SIT$1;EiMil$;bJFP;so0Iu4lYoo~Fm=wc0CWv_M&|lTR>Ok3x6k8gpq_~7A zS`yyv)*2(&lWW5z7nmeb3bdcL>wfIKp`?*{w*C$Db{JKNLJ#HSp!hRe9!82j3Jf*i@JJ<3ULd)>7^wolCKS62g%bf5C zMuZDQs)HL?!4Dt^CoN95RH-gQh_OE4qq+P( zFa;$0Tz%1#M?H%j4qkBv=BT7%8x2;FH7P0U`leM<44*o0Np^9(ZVy;T(feEPqhzHS zI9C(&$xJRnW4)Z)X8Dd;+)$y?E3_3hx1Z`z*N~rlFN}4|PHrc;aqhK)$>g zQ6%O5u<>_i;#X-HCb%CVQ2)+3Ev*heY4siQT4Eb3xzZ2Q+1Ji2 z)QUbqmE_|8B|JMJrd4mw^EIiK%R8%@*&UYFyGC%i?^04yfADBJZYX7nlgliAi_kLY zS+Lzw0MRj`fjM7!U3u>!@@t(sFDym0rJ;YrZO7Lpuy_hpeJvD-$+Xom@DuyxIreeAcX`VrjcXd zk#fkHa;%}N?-74UrY(fP=#U9n#2QL$z{da`>1mT6QsUxoy1m@Uf60tSdEQm~h;ezR zT&uhHt85~_d^@y2#a7`lJY+%8EYFPy?1cfq;Y&QePq3i6%nr~YWq3yEBrVU}Z!D4e zEmEtOt);v`AC{-_IlgI8#vYY9Fa&#YYEc+exC7AWPE+1lsIACr~eDsuFnoy#{|12Q2i zIJRt^eK(>5nXqlA+gwerZ26#C?Rs#w(Oe*tqZeEndp>VO7 z_^jT;HyQ)Su(|BseK#TlscG`woJfwZz@*oL;NAXqf`7CQhLKD|$4|Ech0U%S*m{Uy7AWCzpPjUyhXdR>;5C zug5Ckt5o^;Kkcjdv1+_St;;EcETUe*G}9}BEGb_`Hs=;4-C6Q$yi-}mVa+ICc3IZ( zW+K~^mJ`w0NHu5IG|V?;=i8)@%dWVVv@|Ztd!02lXBUR7G3Qp=w2dpcovW7R3rs=bR`kFP8MrDX7f{XUeVD6+~ zpnpYNhEk6N4KmFeKO;IJ>$K>Jtz}<_xhjcIH^ZBQ4C+>Z!T90Y)2hFn88MGnzSlfg7Kpd%U)1T$z2_6X93 zNe)6nuYmMGw}d321&|(SsY_#;7(f{`1$*{m!bm|GosJ<6YpBao*3|_tZ1+#_Wv2{PU4S$TpYxSPDe7?Y!Bv-zm(jqgUB7LqfW4gI&au6L{5Z| z$cUo5m=9XX2vR&PnIBl62H zyjJ<>51s-%e2eDXI|Lwmztp1_I8a%0GVIY3|p!lwZ{U1?c|M@QePc6UN zyA#egb5LXKW#VxO3P!=<`V;Y6owiW`M>`yYH$qPww~x#XRf3={Vda71p=Pc|RD$p( zo-6;Bhq(TL1)NX25CS_Yv6aG>PKEdNYx?2&3r$++>z(65Zf~x_2=Qxolz>S~9nb!Pu?e!(fgv-He*xB*kK}<3xm31gomHGlFFQEN1 z#DW6^xNWq32ydP>q{a^K18Zt-Pi&O#IoiP>D5yKe2$*$1HH6Pb0=vYwl_PZ?Mi)^K zHa&&A1Pzw+IgE9>$^hnJyK1DcT)X{(hBw<~3cdB&6tCF#G^`f{$2A?zO_F{!#02_< z1P{vpmqu1%d7c)~NQN_XNIF_8@4SOhbyB%vkBqE;pVkxy%@X3x8kK*Fif3mEq0)X) zVX<6{m4_R~yQ)4%!bz*kWphaSoLuc3Il#>Sl17O?x3{scYRuGUzs4*@J^N7#y; ztg26{`bJA0UG}hC)HjY?q^grit+hF!gip9|JDRB zN<$e%#Pr}~oC*u`(|7Mrj@zd04#gI1$5rUoIs%UCDm7P2{M^8@^lJLj0N6OC9BJlO z^&SVlNrv(VZV%%BBJM4N<65(1QQKmvL?s4`nVHFAW@ct)W@ct)wwRfjEtV{n#mw~7 z)2Cm|>9~Jx&%}EX6;%=SbMJ4jy)sv>O!St?7TWoeofHPq z?)Vs;-Hvb-+aH4!u2O?S9o1W`6y6cPp>8d2|8zpZlSGiaz|=0zLIO}E(br4Q?}<}+ zYxEG4)_R)u17#*rD@(?TqpYMg>i(!WC&3^Mj0Ru@b4}ZZc_ttByi_Ogz8X3c{LM6;)5LV<#w8@t2-?WWJX<5*Ff&vQ&iidQy69>jlA)yXscf6i>&T=a-(NCEZ*zZuknp)zP&Yt zd$CJ7BhNv~r=@t^I^%}r;sX*&5H?GbzQn z&mC)TbjQnJ?SQP%UG2>U-l?=0Tx#m&keB3x96`}Oh+%>2M54~6qK6;b+WBzgAcb7e zyn9K!$Wcx-e1d4(8QQ)dRtp_R*SK5tqx-I3om^3y5re?lVNzVS@%H<_!x{uK?1zR9 zW_>znj?B;PPLY#8qngteoKuq#gKnzM$sSf`I}ukHNz2s5k~KCZx~quSsE@TW7a>4% zlYbQMGkeNV=j(B_eR294HL6-#7Dzp8Fbix-wq3hYB_ld~e`gV6kKwLuVsOhK5@BzB zv#qQMG-l`CMMC3!RH+O+OCejjSe=`c;X-gHy0+|h^S4hLH`7X)LfIS_p`^Bu<2EkyeQ`@*s_Tp9O0yh*~E`@5|JDdCNW_&Z3`u2jR$ zHSEe}d*WGVgXp3q)Q_d`zmH=q!cTqD&7oMYRCeT@sl9bZ^cCtozB>5x)` zL7dp*4eVgeG@}k?6WIGlu2%1DsQ>V0N!I@PBf$TfV|(SIF+|n}pMQbp;tPktS;fAnc-knM((#Lr~2PFT!TKV-;L1KG30qqX#LTrlW4lyAuf7$EA^-TU9Ytq zz%SPRsW?IS3?1S7Pn0JPe+}yYiW|v_`Vsz(9zn<_ileHE%?lK*n%a@rT1XFdN56xQ zV`v$eP>dARl@0^Hm1j5yiPQ6}h=R*=!JFO8>5Bml6%R^||K^Y)Uhbratk*KuxgNG& zIx40KwryfmWY<;WUeGsthI9)Bb2L?%H7{&eDp5iyhhxFRhP8}qNyi)#-0OxomA}X8 z$VKc+_t>bcBth7rk{!#eWEuFYV^T&Oyzg~B*b@0k$5o0FdbV*ktUKz$)x&N+HE+mh z!{_h3pv<0K8r!EN2kBFiBlADGF#lZfRxr@B{HOi-Ukyp(+Gnf?Lh#3AExoh`bmBKK zd9qlL1uJ$YV%c0$a)SKO#BxzlUDAvJvRXoqfJB#_;PP)_FgU&;{1Y6~v_KKXuR0S` z6I0&pobP8k-9E3NHR1f<$f-mf1bQ*nCHZ>u0XF07^=kx>FsmbIl z=-I^*DD6eRtlBsC(*HD_wG}jDX;m3(@Hsg^(>R{NE>*P6?zJ=3N`>*$JM9<=^Xt%-o(JB`>>g5(Zx8*4`UDT+h^*iJ#dfo-3Pmv&`f*}f~pO^Y2>ik}j`ygAy4GtAKu{7CH z+-X_*Hjw7!vI7kyVZ}f&8iDwkb)YMA#$fX_Pej=xi6?D)j7*#>^_nM^fj681QSu30 zw3Q<~9wgbZKv=F};(z%G~zj2&~|ha2MTY5m_KgWdt8!)Kp|RPod3OZb0uNS(|q z4UO#oc{V$fC1jA~{t^27jLwnhpG($-AVf_~=d14v)c+DjG!R6LzAiOh1R!TLG%i=( zIOSV^auTE=CG`lseE?gGBPmvuaTn!xw;ZH!M6HPIP{D77SLo$pT; zbaAth<;erIwVfK(?aSsNdcMOwrirNz1ND1u-RHz{LHCblU4*dPpsghR1KKXuoXOa< zT|r@%ATl69YFJ`TCL{T1k}gq(Zk%$jtyynwW$Ha5@Pl^=H~txaNucB9iQD1ze zwH=3}Y=}gFvpn^?qc&=j18{|%j$JhB+ArQI z`N^Mrj0j~Um^F$tF|FOl!z!LFzKoqh%YCl zV@~a429^QG_e6D3!p_)%SXKjF1g9qm%QrxYq$2~9Bizs$nKQzs^cA7c7jiC(@JBJt zvm&3gSa#_9=QGklg-5+C4YE@uNMyl}izr0fs=0|yAo32)8Ogz z*}>etB)gQVL^r(3qSXLFE4SL%&ZFJLw_tPVg(~(bYCn$%6Kjf3grd<71!(Mthq}2 z>Y&31|Az=}d)vb(Uu$6n>h`wh;i-+z6whG>(q~T3;qNbz+wGA4k5u7I=Ye+Akx69F zb}1p#X|1HY=83YqgXNKmpcbkzbC+Nm=i}(Q62WLXW9WvScm|b90ytXo0owaom?3MbE7*w4S zx!D1ubO|!RtKW7mIOzczxN7l3{nG%NK=j6OxWd3kv*vq!b`~byN3_Ac{R-K}2P*+# z-UIr!a$U8FJIZv6M#LL!ie%QUM^upI2U`&_$NuB9q`TFR1iF_kl*Dl|&Z)SIw9 zvptxp6|CUEdb4?kWXht-ZAb`Z2sCy4tV-fJjpwFnR42-x=Gg}7PosO}iPlnUj*cg7 z26Y>>SPi%Fz3a8{3onUgX`8T6hbO0|X5_8X+uFT_{b8oYn{%Dv{TN98aV7Rbyj-!m z0nT{C-@+V^vozX%ctn~z4~}9)kg(2oTrf;DB72Tp^e}_DA@|{l{q`J=hc2!u(goM~ z^h-nH%22XJ8Ko14woF9v1QGcGEe1wI3*pK#(vRSwglxI10oHFR=jJ-5O~{3{uNN6& zM^u*9KTMoQuhKw$e62~~wne5=ry(5u^Wj;{BZ1ED(tFT?NlnH-gjJB3^Z1CM6=!Hl z!^%Y9!O(foBy=19D8}C5nANU@qVBg7hx!RA+W3NZuF##J}sRe zyo1j?gLDKO%(?y^&^F!bK|2S9bG=iWqR&#_hWFzwCn`q}();WP@ww`_!)<3i&<7zczQ|>A6AdfrIjgHy*@1T!AQ)7iL z5X)s`RrDQAyhKV}E00tNK$P5z7#FPSlk>ne8USY%Qsc2U?a6uOS?a;ft5SSTINuoV zfwHYU-^9N!Y4f|+(gxFWI3-IkR&O!v7@iC>09{30^b-to5+u7x!_MQmKJdie#W>kK|7vT08G4SMRrA&$QK$5rxl?I6ptY|6awudZV>ieOh85eg=>IN8H(8RqU^J z;4_KmvsL}Xg(pL7_|z0XFCiQFfZ`kSI7QT6g-i&Lun46Cu%s(Q;7ie#5RI=W)!1yd zUjj=(f$L9lp(|35WXE~vf&^#LE+*Y+HpV8WHotkjzS<(fD*<-v!Vxv2;~j~^30o+F z)lin`NDJNYsUbU0pix6=3i$`?{84JtkowER(XGi;6=GFv6TSk}dz)I@%`vivxxt#z z6E)Ww%-4-X_auGhO_M5u2XQe`A}Gn|0LjNrF)u_^sZB?;45~-^zg8BoqUUQ`&TX1A zgDo1;*W5R1y*4V=x*DrqzVUSr!un_b4)C75gnj#37UAHw+VTVDCJi=tq+DSa@Dfox zxwMvnaxQ*|E*uWm+NRf1?0QZk{91fOKXJA)7*166Q>mjoFqO1e>WHO(AqPnO;t$#1 z*po<3INf@Z7Cng|Z9)bj;OtRv*|jA|`Ke++?00XAg_3mc;N3U$?rE^_jPQ#)Qe9y! zpFYQ|s;*u7puOv5mm0S~T1^M`H=xrnP`$$91gJZ&S|=S!AqKhH*j$%gtL*kTeJi~feFF7)K=qDD~o?W!tPIH%hL$nn1LafDr(XxKg?{>x>;;0@7Nz7_<4Is zOq>wU(3v=Y&s{&H;GIxHajiL4Rsy-uP?%4&Ed^b2PLKS)GC5U8HBzw#Sp`WUr+Lth!v`7Z3P5@RVEl`=4m78{ahNmZ#M41b8$|L2 z3J!{|Mjqd3qB$$`fLxtI1pX$x{tALi|5zNtL+0WDR68+sj!P>&-H<~LV_1lZ|BXT% zV2NgwaSNJ~wkI(VzY-HFZ2lAB1#ovY()Ha`hnMia#QkiF(YP zc1(d`j6c0NCY^4S5x5H!Hsg^Zg_n(wLnjyxwu6-WG>9QF8C)8ti>ASS|2La4qQvTq zice(T^VyR82i`XS6|E&@9x;P%Y;;P{#CxQiUwpT6J9MB(E!{EHmh@~?u&auhL z+~$!o5|?v*U)*wf%fYRBWh=#WP6s-RWnmTiWy`^+&(9o)qkK>r994t?_tUDMgIsiI zA5Wdq^5Fte*&P*JO?fzJg<~A}tl!!|=jI^gI+RJByf&s`1Y=c?dvgF%^AqXz9h}C` z-r9B37L*6_rQb*$UKh_^w5~rh^h#+sd6sJPv))s%PH5G}t8O>6Vls~KFSH4C;Prn2 zGv?M^Z%H1TyI*l>b-%C^inH+0E9z|HE~gr)dePW`KY(GQC+Bc1rwb%&*E}&49%1LE z2yW6mVa&DC>csR$kBsTZ8uCQZ$nOJ#N77p=^B*&ii9?#nfJ2ZgU~?(0a&zFFw+QYB zG|%wmnr_H%8v%dRB8xa96=G%+5+NqXWsNcW7x5-PMB8lt?L;huC3|sxo(SJh)c526 z9`XJ66Yj?D!;%459YxH)iI{do~1^CiX9O;uoiFgijD%>!^QRh=(e3}}2|OE#kg zs*x}Ti;E17Ibv`O2k=cl3Z94&wUPt5#e^8BA618gO?H@ALCBvd#S&k zB_Ilvl?aT=;+CrN95Ct>PqT#NvCsI03KJ-?X!{Z~DlNRahb0dw+v=pHi9Tv663s)H z$)gxr<=48SSHD+f_F^4#!5ZqQsYP|a9@6e8Eo|kF&1nB1?H{cB)xWL-C~fBNFBsHF zx!;~)t%(peqUCqa%BR_eTNyiA;atXYgX3E5WOw&vfUEtXeZmUEY&oSyvCN;vhHW`* zff=|=F@Bz_S^oatY}o!Wu-Zn9nI8TWlFfb^%838p-x)D$LnBuOC;k7!+RLOGjH{*) z%DYMZO8bgsI?GwH6^@aHx)^IvA`x|i8H`yv8t6A7D>Jh^)={atGh3Zq}%7;F4P-jLwbIJX8-{I+joR&DYZc)B!J{k+xQe zqgv~BtV{Rwtsx#4Mw?z#58fodjjp-RU42I%P`F0+1th(D_Ch4R_XIF-4ps>zy-)iK z9>d^y2Mr`~R4xPIl^)a3cn`0|vNsg8xy}X|wO{?!x^H*XXtQs2+^jo$__Y-;DZyx= z_FO`CLEH@tOS_xdSBuUN5GKDV5FD!-Tc?f(t9HozYW-DmZyKiDWmzTX)Y#bW!AB^q zZKA30>EofxHdizVhFo!A6D{26`aKMH1{^efJRPmcap;-CUB5OvR5=g+ZrFXt+~-18 z$c3t01>h)hHzFHU?JR2|qI#k5LP?b0)En(=JjF`91le+;uZ5Xs1|`yn1o6Vr(1MLG zI|DpXBs^JgOcv98SmlgUg={W1?o|uwp6%zVReUFfJPhoe>@vxOJZ0j3D+#f8QgBtQ5ObF zvagP_n8M@;HBn)=TL(WQSsXr2q!x5)NsHyBp*yHC>)}?#ggBMNES`zD7I$>>j?IHX zl0Vd%^qC8;S`Ye37^r2pG-*XBbeJUBC6E#Ce+%CSH)(QC5!;_H{W%y{UxM&+CA$;` z2=J)BANZ7pIo8N@NpC048;l%9LoIygW6`9J7z#wVufwm6V1ZYYC^dOyYA%ZxV`r}> zb&s^8R&NlwC~DYTsc^DR0LsV}a2xKVfvB(3xe#=uk0f@CO0GYW+x4ifgUkhO)>IEF zaSMNg8AolhRBEn3329mPxT+h$LO+eA9Mp6aNC+(*-Om!MiVA8N zGqaj8Y*`tKrj}$HnK-L$3PdA!Q-FX$$LufK`Yj3paa>QIRO)JXmh4QU2e_ydLm*Cfa>Pa-n?rZ1mDe47f{WP z8xrO9k1E&+G7n=I181#v($X9)npH^#Hgxlk4;r zDOsFeV}>=4wkADv0xK%k!mF@-G2K)4_mY25}R<268Kkt6NVtv)><$ZO9v3aS* zxFCB>!TlKW@E)igig~!gvAXj1^!@JrRUf8zKuC%yf{MhNWJQuoGVudiexkC?!m1?`aS@PDaAip4{(F_!WJJJ!`Nq zE+b=5F&I$~9Z^W~X4g_KH}uGxUI6qpUv{F9=?LJ-2tdDNDcMbdkkOD7&LlQ|5sD)# zHW0RTt4X2-%OJ<6g?7f8Qpeb%CvG^Sf*$MlsS99e94+9IJ{x?kn~ssV#NeIkfBI&@ zVCO0;Sk^NO(FO_{&CwI|GnC!&J#7LI>B`FhRn%x{|FnW%Ba9eg2&!*XjIX?k2hRAd zmSP^E!V=-o7*yZs^S?E`sd2~w;=Jq!B})vFme?eqMw+VyX>th?7#7(bR-F zSf0JFE|mq7(9g^VB@WKg|_Kg=NN}PxulL7bY>3= zPa1TVZo;w=PUZ-lIz3^}cd%}SyYQr76|N)a#f@IjRSMFfjBc*FmfZGTFrJ+B1`R7E z+~SC@Cf)N(XiDUcPTfdq8w5ELaJxmbr?>2%<0f`fkK|rn8p$vYE*)EWf|4BFt8pv{ zmJghf(A$~SI7P)cyyZuB-nbK>7mV-@jReJwgbNivf1HF1sa9en{2jWTriG}LK}Lcj zF5DFzuAF3&VU1Tzlcnka1R>S~)C)X*>a0J$Ziv{)&sJ^ymb--{hi6O7?*BtxQsSRo zE*Gj*U^|;26qAvCs`aw9BAeTC5#%DEJWFQ3D!h&L>LC}PcYO%C zv3k+><-E7G1s84mt7de_h&8-Kg%yLPuXAM*Bl;i*Kc%4+_b*NP<1)o@f)&=WN85zp zal=5vgzTGIKXaTpXShipShYsDfKSQ8QP9ezCm(`6awf?H?$&P3QL+?~Eu%DP5w>L3 zz-T@Lt0I^GN?tstT*F%W5%S6%`jHnl4;Q`(0#ooVz%q_;B*=UUPbJ?ruRk0P=a(2U zQ!EF}iRC?18hOt?aB&WwPbo$$uOS!w=(P(C9#D z#Z~bIe8DLB&d-veLFW8u)GAVH;&s2V&~YF2OuQh_(IR@K^>py-Y7^7_jDj&f3+!!q z%t(Mbz{cz?7#G<0uqC4wQujc{Sx?U~O|rG{vqUMh+de_&_Ys8J#8b5qcMS>_h0$(> zqJ=72uNzmkWX!V#Zg;)PF4FPT@gQRk=^0Sz3m$7U&}>o|HC3tDZ=o(F>G8KF~usZLW1%fp`-<#gsd7~PGMb)$%`d}R8bN!F0(PF z)qgR;b}jOxHu5A6rx|O>K}Zw>)Im%TffUaSY1o;IGS@j$>JdWJ&KwjR9fF#jV<$8%06T_@^NZ zU0qSU{nh)5C((+mIH`$Fkt-4*!QPx2vv<24TYZ?VXmKgfES#2KN}^^Q;_GVPWu zwrbCsRf z;RU72whgnDbQ$BRR3_Pg&p;~_;SYl9pG?OvleHD0fZf#O>%#u1S34S(a2Tsp*0fYs z`IGv`b@Yi0z=Xn*EScBlz`DLs$+aH#2_$KJEDxjBF=UA#rj#ZDO+T99lMuGdzB$L< z;*K2l;Z)x9&$8S3t7jB!k>O3sD=anbsNdt)>KtyiZrtxg;~JMT9UiBZAO8jyDBN8i zbU%Sy{3nq651{?u;exP@y_KHhUz6oHiBCX>93+DQDtPH9qqq;lp&B%%eGwTS(5q*@e z%znMF*mxjj?e{wm(Z6KdW#fk)^(csBBIizfqc#k4 zK;n|h%-O=&=>tUzGa+@2HlEPVZV-f%Nqxrd@arTYjwnnUsMHN>prhzvSd723&p%Nu zqL;BCx6}vQxA{BOn#voUR`)3pI7RsKh5vtu`u=(U233w+kd%-=I$0(Qj|zk2&8;Zm zcIJu`b4zFBlWCCs^T?=^2%zT;;{kdg3E+4peDf}LMiO~<1ZIsUWj3cZfK#mLl$75 zw?{X~6(N>U%Vhw66WnizFWF-xt7x0KB!Vura~cL=;eh&Q-OQ zMyI^Jf{?v?_e7`N6M>%zxm|>yZM;Z{-+|d@f!{H;fCas&c$w_WAj{-H-j67|n{31u zz}_iK2MxW63JJG1m3R)l060Tm?pEN#Mh*q6D);`PcF~+hi83Dp0mYmvR% zip^)u-PD2&vBH3YpCPl)#?nz66tXd!v2|%gL2;HH4mk}Qu9|=^!~11h+^gpPN?t*) zM{}LKV0(__Q$748Q_Y}OzIPd)-?4H5$*|c$SDNNYUW4=z7SvT;lw?5JijLuQw^z&U zu3cv+0r4U^?$``%sU=?#sqnU5sfNcbah84@E!_s!h;cztXyw-%U2!q^we^&W9KSQX zgkC87pj6(mh^WBP@0(%ZDNTapSi)|ybB6;||5%Hf{RD-ySR=B90D}cljF@?jwNdF} zZ?voCvD~4SJ%bvnOM4h}>wW1-s5hSC{*JzKN3U_3d{wb5qErXkH?HhW*qN+Nq+V9YCyTzsc<)wo&a>31s2Ku2WYP4U)5fKCI9@j zCz`XCyLNn23QtVkM*-)zx1?i+bBN=g_n{9uE2SBqK6iq0wOL-bc=+C}V!ruW9VhdR zoA0|yb={tbne&~N2T|58v^T=CbJFZ->I8A8sQ500D!;lE@0^z5xGfy*WA>l!)dp%7 z!C7#t-*ZD2fb<~@Cw}t}(?w?rcCL>E6Q!cbCmp^!d8NK4hPR*=p-6;j{R=Dgm8%Q2 zF;903Nboz)&6?8Njil(e2<9b*@xy7Hr4d`krPh0z>v`M@%BQ1px}i17t3za)w#6M# z<#`BeG%sWD+3>Za3JqNlIi(h)D6VlRn>{<4C8_t--JhNIR=(kre+WmZ1v@35zco}x zHZ^_#=;z)b%m<#%)fC^Nk8c_fdz8s1LKc7Yx-Z0SL#AyAXO@yq#6bAG0$KQk0|&sN zDhR^d0eTQ=7$nu=9ylhXcFu^TpjT)F&^v`@tmju79J}qz=x1$#d<|~|=J~9ef^?Xj zU-n4@X>y%Ww)?&aLV(b4kun~y(dmFvVvpjfWDT^`#Ab#LnrhW(aSPsVf10~P` z6$V}jc~e!6twq&UMQA%ayf1FBFD$&T6D25x8nvrA|5AI$fthK_*@L6+nmc-rk`#k zW53pYUkp9KXQRC8(%1c=%S*w8(8uY;$GASC=SW~Y4!A$cr)qi6M5dCyU!Dg^Lz9mB z5wYt|Fs>`Ie10@#s*?c_Mi&vjTNk8l=Kj3id+xl%Y4swwq+Psfb0ZuY>TA-33#;C! zapR_GrXBA7*nKrY!Se%?{L zagi3_YXQDp|BgH_jTDmy#5lglYb==0!sGE~5u@kvj8SEJOCT0jqgti$B=0UsALf#) zVY|%c#1`toY*G4PRaCRv%VeaBt80Q!Q$i0_epvO5(M8|fA|vuK#3aD$(3Li*31b(jyPiY4-~^a$bA3x3hQHArv$x%c;pWchjh#2qYR8 zMH=i#{-FHyvMU%iZYtSD;eo$+d%~9j+xnf(6JpmP;g4c2Q_tC~vKg3Kc8>ndFRvr~ z^sM@GRIB@$USRgWbhY^ZDdr;jZ{adx*2Xqsjz;!+jyC@ic`0kyUPH)i=~h;D~nxm()Qx>Rl0CA|U*dq7HQ@t$MF7ve$%e?Q47 zO-HguiH(DZ2U6#xIebj5)HA-njOO@!ef*+9nA)R+l<+t*K>4Uzn^U?`LUP8#c=<$E zokmxqy9Gn6qqx^bz>2KKpr^mmGe)N@WmuEDz6hwqYBAfA056zbJ!67l23OWakC&oN zZ?)W_oTGB-RF(<^DEB@Hwao3v5fKE-&%&-s6GCEAZ;xz9&p^7Bw4N-0=y1qwq*Q5% zTvL!&WLw@uD_mOWm&{dd%JtphSvt5Gkwkk(#nZTUk)mN(A5Jk{pP~ww6PH2--v~{k z?OV4~@LP?=QiIdjSY6F1cv4B0U1QKto%~^SGI@2EjV0J)$dwMQ2W7jVH7g|+P7pDw zCB%qtU9rIsri^d@o#J{3S^6_+ymjRR`j4RWSkkqrQH$NPs6d`6#6uvsRA&yJqstTb zc|*%@Tb6Q{Aj(nm!X0K$ns$5)mK@eU5^Zx9>>{vgY}5@x;ugyO)_=mCWbQ6@BC^Lj z#w@o9w43Ro++dnz8*IPQYLJy}w6_fC;`UQQBvr3zT7)%S?yp8ua?HVa1|Qk$HV4Qq zTPK997ZDI>fH##?Al^{8rs-WdGOI1irdsb>!2BkYmpf%Tr_^NJ%y0T! zu!q{ho0HNhlKt}>Gj-6!18>QU6zGWR2L9la>J~gPDwgYV`y@wKGirMR_&U2H`_Lyk z6gnasLz@+3;JEoFz?FuTx)=`N?#`P|z0uBK9iT|@EYOYt{W|@JWR4SSj8Kk3MfS(E znUvfk>z$2}rF;1K$_fWJEPM^MdCX%1*VGVr#D9h?D!dDF4JP>d&vHFR~L${YqS zBgX?YnU{}%cS+zHQ663_y(dF(IF_g_1o1W0?lrJEw0_cO|2xIfY(HVI^R-;>yzKLEdw@4z3E;$MZG-lK_6=YL$v z7GxZgM$b|suCjNC)aYmKHhAIQ&`(u}3s$&=PB=TiepwlwPG>JW{~3b6 z`qXx;VE;$yQ-8yb|0=P66^*|zh+2dP@-LK+4a543Q#i2^7$npNpl-dHrZ#kZ2p?ok zdCg%kLM;lVDN31oW7WNt)lonbpPj(KYB_a-@@JI6Ny%W7d4=~I z%jUZ*H&O0`1&f$R^1dc;KG=$qWn(JP~x@VY>Pukrh^u;U1wD+XnaF%M|PsS-EIM4nt z;zyF7-u`GfyW2KbC(TcHoR!ZmtuC3YKGQ)LyAbP{TVV+u=ZPRCVAxo~{M+6_D6y|` zq4_K!`>K**P!nqibHRJtliRb~KYR@Fj+c;?7V{fsP`;ywlCQceC5eEAYq*Si82t`K zg?>>Nps&)-&1FQ699a!5hoq9uu%_GWw;nT6!ndU?R<@WGGeJaCEuQ0G=<< zrOSi~pyaV~WW!Y4-|m(B&Bv7gqz{5f&2>D#$CG|K&rEFhHugRh*LB9#LS7Cxx_WJ{ zfQP@FUhq~ZD`!EL-w*@1BQNig-3ldzDfCuL@LEG|{f&S#%QDB;`THWridjOv;4Aw& zaRq)ht5O4fN6#;`s!~2PGK^*c_&nds1QBq4b4f-JDGN~OFi>)k`x+pWdBFVDFKOn& zw`Q-TMqUzf!mYh9<@rhrW8ts%`JMR4P-ar!TC>w>pi^1U^92@I#=%)92i_`G?!Pgh zk9~tCAQa$W)Nd;@Y^5Ce2_>`2)y9eaoZFoP-Zx<47p|xt14mk_Rr&LD!B9yewk~k$ zy|A#(ac)botdOoWHGcw`IiwKczG1YVq%c7MCB8pT#AQ;ph`brz99EnG;Hlhw+9q5Uy0Vycj>tf~> z^*)Kmq`-_>%Izg|5)zpjrD>DQe4^Ljq_qhT7P}jUg3i7#-GdcC?~vaxpvJRUzZ4eo zEq8E3F-qm@xsS$qnzYyEK8S!%gTQoGY>6wZ1p2{02Q!zt*al&li@^MXfL3VQ!1#0HBPwu>R)wBMfM zF8;hVx@$w+_j@dExT%)CYhVQDF@?Dx9AgpyXTju7j}_%3e5Bs$_B)z2j-gU%*91%; zO4^jLQo0v8ZduT+-AZ=>GbU7ZjNDYgi8C$bGJQ{xVmJy?vs3l@?`dIambjCkle$*S zdfGFKHf&cJU!HOJ=|cV-l&s{FUk4qk`Iy%w*eDc9!Sdf`w$kK;)28kLId=Daj;Y7& z;eT?mEmZCmEUggVUW8BF2Krqb;!-Q!H=-*R(Q1d+i?4-BcOR^Rvq%~ik8r-v5-O=4{ zK@{B%+tD(AoGYg#;*IZ>xac=A>E_kWuYi4XC{N5Qw)L4?I!wKt1nqo@g4nMj4N6ilUsYI@( zXT#>C@h;n+5pGHM!VYHTQlljLnAusCMMbp~1=Tz#EWMvOFRU?n(=BO_8!W8H(je7= zVX{ffHcvw&F0%1YkJB?I1&$Q^8dMJR$irft5A?jzIwXWrEK>SG21CnmHqf0m&&)f% zc#VM&xrp!<=+8yWe}fF^|JuAE*b;6G(22}*#ddh=9-r&1GQ14vfpbh_h)%7oxSEP7 zD2yX+TxzxI+$EdLyKbY3zUFY5i-3!*070HZQVduv19imDG!&0A5{Gp@)@w9C3<{F6 z)64TdwI>%0)*lk>v|hhU)`u{q9xs=aqY_JS6tc!gND3T`M=opl$A`QTnLM*lG_Pm` zbDhsZ6%n_y_TgGJ2+8bxBV})s3u!`>ly&YxDmhlmxRj!^M9vOpP}>IkX2=$bo5JOK z8EQNosC5G}5WOZF7*hafCiNqtI-XsMs3T%}cBc1wz&z^XcLy}IZq}FPf5z&HDPj{g z&1|u)e``FP5rn47{T=iiXEkA^zU7_S!bfQ?vn|gv5oUlrY-Dm7XQ!DRkQHJ-SPQSi zX;#(|2n4YuE%SujZ;9A(gQpqpR29Bek9rxo6B$8+SM^i#Q_ts_cf&ZIF_G^WhE|So z`{M3cZ?*sQ!C!{(k=uFe7UVr&vT?ijse+=$DE6@DCk{Y@k0Zn>zPS&D)6%1A*41?6 zVEb^<*xZkEKM_XHP3CczY)LXcAZLpZ+878rcBJ9TLN4}4rU@K5*ieSHpljVC!jI_m zcgzum4PKp|dp;B$x)B3r6$@hd*rvs^ATneq+M`OeS+GZ9OVbAz9kpf2*z<+$`vT{u zSpaIQRSkb|LP)tyIz=UP#`xT@2$YqN8Q&Tf@Ny#3a6~3QBXQ|QM9L_h+(qG5#s~6M zLLYyU^ioL?dm(NM9ZgBF^~x>}=1=2KJ9DvW9h?x|z+N8)5v5ftrhyUNppM}NPrw{_ zt?pKR4`_~)EcF6JtXqcLQSCgMTIH*t4Wo2M4cGM5Op?xMvql?zi`kJsrb5fCSzKLj zJQr_^9)};U^-uPBEcW1^3@XICL1@{wbs7wU8(pj<*eQ)!>eqmjcCuym-YAs4DR%Uv^;_p7$RH+^NUT$*~SW+a^7t| zaR93w%wV``0!wSkFKry3Fw(&utBBLv+rOIS{f6j5%b$|`h&q4MJY6jacSjuSBk7XL zyWt!-MmfFnECznA-GjOj6Xr?)qfVL_kZdy|C&ww)VDKpE$!4rT_r zLBRm!0lKkD<8xU;3JzKuNtgaV+~w#eI?m|=jRE7A2j~aEbL{Pv$X4y9$Kfg)jS$J3t~s5^-I8_MBq7(vm_I;LilS*W}k<)@CPNFMZC zvYQ;zTdUZ?bC#lh{4{Iq_1ok&q6m}qnMUa-b?SIn|X8Y&0sF}m_4mKUCW@GnS*)qC#O<7Bb zSv8URG_j7K4e`d{#zsX9jzEM?FxP~le}qF6MnEYRuW#J~z=q+-Fd z3{I=4nJKdtJxAYU@*lRjYtj^(_5zx?2-Jsns6^E%FFg5`f7gV<2!4_Xr!kt$M2O7E ze4iQJs55DDI3V;J&up3RptP_7N+0bj1$d5%o(reDhuDx2;?TYtL%=N9^cuF;(~ zeZ(F9%_+!EeOudp2U6-5{vd$Uqf1=0k~q*`^QMB54uGe9s7ic6(mU%Ec(5Y)x#rjD z5JxeT62Lh?Lv($}oQ>H3h!F*+Q_#O; zT4e$H=uur=8*O~$8F^1j42Q_Ww}&uW4bp)9gXRo-dsT-$4am`KKK;Z&-#fMXgBiQL zzPn_MDQ7YEEXM#7LDGOZhw%>U{v_q{puXxu(uGWXCI@878ayzbQulp-Uw3Kl1qB<>MIsKj7oW|Nr9Ss#WIe|5y1qhJPuq z|3f}5(cTJjvN3g4x>u_3a=?|lsk!o>d>lB@*4IEqKi4nXvbf=bNMWM4S-3%LjGIg( z)bM18O`gV6Z=5F6Q;*Z#-X9=6#FkOI-%_L`cVLlZwxtHUikxjS8a#%3+WTAd`+EHS z9uhs};vP_1%SYm&kjBB4gKNzRLcsQ$jCD79m_cyq(5Zcz*Jzm0Rm1c|n>t+&^8Mh& z>ep{xgG13PO(qnC; zoE;~L{=v&-cgL(rw%m~%n%e1N`8s)pFxp$ALcv%;3Q=6`gRyE*SpM6VD=>nMOJxzF z84P>ldE;-?IBwxGff!0W{jEmd-_p+|X8;oNf||h#P>ui(FL1uWHrTh)z36=XJ}%bEfKvrfk-;$|17=+|&E%FnAO)VIc29oKfuJ@i z@Rn$auM{@drrfGSi_?3N3+O5&<^IiXInpPVM=h^jk9tp_Qt46dbBOz(d7xwrb9hx5 zWHzDE5?vSM;5x@Ibf!H72L1x6&0Kl6exFXuK@e-fS^Yl}m3T^# zcutGU#M8~!LrZ_MsEE~L=I6uQ#igsrbK~%LsKiSq?oXevNxVDOzmszLT?cN1pN2)n z82?+O-2YoS{zb~YI3O*dyep5WOK4-Qbyx{(@JGa%A>)Bz^8zgi5o!g(A-9;U{^4Ns zV^v9ovZq06j;Qd6(5oBwh*1i+0StZ_x%r4T>xe1K`5D5AgZuEj;9B2>NWx05r7gyx z>tfwuikky-^Xc%d`78Fo8pDBpZ0?OMdGI*dWhHvR*94OyG9KuN5C(W>fSscLpZ+AW zEbV=xun+7+|w)Ekd2QvG5Plch0lDl!o}*i&4jBDpS3qcw%{=rTN@Ze(K3No9uR zl*?v5?epwYW^_zN)D5%aru|a#J&>E`sV4BgvZ$=-K*PaZSEqA}oUlYoLWRM^jnPb9 z=ogRbAVM~sLwaD8LR%iU8o4o(1Z4bAaC6feO zsj}p$!|E`-;uN|DYXM@*xe7O#>8WI94gW-fn2BTM6=x7B1|SC+l(MBDbb4I#;qlck zH;LwtwXX6NTzGUs{2>U1^F|yCDzK5TIK+*aGkPd2@cXO+-xUWA?${#|9oI_4hkC6> zkV<?N8K86XTK&ybbXo=Wm9rk*u`)JiHsYyM(W<@Kh%iO;5Sm)N?1s0K zz&RUI_|TrSiX662T~5v**X8ru?ED%nO{vnrKc~`^x{}*K($)H1X_yd1!rrFI4Kj3I zmA*nYhdemjG8d3Z*K2@^XoMbXORUh$-r&QP0%2^rQYthQjS8~;^ zGt&B6N^X$Z^@dUlE9vGJ@KzrtL}wHpF!%Nf?Ym4*A4}dg9w8FcUcRre>odF*nMA~^w(@gB_?CzWQxR2-Y!jhD+HnS_xr42Qqk69Z zVlmojq;DHmfFQ?^miF#WrtjA6EtmT_0Y}>(bo4O<;Vi`E zlk6zpUP?z)e)%H32fjrjN%VuK;fA7|3rwaYyw&MGEj2ngpq;h?+Jn1 z)$%b?hbvh0d4ioS&3b&1KBLP|A@a8ZU*TsEI!0viQHu5IPoT;P_ypj*+3d`Cs@}0l zUfAtW?+pWcy3pW*JPY#olE9UQUt{3nsT0PM#2SR}S@z3AL%`ykZ-W9q!8S7ttoCte zrQxfq*1@Up;p{!Pi~fMZD*!y#cdQ<2X0%_Co-NFwf^$b$4rU!-Ut>->Y3(?pR&=>8R)rQv6{ z5oh>$SbJO|Q^P(L&DC#v&eNYH=T6hkaIXMvm)S{~I|?4?p7eg=%c3P?dhdkHV1rgM zK-4if+CtP?16u8e^iQ*f?fGk0tI;T_vsy8x(s6KUHp8BzGlu7>A=EpXO#qcGiU+C1 zVGdHm=_FpKyBlqUqM28-9pRrTypxoZw5H%!F!-Alx6(~lSys#eeJ+FE13~^>e^7kg zq^B32L>*d2n^927!(m@$xVHhyW%WO03aH*rr{q{|=AYV5b8KYpq7+l{wBx*%n^Uc0 zS<|0JSOTn`iV?Sdy(=+2QR+?MLSJe2zfnbY25@@%NeS83%FA7Jh**d2Yo`2E+q~D` zN7HiYY>gF3M(P2%Yi zuqa#Vuic}Q{6)XN_I$awcEZ|@&xI<2ol)@x@ISv9G~-eY`@jD-jK1S0{%;}Be=qXr z{v)&`TH3D(zzuyJu_RuQnqq(ii>l4W`d;uY5Jt1@mxQDN$v641NSeCFTdJZA%L&D% z1U`9SYE4Vo80!SWb_eM#bCOx@o>VymwnNsKnIF>;ys?=rnYFboB+ozcl%I+ULsaVu3D^=%h(*qOz1S=*QqHWKu zc~ttT3_R+?g}**F}hW_m4i z%wkj`nt1hrpA%G6g0?mqA3JuKVXiUQMj?$A$X)W_YB`(%btp7q?di5DGPZj*A|#Wx zF>Y{8bB0|J7;>1Xb#^a)*5^cHl+Za@pTebC zujb&!MLLL};$OikAkl0X2Nl{@WrT2|Pdj7&4!l{fiGoRk_gl|TcM>lJW3jr;MmW~0 zUNd{wgnX0hK4O+-dc#4GmgWyaA?z=v1f)Y7Z4PDDI?gv?nKD zR;;etg@;k#7*sxg3<)i2&e=6b*}|ok!RqdVEZ6YoD>B1n$F=c>OD}#PeOA)xklt2W zs1#j3A9)c;ha#?+n>_5|URYv!t?1TbR&WPrzO_F}2bI(rCs@dA>Too; zxj98VHt1DtB9?)R1R@8~ytU=sXJ_BM##2^c{UBa7S*ia3ggB{+tmeO_&QQ`6q z&p{2w(Qo$a$C1gLGk{VSiPR0=r}$|Fcj~E8(B{#McBKarO>k=He>mfQA2l*V@ew4# zF8Z6l2{=`^tcL#5>+Dr*cqRQlConMvmN%0-m{c7t(O%vg?}d=CAow2OL_gdiyR-lJ zigP$h3a_Yj*n+uwfRu)DD`%UnSt06c8gsFiEO6CupA;QWKdGEmG4G`WW>C3^jIQ7X zA~sQ#49-H-*NAV=3P#vA?pE+9neHQJJSV$adRwsd=z##?GkK-sm{F~jYQ{RCQ1f0S z7#mcvy8`SvzR@PRa-*Aqq`e&LHWANfp%w2li5GmP9YM>hGGYW&GsM;6I90(f|L*I8sn@>$9@? zlcY94KnQ~Iz8X4~fdeKT`mU({RYEa<`!ceNVrg`DjP$RwOWxXBOVU5=CFl&$?f5zu z`EEFpcBR$I+ z=`V7QI$no$uky}VVH7nMYiKFI7QWuae(2WTXtfs5f)G2_GaF8jyx@Gq~| zXX+m~`ubqsJ%oB#S;;QYS>%uO4Q+a}m!&>aptckKKCFP+q%ZCG+T?bcF~Eq#D9{wp zdMg4T_M?^q-K9R5Af0+N7!?;%bu#)oz=1zk?sH4RKst#h^p?yQ1cr5xJ+%GBDX|6d z^+=I7vM(5Xvu6+@*Drx0qMcLMq&Tk&5EndGR-Fi|Vrx%Fn66lWyNDafH%={yG@yH# zl9CE{kC#YK(vRHjGIx|N6$qrQY4Q_&Q*~I>?>%HM8_V$L3!zr@wr>|fU^xeddgIBn zcGhzvz4)C6t3ZLt_^1F+{aGac$bZEKs_8MfDxj!;e{H3GM(Q@>x4iL(IK%k7sw3w5$lHkqD(qvdlB+B%R5dxJKva1;I(aRSM;`i^QbV|DKY z{W9}^cO=>`N4LcBr4`j15gi9ssyrxsO?I8PjF*n1OzufS#w9n{APPbrU6%&Lu69I0 zLOsH_+sT}?P9!9nKx$W7wB!pAfI6{*1za$>4r`L**Ba`$w_?zi`6i(sN-M<$ zFb15Ubkt(rH>TJbj1x z8qRvw;L1;7KeBZCAOd$CZiU}bm&1WnU` zms<}EmL@do8pamoB@T6Sz`dUCA+R4_e80f)MOb7e>H_bBezqiIA4W9J6W%-LLAf=> z%U!fTz0nXIVU}<(=W52Xs5|l29j!TN2M{f7rNorz*!-gN@LAtjCvDea_6OoR473dmMNPpD4A8p#-Y)IDj9)hFr--IL-cF2v zPz zb4S(UiCgnJI%oye3rcURd1TM9U9-+{X8Clz%}EOvAX(V?JmFO_<&b^K<^nOpud1<5 z?wSM`3Z!Mv(o81G|CA1=>Y2`6mRLdyfWCXqm}slqnD_b3a4BM+<2*hI5u|c2V$SsT zc}ZU8^k8esBcLk4I6ysE>`gw%I`S+gDx?8`_cRh?5C%Y~^TQWdnq&)qH%I^kajN11 z8%jP}EAD6GapYDMM7|~_CZ;IWH8WyF_puu=_U{#jG6kYGf7T)+lY$fNHJ;%i!BOtV zY(`_ujq}W5`2gVaiUo0&3%u5UmOPirLz{kCY8zfefscYUyJRmgoCTi9{z*5n1{35{ zi{q&*asmaGID?%bUS;S5VU`^FFTg52Cg*1cI_-u+?fS-0)_(viH)HW=w!Z+YTLm>( zSmI*M{HgQle*sp>kDvbea`ZX&wF+|6MgIb6sNV&XF~}d>=r4fWChLR-h;l{{pPUzX7Ylg1-Q(lz#(Ojsij7 zfECv_U^UEeq4o_}4WfSoR!sU~{!I1k*Y*B&&md10&f974H&EUd_S@A)+^>?hkdK#ql55 zWA1{zLX9T~Z?{covDf5!=l5GTdrt_T-Jb5%NsYz++_%eF*Qx&8d(;T{!ED#cJ`MD| zx@=%1g%Cc5Ni9St;~@e3Kk;qzNZRBqYUSwEp=agykYY#-lt4d)3RaOc+bT)0Xx39n zsn0`%OmhjXh%u2FWqZq0%}zyvw)Y_4Kgn+eYO_w(7 zgu$ypZ~5UZgRro75u#E+c@i*vb9_*Sy zvYiP$53<4SxBH=0L^;cfkHk**xL=I%T7_cl<+STkGo9Q3&!VO4mXhm0V}!kZ#IDv$ zU5%sL<_E{x?Z+%^mrD;L@UQYCd(Sjm_c6gO2^Qtf*z}1+HL3`d^Z-*G!^KKjQXf_l z7`o6~>W!Ex%mO8attS9BCXXW>?k5OM#W7wB$+Fv;)#zuOXM!U(4+w_2p^w^2vn$pX zIRTWhEV74A^5Ba)1E1$S~hh@ zs5>H!B!^WTPanzrG96tJdM@YfTpIc$6G^JUgdry$Vw_+$vw?xdnc4^G8Q70 z5C((evytR!AjXFE2sdSD%Fg_6kjs?pojuGU#XF;(T% zH4`e_p@>*nf@WDpPfKID9R6th%Ko#NZ8t9!kc~M*C~rn7Z)RJEt;CKZqq2;|e0a7- zmBeR2YIM(VGI&vs)E04ATONuu5X&H{EI<&AhtNnZ8UrEz*Fg}E@dXb>QVE?$EVFkI ze7_&q9MeaZDr|YuiFLn|pdrXOFU+zMAC~+Et=fxZy?zR6Aut&uDNmQ>#=S|YEh3$z zZb_Xt0|qoB7BsVi6Y9p4PoWlwnm9zzdOF8A=;F0O)vtVWR{2OwPh{~pB#kmhXr(|N zM>G2o`q&@iOHCH~R&(v8Bq5Lteaybj*G5tndn}b4e$J6QrC1J|X}=@8Ja6I)ZW-gz z{=A7E@RRldB25L^AV=!pMaq?WCWE=hn=eytDNY@fV%h|x9Z_nov&fRf@6igZbTTtj4YXrbNkwVk`%e)ldr z(7ZIQmGXzt!bjbMx*oQ*Y+U4ynhvv zT@rcKc87v2T}IMZ@85YMQiI%6ncr~13WHihfvjJM&<(*|oG2P}^hI2RP*12T^^M%C zW!aRh+v-$ctUU0Tg>|TD++!TiZFBskRiyt2J&Iv1SNq!LC}EvHqfXkhVxp|5&kb|D zcu~LGg4p*uV^E}nHSdh9t{lAHBtx?4JLj|Z83HV59>RCkFe$H(xYE(dl2|xrq=;a7 zjuQUsO26vN&MK=Di>`N>*jc7=?@6;6``uk!OWp4v{;srS0BnpV^nmH}VW;W(B0*V3 z%@qYzNlB*N>`(6d-|bR&bL7b`-#@CT-#q_6BLUL?M*>t*m!IQ7`fQvptfltd(!{B_p$HHi8P)Y#)MH#pYRN+IxHuvJ{#MjgqP z^P>jS$IICp?2ou%L0bq!*?yJuUcxXk6laz72pxxqJstm8U7q*Z}j{7h1ZJ`OK>nZYwm@?!{B`-(j#ztt>c` zLs#rCXDa<($y}HYZ7niS3)GswORN!tj1r+ulOkyZ@*R_!fjgc|ZmiL&I)r8(QUWlh zlj3b^bXeMNse*Dtt|P9t1Cxu|n!YVmUU06j*JRiQcE2ybCb)>sm$Ul7&>^{1BweqiRmChkk0_P?ajQX zVb|0HxO_-35^E?ZFaB-%2*73@Ix%S(Ve;n4Fa|NEi|HV71d{O!5mF9}{^)ub`EU0? zSiQ2%f^l*oa-&>a+i@CEs&AqX>tzmUo zMkY_eZ_kY%nLNu%dHCES?|f1RpW~~oWJ-Gti~zQ37MJvJ?j{qa^1>d{gwA&=5U%cn zi;8AZ&W4NK%C-)%AakCp^*Zk%cR1@&uY8{ghJK>gjKYad0g|Q479Y{&4wlkI{r zaid9d|C=2W*Ml(>3pXgVR%tapr7C46sq^#gwbKhASw9wUEE@5)BC}O&u>rByVb9ra zC36&3o=w$!DY@B&HMI_pqr&5WVSckyl;fDHFS)w3UboCF!{%JQA;T1w^J+mD#vMm< z%C>m7HZsEWrRwuY82~IU;%SyL1DqEi2d(%#yy;yF``ZqA@NI`w<=fbHw))Es>3kAp zlXa=T%3)|?Gtl=pJ0xZhkcq~3*d^I_ke=y3Vuxh-Kb7q)b9_pMY1fXfAJ0n5mLEg`L+_ZX8S z8RTEv1FHg`k>?JJjzPDqDO?AHqA?ZhH$p(W0;_W_E7AIfJJjWEivw$ueFxi~DT!Ld zk5yhXwd@#-$1WS|pa)u2nKj4RLv$L+Bw`9x5MqH)>ZkUqme?*KQ}wwiHJzS>8Y-IM zUocvh?b@`hN+eOviMJWfl`+m?Q^(etxHwuBef)S4vK5C|q9~f|cp2$_V}xkcNe*lp zu1+~GK05gocny&-N)yc}5o10d)_Ws~9$1?=Y0kb*yUz+2X!ru9Ki2t_)=;Q9k!<*T z4?M^n1rLucWqaS-A5SEn+}U1lOCOA=s07v1HR^1u%KnW-c21d!q)3|~N1m2UY5q2) zdY`3z1a55e;K`+R2fe{z(T0-_Y)t?m@H2sFk;cLiDBP)vE}hqvR>S*cbh6>8*%ar= zwVv}>K6gnPx$7E&oWtlS*PxT&bdht%Ic1oQ@8TY@*tRZqlXMfgwZDtmT0!EH5sR)o zlaVFnj@D0$oT-}vg=(jiYsHG6qukQxSlTD)qSSj0iBLED^$QwH z5SXp+VzMbf_vV0RP9_!@@ z-BN4{VG-p5fkQTCg=8OE7*4yF!te9j8<jQjaJL)S&4LV}C6N=PLQ_LK2OzCRxLe1Yd1|nksE^QV z0s*k;S?OT^tDws`x_$PGB4i4GS(F|$c`24i&`9dnMvHKNfoI%a?PK2LMYGc%8{D~9 zU}Cz(M{pfcel{!U=x^}zRPR2=ukiJ~1AzCU7e8N3*|#zMuYiba0HMT8OAKipf_F4Z zx79fWj%NWwkslE|d9ECFFpw65c$xmNVD=+n4pJGa80~reEK#!J{b_4~8^Jp20U_=J zdxaR;rm)Y!mEFq)1!1MCnWHIljcdt0xRAa5@>9MJGub)Tw=s&qP5K{lFrP0<-lqUW|XT*6;X?zTzYH8JdS{Rlog?iW|p1{ zX6k3^WewjxPRfK#dt;|sRbOqrM5WI0^_Hl+th=pmV$7Kv=jA!`?FZIxVz=$?2Xd?J zr-=9E(;ms@6!j(-o8N9Nj%lD=@7)nNyTb3sdqWrR&3%4u>myNX&$i;&jk@er%RNyz zcSoA!myg|H`Dt#iu8UIGYXcST3g?g6NX)nW?yB3;XdhfwAMvOcJ3d*qXGiV9hg(-Y zF)n{lxr_Cvow@p>f>isoZO(YT&JAdzs*|-_zs1YDX?=F;SL7(JNx-f{vMyc3tnJ0P zb+^!+%*j6bXG-o&#T#~5Gqg7)j-d)dYHzIp9H4pNjU1aYc*JRdm|%Eyw8cGJ0xB9? zUecKo=M1FFlT$E()m$Hc_&G$`)MOk>^DxX1WH(YJoUjv40AT2|d;5pdO94`i1-MX> zL-6J_z6*HHnP2ok?T~pDgLdvQ5z8qx<%M`?7v*MALTi0Oin&F(|v}83FuN(aV@% zCvYU_Nn=h;BdI($o&lVbZDOZA^z+CkLo2qs{rQQR%JCWgwl}B@%v#vxT;9nfHkXj@ zxgadG8Y^^?HV1Cyah9cE_%Z$64N5us-(E~ny?3>|7)7$*rctJvI-~+F((h`Uv~U%2 zfx)>SWz+P^RT+q?tJdx_ok!?aFSRk@gO`$Qwh16su$U2`!u-g!x$Dad;lQ5m zM}tP)#TVmBMH{QT74+fJe{_~Y+!M$N6t^iMAB3bB6#|pP#=%-w2*ye_13vT^jTc%F z;J|{AQ&@Zrbf#ooZZK=cFu+zK5WqeK&_7tzXKh-H%uUR~kQo4K1&pxSld=E@osbk? zD63S+Gnm%2e^gUlP#e(v5UDdKhDqTf;`g$N<}a+okE1tgNbXD^ml+22IOHVeUe*Pg zilN;o7G7#-+-Co3Ee_&N)Ek&SP5W?ymC*VqR1P^#CyMVxu!?3|$WHo*Su zx#8kD70U?@&4{*|QYWHVL+{vofff#1&bS$Jkzh!?A9D?Z@IJ$$x%^K9cZ0Yfic z=tlwjPNkAlw>|1@`~Bka#>{$>&xd8Pf&r`!>RKY|+VTLCQ%yhx!ea`;T2H|@EtVwY z(s{V$L+fAosDJ(l)ovagWKNW6G&yiotu&d#?n7iuG@|Iz^pP;G>{S?k2KMDf)@MCg zXJ8YSf>@g{%epk10)L+a4?zdth|ntuD#-GH3+BsWS_4;V+P|NRqlY?pQ*{iuo~E_N z4g~9<%Tq?Qhe=2uv}yldAc|D^nDnUcVVSZA!t%?LqEW(hGXNas+=n>?etHP zBW8V-%eN!r&t>*R8{%G#W-pEr-!jOg4_=cdKhbKw&qZPH8_z=nPf*3C5XN2uGN?SY zWQv3)DM(EmO)&a8f9rZ$p?E;WU*{&e)xSFORV9fWOf)afr^)cTog~7zh-iJB{%Bfz z8O5%(!IotYrSyeYr0qM9Xwk`< zOh#1wlNVC-2eftEs;lx zoD|tra*~NuzLx5c$Lu(h7Y;o_XC}k@=5b7~&=mjm*_8OEHp6!~)W=2Z#@%o-EWm-Z zH`;c-U|6ylw&$iFVf_%Va;({|TSe=Db!C1|mXy}3PYUFl>g&IY@NguA6oYvmB$kh^x9*H2aL*|htZ zy2Jr|QqFH%vJGzLFkT_xUBTmBiR&d!mzoM3xZ(E@%}I}rCnN^3n{I-z{a8sY5Fnr1 z!Bd}#GeFk+*@-L4xphlx@6{a|JYsWMBua&>ozMOvv#>;cpx)q$>Eo9+SK z!D@Q&@_S_NT~whQ`}$9f9V!>@4GzM4SN^Twp@aGXhlgtBJelHn<{wWdZ64{?S9P1Z za{8VQOySBLNj7o%8(OJb{Rq0c0l^rdlIS$LCep{J=n_<+u z+37-9DWMmjH%5z^7Mh1i{TcTig;zT|!^OC9V1ct-h7?yn!&cG51BKpX+LL;S?1;}% zFD!NO#PL=T`O)RvU&)egBsVLg3T12|tP>3#CKP29brbSx)MC5MhiY|uE65xj^QO)w99M5vKtiiDvJ7)a|{>}xB8n5WGw2Rya|vcH4K_b0jlby3q1DE31;48 zDtFm&bcK5G?OLu@6JpREsJsmk?&znABu%XjYjri6LpLNwTi77&>`ES67h~^2h7JmO zE8P;1;`9ghpRPS$I$Tt~hUprF^0b5W{9xF5w9?w-g5UEa^3n%nZVM?jYirHLxuXS%S3`|T!vBL=WJGNC| zOlY*I^L6ebiZe2Rt92uYTlwm&dKNizh8HbqH)lU`X>Cj&0+fhu$2M6hsvHAjm$>{q z&9aw9n81H{5U!E|ZB=K@$f|B(SG#rmV$;Wu^^4?lu`18lm zuxMe0C^sX3a#7}XUNgj$S0FTVIdAt3#p3gP$p}mRy3B}3gD@J~f zRRUdqwj|~d&tkw*0XCtNHD*c&N8*#(J(ydsto)AYtdm>WuMY}c&Zg2=8iLZ7^u+`m zIJHoRo_GX!^_x{J&LHKxc@mpKk z=>GR)y?DvL`qBco9MhY@r7*?Fklys+e-C}9<}jju z!fBA>MYG^ezLWNs>jTvDY9PQoj!#bhb=9?Pb$WjS=s+GIAq*F0L?$=E;Z=*R^xn7DiP-Exf4Let)I{@(Vo zHgMonM)RxuogRZ{fRJYx#`P4@x<)IV+NYH4x&-;E!7=(tZ=Jwv9z;;;bR+E?=-k4p zAe#Y7uWc;z4$-&6aU9z*P#t5Ab;rDJ(`=3oTA^P@S$agNOGaY20)KS4FCoYu@)PQ< zZe>GE@$q0YTm@>R(6U7X%22rdW6d|H*C|}vvYs zmuS8NM|UBJ-_F8$*-TA1GRR%mx>(@&il?zkO5+Wbw6Wc#!};PA8yZ@JjU|Xi{0Z$s z@dD~`gJRCGeC`Y|1}C9t(~p~864Ii4(&>{dd~m2?*I)+I*JbdqW-wRdV>tY7`rGi4 z;9byuv3ti&Y0?lHGb9A7s1AJJc`ALzbm<)kQ?WyE*(#y=qkYPcxqw~eVxm_rd@q#u zS7sYG04aUwrQR2Y%D6%Up0f^gADlRa+=tq^lJ13?J|HxbZOYUrS@M&vNcNm|NPKx; zG{LPQ1XJ=6@%VEPQ3DjOzrl&95*@FV-t)MHXD|-~xM4(jZ6!zG< zv~&}_Dysn>;8Dz3%XyNO-u~$F^~(F}8L}Iz{Kw`H2J_j^aGi-LXpaF&jz2*z`$axL zI2-$iv^Xxk6DZ&B$ruxk$n9#Rzwu8f-6Ux@svUE1cLXh(KfIA?g;qniW+nMagdTmk z74Z=sYE9^sgXQ&~gmGjilZj@_m@i_qLv4CWImN4``YQ;XhG>?K_C?~)kvno{aD5Gn zksz~T1u#=Ii7q9d+2n>p!@r@E!2GyP4I~%H*7oDCj*%oylz2zfl$ff1OBI+HS~et? zk)n?|#LEpj+#(Oi$}=R>rTeNW>>8(BwNP{qqK!T7Hj zt}sXKuJiL&r7XMYDcgx?5v1b+@qcBN8UN8}=#y+XwzBBD7CSat9GTfPhN9oVuK1}q zcP~fAf(k=_j|EOYc0DJEpZ)uW6?4aD-kxD-f-*3IDu?blS8Ws8Vbus9oUo#{;^Yiu zt1d+9&g#~kbPvOq4#}VBd`u=LHfJZ$zaHw>9UWgY6+xf5bIXWb5Ee5CGqr1$FDrV` zF#YjHSO2eC)FqAR8`?^m{?_hcxDGG~&D+X!{JuP^?RjlL_YLlFwnwL@8lq&Vr!9DA z%7i5#qv}NQ_J+~snQ_*xt@y>nt%ucyx$CFUgIkJp`XZUrhLSVYUb?!mf@V<}KQ*Z( zn1E5sh4I_4wMMX*?~WGr3Yu^e$~Wb);Jk{BH^z*gJ&EJ0OpN=nm8)u%i}8hf_j|0* zJF0g3s)!pIcUJU9Ct=1w@BGHTTlVBta%G@d-O`D2C=VWDob1=rfX{qOMO0n9S2fqK z;TC_yWF&i!?*H5-U-vN>{gBNY7b0zc1hxXR#<7yCNII0`azM|NYfBpKncfHIsG4!M zfwfzL&>&V z7*U)uS#5{BrmqM?4CIe1y5>Au>|%LwdwS)qs)H%caiprGh4WgCJ*yX?i2 z=4(^VRwBhBON4bhBeckOg8gDcZEaH9#|xTf(;nPBvnQyq{m0j9X26x#_YH%0Dv}+4 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 z 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 -xRfhH%+XHHFyt>rU9{G+!of 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 CAKj<` literal 0 HcmV?d00001 From f5b5c717fc8f1e3e4c1eea57e3de77cdefa61488 Mon Sep 17 00:00:00 2001 From: mjohnsonengr Date: Thu, 8 Jan 2015 13:25:48 -0700 Subject: [PATCH 3/4] Replaced sanselan with Apache Commons Imaging. --- .../code/appengine/imageio/ImageIO.java | 19 +- .../imaging}/ColorTools.java | 507 +++--- .../commons/imaging/FormatCompliance.java | 157 ++ .../org/apache/commons/imaging/ImageDump.java | 99 ++ .../imaging/ImageFormat.java} | 71 +- .../apache/commons/imaging/ImageFormats.java | 53 + .../imaging}/ImageInfo.java | 735 ++++---- .../apache/commons/imaging/ImageParser.java | 987 +++++++++++ .../commons/imaging/ImageReadException.java | 34 + .../commons/imaging/ImageWriteException.java | 64 + .../org/apache/commons/imaging/Imaging.java | 1504 +++++++++++++++++ .../imaging/ImagingConstants.java} | 241 +-- .../commons/imaging/ImagingException.java | 33 + .../apache/commons/imaging/PixelDensity.java | 123 ++ .../imaging/color/ColorCieLab.java} | 68 +- .../imaging/color/ColorCieLch.java} | 68 +- .../imaging/color/ColorCieLuv.java} | 68 +- .../imaging/color/ColorCmy.java} | 68 +- .../imaging/color/ColorCmyk.java} | 71 +- .../imaging/color/ColorConversions.java | 752 +++++++++ .../imaging/color/ColorHsl.java} | 68 +- .../imaging/color/ColorHsv.java} | 68 +- .../imaging}/color/ColorHunterLab.java | 68 +- .../imaging/color/ColorXyz.java} | 68 +- .../commons/imaging/color/package-info.java | 21 + .../commons/imaging/common/BasicCParser.java | 379 +++++ .../imaging/common/BinaryConstant.java | 85 + .../imaging/common/BinaryFileParser.java | 76 + .../imaging/common/BinaryFunctions.java | 320 ++++ .../imaging/common/BinaryOutputStream.java | 122 ++ .../imaging/common/ByteConversions.java | 390 +++++ .../common/FastByteArrayOutputStream.java} | 115 +- .../common/IBufferedImageFactory.java | 57 +- .../imaging/common/IImageMetadata.java} | 63 +- .../commons/imaging/common/ImageBuilder.java | 212 +++ .../commons/imaging/common/ImageMetadata.java | 98 ++ .../commons/imaging/common/PackBits.java | 163 ++ .../imaging/common/RationalNumber.java | 222 +++ .../common/RgbBufferedImageFactory.java | 74 +- .../common/SimpleBufferedImageFactory.java | 79 +- .../common/bytesource}/ByteSource.java | 140 +- .../common/bytesource}/ByteSourceArray.java | 145 +- .../common/bytesource}/ByteSourceFile.java | 223 ++- .../bytesource/ByteSourceInputStream.java | 271 +++ .../common/bytesource/package-info.java | 22 + .../common/itu_t4/BitArrayOutputStream.java | 99 ++ .../itu_t4}/BitInputStreamFlexible.java | 222 ++- .../imaging/common/itu_t4/HuffmanTree.java | 90 + .../common/itu_t4/HuffmanTreeException.java} | 61 +- .../common/itu_t4/T4AndT6Compression.java | 754 +++++++++ .../imaging/common/itu_t4/T4_T6_Tables.java | 262 +++ .../imaging/common/itu_t4/package-info.java | 22 + .../common/mylzw/BitsToByteInputStream.java | 114 +- .../common/mylzw/MyBitInputStream.java | 97 ++ .../common/mylzw/MyBitOutputStream.java | 104 ++ .../imaging/common/mylzw/MyLzwCompressor.java | 262 +++ .../common/mylzw/MyLzwDecompressor.java} | 430 +++-- .../imaging/common/mylzw/package-info.java | 22 + .../commons/imaging/common/package-info.java | 23 + .../imaging/formats/bmp/BmpHeaderInfo.java | 113 ++ .../imaging/formats/bmp/BmpImageParser.java | 820 +++++++++ .../imaging/formats/bmp/BmpWriter.java} | 69 +- .../imaging/formats/bmp/BmpWriterPalette.java | 120 ++ .../imaging/formats/bmp/BmpWriterRgb.java | 82 + .../imaging}/formats/bmp/ImageContents.java | 70 +- .../imaging/formats/bmp/PixelParser.java | 57 + .../formats/bmp}/PixelParserBitFields.java | 241 ++- .../imaging/formats/bmp/PixelParserRgb.java | 117 ++ .../imaging/formats/bmp/PixelParserRle.java | 164 ++ .../formats/bmp}/PixelParserSimple.java | 101 +- .../imaging/formats/bmp/package-info.java | 22 + .../imaging/formats/dcx/DcxImageParser.java | 269 +++ .../imaging/formats/dcx/package-info.java | 22 + .../imaging/formats/gif/GenericGifBlock.java} | 106 +- .../imaging/formats/gif/GifBlock.java} | 52 +- .../imaging/formats/gif/GifHeaderInfo.java} | 120 +- .../imaging/formats/gif/GifImageParser.java | 1094 ++++++++++++ .../formats/gif/GraphicControlExtension.java | 80 +- .../imaging}/formats/gif/ImageContents.java | 68 +- .../imaging}/formats/gif/ImageDescriptor.java | 112 +- .../imaging/formats/gif/package-info.java | 22 + .../imaging/formats/icns/IcnsDecoder.java | 284 ++++ .../imaging/formats/icns/IcnsImageParser.java | 362 ++++ .../imaging/formats/icns/IcnsType.java | 196 +++ .../formats/icns/Rle24Compression.java | 67 + .../imaging/formats/icns/package-info.java | 22 + .../imaging/formats/ico/IcoImageParser.java | 830 +++++++++ .../imaging/formats/ico/package-info.java | 22 + .../imaging/formats/jpeg/JpegConstants.java | 153 ++ .../formats/jpeg/JpegImageMetadata.java | 241 +++ .../imaging/formats/jpeg/JpegImageParser.java | 1159 +++++++++++++ .../formats/jpeg/JpegPhotoshopMetadata.java | 101 +- .../imaging/formats/jpeg/JpegUtils.java | 189 +++ .../imaging/formats/jpeg/decoder/Block.java | 28 + .../imaging/formats/jpeg/decoder/Dct.java | 384 +++++ .../formats/jpeg/decoder/JpegDecoder.java | 445 +++++ .../formats/jpeg/decoder/JpegInputStream.java | 60 + .../formats/jpeg/decoder/YCbCrConverter.java | 117 ++ .../imaging/formats/jpeg/decoder/ZigZag.java | 44 + .../formats/jpeg/exif/ExifRewriter.java | 591 +++++++ .../imaging/formats/jpeg/iptc/IptcBlock.java} | 75 +- .../formats/jpeg/iptc/IptcConstants.java | 94 ++ .../imaging/formats/jpeg/iptc/IptcParser.java | 460 +++++ .../imaging/formats/jpeg/iptc/IptcRecord.java | 64 + .../imaging/formats/jpeg/iptc/IptcType.java | 25 + .../formats/jpeg/iptc/IptcTypeLookup.java | 40 + .../imaging/formats/jpeg/iptc/IptcTypes.java | 172 ++ .../formats/jpeg/iptc/JpegIptcRewriter.java | 497 +++--- .../formats/jpeg/iptc/PhotoshopApp13Data.java | 107 +- .../imaging/formats/jpeg/package-info.java | 22 + .../formats/jpeg/segments/App13Segment.java | 160 +- .../formats/jpeg/segments/App14Segment.java | 59 + .../formats/jpeg/segments/App2Segment.java | 88 + .../formats/jpeg/segments/AppnSegment.java} | 70 +- .../formats/jpeg/segments/ComSegment.java} | 112 +- .../formats/jpeg/segments/DhtSegment.java | 185 ++ .../formats/jpeg/segments/DqtSegment.java | 86 + .../formats/jpeg/segments/GenericSegment.java | 65 + .../formats/jpeg/segments/JfifSegment.java} | 164 +- .../formats/jpeg/segments/Segment.java | 146 ++ .../formats/jpeg/segments/SofnSegment.java | 111 ++ .../formats/jpeg/segments/SosSegment.java | 122 ++ .../formats/jpeg/segments/UnknownSegment.java | 77 +- .../formats/jpeg/xmp/JpegRewriter.java | 350 ++++ .../formats/jpeg/xmp/JpegXmpParser.java | 53 + .../formats/jpeg/xmp/JpegXmpRewriter.java | 423 +++-- .../commons/imaging/formats/package-info.java | 23 + .../imaging/formats/pcx/PcxConstants.java | 24 + .../imaging/formats/pcx/PcxImageParser.java | 559 ++++++ .../imaging/formats/pcx/PcxWriter.java | 407 +++++ .../imaging/formats/pcx/package-info.java | 22 + .../imaging}/formats/png/BitParser.java | 141 +- .../imaging/formats/png/ChunkType.java | 90 + .../imaging/formats/png/ColorType.java | 87 + .../imaging/formats/png/FilterType.java | 30 + .../imaging}/formats/png/GammaCorrection.java | 149 +- .../imaging/formats/png/InterlaceMethod.java | 38 + .../imaging/formats/png/PngConstants.java | 72 + .../imaging}/formats/png/PngCrc.java | 176 +- .../imaging}/formats/png/PngImageInfo.java | 100 +- .../imaging/formats/png/PngImageParser.java | 731 ++++++++ .../imaging}/formats/png/PngText.java | 134 +- .../imaging/formats/png/PngWriter.java | 666 ++++++++ .../imaging/formats/png/ScanExpediter.java | 215 +++ .../formats/png/ScanExpediterInterlaced.java | 92 + .../formats/png/ScanExpediterSimple.java | 60 + .../imaging/formats/png/chunks/PngChunk.java} | 130 +- .../formats/png/chunks/PngChunkGama.java} | 81 +- .../formats/png/chunks/PngChunkIccp.java | 70 + .../formats/png/chunks/PngChunkIdat.java | 24 + .../formats/png/chunks/PngChunkIhdr.java | 57 + .../formats/png/chunks/PngChunkItxt.java | 122 ++ .../formats/png/chunks/PngChunkPhys.java} | 83 +- .../formats/png/chunks/PngChunkPlte.java} | 166 +- .../formats/png/chunks/PngChunkText.java | 71 + .../formats/png/chunks/PngChunkZtxt.java | 82 + .../formats/png/chunks/PngTextChunk.java} | 72 +- .../imaging/formats/png/package-info.java | 22 + .../png/scanlinefilters/ScanlineFilter.java | 53 +- .../ScanlineFilterAverage.java | 52 + .../scanlinefilters/ScanlineFilterNone.java | 62 +- .../scanlinefilters/ScanlineFilterPaeth.java | 78 + .../scanlinefilters/ScanlineFilterSub.java | 49 + .../scanlinefilters/ScanlineFilterUp.java} | 98 +- .../TransparencyFilter.java | 70 +- .../TransparencyFilterGrayscale.java | 88 +- .../TransparencyFilterIndexedColor.java | 96 +- .../TransparencyFilterTrueColor.java | 52 + .../commons/imaging/formats/pnm/FileInfo.java | 120 ++ .../imaging/formats/pnm/PamFileInfo.java | 174 ++ .../imaging/formats/pnm/PamWriter.java | 76 + .../imaging/formats/pnm/PbmFileInfo.java} | 215 +-- .../imaging/formats/pnm/PbmWriter.java | 88 + .../imaging/formats/pnm/PgmFileInfo.java | 113 ++ .../imaging/formats/pnm/PgmWriter.java | 71 + .../imaging/formats/pnm/PnmConstants.java | 33 + .../imaging/formats/pnm/PnmImageParser.java | 382 +++++ .../imaging/formats/pnm/PnmWriter.java} | 74 +- .../imaging/formats/pnm/PpmFileInfo.java | 116 ++ .../imaging/formats/pnm/PpmWriter.java | 79 + .../formats/pnm/WhiteSpaceReader.java | 144 +- .../imaging/formats/pnm/package-info.java | 22 + .../imaging}/formats/psd/ImageContents.java | 140 +- .../formats/psd/ImageResourceBlock.java | 79 +- .../formats/psd/ImageResourceType.java | 141 ++ .../imaging/formats/psd/PsdHeaderInfo.java | 68 + .../imaging/formats/psd/PsdImageParser.java | 776 +++++++++ .../formats/psd/dataparsers/DataParser.java | 102 +- .../psd/dataparsers/DataParserBitmap.java | 92 +- .../psd/dataparsers/DataParserCmyk.java} | 93 +- .../psd/dataparsers/DataParserGrayscale.java | 79 +- .../psd/dataparsers/DataParserIndexed.java | 50 + .../psd/dataparsers/DataParserLab.java} | 82 +- .../psd/dataparsers/DataParserRgb.java | 41 + .../psd/dataparsers/DataParserStub.java | 67 +- .../psd/datareaders/CompressedDataReader.java | 92 + .../formats/psd/datareaders/DataReader.java | 86 +- .../datareaders/UncompressedDataReader.java | 74 + .../imaging/formats/psd/package-info.java | 22 + .../formats/rgbe/InfoHeaderReader.java} | 85 +- .../imaging/formats/rgbe/RgbeImageParser.java | 159 ++ .../imaging/formats/rgbe/RgbeInfo.java | 191 +++ .../imaging/formats/rgbe/package-info.java | 22 + .../imaging}/formats/tga/TgaConstants.java | 43 +- .../imaging/formats/tga/TgaImageParser.java | 246 +++ .../imaging}/formats/tiff/JpegImageData.java | 62 +- .../imaging}/formats/tiff/TiffContents.java | 219 ++- .../imaging/formats/tiff/TiffDirectory.java | 707 ++++++++ .../imaging}/formats/tiff/TiffElement.java | 150 +- .../imaging/formats/tiff/TiffField.java | 713 ++++++++ .../imaging}/formats/tiff/TiffHeader.java | 87 +- .../imaging/formats/tiff/TiffImageData.java | 176 ++ .../formats/tiff/TiffImageMetadata.java | 593 +++++++ .../imaging/formats/tiff/TiffImageParser.java | 737 ++++++++ .../imaging/formats/tiff/TiffReader.java | 513 ++++++ .../AdobePageMaker6TagConstants.java | 79 + .../constants/AdobePhotoshopTagConstants.java | 47 + .../AliasSketchbookProTagConstants.java | 39 + .../tiff/constants/AllTagConstants.java | 65 + .../tiff/constants/DcfTagConstants.java | 58 + .../tiff/constants/DngTagConstants.java | 503 ++++++ .../tiff/constants/ExifTagConstants.java | 620 +++++++ .../constants/GdalLibraryTagConstants.java | 46 + .../tiff/constants/GeoTiffTagConstants.java | 72 + .../tiff/constants/GpsTagConstants.java | 232 +++ .../tiff/constants/HylaFaxTagConstants.java | 50 + .../MicrosoftHdPhotoTagConstants.java | 246 +++ .../tiff/constants/MicrosoftTagConstants.java | 68 + .../MolecularDynamicsGelTagConstants.java | 77 + .../constants/OceScanjobTagConstants.java | 54 + .../tiff/constants/Rfc2301TagConstants.java | 126 ++ .../tiff/constants/TagConstantsUtils.java | 66 + .../tiff/constants/Tiff4TagConstants.java | 44 + .../formats/tiff/constants/TiffConstants.java | 75 + .../constants/TiffDirectoryConstants.java | 37 + .../tiff/constants/TiffDirectoryType.java | 64 + .../tiff/constants/TiffEpTagConstants.java | 147 ++ .../tiff/constants/TiffTagConstants.java | 474 ++++++ .../tiff/constants/WangTagConstants.java | 38 + .../tiff/datareaders/BitInputStream.java | 141 ++ .../formats/tiff/datareaders/DataReader.java | 204 +++ .../tiff/datareaders/DataReaderStrips.java | 296 ++++ .../tiff/datareaders/DataReaderTiled.java | 246 +++ .../formats/tiff/fieldtypes/FieldType.java | 120 ++ .../tiff/fieldtypes/FieldTypeAscii.java | 123 ++ .../tiff/fieldtypes/FieldTypeByte.java | 49 + .../tiff/fieldtypes/FieldTypeDouble.java | 60 + .../tiff/fieldtypes/FieldTypeFloat.java | 59 + .../tiff/fieldtypes/FieldTypeLong.java | 59 + .../tiff/fieldtypes/FieldTypeRational.java | 71 + .../tiff/fieldtypes/FieldTypeShort.java | 58 + .../imaging/formats/tiff/package-info.java | 25 + .../PhotometricInterpreter.java | 94 +- .../PhotometricInterpreterBiLevel.java | 115 +- .../PhotometricInterpreterCieLab.java | 42 + .../PhotometricInterpreterCmyk.java | 44 + .../PhotometricInterpreterLogLuv.java | 152 ++ .../PhotometricInterpreterPalette.java | 55 + .../PhotometricInterpreterRgb.java | 42 + .../PhotometricInterpreterYCbCr.java | 86 + .../formats/tiff/taginfos/TagInfo.java | 110 ++ .../formats/tiff/taginfos/TagInfoAny.java | 27 + .../formats/tiff/taginfos/TagInfoAscii.java | 73 + .../tiff/taginfos/TagInfoAsciiOrByte.java | 28 + .../tiff/taginfos/TagInfoAsciiOrRational.java | 28 + .../formats/tiff/taginfos/TagInfoByte.java | 43 + .../tiff/taginfos/TagInfoByteOrShort.java | 37 + .../tiff/taginfos/TagInfoDirectory.java | 29 + .../formats/tiff/taginfos/TagInfoDouble.java | 37 + .../formats/tiff/taginfos/TagInfoFloat.java | 37 + .../formats/tiff/taginfos/TagInfoGpsText.java | 187 ++ .../formats/tiff/taginfos/TagInfoLong.java | 42 + .../tiff/taginfos/TagInfoLongOrIFD.java | 42 + .../tiff/taginfos/TagInfoRational.java | 38 + .../formats/tiff/taginfos/TagInfoSByte.java} | 74 +- .../formats/tiff/taginfos/TagInfoSLong.java | 37 + .../tiff/taginfos/TagInfoSRational.java | 38 + .../formats/tiff/taginfos/TagInfoSShort.java | 37 + .../formats/tiff/taginfos/TagInfoShort.java | 37 + .../tiff/taginfos/TagInfoShortOrLong.java | 41 + .../TagInfoShortOrLongOrRational.java | 42 + .../tiff/taginfos/TagInfoShortOrRational.java | 38 + .../tiff/taginfos/TagInfoUndefined.java | 26 + .../formats/tiff/taginfos/TagInfoUnknown.java | 29 + .../tiff/taginfos/TagInfoXpString.java | 63 + .../formats/tiff/write/ImageDataOffsets.java | 85 +- .../tiff/write/TiffImageWriterBase.java | 595 +++++++ .../tiff/write/TiffImageWriterLossless.java | 342 ++++ .../tiff/write/TiffImageWriterLossy.java | 86 + .../tiff/write/TiffOutputDirectory.java | 637 +++++++ .../formats/tiff/write/TiffOutputField.java | 162 ++ .../formats/tiff/write/TiffOutputItem.java | 160 +- .../formats/tiff/write/TiffOutputSet.java | 290 ++++ .../formats/tiff/write/TiffOutputSummary.java | 178 +- .../imaging/formats/wbmp/WbmpImageParser.java | 294 ++++ .../imaging/formats/wbmp/package-info.java | 22 + .../imaging/formats/xbm/XbmImageParser.java | 413 +++++ .../imaging/formats/xbm/package-info.java | 22 + .../imaging/formats/xpm/XpmImageParser.java | 750 ++++++++ .../imaging/formats/xpm/package-info.java | 21 + .../imaging/icc}/CachingInputStream.java | 109 +- .../commons/imaging/icc/IccConstants.java | 27 + .../commons/imaging/icc/IccProfileInfo.java | 138 ++ .../commons/imaging/icc/IccProfileParser.java | 348 ++++ .../apache/commons/imaging/icc/IccTag.java | 131 ++ .../imaging/icc/IccTagDataType.java} | 58 +- .../commons/imaging/icc/IccTagDataTypes.java | 158 ++ .../commons/imaging/icc/IccTagType.java | 26 + .../imaging/icc/IccTagTypes.java} | 790 ++++----- .../commons/imaging/icc/package-info.java | 21 + .../apache/commons/imaging/package-info.java | 23 + .../imaging/palette/ColorComponent.java} | 66 +- .../imaging/palette/ColorCount.java} | 93 +- .../commons/imaging/palette/ColorGroup.java | 140 ++ .../imaging/palette/ColorGroupCut.java | 40 + .../imaging/palette/ColorSpaceSubset.java | 175 ++ .../commons/imaging/palette/Dithering.java | 117 ++ .../palette/MedianCutImplementation.java | 26 + .../MedianCutLongestAxisImplementation.java | 140 ++ ...anCutMostPopulatedBoxesImplementation.java | 162 ++ .../imaging/palette/MedianCutPalette.java} | 71 +- .../imaging/palette/MedianCutQuantizer.java | 151 ++ .../imaging}/palette/Palette.java | 87 +- .../imaging/palette/PaletteFactory.java | 531 ++++++ .../imaging/palette/QuantizedPalette.java | 72 + .../imaging/palette/SimplePalette.java | 50 + .../commons/imaging/palette/package-info.java | 21 + .../apache/commons/imaging/util/Debug.java | 308 ++++ .../apache/commons/imaging/util/IoUtils.java | 48 + .../commons/imaging/util/package-info.java | 21 + .../imageio/plugins/jpeg/JPEGImageWriter.java | 11 +- .../x/imageio/plugins/png/PNGImageWriter.java | 11 +- .../org/apache/sanselan/FormatCompliance.java | 167 -- .../java/org/apache/sanselan/ImageDump.java | 111 -- .../java/org/apache/sanselan/ImageFormat.java | 90 - .../java/org/apache/sanselan/ImageParser.java | 423 ----- .../java/org/apache/sanselan/Sanselan.java | 1413 ---------------- .../sanselan/color/ColorConversions.java | 918 ---------- .../sanselan/common/BinaryConstants.java | 32 - .../sanselan/common/BinaryFileFunctions.java | 1108 ------------ .../sanselan/common/BinaryFileParser.java | 124 -- .../sanselan/common/BinaryInputStream.java | 688 -------- .../sanselan/common/BinaryOutputStream.java | 176 -- .../sanselan/common/BitInputStream.java | 123 -- .../apache/sanselan/common/Compression.java | 61 - .../apache/sanselan/common/ImageMetadata.java | 107 -- .../org/apache/sanselan/common/PackBits.java | 170 -- .../sanselan/common/RationalNumber.java | 127 -- .../common/RationalNumberUtilities.java | 147 -- .../byteSources/ByteSourceInputStream.java | 232 --- .../common/mylzw/MyBitInputStream.java | 114 -- .../common/mylzw/MyBitOutputStream.java | 120 -- .../common/mylzw/MyLZWCompressor.java | 293 ---- .../sanselan/formats/bmp/BmpHeaderInfo.java | 72 - .../sanselan/formats/bmp/BmpImageParser.java | 723 -------- .../formats/bmp/pixelparsers/PixelParser.java | 63 - .../bmp/pixelparsers/PixelParserRgb.java | 128 -- .../bmp/pixelparsers/PixelParserRle.java | 212 --- .../formats/bmp/writers/BMPWriterPalette.java | 130 -- .../formats/bmp/writers/BMPWriterRGB.java | 86 - .../sanselan/formats/gif/GifImageParser.java | 1191 ------------- .../sanselan/formats/icns/IcnsDecoder.java | 540 ------ .../formats/icns/IcnsImageParser.java | 383 ----- .../sanselan/formats/icns/IcnsType.java | 231 --- .../formats/icns/Rle24Compression.java | 68 - .../sanselan/formats/ico/IcoImageParser.java | 793 --------- .../sanselan/formats/jpeg/JpegConstants.java | 145 -- .../formats/jpeg/JpegImageMetadata.java | 236 --- .../formats/jpeg/JpegImageParser.java | 1039 ------------ .../sanselan/formats/jpeg/JpegUtils.java | 210 --- .../jpeg/exifRewrite/ExifRewriter.java | 578 ------- .../formats/jpeg/iptc/IPTCConstants.java | 242 --- .../formats/jpeg/iptc/IPTCParser.java | 451 ----- .../formats/jpeg/iptc/IPTCRecord.java | 54 - .../formats/jpeg/segments/App2Segment.java | 79 - .../formats/jpeg/segments/GenericSegment.java | 63 - .../formats/jpeg/segments/SOFNSegment.java | 75 - .../formats/jpeg/segments/SOSSegment.java | 100 -- .../formats/jpeg/segments/Segment.java | 147 -- .../formats/jpeg/xmp/JpegRewriter.java | 385 ----- .../formats/jpeg/xmp/JpegXmpParser.java | 71 - .../sanselan/formats/png/PngConstants.java | 138 -- .../sanselan/formats/png/PngImageParser.java | 941 ----------- .../sanselan/formats/png/PngWriter.java | 629 ------- .../sanselan/formats/png/ScanExpediter.java | 249 --- .../formats/png/ScanExpediterInterlaced.java | 124 -- .../formats/png/ScanExpediterSimple.java | 66 - .../formats/png/chunks/PNGChunkIHDR.java | 53 - .../formats/png/chunks/PNGChunkiCCP.java | 79 - .../formats/png/chunks/PNGChunkiTXt.java | 129 -- .../formats/png/chunks/PNGChunkpHYs.java | 45 - .../formats/png/chunks/PNGChunktEXt.java | 73 - .../formats/png/chunks/PNGChunkzTXt.java | 82 - .../ScanlineFilterAverage.java | 53 - .../scanlinefilters/ScanlineFilterPaeth.java | 79 - .../scanlinefilters/ScanlineFilterSub.java | 50 - .../png/scanlinefilters/ScanlineFilterUp.java | 51 - .../apache/sanselan/formats/pnm/FileInfo.java | 111 -- .../sanselan/formats/pnm/PBMWriter.java | 95 -- .../sanselan/formats/pnm/PGMFileInfo.java | 92 - .../sanselan/formats/pnm/PGMWriter.java | 77 - .../sanselan/formats/pnm/PNMConstants.java | 32 - .../sanselan/formats/pnm/PNMImageParser.java | 365 ---- .../sanselan/formats/pnm/PPMFileInfo.java | 101 -- .../sanselan/formats/pnm/PPMWriter.java | 81 - .../formats/psd/ImageResourceType.java | 42 - .../sanselan/formats/psd/PSDConstants.java | 204 --- .../sanselan/formats/psd/PSDHeaderInfo.java | 70 - .../sanselan/formats/psd/PsdImageParser.java | 852 ---------- .../psd/dataparsers/DataParserIndexed.java | 56 - .../psd/dataparsers/DataParserLab.java | 57 - .../psd/datareaders/CompressedDataReader.java | 87 - .../datareaders/UncompressedDataReader.java | 68 - .../sanselan/formats/tga/TgaImageParser.java | 288 ---- .../sanselan/formats/tiff/TiffDirectory.java | 305 ---- .../sanselan/formats/tiff/TiffField.java | 796 --------- .../sanselan/formats/tiff/TiffImageData.java | 138 -- .../formats/tiff/TiffImageMetadata.java | 398 ----- .../formats/tiff/TiffImageParser.java | 592 ------- .../sanselan/formats/tiff/TiffReader.java | 505 ------ .../tiff/constants/AllTagConstants.java | 33 - .../tiff/constants/ExifTagConstants.java | 1477 ---------------- .../tiff/constants/GPSTagConstants.java | 221 --- .../tiff/constants/TagConstantsUtils.java | 48 - .../formats/tiff/constants/TagInfo.java | 447 ----- .../formats/tiff/constants/TiffConstants.java | 48 - .../constants/TiffDirectoryConstants.java | 124 -- .../constants/TiffFieldTypeConstants.java | 103 -- .../tiff/constants/TiffTagConstants.java | 349 ---- .../formats/tiff/datareaders/DataReader.java | 142 -- .../tiff/datareaders/DataReaderStrips.java | 106 -- .../tiff/datareaders/DataReaderTiled.java | 128 -- .../formats/tiff/fieldtypes/FieldType.java | 113 -- .../tiff/fieldtypes/FieldTypeASCII.java | 55 - .../tiff/fieldtypes/FieldTypeByte.java | 51 - .../tiff/fieldtypes/FieldTypeDouble.java | 58 - .../tiff/fieldtypes/FieldTypeFloat.java | 65 - .../tiff/fieldtypes/FieldTypeLong.java | 65 - .../tiff/fieldtypes/FieldTypeRational.java | 103 -- .../tiff/fieldtypes/FieldTypeShort.java | 80 - .../tiff/fieldtypes/FieldTypeUnknown.java | 58 - .../PhotometricInterpreterCIELAB.java | 50 - .../PhotometricInterpreterCMYK.java | 48 - .../PhotometricInterpreterLogLUV.java | 156 -- .../PhotometricInterpreterPalette.java | 56 - .../PhotometricInterpreterRGB.java | 46 - .../PhotometricInterpreterYCbCr.java | 89 - .../tiff/write/TiffImageWriterBase.java | 556 ------ .../tiff/write/TiffImageWriterLossless.java | 448 ----- .../tiff/write/TiffImageWriterLossy.java | 91 - .../tiff/write/TiffOutputDirectory.java | 317 ---- .../formats/tiff/write/TiffOutputField.java | 212 --- .../formats/tiff/write/TiffOutputSet.java | 333 ---- .../TransparencyFilterTrueColor.java | 58 - .../apache/sanselan/icc/IccProfileInfo.java | 154 -- .../apache/sanselan/icc/IccProfileParser.java | 383 ----- .../java/org/apache/sanselan/icc/IccTag.java | 128 -- .../org/apache/sanselan/icc/IccTagType.java | 31 - .../sanselan/palette/ColorSpaceSubset.java | 178 -- .../sanselan/palette/MedianCutQuantizer.java | 528 ------ .../sanselan/palette/PaletteFactory.java | 585 ------- .../sanselan/palette/QuantizedPalette.java | 86 - .../sanselan/palette/SimplePalette.java | 72 - .../java/org/apache/sanselan/util/Debug.java | 916 ---------- .../sanselan/util/DebugOutputStream.java | 66 - .../org/apache/sanselan/util/IOUtils.java | 269 --- .../apache/sanselan/util/UnicodeUtils.java | 464 ----- 467 files changed, 47410 insertions(+), 40055 deletions(-) rename src/main/java/org/apache/{sanselan => commons/imaging}/ColorTools.java (57%) create mode 100644 src/main/java/org/apache/commons/imaging/FormatCompliance.java create mode 100644 src/main/java/org/apache/commons/imaging/ImageDump.java rename src/main/java/org/apache/{sanselan/common/IImageMetadata.java => commons/imaging/ImageFormat.java} (67%) create mode 100644 src/main/java/org/apache/commons/imaging/ImageFormats.java rename src/main/java/org/apache/{sanselan => commons/imaging}/ImageInfo.java (75%) create mode 100644 src/main/java/org/apache/commons/imaging/ImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/ImageReadException.java create mode 100644 src/main/java/org/apache/commons/imaging/ImageWriteException.java create mode 100644 src/main/java/org/apache/commons/imaging/Imaging.java rename src/main/java/org/apache/{sanselan/SanselanConstants.java => commons/imaging/ImagingConstants.java} (58%) create mode 100644 src/main/java/org/apache/commons/imaging/ImagingException.java create mode 100644 src/main/java/org/apache/commons/imaging/PixelDensity.java rename src/main/java/org/apache/{sanselan/color/ColorCIELab.java => commons/imaging/color/ColorCieLab.java} (77%) rename src/main/java/org/apache/{sanselan/color/ColorCIELCH.java => commons/imaging/color/ColorCieLch.java} (77%) rename src/main/java/org/apache/{sanselan/color/ColorCIELuv.java => commons/imaging/color/ColorCieLuv.java} (77%) rename src/main/java/org/apache/{sanselan/color/ColorCMY.java => commons/imaging/color/ColorCmy.java} (77%) rename src/main/java/org/apache/{sanselan/color/ColorCMYK.java => commons/imaging/color/ColorCmyk.java} (75%) create mode 100644 src/main/java/org/apache/commons/imaging/color/ColorConversions.java rename src/main/java/org/apache/{sanselan/color/ColorHSL.java => commons/imaging/color/ColorHsl.java} (77%) rename src/main/java/org/apache/{sanselan/color/ColorHSV.java => commons/imaging/color/ColorHsv.java} (77%) rename src/main/java/org/apache/{sanselan => commons/imaging}/color/ColorHunterLab.java (76%) rename src/main/java/org/apache/{sanselan/color/ColorXYZ.java => commons/imaging/color/ColorXyz.java} (77%) create mode 100644 src/main/java/org/apache/commons/imaging/color/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/common/BasicCParser.java create mode 100644 src/main/java/org/apache/commons/imaging/common/BinaryConstant.java create mode 100644 src/main/java/org/apache/commons/imaging/common/BinaryFileParser.java create mode 100644 src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java create mode 100644 src/main/java/org/apache/commons/imaging/common/BinaryOutputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/common/ByteConversions.java rename src/main/java/org/apache/{sanselan/common/MyByteArrayOutputStream.java => commons/imaging/common/FastByteArrayOutputStream.java} (64%) rename src/main/java/org/apache/{sanselan => commons/imaging}/common/IBufferedImageFactory.java (78%) rename src/main/java/org/apache/{sanselan/ImageReadException.java => commons/imaging/common/IImageMetadata.java} (71%) create mode 100644 src/main/java/org/apache/commons/imaging/common/ImageBuilder.java create mode 100644 src/main/java/org/apache/commons/imaging/common/ImageMetadata.java create mode 100644 src/main/java/org/apache/commons/imaging/common/PackBits.java create mode 100644 src/main/java/org/apache/commons/imaging/common/RationalNumber.java rename src/main/java/org/apache/{sanselan => commons/imaging}/common/RgbBufferedImageFactory.java (78%) rename src/main/java/org/apache/{sanselan => commons/imaging}/common/SimpleBufferedImageFactory.java (77%) rename src/main/java/org/apache/{sanselan/common/byteSources => commons/imaging/common/bytesource}/ByteSource.java (52%) rename src/main/java/org/apache/{sanselan/common/byteSources => commons/imaging/common/bytesource}/ByteSourceArray.java (66%) rename src/main/java/org/apache/{sanselan/common/byteSources => commons/imaging/common/bytesource}/ByteSourceFile.java (50%) create mode 100644 src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceInputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/common/bytesource/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/common/itu_t4/BitArrayOutputStream.java rename src/main/java/org/apache/{sanselan/common => commons/imaging/common/itu_t4}/BitInputStreamFlexible.java (61%) create mode 100644 src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTree.java rename src/main/java/org/apache/{sanselan/ImageWriteException.java => commons/imaging/common/itu_t4/HuffmanTreeException.java} (70%) create mode 100644 src/main/java/org/apache/commons/imaging/common/itu_t4/T4AndT6Compression.java create mode 100644 src/main/java/org/apache/commons/imaging/common/itu_t4/T4_T6_Tables.java create mode 100644 src/main/java/org/apache/commons/imaging/common/itu_t4/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/common/mylzw/BitsToByteInputStream.java (67%) create mode 100644 src/main/java/org/apache/commons/imaging/common/mylzw/MyBitInputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/common/mylzw/MyBitOutputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwCompressor.java rename src/main/java/org/apache/{sanselan/common/mylzw/MyLZWDecompressor.java => commons/imaging/common/mylzw/MyLzwDecompressor.java} (53%) create mode 100644 src/main/java/org/apache/commons/imaging/common/mylzw/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/common/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/BmpHeaderInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/BmpImageParser.java rename src/main/java/org/apache/{sanselan/formats/bmp/writers/BMPWriter.java => commons/imaging/formats/bmp/BmpWriter.java} (79%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterPalette.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterRgb.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/bmp/ImageContents.java (65%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/PixelParser.java rename src/main/java/org/apache/{sanselan/formats/bmp/pixelparsers => commons/imaging/formats/bmp}/PixelParserBitFields.java (53%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRgb.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRle.java rename src/main/java/org/apache/{sanselan/formats/bmp/pixelparsers => commons/imaging/formats/bmp}/PixelParserSimple.java (53%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/bmp/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/dcx/package-info.java rename src/main/java/org/apache/{sanselan/formats/gif/GenericGIFBlock.java => commons/imaging/formats/gif/GenericGifBlock.java} (63%) rename src/main/java/org/apache/{sanselan/formats/gif/GIFBlock.java => commons/imaging/formats/gif/GifBlock.java} (87%) rename src/main/java/org/apache/{sanselan/formats/gif/GIFHeaderInfo.java => commons/imaging/formats/gif/GifHeaderInfo.java} (52%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/gif/GraphicControlExtension.java (80%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/gif/ImageContents.java (72%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/gif/ImageDescriptor.java (52%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/gif/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/icns/Rle24Compression.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/icns/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/ico/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/JpegConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageMetadata.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/JpegPhotoshopMetadata.java (58%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/JpegUtils.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Block.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Dct.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegInputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/YCbCrConverter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/ZigZag.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriter.java rename src/main/java/org/apache/{sanselan/formats/jpeg/iptc/IPTCBlock.java => commons/imaging/formats/jpeg/iptc/IptcBlock.java} (78%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcRecord.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypeLookup.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypes.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/iptc/JpegIptcRewriter.java (59%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/iptc/PhotoshopApp13Data.java (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/segments/App13Segment.java (58%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App14Segment.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App2Segment.java rename src/main/java/org/apache/{sanselan/icc/IccTagDataType.java => commons/imaging/formats/jpeg/segments/AppnSegment.java} (63%) rename src/main/java/org/apache/{sanselan/util/DebugInputStream.java => commons/imaging/formats/jpeg/segments/ComSegment.java} (51%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DhtSegment.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DqtSegment.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/GenericSegment.java rename src/main/java/org/apache/{sanselan/formats/jpeg/segments/JFIFSegment.java => commons/imaging/formats/jpeg/segments/JfifSegment.java} (68%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/Segment.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SofnSegment.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/segments/UnknownSegment.java (65%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegRewriter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/jpeg/xmp/JpegXmpRewriter.java (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pcx/PcxConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pcx/PcxImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pcx/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/BitParser.java (57%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/ColorType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/FilterType.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/GammaCorrection.java (58%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/InterlaceMethod.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/PngConstants.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/PngCrc.java (58%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/PngImageInfo.java (54%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/PngText.java (74%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterInterlaced.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java rename src/main/java/org/apache/{sanselan/formats/png/chunks/PNGChunk.java => commons/imaging/formats/png/chunks/PngChunk.java} (57%) rename src/main/java/org/apache/{sanselan/formats/png/chunks/PNGChunkgAMA.java => commons/imaging/formats/png/chunks/PngChunkGama.java} (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIdat.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIhdr.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java rename src/main/java/org/apache/{sanselan/common/ZLibUtils.java => commons/imaging/formats/png/chunks/PngChunkPhys.java} (51%) rename src/main/java/org/apache/{sanselan/formats/png/chunks/PNGChunkPLTE.java => commons/imaging/formats/png/chunks/PngChunkPlte.java} (60%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkText.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java rename src/main/java/org/apache/{sanselan/formats/png/chunks/PNGTextChunk.java => commons/imaging/formats/png/chunks/PngTextChunk.java} (71%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/scanlinefilters/ScanlineFilter.java (78%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterAverage.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/png/scanlinefilters/ScanlineFilterNone.java (67%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterPaeth.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterSub.java rename src/main/java/org/apache/{sanselan/util/CachingOutputStream.java => commons/imaging/formats/png/scanlinefilters/ScanlineFilterUp.java} (53%) rename src/main/java/org/apache/{sanselan/formats => commons/imaging/formats/png}/transparencyfilters/TransparencyFilter.java (78%) rename src/main/java/org/apache/{sanselan/formats => commons/imaging/formats/png}/transparencyfilters/TransparencyFilterGrayscale.java (64%) rename src/main/java/org/apache/{sanselan/formats => commons/imaging/formats/png}/transparencyfilters/TransparencyFilterIndexedColor.java (60%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterTrueColor.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/FileInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PamFileInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PamWriter.java rename src/main/java/org/apache/{sanselan/formats/pnm/PBMFileInfo.java => commons/imaging/formats/pnm/PbmFileInfo.java} (50%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PbmWriter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PgmFileInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PgmWriter.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PnmConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PnmImageParser.java rename src/main/java/org/apache/{sanselan/formats/pnm/PNMWriter.java => commons/imaging/formats/pnm/PnmWriter.java} (74%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PpmFileInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/PpmWriter.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/pnm/WhiteSpaceReader.java (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/pnm/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/ImageContents.java (70%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/ImageResourceBlock.java (65%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/PsdHeaderInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/PsdImageParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/dataparsers/DataParser.java (54%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/dataparsers/DataParserBitmap.java (59%) rename src/main/java/org/apache/{sanselan/formats/psd/dataparsers/DataParserCMYK.java => commons/imaging/formats/psd/dataparsers/DataParserCmyk.java} (70%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/dataparsers/DataParserGrayscale.java (57%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserIndexed.java rename src/main/java/org/apache/{sanselan/formats/psd/dataparsers/DataParserRGB.java => commons/imaging/formats/psd/dataparsers/DataParserLab.java} (56%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserRgb.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/dataparsers/DataParserStub.java (70%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/psd/datareaders/DataReader.java (68%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/psd/package-info.java rename src/main/java/org/apache/{sanselan/formats/jpeg/segments/APPNSegment.java => commons/imaging/formats/rgbe/InfoHeaderReader.java} (57%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/rgbe/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tga/TgaConstants.java (89%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tga/TgaImageParser.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/JpegImageData.java (76%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/TiffContents.java (51%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/TiffElement.java (56%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/TiffHeader.java (68%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageData.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageMetadata.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/TiffReader.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePageMaker6TagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePhotoshopTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/AliasSketchbookProTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/AllTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/DcfTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/DngTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/ExifTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/GdalLibraryTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/GeoTiffTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/GpsTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/HylaFaxTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftHdPhotoTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/MolecularDynamicsGelTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/OceScanjobTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/Rfc2301TagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TagConstantsUtils.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/Tiff4TagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffEpTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/constants/WangTagConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/BitInputStream.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReader.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldType.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeByte.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/package-info.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/photometricinterpreters/PhotometricInterpreter.java (56%) rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java (50%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCieLab.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCmyk.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLuv.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterRgb.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAny.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrByte.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrRational.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByte.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByteOrShort.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDirectory.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDouble.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoFloat.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLong.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLongOrIFD.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java rename src/main/java/org/apache/{sanselan/formats/jpeg/iptc/IPTCType.java => commons/imaging/formats/tiff/taginfos/TagInfoSByte.java} (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSLong.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSShort.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShort.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLong.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLongOrRational.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrRational.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUndefined.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUnknown.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoXpString.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/write/ImageDataOffsets.java (65%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossless.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossy.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputField.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/write/TiffOutputItem.java (59%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSet.java rename src/main/java/org/apache/{sanselan => commons/imaging}/formats/tiff/write/TiffOutputSummary.java (53%) create mode 100644 src/main/java/org/apache/commons/imaging/formats/wbmp/WbmpImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/wbmp/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/xbm/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/xpm/package-info.java rename src/main/java/org/apache/{sanselan/util => commons/imaging/icc}/CachingInputStream.java (74%) create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccConstants.java create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccProfileInfo.java create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccTag.java rename src/main/java/org/apache/{sanselan/formats/png/chunks/PNGChunkIDAT.java => commons/imaging/icc/IccTagDataType.java} (74%) create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccTagDataTypes.java create mode 100644 src/main/java/org/apache/commons/imaging/icc/IccTagType.java rename src/main/java/org/apache/{sanselan/icc/IccConstants.java => commons/imaging/icc/IccTagTypes.java} (61%) create mode 100644 src/main/java/org/apache/commons/imaging/icc/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/package-info.java rename src/main/java/org/apache/{sanselan/SanselanException.java => commons/imaging/palette/ColorComponent.java} (72%) rename src/main/java/org/apache/{sanselan/formats/jpeg/iptc/IPTCTypeLookup.java => commons/imaging/palette/ColorCount.java} (53%) create mode 100644 src/main/java/org/apache/commons/imaging/palette/ColorGroup.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/ColorGroupCut.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/ColorSpaceSubset.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/Dithering.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/MedianCutImplementation.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/MedianCutLongestAxisImplementation.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/MedianCutMostPopulatedBoxesImplementation.java rename src/main/java/org/apache/{sanselan/util/ParamMap.java => commons/imaging/palette/MedianCutPalette.java} (64%) create mode 100644 src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java rename src/main/java/org/apache/{sanselan => commons/imaging}/palette/Palette.java (53%) create mode 100644 src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/QuantizedPalette.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/SimplePalette.java create mode 100644 src/main/java/org/apache/commons/imaging/palette/package-info.java create mode 100644 src/main/java/org/apache/commons/imaging/util/Debug.java create mode 100644 src/main/java/org/apache/commons/imaging/util/IoUtils.java create mode 100644 src/main/java/org/apache/commons/imaging/util/package-info.java delete mode 100644 src/main/java/org/apache/sanselan/FormatCompliance.java delete mode 100644 src/main/java/org/apache/sanselan/ImageDump.java delete mode 100644 src/main/java/org/apache/sanselan/ImageFormat.java delete mode 100644 src/main/java/org/apache/sanselan/ImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/Sanselan.java delete mode 100644 src/main/java/org/apache/sanselan/color/ColorConversions.java delete mode 100644 src/main/java/org/apache/sanselan/common/BinaryConstants.java delete mode 100644 src/main/java/org/apache/sanselan/common/BinaryFileFunctions.java delete mode 100644 src/main/java/org/apache/sanselan/common/BinaryFileParser.java delete mode 100644 src/main/java/org/apache/sanselan/common/BinaryInputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/BinaryOutputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/BitInputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/Compression.java delete mode 100644 src/main/java/org/apache/sanselan/common/ImageMetadata.java delete mode 100644 src/main/java/org/apache/sanselan/common/PackBits.java delete mode 100644 src/main/java/org/apache/sanselan/common/RationalNumber.java delete mode 100644 src/main/java/org/apache/sanselan/common/RationalNumberUtilities.java delete mode 100644 src/main/java/org/apache/sanselan/common/byteSources/ByteSourceInputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/mylzw/MyBitInputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/mylzw/MyBitOutputStream.java delete mode 100644 src/main/java/org/apache/sanselan/common/mylzw/MyLZWCompressor.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/BmpHeaderInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/BmpImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRgb.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRle.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterPalette.java delete mode 100644 src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterRGB.java delete mode 100644 src/main/java/org/apache/sanselan/formats/gif/GifImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/icns/IcnsDecoder.java delete mode 100644 src/main/java/org/apache/sanselan/formats/icns/IcnsImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/icns/IcnsType.java delete mode 100644 src/main/java/org/apache/sanselan/formats/icns/Rle24Compression.java delete mode 100644 src/main/java/org/apache/sanselan/formats/ico/IcoImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/JpegConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/JpegImageMetadata.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/JpegImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/JpegUtils.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/exifRewrite/ExifRewriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCRecord.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/segments/App2Segment.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/segments/GenericSegment.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/segments/SOFNSegment.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/segments/SOSSegment.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/segments/Segment.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegRewriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/PngConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/PngImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/PngWriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/ScanExpediter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/ScanExpediterInterlaced.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/ScanExpediterSimple.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIHDR.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiCCP.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiTXt.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkpHYs.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunktEXt.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkzTXt.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterAverage.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterPaeth.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterSub.java delete mode 100644 src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterUp.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/FileInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PBMWriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PGMFileInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PGMWriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PNMConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PNMImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PPMFileInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/pnm/PPMWriter.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/ImageResourceType.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/PSDConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/PSDHeaderInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/PsdImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserIndexed.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserLab.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/datareaders/CompressedDataReader.java delete mode 100644 src/main/java/org/apache/sanselan/formats/psd/datareaders/UncompressedDataReader.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tga/TgaImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffDirectory.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffField.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffImageData.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffImageMetadata.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffImageParser.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/TiffReader.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/AllTagConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/ExifTagConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/GPSTagConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TagConstantsUtils.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TagInfo.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TiffConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TiffDirectoryConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TiffFieldTypeConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/constants/TiffTagConstants.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReader.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderStrips.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderTiled.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldType.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeASCII.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeByte.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeDouble.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeFloat.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeLong.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeRational.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeShort.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeUnknown.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCIELAB.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCMYK.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLUV.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterRGB.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterBase.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossless.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossy.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputDirectory.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputField.java delete mode 100644 src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSet.java delete mode 100644 src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterTrueColor.java delete mode 100644 src/main/java/org/apache/sanselan/icc/IccProfileInfo.java delete mode 100644 src/main/java/org/apache/sanselan/icc/IccProfileParser.java delete mode 100644 src/main/java/org/apache/sanselan/icc/IccTag.java delete mode 100644 src/main/java/org/apache/sanselan/icc/IccTagType.java delete mode 100644 src/main/java/org/apache/sanselan/palette/ColorSpaceSubset.java delete mode 100644 src/main/java/org/apache/sanselan/palette/MedianCutQuantizer.java delete mode 100644 src/main/java/org/apache/sanselan/palette/PaletteFactory.java delete mode 100644 src/main/java/org/apache/sanselan/palette/QuantizedPalette.java delete mode 100644 src/main/java/org/apache/sanselan/palette/SimplePalette.java delete mode 100644 src/main/java/org/apache/sanselan/util/Debug.java delete mode 100644 src/main/java/org/apache/sanselan/util/DebugOutputStream.java delete mode 100644 src/main/java/org/apache/sanselan/util/IOUtils.java delete mode 100644 src/main/java/org/apache/sanselan/util/UnicodeUtils.java diff --git a/src/main/java/com/google/code/appengine/imageio/ImageIO.java b/src/main/java/com/google/code/appengine/imageio/ImageIO.java index f1b5cd7..68c4533 100644 --- a/src/main/java/com/google/code/appengine/imageio/ImageIO.java +++ b/src/main/java/com/google/code/appengine/imageio/ImageIO.java @@ -22,31 +22,20 @@ import org.apache.harmony.luni.util.NotImplementedException; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.Arrays; import java.util.List; import java.net.URL; import org.apache.harmony.x.imageio.internal.nls.Messages; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.Sanselan; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceInputStream; -import org.apache.sanselan.formats.gif.GifImageParser; -import org.apache.sanselan.formats.png.PngImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.Imaging; import com.google.code.appengine.awt.image.BufferedImage; import com.google.code.appengine.awt.image.RenderedImage; -import com.google.code.appengine.imageio.ImageReader; -import com.google.code.appengine.imageio.ImageTranscoder; -import com.google.code.appengine.imageio.ImageTypeSpecifier; -import com.google.code.appengine.imageio.ImageWriter; import com.google.code.appengine.imageio.spi.*; import com.google.code.appengine.imageio.stream.ImageInputStream; import com.google.code.appengine.imageio.stream.ImageOutputStream; @@ -338,7 +327,7 @@ else if(name.endsWith(".gif")) { }*/ try { - return Sanselan.getBufferedImage(input); + return Imaging.getBufferedImage(input); } catch (ImageReadException e) { // TODO Auto-generated catch block throw new IOException(e); @@ -354,7 +343,7 @@ public static BufferedImage read(InputStream input) throws IOException { } try { - return Sanselan.getBufferedImage(input); + return Imaging.getBufferedImage(input); } catch (ImageReadException e) { // TODO Auto-generated catch block throw new IOException(e); diff --git a/src/main/java/org/apache/sanselan/ColorTools.java b/src/main/java/org/apache/commons/imaging/ColorTools.java similarity index 57% rename from src/main/java/org/apache/sanselan/ColorTools.java rename to src/main/java/org/apache/commons/imaging/ColorTools.java index 6541cdb..bdd340d 100644 --- a/src/main/java/org/apache/sanselan/ColorTools.java +++ b/src/main/java/org/apache/commons/imaging/ColorTools.java @@ -1,259 +1,248 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan; - -import java.io.File; -import java.io.IOException; - -import com.google.code.appengine.awt.RenderingHints; -import com.google.code.appengine.awt.color.ColorSpace; -import com.google.code.appengine.awt.color.ICC_ColorSpace; -import com.google.code.appengine.awt.color.ICC_Profile; -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.ColorConvertOp; -import com.google.code.appengine.awt.image.ColorModel; -import com.google.code.appengine.awt.image.ComponentColorModel; -import com.google.code.appengine.awt.image.DirectColorModel; -import com.google.code.appengine.awt.image.ImagingOpException; - - -/** - * This class is a mess and needs to be cleaned up. - */ -public class ColorTools { - - public BufferedImage correctImage(BufferedImage src, File file) - throws ImageReadException, IOException { - ICC_Profile icc = Sanselan.getICCProfile(file); - if (icc == null) - return src; - - ICC_ColorSpace cs = new ICC_ColorSpace(icc); - - BufferedImage dst = convertFromColorSpace(src, cs); - return dst; - } - - public BufferedImage relabelColorSpace(BufferedImage bi, ICC_Profile profile) - throws ImagingOpException { - ICC_ColorSpace cs = new ICC_ColorSpace(profile); - - return relabelColorSpace(bi, cs); - } - - public BufferedImage relabelColorSpace(BufferedImage bi, ColorSpace cs) - throws ImagingOpException { - // This does not do the conversion. It tries to relabel the - // BufferedImage - // with its actual (presumably correct) Colorspace. - // use this when the image is mislabeled, presumably having been - // wrongly assumed to be sRGB - - ColorModel cm = deriveColorModel(bi, cs); - - return relabelColorSpace(bi, cm); - - } - - public BufferedImage relabelColorSpace(BufferedImage bi, ColorModel cm) - throws ImagingOpException { - // This does not do the conversion. It tries to relabel the - // BufferedImage - // with its actual (presumably correct) Colorspace. - // use this when the image is mislabeled, presumably having been - // wrongly assumed to be sRGB - - BufferedImage result = new BufferedImage(cm, bi.getRaster(), false, - null); - - return result; - } - - public ColorModel deriveColorModel(BufferedImage bi, ColorSpace cs) - throws ImagingOpException { - // boolean hasAlpha = (bi.getAlphaRaster() != null); - return deriveColorModel(bi, cs, false); - } - - public ColorModel deriveColorModel(BufferedImage bi, ColorSpace cs, - boolean force_no_alpha) throws ImagingOpException { - return deriveColorModel(bi.getColorModel(), cs, force_no_alpha); - } - - public ColorModel deriveColorModel(ColorModel old_cm, ColorSpace cs, - boolean force_no_alpha) throws ImagingOpException { - - if (old_cm instanceof ComponentColorModel) { - ComponentColorModel ccm = (ComponentColorModel) old_cm; - // ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); - if (force_no_alpha) - return new ComponentColorModel(cs, false, false, - ComponentColorModel.OPAQUE, ccm.getTransferType()); - else - return new ComponentColorModel(cs, ccm.hasAlpha(), ccm - .isAlphaPremultiplied(), ccm.getTransparency(), ccm - .getTransferType()); - } else if (old_cm instanceof DirectColorModel) { - DirectColorModel dcm = (DirectColorModel) old_cm; - - int old_mask = dcm.getRedMask() | dcm.getGreenMask() - | dcm.getBlueMask() | dcm.getAlphaMask(); - - int old_bits = count_bits_in_mask(old_mask); - - return new DirectColorModel(cs, old_bits, dcm.getRedMask(), dcm - .getGreenMask(), dcm.getBlueMask(), dcm.getAlphaMask(), dcm - .isAlphaPremultiplied(), dcm.getTransferType()); - } - // else if (old_cm instanceof PackedColorModel) - // { - // PackedColorModel pcm = (PackedColorModel) old_cm; - // - // // int old_mask = dcm.getRedMask() | dcm.getGreenMask() - // // | dcm.getBlueMask() | dcm.getAlphaMask(); - // - // int old_masks[] = pcm.getMasks(); - // // System.out.println("old_mask: " + old_mask); - // int old_bits = count_bits_in_mask(old_masks); - // // System.out.println("old_bits: " + old_bits); - // - // // PackedColorModel(ColorSpace space, int bits, int rmask, int gmask, - // int bmask, int amask, boolean isAlphaPremultiplied, int trans, int - // transferType) - // cm = new PackedColorModel(cs, old_bits, pcm.getMasks(), - // - // pcm.isAlphaPremultiplied(), pcm.getTransparency(), pcm - // .getTransferType()); - // } - - throw new ImagingOpException("Could not clone unknown ColorModel Type."); - } - - private int count_bits_in_mask(int i) { - int count = 0; - while (i != 0) { - count += (i & 1); - // uses the unsigned version of java's right shift operator, - // so that left hand bits are zeroed. - i >>>= 1; - } - return count; - } - - public BufferedImage convertToColorSpace(BufferedImage bi, ColorSpace to) { - ColorSpace from = bi.getColorModel().getColorSpace(); - - RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - hints.put(RenderingHints.KEY_COLOR_RENDERING, - RenderingHints.VALUE_COLOR_RENDER_QUALITY); - hints.put(RenderingHints.KEY_DITHERING, - RenderingHints.VALUE_DITHER_ENABLE); - - ColorConvertOp op = new ColorConvertOp(from, to, hints); - - BufferedImage result = op.filter(bi, null); - - result = relabelColorSpace(result, to); - - return result; - } - - public BufferedImage convertTosRGB(BufferedImage bi) { - ColorSpace cs_sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); - - ColorModel srgbCM = ColorModel.getRGBdefault(); - cs_sRGB = srgbCM.getColorSpace(); - - return convertToColorSpace(bi, cs_sRGB); - } - - protected BufferedImage convertFromColorSpace(BufferedImage bi, - ColorSpace from) { - ColorSpace cs_sRGB; - - ColorModel srgbCM = ColorModel.getRGBdefault(); - cs_sRGB = srgbCM.getColorSpace(); - - return convertBetweenColorSpaces(bi, from, cs_sRGB); - - } - - public BufferedImage convertBetweenICCProfiles(BufferedImage bi, - ICC_Profile from, ICC_Profile to) { - ICC_ColorSpace cs_from = new ICC_ColorSpace(from); - ICC_ColorSpace cs_to = new ICC_ColorSpace(to); - - return convertBetweenColorSpaces(bi, cs_from, cs_to); - } - - public BufferedImage convertToICCProfile(BufferedImage bi, ICC_Profile to) { - ICC_ColorSpace cs_to = new ICC_ColorSpace(to); - - return convertToColorSpace(bi, cs_to); - } - - public BufferedImage convertBetweenColorSpacesX2(BufferedImage bi, - ColorSpace from, ColorSpace to) { - RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - hints.put(RenderingHints.KEY_COLOR_RENDERING, - RenderingHints.VALUE_COLOR_RENDER_QUALITY); - hints.put(RenderingHints.KEY_DITHERING, - RenderingHints.VALUE_DITHER_ENABLE); - - // bi = relabelColorSpace(bi, cs); - // dumpColorSpace("\tcs_sRGB", cs_sRGB); - // dumpColorSpace("\tColorModel.getRGBdefaultc", - // ColorModel.getRGBdefault().getColorSpace()); - - bi = relabelColorSpace(bi, from); - ColorConvertOp op = new ColorConvertOp(from, to, hints); - bi = op.filter(bi, null); - - bi = relabelColorSpace(bi, from); - - bi = op.filter(bi, null); - - bi = relabelColorSpace(bi, to); - - return bi; - - } - - public BufferedImage convertBetweenColorSpaces(BufferedImage bi, - ColorSpace from, ColorSpace to) { - RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - hints.put(RenderingHints.KEY_COLOR_RENDERING, - RenderingHints.VALUE_COLOR_RENDER_QUALITY); - hints.put(RenderingHints.KEY_DITHERING, - RenderingHints.VALUE_DITHER_ENABLE); - - ColorConvertOp op = new ColorConvertOp(from, to, hints); - - bi = relabelColorSpace(bi, from); - - BufferedImage result = op.filter(bi, null); - - result = relabelColorSpace(result, to); - - return result; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging; + +import com.google.code.appengine.awt.RenderingHints; +import com.google.code.appengine.awt.Transparency; +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.color.ICC_ColorSpace; +import com.google.code.appengine.awt.color.ICC_Profile; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorConvertOp; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.ComponentColorModel; +import com.google.code.appengine.awt.image.DirectColorModel; +import com.google.code.appengine.awt.image.ImagingOpException; +import java.io.File; +import java.io.IOException; + +/** + * A selection of tools for evaluating and manipulating color + * spaces, color values, etc. + *

The Javadoc provided in the original code gave the + * following notation:

+ * + *     "This class is a mess and needs to be cleaned up." + */ +public class ColorTools { + + public BufferedImage correctImage(final BufferedImage src, final File file) + throws ImageReadException, IOException { + final ICC_Profile icc = Imaging.getICCProfile(file); + if (icc == null) { + return src; + } + + final ICC_ColorSpace cs = new ICC_ColorSpace(icc); + + return convertFromColorSpace(src, cs); + } + + public BufferedImage relabelColorSpace(final BufferedImage bi, final ICC_Profile profile) + throws ImagingOpException { + final ICC_ColorSpace cs = new ICC_ColorSpace(profile); + + return relabelColorSpace(bi, cs); + } + + public BufferedImage relabelColorSpace(final BufferedImage bi, final ColorSpace cs) + throws ImagingOpException { + // This does not do the conversion. It tries to relabel the + // BufferedImage + // with its actual (presumably correct) Colorspace. + // use this when the image is mislabeled, presumably having been + // wrongly assumed to be sRGB + + final ColorModel cm = deriveColorModel(bi, cs); + + return relabelColorSpace(bi, cm); + + } + + public BufferedImage relabelColorSpace(final BufferedImage bi, final ColorModel cm) + throws ImagingOpException { + // This does not do the conversion. It tries to relabel the + // BufferedImage + // with its actual (presumably correct) Colorspace. + // use this when the image is mislabeled, presumably having been + // wrongly assumed to be sRGB + + return new BufferedImage(cm, bi.getRaster(), false, null); + } + + public ColorModel deriveColorModel(final BufferedImage bi, final ColorSpace cs) + throws ImagingOpException { + // boolean hasAlpha = (bi.getAlphaRaster() != null); + return deriveColorModel(bi, cs, false); + } + + public ColorModel deriveColorModel(final BufferedImage bi, final ColorSpace cs, + final boolean forceNoAlpha) throws ImagingOpException { + return deriveColorModel(bi.getColorModel(), cs, forceNoAlpha); + } + + public ColorModel deriveColorModel(final ColorModel colorModel, final ColorSpace cs, + final boolean forceNoAlpha) throws ImagingOpException { + + if (colorModel instanceof ComponentColorModel) { + final ComponentColorModel ccm = (ComponentColorModel) colorModel; + // ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + if (forceNoAlpha) { + return new ComponentColorModel(cs, false, false, + Transparency.OPAQUE, ccm.getTransferType()); + } + return new ComponentColorModel(cs, ccm.hasAlpha(), + ccm.isAlphaPremultiplied(), ccm.getTransparency(), + ccm.getTransferType()); + } else if (colorModel instanceof DirectColorModel) { + final DirectColorModel dcm = (DirectColorModel) colorModel; + + final int oldMask = dcm.getRedMask() | dcm.getGreenMask() + | dcm.getBlueMask() | dcm.getAlphaMask(); + + final int oldBits = countBitsInMask(oldMask); + + return new DirectColorModel(cs, oldBits, dcm.getRedMask(), + dcm.getGreenMask(), dcm.getBlueMask(), dcm.getAlphaMask(), + dcm.isAlphaPremultiplied(), dcm.getTransferType()); + } + // else if (old_cm instanceof PackedColorModel) + // { + // PackedColorModel pcm = (PackedColorModel) old_cm; + // + // // int old_mask = dcm.getRedMask() | dcm.getGreenMask() + // // | dcm.getBlueMask() | dcm.getAlphaMask(); + // + // int old_masks[] = pcm.getMasks(); + // // System.out.println("old_mask: " + old_mask); + // int old_bits = countBitsInMask(old_masks); + // // System.out.println("old_bits: " + old_bits); + // + // // PackedColorModel(ColorSpace space, int bits, int rmask, int gmask, + // int bmask, int amask, boolean isAlphaPremultiplied, int trans, int + // transferType) + // cm = new PackedColorModel(cs, old_bits, pcm.getMasks(), + // + // pcm.isAlphaPremultiplied(), pcm.getTransparency(), pcm + // .getTransferType()); + // } + + throw new ImagingOpException("Could not clone unknown ColorModel Type."); + } + + private int countBitsInMask(int i) { + int count = 0; + while (i != 0) { + count += (i & 1); + // uses the unsigned version of java's right shift operator, + // so that left hand bits are zeroed. + i >>>= 1; + } + return count; + } + + public BufferedImage convertToColorSpace(final BufferedImage bi, final ColorSpace to) { + final ColorSpace from = bi.getColorModel().getColorSpace(); + + final RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + hints.put(RenderingHints.KEY_COLOR_RENDERING, + RenderingHints.VALUE_COLOR_RENDER_QUALITY); + hints.put(RenderingHints.KEY_DITHERING, + RenderingHints.VALUE_DITHER_ENABLE); + + final ColorConvertOp op = new ColorConvertOp(from, to, hints); + + BufferedImage result = op.filter(bi, null); + + result = relabelColorSpace(result, to); + + return result; + } + + public BufferedImage convertTosRGB(final BufferedImage bi) { + final ColorModel srgbCM = ColorModel.getRGBdefault(); + return convertToColorSpace(bi, srgbCM.getColorSpace()); + } + + protected BufferedImage convertFromColorSpace(final BufferedImage bi, final ColorSpace from) { + final ColorModel srgbCM = ColorModel.getRGBdefault(); + return convertBetweenColorSpaces(bi, from, srgbCM.getColorSpace()); + } + + public BufferedImage convertBetweenICCProfiles(BufferedImage bi, ICC_Profile from, ICC_Profile to) { + final ICC_ColorSpace csFrom = new ICC_ColorSpace(from); + final ICC_ColorSpace csTo = new ICC_ColorSpace(to); + + return convertBetweenColorSpaces(bi, csFrom, csTo); + } + + public BufferedImage convertToICCProfile(final BufferedImage bi, final ICC_Profile to) { + final ICC_ColorSpace csTo = new ICC_ColorSpace(to); + return convertToColorSpace(bi, csTo); + } + + public BufferedImage convertBetweenColorSpacesX2(BufferedImage bi, + final ColorSpace from, final ColorSpace to) { + final RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + hints.put(RenderingHints.KEY_COLOR_RENDERING, + RenderingHints.VALUE_COLOR_RENDER_QUALITY); + hints.put(RenderingHints.KEY_DITHERING, + RenderingHints.VALUE_DITHER_ENABLE); + + // bi = relabelColorSpace(bi, cs); + // dumpColorSpace("\tcs_sRGB", cs_sRGB); + // dumpColorSpace("\tColorModel.getRGBdefaultc", + // ColorModel.getRGBdefault().getColorSpace()); + + bi = relabelColorSpace(bi, from); + final ColorConvertOp op = new ColorConvertOp(from, to, hints); + bi = op.filter(bi, null); + + bi = relabelColorSpace(bi, from); + + bi = op.filter(bi, null); + + bi = relabelColorSpace(bi, to); + + return bi; + + } + + public BufferedImage convertBetweenColorSpaces(BufferedImage bi, + final ColorSpace from, final ColorSpace to) { + final RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + hints.put(RenderingHints.KEY_COLOR_RENDERING, + RenderingHints.VALUE_COLOR_RENDER_QUALITY); + hints.put(RenderingHints.KEY_DITHERING, + RenderingHints.VALUE_DITHER_ENABLE); + + final ColorConvertOp op = new ColorConvertOp(from, to, hints); + + bi = relabelColorSpace(bi, from); + + BufferedImage result = op.filter(bi, null); + + result = relabelColorSpace(result, to); + + return result; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/FormatCompliance.java b/src/main/java/org/apache/commons/imaging/FormatCompliance.java new file mode 100644 index 0000000..914d4ab --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/FormatCompliance.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides information about the compliance of a specified data + * source (byte array, file, etc.) to an image format. + */ +public class FormatCompliance { + private final boolean failOnError; + private final String description; + private final List comments = new ArrayList(); + + public FormatCompliance(final String description) { + this.description = description; + failOnError = false; + } + + public FormatCompliance(final String description, final boolean failOnError) { + this.description = description; + this.failOnError = failOnError; + } + + public static FormatCompliance getDefault() { + return new FormatCompliance("ignore", false); + } + + public void addComment(final String comment) throws ImageReadException { + comments.add(comment); + if (failOnError) { + throw new ImageReadException(comment); + } + } + + public void addComment(final String comment, final int value) throws ImageReadException { + addComment(comment + ": " + getValueDescription(value)); + } + + @Override + public String toString() { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + + dump(pw); + + return sw.getBuffer().toString(); + } + + public void dump() { + dump(new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))); + } + + public void dump(final PrintWriter pw) { + pw.println("Format Compliance: " + description); + + if (comments.isEmpty()) { + pw.println("\t" + "No comments."); + } else { + for (int i = 0; i < comments.size(); i++) { + pw.println("\t" + (i + 1) + ": " + comments.get(i)); + } + } + pw.println(""); + pw.flush(); + } + + private String getValueDescription(final int value) { + return value + " (" + Integer.toHexString(value) + ")"; + } + + public boolean compareBytes(final String name, final byte[] expected, final byte[] actual) + throws ImageReadException { + if (expected.length != actual.length) { + addComment(name + ": " + "Unexpected length: (expected: " + + expected.length + ", actual: " + actual.length + ")"); + return false; + } + for (int i = 0; i < expected.length; i++) { + // System.out.println("expected: " + // + getValueDescription(expected[i]) + ", actual: " + // + getValueDescription(actual[i]) + ")"); + if (expected[i] != actual[i]) { + addComment(name + ": " + "Unexpected value: (expected: " + + getValueDescription(expected[i]) + ", actual: " + + getValueDescription(actual[i]) + ")"); + return false; + } + } + + return true; + } + + public boolean checkBounds(final String name, final int min, final int max, final int actual) + throws ImageReadException { + if ((actual < min) || (actual > max)) { + addComment(name + ": " + "bounds check: " + min + " <= " + actual + + " <= " + max + ": false"); + return false; + } + + return true; + } + + public boolean compare(final String name, final int valid, final int actual) + throws ImageReadException { + return compare(name, new int[] { valid, }, actual); + } + + public boolean compare(final String name, final int[] valid, final int actual) + throws ImageReadException { + for (final int element : valid) { + if (actual == element) { + return true; + } + } + + final StringBuilder result = new StringBuilder(43); + result.append(name); + result.append(": Unexpected value: (valid: "); + if (valid.length > 1) { + result.append('{'); + } + for (int i = 0; i < valid.length; i++) { + if (i > 0) { + result.append(", "); + } + result.append(getValueDescription(valid[i])); + } + if (valid.length > 1) { + result.append('}'); + } + result.append(", actual: " + getValueDescription(actual) + ")"); + addComment(result.toString()); + return false; + } +} diff --git a/src/main/java/org/apache/commons/imaging/ImageDump.java b/src/main/java/org/apache/commons/imaging/ImageDump.java new file mode 100644 index 0000000..4262540 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImageDump.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.color.ICC_ColorSpace; +import com.google.code.appengine.awt.color.ICC_Profile; +import com.google.code.appengine.awt.image.BufferedImage; + +import org.apache.commons.imaging.icc.IccProfileInfo; +import org.apache.commons.imaging.icc.IccProfileParser; + +/** + * Used to store metadata and descriptive information extracted from + * image files. + */ +public class ImageDump { + private String colorSpaceTypeToName(final ColorSpace cs) { + // System.out.println(prefix + ": " + "type: " + // + cs.getType() ); + switch (cs.getType()) { + case ColorSpace.TYPE_CMYK: + return "TYPE_CMYK"; + case ColorSpace.TYPE_RGB: + return "TYPE_RGB"; + + case ColorSpace.CS_sRGB: + return "CS_sRGB"; + case ColorSpace.CS_GRAY: + return "CS_GRAY"; + case ColorSpace.CS_CIEXYZ: + return "CS_CIEXYZ"; + case ColorSpace.CS_LINEAR_RGB: + return "CS_LINEAR_RGB"; + case ColorSpace.CS_PYCC: + return "CS_PYCC"; + default: + return "unknown"; + } + } + + public void dumpColorSpace(final String prefix, final ColorSpace cs) { + System.out.println(prefix + ": " + "type: " + cs.getType() + " (" + + colorSpaceTypeToName(cs) + ")"); + + if (!(cs instanceof ICC_ColorSpace)) { + System.out.println(prefix + ": " + "Unknown ColorSpace: " + + cs.getClass().getName()); + return; + } + + final ICC_ColorSpace iccColorSpace = (ICC_ColorSpace) cs; + final ICC_Profile iccProfile = iccColorSpace.getProfile(); + + final byte[] bytes = iccProfile.getData(); + + final IccProfileParser parser = new IccProfileParser(); + + final IccProfileInfo info = parser.getICCProfileInfo(bytes); + info.dump(prefix); + } + + public void dump(final BufferedImage src) { + dump("", src); + } + + public void dump(final String prefix, final BufferedImage src) { + System.out.println(prefix + ": " + "dump"); + dumpColorSpace(prefix, src.getColorModel().getColorSpace()); + dumpBIProps(prefix, src); + } + + public void dumpBIProps(final String prefix, final BufferedImage src) { + final String[] keys = src.getPropertyNames(); + if (keys == null) { + System.out.println(prefix + ": no props"); + return; + } + for (final String key : keys) { + System.out.println(prefix + ": " + key + ": " + + src.getProperty(key)); + } + } + +} diff --git a/src/main/java/org/apache/sanselan/common/IImageMetadata.java b/src/main/java/org/apache/commons/imaging/ImageFormat.java similarity index 67% rename from src/main/java/org/apache/sanselan/common/IImageMetadata.java rename to src/main/java/org/apache/commons/imaging/ImageFormat.java index c907caa..0e0c322 100644 --- a/src/main/java/org/apache/sanselan/common/IImageMetadata.java +++ b/src/main/java/org/apache/commons/imaging/ImageFormat.java @@ -1,33 +1,38 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.util.ArrayList; - -public interface IImageMetadata -{ - public String toString(String prefix); - - public ArrayList getItems(); - - public interface IImageMetadataItem - { - public String toString(String prefix); - - public String toString(); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * Simple image format interface. + */ +public interface ImageFormat { + + /** + * Get the name of this {@link ImageFormat}. + * + * @return String name + */ + String getName(); + + /** + * Get the file extension associated with this {@link ImageFormat}. + * + * @return String extension + */ + String getExtension(); + +} diff --git a/src/main/java/org/apache/commons/imaging/ImageFormats.java b/src/main/java/org/apache/commons/imaging/ImageFormats.java new file mode 100644 index 0000000..4032f52 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImageFormats.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * Enum of known image formats. + */ +public enum ImageFormats implements ImageFormat { + UNKNOWN, + BMP, + DCX, + GIF, + ICNS, + ICO, + JBIG2, + JPEG, + PAM, + PSD, + PBM, + PGM, + PNM, + PPM, + PCX, + PNG, + RGBE, + TGA, + TIFF, + WBMP, + XBM, + XPM; + + public String getName() { + return name(); + } + + public String getExtension() { + return name(); + } +} diff --git a/src/main/java/org/apache/sanselan/ImageInfo.java b/src/main/java/org/apache/commons/imaging/ImageInfo.java similarity index 75% rename from src/main/java/org/apache/sanselan/ImageInfo.java rename to src/main/java/org/apache/commons/imaging/ImageInfo.java index a8724e5..f19ad10 100644 --- a/src/main/java/org/apache/sanselan/ImageInfo.java +++ b/src/main/java/org/apache/commons/imaging/ImageInfo.java @@ -1,374 +1,361 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; - -/** - * ImageInfo represents a collection of basic properties of an image, such as - * width, height, format, bit depth, etc. - */ -public class ImageInfo -{ - private final String formatDetails; // ie version - - private final int bitsPerPixel; - private final ArrayList comments; - - private final ImageFormat format; - private final String formatName; - private final int height; - private final String mimeType; - - private final int numberOfImages; - private final int physicalHeightDpi; - private final float physicalHeightInch; - private final int physicalWidthDpi; - private final float physicalWidthInch; - private final int width; - private final boolean isProgressive; - private final boolean isTransparent; - - private final boolean usesPalette; - - public static final int COLOR_TYPE_BW = 0; - public static final int COLOR_TYPE_GRAYSCALE = 1; - public static final int COLOR_TYPE_RGB = 2; - public static final int COLOR_TYPE_CMYK = 3; - public static final int COLOR_TYPE_OTHER = -1; - public static final int COLOR_TYPE_UNKNOWN = -2; - - private final int colorType; - - public static final String COMPRESSION_ALGORITHM_UNKNOWN = "Unknown"; - public static final String COMPRESSION_ALGORITHM_NONE = "None"; - public static final String COMPRESSION_ALGORITHM_LZW = "LZW"; - public static final String COMPRESSION_ALGORITHM_PACKBITS = "PackBits"; - public static final String COMPRESSION_ALGORITHM_JPEG = "JPEG"; - public static final String COMPRESSION_ALGORITHM_RLE = "RLE: Run-Length Encoding"; - public static final String COMPRESSION_ALGORITHM_PSD = "Photoshop"; - public static final String COMPRESSION_ALGORITHM_PNG_FILTER = "PNG Filter"; - public static final String COMPRESSION_ALGORITHM_CCITT_GROUP_3 = "CCITT Group 3 1-Dimensional Modified Huffman run-length encoding."; - public static final String COMPRESSION_ALGORITHM_CCITT_GROUP_4 = "CCITT Group 4"; - public static final String COMPRESSION_ALGORITHM_CCITT_1D = "CCITT 1D"; - - private final String compressionAlgorithm; - - public ImageInfo(String formatDetails, int bitsPerPixel, - ArrayList comments, ImageFormat format, String formatName, - int height, String mimeType, int numberOfImages, - int physicalHeightDpi, float physicalHeightInch, - int physicalWidthDpi, float physicalWidthInch, int width, - boolean isProgressive, boolean isTransparent, boolean usesPalette, - int colorType, String compressionAlgorithm) - { - this.formatDetails = formatDetails; - - this.bitsPerPixel = bitsPerPixel; - this.comments = comments; - - this.format = format; - this.formatName = formatName; - this.height = height; - this.mimeType = mimeType; - - this.numberOfImages = numberOfImages; - this.physicalHeightDpi = physicalHeightDpi; - this.physicalHeightInch = physicalHeightInch; - this.physicalWidthDpi = physicalWidthDpi; - this.physicalWidthInch = physicalWidthInch; - this.width = width; - this.isProgressive = isProgressive; - - this.isTransparent = isTransparent; - this.usesPalette = usesPalette; - - this.colorType = colorType; - this.compressionAlgorithm = compressionAlgorithm; - } - - /** - * Returns the bits per pixel of the image data. - */ - public int getBitsPerPixel() - { - return bitsPerPixel; - } - - /** - * Returns a list of comments from the image file.

This is mostly - * obsolete. - */ - public ArrayList getComments() - { - return new ArrayList(comments); - } - - /** - * Returns the image file format, ie. ImageFormat.IMAGE_FORMAT_PNG.

- * Returns ImageFormat.IMAGE_FORMAT_UNKNOWN if format is unknown. - * - * @return A constant defined in ImageFormat. - * @see ImageFormat - */ - public ImageFormat getFormat() - { - return format; - } - - /** - * Returns a string with the name of the image file format. - * - * @see #getFormat() - */ - public String getFormatName() - { - return formatName; - } - - /** - * Returns the height of the image in pixels. - * - * @see #getWidth() - */ - public int getHeight() - { - return height; - } - - /** - * Returns the MIME type of the image. - * - * @see #getFormat() - */ - public String getMimeType() - { - return mimeType; - } - - /** - * Returns the number of images in the file. - *

- * Applies mostly to GIF and TIFF; reading PSD/Photoshop layers is not - * supported, and Jpeg/JFIF EXIF thumbnails are not included in this count. - */ - public int getNumberOfImages() - { - return numberOfImages; - } - - /** - * Returns horizontal dpi of the image, if available. - *

- * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg - * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: - * 72). - * - * @return returns -1 if not present. - */ - public int getPhysicalHeightDpi() - { - return physicalHeightDpi; - } - - /** - * Returns physical height of the image in inches, if available. - *

- * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg - * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: - * 72). - * - * @return returns -1 if not present. - */ - public float getPhysicalHeightInch() - { - return physicalHeightInch; - } - - /** - * Returns vertical dpi of the image, if available. - *

- * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg - * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: - * 72). - * - * @return returns -1 if not present. - */ - public int getPhysicalWidthDpi() - { - return physicalWidthDpi; - } - - /** - * Returns physical width of the image in inches, if available. - *

- * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg - * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: - * 72). - * - * @return returns -1 if not present. - */ - public float getPhysicalWidthInch() - { - return physicalWidthInch; - } - - /** - * Returns the width of the image in pixels. - * - * @see #getHeight() - */ - public int getWidth() - { - return width; - } - - /** - * Returns true if the image is progressive or interlaced. - */ - public boolean isProgressive() - { - return isProgressive; - } - - /** - * Returns the color type of the image, as a constant (ie. - * ImageFormat.COLOR_TYPE_CMYK). - * - * @see #getColorTypeDescription() - */ - public int getColorType() - { - return colorType; - } - - /** - * Returns a description of the color type of the image. - * - * @see #getColorType() - */ - public String getColorTypeDescription() - { - switch (colorType) - { - case COLOR_TYPE_BW: - return "Black and White"; - case COLOR_TYPE_GRAYSCALE: - return "Grayscale"; - case COLOR_TYPE_RGB: - return "RGB"; - case COLOR_TYPE_CMYK: - return "CMYK"; - case COLOR_TYPE_OTHER: - return "Other"; - case COLOR_TYPE_UNKNOWN: - return "Unknown"; - - default: - return "Unknown"; - } - - } - - public void dump() - { - System.out.print(toString()); - } - - public String toString() - { - try - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - - toString(pw, ""); - pw.flush(); - - return sw.toString(); - } catch (Exception e) - { - return "Image Data: Error"; - } - } - - public void toString(PrintWriter pw, String prefix) - throws ImageReadException, IOException - { - pw.println("Format Details: " + formatDetails); - - pw.println("Bits Per Pixel: " + bitsPerPixel); - pw.println("Comments: " + comments.size()); - for (int i = 0; i < comments.size(); i++) - { - String s = (String) comments.get(i); - pw.println("\t" + i + ": '" + s + "'"); - - } - pw.println("Format: " + format.name); - pw.println("Format Name: " + formatName); - pw.println("Compression Algorithm: " + compressionAlgorithm); - pw.println("Height: " + height); - pw.println("MimeType: " + mimeType); - pw.println("Number Of Images: " + numberOfImages); - pw.println("Physical Height Dpi: " + physicalHeightDpi); - pw.println("Physical Height Inch: " + physicalHeightInch); - pw.println("Physical Width Dpi: " + physicalWidthDpi); - pw.println("Physical Width Inch: " + physicalWidthInch); - pw.println("Width: " + width); - pw.println("Is Progressive: " + isProgressive); - pw.println("Is Transparent: " + isTransparent); - - pw.println("Color Type: " + getColorTypeDescription()); - pw.println("Uses Palette: " + usesPalette); - - pw.flush(); - - } - - /** - * Returns a description of the file format, ie. format version. - */ - public String getFormatDetails() { - return formatDetails; - } - - /** - * Returns true if the image has transparency. - */ - public boolean isTransparent() { - return isTransparent; - } - - /** - * Returns true if the image uses a palette. - */ - public boolean usesPalette() { - return usesPalette; - } - - /** - * Returns a description of the compression algorithm, if any. - */ - public String getCompressionAlgorithm() { - return compressionAlgorithm; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * ImageInfo represents a collection of basic properties of an image, such as + * width, height, format, bit depth, etc. + */ +public class ImageInfo { + private final String formatDetails; // ie version + + private final int bitsPerPixel; + private final List comments; + + private final ImageFormat format; + private final String formatName; + private final int height; + private final String mimeType; + + private final int numberOfImages; + private final int physicalHeightDpi; + private final float physicalHeightInch; + private final int physicalWidthDpi; + private final float physicalWidthInch; + private final int width; + private final boolean progressive; + private final boolean transparent; + + private final boolean usesPalette; + + public static final int COLOR_TYPE_BW = 0; + public static final int COLOR_TYPE_GRAYSCALE = 1; + public static final int COLOR_TYPE_RGB = 2; + public static final int COLOR_TYPE_CMYK = 3; + public static final int COLOR_TYPE_YCbCr = 4; + public static final int COLOR_TYPE_YCCK = 5; + public static final int COLOR_TYPE_YCC = 6; + public static final int COLOR_TYPE_OTHER = -1; + public static final int COLOR_TYPE_UNKNOWN = -2; + + private final int colorType; + + public static final String COMPRESSION_ALGORITHM_UNKNOWN = "Unknown"; + public static final String COMPRESSION_ALGORITHM_NONE = "None"; + public static final String COMPRESSION_ALGORITHM_LZW = "LZW"; + public static final String COMPRESSION_ALGORITHM_PACKBITS = "PackBits"; + public static final String COMPRESSION_ALGORITHM_JPEG = "JPEG"; + public static final String COMPRESSION_ALGORITHM_RLE = "RLE: Run-Length Encoding"; + public static final String COMPRESSION_ALGORITHM_PSD = "Photoshop"; + public static final String COMPRESSION_ALGORITHM_PNG_FILTER = "PNG Filter"; + public static final String COMPRESSION_ALGORITHM_CCITT_GROUP_3 = "CCITT Group 3 1-Dimensional Modified Huffman run-length encoding."; + public static final String COMPRESSION_ALGORITHM_CCITT_GROUP_4 = "CCITT Group 4"; + public static final String COMPRESSION_ALGORITHM_CCITT_1D = "CCITT 1D"; + + private final String compressionAlgorithm; + + public ImageInfo(final String formatDetails, final int bitsPerPixel, + final List comments, final ImageFormat format, final String formatName, + final int height, final String mimeType, final int numberOfImages, + final int physicalHeightDpi, final float physicalHeightInch, + final int physicalWidthDpi, final float physicalWidthInch, final int width, + final boolean progressive, final boolean transparent, final boolean usesPalette, + final int colorType, final String compressionAlgorithm) { + this.formatDetails = formatDetails; + + this.bitsPerPixel = bitsPerPixel; + this.comments = comments; + + this.format = format; + this.formatName = formatName; + this.height = height; + this.mimeType = mimeType; + + this.numberOfImages = numberOfImages; + this.physicalHeightDpi = physicalHeightDpi; + this.physicalHeightInch = physicalHeightInch; + this.physicalWidthDpi = physicalWidthDpi; + this.physicalWidthInch = physicalWidthInch; + this.width = width; + this.progressive = progressive; + + this.transparent = transparent; + this.usesPalette = usesPalette; + + this.colorType = colorType; + this.compressionAlgorithm = compressionAlgorithm; + } + + /** + * Returns the bits per pixel of the image data. + */ + public int getBitsPerPixel() { + return bitsPerPixel; + } + + /** + * Returns a list of comments from the image file. + *

+ * This is mostly obsolete. + */ + public List getComments() { + return new ArrayList(comments); + } + + /** + * Returns the image file format, ie. ImageFormat.IMAGE_FORMAT_PNG. + *

+ * Returns ImageFormat.IMAGE_FORMAT_UNKNOWN if format is unknown. + * + * @return A constant defined in ImageFormat. + * @see ImageFormats + */ + public ImageFormat getFormat() { + return format; + } + + /** + * Returns a string with the name of the image file format. + * + * @see #getFormat() + */ + public String getFormatName() { + return formatName; + } + + /** + * Returns the height of the image in pixels. + * + * @see #getWidth() + */ + public int getHeight() { + return height; + } + + /** + * Returns the MIME type of the image. + * + * @see #getFormat() + */ + public String getMimeType() { + return mimeType; + } + + /** + * Returns the number of images in the file. + *

+ * Applies mostly to GIF and TIFF; reading PSD/Photoshop layers is not + * supported, and Jpeg/JFIF EXIF thumbnails are not included in this count. + */ + public int getNumberOfImages() { + return numberOfImages; + } + + /** + * Returns horizontal dpi of the image, if available. + *

+ * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg + * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: + * 72). + * + * @return returns -1 if not present. + */ + public int getPhysicalHeightDpi() { + return physicalHeightDpi; + } + + /** + * Returns physical height of the image in inches, if available. + *

+ * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg + * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: + * 72). + * + * @return returns -1 if not present. + */ + public float getPhysicalHeightInch() { + return physicalHeightInch; + } + + /** + * Returns vertical dpi of the image, if available. + *

+ * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg + * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: + * 72). + * + * @return returns -1 if not present. + */ + public int getPhysicalWidthDpi() { + return physicalWidthDpi; + } + + /** + * Returns physical width of the image in inches, if available. + *

+ * Applies to TIFF (optional), BMP (always), GIF (constant: 72), Jpeg + * (optional), PNG (optional), PNM (constant: 72), PSD/Photoshop (constant: + * 72). + * + * @return returns -1 if not present. + */ + public float getPhysicalWidthInch() { + return physicalWidthInch; + } + + /** + * Returns the width of the image in pixels. + * + * @see #getHeight() + */ + public int getWidth() { + return width; + } + + /** + * Returns true if the image is progressive or interlaced. + */ + public boolean isProgressive() { + return progressive; + } + + /** + * Returns the color type of the image, as a constant (ie. + * ImageFormat.COLOR_TYPE_CMYK). + * + * @see #getColorTypeDescription() + */ + public int getColorType() { + return colorType; + } + + /** + * Returns a description of the color type of the image. + * + * @see #getColorType() + */ + public String getColorTypeDescription() { + switch (colorType) { + case COLOR_TYPE_BW: + return "Black and White"; + case COLOR_TYPE_GRAYSCALE: + return "Grayscale"; + case COLOR_TYPE_RGB: + return "RGB"; + case COLOR_TYPE_CMYK: + return "CMYK"; + case COLOR_TYPE_YCbCr: + return "YCbCr"; + case COLOR_TYPE_YCCK: + return "YCCK"; + case COLOR_TYPE_YCC: + return "YCC"; + case COLOR_TYPE_OTHER: + return "Other"; + case COLOR_TYPE_UNKNOWN: + return "Unknown"; + + default: + return "Unknown"; + } + + } + + public void dump() { + System.out.print(toString()); + } + + @Override + public String toString() { + try { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + + toString(pw, ""); + pw.flush(); + + return sw.toString(); + } catch (final Exception e) { + return "Image Data: Error"; + } + } + + public void toString(final PrintWriter pw, final String prefix) { + pw.println("Format Details: " + formatDetails); + + pw.println("Bits Per Pixel: " + bitsPerPixel); + pw.println("Comments: " + comments.size()); + for (int i = 0; i < comments.size(); i++) { + final String s = comments.get(i); + pw.println("\t" + i + ": '" + s + "'"); + + } + pw.println("Format: " + format.getName()); + pw.println("Format Name: " + formatName); + pw.println("Compression Algorithm: " + compressionAlgorithm); + pw.println("Height: " + height); + pw.println("MimeType: " + mimeType); + pw.println("Number Of Images: " + numberOfImages); + pw.println("Physical Height Dpi: " + physicalHeightDpi); + pw.println("Physical Height Inch: " + physicalHeightInch); + pw.println("Physical Width Dpi: " + physicalWidthDpi); + pw.println("Physical Width Inch: " + physicalWidthInch); + pw.println("Width: " + width); + pw.println("Is Progressive: " + progressive); + pw.println("Is Transparent: " + transparent); + + pw.println("Color Type: " + getColorTypeDescription()); + pw.println("Uses Palette: " + usesPalette); + + pw.flush(); + + } + + /** + * Returns a description of the file format, ie. format version. + */ + public String getFormatDetails() { + return formatDetails; + } + + /** + * Returns true if the image has transparency. + */ + public boolean isTransparent() { + return transparent; + } + + /** + * Returns true if the image uses a palette. + */ + public boolean usesPalette() { + return usesPalette; + } + + /** + * Returns a description of the compression algorithm, if any. + */ + public String getCompressionAlgorithm() { + return compressionAlgorithm; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/ImageParser.java b/src/main/java/org/apache/commons/imaging/ImageParser.java new file mode 100644 index 0000000..b157d47 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImageParser.java @@ -0,0 +1,987 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.IBufferedImageFactory; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.SimpleBufferedImageFactory; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.bmp.BmpImageParser; +import org.apache.commons.imaging.formats.dcx.DcxImageParser; +import org.apache.commons.imaging.formats.gif.GifImageParser; +import org.apache.commons.imaging.formats.icns.IcnsImageParser; +import org.apache.commons.imaging.formats.ico.IcoImageParser; +import org.apache.commons.imaging.formats.jpeg.JpegImageParser; +import org.apache.commons.imaging.formats.pcx.PcxImageParser; +import org.apache.commons.imaging.formats.png.PngImageParser; +import org.apache.commons.imaging.formats.pnm.PnmImageParser; +import org.apache.commons.imaging.formats.psd.PsdImageParser; +import org.apache.commons.imaging.formats.rgbe.RgbeImageParser; +import org.apache.commons.imaging.formats.tiff.TiffImageParser; +import org.apache.commons.imaging.formats.wbmp.WbmpImageParser; +import org.apache.commons.imaging.formats.xbm.XbmImageParser; +import org.apache.commons.imaging.formats.xpm.XpmImageParser; + +/** + * Provides the abstract base class for all image reading and writing + * utilities. ImageParser implementations are expected to extend this + * class providing logic for identifying and processing data in their + * own specific format. Specific implementations are found + * under the com.apache.commons.imaging.formats package. + * + *

Application Notes

+ * + *

Format support

+ * + * For the most recent information on format support for the + * Apache Commons Imaging package, refer to + *
Format Support + * at the main project development web site. + * + *

On the accuracy of this Javadoc

+ * + * The original authors of this class did not supply documentation. + * The Javadoc for this class is based on inspection of the + * source code. In some cases, the purpose and usage for particular + * methods was deduced from the source and may not perfectly reflect + * the intentions of the original. Therefore, you should not assume + * that the documentation is perfect, especially in the more obscure + * and specialized areas of implementation. + * + *

The "Map params" argument

+ * + * Many of the methods specified by this class accept an argument of + * type Map giving a list of parameters to be used when processing an + * image. For example, some of the output formats permit the specification + * of different kinds of image compression or color models. Some of the + * reading methods permit the calling application to require strict + * format compliance. In many cases, however, an application will not + * require the use of this argument. While some of the ImageParser + * implementations check for (and ignore) null arguments for this parameter, + * not all of them do (at least not at the time these notes were written). + * Therefore, a prudent programmer will always supply an valid, though + * empty instance of a Map implementation when calling such methods. + * Generally, the java HashMap class is useful for this purpose. + * + *

Additionally, developers creating or enhancing classes derived + * from ImageParser are encouraged to include such checks in their code. + */ +public abstract class ImageParser extends BinaryFileParser { + + /** + * Gets an array of new instances of all image parsers. + * + * @return A valid array of image parsers + */ + public static ImageParser[] getAllImageParsers() { + + return new ImageParser[]{ + new BmpImageParser(), + new DcxImageParser(), + new GifImageParser(), + new IcnsImageParser(), + new IcoImageParser(), + new JpegImageParser(), + new PcxImageParser(), + new PngImageParser(), + new PnmImageParser(), + new PsdImageParser(), + new RgbeImageParser(), + new TiffImageParser(), + new WbmpImageParser(), + new XbmImageParser(), + new XpmImageParser(), + // new JBig2ImageParser(), + // new TgaImageParser(), + }; + } + + /** + * Get image metadata from the specified byte source. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified byte source. + * + * @param byteSource A valid byte source. + * @return A valid, potentially subject-matter-specific implementation of + * the IImageMetadata interface describing the content extracted + * from the source content. + * @throws ImageReadException In the event that the the ByteSource + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful data read operation. + */ + public final IImageMetadata getMetadata(final ByteSource byteSource) throws ImageReadException, IOException { + return getMetadata(byteSource, null); + } + + /** + * Get image metadata from the specified byte source. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified byte source. + * + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param byteSource A valid byte source. + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid, potentially subject-matter-specific implementation of + * the IImageMetadata interface describing the content extracted + * from the source content. + * @throws ImageReadException In the event that the the ByteSource + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful data read operation. + */ + public abstract IImageMetadata getMetadata(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Get image metadata from the specified array of bytes. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified data. + * + * @param bytes A valid array of bytes + * @return A valid, potentially subject-matter-specific implementation of + * the IImageMetadata interface describing the content extracted + * from the source content. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful data read operation. + */ + public final IImageMetadata getMetadata(final byte[] bytes) throws ImageReadException, IOException { + return getMetadata(bytes, null); + } + + /** + * Get image metadata from the specified array of bytes. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified data. + * + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param bytes A valid array of bytes + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid image metadata object describing the content extracted + * from the specified content. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful data read operation. + */ + public final IImageMetadata getMetadata(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getMetadata(new ByteSourceArray(bytes), params); + } + + /** + * Get image metadata from the specified file. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified data. + * + * @param file A valid reference to a file. + * @return A valid image metadata object describing the content extracted + * from the specified content. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful file read or + * access operation. + */ + public final IImageMetadata getMetadata(final File file) throws ImageReadException, IOException { + return getMetadata(file, null); + } + + /** + * Get image metadata from the specified file. Format-specific + * ImageParser implementations are expected to return a valid + * IImageMetadata object or to throw an ImageReadException if unable + * to process the specified data. + * + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param file A valid reference to a file. + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid image metadata object describing the content extracted + * from the specified content. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful file read or + * access operation. + */ + public final IImageMetadata getMetadata(final File file, final Map params) + throws ImageReadException, IOException { + if (getDebug()) { + System.out.println(getName() + ".getMetadata" + ": " + + file.getName()); + } + + if (!canAcceptExtension(file)) { + return null; + } + + return getMetadata(new ByteSourceFile(file), params); + } + + /** + * Get image information from the specified ByteSource. Format-specific + * ImageParser implementations are expected to return a valid + * ImageInfo object or to throw an ImageReadException if unable + * to process the specified data. + * + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param byteSource A valid ByteSource object + * @param params Optional instructions for special-handling or interpretation + * of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid image information object describing the content extracted + * from the specified data. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful data access operation. + */ + public abstract ImageInfo getImageInfo(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Get image information from the specified ByteSource. Format-specific + * ImageParser implementations are expected to return a valid + * ImageInfo object or to throw an ImageReadException if unable + * to process the specified data. + * + * @param byteSource A valid ByteSource object + * @return A valid image information object describing the content extracted + * from the specified data. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful data + * access operation. + */ + public final ImageInfo getImageInfo(final ByteSource byteSource) throws ImageReadException, IOException { + return getImageInfo(byteSource, null); + } + + /** + * Get image information from the specified array of bytes. Format-specific + * ImageParser implementations are expected to return a valid + * ImageInfo object or to throw an ImageReadException if unable + * to process the specified data. + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param bytes A valid array of bytes + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid image information object describing the content extracted + * from the specified data. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful data + * access operation. + */ + public final ImageInfo getImageInfo(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceArray(bytes), params); + } + + /** + * Get image information from the specified file Format-specific + * ImageParser implementations are expected to return a valid + * ImageInfo object or to throw an ImageReadException if unable + * to process the specified data. + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will require this capability. Because the + * base class may call this method with a null params argument, + * implementations should always include logic + * for ignoring null input. + * + * @param file A valid File object + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid image information object describing the content extracted + * from the specified data. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful file read or + * access operation. + */ + public final ImageInfo getImageInfo(final File file, final Map params) + throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + return getImageInfo(new ByteSourceFile(file), params); + } + + /** + * Determines the format compliance of the content of the supplied byte + * source based on rules provided by a specific implementation. + * + * @param byteSource A valid instance of ByteSource + * @return true if the content is format-compliant; otherwise, false + * @throws ImageReadException may be thrown by sub-classes + * @throws IOException may be thrown by sub-classes + */ + public FormatCompliance getFormatCompliance(final ByteSource byteSource) + throws ImageReadException, IOException { + return null; + } + + /** + * Determines the format compliance of the content of the supplied byte + * array based on rules provided by a specific implementation. + * + * @param bytes A valid byte array. + * @return A valid FormatCompliance object. + * @throws ImageReadException may be thrown by sub-classes + * @throws IOException may be thrown by sub-classes + */ + public final FormatCompliance getFormatCompliance(final byte[] bytes) + throws ImageReadException, IOException { + return getFormatCompliance(new ByteSourceArray(bytes)); + } + + /** + * Determines the format compliance of the specified file based on + * rules provided by a specific implementation. + * + * @param file A valid reference to a file. + * @return A valid format compliance object. + * @throws ImageReadException may be thrown by sub-classes + * @throws IOException may be thrown by sub-classes + */ + public final FormatCompliance getFormatCompliance(final File file) + throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + return getFormatCompliance(new ByteSourceFile(file)); + } + + /** + * Gets all images specified by the byte source (some + * formats may include multiple images within a single data source). + * + * @param byteSource A valid instance of ByteSource. + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public List getAllBufferedImages(final ByteSource byteSource) + throws ImageReadException, IOException { + final BufferedImage bi = getBufferedImage(byteSource, null); + + final List result = new ArrayList(); + + result.add(bi); + + return result; + } + + /** + * Gets all images specified by the byte array (some + * formats may include multiple images within a single data source). + * + * @param bytes A valid byte array + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final List getAllBufferedImages(final byte[] bytes) + throws ImageReadException, IOException { + return getAllBufferedImages(new ByteSourceArray(bytes)); + } + + /** + * Gets all images specified by indicated file (some + * formats may include multiple images within a single data source). + * + * @param file A valid reference to a file. + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final List getAllBufferedImages(final File file) throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + return getAllBufferedImages(new ByteSourceFile(file)); + } + + /** + * Gets a buffered image specified by the byte source (for + * sources that specify multiple images, choice of which image + * is returned is implementation dependent). + * + * @param byteSource A valid instance of ByteSource + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid instance of BufferedImage. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public abstract BufferedImage getBufferedImage(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Gets a buffered image specified by the byte array (for + * sources that specify multiple images, choice of which image + * is returned is implementation dependent). + * + * @param bytes A valid byte array + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid instance of BufferedImage. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final BufferedImage getBufferedImage(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getBufferedImage(new ByteSourceArray(bytes), params); + } + + /** + * Gets a buffered image specified by the indicated file (for + * sources that specify multiple images, choice of which image + * is returned is implementation dependent). + * + * @param file A valid file reference. + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid instance of BufferedImage. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final BufferedImage getBufferedImage(final File file, final Map params) + throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + return getBufferedImage(new ByteSourceFile(file), params); + } + + + /** + * Writes the content of a BufferedImage to the specified output + * stream. + * + *

The params argument provides a mechanism for individual + * implementations to pass optional information into the parser. + * Not all formats will support this capability. Currently, + * some of the parsers do not check for null arguments. So in cases + * where no optional specifications are supported, application + * code should pass in an empty instance of an implementation of + * the map interface (i.e. an empty HashMap). + * + * @param src An image giving the source content for output + * @param os A valid output stream for storing the formatted image + * @param params A non-null Map implementation supplying optional, + * format-specific instructions for output + * (such as selections for data compression, color models, etc.) + * @throws ImageWriteException In the event that the output format + * cannot handle the input image or invalid params are specified. + * @throws IOException In the event of an write error from + * the output stream. + */ + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + os.close(); // we are obligated to close stream. + + throw new ImageWriteException("This image format (" + getName() + + ") cannot be written."); + } + + /** + * Get the size of the image described by the specified byte array. + * + * @param bytes A valid byte array. + * @return A valid instance of Dimension. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final Dimension getImageSize(final byte[] bytes) throws ImageReadException, IOException { + return getImageSize(bytes, null); + } + + /** + * Get the size of the image described by the specified byte array. + * + * @param bytes A valid byte array. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return A valid instance of Dimension. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final Dimension getImageSize(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getImageSize(new ByteSourceArray(bytes), params); + } + + /** + * Get the size of the image described by the specified file. + * + * @param file A valid reference to a file. + * @return A valid instance of Dimension. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final Dimension getImageSize(final File file) throws ImageReadException, IOException { + return getImageSize(file, null); + } + + /** + * Get the size of the image described by the specified file. + * + * @param file A valid reference to a file. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return A valid instance of Dimension. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final Dimension getImageSize(final File file, final Map params) + throws ImageReadException, IOException { + + if (!canAcceptExtension(file)) { + return null; + } + + return getImageSize(new ByteSourceFile(file), params); + } + + /** + * Get the size of the image described by the specified ByteSource. + * + * @param byteSource A valid reference to a ByteSource. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return A valid instance of Dimension. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public abstract Dimension getImageSize(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Get a string containing XML-formatted text conforming to the Extensible + * Metadata Platform (EXP) standard for representing information about + * image content. Not all image formats support EXP infomation and + * even for those that do, there is no guarantee that such information + * will be present in an image. + * + * @param byteSource A valid reference to a ByteSource. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return If XMP metadata is present, a valid string; + * if it is not present, a null. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public abstract String getXmpXml(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Get an array of bytes describing the International Color Consortium (ICC) + * specification for the color space of the image contained in the + * input byte array. Not all formats support ICC profiles. + * + * @param bytes A valid array of bytes. + * @return If available, a valid array of bytes; otherwise, a null + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final byte[] getICCProfileBytes(final byte[] bytes) throws ImageReadException, IOException { + return getICCProfileBytes(bytes, null); + } + + /** + * Get an array of bytes describing the International Color Consortium (ICC) + * specification for the color space of the image contained in the + * input byte array. Not all formats support ICC profiles. + * + * @param bytes A valid array of bytes. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return If available, a valid array of bytes; otherwise, a null + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final byte[] getICCProfileBytes(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getICCProfileBytes(new ByteSourceArray(bytes), params); + } + + /** + * Get an array of bytes describing the International Color Consortium (ICC) + * specification for the color space of the image contained in the + * input file. Not all formats support ICC profiles. + * + * @param file A valid file reference. + * @return If available, a valid array of bytes; otherwise, a null + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final byte[] getICCProfileBytes(final File file) throws ImageReadException, IOException { + return getICCProfileBytes(file, null); + } + + /** + * Get an array of bytes describing the International Color Consortium (ICC) + * specification for the color space of the image contained in the + * input file. Not all formats support ICC profiles. + * + * @param file A valid file reference. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return If available, a valid array of bytes; otherwise, a null + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final byte[] getICCProfileBytes(final File file, final Map params) + throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + if (getDebug()) { + System.out.println(getName() + ": " + file.getName()); + } + + return getICCProfileBytes(new ByteSourceFile(file), params); + } + + /** + * Get an array of bytes describing the International Color Consortium (ICC) + * specification for the color space of the image contained in the + * input byteSoruce. Not all formats support ICC profiles. + * + * @param byteSource A valid ByteSource. + * @param params Optional instructions for special-handling or + * interpretation of the input data. + * @return If available, a valid array of bytes; otherwise, a null + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public abstract byte[] getICCProfileBytes(ByteSource byteSource, Map params) + throws ImageReadException, IOException; + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified byte array to a string. + * + * @param bytes A valid array of bytes. + * @return A valid string. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final String dumpImageFile(final byte[] bytes) throws ImageReadException, IOException { + return dumpImageFile(new ByteSourceArray(bytes)); + } + + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified file to a string. + * + * @param file A valid file reference. + * @return A valid string. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final String dumpImageFile(final File file) throws ImageReadException, IOException { + if (!canAcceptExtension(file)) { + return null; + } + + if (getDebug()) { + System.out.println(getName() + ": " + file.getName()); + } + + return dumpImageFile(new ByteSourceFile(file)); + } + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified byte source to a string. + * + * @param byteSource A valid byte source. + * @return A valid string. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public final String dumpImageFile(final ByteSource byteSource) + throws ImageReadException, IOException { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + + dumpImageFile(pw, byteSource); + + pw.flush(); + + return sw.toString(); + } + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified byte source to a PrintWriter + * + * @param byteSource A valid byte source. + * @return A valid PrintWriter. + * @throws ImageReadException In the event that the the specified content + * does not conform to the format of the specific + * parser implementation. + * @throws IOException In the event of unsuccessful read or access operation. + */ + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + return false; + } + + + /** + * Get a descriptive name for the implementation of an ImageParser. + * + * @return a valid, subject-matter-specific string. + */ + public abstract String getName(); + + /** + * Get the default extension for the format specified by an implementation + * of ImageParser. Some parsers can support more than one extension + * (i.e. .JPEG, .JPG; .TIF, .TIFF, etc.). + * + * @return A valid string. + */ + public abstract String getDefaultExtension(); + + /** + * Get an array of all accepted extensions + * + * @return A valid array of one or more elements. + */ + protected abstract String[] getAcceptedExtensions(); + + /** + * Get an array of ImageFormat objects describing all accepted types + * + * @return A valid array of one or more elements. + */ + protected abstract ImageFormat[] getAcceptedTypes(); + + /** + * Indicates whether the ImageParser implementation can accept + * the specified format + * + * @param type An instance of ImageFormat. + * @return If the parser can accept the format, true; otherwise, false. + */ + public boolean canAcceptType(final ImageFormat type) { + final ImageFormat[] types = getAcceptedTypes(); + + for (final ImageFormat type2 : types) { + if (type2.equals(type)) { + return true; + } + } + return false; + } + + /** + * Indicates whether the ImageParser implementation can accept + * the specified file based on its extension. + * + * @param file An valid file reference. + * @return If the parser can accept the format, true; otherwise, false. + */ + protected final boolean canAcceptExtension(final File file) { + return canAcceptExtension(file.getName()); + } + + /** + * Indicates whether the ImageParser implementation can accept + * the specified file name based on its extension. + * + * @param filename An valid string giving a file name or file path. + * @return If the parser can accept the format, true; otherwise, false. + */ + protected final boolean canAcceptExtension(final String filename) { + final String[] exts = getAcceptedExtensions(); + if (exts == null) { + return true; + } + + final int index = filename.lastIndexOf('.'); + if (index >= 0) { + String ext = filename.substring(index); + ext = ext.toLowerCase(Locale.ENGLISH); + + for (final String ext2 : exts) { + final String ext2Lower = ext2.toLowerCase(Locale.ENGLISH); + if (ext2Lower.equals(ext)) { + return true; + } + } + } + return false; + } + + /** + * Get an instance of IBufferedImageFactory based on the presence + * of a specification for ImagingConstants..BUFFERED_IMAGE_FACTORY + * within the supplied params. + * + * @param params A valid Map object, or a null. + * @return A valid instance of an implementation of a IBufferedImageFactory. + */ + protected IBufferedImageFactory getBufferedImageFactory(final Map params) { + if (params == null) { + return new SimpleBufferedImageFactory(); + } + + final IBufferedImageFactory result = (IBufferedImageFactory) params + .get(ImagingConstants.BUFFERED_IMAGE_FACTORY); + + if (null != result) { + return result; + } + + return new SimpleBufferedImageFactory(); + } + + /** + * A utility method to search a params specification and determine + * whether it contains the ImagingConstants.PARAM_KEY_STRICT + * specification. Intended + * for internal use by ImageParser implementations. + * + * @param params A valid Map object (or a null). + * @return If the params specify strict format compliance, true; + * otherwise, false. + */ + public static boolean isStrict(final Map params) { + if (params == null || !params.containsKey(ImagingConstants.PARAM_KEY_STRICT)) { + return false; + } + return ((Boolean) params.get(ImagingConstants.PARAM_KEY_STRICT)).booleanValue(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/ImageReadException.java b/src/main/java/org/apache/commons/imaging/ImageReadException.java new file mode 100644 index 0000000..4fdfeb6 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImageReadException.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * A custom exception thrown when an ImageParser or other utility + * encounters a format-violation, non-supported element, or other + * condition that renders image data unaccessible. + */ +public class ImageReadException extends ImagingException { + private static final long serialVersionUID = -1L; + + public ImageReadException(final String message) { + super(message); + } + + public ImageReadException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/apache/commons/imaging/ImageWriteException.java b/src/main/java/org/apache/commons/imaging/ImageWriteException.java new file mode 100644 index 0000000..fae0e27 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImageWriteException.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * A custom exception thrown when an ImageParser or other utility + * encounters a format-violation, non-supported element, or other + * condition that renders image data unwritable. + */ +public class ImageWriteException extends ImagingException { + private static final long serialVersionUID = -1L; + + public ImageWriteException(final String message) { + super(message); + } + + public ImageWriteException(final String message, final Throwable cause) { + super(message, cause); + } + + public ImageWriteException(final String message, final Object data) { + super(message + ": " + data + " (" + getType(data) + ")"); + } + + private static String getType(final Object value) { + if (value == null) { + return "null"; + } else if (value instanceof Object[]) { + return "[Object[]: " + ((Object[]) value).length + "]"; + } else if (value instanceof char[]) { + return "[char[]: " + ((char[]) value).length + "]"; + } else if (value instanceof byte[]) { + return "[byte[]: " + ((byte[]) value).length + "]"; + } else if (value instanceof short[]) { + return "[short[]: " + ((short[]) value).length + "]"; + } else if (value instanceof int[]) { + return "[int[]: " + ((int[]) value).length + "]"; + } else if (value instanceof long[]) { + return "[long[]: " + ((long[]) value).length + "]"; + } else if (value instanceof float[]) { + return "[float[]: " + ((float[]) value).length + "]"; + } else if (value instanceof double[]) { + return "[double[]: " + ((double[]) value).length + "]"; + } else if (value instanceof boolean[]) { + return "[boolean[]: " + ((boolean[]) value).length + "]"; + } else { + return value.getClass().getName(); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/Imaging.java b/src/main/java/org/apache/commons/imaging/Imaging.java new file mode 100644 index 0000000..74747e6 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/Imaging.java @@ -0,0 +1,1504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.color.ICC_Profile; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; +import org.apache.commons.imaging.icc.IccProfileInfo; +import org.apache.commons.imaging.icc.IccProfileParser; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; + +/** + * The primary application programming interface (API) to the Imaging library. + *

+ + *

Application Notes

+ *

Using this class

+ * Almost all of the Apache Commons Imaging library's core functionality can + * be accessed through the methods provided by this class. + * The use of the Imaging class is similar to the Java API's ImageIO class, + * though Imaging supports formats and options not included in the standard + * Java API. + *

All of methods provided by the Imaging class are declared static. + *

The Apache Commons Imaging package is a pure Java implementation. + *

Format support

+ * While the Apache Commons Imaging package handles a number of different + * graphics formats, support for some formats is not yet complete. + * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. + *

Optional parameters for image reading and writing

+ * Some of the methods provided by this class accept an optional + * params argument that permits the application to specify + * elements for special handling. If these specifications are not required by + * the application, the params argument may be omitted (as appropriate) or + * a null argument may be provided. In image-writing operations, the option + * parameters may include options such as data-compression type (if any), + * color model, or other format-specific data representations. The parameters + * map may also be used to provide EXIF Tags and other metadata to those + * formats that support them. In image-reading operations, + * the parameters may include information about special handling in reading + * the image data. + *

Optional parameters are specified using a Map object (typically, + * a Java HashMap) to specify a set of keys and values for input. + * The specification for support keys is provided by the ImagingConstants + * interface as well as by format-specific interfaces such as + * JpegContants or TiffConstants. + *

Example code

+ * See the source of the SampleUsage class and other classes in the + * org.apache.commons.imaging.examples package for examples. + * + * @see org.apache.commons.imaging.examples.SampleUsage + * @see Format Support + */ +public abstract class Imaging { + private static final int[] MAGIC_NUMBERS_GIF = { 0x47, 0x49, }; + private static final int[] MAGIC_NUMBERS_PNG = { 0x89, 0x50, }; + private static final int[] MAGIC_NUMBERS_JPEG = { 0xff, 0xd8, }; + private static final int[] MAGIC_NUMBERS_BMP = { 0x42, 0x4d, }; + private static final int[] MAGIC_NUMBERS_TIFF_MOTOROLA = { 0x4D, 0x4D, }; + private static final int[] MAGIC_NUMBERS_TIFF_INTEL = { 0x49, 0x49, }; + private static final int[] MAGIC_NUMBERS_PAM = { 0x50, 0x37, }; + private static final int[] MAGIC_NUMBERS_PSD = { 0x38, 0x42, }; + private static final int[] MAGIC_NUMBERS_PBM_A = { 0x50, 0x31, }; + private static final int[] MAGIC_NUMBERS_PBM_B = { 0x50, 0x34, }; + private static final int[] MAGIC_NUMBERS_PGM_A = { 0x50, 0x32, }; + private static final int[] MAGIC_NUMBERS_PGM_B = { 0x50, 0x35, }; + private static final int[] MAGIC_NUMBERS_PPM_A = { 0x50, 0x33, }; + private static final int[] MAGIC_NUMBERS_PPM_B = { 0x50, 0x36, }; + private static final int[] MAGIC_NUMBERS_JBIG2_1 = { 0x97, 0x4A, }; + private static final int[] MAGIC_NUMBERS_JBIG2_2 = { 0x42, 0x32, }; + private static final int[] MAGIC_NUMBERS_ICNS = { 0x69, 0x63, }; + private static final int[] MAGIC_NUMBERS_DCX = { 0xB1, 0x68, }; + private static final int[] MAGIC_NUMBERS_RGBE = { 0x23, 0x3F, }; + + /** + * Attempts to determine if a file contains an image recorded in + * a supported graphics format based on its file-name extension + * (for example ".jpg", ".gif", ".png", etc.). + * + * @param file A valid File object providing a reference to + * a file that may contain an image. + * @return true if the file-name includes a supported image + * format file extension; otherwise, false. + */ + public static boolean hasImageFileExtension(final File file) { + if (file == null || !file.isFile()) { + return false; + } + return hasImageFileExtension(file.getName()); + } + + /** + * Attempts to determine if a file contains an image recorded in + * a supported graphics format based on its file-name extension + * (for example ".jpg", ".gif", ".png", etc.). + * + * @param filename A valid string representing name of file + * which may contain an image. + * @return true if the filename has an image format file extension. + */ + public static boolean hasImageFileExtension(String filename) { + if (filename == null) { + return false; + } + + filename = filename.toLowerCase(Locale.ENGLISH); + + final ImageParser[] imageParsers = ImageParser.getAllImageParsers(); + for (final ImageParser imageParser : imageParsers) { + final String[] exts = imageParser.getAcceptedExtensions(); + + for (final String ext : exts) { + if (filename.endsWith(ext.toLowerCase(Locale.ENGLISH))) { + return true; + } + } + } + + return false; + } + + /** + * Attempts to determine the image format of a file based on its + * "magic numbers," the first bytes of the data. + *

Many graphics format specify identifying byte + * values that appear at the beginning of the data file. This method + * checks for such identifying elements and returns a ImageFormat + * enumeration indicating what it detects. Note that this + * method can return "false positives" in cases where non-image files + * begin with the specified byte values. + * + * @param bytes Byte array containing an image file. + * @return An ImageFormat, such as ImageFormat.IMAGE_FORMAT_JPEG. Returns + * ImageFormat.IMAGE_FORMAT_UNKNOWN if the image type cannot be + * determined. + */ + public static ImageFormat guessFormat(final byte[] bytes) + throws ImageReadException, IOException { + return guessFormat(new ByteSourceArray(bytes)); + } + + /** + * Attempts to determine the image format of a file based on its + * "magic numbers," the first bytes of the data. + *

Many graphics formats specify identifying byte + * values that appear at the beginning of the data file. This method + * checks for such identifying elements and returns a ImageFormat + * enumeration indicating what it detects. Note that this + * method can return "false positives" in cases where non-image files + * begin with the specified byte values. + * + * @param file File containing image data. + * @return An ImageFormat, such as ImageFormat.IMAGE_FORMAT_JPEG. Returns + * ImageFormat.IMAGE_FORMAT_UNKNOWN if the image type cannot be + * determined. + */ + public static ImageFormat guessFormat(final File file) throws ImageReadException, + IOException { + return guessFormat(new ByteSourceFile(file)); + } + + private static boolean compareBytePair(final int[] a, final int[] b) { + if (a.length != 2 && b.length != 2) { + throw new RuntimeException("Invalid Byte Pair."); + } + return (a[0] == b[0]) && (a[1] == b[1]); + } + + + /** + * Attempts to determine the image format of a file based on its + * "magic numbers," the first bytes of the data. + *

Many graphics formats specify identifying byte + * values that appear at the beginning of the data file. This method + * checks for such identifying elements and returns a ImageFormat + * enumeration indicating what it detects. Note that this + * method can return "false positives" in cases where non-image files + * begin with the specified byte values. + * + * @param byteSource a valid ByteSource object potentially supplying + * data for an image. + * @return An ImageFormat, such as ImageFormat.IMAGE_FORMAT_JPEG. Returns + * ImageFormat.IMAGE_FORMAT_UNKNOWN if the image type cannot be + * determined. + * @throws ImageReadException in the event of an unsuccessful + * attempt to read the image data + * @throws IOException in the event of an unrecoverable I/O condition. + */ + public static ImageFormat guessFormat(final ByteSource byteSource) + throws ImageReadException, IOException { + + if (byteSource == null) { + return ImageFormats.UNKNOWN; + } + + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final int i1 = is.read(); + final int i2 = is.read(); + if ((i1 < 0) || (i2 < 0)) { + throw new ImageReadException( + "Couldn't read magic numbers to guess format."); + } + + final int b1 = i1 & 0xff; + final int b2 = i2 & 0xff; + final int[] bytePair = { b1, b2, }; + + if (compareBytePair(MAGIC_NUMBERS_GIF, bytePair)) { + canThrow = true; + return ImageFormats.GIF; + } + // else if (b1 == 0x00 && b2 == 0x00) // too similar to TGA + // { + // return ImageFormat.IMAGE_FORMAT_ICO; + // } + else if (compareBytePair(MAGIC_NUMBERS_PNG, bytePair)) { + canThrow = true; + return ImageFormats.PNG; + } else if (compareBytePair(MAGIC_NUMBERS_JPEG, bytePair)) { + canThrow = true; + return ImageFormats.JPEG; + } else if (compareBytePair(MAGIC_NUMBERS_BMP, bytePair)) { + canThrow = true; + return ImageFormats.BMP; + } else if (compareBytePair(MAGIC_NUMBERS_TIFF_MOTOROLA, bytePair)) { + canThrow = true; + return ImageFormats.TIFF; + } else if (compareBytePair(MAGIC_NUMBERS_TIFF_INTEL, bytePair)) { + canThrow = true; + return ImageFormats.TIFF; + } else if (compareBytePair(MAGIC_NUMBERS_PSD, bytePair)) { + canThrow = true; + return ImageFormats.PSD; + } else if (compareBytePair(MAGIC_NUMBERS_PAM, bytePair)) { + canThrow = true; + return ImageFormats.PAM; + } else if (compareBytePair(MAGIC_NUMBERS_PBM_A, bytePair)) { + canThrow = true; + return ImageFormats.PBM; + } else if (compareBytePair(MAGIC_NUMBERS_PBM_B, bytePair)) { + canThrow = true; + return ImageFormats.PBM; + } else if (compareBytePair(MAGIC_NUMBERS_PGM_A, bytePair)) { + canThrow = true; + return ImageFormats.PGM; + } else if (compareBytePair(MAGIC_NUMBERS_PGM_B, bytePair)) { + canThrow = true; + return ImageFormats.PGM; + } else if (compareBytePair(MAGIC_NUMBERS_PPM_A, bytePair)) { + canThrow = true; + return ImageFormats.PPM; + } else if (compareBytePair(MAGIC_NUMBERS_PPM_B, bytePair)) { + canThrow = true; + return ImageFormats.PPM; + } else if (compareBytePair(MAGIC_NUMBERS_JBIG2_1, bytePair)) { + final int i3 = is.read(); + final int i4 = is.read(); + if ((i3 < 0) || (i4 < 0)) { + throw new ImageReadException( + "Couldn't read magic numbers to guess format."); + } + + final int b3 = i3 & 0xff; + final int b4 = i4 & 0xff; + final int[] bytePair2 = { b3, b4, }; + if (compareBytePair(MAGIC_NUMBERS_JBIG2_2, bytePair2)) { + canThrow = true; + return ImageFormats.JBIG2; + } + } else if (compareBytePair(MAGIC_NUMBERS_ICNS, bytePair)) { + canThrow = true; + return ImageFormats.ICNS; + } else if (compareBytePair(MAGIC_NUMBERS_DCX, bytePair)) { + canThrow = true; + return ImageFormats.DCX; + } else if (compareBytePair(MAGIC_NUMBERS_RGBE, bytePair)) { + canThrow = true; + return ImageFormats.RGBE; + } + canThrow = true; + return ImageFormats.UNKNOWN; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param bytes + * Byte array containing an image file. + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile. + */ + public static ICC_Profile getICCProfile(final byte[] bytes) + throws ImageReadException, IOException { + return getICCProfile(bytes, null); + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile.. + */ + public static ICC_Profile getICCProfile(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getICCProfile(new ByteSourceArray(bytes), params); + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile.. + */ + public static ICC_Profile getICCProfile(final InputStream is, final String filename) + throws ImageReadException, IOException { + return getICCProfile(is, filename, null); + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile.. + */ + public static ICC_Profile getICCProfile(final InputStream is, final String filename, + final Map params) throws ImageReadException, IOException { + return getICCProfile(new ByteSourceInputStream(is, filename), params); + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param file + * File containing image data. + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile.. + */ + public static ICC_Profile getICCProfile(final File file) + throws ImageReadException, IOException { + return getICCProfile(file, null); + } + + /** + * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and + * TIFF images. + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ICC_Profile or null if the image contains no ICC + * profile.. + */ + public static ICC_Profile getICCProfile(final File file, final Map params) + throws ImageReadException, IOException { + return getICCProfile(new ByteSourceFile(file), params); + } + + protected static ICC_Profile getICCProfile(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final byte[] bytes = getICCProfileBytes(byteSource, params); + if (bytes == null) { + return null; + } + + final IccProfileParser parser = new IccProfileParser(); + final IccProfileInfo info = parser.getICCProfileInfo(bytes); + if (info == null) { + return null; + } + if (info.issRGB()) { + return null; + } + + return ICC_Profile.getInstance(bytes); + } + + /** + * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD + * (Photoshop) and TIFF images. + *

+ * To parse the result use IccProfileParser or + * ICC_Profile.getInstance(bytes). + *

+ * + * @param bytes + * Byte array containing an image file. + * @return A byte array. + * @see IccProfileParser + * @see ICC_Profile + */ + public static byte[] getICCProfileBytes(final byte[] bytes) + throws ImageReadException, IOException { + return getICCProfileBytes(bytes, null); + } + + /** + * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD + * (Photoshop) and TIFF images. + *

+ * To parse the result use IccProfileParser or + * ICC_Profile.getInstance(bytes). + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return A byte array. + * @see IccProfileParser + * @see ICC_Profile + */ + public static byte[] getICCProfileBytes(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getICCProfileBytes(new ByteSourceArray(bytes), params); + } + + /** + * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD + * (Photoshop) and TIFF images. + *

+ * To parse the result use IccProfileParser or + * ICC_Profile.getInstance(bytes). + *

+ * + * @param file + * File containing image data. + * @return A byte array. + * @see IccProfileParser + * @see ICC_Profile + */ + public static byte[] getICCProfileBytes(final File file) + throws ImageReadException, IOException { + return getICCProfileBytes(file, null); + } + + /** + * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD + * (Photoshop) and TIFF images. + *

+ * To parse the result use IccProfileParser or + * ICC_Profile.getInstance(bytes). + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return A byte array. + * @see IccProfileParser + * @see ICC_Profile + */ + public static byte[] getICCProfileBytes(final File file, final Map params) + throws ImageReadException, IOException { + return getICCProfileBytes(new ByteSourceFile(file), params); + } + + private static byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getICCProfileBytes(byteSource, params); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param filename + * String. + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final String filename, final byte[] bytes, + final Map params) throws ImageReadException, IOException { + return getImageInfo(new ByteSourceArray(filename, bytes), params); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param filename + * String. + * @param bytes + * Byte array containing an image file. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final String filename, final byte[] bytes) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceArray(filename, bytes), null); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final InputStream is, final String filename) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceInputStream(is, filename), null); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final InputStream is, final String filename, + final Map params) throws ImageReadException, IOException { + return getImageInfo(new ByteSourceInputStream(is, filename), params); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param bytes + * Byte array containing an image file. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final byte[] bytes) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceArray(bytes), null); + } + + /** + * Parses the "image info" of an image. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceArray(bytes), params); + } + + /** + * Parses the "image info" of an image file. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final File file, final Map params) + throws ImageReadException, IOException { + return getImageInfo(new ByteSourceFile(file), params); + } + + /** + * Parses the "image info" of an image file. + *

+ * "Image info" is a summary of basic information about the image such as: + * width, height, file format, bit depth, color type, etc. + *

+ * Not to be confused with "image metadata." + *

+ * + * @param file + * File containing image data. + * @return An instance of ImageInfo. + * @see ImageInfo + */ + public static ImageInfo getImageInfo(final File file) throws ImageReadException, + IOException { + return getImageInfo(file, null); + } + + private static ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getImageInfo(byteSource, params); + } + + private static ImageParser getImageParser(final ByteSource byteSource) + throws ImageReadException, IOException { + final ImageFormat format = guessFormat(byteSource); + if (!format.equals(ImageFormats.UNKNOWN)) { + + final ImageParser[] imageParsers = ImageParser.getAllImageParsers(); + + for (final ImageParser imageParser : imageParsers) { + if (imageParser.canAcceptType(format)) { + return imageParser; + } + } + } + + final String filename = byteSource.getFilename(); + if (filename != null) { + final ImageParser[] imageParsers = ImageParser.getAllImageParsers(); + + for (final ImageParser imageParser : imageParsers) { + if (imageParser.canAcceptExtension(filename)) { + return imageParser; + } + } + } + + throw new ImageReadException("Can't parse this format."); + } + + /** + * Determines the width and height of an image. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @return The width and height of the image. + */ + public static Dimension getImageSize(final InputStream is, final String filename) + throws ImageReadException, IOException { + return getImageSize(is, filename, null); + } + + /** + * Determines the width and height of an image. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return The width and height of the image. + */ + public static Dimension getImageSize(final InputStream is, final String filename, + final Map params) throws ImageReadException, IOException { + return getImageSize(new ByteSourceInputStream(is, filename), params); + } + + /** + * Determines the width and height of an image. + *

+ * + * @param bytes + * Byte array containing an image file. + * @return The width and height of the image. + */ + public static Dimension getImageSize(final byte[] bytes) + throws ImageReadException, IOException { + return getImageSize(bytes, null); + } + + /** + * Determines the width and height of an image. + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return The width and height of the image. + */ + public static Dimension getImageSize(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getImageSize(new ByteSourceArray(bytes), params); + } + + /** + * Determines the width and height of an image file. + *

+ * + * @param file + * File containing image data. + * @return The width and height of the image. + */ + public static Dimension getImageSize(final File file) throws ImageReadException, + IOException { + return getImageSize(file, null); + } + + /** + * Determines the width and height of an image file. + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return The width and height of the image. + */ + public static Dimension getImageSize(final File file, final Map params) + throws ImageReadException, IOException { + return getImageSize(new ByteSourceFile(file), params); + } + + public static Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getImageSize(byteSource, params); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final InputStream is, final String filename) + throws ImageReadException, IOException { + return getXmpXml(is, filename, null); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final InputStream is, final String filename, final Map params) + throws ImageReadException, IOException { + return getXmpXml(new ByteSourceInputStream(is, filename), params); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param bytes + * Byte array containing an image file. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final byte[] bytes) throws ImageReadException, + IOException { + return getXmpXml(bytes, null); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getXmpXml(new ByteSourceArray(bytes), params); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param file + * File containing image data. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final File file) throws ImageReadException, + IOException { + return getXmpXml(file, null); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final File file, final Map params) + throws ImageReadException, IOException { + return getXmpXml(new ByteSourceFile(file), params); + } + + /** + * Extracts the embedded XML metadata as an XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + public static String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getXmpXml(byteSource, params); + } + + /** + * Parses the metadata of an image. This metadata depends on the format of + * the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param bytes + * Byte array containing an image file. + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final byte[] bytes) + throws ImageReadException, IOException { + return getMetadata(bytes, null); + } + + /** + * Parses the metadata of an image. This metadata depends on the format of + * the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param bytes + * Byte array containing an image file. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getMetadata(new ByteSourceArray(bytes), params); + } + + /** + * Parses the metadata of an image file. This metadata depends on the format + * of the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final InputStream is, final String filename) + throws ImageReadException, IOException { + return getMetadata(is, filename, null); + } + + /** + * Parses the metadata of an image file. This metadata depends on the format + * of the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param is + * InputStream from which to read image data. + * @param filename + * Filename associated with image data (optional). + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final InputStream is, final String filename, + final Map params) throws ImageReadException, IOException { + return getMetadata(new ByteSourceInputStream(is, filename), params); + } + + /** + * Parses the metadata of an image file. This metadata depends on the format + * of the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param file + * File containing image data. + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final File file) + throws ImageReadException, IOException { + return getMetadata(file, null); + } + + /** + * Parses the metadata of an image file. This metadata depends on the format + * of the image. + *

+ * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may + * contain comments. TIFF files may contain metadata. + *

+ * The instance of IImageMetadata returned by getMetadata() should be upcast + * (depending on image format). + *

+ * Not to be confused with "image info." + *

+ * + * @param file + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return An instance of IImageMetadata. + * @see IImageMetadata + */ + public static IImageMetadata getMetadata(final File file, final Map params) + throws ImageReadException, IOException { + return getMetadata(new ByteSourceFile(file), params); + } + + private static IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getMetadata(byteSource, params); + } + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified byte array to a string. + * @param bytes A valid array of bytes. + * @return A valid string. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + public static String dumpImageFile(final byte[] bytes) throws ImageReadException, + IOException { + return dumpImageFile(new ByteSourceArray(bytes)); + } + + /** + * Write the ImageInfo and format-specific information for the image + * content of the specified file to a string. + * @param file A valid file reference. + * @return A valid string. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + public static String dumpImageFile(final File file) throws ImageReadException, + IOException { + return dumpImageFile(new ByteSourceFile(file)); + } + + private static String dumpImageFile(final ByteSource byteSource) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.dumpImageFile(byteSource); + } + + /** + * Attempts to determine the image format of the specified data and + * evaluates its format compliance. This method + * returns a FormatCompliance object which includes information + * about the data's compliance to a specific format. + * @param bytes a valid array of bytes containing image data. + * @return if successful, a valid FormatCompliance object. + * @throws ImageReadException in the event of unreadable data. + * @throws IOException in the event of an unrecoverable I/O condition. + */ + public static FormatCompliance getFormatCompliance(final byte[] bytes) + throws ImageReadException, IOException { + return getFormatCompliance(new ByteSourceArray(bytes)); + } + + /** + * Attempts to determine the image format of the specified data and + * evaluates its format compliance. This method + * returns a FormatCompliance object which includes information + * about the data's compliance to a specific format. + * @param file valid file containing image data + * @return if successful, a valid FormatCompliance object. + * @throws ImageReadException in the event of unreadable data. + * @throws IOException in the event of an unrecoverable I/O condition. + */ + public static FormatCompliance getFormatCompliance(final File file) + throws ImageReadException, IOException { + return getFormatCompliance(new ByteSourceFile(file)); + } + + private static FormatCompliance getFormatCompliance(final ByteSource byteSource) + throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getFormatCompliance(byteSource); + } + + /** + * Gets all images specified by the InputStream (some + * formats may include multiple images within a single data source). + * @param is A valid InputStream + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + public static List getAllBufferedImages(final InputStream is, + final String filename) throws ImageReadException, IOException { + return getAllBufferedImages(new ByteSourceInputStream(is, filename)); + } + + /** + * Gets all images specified by the byte array (some + * formats may include multiple images within a single data source). + * @param bytes a valid array of bytes + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + public static List getAllBufferedImages(final byte[] bytes) + throws ImageReadException, IOException { + return getAllBufferedImages(new ByteSourceArray(bytes)); + } + + /** + * Gets all images specified by the file (some + * formats may include multiple images within a single data source). + * @param file A reference to a valid data file. + * @return A valid (potentially empty) list of BufferedImage objects. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + public static List getAllBufferedImages(final File file) + throws ImageReadException, IOException { + return getAllBufferedImages(new ByteSourceFile(file)); + } + + + private static List getAllBufferedImages( + final ByteSource byteSource) throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + + return imageParser.getAllBufferedImages(byteSource); + } + + + /** + * Reads the first image from an InputStream. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param is a valid ImageStream from which to read data. + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + + public static BufferedImage getBufferedImage(final InputStream is) + throws ImageReadException, IOException { + return getBufferedImage(is, null); + } + + + + /** + * Reads the first image from an InputStream + * using data-processing options specified through a parameters + * map. Options may be configured using the ImagingContants + * interface or the various format-specific implementations provided + * by this package. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param is a valid ImageStream from which to read data. + * @param params an optional parameters map specifying options + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + public static BufferedImage getBufferedImage(final InputStream is, final Map params) + throws ImageReadException, IOException { + String filename = null; + if (params != null && params.containsKey(PARAM_KEY_FILENAME)) { + filename = (String) params.get(PARAM_KEY_FILENAME); + } + return getBufferedImage(new ByteSourceInputStream(is, filename), params); + } + + /** + * Reads the first image from a byte array. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param bytes a valid array of bytes from which to read data. + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + public static BufferedImage getBufferedImage(final byte[] bytes) + throws ImageReadException, IOException { + return getBufferedImage(new ByteSourceArray(bytes), null); + } + + + /** + * Reads the first image from a byte array + * using data-processing options specified through a parameters + * map. Options may be configured using the ImagingContants + * interface or the various format-specific implementations provided + * by this package. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param bytes a valid array of bytes from which to read data. + * @param params an optional parameters map specifying options. + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + public static BufferedImage getBufferedImage(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + return getBufferedImage(new ByteSourceArray(bytes), params); + } + + + + + /** + * Reads the first image from a file. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param file a valid reference to a file containing image data. + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + public static BufferedImage getBufferedImage(final File file) + throws ImageReadException, IOException { + return getBufferedImage(new ByteSourceFile(file), null); + } + + + /** + * Reads the first image from a file + * using data-processing options specified through a parameters + * map. Options may be configured using the ImagingContants + * interface or the various format-specific implementations provided + * by this package. + *

+ * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param file a valid reference to a file containing image data. + * @return if successful, a valid buffered image + * @throws ImageReadException in the event of a processing error + * while reading an image (i.e. a format violation, etc.). + * @throws IOException in the event of an unrecoverable I/O exception. + */ + public static BufferedImage getBufferedImage(final File file, final Map params) + throws ImageReadException, IOException { + return getBufferedImage(new ByteSourceFile(file), params); + } + + + + private static BufferedImage getBufferedImage(final ByteSource byteSource, + Map params) throws ImageReadException, IOException { + final ImageParser imageParser = getImageParser(byteSource); + if (null == params) { + params = new HashMap(); + } + + return imageParser.getBufferedImage(byteSource, params); + } + + /** + * Writes the content of a BufferedImage to a file using the specified + * image format. Specifications for storing the file (such as data compression, + * color models, metadata tags, etc.) may be specified using an optional + * parameters map. These specifications are defined in the ImagingConstants + * interface or in various format-specific implementations. + *

+ * Image writing is not supported for all graphics formats. + * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param src a valid BufferedImage object + * @param file the file to which the output image is to be written + * @param format the format in which the output image is to be written + * @param params an optional parameters map (nulls permitted) + * @throws ImageWriteException in the event of a format violation, + * unsupported image format, etc. + * @throws IOException in the event of an unrecoverable I/O exception. + * @see ImagingConstants + */ + public static void writeImage(final BufferedImage src, final File file, + final ImageFormat format, final Map params) throws ImageWriteException, + IOException { + OutputStream os = null; + boolean canThrow = false; + try { + os = new FileOutputStream(file); + os = new BufferedOutputStream(os); + + writeImage(src, os, format, params); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, os); + } + } + + + /** + * Writes the content of a BufferedImage to a byte array using the specified + * image format. Specifications for storing the file (such as data compression, + * color models, metadata tags, etc.) may be specified using an optional + * parameters map. These specifications are defined in the ImagingConstants + * interface or in various format-specific implementations. + *

+ * Image writing is not supported for all graphics formats. + * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param src a valid BufferedImage object + * @param format the format in which the output image is to be written + * @param params an optional parameters map (nulls permitted) + * @return if successful, a valid array of bytes. + * @throws ImageWriteException in the event of a format violation, + * unsupported image format, etc. + * @throws IOException in the event of an unrecoverable I/O exception. + * @see ImagingConstants + */ + public static byte[] writeImageToBytes(final BufferedImage src, + final ImageFormat format, final Map params) throws ImageWriteException, + IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + writeImage(src, os, format, params); + + return os.toByteArray(); + } + + + /** + * Writes the content of a BufferedImage to an OutputStream using the specified + * image format. Specifications for storing the file (such as data compression, + * color models, metadata tags, etc.) may be specified using an optional + * parameters map. These specifications are defined in the ImagingConstants + * interface or in various format-specific implementations. + *

+ * Image writing is not supported for all graphics formats. + * For the most recent information on support for specific formats, refer to + * Format Support + * at the main project development web site. While the Apache Commons + * Imaging package does not fully support all formats, it can read + * image info, metadata and ICC profiles from all image formats that + * provide this data. + * @param src a valid BufferedImage object + * @param os the OutputStream to which the output image is to be written + * @param format the format in which the output image is to be written + * @param params an optional parameters map (nulls permitted) + * @throws ImageWriteException in the event of a format violation, + * unsupported image format, etc. + * @throws IOException in the event of an unrecoverable I/O exception. + * @see ImagingConstants + */ + public static void writeImage(final BufferedImage src, final OutputStream os, + final ImageFormat format, Map params) throws ImageWriteException, + IOException { + final ImageParser[] imageParsers = ImageParser.getAllImageParsers(); + + // make sure params are non-null + if (params == null) { + params = new HashMap(); + } + + params.put(PARAM_KEY_FORMAT, format); + + ImageParser imageParser = null; + for (final ImageParser imageParser2 : imageParsers) { + if (imageParser2.canAcceptType(format)) { + imageParser = imageParser2; + break; + } + } + if (imageParser != null) { + imageParser.writeImage(src, os, params); + } else { + throw new ImageWriteException("Unknown Format: " + format); + } + } + +} diff --git a/src/main/java/org/apache/sanselan/SanselanConstants.java b/src/main/java/org/apache/commons/imaging/ImagingConstants.java similarity index 58% rename from src/main/java/org/apache/sanselan/SanselanConstants.java rename to src/main/java/org/apache/commons/imaging/ImagingConstants.java index 6e266c2..abdb696 100644 --- a/src/main/java/org/apache/sanselan/SanselanConstants.java +++ b/src/main/java/org/apache/commons/imaging/ImagingConstants.java @@ -1,108 +1,133 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.InputStream; - -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -public interface SanselanConstants -{ - /** - * Parameter key. Applies to read and write operations. - *

- * Valid values: Boolean.TRUE and Boolean.FALSE. - */ - public static final String PARAM_KEY_VERBOSE = "VERBOSE"; - - /** - * Parameter key. Used to hint the filename when reading from a byte array - * or InputStream. The filename hint can help disambiguate what file the - * image format. - *

- * Applies to read operations. - *

- * Valid values: filename as string - *

- * - * @see InputStream - */ - public static final String PARAM_KEY_FILENAME = "FILENAME"; - - /** - * Parameter key. Used in write operations to indicate desired image format. - *

- * Valid values: Any format defined in ImageFormat, such as - * ImageFormat.IMAGE_FORMAT_PNG. - *

- * - * @see ImageFormat - */ - public static final String PARAM_KEY_FORMAT = "FORMAT"; - - /** - * Parameter key. Used in write operations to indicate desired compression - * algorithm. - *

- * Currently only applies to writing TIFF image files. - *

- * Valid values: TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED, - * TiffConstants.TIFF_COMPRESSION_LZW, - * TiffConstants.TIFF_COMPRESSION_PACKBITS. - *

- * - * @see TiffConstants - */ - public static final String PARAM_KEY_COMPRESSION = "COMPRESSION"; - - public static final String BUFFERED_IMAGE_FACTORY = "BUFFERED_IMAGE_FACTORY"; - - /** - * Parameter key. Indicates whether to read embedded thumbnails. - *

- * Only applies to read EXIF metadata from JPEG/JFIF files. - *

- * Valid values: Boolean.TRUE and Boolean.FALSE. - *

- * - * @see TiffConstants - */ - public static final String PARAM_KEY_READ_THUMBNAILS = "READ_THUMBNAILS"; - - /** - * Parameter key. Indicates whether to throw exceptions when parsing invalid - * files, or whether to tolerate small problems. - *

- * Valid values: Boolean.TRUE and Boolean.FALSE. Default value: - * Boolean.FALSE. - *

- * - * @see TiffConstants - */ - public static final String PARAM_KEY_STRICT = "STRICT"; - - /** - * Parameter key. - * - * Only used when writing images. - *

- * Valid values: String of XMP XML. - *

- */ - public static final String PARAM_KEY_XMP_XML = "XMP_XML"; - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * Defines constants that may be used in passing options to + * ImageParser read/write implementations, the utility routines + * implemented in the Imaging class, and throughout the + * Apache Commons Imaging package. Individual ImageParser + * implementations may define their own format-specific options. + */ +public interface ImagingConstants { + /** + * Parameter key. Applies to read and write operations. + *

+ * Valid values: Boolean.TRUE and Boolean.FALSE. + */ + String PARAM_KEY_VERBOSE = "VERBOSE"; + + /** + * Parameter key. Used to hint the filename when reading from a byte array + * or InputStream. The filename hint can help disambiguate what file the + * image format. + *

+ * Applies to read operations. + *

+ * Valid values: filename as string + *

+ * + * @see java.io.InputStream + */ + String PARAM_KEY_FILENAME = "FILENAME"; + + /** + * Parameter key. Used in write operations to indicate desired image format. + *

+ * Valid values: Any format defined in ImageFormat, such as + * ImageFormat.IMAGE_FORMAT_PNG. + *

+ * + * @see org.apache.commons.imaging.ImageFormats + */ + String PARAM_KEY_FORMAT = "FORMAT"; + + /** + * Parameter key. Used in write operations to indicate desired compression + * algorithm. + *

+ * Currently only applies to writing TIFF image files. + *

+ * Valid values: TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED, + * TiffConstants.TIFF_COMPRESSION_CCITT_1D, + * TiffConstants.TIFF_COMPRESSION_LZW, + * TiffConstants.TIFF_COMPRESSION_PACKBITS. + *

+ * + * @see org.apache.commons.imaging.formats.tiff.constants.TiffConstants + */ + String PARAM_KEY_COMPRESSION = "COMPRESSION"; + + String BUFFERED_IMAGE_FACTORY = "BUFFERED_IMAGE_FACTORY"; + + /** + * Parameter key. Indicates whether to read embedded thumbnails. + *

+ * Only applies to read EXIF metadata from JPEG/JFIF files. + *

+ * Valid values: Boolean.TRUE and Boolean.FALSE. + *

+ * + * @see org.apache.commons.imaging.formats.tiff.constants.TiffConstants + */ + String PARAM_KEY_READ_THUMBNAILS = "READ_THUMBNAILS"; + + /** + * Parameter key. Indicates whether to throw exceptions when parsing invalid + * files, or whether to tolerate small problems. + *

+ * Valid values: Boolean.TRUE and Boolean.FALSE. Default value: + * Boolean.FALSE. + *

+ * + * @see org.apache.commons.imaging.formats.tiff.constants.TiffConstants + */ + String PARAM_KEY_STRICT = "STRICT"; + + /** + * Parameter key. + * + * Only used when writing images. + *

+ * Valid values: TiffOutputSet to write into the image's EXIF metadata. + *

+ * + * @see org.apache.commons.imaging.formats.tiff.write.TiffOutputSet + */ + String PARAM_KEY_EXIF = "EXIF"; + + /** + * Parameter key. + * + * Only used when writing images. + *

+ * Valid values: String of XMP XML. + *

+ */ + String PARAM_KEY_XMP_XML = "XMP_XML"; + + /** + * Parameter key. Used in write operations to indicate the desired pixel + * density (DPI), and/or aspect ratio. + *

+ * Valid values: PixelDensity + *

+ * + * @see org.apache.commons.imaging.PixelDensity + */ + String PARAM_KEY_PIXEL_DENSITY = "PIXEL_DENSITY"; +} diff --git a/src/main/java/org/apache/commons/imaging/ImagingException.java b/src/main/java/org/apache/commons/imaging/ImagingException.java new file mode 100644 index 0000000..1c5f9fb --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/ImagingException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * The base class for implementing custom exceptions in the + * Apache Commons Imaging package. + */ +public class ImagingException extends Exception { + private static final long serialVersionUID = -1L; + + public ImagingException(final String message) { + super(message); + } + + public ImagingException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/apache/commons/imaging/PixelDensity.java b/src/main/java/org/apache/commons/imaging/PixelDensity.java new file mode 100644 index 0000000..afecd26 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/PixelDensity.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging; + +/** + * Used to specify pixel density and physical dimensions when reading or + * storing image information. + */ +public final class PixelDensity { + private final double horizontalDensity; + private final double verticalDensity; + // / One-tenth of a millimetre units. + private final int unitLength; + + private PixelDensity(final double horizontalDensity, final double verticalDensity, + final int unitLength) { + this.horizontalDensity = horizontalDensity; + this.verticalDensity = verticalDensity; + this.unitLength = unitLength; + } + + public static PixelDensity createUnitless(final double x, final double y) { + return new PixelDensity(x, y, 0); + } + + public static PixelDensity createFromPixelsPerInch(final double x, final double y) { + return new PixelDensity(x, y, 254); + } + + public static PixelDensity createFromPixelsPerMetre(final double x, final double y) { + return new PixelDensity(x, y, 10000); + } + + public static PixelDensity createFromPixelsPerCentimetre(final double x, final double y) { + return new PixelDensity(x, y, 100); + } + + public boolean isUnitless() { + return unitLength == 0; + } + + public boolean isInInches() { + return unitLength == 254; + } + + public boolean isInCentimetres() { + return unitLength == 100; + } + + public boolean isInMetres() { + return unitLength == 10000; + } + + public double getRawHorizontalDensity() { + return horizontalDensity; + } + + public double getRawVerticalDensity() { + return verticalDensity; + } + + public double horizontalDensityInches() { + if (isInInches()) { + return horizontalDensity; + } else { + return horizontalDensity * 254 / unitLength; + } + } + + public double verticalDensityInches() { + if (isInInches()) { + return verticalDensity; + } else { + return verticalDensity * 254 / unitLength; + } + } + + public double horizontalDensityMetres() { + if (isInMetres()) { + return horizontalDensity; + } else { + return horizontalDensity * 10000 / unitLength; + } + } + + public double verticalDensityMetres() { + if (isInMetres()) { + return verticalDensity; + } else { + return verticalDensity * 10000 / unitLength; + } + } + + public double horizontalDensityCentimetres() { + if (isInCentimetres()) { + return horizontalDensity; + } else { + return horizontalDensity * 100 / unitLength; + } + } + + public double verticalDensityCentimetres() { + if (isInCentimetres()) { + return verticalDensity; + } else { + return verticalDensity * 100 / unitLength; + } + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorCIELab.java b/src/main/java/org/apache/commons/imaging/color/ColorCieLab.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorCIELab.java rename to src/main/java/org/apache/commons/imaging/color/ColorCieLab.java index d709ebc..7066735 100644 --- a/src/main/java/org/apache/sanselan/color/ColorCIELab.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorCieLab.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorCIELab -{ - public final double L, a, b; - - public ColorCIELab(double l, double a, double b) - { - L = l; - this.a = a; - this.b = b; - } - - public final String toString() - { - return "{L: " + L + ", a: " + a + ", b: " + b + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorCieLab { + public final double L; + public final double a; + public final double b; + + public ColorCieLab(final double l, final double a, final double b) { + L = l; + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return "{L: " + L + ", a: " + a + ", b: " + b + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorCIELCH.java b/src/main/java/org/apache/commons/imaging/color/ColorCieLch.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorCIELCH.java rename to src/main/java/org/apache/commons/imaging/color/ColorCieLch.java index 47bbfcf..3646dc7 100644 --- a/src/main/java/org/apache/sanselan/color/ColorCIELCH.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorCieLch.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorCIELCH -{ - public final double L, C, H; - - public ColorCIELCH(double l, double C, double H) - { - L = l; - this.C = C; - this.H = H; - } - - public String toString() - { - return "{L: " + L + ", C: " + C + ", H: " + H + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorCieLch { + public final double L; + public final double C; + public final double H; + + public ColorCieLch(final double l, final double C, final double H) { + L = l; + this.C = C; + this.H = H; + } + + @Override + public String toString() { + return "{L: " + L + ", C: " + C + ", H: " + H + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorCIELuv.java b/src/main/java/org/apache/commons/imaging/color/ColorCieLuv.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorCIELuv.java rename to src/main/java/org/apache/commons/imaging/color/ColorCieLuv.java index 597b93c..dc04f8f 100644 --- a/src/main/java/org/apache/sanselan/color/ColorCIELuv.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorCieLuv.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorCIELuv -{ - public final double L, u, v; - - public ColorCIELuv(double l, double u, double v) - { - L = l; - this.u = u; - this.v = v; - } - - public String toString() - { - return "{L: " + L + ", u: " + u + ", v: " + v + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorCieLuv { + public final double L; + public final double u; + public final double v; + + public ColorCieLuv(final double l, final double u, final double v) { + L = l; + this.u = u; + this.v = v; + } + + @Override + public String toString() { + return "{L: " + L + ", u: " + u + ", v: " + v + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorCMY.java b/src/main/java/org/apache/commons/imaging/color/ColorCmy.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorCMY.java rename to src/main/java/org/apache/commons/imaging/color/ColorCmy.java index ca79290..f0a2ebf 100644 --- a/src/main/java/org/apache/sanselan/color/ColorCMY.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorCmy.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorCMY -{ - public final double C, M, Y; - - public ColorCMY(double C, double M, double Y) - { - this.C = C; - this.M = M; - this.Y = Y; - } - - public final String toString() - { - return "{C: " + C + ", M: " + M + ", Y: " + Y + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorCmy { + public final double C; + public final double M; + public final double Y; + + public ColorCmy(final double C, final double M, final double Y) { + this.C = C; + this.M = M; + this.Y = Y; + } + + @Override + public String toString() { + return "{C: " + C + ", M: " + M + ", Y: " + Y + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorCMYK.java b/src/main/java/org/apache/commons/imaging/color/ColorCmyk.java similarity index 75% rename from src/main/java/org/apache/sanselan/color/ColorCMYK.java rename to src/main/java/org/apache/commons/imaging/color/ColorCmyk.java index 7b7e208..ebb239d 100644 --- a/src/main/java/org/apache/sanselan/color/ColorCMYK.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorCmyk.java @@ -1,35 +1,36 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorCMYK -{ - public final double C, M, Y, K; - - public ColorCMYK(double C, double M, double Y, double K) - { - this.C = C; - this.M = M; - this.Y = Y; - this.K = K; - } - - public final String toString() - { - return "{C: " + C + ", M: " + M + ", Y: " + Y + ", K: " + K + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorCmyk { + public final double C; + public final double M; + public final double Y; + public final double K; + + public ColorCmyk(final double C, final double M, final double Y, final double K) { + this.C = C; + this.M = M; + this.Y = Y; + this.K = K; + } + + @Override + public String toString() { + return "{C: " + C + ", M: " + M + ", Y: " + Y + ", K: " + K + "}"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/color/ColorConversions.java b/src/main/java/org/apache/commons/imaging/color/ColorConversions.java new file mode 100644 index 0000000..608313a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/color/ColorConversions.java @@ -0,0 +1,752 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + + +public final class ColorConversions { + private static final double REF_X = 95.047; // Observer= 2°, Illuminant= D65 + private static final double REF_Y = 100.000; + private static final double REF_Z = 108.883; + + private ColorConversions() { + } + + public static ColorCieLab convertXYZtoCIELab(final ColorXyz xyz) { + return convertXYZtoCIELab(xyz.X, xyz.Y, xyz.Z); + } + + public static ColorCieLab convertXYZtoCIELab(final double X, final double Y, + final double Z) { + + double var_X = X / REF_X; // REF_X = 95.047 Observer= 2°, Illuminant= + // D65 + double var_Y = Y / REF_Y; // REF_Y = 100.000 + double var_Z = Z / REF_Z; // REF_Z = 108.883 + + if (var_X > 0.008856) { + var_X = Math.pow(var_X, (1 / 3.0)); + } else { + var_X = (7.787 * var_X) + (16 / 116.0); + } + if (var_Y > 0.008856) { + var_Y = Math.pow(var_Y, 1 / 3.0); + } else { + var_Y = (7.787 * var_Y) + (16 / 116.0); + } + if (var_Z > 0.008856) { + var_Z = Math.pow(var_Z, 1 / 3.0); + } else { + var_Z = (7.787 * var_Z) + (16 / 116.0); + } + + final double L = (116 * var_Y) - 16; + final double a = 500 * (var_X - var_Y); + final double b = 200 * (var_Y - var_Z); + return new ColorCieLab(L, a, b); + } + + public static ColorXyz convertCIELabtoXYZ(final ColorCieLab cielab) { + return convertCIELabtoXYZ(cielab.L, cielab.a, cielab.b); + } + + public static ColorXyz convertCIELabtoXYZ(final double L, final double a, final double b) { + double var_Y = (L + 16) / 116.0; + double var_X = a / 500 + var_Y; + double var_Z = var_Y - b / 200.0; + + if (Math.pow(var_Y, 3) > 0.008856) { + var_Y = Math.pow(var_Y, 3); + } else { + var_Y = (var_Y - 16 / 116.0) / 7.787; + } + if (Math.pow(var_X, 3) > 0.008856) { + var_X = Math.pow(var_X, 3); + } else { + var_X = (var_X - 16 / 116.0) / 7.787; + } + if (Math.pow(var_Z, 3) > 0.008856) { + var_Z = Math.pow(var_Z, 3); + } else { + var_Z = (var_Z - 16 / 116.0) / 7.787; + } + + final double X = REF_X * var_X; // REF_X = 95.047 Observer= 2°, Illuminant= + // D65 + final double Y = REF_Y * var_Y; // REF_Y = 100.000 + final double Z = REF_Z * var_Z; // REF_Z = 108.883 + + return new ColorXyz(X, Y, Z); + } + + public static ColorHunterLab convertXYZtoHunterLab(final ColorXyz xyz) { + return convertXYZtoHunterLab(xyz.X, xyz.Y, xyz.Z); + } + + public static ColorHunterLab convertXYZtoHunterLab(final double X, + final double Y, final double Z) { + final double L = 10 * Math.sqrt(Y); + final double a = 17.5 * (((1.02 * X) - Y) / Math.sqrt(Y)); + final double b = 7 * ((Y - (0.847 * Z)) / Math.sqrt(Y)); + + return new ColorHunterLab(L, a, b); + } + + public static ColorXyz convertHunterLabtoXYZ(final ColorHunterLab cielab) { + return convertHunterLabtoXYZ(cielab.L, cielab.a, cielab.b); + } + + public static ColorXyz convertHunterLabtoXYZ(final double L, final double a, + final double b) { + final double var_Y = L / 10; + final double var_X = a / 17.5 * L / 10; + final double var_Z = b / 7 * L / 10; + + final double Y = Math.pow(var_Y, 2); + final double X = (var_X + Y) / 1.02; + final double Z = -(var_Z - Y) / 0.847; + + return new ColorXyz(X, Y, Z); + } + + public static int convertXYZtoRGB(final ColorXyz xyz) { + return convertXYZtoRGB(xyz.X, xyz.Y, xyz.Z); + } + + public static int convertXYZtoRGB(final double X, final double Y, final double Z) { + // Observer = 2°, Illuminant = D65 + final double var_X = X / 100.0; // Where X = 0 ÷ 95.047 + final double var_Y = Y / 100.0; // Where Y = 0 ÷ 100.000 + final double var_Z = Z / 100.0; // Where Z = 0 ÷ 108.883 + + double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986; + double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415; + double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570; + + if (var_R > 0.0031308) { + var_R = 1.055 * Math.pow(var_R, (1 / 2.4)) - 0.055; + } else { + var_R = 12.92 * var_R; + } + if (var_G > 0.0031308) { + var_G = 1.055 * Math.pow(var_G, (1 / 2.4)) - 0.055; + } else { + var_G = 12.92 * var_G; + } + if (var_B > 0.0031308) { + var_B = 1.055 * Math.pow(var_B, (1 / 2.4)) - 0.055; + } else { + var_B = 12.92 * var_B; + } + + final double R = (var_R * 255); + final double G = (var_G * 255); + final double B = (var_B * 255); + + return convertRGBtoRGB(R, G, B); + } + + public static ColorXyz convertRGBtoXYZ(final int rgb) { + final int r = 0xff & (rgb >> 16); + final int g = 0xff & (rgb >> 8); + final int b = 0xff & (rgb >> 0); + + double var_R = r / 255.0; // Where R = 0 ÷ 255 + double var_G = g / 255.0; // Where G = 0 ÷ 255 + double var_B = b / 255.0; // Where B = 0 ÷ 255 + + if (var_R > 0.04045) { + var_R = Math.pow((var_R + 0.055) / 1.055, 2.4); + } else { + var_R = var_R / 12.92; + } + if (var_G > 0.04045) { + var_G = Math.pow((var_G + 0.055) / 1.055, 2.4); + } else { + var_G = var_G / 12.92; + } + if (var_B > 0.04045) { + var_B = Math.pow((var_B + 0.055) / 1.055, 2.4); + } else { + var_B = var_B / 12.92; + } + + var_R = var_R * 100; + var_G = var_G * 100; + var_B = var_B * 100; + + // Observer. = 2°, Illuminant = D65 + final double X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805; + final double Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722; + final double Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505; + + return new ColorXyz(X, Y, Z); + } + + public static ColorCmy convertRGBtoCMY(final int rgb) { + final int R = 0xff & (rgb >> 16); + final int G = 0xff & (rgb >> 8); + final int B = 0xff & (rgb >> 0); + + // RGB values = 0 ÷ 255 + // CMY values = 0 ÷ 1 + + final double C = 1 - (R / 255.0); + final double M = 1 - (G / 255.0); + final double Y = 1 - (B / 255.0); + + return new ColorCmy(C, M, Y); + } + + public static int convertCMYtoRGB(final ColorCmy cmy) { + // From Ghostscript's gdevcdj.c: + // * Ghostscript: R = (1.0 - C) * (1.0 - K) + // * Adobe: R = 1.0 - min(1.0, C + K) + // and similarly for G and B. + // This is Ghostscript's formula with K = 0. + + // CMY values = 0 ÷ 1 + // RGB values = 0 ÷ 255 + + final double R = (1 - cmy.C) * 255.0; + final double G = (1 - cmy.M) * 255.0; + final double B = (1 - cmy.Y) * 255.0; + + return convertRGBtoRGB(R, G, B); + } + + public static ColorCmyk convertCMYtoCMYK(final ColorCmy cmy) { + // Where CMYK and CMY values = 0 ÷ 1 + + double C = cmy.C; + double M = cmy.M; + double Y = cmy.Y; + + double var_K = 1.0; + + if (C < var_K) { + var_K = C; + } + if (M < var_K) { + var_K = M; + } + if (Y < var_K) { + var_K = Y; + } + if (var_K == 1) { // Black + C = 0; + M = 0; + Y = 0; + } else { + C = (C - var_K) / (1 - var_K); + M = (M - var_K) / (1 - var_K); + Y = (Y - var_K) / (1 - var_K); + } + return new ColorCmyk(C, M, Y, var_K); + } + + public static ColorCmy convertCMYKtoCMY(final ColorCmyk cmyk) { + return convertCMYKtoCMY(cmyk.C, cmyk.M, cmyk.Y, cmyk.K); + } + + public static ColorCmy convertCMYKtoCMY(double C, double M, double Y, + final double K) { + // Where CMYK and CMY values = 0 ÷ 1 + + C = (C * (1 - K) + K); + M = (M * (1 - K) + K); + Y = (Y * (1 - K) + K); + + return new ColorCmy(C, M, Y); + } + + public static int convertCMYKtoRGB(final int c, final int m, final int y, final int k) + // throws ImageReadException, IOException + { + final double C = c / 255.0; + final double M = m / 255.0; + final double Y = y / 255.0; + final double K = k / 255.0; + + return convertCMYtoRGB(convertCMYKtoCMY(C, M, Y, K)); + } + + public static ColorHsl convertRGBtoHSL(final int rgb) { + + final int R = 0xff & (rgb >> 16); + final int G = 0xff & (rgb >> 8); + final int B = 0xff & (rgb >> 0); + + final double var_R = (R / 255.0); // Where RGB values = 0 ÷ 255 + final double var_G = (G / 255.0); + final double var_B = (B / 255.0); + + final double var_Min = Math.min(var_R, Math.min(var_G, var_B)); // Min. value + // of RGB + double var_Max; + boolean maxIsR = false; + boolean maxIsG = false; + if (var_R >= var_G && var_R >= var_B) { + var_Max = var_R; + maxIsR = true; + } else if (var_G > var_B) { + var_Max = var_G; + maxIsG = true; + } else { + var_Max = var_B; + } + final double del_Max = var_Max - var_Min; // Delta RGB value + + final double L = (var_Max + var_Min) / 2.0; + + double H, S; + // Debug.debug("del_Max", del_Max); + if (del_Max == 0) { + // This is a gray, no chroma... + + H = 0; // HSL results = 0 ÷ 1 + S = 0; + } else { + // Chromatic data... + + // Debug.debug("L", L); + + if (L < 0.5) { + S = del_Max / (var_Max + var_Min); + } else { + S = del_Max / (2 - var_Max - var_Min); + } + + // Debug.debug("S", S); + + final double del_R = (((var_Max - var_R) / 6) + (del_Max / 2)) / del_Max; + final double del_G = (((var_Max - var_G) / 6) + (del_Max / 2)) / del_Max; + final double del_B = (((var_Max - var_B) / 6) + (del_Max / 2)) / del_Max; + + if (maxIsR) { + H = del_B - del_G; + } else if (maxIsG) { + H = (1 / 3.0) + del_R - del_B; + } else { + H = (2 / 3.0) + del_G - del_R; + } + + // Debug.debug("H1", H); + + if (H < 0) { + H += 1; + } + if (H > 1) { + H -= 1; + } + + // Debug.debug("H2", H); + } + + return new ColorHsl(H, S, L); + } + + public static int convertHSLtoRGB(final ColorHsl hsl) { + return convertHSLtoRGB(hsl.H, hsl.S, hsl.L); + } + + public static int convertHSLtoRGB(final double H, final double S, final double L) { + double R, G, B; + + if (S == 0) { + // HSL values = 0 ÷ 1 + R = L * 255; // RGB results = 0 ÷ 255 + G = L * 255; + B = L * 255; + } else { + double var_2; + + if (L < 0.5) { + var_2 = L * (1 + S); + } else { + var_2 = (L + S) - (S * L); + } + + final double var_1 = 2 * L - var_2; + + R = 255 * convertHuetoRGB(var_1, var_2, H + (1 / 3.0)); + G = 255 * convertHuetoRGB(var_1, var_2, H); + B = 255 * convertHuetoRGB(var_1, var_2, H - (1 / 3.0)); + } + + return convertRGBtoRGB(R, G, B); + } + + private static double convertHuetoRGB(final double v1, final double v2, double vH) { + if (vH < 0) { + vH += 1; + } + if (vH > 1) { + vH -= 1; + } + if ((6 * vH) < 1) { + return (v1 + (v2 - v1) * 6 * vH); + } + if ((2 * vH) < 1) { + return (v2); + } + if ((3 * vH) < 2) { + return (v1 + (v2 - v1) * ((2 / 3.0) - vH) * 6); + } + return (v1); + } + + public static ColorHsv convertRGBtoHSV(final int rgb) { + final int R = 0xff & (rgb >> 16); + final int G = 0xff & (rgb >> 8); + final int B = 0xff & (rgb >> 0); + + final double var_R = (R / 255.0); // RGB values = 0 ÷ 255 + final double var_G = (G / 255.0); + final double var_B = (B / 255.0); + + final double var_Min = Math.min(var_R, Math.min(var_G, var_B)); // Min. value + // of RGB + boolean maxIsR = false; + boolean maxIsG = false; + double var_Max; + if (var_R >= var_G && var_R >= var_B) { + var_Max = var_R; + maxIsR = true; + } else if (var_G > var_B) { + var_Max = var_G; + maxIsG = true; + } else { + var_Max = var_B; + } + final double del_Max = var_Max - var_Min; // Delta RGB value + + final double V = var_Max; + + double H, S; + if (del_Max == 0) { + // This is a gray, no chroma... + H = 0; // HSV results = 0 ÷ 1 + S = 0; + } else { + // Chromatic data... + S = del_Max / var_Max; + + final double del_R = (((var_Max - var_R) / 6) + (del_Max / 2)) / del_Max; + final double del_G = (((var_Max - var_G) / 6) + (del_Max / 2)) / del_Max; + final double del_B = (((var_Max - var_B) / 6) + (del_Max / 2)) / del_Max; + + if (maxIsR) { + H = del_B - del_G; + } else if (maxIsG) { + H = (1 / 3.0) + del_R - del_B; + } else { + H = (2 / 3.0) + del_G - del_R; + } + + if (H < 0) { + H += 1; + } + if (H > 1) { + H -= 1; + } + } + + return new ColorHsv(H, S, V); + } + + public static int convertHSVtoRGB(final ColorHsv HSV) { + return convertHSVtoRGB(HSV.H, HSV.S, HSV.V); + } + + public static int convertHSVtoRGB(final double H, final double S, final double V) { + double R, G, B; + + if (S == 0) { + // HSV values = 0 ÷ 1 + R = V * 255; + G = V * 255; + B = V * 255; + } else { + double var_h = H * 6; + if (var_h == 6) { + var_h = 0; // H must be < 1 + } + final double var_i = Math.floor(var_h); // Or ... var_i = floor( var_h ) + final double var_1 = V * (1 - S); + final double var_2 = V * (1 - S * (var_h - var_i)); + final double var_3 = V * (1 - S * (1 - (var_h - var_i))); + + double var_r, var_g, var_b; + + if (var_i == 0) { + var_r = V; + var_g = var_3; + var_b = var_1; + } else if (var_i == 1) { + var_r = var_2; + var_g = V; + var_b = var_1; + } else if (var_i == 2) { + var_r = var_1; + var_g = V; + var_b = var_3; + } else if (var_i == 3) { + var_r = var_1; + var_g = var_2; + var_b = V; + } else if (var_i == 4) { + var_r = var_3; + var_g = var_1; + var_b = V; + } else { + var_r = V; + var_g = var_1; + var_b = var_2; + } + + R = var_r * 255; // RGB results = 0 ÷ 255 + G = var_g * 255; + B = var_b * 255; + } + + return convertRGBtoRGB(R, G, B); + } + + public static int convertCMYKtoRGB_Adobe(final int sc, final int sm, final int sy, + final int sk) { + final int red = 255 - (sc + sk); + final int green = 255 - (sm + sk); + final int blue = 255 - (sy + sk); + + return convertRGBtoRGB(red, green, blue); + } + + private static double cube(final double f) { + return f * f * f; + } + + private static double square(final double f) { + return f * f; + } + + public static int convertCIELabtoARGBTest(final int cieL, final int cieA, final int cieB) { + double X, Y, Z; + + { + + double var_Y = ((cieL * 100.0 / 255.0) + 16.0) / 116.0; + double var_X = cieA / 500.0 + var_Y; + double var_Z = var_Y - cieB / 200.0; + + final double var_x_cube = cube(var_X); + final double var_y_cube = cube(var_Y); + final double var_z_cube = cube(var_Z); + + if (var_y_cube > 0.008856) { + var_Y = var_y_cube; + } else { + var_Y = (var_Y - 16 / 116.0) / 7.787; + } + + if (var_x_cube > 0.008856) { + var_X = var_x_cube; + } else { + var_X = (var_X - 16 / 116.0) / 7.787; + } + + if (var_z_cube > 0.008856) { + var_Z = var_z_cube; + } else { + var_Z = (var_Z - 16 / 116.0) / 7.787; + } + + // double REF_X = 95.047; + // double REF_Y = 100.000; + // double REF_Z = 108.883; + + X = REF_X * var_X; // REF_X = 95.047 Observer= 2°, Illuminant= D65 + Y = REF_Y * var_Y; // REF_Y = 100.000 + Z = REF_Z * var_Z; // REF_Z = 108.883 + + } + + double R, G, B; + { + final double var_X = X / 100; // X = From 0 to REF_X + final double var_Y = Y / 100; // Y = From 0 to REF_Y + final double var_Z = Z / 100; // Z = From 0 to REF_Y + + double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986; + double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415; + double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570; + + if (var_R > 0.0031308) { + var_R = 1.055 * Math.pow(var_R, (1 / 2.4)) - 0.055; + } else { + var_R = 12.92 * var_R; + } + if (var_G > 0.0031308) { + var_G = 1.055 * Math.pow(var_G, (1 / 2.4)) - 0.055; + } else { + var_G = 12.92 * var_G; + } + + if (var_B > 0.0031308) { + var_B = 1.055 * Math.pow(var_B, (1 / 2.4)) - 0.055; + } else { + var_B = 12.92 * var_B; + } + + R = (var_R * 255); + G = (var_G * 255); + B = (var_B * 255); + } + + return convertRGBtoRGB(R, G, B); + } + + private static int convertRGBtoRGB(final double R, final double G, final double B) { + int red = (int) Math.round(R); + int green = (int) Math.round(G); + int blue = (int) Math.round(B); + + red = Math.min(255, Math.max(0, red)); + green = Math.min(255, Math.max(0, green)); + blue = Math.min(255, Math.max(0, blue)); + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + return rgb; + } + + private static int convertRGBtoRGB(int red, int green, int blue) { + red = Math.min(255, Math.max(0, red)); + green = Math.min(255, Math.max(0, green)); + blue = Math.min(255, Math.max(0, blue)); + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + return rgb; + } + + public static ColorCieLch convertCIELabtoCIELCH(final ColorCieLab cielab) { + return convertCIELabtoCIELCH(cielab.L, cielab.a, cielab.b); + } + + public static ColorCieLch convertCIELabtoCIELCH(final double L, final double a, final double b) { + double var_H = Math.atan2(b, a); // Quadrant by signs + + if (var_H > 0) { + var_H = (var_H / Math.PI) * 180.0; + } else { + var_H = 360 - radian_2_degree(Math.abs(var_H)); + } + + // L = L; + final double C = Math.sqrt(square(a) + square(b)); + final double H = var_H; + + return new ColorCieLch(L, C, H); + } + + public static ColorCieLab convertCIELCHtoCIELab(final ColorCieLch cielch) { + return convertCIELCHtoCIELab(cielch.L, cielch.C, cielch.H); + } + + public static ColorCieLab convertCIELCHtoCIELab(final double L, final double C, final double H) { + // Where CIE-H° = 0 ÷ 360° + + // CIE-L* = CIE-L; + final double a = Math.cos(degree_2_radian(H)) * C; + final double b = Math.sin(degree_2_radian(H)) * C; + + return new ColorCieLab(L, a, b); + } + + public static double degree_2_radian(final double degree) { + return degree * Math.PI / 180.0; + } + + public static double radian_2_degree(final double radian) { + return radian * 180.0 / Math.PI; + } + + public static ColorCieLuv convertXYZtoCIELuv(final ColorXyz xyz) { + return convertXYZtoCIELuv(xyz.X, xyz.Y, xyz.Z); + } + + public static ColorCieLuv convertXYZtoCIELuv(final double X, final double Y, final double Z) { + // problems here with div by zero + + final double var_U = (4 * X) / (X + (15 * Y) + (3 * Z)); + final double var_V = (9 * Y) / (X + (15 * Y) + (3 * Z)); + + // Debug.debug("var_U", var_U); + // Debug.debug("var_V", var_V); + + double var_Y = Y / 100.0; + // Debug.debug("var_Y", var_Y); + + if (var_Y > 0.008856) { + var_Y = Math.pow(var_Y, (1 / 3.0)); + } else { + var_Y = (7.787 * var_Y) + (16 / 116.0); + } + + // Debug.debug("var_Y", var_Y); + + final double ref_U = (4 * REF_X) / (REF_X + (15 * REF_Y) + (3 * REF_Z)); + final double ref_V = (9 * REF_Y) / (REF_X + (15 * REF_Y) + (3 * REF_Z)); + + // Debug.debug("ref_U", ref_U); + // Debug.debug("ref_V", ref_V); + + final double L = (116 * var_Y) - 16; + final double u = 13 * L * (var_U - ref_U); + final double v = 13 * L * (var_V - ref_V); + + return new ColorCieLuv(L, u, v); + } + + public static ColorXyz convertCIELuvtoXYZ(final ColorCieLuv cielch) { + return convertCIELuvtoXYZ(cielch.L, cielch.u, cielch.v); + } + + public static ColorXyz convertCIELuvtoXYZ(final double L, final double u, final double v) { + // problems here with div by zero + + double var_Y = (L + 16) / 116; + if (Math.pow(var_Y, 3) > 0.008856) { + var_Y = Math.pow(var_Y, 3); + } else { + var_Y = (var_Y - 16 / 116) / 7.787; + } + + final double ref_U = (4 * REF_X) / (REF_X + (15 * REF_Y) + (3 * REF_Z)); + final double ref_V = (9 * REF_Y) / (REF_X + (15 * REF_Y) + (3 * REF_Z)); + final double var_U = u / (13 * L) + ref_U; + final double var_V = v / (13 * L) + ref_V; + + final double Y = var_Y * 100; + final double X = -(9 * Y * var_U) / ((var_U - 4) * var_V - var_U * var_V); + final double Z = (9 * Y - (15 * var_V * Y) - (var_V * X)) / (3 * var_V); + + return new ColorXyz(X, Y, Z); + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorHSL.java b/src/main/java/org/apache/commons/imaging/color/ColorHsl.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorHSL.java rename to src/main/java/org/apache/commons/imaging/color/ColorHsl.java index 29bd17c..1c591f2 100644 --- a/src/main/java/org/apache/sanselan/color/ColorHSL.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorHsl.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorHSL -{ - public final double H, S, L; - - public ColorHSL(double h, double s, double v) - { - H = h; - S = s; - L = v; - } - - public final String toString() - { - return "{H: " + H + ", S: " + S + ", L: " + L + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorHsl { + public final double H; + public final double S; + public final double L; + + public ColorHsl(final double h, final double s, final double v) { + H = h; + S = s; + L = v; + } + + @Override + public String toString() { + return "{H: " + H + ", S: " + S + ", L: " + L + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorHSV.java b/src/main/java/org/apache/commons/imaging/color/ColorHsv.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorHSV.java rename to src/main/java/org/apache/commons/imaging/color/ColorHsv.java index 4eba4c1..7d2be7c 100644 --- a/src/main/java/org/apache/sanselan/color/ColorHSV.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorHsv.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorHSV -{ - public final double H, S, V; - - public ColorHSV(double h, double s, double v) - { - H = h; - S = s; - V = v; - } - - public final String toString() - { - return "{H: " + H + ", S: " + S + ", V: " + V + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorHsv { + public final double H; + public final double S; + public final double V; + + public ColorHsv(final double h, final double s, final double v) { + H = h; + S = s; + V = v; + } + + @Override + public String toString() { + return "{H: " + H + ", S: " + S + ", V: " + V + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorHunterLab.java b/src/main/java/org/apache/commons/imaging/color/ColorHunterLab.java similarity index 76% rename from src/main/java/org/apache/sanselan/color/ColorHunterLab.java rename to src/main/java/org/apache/commons/imaging/color/ColorHunterLab.java index e01fabe..ec1eda2 100644 --- a/src/main/java/org/apache/sanselan/color/ColorHunterLab.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorHunterLab.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorHunterLab -{ - public final double L, a, b; - - public ColorHunterLab(double l, double a, double b) - { - L = l; - this.a = a; - this.b = b; - } - - public final String toString() - { - return "{L: " + L + ", a: " + a + ", b: " + b + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorHunterLab { + public final double L; + public final double a; + public final double b; + + public ColorHunterLab(final double l, final double a, final double b) { + L = l; + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return "{L: " + L + ", a: " + a + ", b: " + b + "}"; + } +} diff --git a/src/main/java/org/apache/sanselan/color/ColorXYZ.java b/src/main/java/org/apache/commons/imaging/color/ColorXyz.java similarity index 77% rename from src/main/java/org/apache/sanselan/color/ColorXYZ.java rename to src/main/java/org/apache/commons/imaging/color/ColorXyz.java index 4a37d25..ce99bbf 100644 --- a/src/main/java/org/apache/sanselan/color/ColorXYZ.java +++ b/src/main/java/org/apache/commons/imaging/color/ColorXyz.java @@ -1,34 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -public final class ColorXYZ -{ - public final double X, Y, Z; - - public ColorXYZ(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - public final String toString() - { - return "{X: " + X + ", Y: " + Y + ", Z: " + Z + "}"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.color; + +public final class ColorXyz { + public final double X; + public final double Y; + public final double Z; + + public ColorXyz(final double x, final double y, final double z) { + X = x; + Y = y; + Z = z; + } + + @Override + public String toString() { + return "{X: " + X + ", Y: " + Y + ", Z: " + Z + "}"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/color/package-info.java b/src/main/java/org/apache/commons/imaging/color/package-info.java new file mode 100644 index 0000000..0533c51 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/color/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Color spaces and conversions between them. + */ +package org.apache.commons.imaging.color; diff --git a/src/main/java/org/apache/commons/imaging/common/BasicCParser.java b/src/main/java/org/apache/commons/imaging/common/BasicCParser.java new file mode 100644 index 0000000..2247f31 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/BasicCParser.java @@ -0,0 +1,379 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.common; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; + +/** + * A rudimentary preprocessor and parser for the C programming + * language. + */ +public class BasicCParser { + private final PushbackInputStream is; + + public BasicCParser(final ByteArrayInputStream is) { + this.is = new PushbackInputStream(is); + } + + public String nextToken() throws IOException, ImageReadException { + // I don't know how complete the C parsing in an XPM file + // is meant to be, this is just the very basics... + + boolean inString = false; + boolean inIdentifier = false; + boolean hadBackSlash = false; + final StringBuilder token = new StringBuilder(); + for (int c = is.read(); c != -1; c = is.read()) { + if (inString) { + if (c == '\\') { + token.append('\\'); + hadBackSlash = !hadBackSlash; + } else if (c == '"') { + token.append('"'); + if (!hadBackSlash) { + return token.toString(); + } + hadBackSlash = false; + } else if (c == '\r' || c == '\n') { + throw new ImageReadException( + "Unterminated string in XPM file"); + } else { + token.append((char) c); + hadBackSlash = false; + } + } else if (inIdentifier) { + if (Character.isLetterOrDigit(c) || c == '_') { + token.append((char) c); + } else { + is.unread(c); + return token.toString(); + } + } else { + if (c == '"') { + token.append('"'); + inString = true; + } else if (Character.isLetterOrDigit(c) || c == '_') { + token.append((char) c); + inIdentifier = true; + } else if (c == '{' || c == '}' || c == '[' || c == ']' + || c == '*' || c == ';' || c == '=' || c == ',') { + token.append((char) c); + return token.toString(); + } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + // ignore + } else { + throw new ImageReadException( + "Unhandled/invalid character '" + ((char) c) + + "' found in XPM file"); + } + } + } + + if (inIdentifier) { + return token.toString(); + } + if (inString) { + throw new ImageReadException("Unterminated string ends XMP file"); + } + return null; + } + + public static ByteArrayOutputStream preprocess(final InputStream is, + final StringBuilder firstComment, final Map defines) + throws IOException, ImageReadException { + boolean inSingleQuotes = false; + boolean inString = false; + boolean inComment = false; + boolean inDirective = false; + boolean hadSlash = false; + boolean hadStar = false; + boolean hadBackSlash = false; + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + boolean seenFirstComment = (firstComment == null); + final StringBuilder directiveBuffer = new StringBuilder(); + for (int c = is.read(); c != -1; c = is.read()) { + if (inComment) { + if (c == '*') { + if (hadStar && !seenFirstComment) { + firstComment.append('*'); + } + hadStar = true; + } else if (c == '/') { + if (hadStar) { + hadStar = false; + inComment = false; + seenFirstComment = true; + } else { + if (!seenFirstComment) { + firstComment.append((char) c); + } + } + } else { + if (hadStar && !seenFirstComment) { + firstComment.append('*'); + } + hadStar = false; + if (!seenFirstComment) { + firstComment.append((char) c); + } + } + } else if (inSingleQuotes) { + if (c == '\\') { + if (hadBackSlash) { + out.write('\\'); + out.write('\\'); + hadBackSlash = false; + } else { + hadBackSlash = true; + } + } else if (c == '\'') { + if (hadBackSlash) { + out.write('\\'); + hadBackSlash = false; + } else { + inSingleQuotes = false; + } + out.write('\''); + } else if (c == '\r' || c == '\n') { + throw new ImageReadException("Unterminated single quote in file"); + } else { + if (hadBackSlash) { + out.write('\\'); + hadBackSlash = false; + } + out.write(c); + } + } else if (inString) { + if (c == '\\') { + if (hadBackSlash) { + out.write('\\'); + out.write('\\'); + hadBackSlash = false; + } else { + hadBackSlash = true; + } + } else if (c == '"') { + if (hadBackSlash) { + out.write('\\'); + hadBackSlash = false; + } else { + inString = false; + } + out.write('"'); + } else if (c == '\r' || c == '\n') { + throw new ImageReadException("Unterminated string in file"); + } else { + if (hadBackSlash) { + out.write('\\'); + hadBackSlash = false; + } + out.write(c); + } + } else if (inDirective) { + if (c == '\r' || c == '\n') { + inDirective = false; + final String[] tokens = tokenizeRow(directiveBuffer.toString()); + if (tokens.length < 2 || tokens.length > 3) { + throw new ImageReadException("Bad preprocessor directive"); + } + if (!tokens[0].equals("define")) { + throw new ImageReadException("Invalid/unsupported " + + "preprocessor directive '" + tokens[0] + "'"); + } + defines.put(tokens[1], (tokens.length == 3) ? tokens[2] + : null); + directiveBuffer.setLength(0); + } else { + directiveBuffer.append((char) c); + } + } else { + if (c == '/') { + if (hadSlash) { + out.write('/'); + } + hadSlash = true; + } else if (c == '*') { + if (hadSlash) { + inComment = true; + hadSlash = false; + } else { + out.write(c); + } + } else if (c == '\'') { + if (hadSlash) { + out.write('/'); + } + hadSlash = false; + out.write(c); + inSingleQuotes = true; + } else if (c == '"') { + if (hadSlash) { + out.write('/'); + } + hadSlash = false; + out.write(c); + inString = true; + } else if (c == '#') { + if (defines == null) { + throw new ImageReadException("Unexpected preprocessor directive"); + } + inDirective = true; + } else { + if (hadSlash) { + out.write('/'); + } + hadSlash = false; + out.write(c); + // Only whitespace allowed before first comment: + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { + seenFirstComment = true; + } + } + } + } + if (hadSlash) { + out.write('/'); + } + if (hadStar) { + out.write('*'); + } + if (inString) { + throw new ImageReadException("Unterminated string at the end of file"); + } + if (inComment) { + throw new ImageReadException("Unterminated comment at the end of file"); + } + return out; + } + + public static String[] tokenizeRow(final String row) { + final String[] tokens = row.split("[ \t]"); + int numLiveTokens = 0; + for (final String token : tokens) { + if (token != null && token.length() > 0) { + ++numLiveTokens; + } + } + final String[] liveTokens = new String[numLiveTokens]; + int next = 0; + for (final String token : tokens) { + if (token != null && token.length() > 0) { + liveTokens[next++] = token; + } + } + return liveTokens; + } + + public static void unescapeString(final StringBuilder stringBuilder, final String string) + throws ImageReadException { + if (string.length() < 2) { + throw new ImageReadException("Parsing XPM file failed, " + + "string is too short"); + } + if (string.charAt(0) != '"' + || string.charAt(string.length() - 1) != '"') { + throw new ImageReadException("Parsing XPM file failed, " + + "string not surrounded by '\"'"); + } + boolean hadBackSlash = false; + for (int i = 1; i < (string.length() - 1); i++) { + final char c = string.charAt(i); + if (hadBackSlash) { + if (c == '\\') { + stringBuilder.append('\\'); + } else if (c == '"') { + stringBuilder.append('"'); + } else if (c == '\'') { + stringBuilder.append('\''); + } else if (c == 'x') { + if (i + 2 >= string.length()) { + throw new ImageReadException( + "Parsing XPM file failed, " + + "hex constant in string too short"); + } + final char hex1 = string.charAt(i + 1); + final char hex2 = string.charAt(i + 2); + i += 2; + int constant; + try { + constant = Integer.parseInt(Character.toString(hex1) + Character.toString(hex2), 16); + } catch (final NumberFormatException nfe) { + throw new ImageReadException( + "Parsing XPM file failed, " + + "hex constant invalid", nfe); + } + stringBuilder.append((char) constant); + } else if (c == '0' || c == '1' || c == '2' || c == '3' + || c == '4' || c == '5' || c == '6' || c == '7') { + int length = 1; + if (i + 1 < string.length() && '0' <= string.charAt(i + 1) + && string.charAt(i + 1) <= '7') { + ++length; + } + if (i + 2 < string.length() && '0' <= string.charAt(i + 2) + && string.charAt(i + 2) <= '7') { + ++length; + } + int constant = 0; + for (int j = 0; j < length; j++) { + constant *= 8; + constant += (string.charAt(i + j) - '0'); + } + i += length - 1; + stringBuilder.append((char) constant); + } else if (c == 'a') { + stringBuilder.append((char) 0x07); + } else if (c == 'b') { + stringBuilder.append((char) 0x08); + } else if (c == 'f') { + stringBuilder.append((char) 0x0c); + } else if (c == 'n') { + stringBuilder.append((char) 0x0a); + } else if (c == 'r') { + stringBuilder.append((char) 0x0d); + } else if (c == 't') { + stringBuilder.append((char) 0x09); + } else if (c == 'v') { + stringBuilder.append((char) 0x0b); + } else { + throw new ImageReadException("Parsing XPM file failed, " + + "invalid escape sequence"); + } + hadBackSlash = false; + } else { + if (c == '\\') { + hadBackSlash = true; + } else if (c == '"') { + throw new ImageReadException("Parsing XPM file failed, " + + "extra '\"' found in string"); + } else { + stringBuilder.append(c); + } + } + } + if (hadBackSlash) { + throw new ImageReadException("Parsing XPM file failed, " + + "unterminated escape sequence found in string"); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/BinaryConstant.java b/src/main/java/org/apache/commons/imaging/common/BinaryConstant.java new file mode 100644 index 0000000..414f5cc --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/BinaryConstant.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +public class BinaryConstant implements Cloneable { + private final byte[] value; + + public BinaryConstant(final byte[] value) { + this.value = value.clone(); + } + + @Override + public BinaryConstant clone() throws CloneNotSupportedException { + return (BinaryConstant) super.clone(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof BinaryConstant)) { + return false; + } + final BinaryConstant other = (BinaryConstant) obj; + return equals(other.value); + } + + public boolean equals(final byte[] bytes) { + return Arrays.equals(value, bytes); + } + + public boolean equals(final byte[] bytes, final int offset, final int length) { + if (value.length != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (value[i] != bytes[offset + i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + + public byte get(final int i) { + return value[i]; + } + + public int size() { + return value.length; + } + + public byte[] toByteArray() { + return value.clone(); + } + + public void writeTo(final OutputStream os) throws IOException { + for (final byte element : value) { + os.write(element); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/BinaryFileParser.java b/src/main/java/org/apache/commons/imaging/common/BinaryFileParser.java new file mode 100644 index 0000000..09c1245 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/BinaryFileParser.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +public class BinaryFileParser { + // default byte order for Java, many file formats. + private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN; + private boolean debug; + + public BinaryFileParser(final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + } + + /** + * Constructs a BinaryFileParser with the default, big-endian, byte order. + */ + public BinaryFileParser() { + // as above + } + + protected void setByteOrder(final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + } + + public ByteOrder getByteOrder() { + return byteOrder; + } + + public boolean getDebug() { + return debug; + } + + public void setDebug(final boolean debug) { + this.debug = debug; + } + + protected final void debugNumber(final String msg, final int data, final int bytes) { + final PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + debugNumber(pw, msg, data, bytes); + pw.flush(); + } + + protected final void debugNumber(final PrintWriter pw, final String msg, final int data, final int bytes) { + pw.print(msg + ": " + data + " ("); + int byteData = data; + for (int i = 0; i < bytes; i++) { + if (i > 0) { + pw.print(","); + } + final int singleByte = 0xff & byteData; + pw.print((char) singleByte + " [" + singleByte + "]"); + byteData >>= 8; + } + pw.println(") [0x" + Integer.toHexString(data) + ", " + Integer.toBinaryString(data) + "]"); + pw.flush(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java b/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java new file mode 100644 index 0000000..eb9b5b9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; + +/** + * Convenience methods for various binary and I/O operations. + */ +public final class BinaryFunctions { + private BinaryFunctions() { + } + + public static boolean startsWith(final byte[] haystack, final byte[] needle) { + if (needle == null) { + return false; + } + if (haystack == null) { + return false; + } + if (needle.length > haystack.length) { + return false; + } + + for (int i = 0; i < needle.length; i++) { + if (needle[i] != haystack[i]) { + return false; + } + } + + return true; + } + + public static boolean startsWith(final byte[] haystack, final BinaryConstant needle) { + if ((haystack == null) || (haystack.length < needle.size())) { + return false; + } + + for (int i = 0; i < needle.size(); i++) { + if (haystack[i] != needle.get(i)) { + return false; + } + } + + return true; + } + + public static byte readByte(final String name, final InputStream is, final String exception) + throws IOException { + final int result = is.read(); + if ((result < 0)) { + throw new IOException(exception); + } + return (byte) (0xff & result); + } + + public static byte[] readBytes(final String name, final InputStream is, final int length) + throws IOException { + final String exception = name + " could not be read."; + return readBytes(name, is, length, exception); + } + + public static byte[] readBytes(final String name, final InputStream is, final int length, + final String exception) throws IOException { + final byte[] result = new byte[length]; + int read = 0; + while (read < length) { + final int count = is.read(result, read, length - read); + if (count < 0) { + throw new IOException(exception + " count: " + count + + " read: " + read + " length: " + length); + } + + read += count; + } + + return result; + } + + public static byte[] readBytes(final InputStream is, final int count) throws IOException { + return readBytes("", is, count, "Unexpected EOF"); + } + + public static void readAndVerifyBytes(final InputStream is, final byte[] expected, + final String exception) throws ImageReadException, IOException { + for (final byte element : expected) { + final int data = is.read(); + final byte b = (byte) (0xff & data); + + if (data < 0) { + throw new ImageReadException("Unexpected EOF."); + } + + if (b != element) { + throw new ImageReadException(exception); + } + } + } + + public static void readAndVerifyBytes(final InputStream is, + final BinaryConstant expected, final String exception) + throws ImageReadException, IOException { + for (int i = 0; i < expected.size(); i++) { + final int data = is.read(); + final byte b = (byte) (0xff & data); + + if (data < 0) { + throw new ImageReadException("Unexpected EOF."); + } + + if (b != expected.get(i)) { + throw new ImageReadException(exception); + } + } + } + + public static void skipBytes(final InputStream is, final long length, final String exception) + throws IOException { + long total = 0; + while (length != total) { + final long skipped = is.skip(length - total); + if (skipped < 1) { + throw new IOException(exception + " (" + skipped + ")"); + } + total += skipped; + } + } + + public static byte[] remainingBytes(final String name, final byte[] bytes, final int count) { + return slice(bytes, count, bytes.length - count); + } + + public static byte[] slice(final byte[] bytes, final int start, final int count) { + final byte[] result = new byte[count]; + System.arraycopy(bytes, start, result, 0, count); + return result; + } + + public static byte[] head(final byte[] bytes, int count) { + if (count > bytes.length) { + count = bytes.length; + } + return slice(bytes, 0, count); + } + + public static boolean compareBytes(final byte[] a, final int aStart, final byte[] b, + final int bStart, final int length) { + if (a.length < (aStart + length)) { + return false; + } + if (b.length < (bStart + length)) { + return false; + } + + for (int i = 0; i < length; i++) { + if (a[aStart + i] != b[bStart + i]) { + return false; + } + } + + return true; + } + + public static int read4Bytes(final String name, final InputStream is, + final String exception, final ByteOrder byteOrder) throws IOException { + final int byte0 = is.read(); + final int byte1 = is.read(); + final int byte2 = is.read(); + final int byte3 = is.read(); + if ((byte0 | byte1 | byte2 | byte3) < 0) { + throw new IOException(exception); + } + + final int result; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result = (byte0 << 24) | (byte1 << 16) + | (byte2 << 8) | (byte3 << 0); + } else { + result = (byte3 << 24) | (byte2 << 16) + | (byte1 << 8) | (byte0 << 0); + } + + return result; + } + + public static int read3Bytes(final String name, final InputStream is, + final String exception, final ByteOrder byteOrder) throws IOException { + final int byte0 = is.read(); + final int byte1 = is.read(); + final int byte2 = is.read(); + if ((byte0 | byte1 | byte2) < 0) { + throw new IOException(exception); + } + + final int result; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result = (byte0 << 16) | (byte1 << 8) + | (byte2 << 0); + } else { + result = (byte2 << 16) | (byte1 << 8) + | (byte0 << 0); + } + + return result; + } + + public static int read2Bytes(final String name, final InputStream is, + final String exception, final ByteOrder byteOrder) throws IOException { + final int byte0 = is.read(); + final int byte1 = is.read(); + if ((byte0 | byte1) < 0) { + throw new IOException(exception); + } + + final int result; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result = (byte0 << 8) | byte1; + } else { + result = (byte1 << 8) | byte0; + } + + return result; + } + + public static void printCharQuad(final String msg, final int i) { + System.out.println(msg + ": '" + (char) (0xff & (i >> 24)) + + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) + + (char) (0xff & (i >> 0)) + "'"); + + } + + public static void printCharQuad(final PrintWriter pw, final String msg, final int i) { + pw.println(msg + ": '" + (char) (0xff & (i >> 24)) + + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) + + (char) (0xff & (i >> 0)) + "'"); + + } + + public static void printByteBits(final String msg, final byte i) { + System.out.println(msg + ": '" + Integer.toBinaryString(0xff & i)); + } + + public static int charsToQuad(final char c1, final char c2, final char c3, final char c4) { + return (((0xff & c1) << 24) | ((0xff & c2) << 16) | ((0xff & c3) << 8) | ((0xff & c4) << 0)); + } + + public static int findNull(final byte[] src) { + return findNull(src, 0); + } + + public static int findNull(final byte[] src, final int start) { + for (int i = start; i < src.length; i++) { + if (src[i] == 0) { + return i; + } + } + return -1; + } + + public static byte[] getRAFBytes(final RandomAccessFile raf, final long pos, + final int length, final String exception) throws IOException { + final byte[] result = new byte[length]; + + raf.seek(pos); + + int read = 0; + while (read < length) { + final int count = raf.read(result, read, length - read); + if (count < 0) { + throw new IOException(exception); + } + + read += count; + } + + return result; + + } + + public static void skipBytes(final InputStream is, final long length) throws IOException { + skipBytes(is, length, "Couldn't skip bytes"); + } + + public static void copyStreamToStream(final InputStream is, final OutputStream os) + throws IOException { + final byte[] buffer = new byte[1024]; + int read; + while ((read = is.read(buffer)) > 0) { + os.write(buffer, 0, read); + } + } + + public static byte[] getStreamBytes(final InputStream is) throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + copyStreamToStream(is, os); + return os.toByteArray(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/BinaryOutputStream.java b/src/main/java/org/apache/commons/imaging/common/BinaryOutputStream.java new file mode 100644 index 0000000..56b3ced --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/BinaryOutputStream.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; + +public class BinaryOutputStream extends OutputStream { + private final OutputStream os; + // default byte order for Java, many file formats. + private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN; + private boolean debug; + private int count; + + public final void setDebug(final boolean b) { + debug = b; + } + + public final boolean getDebug() { + return debug; + } + + public BinaryOutputStream(final OutputStream os, final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + this.os = os; + } + + public BinaryOutputStream(final OutputStream os) { + this.os = os; + } + + protected void setByteOrder(final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + } + + public ByteOrder getByteOrder() { + return byteOrder; + } + + @Override + public void write(final int i) throws IOException { + os.write(i); + count++; + } + + @Override + public final void write(final byte[] bytes) throws IOException { + os.write(bytes, 0, bytes.length); + count += bytes.length; + } + + @Override + public final void write(final byte[] bytes, final int offset, final int length) throws IOException { + os.write(bytes, offset, length); + count += length; + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public void close() throws IOException { + os.close(); + } + + public int getByteCount() { + return count; + } + + public final void write4Bytes(final int value) throws IOException { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + write(0xff & (value >> 24)); + write(0xff & (value >> 16)); + write(0xff & (value >> 8)); + write(0xff & value); + } else { + write(0xff & value); + write(0xff & (value >> 8)); + write(0xff & (value >> 16)); + write(0xff & (value >> 24)); + } + } + + public final void write3Bytes(final int value) throws IOException { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + write(0xff & (value >> 16)); + write(0xff & (value >> 8)); + write(0xff & value); + } else { + write(0xff & value); + write(0xff & (value >> 8)); + write(0xff & (value >> 16)); + } + } + + public final void write2Bytes(final int value) throws IOException { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + write(0xff & (value >> 8)); + write(0xff & value); + } else { + write(0xff & value); + write(0xff & (value >> 8)); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/ByteConversions.java b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java new file mode 100644 index 0000000..10b8333 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java @@ -0,0 +1,390 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.nio.ByteOrder; + +/** + * Convenience methods for converting data types to and from + * byte arrays. + */ +public final class ByteConversions { + private ByteConversions() { + } + + public static byte[] toBytes(final short value, final ByteOrder byteOrder) { + final byte[] result = new byte[2]; + toBytes(value, byteOrder, result, 0); + return result; + } + + public static byte[] toBytes(final short[] values, final ByteOrder byteOrder) { + return toBytes(values, 0, values.length, byteOrder); + } + + private static byte[] toBytes(final short[] values, final int offset, final int length, final ByteOrder byteOrder) { + final byte[] result = new byte[length * 2]; + for (int i = 0; i < length; i++) { + toBytes(values[offset + i], byteOrder, result, i * 2); + } + return result; + } + + private static void toBytes(final short value, final ByteOrder byteOrder, final byte[] result, final int offset) { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result[offset + 0] = (byte) (value >> 8); + result[offset + 1] = (byte) (value >> 0); + } else { + result[offset + 1] = (byte) (value >> 8); + result[offset + 0] = (byte) (value >> 0); + } + } + + public static byte[] toBytes(final int value, final ByteOrder byteOrder) { + final byte[] result = new byte[4]; + toBytes(value, byteOrder, result, 0); + return result; + } + + public static byte[] toBytes(final int[] values, final ByteOrder byteOrder) { + return toBytes(values, 0, values.length, byteOrder); + } + + private static byte[] toBytes(final int[] values, final int offset, final int length, final ByteOrder byteOrder) { + final byte[] result = new byte[length * 4]; + for (int i = 0; i < length; i++) { + toBytes(values[offset + i], byteOrder, result, i * 4); + } + return result; + } + + private static void toBytes(final int value, final ByteOrder byteOrder, final byte[] result, final int offset) { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result[offset + 0] = (byte) (value >> 24); + result[offset + 1] = (byte) (value >> 16); + result[offset + 2] = (byte) (value >> 8); + result[offset + 3] = (byte) (value >> 0); + } else { + result[offset + 3] = (byte) (value >> 24); + result[offset + 2] = (byte) (value >> 16); + result[offset + 1] = (byte) (value >> 8); + result[offset + 0] = (byte) (value >> 0); + } + } + + public static byte[] toBytes(final float value, final ByteOrder byteOrder) { + final byte[] result = new byte[4]; + toBytes(value, byteOrder, result, 0); + return result; + } + + public static byte[] toBytes(final float[] values, final ByteOrder byteOrder) { + return toBytes(values, 0, values.length, byteOrder); + } + + private static byte[] toBytes(final float[] values, final int offset, final int length, final ByteOrder byteOrder) { + final byte[] result = new byte[length * 4]; + for (int i = 0; i < length; i++) { + toBytes(values[offset + i], byteOrder, result, i * 4); + } + return result; + } + + private static void toBytes(final float value, final ByteOrder byteOrder, final byte[] result, final int offset) { + final int bits = Float.floatToRawIntBits(value); + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + result[offset + 0] = (byte) (0xff & (bits >> 0)); + result[offset + 1] = (byte) (0xff & (bits >> 8)); + result[offset + 2] = (byte) (0xff & (bits >> 16)); + result[offset + 3] = (byte) (0xff & (bits >> 24)); + } else { + result[offset + 3] = (byte) (0xff & (bits >> 0)); + result[offset + 2] = (byte) (0xff & (bits >> 8)); + result[offset + 1] = (byte) (0xff & (bits >> 16)); + result[offset + 0] = (byte) (0xff & (bits >> 24)); + } + } + + public static byte[] toBytes(final double value, final ByteOrder byteOrder) { + final byte[] result = new byte[8]; + toBytes(value, byteOrder, result, 0); + return result; + } + + public static byte[] toBytes(final double[] values, final ByteOrder byteOrder) { + return toBytes(values, 0, values.length, byteOrder); + } + + private static byte[] toBytes(final double[] values, final int offset, + final int length, final ByteOrder byteOrder) { + final byte[] result = new byte[length * 8]; + for (int i = 0; i < length; i++) { + toBytes(values[offset + i], byteOrder, result, i * 8); + } + return result; + } + + private static void toBytes(final double value, final ByteOrder byteOrder, final byte[] result, final int offset) { + final long bits = Double.doubleToRawLongBits(value); + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + result[offset + 0] = (byte) (0xff & (bits >> 0)); + result[offset + 1] = (byte) (0xff & (bits >> 8)); + result[offset + 2] = (byte) (0xff & (bits >> 16)); + result[offset + 3] = (byte) (0xff & (bits >> 24)); + result[offset + 4] = (byte) (0xff & (bits >> 32)); + result[offset + 5] = (byte) (0xff & (bits >> 40)); + result[offset + 6] = (byte) (0xff & (bits >> 48)); + result[offset + 7] = (byte) (0xff & (bits >> 56)); + } else { + result[offset + 7] = (byte) (0xff & (bits >> 0)); + result[offset + 6] = (byte) (0xff & (bits >> 8)); + result[offset + 5] = (byte) (0xff & (bits >> 16)); + result[offset + 4] = (byte) (0xff & (bits >> 24)); + result[offset + 3] = (byte) (0xff & (bits >> 32)); + result[offset + 2] = (byte) (0xff & (bits >> 40)); + result[offset + 1] = (byte) (0xff & (bits >> 48)); + result[offset + 0] = (byte) (0xff & (bits >> 56)); + } + } + + public static byte[] toBytes(final RationalNumber value, final ByteOrder byteOrder) { + final byte[] result = new byte[8]; + toBytes(value, byteOrder, result, 0); + return result; + } + + public static byte[] toBytes(final RationalNumber[] values, final ByteOrder byteOrder) { + return toBytes(values, 0, values.length, byteOrder); + } + + private static byte[] toBytes(final RationalNumber[] values, final int offset, + final int length, final ByteOrder byteOrder) { + final byte[] result = new byte[length * 8]; + for (int i = 0; i < length; i++) { + toBytes(values[offset + i], byteOrder, result, i * 8); + } + return result; + } + + private static void toBytes(final RationalNumber value, final ByteOrder byteOrder, + final byte[] result, final int offset) { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + result[offset + 0] = (byte) (value.numerator >> 24); + result[offset + 1] = (byte) (value.numerator >> 16); + result[offset + 2] = (byte) (value.numerator >> 8); + result[offset + 3] = (byte) (value.numerator >> 0); + result[offset + 4] = (byte) (value.divisor >> 24); + result[offset + 5] = (byte) (value.divisor >> 16); + result[offset + 6] = (byte) (value.divisor >> 8); + result[offset + 7] = (byte) (value.divisor >> 0); + } else { + result[offset + 3] = (byte) (value.numerator >> 24); + result[offset + 2] = (byte) (value.numerator >> 16); + result[offset + 1] = (byte) (value.numerator >> 8); + result[offset + 0] = (byte) (value.numerator >> 0); + result[offset + 7] = (byte) (value.divisor >> 24); + result[offset + 6] = (byte) (value.divisor >> 16); + result[offset + 5] = (byte) (value.divisor >> 8); + result[offset + 4] = (byte) (value.divisor >> 0); + } + } + + public static short toShort(final byte[] bytes, final ByteOrder byteOrder) { + return toShort(bytes, 0, byteOrder); + } + + private static short toShort(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + return (short) toUInt16(bytes, offset, byteOrder); + } + + public static short[] toShorts(final byte[] bytes, final ByteOrder byteOrder) { + return toShorts(bytes, 0, bytes.length, byteOrder); + } + + private static short[] toShorts(final byte[] bytes, final int offset, + final int length, final ByteOrder byteOrder) { + final short[] result = new short[length / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = toShort(bytes, offset + 2 * i, byteOrder); + } + return result; + } + + public static int toUInt16(final byte[] bytes, final ByteOrder byteOrder) { + return toUInt16(bytes, 0, byteOrder); + } + + public static int toUInt16(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + final int byte0 = 0xff & bytes[offset + 0]; + final int byte1 = 0xff & bytes[offset + 1]; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return ((byte0 << 8) | byte1); + } else { + return ((byte1 << 8) | byte0); + } + } + + public static int[] toUInt16s(final byte[] bytes, final ByteOrder byteOrder) { + return toUInt16s(bytes, 0, bytes.length, byteOrder); + } + + private static int[] toUInt16s(final byte[] bytes, final int offset, final int length, + final ByteOrder byteOrder) { + final int[] result = new int[length / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = toUInt16(bytes, offset + 2 * i, byteOrder); + } + return result; + } + + public static int toInt(final byte[] bytes, final ByteOrder byteOrder) { + return toInt(bytes, 0, byteOrder); + } + + public static int toInt(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + final int byte0 = 0xff & bytes[offset + 0]; + final int byte1 = 0xff & bytes[offset + 1]; + final int byte2 = 0xff & bytes[offset + 2]; + final int byte3 = 0xff & bytes[offset + 3]; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | byte3; + } else { + return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; + } + } + + public static int[] toInts(final byte[] bytes, final ByteOrder byteOrder) { + return toInts(bytes, 0, bytes.length, byteOrder); + } + + private static int[] toInts(final byte[] bytes, final int offset, final int length, + final ByteOrder byteOrder) { + final int[] result = new int[length / 4]; + for (int i = 0; i < result.length; i++) { + result[i] = toInt(bytes, offset + 4 * i, byteOrder); + } + return result; + } + + public static float toFloat(final byte[] bytes, final ByteOrder byteOrder) { + return toFloat(bytes, 0, byteOrder); + } + + private static float toFloat(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + final int byte0 = 0xff & bytes[offset + 0]; + final int byte1 = 0xff & bytes[offset + 1]; + final int byte2 = 0xff & bytes[offset + 2]; + final int byte3 = 0xff & bytes[offset + 3]; + final int bits; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + bits = (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | (byte3 << 0); + } else { + bits = (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | (byte0 << 0); + } + return Float.intBitsToFloat(bits); + } + + public static float[] toFloats(final byte[] bytes, final ByteOrder byteOrder) { + return toFloats(bytes, 0, bytes.length, byteOrder); + } + + private static float[] toFloats(final byte[] bytes, final int offset, + final int length, final ByteOrder byteOrder) { + final float[] result = new float[length / 4]; + for (int i = 0; i < result.length; i++) { + result[i] = toFloat(bytes, offset + 4 * i, byteOrder); + } + return result; + } + + public static double toDouble(final byte[] bytes, final ByteOrder byteOrder) { + return toDouble(bytes, 0, byteOrder); + } + + private static double toDouble(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + final long byte0 = 0xffL & bytes[offset + 0]; + final long byte1 = 0xffL & bytes[offset + 1]; + final long byte2 = 0xffL & bytes[offset + 2]; + final long byte3 = 0xffL & bytes[offset + 3]; + final long byte4 = 0xffL & bytes[offset + 4]; + final long byte5 = 0xffL & bytes[offset + 5]; + final long byte6 = 0xffL & bytes[offset + 6]; + final long byte7 = 0xffL & bytes[offset + 7]; + final long bits; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + bits = (byte0 << 56) | (byte1 << 48) | (byte2 << 40) + | (byte3 << 32) | (byte4 << 24) | (byte5 << 16) + | (byte6 << 8) | (byte7 << 0); + } else { + bits = (byte7 << 56) | (byte6 << 48) | (byte5 << 40) + | (byte4 << 32) | (byte3 << 24) | (byte2 << 16) + | (byte1 << 8) | (byte0 << 0); + } + return Double.longBitsToDouble(bits); + } + + public static double[] toDoubles(final byte[] bytes, final ByteOrder byteOrder) { + return toDoubles(bytes, 0, bytes.length, byteOrder); + } + + private static double[] toDoubles(final byte[] bytes, final int offset, + final int length, final ByteOrder byteOrder) { + final double[] result = new double[length / 8]; + for (int i = 0; i < result.length; i++) { + result[i] = toDouble(bytes, offset + 8 * i, byteOrder); + } + return result; + } + + public static RationalNumber toRational(final byte[] bytes, final ByteOrder byteOrder) { + return toRational(bytes, 0, byteOrder); + } + + private static RationalNumber toRational(final byte[] bytes, final int offset, final ByteOrder byteOrder) { + final int byte0 = 0xff & bytes[offset + 0]; + final int byte1 = 0xff & bytes[offset + 1]; + final int byte2 = 0xff & bytes[offset + 2]; + final int byte3 = 0xff & bytes[offset + 3]; + final int byte4 = 0xff & bytes[offset + 4]; + final int byte5 = 0xff & bytes[offset + 5]; + final int byte6 = 0xff & bytes[offset + 6]; + final int byte7 = 0xff & bytes[offset + 7]; + final int numerator; + final int divisor; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + numerator = (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | byte3; + divisor = (byte4 << 24) | (byte5 << 16) | (byte6 << 8) | byte7; + } else { + numerator = (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; + divisor = (byte7 << 24) | (byte6 << 16) | (byte5 << 8) | byte4; + } + return new RationalNumber(numerator, divisor); + } + + public static RationalNumber[] toRationals(final byte[] bytes, final ByteOrder byteOrder) { + return toRationals(bytes, 0, bytes.length, byteOrder); + } + + private static RationalNumber[] toRationals(final byte[] bytes, + final int offset, final int length, final ByteOrder byteOrder) { + final RationalNumber[] result = new RationalNumber[length / 8]; + for (int i = 0; i < result.length; i++) { + result[i] = toRational(bytes, offset + 8 * i, byteOrder); + } + return result; + } +} diff --git a/src/main/java/org/apache/sanselan/common/MyByteArrayOutputStream.java b/src/main/java/org/apache/commons/imaging/common/FastByteArrayOutputStream.java similarity index 64% rename from src/main/java/org/apache/sanselan/common/MyByteArrayOutputStream.java rename to src/main/java/org/apache/commons/imaging/common/FastByteArrayOutputStream.java index ffd8a77..0ab4100 100644 --- a/src/main/java/org/apache/sanselan/common/MyByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/imaging/common/FastByteArrayOutputStream.java @@ -1,59 +1,56 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.IOException; -import java.io.OutputStream; - -public class MyByteArrayOutputStream extends OutputStream -// some performace benefit, because not thread safe. -{ - private final byte bytes[]; - - public MyByteArrayOutputStream(int length) - { - bytes = new byte[length]; - } - - private int count = 0; - - public void write(int value) throws IOException - { - if (count >= bytes.length) - throw new IOException("Write exceeded expected length (" + count - + ", " + bytes.length + ")"); - - bytes[count] = (byte) value; - count++; - } - - public byte[] toByteArray() - { - if (count < bytes.length) - { - byte result[] = new byte[count]; - System.arraycopy(bytes, 0, result, 0, count); - return result; - } - return bytes; - } - - public int getBytesWritten() - { - return count; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Like ByteArrayOutputStream, but has some performance benefit, + * because it's not thread safe. + */ +class FastByteArrayOutputStream extends OutputStream { + private final byte[] bytes; + private int count; + + public FastByteArrayOutputStream(final int length) { + bytes = new byte[length]; + } + + @Override + public void write(final int value) throws IOException { + if (count >= bytes.length) { + throw new IOException("Write exceeded expected length (" + count + ", " + bytes.length + ")"); + } + + bytes[count] = (byte) value; + count++; + } + + public byte[] toByteArray() { + if (count < bytes.length) { + final byte[] result = new byte[count]; + System.arraycopy(bytes, 0, result, 0, count); + return result; + } + return bytes; + } + + public int getBytesWritten() { + return count; + } +} diff --git a/src/main/java/org/apache/sanselan/common/IBufferedImageFactory.java b/src/main/java/org/apache/commons/imaging/common/IBufferedImageFactory.java similarity index 78% rename from src/main/java/org/apache/sanselan/common/IBufferedImageFactory.java rename to src/main/java/org/apache/commons/imaging/common/IBufferedImageFactory.java index 857df69..e6dbc4f 100644 --- a/src/main/java/org/apache/sanselan/common/IBufferedImageFactory.java +++ b/src/main/java/org/apache/commons/imaging/common/IBufferedImageFactory.java @@ -1,29 +1,28 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.common; - -import com.google.code.appengine.awt.image.BufferedImage; - -public interface IBufferedImageFactory -{ - public BufferedImage getColorBufferedImage(int width, int height, - boolean hasAlpha); - - public BufferedImage getGrayscaleBufferedImage(int width, int height, - boolean hasAlpha); -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.common; + +import com.google.code.appengine.awt.image.BufferedImage; + +public interface IBufferedImageFactory { + BufferedImage getColorBufferedImage(int width, int height, + boolean hasAlpha); + + BufferedImage getGrayscaleBufferedImage(int width, int height, + boolean hasAlpha); +} diff --git a/src/main/java/org/apache/sanselan/ImageReadException.java b/src/main/java/org/apache/commons/imaging/common/IImageMetadata.java similarity index 71% rename from src/main/java/org/apache/sanselan/ImageReadException.java rename to src/main/java/org/apache/commons/imaging/common/IImageMetadata.java index 472e424..dc23d9c 100644 --- a/src/main/java/org/apache/sanselan/ImageReadException.java +++ b/src/main/java/org/apache/commons/imaging/common/IImageMetadata.java @@ -1,32 +1,31 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -public class ImageReadException extends SanselanException -{ - static final long serialVersionUID = -1L; - - public ImageReadException(String s) - { - super(s); - } - - public ImageReadException(String s, Exception e) - { - super(s, e); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.util.List; + +public interface IImageMetadata { + String toString(String prefix); + + List getItems(); + + interface IImageMetadataItem { + String toString(String prefix); + + String toString(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java new file mode 100644 index 0000000..02e5a2f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Development notes: + * This class was introduced to the Apache Commons Imaging library in + * order to improve performance in building images. The setRGB method + * provided by this class represents a substantial improvement in speed + * compared to that of the BufferedImage class that was originally used + * in Apache Sanselan. + * This increase is attained because ImageBuilder is a highly specialized + * class that does not need to perform the general-purpose logic required + * for BufferedImage. If you need to modify this class to add new + * image formats or functionality, keep in mind that some of its methods + * are invoked literally millions of times when building an image. + * Since even the introduction of something as small as a single conditional + * inside of setRGB could result in a noticeable increase in the + * time to read a file, changes should be made with care. + * During development, I experimented with inlining the setRGB logic + * in some of the code that uses it. This approach did not significantly + * improve performance, leading me to speculate that the Java JIT compiler + * might have inlined the method at run time. Further investigation + * is required. + * + */ +package org.apache.commons.imaging.common; + +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.DataBufferInt; +import com.google.code.appengine.awt.image.DirectColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.RasterFormatException; +import com.google.code.appengine.awt.image.WritableRaster; +import java.util.Properties; + +/** + * A utility class primary intended for storing data obtained by reading + * image files. + */ +public class ImageBuilder { + private final int[] data; + private final int width; + private final int height; + private final boolean hasAlpha; + + /** + * Construct an ImageBuilder instance + * @param width the width of the image to be built + * @param height the height of the image to be built + * @param hasAlpha indicates whether the image has an alpha channel + * (the selection of alpha channel does not change the memory + * requirements for the ImageBuilder or resulting BufferedImage. + */ + public ImageBuilder(final int width, final int height, final boolean hasAlpha) { + if (width <= 0) { + throw new RasterFormatException("zero or negative width value"); + } + if (height <= 0) { + throw new RasterFormatException("zero or negative height value"); + } + + data = new int[width * height]; + this.width = width; + this.height = height; + this.hasAlpha = hasAlpha; + } + + /** + * Get the width of the ImageBuilder pixel field + * @return a positive integer + */ + public int getWidth() { + return width; + } + + /** + * Get the height of the ImageBuilder pixel field + * @return a positive integer + */ + public int getHeight() { + return height; + } + + /** + * Get the RGB or ARGB value for the pixel at the position (x,y) + * within the image builder pixel field. For performance reasons + * no bounds checking is applied. + * @param x the X coordinate of the pixel to be read + * @param y the Y coordinate of the pixel to be read + * @return the RGB or ARGB pixel value + */ + public int getRGB(final int x, final int y) { + final int rowOffset = y * width; + return data[rowOffset + x]; + } + + /** + * Set the RGB or ARGB value for the pixel at position (x,y) + * within the image builder pixel field. For performance reasons, + * no bounds checking is applied. + * @param x the X coordinate of the pixel to be set + * @param y the Y coordinate of the pixel to be set + * @param argb the RGB or ARGB value to be stored. + */ + public void setRGB(final int x, final int y, final int argb) { + final int rowOffset = y * width; + data[rowOffset + x] = argb; + } + + /** + * Create a BufferedImage using the data stored in the ImageBuilder. + * @return a valid BufferedImage. + */ + public BufferedImage getBufferedImage() { + return makeBufferedImage(data, width, height, hasAlpha); + } + + /** + * Gets a subimage from the ImageBuilder using the specified parameters. + * If the parameters specify a rectangular region that is not entirely + * contained within the bounds defined by the ImageBuilder, this method will + * throw a RasterFormatException. This runtime-exception behavior + * is consistent with the behavior of the getSubimage method + * provided by BufferdImage. + * @param x the X coordinate of the upper-left corner of the + * specified rectangular region + * @param y the Y coordinate of the upper-left corner of the + * specified rectangular region + * @param w the width of the specified rectangular region + * @param h the height of the specified rectangular region + * @return a BufferedImage that constructed from the deta within the + * specified rectangular region + * @throws RasterFormatException f the specified area is not contained + * within this ImageBuilder + */ + public BufferedImage getSubimage(final int x, final int y, final int w, final int h) + { + if (w <= 0) { + throw new RasterFormatException("negative or zero subimage width"); + } + if (h <= 0) { + throw new RasterFormatException("negative or zero subimage height"); + } + if (x < 0 || x >= width) { + throw new RasterFormatException("subimage x is outside raster"); + } + if (x + w > width) { + throw new RasterFormatException( + "subimage (x+width) is outside raster"); + } + if (y < 0 || y >= height) { + throw new RasterFormatException("subimage y is outside raster"); + } + if (y + h > height) { + throw new RasterFormatException( + "subimage (y+height) is outside raster"); + } + + + // Transcribe the data to an output image array + final int[] argb = new int[w * h]; + int k = 0; + for (int iRow = 0; iRow < h; iRow++) { + final int dIndex = (iRow + y) * width + x; + System.arraycopy(this.data, dIndex, argb, k, w); + k += w; + + } + + return makeBufferedImage(argb, w, h, hasAlpha); + + } + + private BufferedImage makeBufferedImage( + final int[] argb, final int w, final int h, final boolean useAlpha) + { + ColorModel colorModel; + WritableRaster raster; + final DataBufferInt buffer = new DataBufferInt(argb, w * h); + if (useAlpha) { + colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, + 0x000000ff, 0xff000000); + raster = Raster.createPackedRaster(buffer, w, h, + w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, + 0xff000000 }, null); + } else { + colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, + 0x000000ff); + raster = Raster.createPackedRaster(buffer, w, h, + w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff }, + null); + } + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/ImageMetadata.java b/src/main/java/org/apache/commons/imaging/common/ImageMetadata.java new file mode 100644 index 0000000..b9bbd1a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/ImageMetadata.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.util.ArrayList; +import java.util.List; + +public class ImageMetadata implements IImageMetadata { + private static final String NEWLINE = System.getProperty("line.separator"); + private final List items = new ArrayList(); + + public void add(final String keyword, final String text) { + add(new Item(keyword, text)); + } + + public void add(final IImageMetadataItem item) { + items.add(item); + } + + public List getItems() { + return new ArrayList(items); + } + + @Override + public String toString() { + return toString(null); + } + + public String toString(String prefix) { + if (null == prefix) { + prefix = ""; + } + + final StringBuilder result = new StringBuilder(); + for (int i = 0; i < items.size(); i++) { + if (i > 0) { + result.append(NEWLINE); + } + // if (null != prefix) + // result.append(prefix); + + final ImageMetadata.IImageMetadataItem item = items.get(i); + result.append(item.toString(prefix + "\t")); + + // Debug.debug("prefix", prefix); + // Debug.debug("item", items.get(i)); + // Debug.debug(); + } + return result.toString(); + } + + public static class Item implements IImageMetadataItem { + private final String keyword; + private final String text; + + public Item(final String keyword, final String text) { + this.keyword = keyword; + this.text = text; + } + + public String getKeyword() { + return keyword; + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return toString(null); + } + + public String toString(final String prefix) { + final String result = keyword + ": " + text; + if (null != prefix) { + return prefix + result; + } else { + return result; + } + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/common/PackBits.java b/src/main/java/org/apache/commons/imaging/common/PackBits.java new file mode 100644 index 0000000..ee0a1aa --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/PackBits.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.util.IoUtils; + +public class PackBits { + + public byte[] decompress(final byte[] bytes, final int expected) + throws ImageReadException { + int total = 0; + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // Loop until you get the number of unpacked bytes you are expecting: + int i = 0; + while (total < expected) { + // Read the next source byte into n. + if (i >= bytes.length) { + throw new ImageReadException( + "Tiff: Unpack bits source exhausted: " + i + + ", done + " + total + ", expected + " + + expected); + } + + final int n = bytes[i++]; + if ((n >= 0) && (n <= 127)) { + // If n is between 0 and 127 inclusive, copy the next n+1 bytes + // literally. + final int count = n + 1; + + total += count; + for (int j = 0; j < count; j++) { + baos.write(bytes[i++]); + } + } else if ((n >= -127) && (n <= -1)) { + // Else if n is between -127 and -1 inclusive, copy the next byte + // -n+1 times. + + final int b = bytes[i++]; + final int count = -n + 1; + + total += count; + for (int j = 0; j < count; j++) { + baos.write(b); + } + } else if (n == -128) { + // Else if n is -128, noop. + throw new ImageReadException("Packbits: " + n); + } + } + + return baos.toByteArray(); + + } + + private int findNextDuplicate(final byte[] bytes, final int start) { + // int last = -1; + if (start >= bytes.length) { + return -1; + } + + byte prev = bytes[start]; + + for (int i = start + 1; i < bytes.length; i++) { + final byte b = bytes[i]; + + if (b == prev) { + return i - 1; + } + + prev = b; + } + + return -1; + } + + private int findRunLength(final byte[] bytes, final int start) { + final byte b = bytes[start]; + + int i; + + for (i = start + 1; (i < bytes.length) && (bytes[i] == b); i++) { + // do nothing + } + + return i - start; + } + + public byte[] compress(final byte[] bytes) throws IOException { + FastByteArrayOutputStream baos = null; + boolean canThrow = false; + try { + baos = new FastByteArrayOutputStream( + bytes.length * 2); // max length 1 extra byte for every 128 + + int ptr = 0; + while (ptr < bytes.length) { + int dup = findNextDuplicate(bytes, ptr); + + if (dup == ptr) { + // write run length + final int len = findRunLength(bytes, dup); + final int actualLen = Math.min(len, 128); + baos.write(-(actualLen - 1)); + baos.write(bytes[ptr]); + ptr += actualLen; + } else { + // write literals + int len = dup - ptr; + + if (dup > 0) { + final int runlen = findRunLength(bytes, dup); + if (runlen < 3) { + // may want to discard next run. + final int nextptr = ptr + len + runlen; + final int nextdup = findNextDuplicate(bytes, nextptr); + if (nextdup != nextptr) { + // discard 2-byte run + dup = nextdup; + len = dup - ptr; + } + } + } + + if (dup < 0) { + len = bytes.length - ptr; + } + final int actualLen = Math.min(len, 128); + + baos.write(actualLen - 1); + for (int i = 0; i < actualLen; i++) { + baos.write(bytes[ptr]); + ptr++; + } + } + } + final byte[] result = baos.toByteArray(); + canThrow = true; + return result; + } finally { + IoUtils.closeQuietly(canThrow, baos); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/RationalNumber.java b/src/main/java/org/apache/commons/imaging/common/RationalNumber.java new file mode 100644 index 0000000..a841a6f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/RationalNumber.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common; + +import java.text.NumberFormat; + +/** + * Rational number, as used by the TIFF image format. + */ +public class RationalNumber extends Number { + + private static final long serialVersionUID = -8412262656468158691L; + + // int-precision tolerance + private static final double TOLERANCE = 1E-8; + + public final int numerator; + public final int divisor; + + public RationalNumber(final int numerator, final int divisor) { + this.numerator = numerator; + this.divisor = divisor; + } + + static RationalNumber factoryMethod(long n, long d) { + // safer than constructor - handles values outside min/max range. + // also does some simple finding of common denominators. + + if (n > Integer.MAX_VALUE || n < Integer.MIN_VALUE + || d > Integer.MAX_VALUE || d < Integer.MIN_VALUE) { + while ((n > Integer.MAX_VALUE || n < Integer.MIN_VALUE + || d > Integer.MAX_VALUE || d < Integer.MIN_VALUE) + && (Math.abs(n) > 1) && (Math.abs(d) > 1)) { + // brutal, inprecise truncation =( + // use the sign-preserving right shift operator. + n >>= 1; + d >>= 1; + } + + if (d == 0) { + throw new NumberFormatException("Invalid value, numerator: " + n + ", divisor: " + d); + } + } + + final long gcd = gcd(n, d); + d = d / gcd; + n = n / gcd; + + return new RationalNumber((int) n, (int) d); + } + + /** + * Return the greatest common divisor + */ + private static long gcd(final long a, final long b) { + if (b == 0) { + return a; + } else { + return gcd(b, a % b); + } + } + + public RationalNumber negate() { + return new RationalNumber(-numerator, divisor); + } + + @Override + public double doubleValue() { + return (double) numerator / (double) divisor; + } + + @Override + public float floatValue() { + return (float) numerator / (float) divisor; + } + + @Override + public int intValue() { + return numerator / divisor; + } + + @Override + public long longValue() { + return (long) numerator / (long) divisor; + } + + @Override + public String toString() { + if (divisor == 0) { + return "Invalid rational (" + numerator + "/" + divisor + ")"; + } + final NumberFormat nf = NumberFormat.getInstance(); + + if ((numerator % divisor) == 0) { + return nf.format(numerator / divisor); + } + return numerator + "/" + divisor + " (" + nf.format((double) numerator / divisor) + ")"; + } + + public String toDisplayString() { + if ((numerator % divisor) == 0) { + return Integer.toString(numerator / divisor); + } + final NumberFormat nf = NumberFormat.getInstance(); + nf.setMaximumFractionDigits(3); + return nf.format((double) numerator / (double) divisor); + } + + + private static class Option { + public final RationalNumber rationalNumber; + public final double error; + + private Option(final RationalNumber rationalNumber, final double error) { + this.rationalNumber = rationalNumber; + this.error = error; + } + + public static Option factory(final RationalNumber rationalNumber, final double value) { + return new Option(rationalNumber, Math.abs(rationalNumber .doubleValue() - value)); + } + + @Override + public String toString() { + return rationalNumber.toString(); + } + } + + /** + * Calculate rational number using successive approximations. + */ + public static RationalNumber valueOf(double value) { + if (value >= Integer.MAX_VALUE) { + return new RationalNumber(Integer.MAX_VALUE, 1); + } else if (value <= -Integer.MAX_VALUE) { + return new RationalNumber(-Integer.MAX_VALUE, 1); + } + + boolean negative = false; + if (value < 0) { + negative = true; + value = Math.abs(value); + } + + RationalNumber l; + RationalNumber h; + + if (value == 0) { + return new RationalNumber(0, 1); + } else if (value >= 1) { + final int approx = (int) value; + if (approx < value) { + l = new RationalNumber(approx, 1); + h = new RationalNumber(approx + 1, 1); + } else { + l = new RationalNumber(approx - 1, 1); + h = new RationalNumber(approx, 1); + } + } else { + final int approx = (int) (1.0 / value); + if ((1.0 / approx) < value) { + l = new RationalNumber(1, approx); + h = new RationalNumber(1, approx - 1); + } else { + l = new RationalNumber(1, approx + 1); + h = new RationalNumber(1, approx); + } + } + Option low = Option.factory(l, value); + Option high = Option.factory(h, value); + + Option bestOption = (low.error < high.error) ? low : high; + + final int maxIterations = 100; // value is quite high, actually. + // shouldn't matter. + for (int count = 0; bestOption.error > TOLERANCE + && count < maxIterations; count++) { + final RationalNumber mediant = RationalNumber.factoryMethod( + (long) low.rationalNumber.numerator + + (long) high.rationalNumber.numerator, + (long) low.rationalNumber.divisor + + (long) high.rationalNumber.divisor); + final Option mediantOption = Option.factory(mediant, value); + + if (value < mediant.doubleValue()) { + if (high.error <= mediantOption.error) { + break; + } + + high = mediantOption; + } else { + if (low.error <= mediantOption.error) { + break; + } + + low = mediantOption; + } + + if (mediantOption.error < bestOption.error) { + bestOption = mediantOption; + } + } + + return negative ? bestOption.rationalNumber.negate() + : bestOption.rationalNumber; + } + +} diff --git a/src/main/java/org/apache/sanselan/common/RgbBufferedImageFactory.java b/src/main/java/org/apache/commons/imaging/common/RgbBufferedImageFactory.java similarity index 78% rename from src/main/java/org/apache/sanselan/common/RgbBufferedImageFactory.java rename to src/main/java/org/apache/commons/imaging/common/RgbBufferedImageFactory.java index 65a409b..6de4deb 100644 --- a/src/main/java/org/apache/sanselan/common/RgbBufferedImageFactory.java +++ b/src/main/java/org/apache/commons/imaging/common/RgbBufferedImageFactory.java @@ -1,38 +1,36 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.common; - -import com.google.code.appengine.awt.image.BufferedImage; - -public class RgbBufferedImageFactory implements IBufferedImageFactory -{ - public BufferedImage getColorBufferedImage(int width, int height, - boolean hasAlpha) - { - if (hasAlpha) - return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - } - - public BufferedImage getGrayscaleBufferedImage(int width, int height, - boolean hasAlpha) - { - // always use color. - return getColorBufferedImage(width, height, hasAlpha); - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.common; + +import com.google.code.appengine.awt.image.BufferedImage; + +public class RgbBufferedImageFactory implements IBufferedImageFactory { + public BufferedImage getColorBufferedImage(final int width, final int height, + final boolean hasAlpha) { + if (hasAlpha) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + } + + public BufferedImage getGrayscaleBufferedImage(final int width, final int height, + final boolean hasAlpha) { + // always use color. + return getColorBufferedImage(width, height, hasAlpha); + } +} diff --git a/src/main/java/org/apache/sanselan/common/SimpleBufferedImageFactory.java b/src/main/java/org/apache/commons/imaging/common/SimpleBufferedImageFactory.java similarity index 77% rename from src/main/java/org/apache/sanselan/common/SimpleBufferedImageFactory.java rename to src/main/java/org/apache/commons/imaging/common/SimpleBufferedImageFactory.java index 6d3356b..ed7de91 100644 --- a/src/main/java/org/apache/sanselan/common/SimpleBufferedImageFactory.java +++ b/src/main/java/org/apache/commons/imaging/common/SimpleBufferedImageFactory.java @@ -1,40 +1,39 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.common; - -import com.google.code.appengine.awt.image.BufferedImage; - -public class SimpleBufferedImageFactory implements IBufferedImageFactory -{ - public BufferedImage getColorBufferedImage(int width, int height, - boolean hasAlpha) - { - if (hasAlpha) - return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - } - - public BufferedImage getGrayscaleBufferedImage(int width, int height, - boolean hasAlpha) - { - if (hasAlpha) - return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - - return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.common; + +import com.google.code.appengine.awt.image.BufferedImage; + +public class SimpleBufferedImageFactory implements IBufferedImageFactory { + public BufferedImage getColorBufferedImage(final int width, final int height, + final boolean hasAlpha) { + if (hasAlpha) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + } + + public BufferedImage getGrayscaleBufferedImage(final int width, final int height, + final boolean hasAlpha) { + if (hasAlpha) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + } +} diff --git a/src/main/java/org/apache/sanselan/common/byteSources/ByteSource.java b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSource.java similarity index 52% rename from src/main/java/org/apache/sanselan/common/byteSources/ByteSource.java rename to src/main/java/org/apache/commons/imaging/common/bytesource/ByteSource.java index d00b689..2610652 100644 --- a/src/main/java/org/apache/sanselan/common/byteSources/ByteSource.java +++ b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSource.java @@ -1,67 +1,73 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.byteSources; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.common.BinaryFileFunctions; - -public abstract class ByteSource extends BinaryFileFunctions -{ - protected final String filename; - - public ByteSource(final String filename) - { - this.filename = filename; - } - - public final InputStream getInputStream(int start) throws IOException - { - InputStream is = getInputStream(); - - skipBytes(is, start); - - return is; - } - - public abstract InputStream getInputStream() throws IOException; - - public abstract byte[] getBlock(int start, int length) throws IOException; - - public abstract byte[] getAll() throws IOException; - - /* - * This operation can be VERY expensive; for inputstream - * byte sources, the entire stream must be drained to - * determine its length. - */ - public abstract long getLength() throws IOException; - - // - // public byte[] getAll() throws IOException - // { - // return getBlock(0, (int) getLength()); - // } - - public abstract String getDescription(); - - public final String getFilename() - { - return filename; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.bytesource; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.common.BinaryFunctions; + +public abstract class ByteSource { + protected final String filename; + + public ByteSource(final String filename) { + this.filename = filename; + } + + public final InputStream getInputStream(final long start) throws IOException { + InputStream is = null; + boolean succeeded = false; + try { + is = getInputStream(); + BinaryFunctions.skipBytes(is, start); + succeeded = true; + } finally { + if (!succeeded && is != null) { + is.close(); + } + } + return is; + } + + public abstract InputStream getInputStream() throws IOException; + + public byte[] getBlock(final int start, final int length) throws IOException { + return getBlock(0xFFFFffffL & start, length); + } + + public abstract byte[] getBlock(long start, int length) throws IOException; + + public abstract byte[] getAll() throws IOException; + + /* + * This operation can be VERY expensive; for inputstream byte sources, the + * entire stream must be drained to determine its length. + */ + public abstract long getLength() throws IOException; + + // + // public byte[] getAll() throws IOException + // { + // return getBlock(0, (int) getLength()); + // } + + public abstract String getDescription(); + + public final String getFilename() { + return filename; + } +} diff --git a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceArray.java b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceArray.java similarity index 66% rename from src/main/java/org/apache/sanselan/common/byteSources/ByteSourceArray.java rename to src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceArray.java index a152d5f..4e0911c 100644 --- a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceArray.java +++ b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceArray.java @@ -1,73 +1,72 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.byteSources; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class ByteSourceArray extends ByteSource -{ - private final byte bytes[]; - - public ByteSourceArray(String filename, byte bytes[]) - { - super(filename); - this.bytes = bytes; - } - - public ByteSourceArray(byte bytes[]) - { - super(null); - this.bytes = bytes; - } - - public InputStream getInputStream() - { - return new ByteArrayInputStream(bytes); - } - - public byte[] getBlock(int start, int length) throws IOException - { - // We include a separate check for int overflow. - if ((start < 0) || (length < 0) || (start + length < 0) || (start + length > bytes.length)) { - throw new IOException("Could not read block (block start: " + start - + ", block length: " + length + ", data length: " - + bytes.length + ")."); - } - - byte result[] = new byte[length]; - System.arraycopy(bytes, start, result, 0, length); - return result; - } - - public long getLength() - { - return bytes.length; - } - - public byte[] getAll() throws IOException - { - return bytes; - } - - public String getDescription() - { - return bytes.length + " byte array"; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.bytesource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ByteSourceArray extends ByteSource { + private final byte[] bytes; + + public ByteSourceArray(final String filename, final byte[] bytes) { + super(filename); + this.bytes = bytes; + } + + public ByteSourceArray(final byte[] bytes) { + super(null); + this.bytes = bytes; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public byte[] getBlock(final long startLong, final int length) throws IOException { + final int start = (int) startLong; + // We include a separate check for int overflow. + if ((start < 0) || (length < 0) || (start + length < 0) + || (start + length > bytes.length)) { + throw new IOException("Could not read block (block start: " + start + + ", block length: " + length + ", data length: " + + bytes.length + ")."); + } + + final byte[] result = new byte[length]; + System.arraycopy(bytes, start, result, 0, length); + return result; + } + + @Override + public long getLength() { + return bytes.length; + } + + @Override + public byte[] getAll() throws IOException { + return bytes; + } + + @Override + public String getDescription() { + return bytes.length + " byte array"; + } + +} diff --git a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceFile.java b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceFile.java similarity index 50% rename from src/main/java/org/apache/sanselan/common/byteSources/ByteSourceFile.java rename to src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceFile.java index 652e45a..a8c5f3c 100644 --- a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceFile.java +++ b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceFile.java @@ -1,123 +1,100 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.byteSources; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; - -import org.apache.sanselan.util.Debug; - -public class ByteSourceFile extends ByteSource -{ - private final File file; - - public ByteSourceFile(File file) - { - super(file.getName()); - this.file = file; - } - - public InputStream getInputStream() throws IOException - { - FileInputStream is = null; - BufferedInputStream bis = null; - is = new FileInputStream(file); - bis = new BufferedInputStream(is); - return bis; - } - - public byte[] getBlock(int start, int length) throws IOException - { - - RandomAccessFile raf = null; - try - { - raf = new RandomAccessFile(file, "r"); - - // We include a separate check for int overflow. - if ((start < 0) || (length < 0) || (start + length < 0) || (start + length > raf.length())) { - throw new IOException("Could not read block (block start: " + start - + ", block length: " + length + ", data length: " - + raf.length() + ")."); - } - - return getRAFBytes(raf, start, length, - "Could not read value from file"); - } - finally - { - try - { - if (raf != null) { - raf.close(); - } - } - catch (Exception e) - { - Debug.debug(e); - } - - } - } - - public long getLength() - { - return file.length(); - } - - public byte[] getAll() throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - InputStream is = null; - try - { - is = new FileInputStream(file); - is = new BufferedInputStream(is); - byte buffer[] = new byte[1024]; - int read; - while ((read = is.read(buffer)) > 0) - { - baos.write(buffer, 0, read); - } - return baos.toByteArray(); - } - finally - { - try - { - if (null != is) - is.close(); - } - catch (IOException e) - { - // Debug.d - } - } - } - - public String getDescription() - { - return "File: '" + file.getAbsolutePath() + "'"; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.bytesource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.util.IoUtils; + +public class ByteSourceFile extends ByteSource { + private final File file; + + public ByteSourceFile(final File file) { + super(file.getName()); + this.file = file; + } + + @Override + public InputStream getInputStream() throws IOException { + return new BufferedInputStream(new FileInputStream(file)); + } + + @Override + public byte[] getBlock(final long start, final int length) throws IOException { + + RandomAccessFile raf = null; + boolean canThrow = false; + try { + raf = new RandomAccessFile(file, "r"); + + // We include a separate check for int overflow. + if ((start < 0) || (length < 0) || (start + length < 0) + || (start + length > raf.length())) { + throw new IOException("Could not read block (block start: " + + start + ", block length: " + length + + ", data length: " + raf.length() + ")."); + } + + final byte[] ret = BinaryFunctions.getRAFBytes(raf, start, length, + "Could not read value from file"); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, raf); + } + } + + @Override + public long getLength() { + return file.length(); + } + + @Override + public byte[] getAll() throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + InputStream is = null; + boolean canThrow = false; + try { + is = new FileInputStream(file); + is = new BufferedInputStream(is); + final byte[] buffer = new byte[1024]; + int read; + while ((read = is.read(buffer)) > 0) { + baos.write(buffer, 0, read); + } + final byte[] ret = baos.toByteArray(); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public String getDescription() { + return "File: '" + file.getAbsolutePath() + "'"; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceInputStream.java b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceInputStream.java new file mode 100644 index 0000000..f317056 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/bytesource/ByteSourceInputStream.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.bytesource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.common.BinaryFunctions; + +public class ByteSourceInputStream extends ByteSource { + private final InputStream is; + private CacheBlock cacheHead; + private static final int BLOCK_SIZE = 1024; + private byte[] readBuffer; + private long streamLength = -1; + + public ByteSourceInputStream(final InputStream is, final String filename) { + super(filename); + this.is = new BufferedInputStream(is); + } + + private class CacheBlock { + public final byte[] bytes; + private CacheBlock next; + private boolean triedNext; + + public CacheBlock(final byte[] bytes) { + this.bytes = bytes; + } + + public CacheBlock getNext() throws IOException { + if (null != next) { + return next; + } + if (triedNext) { + return null; + } + triedNext = true; + next = readBlock(); + return next; + } + + } + + private CacheBlock readBlock() throws IOException { + if (null == readBuffer) { + readBuffer = new byte[BLOCK_SIZE]; + } + + final int read = is.read(readBuffer); + if (read < 1) { + return null; + } else if (read < BLOCK_SIZE) { + // return a copy. + final byte[] result = new byte[read]; + System.arraycopy(readBuffer, 0, result, 0, read); + return new CacheBlock(result); + } else { + // return current buffer. + final byte[] result = readBuffer; + readBuffer = null; + return new CacheBlock(result); + } + } + + private CacheBlock getFirstBlock() throws IOException { + if (null == cacheHead) { + cacheHead = readBlock(); + } + return cacheHead; + } + + private class CacheReadingInputStream extends InputStream { + private CacheBlock block; + private boolean readFirst; + private int blockIndex; + + @Override + public int read() throws IOException { + if (null == block) { + if (readFirst) { + return -1; + } + block = getFirstBlock(); + readFirst = true; + } + + if (block != null && blockIndex >= block.bytes.length) { + block = block.getNext(); + blockIndex = 0; + } + + if (null == block) { + return -1; + } + + if (blockIndex >= block.bytes.length) { + return -1; + } + + return 0xff & block.bytes[blockIndex++]; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + // first section copied verbatim from InputStream + if (b == null) { + throw new NullPointerException(); + } else if ((off < 0) || (off > b.length) || (len < 0) + || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + // optimized block read + + if (null == block) { + if (readFirst) { + return -1; + } + block = getFirstBlock(); + readFirst = true; + } + + if (block != null && blockIndex >= block.bytes.length) { + block = block.getNext(); + blockIndex = 0; + } + + if (null == block) { + return -1; + } + + if (blockIndex >= block.bytes.length) { + return -1; + } + + final int readSize = Math.min(len, block.bytes.length - blockIndex); + System.arraycopy(block.bytes, blockIndex, b, off, readSize); + blockIndex += readSize; + return readSize; + } + + @Override + public long skip(final long n) throws IOException { + + long remaining = n; + + if (n <= 0) { + return 0; + } + + while (remaining > 0) { + // read the first block + if (null == block) { + if (readFirst) { + return -1; + } + block = getFirstBlock(); + readFirst = true; + } + + // get next block + if (block != null && blockIndex >= block.bytes.length) { + block = block.getNext(); + blockIndex = 0; + } + + if (null == block) { + break; + } + + if (blockIndex >= block.bytes.length) { + break; + } + + final int readSize = Math.min((int) Math.min(BLOCK_SIZE, remaining), block.bytes.length - blockIndex); + + blockIndex += readSize; + remaining -= readSize; + } + + return n - remaining; + } + + } + + @Override + public InputStream getInputStream() throws IOException { + return new CacheReadingInputStream(); + } + + @Override + public byte[] getBlock(final long blockStart, final int blockLength) throws IOException { + // We include a separate check for int overflow. + if ((blockStart < 0) || (blockLength < 0) + || (blockStart + blockLength < 0) + || (blockStart + blockLength > streamLength)) { + throw new IOException("Could not read block (block start: " + + blockStart + ", block length: " + blockLength + + ", data length: " + streamLength + ")."); + } + + final InputStream cis = getInputStream(); + BinaryFunctions.skipBytes(cis, blockStart); + + final byte[] bytes = new byte[blockLength]; + int total = 0; + while (true) { + final int read = cis.read(bytes, total, bytes.length - total); + if (read < 1) { + throw new IOException("Could not read block."); + } + total += read; + if (total >= blockLength) { + return bytes; + } + } + } + + @Override + public long getLength() throws IOException { + if (streamLength >= 0) { + return streamLength; + } + + final InputStream cis = getInputStream(); + long result = 0; + long skipped; + while ((skipped = cis.skip(1024)) > 0) { + result += skipped; + } + streamLength = result; + return result; + } + + @Override + public byte[] getAll() throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + CacheBlock block = getFirstBlock(); + while (block != null) { + baos.write(block.bytes); + block = block.getNext(); + } + return baos.toByteArray(); + } + + @Override + public String getDescription() { + return "Inputstream: '" + filename + "'"; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/common/bytesource/package-info.java b/src/main/java/org/apache/commons/imaging/common/bytesource/package-info.java new file mode 100644 index 0000000..f249845 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/bytesource/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Encapsulates sources from which data may be read. + */ +package org.apache.commons.imaging.common.bytesource; + diff --git a/src/main/java/org/apache/commons/imaging/common/itu_t4/BitArrayOutputStream.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/BitArrayOutputStream.java new file mode 100644 index 0000000..1f71ea6 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/BitArrayOutputStream.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +import java.io.OutputStream; + +/** + * Output stream writing to a byte array, and capable + * of writing 1 bit at a time, starting from the most significant bit. + */ +class BitArrayOutputStream extends OutputStream { + private byte[] buffer; + private int bytesWritten; + private int cache; + private int cacheMask = 0x80; + + public BitArrayOutputStream() { + buffer = new byte[16]; + } + + public BitArrayOutputStream(final int size) { + buffer = new byte[size]; + } + + public int size() { + return bytesWritten; + } + + public byte[] toByteArray() { + flush(); + if (bytesWritten == buffer.length) { + return buffer; + } + final byte[] out = new byte[bytesWritten]; + System.arraycopy(buffer, 0, out, 0, bytesWritten); + return out; + } + + @Override + public void close() { + flush(); + } + + @Override + public void flush() { + if (cacheMask != 0x80) { + writeByte(cache); + cache = 0; + cacheMask = 0x80; + } + } + + @Override + public void write(final int b) { + flush(); + writeByte(b); + } + + public void writeBit(final int bit) { + if (bit != 0) { + cache |= cacheMask; + } + cacheMask >>>= 1; + if (cacheMask == 0) { + flush(); + } + } + + public int getBitsAvailableInCurrentByte() { + int count = 0; + for (int mask = cacheMask; mask != 0; mask >>>= 1) { + ++count; + } + return count; + } + + private void writeByte(final int b) { + if (bytesWritten >= buffer.length) { + final byte[] bigger = new byte[buffer.length * 2]; + System.arraycopy(buffer, 0, bigger, 0, bytesWritten); + buffer = bigger; + } + buffer[bytesWritten++] = (byte) b; + } +} diff --git a/src/main/java/org/apache/sanselan/common/BitInputStreamFlexible.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/BitInputStreamFlexible.java similarity index 61% rename from src/main/java/org/apache/sanselan/common/BitInputStreamFlexible.java rename to src/main/java/org/apache/commons/imaging/common/itu_t4/BitInputStreamFlexible.java index 6f85b8b..a321ac2 100644 --- a/src/main/java/org/apache/sanselan/common/BitInputStreamFlexible.java +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/BitInputStreamFlexible.java @@ -1,114 +1,108 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.IOException; -import java.io.InputStream; - -public class BitInputStreamFlexible extends InputStream - implements - BinaryConstants -{ - // TODO should be byte order conscious, ie TIFF for reading - // samples size<8 - shuoldn't that effect their order within byte? - private final InputStream is; - - public BitInputStreamFlexible(InputStream is) - { - this.is = is; - // super(is); - } - - public int read() throws IOException - { - if (cacheBitsRemaining > 0) - throw new IOException("BitInputStream: incomplete bit read"); - return is.read(); - } - - private int cache; - private int cacheBitsRemaining = 0; - private long bytesRead = 0; - - public final int readBits(int count) throws IOException - { - - if (count <= 32) // catch-all - { - int result = 0; - // int done = 0; - - if (cacheBitsRemaining > 0) - { - if (count >= cacheBitsRemaining) - { - result = ((1 << cacheBitsRemaining) - 1) & cache; - count -= cacheBitsRemaining; - cacheBitsRemaining = 0; - } - else - { - // cache >>= count; - cacheBitsRemaining -= count; - result = ((1 << count) - 1) & (cache >> cacheBitsRemaining); - count = 0; - } - } - while (count >= 8) - { - cache = is.read(); - if (cache < 0) - throw new IOException("couldn't read bits"); - System.out.println("cache 1: " + cache + " (" - + Integer.toHexString(cache) + ", " - + Integer.toBinaryString(cache) + ")"); - bytesRead++; - result = (result << 8) | (0xff & cache); - count -= 8; - } - if (count > 0) - { - cache = is.read(); - if (cache < 0) - throw new IOException("couldn't read bits"); - System.out.println("cache 2: " + cache + " (" - + Integer.toHexString(cache) + ", " - + Integer.toBinaryString(cache) + ")"); - bytesRead++; - cacheBitsRemaining = 8 - count; - result = (result << count) - | (((1 << count) - 1) & (cache >> cacheBitsRemaining)); - count = 0; - } - - return result; - } - - throw new IOException("BitInputStream: unknown error"); - - } - - public void flushCache() - { - cacheBitsRemaining = 0; - } - - public long getBytesRead() - { - return bytesRead; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Input stream that allows reading up to 32 bits + * across byte boundaries in most significant + * bit first order. + */ +class BitInputStreamFlexible extends InputStream { + // TODO should be byte order conscious, ie TIFF for reading + // samples size<8 - shuoldn't that effect their order within byte? + private final InputStream is; + private int cache; + private int cacheBitsRemaining; + private long bytesRead; + + public BitInputStreamFlexible(final InputStream is) { + this.is = is; + // super(is); + } + + @Override + public int read() throws IOException { + if (cacheBitsRemaining > 0) { + throw new IOException("BitInputStream: incomplete bit read"); + } + return is.read(); + } + + public final int readBits(int count) throws IOException { + + if (count <= 32) { + // catch-all + int result = 0; + // int done = 0; + + if (cacheBitsRemaining > 0) { + if (count >= cacheBitsRemaining) { + result = ((1 << cacheBitsRemaining) - 1) & cache; + count -= cacheBitsRemaining; + cacheBitsRemaining = 0; + } else { + // cache >>= count; + cacheBitsRemaining -= count; + result = ((1 << count) - 1) & (cache >> cacheBitsRemaining); + count = 0; + } + } + while (count >= 8) { + cache = is.read(); + if (cache < 0) { + throw new IOException("couldn't read bits"); + } + // System.out.println("cache 1: " + cache + " (" + // + Integer.toHexString(cache) + ", " + // + Integer.toBinaryString(cache) + ")"); + bytesRead++; + result = (result << 8) | (0xff & cache); + count -= 8; + } + if (count > 0) { + cache = is.read(); + if (cache < 0) { + throw new IOException("couldn't read bits"); + } + // System.out.println("cache 2: " + cache + " (" + // + Integer.toHexString(cache) + ", " + // + Integer.toBinaryString(cache) + ")"); + bytesRead++; + cacheBitsRemaining = 8 - count; + result = (result << count) + | (((1 << count) - 1) & (cache >> cacheBitsRemaining)); + count = 0; + } + + return result; + } + + throw new IOException("BitInputStream: unknown error"); + + } + + public void flushCache() { + cacheBitsRemaining = 0; + } + + public long getBytesRead() { + return bytesRead; + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTree.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTree.java new file mode 100644 index 0000000..119263e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTree.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A Huffman tree implemented as 1 array for high locality of reference. + */ +class HuffmanTree { + private final List> nodes = new ArrayList>(); + + private final static class Node { + boolean empty = true; + T value; + } + + public final void insert(final String pattern, final T value) throws HuffmanTreeException { + int position = 0; + Node node = growAndGetNode(position); + if (node.value != null) { + throw new HuffmanTreeException("Can't add child to a leaf"); + } + for (int patternPosition = 0; patternPosition < pattern.length(); patternPosition++) { + final char nextChar = pattern.charAt(patternPosition); + if (nextChar == '0') { + position = (position << 1) + 1; + } else { + position = (position + 1) << 1; + } + node = growAndGetNode(position); + if (node.value != null) { + throw new HuffmanTreeException("Can't add child to a leaf"); + } + } + node.value = value; + } + + private Node growAndGetNode(final int position) { + while (position >= nodes.size()) { + nodes.add(new Node()); + } + final Node node = nodes.get(position); + node.empty = false; + return node; + } + + public final T decode(final BitInputStreamFlexible bitStream) throws HuffmanTreeException { + int position = 0; + Node node = nodes.get(0); + while (node.value == null) { + int nextBit; + try { + nextBit = bitStream.readBits(1); + } catch (final IOException ioEx) { + throw new HuffmanTreeException( + "Error reading stream for huffman tree", ioEx); + } + if (nextBit == 0) { + position = (position << 1) + 1; + } else { + position = (position + 1) << 1; + } + if (position >= nodes.size()) { + throw new HuffmanTreeException("Invalid bit pattern"); + } + node = nodes.get(position); + if (node.empty) { + throw new HuffmanTreeException("Invalid bit pattern"); + } + } + return node.value; + } +} diff --git a/src/main/java/org/apache/sanselan/ImageWriteException.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTreeException.java similarity index 70% rename from src/main/java/org/apache/sanselan/ImageWriteException.java rename to src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTreeException.java index 05b0ccd..ded4a5b 100644 --- a/src/main/java/org/apache/sanselan/ImageWriteException.java +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/HuffmanTreeException.java @@ -1,32 +1,29 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -public class ImageWriteException extends SanselanException -{ - static final long serialVersionUID = -1L; - - public ImageWriteException(String s) - { - super(s); - } - - public ImageWriteException(String s, Exception e) - { - super(s, e); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +class HuffmanTreeException extends Exception { + private static final long serialVersionUID = 1L; + + public HuffmanTreeException(final String message) { + super(message); + } + + public HuffmanTreeException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/itu_t4/T4AndT6Compression.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/T4AndT6Compression.java new file mode 100644 index 0000000..864b3cf --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/T4AndT6Compression.java @@ -0,0 +1,754 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.itu_t4.T4_T6_Tables.Entry; +import org.apache.commons.imaging.util.IoUtils; + +public final class T4AndT6Compression { + private static final HuffmanTree WHITE_RUN_LENGTHS = new HuffmanTree(); + private static final HuffmanTree BLACK_RUN_LENGTHS = new HuffmanTree(); + private static final HuffmanTree CONTROL_CODES = new HuffmanTree(); + + public static final int WHITE = 0; + public static final int BLACK = 1; + + static { + try { + for (final Entry entry : T4_T6_Tables.WHITE_TERMINATING_CODES) { + WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value); + } + for (final Entry entry : T4_T6_Tables.WHITE_MAKE_UP_CODES) { + WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value); + } + for (final Entry entry : T4_T6_Tables.BLACK_TERMINATING_CODES) { + BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value); + } + for (final Entry entry : T4_T6_Tables.BLACK_MAKE_UP_CODES) { + BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value); + } + for (final Entry entry : T4_T6_Tables.ADDITIONAL_MAKE_UP_CODES) { + WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value); + BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value); + } + CONTROL_CODES.insert(T4_T6_Tables.EOL.bitString, T4_T6_Tables.EOL); + CONTROL_CODES.insert(T4_T6_Tables.EOL13.bitString, T4_T6_Tables.EOL13); + CONTROL_CODES.insert(T4_T6_Tables.EOL14.bitString, T4_T6_Tables.EOL14); + CONTROL_CODES.insert(T4_T6_Tables.EOL15.bitString, T4_T6_Tables.EOL15); + CONTROL_CODES.insert(T4_T6_Tables.EOL16.bitString, T4_T6_Tables.EOL16); + CONTROL_CODES.insert(T4_T6_Tables.EOL17.bitString, T4_T6_Tables.EOL17); + CONTROL_CODES.insert(T4_T6_Tables.EOL18.bitString, T4_T6_Tables.EOL18); + CONTROL_CODES.insert(T4_T6_Tables.EOL19.bitString, T4_T6_Tables.EOL19); + CONTROL_CODES.insert(T4_T6_Tables.P.bitString, T4_T6_Tables.P); + CONTROL_CODES.insert(T4_T6_Tables.H.bitString, T4_T6_Tables.H); + CONTROL_CODES.insert(T4_T6_Tables.V0.bitString, T4_T6_Tables.V0); + CONTROL_CODES.insert(T4_T6_Tables.VL1.bitString, T4_T6_Tables.VL1); + CONTROL_CODES.insert(T4_T6_Tables.VL2.bitString, T4_T6_Tables.VL2); + CONTROL_CODES.insert(T4_T6_Tables.VL3.bitString, T4_T6_Tables.VL3); + CONTROL_CODES.insert(T4_T6_Tables.VR1.bitString, T4_T6_Tables.VR1); + CONTROL_CODES.insert(T4_T6_Tables.VR2.bitString, T4_T6_Tables.VR2); + CONTROL_CODES.insert(T4_T6_Tables.VR3.bitString, T4_T6_Tables.VR3); + } catch (final HuffmanTreeException cannotHappen) { + throw new Error(cannotHappen); + } + } + + private T4AndT6Compression() { + } + + private static void compress1DLine(final BitInputStreamFlexible inputStream, + final BitArrayOutputStream outputStream, final int[] referenceLine, final int width) + throws ImageWriteException { + int color = WHITE; + int runLength = 0; + + for (int x = 0; x < width; x++) { + try { + final int nextColor = inputStream.readBits(1); + if (referenceLine != null) { + referenceLine[x] = nextColor; + } + if (color == nextColor) { + ++runLength; + } else { + writeRunLength(outputStream, runLength, color); + color = nextColor; + runLength = 1; + } + } catch (final IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + + writeRunLength(outputStream, runLength, color); + } + + /** + * Compressed with the "Modified Huffman" encoding of section 10 in the + * TIFF6 specification. No EOLs, no RTC, rows are padded to end on a byte + * boundary. + * + * @param uncompressed + * @param width + * @param height + * @return the compressed data + * @throws ImageWriteException + */ + public static byte[] compressModifiedHuffman(final byte[] uncompressed, + final int width, final int height) throws ImageWriteException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + for (int y = 0; y < height; y++) { + compress1DLine(inputStream, outputStream, null, width); + inputStream.flushCache(); + outputStream.flush(); + } + return outputStream.toByteArray(); + } + + /** + * Decompresses the "Modified Huffman" encoding of section 10 in the TIFF6 + * specification. No EOLs, no RTC, rows are padded to end on a byte + * boundary. + * + * @param compressed + * @param width + * @param height + * @return the decompressed data + * @throws ImageReadException + */ + public static byte[] decompressModifiedHuffman(final byte[] compressed, + final int width, final int height) throws ImageReadException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible( + new ByteArrayInputStream(compressed)); + BitArrayOutputStream outputStream = null; + boolean canThrow = false; + try { + outputStream = new BitArrayOutputStream(); + for (int y = 0; y < height; y++) { + int color = WHITE; + int rowLength; + for (rowLength = 0; rowLength < width;) { + final int runLength = readTotalRunLength(inputStream, color); + for (int i = 0; i < runLength; i++) { + outputStream.writeBit(color); + } + color = 1 - color; + rowLength += runLength; + } + + if (rowLength == width) { + inputStream.flushCache(); + outputStream.flush(); + } else if (rowLength > width) { + throw new ImageReadException("Unrecoverable row length error in image row " + y); + } + } + final byte[] ret = outputStream.toByteArray(); + canThrow = true; + return ret; + } finally { + try { + IoUtils.closeQuietly(canThrow, outputStream); + } catch (final IOException ioException) { + throw new ImageReadException("I/O error", ioException); + } + } + } + + public static byte[] compressT4_1D(final byte[] uncompressed, final int width, + final int height, final boolean hasFill) throws ImageWriteException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + if (hasFill) { + T4_T6_Tables.EOL16.writeBits(outputStream); + } else { + T4_T6_Tables.EOL.writeBits(outputStream); + } + + for (int y = 0; y < height; y++) { + compress1DLine(inputStream, outputStream, null, width); + if (hasFill) { + int bitsAvailable = outputStream + .getBitsAvailableInCurrentByte(); + if (bitsAvailable < 4) { + outputStream.flush(); + bitsAvailable = 8; + } + for (; bitsAvailable > 4; bitsAvailable--) { + outputStream.writeBit(0); + } + } + T4_T6_Tables.EOL.writeBits(outputStream); + inputStream.flushCache(); + } + + return outputStream.toByteArray(); + } + + /** + * Decompresses T.4 1D encoded data. EOL at the beginning and after each + * row, can be preceded by fill bits to fit on a byte boundary, no RTC. + * + * @param compressed + * @param width + * @param height + * @return the decompressed data + * @throws ImageReadException + */ + public static byte[] decompressT4_1D(final byte[] compressed, final int width, + final int height, final boolean hasFill) throws ImageReadException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed)); + BitArrayOutputStream outputStream = null; + boolean canThrow = false; + try { + outputStream = new BitArrayOutputStream(); + for (int y = 0; y < height; y++) { + int rowLength; + try { + T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream); + if (!isEOL(entry, hasFill)) { + throw new ImageReadException("Expected EOL not found"); + } + int color = WHITE; + for (rowLength = 0; rowLength < width;) { + final int runLength = readTotalRunLength(inputStream, color); + for (int i = 0; i < runLength; i++) { + outputStream.writeBit(color); + } + color = 1 - color; + rowLength += runLength; + } + } catch (final HuffmanTreeException huffmanException) { + throw new ImageReadException("Decompression error", huffmanException); + } + + if (rowLength == width) { + outputStream.flush(); + } else if (rowLength > width) { + throw new ImageReadException("Unrecoverable row length error in image row " + y); + } + } + final byte[] ret = outputStream.toByteArray(); + canThrow = true; + return ret; + } finally { + try { + IoUtils.closeQuietly(canThrow, outputStream); + } catch (final IOException ioException) { + throw new ImageReadException("I/O error", ioException); + } + } + } + + public static byte[] compressT4_2D(final byte[] uncompressed, final int width, + final int height, final boolean hasFill, final int parameterK) + throws ImageWriteException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + int[] referenceLine = new int[width]; + int[] codingLine = new int[width]; + int kCounter = 0; + if (hasFill) { + T4_T6_Tables.EOL16.writeBits(outputStream); + } else { + T4_T6_Tables.EOL.writeBits(outputStream); + } + + for (int y = 0; y < height; y++) { + if (kCounter > 0) { + // 2D + outputStream.writeBit(0); + for (int i = 0; i < width; i++) { + try { + codingLine[i] = inputStream.readBits(1); + } catch (final IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int a1 = nextChangingElement(codingLine, codingA0Color, 0); + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width;) { + if (b2 < a1) { + T4_T6_Tables.P.writeBits(outputStream); + a0 = b2; + } else { + final int a1b1 = a1 - b1; + if (-3 <= a1b1 && a1b1 <= 3) { + T4_T6_Tables.Entry entry; + if (a1b1 == -3) { + entry = T4_T6_Tables.VL3; + } else if (a1b1 == -2) { + entry = T4_T6_Tables.VL2; + } else if (a1b1 == -1) { + entry = T4_T6_Tables.VL1; + } else if (a1b1 == 0) { + entry = T4_T6_Tables.V0; + } else if (a1b1 == 1) { + entry = T4_T6_Tables.VR1; + } else if (a1b1 == 2) { + entry = T4_T6_Tables.VR2; + } else { + entry = T4_T6_Tables.VR3; + } + entry.writeBits(outputStream); + codingA0Color = 1 - codingA0Color; + a0 = a1; + } else { + final int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1); + final int a0a1 = a1 - a0; + final int a1a2 = a2 - a1; + T4_T6_Tables.H.writeBits(outputStream); + writeRunLength(outputStream, a0a1, codingA0Color); + writeRunLength(outputStream, a1a2, 1 - codingA0Color); + a0 = a2; + } + } + referenceA0Color = changingElementAt(referenceLine, a0); + a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + } + final int[] swap = referenceLine; + referenceLine = codingLine; + codingLine = swap; + } else { + // 1D + outputStream.writeBit(1); + compress1DLine(inputStream, outputStream, referenceLine, width); + } + if (hasFill) { + int bitsAvailable = outputStream + .getBitsAvailableInCurrentByte(); + if (bitsAvailable < 4) { + outputStream.flush(); + bitsAvailable = 8; + } + for (; bitsAvailable > 4; bitsAvailable--) { + outputStream.writeBit(0); + } + } + T4_T6_Tables.EOL.writeBits(outputStream); + kCounter++; + if (kCounter == parameterK) { + kCounter = 0; + } + inputStream.flushCache(); + } + + return outputStream.toByteArray(); + } + + /** + * Decompressed T.4 2D encoded data. EOL at the beginning and after each + * row, can be preceded by fill bits to fit on a byte boundary, and is + * succeeded by a tag bit determining whether the next line is encoded using + * 1D or 2D. No RTC. + * + * @param compressed + * @param width + * @param height + * @return the decompressed data + * @throws ImageReadException + */ + public static byte[] decompressT4_2D(final byte[] compressed, final int width, + final int height, final boolean hasFill) throws ImageReadException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + final int[] referenceLine = new int[width]; + for (int y = 0; y < height; y++) { + int rowLength = 0; + try { + T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream); + if (!isEOL(entry, hasFill)) { + throw new ImageReadException("Expected EOL not found"); + } + final int tagBit = inputStream.readBits(1); + if (tagBit == 0) { + // 2D + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width;) { + int a1; + int a2; + entry = CONTROL_CODES.decode(inputStream); + if (entry == T4_T6_Tables.P) { + fillRange(outputStream, referenceLine, a0, b2, codingA0Color); + a0 = b2; + } else if (entry == T4_T6_Tables.H) { + final int a0a1 = readTotalRunLength(inputStream, codingA0Color); + a1 = a0 + a0a1; + fillRange(outputStream, referenceLine, a0, a1, codingA0Color); + final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color); + a2 = a1 + a1a2; + fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color); + a0 = a2; + } else { + int a1b1; + if (entry == T4_T6_Tables.V0) { + a1b1 = 0; + } else if (entry == T4_T6_Tables.VL1) { + a1b1 = -1; + } else if (entry == T4_T6_Tables.VL2) { + a1b1 = -2; + } else if (entry == T4_T6_Tables.VL3) { + a1b1 = -3; + } else if (entry == T4_T6_Tables.VR1) { + a1b1 = 1; + } else if (entry == T4_T6_Tables.VR2) { + a1b1 = 2; + } else if (entry == T4_T6_Tables.VR3) { + a1b1 = 3; + } else { + throw new ImageReadException("Invalid/unknown T.4 control code " + entry.bitString); + } + a1 = b1 + a1b1; + fillRange(outputStream, referenceLine, a0, a1, codingA0Color); + a0 = a1; + codingA0Color = 1 - codingA0Color; + } + referenceA0Color = changingElementAt(referenceLine, a0); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + rowLength = a0; + } + } else { + // 1D + int color = WHITE; + for (rowLength = 0; rowLength < width;) { + final int runLength = readTotalRunLength(inputStream, color); + for (int i = 0; i < runLength; i++) { + outputStream.writeBit(color); + referenceLine[rowLength + i] = color; + } + color = 1 - color; + rowLength += runLength; + } + } + } catch (final IOException ioException) { + throw new ImageReadException("Decompression error", ioException); + } catch (final HuffmanTreeException huffmanException) { + throw new ImageReadException("Decompression error", huffmanException); + } + + if (rowLength == width) { + outputStream.flush(); + } else if (rowLength > width) { + throw new ImageReadException("Unrecoverable row length error in image row " + y); + } + } + + return outputStream.toByteArray(); + } + + public static byte[] compressT6(final byte[] uncompressed, final int width, final int height) + throws ImageWriteException { + BitInputStreamFlexible inputStream = null; + boolean canThrow = false; + try { + inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + int[] referenceLine = new int[width]; + int[] codingLine = new int[width]; + for (int y = 0; y < height; y++) { + for (int i = 0; i < width; i++) { + try { + codingLine[i] = inputStream.readBits(1); + } catch (final IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int a1 = nextChangingElement(codingLine, codingA0Color, 0); + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width;) { + if (b2 < a1) { + T4_T6_Tables.P.writeBits(outputStream); + a0 = b2; + } else { + final int a1b1 = a1 - b1; + if (-3 <= a1b1 && a1b1 <= 3) { + T4_T6_Tables.Entry entry; + if (a1b1 == -3) { + entry = T4_T6_Tables.VL3; + } else if (a1b1 == -2) { + entry = T4_T6_Tables.VL2; + } else if (a1b1 == -1) { + entry = T4_T6_Tables.VL1; + } else if (a1b1 == 0) { + entry = T4_T6_Tables.V0; + } else if (a1b1 == 1) { + entry = T4_T6_Tables.VR1; + } else if (a1b1 == 2) { + entry = T4_T6_Tables.VR2; + } else { + entry = T4_T6_Tables.VR3; + } + entry.writeBits(outputStream); + codingA0Color = 1 - codingA0Color; + a0 = a1; + } else { + final int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1); + final int a0a1 = a1 - a0; + final int a1a2 = a2 - a1; + T4_T6_Tables.H.writeBits(outputStream); + writeRunLength(outputStream, a0a1, codingA0Color); + writeRunLength(outputStream, a1a2, 1 - codingA0Color); + a0 = a2; + } + } + referenceA0Color = changingElementAt(referenceLine, a0); + a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + } + final int[] swap = referenceLine; + referenceLine = codingLine; + codingLine = swap; + inputStream.flushCache(); + } + // EOFB + T4_T6_Tables.EOL.writeBits(outputStream); + T4_T6_Tables.EOL.writeBits(outputStream); + final byte[] ret = outputStream.toByteArray(); + canThrow = true; + return ret; + } finally { + try { + IoUtils.closeQuietly(canThrow, inputStream); + } catch (final IOException ioException) { + throw new ImageWriteException("I/O error", ioException); + } + } + } + + /** + * Decompress T.6 encoded data. No EOLs, except for 2 consecutive ones at + * the end (the EOFB, end of fax block). No RTC. No fill bits anywhere. All + * data is 2D encoded. + * + * @param compressed + * @param width + * @param height + * @return the decompressed data + * @throws ImageReadException + */ + public static byte[] decompressT6(final byte[] compressed, final int width, final int height) + throws ImageReadException { + final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed)); + final BitArrayOutputStream outputStream = new BitArrayOutputStream(); + final int[] referenceLine = new int[width]; + for (int y = 0; y < height; y++) { + int rowLength = 0; + try { + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width;) { + int a1; + int a2; + T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream); + if (entry == T4_T6_Tables.P) { + fillRange(outputStream, referenceLine, a0, b2, codingA0Color); + a0 = b2; + } else if (entry == T4_T6_Tables.H) { + final int a0a1 = readTotalRunLength(inputStream, codingA0Color); + a1 = a0 + a0a1; + fillRange(outputStream, referenceLine, a0, a1, codingA0Color); + final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color); + a2 = a1 + a1a2; + fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color); + a0 = a2; + } else { + int a1b1; + if (entry == T4_T6_Tables.V0) { + a1b1 = 0; + } else if (entry == T4_T6_Tables.VL1) { + a1b1 = -1; + } else if (entry == T4_T6_Tables.VL2) { + a1b1 = -2; + } else if (entry == T4_T6_Tables.VL3) { + a1b1 = -3; + } else if (entry == T4_T6_Tables.VR1) { + a1b1 = 1; + } else if (entry == T4_T6_Tables.VR2) { + a1b1 = 2; + } else if (entry == T4_T6_Tables.VR3) { + a1b1 = 3; + } else { + throw new ImageReadException("Invalid/unknown T.6 control code " + entry.bitString); + } + a1 = b1 + a1b1; + fillRange(outputStream, referenceLine, a0, a1, codingA0Color); + a0 = a1; + codingA0Color = 1 - codingA0Color; + } + referenceA0Color = changingElementAt(referenceLine, a0); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + rowLength = a0; + } + } catch (final HuffmanTreeException huffmanException) { + throw new ImageReadException("Decompression error", huffmanException); + } + + if (rowLength == width) { + outputStream.flush(); + } else if (rowLength > width) { + throw new ImageReadException("Unrecoverable row length error in image row " + y); + } + } + + return outputStream.toByteArray(); + } + + private static boolean isEOL(final T4_T6_Tables.Entry entry, final boolean hasFill) { + if (entry == T4_T6_Tables.EOL) { + return true; + } + if (hasFill) { + return entry == T4_T6_Tables.EOL13 || entry == T4_T6_Tables.EOL14 + || entry == T4_T6_Tables.EOL15 + || entry == T4_T6_Tables.EOL16 + || entry == T4_T6_Tables.EOL17 + || entry == T4_T6_Tables.EOL18 + || entry == T4_T6_Tables.EOL19; + } + return false; + } + + private static void writeRunLength(final BitArrayOutputStream bitStream, + int runLength, final int color) { + final T4_T6_Tables.Entry[] makeUpCodes; + final T4_T6_Tables.Entry[] terminatingCodes; + if (color == WHITE) { + makeUpCodes = T4_T6_Tables.WHITE_MAKE_UP_CODES; + terminatingCodes = T4_T6_Tables.WHITE_TERMINATING_CODES; + } else { + makeUpCodes = T4_T6_Tables.BLACK_MAKE_UP_CODES; + terminatingCodes = T4_T6_Tables.BLACK_TERMINATING_CODES; + } + while (runLength >= 1792) { + final T4_T6_Tables.Entry entry = lowerBound( + T4_T6_Tables.ADDITIONAL_MAKE_UP_CODES, runLength); + entry.writeBits(bitStream); + runLength -= entry.value; + } + while (runLength >= 64) { + final T4_T6_Tables.Entry entry = lowerBound(makeUpCodes, runLength); + entry.writeBits(bitStream); + runLength -= entry.value; + } + final T4_T6_Tables.Entry terminatingEntry = terminatingCodes[runLength]; + terminatingEntry.writeBits(bitStream); + } + + private static T4_T6_Tables.Entry lowerBound(final T4_T6_Tables.Entry[] entries, final int value) { + int first = 0; + int last = entries.length - 1; + do { + final int middle = (first + last) >>> 1; + if (entries[middle].value <= value + && ((middle + 1) >= entries.length || value < entries[middle + 1].value)) { + return entries[middle]; + } else if (entries[middle].value > value) { + last = middle - 1; + } else { + first = middle + 1; + } + } while (first < last); + + return entries[first]; + } + + private static int readTotalRunLength(final BitInputStreamFlexible bitStream, + final int color) throws ImageReadException { + try { + int totalLength = 0; + Integer runLength; + do { + if (color == WHITE) { + runLength = WHITE_RUN_LENGTHS.decode(bitStream); + } else { + runLength = BLACK_RUN_LENGTHS.decode(bitStream); + } + totalLength += runLength; + } while (runLength > 63); + return totalLength; + } catch (final HuffmanTreeException huffmanException) { + throw new ImageReadException("Decompression error", huffmanException); + } + } + + private static int changingElementAt(final int[] line, final int position) { + if (position < 0 || position >= line.length) { + return WHITE; + } + return line[position]; + } + + private static int nextChangingElement(final int[] line, final int currentColour, final int start) { + int position; + for (position = start; position < line.length + && line[position] == currentColour; position++) { + // noop + } + + return position < line.length ? position : line.length; + } + + private static void fillRange(final BitArrayOutputStream outputStream, + final int[] referenceRow, final int a0, final int end, final int color) { + for (int i = a0; i < end; i++) { + referenceRow[i] = color; + outputStream.writeBit(color); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/itu_t4/T4_T6_Tables.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/T4_T6_Tables.java new file mode 100644 index 0000000..f396c3d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/T4_T6_Tables.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.itu_t4; + +class T4_T6_Tables { + public static final Entry[] WHITE_TERMINATING_CODES = { + new Entry("00110101", 0), + new Entry("000111", 1), + new Entry("0111", 2), + new Entry("1000", 3), + new Entry("1011", 4), + new Entry("1100", 5), + new Entry("1110", 6), + new Entry("1111", 7), + new Entry("10011", 8), + new Entry("10100", 9), + new Entry("00111", 10), + new Entry("01000", 11), + new Entry("001000", 12), + new Entry("000011", 13), + new Entry("110100", 14), + new Entry("110101", 15), + new Entry("101010", 16), + new Entry("101011", 17), + new Entry("0100111", 18), + new Entry("0001100", 19), + new Entry("0001000", 20), + new Entry("0010111", 21), + new Entry("0000011", 22), + new Entry("0000100", 23), + new Entry("0101000", 24), + new Entry("0101011", 25), + new Entry("0010011", 26), + new Entry("0100100", 27), + new Entry("0011000", 28), + new Entry("00000010", 29), + new Entry("00000011", 30), + new Entry("00011010", 31), + new Entry("00011011", 32), + new Entry("00010010", 33), + new Entry("00010011", 34), + new Entry("00010100", 35), + new Entry("00010101", 36), + new Entry("00010110", 37), + new Entry("00010111", 38), + new Entry("00101000", 39), + new Entry("00101001", 40), + new Entry("00101010", 41), + new Entry("00101011", 42), + new Entry("00101100", 43), + new Entry("00101101", 44), + new Entry("00000100", 45), + new Entry("00000101", 46), + new Entry("00001010", 47), + new Entry("00001011", 48), + new Entry("01010010", 49), + new Entry("01010011", 50), + new Entry("01010100", 51), + new Entry("01010101", 52), + new Entry("00100100", 53), + new Entry("00100101", 54), + new Entry("01011000", 55), + new Entry("01011001", 56), + new Entry("01011010", 57), + new Entry("01011011", 58), + new Entry("01001010", 59), + new Entry("01001011", 60), + new Entry("00110010", 61), + new Entry("00110011", 62), + new Entry("00110100", 63), }; + + public static final Entry[] BLACK_TERMINATING_CODES = { + new Entry("0000110111", 0), + new Entry("010", 1), + new Entry("11", 2), + new Entry("10", 3), + new Entry("011", 4), + new Entry("0011", 5), + new Entry("0010", 6), + new Entry("00011", 7), + new Entry("000101", 8), + new Entry("000100", 9), + new Entry("0000100", 10), + new Entry("0000101", 11), + new Entry("0000111", 12), + new Entry("00000100", 13), + new Entry("00000111", 14), + new Entry("000011000", 15), + new Entry("0000010111", 16), + new Entry("0000011000", 17), + new Entry("0000001000", 18), + new Entry("00001100111", 19), + new Entry("00001101000", 20), + new Entry("00001101100", 21), + new Entry("00000110111", 22), + new Entry("00000101000", 23), + new Entry("00000010111", 24), + new Entry("00000011000", 25), + new Entry("000011001010", 26), + new Entry("000011001011", 27), + new Entry("000011001100", 28), + new Entry("000011001101", 29), + new Entry("000001101000", 30), + new Entry("000001101001", 31), + new Entry("000001101010", 32), + new Entry("000001101011", 33), + new Entry("000011010010", 34), + new Entry("000011010011", 35), + new Entry("000011010100", 36), + new Entry("000011010101", 37), + new Entry("000011010110", 38), + new Entry("000011010111", 39), + new Entry("000001101100", 40), + new Entry("000001101101", 41), + new Entry("000011011010", 42), + new Entry("000011011011", 43), + new Entry("000001010100", 44), + new Entry("000001010101", 45), + new Entry("000001010110", 46), + new Entry("000001010111", 47), + new Entry("000001100100", 48), + new Entry("000001100101", 49), + new Entry("000001010010", 50), + new Entry("000001010011", 51), + new Entry("000000100100", 52), + new Entry("000000110111", 53), + new Entry("000000111000", 54), + new Entry("000000100111", 55), + new Entry("000000101000", 56), + new Entry("000001011000", 57), + new Entry("000001011001", 58), + new Entry("000000101011", 59), + new Entry("000000101100", 60), + new Entry("000001011010", 61), + new Entry("000001100110", 62), + new Entry("000001100111", 63), }; + + public static final Entry[] WHITE_MAKE_UP_CODES = { + new Entry("11011", 64), + new Entry("10010", 128), + new Entry("010111", 192), + new Entry("0110111", 256), + new Entry("00110110", 320), + new Entry("00110111", 384), + new Entry("01100100", 448), + new Entry("01100101", 512), + new Entry("01101000", 576), + new Entry("01100111", 640), + new Entry("011001100", 704), + new Entry("011001101", 768), + new Entry("011010010", 832), + new Entry("011010011", 896), + new Entry("011010100", 960), + new Entry("011010101", 1024), + new Entry("011010110", 1088), + new Entry("011010111", 1152), + new Entry("011011000", 1216), + new Entry("011011001", 1280), + new Entry("011011010", 1344), + new Entry("011011011", 1408), + new Entry("010011000", 1472), + new Entry("010011001", 1536), + new Entry("010011010", 1600), + new Entry("011000", 1664), + new Entry("010011011", 1728), }; + + public static final Entry[] BLACK_MAKE_UP_CODES = { + new Entry("0000001111", 64), + new Entry("000011001000", 128), + new Entry("000011001001", 192), + new Entry("000001011011", 256), + new Entry("000000110011", 320), + new Entry("000000110100", 384), + new Entry("000000110101", 448), + new Entry("0000001101100", 512), + new Entry("0000001101101", 576), + new Entry("0000001001010", 640), + new Entry("0000001001011", 704), + new Entry("0000001001100", 768), + new Entry("0000001001101", 832), + new Entry("0000001110010", 896), + new Entry("0000001110011", 960), + new Entry("0000001110100", 1024), + new Entry("0000001110101", 1088), + new Entry("0000001110110", 1152), + new Entry("0000001110111", 1216), + new Entry("0000001010010", 1280), + new Entry("0000001010011", 1344), + new Entry("0000001010100", 1408), + new Entry("0000001010101", 1472), + new Entry("0000001011010", 1536), + new Entry("0000001011011", 1600), + new Entry("0000001100100", 1664), + new Entry("0000001100101", 1728), }; + + public static final Entry[] ADDITIONAL_MAKE_UP_CODES = { + new Entry("00000001000", 1792), + new Entry("00000001100", 1856), + new Entry("00000001101", 1920), + new Entry("000000010010", 1984), + new Entry("000000010011", 2048), + new Entry("000000010100", 2112), + new Entry("000000010101", 2176), + new Entry("000000010110", 2240), + new Entry("000000010111", 2304), + new Entry("000000011100", 2368), + new Entry("000000011101", 2432), + new Entry("000000011110", 2496), + new Entry("000000011111", 2560), }; + + public static final Entry EOL = new Entry("000000000001", 0); + public static final Entry EOL13 = new Entry("0000000000001", 0); + public static final Entry EOL14 = new Entry("00000000000001", 0); + public static final Entry EOL15 = new Entry("000000000000001", 0); + public static final Entry EOL16 = new Entry("0000000000000001", 0); + public static final Entry EOL17 = new Entry("00000000000000001", 0); + public static final Entry EOL18 = new Entry("000000000000000001", 0); + public static final Entry EOL19 = new Entry("0000000000000000001", 0); + public static final Entry P = new Entry("0001", 0); + public static final Entry H = new Entry("001", 0); + public static final Entry V0 = new Entry("1", 0); + public static final Entry VR1 = new Entry("011", 0); + public static final Entry VR2 = new Entry("000011", 0); + public static final Entry VR3 = new Entry("0000011", 0); + public static final Entry VL1 = new Entry("010", 0); + public static final Entry VL2 = new Entry("000010", 0); + public static final Entry VL3 = new Entry("0000010", 0); + + public static class Entry { + String bitString; + Integer value; + + public Entry(final String bitString, final int value) { + this.bitString = bitString; + this.value = value; + } + + public void writeBits(final BitArrayOutputStream outputStream) { + for (int i = 0; i < bitString.length(); i++) { + if (bitString.charAt(i) == '0') { + outputStream.writeBit(0); + } else { + outputStream.writeBit(1); + } + } + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/itu_t4/package-info.java b/src/main/java/org/apache/commons/imaging/common/itu_t4/package-info.java new file mode 100644 index 0000000..9c1510f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/itu_t4/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides ITU-T T.4 and T.6 compression classes. + */ +package org.apache.commons.imaging.common.itu_t4; + diff --git a/src/main/java/org/apache/sanselan/common/mylzw/BitsToByteInputStream.java b/src/main/java/org/apache/commons/imaging/common/mylzw/BitsToByteInputStream.java similarity index 67% rename from src/main/java/org/apache/sanselan/common/mylzw/BitsToByteInputStream.java rename to src/main/java/org/apache/commons/imaging/common/mylzw/BitsToByteInputStream.java index fe78877..ba01162 100644 --- a/src/main/java/org/apache/sanselan/common/mylzw/BitsToByteInputStream.java +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/BitsToByteInputStream.java @@ -1,58 +1,56 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.mylzw; - -import java.io.IOException; -import java.io.InputStream; - -public class BitsToByteInputStream extends InputStream -{ - private final MyBitInputStream is; - private final int desiredDepth; - - public BitsToByteInputStream(MyBitInputStream is, int desiredDepth) - { - this.is = is; - this.desiredDepth = desiredDepth; - } - - public int read() throws IOException - { - return readBits(8); - } - - public int readBits(int bitCount) throws IOException - { - int i = is.readBits(bitCount); - if (bitCount < desiredDepth) - i <<= (desiredDepth - bitCount); - else if (bitCount > desiredDepth) - i >>= (bitCount - desiredDepth); - - return i; - } - - public int[] readBitsArray(int sampleBits, int length) throws IOException - { - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - result[i] = readBits(sampleBits); - - return result; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.mylzw; + +import java.io.IOException; +import java.io.InputStream; + +public class BitsToByteInputStream extends InputStream { + private final MyBitInputStream is; + private final int desiredDepth; + + public BitsToByteInputStream(final MyBitInputStream is, final int desiredDepth) { + this.is = is; + this.desiredDepth = desiredDepth; + } + + @Override + public int read() throws IOException { + return readBits(8); + } + + public int readBits(final int bitCount) throws IOException { + int i = is.readBits(bitCount); + if (bitCount < desiredDepth) { + i <<= (desiredDepth - bitCount); + } else if (bitCount > desiredDepth) { + i >>= (bitCount - desiredDepth); + } + + return i; + } + + public int[] readBitsArray(final int sampleBits, final int length) throws IOException { + final int[] result = new int[length]; + + for (int i = 0; i < length; i++) { + result[i] = readBits(sampleBits); + } + + return result; + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitInputStream.java b/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitInputStream.java new file mode 100644 index 0000000..721934a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitInputStream.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.mylzw; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +public class MyBitInputStream extends InputStream { + private final InputStream is; + private final ByteOrder byteOrder; + private boolean tiffLZWMode; + private long bytesRead; + private int bitsInCache; + private int bitCache; + + public MyBitInputStream(final InputStream is, final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + this.is = is; + } + + @Override + public int read() throws IOException { + return readBits(8); + } + + public void setTiffLZWMode() { + tiffLZWMode = true; + } + + public int readBits(final int sampleBits) throws IOException { + while (bitsInCache < sampleBits) { + final int next = is.read(); + + if (next < 0) { + if (tiffLZWMode) { + // pernicious special case! + return 257; + } + return -1; + } + + final int newByte = (0xff & next); + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + bitCache = (bitCache << 8) | newByte; + } else { + bitCache = (newByte << bitsInCache) | bitCache; + } + + bytesRead++; + bitsInCache += 8; + } + final int sampleMask = (1 << sampleBits) - 1; + + int sample; + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + sample = sampleMask & (bitCache >> (bitsInCache - sampleBits)); + } else { + sample = sampleMask & bitCache; + bitCache >>= sampleBits; + } + + final int result = sample; + + bitsInCache -= sampleBits; + final int remainderMask = (1 << bitsInCache) - 1; + bitCache &= remainderMask; + + return result; + } + + public void flushCache() { + bitsInCache = 0; + bitCache = 0; + } + + public long getBytesRead() { + return bytesRead; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitOutputStream.java b/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitOutputStream.java new file mode 100644 index 0000000..5b90cb0 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/MyBitOutputStream.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.mylzw; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; + +public class MyBitOutputStream extends OutputStream { + private final OutputStream os; + private final ByteOrder byteOrder; + private int bitsInCache; + private int bitCache; + private int bytesWritten; + + public MyBitOutputStream(final OutputStream os, final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + this.os = os; + } + + @Override + public void write(final int value) throws IOException { + writeBits(value, 8); + } + + // TODO: in and out streams CANNOT accurately read/write 32bits at a time, + // as int will overflow. should have used a long + public void writeBits(int value, final int sampleBits) throws IOException { + final int sampleMask = (1 << sampleBits) - 1; + value &= sampleMask; + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + // MSB, so add to right + bitCache = (bitCache << sampleBits) | value; + } else { + // LSB, so add to left + bitCache = bitCache | (value << bitsInCache); + } + bitsInCache += sampleBits; + + while (bitsInCache >= 8) { + if (byteOrder == ByteOrder.BIG_ENDIAN) { + // MSB, so write from left + final int b = 0xff & (bitCache >> (bitsInCache - 8)); + actualWrite(b); + + bitsInCache -= 8; + } else { + // LSB, so write from right + final int b = 0xff & bitCache; + actualWrite(b); + + bitCache >>= 8; + bitsInCache -= 8; + } + final int remainderMask = (1 << bitsInCache) - 1; // unneccesary + bitCache &= remainderMask; // unneccesary + } + + } + + private void actualWrite(final int value) throws IOException { + os.write(value); + bytesWritten++; + } + + public void flushCache() throws IOException { + if (bitsInCache > 0) { + final int bitMask = (1 << bitsInCache) - 1; + int b = bitMask & bitCache; + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + // MSB, so write from left + b <<= 8 - bitsInCache; // left align fragment. + os.write(b); + } else { + // LSB, so write from right + os.write(b); + } + } + + bitsInCache = 0; + bitCache = 0; + } + + public int getBytesWritten() { + return bytesWritten + ((bitsInCache > 0) ? 1 : 0); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwCompressor.java b/src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwCompressor.java new file mode 100644 index 0000000..418cee2 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwCompressor.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.mylzw; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +public class MyLzwCompressor { + private int codeSize; + private final int initialCodeSize; + private int codes = -1; + + private final ByteOrder byteOrder; + private final boolean earlyLimit; + private final int clearCode; + private final int eoiCode; + private final Listener listener; + private final Map map = new HashMap(); + + public MyLzwCompressor(final int initialCodeSize, final ByteOrder byteOrder, + final boolean earlyLimit) { + this(initialCodeSize, byteOrder, earlyLimit, null); + } + + public MyLzwCompressor(final int initialCodeSize, final ByteOrder byteOrder, + final boolean earlyLimit, final Listener listener) { + this.listener = listener; + this.byteOrder = byteOrder; + this.earlyLimit = earlyLimit; + + this.initialCodeSize = initialCodeSize; + + clearCode = 1 << initialCodeSize; + eoiCode = clearCode + 1; + + if (null != listener) { + listener.init(clearCode, eoiCode); + } + + initializeStringTable(); + } + + private void initializeStringTable() { + codeSize = initialCodeSize; + + final int intialEntriesCount = (1 << codeSize) + 2; + + map.clear(); + for (codes = 0; codes < intialEntriesCount; codes++) { + if ((codes != clearCode) && (codes != eoiCode)) { + final ByteArray key = arrayToKey((byte) codes); + + map.put(key, codes); + } + } + } + + private void clearTable() { + initializeStringTable(); + incrementCodeSize(); + } + + private void incrementCodeSize() { + if (codeSize != 12) { + codeSize++; + } + } + + private ByteArray arrayToKey(final byte b) { + return arrayToKey(new byte[] { b, }, 0, 1); + } + + private final static class ByteArray { + private final byte[] bytes; + private final int start; + private final int length; + private final int hash; + + public ByteArray(final byte[] bytes, final int start, final int length) { + this.bytes = bytes; + this.start = start; + this.length = length; + + int tempHash = length; + + for (int i = 0; i < length; i++) { + final int b = 0xff & bytes[i + start]; + tempHash = tempHash + (tempHash << 8) ^ b ^ i; + } + + hash = tempHash; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof ByteArray) { + final ByteArray other = (ByteArray) o; + if (other.hash != hash) { + return false; + } + if (other.length != length) { + return false; + } + + for (int i = 0; i < length; i++) { + if (other.bytes[i + other.start] != bytes[i + start]) { + return false; + } + } + + return true; + } + return false; + } + } + + private ByteArray arrayToKey(final byte[] bytes, final int start, final int length) { + return new ByteArray(bytes, start, length); + } + + private void writeDataCode(final MyBitOutputStream bos, final int code) + throws IOException { + if (null != listener) { + listener.dataCode(code); + } + writeCode(bos, code); + } + + private void writeClearCode(final MyBitOutputStream bos) throws IOException { + if (null != listener) { + listener.dataCode(clearCode); + } + writeCode(bos, clearCode); + } + + private void writeEoiCode(final MyBitOutputStream bos) throws IOException { + if (null != listener) { + listener.eoiCode(eoiCode); + } + writeCode(bos, eoiCode); + } + + private void writeCode(final MyBitOutputStream bos, final int code) + throws IOException { + bos.writeBits(code, codeSize); + } + + private boolean isInTable(final byte[] bytes, final int start, final int length) { + final ByteArray key = arrayToKey(bytes, start, length); + + return map.containsKey(key); + } + + private int codeFromString(final byte[] bytes, final int start, final int length) + throws IOException { + final ByteArray key = arrayToKey(bytes, start, length); + final Integer code = map.get(key); + if (code == null) { + throw new IOException("CodeFromString"); + } + return code; + } + + private boolean addTableEntry(final MyBitOutputStream bos, final byte[] bytes, + final int start, final int length) throws IOException { + final ByteArray key = arrayToKey(bytes, start, length); + return addTableEntry(bos, key); + } + + private boolean addTableEntry(final MyBitOutputStream bos, final ByteArray key) + throws IOException { + boolean cleared = false; + + int limit = (1 << codeSize); + if (earlyLimit) { + limit--; + } + + if (codes == limit) { + if (codeSize < 12) { + incrementCodeSize(); + } else { + writeClearCode(bos); + clearTable(); + cleared = true; + } + } + + if (!cleared) { + map.put(key, codes); + codes++; + } + + return cleared; + } + + public interface Listener { + void dataCode(int code); + + void eoiCode(int code); + + void clearCode(int code); + + void init(int clearCode, int eoiCode); + } + + public byte[] compress(final byte[] bytes) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length); + final MyBitOutputStream bos = new MyBitOutputStream(baos, byteOrder); + + initializeStringTable(); + clearTable(); + writeClearCode(bos); + + int wStart = 0; + int wLength = 0; + + for (int i = 0; i < bytes.length; i++) { + if (isInTable(bytes, wStart, wLength + 1)) { + wLength++; + } else { + final int code = codeFromString(bytes, wStart, wLength); + writeDataCode(bos, code); + addTableEntry(bos, bytes, wStart, wLength + 1); + + wStart = i; + wLength = 1; + } + } + + final int code = codeFromString(bytes, wStart, wLength); + writeDataCode(bos, code); + + writeEoiCode(bos); + + bos.flushCache(); + + return baos.toByteArray(); + } +} diff --git a/src/main/java/org/apache/sanselan/common/mylzw/MyLZWDecompressor.java b/src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwDecompressor.java similarity index 53% rename from src/main/java/org/apache/sanselan/common/mylzw/MyLZWDecompressor.java rename to src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwDecompressor.java index cb12b8d..34e4879 100644 --- a/src/main/java/org/apache/sanselan/common/mylzw/MyLZWDecompressor.java +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/MyLzwDecompressor.java @@ -1,226 +1,204 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.mylzw; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public final class MyLZWDecompressor -{ - private static final int MAX_TABLE_SIZE = 1 << 12; - - private final byte[][] table; - private int codeSize; - private final int initialCodeSize; - private int codes = -1; - - private final int byteOrder; - - private final Listener listener; - - public static interface Listener - { - public void code(int code); - - public void init(int clearCode, int eoiCode); - } - - public MyLZWDecompressor(int initialCodeSize, int byteOrder) - { - this(initialCodeSize, byteOrder, null); - } - - public MyLZWDecompressor(int initialCodeSize, int byteOrder, - Listener listener) - { - this.listener = listener; - this.byteOrder = byteOrder; - - this.initialCodeSize = initialCodeSize; - - table = new byte[MAX_TABLE_SIZE][]; - clearCode = 1 << initialCodeSize; - eoiCode = clearCode + 1; - - if (null != listener) - listener.init(clearCode, eoiCode); - - InitializeTable(); - } - - private final void InitializeTable() - { - codeSize = initialCodeSize; - - int intial_entries_count = 1 << codeSize + 2; - - for (int i = 0; i < intial_entries_count; i++) - table[i] = new byte[] { (byte) i, }; - } - - private final void clearTable() - { - codes = (1 << initialCodeSize) + 2; - codeSize = initialCodeSize; - incrementCodeSize(); - } - - private final int clearCode; - private final int eoiCode; - - private final int getNextCode(MyBitInputStream is) throws IOException - { - int code = is.readBits(codeSize); - - if (null != listener) - listener.code(code); - return code; - } - - private final byte[] stringFromCode(int code) throws IOException - { - if ((code >= codes) || (code < 0)) - throw new IOException("Bad Code: " + code + " codes: " + codes - + " code_size: " + codeSize + ", table: " + table.length); - - return table[code]; - } - - private final boolean isInTable(int Code) - { - return Code < codes; - } - - private final byte firstChar(byte bytes[]) - { - return bytes[0]; - } - - private final void addStringToTable(byte bytes[]) throws IOException - { - if (codes < (1 << codeSize)) - { - table[codes] = bytes; - codes++; - } else - throw new IOException("AddStringToTable: codes: " + codes - + " code_size: " + codeSize); - - checkCodeSize(); - } - - private final byte[] appendBytes(byte bytes[], byte b) - { - byte result[] = new byte[bytes.length + 1]; - - System.arraycopy(bytes, 0, result, 0, bytes.length); - result[result.length - 1] = b; - return result; - } - - private int written = 0; - - private final void writeToResult(OutputStream os, byte bytes[]) - throws IOException - { - os.write(bytes); - written += bytes.length; - } - - private boolean tiffLZWMode = false; - - public void setTiffLZWMode() - { - tiffLZWMode = true; - } - - public byte[] decompress(InputStream is, int expectedLength) - throws IOException - { - int code, oldCode = -1; - MyBitInputStream mbis = new MyBitInputStream(is, byteOrder); - if (tiffLZWMode) - mbis.setTiffLZWMode(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(expectedLength); - - clearTable(); - - while ((code = getNextCode(mbis)) != eoiCode) - { - if (code == clearCode) - { - clearTable(); - - if (written >= expectedLength) - break; - code = getNextCode(mbis); - - if (code == eoiCode) - { - break; - } - writeToResult(baos, stringFromCode(code)); - - oldCode = code; - } // end of ClearCode case - else - { - if (isInTable(code)) - { - writeToResult(baos, stringFromCode(code)); - - addStringToTable(appendBytes(stringFromCode(oldCode), - firstChar(stringFromCode(code)))); - oldCode = code; - } else - { - byte OutString[] = appendBytes(stringFromCode(oldCode), - firstChar(stringFromCode(oldCode))); - writeToResult(baos, OutString); - addStringToTable(OutString); - oldCode = code; - } - } // end of not-ClearCode case - - if (written >= expectedLength) - break; - } // end of while loop - - byte result[] = baos.toByteArray(); - - return result; - } - - private final void checkCodeSize() // throws IOException - { - int limit = (1 << codeSize); - if (tiffLZWMode) - limit--; - - if (codes == limit) - incrementCodeSize(); - } - - private final void incrementCodeSize() // throws IOException - { - if (codeSize != 12) - codeSize++; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.common.mylzw; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteOrder; + +public final class MyLzwDecompressor { + private static final int MAX_TABLE_SIZE = 1 << 12; + private final byte[][] table; + private int codeSize; + private final int initialCodeSize; + private int codes = -1; + private final ByteOrder byteOrder; + private final Listener listener; + private final int clearCode; + private final int eoiCode; + private int written; + private boolean tiffLZWMode; + + public interface Listener { + void code(int code); + + void init(int clearCode, int eoiCode); + } + + public MyLzwDecompressor(final int initialCodeSize, final ByteOrder byteOrder) { + this(initialCodeSize, byteOrder, null); + } + + public MyLzwDecompressor(final int initialCodeSize, final ByteOrder byteOrder, + final Listener listener) { + this.listener = listener; + this.byteOrder = byteOrder; + + this.initialCodeSize = initialCodeSize; + + table = new byte[MAX_TABLE_SIZE][]; + clearCode = 1 << initialCodeSize; + eoiCode = clearCode + 1; + + if (null != listener) { + listener.init(clearCode, eoiCode); + } + + initializeTable(); + } + + private void initializeTable() { + codeSize = initialCodeSize; + + final int intialEntriesCount = 1 << codeSize + 2; + + for (int i = 0; i < intialEntriesCount; i++) { + table[i] = new byte[] { (byte) i, }; + } + } + + private void clearTable() { + codes = (1 << initialCodeSize) + 2; + codeSize = initialCodeSize; + incrementCodeSize(); + } + + private int getNextCode(final MyBitInputStream is) throws IOException { + final int code = is.readBits(codeSize); + + if (null != listener) { + listener.code(code); + } + return code; + } + + private byte[] stringFromCode(final int code) throws IOException { + if ((code >= codes) || (code < 0)) { + throw new IOException("Bad Code: " + code + " codes: " + codes + + " code_size: " + codeSize + ", table: " + table.length); + } + return table[code]; + } + + private boolean isInTable(final int code) { + return code < codes; + } + + private byte firstChar(final byte[] bytes) { + return bytes[0]; + } + + private void addStringToTable(final byte[] bytes) throws IOException { + if (codes < (1 << codeSize)) { + table[codes] = bytes; + codes++; + } else { + throw new IOException("AddStringToTable: codes: " + codes + + " code_size: " + codeSize); + } + checkCodeSize(); + } + + private byte[] appendBytes(final byte[] bytes, final byte b) { + final byte[] result = new byte[bytes.length + 1]; + + System.arraycopy(bytes, 0, result, 0, bytes.length); + result[result.length - 1] = b; + return result; + } + + private void writeToResult(final OutputStream os, final byte[] bytes) + throws IOException { + os.write(bytes); + written += bytes.length; + } + + public void setTiffLZWMode() { + tiffLZWMode = true; + } + + public byte[] decompress(final InputStream is, final int expectedLength) throws IOException { + int code; + int oldCode = -1; + final MyBitInputStream mbis = new MyBitInputStream(is, byteOrder); + if (tiffLZWMode) { + mbis.setTiffLZWMode(); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(expectedLength); + + clearTable(); + + while ((code = getNextCode(mbis)) != eoiCode) { + if (code == clearCode) { + clearTable(); + + if (written >= expectedLength) { + break; + } + code = getNextCode(mbis); + + if (code == eoiCode) { + break; + } + writeToResult(baos, stringFromCode(code)); + + oldCode = code; + } // end of ClearCode case + else { + if (isInTable(code)) { + writeToResult(baos, stringFromCode(code)); + + addStringToTable(appendBytes(stringFromCode(oldCode), + firstChar(stringFromCode(code)))); + oldCode = code; + } else { + final byte[] outString = appendBytes(stringFromCode(oldCode), + firstChar(stringFromCode(oldCode))); + writeToResult(baos, outString); + addStringToTable(outString); + oldCode = code; + } + } // end of not-ClearCode case + + if (written >= expectedLength) { + break; + } + } // end of while loop + + return baos.toByteArray(); + } + + private void checkCodeSize() { + int limit = (1 << codeSize); + if (tiffLZWMode) { + limit--; + } + + if (codes == limit) { + incrementCodeSize(); + } + } + + private void incrementCodeSize() { + if (codeSize != 12) { + codeSize++; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/mylzw/package-info.java b/src/main/java/org/apache/commons/imaging/common/mylzw/package-info.java new file mode 100644 index 0000000..1255a06 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/mylzw/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides LZW compression. + */ +package org.apache.commons.imaging.common.mylzw; + diff --git a/src/main/java/org/apache/commons/imaging/common/package-info.java b/src/main/java/org/apache/commons/imaging/common/package-info.java new file mode 100644 index 0000000..b1b8471 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides utility classes that are employed across multiple + * image formats and sub-packages. + */ +package org.apache.commons.imaging.common; + diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/BmpHeaderInfo.java b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpHeaderInfo.java new file mode 100644 index 0000000..ea1e53c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpHeaderInfo.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +class BmpHeaderInfo { + // BM - Windows 3.1x, 95, NT + // BA - OS/2 Bitmap Array + // CI - OS/2 Color Icon + // CP - OS/2 Color Pointer + // IC - OS/2 Icon + // PT - OS/2 Pointer + public final byte identifier1; + public final byte identifier2; + + public final int fileSize; + public final int reserved; + public final int bitmapDataOffset; + + public final int bitmapHeaderSize; + public final int width; + public final int height; + public final int planes; + public final int bitsPerPixel; + public final int compression; + public final int bitmapDataSize; + public final int hResolution; + public final int vResolution; + public final int colorsUsed; + public final int colorsImportant; + + public final int redMask; + public final int greenMask; + public final int blueMask; + public final int alphaMask; + + public final int colorSpaceType; + public final ColorSpace colorSpace; + public final int gammaRed; + public final int gammaGreen; + public final int gammaBlue; + public final int intent; + public final int profileData; + public final int profileSize; + public final int reservedV5; + + public static class ColorSpaceCoordinate { + public int x; + public int y; + public int z; + } + + public static class ColorSpace { + public ColorSpaceCoordinate red; + public ColorSpaceCoordinate green; + public ColorSpaceCoordinate blue; + } + + public BmpHeaderInfo(final byte identifier1, final byte identifier2, final int fileSize, + final int reserved, final int bitmapDataOffset, final int bitmapHeaderSize, + final int width, final int height, final int planes, final int bitsPerPixel, + final int compression, final int bitmapDataSize, final int hResolution, + final int vResolution, final int colorsUsed, final int colorsImportant, final int redMask, + final int greenMask, final int blueMask, final int alphaMask, final int colorSpaceType, + final ColorSpace colorSpace, final int gammaRed, final int gammaGreen, final int gammaBlue, + final int intent, final int profileData, final int profileSize, final int reservedV5) { + this.identifier1 = identifier1; + this.identifier2 = identifier2; + this.fileSize = fileSize; + this.reserved = reserved; + this.bitmapDataOffset = bitmapDataOffset; + + this.bitmapHeaderSize = bitmapHeaderSize; + this.width = width; + this.height = height; + this.planes = planes; + this.bitsPerPixel = bitsPerPixel; + this.compression = compression; + this.bitmapDataSize = bitmapDataSize; + this.hResolution = hResolution; + this.vResolution = vResolution; + this.colorsUsed = colorsUsed; + this.colorsImportant = colorsImportant; + + this.redMask = redMask; + this.greenMask = greenMask; + this.blueMask = blueMask; + this.alphaMask = alphaMask; + this.colorSpaceType = colorSpaceType; + this.colorSpace = colorSpace; + this.gammaRed = gammaRed; + this.gammaGreen = gammaGreen; + this.gammaBlue = gammaBlue; + this.intent = intent; + this.profileData = profileData; + this.profileSize = profileSize; + this.reservedV5 = reservedV5; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/BmpImageParser.java b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpImageParser.java new file mode 100644 index 0000000..a28c5a8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpImageParser.java @@ -0,0 +1,820 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.palette.SimplePalette; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class BmpImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".bmp"; + private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; + private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, }; + private static final int BI_RGB = 0; + private static final int BI_RLE4 = 2; + private static final int BI_RLE8 = 1; + private static final int BI_BITFIELDS = 3; + private static final int BITMAP_FILE_HEADER_SIZE = 14; + private static final int BITMAP_INFO_HEADER_SIZE = 40; + + public BmpImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public String getName() { + return "Bmp-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.BMP, // + }; + } + + private BmpHeaderInfo readBmpHeaderInfo(final InputStream is, + final FormatCompliance formatCompliance, final boolean verbose) + throws ImageReadException, IOException { + final byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File"); + final byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File"); + + if (formatCompliance != null) { + formatCompliance.compareBytes("Signature", BMP_HEADER_SIGNATURE, + new byte[]{identifier1, identifier2,}); + } + + final int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File", getByteOrder()); + final int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder()); + final int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, "Not a Valid BMP File", getByteOrder()); + + final int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, "Not a Valid BMP File", getByteOrder()); + int width = 0; + int height = 0; + int planes = 0; + int bitsPerPixel = 0; + int compression = 0; + int bitmapDataSize = 0; + int hResolution = 0; + int vResolution = 0; + int colorsUsed = 0; + int colorsImportant = 0; + int redMask = 0; + int greenMask = 0; + int blueMask = 0; + int alphaMask = 0; + int colorSpaceType = 0; + final BmpHeaderInfo.ColorSpace colorSpace = new BmpHeaderInfo.ColorSpace(); + colorSpace.red = new BmpHeaderInfo.ColorSpaceCoordinate(); + colorSpace.green = new BmpHeaderInfo.ColorSpaceCoordinate(); + colorSpace.blue = new BmpHeaderInfo.ColorSpaceCoordinate(); + int gammaRed = 0; + int gammaGreen = 0; + int gammaBlue = 0; + int intent = 0; + int profileData = 0; + int profileSize = 0; + int reservedV5 = 0; + + if (bitmapHeaderSize >= 40) { + // BITMAPINFOHEADER + width = read4Bytes("Width", is, "Not a Valid BMP File", getByteOrder()); + height = read4Bytes("Height", is, "Not a Valid BMP File", getByteOrder()); + planes = read2Bytes("Planes", is, "Not a Valid BMP File", getByteOrder()); + bitsPerPixel = read2Bytes("Bits Per Pixel", is, "Not a Valid BMP File", getByteOrder()); + compression = read4Bytes("Compression", is, "Not a Valid BMP File", getByteOrder()); + bitmapDataSize = read4Bytes("Bitmap Data Size", is, "Not a Valid BMP File", getByteOrder()); + hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File", getByteOrder()); + vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File", getByteOrder()); + colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File", getByteOrder()); + colorsImportant = read4Bytes("ColorsImportant", is, "Not a Valid BMP File", getByteOrder()); + if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) { + // 52 = BITMAPV2INFOHEADER, now undocumented + // see http://en.wikipedia.org/wiki/BMP_file_format + redMask = read4Bytes("RedMask", is, "Not a Valid BMP File", getByteOrder()); + greenMask = read4Bytes("GreenMask", is, "Not a Valid BMP File", getByteOrder()); + blueMask = read4Bytes("BlueMask", is, "Not a Valid BMP File", getByteOrder()); + } + if (bitmapHeaderSize >= 56) { + // 56 = the now undocumented BITMAPV3HEADER sometimes used by + // Photoshop + // see http://forums.adobe.com/thread/751592?tstart=1 + alphaMask = read4Bytes("AlphaMask", is, "Not a Valid BMP File", getByteOrder()); + } + if (bitmapHeaderSize >= 108) { + // BITMAPV4HEADER + colorSpaceType = read4Bytes("ColorSpaceType", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.red.x = read4Bytes("ColorSpaceRedX", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.red.y = read4Bytes("ColorSpaceRedY", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.red.z = read4Bytes("ColorSpaceRedZ", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.green.x = read4Bytes("ColorSpaceGreenX", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.green.y = read4Bytes("ColorSpaceGreenY", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.green.z = read4Bytes("ColorSpaceGreenZ", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.blue.x = read4Bytes("ColorSpaceBlueX", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.blue.y = read4Bytes("ColorSpaceBlueY", is, "Not a Valid BMP File", getByteOrder()); + colorSpace.blue.z = read4Bytes("ColorSpaceBlueZ", is, "Not a Valid BMP File", getByteOrder()); + gammaRed = read4Bytes("GammaRed", is, "Not a Valid BMP File", getByteOrder()); + gammaGreen = read4Bytes("GammaGreen", is, "Not a Valid BMP File", getByteOrder()); + gammaBlue = read4Bytes("GammaBlue", is, "Not a Valid BMP File", getByteOrder()); + } + if (bitmapHeaderSize >= 124) { + // BITMAPV5HEADER + intent = read4Bytes("Intent", is, "Not a Valid BMP File", getByteOrder()); + profileData = read4Bytes("ProfileData", is, "Not a Valid BMP File", getByteOrder()); + profileSize = read4Bytes("ProfileSize", is, "Not a Valid BMP File", getByteOrder()); + reservedV5 = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder()); + } + } else { + throw new ImageReadException("Invalid/unsupported BMP file"); + } + + if (verbose) { + debugNumber("identifier1", identifier1, 1); + debugNumber("identifier2", identifier2, 1); + debugNumber("fileSize", fileSize, 4); + debugNumber("reserved", reserved, 4); + debugNumber("bitmapDataOffset", bitmapDataOffset, 4); + debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4); + debugNumber("width", width, 4); + debugNumber("height", height, 4); + debugNumber("planes", planes, 2); + debugNumber("bitsPerPixel", bitsPerPixel, 2); + debugNumber("compression", compression, 4); + debugNumber("bitmapDataSize", bitmapDataSize, 4); + debugNumber("hResolution", hResolution, 4); + debugNumber("vResolution", vResolution, 4); + debugNumber("colorsUsed", colorsUsed, 4); + debugNumber("colorsImportant", colorsImportant, 4); + if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) { + debugNumber("redMask", redMask, 4); + debugNumber("greenMask", greenMask, 4); + debugNumber("blueMask", blueMask, 4); + } + if (bitmapHeaderSize >= 56) { + debugNumber("alphaMask", alphaMask, 4); + } + if (bitmapHeaderSize >= 108) { + debugNumber("colorSpaceType", colorSpaceType, 4); + debugNumber("colorSpace.red.x", colorSpace.red.x, 1); + debugNumber("colorSpace.red.y", colorSpace.red.y, 1); + debugNumber("colorSpace.red.z", colorSpace.red.z, 1); + debugNumber("colorSpace.green.x", colorSpace.green.x, 1); + debugNumber("colorSpace.green.y", colorSpace.green.y, 1); + debugNumber("colorSpace.green.z", colorSpace.green.z, 1); + debugNumber("colorSpace.blue.x", colorSpace.blue.x, 1); + debugNumber("colorSpace.blue.y", colorSpace.blue.y, 1); + debugNumber("colorSpace.blue.z", colorSpace.blue.z, 1); + debugNumber("gammaRed", gammaRed, 4); + debugNumber("gammaGreen", gammaGreen, 4); + debugNumber("gammaBlue", gammaBlue, 4); + } + if (bitmapHeaderSize >= 124) { + debugNumber("intent", intent, 4); + debugNumber("profileData", profileData, 4); + debugNumber("profileSize", profileSize, 4); + debugNumber("reservedV5", reservedV5, 4); + } + } + + return new BmpHeaderInfo(identifier1, identifier2, + fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width, + height, planes, bitsPerPixel, compression, bitmapDataSize, + hResolution, vResolution, colorsUsed, colorsImportant, redMask, + greenMask, blueMask, alphaMask, colorSpaceType, colorSpace, + gammaRed, gammaGreen, gammaBlue, intent, profileData, + profileSize, reservedV5); + } + + private byte[] getRLEBytes(final InputStream is, final int rleSamplesPerByte) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // this.setDebug(true); + + boolean done = false; + while (!done) { + final int a = 0xff & readByte("RLE a", is, "BMP: Bad RLE"); + baos.write(a); + final int b = 0xff & readByte("RLE b", is, "BMP: Bad RLE"); + baos.write(b); + + if (a == 0) { + switch (b) { + case 0: // EOL + break; + case 1: // EOF + // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + // ); + done = true; + break; + case 2: { + // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + // ); + final int c = 0xff & readByte("RLE c", is, "BMP: Bad RLE"); + baos.write(c); + final int d = 0xff & readByte("RLE d", is, "BMP: Bad RLE"); + baos.write(d); + + } + break; + default: { + int size = b / rleSamplesPerByte; + if ((b % rleSamplesPerByte) > 0) { + size++; + } + if ((size % 2) != 0) { + size++; + } + + // System.out.println("b: " + b); + // System.out.println("size: " + size); + // System.out.println("RLESamplesPerByte: " + + // RLESamplesPerByte); + // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + // ); + final byte[] bytes = readBytes("bytes", is, size, + "RLE: Absolute Mode"); + baos.write(bytes); + } + break; + } + } + } + + return baos.toByteArray(); + } + + private ImageContents readImageContents(final InputStream is, + final FormatCompliance formatCompliance, final boolean verbose) + throws ImageReadException, IOException { + final BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance, verbose); + + int colorTableSize = bhi.colorsUsed; + if (colorTableSize == 0) { + colorTableSize = (1 << bhi.bitsPerPixel); + } + + if (verbose) { + debugNumber("ColorsUsed", bhi.colorsUsed, 4); + debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4); + debugNumber("ColorTableSize", colorTableSize, 4); + debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4); + debugNumber("Compression", bhi.compression, 4); + } + + // A palette is always valid, even for images that don't need it + // (like 32 bpp), it specifies the "optimal color palette" for + // when the image is displayed on a <= 256 color graphics card. + int paletteLength; + int rleSamplesPerByte = 0; + boolean rle = false; + + switch (bhi.compression) { + case BI_RGB: + if (verbose) { + System.out.println("Compression: BI_RGB"); + } + if (bhi.bitsPerPixel <= 8) { + paletteLength = 4 * colorTableSize; + } else { + paletteLength = 0; + } + // BytesPerPaletteEntry = 0; + // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel); + // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel + // <= 16)); + break; + + case BI_RLE4: + if (verbose) { + System.out.println("Compression: BI_RLE4"); + } + paletteLength = 4 * colorTableSize; + rleSamplesPerByte = 2; + // ExtraBitsPerPixel = 4; + rle = true; + // // BytesPerPixel = 2; + // // BytesPerPaletteEntry = 0; + break; + // + case BI_RLE8: + if (verbose) { + System.out.println("Compression: BI_RLE8"); + } + paletteLength = 4 * colorTableSize; + rleSamplesPerByte = 1; + // ExtraBitsPerPixel = 8; + rle = true; + // BytesPerPixel = 2; + // BytesPerPaletteEntry = 0; + break; + // + case BI_BITFIELDS: + if (verbose) { + System.out.println("Compression: BI_BITFIELDS"); + } + if (bhi.bitsPerPixel <= 8) { + paletteLength = 4 * colorTableSize; + } else { + paletteLength = 0; + } + // BytesPerPixel = 2; + // BytesPerPaletteEntry = 4; + break; + + default: + throw new ImageReadException("BMP: Unknown Compression: " + + bhi.compression); + } + + byte[] colorTable = null; + if (paletteLength > 0) { + colorTable = readBytes("ColorTable", is, paletteLength, + "Not a Valid BMP File"); + } + + if (verbose) { + debugNumber("paletteLength", paletteLength, 4); + System.out.println("ColorTable: " + + ((colorTable == null) ? "null" : Integer.toString(colorTable.length))); + } + + final int pixelCount = bhi.width * bhi.height; + + int imageLineLength = (((bhi.bitsPerPixel) * bhi.width) + 7) / 8; + + if (verbose) { + // this.debugNumber("Total BitsPerPixel", + // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4); + // this.debugNumber("Total Bit Per Line", + // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4); + // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4); + debugNumber("bhi.Width", bhi.width, 4); + debugNumber("bhi.Height", bhi.height, 4); + debugNumber("ImageLineLength", imageLineLength, 4); + // this.debugNumber("imageDataSize", imageDataSize, 4); + debugNumber("PixelCount", pixelCount, 4); + } + // int ImageLineLength = BytesPerPixel * bhi.Width; + while ((imageLineLength % 4) != 0) { + imageLineLength++; + } + + final int headerSize = BITMAP_FILE_HEADER_SIZE + + bhi.bitmapHeaderSize + + (bhi.bitmapHeaderSize == 40 + && bhi.compression == BI_BITFIELDS ? 3 * 4 : 0); + final int expectedDataOffset = headerSize + paletteLength; + + if (verbose) { + debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4); + debugNumber("expectedDataOffset", expectedDataOffset, 4); + } + final int extraBytes = bhi.bitmapDataOffset - expectedDataOffset; + if (extraBytes < 0) { + throw new ImageReadException("BMP has invalid image data offset: " + + bhi.bitmapDataOffset + " (expected: " + + expectedDataOffset + ", paletteLength: " + paletteLength + + ", headerSize: " + headerSize + ")"); + } else if (extraBytes > 0) { + readBytes("BitmapDataOffset", is, extraBytes, "Not a Valid BMP File"); + } + + final int imageDataSize = bhi.height * imageLineLength; + + if (verbose) { + debugNumber("imageDataSize", imageDataSize, 4); + } + + byte[] imageData; + if (rle) { + imageData = getRLEBytes(is, rleSamplesPerByte); + } else { + imageData = readBytes("ImageData", is, imageDataSize, + "Not a Valid BMP File"); + } + + if (verbose) { + debugNumber("ImageData.length", imageData.length, 4); + } + + PixelParser pixelParser; + + switch (bhi.compression) { + case BI_RLE4: + case BI_RLE8: + pixelParser = new PixelParserRle(bhi, colorTable, imageData); + break; + case BI_RGB: + pixelParser = new PixelParserRgb(bhi, colorTable, imageData); + break; + case BI_BITFIELDS: + pixelParser = new PixelParserBitFields(bhi, colorTable, imageData); + break; + default: + throw new ImageReadException("BMP: Unknown Compression: " + + bhi.compression); + } + + return new ImageContents(bhi, colorTable, imageData, pixelParser); + } + + private BmpHeaderInfo readBmpHeaderInfo(final ByteSource byteSource, + final boolean verbose) throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + // readSignature(is); + final BmpHeaderInfo ret = readBmpHeaderInfo(is, null, verbose); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + final boolean verbose = Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE)); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageReadException("Unknown parameter: " + firstKey); + } + + final BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource, verbose); + + if (bhi == null) { + throw new ImageReadException("BMP: couldn't read header"); + } + + return new Dimension(bhi.width, bhi.height); + + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private String getBmpTypeDescription(final int identifier1, final int identifier2) { + if ((identifier1 == 'B') && (identifier2 == 'M')) { + return "Windows 3.1x, 95, NT,"; + } + if ((identifier1 == 'B') && (identifier2 == 'A')) { + return "OS/2 Bitmap Array"; + } + if ((identifier1 == 'C') && (identifier2 == 'I')) { + return "OS/2 Color Icon"; + } + if ((identifier1 == 'C') && (identifier2 == 'P')) { + return "OS/2 Color Pointer"; + } + if ((identifier1 == 'I') && (identifier2 == 'C')) { + return "OS/2 Icon"; + } + if ((identifier1 == 'P') && (identifier2 == 'T')) { + return "OS/2 Pointer"; + } + + return "Unknown"; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + final boolean verbose = Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE)); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageReadException("Unknown parameter: " + firstKey); + } + + InputStream is = null; + ImageContents ic = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + ic = readImageContents(is, FormatCompliance.getDefault(), verbose); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + + if (ic == null) { + throw new ImageReadException("Couldn't read BMP Data"); + } + + final BmpHeaderInfo bhi = ic.bhi; + final byte[] colorTable = ic.colorTable; + + if (bhi == null) { + throw new ImageReadException("BMP: couldn't read header"); + } + + final int height = bhi.height; + final int width = bhi.width; + + final List comments = new ArrayList(); + // TODO: comments... + + final int bitsPerPixel = bhi.bitsPerPixel; + final ImageFormat format = ImageFormats.BMP; + final String name = "BMP Windows Bitmap"; + final String mimeType = "image/x-ms-bmp"; + // we ought to count images, but don't yet. + final int numberOfImages = -1; + // not accurate ... only reflects first + final boolean progressive = false; + // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0); + // + // pixels per meter + final int physicalWidthDpi = (int) (bhi.hResolution * .0254); + final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); + // int physicalHeightDpi = 72; + final int physicalHeightDpi = (int) (bhi.vResolution * .0254); + final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); + + final String formatDetails = "Bmp (" + (char) bhi.identifier1 + + (char) bhi.identifier2 + ": " + + getBmpTypeDescription(bhi.identifier1, bhi.identifier2) + ")"; + + final boolean transparent = false; + + final boolean usesPalette = colorTable != null; + final int colorType = ImageInfo.COLOR_TYPE_RGB; + final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_RLE; + + return new ImageInfo(formatDetails, bitsPerPixel, comments, + format, name, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + pw.println("bmp.dumpImageFile"); + + final ImageInfo imageData = getImageInfo(byteSource, null); + + imageData.toString(pw, ""); + + pw.println(""); + + return true; + } + + @Override + public FormatCompliance getFormatCompliance(final ByteSource byteSource) + throws ImageReadException, IOException { + final boolean verbose = false; + + final FormatCompliance result = new FormatCompliance( + byteSource.getDescription()); + + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + readImageContents(is, result, verbose); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + + return result; + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final BufferedImage ret = getBufferedImage(is, params); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + public BufferedImage getBufferedImage(final InputStream inputStream, Map params) + throws ImageReadException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + final boolean verbose = Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE)); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + if (params.containsKey(BUFFERED_IMAGE_FACTORY)) { + params.remove(BUFFERED_IMAGE_FACTORY); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageReadException("Unknown parameter: " + firstKey); + } + + final ImageContents ic = readImageContents(inputStream, + FormatCompliance.getDefault(), verbose); + if (ic == null) { + throw new ImageReadException("Couldn't read BMP Data"); + } + + final BmpHeaderInfo bhi = ic.bhi; + // byte colorTable[] = ic.colorTable; + // byte imageData[] = ic.imageData; + + final int width = bhi.width; + final int height = bhi.height; + + if (verbose) { + System.out.println("width: " + width); + System.out.println("height: " + height); + System.out.println("width*height: " + width * height); + System.out.println("width*height*4: " + width * height * 4); + } + + final PixelParser pixelParser = ic.pixelParser; + final ImageBuilder imageBuilder = new ImageBuilder(width, height, true); + pixelParser.processImage(imageBuilder); + + return imageBuilder.getBufferedImage(); + + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + PixelDensity pixelDensity = null; + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + if (params.containsKey(PARAM_KEY_PIXEL_DENSITY)) { + pixelDensity = (PixelDensity) params + .remove(PARAM_KEY_PIXEL_DENSITY); + } + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final SimplePalette palette = new PaletteFactory().makeExactRgbPaletteSimple( + src, 256); + + BmpWriter writer; + if (palette == null) { + writer = new BmpWriterRgb(); + } else { + writer = new BmpWriterPalette(palette); + } + + final byte[] imagedata = writer.getImageData(src); + final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); + + // write BitmapFileHeader + os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap + os.write(0x4d); // M + + final int filesize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header + // size + 4 * writer.getPaletteSize() + // palette size in bytes + imagedata.length; + bos.write4Bytes(filesize); + + bos.write4Bytes(0); // reserved + bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + + 4 * writer.getPaletteSize()); // Bitmap Data Offset + + final int width = src.getWidth(); + final int height = src.getHeight(); + + // write BitmapInfoHeader + bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size + bos.write4Bytes(width); // width + bos.write4Bytes(height); // height + bos.write2Bytes(1); // Number of Planes + bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel + + bos.write4Bytes(BI_RGB); // Compression + bos.write4Bytes(imagedata.length); // Bitmap Data Size + bos.write4Bytes(pixelDensity != null ? (int) Math + .round(pixelDensity.horizontalDensityMetres()) : 0); // HResolution + bos.write4Bytes(pixelDensity != null ? (int) Math + .round(pixelDensity.verticalDensityMetres()) : 0); // VResolution + if (palette == null) { + bos.write4Bytes(0); // Colors + } else { + bos.write4Bytes(palette.length()); // Colors + } + bos.write4Bytes(0); // Important Colors + // bos.write_4_bytes(0); // Compression + + // write Palette + writer.writePalette(bos); + // write Image Data + bos.write(imagedata); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriter.java b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriter.java similarity index 79% rename from src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriter.java rename to src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriter.java index 76c41ec..81401ed 100644 --- a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriter.java +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriter.java @@ -1,36 +1,33 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.writers; - -import java.io.IOException; - -import org.apache.sanselan.common.BinaryOutputStream; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class BMPWriter -{ - public abstract int getPaletteSize(); - - public abstract int getBitsPerPixel(); - - public abstract void writePalette(BinaryOutputStream bos) - throws IOException; - - public abstract byte[] getImageData(BufferedImage src); -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; + +import org.apache.commons.imaging.common.BinaryOutputStream; + +abstract class BmpWriter { + + public abstract int getPaletteSize(); + + public abstract int getBitsPerPixel(); + + public abstract void writePalette(BinaryOutputStream bos) throws IOException; + + public abstract byte[] getImageData(BufferedImage src); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterPalette.java b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterPalette.java new file mode 100644 index 0000000..434cc34 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterPalette.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.palette.SimplePalette; + +class BmpWriterPalette extends BmpWriter { + + private final SimplePalette palette; + private final int bitsPerSample; + + public BmpWriterPalette(final SimplePalette palette) { + this.palette = palette; + + if (palette.length() <= 2) { + bitsPerSample = 1; + } else if (palette.length() <= 16) { + bitsPerSample = 4; + } else { + bitsPerSample = 8; + } + } + + @Override + public int getPaletteSize() { + return palette.length(); + } + + @Override + public int getBitsPerPixel() { + return bitsPerSample; + } + + @Override + public void writePalette(final BinaryOutputStream bos) throws IOException { + for (int i = 0; i < palette.length(); i++) { + final int rgb = palette.getEntry(i); + + final int red = 0xff & (rgb >> 16); + final int green = 0xff & (rgb >> 8); + final int blue = 0xff & (rgb >> 0); + + bos.write(blue); + bos.write(green); + bos.write(red); + bos.write(0); + } + } + + @Override + public byte[] getImageData(final BufferedImage src) { + final int width = src.getWidth(); + final int height = src.getHeight(); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int bitCache = 0; + int bitsInCache = 0; + + int bytecount = 0; + for (int y = height - 1; y >= 0; y--) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int rgb = 0xffffff & argb; + + final int index = palette.getPaletteIndex(rgb); + + if (bitsPerSample == 8) { + baos.write(0xff & index); + bytecount++; + } else { + // 4 or 1 + bitCache = (bitCache << bitsPerSample) | index; + bitsInCache += bitsPerSample; + if (bitsInCache >= 8) { + baos.write(0xff & bitCache); + bytecount++; + bitCache = 0; + bitsInCache = 0; + } + } + } + + if (bitsInCache > 0) { + bitCache = (bitCache << (8 - bitsInCache)); + + baos.write(0xff & bitCache); + bytecount++; + bitCache = 0; + bitsInCache = 0; + } + + while ((bytecount % 4) != 0) { + baos.write(0); + bytecount++; + } + } + + return baos.toByteArray(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterRgb.java b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterRgb.java new file mode 100644 index 0000000..7652154 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/BmpWriterRgb.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.apache.commons.imaging.common.BinaryOutputStream; + +class BmpWriterRgb extends BmpWriter { + // private final boolean alpha; + // + // public BmpWriterRgb(boolean alpha) + // { + // this.alpha = alpha; + // } + + @Override + public int getPaletteSize() { + return 0; + } + + @Override + public int getBitsPerPixel() { + // return alpha ? 32 : 24; + return 24; + } + + @Override + public void writePalette(final BinaryOutputStream bos) throws IOException { + // no palette + } + + @Override + public byte[] getImageData(final BufferedImage src) { + final int width = src.getWidth(); + final int height = src.getHeight(); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // BinaryOutputStream bos = new BinaryOutputStream(baos, + // BYTE_ORDER_Network); + + int bytecount = 0; + for (int y = height - 1; y >= 0; y--) { + // for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int rgb = 0xffffff & argb; + + final int red = 0xff & (rgb >> 16); + final int green = 0xff & (rgb >> 8); + final int blue = 0xff & (rgb >> 0); + + baos.write(blue); + baos.write(green); + baos.write(red); + bytecount += 3; + } + while ((bytecount % 4) != 0) { + baos.write(0); + bytecount++; + } + } + + return baos.toByteArray(); + } +} diff --git a/src/main/java/org/apache/sanselan/formats/bmp/ImageContents.java b/src/main/java/org/apache/commons/imaging/formats/bmp/ImageContents.java similarity index 65% rename from src/main/java/org/apache/sanselan/formats/bmp/ImageContents.java rename to src/main/java/org/apache/commons/imaging/formats/bmp/ImageContents.java index 126e8f5..6096c29 100644 --- a/src/main/java/org/apache/sanselan/formats/bmp/ImageContents.java +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/ImageContents.java @@ -1,37 +1,33 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp; - -import org.apache.sanselan.formats.bmp.pixelparsers.PixelParser; - -class ImageContents -{ - public final BmpHeaderInfo bhi; - public final byte colorTable[]; - public final byte imageData[]; - public final PixelParser pixelParser; - - public ImageContents(BmpHeaderInfo bhi, byte ColorTable[], - byte ImageData[], PixelParser fPixelParser) - { - this.bhi = bhi; - this.colorTable = ColorTable; - this.imageData = ImageData; - this.pixelParser = fPixelParser; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +class ImageContents { + + public final BmpHeaderInfo bhi; + public final byte[] colorTable; + public final byte[] imageData; + public final PixelParser pixelParser; + + public ImageContents(BmpHeaderInfo bhi, byte[] colorTable, byte[] imageData, PixelParser pixelParser) { + this.bhi = bhi; + this.colorTable = colorTable; + this.imageData = imageData; + this.pixelParser = pixelParser; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParser.java b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParser.java new file mode 100644 index 0000000..d9fb8aa --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParser.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +abstract class PixelParser { + + public final BmpHeaderInfo bhi; + public final byte[] colorTable; + public final byte[] imageData; + + protected final InputStream is; + + public PixelParser(final BmpHeaderInfo bhi, final byte[] colorTable, final byte[] imageData) { + this.bhi = bhi; + this.colorTable = colorTable; + this.imageData = imageData; + + is = new ByteArrayInputStream(imageData); + } + + public abstract void processImage(ImageBuilder imageBuilder) throws ImageReadException, IOException; + + protected int getColorTableRGB(int index) { + index *= 4; + final int blue = 0xff & colorTable[index + 0]; + final int green = 0xff & colorTable[index + 1]; + final int red = 0xff & colorTable[index + 2]; + final int alpha = 0xff; + + return (alpha << 24) + | (red << 16) + | (green << 8) + | (blue << 0); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserBitFields.java b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFields.java similarity index 53% rename from src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserBitFields.java rename to src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFields.java index c09db11..f02abb1 100644 --- a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserBitFields.java +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFields.java @@ -1,129 +1,112 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.pixelparsers; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.bmp.BmpHeaderInfo; - -public class PixelParserBitFields extends PixelParserSimple -{ - - private final int redShift; - private final int greenShift; - private final int blueShift; - - private final int redMask; - private final int greenMask; - private final int blueMask; - - public PixelParserBitFields(BmpHeaderInfo bhi, byte ColorTable[], - byte ImageData[]) throws ImageReadException, IOException - { - super(bhi, ColorTable, ImageData); - - InputStream bais = new ByteArrayInputStream(ColorTable); - - redMask = bfp.read4Bytes("redMask", bais, - "BMP BI_BITFIELDS Bad Color Table"); - greenMask = bfp.read4Bytes("greenMask", bais, - "BMP BI_BITFIELDS Bad Color Table"); - blueMask = bfp.read4Bytes("blueMask", bais, - "BMP BI_BITFIELDS Bad Color Table"); - - redShift = getMaskShift(redMask); - greenShift = getMaskShift(greenMask); - blueShift = getMaskShift(blueMask); - } - - private int getMaskShift(int mask) - { - int trailingZeroes = 0; - - while ((0x1 & mask) == 0) - { - mask = 0x7fffffff & (mask >> 1); - trailingZeroes++; - } - - int maskLength = 0; - - while ((0x1 & mask) == 1) - { - mask = 0x7fffffff & (mask >> 1); - maskLength++; - } - - return (trailingZeroes - (8 - maskLength)); - } - private int bytecount = 0; - - public int getNextRGB() throws ImageReadException, IOException - { - int data; - - if (bhi.bitsPerPixel == 8) - { - data = 0xff & imageData[bytecount + 0]; - bytecount += 1; - } - else if (bhi.bitsPerPixel == 24) - { - data = bfp.read3Bytes("Pixel", is, "BMP Image Data"); - bytecount += 3; - } - else if (bhi.bitsPerPixel == 32) - { - data = bfp.read4Bytes("Pixel", is, "BMP Image Data"); - bytecount += 4; - } - else if (bhi.bitsPerPixel == 16) - { - data = bfp.read2Bytes("Pixel", is, "BMP Image Data"); - bytecount += 2; - } - else - throw new ImageReadException("Unknown BitsPerPixel: " - + bhi.bitsPerPixel); - - int red = (redMask & data); - int green = (greenMask & data); - int blue = (blueMask & data); - - red = (redShift >= 0) ? red >> redShift : red << -redShift; - green = (greenShift >= 0) ? green >> greenShift : green << -greenShift; - blue = (blueShift >= 0) ? blue >> blueShift : blue << -blueShift; - - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - return rgb; - } - - public void newline() throws ImageReadException, IOException - { - while (((bytecount) % 4) != 0) - { - bfp.readByte("Pixel", is, "BMP Image Data"); - bytecount++; - } - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +class PixelParserBitFields extends PixelParserSimple { + + private final int redShift; + private final int greenShift; + private final int blueShift; + private final int alphaShift; + + private final int redMask; + private final int greenMask; + private final int blueMask; + private final int alphaMask; + + private int bytecount; + + public PixelParserBitFields(final BmpHeaderInfo bhi, final byte[] colorTable, final byte[] imageData) { + super(bhi, colorTable, imageData); + + redMask = bhi.redMask; + greenMask = bhi.greenMask; + blueMask = bhi.blueMask; + alphaMask = bhi.alphaMask; + + redShift = getMaskShift(redMask); + greenShift = getMaskShift(greenMask); + blueShift = getMaskShift(blueMask); + alphaShift = (alphaMask != 0 ? getMaskShift(alphaMask) : 0); + } + + private int getMaskShift(int mask) { + int trailingZeroes = 0; + + while ((0x1 & mask) == 0) { + mask = 0x7fffffff & (mask >> 1); + trailingZeroes++; + } + + int maskLength = 0; + + while ((0x1 & mask) == 1) { + mask = 0x7fffffff & (mask >> 1); + maskLength++; + } + + return trailingZeroes - (8 - maskLength); + } + + @Override + public int getNextRGB() throws ImageReadException, IOException { + int data; + + if (bhi.bitsPerPixel == 8) { + data = 0xff & imageData[bytecount + 0]; + bytecount += 1; + } else if (bhi.bitsPerPixel == 24) { + data = read3Bytes("Pixel", is, "BMP Image Data", ByteOrder.LITTLE_ENDIAN); + bytecount += 3; + } else if (bhi.bitsPerPixel == 32) { + data = read4Bytes("Pixel", is, "BMP Image Data", ByteOrder.LITTLE_ENDIAN); + bytecount += 4; + } else if (bhi.bitsPerPixel == 16) { + data = read2Bytes("Pixel", is, "BMP Image Data", ByteOrder.LITTLE_ENDIAN); + bytecount += 2; + } else { + throw new ImageReadException("Unknown BitsPerPixel: " + bhi.bitsPerPixel); + } + + int red = (redMask & data); + int green = (greenMask & data); + int blue = (blueMask & data); + int alpha = (alphaMask != 0 ? alphaMask & data : 0xff); + + red = (redShift >= 0) ? red >> redShift : red << -redShift; + green = (greenShift >= 0) ? green >> greenShift : green << -greenShift; + blue = (blueShift >= 0) ? blue >> blueShift : blue << -blueShift; + alpha = (alphaShift >= 0) ? alpha >> alphaShift : alpha << -alphaShift; + + return (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + } + + @Override + public void newline() throws ImageReadException, IOException { + while ((bytecount % 4) != 0) { + readByte("Pixel", is, "BMP Image Data"); + bytecount++; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRgb.java b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRgb.java new file mode 100644 index 0000000..4c53193 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRgb.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +class PixelParserRgb extends PixelParserSimple { + private int bytecount; + private int cachedBitCount; + private int cachedByte; + private int pixelCount; + + public PixelParserRgb(final BmpHeaderInfo bhi, final byte[] colorTable, final byte[] imageData) { + super(bhi, colorTable, imageData); + + } + + @Override + public int getNextRGB() throws ImageReadException, IOException { + pixelCount++; + + if ((bhi.bitsPerPixel == 1) + || (bhi.bitsPerPixel == 4)) { // always grayscale? + if (cachedBitCount < bhi.bitsPerPixel) { + if (cachedBitCount != 0) { + throw new ImageReadException("Unexpected leftover bits: " + + cachedBitCount + "/" + bhi.bitsPerPixel); + } + + cachedBitCount += 8; + cachedByte = (0xff & imageData[bytecount]); + bytecount++; + } + final int cacheMask = (1 << bhi.bitsPerPixel) - 1; + final int sample = cacheMask & (cachedByte >> (8 - bhi.bitsPerPixel)); + cachedByte = 0xff & (cachedByte << bhi.bitsPerPixel); + cachedBitCount -= bhi.bitsPerPixel; + + return getColorTableRGB(sample); + } else if (bhi.bitsPerPixel == 8) { // always grayscale? + final int sample = 0xff & imageData[bytecount + 0]; + + final int rgb = getColorTableRGB(sample); + + bytecount += 1; + + return rgb; + } else if (bhi.bitsPerPixel == 16) { + final int data = read2Bytes("Pixel", is, "BMP Image Data", ByteOrder.LITTLE_ENDIAN); + + final int blue = (0x1f & (data >> 0)) << 3; + final int green = (0x1f & (data >> 5)) << 3; + final int red = (0x1f & (data >> 10)) << 3; + final int alpha = 0xff; + + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + bytecount += 2; + + return rgb; + } else if (bhi.bitsPerPixel == 24) { + final int blue = 0xff & imageData[bytecount + 0]; + final int green = 0xff & imageData[bytecount + 1]; + final int red = 0xff & imageData[bytecount + 2]; + final int alpha = 0xff; + + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + bytecount += 3; + + return rgb; + } else if (bhi.bitsPerPixel == 32) { + final int blue = 0xff & imageData[bytecount + 0]; + final int green = 0xff & imageData[bytecount + 1]; + final int red = 0xff & imageData[bytecount + 2]; + final int alpha = 0xff; + + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + bytecount += 4; + + return rgb; + } + + throw new ImageReadException("Unknown BitsPerPixel: " + + bhi.bitsPerPixel); + } + + @Override + public void newline() throws ImageReadException, IOException { + cachedBitCount = 0; + + while (((bytecount) % 4) != 0) { + readByte("Pixel", is, "BMP Image Data"); + bytecount++; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRle.java b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRle.java new file mode 100644 index 0000000..1106520 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserRle.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.common.ImageBuilder; + +class PixelParserRle extends PixelParser { + + public PixelParserRle(final BmpHeaderInfo bhi, final byte[] colorTable, final byte[] imageData) { + super(bhi, colorTable, imageData); + + } + + private int getSamplesPerByte() throws ImageReadException { + if (bhi.bitsPerPixel == 8) { + return 1; + } else if (bhi.bitsPerPixel == 4) { + return 2; + } else { + throw new ImageReadException("BMP RLE: bad BitsPerPixel: " + + bhi.bitsPerPixel); + } + } + + private int[] convertDataToSamples(final int data) throws ImageReadException { + int[] rgbs; + if (bhi.bitsPerPixel == 8) { + rgbs = new int[1]; + rgbs[0] = getColorTableRGB(data); + // pixels_written = 1; + } else if (bhi.bitsPerPixel == 4) { + rgbs = new int[2]; + final int sample1 = data >> 4; + final int sample2 = 0x0f & data; + rgbs[0] = getColorTableRGB(sample1); + rgbs[1] = getColorTableRGB(sample2); + // pixels_written = 2; + } else { + throw new ImageReadException("BMP RLE: bad BitsPerPixel: " + + bhi.bitsPerPixel); + } + + return rgbs; + } + + private int processByteOfData(final int[] rgbs, final int repeat, int x, final int y, + final int width, final int height, final ImageBuilder imageBuilder) { + // int rbg + int pixelsWritten = 0; + for (int i = 0; i < repeat; i++) { + + if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) { + // int rgb = 0xff000000; + // rgb = getNextRGB(); + final int rgb = rgbs[i % rgbs.length]; + // bi.setRGB(x, y, rgb); + imageBuilder.setRGB(x, y, rgb); + // bi.setRGB(x, y, 0xff00ff00); + } else { + System.out.println("skipping bad pixel (" + x + "," + y + ")"); + } + + x++; + pixelsWritten++; + } + + return pixelsWritten; + } + + @Override + public void processImage(final ImageBuilder imageBuilder) + throws ImageReadException, IOException { + final int width = bhi.width; + final int height = bhi.height; + int x = 0; + int y = height - 1; + + boolean done = false; + while (!done) { + final int a = 0xff & BinaryFunctions.readByte("RLE (" + x + "," + y + ") a", is, "BMP: Bad RLE"); + final int b = 0xff & BinaryFunctions.readByte("RLE (" + x + "," + y + ") b", is, "BMP: Bad RLE"); + + if (a == 0) { + switch (b) { + case 0: { + // EOL + y--; + x = 0; + break; + } + case 1: + // EOF + done = true; + break; + case 2: { + final int deltaX = 0xff & BinaryFunctions.readByte("RLE deltaX", is, "BMP: Bad RLE"); + final int deltaY = 0xff & BinaryFunctions.readByte("RLE deltaY", is, "BMP: Bad RLE"); + x += deltaX; + y -= deltaY; + break; + } + default: { + final int samplesPerByte = getSamplesPerByte(); + int size = b / samplesPerByte; + if ((b % samplesPerByte) > 0) { + size++; + } + if ((size % 2) != 0) { + size++; + } + + // System.out.println("b: " + b); + // System.out.println("size: " + size); + // System.out.println("SamplesPerByte: " + SamplesPerByte); + + final byte[] bytes = BinaryFunctions.readBytes("bytes", is, size, "RLE: Absolute Mode"); + + int remaining = b; + + for (int i = 0; remaining > 0; i++) { + // for (int i = 0; i < bytes.length; i++) + final int[] samples = convertDataToSamples(0xff & bytes[i]); + final int towrite = Math.min(remaining, samplesPerByte); + // System.out.println("remaining: " + remaining); + // System.out.println("SamplesPerByte: " + // + SamplesPerByte); + // System.out.println("towrite: " + towrite); + final int written = processByteOfData(samples, towrite, x, y, + width, height, imageBuilder); + // System.out.println("written: " + written); + // System.out.println(""); + x += written; + remaining -= written; + } + break; + } + } + } else { + final int[] rgbs = convertDataToSamples(b); + + x += processByteOfData(rgbs, a, x, y, width, height, + imageBuilder); + } + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserSimple.java b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserSimple.java similarity index 53% rename from src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserSimple.java rename to src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserSimple.java index 45b91b0..b2171d4 100644 --- a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserSimple.java +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/PixelParserSimple.java @@ -1,56 +1,45 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.pixelparsers; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.bmp.BmpHeaderInfo; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class PixelParserSimple extends PixelParser -{ - public PixelParserSimple(BmpHeaderInfo bhi, byte ColorTable[], - byte ImageData[]) - { - super(bhi, ColorTable, ImageData); - } - - public abstract int getNextRGB() throws ImageReadException, IOException; - - public abstract void newline() throws ImageReadException, IOException; - - public void processImage(BufferedImage bi) throws ImageReadException, - IOException - { -// DataBuffer db = bi.getRaster().getDataBuffer(); - - for (int y = bhi.height - 1; y >= 0; y--) - { - for (int x = 0; x < bhi.width; x++) - { - int rgb = getNextRGB(); - - bi.setRGB(x, y, rgb); -// db.setElem(y * bhi.width + x, rgb); - } - newline(); - } - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.bmp; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +abstract class PixelParserSimple extends PixelParser { + public PixelParserSimple(final BmpHeaderInfo bhi, final byte[] colorTable, final byte[] imageData) { + super(bhi, colorTable, imageData); + } + + public abstract int getNextRGB() throws ImageReadException, IOException; + + public abstract void newline() throws ImageReadException, IOException; + + @Override + public void processImage(final ImageBuilder imageBuilder) throws ImageReadException, IOException { + for (int y = bhi.height - 1; y >= 0; y--) { + for (int x = 0; x < bhi.width; x++) { + final int rgb = getNextRGB(); + + imageBuilder.setRGB(x, y, rgb); + // db.setElem(y * bhi.width + x, rgb); + } + newline(); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/bmp/package-info.java b/src/main/java/org/apache/commons/imaging/formats/bmp/package-info.java new file mode 100644 index 0000000..138a8a4 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/bmp/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The BMP image format. + */ +package org.apache.commons.imaging.formats.bmp; + diff --git a/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java b/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java new file mode 100644 index 0000000..f600669 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.dcx; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; +import org.apache.commons.imaging.formats.pcx.PcxConstants; +import org.apache.commons.imaging.formats.pcx.PcxImageParser; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class DcxImageParser extends ImageParser { + // See http://www.fileformat.info/format/pcx/egff.htm for documentation + private static final String DEFAULT_EXTENSION = ".dcx"; + private static final String[] ACCEPTED_EXTENSIONS = { ".dcx", }; + + public DcxImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public String getName() { + return "Dcx-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { + ImageFormats.DCX, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private static class DcxHeader { + + public static final int DCX_ID = 0x3ADE68B1; + public final int id; + public final long[] pageTable; + + public DcxHeader(final int id, final long[] pageTable) { + this.id = id; + this.pageTable = pageTable; + } + + public void dump(final PrintWriter pw) { + pw.println("DcxHeader"); + pw.println("Id: 0x" + Integer.toHexString(id)); + pw.println("Pages: " + pageTable.length); + pw.println(); + } + } + + private DcxHeader readDcxHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final int id = read4Bytes("Id", is, "Not a Valid DCX File", getByteOrder()); + final List pageTable = new ArrayList(1024); + for (int i = 0; i < 1024; i++) { + final long pageOffset = 0xFFFFffffL & read4Bytes("PageTable", is, + "Not a Valid DCX File", getByteOrder()); + if (pageOffset == 0) { + break; + } + pageTable.add(pageOffset); + } + + if (id != DcxHeader.DCX_ID) { + throw new ImageReadException( + "Not a Valid DCX File: file id incorrect"); + } + if (pageTable.size() == 1024) { + throw new ImageReadException( + "DCX page table not terminated by zero entry"); + } + + final Object[] objects = pageTable.toArray(); + final long[] pages = new long[objects.length]; + for (int i = 0; i < objects.length; i++) { + pages[i] = ((Long) objects[i]); + } + + final DcxHeader ret = new DcxHeader(id, pages); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + readDcxHeader(byteSource).dump(pw); + return true; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final List list = getAllBufferedImages(byteSource); + if (list.isEmpty()) { + return null; + } + return list.get(0); + } + + @Override + public List getAllBufferedImages(final ByteSource byteSource) + throws ImageReadException, IOException { + final DcxHeader dcxHeader = readDcxHeader(byteSource); + final List images = new ArrayList(); + final PcxImageParser pcxImageParser = new PcxImageParser(); + for (final long element : dcxHeader.pageTable) { + InputStream stream = null; + boolean canThrow = false; + try { + stream = byteSource.getInputStream(element); + final ByteSourceInputStream pcxSource = new ByteSourceInputStream( + stream, null); + final BufferedImage image = pcxImageParser.getBufferedImage( + pcxSource, new HashMap()); + images.add(image); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, stream); + } + } + return images; + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + final HashMap pcxParams = new HashMap(); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (params.containsKey(PcxConstants.PARAM_KEY_PCX_COMPRESSION)) { + final Object value = params + .remove(PcxConstants.PARAM_KEY_PCX_COMPRESSION); + pcxParams.put(PcxConstants.PARAM_KEY_PCX_COMPRESSION, value); + } + + if (params.containsKey(PARAM_KEY_PIXEL_DENSITY)) { + final Object value = params.remove(PARAM_KEY_PIXEL_DENSITY); + if (value != null) { + if (!(value instanceof PixelDensity)) { + throw new ImageWriteException( + "Invalid pixel density parameter"); + } + pcxParams.put(PARAM_KEY_PIXEL_DENSITY, value); + } + } + + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final int headerSize = 4 + 1024 * 4; + + final BinaryOutputStream bos = new BinaryOutputStream(os, + ByteOrder.LITTLE_ENDIAN); + bos.write4Bytes(DcxHeader.DCX_ID); + // Some apps may need a full 1024 entry table + bos.write4Bytes(headerSize); + for (int i = 0; i < 1023; i++) { + bos.write4Bytes(0); + } + final PcxImageParser pcxImageParser = new PcxImageParser(); + pcxImageParser.writeImage(src, bos, pcxParams); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/dcx/package-info.java b/src/main/java/org/apache/commons/imaging/formats/dcx/package-info.java new file mode 100644 index 0000000..9c4706c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/dcx/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The DCX image format. + */ +package org.apache.commons.imaging.formats.dcx; + diff --git a/src/main/java/org/apache/sanselan/formats/gif/GenericGIFBlock.java b/src/main/java/org/apache/commons/imaging/formats/gif/GenericGifBlock.java similarity index 63% rename from src/main/java/org/apache/sanselan/formats/gif/GenericGIFBlock.java rename to src/main/java/org/apache/commons/imaging/formats/gif/GenericGifBlock.java index d85cbde..c4c6ae3 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/GenericGIFBlock.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GenericGifBlock.java @@ -1,55 +1,51 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; - -class GenericGIFBlock extends GIFBlock -{ - public final ArrayList subblocks; - - public GenericGIFBlock(int blockCode, ArrayList subblocks) - { - super(blockCode); - - this.subblocks = subblocks; - - } - - public byte[] appendSubBlocks() throws IOException - { - return appendSubBlocks(false); - } - - public byte[] appendSubBlocks(boolean includeLengths) throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - for (int i = 0; i < subblocks.size(); i++) - { - byte subblock[] = (byte[]) subblocks.get(i); - if(includeLengths && i>0) - out.write(subblock.length); - out.write(subblock); - } - - return out.toByteArray(); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +class GenericGifBlock extends GifBlock { + public final List subblocks; + + public GenericGifBlock(final int blockCode, final List subblocks) { + super(blockCode); + + this.subblocks = subblocks; + + } + + public byte[] appendSubBlocks() throws IOException { + return appendSubBlocks(false); + } + + public byte[] appendSubBlocks(final boolean includeLengths) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + for (int i = 0; i < subblocks.size(); i++) { + final byte[] subblock = subblocks.get(i); + if (includeLengths && i > 0) { + out.write(subblock.length); + } + out.write(subblock); + } + + return out.toByteArray(); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/gif/GIFBlock.java b/src/main/java/org/apache/commons/imaging/formats/gif/GifBlock.java similarity index 87% rename from src/main/java/org/apache/sanselan/formats/gif/GIFBlock.java rename to src/main/java/org/apache/commons/imaging/formats/gif/GifBlock.java index 27c9089..71d8f5e 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/GIFBlock.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GifBlock.java @@ -1,27 +1,25 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -class GIFBlock -{ - public final int blockCode; - - public GIFBlock(int blockCode) - { - this.blockCode = blockCode; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +class GifBlock { + public final int blockCode; + + public GifBlock(final int blockCode) { + this.blockCode = blockCode; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/gif/GIFHeaderInfo.java b/src/main/java/org/apache/commons/imaging/formats/gif/GifHeaderInfo.java similarity index 52% rename from src/main/java/org/apache/sanselan/formats/gif/GIFHeaderInfo.java rename to src/main/java/org/apache/commons/imaging/formats/gif/GifHeaderInfo.java index 3443198..ee4f11f 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/GIFHeaderInfo.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GifHeaderInfo.java @@ -1,61 +1,59 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -class GIFHeaderInfo -{ - public final byte identifier1; - public final byte identifier2; - public final byte identifier3; - public final byte version1; - public final byte version2; - public final byte version3; - - public final int logicalScreenWidth; - public final int logicalScreenHeight; - public final byte packedFields; - public final byte backgroundColorIndex; - public final byte pixelAspectRatio; - public final boolean globalColorTableFlag; - public final byte colorResolution; - public final boolean sortFlag; - public final byte sizeOfGlobalColorTable; - - public GIFHeaderInfo(byte Identifier1, byte Identifier2, byte Identifier3, - byte Version1, byte Version2, byte Version3, - int LogicalScreenWidth, int LogicalScreenHeight, byte PackedFields, - byte BackgroundColorIndex, byte PixelAspectRatio, - boolean GlobalColorTableFlag, byte ColorResolution, - boolean SortFlag, byte SizeofGlobalColorTable) - { - this.identifier1 = Identifier1; - this.identifier2 = Identifier2; - this.identifier3 = Identifier3; - this.version1 = Version1; - this.version2 = Version2; - this.version3 = Version3; - this.logicalScreenWidth = LogicalScreenWidth; - this.logicalScreenHeight = LogicalScreenHeight; - this.packedFields = PackedFields; - this.backgroundColorIndex = BackgroundColorIndex; - this.pixelAspectRatio = PixelAspectRatio; - this.globalColorTableFlag = GlobalColorTableFlag; - this.colorResolution = ColorResolution; - this.sortFlag = SortFlag; - this.sizeOfGlobalColorTable = SizeofGlobalColorTable; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +class GifHeaderInfo { + public final byte identifier1; + public final byte identifier2; + public final byte identifier3; + public final byte version1; + public final byte version2; + public final byte version3; + + public final int logicalScreenWidth; + public final int logicalScreenHeight; + public final byte packedFields; + public final byte backgroundColorIndex; + public final byte pixelAspectRatio; + public final boolean globalColorTableFlag; + public final byte colorResolution; + public final boolean sortFlag; + public final byte sizeOfGlobalColorTable; + + public GifHeaderInfo(final byte identifier1, final byte identifier2, final byte identifier3, + final byte version1, final byte version2, final byte version3, + final int logicalScreenWidth, final int logicalScreenHeight, final byte packedFields, + final byte backgroundColorIndex, final byte pixelAspectRatio, + final boolean globalColorTableFlag, final byte colorResolution, + final boolean sortFlag, final byte sizeOfGlobalColorTable) { + this.identifier1 = identifier1; + this.identifier2 = identifier2; + this.identifier3 = identifier3; + this.version1 = version1; + this.version2 = version2; + this.version3 = version3; + this.logicalScreenWidth = logicalScreenWidth; + this.logicalScreenHeight = logicalScreenHeight; + this.packedFields = packedFields; + this.backgroundColorIndex = backgroundColorIndex; + this.pixelAspectRatio = pixelAspectRatio; + this.globalColorTableFlag = globalColorTableFlag; + this.colorResolution = colorResolution; + this.sortFlag = sortFlag; + this.sizeOfGlobalColorTable = sizeOfGlobalColorTable; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java b/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java new file mode 100644 index 0000000..eadc1be --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java @@ -0,0 +1,1094 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.mylzw.MyLzwCompressor; +import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor; +import org.apache.commons.imaging.palette.Palette; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class GifImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".gif"; + private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; + private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 }; + private final static int EXTENSION_CODE = 0x21; + private final static int IMAGE_SEPARATOR = 0x2C; + private final static int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9; + private final static int COMMENT_EXTENSION = 0xfe; + private final static int PLAIN_TEXT_EXTENSION = 0x01; + private final static int XMP_EXTENSION = 0xff; + private final static int TERMINATOR_BYTE = 0x3b; + private final static int APPLICATION_EXTENSION_LABEL = 0xff; + private final static int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8) + | XMP_EXTENSION; + private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7; + private static final int INTERLACE_FLAG_MASK = 1 << 6; + private static final int SORT_FLAG_MASK = 1 << 5; + private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { + 0x58, // X + 0x4D, // M + 0x50, // P + 0x20, // + 0x44, // D + 0x61, // a + 0x74, // t + 0x61, // a + 0x58, // X + 0x4D, // M + 0x50, // P + }; + + public GifImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public String getName() { + return "Graphics Interchange Format"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.GIF, // + }; + } + + private GifHeaderInfo readHeader(final InputStream is, + final FormatCompliance formatCompliance) throws ImageReadException, + IOException { + final byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File"); + final byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File"); + final byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File"); + + final byte version1 = readByte("version1", is, "Not a Valid GIF File"); + final byte version2 = readByte("version2", is, "Not a Valid GIF File"); + final byte version3 = readByte("version3", is, "Not a Valid GIF File"); + + if (formatCompliance != null) { + formatCompliance.compareBytes("Signature", GIF_HEADER_SIGNATURE, + new byte[]{identifier1, identifier2, identifier3,}); + formatCompliance.compare("version", 56, version1); + formatCompliance + .compare("version", new int[] { 55, 57, }, version2); + formatCompliance.compare("version", 97, version3); + } + + if (getDebug()) { + printCharQuad("identifier: ", ((identifier1 << 16) + | (identifier2 << 8) | (identifier3 << 0))); + printCharQuad("version: ", + ((version1 << 16) | (version2 << 8) | (version3 << 0))); + } + + final int logicalScreenWidth = read2Bytes("Logical Screen Width", is, "Not a Valid GIF File", getByteOrder()); + final int logicalScreenHeight = read2Bytes("Logical Screen Height", is, "Not a Valid GIF File", getByteOrder()); + + if (formatCompliance != null) { + formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, + logicalScreenWidth); + formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, + logicalScreenHeight); + } + + final byte packedFields = readByte("Packed Fields", is, + "Not a Valid GIF File"); + final byte backgroundColorIndex = readByte("Background Color Index", is, + "Not a Valid GIF File"); + final byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, + "Not a Valid GIF File"); + + if (getDebug()) { + printByteBits("PackedFields bits", packedFields); + } + + final boolean globalColorTableFlag = ((packedFields & 128) > 0); + if (getDebug()) { + System.out.println("GlobalColorTableFlag: " + globalColorTableFlag); + } + final byte colorResolution = (byte) ((packedFields >> 4) & 7); + if (getDebug()) { + System.out.println("ColorResolution: " + colorResolution); + } + final boolean sortFlag = ((packedFields & 8) > 0); + if (getDebug()) { + System.out.println("SortFlag: " + sortFlag); + } + final byte sizeofGlobalColorTable = (byte) (packedFields & 7); + if (getDebug()) { + System.out.println("SizeofGlobalColorTable: " + + sizeofGlobalColorTable); + } + + if (formatCompliance != null) { + if (globalColorTableFlag && backgroundColorIndex != -1) { + formatCompliance.checkBounds("Background Color Index", 0, + convertColorTableSize(sizeofGlobalColorTable), + backgroundColorIndex); + } + } + + return new GifHeaderInfo(identifier1, identifier2, identifier3, + version1, version2, version3, logicalScreenWidth, + logicalScreenHeight, packedFields, backgroundColorIndex, + pixelAspectRatio, globalColorTableFlag, colorResolution, + sortFlag, sizeofGlobalColorTable); + } + + private GraphicControlExtension readGraphicControlExtension(final int code, + final InputStream is) throws IOException { + readByte("block_size", is, "GIF: corrupt GraphicControlExt"); + final int packed = readByte("packed fields", is, + "GIF: corrupt GraphicControlExt"); + + final int dispose = (packed & 0x1c) >> 2; // disposal method + final boolean transparency = (packed & 1) != 0; + + final int delay = read2Bytes("delay in milliseconds", is, "GIF: corrupt GraphicControlExt", getByteOrder()); + final int transparentColorIndex = 0xff & readByte("transparent color index", + is, "GIF: corrupt GraphicControlExt"); + readByte("block terminator", is, "GIF: corrupt GraphicControlExt"); + + return new GraphicControlExtension(code, packed, dispose, transparency, + delay, transparentColorIndex); + } + + private byte[] readSubBlock(final InputStream is) throws IOException { + final int blockSize = 0xff & readByte("block_size", is, "GIF: corrupt block"); + + return readBytes("block", is, blockSize, "GIF: corrupt block"); + } + + private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code) + throws IOException { + return readGenericGIFBlock(is, code, null); + } + + private GenericGifBlock readGenericGIFBlock(final InputStream is, final int code, + final byte[] first) throws IOException { + final List subblocks = new ArrayList(); + + if (first != null) { + subblocks.add(first); + } + + while (true) { + final byte[] bytes = readSubBlock(is); + if (bytes.length < 1) { + break; + } + subblocks.add(bytes); + } + + return new GenericGifBlock(code, subblocks); + } + + private List readBlocks(final GifHeaderInfo ghi, final InputStream is, + final boolean stopBeforeImageData, final FormatCompliance formatCompliance) + throws ImageReadException, IOException { + final List result = new ArrayList(); + + while (true) { + final int code = is.read(); + + switch (code) { + case -1: + throw new ImageReadException("GIF: unexpected end of data"); + + case IMAGE_SEPARATOR: + final ImageDescriptor id = readImageDescriptor(ghi, code, is, + stopBeforeImageData, formatCompliance); + result.add(id); + // if (stopBeforeImageData) + // return result; + + break; + + case EXTENSION_CODE: // extension + { + final int extensionCode = is.read(); + final int completeCode = ((0xff & code) << 8) + | (0xff & extensionCode); + + switch (extensionCode) { + case 0xf9: + final GraphicControlExtension gce = readGraphicControlExtension( + completeCode, is); + result.add(gce); + break; + + case COMMENT_EXTENSION: + case PLAIN_TEXT_EXTENSION: { + final GenericGifBlock block = readGenericGIFBlock(is, + completeCode); + result.add(block); + break; + } + + case APPLICATION_EXTENSION_LABEL: // 255 (hex 0xFF) Application + // Extension Label + { + final byte[] label = readSubBlock(is); + + if (formatCompliance != null) { + formatCompliance.addComment( + "Unknown Application Extension (" + + new String(label, "US-ASCII") + ")", + completeCode); + } + + // if (label == new String("ICCRGBG1")) + //{ + // GIF's can have embedded ICC Profiles - who knew? + //} + + if ((label != null) && (label.length > 0)) { + final GenericGifBlock block = readGenericGIFBlock(is, + completeCode, label); + result.add(block); + } + break; + } + + default: { + + if (formatCompliance != null) { + formatCompliance.addComment("Unknown block", + completeCode); + } + + final GenericGifBlock block = readGenericGIFBlock(is, + completeCode); + result.add(block); + break; + } + } + } + break; + + case TERMINATOR_BYTE: + return result; + + case 0x00: // bad byte, but keep going and see what happens + break; + + default: + throw new ImageReadException("GIF: unknown code: " + code); + } + } + } + + private ImageDescriptor readImageDescriptor(final GifHeaderInfo ghi, + final int blockCode, final InputStream is, final boolean stopBeforeImageData, + final FormatCompliance formatCompliance) throws ImageReadException, + IOException { + final int imageLeftPosition = read2Bytes("Image Left Position", is, "Not a Valid GIF File", getByteOrder()); + final int imageTopPosition = read2Bytes("Image Top Position", is, "Not a Valid GIF File", getByteOrder()); + final int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File", getByteOrder()); + final int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File", getByteOrder()); + final byte packedFields = readByte("Packed Fields", is, "Not a Valid GIF File"); + + if (formatCompliance != null) { + formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth, imageWidth); + formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight, imageHeight); + formatCompliance.checkBounds("Left Position", 0, ghi.logicalScreenWidth - imageWidth, imageLeftPosition); + formatCompliance.checkBounds("Top Position", 0, ghi.logicalScreenHeight - imageHeight, imageTopPosition); + } + + if (getDebug()) { + printByteBits("PackedFields bits", packedFields); + } + + final boolean localColorTableFlag = (((packedFields >> 7) & 1) > 0); + if (getDebug()) { + System.out.println("LocalColorTableFlag: " + localColorTableFlag); + } + final boolean interlaceFlag = (((packedFields >> 6) & 1) > 0); + if (getDebug()) { + System.out.println("Interlace Flag: " + interlaceFlag); + } + final boolean sortFlag = (((packedFields >> 5) & 1) > 0); + if (getDebug()) { + System.out.println("Sort Flag: " + sortFlag); + } + + final byte sizeOfLocalColorTable = (byte) (packedFields & 7); + if (getDebug()) { + System.out.println("SizeofLocalColorTable: " + sizeOfLocalColorTable); + } + + byte[] localColorTable = null; + if (localColorTableFlag) { + localColorTable = readColorTable(is, sizeOfLocalColorTable); + } + + byte[] imageData = null; + if (!stopBeforeImageData) { + final int lzwMinimumCodeSize = is.read(); + + final GenericGifBlock block = readGenericGIFBlock(is, -1); + final byte[] bytes = block.appendSubBlocks(); + final InputStream bais = new ByteArrayInputStream(bytes); + + final int size = imageWidth * imageHeight; + final MyLzwDecompressor myLzwDecompressor = new MyLzwDecompressor( + lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN); + imageData = myLzwDecompressor.decompress(bais, size); + } else { + final int LZWMinimumCodeSize = is.read(); + if (getDebug()) { + System.out.println("LZWMinimumCodeSize: " + LZWMinimumCodeSize); + } + + readGenericGIFBlock(is, -1); + } + + return new ImageDescriptor(blockCode, + imageLeftPosition, imageTopPosition, imageWidth, imageHeight, + packedFields, localColorTableFlag, interlaceFlag, sortFlag, + sizeOfLocalColorTable, localColorTable, imageData); + } + + private int simplePow(final int base, final int power) { + int result = 1; + + for (int i = 0; i < power; i++) { + result *= base; + } + + return result; + } + + private int convertColorTableSize(final int tableSize) { + return 3 * simplePow(2, tableSize + 1); + } + + private byte[] readColorTable(final InputStream is, final int tableSize) throws IOException { + final int actualSize = convertColorTableSize(tableSize); + + return readBytes("block", is, actualSize, "GIF: corrupt Color Table"); + } + + private GifBlock findBlock(final List blocks, final int code) { + for (GifBlock gifBlock : blocks) { + if (gifBlock.blockCode == code) { + return gifBlock; + } + } + return null; + } + + private ImageContents readFile(final ByteSource byteSource, + final boolean stopBeforeImageData) throws ImageReadException, IOException { + return readFile(byteSource, stopBeforeImageData, + FormatCompliance.getDefault()); + } + + private ImageContents readFile(final ByteSource byteSource, + final boolean stopBeforeImageData, final FormatCompliance formatCompliance) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final GifHeaderInfo ghi = readHeader(is, formatCompliance); + + byte[] globalColorTable = null; + if (ghi.globalColorTableFlag) { + globalColorTable = readColorTable(is, + ghi.sizeOfGlobalColorTable); + } + + final List blocks = readBlocks(ghi, is, stopBeforeImageData, + formatCompliance); + + final ImageContents result = new ImageContents(ghi, globalColorTable, + blocks); + canThrow = true; + return result; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageContents blocks = readFile(byteSource, false); + + if (blocks == null) { + throw new ImageReadException("GIF: Couldn't read blocks"); + } + + final GifHeaderInfo bhi = blocks.gifHeaderInfo; + if (bhi == null) { + throw new ImageReadException("GIF: Couldn't read Header"); + } + + final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, + IMAGE_SEPARATOR); + if (id == null) { + throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); + } + + // Prefer the size information in the ImageDescriptor; it is more + // reliable + // than the size information in the header. + return new Dimension(id.imageWidth, id.imageHeight); + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private List getComments(final List blocks) throws IOException { + final List result = new ArrayList(); + final int code = 0x21fe; + + for (GifBlock block : blocks) { + if (block.blockCode == code) { + final byte[] bytes = ((GenericGifBlock) block).appendSubBlocks(); + result.add(new String(bytes, "US-ASCII")); + } + } + + return result; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageContents blocks = readFile(byteSource, false); + + if (blocks == null) { + throw new ImageReadException("GIF: Couldn't read blocks"); + } + + final GifHeaderInfo bhi = blocks.gifHeaderInfo; + if (bhi == null) { + throw new ImageReadException("GIF: Couldn't read Header"); + } + + final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, + IMAGE_SEPARATOR); + if (id == null) { + throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); + } + + final GraphicControlExtension gce = (GraphicControlExtension) findBlock( + blocks.blocks, GRAPHIC_CONTROL_EXTENSION); + + // Prefer the size information in the ImageDescriptor; it is more + // reliable than the size information in the header. + final int height = id.imageHeight; + final int width = id.imageWidth; + + final List comments = getComments(blocks.blocks); + final int bitsPerPixel = (bhi.colorResolution + 1); + final ImageFormat format = ImageFormats.GIF; + final String formatName = "GIF Graphics Interchange Format"; + final String mimeType = "image/gif"; + // we ought to count images, but don't yet. + final int numberOfImages = -1; + + final boolean progressive = id.interlaceFlag; + + final int physicalWidthDpi = 72; + final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); + final int physicalHeightDpi = 72; + final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); + + final String formatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1) + + ((char) blocks.gifHeaderInfo.version2) + + ((char) blocks.gifHeaderInfo.version3); + + boolean transparent = false; + if (gce != null && gce.transparency) { + transparent = true; + } + + final boolean usesPalette = true; + final int colorType = ImageInfo.COLOR_TYPE_RGB; + final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; + + return new ImageInfo(formatDetails, bitsPerPixel, comments, + format, formatName, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + pw.println("gif.dumpImageFile"); + + final ImageInfo imageData = getImageInfo(byteSource); + if (imageData == null) { + return false; + } + + imageData.toString(pw, ""); + + final ImageContents blocks = readFile(byteSource, false); + + pw.println("gif.blocks: " + blocks.blocks.size()); + for (int i = 0; i < blocks.blocks.size(); i++) { + final GifBlock gifBlock = blocks.blocks.get(i); + this.debugNumber(pw, "\t" + i + " (" + + gifBlock.getClass().getName() + ")", + gifBlock.blockCode, 4); + } + + pw.println(""); + + return true; + } + + private int[] getColorTable(final byte[] bytes) throws ImageReadException { + if ((bytes.length % 3) != 0) { + throw new ImageReadException("Bad Color Table Length: " + + bytes.length); + } + final int length = bytes.length / 3; + + final int[] result = new int[length]; + + for (int i = 0; i < length; i++) { + final int red = 0xff & bytes[(i * 3) + 0]; + final int green = 0xff & bytes[(i * 3) + 1]; + final int blue = 0xff & bytes[(i * 3) + 2]; + + final int alpha = 0xff; + + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + result[i] = rgb; + } + + return result; + } + + @Override + public FormatCompliance getFormatCompliance(final ByteSource byteSource) + throws ImageReadException, IOException { + final FormatCompliance result = new FormatCompliance( + byteSource.getDescription()); + + readFile(byteSource, false, result); + + return result; + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageContents imageContents = readFile(byteSource, false); + + if (imageContents == null) { + throw new ImageReadException("GIF: Couldn't read blocks"); + } + + final GifHeaderInfo ghi = imageContents.gifHeaderInfo; + if (ghi == null) { + throw new ImageReadException("GIF: Couldn't read Header"); + } + + final ImageDescriptor id = (ImageDescriptor) findBlock(imageContents.blocks, + IMAGE_SEPARATOR); + if (id == null) { + throw new ImageReadException("GIF: Couldn't read Image Descriptor"); + } + final GraphicControlExtension gce = (GraphicControlExtension) findBlock( + imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); + + // Prefer the size information in the ImageDescriptor; it is more + // reliable + // than the size information in the header. + final int width = id.imageWidth; + final int height = id.imageHeight; + + boolean hasAlpha = false; + if (gce != null && gce.transparency) { + hasAlpha = true; + } + + final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha); + + int[] colorTable; + if (id.localColorTable != null) { + colorTable = getColorTable(id.localColorTable); + } else if (imageContents.globalColorTable != null) { + colorTable = getColorTable(imageContents.globalColorTable); + } else { + throw new ImageReadException("Gif: No Color Table"); + } + + int transparentIndex = -1; + if (gce != null && hasAlpha) { + transparentIndex = gce.transparentColorIndex; + } + + int counter = 0; + + final int rowsInPass1 = (height + 7) / 8; + final int rowsInPass2 = (height + 3) / 8; + final int rowsInPass3 = (height + 1) / 4; + final int rowsInPass4 = (height) / 2; + + for (int row = 0; row < height; row++) { + int y; + if (id.interlaceFlag) { + int theRow = row; + if (theRow < rowsInPass1) { + y = theRow * 8; + } else { + theRow -= rowsInPass1; + if (theRow < (rowsInPass2)) { + y = 4 + (theRow * 8); + } else { + theRow -= rowsInPass2; + if (theRow < (rowsInPass3)) { + y = 2 + (theRow * 4); + } else { + theRow -= rowsInPass3; + if (theRow < (rowsInPass4)) { + y = 1 + (theRow * 2); + } else { + throw new ImageReadException("Gif: Strange Row"); + } + } + } + } + } else { + y = row; + } + + for (int x = 0; x < width; x++) { + final int index = 0xff & id.imageData[counter++]; + int rgb = colorTable[index]; + + if (transparentIndex == index) { + rgb = 0x00; + } + + imageBuilder.setRGB(x, y, rgb); + } + + } + + return imageBuilder.getBufferedImage(); + + } + + private void writeAsSubBlocks(final OutputStream os, final byte[] bytes) throws IOException { + int index = 0; + + while (index < bytes.length) { + final int blockSize = Math.min(bytes.length - index, 255); + os.write(blockSize); + os.write(bytes, index, blockSize); + index += blockSize; + } + os.write(0); // last block + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = new HashMap(params); + + final boolean verbose = Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE)); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + String xmpXml = null; + if (params.containsKey(PARAM_KEY_XMP_XML)) { + xmpXml = (String) params.get(PARAM_KEY_XMP_XML); + params.remove(PARAM_KEY_XMP_XML); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final int width = src.getWidth(); + final int height = src.getHeight(); + + final boolean hasAlpha = new PaletteFactory().hasTransparency(src); + + final int maxColors = hasAlpha ? 255 : 256; + + Palette palette2 = new PaletteFactory().makeExactRgbPaletteSimple(src, maxColors); + // int palette[] = new PaletteFactory().makePaletteSimple(src, 256); + // Map palette_map = paletteToMap(palette); + + if (palette2 == null) { + palette2 = new PaletteFactory().makeQuantizedRgbPalette(src, maxColors); + if (verbose) { + System.out.println("quantizing"); + } + } else if (verbose) { + System.out.println("exact palette"); + } + + if (palette2 == null) { + throw new ImageWriteException("Gif: can't write images with more than 256 colors"); + } + final int paletteSize = palette2.length() + (hasAlpha ? 1 : 0); + + final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); + + // write Header + os.write(0x47); // G magic numbers + os.write(0x49); // I + os.write(0x46); // F + + os.write(0x38); // 8 version magic numbers + os.write(0x39); // 9 + os.write(0x61); // a + + // Logical Screen Descriptor. + + bos.write2Bytes(width); + bos.write2Bytes(height); + + final int colorTableScaleLessOne = (paletteSize > 128) ? 7 + : (paletteSize > 64) ? 6 : (paletteSize > 32) ? 5 + : (paletteSize > 16) ? 4 : (paletteSize > 8) ? 3 + : (paletteSize > 4) ? 2 + : (paletteSize > 2) ? 1 : 0; + + final int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1); + { + final byte colorResolution = (byte) colorTableScaleLessOne; // TODO: + + final boolean globalColorTableFlag = false; + final boolean sortFlag = false; + final int globalColorTableFlagMask = 1 << 7; + final int sortFlagMask = 8; + final int sizeOfGlobalColorTable = 0; + + final int packedFields = ((globalColorTableFlag ? globalColorTableFlagMask + : 0) + | (sortFlag ? sortFlagMask : 0) + | ((7 & colorResolution) << 4) | (7 & sizeOfGlobalColorTable)); + bos.write(packedFields); // one byte + } + { + final byte backgroundColorIndex = 0; + bos.write(backgroundColorIndex); + } + { + final byte pixelAspectRatio = 0; + bos.write(pixelAspectRatio); + } + + //{ + // write Global Color Table. + + //} + + { // ALWAYS write GraphicControlExtension + bos.write(EXTENSION_CODE); + bos.write((byte) 0xf9); + // bos.write(0xff & (kGraphicControlExtension >> 8)); + // bos.write(0xff & (kGraphicControlExtension >> 0)); + + bos.write((byte) 4); // block size; + final int packedFields = hasAlpha ? 1 : 0; // transparency flag + bos.write((byte) packedFields); + bos.write((byte) 0); // Delay Time + bos.write((byte) 0); // Delay Time + bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent + // Color + // Index + bos.write((byte) 0); // terminator + } + + if (null != xmpXml) { + bos.write(EXTENSION_CODE); + bos.write(APPLICATION_EXTENSION_LABEL); + + bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B + bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE); + + final byte[] xmpXmlBytes = xmpXml.getBytes("utf-8"); + bos.write(xmpXmlBytes); + + // write "magic trailer" + for (int magic = 0; magic <= 0xff; magic++) { + bos.write(0xff - magic); + } + + bos.write((byte) 0); // terminator + + } + + { // Image Descriptor. + bos.write(IMAGE_SEPARATOR); + bos.write2Bytes(0); // Image Left Position + bos.write2Bytes(0); // Image Top Position + bos.write2Bytes(width); // Image Width + bos.write2Bytes(height); // Image Height + + { + final boolean localColorTableFlag = true; + // boolean LocalColorTableFlag = false; + final boolean interlaceFlag = false; + final boolean sortFlag = false; + final int sizeOfLocalColorTable = colorTableScaleLessOne; + + // int SizeOfLocalColorTable = 0; + + final int packedFields; + if (localColorTableFlag) { + packedFields = (LOCAL_COLOR_TABLE_FLAG_MASK + | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) + | (sortFlag ? SORT_FLAG_MASK : 0) + | (7 & sizeOfLocalColorTable)); + } else { + packedFields = (0 + | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) + | (sortFlag ? SORT_FLAG_MASK : 0) + | (7 & sizeOfLocalColorTable)); + } + bos.write(packedFields); // one byte + } + } + + { // write Local Color Table. + for (int i = 0; i < colorTableSizeInFormat; i++) { + if (i < palette2.length()) { + final int rgb = palette2.getEntry(i); + + final int red = 0xff & (rgb >> 16); + final int green = 0xff & (rgb >> 8); + final int blue = 0xff & (rgb >> 0); + + bos.write(red); + bos.write(green); + bos.write(blue); + } else { + bos.write(0); + bos.write(0); + bos.write(0); + } + } + } + + { // get Image Data. +// int image_data_total = 0; + + int lzwMinimumCodeSize = colorTableScaleLessOne + 1; + // LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize); + if (lzwMinimumCodeSize < 2) { + lzwMinimumCodeSize = 2; + } + + // TODO: + // make + // better + // choice + // here. + bos.write(lzwMinimumCodeSize); + + final MyLzwCompressor compressor = new MyLzwCompressor( + lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false); // GIF + // Mode); + + final byte[] imagedata = new byte[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int rgb = 0xffffff & argb; + int index; + + if (hasAlpha) { + final int alpha = 0xff & (argb >> 24); + final int alphaThreshold = 255; + if (alpha < alphaThreshold) { + index = palette2.length(); // is transparent + } else { + index = palette2.getPaletteIndex(rgb); + } + } else { + index = palette2.getPaletteIndex(rgb); + } + + imagedata[y * width + x] = (byte) index; + } + } + + final byte[] compressed = compressor.compress(imagedata); + writeAsSubBlocks(bos, compressed); +// image_data_total += compressed.length; + } + + // palette2.dump(); + + bos.write(TERMINATOR_BYTE); + + bos.close(); + os.close(); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final FormatCompliance formatCompliance = null; + final GifHeaderInfo ghi = readHeader(is, formatCompliance); + + if (ghi.globalColorTableFlag) { + readColorTable(is, ghi.sizeOfGlobalColorTable); + } + + final List blocks = readBlocks(ghi, is, true, formatCompliance); + + final List result = new ArrayList(); + for (GifBlock block : blocks) { + if (block.blockCode != XMP_COMPLETE_CODE) { + continue; + } + + final GenericGifBlock genericBlock = (GenericGifBlock) block; + + final byte[] blockBytes = genericBlock.appendSubBlocks(true); + if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) { + continue; + } + + if (!compareBytes(blockBytes, 0, + XMP_APPLICATION_ID_AND_AUTH_CODE, 0, + XMP_APPLICATION_ID_AND_AUTH_CODE.length)) { + continue; + } + + final byte[] GIF_MAGIC_TRAILER = new byte[256]; + for (int magic = 0; magic <= 0xff; magic++) { + GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic); + } + + if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length + + GIF_MAGIC_TRAILER.length) { + continue; + } + if (!compareBytes(blockBytes, blockBytes.length + - GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0, + GIF_MAGIC_TRAILER.length)) { + throw new ImageReadException( + "XMP block in GIF missing magic trailer."); + } + + try { + // XMP is UTF-8 encoded xml. + final String xml = new String( + blockBytes, + XMP_APPLICATION_ID_AND_AUTH_CODE.length, + blockBytes.length + - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length), + "utf-8"); + result.add(xml); + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("Invalid XMP Block in GIF.", e); + } + } + + if (result.size() < 1) { + return null; + } + if (result.size() > 1) { + throw new ImageReadException("More than one XMP Block in GIF."); + } + canThrow = true; + return result.get(0); + + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/gif/GraphicControlExtension.java b/src/main/java/org/apache/commons/imaging/formats/gif/GraphicControlExtension.java similarity index 80% rename from src/main/java/org/apache/sanselan/formats/gif/GraphicControlExtension.java rename to src/main/java/org/apache/commons/imaging/formats/gif/GraphicControlExtension.java index 68e54a8..4a46f99 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/GraphicControlExtension.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GraphicControlExtension.java @@ -1,41 +1,39 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -class GraphicControlExtension extends GIFBlock -{ - - public final int packed; - public final int dispose; - public final boolean transparency; - public final int delay; - public final int transparentColorIndex; - - public GraphicControlExtension(int blockCode, int packed, int dispose, - boolean transparency, int delay, int transparentColorIndex) - { - super(blockCode); - - this.packed = packed; - this.dispose = dispose; - this.transparency = transparency; - this.delay = delay; - this.transparentColorIndex = transparentColorIndex; - - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +class GraphicControlExtension extends GifBlock { + + public final int packed; + public final int dispose; + public final boolean transparency; + public final int delay; + public final int transparentColorIndex; + + public GraphicControlExtension(final int blockCode, final int packed, final int dispose, + final boolean transparency, final int delay, final int transparentColorIndex) { + super(blockCode); + + this.packed = packed; + this.dispose = dispose; + this.transparency = transparency; + this.delay = delay; + this.transparentColorIndex = transparentColorIndex; + + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/gif/ImageContents.java b/src/main/java/org/apache/commons/imaging/formats/gif/ImageContents.java similarity index 72% rename from src/main/java/org/apache/sanselan/formats/gif/ImageContents.java rename to src/main/java/org/apache/commons/imaging/formats/gif/ImageContents.java index d20eeb8..6585bb9 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/ImageContents.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/ImageContents.java @@ -1,35 +1,33 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -import java.util.ArrayList; - -class ImageContents -{ - public final GIFHeaderInfo gifHeaderInfo; - - public final ArrayList blocks; - public final byte globalColorTable[]; - - public ImageContents(GIFHeaderInfo gifHeaderInfo, byte globalColorTable[], - ArrayList blocks) - { - this.gifHeaderInfo = gifHeaderInfo; - this.globalColorTable = globalColorTable; - this.blocks = blocks; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +import java.util.List; + +class ImageContents { + public final GifHeaderInfo gifHeaderInfo; + + public final List blocks; + public final byte[] globalColorTable; + + public ImageContents(final GifHeaderInfo gifHeaderInfo, final byte[] globalColorTable, + final List blocks) { + this.gifHeaderInfo = gifHeaderInfo; + this.globalColorTable = globalColorTable; + this.blocks = blocks; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/gif/ImageDescriptor.java b/src/main/java/org/apache/commons/imaging/formats/gif/ImageDescriptor.java similarity index 52% rename from src/main/java/org/apache/sanselan/formats/gif/ImageDescriptor.java rename to src/main/java/org/apache/commons/imaging/formats/gif/ImageDescriptor.java index fc98f3f..a34f3c7 100644 --- a/src/main/java/org/apache/sanselan/formats/gif/ImageDescriptor.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/ImageDescriptor.java @@ -1,57 +1,55 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -public class ImageDescriptor extends GIFBlock -{ - - public final int imageLeftPosition; - public final int imageTopPosition; - public final int imageWidth; - public final int imageHeight; - public final byte packedFields; - public final boolean localColorTableFlag; - public final boolean interlaceFlag; - public final boolean sortFlag; - public final byte sizeOfLocalColorTable; - - public final byte localColorTable[]; - public final byte imageData[]; - - public ImageDescriptor(int blockCode, int ImageLeftPosition, - int ImageTopPosition, int ImageWidth, int ImageHeight, - byte PackedFields, boolean LocalColorTableFlag, - boolean InterlaceFlag, boolean SortFlag, - byte SizeofLocalColorTable, byte LocalColorTable[], - byte ImageData[]) - { - super(blockCode); - - this.imageLeftPosition = ImageLeftPosition; - this.imageTopPosition = ImageTopPosition; - this.imageWidth = ImageWidth; - this.imageHeight = ImageHeight; - this.packedFields = PackedFields; - this.localColorTableFlag = LocalColorTableFlag; - this.interlaceFlag = InterlaceFlag; - this.sortFlag = SortFlag; - this.sizeOfLocalColorTable = SizeofLocalColorTable; - - this.localColorTable = LocalColorTable; - this.imageData = ImageData; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.gif; + +class ImageDescriptor extends GifBlock { + + public final int imageLeftPosition; + public final int imageTopPosition; + public final int imageWidth; + public final int imageHeight; + public final byte packedFields; + public final boolean localColorTableFlag; + public final boolean interlaceFlag; + public final boolean sortFlag; + public final byte sizeOfLocalColorTable; + + public final byte[] localColorTable; + public final byte[] imageData; + + public ImageDescriptor(final int blockCode, final int imageLeftPosition, + final int imageTopPosition, final int imageWidth, final int imageHeight, + final byte packedFields, final boolean localColorTableFlag, + final boolean interlaceFlag, final boolean sortFlag, + final byte sizeofLocalColorTable, final byte[] localColorTable, + final byte[] imageData) { + super(blockCode); + + this.imageLeftPosition = imageLeftPosition; + this.imageTopPosition = imageTopPosition; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.packedFields = packedFields; + this.localColorTableFlag = localColorTableFlag; + this.interlaceFlag = interlaceFlag; + this.sortFlag = sortFlag; + this.sizeOfLocalColorTable = sizeofLocalColorTable; + + this.localColorTable = localColorTable; + this.imageData = imageData; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/gif/package-info.java b/src/main/java/org/apache/commons/imaging/formats/gif/package-info.java new file mode 100644 index 0000000..1c7940d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/gif/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The GIF image format. + */ +package org.apache.commons.imaging.formats.gif; + diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java new file mode 100644 index 0000000..5168529 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsDecoder.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.icns; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.formats.icns.IcnsImageParser.IcnsElement; + +final class IcnsDecoder { + private static final int[] PALETTE_4BPP = { 0xffffffff, 0xfffcf305, + 0xffff6402, 0xffdd0806, 0xfff20884, 0xff4600a5, 0xff0000d4, + 0xff02abea, 0xff1fb714, 0xff006411, 0xff562c05, 0xff90713a, + 0xffc0c0c0, 0xff808080, 0xff404040, 0xff000000 }; + + private static final int[] PALETTE_8BPP = { 0xFFFFFFFF, 0xFFFFFFCC, + 0xFFFFFF99, 0xFFFFFF66, 0xFFFFFF33, 0xFFFFFF00, 0xFFFFCCFF, + 0xFFFFCCCC, 0xFFFFCC99, 0xFFFFCC66, 0xFFFFCC33, 0xFFFFCC00, + 0xFFFF99FF, 0xFFFF99CC, 0xFFFF9999, 0xFFFF9966, 0xFFFF9933, + 0xFFFF9900, 0xFFFF66FF, 0xFFFF66CC, 0xFFFF6699, 0xFFFF6666, + 0xFFFF6633, 0xFFFF6600, 0xFFFF33FF, 0xFFFF33CC, 0xFFFF3399, + 0xFFFF3366, 0xFFFF3333, 0xFFFF3300, 0xFFFF00FF, 0xFFFF00CC, + 0xFFFF0099, 0xFFFF0066, 0xFFFF0033, 0xFFFF0000, 0xFFCCFFFF, + 0xFFCCFFCC, 0xFFCCFF99, 0xFFCCFF66, 0xFFCCFF33, 0xFFCCFF00, + 0xFFCCCCFF, 0xFFCCCCCC, 0xFFCCCC99, 0xFFCCCC66, 0xFFCCCC33, + 0xFFCCCC00, 0xFFCC99FF, 0xFFCC99CC, 0xFFCC9999, 0xFFCC9966, + 0xFFCC9933, 0xFFCC9900, 0xFFCC66FF, 0xFFCC66CC, 0xFFCC6699, + 0xFFCC6666, 0xFFCC6633, 0xFFCC6600, 0xFFCC33FF, 0xFFCC33CC, + 0xFFCC3399, 0xFFCC3366, 0xFFCC3333, 0xFFCC3300, 0xFFCC00FF, + 0xFFCC00CC, 0xFFCC0099, 0xFFCC0066, 0xFFCC0033, 0xFFCC0000, + 0xFF99FFFF, 0xFF99FFCC, 0xFF99FF99, 0xFF99FF66, 0xFF99FF33, + 0xFF99FF00, 0xFF99CCFF, 0xFF99CCCC, 0xFF99CC99, 0xFF99CC66, + 0xFF99CC33, 0xFF99CC00, 0xFF9999FF, 0xFF9999CC, 0xFF999999, + 0xFF999966, 0xFF999933, 0xFF999900, 0xFF9966FF, 0xFF9966CC, + 0xFF996699, 0xFF996666, 0xFF996633, 0xFF996600, 0xFF9933FF, + 0xFF9933CC, 0xFF993399, 0xFF993366, 0xFF993333, 0xFF993300, + 0xFF9900FF, 0xFF9900CC, 0xFF990099, 0xFF990066, 0xFF990033, + 0xFF990000, 0xFF66FFFF, 0xFF66FFCC, 0xFF66FF99, 0xFF66FF66, + 0xFF66FF33, 0xFF66FF00, 0xFF66CCFF, 0xFF66CCCC, 0xFF66CC99, + 0xFF66CC66, 0xFF66CC33, 0xFF66CC00, 0xFF6699FF, 0xFF6699CC, + 0xFF669999, 0xFF669966, 0xFF669933, 0xFF669900, 0xFF6666FF, + 0xFF6666CC, 0xFF666699, 0xFF666666, 0xFF666633, 0xFF666600, + 0xFF6633FF, 0xFF6633CC, 0xFF663399, 0xFF663366, 0xFF663333, + 0xFF663300, 0xFF6600FF, 0xFF6600CC, 0xFF660099, 0xFF660066, + 0xFF660033, 0xFF660000, 0xFF33FFFF, 0xFF33FFCC, 0xFF33FF99, + 0xFF33FF66, 0xFF33FF33, 0xFF33FF00, 0xFF33CCFF, 0xFF33CCCC, + 0xFF33CC99, 0xFF33CC66, 0xFF33CC33, 0xFF33CC00, 0xFF3399FF, + 0xFF3399CC, 0xFF339999, 0xFF339966, 0xFF339933, 0xFF339900, + 0xFF3366FF, 0xFF3366CC, 0xFF336699, 0xFF336666, 0xFF336633, + 0xFF336600, 0xFF3333FF, 0xFF3333CC, 0xFF333399, 0xFF333366, + 0xFF333333, 0xFF333300, 0xFF3300FF, 0xFF3300CC, 0xFF330099, + 0xFF330066, 0xFF330033, 0xFF330000, 0xFF00FFFF, 0xFF00FFCC, + 0xFF00FF99, 0xFF00FF66, 0xFF00FF33, 0xFF00FF00, 0xFF00CCFF, + 0xFF00CCCC, 0xFF00CC99, 0xFF00CC66, 0xFF00CC33, 0xFF00CC00, + 0xFF0099FF, 0xFF0099CC, 0xFF009999, 0xFF009966, 0xFF009933, + 0xFF009900, 0xFF0066FF, 0xFF0066CC, 0xFF006699, 0xFF006666, + 0xFF006633, 0xFF006600, 0xFF0033FF, 0xFF0033CC, 0xFF003399, + 0xFF003366, 0xFF003333, 0xFF003300, 0xFF0000FF, 0xFF0000CC, + 0xFF000099, 0xFF000066, 0xFF000033, 0xFFEE0000, 0xFFDD0000, + 0xFFBB0000, 0xFFAA0000, 0xFF880000, 0xFF770000, 0xFF550000, + 0xFF440000, 0xFF220000, 0xFF110000, 0xFF00EE00, 0xFF00DD00, + 0xFF00BB00, 0xFF00AA00, 0xFF008800, 0xFF007700, 0xFF005500, + 0xFF004400, 0xFF002200, 0xFF001100, 0xFF0000EE, 0xFF0000DD, + 0xFF0000BB, 0xFF0000AA, 0xFF000088, 0xFF000077, 0xFF000055, + 0xFF000044, 0xFF000022, 0xFF000011, 0xFFEEEEEE, 0xFFDDDDDD, + 0xFFBBBBBB, 0xFFAAAAAA, 0xFF888888, 0xFF777777, 0xFF555555, + 0xFF444444, 0xFF222222, 0xFF111111, 0xFF000000 }; + + private IcnsDecoder() { + } + + private static void decode1BPPImage(final IcnsType imageType, final byte[] imageData, final ImageBuilder image) { + int position = 0; + int bitsLeft = 0; + int value = 0; + for (int y = 0; y < imageType.getHeight(); y++) { + for (int x = 0; x < imageType.getWidth(); x++) { + if (bitsLeft == 0) { + value = 0xff & imageData[position++]; + bitsLeft = 8; + } + int argb; + if ((value & 0x80) != 0) { + argb = 0xff000000; + } else { + argb = 0xffffffff; + } + value <<= 1; + bitsLeft--; + image.setRGB(x, y, argb); + } + } + } + + private static void decode4BPPImage(final IcnsType imageType, final byte[] imageData, final ImageBuilder image) { + int i = 0; + boolean visited = false; + for (int y = 0; y < imageType.getHeight(); y++) { + for (int x = 0; x < imageType.getWidth(); x++) { + int index; + if (!visited) { + index = 0xf & (imageData[i] >> 4); + } else { + index = 0xf & imageData[i++]; + } + visited = !visited; + image.setRGB(x, y, PALETTE_4BPP[index]); + } + } + } + + private static void decode8BPPImage(final IcnsType imageType, final byte[] imageData, final ImageBuilder image) { + for (int y = 0; y < imageType.getHeight(); y++) { + for (int x = 0; x < imageType.getWidth(); x++) { + final int index = 0xff & imageData[y * imageType.getWidth() + x]; + image.setRGB(x, y, PALETTE_8BPP[index]); + } + } + } + + private static void decode32BPPImage(final IcnsType imageType, final byte[] imageData, final ImageBuilder image) { + for (int y = 0; y < imageType.getHeight(); y++) { + for (int x = 0; x < imageType.getWidth(); x++) { + final int argb = 0xff000000 /* the "alpha" is ignored */ + | ((0xff & imageData[4 * (y * imageType.getWidth() + x) + 1]) << 16) + | ((0xff & imageData[4 * (y * imageType.getWidth() + x) + 2]) << 8) + | (0xff & imageData[4 * (y * imageType.getWidth() + x) + 3]); + image.setRGB(x, y, argb); + } + } + } + + private static void apply1BPPMask(final byte[] maskData, final ImageBuilder image) throws ImageReadException { + int position = 0; + int bitsLeft = 0; + int value = 0; + + // 1 bit icon types have image data followed by mask data in the same + // entry + final int totalBytes = (image.getWidth() * image.getHeight() + 7) / 8; + if (maskData.length >= 2 * totalBytes) { + position = totalBytes; + } else { + throw new ImageReadException("1 BPP mask underrun parsing ICNS file"); + } + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + if (bitsLeft == 0) { + value = 0xff & maskData[position++]; + bitsLeft = 8; + } + int alpha; + if ((value & 0x80) != 0) { + alpha = 0xff; + } else { + alpha = 0x00; + } + value <<= 1; + bitsLeft--; + image.setRGB(x, y, (alpha << 24) | (0xffffff & image.getRGB(x, y))); + } + } + } + + private static void apply8BPPMask(final byte[] maskData, final ImageBuilder image) { + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + final int alpha = 0xff & maskData[y * image.getWidth() + x]; + image.setRGB(x, y, + (alpha << 24) | (0xffffff & image.getRGB(x, y))); + } + } + } + + public static List decodeAllImages(final IcnsImageParser.IcnsElement[] icnsElements) + throws ImageReadException { + final List result = new ArrayList(); + for (final IcnsElement imageElement : icnsElements) { + final IcnsType imageType = IcnsType.findImageType(imageElement.type); + if (imageType == null) { + continue; + } + + IcnsType maskType; + IcnsImageParser.IcnsElement maskElement = null; + if (imageType.hasMask()) { + maskType = imageType; + maskElement = imageElement; + } else { + maskType = IcnsType.find8BPPMaskType(imageType); + if (maskType != null) { + for (final IcnsElement icnsElement : icnsElements) { + if (icnsElement.type == maskType.getType()) { + maskElement = icnsElement; + break; + } + } + } + if (maskElement == null) { + maskType = IcnsType.find1BPPMaskType(imageType); + if (maskType != null) { + for (final IcnsElement icnsElement : icnsElements) { + if (icnsElement.type == maskType.getType()) { + maskElement = icnsElement; + break; + } + } + } + } + } + + // FIXME: don't skip these when JPEG 2000 support is added: + if (imageType == IcnsType.ICNS_256x256_32BIT_ARGB_IMAGE + || imageType == IcnsType.ICNS_512x512_32BIT_ARGB_IMAGE) { + continue; + } + + final int expectedSize = (imageType.getWidth() * imageType.getHeight() + * imageType.getBitsPerPixel() + 7) / 8; + byte[] imageData; + if (imageElement.data.length < expectedSize) { + if (imageType.getBitsPerPixel() == 32) { + imageData = Rle24Compression.decompress( + imageType.getWidth(), imageType.getHeight(), + imageElement.data); + } else { + throw new ImageReadException("Short image data but not a 32 bit compressed type"); + } + } else { + imageData = imageElement.data; + } + + final ImageBuilder imageBuilder = new ImageBuilder(imageType.getWidth(), + imageType.getHeight(), true); + switch (imageType.getBitsPerPixel()) { + case 1: + decode1BPPImage(imageType, imageData, imageBuilder); + break; + case 4: + decode4BPPImage(imageType, imageData, imageBuilder); + break; + case 8: + decode8BPPImage(imageType, imageData, imageBuilder); + break; + case 32: + decode32BPPImage(imageType, imageData, imageBuilder); + break; + default: + throw new ImageReadException("Unsupported bit depth " + imageType.getBitsPerPixel()); + } + + if (maskElement != null) { + if (maskType.getBitsPerPixel() == 1) { + apply1BPPMask(maskElement.data, imageBuilder); + } else if (maskType.getBitsPerPixel() == 8) { + apply8BPPMask(maskElement.data, imageBuilder); + } else { + throw new ImageReadException("Unsupport mask bit depth " + maskType.getBitsPerPixel()); + } + } + + result.add(imageBuilder.getBufferedImage()); + } + return result; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java new file mode 100644 index 0000000..263c245 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsImageParser.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.icns; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class IcnsImageParser extends ImageParser { + static final int ICNS_MAGIC = IcnsType.typeAsInt("icns"); + private static final String DEFAULT_EXTENSION = ".icns"; + private static final String[] ACCEPTED_EXTENSIONS = { ".icns", }; + + public IcnsImageParser() { + super.setByteOrder(ByteOrder.BIG_ENDIAN); + } + + @Override + public String getName() { + return "Apple Icon Image"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.ICNS }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageReadException("Unknown parameter: " + firstKey); + } + + final IcnsContents contents = readImage(byteSource); + final List images = IcnsDecoder + .decodeAllImages(contents.icnsElements); + if (images.isEmpty()) { + throw new ImageReadException("No icons in ICNS file"); + } + final BufferedImage image0 = images.get(0); + return new ImageInfo("Icns", 32, new ArrayList(), + ImageFormats.ICNS, "ICNS Apple Icon Image", + image0.getHeight(), "image/x-icns", images.size(), 0, 0, 0, 0, + image0.getWidth(), false, true, false, + ImageInfo.COLOR_TYPE_RGB, + ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageReadException("Unknown parameter: " + firstKey); + } + + final IcnsContents contents = readImage(byteSource); + final List images = IcnsDecoder + .decodeAllImages(contents.icnsElements); + if (images.isEmpty()) { + throw new ImageReadException("No icons in ICNS file"); + } + final BufferedImage image0 = images.get(0); + return new Dimension(image0.getWidth(), image0.getHeight()); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private static class IcnsHeader { + public final int magic; // Magic literal (4 bytes), always "icns" + public final int fileSize; // Length of file (4 bytes), in bytes. + + public IcnsHeader(final int magic, final int fileSize) { + this.magic = magic; + this.fileSize = fileSize; + } + + public void dump(final PrintWriter pw) { + pw.println("IcnsHeader"); + pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" + + IcnsType.describeType(magic) + ")"); + pw.println("FileSize: " + fileSize); + pw.println(""); + } + } + + private IcnsHeader readIcnsHeader(final InputStream is) + throws ImageReadException, IOException { + final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder()); + final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder()); + + if (magic != ICNS_MAGIC) { + throw new ImageReadException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic)); + } + + return new IcnsHeader(magic, fileSize); + } + + static class IcnsElement { + public final int type; + public final int elementSize; + public final byte[] data; + + public IcnsElement(final int type, final int elementSize, final byte[] data) { + this.type = type; + this.elementSize = elementSize; + this.data = data; + } + + public void dump(final PrintWriter pw) { + pw.println("IcnsElement"); + final IcnsType icnsType = IcnsType.findAnyType(type); + String typeDescription; + if (icnsType == null) { + typeDescription = ""; + } else { + typeDescription = " " + icnsType.toString(); + } + pw.println("Type: 0x" + Integer.toHexString(type) + " (" + + IcnsType.describeType(type) + ")" + typeDescription); + pw.println("ElementSize: " + elementSize); + pw.println(""); + } + } + + private IcnsElement readIcnsElement(final InputStream is) throws IOException { + final int type = read4Bytes("Type", is, "Not a Valid ICNS File", getByteOrder()); // Icon type + // (4 bytes) + final int elementSize = read4Bytes("ElementSize", is, "Not a Valid ICNS File", getByteOrder()); // Length + // of + // data + // (4 + // bytes), + // in + // bytes, + // including + // this + // header + final byte[] data = readBytes("Data", is, elementSize - 8, + "Not a Valid ICNS File"); + + return new IcnsElement(type, elementSize, data); + } + + private static class IcnsContents { + public final IcnsHeader icnsHeader; + public final IcnsElement[] icnsElements; + + public IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) { + super(); + this.icnsHeader = icnsHeader; + this.icnsElements = icnsElements; + } + } + + private IcnsContents readImage(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final IcnsHeader icnsHeader = readIcnsHeader(is); + + final List icnsElementList = new ArrayList(); + for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) { + final IcnsElement icnsElement = readIcnsElement(is); + icnsElementList.add(icnsElement); + remainingSize -= icnsElement.elementSize; + } + + final IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()]; + for (int i = 0; i < icnsElements.length; i++) { + icnsElements[i] = icnsElementList.get(i); + } + + final IcnsContents ret = new IcnsContents(icnsHeader, icnsElements); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + final IcnsContents icnsContents = readImage(byteSource); + icnsContents.icnsHeader.dump(pw); + for (final IcnsElement icnsElement : icnsContents.icnsElements) { + icnsElement.dump(pw); + } + return true; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final IcnsContents icnsContents = readImage(byteSource); + final List result = IcnsDecoder + .decodeAllImages(icnsContents.icnsElements); + if (!result.isEmpty()) { + return result.get(0); + } + throw new ImageReadException("No icons in ICNS file"); + } + + @Override + public List getAllBufferedImages(final ByteSource byteSource) + throws ImageReadException, IOException { + final IcnsContents icnsContents = readImage(byteSource); + return IcnsDecoder.decodeAllImages(icnsContents.icnsElements); + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + IcnsType imageType; + if (src.getWidth() == 16 && src.getHeight() == 16) { + imageType = IcnsType.ICNS_16x16_32BIT_IMAGE; + } else if (src.getWidth() == 32 && src.getHeight() == 32) { + imageType = IcnsType.ICNS_32x32_32BIT_IMAGE; + } else if (src.getWidth() == 48 && src.getHeight() == 48) { + imageType = IcnsType.ICNS_48x48_32BIT_IMAGE; + } else if (src.getWidth() == 128 && src.getHeight() == 128) { + imageType = IcnsType.ICNS_128x128_32BIT_IMAGE; + } else { + throw new ImageWriteException("Invalid/unsupported source width " + + src.getWidth() + " and height " + src.getHeight()); + } + + final BinaryOutputStream bos = new BinaryOutputStream(os, + ByteOrder.BIG_ENDIAN); + bos.write4Bytes(ICNS_MAGIC); + bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() + * imageType.getHeight() + 4 + 4 + imageType.getWidth() + * imageType.getHeight()); + + bos.write4Bytes(imageType.getType()); + bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() + * imageType.getHeight()); + for (int y = 0; y < src.getHeight(); y++) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + bos.write(0); + bos.write(argb >> 16); + bos.write(argb >> 8); + bos.write(argb); + } + } + + final IcnsType maskType = IcnsType.find8BPPMaskType(imageType); + bos.write4Bytes(maskType.getType()); + bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth()); + for (int y = 0; y < src.getHeight(); y++) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + bos.write(argb >> 24); + } + } + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java new file mode 100644 index 0000000..6d9a4fd --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/icns/IcnsType.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.icns; + +import java.io.UnsupportedEncodingException; + +enum IcnsType { + + ICNS_16x12_1BIT_IMAGE_AND_MASK("icm#", 16, 12, 1, true), + ICNS_16x12_4BIT_IMAGE("icm4", 16, 12, 4, false), + ICNS_16x12_8BIT_IMAGE("icm8", 16, 12, 8, false), + + ICNS_16x16_8BIT_MASK("s8mk", 16, 16, 8, true), + ICNS_16x16_1BIT_IMAGE_AND_MASK("ics#", 16, 16, 1, true), + ICNS_16x16_4BIT_IMAGE("ics4", 16, 16, 4, false), + ICNS_16x16_8BIT_IMAGE("ics8", 16, 16, 8, false), + ICNS_16x16_32BIT_IMAGE("is32", 16, 16, 32, false), + + ICNS_32x32_8BIT_MASK("l8mk", 32, 32, 8, true), + ICNS_32x32_1BIT_IMAGE_AND_MASK("ICN#", 32, 32, 1, true), + ICNS_32x32_4BIT_IMAGE("icl4", 32, 32, 4, false), + ICNS_32x32_8BIT_IMAGE("icl8", 32, 32, 8, false), + ICNS_32x32_32BIT_IMAGE("il32", 32, 32, 32, false), + + ICNS_48x48_8BIT_MASK("h8mk", 48, 48, 8, true), + ICNS_48x48_1BIT_IMAGE_AND_MASK("ich#", 48, 48, 1, true), + ICNS_48x48_4BIT_IMAGE("ich4", 48, 48, 4, false), + ICNS_48x48_8BIT_IMAGE("ich8", 48, 48, 8, false), + ICNS_48x48_32BIT_IMAGE("ih32", 48, 48, 32, false), + + ICNS_128x128_8BIT_MASK("t8mk", 128, 128, 8, true), + ICNS_128x128_32BIT_IMAGE("it32", 128, 128, 32, false), + + ICNS_256x256_32BIT_ARGB_IMAGE("ic08", 256, 256, 32, false), + + ICNS_512x512_32BIT_ARGB_IMAGE("ic09", 512, 512, 32, false); + + private static final IcnsType[] ALL_IMAGE_TYPES = { + ICNS_16x12_1BIT_IMAGE_AND_MASK, + ICNS_16x12_4BIT_IMAGE, + ICNS_16x12_8BIT_IMAGE, + ICNS_16x16_1BIT_IMAGE_AND_MASK, + ICNS_16x16_4BIT_IMAGE, + ICNS_16x16_8BIT_IMAGE, + ICNS_16x16_32BIT_IMAGE, + ICNS_32x32_1BIT_IMAGE_AND_MASK, + ICNS_32x32_4BIT_IMAGE, + ICNS_32x32_8BIT_IMAGE, + ICNS_32x32_32BIT_IMAGE, + ICNS_48x48_1BIT_IMAGE_AND_MASK, + ICNS_48x48_4BIT_IMAGE, + ICNS_48x48_8BIT_IMAGE, + ICNS_48x48_32BIT_IMAGE, + ICNS_128x128_32BIT_IMAGE, + ICNS_256x256_32BIT_ARGB_IMAGE, + ICNS_512x512_32BIT_ARGB_IMAGE}; + + private static final IcnsType[] ALL_MASK_TYPES = { + ICNS_16x12_1BIT_IMAGE_AND_MASK, + ICNS_16x16_1BIT_IMAGE_AND_MASK, + ICNS_16x16_8BIT_MASK, + ICNS_32x32_1BIT_IMAGE_AND_MASK, + ICNS_32x32_8BIT_MASK, + ICNS_48x48_1BIT_IMAGE_AND_MASK, + ICNS_48x48_8BIT_MASK, + ICNS_128x128_8BIT_MASK}; + + private final int type; + private final int width; + private final int height; + private final int bitsPerPixel; + private final boolean hasMask; + + private IcnsType(String type, int width, int height, int bitsPerPixel, boolean hasMask) { + this.type = typeAsInt(type); + this.width = width; + this.height = height; + this.bitsPerPixel = bitsPerPixel; + this.hasMask = hasMask; + } + + public int getType() { + return type; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getBitsPerPixel() { + return bitsPerPixel; + } + + public boolean hasMask() { + return hasMask; + } + + @Override + public String toString() { + return getClass().getName() + "[" + "width=" + width + "," + "height=" + + height + "," + "bpp=" + bitsPerPixel + "," + "hasMask=" + + hasMask + "]"; + } + + public static IcnsType findAnyType(final int type) { + for (final IcnsType allImageType : ALL_IMAGE_TYPES) { + if (allImageType.getType() == type) { + return allImageType; + } + } + for (final IcnsType allMaskType : ALL_MASK_TYPES) { + if (allMaskType.getType() == type) { + return allMaskType; + } + } + return null; + } + + public static IcnsType findImageType(final int type) { + for (final IcnsType allImageType : ALL_IMAGE_TYPES) { + if (allImageType.getType() == type) { + return allImageType; + } + } + return null; + } + + public static IcnsType find8BPPMaskType(final IcnsType imageType) { + for (final IcnsType allMaskType : ALL_MASK_TYPES) { + if (allMaskType.getBitsPerPixel() == 8 + && allMaskType.getWidth() == imageType.getWidth() + && allMaskType.getHeight() == imageType.getHeight()) { + return allMaskType; + } + } + return null; + } + + public static IcnsType find1BPPMaskType(final IcnsType imageType) { + for (final IcnsType allMaskType : ALL_MASK_TYPES) { + if (allMaskType.getBitsPerPixel() == 1 + && allMaskType.getWidth() == imageType.getWidth() + && allMaskType.getHeight() == imageType.getHeight()) { + return allMaskType; + } + } + return null; + } + + public static int typeAsInt(final String type) { + byte[] bytes; + try { + bytes = type.getBytes("US-ASCII"); + } catch (final UnsupportedEncodingException unsupportedEncodingException) { + throw new IllegalArgumentException("Your Java doesn't support US-ASCII", unsupportedEncodingException); + } + if (bytes.length != 4) { + throw new IllegalArgumentException("Invalid ICNS type"); + } + return ((0xff & bytes[0]) << 24) + | ((0xff & bytes[1]) << 16) + | ((0xff & bytes[2]) << 8) + | (0xff & bytes[3]); + } + + public static String describeType(final int type) { + final byte[] bytes = new byte[4]; + bytes[0] = (byte) (0xff & (type >> 24)); + bytes[1] = (byte) (0xff & (type >> 16)); + bytes[2] = (byte) (0xff & (type >> 8)); + bytes[3] = (byte) (0xff & type); + try { + return new String(bytes, "US-ASCII"); + } catch (final UnsupportedEncodingException unsupportedEncodingException) { + throw new IllegalArgumentException("Your Java doesn't support US-ASCII", unsupportedEncodingException); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/Rle24Compression.java b/src/main/java/org/apache/commons/imaging/formats/icns/Rle24Compression.java new file mode 100644 index 0000000..a7176ea --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/icns/Rle24Compression.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.icns; + +final class Rle24Compression { + private Rle24Compression() { + } + + public static byte[] decompress(final int width, final int height, final byte[] data) { + final int pixelCount = width * height; + final byte[] result = new byte[4 * pixelCount]; + + // Several ICNS parsers advance by 4 bytes here: + // http://code.google.com/p/icns2png/ - when the width is >= 128 + // http://icns.sourceforge.net/ - when those 4 bytes are all zero + // + // A scan of all .icns files on MacOS shows that + // all 128x128 images indeed start with 4 zeroes, + // while all smaller images don't. + // However it is dangerous to assume + // that 4 initial zeroes always need to be skipped, + // because they could encode valid pixels on smaller images. + // So always skip on 128x128, and never skip on anything else. + int dataPos = 0; + if (width >= 128 && height >= 128) { + dataPos = 4; + } + + // argb, band by band in 3 passes, with no alpha + for (int band = 1; band <= 3; band++) { + int remaining = pixelCount; + int resultPos = 0; + while (remaining > 0) { + if ((data[dataPos] & 0x80) != 0) { + final int count = (0xff & data[dataPos]) - 125; + for (int i = 0; i < count; i++) { + result[band + 4 * (resultPos++)] = data[dataPos + 1]; + } + dataPos += 2; + remaining -= count; + } else { + final int count = (0xff & data[dataPos]) + 1; + dataPos++; + for (int i = 0; i < count; i++) { + result[band + 4 * (resultPos++)] = data[dataPos++]; + } + remaining -= count; + } + } + } + return result; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/icns/package-info.java b/src/main/java/org/apache/commons/imaging/formats/icns/package-info.java new file mode 100644 index 0000000..771d436 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/icns/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The ICNS image format. + */ +package org.apache.commons.imaging.formats.icns; + diff --git a/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java b/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java new file mode 100644 index 0000000..9b458f2 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java @@ -0,0 +1,830 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.ico; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.bmp.BmpImageParser; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.palette.SimplePalette; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class IcoImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".ico"; + private static final String[] ACCEPTED_EXTENSIONS = { ".ico", ".cur", }; + + public IcoImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public String getName() { + return "ico-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.ICO, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private static class FileHeader { + public final int reserved; // Reserved (2 bytes), always 0 + public final int iconType; // IconType (2 bytes), if the image is an + // icon it?s 1, for cursors the value is 2. + public final int iconCount; // IconCount (2 bytes), number of icons in + // this file. + + public FileHeader(final int reserved, final int iconType, final int iconCount) { + this.reserved = reserved; + this.iconType = iconType; + this.iconCount = iconCount; + } + + public void dump(final PrintWriter pw) { + pw.println("FileHeader"); + pw.println("Reserved: " + reserved); + pw.println("IconType: " + iconType); + pw.println("IconCount: " + iconCount); + pw.println(); + } + } + + private FileHeader readFileHeader(final InputStream is) throws ImageReadException, IOException { + final int reserved = read2Bytes("Reserved", is, "Not a Valid ICO File", getByteOrder()); + final int iconType = read2Bytes("IconType", is, "Not a Valid ICO File", getByteOrder()); + final int iconCount = read2Bytes("IconCount", is, "Not a Valid ICO File", getByteOrder()); + + if (reserved != 0) { + throw new ImageReadException("Not a Valid ICO File: reserved is " + reserved); + } + if (iconType != 1 && iconType != 2) { + throw new ImageReadException("Not a Valid ICO File: icon type is " + iconType); + } + + return new FileHeader(reserved, iconType, iconCount); + + } + + private static class IconInfo { + public final byte width; + public final byte height; + public final byte colorCount; + public final byte reserved; + public final int planes; + public final int bitCount; + public final int imageSize; + public final int imageOffset; + + public IconInfo(final byte width, final byte height, + final byte colorCount, final byte reserved, final int planes, + final int bitCount, final int imageSize, final int imageOffset) { + this.width = width; + this.height = height; + this.colorCount = colorCount; + this.reserved = reserved; + this.planes = planes; + this.bitCount = bitCount; + this.imageSize = imageSize; + this.imageOffset = imageOffset; + } + + public void dump(final PrintWriter pw) { + pw.println("IconInfo"); + pw.println("Width: " + width); + pw.println("Height: " + height); + pw.println("ColorCount: " + colorCount); + pw.println("Reserved: " + reserved); + pw.println("Planes: " + planes); + pw.println("BitCount: " + bitCount); + pw.println("ImageSize: " + imageSize); + pw.println("ImageOffset: " + imageOffset); + } + } + + private IconInfo readIconInfo(final InputStream is) throws IOException { + // Width (1 byte), Width of Icon (1 to 255) + final byte width = readByte("Width", is, "Not a Valid ICO File"); + // Height (1 byte), Height of Icon (1 to 255) + final byte height = readByte("Height", is, "Not a Valid ICO File"); + // ColorCount (1 byte), Number of colors, either + // 0 for 24 bit or higher, + // 2 for monochrome or 16 for 16 color images. + final byte colorCount = readByte("ColorCount", is, "Not a Valid ICO File"); + // Reserved (1 byte), Not used (always 0) + final byte reserved = readByte("Reserved", is, "Not a Valid ICO File"); + // Planes (2 bytes), always 1 + final int planes = read2Bytes("Planes", is, "Not a Valid ICO File", getByteOrder()); + // BitCount (2 bytes), number of bits per pixel (1 for monchrome, + // 4 for 16 colors, 8 for 256 colors, 24 for true colors, + // 32 for true colors + alpha channel) + final int bitCount = read2Bytes("BitCount", is, "Not a Valid ICO File", getByteOrder()); + // ImageSize (4 bytes), Length of resource in bytes + final int imageSize = read4Bytes("ImageSize", is, "Not a Valid ICO File", getByteOrder()); + // ImageOffset (4 bytes), start of the image in the file + final int imageOffset = read4Bytes("ImageOffset", is, "Not a Valid ICO File", getByteOrder()); + + return new IconInfo(width, height, colorCount, reserved, planes, bitCount, imageSize, imageOffset); + } + + private static class BitmapHeader { + public final int size; + public final int width; + public final int height; + public final int planes; + public final int bitCount; + public final int compression; + public final int sizeImage; + public final int xPelsPerMeter; + public final int yPelsPerMeter; + public final int colorsUsed; + public final int colorsImportant; + + public BitmapHeader(final int size, final int width, final int height, + final int planes, final int bitCount, final int compression, + final int sizeImage, final int pelsPerMeter, + final int pelsPerMeter2, final int colorsUsed, + final int colorsImportant) { + this.size = size; + this.width = width; + this.height = height; + this.planes = planes; + this.bitCount = bitCount; + this.compression = compression; + this.sizeImage = sizeImage; + xPelsPerMeter = pelsPerMeter; + yPelsPerMeter = pelsPerMeter2; + this.colorsUsed = colorsUsed; + this.colorsImportant = colorsImportant; + } + + public void dump(final PrintWriter pw) { + pw.println("BitmapHeader"); + + pw.println("Size: " + size); + pw.println("Width: " + width); + pw.println("Height: " + height); + pw.println("Planes: " + planes); + pw.println("BitCount: " + bitCount); + pw.println("Compression: " + compression); + pw.println("SizeImage: " + sizeImage); + pw.println("XPelsPerMeter: " + xPelsPerMeter); + pw.println("YPelsPerMeter: " + yPelsPerMeter); + pw.println("ColorsUsed: " + colorsUsed); + pw.println("ColorsImportant: " + colorsImportant); + } + } + + private static abstract class IconData { + public final IconInfo iconInfo; + + public IconData(final IconInfo iconInfo) { + this.iconInfo = iconInfo; + } + + public void dump(final PrintWriter pw) { + iconInfo.dump(pw); + pw.println(); + dumpSubclass(pw); + } + + protected abstract void dumpSubclass(PrintWriter pw); + + public abstract BufferedImage readBufferedImage() + throws ImageReadException; + } + + private static class BitmapIconData extends IconData { + public final BitmapHeader header; + public final BufferedImage bufferedImage; + + public BitmapIconData(final IconInfo iconInfo, + final BitmapHeader header, final BufferedImage bufferedImage) { + super(iconInfo); + this.header = header; + this.bufferedImage = bufferedImage; + } + + @Override + public BufferedImage readBufferedImage() throws ImageReadException { + return bufferedImage; + } + + @Override + protected void dumpSubclass(final PrintWriter pw) { + pw.println("BitmapIconData"); + header.dump(pw); + pw.println(); + } + } + + private static class PNGIconData extends IconData { + public final BufferedImage bufferedImage; + + public PNGIconData(final IconInfo iconInfo, + final BufferedImage bufferedImage) { + super(iconInfo); + this.bufferedImage = bufferedImage; + } + + @Override + public BufferedImage readBufferedImage() { + return bufferedImage; + } + + @Override + protected void dumpSubclass(final PrintWriter pw) { + pw.println("PNGIconData"); + pw.println(); + } + } + + private IconData readBitmapIconData(final byte[] iconData, final IconInfo fIconInfo) + throws ImageReadException, IOException { + final ByteArrayInputStream is = new ByteArrayInputStream(iconData); + final int size = read4Bytes("size", is, "Not a Valid ICO File", getByteOrder()); // Size (4 + // bytes), + // size of + // this + // structure + // (always + // 40) + final int width = read4Bytes("width", is, "Not a Valid ICO File", getByteOrder()); // Width (4 + // bytes), + // width of + // the + // image + // (same as + // iconinfo.width) + final int height = read4Bytes("height", is, "Not a Valid ICO File", getByteOrder()); // Height + // (4 + // bytes), + // scanlines + // in the + // color + // map + + // transparent + // map + // (iconinfo.height + // * 2) + final int planes = read2Bytes("planes", is, "Not a Valid ICO File", getByteOrder()); // Planes + // (2 + // bytes), + // always + // 1 + final int bitCount = read2Bytes("bitCount", is, "Not a Valid ICO File", getByteOrder()); // BitCount + // (2 + // bytes), + // 1,4,8,16,24,32 + // (see + // iconinfo + // for + // details) + int compression = read4Bytes("compression", is, "Not a Valid ICO File", getByteOrder()); // Compression + // (4 + // bytes), + // we + // don?t + // use + // this + // (0) + final int sizeImage = read4Bytes("sizeImage", is, "Not a Valid ICO File", getByteOrder()); // SizeImage + // (4 + // bytes), + // we + // don?t + // use + // this + // (0) + final int xPelsPerMeter = read4Bytes("xPelsPerMeter", is, + "Not a Valid ICO File", getByteOrder()); // XPelsPerMeter (4 bytes), we don?t + // use this (0) + final int yPelsPerMeter = read4Bytes("yPelsPerMeter", is, + "Not a Valid ICO File", getByteOrder()); // YPelsPerMeter (4 bytes), we don?t + // use this (0) + final int colorsUsed = read4Bytes("colorsUsed", is, "Not a Valid ICO File", getByteOrder()); // ColorsUsed + // (4 + // bytes), + // we + // don?t + // use + // this + // (0) + final int colorsImportant = read4Bytes("ColorsImportant", is, + "Not a Valid ICO File", getByteOrder()); // ColorsImportant (4 bytes), we don?t + // use this (0) + int redMask = 0; + int greenMask = 0; + int blueMask = 0; + int alphaMask = 0; + if (compression == 3) { + redMask = read4Bytes("redMask", is, "Not a Valid ICO File", getByteOrder()); + greenMask = read4Bytes("greenMask", is, "Not a Valid ICO File", getByteOrder()); + blueMask = read4Bytes("blueMask", is, "Not a Valid ICO File", getByteOrder()); + } + final byte[] restOfFile = readBytes("RestOfFile", is, is.available()); + + if (size != 40) { + throw new ImageReadException("Not a Valid ICO File: Wrong bitmap header size " + size); + } + if (planes != 1) { + throw new ImageReadException("Not a Valid ICO File: Planes can't be " + planes); + } + + if (compression == 0 && bitCount == 32) { + // 32 BPP RGB icons need an alpha channel, but BMP files don't have + // one unless BI_BITFIELDS is used... + compression = 3; + redMask = 0x00ff0000; + greenMask = 0x0000ff00; + blueMask = 0x000000ff; + alphaMask = 0xff000000; + } + + final BitmapHeader header = new BitmapHeader(size, width, height, planes, + bitCount, compression, sizeImage, xPelsPerMeter, yPelsPerMeter, + colorsUsed, colorsImportant); + + final int bitmapPixelsOffset = 14 + 56 + 4 * ((colorsUsed == 0 && bitCount <= 8) ? (1 << bitCount) + : colorsUsed); + final int bitmapSize = 14 + 56 + restOfFile.length; + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmapSize); + BinaryOutputStream bos = null; + boolean canThrow = false; + try { + bos = new BinaryOutputStream(baos, + ByteOrder.LITTLE_ENDIAN); + + bos.write('B'); + bos.write('M'); + bos.write4Bytes(bitmapSize); + bos.write4Bytes(0); + bos.write4Bytes(bitmapPixelsOffset); + + bos.write4Bytes(56); + bos.write4Bytes(width); + bos.write4Bytes(height / 2); + bos.write2Bytes(planes); + bos.write2Bytes(bitCount); + bos.write4Bytes(compression); + bos.write4Bytes(sizeImage); + bos.write4Bytes(xPelsPerMeter); + bos.write4Bytes(yPelsPerMeter); + bos.write4Bytes(colorsUsed); + bos.write4Bytes(colorsImportant); + bos.write4Bytes(redMask); + bos.write4Bytes(greenMask); + bos.write4Bytes(blueMask); + bos.write4Bytes(alphaMask); + bos.write(restOfFile); + bos.flush(); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bos); + } + + final ByteArrayInputStream bmpInputStream = new ByteArrayInputStream(baos.toByteArray()); + final BufferedImage bmpImage = new BmpImageParser().getBufferedImage(bmpInputStream, null); + + // Transparency map is optional with 32 BPP icons, because they already + // have + // an alpha channel, and Windows only uses the transparency map when it + // has to + // display the icon on a < 32 BPP screen. But it's still used instead of + // alpha + // if the image would be completely transparent with alpha... + int t_scanline_size = (width + 7) / 8; + if ((t_scanline_size % 4) != 0) { + t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 + // byte size. + } + final int colorMapSizeBytes = t_scanline_size * (height / 2); + byte[] transparencyMap = null; + try { + transparencyMap = readBytes("transparency_map", + bmpInputStream, colorMapSizeBytes, + "Not a Valid ICO File"); + } catch (final IOException ioEx) { + if (bitCount != 32) { + throw ioEx; + } + } + + boolean allAlphasZero = true; + if (bitCount == 32) { + for (int y = 0; allAlphasZero && y < bmpImage.getHeight(); y++) { + for (int x = 0; x < bmpImage.getWidth(); x++) { + if ((bmpImage.getRGB(x, y) & 0xff000000) != 0) { + allAlphasZero = false; + break; + } + } + } + } + BufferedImage resultImage; + if (allAlphasZero) { + resultImage = new BufferedImage(bmpImage.getWidth(), + bmpImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < resultImage.getHeight(); y++) { + for (int x = 0; x < resultImage.getWidth(); x++) { + int alpha = 0xff; + if (transparencyMap != null) { + final int alphaByte = 0xff & transparencyMap[t_scanline_size + * (bmpImage.getHeight() - y - 1) + (x / 8)]; + alpha = 0x01 & (alphaByte >> (7 - (x % 8))); + alpha = (alpha == 0) ? 0xff : 0x00; + } + resultImage.setRGB(x, y, (alpha << 24) + | (0xffffff & bmpImage.getRGB(x, y))); + } + } + } else { + resultImage = bmpImage; + } + return new BitmapIconData(fIconInfo, header, resultImage); + } + + private IconData readIconData(final byte[] iconData, final IconInfo fIconInfo) + throws ImageReadException, IOException { + final ImageFormat imageFormat = Imaging.guessFormat(iconData); + if (imageFormat.equals(ImageFormats.PNG)) { + final BufferedImage bufferedImage = Imaging.getBufferedImage(iconData); + return new PNGIconData(fIconInfo, bufferedImage); + } + return readBitmapIconData(iconData, fIconInfo); + } + + private static class ImageContents { + public final FileHeader fileHeader; + public final IconData[] iconDatas; + + public ImageContents(final FileHeader fileHeader, + final IconData[] iconDatas) { + super(); + this.fileHeader = fileHeader; + this.iconDatas = iconDatas; + } + } + + private ImageContents readImage(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final FileHeader fileHeader = readFileHeader(is); + + final IconInfo[] fIconInfos = new IconInfo[fileHeader.iconCount]; + for (int i = 0; i < fileHeader.iconCount; i++) { + fIconInfos[i] = readIconInfo(is); + } + + final IconData[] fIconDatas = new IconData[fileHeader.iconCount]; + for (int i = 0; i < fileHeader.iconCount; i++) { + final byte[] iconData = byteSource.getBlock( + fIconInfos[i].imageOffset, fIconInfos[i].imageSize); + fIconDatas[i] = readIconData(iconData, fIconInfos[i]); + } + + final ImageContents ret = new ImageContents(fileHeader, fIconDatas); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + final ImageContents contents = readImage(byteSource); + contents.fileHeader.dump(pw); + for (final IconData iconData : contents.iconDatas) { + iconData.dump(pw); + } + return true; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final ImageContents contents = readImage(byteSource); + final FileHeader fileHeader = contents.fileHeader; + if (fileHeader.iconCount > 0) { + return contents.iconDatas[0].readBufferedImage(); + } + throw new ImageReadException("No icons in ICO file"); + } + + @Override + public List getAllBufferedImages(final ByteSource byteSource) + throws ImageReadException, IOException { + final List result = new ArrayList(); + final ImageContents contents = readImage(byteSource); + + final FileHeader fileHeader = contents.fileHeader; + for (int i = 0; i < fileHeader.iconCount; i++) { + final IconData iconData = contents.iconDatas[i]; + + final BufferedImage image = iconData.readBufferedImage(); + + result.add(image); + } + + return result; + } + + // public boolean extractImages(ByteSource byteSource, File dst_dir, + // String dst_root, ImageParser encoder) throws ImageReadException, + // IOException, ImageWriteException + // { + // ImageContents contents = readImage(byteSource); + // + // FileHeader fileHeader = contents.fileHeader; + // for (int i = 0; i < fileHeader.iconCount; i++) + // { + // IconData iconData = contents.iconDatas[i]; + // + // BufferedImage image = readBufferedImage(iconData); + // + // int size = Math.max(iconData.iconInfo.Width, + // iconData.iconInfo.Height); + // File file = new File(dst_dir, dst_root + "_" + size + "_" + // + iconData.iconInfo.BitCount + // + encoder.getDefaultExtension()); + // encoder.writeImage(image, new FileOutputStream(file), null); + // } + // + // return true; + // } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + final PixelDensity pixelDensity = (PixelDensity) params.remove(PARAM_KEY_PIXEL_DENSITY); + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final PaletteFactory paletteFactory = new PaletteFactory(); + final SimplePalette palette = paletteFactory + .makeExactRgbPaletteSimple(src, 256); + final int bitCount; + final boolean hasTransparency = paletteFactory.hasTransparency(src); + if (palette == null) { + if (hasTransparency) { + bitCount = 32; + } else { + bitCount = 24; + } + } else if (palette.length() <= 2) { + bitCount = 1; + } else if (palette.length() <= 16) { + bitCount = 4; + } else { + bitCount = 8; + } + + final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); + + int scanline_size = (bitCount * src.getWidth() + 7) / 8; + if ((scanline_size % 4) != 0) { + scanline_size += 4 - (scanline_size % 4); // pad scanline to 4 byte + // size. + } + int t_scanline_size = (src.getWidth() + 7) / 8; + if ((t_scanline_size % 4) != 0) { + t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 + // byte size. + } + final int imageSize = 40 + 4 * (bitCount <= 8 ? (1 << bitCount) : 0) + + src.getHeight() * scanline_size + src.getHeight() + * t_scanline_size; + + // ICONDIR + bos.write2Bytes(0); // reserved + bos.write2Bytes(1); // 1=ICO, 2=CUR + bos.write2Bytes(1); // count + + // ICONDIRENTRY + int iconDirEntryWidth = src.getWidth(); + int iconDirEntryHeight = src.getHeight(); + if (iconDirEntryWidth > 255 || iconDirEntryHeight > 255) { + iconDirEntryWidth = 0; + iconDirEntryHeight = 0; + } + bos.write(iconDirEntryWidth); + bos.write(iconDirEntryHeight); + bos.write((bitCount >= 8) ? 0 : (1 << bitCount)); + bos.write(0); // reserved + bos.write2Bytes(1); // color planes + bos.write2Bytes(bitCount); + bos.write4Bytes(imageSize); + bos.write4Bytes(22); // image offset + + // BITMAPINFOHEADER + bos.write4Bytes(40); // size + bos.write4Bytes(src.getWidth()); + bos.write4Bytes(2 * src.getHeight()); + bos.write2Bytes(1); // planes + bos.write2Bytes(bitCount); + bos.write4Bytes(0); // compression + bos.write4Bytes(0); // image size + bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // x pixels per meter + bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // y pixels per meter + bos.write4Bytes(0); // colors used, 0 = (1 << bitCount) (ignored) + bos.write4Bytes(0); // colors important + + if (palette != null) { + for (int i = 0; i < (1 << bitCount); i++) { + if (i < palette.length()) { + final int argb = palette.getEntry(i); + bos.write(0xff & argb); + bos.write(0xff & (argb >> 8)); + bos.write(0xff & (argb >> 16)); + bos.write(0); + } else { + bos.write(0); + bos.write(0); + bos.write(0); + bos.write(0); + } + } + } + + int bitCache = 0; + int bitsInCache = 0; + final int rowPadding = scanline_size - (bitCount * src.getWidth() + 7) / 8; + for (int y = src.getHeight() - 1; y >= 0; y--) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + if (bitCount < 8) { + final int rgb = 0xffffff & argb; + final int index = palette.getPaletteIndex(rgb); + bitCache <<= bitCount; + bitCache |= index; + bitsInCache += bitCount; + if (bitsInCache >= 8) { + bos.write(0xff & bitCache); + bitCache = 0; + bitsInCache = 0; + } + } else if (bitCount == 8) { + final int rgb = 0xffffff & argb; + final int index = palette.getPaletteIndex(rgb); + bos.write(0xff & index); + } else if (bitCount == 24) { + bos.write(0xff & argb); + bos.write(0xff & (argb >> 8)); + bos.write(0xff & (argb >> 16)); + } else if (bitCount == 32) { + bos.write(0xff & argb); + bos.write(0xff & (argb >> 8)); + bos.write(0xff & (argb >> 16)); + bos.write(0xff & (argb >> 24)); + } + } + + if (bitsInCache > 0) { + bitCache <<= (8 - bitsInCache); + bos.write(0xff & bitCache); + bitCache = 0; + bitsInCache = 0; + } + + for (int x = 0; x < rowPadding; x++) { + bos.write(0); + } + } + + final int t_row_padding = t_scanline_size - (src.getWidth() + 7) / 8; + for (int y = src.getHeight() - 1; y >= 0; y--) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + final int alpha = 0xff & (argb >> 24); + bitCache <<= 1; + if (alpha == 0) { + bitCache |= 1; + } + bitsInCache++; + if (bitsInCache >= 8) { + bos.write(0xff & bitCache); + bitCache = 0; + bitsInCache = 0; + } + } + + if (bitsInCache > 0) { + bitCache <<= (8 - bitsInCache); + bos.write(0xff & bitCache); + bitCache = 0; + bitsInCache = 0; + } + + for (int x = 0; x < t_row_padding; x++) { + bos.write(0); + } + } + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/ico/package-info.java b/src/main/java/org/apache/commons/imaging/formats/ico/package-info.java new file mode 100644 index 0000000..b5a03b1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/ico/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The ICO/CUR image formats. + */ +package org.apache.commons.imaging.formats.ico; + diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegConstants.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegConstants.java new file mode 100644 index 0000000..9ae530e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegConstants.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.common.BinaryConstant; +import org.apache.commons.imaging.common.BinaryFunctions; + +public final class JpegConstants { + public static final int MAX_SEGMENT_SIZE = 0xffff; + + public static final BinaryConstant JFIF0_SIGNATURE = new BinaryConstant( + new byte[] { 0x4a, // J + 0x46, // F + 0x49, // I + 0x46, // F + 0x0, // + }); + public static final BinaryConstant JFIF0_SIGNATURE_ALTERNATIVE = new BinaryConstant( + new byte[] { 0x4a, // J + 0x46, // F + 0x49, // I + 0x46, // F + 0x20, // + }); + + public static final BinaryConstant EXIF_IDENTIFIER_CODE = new BinaryConstant( + new byte[] { 0x45, // E + 0x78, // x + 0x69, // i + 0x66, // f + }); + + public static final BinaryConstant XMP_IDENTIFIER = new BinaryConstant( + new byte[] { 0x68, // h + 0x74, // t + 0x74, // t + 0x70, // p + 0x3A, // : + 0x2F, // / + 0x2F, // / + 0x6E, // n + 0x73, // s + 0x2E, // . + 0x61, // a + 0x64, // d + 0x6F, // o + 0x62, // b + 0x65, // e + 0x2E, // . + 0x63, // c + 0x6F, // o + 0x6D, // m + 0x2F, // / + 0x78, // x + 0x61, // a + 0x70, // p + 0x2F, // / + 0x31, // 1 + 0x2E, // . + 0x30, // 0 + 0x2F, // / + 0, // 0-terminated us-ascii string. + }); + + public static final BinaryConstant SOI = new BinaryConstant(new byte[] { + (byte) 0xff, (byte) 0xd8 }); + public static final BinaryConstant EOI = new BinaryConstant(new byte[] { + (byte) 0xff, (byte) 0xd9 }); + + public static final int JPEG_APP0 = 0xE0; + public static final int JPEG_APP0_MARKER = (0xff00) | (JPEG_APP0); + public static final int JPEG_APP1_MARKER = (0xff00) | (JPEG_APP0 + 1); + public static final int JPEG_APP2_MARKER = (0xff00) | (JPEG_APP0 + 2); + public static final int JPEG_APP13_MARKER = (0xff00) | (JPEG_APP0 + 13); + public static final int JPEG_APP14_MARKER = (0xff00) | (JPEG_APP0 + 14); + public static final int JPEG_APP15_MARKER = (0xff00) | (JPEG_APP0 + 15); + + public static final int JFIF_MARKER = 0xFFE0; + public static final int SOF0_MARKER = 0xFFc0; + public static final int SOF1_MARKER = 0xFFc0 + 0x1; + public static final int SOF2_MARKER = 0xFFc0 + 0x2; + public static final int SOF3_MARKER = 0xFFc0 + 0x3; + public static final int DHT_MARKER = 0xFFc0 + 0x4; + public static final int SOF5_MARKER = 0xFFc0 + 0x5; + public static final int SOF6_MARKER = 0xFFc0 + 0x6; + public static final int SOF7_MARKER = 0xFFc0 + 0x7; + public static final int SOF8_MARKER = 0xFFc0 + 0x8; + public static final int SOF9_MARKER = 0xFFc0 + 0x9; + public static final int SOF10_MARKER = 0xFFc0 + 0xa; + public static final int SOF11_MARKER = 0xFFc0 + 0xb; + public static final int DAC_MARKER = 0xFFc0 + 0xc; + public static final int SOF13_MARKER = 0xFFc0 + 0xd; + public static final int SOF14_MARKER = 0xFFc0 + 0xe; + public static final int SOF15_MARKER = 0xFFc0 + 0xf; + + public static final int EOI_MARKER = 0xFFd9; + public static final int SOS_MARKER = 0xFFda; + public static final int DQT_MARKER = 0xFFdb; + public static final int DNL_MARKER = 0xFFdc; + public static final int COM_MARKER = 0xFFfe; + + public static final List MARKERS = Collections + .unmodifiableList(Arrays.asList(JPEG_APP0, JPEG_APP0_MARKER, + JPEG_APP1_MARKER, JPEG_APP2_MARKER, JPEG_APP13_MARKER, + JPEG_APP14_MARKER, JPEG_APP15_MARKER, JFIF_MARKER, + SOF0_MARKER, SOF1_MARKER, SOF2_MARKER, SOF3_MARKER, DHT_MARKER, + SOF5_MARKER, SOF6_MARKER, SOF7_MARKER, SOF8_MARKER, SOF9_MARKER, + SOF10_MARKER, SOF11_MARKER, DAC_MARKER, SOF13_MARKER, + SOF14_MARKER, SOF15_MARKER, EOI_MARKER, SOS_MARKER, DQT_MARKER, + DNL_MARKER, COM_MARKER)); + + public static final BinaryConstant ICC_PROFILE_LABEL = new BinaryConstant( + new byte[] { 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, + 0x4C, 0x45, 0x0 }); + + public static final BinaryConstant PHOTOSHOP_IDENTIFICATION_STRING = new BinaryConstant( + new byte[] { 0x50, // P + 0x68, // h + 0x6F, // o + 0x74, // t + 0x6F, // o + 0x73, // s + 0x68, // h + 0x6F, // o + 0x70, // p + 0x20, // + 0x33, // 3 + 0x2E, // . + 0x30, // 0 + 0, }); + public static final int CONST_8BIM = BinaryFunctions.charsToQuad('8', 'B', 'I', 'M'); + + private JpegConstants() { + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageMetadata.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageMetadata.java new file mode 100644 index 0000000..af1b313 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageMetadata.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.ImagingException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.formats.tiff.JpegImageData; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.util.Debug; + +public class JpegImageMetadata implements IImageMetadata { + private final JpegPhotoshopMetadata photoshop; + private final TiffImageMetadata exif; + private static final String NEWLINE = System.getProperty("line.separator"); + + public JpegImageMetadata(final JpegPhotoshopMetadata photoshop, + final TiffImageMetadata exif) { + this.photoshop = photoshop; + this.exif = exif; + } + + public TiffImageMetadata getExif() { + return exif; + } + + public JpegPhotoshopMetadata getPhotoshop() { + return photoshop; + } + + public TiffField findEXIFValue(final TagInfo tagInfo) { + try { + return exif != null ? exif.findField(tagInfo) : null; + } catch (final ImageReadException cannotHappen) { + return null; + } + } + + public TiffField findEXIFValueWithExactMatch(final TagInfo tagInfo) { + try { + return exif != null ? exif.findField(tagInfo, true) : null; + } catch (final ImageReadException cannotHappen) { + return null; + } + } + + /** + * Returns the size of the first JPEG thumbnail found in the EXIF metadata. + * + * @return Thumbnail width and height or null if no thumbnail. + * @throws ImageReadException + * @throws IOException + */ + public Dimension getEXIFThumbnailSize() throws ImageReadException, + IOException { + final byte[] data = getEXIFThumbnailData(); + + if (data != null) { + return Imaging.getImageSize(data); + } + return null; + } + + /** + * Returns the data of the first JPEG thumbnail found in the EXIF metadata. + * + * @return JPEG data or null if no thumbnail. + * @throws ImageReadException + * @throws IOException + */ + public byte[] getEXIFThumbnailData() throws ImageReadException, IOException { + if (exif == null) { + return null; + } + final List dirs = exif.getDirectories(); + for (IImageMetadataItem d : dirs) { + final TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) d; + + byte[] data = null; + if (dir.getJpegImageData() != null) { + data = dir.getJpegImageData().data; + } + // Support other image formats here. + + if (data != null) { + return data; + } + } + return null; + } + + /** + * Get the thumbnail image if available. + * + * @return the thumbnail image. May be null if no image could + * be found. + * @throws ImageReadException + * @throws IOException + */ + public BufferedImage getEXIFThumbnail() throws ImageReadException, + IOException { + + if (exif == null) { + return null; + } + + final List dirs = exif.getDirectories(); + for (IImageMetadataItem d : dirs) { + final TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) d; + // Debug.debug("dir", dir); + BufferedImage image = dir.getThumbnail(); + if (null != image) { + return image; + } + + final JpegImageData jpegImageData = dir.getJpegImageData(); + if (jpegImageData != null) { + // JPEG thumbnail as JPEG or other format; try to parse. + //boolean imageSucceeded = false; + //try { + image = Imaging.getBufferedImage(jpegImageData.data); + //imageSucceeded = true; + /*} catch (final ImagingException imagingException) { // NOPMD + } catch (final IOException ioException) { // NOPMD + } finally { + // our JPEG reading is still a bit buggy - + // fall back to ImageIO on error + if (!imageSucceeded) { + final ByteArrayInputStream input = new ByteArrayInputStream( + jpegImageData.data); + image = ImageIO.read(input); + } + }*/ + if (image != null) { + return image; + } + } + } + + return null; + } + + public TiffImageData getRawImageData() { + if (exif == null) { + return null; + } + final List dirs = exif.getDirectories(); + for (IImageMetadataItem d : dirs) { + final TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) d; + // Debug.debug("dir", dir); + final TiffImageData rawImageData = dir.getTiffImageData(); + if (null != rawImageData) { + return rawImageData; + } + } + + return null; + } + + public List getItems() { + final List result = new ArrayList(); + + if (null != exif) { + result.addAll(exif.getItems()); + } + + if (null != photoshop) { + result.addAll(photoshop.getItems()); + } + + return result; + } + + @Override + public String toString() { + return toString(null); + } + + public String toString(String prefix) { + if (prefix == null) { + prefix = ""; + } + + final StringBuilder result = new StringBuilder(); + + result.append(prefix); + if (null == exif) { + result.append("No Exif metadata."); + } else { + result.append("Exif metadata:"); + result.append(NEWLINE); + result.append(exif.toString("\t")); + } + + // if (null != exif && null != photoshop) + result.append(NEWLINE); + + result.append(prefix); + if (null == photoshop) { + result.append("No Photoshop (IPTC) metadata."); + } else { + result.append("Photoshop (IPTC) metadata:"); + result.append(NEWLINE); + result.append(photoshop.toString("\t")); + } + + return result.toString(); + } + + public void dump() { + Debug.debug(this.toString()); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java new file mode 100644 index 0000000..cfab48a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java @@ -0,0 +1,1159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.jpeg.decoder.JpegDecoder; +import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser; +import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data; +import org.apache.commons.imaging.formats.jpeg.segments.App13Segment; +import org.apache.commons.imaging.formats.jpeg.segments.App14Segment; +import org.apache.commons.imaging.formats.jpeg.segments.App2Segment; +import org.apache.commons.imaging.formats.jpeg.segments.ComSegment; +import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment; +import org.apache.commons.imaging.formats.jpeg.segments.GenericSegment; +import org.apache.commons.imaging.formats.jpeg.segments.JfifSegment; +import org.apache.commons.imaging.formats.jpeg.segments.Segment; +import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment; +import org.apache.commons.imaging.formats.jpeg.segments.UnknownSegment; +import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpParser; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffImageParser; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.util.Debug; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class JpegImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".jpg"; + private static final String[] ACCEPTED_EXTENSIONS = { ".jpg", ".jpeg", }; + + public JpegImageParser() { + setByteOrder(ByteOrder.BIG_ENDIAN); + // setDebug(true); + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.JPEG, // + }; + } + + @Override + public String getName() { + return "Jpeg-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final JpegDecoder jpegDecoder = new JpegDecoder(); + return jpegDecoder.decode(byteSource); + } + + private boolean keepMarker(final int marker, final int[] markers) { + if (markers == null) { + return true; + } + + for (final int marker2 : markers) { + if (marker2 == marker) { + return true; + } + } + + return false; + } + + public List readSegments(final ByteSource byteSource, + final int[] markers, final boolean returnAfterFirst, + final boolean readEverything) throws ImageReadException, IOException { + final List result = new ArrayList(); + final JpegImageParser parser = this; + final int[] sofnSegments = { + // kJFIFMarker, + JpegConstants.SOF0_MARKER, + JpegConstants.SOF1_MARKER, + JpegConstants.SOF2_MARKER, + JpegConstants.SOF3_MARKER, + JpegConstants.SOF5_MARKER, + JpegConstants.SOF6_MARKER, + JpegConstants.SOF7_MARKER, + JpegConstants.SOF9_MARKER, + JpegConstants.SOF10_MARKER, + JpegConstants.SOF11_MARKER, + JpegConstants.SOF13_MARKER, + JpegConstants.SOF14_MARKER, + JpegConstants.SOF15_MARKER, + }; + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return false; + } + + public void visitSOS(final int marker, final byte[] markerBytes, + final byte[] imageData) { + // don't need image data + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + if (marker == JpegConstants.EOI_MARKER) { + return false; + } + + // Debug.debug("visitSegment marker", marker); + // // Debug.debug("visitSegment keepMarker(marker, markers)", + // keepMarker(marker, markers)); + // Debug.debug("visitSegment keepMarker(marker, markers)", + // keepMarker(marker, markers)); + + if (!keepMarker(marker, markers)) { + return true; + } + + if (marker == JpegConstants.JPEG_APP13_MARKER) { + // Debug.debug("app 13 segment data", segmentData.length); + result.add(new App13Segment(parser, marker, segmentData)); + } else if (marker == JpegConstants.JPEG_APP14_MARKER) { + result.add(new App14Segment(marker, segmentData)); + } else if (marker == JpegConstants.JPEG_APP2_MARKER) { + result.add(new App2Segment(marker, segmentData)); + } else if (marker == JpegConstants.JFIF_MARKER) { + result.add(new JfifSegment(marker, segmentData)); + } else if (Arrays.binarySearch(sofnSegments, marker) >= 0) { + result.add(new SofnSegment(marker, segmentData)); + } else if (marker == JpegConstants.DQT_MARKER) { + result.add(new DqtSegment(marker, segmentData)); + } else if ((marker >= JpegConstants.JPEG_APP1_MARKER) + && (marker <= JpegConstants.JPEG_APP15_MARKER)) { + result.add(new UnknownSegment(marker, segmentData)); + } else if (marker == JpegConstants.COM_MARKER) { + result.add(new ComSegment(marker, segmentData)); + } + + if (returnAfterFirst) { + return false; + } + + return true; + } + }; + + new JpegUtils().traverseJFIF(byteSource, visitor); + + return result; + } + + private byte[] assembleSegments(List segments) throws ImageReadException { + try { + return assembleSegments(segments, false); + } catch (ImageReadException e) { + return assembleSegments(segments, true); + } + } + + private byte[] assembleSegments(final List segments, final boolean startWithZero) + throws ImageReadException { + if (segments.isEmpty()) { + throw new ImageReadException("No App2 Segments Found."); + } + + final int markerCount = segments.get(0).numMarkers; + + if (segments.size() != markerCount) { + throw new ImageReadException("App2 Segments Missing. Found: " + + segments.size() + ", Expected: " + markerCount + "."); + } + + Collections.sort(segments); + + final int offset = startWithZero ? 0 : 1; + + int total = 0; + for (int i = 0; i < segments.size(); i++) { + final App2Segment segment = segments.get(i); + + if ((i + offset) != segment.curMarker) { + dumpSegments(segments); + throw new ImageReadException( + "Incoherent App2 Segment Ordering. i: " + i + + ", segment[" + i + "].curMarker: " + + segment.curMarker + "."); + } + + if (markerCount != segment.numMarkers) { + dumpSegments(segments); + throw new ImageReadException( + "Inconsistent App2 Segment Count info. markerCount: " + + markerCount + ", segment[" + i + + "].numMarkers: " + segment.numMarkers + "."); + } + + total += segment.iccBytes.length; + } + + final byte[] result = new byte[total]; + int progress = 0; + + for (App2Segment segment : segments) { + System.arraycopy(segment.iccBytes, 0, result, progress, segment.iccBytes.length); + progress += segment.iccBytes.length; + } + + return result; + } + + private void dumpSegments(final List v) { + Debug.debug(); + Debug.debug("dumpSegments: " + v.size()); + + for (int i = 0; i < v.size(); i++) { + final App2Segment segment = (App2Segment) v.get(i); + + Debug.debug(i + ": " + segment.curMarker + " / " + segment.numMarkers); + } + Debug.debug(); + } + + public List readSegments(final ByteSource byteSource, final int[] markers, + final boolean returnAfterFirst) throws ImageReadException, IOException { + return readSegments(byteSource, markers, returnAfterFirst, false); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List segments = readSegments(byteSource, + new int[] { JpegConstants.JPEG_APP2_MARKER, }, false); + + final List filtered = new ArrayList(); + if (segments != null) { + // throw away non-icc profile app2 segments. + for (Segment s : segments) { + final App2Segment segment = (App2Segment) s; + if (segment.iccBytes != null) { + filtered.add(segment); + } + } + } + + if (filtered.isEmpty()) { + return null; + } + + final byte[] bytes = assembleSegments(filtered); + + if (getDebug()) { + System.out.println("bytes" + ": " + bytes.length); + } + + if (getDebug()) { + System.out.println(""); + } + + return bytes; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final TiffImageMetadata exif = getExifMetadata(byteSource, params); + + final JpegPhotoshopMetadata photoshop = getPhotoshopMetadata(byteSource, + params); + + if (null == exif && null == photoshop) { + return null; + } + + return new JpegImageMetadata(photoshop, exif); + } + + public static boolean isExifAPP1Segment(final GenericSegment segment) { + return startsWith(segment.getSegmentData(), JpegConstants.EXIF_IDENTIFIER_CODE); + } + + private List filterAPP1Segments(final List segments) { + final List result = new ArrayList(); + + for (Segment s : segments) { + final GenericSegment segment = (GenericSegment) s; + if (isExifAPP1Segment(segment)) { + result.add(segment); + } + } + + return result; + } + + public TiffImageMetadata getExifMetadata(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + final byte[] bytes = getExifRawData(byteSource); + if (null == bytes) { + return null; + } + + if (params == null) { + params = new HashMap(); + } + if (!params.containsKey(PARAM_KEY_READ_THUMBNAILS)) { + params.put(PARAM_KEY_READ_THUMBNAILS, Boolean.TRUE); + } + + return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, + params); + } + + public byte[] getExifRawData(final ByteSource byteSource) + throws ImageReadException, IOException { + final List segments = readSegments(byteSource, + new int[] { JpegConstants.JPEG_APP1_MARKER, }, false); + + if ((segments == null) || (segments.isEmpty())) { + return null; + } + + final List exifSegments = filterAPP1Segments(segments); + if (getDebug()) { + System.out.println("exif_segments.size" + ": " + + exifSegments.size()); + } + + // Debug.debug("segments", segments); + // Debug.debug("exifSegments", exifSegments); + + // TODO: concatenate if multiple segments, need example. + if (exifSegments.isEmpty()) { + return null; + } + if (exifSegments.size() > 1) { + throw new ImageReadException( + "Imaging currently can't parse EXIF metadata split across multiple APP1 segments. " + + "Please send this image to the Imaging project."); + } + + final GenericSegment segment = (GenericSegment) exifSegments.get(0); + final byte[] bytes = segment.getSegmentData(); + + // byte head[] = readBytearray("exif head", bytes, 0, 6); + // + // Debug.debug("head", head); + + return remainingBytes("trimmed exif bytes", bytes, 6); + } + + public boolean hasExifSegment(final ByteSource byteSource) + throws ImageReadException, IOException { + final boolean[] result = { false, }; + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return false; + } + + public void visitSOS(final int marker, final byte[] markerBytes, + final byte[] imageData) { + // don't need image data + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + if (marker == 0xffd9) { + return false; + } + + if (marker == JpegConstants.JPEG_APP1_MARKER) { + if (startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) { + result[0] = true; + return false; + } + } + + return true; + } + }; + + new JpegUtils().traverseJFIF(byteSource, visitor); + + return result[0]; + } + + public boolean hasIptcSegment(final ByteSource byteSource) + throws ImageReadException, IOException { + final boolean[] result = { false, }; + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return false; + } + + public void visitSOS(final int marker, final byte[] markerBytes, + final byte[] imageData) { + // don't need image data + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + if (marker == 0xffd9) { + return false; + } + + if (marker == JpegConstants.JPEG_APP13_MARKER) { + if (new IptcParser().isPhotoshopJpegSegment(segmentData)) { + result[0] = true; + return false; + } + } + + return true; + } + }; + + new JpegUtils().traverseJFIF(byteSource, visitor); + + return result[0]; + } + + public boolean hasXmpSegment(final ByteSource byteSource) + throws ImageReadException, IOException { + final boolean[] result = { false, }; + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return false; + } + + public void visitSOS(final int marker, final byte[] markerBytes, + final byte[] imageData) { + // don't need image data + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + if (marker == 0xffd9) { + return false; + } + + if (marker == JpegConstants.JPEG_APP1_MARKER) { + if (new JpegXmpParser().isXmpJpegSegment(segmentData)) { + result[0] = true; + return false; + } + } + + return true; + } + }; + new JpegUtils().traverseJFIF(byteSource, visitor); + + return result[0]; + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + + final List result = new ArrayList(); + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return false; + } + + public void visitSOS(final int marker, final byte[] markerBytes, + final byte[] imageData) { + // don't need image data + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + if (marker == 0xffd9) { + return false; + } + + if (marker == JpegConstants.JPEG_APP1_MARKER) { + if (new JpegXmpParser().isXmpJpegSegment(segmentData)) { + result.add(new JpegXmpParser() + .parseXmpJpegSegment(segmentData)); + return false; + } + } + + return true; + } + }; + new JpegUtils().traverseJFIF(byteSource, visitor); + + if (result.isEmpty()) { + return null; + } + if (result.size() > 1) { + throw new ImageReadException( + "Jpeg file contains more than one XMP segment."); + } + return result.get(0); + } + + public JpegPhotoshopMetadata getPhotoshopMetadata(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final List segments = readSegments(byteSource, + new int[] { JpegConstants.JPEG_APP13_MARKER, }, false); + + if ((segments == null) || (segments.isEmpty())) { + return null; + } + + PhotoshopApp13Data photoshopApp13Data = null; + + for (Segment s : segments) { + final App13Segment segment = (App13Segment) s; + + final PhotoshopApp13Data data = segment.parsePhotoshopSegment(params); + if (data != null && photoshopApp13Data != null) { + throw new ImageReadException( + "Jpeg contains more than one Photoshop App13 segment."); + } + + photoshopApp13Data = data; + } + + if (null == photoshopApp13Data) { + return null; + } + return new JpegPhotoshopMetadata(photoshopApp13Data); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List segments = readSegments(byteSource, new int[] { + // kJFIFMarker, + JpegConstants.SOF0_MARKER, + JpegConstants.SOF1_MARKER, + JpegConstants.SOF2_MARKER, + JpegConstants.SOF3_MARKER, + JpegConstants.SOF5_MARKER, + JpegConstants.SOF6_MARKER, + JpegConstants.SOF7_MARKER, + JpegConstants.SOF9_MARKER, + JpegConstants.SOF10_MARKER, + JpegConstants.SOF11_MARKER, + JpegConstants.SOF13_MARKER, + JpegConstants.SOF14_MARKER, + JpegConstants.SOF15_MARKER, + + }, true); + + if ((segments == null) || (segments.isEmpty())) { + throw new ImageReadException("No JFIF Data Found."); + } + + if (segments.size() > 1) { + throw new ImageReadException("Redundant JFIF Data Found."); + } + + final SofnSegment fSOFNSegment = (SofnSegment) segments.get(0); + + return new Dimension(fSOFNSegment.width, fSOFNSegment.height); + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + // List allSegments = readSegments(byteSource, null, false); + + final List SOF_segments = readSegments(byteSource, new int[] { + // kJFIFMarker, + + JpegConstants.SOF0_MARKER, + JpegConstants.SOF1_MARKER, + JpegConstants.SOF2_MARKER, + JpegConstants.SOF3_MARKER, + JpegConstants.SOF5_MARKER, + JpegConstants.SOF6_MARKER, + JpegConstants.SOF7_MARKER, + JpegConstants.SOF9_MARKER, + JpegConstants.SOF10_MARKER, + JpegConstants.SOF11_MARKER, + JpegConstants.SOF13_MARKER, + JpegConstants.SOF14_MARKER, + JpegConstants.SOF15_MARKER, + + }, false); + + if (SOF_segments == null) { + throw new ImageReadException("No SOFN Data Found."); + } + + // if (SOF_segments.size() != 1) + // System.out.println("Incoherent SOFN Data Found: " + // + SOF_segments.size()); + + final List jfifSegments = readSegments(byteSource, + new int[] { JpegConstants.JFIF_MARKER, }, true); + + final SofnSegment fSOFNSegment = (SofnSegment) SOF_segments.get(0); + // SofnSegment fSOFNSegment = (SofnSegment) findSegment(segments, + // SOFNmarkers); + + if (fSOFNSegment == null) { + throw new ImageReadException("No SOFN Data Found."); + } + + final int width = fSOFNSegment.width; + final int height = fSOFNSegment.height; + + JfifSegment jfifSegment = null; + + if ((jfifSegments != null) && (!jfifSegments.isEmpty())) { + jfifSegment = (JfifSegment) jfifSegments.get(0); + } + + final List app14Segments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP14_MARKER}, true); + App14Segment app14Segment = null; + if (app14Segments != null && !app14Segments.isEmpty()) { + app14Segment = (App14Segment) app14Segments.get(0); + } + + // JfifSegment fTheJFIFSegment = (JfifSegment) findSegment(segments, + // kJFIFMarker); + + double xDensity = -1.0; + double yDensity = -1.0; + double unitsPerInch = -1.0; + // int JFIF_major_version; + // int JFIF_minor_version; + String formatDetails; + + if (jfifSegment != null) { + xDensity = jfifSegment.xDensity; + yDensity = jfifSegment.yDensity; + final int densityUnits = jfifSegment.densityUnits; + // JFIF_major_version = fTheJFIFSegment.JFIF_major_version; + // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version; + + formatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + "." + + jfifSegment.jfifMinorVersion; + + switch (densityUnits) { + case 0: + break; + case 1: // inches + unitsPerInch = 1.0; + break; + case 2: // cms + unitsPerInch = 2.54; + break; + default: + break; + } + } else { + final JpegImageMetadata metadata = (JpegImageMetadata) getMetadata( + byteSource, params); + + if (metadata != null) { + { + final TiffField field = metadata + .findEXIFValue(TiffTagConstants.TIFF_TAG_XRESOLUTION); + if (field != null) { + xDensity = ((Number) field.getValue()).doubleValue(); + } + } + { + final TiffField field = metadata + .findEXIFValue(TiffTagConstants.TIFF_TAG_YRESOLUTION); + if (field != null) { + yDensity = ((Number) field.getValue()).doubleValue(); + } + } + { + final TiffField field = metadata + .findEXIFValue(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT); + if (field != null) { + final int densityUnits = ((Number) field.getValue()).intValue(); + + switch (densityUnits) { + case 1: + break; + case 2: // inches + unitsPerInch = 1.0; + break; + case 3: // cms + unitsPerInch = 2.54; + break; + default: + break; + } + } + + } + } + + formatDetails = "Jpeg/DCM"; + + } + + int physicalHeightDpi = -1; + float physicalHeightInch = -1; + int physicalWidthDpi = -1; + float physicalWidthInch = -1; + + if (unitsPerInch > 0) { + physicalWidthDpi = (int) Math.round(xDensity * unitsPerInch); + physicalWidthInch = (float) (width / (xDensity * unitsPerInch)); + physicalHeightDpi = (int) Math.round(yDensity * unitsPerInch); + physicalHeightInch = (float) (height / (yDensity * unitsPerInch)); + } + + final List comments = new ArrayList(); + final List commentSegments = readSegments(byteSource, + new int[] { JpegConstants.COM_MARKER}, false); + for (Segment commentSegment : commentSegments) { + final ComSegment comSegment = (ComSegment) commentSegment; + String comment = ""; + try { + comment = new String(comSegment.getComment(), "UTF-8"); + } catch (final UnsupportedEncodingException cannotHappen) { // NOPMD - can't happen + } + comments.add(comment); + } + + final int numberOfComponents = fSOFNSegment.numberOfComponents; + final int precision = fSOFNSegment.precision; + + final int bitsPerPixel = numberOfComponents * precision; + final ImageFormat format = ImageFormats.JPEG; + final String formatName = "JPEG (Joint Photographic Experts Group) Format"; + final String mimeType = "image/jpeg"; + // we ought to count images, but don't yet. + final int numberOfImages = 1; + // not accurate ... only reflects first + final boolean progressive = fSOFNSegment.marker == JpegConstants.SOF2_MARKER; + + boolean transparent = false; + final boolean usesPalette = false; // TODO: inaccurate. + + // See http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color + int colorType = ImageInfo.COLOR_TYPE_UNKNOWN; + // Some images have both JFIF/APP0 and APP14. + // JFIF is meant to win but in them APP14 is clearly right, so make it win. + if (app14Segment != null && app14Segment.isAdobeJpegSegment()) { + final int colorTransform = app14Segment.getAdobeColorTransform(); + if (colorTransform == App14Segment.ADOBE_COLOR_TRANSFORM_UNKNOWN) { + if (numberOfComponents == 3) { + colorType = ImageInfo.COLOR_TYPE_RGB; + } else if (numberOfComponents == 4) { + colorType = ImageInfo.COLOR_TYPE_CMYK; + } + } else if (colorTransform == App14Segment.ADOBE_COLOR_TRANSFORM_YCbCr) { + colorType = ImageInfo.COLOR_TYPE_YCbCr; + } else if (colorTransform == App14Segment.ADOBE_COLOR_TRANSFORM_YCCK) { + colorType = ImageInfo.COLOR_TYPE_YCCK; + } + } else if (jfifSegment != null) { + if (numberOfComponents == 1) { + colorType = ImageInfo.COLOR_TYPE_GRAYSCALE; + } else if (numberOfComponents == 3) { + colorType = ImageInfo.COLOR_TYPE_YCbCr; + } + } else { + if (numberOfComponents == 1) { + colorType = ImageInfo.COLOR_TYPE_GRAYSCALE; + } else if (numberOfComponents == 2) { + colorType = ImageInfo.COLOR_TYPE_GRAYSCALE; + transparent = true; + } else if (numberOfComponents == 3 || numberOfComponents == 4) { + boolean have1 = false; + boolean have2 = false; + boolean have3 = false; + boolean have4 = false; + boolean haveOther = false; + for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { + final int id = component.componentIdentifier; + if (id == 1) { + have1 = true; + } else if (id == 2) { + have2 = true; + } else if (id == 3) { + have3 = true; + } else if (id == 4) { + have4 = true; + } else { + haveOther = true; + } + } + if (numberOfComponents == 3 && have1 && have2 && have3 && !have4 && !haveOther) { + colorType = ImageInfo.COLOR_TYPE_YCbCr; + } else if (numberOfComponents == 4 && have1 && have2 && have3 && have4 && !haveOther) { + colorType = ImageInfo.COLOR_TYPE_YCbCr; + transparent = true; + } else { + boolean haveR = false; + boolean haveG = false; + boolean haveB = false; + boolean haveA = false; + boolean haveC = false; + boolean havec = false; + boolean haveY = false; + for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { + final int id = component.componentIdentifier; + if (id == 'R') { + haveR = true; + } else if (id == 'G') { + haveG = true; + } else if (id == 'B') { + haveB = true; + } else if (id == 'A') { + haveA = true; + } else if (id == 'C') { + haveC = true; + } else if (id == 'c') { + havec = true; + } else if (id == 'Y') { + haveY = true; + } + } + if (haveR && haveG && haveB && !haveA && !haveC && !havec && !haveY) { + colorType = ImageInfo.COLOR_TYPE_RGB; + } else if (haveR && haveG && haveB && haveA && !haveC && !havec && !haveY) { + colorType = ImageInfo.COLOR_TYPE_RGB; + transparent = true; + } else if (haveY && haveC && havec && !haveR && !haveG && !haveB && !haveA) { + colorType = ImageInfo.COLOR_TYPE_YCC; + } else if (haveY && haveC && havec && haveA && !haveR && !haveG && !haveB) { + colorType = ImageInfo.COLOR_TYPE_YCC; + transparent = true; + } else { + int minHorizontalSamplingFactor = Integer.MAX_VALUE; + int maxHorizontalSmaplingFactor = Integer.MIN_VALUE; + int minVerticalSamplingFactor = Integer.MAX_VALUE; + int maxVerticalSamplingFactor = Integer.MIN_VALUE; + for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { + if (minHorizontalSamplingFactor > component.horizontalSamplingFactor) { + minHorizontalSamplingFactor = component.horizontalSamplingFactor; + } + if (maxHorizontalSmaplingFactor < component.horizontalSamplingFactor) { + maxHorizontalSmaplingFactor = component.horizontalSamplingFactor; + } + if (minVerticalSamplingFactor > component.verticalSamplingFactor) { + minVerticalSamplingFactor = component.verticalSamplingFactor; + } + if (maxVerticalSamplingFactor < component.verticalSamplingFactor) { + maxVerticalSamplingFactor = component.verticalSamplingFactor; + } + } + final boolean isSubsampled = (minHorizontalSamplingFactor != maxHorizontalSmaplingFactor) + || (minVerticalSamplingFactor != maxVerticalSamplingFactor); + if (numberOfComponents == 3) { + if (isSubsampled) { + colorType = ImageInfo.COLOR_TYPE_YCbCr; + } else { + colorType = ImageInfo.COLOR_TYPE_RGB; + } + } else if (numberOfComponents == 4) { + if (isSubsampled) { + colorType = ImageInfo.COLOR_TYPE_YCCK; + } else { + colorType = ImageInfo.COLOR_TYPE_CMYK; + } + } + } + } + } + } + + final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; + + return new ImageInfo(formatDetails, bitsPerPixel, comments, + format, formatName, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + } + + // public ImageInfo getImageInfo(ByteSource byteSource, Map params) + // throws ImageReadException, IOException + // { + // + // List allSegments = readSegments(byteSource, null, false); + // + // final int SOF_MARKERS[] = new int[]{ + // SOF0_MARKER, SOF1_MARKER, SOF2_MARKER, SOF3_MARKER, SOF5_MARKER, + // SOF6_MARKER, SOF7_MARKER, SOF9_MARKER, SOF10_MARKER, SOF11_MARKER, + // SOF13_MARKER, SOF14_MARKER, SOF15_MARKER, + // }; + // + // List sofMarkers = new ArrayList(); + // for(int i=0;i 0) + // jfifSegment = (JfifSegment) jfifSegments.get(0); + // + // double x_density = -1.0; + // double y_density = -1.0; + // double units_per_inch = -1.0; + // // int JFIF_major_version; + // // int JFIF_minor_version; + // String FormatDetails; + // + // if (jfifSegment != null) + // { + // x_density = jfifSegment.xDensity; + // y_density = jfifSegment.yDensity; + // int density_units = jfifSegment.densityUnits; + // // JFIF_major_version = fTheJFIFSegment.JFIF_major_version; + // // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version; + // + // FormatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + // + "." + jfifSegment.jfifMinorVersion; + // + // switch (density_units) + // { + // case 0 : + // break; + // case 1 : // inches + // units_per_inch = 1.0; + // break; + // case 2 : // cms + // units_per_inch = 2.54; + // break; + // default : + // break; + // } + // } + // else + // { + // JpegImageMetadata metadata = (JpegImageMetadata) getMetadata(byteSource, + // params); + // + // { + // TiffField field = metadata + // .findEXIFValue(TiffField.TIFF_TAG_XRESOLUTION); + // if (field == null) + // throw new ImageReadException("No XResolution"); + // + // x_density = ((Number) field.getValue()).doubleValue(); + // } + // { + // TiffField field = metadata + // .findEXIFValue(TiffField.TIFF_TAG_YRESOLUTION); + // if (field == null) + // throw new ImageReadException("No YResolution"); + // + // y_density = ((Number) field.getValue()).doubleValue(); + // } + // { + // TiffField field = metadata + // .findEXIFValue(TiffField.TIFF_TAG_RESOLUTION_UNIT); + // if (field == null) + // throw new ImageReadException("No ResolutionUnits"); + // + // int density_units = ((Number) field.getValue()).intValue(); + // + // switch (density_units) + // { + // case 1 : + // break; + // case 2 : // inches + // units_per_inch = 1.0; + // break; + // case 3 : // cms + // units_per_inch = 2.54; + // break; + // default : + // break; + // } + // + // } + // + // FormatDetails = "Jpeg/DCM"; + // + // } + // + // int PhysicalHeightDpi = -1; + // float PhysicalHeightInch = -1; + // int PhysicalWidthDpi = -1; + // float PhysicalWidthInch = -1; + // + // if (units_per_inch > 0) + // { + // PhysicalWidthDpi = (int) Math.round((double) x_density + // / units_per_inch); + // PhysicalWidthInch = (float) ((double) Width / (x_density * + // units_per_inch)); + // PhysicalHeightDpi = (int) Math.round((double) y_density + // * units_per_inch); + // PhysicalHeightInch = (float) ((double) Height / (y_density * + // units_per_inch)); + // } + // + // List Comments = new ArrayList(); + // // TODO: comments... + // + // int Number_of_components = firstSOFNSegment.numberOfComponents; + // int Precision = firstSOFNSegment.precision; + // + // int BitsPerPixel = Number_of_components * Precision; + // ImageFormat Format = ImageFormat.IMAGE_FORMAT_JPEG; + // String FormatName = "JPEG (Joint Photographic Experts Group) Format"; + // String MimeType = "image/jpeg"; + // // we ought to count images, but don't yet. + // int NumberOfImages = -1; + // // not accurate ... only reflects first + // boolean progressive = firstSOFNSegment.marker == SOF2_MARKER; + // + // boolean transparent = false; // TODO: inaccurate. + // boolean usesPalette = false; // TODO: inaccurate. + // int ColorType; + // if (Number_of_components == 1) + // ColorType = ImageInfo.COLOR_TYPE_BW; + // else if (Number_of_components == 3) + // ColorType = ImageInfo.COLOR_TYPE_RGB; + // else if (Number_of_components == 4) + // ColorType = ImageInfo.COLOR_TYPE_CMYK; + // else + // ColorType = ImageInfo.COLOR_TYPE_UNKNOWN; + // + // String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; + // + // ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, + // Format, FormatName, Height, MimeType, NumberOfImages, + // PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, + // PhysicalWidthInch, Width, progressive, transparent, + // usesPalette, ColorType, compressionAlgorithm); + // + // return result; + // } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + pw.println("jpeg.dumpImageFile"); + + { + final ImageInfo imageInfo = getImageInfo(byteSource); + if (imageInfo == null) { + return false; + } + + imageInfo.toString(pw, ""); + } + + pw.println(""); + + { + final List segments = readSegments(byteSource, null, false); + + if (segments == null) { + throw new ImageReadException("No Segments Found."); + } + + for (int d = 0; d < segments.size(); d++) { + + final Segment segment = segments.get(d); + + final NumberFormat nf = NumberFormat.getIntegerInstance(); + // this.debugNumber("found, marker: ", marker, 4); + pw.println(d + ": marker: " + + Integer.toHexString(segment.marker) + ", " + + segment.getDescription() + " (length: " + + nf.format(segment.length) + ")"); + segment.dump(pw); + } + + pw.println(""); + } + + return true; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/JpegPhotoshopMetadata.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegPhotoshopMetadata.java similarity index 58% rename from src/main/java/org/apache/sanselan/formats/jpeg/JpegPhotoshopMetadata.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/JpegPhotoshopMetadata.java index b582268..3ef26d5 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/JpegPhotoshopMetadata.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegPhotoshopMetadata.java @@ -1,53 +1,48 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg; - -import java.util.Collections; -import java.util.List; - -import org.apache.sanselan.common.ImageMetadata; -import org.apache.sanselan.formats.jpeg.iptc.IPTCConstants; -import org.apache.sanselan.formats.jpeg.iptc.IPTCRecord; -import org.apache.sanselan.formats.jpeg.iptc.PhotoshopApp13Data; -import org.apache.sanselan.util.Debug; - -public class JpegPhotoshopMetadata extends ImageMetadata implements - IPTCConstants -{ - - public final PhotoshopApp13Data photoshopApp13Data; - - public JpegPhotoshopMetadata(final PhotoshopApp13Data photoshopApp13Data) - { - this.photoshopApp13Data = photoshopApp13Data; - - List records = photoshopApp13Data.getRecords(); - Collections.sort(records, IPTCRecord.COMPARATOR); - for (int j = 0; j < records.size(); j++) - { - IPTCRecord element = (IPTCRecord) records.get(j); - if (element.iptcType.type != IPTC_TYPE_RECORD_VERSION.type) - add(element.getIptcTypeName(), element.getValue()); - } - } - - public void dump() - { - Debug.debug(this.toString()); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.jpeg.iptc.IptcRecord; +import org.apache.commons.imaging.formats.jpeg.iptc.IptcTypes; +import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data; +import org.apache.commons.imaging.util.Debug; + +public class JpegPhotoshopMetadata extends ImageMetadata { + + public final PhotoshopApp13Data photoshopApp13Data; + + public JpegPhotoshopMetadata(final PhotoshopApp13Data photoshopApp13Data) { + this.photoshopApp13Data = photoshopApp13Data; + + final List records = photoshopApp13Data.getRecords(); + Collections.sort(records, IptcRecord.COMPARATOR); + for (IptcRecord element : records) { + if (element.iptcType != IptcTypes.RECORD_VERSION) { + add(element.getIptcTypeName(), element.getValue()); + } + } + } + + public void dump() { + Debug.debug(this.toString()); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegUtils.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegUtils.java new file mode 100644 index 0000000..e8b674b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegUtils.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.Debug; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class JpegUtils extends BinaryFileParser { + public JpegUtils() { + setByteOrder(ByteOrder.BIG_ENDIAN); + } + + public interface Visitor { + // return false to exit before reading image data. + boolean beginSOS(); + + void visitSOS(int marker, byte[] markerBytes, byte[] imageData); + + // return false to exit traversal. + boolean visitSegment(int marker, byte[] markerBytes, + int segmentLength, byte[] segmentLengthBytes, + byte[] segmentData) throws ImageReadException, + IOException; + } + + public void traverseJFIF(final ByteSource byteSource, final Visitor visitor) + throws ImageReadException, + IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + readAndVerifyBytes(is, JpegConstants.SOI, + "Not a Valid JPEG File: doesn't begin with 0xffd8"); + + int markerCount; + for (markerCount = 0; true; markerCount++) { + final byte[] markerBytes = new byte[2]; + do { + markerBytes[0] = markerBytes[1]; + markerBytes[1] = readByte("marker", is, + "Could not read marker"); + } while ((0xff & markerBytes[0]) != 0xff + || (0xff & markerBytes[1]) == 0xff); + final int marker = ((0xff & markerBytes[0]) << 8) + | (0xff & markerBytes[1]); + + if (marker == JpegConstants.EOI_MARKER || marker == JpegConstants.SOS_MARKER) { + if (!visitor.beginSOS()) { + canThrow = true; + return; + } + + final byte[] imageData = getStreamBytes(is); + visitor.visitSOS(marker, markerBytes, imageData); + break; + } + + final byte[] segmentLengthBytes = readBytes("segmentLengthBytes", is, 2, "segmentLengthBytes"); + final int segmentLength = ByteConversions.toUInt16(segmentLengthBytes, getByteOrder()); + + final byte[] segmentData = readBytes("Segment Data", + is, segmentLength - 2, + "Invalid Segment: insufficient data"); + + if (!visitor.visitSegment(marker, markerBytes, segmentLength, segmentLengthBytes, segmentData)) { + canThrow = true; + return; + } + } + + Debug.debug(Integer.toString(markerCount) + " markers"); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + public static String getMarkerName(final int marker) { + switch (marker) { + case JpegConstants.SOS_MARKER: + return "SOS_MARKER"; + // case JPEG_APP0 : + // return "JPEG_APP0"; + // case JPEG_APP0_MARKER : + // return "JPEG_APP0_MARKER"; + case JpegConstants.JPEG_APP1_MARKER: + return "JPEG_APP1_MARKER"; + case JpegConstants.JPEG_APP2_MARKER: + return "JPEG_APP2_MARKER"; + case JpegConstants.JPEG_APP13_MARKER: + return "JPEG_APP13_MARKER"; + case JpegConstants.JPEG_APP14_MARKER: + return "JPEG_APP14_MARKER"; + case JpegConstants.JPEG_APP15_MARKER: + return "JPEG_APP15_MARKER"; + case JpegConstants.JFIF_MARKER: + return "JFIF_MARKER"; + case JpegConstants.SOF0_MARKER: + return "SOF0_MARKER"; + case JpegConstants.SOF1_MARKER: + return "SOF1_MARKER"; + case JpegConstants.SOF2_MARKER: + return "SOF2_MARKER"; + case JpegConstants.SOF3_MARKER: + return "SOF3_MARKER"; + case JpegConstants.DHT_MARKER: + return "SOF4_MARKER"; + case JpegConstants.SOF5_MARKER: + return "SOF5_MARKER"; + case JpegConstants.SOF6_MARKER: + return "SOF6_MARKER"; + case JpegConstants.SOF7_MARKER: + return "SOF7_MARKER"; + case JpegConstants.SOF8_MARKER: + return "SOF8_MARKER"; + case JpegConstants.SOF9_MARKER: + return "SOF9_MARKER"; + case JpegConstants.SOF10_MARKER: + return "SOF10_MARKER"; + case JpegConstants.SOF11_MARKER: + return "SOF11_MARKER"; + case JpegConstants.DAC_MARKER: + return "DAC_MARKER"; + case JpegConstants.SOF13_MARKER: + return "SOF13_MARKER"; + case JpegConstants.SOF14_MARKER: + return "SOF14_MARKER"; + case JpegConstants.SOF15_MARKER: + return "SOF15_MARKER"; + case JpegConstants.DQT_MARKER: + return "DQT_MARKER"; + default: + return "Unknown"; + } + } + + public void dumpJFIF(final ByteSource byteSource) throws ImageReadException, + IOException { + final Visitor visitor = new Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return true; + } + + public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { + Debug.debug("SOS marker. " + imageData.length + " bytes of image data."); + Debug.debug(""); + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int segmentLength, final byte[] segmentLengthBytes, + final byte[] segmentData) { + Debug.debug("Segment marker: " + Integer.toHexString(marker) + + " (" + getMarkerName(marker) + "), " + + segmentData.length + " bytes of segment data."); + return true; + } + }; + + traverseJFIF(byteSource, visitor); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Block.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Block.java new file mode 100644 index 0000000..2ac966d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Block.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +final class Block { + final int[] samples; + final int width; + final int height; + + Block(final int width, final int height) { + samples = new int[width * height]; + this.width = width; + this.height = height; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Dct.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Dct.java new file mode 100644 index 0000000..9e443c9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/Dct.java @@ -0,0 +1,384 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +final class Dct { + /* + * The book "JPEG still image data compression standard", by Pennebaker and + * Mitchell, Chapter 4, discusses a number of approaches to the fast DCT. + * Here's the cost, exluding modified (de)quantization, for transforming an + * 8x8 block: + * + * Algorithm Adds Multiplies RightShifts Total + * Naive 896 1024 0 1920 + * "Symmetries" 448 224 0 672 + * Vetterli and Ligtenberg 464 208 0 672 + * Arai, Agui and Nakajima (AA&N) 464 80 0 544 + * Feig 8x8 462 54 6 522 + * Fused mul/add (a pipe dream) 416 + * + * IJG's libjpeg, FFmpeg, and a number of others use AA&N. + * + * It would appear that Feig does 4-5% less operations, and multiplications + * are reduced from 80 in AA&N to only 54. But in practice: + * + * Benchmarks, Intel Core i3 @ 2.93 GHz in long mode, 4 GB RAM Time taken to + * do 100 million IDCTs (less is better): + * Rene' Stöckel's Feig, int: 45.07 seconds + * My Feig, floating point: 36.252 seconds + * AA&N, unrolled loops, double[][] -> double[][]: 25.167 seconds + * + * Clearly Feig is hopeless. I suspect the performance killer is simply the + * weight of the algorithm: massive number of local variables, large code + * size, and lots of random array accesses. + * + * Also, AA&N can be optimized a lot: + * AA&N, rolled loops, double[][] -> double[][]: 21.162 seconds + * AA&N, rolled loops, float[][] -> float[][]: no improvement, + * but at some stage Hotspot might start doing SIMD, so let's + * use float AA&N, rolled loops, float[] -> float[][]: 19.979 seconds + * apparently 2D arrays are slow! + * AA&N, rolled loops, inlined 1D AA&N + * transform, float[] transformed in-place: 18.5 seconds + * AA&N, previous version rewritten in C and compiled with "gcc -O3" + * takes: 8.5 seconds + * (probably due to heavy use of SIMD) + * + * Other brave attempts: AA&N, best float version converted to 16:16 fixed + * point: 23.923 seconds + * + * Anyway the best float version stays. 18.5 seconds = 5.4 million + * transforms per second per core :-) + */ + + private static final float[] DCT_SCALING_FACTORS = { + (float) (0.5 / Math.sqrt(2.0)), + (float) (0.25 / Math.cos(Math.PI / 16.0)), + (float) (0.25 / Math.cos(2.0 * Math.PI / 16.0)), + (float) (0.25 / Math.cos(3.0 * Math.PI / 16.0)), + (float) (0.25 / Math.cos(4.0 * Math.PI / 16.0)), + (float) (0.25 / Math.cos(5.0 * Math.PI / 16.0)), + (float) (0.25 / Math.cos(6.0 * Math.PI / 16.0)), + (float) (0.25 / Math.cos(7.0 * Math.PI / 16.0)), }; + + private static final float[] IDCT_SCALING_FACTORS = { + (float) (2.0 * 4.0 / Math.sqrt(2.0) * 0.0625), + (float) (4.0 * Math.cos(Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(2.0 * Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(3.0 * Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(4.0 * Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(5.0 * Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(6.0 * Math.PI / 16.0) * 0.125), + (float) (4.0 * Math.cos(7.0 * Math.PI / 16.0) * 0.125), }; + + private static final float A1 = (float) (Math.cos(2.0 * Math.PI / 8.0)); + private static final float A2 = (float) (Math.cos(Math.PI / 8.0) - Math + .cos(3.0 * Math.PI / 8.0)); + private static final float A3 = A1; + private static final float A4 = (float) (Math.cos(Math.PI / 8.0) + Math + .cos(3.0 * Math.PI / 8.0)); + private static final float A5 = (float) (Math.cos(3.0 * Math.PI / 8.0)); + + private static final float C2 = (float) (2.0 * Math.cos(Math.PI / 8)); + private static final float C4 = (float) (2.0 * Math.cos(2 * Math.PI / 8)); + private static final float C6 = (float) (2.0 * Math.cos(3 * Math.PI / 8)); + private static final float Q = C2 - C6; + private static final float R = C2 + C6; + + private Dct() { + } + + public static void scaleQuantizationVector(final float[] vector) { + for (int x = 0; x < 8; x++) { + vector[x] *= DCT_SCALING_FACTORS[x]; + } + } + + public static void scaleDequantizationVector(final float[] vector) { + for (int x = 0; x < 8; x++) { + vector[x] *= IDCT_SCALING_FACTORS[x]; + } + } + + public static void scaleQuantizationMatrix(final float[] matrix) { + for (int y = 0; y < 8; y++) { + for (int x = 0; x < 8; x++) { + matrix[8 * y + x] *= DCT_SCALING_FACTORS[y] + * DCT_SCALING_FACTORS[x]; + } + } + } + + public static void scaleDequantizationMatrix(final float[] matrix) { + for (int y = 0; y < 8; y++) { + for (int x = 0; x < 8; x++) { + matrix[8 * y + x] *= IDCT_SCALING_FACTORS[y] + * IDCT_SCALING_FACTORS[x]; + } + } + } + + /** + * Fast forward Dct using AA&N. Taken from the book + * "JPEG still image data compression standard", by Pennebaker and Mitchell, + * chapter 4, figure "4-8". + */ + public static void forwardDCT8(final float[] vector) { + final float a00 = vector[0] + vector[7]; + final float a10 = vector[1] + vector[6]; + final float a20 = vector[2] + vector[5]; + final float a30 = vector[3] + vector[4]; + final float a40 = vector[3] - vector[4]; + final float a50 = vector[2] - vector[5]; + final float a60 = vector[1] - vector[6]; + final float a70 = vector[0] - vector[7]; + + final float a01 = a00 + a30; + final float a11 = a10 + a20; + final float a21 = a10 - a20; + final float a31 = a00 - a30; + // Avoid some negations: + // float a41 = -a40 - a50; + final float neg_a41 = a40 + a50; + final float a51 = a50 + a60; + final float a61 = a60 + a70; + + final float a22 = a21 + a31; + + final float a23 = a22 * A1; + final float mul5 = (a61 - neg_a41) * A5; + final float a43 = neg_a41 * A2 - mul5; + final float a53 = a51 * A3; + final float a63 = a61 * A4 - mul5; + + final float a54 = a70 + a53; + final float a74 = a70 - a53; + + vector[0] = a01 + a11; + vector[4] = a01 - a11; + vector[2] = a31 + a23; + vector[6] = a31 - a23; + vector[5] = a74 + a43; + vector[1] = a54 + a63; + vector[7] = a54 - a63; + vector[3] = a74 - a43; + } + + public static void forwardDCT8x8(final float[] matrix) { + float a00, a10, a20, a30, a40, a50, a60, a70; + float a01, a11, a21, a31, neg_a41, a51, a61; + float a22, a23, mul5, a43, a53, a63; + float a54, a74; + + for (int i = 0; i < 8; i++) { + a00 = matrix[8 * i] + matrix[8 * i + 7]; + a10 = matrix[8 * i + 1] + matrix[8 * i + 6]; + a20 = matrix[8 * i + 2] + matrix[8 * i + 5]; + a30 = matrix[8 * i + 3] + matrix[8 * i + 4]; + a40 = matrix[8 * i + 3] - matrix[8 * i + 4]; + a50 = matrix[8 * i + 2] - matrix[8 * i + 5]; + a60 = matrix[8 * i + 1] - matrix[8 * i + 6]; + a70 = matrix[8 * i] - matrix[8 * i + 7]; + a01 = a00 + a30; + a11 = a10 + a20; + a21 = a10 - a20; + a31 = a00 - a30; + neg_a41 = a40 + a50; + a51 = a50 + a60; + a61 = a60 + a70; + a22 = a21 + a31; + a23 = a22 * A1; + mul5 = (a61 - neg_a41) * A5; + a43 = neg_a41 * A2 - mul5; + a53 = a51 * A3; + a63 = a61 * A4 - mul5; + a54 = a70 + a53; + a74 = a70 - a53; + matrix[8 * i] = a01 + a11; + matrix[8 * i + 4] = a01 - a11; + matrix[8 * i + 2] = a31 + a23; + matrix[8 * i + 6] = a31 - a23; + matrix[8 * i + 5] = a74 + a43; + matrix[8 * i + 1] = a54 + a63; + matrix[8 * i + 7] = a54 - a63; + matrix[8 * i + 3] = a74 - a43; + } + + for (int i = 0; i < 8; i++) { + a00 = matrix[i] + matrix[56 + i]; + a10 = matrix[8 + i] + matrix[48 + i]; + a20 = matrix[16 + i] + matrix[40 + i]; + a30 = matrix[24 + i] + matrix[32 + i]; + a40 = matrix[24 + i] - matrix[32 + i]; + a50 = matrix[16 + i] - matrix[40 + i]; + a60 = matrix[8 + i] - matrix[48 + i]; + a70 = matrix[i] - matrix[56 + i]; + a01 = a00 + a30; + a11 = a10 + a20; + a21 = a10 - a20; + a31 = a00 - a30; + neg_a41 = a40 + a50; + a51 = a50 + a60; + a61 = a60 + a70; + a22 = a21 + a31; + a23 = a22 * A1; + mul5 = (a61 - neg_a41) * A5; + a43 = neg_a41 * A2 - mul5; + a53 = a51 * A3; + a63 = a61 * A4 - mul5; + a54 = a70 + a53; + a74 = a70 - a53; + matrix[i] = a01 + a11; + matrix[32 + i] = a01 - a11; + matrix[16 + i] = a31 + a23; + matrix[48 + i] = a31 - a23; + matrix[40 + i] = a74 + a43; + matrix[8 + i] = a54 + a63; + matrix[56 + i] = a54 - a63; + matrix[24 + i] = a74 - a43; + } + } + + /** + * Fast inverse Dct using AA&N. This is taken from the beautiful + * http://vsr.informatik.tu-chemnitz.de/~jan/MPEG/HTML/IDCT.html which gives + * easy equations and properly explains constants and scaling factors. Terms + * have been inlined and the negation optimized out of existence. + */ + public static void inverseDCT8(final float[] vector) { + // B1 + final float a2 = vector[2] - vector[6]; + final float a3 = vector[2] + vector[6]; + final float a4 = vector[5] - vector[3]; + final float tmp1 = vector[1] + vector[7]; + final float tmp2 = vector[3] + vector[5]; + final float a5 = tmp1 - tmp2; + final float a6 = vector[1] - vector[7]; + final float a7 = tmp1 + tmp2; + + // M + final float tmp4 = C6 * (a4 + a6); + // Eliminate the negative: + // float b4 = -Q*a4 - tmp4; + final float neg_b4 = Q * a4 + tmp4; + final float b6 = R * a6 - tmp4; + final float b2 = a2 * C4; + final float b5 = a5 * C4; + + // A1 + final float tmp3 = b6 - a7; + final float n0 = tmp3 - b5; + final float n1 = vector[0] - vector[4]; + final float n2 = b2 - a3; + final float n3 = vector[0] + vector[4]; + final float neg_n5 = neg_b4; + + // A2 + final float m3 = n1 + n2; + final float m4 = n3 + a3; + final float m5 = n1 - n2; + final float m6 = n3 - a3; + // float m7 = n5 - n0; + final float neg_m7 = neg_n5 + n0; + + // A3 + vector[0] = m4 + a7; + vector[1] = m3 + tmp3; + vector[2] = m5 - n0; + vector[3] = m6 + neg_m7; + vector[4] = m6 - neg_m7; + vector[5] = m5 + n0; + vector[6] = m3 - tmp3; + vector[7] = m4 - a7; + } + + public static void inverseDCT8x8(final float[] matrix) { + float a2, a3, a4, tmp1, tmp2, a5, a6, a7; + float tmp4, neg_b4, b6, b2, b5; + float tmp3, n0, n1, n2, n3, neg_n5; + float m3, m4, m5, m6, neg_m7; + + for (int i = 0; i < 8; i++) { + a2 = matrix[8 * i + 2] - matrix[8 * i + 6]; + a3 = matrix[8 * i + 2] + matrix[8 * i + 6]; + a4 = matrix[8 * i + 5] - matrix[8 * i + 3]; + tmp1 = matrix[8 * i + 1] + matrix[8 * i + 7]; + tmp2 = matrix[8 * i + 3] + matrix[8 * i + 5]; + a5 = tmp1 - tmp2; + a6 = matrix[8 * i + 1] - matrix[8 * i + 7]; + a7 = tmp1 + tmp2; + tmp4 = C6 * (a4 + a6); + neg_b4 = Q * a4 + tmp4; + b6 = R * a6 - tmp4; + b2 = a2 * C4; + b5 = a5 * C4; + tmp3 = b6 - a7; + n0 = tmp3 - b5; + n1 = matrix[8 * i] - matrix[8 * i + 4]; + n2 = b2 - a3; + n3 = matrix[8 * i] + matrix[8 * i + 4]; + neg_n5 = neg_b4; + m3 = n1 + n2; + m4 = n3 + a3; + m5 = n1 - n2; + m6 = n3 - a3; + neg_m7 = neg_n5 + n0; + matrix[8 * i] = m4 + a7; + matrix[8 * i + 1] = m3 + tmp3; + matrix[8 * i + 2] = m5 - n0; + matrix[8 * i + 3] = m6 + neg_m7; + matrix[8 * i + 4] = m6 - neg_m7; + matrix[8 * i + 5] = m5 + n0; + matrix[8 * i + 6] = m3 - tmp3; + matrix[8 * i + 7] = m4 - a7; + } + + for (int i = 0; i < 8; i++) { + a2 = matrix[16 + i] - matrix[48 + i]; + a3 = matrix[16 + i] + matrix[48 + i]; + a4 = matrix[40 + i] - matrix[24 + i]; + tmp1 = matrix[8 + i] + matrix[56 + i]; + tmp2 = matrix[24 + i] + matrix[40 + i]; + a5 = tmp1 - tmp2; + a6 = matrix[8 + i] - matrix[56 + i]; + a7 = tmp1 + tmp2; + tmp4 = C6 * (a4 + a6); + neg_b4 = Q * a4 + tmp4; + b6 = R * a6 - tmp4; + b2 = a2 * C4; + b5 = a5 * C4; + tmp3 = b6 - a7; + n0 = tmp3 - b5; + n1 = matrix[i] - matrix[32 + i]; + n2 = b2 - a3; + n3 = matrix[i] + matrix[32 + i]; + neg_n5 = neg_b4; + m3 = n1 + n2; + m4 = n3 + a3; + m5 = n1 - n2; + m6 = n3 - a3; + neg_m7 = neg_n5 + n0; + matrix[i] = m4 + a7; + matrix[8 + i] = m3 + tmp3; + matrix[16 + i] = m5 - n0; + matrix[24 + i] = m6 + neg_m7; + matrix[32 + i] = m6 - neg_m7; + matrix[40 + i] = m5 + n0; + matrix[48 + i] = m3 - tmp3; + matrix[56 + i] = m4 - a7; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java new file mode 100644 index 0000000..d1d2fb7 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java @@ -0,0 +1,445 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DirectColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.WritableRaster; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Properties; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.formats.jpeg.JpegUtils; +import org.apache.commons.imaging.formats.jpeg.segments.DhtSegment; +import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment; +import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment; +import org.apache.commons.imaging.formats.jpeg.segments.SosSegment; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class JpegDecoder extends BinaryFileParser implements JpegUtils.Visitor { + /* + * JPEG is an advanced image format that takes significant computation to + * decode. Keep decoding fast: - Don't allocate memory inside loops, + * allocate it once and reuse. - Minimize calculations per pixel and per + * block (using lookup tables for YCbCr->RGB conversion doubled + * performance). - Math.round() is slow, use (int)(x+0.5f) instead for + * positive numbers. + */ + + private final DqtSegment.QuantizationTable[] quantizationTables = new DqtSegment.QuantizationTable[4]; + private final DhtSegment.HuffmanTable[] huffmanDCTables = new DhtSegment.HuffmanTable[4]; + private final DhtSegment.HuffmanTable[] huffmanACTables = new DhtSegment.HuffmanTable[4]; + private SofnSegment sofnSegment; + private SosSegment sosSegment; + private final float[][] scaledQuantizationTables = new float[4][]; + private BufferedImage image; + private ImageReadException imageReadException; + private IOException ioException; + private final int[] zz = new int[64]; + private final int[] blockInt = new int[64]; + private final float[] block = new float[64]; + + public boolean beginSOS() { + return true; + } + + public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { + final ByteArrayInputStream is = new ByteArrayInputStream(imageData); + try { + final int segmentLength = read2Bytes("segmentLength", is, "Not a Valid JPEG File", getByteOrder()); + final byte[] sosSegmentBytes = readBytes("SosSegment", + is, segmentLength - 2, "Not a Valid JPEG File"); + sosSegment = new SosSegment(marker, sosSegmentBytes); + + int hMax = 0; + int vMax = 0; + for (int i = 0; i < sofnSegment.numberOfComponents; i++) { + hMax = Math.max(hMax, + sofnSegment.getComponents(i).horizontalSamplingFactor); + vMax = Math.max(vMax, + sofnSegment.getComponents(i).verticalSamplingFactor); + } + final int hSize = 8 * hMax; + final int vSize = 8 * vMax; + + final JpegInputStream bitInputStream = new JpegInputStream(is); + final int xMCUs = (sofnSegment.width + hSize - 1) / hSize; + final int yMCUs = (sofnSegment.height + vSize - 1) / vSize; + final Block[] mcu = allocateMCUMemory(); + final Block[] scaledMCU = new Block[mcu.length]; + for (int i = 0; i < scaledMCU.length; i++) { + scaledMCU[i] = new Block(hSize, vSize); + } + final int[] preds = new int[sofnSegment.numberOfComponents]; + ColorModel colorModel; + WritableRaster raster; + if (sofnSegment.numberOfComponents == 3) { + colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, + 0x000000ff); + raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, + sofnSegment.width, sofnSegment.height, new int[] { + 0x00ff0000, 0x0000ff00, 0x000000ff }, null); + } else if (sofnSegment.numberOfComponents == 1) { + colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, + 0x000000ff); + raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, + sofnSegment.width, sofnSegment.height, new int[] { + 0x00ff0000, 0x0000ff00, 0x000000ff }, null); + // FIXME: why do images come out too bright with CS_GRAY? + // colorModel = new ComponentColorModel( + // ColorSpace.getInstance(ColorSpace.CS_GRAY), false, true, + // Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + // raster = colorModel.createCompatibleWritableRaster( + // sofnSegment.width, sofnSegment.height); + } else { + throw new ImageReadException(sofnSegment.numberOfComponents + + " components are invalid or unsupported"); + } + final DataBuffer dataBuffer = raster.getDataBuffer(); + + for (int y1 = 0; y1 < vSize * yMCUs; y1 += vSize) { + for (int x1 = 0; x1 < hSize * xMCUs; x1 += hSize) { + readMCU(bitInputStream, preds, mcu); + rescaleMCU(mcu, hSize, vSize, scaledMCU); + int srcRowOffset = 0; + int dstRowOffset = y1 * sofnSegment.width + x1; + for (int y2 = 0; y2 < vSize && y1 + y2 < sofnSegment.height; y2++) { + for (int x2 = 0; x2 < hSize + && x1 + x2 < sofnSegment.width; x2++) { + if (scaledMCU.length == 3) { + final int Y = scaledMCU[0].samples[srcRowOffset + x2]; + final int Cb = scaledMCU[1].samples[srcRowOffset + x2]; + final int Cr = scaledMCU[2].samples[srcRowOffset + x2]; + final int rgb = YCbCrConverter.convertYCbCrToRGB(Y, + Cb, Cr); + dataBuffer.setElem(dstRowOffset + x2, rgb); + } else if (mcu.length == 1) { + final int Y = scaledMCU[0].samples[srcRowOffset + x2]; + dataBuffer.setElem(dstRowOffset + x2, (Y << 16) + | (Y << 8) | Y); + } else { + throw new ImageReadException( + "Unsupported JPEG with " + mcu.length + + " components"); + } + } + srcRowOffset += hSize; + dstRowOffset += sofnSegment.width; + } + } + } + image = new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + // byte[] remainder = super.getStreamBytes(is); + // for (int i = 0; i < remainder.length; i++) + // { + // System.out.println("" + i + " = " + + // Integer.toHexString(remainder[i])); + // } + } catch (final ImageReadException imageReadEx) { + imageReadException = imageReadEx; + } catch (final IOException ioEx) { + ioException = ioEx; + } catch (final RuntimeException ex) { + // Corrupt images can throw NPE and IOOBE + imageReadException = new ImageReadException("Error parsing JPEG", + ex); + } + } + + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int segmentLength, final byte[] segmentLengthBytes, final byte[] segmentData) + throws ImageReadException, IOException { + final int[] sofnSegments = { + JpegConstants.SOF0_MARKER, + JpegConstants.SOF1_MARKER, + JpegConstants.SOF2_MARKER, + JpegConstants.SOF3_MARKER, + JpegConstants.SOF5_MARKER, + JpegConstants.SOF6_MARKER, + JpegConstants.SOF7_MARKER, + JpegConstants.SOF9_MARKER, + JpegConstants.SOF10_MARKER, + JpegConstants.SOF11_MARKER, + JpegConstants.SOF13_MARKER, + JpegConstants.SOF14_MARKER, + JpegConstants.SOF15_MARKER, + }; + + if (Arrays.binarySearch(sofnSegments, marker) >= 0) { + if (marker != JpegConstants.SOF0_MARKER) { + throw new ImageReadException("Only sequential, baseline JPEGs " + + "are supported at the moment"); + } + sofnSegment = new SofnSegment(marker, segmentData); + } else if (marker == JpegConstants.DQT_MARKER) { + final DqtSegment dqtSegment = new DqtSegment(marker, segmentData); + for (int i = 0; i < dqtSegment.quantizationTables.size(); i++) { + final DqtSegment.QuantizationTable table = dqtSegment.quantizationTables + .get(i); + if (0 > table.destinationIdentifier + || table.destinationIdentifier >= quantizationTables.length) { + throw new ImageReadException( + "Invalid quantization table identifier " + + table.destinationIdentifier); + } + quantizationTables[table.destinationIdentifier] = table; + final int[] quantizationMatrixInt = new int[64]; + ZigZag.zigZagToBlock(table.elements, quantizationMatrixInt); + final float[] quantizationMatrixFloat = new float[64]; + for (int j = 0; j < 64; j++) { + quantizationMatrixFloat[j] = quantizationMatrixInt[j]; + } + Dct.scaleDequantizationMatrix(quantizationMatrixFloat); + scaledQuantizationTables[table.destinationIdentifier] = quantizationMatrixFloat; + } + } else if (marker == JpegConstants.DHT_MARKER) { + final DhtSegment dhtSegment = new DhtSegment(marker, segmentData); + for (int i = 0; i < dhtSegment.huffmanTables.size(); i++) { + final DhtSegment.HuffmanTable table = dhtSegment.huffmanTables.get(i); + DhtSegment.HuffmanTable[] tables; + if (table.tableClass == 0) { + tables = huffmanDCTables; + } else if (table.tableClass == 1) { + tables = huffmanACTables; + } else { + throw new ImageReadException("Invalid huffman table class " + + table.tableClass); + } + if (0 > table.destinationIdentifier + || table.destinationIdentifier >= tables.length) { + throw new ImageReadException( + "Invalid huffman table identifier " + + table.destinationIdentifier); + } + tables[table.destinationIdentifier] = table; + } + } + return true; + } + + private void rescaleMCU(final Block[] dataUnits, final int hSize, final int vSize, final Block[] ret) { + for (int i = 0; i < dataUnits.length; i++) { + final Block dataUnit = dataUnits[i]; + if (dataUnit.width == hSize && dataUnit.height == vSize) { + System.arraycopy(dataUnit.samples, 0, ret[i].samples, 0, hSize + * vSize); + } else { + final int hScale = hSize / dataUnit.width; + final int vScale = vSize / dataUnit.height; + if (hScale == 2 && vScale == 2) { + int srcRowOffset = 0; + int dstRowOffset = 0; + for (int y = 0; y < dataUnit.height; y++) { + for (int x = 0; x < hSize; x++) { + final int sample = dataUnit.samples[srcRowOffset + (x >> 1)]; + ret[i].samples[dstRowOffset + x] = sample; + ret[i].samples[dstRowOffset + hSize + x] = sample; + } + srcRowOffset += dataUnit.width; + dstRowOffset += 2 * hSize; + } + } else { + // FIXME: optimize + int dstRowOffset = 0; + for (int y = 0; y < vSize; y++) { + for (int x = 0; x < hSize; x++) { + ret[i].samples[dstRowOffset + x] = dataUnit.samples[(y / vScale) + * dataUnit.width + (x / hScale)]; + } + dstRowOffset += hSize; + } + } + } + } + } + + private Block[] allocateMCUMemory() throws ImageReadException { + final Block[] mcu = new Block[sosSegment.numberOfComponents]; + for (int i = 0; i < sosSegment.numberOfComponents; i++) { + final SosSegment.Component scanComponent = sosSegment.getComponents(i); + SofnSegment.Component frameComponent = null; + for (int j = 0; j < sofnSegment.numberOfComponents; j++) { + if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) { + frameComponent = sofnSegment.getComponents(j); + break; + } + } + if (frameComponent == null) { + throw new ImageReadException("Invalid component"); + } + final Block fullBlock = new Block( + 8 * frameComponent.horizontalSamplingFactor, + 8 * frameComponent.verticalSamplingFactor); + mcu[i] = fullBlock; + } + return mcu; + } + + private void readMCU(final JpegInputStream is, final int[] preds, final Block[] mcu) + throws IOException, ImageReadException { + for (int i = 0; i < sosSegment.numberOfComponents; i++) { + final SosSegment.Component scanComponent = sosSegment.getComponents(i); + SofnSegment.Component frameComponent = null; + for (int j = 0; j < sofnSegment.numberOfComponents; j++) { + if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) { + frameComponent = sofnSegment.getComponents(j); + break; + } + } + if (frameComponent == null) { + throw new ImageReadException("Invalid component"); + } + final Block fullBlock = mcu[i]; + for (int y = 0; y < frameComponent.verticalSamplingFactor; y++) { + for (int x = 0; x < frameComponent.horizontalSamplingFactor; x++) { + Arrays.fill(zz, 0); + // page 104 of T.81 + final int t = decode( + is, + huffmanDCTables[scanComponent.dcCodingTableSelector]); + int diff = receive(t, is); + diff = extend(diff, t); + zz[0] = preds[i] + diff; + preds[i] = zz[0]; + + // "Decode_AC_coefficients", figure F.13, page 106 of T.81 + int k = 1; + while (true) { + final int rs = decode( + is, + huffmanACTables[scanComponent.acCodingTableSelector]); + final int ssss = rs & 0xf; + final int rrrr = rs >> 4; + final int r = rrrr; + + if (ssss == 0) { + if (r == 15) { + k += 16; + } else { + break; + } + } else { + k += r; + + // "Decode_ZZ(k)", figure F.14, page 107 of T.81 + zz[k] = receive(ssss, is); + zz[k] = extend(zz[k], ssss); + + if (k == 63) { + break; + } else { + k++; + } + } + } + + final int shift = (1 << (sofnSegment.precision - 1)); + final int max = (1 << sofnSegment.precision) - 1; + + final float[] scaledQuantizationTable = scaledQuantizationTables[frameComponent.quantTabDestSelector]; + ZigZag.zigZagToBlock(zz, blockInt); + for (int j = 0; j < 64; j++) { + block[j] = blockInt[j] * scaledQuantizationTable[j]; + } + Dct.inverseDCT8x8(block); + + int dstRowOffset = 8 * y * 8 + * frameComponent.horizontalSamplingFactor + 8 * x; + int srcNext = 0; + for (int yy = 0; yy < 8; yy++) { + for (int xx = 0; xx < 8; xx++) { + float sample = block[srcNext++]; + sample += shift; + int result; + if (sample < 0) { + result = 0; + } else if (sample > max) { + result = max; + } else { + result = fastRound(sample); + } + fullBlock.samples[dstRowOffset + xx] = result; + } + dstRowOffset += 8 * frameComponent.horizontalSamplingFactor; + } + } + } + } + } + + private static int fastRound(final float x) { + return (int) (x + 0.5f); + } + + private int extend(int v, final int t) { + // "EXTEND", section F.2.2.1, figure F.12, page 105 of T.81 + int vt = (1 << (t - 1)); + while (v < vt) { + vt = (-1 << t) + 1; + v += vt; + } + return v; + } + + private int receive(final int ssss, final JpegInputStream is) throws IOException, + ImageReadException { + // "RECEIVE", section F.2.2.4, figure F.17, page 110 of T.81 + int i = 0; + int v = 0; + while (i != ssss) { + i++; + v = (v << 1) + is.nextBit(); + } + return v; + } + + private int decode(final JpegInputStream is, final DhtSegment.HuffmanTable huffmanTable) + throws IOException, ImageReadException { + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 + int i = 1; + int code = is.nextBit(); + while (code > huffmanTable.getMaxCode()[i]) { + i++; + code = (code << 1) | is.nextBit(); + } + int j = huffmanTable.getValPtr()[i]; + j += code - huffmanTable.getMinCode()[i]; + return huffmanTable.getHuffVal()[j]; + } + + public BufferedImage decode(final ByteSource byteSource) throws IOException, + ImageReadException { + final JpegUtils jpegUtils = new JpegUtils(); + jpegUtils.traverseJFIF(byteSource, this); + if (imageReadException != null) { + throw imageReadException; + } + if (ioException != null) { + throw ioException; + } + return image; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegInputStream.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegInputStream.java new file mode 100644 index 0000000..d89b67f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegInputStream.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +class JpegInputStream { + // Figure F.18, F.2.2.5, page 111 of ITU-T T.81 + private final InputStream is; + private int cnt; + private int b; + + public JpegInputStream(final InputStream is) { + this.is = is; + } + + public int nextBit() throws IOException, ImageReadException { + if (cnt == 0) { + b = is.read(); + if (b < 0) { + throw new ImageReadException("Premature End of File"); + } + cnt = 8; + if (b == 0xff) { + final int b2 = is.read(); + if (b2 < 0) { + throw new ImageReadException("Premature End of File"); + } + if (b2 != 0) { + if (b2 == (0xff & JpegConstants.DNL_MARKER)) { + throw new ImageReadException("DNL not yet supported"); + } + throw new ImageReadException("Invalid marker found " + + "in entropy data"); + } + } + } + final int bit = (b >> 7) & 0x1; + cnt--; + b <<= 1; + return bit; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/YCbCrConverter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/YCbCrConverter.java new file mode 100644 index 0000000..0684d25 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/YCbCrConverter.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +final class YCbCrConverter { + private static final int[] REDS = new int[256 * 256]; + private static final int[] BLUES = new int[256 * 256]; + private static final int[] GREENS1 = new int[256 * 256]; + private static final int[] GREENS2 = new int[256 * 512]; + + static { + /* + * Why use (Cr << 8) | Y and not (Y << 8) | Cr as the index? Y changes + * often, while Cb and Cr is usually subsampled less often and repeats + * itself between adjacent pixels, so using it as the high order byte + * gives higher locality of reference. + */ + for (int Y = 0; Y < 256; Y++) { + for (int Cr = 0; Cr < 256; Cr++) { + int r = Y + fastRound(1.402f * (Cr - 128)); + if (r < 0) { + r = 0; + } + if (r > 255) { + r = 255; + } + REDS[(Cr << 8) | Y] = r << 16; + } + } + for (int Y = 0; Y < 256; Y++) { + for (int Cb = 0; Cb < 256; Cb++) { + int b = Y + fastRound(1.772f * (Cb - 128)); + if (b < 0) { + b = 0; + } + if (b > 255) { + b = 255; + } + BLUES[(Cb << 8) | Y] = b; + } + } + // green is the hardest + // Math.round((float) (Y - 0.34414*(Cb-128) - 0.71414*(Cr-128))) + // but Y is integral + // = Y - Math.round((float) (0.34414*(Cb-128) + 0.71414*(Cr-128))) + // = Y - Math.round(f(Cb, Cr)) + // where + // f(Cb, Cr) = 0.34414*(Cb-128) + 0.71414*(Cr-128) + // Cb and Cr terms each vary from 255-128 = 127 to 0-128 = -128 + // Linear function, so only examine endpoints: + // Cb term Cr term Result + // 127 127 134.4 + // -128 -128 -135.4 + // 127 -128 -47.7 + // -128 127 46.6 + // Thus with -135 being the minimum and 134 the maximum, + // there is a range of 269 values, + // and 135 needs to be added to make it zero-based. + + // As for Y - f(Cb, Cr) + // the range becomes: + // Y f(Cb, Cr) + // 255 -135 + // 255 134 + // 0 -135 + // 0 134 + // thus the range is [-134,390] and has 524 values + // but is clamped to [0, 255] + for (int Cb = 0; Cb < 256; Cb++) { + for (int Cr = 0; Cr < 256; Cr++) { + final int value = fastRound(0.34414f * (Cb - 128) + 0.71414f + * (Cr - 128)); + GREENS1[(Cb << 8) | Cr] = value + 135; + } + } + for (int Y = 0; Y < 256; Y++) { + for (int value = 0; value < 270; value++) { + int green = Y - (value - 135); + if (green < 0) { + green = 0; + } else if (green > 255) { + green = 255; + } + GREENS2[(value << 8) | Y] = green << 8; + } + } + } + + private YCbCrConverter() { + } + + private static int fastRound(final float x) { + // Math.round() is very slow + return (int) (x + 0.5f); + } + + public static int convertYCbCrToRGB(final int Y, final int Cb, final int Cr) { + final int r = REDS[(Cr << 8) | Y]; + final int g1 = GREENS1[(Cb << 8) | Cr]; + final int g = GREENS2[(g1 << 8) | Y]; + final int b = BLUES[(Cb << 8) | Y]; + return r | g | b; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/ZigZag.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/ZigZag.java new file mode 100644 index 0000000..da6d58f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/ZigZag.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.decoder; + +final class ZigZag { + private static final int[] ZIG_ZAG = { + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 + }; + + private ZigZag() { + } + + public static void zigZagToBlock(final int[] zz, final int[] block) { + for (int i = 0; i < 64; i++) { + block[i] = zz[ZIG_ZAG[i]]; + } + } + + public static void blockToZigZag(final int[] block, final int[] zz) { + for (int i = 0; i < 64; i++) { + zz[ZIG_ZAG[i]] = block[i]; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriter.java new file mode 100644 index 0000000..4a68485 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriter.java @@ -0,0 +1,591 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.exif; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.formats.jpeg.JpegUtils; +import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterBase; +import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossless; +import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +/** + * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. + *

+ *

+ * See the source of the ExifMetadataUpdateExample class for example usage. + * + * @see org.apache.commons.imaging.examples.WriteExifMetadataExample + */ +public class ExifRewriter extends BinaryFileParser { + /** + * Constructor. to guess whether a file contains an image based on its file + * extension. + */ + public ExifRewriter() { + this(ByteOrder.BIG_ENDIAN); + } + + /** + * Constructor. + *

+ * + * @param byteOrder + * byte order of EXIF segment. + */ + public ExifRewriter(final ByteOrder byteOrder) { + setByteOrder(byteOrder); + } + + private static class JFIFPieces { + public final List pieces; + public final List exifPieces; + + public JFIFPieces(final List pieces, + final List exifPieces) { + this.pieces = pieces; + this.exifPieces = exifPieces; + } + + } + + private abstract static class JFIFPiece { + protected abstract void write(OutputStream os) throws IOException; + } + + private static class JFIFPieceSegment extends JFIFPiece { + public final int marker; + public final byte[] markerBytes; + public final byte[] markerLengthBytes; + public final byte[] segmentData; + + public JFIFPieceSegment(final int marker, final byte[] markerBytes, + final byte[] markerLengthBytes, final byte[] segmentData) { + this.marker = marker; + this.markerBytes = markerBytes; + this.markerLengthBytes = markerLengthBytes; + this.segmentData = segmentData; + } + + @Override + protected void write(final OutputStream os) throws IOException { + os.write(markerBytes); + os.write(markerLengthBytes); + os.write(segmentData); + } + } + + private static class JFIFPieceSegmentExif extends JFIFPieceSegment { + + public JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, + final byte[] markerLengthBytes, final byte[] segmentData) { + super(marker, markerBytes, markerLengthBytes, segmentData); + } + } + + private static class JFIFPieceImageData extends JFIFPiece { + public final byte[] markerBytes; + public final byte[] imageData; + + public JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { + super(); + this.markerBytes = markerBytes; + this.imageData = imageData; + } + + @Override + protected void write(final OutputStream os) throws IOException { + os.write(markerBytes); + os.write(imageData); + } + } + + private JFIFPieces analyzeJFIF(final ByteSource byteSource) + throws ImageReadException, IOException + // , ImageWriteException + { + final List pieces = new ArrayList(); + final List exifPieces = new ArrayList(); + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return true; + } + + public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { + pieces.add(new JFIFPieceImageData(markerBytes, imageData)); + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int markerLength, final byte[] markerLengthBytes, + final byte[] segmentData) throws + // ImageWriteException, + ImageReadException, IOException { + if (marker != JpegConstants.JPEG_APP1_MARKER) { + pieces.add(new JFIFPieceSegment(marker, markerBytes, + markerLengthBytes, segmentData)); + } else if (!startsWith(segmentData, + JpegConstants.EXIF_IDENTIFIER_CODE)) { + pieces.add(new JFIFPieceSegment(marker, markerBytes, + markerLengthBytes, segmentData)); + // } else if (exifSegmentArray[0] != null) { + // // TODO: add support for multiple segments + // throw new ImageReadException( + // "More than one APP1 EXIF segment."); + } else { + final JFIFPiece piece = new JFIFPieceSegmentExif(marker, + markerBytes, markerLengthBytes, segmentData); + pieces.add(piece); + exifPieces.add(piece); + } + return true; + } + }; + + new JpegUtils().traverseJFIF(byteSource, visitor); + + // GenericSegment exifSegment = exifSegmentArray[0]; + // if (exifSegments.size() < 1) + // { + // // TODO: add support for adding, not just replacing. + // throw new ImageReadException("No APP1 EXIF segment found."); + // } + + return new JFIFPieces(pieces, exifPieces); + } + + /** + * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 + * segment), and writes the result to a stream. + *

+ * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * + * @see java.io.File + * @see java.io.OutputStream + * @see java.io.File + * @see java.io.OutputStream + */ + public void removeExifMetadata(final File src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + removeExifMetadata(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 + * segment), and writes the result to a stream. + *

+ * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeExifMetadata(final byte[] src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + removeExifMetadata(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 + * segment), and writes the result to a stream. + *

+ * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeExifMetadata(final InputStream src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + removeExifMetadata(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 + * segment), and writes the result to a stream. + *

+ * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeExifMetadata(final ByteSource byteSource, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + final List pieces = jfifPieces.pieces; + + // Debug.debug("pieces", pieces); + + // pieces.removeAll(jfifPieces.exifSegments); + + // Debug.debug("pieces", pieces); + + writeSegmentsReplacingExif(os, pieces, null); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossless" approach - in order to preserve data + * embedded in the EXIF segment that it can't parse (such as Maker Notes), + * this algorithm avoids overwriting any part of the original segment that + * it couldn't parse. This can cause the EXIF segment to grow with each + * update, which is a serious issue, since all EXIF data must fit in a + * single APP1 segment of the Jpeg image. + *

+ * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossless(final File src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + updateExifMetadataLossless(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossless" approach - in order to preserve data + * embedded in the EXIF segment that it can't parse (such as Maker Notes), + * this algorithm avoids overwriting any part of the original segment that + * it couldn't parse. This can cause the EXIF segment to grow with each + * update, which is a serious issue, since all EXIF data must fit in a + * single APP1 segment of the Jpeg image. + *

+ * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossless(final byte[] src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + updateExifMetadataLossless(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossless" approach - in order to preserve data + * embedded in the EXIF segment that it can't parse (such as Maker Notes), + * this algorithm avoids overwriting any part of the original segment that + * it couldn't parse. This can cause the EXIF segment to grow with each + * update, which is a serious issue, since all EXIF data must fit in a + * single APP1 segment of the Jpeg image. + *

+ * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossless(final InputStream src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + updateExifMetadataLossless(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossless" approach - in order to preserve data + * embedded in the EXIF segment that it can't parse (such as Maker Notes), + * this algorithm avoids overwriting any part of the original segment that + * it couldn't parse. This can cause the EXIF segment to grow with each + * update, which is a serious issue, since all EXIF data must fit in a + * single APP1 segment of the Jpeg image. + *

+ * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossless(final ByteSource byteSource, + final OutputStream os, final TiffOutputSet outputSet) + throws ImageReadException, IOException, ImageWriteException { + // List outputDirectories = outputSet.getDirectories(); + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + final List pieces = jfifPieces.pieces; + + TiffImageWriterBase writer; + // Just use first APP1 segment for now. + // Multiple APP1 segments are rare and poorly supported. + if (jfifPieces.exifPieces.size() > 0) { + JFIFPieceSegment exifPiece = null; + exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0); + + byte[] exifBytes = exifPiece.segmentData; + exifBytes = remainingBytes("trimmed exif bytes", exifBytes, 6); + + writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes); + + } else { + writer = new TiffImageWriterLossy(outputSet.byteOrder); + } + + final boolean includeEXIFPrefix = true; + final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix); + + writeSegmentsReplacingExif(os, pieces, newBytes); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossy" approach - the algorithm overwrites the + * entire EXIF segment, ignoring the possibility that it may be discarding + * data it couldn't parse (such as Maker Notes). + *

+ * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossy(final byte[] src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + updateExifMetadataLossy(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossy" approach - the algorithm overwrites the + * entire EXIF segment, ignoring the possibility that it may be discarding + * data it couldn't parse (such as Maker Notes). + *

+ * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossy(final InputStream src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + updateExifMetadataLossy(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossy" approach - the algorithm overwrites the + * entire EXIF segment, ignoring the possibility that it may be discarding + * data it couldn't parse (such as Maker Notes). + *

+ * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossy(final File src, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + updateExifMetadataLossy(byteSource, os, outputSet); + } + + /** + * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a + * stream. + *

+ * Note that this uses the "Lossy" approach - the algorithm overwrites the + * entire EXIF segment, ignoring the possibility that it may be discarding + * data it couldn't parse (such as Maker Notes). + *

+ * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param outputSet + * TiffOutputSet containing the EXIF data to write. + */ + public void updateExifMetadataLossy(final ByteSource byteSource, final OutputStream os, + final TiffOutputSet outputSet) throws ImageReadException, IOException, + ImageWriteException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + final List pieces = jfifPieces.pieces; + + final TiffImageWriterBase writer = new TiffImageWriterLossy( + outputSet.byteOrder); + + final boolean includeEXIFPrefix = true; + final byte[] newBytes = writeExifSegment(writer, outputSet, includeEXIFPrefix); + + writeSegmentsReplacingExif(os, pieces, newBytes); + } + + private void writeSegmentsReplacingExif(final OutputStream os, + final List segments, final byte[] newBytes) + throws ImageWriteException, IOException { + + boolean canThrow = false; + try { + JpegConstants.SOI.writeTo(os); + + boolean hasExif = false; + + for (final JFIFPiece piece : segments) { + if (piece instanceof JFIFPieceSegmentExif) { + hasExif = true; + } + } + + if (!hasExif && newBytes != null) { + final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder()); + if (newBytes.length > 0xffff) { + throw new ExifOverflowException( + "APP1 Segment is too long: " + newBytes.length); + } + final int markerLength = newBytes.length + 2; + final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder()); + + int index = 0; + final JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments + .get(index); + if (firstSegment.marker == JpegConstants.JFIF_MARKER) { + index = 1; + } + segments.add(index, new JFIFPieceSegmentExif(JpegConstants.JPEG_APP1_MARKER, + markerBytes, markerLengthBytes, newBytes)); + } + + boolean APP1Written = false; + + for (final JFIFPiece piece : segments) { + if (piece instanceof JFIFPieceSegmentExif) { + // only replace first APP1 segment; skips others. + if (APP1Written) { + continue; + } + APP1Written = true; + + if (newBytes == null) { + continue; + } + + final byte[] markerBytes = ByteConversions.toBytes((short) JpegConstants.JPEG_APP1_MARKER, getByteOrder()); + if (newBytes.length > 0xffff) { + throw new ExifOverflowException( + "APP1 Segment is too long: " + newBytes.length); + } + final int markerLength = newBytes.length + 2; + final byte[] markerLengthBytes = ByteConversions.toBytes((short) markerLength, getByteOrder()); + + os.write(markerBytes); + os.write(markerLengthBytes); + os.write(newBytes); + } else { + piece.write(os); + } + } + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, os); + } + } + + public static class ExifOverflowException extends ImageWriteException { + private static final long serialVersionUID = 1401484357224931218L; + + public ExifOverflowException(final String message) { + super(message); + } + } + + private byte[] writeExifSegment(final TiffImageWriterBase writer, + final TiffOutputSet outputSet, final boolean includeEXIFPrefix) + throws IOException, ImageWriteException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + if (includeEXIFPrefix) { + JpegConstants.EXIF_IDENTIFIER_CODE.writeTo(os); + os.write(0); + os.write(0); + } + + writer.write(os, outputSet); + + return os.toByteArray(); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCBlock.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcBlock.java similarity index 78% rename from src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCBlock.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcBlock.java index 7d9f8df..b8badb8 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCBlock.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcBlock.java @@ -1,39 +1,36 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -/* - * Represents an IPTC block, a set of key-value pairs of Photoshop IPTC data. - */ -public class IPTCBlock -{ - public final int blockType; - public final byte[] blockNameBytes; - public final byte[] blockData; - - public IPTCBlock(int blockType, byte[] blockNameBytes, byte[] blockData) - { - this.blockData = blockData; - this.blockNameBytes = blockNameBytes; - this.blockType = blockType; - } - - public boolean isIPTCBlock() - { - return blockType == IPTCParser.IMAGE_RESOURCE_BLOCK_IPTC_DATA; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +/* + * Represents an IPTC block, a set of key-value pairs of Photoshop IPTC data. + */ +public class IptcBlock { + public final int blockType; + public final byte[] blockNameBytes; + public final byte[] blockData; + + public IptcBlock(final int blockType, final byte[] blockNameBytes, final byte[] blockData) { + this.blockData = blockData; + this.blockNameBytes = blockNameBytes; + this.blockType = blockType; + } + + public boolean isIPTCBlock() { + return blockType == IptcConstants.IMAGE_RESOURCE_BLOCK_IPTC_DATA; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcConstants.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcConstants.java new file mode 100644 index 0000000..4768efa --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcConstants.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + + +public final class IptcConstants { + public static final int IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE = 32767; + + public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP2_INFO = 0x03e8; + public static final int IMAGE_RESOURCE_BLOCK_MACINTOSH_PRINT_INFO = 0x03e9; + public static final int IMAGE_RESOURCE_BLOCK_XML_DATA = 0x03ea; + public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP2_COLOR_TABLE = 0x03eb; + public static final int IMAGE_RESOURCE_BLOCK_RESOLUTION_INFO = 0x03ed; + public static final int IMAGE_RESOURCE_BLOCK_ALPHA_CHANNELS_NAMES = 0x03ee; + public static final int IMAGE_RESOURCE_BLOCK_DISPLAY_INFO = 0x03ef; + public static final int IMAGE_RESOURCE_BLOCK_PSTRING_CAPTION = 0x03f0; + public static final int IMAGE_RESOURCE_BLOCK_BORDER_INFORMATION = 0x03f1; + public static final int IMAGE_RESOURCE_BLOCK_BACKGROUND_COLOR = 0x03f2; + public static final int IMAGE_RESOURCE_BLOCK_PRINT_FLAGS = 0x03f3; + public static final int IMAGE_RESOURCE_BLOCK_BW_HALFTONING_INFO = 0x03f4; + public static final int IMAGE_RESOURCE_BLOCK_COLOR_HALFTONING_INFO = 0x03f5; + public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_HALFTONING_INFO = 0x03f6; + public static final int IMAGE_RESOURCE_BLOCK_BW_TRANSFER_FUNC = 0x03f7; + public static final int IMAGE_RESOURCE_BLOCK_COLOR_TRANSFER_FUNCS = 0x03f8; + public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_TRANSFER_FUNCS = 0x03f9; + public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_IMAGE_INFO = 0x03fa; + public static final int IMAGE_RESOURCE_BLOCK_EFFECTIVE_BW = 0x03fb; + public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG1 = 0x03fc; + public static final int IMAGE_RESOURCE_BLOCK_EPS_OPTIONS = 0x03fd; + public static final int IMAGE_RESOURCE_BLOCK_QUICK_MASK_INFO = 0x03fe; + public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG2 = 0x03ff; + public static final int IMAGE_RESOURCE_BLOCK_LAYER_STATE_INFO = 0x0400; + public static final int IMAGE_RESOURCE_BLOCK_WORKING_PATH = 0x0401; + public static final int IMAGE_RESOURCE_BLOCK_LAYERS_GROUP_INFO = 0x0402; + public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG3 = 0x0403; + public static final int IMAGE_RESOURCE_BLOCK_IPTC_DATA = 0x0404; + public static final int IMAGE_RESOURCE_BLOCK_RAW_IMAGE_MODE = 0x0405; + public static final int IMAGE_RESOURCE_BLOCK_JPEG_QUALITY = 0x0406; + public static final int IMAGE_RESOURCE_BLOCK_GRID_GUIDES_INFO = 0x0408; + public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP_BGR_THUMBNAIL = 0x0409; + public static final int IMAGE_RESOURCE_BLOCK_COPYRIGHT_FLAG = 0x040a; + public static final int IMAGE_RESOURCE_BLOCK_URL = 0x040b; + public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP_THUMBNAIL = 0x040c; + public static final int IMAGE_RESOURCE_BLOCK_GLOBAL_ANGLE = 0x040d; + public static final int IMAGE_RESOURCE_BLOCK_COLOR_SAMPLERS_RESOURCE = 0x040e; + public static final int IMAGE_RESOURCE_BLOCK_ICC_PROFILE = 0x040f; + public static final int IMAGE_RESOURCE_BLOCK_WATERMARK = 0x0410; + public static final int IMAGE_RESOURCE_BLOCK_ICC_UNTAGGED = 0x0411; + public static final int IMAGE_RESOURCE_BLOCK_EFFECTS_VISIBLE = 0x0412; + public static final int IMAGE_RESOURCE_BLOCK_SPOT_HALFTONE = 0x0413; + public static final int IMAGE_RESOURCE_BLOCK_IDS_BASE_VALUE = 0x0414; + public static final int IMAGE_RESOURCE_BLOCK_UNICODE_ALPHA_NAMES = 0x0415; + public static final int IMAGE_RESOURCE_BLOCK_INDEXED_COLOUR_TABLE_COUNT = 0x0416; + public static final int IMAGE_RESOURCE_BLOCK_TRANSPARENT_INDEX = 0x0417; + public static final int IMAGE_RESOURCE_BLOCK_GLOBAL_ALTITUDE = 0x0419; + public static final int IMAGE_RESOURCE_BLOCK_SLICES = 0x041a; + public static final int IMAGE_RESOURCE_BLOCK_WORKFLOW_URL = 0x041b; + public static final int IMAGE_RESOURCE_BLOCK_JUMP_TO_XPEP = 0x041c; + public static final int IMAGE_RESOURCE_BLOCK_ALPHA_IDENTIFIERS = 0x041d; + public static final int IMAGE_RESOURCE_BLOCK_URL_LIST = 0x041e; + public static final int IMAGE_RESOURCE_BLOCK_VERSION_INFO = 0x0421; + public static final int IMAGE_RESOURCE_BLOCK_EXIFINFO = 0x0422; + public static final int IMAGE_RESOURCE_BLOCK_EXIF_INFO2 = 0x0423; + public static final int IMAGE_RESOURCE_BLOCK_XMP = 0x0424; + public static final int IMAGE_RESOURCE_BLOCK_CAPTION_DIGEST = 0x0425; + public static final int IMAGE_RESOURCE_BLOCK_PRINT_SCALE = 0x0426; + public static final int IMAGE_RESOURCE_BLOCK_PIXEL_ASPECT_RATIO = 0x0428; + public static final int IMAGE_RESOURCE_BLOCK_LAYER_COMPS = 0x0429; + public static final int IMAGE_RESOURCE_BLOCK_ALTERNATE_DUOTONE_COLORS = 0x042a; + public static final int IMAGE_RESOURCE_BLOCK_ALTERNATE_SPOT_COLORS = 0x042b; + public static final int IMAGE_RESOURCE_BLOCK_CLIPPING_PATH_NAME = 0x0bb7; + public static final int IMAGE_RESOURCE_BLOCK_PRINT_FLAGS_INFO = 0x2710; + + public static final int IPTC_RECORD_TAG_MARKER = 0x1c; + public static final int IPTC_ENVELOPE_RECORD_NUMBER = 0x01; + public static final int IPTC_APPLICATION_2_RECORD_NUMBER = 0x02; + + private IptcConstants() { + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java new file mode 100644 index 0000000..17698a0 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java @@ -0,0 +1,460 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.iptc; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.ImagingConstants; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.util.Debug; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class IptcParser extends BinaryFileParser { + private static final ByteOrder APP13_BYTE_ORDER = ByteOrder.BIG_ENDIAN; + + public IptcParser() { + setByteOrder(ByteOrder.BIG_ENDIAN); + } + + public boolean isPhotoshopJpegSegment(final byte[] segmentData) { + if (!startsWith(segmentData, + JpegConstants.PHOTOSHOP_IDENTIFICATION_STRING)) { + return false; + } + + final int index = JpegConstants.PHOTOSHOP_IDENTIFICATION_STRING.size(); + return (index + 4) <= segmentData.length + && ByteConversions.toInt(segmentData, index, APP13_BYTE_ORDER) == JpegConstants.CONST_8BIM; + } + + /* + * In practice, App13 segments are only used for Photoshop/IPTC metadata. + * However, we should not treat App13 signatures without Photoshop's + * signature as Photoshop/IPTC segments. + * + * A Photoshop/IPTC App13 segment begins with the Photoshop Identification + * string. + * + * There follows 0-N blocks (Photoshop calls them "Image Resource Blocks"). + * + * Each block has the following structure: + * + * 1. 4-byte type. This is always "8BIM" for blocks in a Photoshop App13 + * segment. 2. 2-byte id. IPTC data is stored in blocks with id 0x0404, aka. + * IPTC_NAA_RECORD_IMAGE_RESOURCE_ID 3. Block name as a Pascal String. This + * is padded to have an even length. 4. 4-byte size (in bytes). 5. Block + * data. This is also padded to have an even length. + * + * The block data consists of a 0-N records. A record has the following + * structure: + * + * 1. 2-byte prefix. The value is always 0x1C02 2. 1-byte record type. The + * record types are documented by the IPTC. See IptcConstants. 3. 2-byte + * record size (in bytes). 4. Record data, "record size" bytes long. + * + * Record data (unlike block data) is NOT padded to have an even length. + * + * Record data, for IPTC record, should always be ISO-8859-1. But according + * to SANSELAN-33, this isn't always the case. + * + * The exception is the first record in the block, which must always be a + * record version record, whose value is a two-byte number; the value is + * 0x02. + * + * Some IPTC blocks are missing this first "record version" record, so we + * don't require it. + */ + public PhotoshopApp13Data parsePhotoshopSegment(final byte[] bytes, final Map params) + throws ImageReadException, IOException { + final boolean strict = params != null && Boolean.TRUE.equals(params.get(ImagingConstants.PARAM_KEY_STRICT)); + final boolean verbose = params != null && Boolean.TRUE.equals(params.get(ImagingConstants.PARAM_KEY_VERBOSE)); + + return parsePhotoshopSegment(bytes, verbose, strict); + } + + public PhotoshopApp13Data parsePhotoshopSegment(final byte[] bytes, + final boolean verbose, final boolean strict) throws ImageReadException, + IOException { + final List records = new ArrayList(); + + final List blocks = parseAllBlocks(bytes, verbose, strict); + + for (IptcBlock block : blocks) { + // Ignore everything but IPTC data. + if (!block.isIPTCBlock()) { + continue; + } + + records.addAll(parseIPTCBlock(block.blockData, verbose)); + } + + return new PhotoshopApp13Data(records, blocks); + } + + protected List parseIPTCBlock(final byte[] bytes, final boolean verbose) + throws IOException { + final List elements = new ArrayList(); + + int index = 0; + // Integer recordVersion = null; + while (index + 1 < bytes.length) { + final int tagMarker = 0xff & bytes[index++]; + if (verbose) { + Debug.debug("tagMarker: " + tagMarker + " (0x" + Integer.toHexString(tagMarker) + ")"); + } + + if (tagMarker != IptcConstants.IPTC_RECORD_TAG_MARKER) { + if (verbose) { + System.out.println("Unexpected record tag marker in IPTC data."); + } + return elements; + } + + final int recordNumber = 0xff & bytes[index++]; + if (verbose) { + Debug.debug("recordNumber: " + recordNumber + " (0x" + Integer.toHexString(recordNumber) + ")"); + } + + // int recordPrefix = convertByteArrayToShort("recordPrefix", index, + // bytes); + // if (verbose) + // Debug.debug("recordPrefix", recordPrefix + " (0x" + // + Integer.toHexString(recordPrefix) + ")"); + // index += 2; + // + // if (recordPrefix != IPTC_RECORD_PREFIX) + // { + // if (verbose) + // System.out + // .println("Unexpected record prefix in IPTC data!"); + // return elements; + // } + + // throw new ImageReadException( + // "Unexpected record prefix in IPTC data."); + + final int recordType = 0xff & bytes[index]; + if (verbose) { + Debug.debug("recordType: " + recordType + " (0x" + Integer.toHexString(recordType) + ")"); + } + index++; + + final int recordSize = ByteConversions.toUInt16(bytes, index, getByteOrder()); + index += 2; + + final boolean extendedDataset = recordSize > IptcConstants.IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE; + final int dataFieldCountLength = recordSize & 0x7fff; + if (extendedDataset && verbose) { + Debug.debug("extendedDataset. dataFieldCountLength: " + + dataFieldCountLength); + } + if (extendedDataset) { + // ignore extended dataset and everything after. + return elements; + } + + final byte[] recordData = slice(bytes, index, recordSize); + index += recordSize; + + // Debug.debug("recordSize", recordSize + " (0x" + // + Integer.toHexString(recordSize) + ")"); + + if (recordNumber != IptcConstants.IPTC_APPLICATION_2_RECORD_NUMBER) { + continue; + } + + if (recordType == 0) { + if (verbose) { + System.out.println("ignore record version record! " + + elements.size()); + } + // ignore "record version" record; + continue; + } + // if (recordVersion == null) + // { + // // The first record in a JPEG/Photoshop IPTC block must be + // // the record version. + // if (recordType != 0) + // throw new ImageReadException("Missing record version: " + // + recordType); + // recordVersion = new Integer(convertByteArrayToShort( + // "recordNumber", recordData)); + // + // if (recordSize != 2) + // throw new ImageReadException( + // "Invalid record version record size: " + recordSize); + // + // // JPEG/Photoshop IPTC metadata is always in Record version + // // 2 + // if (recordVersion.intValue() != 2) + // throw new ImageReadException( + // "Invalid IPTC record version: " + recordVersion); + // + // // Debug.debug("recordVersion", recordVersion); + // continue; + // } + + final String value = new String(recordData, "ISO-8859-1"); + + final IptcType iptcType = IptcTypeLookup.getIptcType(recordType); + + // Debug.debug("iptcType", iptcType); + // debugByteArray("iptcData", iptcData); + // Debug.debug(); + + // if (recordType == IPTC_TYPE_CREDIT.type + // || recordType == IPTC_TYPE_OBJECT_NAME.type) + // { + // this.debugByteArray("recordData", recordData); + // Debug.debug("index", IPTC_TYPE_CREDIT.name); + // } + + final IptcRecord element = new IptcRecord(iptcType, recordData, value); + elements.add(element); + } + + return elements; + } + + protected List parseAllBlocks(final byte[] bytes, final boolean verbose, + final boolean strict) throws ImageReadException, IOException { + final List blocks = new ArrayList(); + + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + + // Note that these are unsigned quantities. Name is always an even + // number of bytes (including the 1st byte, which is the size.) + + final byte[] idString = readBytes("", bis, + JpegConstants.PHOTOSHOP_IDENTIFICATION_STRING.size(), + "App13 Segment missing identification string"); + if (!JpegConstants.PHOTOSHOP_IDENTIFICATION_STRING.equals(idString)) { + throw new ImageReadException("Not a Photoshop App13 Segment"); + } + + // int index = PHOTOSHOP_IDENTIFICATION_STRING.length; + + while (true) { + final int imageResourceBlockSignature; + try { + imageResourceBlockSignature = read4Bytes("", bis, + "Image Resource Block missing identification string", APP13_BYTE_ORDER); + } catch (final IOException ioEx) { + break; + } + if (imageResourceBlockSignature != JpegConstants.CONST_8BIM) { + throw new ImageReadException( + "Invalid Image Resource Block Signature"); + } + + final int blockType = read2Bytes("", bis, "Image Resource Block missing type", APP13_BYTE_ORDER); + if (verbose) { + Debug.debug("blockType: " + blockType + " (0x" + Integer.toHexString(blockType) + ")"); + } + + final int blockNameLength = readByte("Name length", bis, "Image Resource Block missing name length"); + if (verbose && blockNameLength > 0) { + Debug.debug("blockNameLength: " + blockNameLength + " (0x" + + Integer.toHexString(blockNameLength) + ")"); + } + byte[] blockNameBytes; + if (blockNameLength == 0) { + readByte("Block name bytes", bis, "Image Resource Block has invalid name"); + blockNameBytes = new byte[0]; + } else { + try { + blockNameBytes = readBytes("", bis, blockNameLength, + "Invalid Image Resource Block name"); + } catch (final IOException ioEx) { + if (strict) { + throw ioEx; + } + break; + } + + if (blockNameLength % 2 == 0) { + readByte("Padding byte", bis, "Image Resource Block missing padding byte"); + } + } + + final int blockSize = read4Bytes("", bis, "Image Resource Block missing size", APP13_BYTE_ORDER); + if (verbose) { + Debug.debug("blockSize: " + blockSize + " (0x" + Integer.toHexString(blockSize) + ")"); + } + + /* + * doesn't catch cases where blocksize is invalid but is still less + * than bytes.length but will at least prevent OutOfMemory errors + */ + if (blockSize > bytes.length) { + throw new ImageReadException("Invalid Block Size : " + blockSize + " > " + bytes.length); + } + + final byte[] blockData; + try { + blockData = readBytes("", bis, blockSize, "Invalid Image Resource Block data"); + } catch (final IOException ioEx) { + if (strict) { + throw ioEx; + } + break; + } + + blocks.add(new IptcBlock(blockType, blockNameBytes, blockData)); + + if ((blockSize % 2) != 0) { + readByte("Padding byte", bis, "Image Resource Block missing padding byte"); + } + } + + canThrow = true; + return blocks; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + // private void writeIPTCRecord(BinaryOutputStream bos, ) + + public byte[] writePhotoshopApp13Segment(final PhotoshopApp13Data data) + throws IOException, ImageWriteException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + final BinaryOutputStream bos = new BinaryOutputStream(os); + + JpegConstants.PHOTOSHOP_IDENTIFICATION_STRING.writeTo(bos); + + final List blocks = data.getRawBlocks(); + for (IptcBlock block : blocks) { + bos.write4Bytes(JpegConstants.CONST_8BIM); + + if (block.blockType < 0 || block.blockType > 0xffff) { + throw new ImageWriteException("Invalid IPTC block type."); + } + bos.write2Bytes(block.blockType); + + if (block.blockNameBytes.length > 255) { + throw new ImageWriteException("IPTC block name is too long: " + + block.blockNameBytes.length); + } + bos.write(block.blockNameBytes.length); + bos.write(block.blockNameBytes); + if (block.blockNameBytes.length % 2 == 0) { + bos.write(0); // pad to even size, including length byte. + } + + if (block.blockData.length > IptcConstants.IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE) { + throw new ImageWriteException("IPTC block data is too long: " + + block.blockData.length); + } + bos.write4Bytes(block.blockData.length); + bos.write(block.blockData); + if (block.blockData.length % 2 == 1) { + bos.write(0); // pad to even size + } + + } + + bos.flush(); + return os.toByteArray(); + } + + public byte[] writeIPTCBlock(List elements) + throws ImageWriteException, IOException { + byte[] blockData; + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BinaryOutputStream bos = null; + boolean canThrow = false; + try { + bos = new BinaryOutputStream(baos, + getByteOrder()); + + // first, right record version record + bos.write(IptcConstants.IPTC_RECORD_TAG_MARKER); + bos.write(IptcConstants.IPTC_APPLICATION_2_RECORD_NUMBER); + bos.write(IptcTypes.RECORD_VERSION.type); // record version record + // type. + bos.write2Bytes(2); // record version record size + bos.write2Bytes(2); // record version value + + // make a copy of the list. + elements = new ArrayList(elements); + + // sort the list. Records must be in numerical order. + final Comparator comparator = new Comparator() { + public int compare(final IptcRecord e1, final IptcRecord e2) { + return e2.iptcType.getType() - e1.iptcType.getType(); + } + }; + Collections.sort(elements, comparator); + // TODO: make sure order right + + // write the list. + for (IptcRecord element : elements) { + if (element.iptcType == IptcTypes.RECORD_VERSION) { + continue; // ignore + } + + bos.write(IptcConstants.IPTC_RECORD_TAG_MARKER); + bos.write(IptcConstants.IPTC_APPLICATION_2_RECORD_NUMBER); + if (element.iptcType.getType() < 0 + || element.iptcType.getType() > 0xff) { + throw new ImageWriteException("Invalid record type: " + + element.iptcType.getType()); + } + bos.write(element.iptcType.getType()); + + final byte[] recordData = element.value.getBytes("ISO-8859-1"); + if (!new String(recordData, "ISO-8859-1").equals(element.value)) { + throw new ImageWriteException( + "Invalid record value, not ISO-8859-1"); + } + + bos.write2Bytes(recordData.length); + bos.write(recordData); + } + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bos); + } + + blockData = baos.toByteArray(); + + return blockData; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcRecord.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcRecord.java new file mode 100644 index 0000000..97b0011 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcRecord.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +import java.io.UnsupportedEncodingException; +import java.util.Comparator; + +/* + * Represents an IPTC record, a single key-value pair of Photoshop IPTC data. + */ +public class IptcRecord { + public final IptcType iptcType; + private final byte[] bytes; + public final String value; + public static final Comparator COMPARATOR = new Comparator() { + public int compare(final IptcRecord e1, final IptcRecord e2) { + return e1.iptcType.getType() - e2.iptcType.getType(); + } + }; + + public IptcRecord(final IptcType iptcType, final byte[] bytes, final String value) { + this.iptcType = iptcType; + this.bytes = bytes; + this.value = value; + } + + public IptcRecord(final IptcType iptcType, final String value) { + this.iptcType = iptcType; + byte[] tempBytes; + try { + tempBytes = value.getBytes("ISO-8859-1"); + } catch (final UnsupportedEncodingException cannotHappen) { + tempBytes = null; + } + this.bytes = tempBytes; + this.value = value; + } + + public byte[] getRawBytes() { + return bytes.clone(); + } + + public String getValue() { + return value; + } + + public String getIptcTypeName() { + return iptcType.getName(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcType.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcType.java new file mode 100644 index 0000000..c3db24d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcType.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +public interface IptcType { + int getType(); + + String getName(); + + String toString(); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypeLookup.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypeLookup.java new file mode 100644 index 0000000..b3f3990 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypeLookup.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +import java.util.HashMap; +import java.util.Map; + +public final class IptcTypeLookup { + + private static final Map IPTC_TYPE_MAP = new HashMap(); + static { + for (final IptcType iptcType : IptcTypes.values()) { + IPTC_TYPE_MAP.put(iptcType.getType(), iptcType); + } + } + + private IptcTypeLookup() { + } + + public static IptcType getIptcType(final int type) { + if (!IPTC_TYPE_MAP.containsKey(type)) { + return IptcTypes.getUnknown(type); + } + return IPTC_TYPE_MAP.get(type); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypes.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypes.java new file mode 100644 index 0000000..fa1df82 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcTypes.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +public enum IptcTypes implements IptcType { + RECORD_VERSION( + 0, "Record Version"), + OBJECT_TYPE_REFERENCE( + 3, "Object Type Reference"), + OBJECT_ATTRIBUTE_REFERENCE( + 4, "Object Attribute Reference"), + OBJECT_NAME( + 5, "Object Name"), + EDIT_STATUS( + 7, "Edit Status"), + EDITORIAL_UPDATE( + 8, "Editorial Update"), + URGENCY( + 10, "Urgency"), + SUBJECT_REFERENCE( + 12, "Subject Reference"), + CATEGORY( + 15, "Category"), + SUPPLEMENTAL_CATEGORY( + 20, "Supplemental Category"), + FIXTURE_IDENTIFIER( + 22, "Fixture Identifier"), + KEYWORDS( + 25, "Keywords"), + CONTENT_LOCATION_CODE( + 26, "Content Location Code"), + CONTENT_LOCATION_NAME( + 27, "Content Location Name"), + RELEASE_DATE( + 30, "Release Date"), + RELEASE_TIME( + 35, "Release Time"), + EXPIRATION_DATE( + 37, "Expiration Date"), + EXPIRATION_TIME( + 38, "Expiration Time"), + SPECIAL_INSTRUCTIONS( + 40, "Special Instructions"), + ACTION_ADVISED( + 42, "Action Advised"), + REFERENCE_SERVICE( + 45, "Reference Service"), + REFERENCE_DATE( + 47, "Reference Date"), + REFERENCE_NUMBER( + 50, "Reference Number"), + DATE_CREATED( + 55, "Date Created"), + TIME_CREATED( + 60, "Time Created"), + DIGITAL_CREATION_DATE( + 62, "Digital Creation Date"), + DIGITAL_CREATION_TIME( + 63, "Digital Creation Time"), + ORIGINATING_PROGRAM( + 65, "Originating Program"), + PROGRAM_VERSION( + 70, "Program Version"), + OBJECT_CYCLE( + 75, "Object Cycle"), + BYLINE( + 80, "By-line"), + BYLINE_TITLE( + 85, "By-line Title"), + CITY( + 90, "City"), + SUBLOCATION( + 92, "Sublocation"), + PROVINCE_STATE( + 95, "Province/State"), + COUNTRY_PRIMARY_LOCATION_CODE( + 100, "Country/Primary Location Code"), + COUNTRY_PRIMARY_LOCATION_NAME( + 101, "Country/Primary Location Name"), + ORIGINAL_TRANSMISSION_REFERENCE( + 103, "Original Transmission, Reference"), + HEADLINE( + 105, "Headline"), + CREDIT( + 110, "Credit"), + SOURCE( + 115, "Source"), + COPYRIGHT_NOTICE( + 116, "Copyright Notice"), + CONTACT( + 118, "Contact"), + CAPTION_ABSTRACT( + 120, "Caption/Abstract"), + WRITER_EDITOR( + 122, "Writer/Editor"), + RASTERIZED_CAPTION( + 125, "Rasterized Caption"), + IMAGE_TYPE( + 130, "ImageType"), + IMAGE_ORIENTATION( + 131, "Image Orientation"), + LANGUAGE_IDENTIFIER( + 135, "Language Identifier"), + AUDIO_TYPE( + 150, "Audio Type"), + AUDIO_SAMPLING_RATE( + 151, "Audio Sampling Rate"), + AUDIO_SAMPLING_RESOLUTION( + 152, "Audio Sampling Resolution"), + AUDIO_DURATION( + 153, "Audio Duration"), + AUDIO_OUTCUE( + 154, "Audio Outcue"), + OBJECT_DATA_PREVIEW_FILE_FORMAT( + 200, "Object Data Preview, File Format"), + OBJECT_DATA_PREVIEW_FILE_FORMAT_VERSION( + 201, "Object Data Preview, File Format Version"), + OBJECT_DATA_PREVIEW_DATA( + 202, "Object Data Preview Data"); + + public final int type; + public final String name; + + IptcTypes(final int type, final String name) { + this.type = type; + this.name = name; + } + + public String getName() { + return name; + } + + public int getType() { + return type; + } + + @Override + public String toString() { + return name + " (" + type + ")"; + } + + public static IptcType getUnknown(final int type) { + return new IptcType() { + public String getName() { + return "Unknown"; + } + + public int getType() { + return type; + } + + @Override + public String toString() { + return "Unknown (" + type + ")"; + } + }; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/JpegIptcRewriter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/JpegIptcRewriter.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/jpeg/iptc/JpegIptcRewriter.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/JpegIptcRewriter.java index 7971814..78c3dc4 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/JpegIptcRewriter.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/JpegIptcRewriter.java @@ -1,257 +1,240 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.common.byteSources.ByteSourceInputStream; -import org.apache.sanselan.formats.jpeg.xmp.JpegRewriter; - -/** - * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. - *

- *

- * See the source of the IPTCUpdateExample class for example usage. - * - * @see org.apache.sanselan.sampleUsage.WriteIPTCExample - */ -public class JpegIptcRewriter extends JpegRewriter implements IPTCConstants -{ - - /** - * Reads a Jpeg image, removes all IPTC data from the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - *

- * - * @param src - * Image file. - * @param os - * OutputStream to write the image to. - * - * @see java.io.File - * @see java.io.OutputStream - */ - public void removeIPTC(File src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - removeIPTC(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all IPTC data from the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - *

- * - * @param src - * Byte array containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeIPTC(byte src[], OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - removeIPTC(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all IPTC data from the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - *

- * - * @param src - * InputStream containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeIPTC(InputStream src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - removeIPTC(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all IPTC data from the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - *

- * - * @param byteSource - * ByteSource containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeIPTC(ByteSource byteSource, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List oldPieces = jfifPieces.pieces; - List photoshopApp13Segments = findPhotoshopApp13Segments(oldPieces); - - if (photoshopApp13Segments.size() > 1) - throw new ImageReadException( - "Image contains more than one Photoshop App13 segment."); - List newPieces = removePhotoshopApp13Segments(oldPieces); - if (photoshopApp13Segments.size() == 1) - { - JFIFPieceSegment oldSegment = (JFIFPieceSegment) photoshopApp13Segments - .get(0); - Map params = new HashMap(); - PhotoshopApp13Data oldData = new IPTCParser() - .parsePhotoshopSegment(oldSegment.segmentData, params); - List newBlocks = oldData.getNonIptcBlocks(); - List newRecords = new ArrayList(); - PhotoshopApp13Data newData = new PhotoshopApp13Data(newRecords, - newBlocks); - byte segmentBytes[] = new IPTCParser() - .writePhotoshopApp13Segment(newData); - JFIFPieceSegment newSegment = new JFIFPieceSegment( - oldSegment.marker, segmentBytes); - newPieces.add(oldPieces.indexOf(oldSegment), newSegment); - } - writeSegments(os, newPieces); - } - - /** - * Reads a Jpeg image, replaces the IPTC data in the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - * - * @param src - * Byte array containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param newData - * structure containing IPTC data. - */ - public void writeIPTC(byte src[], OutputStream os, - PhotoshopApp13Data newData) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - writeIPTC(byteSource, os, newData); - } - - /** - * Reads a Jpeg image, replaces the IPTC data in the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - * - * @param src - * InputStream containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param newData - * structure containing IPTC data. - */ - public void writeIPTC(InputStream src, OutputStream os, - PhotoshopApp13Data newData) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - writeIPTC(byteSource, os, newData); - } - - /** - * Reads a Jpeg image, replaces the IPTC data in the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - * - * @param src - * Image file. - * @param os - * OutputStream to write the image to. - * @param newData - * structure containing IPTC data. - */ - public void writeIPTC(File src, OutputStream os, PhotoshopApp13Data newData) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - writeIPTC(byteSource, os, newData); - } - - /** - * Reads a Jpeg image, replaces the IPTC data in the App13 segment but - * leaves the other data in that segment (if present) unchanged and writes - * the result to a stream. - * - * @param byteSource - * ByteSource containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param newData - * structure containing IPTC data. - */ - public void writeIPTC(ByteSource byteSource, OutputStream os, - PhotoshopApp13Data newData) throws ImageReadException, IOException, - ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List oldPieces = jfifPieces.pieces; - List photoshopApp13Segments = findPhotoshopApp13Segments(oldPieces); - - if (photoshopApp13Segments.size() > 1) - throw new ImageReadException( - "Image contains more than one Photoshop App13 segment."); - List newPieces = removePhotoshopApp13Segments(oldPieces); - - { - // discard old iptc blocks. - List newBlocks = newData.getNonIptcBlocks(); - byte[] newBlockBytes = new IPTCParser().writeIPTCBlock(newData - .getRecords()); - - int blockType = IMAGE_RESOURCE_BLOCK_IPTC_DATA; - byte[] blockNameBytes = new byte[0]; - IPTCBlock newBlock = new IPTCBlock(blockType, blockNameBytes, - newBlockBytes); - newBlocks.add(newBlock); - - newData = new PhotoshopApp13Data(newData.getRecords(), newBlocks); - - byte segmentBytes[] = new IPTCParser() - .writePhotoshopApp13Segment(newData); - JFIFPieceSegment newSegment = new JFIFPieceSegment( - JPEG_APP13_Marker, segmentBytes); - - newPieces = insertAfterLastAppSegments(newPieces, Arrays - .asList(new JFIFPieceSegment[] { newSegment, })); - } - - writeSegments(os, newPieces); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.iptc; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.formats.jpeg.xmp.JpegRewriter; + +/** + * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. + */ +public class JpegIptcRewriter extends JpegRewriter { + + /** + * Reads a Jpeg image, removes all IPTC data from the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + *

+ * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * + * @see java.io.File + * @see java.io.OutputStream + */ + public void removeIPTC(final File src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + removeIPTC(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all IPTC data from the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + *

+ * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeIPTC(final byte[] src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + removeIPTC(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all IPTC data from the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + *

+ * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeIPTC(final InputStream src, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + removeIPTC(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all IPTC data from the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + *

+ * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeIPTC(final ByteSource byteSource, final OutputStream os) + throws ImageReadException, IOException, ImageWriteException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + final List oldPieces = jfifPieces.pieces; + final List photoshopApp13Segments = findPhotoshopApp13Segments(oldPieces); + + if (photoshopApp13Segments.size() > 1) { + throw new ImageReadException( + "Image contains more than one Photoshop App13 segment."); + } + final List newPieces = removePhotoshopApp13Segments(oldPieces); + if (photoshopApp13Segments.size() == 1) { + final JFIFPieceSegment oldSegment = (JFIFPieceSegment) photoshopApp13Segments + .get(0); + final Map params = new HashMap(); + final PhotoshopApp13Data oldData = new IptcParser() + .parsePhotoshopSegment(oldSegment.segmentData, params); + final List newBlocks = oldData.getNonIptcBlocks(); + final List newRecords = new ArrayList(); + final PhotoshopApp13Data newData = new PhotoshopApp13Data(newRecords, + newBlocks); + final byte[] segmentBytes = new IptcParser() + .writePhotoshopApp13Segment(newData); + final JFIFPieceSegment newSegment = new JFIFPieceSegment( + oldSegment.marker, segmentBytes); + newPieces.add(oldPieces.indexOf(oldSegment), newSegment); + } + writeSegments(os, newPieces); + } + + /** + * Reads a Jpeg image, replaces the IPTC data in the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param newData + * structure containing IPTC data. + */ + public void writeIPTC(final byte[] src, final OutputStream os, + final PhotoshopApp13Data newData) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + writeIPTC(byteSource, os, newData); + } + + /** + * Reads a Jpeg image, replaces the IPTC data in the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param newData + * structure containing IPTC data. + */ + public void writeIPTC(final InputStream src, final OutputStream os, + final PhotoshopApp13Data newData) throws ImageReadException, IOException, + ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + writeIPTC(byteSource, os, newData); + } + + /** + * Reads a Jpeg image, replaces the IPTC data in the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * @param newData + * structure containing IPTC data. + */ + public void writeIPTC(final File src, final OutputStream os, final PhotoshopApp13Data newData) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + writeIPTC(byteSource, os, newData); + } + + /** + * Reads a Jpeg image, replaces the IPTC data in the App13 segment but + * leaves the other data in that segment (if present) unchanged and writes + * the result to a stream. + * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param newData + * structure containing IPTC data. + */ + public void writeIPTC(final ByteSource byteSource, final OutputStream os, + PhotoshopApp13Data newData) throws ImageReadException, IOException, + ImageWriteException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + final List oldPieces = jfifPieces.pieces; + final List photoshopApp13Segments = findPhotoshopApp13Segments(oldPieces); + + if (photoshopApp13Segments.size() > 1) { + throw new ImageReadException( + "Image contains more than one Photoshop App13 segment."); + } + List newPieces = removePhotoshopApp13Segments(oldPieces); + + { + // discard old iptc blocks. + final List newBlocks = newData.getNonIptcBlocks(); + final byte[] newBlockBytes = new IptcParser().writeIPTCBlock(newData.getRecords()); + + final int blockType = IptcConstants.IMAGE_RESOURCE_BLOCK_IPTC_DATA; + final byte[] blockNameBytes = new byte[0]; + final IptcBlock newBlock = new IptcBlock(blockType, blockNameBytes, newBlockBytes); + newBlocks.add(newBlock); + + newData = new PhotoshopApp13Data(newData.getRecords(), newBlocks); + + byte[] segmentBytes = new IptcParser().writePhotoshopApp13Segment(newData); + JFIFPieceSegment newSegment = new JFIFPieceSegment(JpegConstants.JPEG_APP13_MARKER, segmentBytes); + + newPieces = insertAfterLastAppSegments(newPieces, Arrays.asList(newSegment)); + } + + writeSegments(os, newPieces); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/PhotoshopApp13Data.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/PhotoshopApp13Data.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/jpeg/iptc/PhotoshopApp13Data.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/PhotoshopApp13Data.java index 8823778..7c6c5ad 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/PhotoshopApp13Data.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/PhotoshopApp13Data.java @@ -1,56 +1,51 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.jpeg.iptc; - -import java.util.ArrayList; -import java.util.List; - -public class PhotoshopApp13Data implements IPTCConstants -{ - private final List records; - private final List rawBlocks; - - public PhotoshopApp13Data(List records, List rawBlocks) - { - this.rawBlocks = rawBlocks; - this.records = records; - } - - public List getRecords() - { - return new ArrayList(records); - } - - public List getRawBlocks() - { - return new ArrayList(rawBlocks); - } - - public List getNonIptcBlocks() - { - List result = new ArrayList(); - for (int i = 0; i < rawBlocks.size(); i++) - { - IPTCBlock block = (IPTCBlock) rawBlocks.get(i); - if (!block.isIPTCBlock()) - result.add(block); - } - return result; - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.iptc; + +import java.util.ArrayList; +import java.util.List; + +public class PhotoshopApp13Data { + private final List records; + private final List rawBlocks; + + public PhotoshopApp13Data(final List records, + final List rawBlocks) { + this.rawBlocks = rawBlocks; + this.records = records; + } + + public List getRecords() { + return new ArrayList(records); + } + + public List getRawBlocks() { + return new ArrayList(rawBlocks); + } + + public List getNonIptcBlocks() { + final List result = new ArrayList(); + for (IptcBlock block : rawBlocks) { + if (!block.isIPTCBlock()) { + result.add(block); + } + } + return result; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/package-info.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/package-info.java new file mode 100644 index 0000000..d6d375e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The JPEG image format. + */ +package org.apache.commons.imaging.formats.jpeg; + diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/App13Segment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App13Segment.java similarity index 58% rename from src/main/java/org/apache/sanselan/formats/jpeg/segments/App13Segment.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App13Segment.java index 24a240a..18c63f4 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/App13Segment.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App13Segment.java @@ -1,82 +1,78 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.jpeg.JpegImageParser; -import org.apache.sanselan.formats.jpeg.iptc.IPTCParser; -import org.apache.sanselan.formats.jpeg.iptc.PhotoshopApp13Data; - -public class App13Segment extends APPNSegment -{ - protected final JpegImageParser parser; - - // public final ArrayList elements = new ArrayList(); - // public final boolean isIPTCJpegSegment; - - public App13Segment(JpegImageParser parser, int marker, byte segmentData[]) - throws ImageReadException, IOException - { - this(parser, marker, segmentData.length, new ByteArrayInputStream( - segmentData)); - } - - public App13Segment(JpegImageParser parser, int marker, int marker_length, - InputStream is) throws ImageReadException, IOException - { - super(marker, marker_length, is); - this.parser = parser; - - // isIPTCJpegSegment = new IPTCParser().isIPTCJpegSegment(bytes); - // if (isIPTCJpegSegment) - // { - // /* - // * In practice, App13 segments are only used for Photoshop/IPTC - // * metadata. However, we should not treat App13 signatures without - // * Photoshop's signature as Photoshop/IPTC segments. - // */ - // boolean verbose = false; - // boolean strict = false; - // elements.addAll(new IPTCParser().parseIPTCJPEGSegment(bytes, - // verbose, strict)); - // } - } - - public boolean isPhotoshopJpegSegment() throws ImageReadException, IOException - { - return new IPTCParser().isPhotoshopJpegSegment(bytes); - } - - public PhotoshopApp13Data parsePhotoshopSegment(Map params) - throws ImageReadException, IOException - { - /* - * In practice, App13 segments are only used for Photoshop/IPTC - * metadata. However, we should not treat App13 signatures without - * Photoshop's signature as Photoshop/IPTC segments. - */ - if (!new IPTCParser().isPhotoshopJpegSegment(bytes)) - return null; - - return new IPTCParser().parsePhotoshopSegment(bytes, params); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.jpeg.JpegImageParser; +import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser; +import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data; + +public class App13Segment extends AppnSegment { + protected final JpegImageParser parser; + + // public final List elements = new ArrayList(); + // public final boolean isIPTCJpegSegment; + + public App13Segment(final JpegImageParser parser, final int marker, final byte[] segmentData) + throws IOException { + this(parser, marker, segmentData.length, new ByteArrayInputStream( + segmentData)); + } + + public App13Segment(final JpegImageParser parser, final int marker, final int markerLength, + final InputStream is) throws IOException { + super(marker, markerLength, is); + this.parser = parser; + + // isIPTCJpegSegment = new IptcParser().isIPTCJpegSegment(bytes); + // if (isIPTCJpegSegment) + // { + // /* + // * In practice, App13 segments are only used for Photoshop/IPTC + // * metadata. However, we should not treat App13 signatures without + // * Photoshop's signature as Photoshop/IPTC segments. + // */ + // boolean verbose = false; + // boolean strict = false; + // elements.addAll(new IptcParser().parseIPTCJPEGSegment(bytes, + // verbose, strict)); + // } + } + + public boolean isPhotoshopJpegSegment() { + return new IptcParser().isPhotoshopJpegSegment(getSegmentData()); + } + + public PhotoshopApp13Data parsePhotoshopSegment(final Map params) + throws ImageReadException, IOException { + /* + * In practice, App13 segments are only used for Photoshop/IPTC + * metadata. However, we should not treat App13 signatures without + * Photoshop's signature as Photoshop/IPTC segments. + */ + if (!isPhotoshopJpegSegment()) { + return null; + } + + return new IptcParser().parsePhotoshopSegment(getSegmentData(), params); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App14Segment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App14Segment.java new file mode 100644 index 0000000..4e19b73 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App14Segment.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +/** + * http://www.aiim.org/documents/standards/PDF-Ref/References/Adobe/5116.DCT_Filter.pdf + */ +public class App14Segment extends AppnSegment { + private static final byte[] ADOBE_PREFIX; + public static final int ADOBE_COLOR_TRANSFORM_UNKNOWN = 0; + public static final int ADOBE_COLOR_TRANSFORM_YCbCr = 1; + public static final int ADOBE_COLOR_TRANSFORM_YCCK = 2; + + static { + byte[] adobe = null; + try { + adobe = "Adobe".getBytes("US-ASCII"); + } catch (final UnsupportedEncodingException cannotHappen) { // NOPMD - can't happen + } + ADOBE_PREFIX = adobe; + } + + public App14Segment(int marker, byte[] segmentData) throws IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public App14Segment(int marker, int markerLength, InputStream is) throws IOException { + super(marker, markerLength, is); + } + + public boolean isAdobeJpegSegment() { + return startsWith(getSegmentData(), ADOBE_PREFIX); + } + + public int getAdobeColorTransform() { + return 0xff & segmentData[11]; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App2Segment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App2Segment.java new file mode 100644 index 0000000..13ad768 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/App2Segment.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class App2Segment extends AppnSegment implements Comparable { + public final byte[] iccBytes; + public final int curMarker; + public final int numMarkers; + + public App2Segment(final int marker, final byte[] segmentData) + throws ImageReadException, IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public App2Segment(final int marker, int markerLength, final InputStream is2) + throws ImageReadException, IOException { + super(marker, markerLength, is2); + + if (startsWith(getSegmentData(), + JpegConstants.ICC_PROFILE_LABEL)) { + final InputStream is = new ByteArrayInputStream(getSegmentData()); + + readAndVerifyBytes(is, JpegConstants.ICC_PROFILE_LABEL, + "Not a Valid App2 Segment: missing ICC Profile label"); + + curMarker = readByte("curMarker", is, "Not a valid App2 Marker"); + numMarkers = readByte("numMarkers", is, "Not a valid App2 Marker"); + + markerLength -= JpegConstants.ICC_PROFILE_LABEL.size(); + markerLength -= (1 + 1); + + iccBytes = readBytes("App2 Data", is, markerLength, "Invalid App2 Segment: insufficient data"); + } else { + // debugByteArray("Unknown APP2 Segment Type", bytes); + curMarker = -1; + numMarkers = -1; + iccBytes = null; + } + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof App2Segment) { + final App2Segment other = (App2Segment) obj; + return curMarker == other.curMarker; + } + return false; + } + + @Override + public int hashCode() { + return curMarker; + } + + public int compareTo(final App2Segment other) { + return curMarker - other.curMarker; + } + + // public String getDescription() + // { + // return "APPN (APP" + // + (marker - JpegImageParser.JPEG_APP0) + // + ") (" + getDescription() + ")"; + // } +} diff --git a/src/main/java/org/apache/sanselan/icc/IccTagDataType.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/AppnSegment.java similarity index 63% rename from src/main/java/org/apache/sanselan/icc/IccTagDataType.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/segments/AppnSegment.java index 18f228e..bee3a41 100644 --- a/src/main/java/org/apache/sanselan/icc/IccTagDataType.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/AppnSegment.java @@ -1,36 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public abstract class IccTagDataType -{ - public final String name; - public final int signature; - - public IccTagDataType(String name, int signature) - { - this.name = name; - this.signature = signature; - } - - public abstract void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException; -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +public class AppnSegment extends GenericSegment { + + public AppnSegment(int marker, int markerLength, InputStream is) throws IOException { + super(marker, markerLength, is); + } + + @Override + public String getDescription() { + return "APPN (APP" + (marker - JpegConstants.JPEG_APP0_MARKER) + ") (" + getSegmentType() + ")"; + } +} diff --git a/src/main/java/org/apache/sanselan/util/DebugInputStream.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/ComSegment.java similarity index 51% rename from src/main/java/org/apache/sanselan/util/DebugInputStream.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/segments/ComSegment.java index 88c38a1..5b7157d 100644 --- a/src/main/java/org/apache/sanselan/util/DebugInputStream.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/ComSegment.java @@ -1,62 +1,50 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.io.IOException; -import java.io.InputStream; - -public class DebugInputStream extends InputStream -{ - private final InputStream is; - - public DebugInputStream(InputStream is) - { - this.is = is; - } - - private long bytes_read = 0; - - public int read() throws IOException - { - int result = is.read(); - bytes_read++; - return result; - } - - public long skip(long n) throws IOException - { - long result = is.skip(n); - bytes_read += n; - return result; - } - - public int available() throws IOException - { - return is.available(); - } - - public void close() throws IOException - { - is.close(); - } - - public long getBytesRead() - { - return bytes_read; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +public class ComSegment extends GenericSegment { + public ComSegment(final int marker, final byte[] segmentData) { + super(marker, segmentData); + } + + public ComSegment(int marker, int markerLength, InputStream is) throws IOException { + super(marker, markerLength, is); + } + + /** + * Returns a copy of the comment. + * @return a copy of the comment's bytes + */ + public byte[] getComment() { + return getSegmentData(); + } + + @Override + public String getDescription() { + String commentString = ""; + try { + commentString = new String(segmentData, "UTF-8"); + } catch (final UnsupportedEncodingException cannotHappen) { // NOPMD + } + return "COM (" + commentString + ")"; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DhtSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DhtSegment.java new file mode 100644 index 0000000..e76ed2f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DhtSegment.java @@ -0,0 +1,185 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class DhtSegment extends Segment { + public final List huffmanTables; + + public static class HuffmanTable { + // some arrays are better off one-based + // to avoid subtractions by one later when indexing them + public final int tableClass; + public final int destinationIdentifier; + private final int[] bits; // 1-based + private final int[] huffVal; // 0-based + + // derived properties: + private final int[] huffSize = new int[16 * 256]; // 0-based + private final int[] huffCode; // 0-based + private final int[] minCode = new int[1 + 16]; // 1-based + private final int[] maxCode = new int[1 + 16]; // 1-based + private final int[] valPtr = new int[1 + 16]; // 1-based + + public HuffmanTable(final int tableClass, final int destinationIdentifier, + final int[] bits, final int[] huffVal) { + this.tableClass = tableClass; + this.destinationIdentifier = destinationIdentifier; + this.bits = bits; + this.huffVal = huffVal; + + // "generate_size_table", section C.2, figure C.1, page 51 of ITU-T + // T.81: + int k = 0; + int i = 1; + int j = 1; + int lastK = -1; + while (true) { + if (j > bits[i]) { + i++; + j = 1; + if (i > 16) { + huffSize[k] = 0; + lastK = k; + break; + } + } else { + huffSize[k] = i; + k++; + j++; + } + } + + // "generate_code_table", section C.2, figure C.2, page 52 of ITU-T + // T.81: + k = 0; + int code = 0; + int si = huffSize[0]; + huffCode = new int[lastK]; + while (true) { + huffCode[k] = code; + code++; + k++; + + if (huffSize[k] == si) { + continue; + } + if (huffSize[k] == 0) { + break; + } + do { + code <<= 1; + si++; + } while (huffSize[k] != si); + } + + // "Decoder_tables", section F.2.2.3, figure F.15, page 108 of T.81: + i = 0; + j = 0; + while (true) { + i++; + if (i > 16) { + break; + } + if (bits[i] == 0) { + maxCode[i] = -1; + } else { + valPtr[i] = j; + minCode[i] = huffCode[j]; + j += bits[i] - 1; + maxCode[i] = huffCode[j]; + j++; + } + } + + } + + public int[] getBits() { + return bits; + } + + public int[] getHuffVal() { + return huffVal; + } + + public int[] getHuffSize() { + return huffSize; + } + + public int[] getHuffCode() { + return huffCode; + } + + public int[] getMinCode() { + return minCode; + } + + public int[] getMaxCode() { + return maxCode; + } + + public int[] getValPtr() { + return valPtr; + } + } + + public DhtSegment(final int marker, final byte[] segmentData) throws IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public DhtSegment(final int marker, int length, final InputStream is) + throws IOException { + super(marker, length); + + final ArrayList huffmanTables = new ArrayList(); + while (length > 0) { + final int tableClassAndDestinationId = 0xff & readByte( + "TableClassAndDestinationId", is, "Not a Valid JPEG File"); + length--; + final int tableClass = (tableClassAndDestinationId >> 4) & 0xf; + final int destinationIdentifier = tableClassAndDestinationId & 0xf; + final int[] bits = new int[1 + 16]; + int bitsSum = 0; + for (int i = 1; i < bits.length; i++) { + bits[i] = 0xff & readByte("Li", is, "Not a Valid JPEG File"); + length--; + bitsSum += bits[i]; + } + final int[] huffVal = new int[bitsSum]; + for (int i = 0; i < bitsSum; i++) { + huffVal[i] = 0xff & readByte("Vij", is, "Not a Valid JPEG File"); + length--; + } + + huffmanTables.add(new HuffmanTable(tableClass, + destinationIdentifier, bits, huffVal)); + } + this.huffmanTables = Collections.unmodifiableList(huffmanTables); + } + + @Override + public String getDescription() { + return "DHT (" + getSegmentType() + ")"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DqtSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DqtSegment.java new file mode 100644 index 0000000..01f4346 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/DqtSegment.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class DqtSegment extends Segment { + public final List quantizationTables = new ArrayList(); + + public static class QuantizationTable { + public final int precision; + public final int destinationIdentifier; + public final int[] elements; + + public QuantizationTable(final int precision, final int destinationIdentifier, + final int[] elements) { + this.precision = precision; + this.destinationIdentifier = destinationIdentifier; + this.elements = elements; + } + } + + public DqtSegment(final int marker, final byte[] segmentData) + throws ImageReadException, IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public DqtSegment(final int marker, int length, final InputStream is) + throws ImageReadException, IOException { + super(marker, length); + + while (length > 0) { + final int precisionAndDestination = readByte( + "QuantizationTablePrecisionAndDestination", is, + "Not a Valid JPEG File"); + length--; + final int precision = (precisionAndDestination >> 4) & 0xf; + final int destinationIdentifier = precisionAndDestination & 0xf; + + final int[] elements = new int[64]; + for (int i = 0; i < 64; i++) { + if (precision == 0) { + elements[i] = 0xff & readByte("QuantizationTableElement", + is, "Not a Valid JPEG File"); + length--; + } else if (precision == 1) { + elements[i] = read2Bytes("QuantizationTableElement", is, "Not a Valid JPEG File", getByteOrder()); + length -= 2; + } else { + throw new ImageReadException( + "Quantization table precision '" + precision + + "' is invalid"); + } + } + + quantizationTables.add(new QuantizationTable(precision, + destinationIdentifier, elements)); + } + } + + @Override + public String getDescription() { + return "DQT (" + getSegmentType() + ")"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/GenericSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/GenericSegment.java new file mode 100644 index 0000000..7f1e00e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/GenericSegment.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public abstract class GenericSegment extends Segment { + protected final byte[] segmentData; + + public GenericSegment(int marker, int markerLength, InputStream is) throws IOException { + super(marker, markerLength); + + segmentData = readBytes("Segment Data", is, markerLength, "Invalid Segment: insufficient data"); + } + + public GenericSegment(final int marker, final byte[] bytes) { + super(marker, bytes.length); + + this.segmentData = bytes; + } + + @Override + public void dump(final PrintWriter pw) { + dump(pw, 0); + } + + public void dump(final PrintWriter pw, final int start) { + for (int i = 0; (i < 50) && ((i + start) < segmentData.length); i++) { + debugNumber(pw, "\t" + (i + start), segmentData[i + start], 1); + } + } + + /** + * Returns a copy of the segment's contents, + * excluding the marker and length bytes at + * the beginning. + * @return the segment's contents + */ + public byte[] getSegmentData() { + return segmentData.clone(); + } + + // public String getDescription() + // { + // return "Unknown"; + // } +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/JFIFSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/JfifSegment.java similarity index 68% rename from src/main/java/org/apache/sanselan/formats/jpeg/segments/JFIFSegment.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/segments/JfifSegment.java index 18016d1..77105df 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/JFIFSegment.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/JfifSegment.java @@ -1,82 +1,82 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.jpeg.JpegConstants; - -public class JFIFSegment extends Segment implements JpegConstants -{ - public final int jfifMajorVersion; - public final int jfifMinorVersion; - public final int densityUnits; - public final int xDensity; - public final int yDensity; - - public final int xThumbnail; - public final int yThumbnail; - public final int thumbnailSize; - - public String getDescription() - { - return "JFIF (" + getSegmentType() + ")"; - } - - public JFIFSegment(int marker, byte segmentData[]) - throws ImageReadException, IOException - { - this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); - } - - public JFIFSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length); - - byte signature[] = readBytes(is, JFIF0_SIGNATURE.length); - if (!compareByteArrays(signature, JFIF0_SIGNATURE) - && !compareByteArrays(signature, JFIF0_SIGNATURE_ALTERNATIVE)) - throw new ImageReadException( - "Not a Valid JPEG File: missing JFIF string"); - - jfifMajorVersion = readByte("JFIF_major_version", is, - "Not a Valid JPEG File"); - jfifMinorVersion = readByte("JFIF_minor_version", is, - "Not a Valid JPEG File"); - densityUnits = readByte("density_units", is, "Not a Valid JPEG File"); - xDensity = read2Bytes("x_density", is, "Not a Valid JPEG File"); - yDensity = read2Bytes("y_density", is, "Not a Valid JPEG File"); - - xThumbnail = readByte("x_thumbnail", is, "Not a Valid JPEG File"); - yThumbnail = readByte("y_thumbnail", is, "Not a Valid JPEG File"); - thumbnailSize = xThumbnail * yThumbnail; - if (thumbnailSize > 0) - { - skipBytes(is, thumbnailSize, - "Not a Valid JPEG File: missing thumbnail"); - - } - - if (getDebug()) - System.out.println(""); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class JfifSegment extends Segment { + public final int jfifMajorVersion; + public final int jfifMinorVersion; + public final int densityUnits; + public final int xDensity; + public final int yDensity; + + public final int xThumbnail; + public final int yThumbnail; + public final int thumbnailSize; + + @Override + public String getDescription() { + return "JFIF (" + getSegmentType() + ")"; + } + + public JfifSegment(final int marker, final byte[] segmentData) + throws ImageReadException, IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public JfifSegment(final int marker, final int markerLength, final InputStream is) + throws ImageReadException, IOException { + super(marker, markerLength); + + final byte[] signature = readBytes(is, JpegConstants.JFIF0_SIGNATURE.size()); + if (!JpegConstants.JFIF0_SIGNATURE.equals(signature) + && !JpegConstants.JFIF0_SIGNATURE_ALTERNATIVE.equals(signature)) { + throw new ImageReadException( + "Not a Valid JPEG File: missing JFIF string"); + } + + jfifMajorVersion = readByte("JFIF_major_version", is, + "Not a Valid JPEG File"); + jfifMinorVersion = readByte("JFIF_minor_version", is, + "Not a Valid JPEG File"); + densityUnits = readByte("density_units", is, "Not a Valid JPEG File"); + xDensity = read2Bytes("x_density", is, "Not a Valid JPEG File", getByteOrder()); + yDensity = read2Bytes("y_density", is, "Not a Valid JPEG File", getByteOrder()); + + xThumbnail = readByte("x_thumbnail", is, "Not a Valid JPEG File"); + yThumbnail = readByte("y_thumbnail", is, "Not a Valid JPEG File"); + thumbnailSize = xThumbnail * yThumbnail; + if (thumbnailSize > 0) { + skipBytes(is, thumbnailSize, + "Not a Valid JPEG File: missing thumbnail"); + + } + + if (getDebug()) { + System.out.println(""); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/Segment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/Segment.java new file mode 100644 index 0000000..c974e6f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/Segment.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.PrintWriter; + +import org.apache.commons.imaging.common.BinaryFileParser; + +public abstract class Segment extends BinaryFileParser { + public final int marker; + public final int length; + + public Segment(final int marker, final int length) { + // super(); + + this.marker = marker; + this.length = length; + } + + public void dump(final PrintWriter pw) { + // empty + } + + public abstract String getDescription(); + + @Override + public String toString() { + return "[Segment: " + getDescription() + "]"; + } + + public String getSegmentType() { + + switch (marker) { + case 0xffc0: + return "Start Of Frame, Baseline Dct, Huffman coding"; + case 0xffc1: + return "Start Of Frame, Extended sequential Dct, Huffman coding"; + case 0xffc2: + return "Start Of Frame, Progressive Dct, Huffman coding"; + case 0xffc3: + return "Start Of Frame, Lossless (sequential), Huffman coding"; + + case 0xffc5: + return "Start Of Frame, Differential sequential Dct, Huffman coding"; + case 0xffc6: + return "Start Of Frame, Differential progressive Dct, Huffman coding"; + case 0xffc7: + return "Start Of Frame, Differential lossless (sequential), Huffman coding"; + + case 0xffc8: + return "Start Of Frame, Reserved for JPEG extensions, arithmetic coding"; + case 0xffc9: + return "Start Of Frame, Extended sequential Dct, arithmetic coding"; + case 0xffca: + return "Start Of Frame, Progressive Dct, arithmetic coding"; + case 0xffcb: + return "Start Of Frame, Lossless (sequential), arithmetic coding"; + + case 0xffcd: + return "Start Of Frame, Differential sequential Dct, arithmetic coding"; + case 0xffce: + return "Start Of Frame, Differential progressive Dct, arithmetic coding"; + case 0xffcf: + return "Start Of Frame, Differential lossless (sequential), arithmetic coding"; + + case 0xffc4: + return "Define Huffman table(s)"; + case 0xffcc: + return "Define arithmetic coding conditioning(s)"; + + case 0xffd0: + return "Restart with modulo 8 count 0"; + case 0xffd1: + return "Restart with modulo 8 count 1"; + case 0xffd2: + return "Restart with modulo 8 count 2"; + case 0xffd3: + return "Restart with modulo 8 count 3"; + case 0xffd4: + return "Restart with modulo 8 count 4"; + case 0xffd5: + return "Restart with modulo 8 count 5"; + case 0xffd6: + return "Restart with modulo 8 count 6"; + case 0xffd7: + return "Restart with modulo 8 count 7"; + + case 0xffd8: + return "Start of image"; + case 0xffd9: + return "End of image"; + case 0xffda: + return "Start of scan"; + case 0xffdb: + return "Define quantization table(s)"; + case 0xffdc: + return "Define number of lines"; + case 0xffdd: + return "Define restart interval"; + case 0xffde: + return "Define hierarchical progression"; + case 0xffdf: + return "Expand reference component(s)"; + // case 0xffd8 : + // return "Reserved for application segments"; + // case 0xffd8 : + // return "Reserved for JPEG extensions"; + case 0xfffe: + return "Comment"; + case 0xff01: + return "For temporary private use in arithmetic coding"; + // case 0xffd8 : + // return "Reserved"; + + default: + } + + if ((marker >= 0xff02) && (marker <= 0xffbf)) { + return "Reserved"; + } + if ((marker >= 0xffe0) && (marker <= 0xffef)) { + return "APP" + (marker - 0xffe0); + } + if ((marker >= 0xfff0) && (marker <= 0xfffd)) { + return "JPG" + (marker - 0xffe0); + } + + return "Unknown"; + + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SofnSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SofnSegment.java new file mode 100644 index 0000000..8303a89 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SofnSegment.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class SofnSegment extends Segment { + public final int width; + public final int height; + public final int numberOfComponents; + public final int precision; + private final Component[] components; + + public static class Component { + public final int componentIdentifier; + public final int horizontalSamplingFactor; + public final int verticalSamplingFactor; + public final int quantTabDestSelector; + + public Component(final int componentIdentifier, final int horizontalSamplingFactor, + final int veritcalSamplingFactor, final int quantTabDestSelector) { + this.componentIdentifier = componentIdentifier; + this.horizontalSamplingFactor = horizontalSamplingFactor; + this.verticalSamplingFactor = veritcalSamplingFactor; + this.quantTabDestSelector = quantTabDestSelector; + } + } + + public SofnSegment(final int marker, final byte[] segmentData) throws IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public SofnSegment(final int marker, final int markerLength, final InputStream is) + throws IOException { + super(marker, markerLength); + + if (getDebug()) { + System.out.println("SOF0Segment marker_length: " + markerLength); + } + + precision = readByte("Data_precision", is, "Not a Valid JPEG File"); + height = read2Bytes("Image_height", is, "Not a Valid JPEG File", getByteOrder()); + width = read2Bytes("Image_Width", is, "Not a Valid JPEG File", getByteOrder()); + numberOfComponents = readByte("Number_of_components", is, + "Not a Valid JPEG File"); + components = new Component[numberOfComponents]; + for (int i = 0; i < numberOfComponents; i++) { + final int componentIdentifier = readByte("ComponentIdentifier", is, + "Not a Valid JPEG File"); + + final int hvSamplingFactors = readByte("SamplingFactors", is, + "Not a Valid JPEG File"); + final int horizontalSamplingFactor = (hvSamplingFactors >> 4) & 0xf; + final int verticalSamplingFactor = hvSamplingFactors & 0xf; + final int quantTabDestSelector = readByte("QuantTabDestSel", is, + "Not a Valid JPEG File"); + components[i] = new Component(componentIdentifier, + horizontalSamplingFactor, verticalSamplingFactor, + quantTabDestSelector); + } + + if (getDebug()) { + System.out.println(""); + } + } + + /** + * Returns a copy of all the components. + * @return the components + */ + public Component[] getComponents() { + return components.clone(); + } + + /** + * Returns the component at the specified index. + * @param index the array index + * @return the component + */ + public Component getComponents(final int index) { + return components[index]; + } + + + @Override + public String getDescription() { + return "SOFN (SOF" + (marker - JpegConstants.SOF0_MARKER) + ") (" + + getSegmentType() + ")"; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java new file mode 100644 index 0000000..3127c0f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class SosSegment extends Segment { + public final int numberOfComponents; + private final Component[] components; + public final int startOfSpectralSelection; + public final int endOfSpectralSelection; + public final int successiveApproximationBitHigh; + public final int successiveApproximationBitLow; + + public static class Component { + public final int scanComponentSelector; + public final int dcCodingTableSelector; + public final int acCodingTableSelector; + + public Component(final int scanComponentSelector, final int dcCodingTableSelector, + final int acCodingTableSelector) { + this.scanComponentSelector = scanComponentSelector; + this.dcCodingTableSelector = dcCodingTableSelector; + this.acCodingTableSelector = acCodingTableSelector; + } + } + + public SosSegment(final int marker, final byte[] segmentData) throws IOException { + this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); + } + + public SosSegment(final int marker, final int markerLength, final InputStream is) throws IOException { + super(marker, markerLength); + + if (getDebug()) { + System.out.println("SosSegment marker_length: " + markerLength); + } + + // Debug.debug("SOS", marker_length); + + numberOfComponents = readByte("number_of_components_in_scan", is, + "Not a Valid JPEG File"); + // Debug.debug("number_of_components_in_scan", + // numberOfComponents); + + components = new Component[numberOfComponents]; + for (int i = 0; i < numberOfComponents; i++) { + final int scanComponentSelector = readByte("scanComponentSelector", is, "Not a Valid JPEG File"); + // Debug.debug("scanComponentSelector", scanComponentSelector); + + final int acDcEntropoyCodingTableSelector = readByte( + "acDcEntropoyCodingTableSelector", is, + "Not a Valid JPEG File"); + // Debug.debug("ac_dc_entrooy_coding_table_selector", + // acDcEntropoyCodingTableSelector); + + final int dcCodingTableSelector = (acDcEntropoyCodingTableSelector >> 4) & 0xf; + final int acCodingTableSelector = acDcEntropoyCodingTableSelector & 0xf; + components[i] = new Component(scanComponentSelector, + dcCodingTableSelector, acCodingTableSelector); + } + + startOfSpectralSelection = readByte("start_of_spectral_selection", is, + "Not a Valid JPEG File"); + // Debug.debug("start_of_spectral_selection", startOfSpectralSelection); + endOfSpectralSelection = readByte("end_of_spectral_selection", is, + "Not a Valid JPEG File"); + // Debug.debug("end_of_spectral_selection", endOfSpectralSelection); + final int successiveApproximationBitPosition = readByte( + "successive_approximation_bit_position", is, + "Not a Valid JPEG File"); + // Debug.debug("successive_approximation_bit_position", + // successive_approximation_bit_position); + successiveApproximationBitHigh = (successiveApproximationBitPosition >> 4) & 0xf; + successiveApproximationBitLow = successiveApproximationBitPosition & 0xf; + + if (getDebug()) { + System.out.println(""); + } + } + + /** + * Returns a copy of all the components. + * @return all the components + */ + public Component[] getComponents() { + return components.clone(); + } + + /** + * Return a component at the specified index. + * @param index the component index + * @return the component + */ + public Component getComponents(final int index) { + return components[index]; + } + + @Override + public String getDescription() { + return "SOS (" + getSegmentType() + ")"; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/UnknownSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/UnknownSegment.java similarity index 65% rename from src/main/java/org/apache/sanselan/formats/jpeg/segments/UnknownSegment.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/segments/UnknownSegment.java index c964e66..c94f37c 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/UnknownSegment.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/UnknownSegment.java @@ -1,42 +1,35 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; - -public class UnknownSegment extends GenericSegment -{ - public UnknownSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length, is); - } - - public UnknownSegment(int marker, byte bytes[]) throws ImageReadException, - IOException - { - super(marker, bytes); - } - - public String getDescription() - { - return "Unknown (" + getSegmentType() + ")"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.segments; + +import java.io.IOException; +import java.io.InputStream; + +public class UnknownSegment extends GenericSegment { + public UnknownSegment(int marker, int markerLength, InputStream is) throws IOException { + super(marker, markerLength, is); + } + + public UnknownSegment(final int marker, final byte[] bytes) { + super(marker, bytes); + } + + @Override + public String getDescription() { + return "Unknown (" + getSegmentType() + ")"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegRewriter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegRewriter.java new file mode 100644 index 0000000..0da58e9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegRewriter.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.xmp; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.formats.jpeg.JpegUtils; +import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +/** + * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. + */ +public class JpegRewriter extends BinaryFileParser { + private static final ByteOrder JPEG_BYTE_ORDER = ByteOrder.BIG_ENDIAN; + private static final SegmentFilter EXIF_SEGMENT_FILTER = new SegmentFilter() { + public boolean filter(final JFIFPieceSegment segment) { + return segment.isExifSegment(); + } + }; + private static final SegmentFilter XMP_SEGMENT_FILTER = new SegmentFilter() { + public boolean filter(final JFIFPieceSegment segment) { + return segment.isXmpSegment(); + } + }; + private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = new SegmentFilter() { + public boolean filter(final JFIFPieceSegment segment) { + return segment.isPhotoshopApp13Segment(); + } + }; + + /** + * Constructor. to guess whether a file contains an image based on its file + * extension. + */ + public JpegRewriter() { + setByteOrder(JPEG_BYTE_ORDER); + } + + protected static class JFIFPieces { + public final List pieces; + public final List segmentPieces; + + public JFIFPieces(final List pieces, + final List segmentPieces) { + this.pieces = pieces; + this.segmentPieces = segmentPieces; + } + + } + + protected abstract static class JFIFPiece { + protected abstract void write(OutputStream os) throws IOException; + + @Override + public String toString() { + return "[" + this.getClass().getName() + "]"; + } + } + + protected static class JFIFPieceSegment extends JFIFPiece { + public final int marker; + public final byte[] markerBytes; + public final byte[] segmentLengthBytes; + public final byte[] segmentData; + + public JFIFPieceSegment(final int marker, final byte[] segmentData) { + this(marker, + ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER), + ByteConversions.toBytes((short) (segmentData.length + 2), JPEG_BYTE_ORDER), + segmentData); + } + + public JFIFPieceSegment(final int marker, final byte[] markerBytes, + final byte[] segmentLengthBytes, final byte[] segmentData) { + this.marker = marker; + this.markerBytes = markerBytes; + this.segmentLengthBytes = segmentLengthBytes; + this.segmentData = segmentData; + } + + @Override + public String toString() { + return "[" + this.getClass().getName() + " (0x" + + Integer.toHexString(marker) + ")]"; + } + + @Override + protected void write(final OutputStream os) throws IOException { + os.write(markerBytes); + os.write(segmentLengthBytes); + os.write(segmentData); + } + + public boolean isApp1Segment() { + return marker == JpegConstants.JPEG_APP1_MARKER; + } + + public boolean isAppSegment() { + return marker >= JpegConstants.JPEG_APP0_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER; + } + + public boolean isExifSegment() { + if (marker != JpegConstants.JPEG_APP1_MARKER) { + return false; + } + if (!startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) { + return false; + } + return true; + } + + public boolean isPhotoshopApp13Segment() { + if (marker != JpegConstants.JPEG_APP13_MARKER) { + return false; + } + if (!new IptcParser().isPhotoshopJpegSegment(segmentData)) { + return false; + } + return true; + } + + public boolean isXmpSegment() { + if (marker != JpegConstants.JPEG_APP1_MARKER) { + return false; + } + if (!startsWith(segmentData, JpegConstants.XMP_IDENTIFIER)) { + return false; + } + return true; + } + + } + + protected static class JFIFPieceImageData extends JFIFPiece { + public final byte[] markerBytes; + public final byte[] imageData; + + public JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { + super(); + this.markerBytes = markerBytes; + this.imageData = imageData; + } + + @Override + protected void write(final OutputStream os) throws IOException { + os.write(markerBytes); + os.write(imageData); + } + } + + protected JFIFPieces analyzeJFIF(final ByteSource byteSource) + throws ImageReadException, IOException + // , ImageWriteException + { + final List pieces = new ArrayList(); + final List segmentPieces = new ArrayList(); + + final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { + // return false to exit before reading image data. + public boolean beginSOS() { + return true; + } + + public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { + pieces.add(new JFIFPieceImageData(markerBytes, imageData)); + } + + // return false to exit traversal. + public boolean visitSegment(final int marker, final byte[] markerBytes, + final int segmentLength, final byte[] segmentLengthBytes, + final byte[] segmentData) throws ImageReadException, IOException { + final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes, + segmentLengthBytes, segmentData); + pieces.add(piece); + segmentPieces.add(piece); + + return true; + } + }; + + new JpegUtils().traverseJFIF(byteSource, visitor); + + return new JFIFPieces(pieces, segmentPieces); + } + + private interface SegmentFilter { + boolean filter(JFIFPieceSegment segment); + } + + protected List removeXmpSegments(final List segments) { + return filterSegments(segments, XMP_SEGMENT_FILTER); + } + + protected List removePhotoshopApp13Segments( + final List segments) { + return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER); + } + + protected List findPhotoshopApp13Segments( + final List segments) { + return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true); + } + + protected List removeExifSegments(final List segments) { + return filterSegments(segments, EXIF_SEGMENT_FILTER); + } + + protected List filterSegments(final List segments, + final SegmentFilter filter) { + return filterSegments(segments, filter, false); + } + + protected List filterSegments(final List segments, + final SegmentFilter filter, final boolean reverse) { + final List result = new ArrayList(); + + for (T piece : segments) { + if (piece instanceof JFIFPieceSegment) { + if (filter.filter((JFIFPieceSegment) piece) ^ !reverse) { + result.add(piece); + } + } else if (!reverse) { + result.add(piece); + } + } + + return result; + } + + protected List insertBeforeFirstAppSegments( + final List segments, final List newSegments) throws ImageWriteException { + int firstAppIndex = -1; + for (int i = 0; i < segments.size(); i++) { + final JFIFPiece piece = segments.get(i); + if (!(piece instanceof JFIFPieceSegment)) { + continue; + } + + final JFIFPieceSegment segment = (JFIFPieceSegment) piece; + if (segment.isAppSegment()) { + if (firstAppIndex == -1) { + firstAppIndex = i; + } + } + } + + final List result = new ArrayList(segments); + if (firstAppIndex == -1) { + throw new ImageWriteException("JPEG file has no APP segments."); + } + result.addAll(firstAppIndex, newSegments); + return result; + } + + protected List insertAfterLastAppSegments( + final List segments, final List newSegments) throws ImageWriteException { + int lastAppIndex = -1; + for (int i = 0; i < segments.size(); i++) { + final JFIFPiece piece = segments.get(i); + if (!(piece instanceof JFIFPieceSegment)) { + continue; + } + + final JFIFPieceSegment segment = (JFIFPieceSegment) piece; + if (segment.isAppSegment()) { + lastAppIndex = i; + } + } + + final List result = new ArrayList(segments); + if (lastAppIndex == -1) { + if (segments.size() < 1) { + throw new ImageWriteException("JPEG file has no APP segments."); + } + result.addAll(1, newSegments); + } else { + result.addAll(lastAppIndex + 1, newSegments); + } + + return result; + } + + protected void writeSegments(final OutputStream os, + final List segments) throws IOException { + boolean canThrow = false; + try { + JpegConstants.SOI.writeTo(os); + + for (JFIFPiece piece : segments) { + piece.write(os); + } + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, os); + } + } + + // private void writeSegment(OutputStream os, JFIFPieceSegment piece) + // throws ImageWriteException, IOException + // { + // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_MARKER, + // JPEG_BYTE_ORDER); + // if (piece.segmentData.length > 0xffff) + // throw new JpegSegmentOverflowException("Jpeg segment is too long: " + // + piece.segmentData.length); + // int segmentLength = piece.segmentData.length + 2; + // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength, + // JPEG_BYTE_ORDER); + // + // os.write(markerBytes); + // os.write(segmentLengthBytes); + // os.write(piece.segmentData); + // } + + public static class JpegSegmentOverflowException extends ImageWriteException { + private static final long serialVersionUID = -1062145751550646846L; + + public JpegSegmentOverflowException(final String message) { + super(message); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpParser.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpParser.java new file mode 100644 index 0000000..90b28aa --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpParser.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.jpeg.xmp; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +public class JpegXmpParser extends BinaryFileParser { + + public JpegXmpParser() { + setByteOrder(ByteOrder.BIG_ENDIAN); + } + + public boolean isXmpJpegSegment(final byte[] segmentData) { + return BinaryFunctions.startsWith(segmentData, JpegConstants.XMP_IDENTIFIER); + } + + public String parseXmpJpegSegment(final byte[] segmentData) + throws ImageReadException { + if (!isXmpJpegSegment(segmentData)) { + throw new ImageReadException("Invalid JPEG XMP Segment."); + } + final int index = JpegConstants.XMP_IDENTIFIER.size(); + + try { + // segment data is UTF-8 encoded xml. + return new String(segmentData, index, segmentData.length - index, "utf-8"); + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("Invalid JPEG XMP Segment.", e); + } + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpRewriter.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpRewriter.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpRewriter.java rename to src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpRewriter.java index 8decc5f..966fcb6 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpRewriter.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/xmp/JpegXmpRewriter.java @@ -1,219 +1,204 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.xmp; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.common.byteSources.ByteSourceInputStream; - -/** - * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. - *

- *

- * See the source of the XmpXmlUpdateExample class for example usage. - * - * @see org.apache.sanselan.sampleUsage.WriteXmpXmlExample - */ -public class JpegXmpRewriter extends JpegRewriter -{ - - /** - * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), - * and writes the result to a stream. - *

- * - * @param src - * Image file. - * @param os - * OutputStream to write the image to. - * - * @see java.io.File - * @see java.io.OutputStream - */ - public void removeXmpXml(File src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - removeXmpXml(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), - * and writes the result to a stream. - *

- * - * @param src - * Byte array containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeXmpXml(byte src[], OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - removeXmpXml(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), - * and writes the result to a stream. - *

- * - * @param src - * InputStream containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeXmpXml(InputStream src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - removeXmpXml(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), - * and writes the result to a stream. - *

- * - * @param byteSource - * ByteSource containing Jpeg image data. - * @param os - * OutputStream to write the image to. - */ - public void removeXmpXml(ByteSource byteSource, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List pieces = jfifPieces.pieces; - pieces = removeXmpSegments(pieces); - writeSegments(os, pieces); - } - - /** - * Reads a Jpeg image, replaces the XMP XML and writes the result to a - * stream. - * - * @param src - * Byte array containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param xmpXml - * String containing XMP XML. - */ - public void updateXmpXml(byte src[], OutputStream os, String xmpXml) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - updateXmpXml(byteSource, os, xmpXml); - } - - /** - * Reads a Jpeg image, replaces the XMP XML and writes the result to a - * stream. - * - * @param src - * InputStream containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param xmpXml - * String containing XMP XML. - */ - public void updateXmpXml(InputStream src, OutputStream os, String xmpXml) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - updateXmpXml(byteSource, os, xmpXml); - } - - /** - * Reads a Jpeg image, replaces the XMP XML and writes the result to a - * stream. - * - * @param src - * Image file. - * @param os - * OutputStream to write the image to. - * @param xmpXml - * String containing XMP XML. - */ - public void updateXmpXml(File src, OutputStream os, String xmpXml) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - updateXmpXml(byteSource, os, xmpXml); - } - - /** - * Reads a Jpeg image, replaces the XMP XML and writes the result to a - * stream. - * - * @param byteSource - * ByteSource containing Jpeg image data. - * @param os - * OutputStream to write the image to. - * @param xmpXml - * String containing XMP XML. - */ - public void updateXmpXml(ByteSource byteSource, OutputStream os, - String xmpXml) throws ImageReadException, IOException, - ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List pieces = jfifPieces.pieces; - pieces = removeXmpSegments(pieces); - - List newPieces = new ArrayList(); - byte xmpXmlBytes[] = xmpXml.getBytes("utf-8"); - int index = 0; - while (index < xmpXmlBytes.length) - { - int segmentSize = Math.min(xmpXmlBytes.length, MAX_SEGMENT_SIZE); - byte segmentData[] = writeXmpSegment(xmpXmlBytes, index, - segmentSize); - newPieces.add(new JFIFPieceSegment(JPEG_APP1_Marker, segmentData)); - index += segmentSize; - } - - pieces = insertAfterLastAppSegments(pieces, newPieces); - - writeSegments(os, pieces); - } - - private byte[] writeXmpSegment(byte xmpXmlData[], int start, int length) - throws IOException - { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - os.write(XMP_IDENTIFIER); - os.write(xmpXmlData, start, length); - - return os.toByteArray(); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.jpeg.xmp; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; + +/** + * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. + */ +public class JpegXmpRewriter extends JpegRewriter { + + /** + * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), + * and writes the result to a stream. + *

+ * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * + * @see java.io.File + * @see java.io.OutputStream + */ + public void removeXmpXml(final File src, final OutputStream os) + throws ImageReadException, IOException { + final ByteSource byteSource = new ByteSourceFile(src); + removeXmpXml(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), + * and writes the result to a stream. + *

+ * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeXmpXml(final byte[] src, final OutputStream os) + throws ImageReadException, IOException { + final ByteSource byteSource = new ByteSourceArray(src); + removeXmpXml(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), + * and writes the result to a stream. + *

+ * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeXmpXml(final InputStream src, final OutputStream os) + throws ImageReadException, IOException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + removeXmpXml(byteSource, os); + } + + /** + * Reads a Jpeg image, removes all XMP XML (by removing the APP1 segment), + * and writes the result to a stream. + *

+ * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + */ + public void removeXmpXml(final ByteSource byteSource, final OutputStream os) + throws ImageReadException, IOException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + List pieces = jfifPieces.pieces; + pieces = removeXmpSegments(pieces); + writeSegments(os, pieces); + } + + /** + * Reads a Jpeg image, replaces the XMP XML and writes the result to a + * stream. + * + * @param src + * Byte array containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param xmpXml + * String containing XMP XML. + */ + public void updateXmpXml(final byte[] src, final OutputStream os, final String xmpXml) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceArray(src); + updateXmpXml(byteSource, os, xmpXml); + } + + /** + * Reads a Jpeg image, replaces the XMP XML and writes the result to a + * stream. + * + * @param src + * InputStream containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param xmpXml + * String containing XMP XML. + */ + public void updateXmpXml(final InputStream src, final OutputStream os, final String xmpXml) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceInputStream(src, null); + updateXmpXml(byteSource, os, xmpXml); + } + + /** + * Reads a Jpeg image, replaces the XMP XML and writes the result to a + * stream. + * + * @param src + * Image file. + * @param os + * OutputStream to write the image to. + * @param xmpXml + * String containing XMP XML. + */ + public void updateXmpXml(final File src, final OutputStream os, final String xmpXml) + throws ImageReadException, IOException, ImageWriteException { + final ByteSource byteSource = new ByteSourceFile(src); + updateXmpXml(byteSource, os, xmpXml); + } + + /** + * Reads a Jpeg image, replaces the XMP XML and writes the result to a + * stream. + * + * @param byteSource + * ByteSource containing Jpeg image data. + * @param os + * OutputStream to write the image to. + * @param xmpXml + * String containing XMP XML. + */ + public void updateXmpXml(final ByteSource byteSource, final OutputStream os, + final String xmpXml) throws ImageReadException, IOException, + ImageWriteException { + final JFIFPieces jfifPieces = analyzeJFIF(byteSource); + List pieces = jfifPieces.pieces; + pieces = removeXmpSegments(pieces); + + final List newPieces = new ArrayList(); + final byte[] xmpXmlBytes = xmpXml.getBytes("utf-8"); + int index = 0; + while (index < xmpXmlBytes.length) { + final int segmentSize = Math.min(xmpXmlBytes.length, JpegConstants.MAX_SEGMENT_SIZE); + final byte[] segmentData = writeXmpSegment(xmpXmlBytes, index, + segmentSize); + newPieces.add(new JFIFPieceSegment(JpegConstants.JPEG_APP1_MARKER, segmentData)); + index += segmentSize; + } + + pieces = insertAfterLastAppSegments(pieces, newPieces); + + writeSegments(os, pieces); + } + + private byte[] writeXmpSegment(final byte[] xmpXmlData, final int start, final int length) + throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + JpegConstants.XMP_IDENTIFIER.writeTo(os); + os.write(xmpXmlData, start, length); + + return os.toByteArray(); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/package-info.java b/src/main/java/org/apache/commons/imaging/formats/package-info.java new file mode 100644 index 0000000..f146d94 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Image file formats. + */ +package org.apache.commons.imaging.formats; + diff --git a/src/main/java/org/apache/commons/imaging/formats/pcx/PcxConstants.java b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxConstants.java new file mode 100644 index 0000000..58ed0ea --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxConstants.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ +package org.apache.commons.imaging.formats.pcx; + + +public interface PcxConstants { + public static final String PARAM_KEY_PCX_COMPRESSION = "PCX_COMPRESSION"; + public static final int PCX_COMPRESSION_UNCOMPRESSED = 0; + public static final int PCX_COMPRESSION_RLE = 1; + + public static final String PARAM_KEY_PCX_BIT_DEPTH = "PCX_BIT_DEPTH"; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pcx/PcxImageParser.java b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxImageParser.java new file mode 100644 index 0000000..a5190b1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxImageParser.java @@ -0,0 +1,559 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pcx; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.Transparency; +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.ComponentColorModel; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DataBufferByte; +import com.google.code.appengine.awt.image.IndexColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.WritableRaster; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; +import static org.apache.commons.imaging.common.ByteConversions.*; + +public class PcxImageParser extends ImageParser { + // ZSoft's official spec is at http://www.qzx.com/pc-gpe/pcx.txt + // (among other places) but it's pretty thin. The fileformat.info document + // at http://www.fileformat.info/format/pcx/egff.htm is a little better + // but their gray sample image seems corrupt. PCX files themselves are + // the ultimate test but pretty hard to find nowdays, so the best + // test is against other image viewers (Irfanview is pretty good). + // + // Open source projects are generally poor at parsing PCX, + // SDL_Image/gdk-pixbuf/Eye of Gnome/GIMP/F-Spot all only do some formats, + // don't support uncompressed PCX, and/or don't handle black and white + // images properly. + + private static final String DEFAULT_EXTENSION = ".pcx"; + private static final String[] ACCEPTED_EXTENSIONS = { ".pcx", ".pcc", }; + + public PcxImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public String getName() { + return "Pcx-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.PCX, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final PcxHeader pcxHeader = readPcxHeader(byteSource); + final Dimension size = getImageSize(byteSource, params); + return new ImageInfo( + "PCX", + pcxHeader.nPlanes * pcxHeader.bitsPerPixel, + new ArrayList(), + ImageFormats.PCX, + "ZSoft PCX Image", + size.height, + "image/x-pcx", + 1, + pcxHeader.vDpi, + Math.round(size.getHeight() / pcxHeader.vDpi), + pcxHeader.hDpi, + Math.round(size.getWidth() / pcxHeader.hDpi), + size.width, + false, + false, + !(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8), + ImageInfo.COLOR_TYPE_RGB, + pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.COMPRESSION_ALGORITHM_RLE + : ImageInfo.COMPRESSION_ALGORITHM_NONE); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final PcxHeader pcxHeader = readPcxHeader(byteSource); + final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; + if (xSize < 0) { + throw new ImageReadException("Image width is negative"); + } + final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; + if (ySize < 0) { + throw new ImageReadException("Image height is negative"); + } + return new Dimension(xSize, ySize); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + static class PcxHeader { + + public static final int ENCODING_UNCOMPRESSED = 0; + public static final int ENCODING_RLE = 1; + public static final int PALETTE_INFO_COLOR = 1; + public static final int PALETTE_INFO_GRAYSCALE = 2; + public final int manufacturer; // Always 10 = ZSoft .pcx + public final int version; // 0 = PC Paintbrush 2.5 + // 2 = PC Paintbrush 2.8 with palette + // 3 = PC Paintbrush 2.8 w/o palette + // 4 = PC Paintbrush for Windows + // 5 = PC Paintbrush >= 3.0 + public final int encoding; // 0 = very old uncompressed format, 1 = .pcx + // run length encoding + public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel + public final int xMin; // window + public final int yMin; + public final int xMax; + public final int yMax; + public final int hDpi; // horizontal dpi + public final int vDpi; // vertical dpi + public final int[] colormap; // palette for <= 16 colors + public final int reserved; // Always 0 + public final int nPlanes; // Number of color planes + public final int bytesPerLine; // Number of bytes per scanline plane, + // must be an even number. + public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in + // Paintbrush IV/IV+ + public final int hScreenSize; // horizontal screen size, in pixels. + // PaintBrush >= IV only. + public final int vScreenSize; // vertical screen size, in pixels. + // PaintBrush >= IV only. + + public PcxHeader(final int manufacturer, final int version, + final int encoding, final int bitsPerPixel, final int xMin, + final int yMin, final int xMax, final int yMax, final int hDpi, + final int vDpi, final int[] colormap, final int reserved, + final int nPlanes, final int bytesPerLine, + final int paletteInfo, final int hScreenSize, + final int vScreenSize) { + this.manufacturer = manufacturer; + this.version = version; + this.encoding = encoding; + this.bitsPerPixel = bitsPerPixel; + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + this.hDpi = hDpi; + this.vDpi = vDpi; + this.colormap = colormap; + this.reserved = reserved; + this.nPlanes = nPlanes; + this.bytesPerLine = bytesPerLine; + this.paletteInfo = paletteInfo; + this.hScreenSize = hScreenSize; + this.vScreenSize = vScreenSize; + } + + public void dump(final PrintWriter pw) { + pw.println("PcxHeader"); + pw.println("Manufacturer: " + manufacturer); + pw.println("Version: " + version); + pw.println("Encoding: " + encoding); + pw.println("BitsPerPixel: " + bitsPerPixel); + pw.println("xMin: " + xMin); + pw.println("yMin: " + yMin); + pw.println("xMax: " + xMax); + pw.println("yMax: " + yMax); + pw.println("hDpi: " + hDpi); + pw.println("vDpi: " + vDpi); + pw.print("ColorMap: "); + for (int i = 0; i < colormap.length; i++) { + if (i > 0) { + pw.print(","); + } + pw.print("(" + (0xff & (colormap[i] >> 16)) + "," + + (0xff & (colormap[i] >> 8)) + "," + + (0xff & colormap[i]) + ")"); + } + pw.println(); + pw.println("Reserved: " + reserved); + pw.println("nPlanes: " + nPlanes); + pw.println("BytesPerLine: " + bytesPerLine); + pw.println("PaletteInfo: " + paletteInfo); + pw.println("hScreenSize: " + hScreenSize); + pw.println("vScreenSize: " + vScreenSize); + pw.println(); + } + } + + private PcxHeader readPcxHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final PcxHeader ret = readPcxHeader(is, false); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict) + throws ImageReadException, IOException { + final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128, + "Not a Valid PCX File"); + final int manufacturer = 0xff & pcxHeaderBytes[0]; + final int version = 0xff & pcxHeaderBytes[1]; + final int encoding = 0xff & pcxHeaderBytes[2]; + final int bitsPerPixel = 0xff & pcxHeaderBytes[3]; + final int xMin = toUInt16(pcxHeaderBytes, 4, getByteOrder()); + final int yMin = toUInt16(pcxHeaderBytes, 6, getByteOrder()); + final int xMax = toUInt16(pcxHeaderBytes, 8, getByteOrder()); + final int yMax = toUInt16(pcxHeaderBytes, 10, getByteOrder()); + final int hDpi = toUInt16(pcxHeaderBytes, 12, getByteOrder()); + final int vDpi = toUInt16(pcxHeaderBytes, 14, getByteOrder()); + final int[] colormap = new int[16]; + for (int i = 0; i < 16; i++) { + colormap[i] = 0xff000000 + | ((0xff & pcxHeaderBytes[16 + 3 * i]) << 16) + | ((0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8) + | (0xff & pcxHeaderBytes[16 + 3 * i + 2]); + } + final int reserved = 0xff & pcxHeaderBytes[64]; + final int nPlanes = 0xff & pcxHeaderBytes[65]; + final int bytesPerLine = toUInt16(pcxHeaderBytes, 66, getByteOrder()); + final int paletteInfo = toUInt16(pcxHeaderBytes, 68, getByteOrder()); + final int hScreenSize = toUInt16(pcxHeaderBytes, 70, getByteOrder()); + final int vScreenSize = toUInt16(pcxHeaderBytes, 72, getByteOrder()); + + if (manufacturer != 10) { + throw new ImageReadException( + "Not a Valid PCX File: manufacturer is " + manufacturer); + } + if (isStrict) { + // Note that reserved is sometimes set to a non-zero value + // by Paintbrush itself, so it shouldn't be enforced. + if (bytesPerLine % 2 != 0) { + throw new ImageReadException( + "Not a Valid PCX File: bytesPerLine is odd"); + } + } + + return new PcxHeader(manufacturer, version, encoding, bitsPerPixel, + xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved, + nPlanes, bytesPerLine, paletteInfo, hScreenSize, vScreenSize); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + readPcxHeader(byteSource).dump(pw); + return true; + } + + private void readScanLine(final PcxHeader pcxHeader, final InputStream is, + final byte[] samples) throws IOException, ImageReadException { + if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) { + int r; + for (int bytesRead = 0; bytesRead < samples.length; bytesRead += r) { + r = is.read(samples, bytesRead, samples.length - bytesRead); + if (r < 0) { + throw new ImageReadException( + "Premature end of file reading image data"); + } + } + } else { + if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) { + for (int bytesRead = 0; bytesRead < samples.length;) { + final byte b = readByte("Pixel", is, "Error reading image data"); + int count; + byte sample; + if ((b & 0xc0) == 0xc0) { + count = b & 0x3f; + sample = readByte("Pixel", is, + "Error reading image data"); + } else { + count = 1; + sample = b; + } + for (int i = 0; i < count && bytesRead + i < samples.length; i++) { + samples[bytesRead + i] = sample; + } + bytesRead += count; + } + } else { + throw new ImageReadException("Invalid PCX encoding " + + pcxHeader.encoding); + } + } + } + + private int[] read256ColorPalette(final InputStream stream) throws IOException { + final byte[] paletteBytes = readBytes("Palette", stream, 769, + "Error reading palette"); + if (paletteBytes[0] != 12) { + return null; + } + final int[] palette = new int[256]; + for (int i = 0; i < palette.length; i++) { + palette[i] = ((0xff & paletteBytes[1 + 3 * i]) << 16) + | ((0xff & paletteBytes[1 + 3 * i + 1]) << 8) + | (0xff & paletteBytes[1 + 3 * i + 2]); + } + return palette; + } + + private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource) + throws IOException { + InputStream stream = null; + boolean canThrow = false; + try { + stream = byteSource.getInputStream(); + final long toSkip = byteSource.getLength() - 769; + skipBytes(stream, (int) toSkip); + final int[] ret = read256ColorPalette(stream); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, stream); + } + } + + private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is, + final ByteSource byteSource) throws ImageReadException, IOException { + final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; + if (xSize < 0) { + throw new ImageReadException("Image width is negative"); + } + final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; + if (ySize < 0) { + throw new ImageReadException("Image height is negative"); + } + + final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes; + final byte[] scanline = new byte[scanlineLength]; + if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2 + || pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8) + && pcxHeader.nPlanes == 1) { + final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8; + final byte[] image = new byte[ySize * bytesPerImageRow]; + for (int y = 0; y < ySize; y++) { + readScanLine(pcxHeader, is, scanline); + System.arraycopy(scanline, 0, image, y * bytesPerImageRow, + bytesPerImageRow); + } + final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); + int[] palette; + if (pcxHeader.bitsPerPixel == 1) { + palette = new int[] { 0x000000, 0xffffff }; + } else if (pcxHeader.bitsPerPixel == 8) { + // Normally the palette is read 769 bytes from the end of the + // file. + // However DCX files have multiple PCX images in one file, so + // there could be extra data before the end! So try look for the + // palette + // immediately after the image data first. + palette = read256ColorPalette(is); + if (palette == null) { + palette = read256ColorPaletteFromEndOfFile(byteSource); + } + if (palette == null) { + throw new ImageReadException( + "No 256 color palette found in image that needs it"); + } + } else { + palette = pcxHeader.colormap; + } + WritableRaster raster; + if (pcxHeader.bitsPerPixel == 8) { + raster = Raster.createInterleavedRaster(dataBuffer, + xSize, ySize, bytesPerImageRow, 1, new int[] { 0 }, + null); + } else { + raster = Raster.createPackedRaster(dataBuffer, xSize, + ySize, pcxHeader.bitsPerPixel, null); + } + final IndexColorModel colorModel = new IndexColorModel( + pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel, + palette, 0, false, -1, DataBuffer.TYPE_BYTE); + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + } else if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes + && pcxHeader.nPlanes <= 4) { + final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes, + 1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1, + DataBuffer.TYPE_BYTE); + final BufferedImage image = new BufferedImage(xSize, ySize, + BufferedImage.TYPE_BYTE_BINARY, colorModel); + final byte[] unpacked = new byte[xSize]; + for (int y = 0; y < ySize; y++) { + readScanLine(pcxHeader, is, scanline); + int nextByte = 0; + Arrays.fill(unpacked, (byte) 0); + for (int plane = 0; plane < pcxHeader.nPlanes; plane++) { + for (int i = 0; i < pcxHeader.bytesPerLine; i++) { + final int b = 0xff & scanline[nextByte++]; + for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) { + unpacked[8 * i + j] |= (byte) (((b >> (7 - j)) & 0x1) << plane); + } + } + } + image.getRaster().setDataElements(0, y, xSize, 1, unpacked); + } + return image; + } else if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) { + final byte[][] image = new byte[3][]; + image[0] = new byte[xSize * ySize]; + image[1] = new byte[xSize * ySize]; + image[2] = new byte[xSize * ySize]; + for (int y = 0; y < ySize; y++) { + readScanLine(pcxHeader, is, scanline); + System.arraycopy(scanline, 0, image[0], y * xSize, xSize); + System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y + * xSize, xSize); + System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine, + image[2], y * xSize, xSize); + } + final DataBufferByte dataBuffer = new DataBufferByte(image, + image[0].length); + final WritableRaster raster = Raster.createBandedRaster( + dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 }, + new int[] { 0, 0, 0 }, null); + final ColorModel colorModel = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, + Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + } else if ((pcxHeader.bitsPerPixel == 24 && pcxHeader.nPlanes == 1) + || (pcxHeader.bitsPerPixel == 32 && pcxHeader.nPlanes == 1)) { + final int rowLength = 3 * xSize; + final byte[] image = new byte[rowLength * ySize]; + for (int y = 0; y < ySize; y++) { + readScanLine(pcxHeader, is, scanline); + if (pcxHeader.bitsPerPixel == 24) { + System.arraycopy(scanline, 0, image, y * rowLength, + rowLength); + } else { + for (int x = 0; x < xSize; x++) { + image[y * rowLength + 3 * x] = scanline[4 * x]; + image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1]; + image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2]; + } + } + } + final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); + final WritableRaster raster = Raster.createInterleavedRaster( + dataBuffer, xSize, ySize, rowLength, 3, + new int[] { 2, 1, 0 }, null); + final ColorModel colorModel = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, + Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + } else { + throw new ImageReadException( + "Invalid/unsupported image with bitsPerPixel " + + pcxHeader.bitsPerPixel + " and planes " + + pcxHeader.nPlanes); + } + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + Map params) throws ImageReadException, IOException { + params = (params == null) ? new HashMap() : new HashMap(params); + boolean isStrict = false; + final Object strictness = params.get(PARAM_KEY_STRICT); + if (strictness != null) { + isStrict = ((Boolean) strictness).booleanValue(); + } + + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final PcxHeader pcxHeader = readPcxHeader(is, isStrict); + final BufferedImage ret = readImage(pcxHeader, is, byteSource); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + new PcxWriter(params).writeImage(src, os); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java new file mode 100644 index 0000000..8cc6464 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java @@ -0,0 +1,407 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.pcx; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.ImagingConstants; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.palette.SimplePalette; + +class PcxWriter { + private int encoding; + private int bitDepth = -1; + private PixelDensity pixelDensity; + + public PcxWriter(Map params) throws ImageWriteException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(ImagingConstants.PARAM_KEY_FORMAT)) { + params.remove(ImagingConstants.PARAM_KEY_FORMAT); + } + + // uncompressed PCX files are not even documented in ZSoft's spec, + // let alone supported by most image viewers + encoding = PcxImageParser.PcxHeader.ENCODING_RLE; + if (params.containsKey(PcxConstants.PARAM_KEY_PCX_COMPRESSION)) { + final Object value = params.remove(PcxConstants.PARAM_KEY_PCX_COMPRESSION); + if (value != null) { + if (!(value instanceof Number)) { + throw new ImageWriteException( + "Invalid compression parameter: " + value); + } + final int compression = ((Number) value).intValue(); + if (compression == PcxConstants.PCX_COMPRESSION_UNCOMPRESSED) { + encoding = PcxImageParser.PcxHeader.ENCODING_UNCOMPRESSED; + } + } + } + + if (params.containsKey(PcxConstants.PARAM_KEY_PCX_BIT_DEPTH)) { + final Object value = params.remove(PcxConstants.PARAM_KEY_PCX_BIT_DEPTH); + if (value != null) { + if (!(value instanceof Number)) { + throw new ImageWriteException( + "Invalid bit depth parameter: " + value); + } + bitDepth = ((Number) value).intValue(); + } + } + + if (params.containsKey(ImagingConstants.PARAM_KEY_PIXEL_DENSITY)) { + final Object value = params.remove(ImagingConstants.PARAM_KEY_PIXEL_DENSITY); + if (value != null) { + if (!(value instanceof PixelDensity)) { + throw new ImageWriteException( + "Invalid pixel density parameter"); + } + pixelDensity = (PixelDensity) value; + } + } + if (pixelDensity == null) { + // DPI is mandatory, so we have to invent something + pixelDensity = PixelDensity.createFromPixelsPerInch(72, 72); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + } + + private void writeScanLine(final BinaryOutputStream bos, final byte[] scanline) + throws IOException, ImageWriteException { + if (encoding == PcxImageParser.PcxHeader.ENCODING_UNCOMPRESSED) { + bos.write(scanline); + } else { + if (encoding == PcxImageParser.PcxHeader.ENCODING_RLE) { + int previousByte = -1; + int repeatCount = 0; + for (final byte element : scanline) { + if ((element & 0xff) == previousByte + && repeatCount < 63) { + ++repeatCount; + } else { + if (repeatCount > 0) { + if (repeatCount == 1 + && (previousByte & 0xc0) != 0xc0) { + bos.write(previousByte); + } else { + bos.write(0xc0 | repeatCount); + bos.write(previousByte); + } + } + previousByte = 0xff & element; + repeatCount = 1; + } + } + if (repeatCount > 0) { + if (repeatCount == 1 && (previousByte & 0xc0) != 0xc0) { + bos.write(previousByte); + } else { + bos.write(0xc0 | repeatCount); + bos.write(previousByte); + } + } + } else { + throw new ImageWriteException("Invalid PCX encoding " + + encoding); + } + } + } + + public void writeImage(final BufferedImage src, final OutputStream os) + throws ImageWriteException, IOException { + final PaletteFactory paletteFactory = new PaletteFactory(); + final SimplePalette palette = paletteFactory + .makeExactRgbPaletteSimple(src, 256); + final BinaryOutputStream bos = new BinaryOutputStream(os, + ByteOrder.LITTLE_ENDIAN); + if (palette == null || bitDepth == 24 || bitDepth == 32) { + if (bitDepth == 32) { + write32BppPCX(src, bos); + } else { + write24BppPCX(src, bos); + } + } else if (palette.length() > 16 || bitDepth == 8) { + write256ColorPCX(src, palette, bos); + } else if (palette.length() > 2 || bitDepth == 4) { + write16ColorPCX(src, palette, bos); + } else { + boolean onlyBlackAndWhite = true; + if (palette.length() >= 1) { + final int rgb = palette.getEntry(0); + if (rgb != 0 && rgb != 0xffffff) { + onlyBlackAndWhite = false; + } + } + if (palette.length() == 2) { + final int rgb = palette.getEntry(1); + if (rgb != 0 && rgb != 0xffffff) { + onlyBlackAndWhite = false; + } + } + if (onlyBlackAndWhite) { + writeBlackAndWhitePCX(src, bos); + } else { + write16ColorPCX(src, palette, bos); + } + } + } + + private void write32BppPCX(final BufferedImage src, final BinaryOutputStream bos) + throws ImageWriteException, IOException { + final int bytesPerLine = src.getWidth() % 2 == 0 ? src.getWidth() : src + .getWidth() + 1; + + // PCX header + bos.write(10); // manufacturer + bos.write(5); // version + bos.write(encoding); // encoding + bos.write(32); // bits per pixel + bos.write2Bytes(0); // xMin + bos.write2Bytes(0); // yMin + bos.write2Bytes(src.getWidth() - 1); // xMax + bos.write2Bytes(src.getHeight() - 1); // yMax + bos.write2Bytes((short) Math.round(pixelDensity + .horizontalDensityInches())); // hDpi + bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi + bos.write(new byte[48]); // 16 color palette + bos.write(0); // reserved + bos.write(1); // planes + bos.write2Bytes(bytesPerLine); // bytes per line + bos.write2Bytes(1); // palette info + bos.write2Bytes(0); // hScreenSize + bos.write2Bytes(0); // vScreenSize + bos.write(new byte[54]); + + final int[] rgbs = new int[src.getWidth()]; + final byte[] rgbBytes = new byte[4 * bytesPerLine]; + for (int y = 0; y < src.getHeight(); y++) { + src.getRGB(0, y, src.getWidth(), 1, rgbs, 0, src.getWidth()); + for (int x = 0; x < rgbs.length; x++) { + rgbBytes[4 * x + 0] = (byte) (rgbs[x] & 0xff); + rgbBytes[4 * x + 1] = (byte) ((rgbs[x] >> 8) & 0xff); + rgbBytes[4 * x + 2] = (byte) ((rgbs[x] >> 16) & 0xff); + rgbBytes[4 * x + 3] = 0; + } + writeScanLine(bos, rgbBytes); + } + } + + private void write24BppPCX(final BufferedImage src, final BinaryOutputStream bos) + throws ImageWriteException, IOException { + final int bytesPerLine = src.getWidth() % 2 == 0 ? src.getWidth() : src + .getWidth() + 1; + + // PCX header + bos.write(10); // manufacturer + bos.write(5); // version + bos.write(encoding); // encoding + bos.write(8); // bits per pixel + bos.write2Bytes(0); // xMin + bos.write2Bytes(0); // yMin + bos.write2Bytes(src.getWidth() - 1); // xMax + bos.write2Bytes(src.getHeight() - 1); // yMax + bos.write2Bytes((short) Math.round(pixelDensity + .horizontalDensityInches())); // hDpi + bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi + bos.write(new byte[48]); // 16 color palette + bos.write(0); // reserved + bos.write(3); // planes + bos.write2Bytes(bytesPerLine); // bytes per line + bos.write2Bytes(1); // palette info + bos.write2Bytes(0); // hScreenSize + bos.write2Bytes(0); // vScreenSize + bos.write(new byte[54]); + + final int[] rgbs = new int[src.getWidth()]; + final byte[] rgbBytes = new byte[3 * bytesPerLine]; + for (int y = 0; y < src.getHeight(); y++) { + src.getRGB(0, y, src.getWidth(), 1, rgbs, 0, src.getWidth()); + for (int x = 0; x < rgbs.length; x++) { + rgbBytes[x] = (byte) ((rgbs[x] >> 16) & 0xff); + rgbBytes[bytesPerLine + x] = (byte) ((rgbs[x] >> 8) & 0xff); + rgbBytes[2 * bytesPerLine + x] = (byte) (rgbs[x] & 0xff); + } + writeScanLine(bos, rgbBytes); + } + } + + private void writeBlackAndWhitePCX(final BufferedImage src, + final BinaryOutputStream bos) + throws ImageWriteException, IOException { + int bytesPerLine = (src.getWidth() + 7) / 8; + if (bytesPerLine % 2 != 0) { + ++bytesPerLine; + } + + // PCX header + bos.write(10); // manufacturer + bos.write(3); // version - it seems some apps only open + // black and white files if the version is 3... + bos.write(encoding); // encoding + bos.write(1); // bits per pixel + bos.write2Bytes(0); // xMin + bos.write2Bytes(0); // yMin + bos.write2Bytes(src.getWidth() - 1); // xMax + bos.write2Bytes(src.getHeight() - 1); // yMax + bos.write2Bytes((short) Math.round(pixelDensity + .horizontalDensityInches())); // hDpi + bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi + bos.write(new byte[48]); // 16 color palette + bos.write(0); // reserved + bos.write(1); // planes + bos.write2Bytes(bytesPerLine); // bytes per line + bos.write2Bytes(1); // palette info + bos.write2Bytes(0); // hScreenSize + bos.write2Bytes(0); // vScreenSize + bos.write(new byte[54]); + + final byte[] row = new byte[bytesPerLine]; + for (int y = 0; y < src.getHeight(); y++) { + Arrays.fill(row, (byte) 0); + for (int x = 0; x < src.getWidth(); x++) { + final int rgb = 0xffffff & src.getRGB(x, y); + int bit; + if (rgb == 0x000000) { + bit = 0; + } else if (rgb == 0xffffff) { + bit = 1; + } else { + throw new ImageWriteException( + "Pixel neither black nor white"); + } + row[x / 8] |= (bit << (7 - (x % 8))); + } + writeScanLine(bos, row); + } + } + + private void write16ColorPCX(final BufferedImage src, final SimplePalette palette, + final BinaryOutputStream bos) throws ImageWriteException, IOException { + int bytesPerLine = (src.getWidth() + 1) / 2; + if (bytesPerLine % 2 != 0) { + ++bytesPerLine; + } + + final byte[] palette16 = new byte[16 * 3]; + for (int i = 0; i < 16; i++) { + int rgb; + if (i < palette.length()) { + rgb = palette.getEntry(i); + } else { + rgb = 0; + } + palette16[3 * i + 0] = (byte) (0xff & (rgb >> 16)); + palette16[3 * i + 1] = (byte) (0xff & (rgb >> 8)); + palette16[3 * i + 2] = (byte) (0xff & rgb); + } + + // PCX header + bos.write(10); // manufacturer + bos.write(5); // version + bos.write(encoding); // encoding + bos.write(4); // bits per pixel + bos.write2Bytes(0); // xMin + bos.write2Bytes(0); // yMin + bos.write2Bytes(src.getWidth() - 1); // xMax + bos.write2Bytes(src.getHeight() - 1); // yMax + bos.write2Bytes((short) Math.round(pixelDensity + .horizontalDensityInches())); // hDpi + bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi + bos.write(palette16); // 16 color palette + bos.write(0); // reserved + bos.write(1); // planes + bos.write2Bytes(bytesPerLine); // bytes per line + bos.write2Bytes(1); // palette info + bos.write2Bytes(0); // hScreenSize + bos.write2Bytes(0); // vScreenSize + bos.write(new byte[54]); + + final byte[] indeces = new byte[bytesPerLine]; + for (int y = 0; y < src.getHeight(); y++) { + Arrays.fill(indeces, (byte) 0); + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + final int index = palette.getPaletteIndex(0xffffff & argb); + indeces[x / 2] |= (index << 4 * (1 - (x % 2))); + } + writeScanLine(bos, indeces); + } + } + + private void write256ColorPCX(final BufferedImage src, final SimplePalette palette, + final BinaryOutputStream bos) throws ImageWriteException, IOException { + final int bytesPerLine = src.getWidth() % 2 == 0 ? src.getWidth() : src + .getWidth() + 1; + + // PCX header + bos.write(10); // manufacturer + bos.write(5); // version + bos.write(encoding); // encoding + bos.write(8); // bits per pixel + bos.write2Bytes(0); // xMin + bos.write2Bytes(0); // yMin + bos.write2Bytes(src.getWidth() - 1); // xMax + bos.write2Bytes(src.getHeight() - 1); // yMax + bos.write2Bytes((short) Math.round(pixelDensity + .horizontalDensityInches())); // hDpi + bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi + bos.write(new byte[48]); // 16 color palette + bos.write(0); // reserved + bos.write(1); // planes + bos.write2Bytes(bytesPerLine); // bytes per line + bos.write2Bytes(1); // palette info + bos.write2Bytes(0); // hScreenSize + bos.write2Bytes(0); // vScreenSize + bos.write(new byte[54]); + + final byte[] indeces = new byte[bytesPerLine]; + for (int y = 0; y < src.getHeight(); y++) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + final int index = palette.getPaletteIndex(0xffffff & argb); + indeces[x] = (byte) index; + } + writeScanLine(bos, indeces); + } + // palette + bos.write(12); + for (int i = 0; i < 256; i++) { + int rgb; + if (i < palette.length()) { + rgb = palette.getEntry(i); + } else { + rgb = 0; + } + bos.write((rgb >> 16) & 0xff); + bos.write((rgb >> 8) & 0xff); + bos.write(rgb & 0xff); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pcx/package-info.java b/src/main/java/org/apache/commons/imaging/formats/pcx/package-info.java new file mode 100644 index 0000000..6bbde6a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pcx/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The PCX image format. + */ +package org.apache.commons.imaging.formats.pcx; + diff --git a/src/main/java/org/apache/sanselan/formats/png/BitParser.java b/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java similarity index 57% rename from src/main/java/org/apache/sanselan/formats/png/BitParser.java rename to src/main/java/org/apache/commons/imaging/formats/png/BitParser.java index 6827f09..5a82faa 100644 --- a/src/main/java/org/apache/sanselan/formats/png/BitParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java @@ -1,74 +1,67 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class BitParser -{ - private final byte bytes[]; - private final int bitsPerPixel; - private final int bitDepth; - - public BitParser(byte bytes[], int bitsPerPixel, int bitDepth) - { - this.bytes = bytes; - this.bitsPerPixel = bitsPerPixel; - this.bitDepth = bitDepth; - } - - public int getSample(int pixelIndexInScanline, int sampleIndex) - throws ImageReadException, IOException - { - int pixelIndexBits = bitsPerPixel * pixelIndexInScanline; - int sampleIndexBits = pixelIndexBits + (sampleIndex * bitDepth); - int sampleIndexBytes = sampleIndexBits >> 3; - - if (bitDepth == 8) - return 0xff & bytes[sampleIndexBytes]; - else if (bitDepth < 8) - { - int b = 0xff & bytes[sampleIndexBytes]; - int bitsToShift = 8 - ((pixelIndexBits & 7) + bitDepth); - b >>= bitsToShift; - - int bitmask = (1 << bitDepth) - 1; - return b & bitmask; - } else if (bitDepth == 16) - { - return (((0xff & bytes[sampleIndexBytes]) << 8) | (0xff & bytes[sampleIndexBytes + 1])); - } - - throw new ImageReadException("PNG: bad BitDepth: " + bitDepth); - } - - public int getSampleAsByte(int pixelIndexInScanline, int sampleIndex) - throws ImageReadException, IOException - { - int sample = getSample(pixelIndexInScanline, sampleIndex); - - int rot = 8 - bitDepth; - if (rot > 0) - sample <<= rot; - else if (rot < 0) - sample >>= -rot; - - return 0xff & sample; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import org.apache.commons.imaging.ImageReadException; + +class BitParser { + private final byte[] bytes; + private final int bitsPerPixel; + private final int bitDepth; + + public BitParser(final byte[] bytes, final int bitsPerPixel, final int bitDepth) { + this.bytes = bytes; + this.bitsPerPixel = bitsPerPixel; + this.bitDepth = bitDepth; + } + + public int getSample(final int pixelIndexInScanline, final int sampleIndex) + throws ImageReadException { + final int pixelIndexBits = bitsPerPixel * pixelIndexInScanline; + final int sampleIndexBits = pixelIndexBits + (sampleIndex * bitDepth); + final int sampleIndexBytes = sampleIndexBits >> 3; + + if (bitDepth == 8) { + return 0xff & bytes[sampleIndexBytes]; + } else if (bitDepth < 8) { + int b = 0xff & bytes[sampleIndexBytes]; + final int bitsToShift = 8 - ((pixelIndexBits & 7) + bitDepth); + b >>= bitsToShift; + + final int bitmask = (1 << bitDepth) - 1; + return b & bitmask; + } else if (bitDepth == 16) { + return (((0xff & bytes[sampleIndexBytes]) << 8) | (0xff & bytes[sampleIndexBytes + 1])); + } + + throw new ImageReadException("PNG: bad BitDepth: " + bitDepth); + } + + public int getSampleAsByte(final int pixelIndexInScanline, final int sampleIndex) + throws ImageReadException { + int sample = getSample(pixelIndexInScanline, sampleIndex); + + final int rot = 8 - bitDepth; + if (rot > 0) { + sample = sample * 255 / ((1 << bitDepth) - 1); + } else if (rot < 0) { + sample >>= -rot; + } + + return 0xff & sample; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java b/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java new file mode 100644 index 0000000..cd80822 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import org.apache.commons.imaging.common.BinaryFunctions; + +/** + * Type of a PNG chunk. + * + * @see Portable Network Graphics Specification - Chunk specifications + */ +public enum ChunkType { + + /** Image header */ + IHDR, + + /** Palette */ + PLTE, + + /** Image data */ + IDAT, + + /** Image trailer */ + IEND, + + /** Transparency */ + tRNS, + + /** Primary chromaticities and white point */ + cHRM, + + /** Image gamma */ + gAMA, + + /** Embedded ICC profile */ + iCCP, + + /** Significant bits*/ + sBIT, + + /** Standard RGB colour space */ + sRGB, + + /** Textual data */ + tEXt, + + /** Compressed textual data */ + zTXt, + + /** International textual data */ + iTXt, + + /** Background colour */ + bKGD, + + /** Image histogram */ + hIST, + + /** Physical pixel dimensions */ + pHYs, + + /** Suggested palette */ + sPLT, + + /** Image last-modification time */ + tIME; + + final byte[] array; + final int value; + + private ChunkType() { + char[] chars = name().toCharArray(); + array = name().getBytes(); + value = BinaryFunctions.charsToQuad(chars[0], chars[1], chars[2], chars[3]); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ColorType.java b/src/main/java/org/apache/commons/imaging/formats/png/ColorType.java new file mode 100644 index 0000000..f15a162 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/ColorType.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.png; + +import java.util.Arrays; + +public enum ColorType { + + GREYSCALE(0, true, false, 1, new int[]{1, 2, 4, 8, 16}), + TRUE_COLOR(2, false, false, 3, new int[]{8, 16}), + INDEXED_COLOR(3, false, false, 1, new int[]{1, 2, 4, 8}), + GREYSCALE_WITH_ALPHA(4, true, true, 2, new int[]{8, 16}), + TRUE_COLOR_WITH_ALPHA(6, false, true, 4, new int[]{8, 16}); + + private final int value; + private final boolean greyscale; + private final boolean alpha; + private final int samplesPerPixel; + private final int[] allowedBitDepths; + + ColorType(int value, boolean greyscale, boolean alpha, int samplesPerPixel, int[] allowedBitDepths) { + this.value = value; + this.greyscale = greyscale; + this.alpha = alpha; + this.samplesPerPixel = samplesPerPixel; + this.allowedBitDepths = allowedBitDepths; + } + + int getValue() { + return value; + } + + boolean isGreyscale() { + return greyscale; + } + + boolean hasAlpha() { + return alpha; + } + + int getSamplesPerPixel() { + return samplesPerPixel; + } + + boolean isBitDepthAllowed(int bitDepth) { + return Arrays.binarySearch(allowedBitDepths, bitDepth) >= 0; + } + + public static ColorType getColorType(int value) { + for (ColorType type : values()) { + if (type.value == value) { + return type; + } + } + + return null; + } + + static ColorType getColorType(boolean alpha, boolean grayscale) { + if (grayscale) { + if (alpha) { + return ColorType.GREYSCALE_WITH_ALPHA; + } else { + return ColorType.GREYSCALE; + } + } else if (alpha) { + return ColorType.TRUE_COLOR_WITH_ALPHA; + } else { + return ColorType.TRUE_COLOR; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/FilterType.java b/src/main/java/org/apache/commons/imaging/formats/png/FilterType.java new file mode 100644 index 0000000..2dd9f7b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/FilterType.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +/** + * Filter types for filter method 0. + * + * @see Portable Network Graphics Specification - Filtering + */ +enum FilterType { + NONE, + SUB, + UP, + AVERAGE, + PAETH +} diff --git a/src/main/java/org/apache/sanselan/formats/png/GammaCorrection.java b/src/main/java/org/apache/commons/imaging/formats/png/GammaCorrection.java similarity index 58% rename from src/main/java/org/apache/sanselan/formats/png/GammaCorrection.java rename to src/main/java/org/apache/commons/imaging/formats/png/GammaCorrection.java index e9cfcef..5f7fcc5 100644 --- a/src/main/java/org/apache/sanselan/formats/png/GammaCorrection.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/GammaCorrection.java @@ -1,81 +1,68 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -public class GammaCorrection -{ - private static final boolean DEBUG = false; - - private final int lookupTable[]; - - public GammaCorrection(double src_gamma, double dst_gamma) - { - - if (DEBUG) - { - System.out.println("src_gamma: " + src_gamma); - System.out.println("dst_gamma: " + dst_gamma); - } - - lookupTable = new int[256]; - for (int i = 0; i < 256; i++) - { - lookupTable[i] = correctSample(i, src_gamma, dst_gamma); - if (DEBUG) - { - System.out - .println("lookup_table[" + i + "]: " + lookupTable[i]); - } - } - } - - public int correctSample(int sample) - { - return lookupTable[sample]; - } - - public int correctARGB(int pixel) - { - int alpha = (0xff000000) & pixel; - int red = (pixel >> 16) & 0xff; - int green = (pixel >> 8) & 0xff; - int blue = (pixel >> 0) & 0xff; - - red = correctSample(red); - green = correctSample(green); - blue = correctSample(blue); - - int rgb = alpha | ((0xff & red) << 16) | ((0xff & green) << 8) - | ((0xff & blue) << 0); - - return rgb; - } - - private int correctSample(int sample, double src_gamma, double dst_gamma) - { - // if (kUseAdobeGammaMethod && val <= 32) - // { - // double slope = Math.round(255.0d * Math.pow((32.0 / 255.0d), - // src_gamma / dst_gamma)) / 32.d; - // return (int) (sample * slope); - // } - - return (int) Math.round(255.0d * Math.pow((sample / 255.0d), src_gamma - / dst_gamma)); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +public class GammaCorrection { + private static final boolean DEBUG = false; + + private final int[] lookupTable; + + public GammaCorrection(final double srcGamma, final double dstGamma) { + + if (DEBUG) { + System.out.println("src_gamma: " + srcGamma); + System.out.println("dst_gamma: " + dstGamma); + } + + lookupTable = new int[256]; + for (int i = 0; i < 256; i++) { + lookupTable[i] = correctSample(i, srcGamma, dstGamma); + if (DEBUG) { + System.out.println("lookup_table[" + i + "]: " + lookupTable[i]); + } + } + } + + public int correctSample(final int sample) { + return lookupTable[sample]; + } + + public int correctARGB(final int pixel) { + final int alpha = (0xff000000) & pixel; + int red = (pixel >> 16) & 0xff; + int green = (pixel >> 8) & 0xff; + int blue = (pixel >> 0) & 0xff; + + red = correctSample(red); + green = correctSample(green); + blue = correctSample(blue); + + return alpha | ((0xff & red) << 16) | ((0xff & green) << 8) | ((0xff & blue) << 0); + } + + private int correctSample(final int sample, final double srcGamma, final double dstGamma) { + // if (kUseAdobeGammaMethod && val <= 32) + // { + // double slope = Math.round(255.0d * Math.pow((32.0 / 255.0d), + // src_gamma / dst_gamma)) / 32.d; + // return (int) (sample * slope); + // } + + return (int) Math.round(255.0d * Math.pow((sample / 255.0d), srcGamma / dstGamma)); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/InterlaceMethod.java b/src/main/java/org/apache/commons/imaging/formats/png/InterlaceMethod.java new file mode 100644 index 0000000..c8d890d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/InterlaceMethod.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +/** + * Interlace methods. + * + * @see Portable Network Graphics Specification - Interlacing and pass extraction + */ +public enum InterlaceMethod { + + NONE(false), + ADAM7(true); + + private final boolean progressive; + + private InterlaceMethod(boolean progressive) { + this.progressive = progressive; + } + + public boolean isProgressive() { + return progressive; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngConstants.java b/src/main/java/org/apache/commons/imaging/formats/png/PngConstants.java new file mode 100644 index 0000000..c30a4b5 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngConstants.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import org.apache.commons.imaging.common.BinaryConstant; + +public interface PngConstants { + + public static final int COMPRESSION_DEFLATE_INFLATE = 0; + + public static final BinaryConstant PNG_SIGNATURE = new BinaryConstant( + new byte[] { (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n', }); + + public static final String PARAM_KEY_PNG_BIT_DEPTH = "PNG_BIT_DEPTH"; + public static final String PARAM_KEY_PNG_FORCE_INDEXED_COLOR = "PNG_FORCE_INDEXED_COLOR"; + public static final String PARAM_KEY_PNG_FORCE_TRUE_COLOR = "PNG_FORCE_TRUE_COLOR"; + + // public static final Object PARAM_KEY_PNG_BIT_DEPTH_YES = "YES"; + // public static final Object PARAM_KEY_PNG_BIT_DEPTH_NO = "NO"; + + public static final byte COMPRESSION_TYPE_INFLATE_DEFLATE = 0; + public static final byte FILTER_METHOD_ADAPTIVE = 0; + + /* + * Background colour Solid background colour to be used when presenting the + * image if no better option is available. Gamma and chromaticity Gamma + * characteristic of the image with respect to the desired output intensity, + * and chromaticity characteristics of the RGB values used in the image. ICC + * profile Description of the colour space (in the form of an International + * Color Consortium (ICC) profile) to which the samples in the image + * conform. Image histogram Estimates of how frequently the image uses each + * palette entry. Physical pixel dimensions Intended pixel size and aspect + * ratio to be used in presenting the PNG image. Significant bits The number + * of bits that are significant in the samples. sRGB colour space A + * rendering intent (as defined by the International Color Consortium) and + * an indication that the image samples conform to this colour space. + * Suggested palette A reduced palette that may be used when the display + * device is not capable of displaying the full range of colours in the + * image. Textual data Textual information (which may be compressed) + * associated with the image. Time The time when the PNG image was last + * modified. Transparency Alpha information that allows the reference image + * to be reconstructed when the alpha channel is not retained in the PNG + * image. + */ + + public static final String XMP_KEYWORD = "XML:com.adobe.xmp"; + + /** + * Parameter key. + * + * Only used when writing Png images. + *

+ * Valid values: a list of WriteTexts. + *

+ */ + public static final String PARAM_KEY_PNG_TEXT_CHUNKS = "PNG_TEXT_CHUNKS"; + +} diff --git a/src/main/java/org/apache/sanselan/formats/png/PngCrc.java b/src/main/java/org/apache/commons/imaging/formats/png/PngCrc.java similarity index 58% rename from src/main/java/org/apache/sanselan/formats/png/PngCrc.java rename to src/main/java/org/apache/commons/imaging/formats/png/PngCrc.java index 66464eb..d10594e 100644 --- a/src/main/java/org/apache/sanselan/formats/png/PngCrc.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngCrc.java @@ -1,91 +1,85 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -// should just use ints, not longs -public class PngCrc -{ - /* Table of CRCs of all 8-bit messages. */ - private final long crc_table[] = new long[256]; - - /* Flag: has the table been computed? Initially false. */ - private boolean crc_table_computed = false; - - /* Make the table for a fast CRC. */ - private void make_crc_table() - { - long c; - int n, k; - - for (n = 0; n < 256; n++) - { - c = n; - for (k = 0; k < 8; k++) - { - if ((c & 1) != 0) - - c = 0xedb88320L ^ (c >> 1); - else - c = c >> 1; - } - crc_table[n] = c; - } - crc_table_computed = true; - } - - /* Update a running CRC with the bytes buf[0..len-1]--the CRC - should be initialized to all 1's, and the transmitted value - is the 1's complement of the final running CRC (see the - crc() routine below)). */ - - private final long update_crc(long crc, byte buf[]) - { - long c = crc; - int n; - - if (!crc_table_computed) - make_crc_table(); - for (n = 0; n < buf.length; n++) - { - // Debug.debug("crc[" + n + "]", c + " (" + Long.toHexString(c) + ")"); - - c = crc_table[(int) ((c ^ buf[n]) & 0xff)] ^ (c >> 8); - } - return c; - } - - /* Return the CRC of the bytes buf[0..len-1]. */ - public final int crc(byte buf[], int len) - { - return (int) (update_crc(0xffffffffL, buf) ^ 0xffffffffL); - } - - public final long start_partial_crc(byte buf[], int len) - { - return update_crc(0xffffffffL, buf); - } - - public final long continue_partial_crc(long old_crc, byte buf[], int len) - { - return update_crc(old_crc, buf); - } - - public final long finish_partial_crc(long old_crc) - { - return (old_crc ^ 0xffffffffL); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +// should just use ints, not longs +class PngCrc { + /* Table of CRCs of all 8-bit messages. */ + private final long[] crc_table = new long[256]; + + /* Flag: has the table been computed? Initially false. */ + private boolean crc_table_computed; + + /* Make the table for a fast CRC. */ + private void make_crc_table() { + long c; + int n; + int k; + + for (n = 0; n < 256; n++) { + c = n; + for (k = 0; k < 8; k++) { + if ((c & 1) != 0) { + c = 0xedb88320L ^ (c >> 1); + } else { + c = c >> 1; + } + } + crc_table[n] = c; + } + crc_table_computed = true; + } + + /* + * Update a running CRC with the bytes buf[0..len-1]--the CRC should be + * initialized to all 1's, and the transmitted value is the 1's complement + * of the final running CRC (see the crc() routine below)). + */ + + private long update_crc(final long crc, final byte[] buf) { + long c = crc; + int n; + + if (!crc_table_computed) { + make_crc_table(); + } + for (n = 0; n < buf.length; n++) { + // Debug.debug("crc[" + n + "]", c + " (" + Long.toHexString(c) + + // ")"); + + c = crc_table[(int) ((c ^ buf[n]) & 0xff)] ^ (c >> 8); + } + return c; + } + + /* Return the CRC of the bytes buf[0..len-1]. */ + public final int crc(final byte[] buf, final int len) { + return (int) (update_crc(0xffffffffL, buf) ^ 0xffffffffL); + } + + public final long start_partial_crc(final byte[] buf, final int len) { + return update_crc(0xffffffffL, buf); + } + + public final long continue_partial_crc(final long old_crc, final byte[] buf, final int len) { + return update_crc(old_crc, buf); + } + + public final long finish_partial_crc(final long old_crc) { + return (old_crc ^ 0xffffffffL); + } +} diff --git a/src/main/java/org/apache/sanselan/formats/png/PngImageInfo.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImageInfo.java similarity index 54% rename from src/main/java/org/apache/sanselan/formats/png/PngImageInfo.java rename to src/main/java/org/apache/commons/imaging/formats/png/PngImageInfo.java index dab05ee..23b623c 100644 --- a/src/main/java/org/apache/sanselan/formats/png/PngImageInfo.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImageInfo.java @@ -1,51 +1,49 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; - -public class PngImageInfo extends ImageInfo -{ - private final List textChunks; - - public PngImageInfo(String formatDetails, int bitsPerPixel, - ArrayList comments, ImageFormat format, String formatName, - int height, String mimeType, int numberOfImages, - int physicalHeightDpi, float physicalHeightInch, - int physicalWidthDpi, float physicalWidthInch, int width, - boolean isProgressive, boolean isTransparent, boolean usesPalette, - int colorType, String compressionAlgorithm, final List textChunks) - { - super(formatDetails, bitsPerPixel, comments, format, formatName, - height, mimeType, numberOfImages, physicalHeightDpi, - physicalHeightInch, physicalWidthDpi, physicalWidthInch, width, - isProgressive, isTransparent, usesPalette, colorType, - compressionAlgorithm); - - this.textChunks = textChunks; - } - - public List getTextChunks() - { - return new ArrayList(textChunks); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageInfo; + +public class PngImageInfo extends ImageInfo { + private final List textChunks; + + PngImageInfo(final String formatDetails, final int bitsPerPixel, + final List comments, final ImageFormat format, final String formatName, + final int height, final String mimeType, final int numberOfImages, + final int physicalHeightDpi, final float physicalHeightInch, + final int physicalWidthDpi, final float physicalWidthInch, final int width, + final boolean progressive, final boolean transparent, final boolean usesPalette, + final int colorType, final String compressionAlgorithm, + final List textChunks) { + super(formatDetails, bitsPerPixel, comments, format, formatName, + height, mimeType, numberOfImages, physicalHeightDpi, + physicalHeightInch, physicalWidthDpi, physicalWidthInch, width, + progressive, transparent, usesPalette, colorType, + compressionAlgorithm); + + this.textChunks = textChunks; + } + + public List getTextChunks() { + return new ArrayList(textChunks); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java new file mode 100644 index 0000000..4e47882 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java @@ -0,0 +1,731 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.color.ICC_ColorSpace; +import com.google.code.appengine.awt.color.ICC_Profile; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.imaging.ColorTools; +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.png.chunks.PngChunk; +import org.apache.commons.imaging.formats.png.chunks.PngChunkGama; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIccp; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIdat; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIhdr; +import org.apache.commons.imaging.formats.png.chunks.PngChunkItxt; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPhys; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte; +import org.apache.commons.imaging.formats.png.chunks.PngChunkText; +import org.apache.commons.imaging.formats.png.chunks.PngChunkZtxt; +import org.apache.commons.imaging.formats.png.chunks.PngTextChunk; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterGrayscale; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterIndexedColor; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterTrueColor; +import org.apache.commons.imaging.icc.IccProfileParser; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".png"; + private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; + + @Override + public String getName() { + return "Png-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.PNG, // + }; + } + + // private final static int tRNS = CharsToQuad('t', 'R', 'N', 's'); + + public static String getChunkTypeName(final int chunkType) { + final StringBuilder result = new StringBuilder(); + result.append((char) (0xff & (chunkType >> 24))); + result.append((char) (0xff & (chunkType >> 16))); + result.append((char) (0xff & (chunkType >> 8))); + result.append((char) (0xff & (chunkType >> 0))); + return result.toString(); + } + + /** + * @return List of String-formatted chunk types, ie. "tRNs". + */ + public List getChuckTypes(final InputStream is) + throws ImageReadException, IOException { + final List chunks = readChunks(is, null, false); + final List chunkTypes = new ArrayList(); + for (PngChunk chunk : chunks) { + chunkTypes.add(getChunkTypeName(chunk.chunkType)); + } + return chunkTypes; + } + + public boolean hasChuckType(final ByteSource byteSource, final ChunkType chunkType) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + readSignature(is); + final List chunks = readChunks(is, new ChunkType[] { chunkType }, true); + canThrow = true; + return !chunks.isEmpty(); + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private boolean keepChunk(final int chunkType, final ChunkType[] chunkTypes) { + // System.out.println("keepChunk: "); + if (chunkTypes == null) { + return true; + } + + for (final ChunkType chunkType2 : chunkTypes) { + if (chunkType2.value == chunkType) { + return true; + } + } + return false; + } + + private List readChunks(final InputStream is, final ChunkType[] chunkTypes, + final boolean returnAfterFirst) throws ImageReadException, IOException { + final List result = new ArrayList(); + + while (true) { + if (getDebug()) { + System.out.println(""); + } + + final int length = read4Bytes("Length", is, "Not a Valid PNG File", getByteOrder()); + final int chunkType = read4Bytes("ChunkType", is, "Not a Valid PNG File", getByteOrder()); + + if (getDebug()) { + printCharQuad("ChunkType", chunkType); + debugNumber("Length", length, 4); + } + final boolean keep = keepChunk(chunkType, chunkTypes); + + byte[] bytes = null; + if (keep) { + bytes = readBytes("Chunk Data", is, length, + "Not a Valid PNG File: Couldn't read Chunk Data."); + } else { + skipBytes(is, length, "Not a Valid PNG File"); + } + + if (getDebug()) { + if (bytes != null) { + debugNumber("bytes", bytes.length, 4); + } + } + + final int crc = read4Bytes("CRC", is, "Not a Valid PNG File", getByteOrder()); + + if (keep) { + if (chunkType == ChunkType.iCCP.value) { + result.add(new PngChunkIccp(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.tEXt.value) { + result.add(new PngChunkText(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.zTXt.value) { + result.add(new PngChunkZtxt(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.IHDR.value) { + result.add(new PngChunkIhdr(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.PLTE.value) { + result.add(new PngChunkPlte(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.pHYs.value) { + result.add(new PngChunkPhys(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.IDAT.value) { + result.add(new PngChunkIdat(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.gAMA.value) { + result.add(new PngChunkGama(length, chunkType, crc, bytes)); + } else if (chunkType == ChunkType.iTXt.value) { + result.add(new PngChunkItxt(length, chunkType, crc, bytes)); + } else { + result.add(new PngChunk(length, chunkType, crc, bytes)); + } + + if (returnAfterFirst) { + return result; + } + } + + if (chunkType == ChunkType.IEND.value) { + break; + } + + } + + return result; + + } + + public void readSignature(final InputStream is) throws ImageReadException, + IOException { + readAndVerifyBytes(is, PngConstants.PNG_SIGNATURE, + "Not a Valid PNG Segment: Incorrect Signature"); + + } + + private List readChunks(final ByteSource byteSource, final ChunkType[] chunkTypes, + final boolean returnAfterFirst) throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + readSignature(is); + + final List ret = readChunks(is, chunkTypes, returnAfterFirst); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.iCCP }, + true); + + if ((chunks == null) || (chunks.isEmpty())) { + // throw new ImageReadException("Png: No chunks"); + return null; + } + + if (chunks.size() > 1) { + throw new ImageReadException( + "PNG contains more than one ICC Profile "); + } + + final PngChunkIccp pngChunkiCCP = (PngChunkIccp) chunks.get(0); + final byte[] bytes = pngChunkiCCP.uncompressedProfile; + + return (bytes); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.IHDR, }, true); + + if ((chunks == null) || (chunks.isEmpty())) { + throw new ImageReadException("Png: No chunks"); + } + + if (chunks.size() > 1) { + throw new ImageReadException("PNG contains more than one Header"); + } + + final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) chunks.get(0); + + return new Dimension(pngChunkIHDR.width, pngChunkIHDR.height); + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.tEXt, ChunkType.zTXt, }, true); + + if ((chunks == null) || (chunks.isEmpty())) { + return null; + } + + final ImageMetadata result = new ImageMetadata(); + + for (PngChunk chunk : chunks) { + final PngTextChunk textChunk = (PngTextChunk) chunk; + + result.add(textChunk.getKeyword(), textChunk.getText()); + } + + return result; + } + + private List filterChunks(final List chunks, final ChunkType type) { + final List result = new ArrayList(); + + for (PngChunk chunk : chunks) { + if (chunk.chunkType == type.value) { + result.add(chunk); + } + } + + return result; + } + + // TODO: I have been too casual about making inner classes subclass of + // BinaryFileParser + // I may not have always preserved byte order correctly. + + private TransparencyFilter getTransparencyFilter(ColorType colorType, PngChunk pngChunktRNS) + throws ImageReadException, IOException { + switch (colorType) { + case GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale sample. + return new TransparencyFilterGrayscale(pngChunktRNS.getBytes()); + case TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple. + return new TransparencyFilterTrueColor(pngChunktRNS.getBytes()); + case INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette index; + return new TransparencyFilterIndexedColor(pngChunktRNS.getBytes()); + case GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a grayscale sample, + case TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an R,G,B triple, + default: + throw new ImageReadException("Simple Transparency not compatible with ColorType: " + colorType); + } + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List chunks = readChunks(byteSource, new ChunkType[] { + ChunkType.IHDR, + ChunkType.pHYs, + ChunkType.tEXt, + ChunkType.zTXt, + ChunkType.tRNS, + ChunkType.PLTE, + ChunkType.iTXt, + }, false); + + // if(chunks!=null) + // System.out.println("chunks: " + chunks.size()); + + if ((chunks == null) || (chunks.isEmpty())) { + throw new ImageReadException("PNG: no chunks"); + } + + final List IHDRs = filterChunks(chunks, ChunkType.IHDR); + if (IHDRs.size() != 1) { + throw new ImageReadException("PNG contains more than one Header"); + } + + final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0); + + boolean transparent = false; + + final List tRNSs = filterChunks(chunks, ChunkType.tRNS); + if (!tRNSs.isEmpty()) { + transparent = true; + } else { + // CE - Fix Alpha. + transparent = pngChunkIHDR.colorType.hasAlpha(); + // END FIX + } + + PngChunkPhys pngChunkpHYs = null; + + final List pHYss = filterChunks(chunks, ChunkType.pHYs); + if (pHYss.size() > 1) { + throw new ImageReadException("PNG contains more than one pHYs: " + + pHYss.size()); + } else if (pHYss.size() == 1) { + pngChunkpHYs = (PngChunkPhys) pHYss.get(0); + } + + final List tEXts = filterChunks(chunks, ChunkType.tEXt); + final List zTXts = filterChunks(chunks, ChunkType.zTXt); + final List iTXts = filterChunks(chunks, ChunkType.iTXt); + + final List comments = new ArrayList(); + final List textChunks = new ArrayList(); + + for (PngChunk tEXt : tEXts) { + final PngChunkText pngChunktEXt = (PngChunkText) tEXt; + comments.add(pngChunktEXt.keyword + ": " + pngChunktEXt.text); + textChunks.add(pngChunktEXt.getContents()); + } + for (PngChunk zTXt : zTXts) { + final PngChunkZtxt pngChunkzTXt = (PngChunkZtxt) zTXt; + comments.add(pngChunkzTXt.keyword + ": " + pngChunkzTXt.text); + textChunks.add(pngChunkzTXt.getContents()); + } + for (PngChunk iTXt : iTXts) { + final PngChunkItxt pngChunkiTXt = (PngChunkItxt) iTXt; + comments.add(pngChunkiTXt.keyword + ": " + pngChunkiTXt.text); + textChunks.add(pngChunkiTXt.getContents()); + } + + final int bitsPerPixel = pngChunkIHDR.bitDepth * pngChunkIHDR.colorType.getSamplesPerPixel(); + final ImageFormat format = ImageFormats.PNG; + final String formatName = "PNG Portable Network Graphics"; + final int height = pngChunkIHDR.height; + final String mimeType = "image/png"; + final int numberOfImages = 1; + final int width = pngChunkIHDR.width; + final boolean progressive = pngChunkIHDR.interlaceMethod.isProgressive(); + + int physicalHeightDpi = -1; + float physicalHeightInch = -1; + int physicalWidthDpi = -1; + float physicalWidthInch = -1; + + // if (pngChunkpHYs != null) + // { + // System.out.println("\t" + "pngChunkpHYs.UnitSpecifier: " + + // pngChunkpHYs.UnitSpecifier ); + // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitYAxis: " + + // pngChunkpHYs.PixelsPerUnitYAxis ); + // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitXAxis: " + + // pngChunkpHYs.PixelsPerUnitXAxis ); + // } + if ((pngChunkpHYs != null) && (pngChunkpHYs.unitSpecifier == 1)) { // meters + final double metersPerInch = 0.0254; + + physicalWidthDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch); + physicalWidthInch = (float) (width / (pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch)); + physicalHeightDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch); + physicalHeightInch = (float) (height / (pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch)); + } + + final String formatDetails = "Png"; + + boolean usesPalette = false; + + final List PLTEs = filterChunks(chunks, ChunkType.PLTE); + if (PLTEs.size() > 1) { + usesPalette = true; + } + + int colorType; + switch (pngChunkIHDR.colorType) { + case GREYSCALE: + case GREYSCALE_WITH_ALPHA: + colorType = ImageInfo.COLOR_TYPE_GRAYSCALE; + break; + case TRUE_COLOR: + case INDEXED_COLOR: + case TRUE_COLOR_WITH_ALPHA: + colorType = ImageInfo.COLOR_TYPE_RGB; + break; + default: + throw new ImageReadException("Png: Unknown ColorType: " + pngChunkIHDR.colorType); + } + + final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PNG_FILTER; + + return new PngImageInfo(formatDetails, bitsPerPixel, comments, + format, formatName, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm, textChunks); + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, Map params) + throws ImageReadException, IOException { + params = (params == null) ? new HashMap() : new HashMap(params); + + if (params.containsKey(PARAM_KEY_VERBOSE)) { + params.remove(PARAM_KEY_VERBOSE); + } + + // if (params.size() > 0) { + // Object firstKey = params.keySet().iterator().next(); + // throw new ImageWriteException("Unknown parameter: " + firstKey); + // } + + final List chunks = readChunks(byteSource, new ChunkType[] { + ChunkType.IHDR, + ChunkType.PLTE, + ChunkType.IDAT, + ChunkType.tRNS, + ChunkType.iCCP, + ChunkType.gAMA, + ChunkType.sRGB, + }, false); + + if ((chunks == null) || (chunks.isEmpty())) { + throw new ImageReadException("PNG: no chunks"); + } + + final List IHDRs = filterChunks(chunks, ChunkType.IHDR); + if (IHDRs.size() != 1) { + throw new ImageReadException("PNG contains more than one Header"); + } + + final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0); + + final List PLTEs = filterChunks(chunks, ChunkType.PLTE); + if (PLTEs.size() > 1) { + throw new ImageReadException("PNG contains more than one Palette"); + } + + PngChunkPlte pngChunkPLTE = null; + if (PLTEs.size() == 1) { + pngChunkPLTE = (PngChunkPlte) PLTEs.get(0); + } + + // ----- + + final List IDATs = filterChunks(chunks, ChunkType.IDAT); + if (IDATs.isEmpty()) { + throw new ImageReadException("PNG missing image data"); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (PngChunk IDAT : IDATs) { + final PngChunkIdat pngChunkIDAT = (PngChunkIdat) IDAT; + final byte[] bytes = pngChunkIDAT.getBytes(); + // System.out.println(i + ": bytes: " + bytes.length); + baos.write(bytes); + } + + final byte[] compressed = baos.toByteArray(); + + baos = null; + + TransparencyFilter transparencyFilter = null; + + final List tRNSs = filterChunks(chunks, ChunkType.tRNS); + if (!tRNSs.isEmpty()) { + final PngChunk pngChunktRNS = tRNSs.get(0); + transparencyFilter = getTransparencyFilter(pngChunkIHDR.colorType, pngChunktRNS); + } + + ICC_Profile iccProfile = null; + GammaCorrection gammaCorrection = null; + { + final List sRGBs = filterChunks(chunks, ChunkType.sRGB); + final List gAMAs = filterChunks(chunks, ChunkType.gAMA); + final List iCCPs = filterChunks(chunks, ChunkType.iCCP); + if (sRGBs.size() > 1) { + throw new ImageReadException("PNG: unexpected sRGB chunk"); + } + if (gAMAs.size() > 1) { + throw new ImageReadException("PNG: unexpected gAMA chunk"); + } + if (iCCPs.size() > 1) { + throw new ImageReadException("PNG: unexpected iCCP chunk"); + } + + if (sRGBs.size() == 1) { + // no color management neccesary. + if (getDebug()) { + System.out.println("sRGB, no color management neccesary."); + } + } else if (iCCPs.size() == 1) { + if (getDebug()) { + System.out.println("iCCP."); + } + + final PngChunkIccp pngChunkiCCP = (PngChunkIccp) iCCPs.get(0); + final byte[] bytes = pngChunkiCCP.uncompressedProfile; + + iccProfile = ICC_Profile.getInstance(bytes); + } else if (gAMAs.size() == 1) { + final PngChunkGama pngChunkgAMA = (PngChunkGama) gAMAs.get(0); + final double gamma = pngChunkgAMA.getGamma(); + + // charles: what is the correct target value here? + // double targetGamma = 2.2; + final double targetGamma = 1.0; + final double diff = Math.abs(targetGamma - gamma); + if (diff >= 0.5) { + gammaCorrection = new GammaCorrection(gamma, targetGamma); + } + + if (gammaCorrection != null) { + if (pngChunkPLTE != null) { + pngChunkPLTE.correct(gammaCorrection); + } + } + + } + } + + { + final int width = pngChunkIHDR.width; + final int height = pngChunkIHDR.height; + final ColorType colorType = pngChunkIHDR.colorType; + final int bitDepth = pngChunkIHDR.bitDepth; + + if (pngChunkIHDR.filterMethod != 0) { + throw new ImageReadException("PNG: unknown FilterMethod: " + pngChunkIHDR.filterMethod); + } + + final int bitsPerPixel = bitDepth * colorType.getSamplesPerPixel(); + + final boolean hasAlpha = colorType.hasAlpha() || transparencyFilter != null; + + BufferedImage result; + if (colorType.isGreyscale()) { + result = getBufferedImageFactory(params).getGrayscaleBufferedImage(width, height, hasAlpha); + } else { + result = getBufferedImageFactory(params).getColorBufferedImage(width, height, hasAlpha); + } + + final ByteArrayInputStream bais = new ByteArrayInputStream(compressed); + final InflaterInputStream iis = new InflaterInputStream(bais); + + ScanExpediter scanExpediter; + + switch (pngChunkIHDR.interlaceMethod) { + case NONE: + scanExpediter = new ScanExpediterSimple(width, height, iis, + result, colorType, bitDepth, bitsPerPixel, + pngChunkPLTE, gammaCorrection, transparencyFilter); + break; + case ADAM7: + scanExpediter = new ScanExpediterInterlaced(width, height, iis, + result, colorType, bitDepth, bitsPerPixel, + pngChunkPLTE, gammaCorrection, transparencyFilter); + break; + default: + throw new ImageReadException("Unknown InterlaceMethod: " + pngChunkIHDR.interlaceMethod); + } + + scanExpediter.drive(); + + if (iccProfile != null) { + final Boolean is_srgb = new IccProfileParser().issRGB(iccProfile); + if (is_srgb == null || !is_srgb.booleanValue()) { + final ICC_ColorSpace cs = new ICC_ColorSpace(iccProfile); + + final ColorModel srgbCM = ColorModel.getRGBdefault(); + final ColorSpace cs_sRGB = srgbCM.getColorSpace(); + + result = new ColorTools().convertBetweenColorSpaces(result, cs, cs_sRGB); + } + } + + return result; + + } + + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + final ImageInfo imageInfo = getImageInfo(byteSource); + if (imageInfo == null) { + return false; + } + + imageInfo.toString(pw, ""); + + final List chunks = readChunks(byteSource, null, false); + final List IHDRs = filterChunks(chunks, ChunkType.IHDR); + if (IHDRs.size() != 1) { + if (getDebug()) { + System.out.println("PNG contains more than one Header"); + } + return false; + } + final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0); + pw.println("Color: " + pngChunkIHDR.colorType.name()); + + pw.println("chunks: " + chunks.size()); + + if ((chunks.isEmpty())) { + return false; + } + + for (int i = 0; i < chunks.size(); i++) { + final PngChunk chunk = chunks.get(i); + printCharQuad(pw, "\t" + i + ": ", chunk.chunkType); + } + + pw.println(""); + + pw.flush(); + + return true; + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + new PngWriter(params).writeImage(src, os, params); + } + + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + + final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.iTXt }, false); + + if ((chunks == null) || (chunks.isEmpty())) { + return null; + } + + final List xmpChunks = new ArrayList(); + for (PngChunk chunk : chunks) { + final PngChunkItxt itxtChunk = (PngChunkItxt) chunk; + if (!itxtChunk.getKeyword().equals(PngConstants.XMP_KEYWORD)) { + continue; + } + xmpChunks.add(itxtChunk); + } + + if (xmpChunks.isEmpty()) { + return null; + } + if (xmpChunks.size() > 1) { + throw new ImageReadException( + "PNG contains more than one XMP chunk."); + } + + final PngChunkItxt chunk = xmpChunks.get(0); + return chunk.getText(); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/png/PngText.java b/src/main/java/org/apache/commons/imaging/formats/png/PngText.java similarity index 74% rename from src/main/java/org/apache/sanselan/formats/png/PngText.java rename to src/main/java/org/apache/commons/imaging/formats/png/PngText.java index 4791f6b..d3fb00c 100644 --- a/src/main/java/org/apache/sanselan/formats/png/PngText.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngText.java @@ -1,71 +1,63 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -public abstract class PngText -{ - public PngText(String keyword, String text) - { - this.keyword = keyword; - this.text = text; - } - - public final String keyword, text; - - public static class tEXt extends PngText - { - public tEXt(String keyword, String text) - { - super(keyword, text); - } - } - - public static class zTXt extends PngText - { - public zTXt(String keyword, String text) - { - super(keyword, text); - } - } - - public static class iTXt extends PngText - { - - /* - * The language tag defined in [RFC-3066] indicates the human language - * used by the translated keyword and the text. Unlike the keyword, the - * language tag is case-insensitive. It is an ISO 646.IRV:1991 [ISO 646] - * string consisting of hyphen-separated words of 1-8 alphanumeric - * characters each (for example cn, en-uk, no-bok, x-klingon, - * x-KlInGoN). If the first word is two or three letters long, it is an - * ISO language code [ISO-639]. If the language tag is empty, the - * language is unspecified. - */ - public final String languageTag; - - public final String translatedKeyword; - - public iTXt(String keyword, String text, String languageTag, - String translatedKeyword) - { - super(keyword, text); - this.languageTag = languageTag; - this.translatedKeyword = translatedKeyword; - } - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +public abstract class PngText { + public final String keyword; + public final String text; + + public PngText(final String keyword, final String text) { + this.keyword = keyword; + this.text = text; + } + + public static class Text extends PngText { + public Text(final String keyword, final String text) { + super(keyword, text); + } + } + + public static class Ztxt extends PngText { + public Ztxt(final String keyword, final String text) { + super(keyword, text); + } + } + + public static class Itxt extends PngText { + + /* + * The language tag defined in [RFC-3066] indicates the human language + * used by the translated keyword and the text. Unlike the keyword, the + * language tag is case-insensitive. It is an ISO 646.IRV:1991 [ISO 646] + * string consisting of hyphen-separated words of 1-8 alphanumeric + * characters each (for example cn, en-uk, no-bok, x-klingon, + * x-KlInGoN). If the first word is two or three letters long, it is an + * ISO language code [ISO-639]. If the language tag is empty, the + * language is unspecified. + */ + public final String languageTag; + + public final String translatedKeyword; + + public Itxt(final String keyword, final String text, final String languageTag, final String translatedKeyword) { + super(keyword, text); + this.languageTag = languageTag; + this.translatedKeyword = translatedKeyword; + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java b/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java new file mode 100644 index 0000000..5c5a4c9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java @@ -0,0 +1,666 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.DeflaterOutputStream; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.ImagingConstants; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.palette.Palette; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.palette.SimplePalette; +import org.apache.commons.imaging.util.Debug; +import org.apache.commons.imaging.util.IoUtils; + +class PngWriter { + private final boolean verbose; + + public PngWriter(final boolean verbose) { + this.verbose = verbose; + } + + public PngWriter(final Map params) { + this.verbose = params != null && Boolean.TRUE.equals(params.get(ImagingConstants.PARAM_KEY_VERBOSE)); + } + + /* + 1. IHDR: image header, which is the first chunk in a PNG datastream. + 2. PLTE: palette table associated with indexed PNG images. + 3. IDAT: image data chunks. + 4. IEND: image trailer, which is the last chunk in a PNG datastream. + + The remaining 14 chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret. + + 1. Transparency information: tRNS (see 11.3.2: Transparency information). + 2. Colour space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: Colour space information). + 3. Textual information: iTXt, tEXt, zTXt (see 11.3.4: Textual information). + 4. Miscellaneous information: bKGD, hIST, pHYs, sPLT (see 11.3.5: Miscellaneous information). + 5. Time information: tIME (see 11.3.6: Time stamp information). + */ + + private void writeInt(final OutputStream os, final int value) throws IOException { + os.write(0xff & (value >> 24)); + os.write(0xff & (value >> 16)); + os.write(0xff & (value >> 8)); + os.write(0xff & (value >> 0)); + } + + private void writeChunk(final OutputStream os, final ChunkType chunkType, + final byte[] data) throws IOException { + final int dataLength = data == null ? 0 : data.length; + writeInt(os, dataLength); + os.write(chunkType.array); + if (data != null) { + os.write(data); + } + + final PngCrc png_crc = new PngCrc(); + + final long crc1 = png_crc.start_partial_crc(chunkType.array, chunkType.array.length); + final long crc2 = data == null ? crc1 : png_crc.continue_partial_crc( + crc1, data, data.length); + final int crc = (int) png_crc.finish_partial_crc(crc2); + + writeInt(os, crc); + } + + private static class ImageHeader { + public final int width; + public final int height; + public final byte bitDepth; + public final ColorType colorType; + public final byte compressionMethod; + public final byte filterMethod; + public final InterlaceMethod interlaceMethod; + + public ImageHeader(final int width, final int height, final byte bitDepth, + final ColorType colorType, final byte compressionMethod, final byte filterMethod, + InterlaceMethod interlaceMethod) { + this.width = width; + this.height = height; + this.bitDepth = bitDepth; + this.colorType = colorType; + this.compressionMethod = compressionMethod; + this.filterMethod = filterMethod; + this.interlaceMethod = interlaceMethod; + } + + } + + private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeInt(baos, value.width); + writeInt(baos, value.height); + baos.write(0xff & value.bitDepth); + baos.write(0xff & value.colorType.getValue()); + baos.write(0xff & value.compressionMethod); + baos.write(0xff & value.filterMethod); + baos.write(0xff & value.interlaceMethod.ordinal()); + + writeChunk(os, ChunkType.IHDR, baos.toByteArray()); + } + + private void writeChunkiTXt(final OutputStream os, final PngText.Itxt text) + throws IOException, ImageWriteException { + if (!isValidISO_8859_1(text.keyword)) { + throw new ImageWriteException("Png tEXt chunk keyword is not ISO-8859-1: " + text.keyword); + } + if (!isValidISO_8859_1(text.languageTag)) { + throw new ImageWriteException("Png tEXt chunk language tag is not ISO-8859-1: " + text.languageTag); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // keyword + baos.write(text.keyword.getBytes("ISO-8859-1")); + baos.write(0); + + baos.write(1); // compressed flag, true + baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method + + // language tag + baos.write(text.languageTag.getBytes("ISO-8859-1")); + baos.write(0); + + // translated keyword + baos.write(text.translatedKeyword.getBytes("utf-8")); + baos.write(0); + + baos.write(deflate(text.text.getBytes("utf-8"))); + + writeChunk(os, ChunkType.iTXt, baos.toByteArray()); + } + + private void writeChunkzTXt(final OutputStream os, final PngText.Ztxt text) + throws IOException, ImageWriteException { + if (!isValidISO_8859_1(text.keyword)) { + throw new ImageWriteException("Png zTXt chunk keyword is not ISO-8859-1: " + text.keyword); + } + if (!isValidISO_8859_1(text.text)) { + throw new ImageWriteException("Png zTXt chunk text is not ISO-8859-1: " + text.text); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // keyword + baos.write(text.keyword.getBytes("ISO-8859-1")); + baos.write(0); + + // compression method + baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); + + // text + baos.write(deflate(text.text.getBytes("ISO-8859-1"))); + + writeChunk(os, ChunkType.zTXt, baos.toByteArray()); + } + + private void writeChunktEXt(final OutputStream os, final PngText.Text text) + throws IOException, ImageWriteException { + if (!isValidISO_8859_1(text.keyword)) { + throw new ImageWriteException("Png tEXt chunk keyword is not ISO-8859-1: " + text.keyword); + } + if (!isValidISO_8859_1(text.text)) { + throw new ImageWriteException("Png tEXt chunk text is not ISO-8859-1: " + text.text); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // keyword + baos.write(text.keyword.getBytes("ISO-8859-1")); + baos.write(0); + + // text + baos.write(text.text.getBytes("ISO-8859-1")); + + writeChunk(os, ChunkType.tEXt, baos.toByteArray()); + } + + private byte[] deflate(final byte[] bytes) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DeflaterOutputStream dos = new DeflaterOutputStream(baos); + boolean canThrow = false; + try { + dos.write(bytes); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, dos); + } + return baos.toByteArray(); + } + + private boolean isValidISO_8859_1(final String s) { + try { + final String roundtrip = new String(s.getBytes("ISO-8859-1"), "ISO-8859-1"); + return s.equals(roundtrip); + } catch (final UnsupportedEncodingException e) { + // should never be thrown. + throw new RuntimeException("Error parsing string.", e); + } + } + + private void writeChunkXmpiTXt(final OutputStream os, final String xmpXml) + throws IOException { + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // keyword + baos.write(PngConstants.XMP_KEYWORD.getBytes("ISO-8859-1")); + baos.write(0); + + baos.write(1); // compressed flag, true + baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method + + baos.write(0); // language tag (ignore). TODO + + // translated keyword + baos.write(PngConstants.XMP_KEYWORD.getBytes("utf-8")); + baos.write(0); + + baos.write(deflate(xmpXml.getBytes("utf-8"))); + + writeChunk(os, ChunkType.iTXt, baos.toByteArray()); + } + + private void writeChunkPLTE(final OutputStream os, final Palette palette) + throws IOException { + final int length = palette.length(); + final byte[] bytes = new byte[length * 3]; + + // Debug.debug("length", length); + for (int i = 0; i < length; i++) { + final int rgb = palette.getEntry(i); + final int index = i * 3; + // Debug.debug("index", index); + bytes[index + 0] = (byte) (0xff & (rgb >> 16)); + bytes[index + 1] = (byte) (0xff & (rgb >> 8)); + bytes[index + 2] = (byte) (0xff & (rgb >> 0)); + } + + writeChunk(os, ChunkType.PLTE, bytes); + } + + private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException { + final byte[] bytes = new byte[palette.length()]; + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (0xff & (palette.getEntry(i) >> 24)); + } + + writeChunk(os, ChunkType.tRNS, bytes); + } + + private void writeChunkIEND(final OutputStream os) throws IOException { + writeChunk(os, ChunkType.IEND, null); + } + + private void writeChunkIDAT(final OutputStream os, final byte[] bytes) + throws IOException { + writeChunk(os, ChunkType.IDAT, bytes); + } + + private void writeChunkPHYS(final OutputStream os, final int xPPU, final int yPPU, final byte units) + throws IOException { + final byte[] bytes = new byte[9]; + bytes[0] = (byte) (0xff & (xPPU >> 24)); + bytes[1] = (byte) (0xff & (xPPU >> 16)); + bytes[2] = (byte) (0xff & (xPPU >> 8)); + bytes[3] = (byte) (0xff & (xPPU >> 0)); + bytes[4] = (byte) (0xff & (yPPU >> 24)); + bytes[5] = (byte) (0xff & (yPPU >> 16)); + bytes[6] = (byte) (0xff & (yPPU >> 8)); + bytes[7] = (byte) (0xff & (yPPU >> 0)); + bytes[8] = units; + writeChunk(os, ChunkType.pHYs, bytes); + } + + private byte getBitDepth(final ColorType colorType, final Map params) { + byte depth = 8; + + Object o = params.get(PngConstants.PARAM_KEY_PNG_BIT_DEPTH); + if (o instanceof Number) { + depth = ((Number) o).byteValue(); + } + + return colorType.isBitDepthAllowed(depth) ? depth : 8; + } + + /// Wraps a palette by adding a single transparent entry at index 0. + private static class TransparentPalette extends Palette { + private final Palette palette; + + TransparentPalette(final Palette palette) { + this.palette = palette; + } + + @Override + public int getEntry(final int index) { + if (index == 0) { + return 0x00000000; + } + return palette.getEntry(index - 1); + } + + @Override + public int length() { + return 1 + palette.length(); + } + + @Override + public int getPaletteIndex(final int rgb) throws ImageWriteException { + if (rgb == 0x00000000) { + return 0; + } + final int index = palette.getPaletteIndex(rgb); + if (index >= 0) { + return 1 + index; + } + return index; + } + } + /* + between two chunk types indicates alternatives. + Table 5.3 - Chunk ordering rules + Critical chunks + (shall appear in this order, except PLTE is optional) + Chunk name Multiple allowed Ordering constraints + IHDR No Shall be first + PLTE No Before first IDAT + IDAT Yes Multiple IDAT chunks shall be consecutive + IEND No Shall be last + Ancillary chunks + (need not appear in this order) + Chunk name Multiple allowed Ordering constraints + cHRM No Before PLTE and IDAT + gAMA No Before PLTE and IDAT + iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. + sBIT No Before PLTE and IDAT + sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. + bKGD No After PLTE; before IDAT + hIST No After PLTE; before IDAT + tRNS No After PLTE; before IDAT + pHYs No Before IDAT + sPLT Yes Before IDAT + tIME No None + iTXt Yes None + tEXt Yes None + zTXt Yes None + */ + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = new HashMap(params); + + // clear format key. + if (params.containsKey(ImagingConstants.PARAM_KEY_FORMAT)) { + params.remove(ImagingConstants.PARAM_KEY_FORMAT); + } + // clear verbose key. + if (params.containsKey(ImagingConstants.PARAM_KEY_VERBOSE)) { + params.remove(ImagingConstants.PARAM_KEY_VERBOSE); + } + + final Map rawParams = new HashMap(params); + if (params.containsKey(PngConstants.PARAM_KEY_PNG_FORCE_TRUE_COLOR)) { + params.remove(PngConstants.PARAM_KEY_PNG_FORCE_TRUE_COLOR); + } + if (params.containsKey(PngConstants.PARAM_KEY_PNG_FORCE_INDEXED_COLOR)) { + params.remove(PngConstants.PARAM_KEY_PNG_FORCE_INDEXED_COLOR); + } + if (params.containsKey(PngConstants.PARAM_KEY_PNG_BIT_DEPTH)) { + params.remove(PngConstants.PARAM_KEY_PNG_BIT_DEPTH); + } + if (params.containsKey(ImagingConstants.PARAM_KEY_XMP_XML)) { + params.remove(ImagingConstants.PARAM_KEY_XMP_XML); + } + if (params.containsKey(PngConstants.PARAM_KEY_PNG_TEXT_CHUNKS)) { + params.remove(PngConstants.PARAM_KEY_PNG_TEXT_CHUNKS); + } + params.remove(ImagingConstants.PARAM_KEY_PIXEL_DENSITY); + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + params = rawParams; + + final int width = src.getWidth(); + final int height = src.getHeight(); + + final boolean hasAlpha = new PaletteFactory().hasTransparency(src); + if (verbose) { + Debug.debug("hasAlpha: " + hasAlpha); + } + // int transparency = new PaletteFactory().getTransparency(src); + + boolean isGrayscale = new PaletteFactory().isGrayscale(src); + if (verbose) { + Debug.debug("isGrayscale: " + isGrayscale); + } + + ColorType colorType; + { + final boolean forceIndexedColor = Boolean.TRUE.equals(params.get(PngConstants.PARAM_KEY_PNG_FORCE_INDEXED_COLOR)); + final boolean forceTrueColor = Boolean.TRUE.equals(params.get(PngConstants.PARAM_KEY_PNG_FORCE_TRUE_COLOR)); + + if (forceIndexedColor && forceTrueColor) { + throw new ImageWriteException( + "Params: Cannot force both indexed and true color modes"); + } else if (forceIndexedColor) { + colorType = ColorType.INDEXED_COLOR; + } else if (forceTrueColor) { + colorType = (hasAlpha ? ColorType.TRUE_COLOR_WITH_ALPHA : ColorType.TRUE_COLOR); + isGrayscale = false; + } else { + colorType = ColorType.getColorType(hasAlpha, isGrayscale); + } + if (verbose) { + Debug.debug("colorType: " + colorType); + } + } + + final byte bitDepth = getBitDepth(colorType, params); + if (verbose) { + Debug.debug("bitDepth: " + bitDepth); + } + + int sampleDepth; + if (colorType == ColorType.INDEXED_COLOR) { + sampleDepth = 8; + } else { + sampleDepth = bitDepth; + } + if (verbose) { + Debug.debug("sampleDepth: " + sampleDepth); + } + + { + PngConstants.PNG_SIGNATURE.writeTo(os); + } + { + // IHDR must be first + + final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE; + final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE; + final InterlaceMethod interlaceMethod = InterlaceMethod.NONE; + + final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, + colorType, compressionMethod, filterMethod, interlaceMethod); + + writeChunkIHDR(os, imageHeader); + } + + //{ + // sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the + // iCCP chunk should not be present. + + // charles + //} + + Palette palette = null; + if (colorType == ColorType.INDEXED_COLOR) { + // PLTE No Before first IDAT + + final int maxColors = hasAlpha ? 255 : 256; + + final PaletteFactory paletteFactory = new PaletteFactory(); + palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors); + // Palette palette2 = new PaletteFactory().makePaletteSimple(src, + // maxColors); + + // palette.dump(); + + if (hasAlpha) { + palette = new TransparentPalette(palette); + writeChunkPLTE(os, palette); + writeChunkTRNS(os, new SimplePalette(new int[] { 0x00000000 })); + } else { + writeChunkPLTE(os, palette); + } + } + + final Object pixelDensityObj = params.get(ImagingConstants.PARAM_KEY_PIXEL_DENSITY); + if (pixelDensityObj instanceof PixelDensity) { + final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj; + if (pixelDensity.isUnitless()) { + writeChunkPHYS(os, (int) Math.round(pixelDensity + .getRawHorizontalDensity()), + (int) Math.round(pixelDensity.getRawVerticalDensity()), + (byte) 0); + } else { + writeChunkPHYS(os, (int) Math.round(pixelDensity + .horizontalDensityMetres()), + (int) Math.round(pixelDensity.verticalDensityMetres()), + (byte) 1); + } + } + + if (params.containsKey(ImagingConstants.PARAM_KEY_XMP_XML)) { + final String xmpXml = (String) params.get(ImagingConstants.PARAM_KEY_XMP_XML); + writeChunkXmpiTXt(os, xmpXml); + } + + if (params.containsKey(PngConstants.PARAM_KEY_PNG_TEXT_CHUNKS)) { + final List outputTexts = (List) params.get(PngConstants.PARAM_KEY_PNG_TEXT_CHUNKS); + for (Object outputText : outputTexts) { + final PngText text = (PngText) outputText; + if (text instanceof PngText.Text) { + writeChunktEXt(os, (PngText.Text) text); + } else if (text instanceof PngText.Ztxt) { + writeChunkzTXt(os, (PngText.Ztxt) text); + } else if (text instanceof PngText.Itxt) { + writeChunkiTXt(os, (PngText.Itxt) text); + } else { + throw new ImageWriteException( + "Unknown text to embed in PNG: " + text); + } + } + } + + { + // Debug.debug("writing IDAT"); + + // IDAT Yes Multiple IDAT chunks shall be consecutive + + byte[] uncompressed; + { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + final boolean useAlpha = colorType == ColorType.GREYSCALE_WITH_ALPHA + || colorType == ColorType.TRUE_COLOR_WITH_ALPHA; + + final int[] row = new int[width]; + for (int y = 0; y < height; y++) { + // Debug.debug("y", y + "/" + height); + src.getRGB(0, y, width, 1, row, 0, width); + + baos.write(FilterType.NONE.ordinal()); + for (int x = 0; x < width; x++) { + final int argb = row[x]; + + if (palette != null) { + if (hasAlpha && (argb >>> 24) == 0x00) { + baos.write(0); + } else { + final int index = palette.getPaletteIndex(argb); + baos.write(0xff & index); + } + } else { + final int alpha = 0xff & (argb >> 24); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + + if (isGrayscale) { + final int gray = (red + green + blue) / 3; + // if (y == 0) + // { + // Debug.debug("gray: " + x + ", " + y + + // " argb: 0x" + // + Integer.toHexString(argb) + " gray: 0x" + // + Integer.toHexString(gray)); + // // Debug.debug(x + ", " + y + " gray", gray); + // // Debug.debug(x + ", " + y + " gray", gray); + // Debug.debug(x + ", " + y + " gray", gray + + // " " + Integer.toHexString(gray)); + // Debug.debug(); + // } + baos.write(gray); + } else { + baos.write(red); + baos.write(green); + baos.write(blue); + } + if (useAlpha) { + baos.write(alpha); + } + } + } + } + uncompressed = baos.toByteArray(); + } + + // Debug.debug("uncompressed", uncompressed.length); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DeflaterOutputStream dos = new DeflaterOutputStream(baos); + final int chunkSize = 256 * 1024; + for (int index = 0; index < uncompressed.length; index += chunkSize) { + final int end = Math.min(uncompressed.length, index + chunkSize); + final int length = end - index; + + dos.write(uncompressed, index, length); + dos.flush(); + baos.flush(); + + final byte[] compressed = baos.toByteArray(); + baos.reset(); + if (compressed.length > 0) { + // Debug.debug("compressed", compressed.length); + writeChunkIDAT(os, compressed); + } + + } + { + dos.finish(); + final byte[] compressed = baos.toByteArray(); + if (compressed.length > 0) { + // Debug.debug("compressed final", compressed.length); + writeChunkIDAT(os, compressed); + } + } + } + + { + // IEND No Shall be last + + writeChunkIEND(os); + } + + /* + Ancillary chunks + (need not appear in this order) + Chunk name Multiple allowed Ordering constraints + cHRM No Before PLTE and IDAT + gAMA No Before PLTE and IDAT + iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. + sBIT No Before PLTE and IDAT + sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. + bKGD No After PLTE; before IDAT + hIST No After PLTE; before IDAT + tRNS No After PLTE; before IDAT + pHYs No Before IDAT + sPLT Yes Before IDAT + tIME No None + iTXt Yes None + tEXt Yes None + zTXt Yes None + */ + + os.close(); + } // todo: filter types + // proper colour types + // srgb, etc. +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java new file mode 100644 index 0000000..90f80b8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilter; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilterAverage; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilterNone; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilterPaeth; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilterSub; +import org.apache.commons.imaging.formats.png.scanlinefilters.ScanlineFilterUp; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +abstract class ScanExpediter { + protected final int width; + protected final int height; + protected final InputStream is; + protected final BufferedImage bi; + protected final ColorType colorType; + protected final int bitDepth; + protected final int bytesPerPixel; + protected final int bitsPerPixel; + protected final PngChunkPlte pngChunkPLTE; + protected final GammaCorrection gammaCorrection; + protected final TransparencyFilter transparencyFilter; + + public ScanExpediter(final int width, final int height, final InputStream is, + final BufferedImage bi, final ColorType colorType, final int bitDepth, final int bitsPerPixel, + final PngChunkPlte pngChunkPLTE, final GammaCorrection gammaCorrection, + final TransparencyFilter transparencyFilter) + + { + this.width = width; + this.height = height; + this.is = is; + this.bi = bi; + this.colorType = colorType; + this.bitDepth = bitDepth; + this.bytesPerPixel = this.getBitsToBytesRoundingUp(bitsPerPixel); + this.bitsPerPixel = bitsPerPixel; + this.pngChunkPLTE = pngChunkPLTE; + this.gammaCorrection = gammaCorrection; + this.transparencyFilter = transparencyFilter; + } + + protected final int getBitsToBytesRoundingUp(final int bits) { + return (bits + 7) / 8; + } + + protected final int getPixelARGB(final int alpha, final int red, final int green, final int blue) { + return ((0xff & alpha) << 24) + | ((0xff & red) << 16) + | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } + + protected final int getPixelRGB(final int red, final int green, final int blue) { + return getPixelARGB(0xff, red, green, blue); + } + + public abstract void drive() throws ImageReadException, IOException; + + protected int getRGB(final BitParser bitParser, final int pixelIndexInScanline) + throws ImageReadException, IOException { + + switch (colorType) { + case GREYSCALE: { + // 1,2,4,8,16 Each pixel is a grayscale sample. + int sample = bitParser.getSampleAsByte(pixelIndexInScanline, 0); + + if (gammaCorrection != null) { + sample = gammaCorrection.correctSample(sample); + } + + int rgb = getPixelRGB(sample, sample, sample); + + if (transparencyFilter != null) { + rgb = transparencyFilter.filter(rgb, sample); + } + + return rgb; + + } + case TRUE_COLOR: { + // 8,16 Each pixel is an R,G,B triple. + int red = bitParser.getSampleAsByte(pixelIndexInScanline, 0); + int green = bitParser.getSampleAsByte(pixelIndexInScanline, 1); + int blue = bitParser.getSampleAsByte(pixelIndexInScanline, 2); + + int rgb = getPixelRGB(red, green, blue); + + if (transparencyFilter != null) { + rgb = transparencyFilter.filter(rgb, -1); + } + + if (gammaCorrection != null) { + final int alpha = (0xff000000 & rgb) >> 24; // make sure to preserve + // transparency + red = gammaCorrection.correctSample(red); + green = gammaCorrection.correctSample(green); + blue = gammaCorrection.correctSample(blue); + rgb = getPixelARGB(alpha, red, green, blue); + } + + return rgb; + } + // + case INDEXED_COLOR: { + // 1,2,4,8 Each pixel is a palette index; + // a PLTE chunk must appear. + final int index = bitParser.getSample(pixelIndexInScanline, 0); + + int rgb = pngChunkPLTE.getRGB(index); + + if (transparencyFilter != null) { + rgb = transparencyFilter.filter(rgb, index); + } + + return rgb; + } + case GREYSCALE_WITH_ALPHA: { + // 8,16 Each pixel is a grayscale sample, + // followed by an alpha sample. + int sample = bitParser.getSampleAsByte(pixelIndexInScanline, 0); + final int alpha = bitParser.getSampleAsByte(pixelIndexInScanline, 1); + + if (gammaCorrection != null) { + sample = gammaCorrection.correctSample(sample); + } + + return getPixelARGB(alpha, sample, sample, sample); + } + case TRUE_COLOR_WITH_ALPHA: { + // 8,16 Each pixel is an R,G,B triple, + int red = bitParser.getSampleAsByte(pixelIndexInScanline, 0); + int green = bitParser.getSampleAsByte(pixelIndexInScanline, 1); + int blue = bitParser.getSampleAsByte(pixelIndexInScanline, 2); + final int alpha = bitParser.getSampleAsByte(pixelIndexInScanline, 3); + + if (gammaCorrection != null) { + red = gammaCorrection.correctSample(red); + green = gammaCorrection.correctSample(green); + blue = gammaCorrection.correctSample(blue); + } + + return getPixelARGB(alpha, red, green, blue); + } + default: + throw new ImageReadException("PNG: unknown color type: " + colorType); + } + } + + protected ScanlineFilter getScanlineFilter(FilterType filterType, int bytesPerPixel) throws ImageReadException { + switch (filterType) { + case NONE: + return new ScanlineFilterNone(); + case SUB: + return new ScanlineFilterSub(bytesPerPixel); + case UP: + return new ScanlineFilterUp(); + case AVERAGE: + return new ScanlineFilterAverage(bytesPerPixel); + case PAETH: + return new ScanlineFilterPaeth(bytesPerPixel); + } + + return null; + } + + protected byte[] unfilterScanline(final FilterType filterType, final byte[] src, final byte[] prev, + final int bytesPerPixel) throws ImageReadException, IOException { + final ScanlineFilter filter = getScanlineFilter(filterType, bytesPerPixel); + + final byte[] dst = new byte[src.length]; + filter.unfilter(src, dst, prev); + return dst; + } + + protected byte[] getNextScanline(final InputStream is, final int length, final byte[] prev, + final int bytesPerPixel) throws ImageReadException, IOException { + final int filterType = is.read(); + if (filterType < 0) { + throw new ImageReadException("PNG: missing filter type"); + } + if (filterType >= FilterType.values().length) { + throw new ImageReadException("PNG: unknown filterType: " + filterType); + } + + byte[] scanline = readBytes("scanline", is, length, "PNG: missing image data"); + + return unfilterScanline(FilterType.values()[filterType], scanline, prev, bytesPerPixel); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterInterlaced.java b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterInterlaced.java new file mode 100644 index 0000000..166d829 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterInterlaced.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter; + +class ScanExpediterInterlaced extends ScanExpediter { + private static final int[] STARTING_ROW = { 0, 0, 4, 0, 2, 0, 1 }; + private static final int[] STARTING_COL = { 0, 4, 0, 2, 0, 1, 0 }; + private static final int[] ROW_INCREMENT = { 8, 8, 8, 4, 4, 2, 2 }; + private static final int[] COL_INCREMENT = { 8, 8, 4, 4, 2, 2, 1 }; +// private static final int Block_Height[] = { 8, 8, 4, 4, 2, 2, 1 }; +// private static final int Block_Width[] = { 8, 4, 4, 2, 2, 1, 1 }; + + public ScanExpediterInterlaced(int width, int height, InputStream is, + BufferedImage bi, + ColorType colorType, int bitDepth, int bitsPerPixel, + PngChunkPlte fPNGChunkPLTE, + GammaCorrection gammaCorrection, + TransparencyFilter transparencyFilter) + + { + super(width, height, is, bi, colorType, bitDepth, bitsPerPixel, + fPNGChunkPLTE, gammaCorrection, transparencyFilter); + } + + private void visit(final int x, final int y, final BufferedImage bi, final BitParser fBitParser, + final int pixelIndexInScanline) + throws ImageReadException, IOException { + final int rgb = getRGB(fBitParser, pixelIndexInScanline); + bi.setRGB(x, y, rgb); + } + + @Override + public void drive() throws ImageReadException, IOException { + + int pass = 1; + while (pass <= 7) { + byte[] prev = null; + + int y = STARTING_ROW[pass - 1]; + // int y_stride = ROW_INCREMENT[pass - 1]; + //final boolean rows_in_pass = (y < height); + while (y < height) { + int x = STARTING_COL[pass - 1]; + int pixelIndexInScanline = 0; + + if (x < width) { + // only get data if there are pixels in this scanline/pass + final int columnsInRow = 1 + ((width - STARTING_COL[pass - 1] - 1) / COL_INCREMENT[pass - 1]); + final int bitsPerScanLine = bitsPerPixel * columnsInRow; + final int pixelBytesPerScanLine = getBitsToBytesRoundingUp(bitsPerScanLine); + + final byte[] unfiltered = getNextScanline(is, pixelBytesPerScanLine, prev, bytesPerPixel); + + prev = unfiltered; + + final BitParser fBitParser = new BitParser(unfiltered, bitsPerPixel, bitDepth); + + while (x < width) { + visit(x, y, bi, fBitParser, pixelIndexInScanline); + + x = x + COL_INCREMENT[pass - 1]; + pixelIndexInScanline++; + } + } + y = y + ROW_INCREMENT[pass - 1]; + } + pass = pass + 1; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java new file mode 100644 index 0000000..519e036 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte; +import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter; + +class ScanExpediterSimple extends ScanExpediter { + public ScanExpediterSimple(final int width, final int height, final InputStream is, + final BufferedImage bi, final ColorType colorType, final int bitDepth, final int bitsPerPixel, + final PngChunkPlte pngChunkPLTE, final GammaCorrection gammaCorrection, + final TransparencyFilter transparencyFilter) + + { + super(width, height, is, bi, colorType, bitDepth, bitsPerPixel, + pngChunkPLTE, gammaCorrection, transparencyFilter); + } + + @Override + public void drive() throws ImageReadException, IOException { + final int bitsPerScanLine = bitsPerPixel * width; + final int pixelBytesPerScanLine = getBitsToBytesRoundingUp(bitsPerScanLine); + byte[] prev = null; + + for (int y = 0; y < height; y++) { + final byte[] unfiltered = getNextScanline(is, pixelBytesPerScanLine, prev, bytesPerPixel); + + prev = unfiltered; + + final BitParser bitParser = new BitParser(unfiltered, bitsPerPixel, + bitDepth); + + for (int x = 0; x < width; x++) { + final int rgb = getRGB(bitParser, x); + + bi.setRGB(x, y, rgb); + } + } + + } +} diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunk.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunk.java similarity index 57% rename from src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunk.java rename to src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunk.java index dfc0339..3342cbf 100644 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunk.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunk.java @@ -1,62 +1,68 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.ByteArrayInputStream; - -import org.apache.sanselan.common.BinaryFileParser; - -public class PNGChunk extends BinaryFileParser -{ - public final int length; - public final int chunkType; - public final int crc; - public final byte bytes[]; - - public final boolean propertyBits[]; - public final boolean ancillary, isPrivate, reserved, safeToCopy; - - public PNGChunk(int Length, int ChunkType, int CRC, byte bytes[]) - { - this.length = Length; - this.chunkType = ChunkType; - this.crc = CRC; - this.bytes = bytes; - - propertyBits = new boolean[4]; - int shift = 24; - for (int i = 0; i < 4; i++) - { - int theByte = 0xff & (ChunkType >> shift); - shift -= 8; - int theMask = 1 << 5; - propertyBits[i] = ((theByte & theMask) > 0); - } - - ancillary = propertyBits[0]; - isPrivate = propertyBits[1]; - reserved = propertyBits[2]; - safeToCopy = propertyBits[3]; - } - - protected ByteArrayInputStream getDataStream() - { - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - return is; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; + +import org.apache.commons.imaging.common.BinaryFileParser; + +public class PngChunk extends BinaryFileParser { + public final int length; + public final int chunkType; + public final int crc; + private final byte[] bytes; + + private final boolean[] propertyBits; + public final boolean ancillary; + public final boolean isPrivate; + public final boolean reserved; + public final boolean safeToCopy; + + public PngChunk(final int length, final int chunkType, final int crc, final byte[] bytes) { + this.length = length; + this.chunkType = chunkType; + this.crc = crc; + this.bytes = bytes; + + propertyBits = new boolean[4]; + int shift = 24; + for (int i = 0; i < 4; i++) { + final int theByte = 0xff & (chunkType >> shift); + shift -= 8; + final int theMask = 1 << 5; + propertyBits[i] = ((theByte & theMask) > 0); + } + + ancillary = propertyBits[0]; + isPrivate = propertyBits[1]; + reserved = propertyBits[2]; + safeToCopy = propertyBits[3]; + } + + public byte[] getBytes() { + return bytes; + } + + public boolean[] getPropertyBits() { + return propertyBits.clone(); + } + + protected ByteArrayInputStream getDataStream() { + return new ByteArrayInputStream(getBytes()); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkgAMA.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkGama.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkgAMA.java rename to src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkGama.java index 9180da6..53f197d 100644 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkgAMA.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkGama.java @@ -1,42 +1,39 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class PNGChunkgAMA extends PNGChunk -{ - public final int Gamma; - - public PNGChunkgAMA(int Length, int ChunkType, int CRC, byte bytes[]) - throws ImageReadException, IOException - { - super(Length, ChunkType, CRC, bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - Gamma = read4Bytes("Gamma", is, "Not a Valid Png File: gAMA Corrupt"); - } - - public double getGamma() - { - return 1.0 / (Gamma / 100000.0); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkGama extends PngChunk { + public final int gamma; + + public PngChunkGama(final int length, final int chunkType, final int crc, final byte[] bytes) + throws IOException { + super(length, chunkType, crc, bytes); + + final ByteArrayInputStream is = new ByteArrayInputStream(bytes); + gamma = read4Bytes("Gamma", is, "Not a Valid Png File: gAMA Corrupt", getByteOrder()); + } + + public double getGamma() { + return 1.0 / (gamma / 100000.0); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java new file mode 100644 index 0000000..e79fd0f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkIccp extends PngChunk { + // private final PngImageParser parser; + public final String profileName; + public final int compressionMethod; + public final byte[] compressedProfile; + public final byte[] uncompressedProfile; + + public PngChunkIccp( + // PngImageParser parser, + final int length, final int chunkType, final int crc, final byte[] bytes) + throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + // this.parser = parser; + + final int index = findNull(bytes); + if (index < 0) { + throw new ImageReadException("PngChunkIccp: No Profile Name"); + } + final byte[] nameBytes = new byte[index]; + System.arraycopy(bytes, 0, nameBytes, 0, index); + profileName = new String(nameBytes, "ISO-8859-1"); + + compressionMethod = bytes[index + 1]; + + final int compressedProfileLength = bytes.length - (index + 1 + 1); + compressedProfile = new byte[compressedProfileLength]; + System.arraycopy(bytes, index + 1 + 1, compressedProfile, 0, compressedProfileLength); + + if (getDebug()) { + System.out.println("ProfileName: " + profileName); + System.out.println("ProfileName.length(): " + profileName.length()); + System.out.println("CompressionMethod: " + compressionMethod); + System.out.println("CompressedProfileLength: " + compressedProfileLength); + System.out.println("bytes.length: " + bytes.length); + } + + uncompressedProfile = getStreamBytes(new InflaterInputStream(new ByteArrayInputStream(compressedProfile))); + + if (getDebug()) { + System.out.println("UncompressedProfile: " + Integer.toString(bytes.length)); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIdat.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIdat.java new file mode 100644 index 0000000..27dcaf7 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIdat.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +public class PngChunkIdat extends PngChunk { + + public PngChunkIdat(int length, int chunkType, int crc, byte[] bytes) { + super(length, chunkType, crc, bytes); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIhdr.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIhdr.java new file mode 100644 index 0000000..380bc38 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIhdr.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.ColorType; +import org.apache.commons.imaging.formats.png.InterlaceMethod; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkIhdr extends PngChunk { + public final int width; + public final int height; + public final int bitDepth; + public final ColorType colorType; + public final int compressionMethod; + public final int filterMethod; + public final InterlaceMethod interlaceMethod; + + public PngChunkIhdr(int length, int chunkType, int crc, byte[] bytes) throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + + final ByteArrayInputStream is = new ByteArrayInputStream(bytes); + width = read4Bytes("Width", is, "Not a Valid Png File: IHDR Corrupt", getByteOrder()); + height = read4Bytes("Height", is, "Not a Valid Png File: IHDR Corrupt", getByteOrder()); + bitDepth = readByte("BitDepth", is, "Not a Valid Png File: IHDR Corrupt"); + int type = readByte("ColorType", is, "Not a Valid Png File: IHDR Corrupt"); + colorType = ColorType.getColorType(type); + if (colorType == null) { + throw new ImageReadException("PNG: unknown color type: " + type); + } + compressionMethod = readByte("CompressionMethod", is, "Not a Valid Png File: IHDR Corrupt"); + filterMethod = readByte("FilterMethod", is, "Not a Valid Png File: IHDR Corrupt"); + int method = readByte("InterlaceMethod", is, "Not a Valid Png File: IHDR Corrupt"); + if (method < 0 && method >= InterlaceMethod.values().length) { + throw new ImageReadException("PNG: unknown interlace method: " + method); + } + interlaceMethod = InterlaceMethod.values()[method]; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java new file mode 100644 index 0000000..bd16af1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.PngConstants; +import org.apache.commons.imaging.formats.png.PngText; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkItxt extends PngTextChunk { + public final String keyword; + public final String text; + + /* + * The language tag defined in [RFC-3066] indicates the human language used + * by the translated keyword and the text. Unlike the keyword, the language + * tag is case-insensitive. It is an ISO 646.IRV:1991 [ISO 646] string + * consisting of hyphen-separated words of 1-8 alphanumeric characters each + * (for example cn, en-uk, no-bok, x-klingon, x-KlInGoN). If the first word + * is two or three letters long, it is an ISO language code [ISO-639]. If + * the language tag is empty, the language is unspecified. + */ + public final String languageTag; + + public final String translatedKeyword; + + public PngChunkItxt(final int length, final int chunkType, final int crc, final byte[] bytes) + throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + int terminator = findNull(bytes); + if (terminator < 0) { + throw new ImageReadException( + "PNG iTXt chunk keyword is not terminated."); + } + + keyword = new String(bytes, 0, terminator, "ISO-8859-1"); + int index = terminator + 1; + + final int compressionFlag = bytes[index++]; + if (compressionFlag != 0 && compressionFlag != 1) { + throw new ImageReadException( + "PNG iTXt chunk has invalid compression flag: " + + compressionFlag); + } + + final boolean compressed = compressionFlag == 1; + + final int compressionMethod = bytes[index++]; + if (compressed && compressionMethod != PngConstants.COMPRESSION_DEFLATE_INFLATE) { + throw new ImageReadException("PNG iTXt chunk has unexpected compression method: " + compressionMethod); + } + + terminator = findNull(bytes, index); + if (terminator < 0) { + throw new ImageReadException("PNG iTXt chunk language tag is not terminated."); + } + + languageTag = new String(bytes, index, terminator - index, "ISO-8859-1"); + index = terminator + 1; + + terminator = findNull(bytes, index); + if (terminator < 0) { + throw new ImageReadException("PNG iTXt chunk translated keyword is not terminated."); + } + + translatedKeyword = new String(bytes, index, terminator - index, "utf-8"); + index = terminator + 1; + + if (compressed) { + final int compressedTextLength = bytes.length - index; + + final byte[] compressedText = new byte[compressedTextLength]; + System.arraycopy(bytes, index, compressedText, 0, compressedTextLength); + + text = new String(getStreamBytes( + new InflaterInputStream(new ByteArrayInputStream(compressedText))), "utf-8"); + + } else { + text = new String(bytes, index, bytes.length - index, "utf-8"); + } + } + + /** + * @return Returns the keyword. + */ + @Override + public String getKeyword() { + return keyword; + } + + /** + * @return Returns the text. + */ + @Override + public String getText() { + return text; + } + + @Override + public PngText getContents() { + return new PngText.Itxt(keyword, text, languageTag, translatedKeyword); + } +} diff --git a/src/main/java/org/apache/sanselan/common/ZLibUtils.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPhys.java similarity index 51% rename from src/main/java/org/apache/sanselan/common/ZLibUtils.java rename to src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPhys.java index f905f39..2e37dd3 100644 --- a/src/main/java/org/apache/sanselan/common/ZLibUtils.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPhys.java @@ -1,44 +1,39 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; - -public class ZLibUtils extends BinaryFileFunctions -{ - public final byte[] inflate(byte bytes[]) throws IOException - // slow, probably. - { - ByteArrayInputStream in = new ByteArrayInputStream(bytes); - InflaterInputStream zIn = new InflaterInputStream(in); - return getStreamBytes(zIn); - } - - public final byte[] deflate(byte bytes[]) throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DeflaterOutputStream dos = new DeflaterOutputStream(baos); - dos.write(bytes); - dos.close(); - return baos.toByteArray(); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkPhys extends PngChunk { + public final int pixelsPerUnitXAxis; + public final int pixelsPerUnitYAxis; + public final int unitSpecifier; + + public PngChunkPhys(int length, int chunkType, int crc, byte[] bytes) throws IOException { + super(length, chunkType, crc, bytes); + + final ByteArrayInputStream is = new ByteArrayInputStream(bytes); + + pixelsPerUnitXAxis = read4Bytes("PixelsPerUnitXAxis", is, "Not a Valid Png File: pHYs Corrupt", getByteOrder()); + pixelsPerUnitYAxis = read4Bytes("PixelsPerUnitYAxis", is, "Not a Valid Png File: pHYs Corrupt", getByteOrder()); + unitSpecifier = readByte("Unit specifier", is, "Not a Valid Png File: pHYs Corrupt"); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkPLTE.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPlte.java similarity index 60% rename from src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkPLTE.java rename to src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPlte.java index 0099891..3c293bb 100644 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkPLTE.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkPlte.java @@ -1,81 +1,85 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.png.GammaCorrection; - -public class PNGChunkPLTE extends PNGChunk -{ - public final int rgb[]; - - public PNGChunkPLTE(int length, int ChunkType, int CRC, byte bytes[]) - throws ImageReadException, IOException - { - super(length, ChunkType, CRC, bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - - if ((length % 3) != 0) - throw new ImageReadException("PLTE: wrong length: " + length); - - int count = length / 3; - - rgb = new int[count]; - - for (int i = 0; i < count; i++) - { - int red = readByte("red[" + i + "]", is, - "Not a Valid Png File: PLTE Corrupt"); - int green = readByte("green[" + i + "]", is, - "Not a Valid Png File: PLTE Corrupt"); - int blue = readByte("blue[" + i + "]", is, - "Not a Valid Png File: PLTE Corrupt"); - rgb[i] = 0xff000000 | ((0xff & red) << 16) | ((0xff & green) << 8) - | ((0xff & blue) << 0); - } - } - - public int getRGB(int index) throws ImageReadException, IOException - { - if ((index < 0) || (index >= rgb.length)) - throw new ImageReadException("PNG: unknown Palette reference: " - + index); - return rgb[index]; - } - - // public void printPalette() { - // Debug.debug(); - // Debug.debug("palette"); - // for (int i = 0; i < rgb.length; i++) { - // Debug.debug("\t" + "palette[" + i + "];", rgb[i] + " (0x" - // + Integer.toHexString(rgb[i]) + ")"); - // - // } - // Debug.debug(); - // } - - public void correct(GammaCorrection gammaCorrection) - { - for (int i = 0; i < rgb.length; i++) - rgb[i] = gammaCorrection.correctARGB(rgb[i]); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.GammaCorrection; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkPlte extends PngChunk { + private final int[] rgb; + + public PngChunkPlte(final int length, final int chunkType, final int crc, final byte[] bytes) + throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + + final ByteArrayInputStream is = new ByteArrayInputStream(bytes); + + if ((length % 3) != 0) { + throw new ImageReadException("PLTE: wrong length: " + length); + } + + final int count = length / 3; + + rgb = new int[count]; + + for (int i = 0; i < count; i++) { + final int red = readByte("red[" + i + "]", is, + "Not a Valid Png File: PLTE Corrupt"); + final int green = readByte("green[" + i + "]", is, + "Not a Valid Png File: PLTE Corrupt"); + final int blue = readByte("blue[" + i + "]", is, + "Not a Valid Png File: PLTE Corrupt"); + rgb[i] = 0xff000000 | ((0xff & red) << 16) | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } + } + + public int[] getRgb() { + return rgb; + } + + public int getRGB(final int index) throws ImageReadException { + if ((index < 0) || (index >= rgb.length)) { + throw new ImageReadException("PNG: unknown Palette reference: " + + index); + } + return rgb[index]; + } + + // public void printPalette() { + // Debug.debug(); + // Debug.debug("palette"); + // for (int i = 0; i < rgb.length; i++) { + // Debug.debug("\t" + "palette[" + i + "];", rgb[i] + " (0x" + // + Integer.toHexString(rgb[i]) + ")"); + // + // } + // Debug.debug(); + // } + + public void correct(final GammaCorrection gammaCorrection) { + for (int i = 0; i < rgb.length; i++) { + rgb[i] = gammaCorrection.correctARGB(rgb[i]); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkText.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkText.java new file mode 100644 index 0000000..271b237 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkText.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.PngText; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkText extends PngTextChunk { + public final String keyword; + public final String text; + + public PngChunkText(final int length, final int chunkType, final int crc, final byte[] bytes) + throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + final int index = findNull(bytes); + if (index < 0) { + throw new ImageReadException( + "PNG tEXt chunk keyword is not terminated."); + } + + keyword = new String(bytes, 0, index, "ISO-8859-1"); + + final int textLength = bytes.length - (index + 1); + text = new String(bytes, index + 1, textLength, "ISO-8859-1"); + + if (getDebug()) { + System.out.println("Keyword: " + keyword); + System.out.println("Text: " + text); + } + } + + /** + * @return Returns the keyword. + */ + @Override + public String getKeyword() { + return keyword; + } + + /** + * @return Returns the text. + */ + @Override + public String getText() { + return text; + } + + @Override + public PngText getContents() { + return new PngText.Text(keyword, text); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java new file mode 100644 index 0000000..a7afd43 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.png.PngConstants; +import org.apache.commons.imaging.formats.png.PngText; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PngChunkZtxt extends PngTextChunk { + + public final String keyword; + public final String text; + + public PngChunkZtxt(final int length, final int chunkType, final int crc, final byte[] bytes) + throws ImageReadException, IOException { + super(length, chunkType, crc, bytes); + + int index = findNull(bytes); + if (index < 0) { + throw new ImageReadException( + "PNG zTXt chunk keyword is unterminated."); + } + + keyword = new String(bytes, 0, index, "ISO-8859-1"); + index++; + + final int compressionMethod = bytes[index++]; + if (compressionMethod != PngConstants.COMPRESSION_DEFLATE_INFLATE) { + throw new ImageReadException( + "PNG zTXt chunk has unexpected compression method: " + + compressionMethod); + } + + final int compressedTextLength = bytes.length - index; + final byte[] compressedText = new byte[compressedTextLength]; + System.arraycopy(bytes, index, compressedText, 0, compressedTextLength); + + text = new String(getStreamBytes(new InflaterInputStream(new ByteArrayInputStream(compressedText))), "ISO-8859-1"); + } + + /** + * @return Returns the keyword. + */ + @Override + public String getKeyword() { + return keyword; + } + + /** + * @return Returns the text. + */ + @Override + public String getText() { + return text; + } + + @Override + public PngText getContents() { + return new PngText.Ztxt(keyword, text); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGTextChunk.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngTextChunk.java similarity index 71% rename from src/main/java/org/apache/sanselan/formats/png/chunks/PNGTextChunk.java rename to src/main/java/org/apache/commons/imaging/formats/png/chunks/PngTextChunk.java index 58be778..248f57f 100644 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGTextChunk.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngTextChunk.java @@ -1,39 +1,33 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -import org.apache.sanselan.formats.png.PngText; - -public abstract class PNGTextChunk extends PNGChunk -{ - - public PNGTextChunk(int Length, int ChunkType, int CRC, byte bytes[]) - throws IOException - { - super(Length, ChunkType, CRC, bytes); - - } - - public abstract String getKeyword(); - - public abstract String getText(); - - public abstract PngText getContents(); - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.chunks; + +import org.apache.commons.imaging.formats.png.PngText; + +public abstract class PngTextChunk extends PngChunk { + + public PngTextChunk(final int length, final int chunkType, final int crc, final byte[] bytes) { + super(length, chunkType, crc, bytes); + } + + public abstract String getKeyword(); + + public abstract String getText(); + + public abstract PngText getContents(); + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/package-info.java b/src/main/java/org/apache/commons/imaging/formats/png/package-info.java new file mode 100644 index 0000000..4b65d34 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The PNG image format. + */ +package org.apache.commons.imaging.formats.png; + diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilter.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilter.java similarity index 78% rename from src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilter.java rename to src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilter.java index 1fc1e4c..c99f58c 100644 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilter.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilter.java @@ -1,27 +1,26 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public abstract class ScanlineFilter -{ - public abstract void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException; -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public abstract class ScanlineFilter { + public abstract void unfilter(byte[] src, byte[] dst, byte[] up) + throws ImageReadException, IOException; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterAverage.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterAverage.java new file mode 100644 index 0000000..9786549 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterAverage.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class ScanlineFilterAverage extends ScanlineFilter { + private final int bytesPerPixel; + + public ScanlineFilterAverage(final int bytesPerPixel) { + this.bytesPerPixel = bytesPerPixel; + } + + @Override + public void unfilter(final byte[] src, final byte[] dst, final byte[] up) + throws ImageReadException, IOException { + for (int i = 0; i < src.length; i++) { + int raw = 0; + final int prevIndex = i - bytesPerPixel; + if (prevIndex >= 0) { + raw = dst[prevIndex]; + } + + int prior = 0; + if (up != null) { + prior = up[i]; + } + + final int average = ((0xff & raw) + (0xff & prior)) / 2; + + dst[i] = (byte) ((src[i] + average) % 256); + // dst[i] = src[i]; + // dst[i] = (byte) 255; + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterNone.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterNone.java similarity index 67% rename from src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterNone.java rename to src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterNone.java index c62668e..564555d 100644 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterNone.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterNone.java @@ -1,33 +1,29 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ScanlineFilterNone extends ScanlineFilter -{ - public void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException - { - for (int i = 0; i < src.length; i++) - { - dst[i] = src[i]; - } - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class ScanlineFilterNone extends ScanlineFilter { + @Override + public void unfilter(final byte[] src, final byte[] dst, final byte[] up) + throws ImageReadException, IOException { + System.arraycopy(src, 0, dst, 0, src.length); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterPaeth.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterPaeth.java new file mode 100644 index 0000000..ab5f665 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterPaeth.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class ScanlineFilterPaeth extends ScanlineFilter { + private final int bytesPerPixel; + + public ScanlineFilterPaeth(final int bytesPerPixel) { + this.bytesPerPixel = bytesPerPixel; + } + + private int paethPredictor(final int a, final int b, final int c) { + // ; a = left, b = above, c = upper left + final int p = a + b - c; // ; initial estimate + final int pa = Math.abs(p - a); // ; distances to a, b, c + final int pb = Math.abs(p - b); + final int pc = Math.abs(p - c); + // ; return nearest of a,b,c, + // ; breaking ties in order a,b,c. + if ((pa <= pb) && (pa <= pc)) { + return a; + } else if (pb <= pc) { + return b; + } else { + return c; + } + } + + @Override + public void unfilter(final byte[] src, final byte[] dst, final byte[] up) + throws ImageReadException, IOException { + for (int i = 0; i < src.length; i++) { + int left = 0; + final int prevIndex = i - bytesPerPixel; + if (prevIndex >= 0) { + left = dst[prevIndex]; + } + + int above = 0; + if (up != null) { + above = up[i]; + } + // above = 255; + + int upperleft = 0; + if ((prevIndex >= 0) && (up != null)) { + upperleft = up[prevIndex]; + } + // upperleft = 255; + + int paethPredictor = paethPredictor(0xff & left, 0xff & above, 0xff & upperleft); + + dst[i] = (byte) ((src[i] + paethPredictor) % 256); + // dst[i] = (byte) ((src[i] + paethPredictor) ); + // dst[i] = src[i]; + + // dst[i] = (byte) 0; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterSub.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterSub.java new file mode 100644 index 0000000..934de5d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterSub.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class ScanlineFilterSub extends ScanlineFilter { + private final int bytesPerPixel; + + public ScanlineFilterSub(final int bytesPerPixel) { + this.bytesPerPixel = bytesPerPixel; + } + + @Override + public void unfilter(final byte[] src, final byte[] dst, final byte[] up) + throws ImageReadException, IOException { + for (int i = 0; i < src.length; i++) { + final int prevIndex = i - bytesPerPixel; + if (prevIndex >= 0) { + dst[i] = (byte) ((src[i] + dst[prevIndex]) % 256); + // dst[i] = 0xff & (src[i] + src[prevIndex]); + } else { + dst[i] = src[i]; + } + + // if(i<10) + // System.out.println("\t" + i + ": " + dst[i] + " (" + src[i] + + // ", " + prevIndex + ")"); + + // dst[i] = src[i]; + } + } +} diff --git a/src/main/java/org/apache/sanselan/util/CachingOutputStream.java b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterUp.java similarity index 53% rename from src/main/java/org/apache/sanselan/util/CachingOutputStream.java rename to src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterUp.java index 9551829..d6a695d 100644 --- a/src/main/java/org/apache/sanselan/util/CachingOutputStream.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/scanlinefilters/ScanlineFilterUp.java @@ -1,55 +1,43 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public class CachingOutputStream extends OutputStream -{ - private final OutputStream os; - private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - public CachingOutputStream(OutputStream os) - { - this.os = os; - } - - public void write(int b) throws IOException - { - os.write(b); - baos.write(b); - } - - public byte[] getCache() - { - return baos.toByteArray(); - } - - public void close() throws IOException - { - os.close(); - } - - public void flush() throws IOException - { - os.flush(); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.scanlinefilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class ScanlineFilterUp extends ScanlineFilter { + @Override + public void unfilter(final byte[] src, final byte[] dst, final byte[] up) + throws ImageReadException, IOException { + for (int i = 0; i < src.length; i++) { + // byte b; + + if (up != null) { + dst[i] = (byte) ((src[i] + up[i]) % 256); + } else { + dst[i] = src[i]; + } + + // if(i<10) + // System.out.println("\t" + i + ": " + dst[i]); + // dst[i] = b; + // dst[i] = src[i]; + // dst[i] = (byte) 0; + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilter.java b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilter.java similarity index 78% rename from src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilter.java rename to src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilter.java index 22535e3..916650e 100644 --- a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilter.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilter.java @@ -1,36 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.transparencyfilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; - -public abstract class TransparencyFilter extends BinaryFileParser -{ - protected final byte bytes[]; - - public TransparencyFilter(byte bytes[]) - { - this.bytes = bytes; - - } - - public abstract int filter(int rgb, int index) throws ImageReadException, - IOException; -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.transparencyfilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; + +public abstract class TransparencyFilter extends BinaryFileParser { + protected final byte[] bytes; + + public TransparencyFilter(final byte[] bytes) { + this.bytes = bytes; + + } + + public abstract int filter(int rgb, int index) throws ImageReadException, + IOException; +} diff --git a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterGrayscale.java b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterGrayscale.java similarity index 64% rename from src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterGrayscale.java rename to src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterGrayscale.java index e39933c..fc9281d 100644 --- a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterGrayscale.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterGrayscale.java @@ -1,45 +1,43 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.transparencyfilters; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class TransparencyFilterGrayscale extends TransparencyFilter -{ - private final int transparent_color; - - public TransparencyFilterGrayscale(byte bytes[]) throws ImageReadException, - IOException - { - super(bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - transparent_color = read2Bytes("transparent_color", is, - "tRNS: Missing transparent_color"); - } - - public int filter(int rgb, int index) throws ImageReadException, - IOException - { - if (index != transparent_color) - return rgb; - return 0x00; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.transparencyfilters; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class TransparencyFilterGrayscale extends TransparencyFilter { + private final int transparentColor; + + public TransparencyFilterGrayscale(byte[] bytes) throws IOException { + super(bytes); + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + transparentColor = read2Bytes("transparentColor", is, "tRNS: Missing transparentColor", getByteOrder()); + } + + @Override + public int filter(final int rgb, final int index) throws ImageReadException, IOException { + if (index != transparentColor) { + return rgb; + } + return 0x00; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterIndexedColor.java b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterIndexedColor.java similarity index 60% rename from src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterIndexedColor.java rename to src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterIndexedColor.java index 499c935..93c8d72 100644 --- a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterIndexedColor.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterIndexedColor.java @@ -1,53 +1,43 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.transparencyfilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class TransparencyFilterIndexedColor extends TransparencyFilter -{ - - public TransparencyFilterIndexedColor(byte bytes[]) - { - super(bytes); - } - - int count = 0; - - public int filter(int rgb, int index) throws ImageReadException, - IOException - { - if (index >= bytes.length) - return rgb; - - if ((index < 0) || (index > bytes.length)) - throw new ImageReadException( - "TransparencyFilterIndexedColor index: " + index - + ", bytes.length: " + bytes.length); - - int alpha = bytes[index]; - int result = ((0xff & alpha) << 24) | (0x00ffffff & rgb); - - if ((count < 100) && (index > 0)) - { - count++; - } - return result; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.transparencyfilters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +public class TransparencyFilterIndexedColor extends TransparencyFilter { + + public TransparencyFilterIndexedColor(final byte[] bytes) { + super(bytes); + } + + @Override + public int filter(final int rgb, final int index) throws ImageReadException, IOException { + if (index >= bytes.length) { + return rgb; + } + + if ((index < 0) || (index > bytes.length)) { + throw new ImageReadException( + "TransparencyFilterIndexedColor index: " + index + ", bytes.length: " + bytes.length); + } + + final int alpha = bytes[index]; + return ((0xff & alpha) << 24) | (0x00ffffff & rgb); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterTrueColor.java b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterTrueColor.java new file mode 100644 index 0000000..31bc130 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/transparencyfilters/TransparencyFilterTrueColor.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png.transparencyfilters; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class TransparencyFilterTrueColor extends TransparencyFilter { + private final int transparentColor; + + public TransparencyFilterTrueColor(byte[] bytes) throws IOException { + super(bytes); + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + int transparentRed = read2Bytes("transparentRed", is, "tRNS: Missing transparentColor", getByteOrder()); + int transparentGreen = read2Bytes("transparentGreen", is, "tRNS: Missing transparentColor", getByteOrder()); + int transparentBlue = read2Bytes("transparentBlue", is, "tRNS: Missing transparentColor", getByteOrder()); + + transparentColor = ((0xff & transparentRed) << 16) + | ((0xff & transparentGreen) << 8) + | ((0xff & transparentBlue) << 0); + + } + + @Override + public int filter(final int rgb, final int sample) throws ImageReadException, + IOException { + if ((0x00ffffff & rgb) == transparentColor) { + return 0x00; + } + + return rgb; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/FileInfo.java b/src/main/java/org/apache/commons/imaging/formats/pnm/FileInfo.java new file mode 100644 index 0000000..eeb9922 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/FileInfo.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.common.ImageBuilder; + +abstract class FileInfo { + protected final int width; + protected final int height; + protected final boolean rawbits; + + public FileInfo(final int width, final int height, final boolean rawbits) { + this.width = width; + this.height = height; + this.rawbits = rawbits; + } + + public abstract boolean hasAlpha(); + + public abstract int getNumComponents(); + + public abstract int getBitDepth(); + + public abstract ImageFormat getImageType(); + + public abstract String getImageTypeDescription(); + + public abstract String getMIMEType(); + + public abstract int getColorType(); + + public abstract int getRGB(WhiteSpaceReader wsr) throws IOException; + + public abstract int getRGB(InputStream is) throws IOException; + + protected void newline() { + // do nothing by default. + } + + protected static int readSample(final InputStream is, final int bytesPerSample) throws IOException { + int sample = 0; + for (int i = 0; i < bytesPerSample; i++) { + final int nextByte = is.read(); + if (nextByte < 0) { + throw new IOException("PNM: Unexpected EOF"); + } + sample <<= 8; + sample |= nextByte; + } + return sample; + } + + protected static int scaleSample(int sample, final float scale, final int max) throws IOException { + if (sample < 0) { + // Even netpbm tools break for files like this + throw new IOException("Negative pixel values are invalid in PNM files"); + } else if (sample > max) { + // invalid values -> black + sample = 0; + } + return (int) ((sample * scale / max) + 0.5f); + } + + public void readImage(final ImageBuilder imageBuilder, final InputStream is) + throws IOException { + // is = new BufferedInputStream(is); + // int count = 0; + // + // try + // { + + if (!rawbits) { + final WhiteSpaceReader wsr = new WhiteSpaceReader(is); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int rgb = getRGB(wsr); + + imageBuilder.setRGB(x, y, rgb); + // count++; + } + newline(); + } + } else { + for (int y = 0; y < height; y++) { + // System.out.println("y: " + y); + for (int x = 0; x < width; x++) { + final int rgb = getRGB(is); + imageBuilder.setRGB(x, y, rgb); + // count++; + } + newline(); + } + } + // } + // finally + // { + // System.out.println("count: " + count); + // dump(); + // } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PamFileInfo.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PamFileInfo.java new file mode 100644 index 0000000..96b826c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PamFileInfo.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageReadException; + +class PamFileInfo extends FileInfo { + private final int depth; + private final int maxval; + private final float scale; + private final int bytesPerSample; + private final boolean hasAlpha; + private final TupleReader tupleReader; + + PamFileInfo(final int width, final int height, final int depth, final int maxval, final String tupleType) throws ImageReadException { + super(width, height, true); + this.depth = depth; + this.maxval = maxval; + if (maxval <= 0) { + throw new ImageReadException("PAM maxVal " + maxval + + " is out of range [1;65535]"); + } else if (maxval <= 255) { + scale = 255f; + bytesPerSample = 1; + } else if (maxval <= 65535) { + scale = 65535f; + bytesPerSample = 2; + } else { + throw new ImageReadException("PAM maxVal " + maxval + + " is out of range [1;65535]"); + } + + hasAlpha = tupleType.endsWith("_ALPHA"); + if ("BLACKANDWHITE".equals(tupleType) || "BLACKANDWHITE_ALPHA".equals(tupleType)) { + tupleReader = new GrayscaleTupleReader(ImageInfo.COLOR_TYPE_BW); + } else if ("GRAYSCALE".equals(tupleType) || "GRAYSCALE_ALPHA".equals(tupleType)) { + tupleReader = new GrayscaleTupleReader(ImageInfo.COLOR_TYPE_GRAYSCALE); + } else if ("RGB".equals(tupleType) || "RGB_ALPHA".equals(tupleType)) { + tupleReader = new ColorTupleReader(); + } else { + throw new ImageReadException("Unknown PAM tupletype '" + tupleType + "'"); + } + } + + @Override + public boolean hasAlpha() { + return hasAlpha; + } + + @Override + public int getNumComponents() { + return depth; + } + + @Override + public int getBitDepth() { + return maxval; + } + + @Override + public ImageFormat getImageType() { + return ImageFormats.PAM; + } + + @Override + public String getImageTypeDescription() { + return "PAM: portable arbitrary map file format"; + } + + @Override + public String getMIMEType() { + return "image/x-portable-arbitrary-map"; + } + + @Override + public int getColorType() { + return tupleReader.getColorType(); + } + + @Override + public int getRGB(final WhiteSpaceReader wsr) throws IOException { + throw new UnsupportedOperationException("PAM files are only ever binary"); + } + + @Override + public int getRGB(final InputStream is) throws IOException { + return tupleReader.getRGB(is); + } + + private abstract class TupleReader { + public abstract int getColorType(); + public abstract int getRGB(InputStream is) throws IOException; + } + + private class GrayscaleTupleReader extends TupleReader { + private final int colorType; + + public GrayscaleTupleReader(final int colorType) { + this.colorType = colorType; + } + + @Override + public int getColorType() { + return colorType; + } + + @Override + public int getRGB(final InputStream is) throws IOException { + int sample = readSample(is, bytesPerSample); + sample = scaleSample(sample, scale, maxval); + + int alpha = 0xff; + if (hasAlpha) { + alpha = readSample(is, bytesPerSample); + alpha = scaleSample(alpha, scale, maxval); + } + + return ((0xff & alpha) << 24) + | ((0xff & sample) << 16) + | ((0xff & sample) << 8) + | ((0xff & sample) << 0); + } + } + + private class ColorTupleReader extends TupleReader { + @Override + public int getColorType() { + return ImageInfo.COLOR_TYPE_RGB; + } + + @Override + public int getRGB(final InputStream is) throws IOException { + int red = readSample(is, bytesPerSample); + int green = readSample(is, bytesPerSample); + int blue = readSample(is, bytesPerSample); + + red = scaleSample(red, scale, maxval); + green = scaleSample(green, scale, maxval); + blue = scaleSample(blue, scale, maxval); + + int alpha = 0xff; + if (hasAlpha) { + alpha = readSample(is, bytesPerSample); + alpha = scaleSample(alpha, scale, maxval); + } + + return ((0xff & alpha) << 24) + | ((0xff & red) << 16) + | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PamWriter.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PamWriter.java new file mode 100644 index 0000000..ab0fa1a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PamWriter.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; + +class PamWriter extends PnmWriter { + public PamWriter() { + super(true); + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, + final Map params) throws ImageWriteException, IOException { + + os.write(PnmConstants.PNM_PREFIX_BYTE); + os.write(PnmConstants.PAM_RAW_CODE); + os.write(PnmConstants.PNM_NEWLINE); + + final int width = src.getWidth(); + final int height = src.getHeight(); + + os.write(("WIDTH " + width).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + os.write(("HEIGHT " + height).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + os.write(("DEPTH 4").getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + os.write(("MAXVAL 255").getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + os.write(("TUPLTYPE RGB_ALPHA").getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + os.write(("ENDHDR").getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int alpha = 0xff & (argb >> 24); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + + os.write((byte) red); + os.write((byte) green); + os.write((byte) blue); + os.write((byte) alpha); + } + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PBMFileInfo.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PbmFileInfo.java similarity index 50% rename from src/main/java/org/apache/sanselan/formats/pnm/PBMFileInfo.java rename to src/main/java/org/apache/commons/imaging/formats/pnm/PbmFileInfo.java index 1980c5a..4b9b919 100644 --- a/src/main/java/org/apache/sanselan/formats/pnm/PBMFileInfo.java +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PbmFileInfo.java @@ -1,104 +1,111 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; - -public class PBMFileInfo extends FileInfo -{ - public PBMFileInfo(int width, int height, boolean RAWBITS) - { - super(width, height, RAWBITS); - } - - public int getNumComponents() - { - return 1; - } - - public int getBitDepth() - { - return 1; - } - - public ImageFormat getImageType() - { - return ImageFormat.IMAGE_FORMAT_PBM; - } - - public int getColorType() - { - return ImageInfo.COLOR_TYPE_BW; - } - - public String getImageTypeDescription() - { - return "PBM: portable bitmap fileformat"; - } - - public String getMIMEType() - { - return "image/x-portable-bitmap"; - } - - protected void newline() - { - bitcache = 0; - bits_in_cache = 0; - } - - - private int bitcache = 0; - private int bits_in_cache = 0; - - public int getRGB(InputStream is) throws IOException - { - if (bits_in_cache < 1) - { - int bits = is.read(); - if (bits < 0) - throw new IOException("PBM: Unexpected EOF"); - bitcache = 0xff & bits; - bits_in_cache += 8; - } - - int bit = 0x1 & (bitcache >> 7); - bitcache <<= 1; - bits_in_cache--; - - if (bit == 0) - return 0xffffffff; - if (bit == 1) - return 0xff000000; - throw new IOException("PBM: bad bit: " + bit); - } - - public int getRGB(WhiteSpaceReader wsr) throws IOException - { - int bit = Integer.parseInt(wsr.readtoWhiteSpace()); - if (bit == 0) - return 0xff000000; - if (bit == 1) - return 0xffffffff; - throw new IOException("PBM: bad bit: " + bit); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; + +class PbmFileInfo extends FileInfo { + private int bitcache; + private int bitsInCache; + + public PbmFileInfo(final int width, final int height, final boolean rawbits) { + super(width, height, rawbits); + } + + @Override + public boolean hasAlpha() { + return false; + } + + @Override + public int getNumComponents() { + return 1; + } + + @Override + public int getBitDepth() { + return 1; + } + + @Override + public ImageFormat getImageType() { + return ImageFormats.PBM; + } + + @Override + public int getColorType() { + return ImageInfo.COLOR_TYPE_BW; + } + + @Override + public String getImageTypeDescription() { + return "PBM: portable bitmap fileformat"; + } + + @Override + public String getMIMEType() { + return "image/x-portable-bitmap"; + } + + @Override + protected void newline() { + bitcache = 0; + bitsInCache = 0; + } + + @Override + public int getRGB(final InputStream is) throws IOException { + if (bitsInCache < 1) { + final int bits = is.read(); + if (bits < 0) { + throw new IOException("PBM: Unexpected EOF"); + } + bitcache = 0xff & bits; + bitsInCache += 8; + } + + final int bit = 0x1 & (bitcache >> 7); + bitcache <<= 1; + bitsInCache--; + + if (bit == 0) { + return 0xffffffff; + } + if (bit == 1) { + return 0xff000000; + } + throw new IOException("PBM: bad bit: " + bit); + } + + @Override + public int getRGB(final WhiteSpaceReader wsr) throws IOException { + final int bit = Integer.parseInt(wsr.readtoWhiteSpace()); + if (bit == 0) { + return 0xff000000; + } + if (bit == 1) { + return 0xffffffff; + } + throw new IOException("PBM: bad bit: " + bit); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PbmWriter.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PbmWriter.java new file mode 100644 index 0000000..fa0554f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PbmWriter.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; + +class PbmWriter extends PnmWriter { + public PbmWriter(final boolean rawbits) { + super(rawbits); + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + os.write(PnmConstants.PNM_PREFIX_BYTE); + os.write(rawbits ? PnmConstants.PBM_RAW_CODE : PnmConstants.PBM_TEXT_CODE); + os.write(PnmConstants.PNM_SEPARATOR); + + final int width = src.getWidth(); + final int height = src.getHeight(); + + os.write(Integer.toString(width).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_SEPARATOR); + + os.write(Integer.toString(height).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_NEWLINE); + + int bitcache = 0; + int bitsInCache = 0; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + int sample = (red + green + blue) / 3; + if (sample > 127) { + sample = 0; + } else { + sample = 1; + } + + if (rawbits) { + bitcache = (bitcache << 1) | (0x1 & sample); + bitsInCache++; + + if (bitsInCache >= 8) { + os.write((byte) bitcache); + bitcache = 0; + bitsInCache = 0; + } + } else { + os.write(Integer.toString(sample).getBytes("US-ASCII")); // max + // component + // value + os.write(PnmConstants.PNM_SEPARATOR); + } + } + + if (rawbits && (bitsInCache > 0)) { + bitcache = bitcache << (8 - bitsInCache); + os.write((byte) bitcache); + bitcache = 0; + bitsInCache = 0; + } + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PgmFileInfo.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PgmFileInfo.java new file mode 100644 index 0000000..f7ecec1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PgmFileInfo.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageReadException; + +class PgmFileInfo extends FileInfo { + private final int max; + private final float scale; + private final int bytesPerSample; + + public PgmFileInfo(final int width, final int height, final boolean rawbits, final int max) throws ImageReadException { + super(width, height, rawbits); + if (max <= 0) { + throw new ImageReadException("PGM maxVal " + max + + " is out of range [1;65535]"); + } else if (max <= 255) { + scale = 255f; + bytesPerSample = 1; + } else if (max <= 65535) { + scale = 65535f; + bytesPerSample = 2; + } else { + throw new ImageReadException("PGM maxVal " + max + + " is out of range [1;65535]"); + } + this.max = max; + } + + @Override + public boolean hasAlpha() { + return false; + } + + @Override + public int getNumComponents() { + return 1; + } + + @Override + public int getBitDepth() { + return max; + } + + @Override + public ImageFormat getImageType() { + return ImageFormats.PGM; + } + + @Override + public String getImageTypeDescription() { + return "PGM: portable graymap file format"; + } + + @Override + public String getMIMEType() { + return "image/x-portable-graymap"; + } + + @Override + public int getColorType() { + return ImageInfo.COLOR_TYPE_GRAYSCALE; + } + + @Override + public int getRGB(final InputStream is) throws IOException { + int sample = readSample(is, bytesPerSample); + + sample = scaleSample(sample, scale, max); + + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & sample) << 16) + | ((0xff & sample) << 8) + | ((0xff & sample) << 0); + } + + @Override + public int getRGB(final WhiteSpaceReader wsr) throws IOException { + int sample = Integer.parseInt(wsr.readtoWhiteSpace()); + + sample = scaleSample(sample, scale, max); + + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & sample) << 16) + | ((0xff & sample) << 8) + | ((0xff & sample) << 0); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PgmWriter.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PgmWriter.java new file mode 100644 index 0000000..7d5d24d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PgmWriter.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; + +class PgmWriter extends PnmWriter { + + public PgmWriter(boolean rawbits) { + super(rawbits); + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + // System.out.println + // (b1 == 0x50 && b2 == 0x36) + os.write(0x50); + os.write(rawbits ? 0x35 : 0x32); + os.write(PnmConstants.PNM_SEPARATOR); + + final int width = src.getWidth(); + final int height = src.getHeight(); + + os.write(Integer.toString(width).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_SEPARATOR); + + os.write(Integer.toString(height).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_SEPARATOR); + + os.write(Integer.toString(255).getBytes("US-ASCII")); // max component value + os.write(PnmConstants.PNM_NEWLINE); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + final int sample = (red + green + blue) / 3; + + if (rawbits) { + os.write((byte) sample); + } else { + os.write(Integer.toString(sample).getBytes("US-ASCII")); // max component value + os.write(PnmConstants.PNM_SEPARATOR); + } + } + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PnmConstants.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmConstants.java new file mode 100644 index 0000000..dd7d65f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmConstants.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +interface PnmConstants { + byte PNM_PREFIX_BYTE = 0x50; // P + + byte PBM_TEXT_CODE = 0x31; // Textual Bitmap + byte PGM_TEXT_CODE = 0x32; // Textual GrayMap + byte PPM_TEXT_CODE = 0x33; // Textual Pixmap + byte PGM_RAW_CODE = 0x35; // RAW GrayMap + byte PBM_RAW_CODE = 0x34; // RAW Bitmap + byte PPM_RAW_CODE = 0x36; // RAW Pixmap + byte PAM_RAW_CODE = 0x37; // PAM Pixmap + + byte PNM_SEPARATOR = 0x20; // Space + byte PNM_NEWLINE = 0x0A; // "usually a newline" + // (http://netpbm.sourceforge.net/doc/pbm.html) +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PnmImageParser.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmImageParser.java new file mode 100644 index 0000000..8c1b1ca --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmImageParser.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PnmImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".pnm"; + private static final String[] ACCEPTED_EXTENSIONS = { ".pbm", ".pgm", + ".ppm", ".pnm", ".pam" }; + public static final String PARAM_KEY_PNM_RAWBITS = "PNM_RAWBITS"; + public static final String PARAM_VALUE_PNM_RAWBITS_YES = "YES"; + public static final String PARAM_VALUE_PNM_RAWBITS_NO = "NO"; + + public PnmImageParser() { + super.setByteOrder(ByteOrder.LITTLE_ENDIAN); + // setDebug(true); + } + + @Override + public String getName() { + return "Pbm-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { + ImageFormats.PBM, + ImageFormats.PGM, + ImageFormats.PPM, + ImageFormats.PNM, + ImageFormats.PAM + }; + } + + private FileInfo readHeader(final InputStream is) throws ImageReadException, + IOException { + final byte identifier1 = readByte("Identifier1", is, "Not a Valid PNM File"); + final byte identifier2 = readByte("Identifier2", is, "Not a Valid PNM File"); + + if (identifier1 != PnmConstants.PNM_PREFIX_BYTE) { + throw new ImageReadException("PNM file has invalid prefix byte 1"); + } + + final WhiteSpaceReader wsr = new WhiteSpaceReader(is); + + if (identifier2 == PnmConstants.PBM_TEXT_CODE + || identifier2 == PnmConstants.PBM_RAW_CODE + || identifier2 == PnmConstants.PGM_TEXT_CODE + || identifier2 == PnmConstants.PGM_RAW_CODE + || identifier2 == PnmConstants.PPM_TEXT_CODE + || identifier2 == PnmConstants.PPM_RAW_CODE) { + + final int width = Integer.parseInt(wsr.readtoWhiteSpace()); + final int height = Integer.parseInt(wsr.readtoWhiteSpace()); + + if (identifier2 == PnmConstants.PBM_TEXT_CODE) { + return new PbmFileInfo(width, height, false); + } else if (identifier2 == PnmConstants.PBM_RAW_CODE) { + return new PbmFileInfo(width, height, true); + } else if (identifier2 == PnmConstants.PGM_TEXT_CODE) { + final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); + return new PgmFileInfo(width, height, false, maxgray); + } else if (identifier2 == PnmConstants.PGM_RAW_CODE) { + final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); + return new PgmFileInfo(width, height, true, maxgray); + } else if (identifier2 == PnmConstants.PPM_TEXT_CODE) { + final int max = Integer.parseInt(wsr.readtoWhiteSpace()); + return new PpmFileInfo(width, height, false, max); + } else if (identifier2 == PnmConstants.PPM_RAW_CODE) { + final int max = Integer.parseInt(wsr.readtoWhiteSpace()); + return new PpmFileInfo(width, height, true, max); + } else { + throw new ImageReadException("PNM file has invalid header."); + } + } else if (identifier2 == PnmConstants.PAM_RAW_CODE) { + int width = -1; + boolean seenWidth = false; + int height = -1; + boolean seenHeight = false; + int depth = -1; + boolean seenDepth = false; + int maxVal = -1; + boolean seenMaxVal = false; + final StringBuilder tupleType = new StringBuilder(); + boolean seenTupleType = false; + + // Advance to next line + wsr.readLine(); + String line; + while ((line = wsr.readLine()) != null) { + line = line.trim(); + if (line.charAt(0) == '#') { + continue; + } + final StringTokenizer tokenizer = new StringTokenizer(line, " ", false); + final String type = tokenizer.nextToken(); + if ("WIDTH".equals(type)) { + seenWidth = true; + width = Integer.parseInt(tokenizer.nextToken()); + } else if ("HEIGHT".equals(type)) { + seenHeight = true; + height = Integer.parseInt(tokenizer.nextToken()); + } else if ("DEPTH".equals(type)) { + seenDepth = true; + depth = Integer.parseInt(tokenizer.nextToken()); + } else if ("MAXVAL".equals(type)) { + seenMaxVal = true; + maxVal = Integer.parseInt(tokenizer.nextToken()); + } else if ("TUPLTYPE".equals(type)) { + seenTupleType = true; + tupleType.append(tokenizer.nextToken()); + } else if ("ENDHDR".equals(type)) { + break; + } else { + throw new ImageReadException("Invalid PAM file header type " + type); + } + } + + if (!seenWidth) { + throw new ImageReadException("PAM header has no WIDTH"); + } else if (!seenHeight) { + throw new ImageReadException("PAM header has no HEIGHT"); + } else if (!seenDepth) { + throw new ImageReadException("PAM header has no DEPTH"); + } else if (!seenMaxVal) { + throw new ImageReadException("PAM header has no MAXVAL"); + } else if (!seenTupleType) { + throw new ImageReadException("PAM header has no TUPLTYPE"); + } + + return new PamFileInfo(width, height, depth, maxVal, tupleType.toString()); + } else { + throw new ImageReadException("PNM file has invalid prefix byte 2"); + } + } + + private FileInfo readHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final FileInfo ret = readHeader(is); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FileInfo info = readHeader(byteSource); + + if (info == null) { + throw new ImageReadException("PNM: Couldn't read Header"); + } + + return new Dimension(info.width, info.height); + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FileInfo info = readHeader(byteSource); + + if (info == null) { + throw new ImageReadException("PNM: Couldn't read Header"); + } + + final List comments = new ArrayList(); + + final int bitsPerPixel = info.getBitDepth() * info.getNumComponents(); + final ImageFormat format = info.getImageType(); + final String formatName = info.getImageTypeDescription(); + final String mimeType = info.getMIMEType(); + final int numberOfImages = 1; + final boolean progressive = false; + + // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0); + // + final int physicalWidthDpi = 72; + final float physicalWidthInch = (float) ((double) info.width / (double) physicalWidthDpi); + final int physicalHeightDpi = 72; + final float physicalHeightInch = (float) ((double) info.height / (double) physicalHeightDpi); + + final String formatDetails = info.getImageTypeDescription(); + + final boolean transparent = info.hasAlpha(); + final boolean usesPalette = false; + + final int colorType = info.getColorType(); + final String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; + + return new ImageInfo(formatDetails, bitsPerPixel, comments, + format, formatName, info.height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, info.width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + pw.println("pnm.dumpImageFile"); + + final ImageInfo imageData = getImageInfo(byteSource); + if (imageData == null) { + return false; + } + + imageData.toString(pw, ""); + + pw.println(""); + + return true; + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final FileInfo info = readHeader(is); + + final int width = info.width; + final int height = info.height; + + final boolean hasAlpha = info.hasAlpha(); + final ImageBuilder imageBuilder = new ImageBuilder(width, height, + hasAlpha); + info.readImage(imageBuilder, is); + + final BufferedImage ret = imageBuilder.getBufferedImage(); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + PnmWriter writer = null; + boolean useRawbits = true; + final boolean hasAlpha = new PaletteFactory().hasTransparency(src); + + if (params != null) { + final Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS); + if (useRawbitsParam != null) { + if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) { + useRawbits = false; + } + } + + final Object subtype = params.get(PARAM_KEY_FORMAT); + if (subtype != null) { + if (subtype.equals(ImageFormats.PBM)) { + writer = new PbmWriter(useRawbits); + } else if (subtype.equals(ImageFormats.PGM)) { + writer = new PgmWriter(useRawbits); + } else if (subtype.equals(ImageFormats.PPM)) { + writer = new PpmWriter(useRawbits); + } else if (subtype.equals(ImageFormats.PAM)) { + writer = new PamWriter(); + } + } + } + + if (writer == null) { + if (hasAlpha) { + writer = new PamWriter(); + } else { + writer = new PpmWriter(useRawbits); + } + } + + // make copy of params; we'll clear keys as we consume them. + if (params != null) { + params = new HashMap(params); + } else { + params = new HashMap(); + } + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + writer.writeImage(src, os, params); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PNMWriter.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmWriter.java similarity index 74% rename from src/main/java/org/apache/sanselan/formats/pnm/PNMWriter.java rename to src/main/java/org/apache/commons/imaging/formats/pnm/PnmWriter.java index af5ac4f..4a5d56f 100644 --- a/src/main/java/org/apache/sanselan/formats/pnm/PNMWriter.java +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PnmWriter.java @@ -1,39 +1,35 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class PNMWriter -{ - protected final boolean RAWBITS; - - public PNMWriter(boolean RAWBITS) - { - this.RAWBITS = RAWBITS; - } - - public abstract void writeImage(BufferedImage src, OutputStream os, - Map params) throws ImageWriteException, IOException; -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; + +abstract class PnmWriter { + protected final boolean rawbits; + + public PnmWriter(final boolean rawbits) { + this.rawbits = rawbits; + } + + public abstract void writeImage(BufferedImage src, OutputStream os, + Map params) throws ImageWriteException, IOException; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PpmFileInfo.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PpmFileInfo.java new file mode 100644 index 0000000..8433442 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PpmFileInfo.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageReadException; + +class PpmFileInfo extends FileInfo { + private final int max; + private final float scale; + private final int bytesPerSample; + + public PpmFileInfo(final int width, final int height, final boolean rawbits, final int max) throws ImageReadException { + super(width, height, rawbits); + if (max <= 0) { + throw new ImageReadException("PPM maxVal " + max + " is out of range [1;65535]"); + } else if (max <= 255) { + scale = 255f; + bytesPerSample = 1; + } else if (max <= 65535) { + scale = 65535f; + bytesPerSample = 2; + } else { + throw new ImageReadException("PPM maxVal " + max + " is out of range [1;65535]"); + } + this.max = max; + } + + @Override + public boolean hasAlpha() { + return false; + } + + @Override + public int getNumComponents() { + return 3; + } + + @Override + public int getBitDepth() { + return max; + } + + @Override + public ImageFormat getImageType() { + return ImageFormats.PPM; + } + + @Override + public String getImageTypeDescription() { + return "PPM: portable pixmap file format"; + } + + @Override + public String getMIMEType() { + return "image/x-portable-pixmap"; + } + + @Override + public int getColorType() { + return ImageInfo.COLOR_TYPE_RGB; + } + + @Override + public int getRGB(final InputStream is) throws IOException { + int red = readSample(is, bytesPerSample); + int green = readSample(is, bytesPerSample); + int blue = readSample(is, bytesPerSample); + + red = scaleSample(red, scale, max); + green = scaleSample(green, scale, max); + blue = scaleSample(blue, scale, max); + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & red) << 16) + | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } + + @Override + public int getRGB(final WhiteSpaceReader wsr) throws IOException { + int red = Integer.parseInt(wsr.readtoWhiteSpace()); + int green = Integer.parseInt(wsr.readtoWhiteSpace()); + int blue = Integer.parseInt(wsr.readtoWhiteSpace()); + + red = scaleSample(red, scale, max); + green = scaleSample(green, scale, max); + blue = scaleSample(blue, scale, max); + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & red) << 16) + | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/PpmWriter.java b/src/main/java/org/apache/commons/imaging/formats/pnm/PpmWriter.java new file mode 100644 index 0000000..ae8e5e3 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/PpmWriter.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; + +class PpmWriter extends PnmWriter { + + public PpmWriter(boolean rawbits) { + super(rawbits); + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + // System.out.println + // (b1 == 0x50 && b2 == 0x36) + os.write(0x50); + os.write(rawbits ? 0x36 : 0x33); + os.write(PnmConstants.PNM_SEPARATOR); + + final int width = src.getWidth(); + final int height = src.getHeight(); + + os.write(Integer.toString(width).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_SEPARATOR); + + os.write(Integer.toString(height).getBytes("US-ASCII")); + os.write(PnmConstants.PNM_SEPARATOR); + + os.write(Integer.toString(255).getBytes("US-ASCII")); // max component value + os.write(PnmConstants.PNM_NEWLINE); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + + if (rawbits) { + os.write((byte) red); + os.write((byte) green); + os.write((byte) blue); + } else { + os.write(Integer.toString(red).getBytes("US-ASCII")); // max component + // value + os.write(PnmConstants.PNM_SEPARATOR); + os.write(Integer.toString(green).getBytes("US-ASCII")); // max + // component + // value + os.write(PnmConstants.PNM_SEPARATOR); + os.write(Integer.toString(blue).getBytes("US-ASCII")); // max component + // value + os.write(PnmConstants.PNM_SEPARATOR); + } + } + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/pnm/WhiteSpaceReader.java b/src/main/java/org/apache/commons/imaging/formats/pnm/WhiteSpaceReader.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/pnm/WhiteSpaceReader.java rename to src/main/java/org/apache/commons/imaging/formats/pnm/WhiteSpaceReader.java index eef878b..d6d01eb 100644 --- a/src/main/java/org/apache/sanselan/formats/pnm/WhiteSpaceReader.java +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/WhiteSpaceReader.java @@ -1,72 +1,72 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.InputStream; - -class WhiteSpaceReader -{ - private final InputStream is; - - public WhiteSpaceReader(InputStream is) - { - this.is = is; - } - - int count = 0; - - private char read() throws IOException - { - int result = is.read(); - if (result < 0) - throw new IOException("PNM: Unexpected EOF"); - return (char) result; - } - - public char nextChar() throws IOException - { - char c = read(); - - if (c == '#') - { - while ((c != '\n') && (c != '\r')) - { - c = read(); - } - } - return c; - } - - public String readtoWhiteSpace() throws IOException - { - char c = nextChar(); - - while (Character.isWhitespace(c)) - c = nextChar(); - - StringBuffer buffer = new StringBuffer(); - - while (!Character.isWhitespace(c)) - { - buffer.append(c); - c = nextChar(); - } - - return buffer.toString(); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.pnm; + +import java.io.IOException; +import java.io.InputStream; + +class WhiteSpaceReader { + private final InputStream is; + + public WhiteSpaceReader(final InputStream is) { + this.is = is; + } + + private char read() throws IOException { + final int result = is.read(); + if (result < 0) { + throw new IOException("PNM: Unexpected EOF"); + } + return (char) result; + } + + public char nextChar() throws IOException { + char c = read(); + + if (c == '#') { + while ((c != '\n') && (c != '\r')) { + c = read(); + } + } + return c; + } + + public String readtoWhiteSpace() throws IOException { + char c = nextChar(); + + while (Character.isWhitespace(c)) { + c = nextChar(); + } + + final StringBuilder buffer = new StringBuilder(); + + while (!Character.isWhitespace(c)) { + buffer.append(c); + c = nextChar(); + } + + return buffer.toString(); + } + + public String readLine() throws IOException { + final StringBuilder buffer = new StringBuilder(); + for (char c = read(); (c != '\n') && (c != '\r'); c = read()) { + buffer.append(c); + } + return buffer.length() > 0 ? buffer.toString() : null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/pnm/package-info.java b/src/main/java/org/apache/commons/imaging/formats/pnm/package-info.java new file mode 100644 index 0000000..69ef17b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/pnm/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The PNM image format family. + */ +package org.apache.commons.imaging.formats.pnm; + diff --git a/src/main/java/org/apache/sanselan/formats/psd/ImageContents.java b/src/main/java/org/apache/commons/imaging/formats/psd/ImageContents.java similarity index 70% rename from src/main/java/org/apache/sanselan/formats/psd/ImageContents.java rename to src/main/java/org/apache/commons/imaging/formats/psd/ImageContents.java index 4460c3d..ba1c0af 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/ImageContents.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/ImageContents.java @@ -1,71 +1,69 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import java.io.PrintWriter; - -public class ImageContents -{ - public final PSDHeaderInfo header; - - public final int ColorModeDataLength; - public final int ImageResourcesLength; - public final int LayerAndMaskDataLength; - public final int Compression; - - public ImageContents(PSDHeaderInfo header, - - int ColorModeDataLength, int ImageResourcesLength, - int LayerAndMaskDataLength, int Compression) - { - this.header = header; - this.ColorModeDataLength = ColorModeDataLength; - this.ImageResourcesLength = ImageResourcesLength; - this.LayerAndMaskDataLength = LayerAndMaskDataLength; - this.Compression = Compression; - } - - public void dump() - { - PrintWriter pw = new PrintWriter(System.out); - dump(pw); - pw.flush(); - } - - public void dump(PrintWriter pw) - { - pw.println(""); - pw.println("ImageContents"); - pw.println("Compression: " + Compression + " (" - + Integer.toHexString(Compression) + ")"); - pw.println("ColorModeDataLength: " + ColorModeDataLength + " (" - + Integer.toHexString(ColorModeDataLength) + ")"); - pw.println("ImageResourcesLength: " + ImageResourcesLength + " (" - + Integer.toHexString(ImageResourcesLength) + ")"); - pw.println("LayerAndMaskDataLength: " + LayerAndMaskDataLength + " (" - + Integer.toHexString(LayerAndMaskDataLength) + ")"); - // System.out.println("Depth: " + Depth + " (" - // + Integer.toHexString(Depth) + ")"); - // System.out.println("Mode: " + Mode + " (" + Integer.toHexString(Mode) - // + ")"); - // System.out.println("Reserved: " + Reserved.length); - pw.println(""); - pw.flush(); - - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; + +public class ImageContents { + public final PsdHeaderInfo header; + + public final int ColorModeDataLength; + public final int ImageResourcesLength; + public final int LayerAndMaskDataLength; + public final int Compression; + + public ImageContents(final PsdHeaderInfo header, + + final int ColorModeDataLength, final int ImageResourcesLength, + final int LayerAndMaskDataLength, final int Compression) { + this.header = header; + this.ColorModeDataLength = ColorModeDataLength; + this.ImageResourcesLength = ImageResourcesLength; + this.LayerAndMaskDataLength = LayerAndMaskDataLength; + this.Compression = Compression; + } + + public void dump() { + final PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + dump(pw); + pw.flush(); + } + + public void dump(final PrintWriter pw) { + pw.println(""); + pw.println("ImageContents"); + pw.println("Compression: " + Compression + " (" + + Integer.toHexString(Compression) + ")"); + pw.println("ColorModeDataLength: " + ColorModeDataLength + " (" + + Integer.toHexString(ColorModeDataLength) + ")"); + pw.println("ImageResourcesLength: " + ImageResourcesLength + " (" + + Integer.toHexString(ImageResourcesLength) + ")"); + pw.println("LayerAndMaskDataLength: " + LayerAndMaskDataLength + " (" + + Integer.toHexString(LayerAndMaskDataLength) + ")"); + // System.out.println("Depth: " + Depth + " (" + // + Integer.toHexString(Depth) + ")"); + // System.out.println("Mode: " + Mode + " (" + Integer.toHexString(Mode) + // + ")"); + // System.out.println("Reserved: " + Reserved.length); + pw.println(""); + pw.flush(); + + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/ImageResourceBlock.java b/src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceBlock.java similarity index 65% rename from src/main/java/org/apache/sanselan/formats/psd/ImageResourceBlock.java rename to src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceBlock.java index 8cd611b..3121e9f 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/ImageResourceBlock.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceBlock.java @@ -1,41 +1,38 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import java.io.UnsupportedEncodingException; - -import org.apache.sanselan.util.Debug; - -class ImageResourceBlock -{ - protected final int id; - protected final byte nameData[]; - protected final byte data[]; - - public ImageResourceBlock(int ID, byte NameData[], byte Data[]) - { - this.id = ID; - this.nameData = NameData; - this.data = Data; - } - - public String getName() throws UnsupportedEncodingException - { - Debug.debug("getName", nameData.length); - return new String(nameData, "ISO-8859-1"); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd; + +import java.io.UnsupportedEncodingException; + +import org.apache.commons.imaging.util.Debug; + +class ImageResourceBlock { + protected final int id; + protected final byte[] nameData; + protected final byte[] data; + + public ImageResourceBlock(final int id, final byte[] nameData, final byte[] data) { + this.id = id; + this.nameData = nameData; + this.data = data; + } + + public String getName() throws UnsupportedEncodingException { + Debug.debug("getName: " + nameData.length); + return new String(nameData, "ISO-8859-1"); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceType.java b/src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceType.java new file mode 100644 index 0000000..6cf293b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/ImageResourceType.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd; + +/** + * TODO Turn into an enum + * + * @see Photoshop File Formats Specification - Image Resource IDs + */ +class ImageResourceType { + + private static final ImageResourceType[] TYPES = new ImageResourceType[]{ + new ImageResourceType(1000, "Number of channels, rows, columns, depth, and mode."), + new ImageResourceType(1001, "Optional. Macintosh print manager print info record"), + new ImageResourceType(1003, "Indexed color table."), + new ImageResourceType(1005, "ResolutionInfo structure"), + new ImageResourceType(1006, "Names of the alpha channels as a series of Pascal strings."), + new ImageResourceType(1007, "DisplayInfo structure"), + new ImageResourceType(1008, "Optional. The caption as a Pascal string."), + new ImageResourceType(1009, "Border information"), + new ImageResourceType(1010, "Background color"), + new ImageResourceType(1011, "Print flags (labels, crop marks, color bars, registration marks, negative, flip, interpolate, caption)"), + new ImageResourceType(1012, "Grayscale and multichannel halftoning information."), + new ImageResourceType(1013, "Color halftoning information"), + new ImageResourceType(1014, "Duotone halftoning information"), + new ImageResourceType(1015, "Grayscale and multichannel transfer function"), + new ImageResourceType(1016, "Color transfer functions"), + new ImageResourceType(1017, "Duotone transfer functions"), + new ImageResourceType(1018, "Duotone image information"), + new ImageResourceType(1019, "Effective black and white values for the dot range."), + new ImageResourceType(1020, "Obsolete"), + new ImageResourceType(1021, "EPS options"), + new ImageResourceType(1022, "Quick Mask information"), + new ImageResourceType(1023, "Obsolete"), + new ImageResourceType(1024, "Layer state information"), + new ImageResourceType(1025, "Working path (not saved)"), + new ImageResourceType(1026, "Layers group information"), + new ImageResourceType(1027, "Obsolete"), + new ImageResourceType(1028, "IPTC-NAA record"), + new ImageResourceType(1029, "Image mode for raw format files"), + new ImageResourceType(1030, "JPEG quality"), + new ImageResourceType(1032, "Grid and guides information"), + new ImageResourceType(1033, "Thumbnail resource"), + new ImageResourceType(1034, "Copyright flag"), + new ImageResourceType(1035, "URL"), + new ImageResourceType(1036, "Thumbnail resource"), + new ImageResourceType(1037, "Global lighting angle for effects layer"), + new ImageResourceType(1038, "Color samplers resource"), + new ImageResourceType(1039, "ICC Profile"), + new ImageResourceType(1040, "Watermark"), + new ImageResourceType(1041, "ICC Untagged. Disables any assumed profile handling when opening the file"), + new ImageResourceType(1042, "Effects visible. global flag to show/hide all the effects layer. Only present when they are hidden."), + new ImageResourceType(1043, "Spot Halftone"), + new ImageResourceType(1044, "Document specific IDs"), + new ImageResourceType(1045, "Unicode Alpha Names"), + new ImageResourceType(1046, "Indexed Color Table Count. Number of colors in table that are actually defined"), + new ImageResourceType(1047, "Transparency Index. Index of transparent color, if any"), + new ImageResourceType(1049, "Global Altitude"), + new ImageResourceType(1050, "Slices"), + new ImageResourceType(1051, "Workflow URL"), + new ImageResourceType(1052, "Jump To XPEP"), + new ImageResourceType(1053, "Alpha Identifiers"), + new ImageResourceType(1054, "URL List"), + new ImageResourceType(1057, "Version Info"), + new ImageResourceType(1058, "EXIF data 1"), + new ImageResourceType(1059, "EXIF data 3"), + new ImageResourceType(1060, "XMP metadata"), + new ImageResourceType(1061, "Caption digest"), + new ImageResourceType(1062, "Print scale"), + new ImageResourceType(1064, "Pixel Aspect Ratio"), + new ImageResourceType(1065, "Layer Comps"), + new ImageResourceType(1066, "Alternate Duotone Colors"), + new ImageResourceType(1067, "Alternate Spot Colors"), + new ImageResourceType(1069, "Layer Selection ID(s)"), + new ImageResourceType(1070, "HDR Toning information"), + new ImageResourceType(1071, "Print info"), + new ImageResourceType(1072, "Layer Group(s) Enabled ID"), + new ImageResourceType(1073, "Color samplers resource"), + new ImageResourceType(1074, "Measurement Scale"), + new ImageResourceType(1075, "Timeline Information"), + new ImageResourceType(1076, "Sheet Disclosure"), + new ImageResourceType(1077, "DisplayInfo structure to support floating point colors"), + new ImageResourceType(1078, "Onion Skins"), + new ImageResourceType(1080, "Count Information. Information about the count in the document."), + new ImageResourceType(1082, "Print Information. Information about the current print settings in the document. The color management options."), + new ImageResourceType(1083, "Print Style. Information about the current print style in the document. The printing marks, labels, ornaments, etc."), + new ImageResourceType(1084, "Macintosh NSPrintInfo. Variable OS specific info for Macintosh. NSPrintInfo."), + new ImageResourceType(1085, "Windows DEVMODE. Variable OS specific info for Windows. DEVMODE."), + new ImageResourceType(1086, "Auto Save File Path"), + new ImageResourceType(1087, "Auto Save Format"), + new ImageResourceType(1088, "Path Selection State. Information about the current path selection state"), + new ImageResourceType(2000, 2998, "Path Information (saved paths)."), + new ImageResourceType(2999, "Name of clipping path"), + new ImageResourceType(3000, "Origin Path Info"), + new ImageResourceType(4000, 4999, "Plug-In resource(s). Resources added by a plug-in"), + new ImageResourceType(7000, "Image Ready variables. XML representation of variables definition"), + new ImageResourceType(7001, "Image Ready data sets"), + new ImageResourceType(8000, "Lightroom workflow, if present the document is in the middle of a Lightroom workflow"), + new ImageResourceType(10000, "Print flags information (center crop marks, bleed width value, bleed width scale)") + }; + + public static String getDescription(final int id) { + for (ImageResourceType type : TYPES) { + if (type.from <= id && id <= type.to) { + return type.description; + } + } + return "Unknown"; + } + + public final int from; + public final int to; + + public final String description; + + public ImageResourceType(int id, String description) { + this.from = id; + this.to = id; + this.description = description; + } + + public ImageResourceType(int id, int id2, String description) { + this.from = id; + this.to = id2; + this.description = description; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/PsdHeaderInfo.java b/src/main/java/org/apache/commons/imaging/formats/psd/PsdHeaderInfo.java new file mode 100644 index 0000000..a447a8b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/PsdHeaderInfo.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; + +public class PsdHeaderInfo { + public final int version; + private final byte[] reserved; + public final int channels; + public final int rows; + public final int columns; + public final int depth; + public final int mode; + + public PsdHeaderInfo(int version, byte[] reserved, int channels, int rows, int columns, int depth, int mode) { + this.version = version; + this.reserved = reserved; + this.channels = channels; + this.rows = rows; + this.columns = columns; + this.depth = depth; + this.mode = mode; + + } + + public byte[] getReserved() { + return reserved.clone(); + } + + public void dump() { + final PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + dump(pw); + pw.flush(); + } + + public void dump(final PrintWriter pw) { + pw.println(""); + pw.println("Header"); + pw.println("Version: " + version + " (" + Integer.toHexString(version) + ")"); + pw.println("Channels: " + channels + " (" + Integer.toHexString(channels) + ")"); + pw.println("Rows: " + rows + " (" + Integer.toHexString(rows) + ")"); + pw.println("Columns: " + columns + " (" + Integer.toHexString(columns) + ")"); + pw.println("Depth: " + depth + " (" + Integer.toHexString(depth) + ")"); + pw.println("Mode: " + mode + " (" + Integer.toHexString(mode) + ")"); + pw.println("Reserved: " + reserved.length); + pw.println(""); + pw.flush(); + + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/PsdImageParser.java b/src/main/java/org/apache/commons/imaging/formats/psd/PsdImageParser.java new file mode 100644 index 0000000..78aa0b6 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/PsdImageParser.java @@ -0,0 +1,776 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParser; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserBitmap; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserCmyk; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserGrayscale; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserIndexed; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserLab; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParserRgb; +import org.apache.commons.imaging.formats.psd.datareaders.CompressedDataReader; +import org.apache.commons.imaging.formats.psd.datareaders.DataReader; +import org.apache.commons.imaging.formats.psd.datareaders.UncompressedDataReader; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class PsdImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".psd"; + private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, }; + private static final int PSD_SECTION_HEADER = 0; + private static final int PSD_SECTION_COLOR_MODE = 1; + private static final int PSD_SECTION_IMAGE_RESOURCES = 2; + private static final int PSD_SECTION_LAYER_AND_MASK_DATA = 3; + private static final int PSD_SECTION_IMAGE_DATA = 4; + private static final int PSD_HEADER_LENGTH = 26; + private static final int COLOR_MODE_INDEXED = 2; + public static final int IMAGE_RESOURCE_ID_ICC_PROFILE = 0x040F; + public static final int IMAGE_RESOURCE_ID_XMP = 0x0424; + public static final String BLOCK_NAME_XMP = "XMP"; + + public PsdImageParser() { + super.setByteOrder(ByteOrder.BIG_ENDIAN); + // setDebug(true); + } + + @Override + public String getName() { + return "PSD-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.PSD, // + }; + } + + private PsdHeaderInfo readHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final PsdHeaderInfo ret = readHeader(is); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private PsdHeaderInfo readHeader(final InputStream is) throws ImageReadException, IOException { + readAndVerifyBytes(is, new byte[] { 56, 66, 80, 83 }, "Not a Valid PSD File"); + + final int version = read2Bytes("Version", is, "Not a Valid PSD File", getByteOrder()); + final byte[] reserved = readBytes("Reserved", is, 6, "Not a Valid PSD File"); + final int channels = read2Bytes("Channels", is, "Not a Valid PSD File", getByteOrder()); + final int rows = read4Bytes("Rows", is, "Not a Valid PSD File", getByteOrder()); + final int columns = read4Bytes("Columns", is, "Not a Valid PSD File", getByteOrder()); + final int depth = read2Bytes("Depth", is, "Not a Valid PSD File", getByteOrder()); + final int mode = read2Bytes("Mode", is, "Not a Valid PSD File", getByteOrder()); + + return new PsdHeaderInfo(version, reserved, channels, rows, columns, depth, mode); + } + + private ImageContents readImageContents(final InputStream is) + throws ImageReadException, IOException { + final PsdHeaderInfo header = readHeader(is); + + final int ColorModeDataLength = read4Bytes("ColorModeDataLength", is, + "Not a Valid PSD File", getByteOrder()); + skipBytes(is, ColorModeDataLength); + // is.skip(ColorModeDataLength); + // byte ColorModeData[] = readByteArray("ColorModeData", + // ColorModeDataLength, is, "Not a Valid PSD File"); + + final int ImageResourcesLength = read4Bytes("ImageResourcesLength", is, + "Not a Valid PSD File", getByteOrder()); + skipBytes(is, ImageResourcesLength); + // long skipped = is.skip(ImageResourcesLength); + // byte ImageResources[] = readByteArray("ImageResources", + // ImageResourcesLength, is, "Not a Valid PSD File"); + + final int LayerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", is, + "Not a Valid PSD File", getByteOrder()); + skipBytes(is, LayerAndMaskDataLength); + // is.skip(LayerAndMaskDataLength); + // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", + // LayerAndMaskDataLength, is, "Not a Valid PSD File"); + + final int Compression = read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder()); + + // skip_bytes(is, LayerAndMaskDataLength); + // byte ImageData[] = readByteArray("ImageData", LayerAndMaskDataLength, + // is, "Not a Valid PSD File"); + + // System.out.println("Compression: " + Compression); + + return new ImageContents(header, ColorModeDataLength, + // ColorModeData, + ImageResourcesLength, + // ImageResources, + LayerAndMaskDataLength, + // LayerAndMaskData, + Compression); + } + + private List readImageResourceBlocks(final byte[] bytes, + final int[] imageResourceIDs, final int maxBlocksToRead) + throws ImageReadException, IOException { + return readImageResourceBlocks(new ByteArrayInputStream(bytes), + imageResourceIDs, maxBlocksToRead, bytes.length); + } + + private boolean keepImageResourceBlock(final int ID, final int[] imageResourceIDs) { + if (imageResourceIDs == null) { + return true; + } + + for (final int imageResourceID : imageResourceIDs) { + if (ID == imageResourceID) { + return true; + } + } + + return false; + } + + private List readImageResourceBlocks(final InputStream is, + final int[] imageResourceIDs, final int maxBlocksToRead, int available) + throws ImageReadException, IOException { + final List result = new ArrayList(); + + while (available > 0) { + readAndVerifyBytes(is, new byte[] { 56, 66, 73, 77 }, + "Not a Valid PSD File"); + available -= 4; + + final int id = read2Bytes("ID", is, "Not a Valid PSD File", getByteOrder()); + available -= 2; + + final int nameLength = readByte("NameLength", is, "Not a Valid PSD File"); + + available -= 1; + final byte[] nameBytes = readBytes("NameData", is, nameLength, + "Not a Valid PSD File"); + available -= nameLength; + if (((nameLength + 1) % 2) != 0) { + //final int NameDiscard = + readByte("NameDiscard", is, + "Not a Valid PSD File"); + available -= 1; + } + // String Name = readPString("Name", 6, is, "Not a Valid PSD File"); + final int dataSize = read4Bytes("Size", is, "Not a Valid PSD File", getByteOrder()); + available -= 4; + // int ActualDataSize = ((DataSize % 2) == 0) + // ? DataSize + // : DataSize + 1; // pad to make even + + final byte[] data = readBytes("Data", is, dataSize, "Not a Valid PSD File"); + available -= dataSize; + + if ((dataSize % 2) != 0) { + //final int DataDiscard = + readByte("DataDiscard", is, "Not a Valid PSD File"); + available -= 1; + } + + if (keepImageResourceBlock(id, imageResourceIDs)) { + result.add(new ImageResourceBlock(id, nameBytes, data)); + + if ((maxBlocksToRead >= 0) + && (result.size() >= maxBlocksToRead)) { + return result; + } + } + // debugNumber("ID", ID, 2); + + } + + return result; + } + + private List readImageResourceBlocks( + final ByteSource byteSource, final int[] imageResourceIDs, final int maxBlocksToRead) + throws ImageReadException, IOException { + InputStream imageStream = null; + InputStream resourceStream = null; + boolean canThrow = false; + try { + imageStream = byteSource.getInputStream(); + + final ImageContents imageContents = readImageContents(imageStream); + + resourceStream = this.getInputStream(byteSource, PSD_SECTION_IMAGE_RESOURCES); + final byte[] ImageResources = readBytes("ImageResources", + resourceStream, imageContents.ImageResourcesLength, + "Not a Valid PSD File"); + + final List ret = readImageResourceBlocks(ImageResources, imageResourceIDs, + maxBlocksToRead); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, imageStream, resourceStream); + } + } + + private InputStream getInputStream(final ByteSource byteSource, final int section) + throws ImageReadException, IOException { + InputStream is = null; + boolean notFound = false; + try { + is = byteSource.getInputStream(); + + if (section == PSD_SECTION_HEADER) { + return is; + } + + skipBytes(is, PSD_HEADER_LENGTH); + // is.skip(kHeaderLength); + + final int colorModeDataLength = read4Bytes("ColorModeDataLength", is, "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_COLOR_MODE) { + return is; + } + + skipBytes(is, colorModeDataLength); + // byte ColorModeData[] = readByteArray("ColorModeData", + // ColorModeDataLength, is, "Not a Valid PSD File"); + + final int imageResourcesLength = read4Bytes("ImageResourcesLength", is, "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_IMAGE_RESOURCES) { + return is; + } + + skipBytes(is, imageResourcesLength); + // byte ImageResources[] = readByteArray("ImageResources", + // ImageResourcesLength, is, "Not a Valid PSD File"); + + final int layerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", is, "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_LAYER_AND_MASK_DATA) { + return is; + } + + skipBytes(is, layerAndMaskDataLength); + // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", + // LayerAndMaskDataLength, is, "Not a Valid PSD File"); + + read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder()); + + // byte ImageData[] = readByteArray("ImageData", + // LayerAndMaskDataLength, is, "Not a Valid PSD File"); + + if (section == PSD_SECTION_IMAGE_DATA) { + return is; + } + notFound = true; + } finally { + if (notFound && is != null) { + is.close(); + } + } + throw new ImageReadException("getInputStream: Unknown Section: " + + section); + } + + private byte[] getData(final ByteSource byteSource, final int section) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + // PsdHeaderInfo header = readHeader(is); + if (section == PSD_SECTION_HEADER) { + canThrow = true; + return readBytes("Header", is, PSD_HEADER_LENGTH, + "Not a Valid PSD File"); + } + skipBytes(is, PSD_HEADER_LENGTH); + + final int ColorModeDataLength = read4Bytes("ColorModeDataLength", is, + "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_COLOR_MODE) { + canThrow = true; + return readBytes("ColorModeData", is, ColorModeDataLength, + "Not a Valid PSD File"); + } + + skipBytes(is, ColorModeDataLength); + // byte ColorModeData[] = readByteArray("ColorModeData", + // ColorModeDataLength, is, "Not a Valid PSD File"); + + final int ImageResourcesLength = read4Bytes("ImageResourcesLength", is, + "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_IMAGE_RESOURCES) { + canThrow = true; + return readBytes("ImageResources", is, + ImageResourcesLength, "Not a Valid PSD File"); + } + + skipBytes(is, ImageResourcesLength); + // byte ImageResources[] = readByteArray("ImageResources", + // ImageResourcesLength, is, "Not a Valid PSD File"); + + final int LayerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", + is, "Not a Valid PSD File", getByteOrder()); + + if (section == PSD_SECTION_LAYER_AND_MASK_DATA) { + canThrow = true; + return readBytes("LayerAndMaskData", + is, LayerAndMaskDataLength, "Not a Valid PSD File"); + } + + skipBytes(is, LayerAndMaskDataLength); + // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", + // LayerAndMaskDataLength, is, "Not a Valid PSD File"); + + read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder()); + + // byte ImageData[] = readByteArray("ImageData", + // LayerAndMaskDataLength, is, "Not a Valid PSD File"); + + // if (section == kPSD_SECTION_IMAGE_DATA) + // return readByteArray("LayerAndMaskData", LayerAndMaskDataLength, + // is, + // "Not a Valid PSD File"); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + throw new ImageReadException("getInputStream: Unknown Section: " + + section); + } + + private ImageContents readImageContents(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + final ImageContents imageContents = readImageContents(is); + canThrow = true; + return imageContents; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final List blocks = readImageResourceBlocks(byteSource, + new int[] { IMAGE_RESOURCE_ID_ICC_PROFILE, }, 1); + + if ((blocks == null) || (blocks.size() < 1)) { + return null; + } + + final ImageResourceBlock irb = blocks.get(0); + final byte[] bytes = irb.data; + if ((bytes == null) || (bytes.length < 1)) { + return null; + } + return bytes; + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final PsdHeaderInfo bhi = readHeader(byteSource); + if (bhi == null) { + throw new ImageReadException("PSD: couldn't read header"); + } + + return new Dimension(bhi.columns, bhi.rows); + + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private int getChannelsPerMode(final int mode) { + switch (mode) { + case 0: // Bitmap + return 1; + case 1: // Grayscale + return 1; + case 2: // Indexed + return -1; + case 3: // RGB + return 3; + case 4: // CMYK + return 4; + case 7: // Multichannel + return -1; + case 8: // Duotone + return -1; + case 9: // Lab + return 4; + default: + return -1; + + } + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageContents imageContents = readImageContents(byteSource); + // ImageContents imageContents = readImage(byteSource, false); + + if (imageContents == null) { + throw new ImageReadException("PSD: Couldn't read blocks"); + } + + final PsdHeaderInfo header = imageContents.header; + if (header == null) { + throw new ImageReadException("PSD: Couldn't read Header"); + } + + final int width = header.columns; + final int height = header.rows; + + final List comments = new ArrayList(); + // TODO: comments... + + int BitsPerPixel = header.depth * getChannelsPerMode(header.mode); + // System.out.println("header.Depth: " + header.Depth); + // System.out.println("header.Mode: " + header.Mode); + // System.out.println("getChannelsPerMode(header.Mode): " + + // getChannelsPerMode(header.Mode)); + if (BitsPerPixel < 0) { + BitsPerPixel = 0; + } + final ImageFormat format = ImageFormats.PSD; + final String formatName = "Photoshop"; + final String mimeType = "image/x-photoshop"; + // we ought to count images, but don't yet. + final int numberOfImages = -1; + // not accurate ... only reflects first + final boolean progressive = false; + + final int physicalWidthDpi = 72; + final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); + final int physicalHeightDpi = 72; + final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); + + final String formatDetails = "Psd"; + + final boolean transparent = false; // TODO: inaccurate. + final boolean usesPalette = header.mode == COLOR_MODE_INDEXED; + final int colorType = ImageInfo.COLOR_TYPE_UNKNOWN; + + String compressionAlgorithm; + switch (imageContents.Compression) { + case 0: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; + break; + case 1: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PSD; + break; + default: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN; + } + + return new ImageInfo(formatDetails, BitsPerPixel, comments, + format, formatName, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + } + +// // TODO not used +// private ImageResourceBlock findImageResourceBlock( +// final List blocks, final int ID) { +// for (int i = 0; i < blocks.size(); i++) { +// final ImageResourceBlock block = blocks.get(i); +// +// if (block.id == ID) { +// return block; +// } +// } +// return null; +// } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + pw.println("gif.dumpImageFile"); + + final ImageInfo fImageData = getImageInfo(byteSource); + if (fImageData == null) { + return false; + } + + fImageData.toString(pw, ""); + final ImageContents imageContents = readImageContents(byteSource); + + imageContents.dump(pw); + imageContents.header.dump(pw); + + final List blocks = readImageResourceBlocks( + byteSource, + // fImageContents.ImageResources, + null, -1); + + pw.println("blocks.size(): " + blocks.size()); + + // System.out.println("gif.blocks: " + blocks.blocks.size()); + for (int i = 0; i < blocks.size(); i++) { + final ImageResourceBlock block = blocks.get(i); + pw.println("\t" + i + " (" + Integer.toHexString(block.id) + + ", " + "'" + + new String(block.nameData, "ISO-8859-1") + + "' (" + + block.nameData.length + + "), " + // + block.getClass().getName() + // + ", " + + " data: " + block.data.length + " type: '" + + ImageResourceType.getDescription(block.id) + "' " + + ")"); + } + + pw.println(""); + + return true; + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final ImageContents imageContents = readImageContents(byteSource); + // ImageContents imageContents = readImage(byteSource, false); + + if (imageContents == null) { + throw new ImageReadException("PSD: Couldn't read blocks"); + } + + final PsdHeaderInfo header = imageContents.header; + if (header == null) { + throw new ImageReadException("PSD: Couldn't read Header"); + } + + // ImageDescriptor id = (ImageDescriptor) + // findBlock(fImageContents.blocks, + // kImageSeperator); + // if (id == null) + // throw new ImageReadException("PSD: Couldn't read Image Descriptor"); + // GraphicControlExtension gce = (GraphicControlExtension) findBlock( + // fImageContents.blocks, kGraphicControlExtension); + + readImageResourceBlocks(byteSource, + // fImageContents.ImageResources, + null, -1); + + final int width = header.columns; + final int height = header.rows; + // int height = header.Columns; + + // int transfer_type; + + // transfer_type = DataBuffer.TYPE_BYTE; + + final boolean hasAlpha = false; + final BufferedImage result = getBufferedImageFactory(params) + .getColorBufferedImage(width, height, hasAlpha); + + DataParser dataParser; + switch (imageContents.header.mode) { + case 0: // bitmap + dataParser = new DataParserBitmap(); + break; + case 1: + case 8: // Duotone=8; + dataParser = new DataParserGrayscale(); + break; + case 3: + dataParser = new DataParserRgb(); + break; + case 4: + dataParser = new DataParserCmyk(); + break; + case 9: + dataParser = new DataParserLab(); + break; + case COLOR_MODE_INDEXED: + // case 2 : // Indexed=2; + { + + final byte[] ColorModeData = getData(byteSource, PSD_SECTION_COLOR_MODE); + + // ImageResourceBlock block = findImageResourceBlock(blocks, + // 0x03EB); + // if (block == null) + // throw new ImageReadException( + // "Missing: Indexed Color Image Resource Block"); + + dataParser = new DataParserIndexed(ColorModeData); + break; + } + case 7: // Multichannel=7; + // fDataParser = new DataParserStub(); + // break; + + // case 1 : + // fDataReader = new CompressedDataReader(); + // break; + default: + throw new ImageReadException("Unknown Mode: " + + imageContents.header.mode); + } + DataReader fDataReader; + switch (imageContents.Compression) { + case 0: + fDataReader = new UncompressedDataReader(dataParser); + break; + case 1: + fDataReader = new CompressedDataReader(dataParser); + break; + default: + throw new ImageReadException("Unknown Compression: " + + imageContents.Compression); + } + + InputStream is = null; + boolean canThrow = false; + try { + is = getInputStream(byteSource, PSD_SECTION_IMAGE_DATA); + fDataReader.readData(is, result, imageContents, this); + + canThrow = true; + // is. + // ImageContents imageContents = readImageContents(is); + // return imageContents; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + + return result; + + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + + final ImageContents imageContents = readImageContents(byteSource); + + if (imageContents == null) { + throw new ImageReadException("PSD: Couldn't read blocks"); + } + + final PsdHeaderInfo header = imageContents.header; + if (header == null) { + throw new ImageReadException("PSD: Couldn't read Header"); + } + + final List blocks = readImageResourceBlocks(byteSource, + new int[] { IMAGE_RESOURCE_ID_XMP, }, -1); + + if ((blocks == null) || (blocks.size() < 1)) { + return null; + } + + final List xmpBlocks = new ArrayList(); +// if (false) { +// // TODO: for PSD 7 and later, verify "XMP" name. +// for (int i = 0; i < blocks.size(); i++) { +// final ImageResourceBlock block = blocks.get(i); +// if (!block.getName().equals(BLOCK_NAME_XMP)) { +// continue; +// } +// xmpBlocks.add(block); +// } +// } else { + xmpBlocks.addAll(blocks); +// } + + if (xmpBlocks.size() < 1) { + return null; + } + if (xmpBlocks.size() > 1) { + throw new ImageReadException( + "PSD contains more than one XMP block."); + } + + final ImageResourceBlock block = xmpBlocks.get(0); + + try { + // segment data is UTF-8 encoded xml. + return new String(block.data, 0, block.data.length, "utf-8"); + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("Invalid JPEG XMP Segment.", e); + } + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParser.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParser.java similarity index 54% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParser.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParser.java index 61fd112..7318dd8 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParser.java @@ -1,57 +1,45 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - - -import org.apache.sanselan.formats.psd.ImageContents; -import org.apache.sanselan.formats.psd.PSDHeaderInfo; - -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.DataBuffer; - - -public abstract class DataParser -{ - public final void parseData(int data[][][], BufferedImage bi, - ImageContents imageContents) - { - DataBuffer buffer = bi.getRaster().getDataBuffer(); - - PSDHeaderInfo header = imageContents.header; - int width = header.Columns; - int height = header.Rows; - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int rgb = getRGB(data, x, y, imageContents); - buffer.setElem(y * width + x, rgb); - } - - } - - protected abstract int getRGB(int data[][][], int x, int y, - ImageContents imageContents); - - public abstract int getBasicChannelsCount(); - - public void dump() - { - - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.DataBuffer; + +import org.apache.commons.imaging.formats.psd.ImageContents; +import org.apache.commons.imaging.formats.psd.PsdHeaderInfo; + +public abstract class DataParser { + public final void parseData(final int[][][] data, final BufferedImage bi, + final ImageContents imageContents) { + final DataBuffer buffer = bi.getRaster().getDataBuffer(); + + final PsdHeaderInfo header = imageContents.header; + final int width = header.columns; + final int height = header.rows; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int rgb = getRGB(data, x, y, imageContents); + buffer.setElem(y * width + x, rgb); + } + } + } + + protected abstract int getRGB(int[][][] data, int x, int y, ImageContents imageContents); + + public abstract int getBasicChannelsCount(); +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserBitmap.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserBitmap.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserBitmap.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserBitmap.java index 592374d..22989e9 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserBitmap.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserBitmap.java @@ -1,46 +1,46 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserBitmap extends DataParser -{ - - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int sample = 0xff & data[0][y][x]; - if (sample == 0) - sample = 255; - else - sample = 0; - // sample = 255- sample; - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & sample) << 16) - | ((0xff & sample) << 8) | ((0xff & sample) << 0); - - return rgb; - } - - public int getBasicChannelsCount() - { - return 1; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserBitmap extends DataParser { + + @Override + protected int getRGB(final int[][][] data, final int x, final int y, + final ImageContents imageContents) { + int sample = 0xff & data[0][y][x]; + if (sample == 0) { + sample = 255; + } else { + sample = 0; + } + // sample = 255- sample; + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & sample) << 16) + | ((0xff & sample) << 8) + | ((0xff & sample) << 0); + } + + @Override + public int getBasicChannelsCount() { + return 1; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserCMYK.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserCmyk.java similarity index 70% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserCMYK.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserCmyk.java index 04e3417..a3b87e7 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserCMYK.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserCmyk.java @@ -1,48 +1,45 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.color.ColorConversions; -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserCMYK extends DataParser -{ - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int sc = 0xff & data[0][y][x]; - int sm = 0xff & data[1][y][x]; - int sy = 0xff & data[2][y][x]; - int sk = 0xff & data[3][y][x]; - - // CRAZY adobe has to store the bytes in reverse form. - sc = 255 - sc; - sm = 255 - sm; - sy = 255 - sy; - sk = 255 - sk; - - int rgb = ColorConversions.convertCMYKtoRGB(sc, sm, sy, sk); - - return rgb; - } - - public int getBasicChannelsCount() - { - return 4; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.color.ColorConversions; +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserCmyk extends DataParser { + @Override + protected int getRGB(final int[][][] data, final int x, final int y, + final ImageContents imageContents) { + int sc = 0xff & data[0][y][x]; + int sm = 0xff & data[1][y][x]; + int sy = 0xff & data[2][y][x]; + int sk = 0xff & data[3][y][x]; + + // CRAZY adobe has to store the bytes in reverse form. + sc = 255 - sc; + sm = 255 - sm; + sy = 255 - sy; + sk = 255 - sk; + + return ColorConversions.convertCMYKtoRGB(sc, sm, sy, sk); + } + + @Override + public int getBasicChannelsCount() { + return 4; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserGrayscale.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserGrayscale.java similarity index 57% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserGrayscale.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserGrayscale.java index 29096fe..97e6715 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserGrayscale.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserGrayscale.java @@ -1,40 +1,39 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserGrayscale extends DataParser -{ - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int sample = 0xff & data[0][y][x]; - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & sample) << 16) - | ((0xff & sample) << 8) | ((0xff & sample) << 0); - - return rgb; - } - - public int getBasicChannelsCount() - { - return 1; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserGrayscale extends DataParser { + @Override + protected int getRGB(final int[][][] data, final int x, final int y, + final ImageContents imageContents) { + final int sample = 0xff & data[0][y][x]; + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & sample) << 16) + | ((0xff & sample) << 8) + | ((0xff & sample) << 0); + } + + @Override + public int getBasicChannelsCount() { + return 1; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserIndexed.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserIndexed.java new file mode 100644 index 0000000..f444c6d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserIndexed.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserIndexed extends DataParser { + private final int[] colorTable; + + public DataParserIndexed(final byte[] colorModeData) { + colorTable = new int[256]; + for (int i = 0; i < 256; i++) { + final int red = 0xff & colorModeData[0 * 256 + i]; + final int green = 0xff & colorModeData[1 * 256 + i]; + final int blue = 0xff & colorModeData[2 * 256 + i]; + final int alpha = 0xff; + + final int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) + | ((0xff & green) << 8) | ((0xff & blue) << 0); + + colorTable[i] = rgb; + } + } + + @Override + protected int getRGB(final int[][][] data, final int x, final int y, final ImageContents imageContents) { + final int sample = 0xff & data[0][y][x]; + return colorTable[sample]; + } + + @Override + public int getBasicChannelsCount() { + return 1; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserRGB.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserLab.java similarity index 56% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserRGB.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserLab.java index 75d7650..82a5d7a 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserRGB.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserLab.java @@ -1,42 +1,40 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserRGB extends DataParser -{ - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int red = 0xff & data[0][y][x]; - int green = 0xff & data[1][y][x]; - int blue = 0xff & data[2][y][x]; - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) - | ((0xff & green) << 8) | ((0xff & blue) << 0); - - return rgb; - } - - public int getBasicChannelsCount() - { - return 3; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.color.ColorConversions; +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserLab extends DataParser { + + @Override + protected int getRGB(final int[][][] data, final int x, final int y, final ImageContents imageContents) { + final int cieL = 0xff & data[0][y][x]; + int cieA = 0xff & data[1][y][x]; + int cieB = 0xff & data[2][y][x]; + + cieA -= 128; + cieB -= 128; + + return ColorConversions.convertCIELabtoARGBTest(cieL, cieA, cieB); + } + + @Override + public int getBasicChannelsCount() { + return 3; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserRgb.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserRgb.java new file mode 100644 index 0000000..65847af --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserRgb.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserRgb extends DataParser { + @Override + protected int getRGB(final int[][][] data, final int x, final int y, + final ImageContents imageContents) { + final int red = 0xff & data[0][y][x]; + final int green = 0xff & data[1][y][x]; + final int blue = 0xff & data[2][y][x]; + final int alpha = 0xff; + + return ((0xff & alpha) << 24) + | ((0xff & red) << 16) + | ((0xff & green) << 8) + | ((0xff & blue) << 0); + } + + @Override + public int getBasicChannelsCount() { + return 3; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserStub.java b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserStub.java similarity index 70% rename from src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserStub.java rename to src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserStub.java index c647b15..096b838 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserStub.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/dataparsers/DataParserStub.java @@ -1,34 +1,33 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserStub extends DataParser -{ - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - return 0; - } - - public int getBasicChannelsCount() - { - return 1; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.dataparsers; + +import org.apache.commons.imaging.formats.psd.ImageContents; + +public class DataParserStub extends DataParser { + @Override + protected int getRGB(final int[][][] data, final int x, final int y, + final ImageContents imageContents) { + return 0; + } + + @Override + public int getBasicChannelsCount() { + return 1; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java new file mode 100644 index 0000000..2aa4c2e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.datareaders; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.common.PackBits; +import org.apache.commons.imaging.common.mylzw.BitsToByteInputStream; +import org.apache.commons.imaging.common.mylzw.MyBitInputStream; +import org.apache.commons.imaging.formats.psd.ImageContents; +import org.apache.commons.imaging.formats.psd.PsdHeaderInfo; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParser; +import org.apache.commons.imaging.util.IoUtils; + +public class CompressedDataReader extends DataReader { + + public CompressedDataReader(final DataParser fDataParser) { + super(fDataParser); + } + + @Override + public void readData(final InputStream is, final BufferedImage bi, + final ImageContents imageContents, final BinaryFileParser bfp) + throws ImageReadException, IOException { + final PsdHeaderInfo header = imageContents.header; + final int width = header.columns; + final int height = header.rows; + + // this.setDebug(true); + final int scanlineCount = height * header.channels; + final int[] scanlineBytecounts = new int[scanlineCount]; + for (int i = 0; i < scanlineCount; i++) { + scanlineBytecounts[i] = BinaryFunctions.read2Bytes("scanline_bytecount[" + i + + "]", is, "PSD: bad Image Data", bfp.getByteOrder()); + } + bfp.setDebug(false); + // System.out.println("fImageContents.Compression: " + // + imageContents.Compression); + + final int depth = header.depth; + + final int channelCount = dataParser.getBasicChannelsCount(); + final int[][][] data = new int[channelCount][height][]; + // channels[0] = + for (int channel = 0; channel < channelCount; channel++) { + for (int y = 0; y < height; y++) { + final int index = channel * height + y; + final byte[] packed = BinaryFunctions.readBytes("scanline", + is, scanlineBytecounts[index], + "PSD: Missing Image Data"); + + final byte[] unpacked = new PackBits().decompress(packed, width); + final InputStream bais = new ByteArrayInputStream(unpacked); + final MyBitInputStream mbis = new MyBitInputStream(bais, ByteOrder.BIG_ENDIAN); + BitsToByteInputStream bbis = null; + boolean canThrow = false; + try { + bbis = new BitsToByteInputStream(mbis, 8); // we want all samples to be bytes + final int[] scanline = bbis.readBitsArray(depth, width); + data[channel][y] = scanline; + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bbis); + } + } + } + + dataParser.parseData(data, bi, imageContents); + + } +} diff --git a/src/main/java/org/apache/sanselan/formats/psd/datareaders/DataReader.java b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/DataReader.java similarity index 68% rename from src/main/java/org/apache/sanselan/formats/psd/datareaders/DataReader.java rename to src/main/java/org/apache/commons/imaging/formats/psd/datareaders/DataReader.java index dbcf3fe..a4e2ccc 100644 --- a/src/main/java/org/apache/sanselan/formats/psd/datareaders/DataReader.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/DataReader.java @@ -1,48 +1,38 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.datareaders; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryConstants; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.formats.psd.ImageContents; -import org.apache.sanselan.formats.psd.dataparsers.DataParser; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class DataReader implements BinaryConstants -{ - protected final DataParser dataParser; - - public DataReader(DataParser fDataParser) - { - this.dataParser = fDataParser; - } - - public abstract void readData(InputStream is, BufferedImage bi, - ImageContents imageContents, BinaryFileParser bfp) - throws ImageReadException, IOException; - - public void dump() - { - dataParser.dump(); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.datareaders; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.formats.psd.ImageContents; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParser; + +public abstract class DataReader { + protected final DataParser dataParser; + + public DataReader(final DataParser fDataParser) { + this.dataParser = fDataParser; + } + + public abstract void readData(InputStream is, BufferedImage bi, + ImageContents imageContents, BinaryFileParser bfp) + throws ImageReadException, IOException; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java new file mode 100644 index 0000000..204332a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.psd.datareaders; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.mylzw.BitsToByteInputStream; +import org.apache.commons.imaging.common.mylzw.MyBitInputStream; +import org.apache.commons.imaging.formats.psd.ImageContents; +import org.apache.commons.imaging.formats.psd.PsdHeaderInfo; +import org.apache.commons.imaging.formats.psd.dataparsers.DataParser; +import org.apache.commons.imaging.util.IoUtils; + +public class UncompressedDataReader extends DataReader { + public UncompressedDataReader(final DataParser fDataParser) { + super(fDataParser); + } + + @Override + public void readData(final InputStream is, final BufferedImage bi, + final ImageContents imageContents, final BinaryFileParser bfp) + throws ImageReadException, IOException { + final PsdHeaderInfo header = imageContents.header; + final int width = header.columns; + final int height = header.rows; + + bfp.setDebug(false); + + final int channelCount = dataParser.getBasicChannelsCount(); + final int depth = header.depth; + final MyBitInputStream mbis = new MyBitInputStream(is, ByteOrder.BIG_ENDIAN); + // we want all samples to be bytes + BitsToByteInputStream bbis = null; + boolean canThrow = false; + try { + bbis = new BitsToByteInputStream(mbis, 8); + + final int[][][] data = new int[channelCount][height][width]; + for (int channel = 0; channel < channelCount; channel++) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int b = bbis.readBits(depth); + + data[channel][y][x] = (byte) b; + } + } + } + + dataParser.parseData(data, bi, imageContents); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bbis); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/package-info.java b/src/main/java/org/apache/commons/imaging/formats/psd/package-info.java new file mode 100644 index 0000000..b2f33ec --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/psd/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The PSD image format. + */ +package org.apache.commons.imaging.formats.psd; + diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/APPNSegment.java b/src/main/java/org/apache/commons/imaging/formats/rgbe/InfoHeaderReader.java similarity index 57% rename from src/main/java/org/apache/sanselan/formats/jpeg/segments/APPNSegment.java rename to src/main/java/org/apache/commons/imaging/formats/rgbe/InfoHeaderReader.java index 3412243..331ed5b 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/APPNSegment.java +++ b/src/main/java/org/apache/commons/imaging/formats/rgbe/InfoHeaderReader.java @@ -1,38 +1,47 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.jpeg.JpegImageParser; - -public class APPNSegment extends GenericSegment -{ - public APPNSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length, is); - } - - public String getDescription() - { - return "APPN (APP" + (marker - JpegImageParser.JPEG_APP0_Marker) - + ") (" + getSegmentType() + ")"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.rgbe; + +import java.io.IOException; +import java.io.InputStream; + +class InfoHeaderReader { + private final InputStream is; + + public InfoHeaderReader(final InputStream is) { + this.is = is; + } + + private char read() throws IOException { + final int result = is.read(); + if (result < 0) { + throw new IOException("HDR: Unexpected EOF"); + } + return (char) result; + } + + public String readNextLine() throws IOException { + final StringBuilder buffer = new StringBuilder(); + char c; + + while ((c = read()) != '\n') { + buffer.append(c); + } + + return buffer.toString(); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeImageParser.java b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeImageParser.java new file mode 100644 index 0000000..28662ba --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeImageParser.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.rgbe; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.Point; +import com.google.code.appengine.awt.Transparency; +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.image.BandedSampleModel; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ComponentColorModel; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DataBufferFloat; +import com.google.code.appengine.awt.image.Raster; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Map; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.IoUtils; + +/** + * Parser for Radiance HDR images + * + * @author peter royal + */ +public class RgbeImageParser extends ImageParser { + + public RgbeImageParser() { + setByteOrder(ByteOrder.BIG_ENDIAN); + } + + @Override + public String getName() { + return "Radiance HDR"; + } + + @Override + public String getDefaultExtension() { + return ".hdr"; + } + + @Override + protected String[] getAcceptedExtensions() { + return new String[] { ".hdr", ".pic" }; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.RGBE }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final RgbeInfo info = new RgbeInfo(byteSource); + boolean canThrow = false; + try { + final IImageMetadata ret = info.getMetadata(); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, info); + } + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final RgbeInfo info = new RgbeInfo(byteSource); + boolean canThrow = false; + try { + final ImageInfo ret = new ImageInfo( + getName(), + 32, // todo may be 64 if double? + new ArrayList(), ImageFormats.RGBE, getName(), + info.getHeight(), "image/vnd.radiance", 1, -1, -1, -1, -1, + info.getWidth(), false, false, false, + ImageInfo.COLOR_TYPE_RGB, "Adaptive RLE"); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, info); + } + } + + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final RgbeInfo info = new RgbeInfo(byteSource); + boolean canThrow = false; + try { + // It is necessary to create our own BufferedImage here as the + // org.apache.commons.imaging.common.IBufferedImageFactory interface does + // not expose this complexity + final DataBuffer buffer = new DataBufferFloat(info.getPixelData(), + info.getWidth() * info.getHeight()); + + final BufferedImage ret = new BufferedImage(new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, + Transparency.OPAQUE, buffer.getDataType()), + Raster.createWritableRaster( + new BandedSampleModel(buffer.getDataType(), info + .getWidth(), info.getHeight(), 3), buffer, + new Point()), false, null); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, info); + } + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final RgbeInfo info = new RgbeInfo(byteSource); + boolean canThrow = false; + try { + final Dimension ret = new Dimension(info.getWidth(), info.getHeight()); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, info); + } + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java new file mode 100644 index 0000000..2f66585 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.rgbe; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; + +class RgbeInfo implements Closeable { + // #?RADIANCE + private static final byte[] HEADER = new byte[] { + 0x23, 0x3F, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4E, 0x43, 0x45 + }; + private static final Pattern RESOLUTION_STRING = Pattern.compile("-Y (\\d+) \\+X (\\d+)"); + + private final InputStream in; + private ImageMetadata metadata; + private int width = -1; + private int height = -1; + private static final byte[] TWO_TWO = new byte[] { 0x2, 0x2 }; + + RgbeInfo(final ByteSource byteSource) throws IOException { + this.in = byteSource.getInputStream(); + } + + IImageMetadata getMetadata() throws IOException, ImageReadException { + if (null == metadata) { + readMetadata(); + } + + return metadata; + } + + int getWidth() throws IOException, ImageReadException { + if (-1 == width) { + readDimensions(); + } + + return width; + } + + int getHeight() throws IOException, ImageReadException { + if (-1 == height) { + readDimensions(); + } + + return height; + } + + public void close() throws IOException { + in.close(); + } + + private void readDimensions() throws IOException, ImageReadException { + getMetadata(); // Ensure we've read past this + + final InfoHeaderReader reader = new InfoHeaderReader(in); + final String resolution = reader.readNextLine(); + final Matcher matcher = RESOLUTION_STRING.matcher(resolution); + + if (!matcher.matches()) { + throw new ImageReadException( + "Invalid HDR resolution string. Only \"-Y N +X M\" is supported. Found \"" + + resolution + "\""); + } + + height = Integer.parseInt(matcher.group(1)); + width = Integer.parseInt(matcher.group(2)); + } + + private void readMetadata() throws IOException, ImageReadException { + BinaryFunctions.readAndVerifyBytes(in, HEADER, "Not a valid HDR: Incorrect Header"); + + final InfoHeaderReader reader = new InfoHeaderReader(in); + + if (reader.readNextLine().length() != 0) { + throw new ImageReadException("Not a valid HDR: Incorrect Header"); + } + + metadata = new ImageMetadata(); + + String info = reader.readNextLine(); + + while (info.length() != 0) { + final int equals = info.indexOf('='); + + if (equals > 0) { + final String variable = info.substring(0, equals); + final String value = info.substring(equals + 1); + + if ("FORMAT".equals(value) && !"32-bit_rle_rgbe".equals(value)) { + throw new ImageReadException("Only 32-bit_rle_rgbe images are supported, trying to read " + value); + } + + metadata.add(variable, value); + } else { + metadata.add("", info); + } + + info = reader.readNextLine(); + } + } + + public float[][] getPixelData() throws IOException, ImageReadException { + // Read into local variables to ensure that we have seeked into the file + // far enough + final int ht = getHeight(); + final int wd = getWidth(); + + if (wd >= 32768) { + throw new ImageReadException("Scan lines must be less than 32768 bytes long"); + } + + final byte[] scanLineBytes = ByteConversions.toBytes((short) wd, + ByteOrder.BIG_ENDIAN); + final byte[] rgbe = new byte[wd * 4]; + final float[][] out = new float[3][wd * ht]; + + for (int i = 0; i < ht; i++) { + BinaryFunctions.readAndVerifyBytes(in, TWO_TWO, "Scan line " + i + " expected to start with 0x2 0x2"); + BinaryFunctions.readAndVerifyBytes(in, scanLineBytes, "Scan line " + i + " length expected"); + + decompress(in, rgbe); + + for (int channel = 0; channel < 3; channel++) { + final int channelOffset = channel * wd; + final int eOffset = 3 * wd; + + for (int p = 0; p < wd; p++) { + final int mantissa = rgbe[p + eOffset] & 0xff; + final int pos = p + i * wd; + + if (0 == mantissa) { + out[channel][pos] = 0; + } else { + final float mult = (float) Math.pow(2, mantissa - (128 + 8)); + out[channel][pos] = ((rgbe[p + channelOffset] & 0xff) + 0.5f) * mult; + } + } + } + } + + return out; + } + + private static void decompress(final InputStream in, final byte[] out) + throws IOException { + int position = 0; + final int total = out.length; + + while (position < total) { + final int n = in.read(); + + if (n > 128) { + final int value = in.read(); + + for (int i = 0; i < (n & 0x7f); i++) { + out[position++] = (byte) value; + } + } else { + for (int i = 0; i < n; i++) { + out[position++] = (byte) in.read(); + } + } + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/rgbe/package-info.java b/src/main/java/org/apache/commons/imaging/formats/rgbe/package-info.java new file mode 100644 index 0000000..d3a572c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/rgbe/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Radiance HDR image format. + */ +package org.apache.commons.imaging.formats.rgbe; + diff --git a/src/main/java/org/apache/sanselan/formats/tga/TgaConstants.java b/src/main/java/org/apache/commons/imaging/formats/tga/TgaConstants.java similarity index 89% rename from src/main/java/org/apache/sanselan/formats/tga/TgaConstants.java rename to src/main/java/org/apache/commons/imaging/formats/tga/TgaConstants.java index 2aa459e..88ab5b5 100644 --- a/src/main/java/org/apache/sanselan/formats/tga/TgaConstants.java +++ b/src/main/java/org/apache/commons/imaging/formats/tga/TgaConstants.java @@ -1,22 +1,21 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tga; - -public interface TgaConstants -{ - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tga; + +//public class TgaConstants { +// +//} diff --git a/src/main/java/org/apache/commons/imaging/formats/tga/TgaImageParser.java b/src/main/java/org/apache/commons/imaging/formats/tga/TgaImageParser.java new file mode 100644 index 0000000..2793604 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tga/TgaImageParser.java @@ -0,0 +1,246 @@ +package org.apache.commons.imaging.formats.tga; + +///* +// * Licensed to the Apache Software Foundation (ASF) under one or more +// * contributor license agreements. See the NOTICE file distributed with +// * this work for additional information regarding copyright ownership. +// * The ASF licenses this file to You under the Apache License, Version 2.0 +// * (the "License"); you may not use this file except in compliance with +// * the License. You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +//package org.apache.commons.imaging.formats.tga; +// +//import com.google.code.appengine.awt.Dimension; +//import com.google.code.appengine.awt.image.BufferedImage; +//import java.io.File; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.PrintWriter; +//import java.util.Map; +//import java.util.ArrayList; +// +//import org.apache.commons.imaging.ImageFormat; +//import org.apache.commons.imaging.ImageInfo; +//import org.apache.commons.imaging.ImageParser; +//import org.apache.commons.imaging.ImageReadException; +//import org.apache.commons.imaging.common.IImageMetadata; +//import org.apache.commons.imaging.common.bytesource.ByteSource; +//import org.apache.commons.imaging.util.Debug; +// +///* +// * This class is just a placeholder. TGA format is not yet supported. +// */ +//public class TgaImageParser extends ImageParser implements TgaConstants +//{ +// public TgaImageParser() +// { +// this.setByteOrder(BYTE_ORDER_INTEL); +// setDebug(true); +// } +// +// public String getName() +// { +// return "Tga"; +// } +// +// public String getDefaultExtension() +// { +// return DEFAULT_EXTENSION; +// } +// +// private static final String DEFAULT_EXTENSION = ".tga"; +// +// private static final String[] ACCEPTED_EXTENSIONS = { +// ".tga", ".tpic", +// }; +// +// protected String[] getAcceptedExtensions() +// { +// return ACCEPTED_EXTENSIONS; +// } +// +// protected ImageFormat[] getAcceptedTypes() +// { +// return new ImageFormat[]{ +// ImageFormat.IMAGE_FORMAT_TGA, // +// }; +// } +// +// public IImageMetadata getMetadata(ByteSource byteSource, Map params) +// throws ImageReadException, IOException +// { +// return null; +// } +// +// public byte[] getICCProfileBytes(ByteSource byteSource) +// throws ImageReadException, IOException +// { +// return null; +// } +// +// private static final int TGA_FILE_HEADER_LENGTH = 18; +// +// public Dimension getImageSize(ByteSource byteSource) +// throws ImageReadException, IOException +// { +//// int length = (int) byteSource.getLength(); +//// if (length < TGA_FILE_HEADER_LENGTH) +//// return null; +// +// InputStream is = byteSource.getInputStream(); +// +// is.skip(12); +// +// int width = this.read2Bytes("image width", is, "image width"); +// int height = this.read2Bytes("image height", is, "image height"); +// +// return new Dimension(width, height); +// } +// +// private static final int TGA_FILE_FOOTER_LENGTH = 26; +// private static final String TGA_FILE_FOOTER_SIGNATURE = "TRUEVISION-XFILE"; +// +// private final boolean isNewTGAFormat(ByteSource byteSource) +// throws ImageReadException, IOException +// { +// int length = (int) byteSource.getLength(); +// if (length < TGA_FILE_FOOTER_LENGTH) +// return true; +// +// InputStream is = byteSource.getInputStream(length +// - TGA_FILE_FOOTER_LENGTH); +// +// byte bytes[] = this.readByteArray("tga_file_footer", +// TGA_FILE_FOOTER_LENGTH, is, "tga_file_footer"); +// +// Debug.debug("bytes", bytes); +// +// Debug.debug("kTGA_FILE_FOOTER_SIGNATURE", TGA_FILE_FOOTER_SIGNATURE); +// Debug.debug("kTGA_FILE_FOOTER_SIGNATURE", TGA_FILE_FOOTER_SIGNATURE +// .length()); +// +// return this.compareByteArrays(bytes, 8, TGA_FILE_FOOTER_SIGNATURE +// .getBytes(), 0, TGA_FILE_FOOTER_SIGNATURE.length()); +// } +// +// private static final int TGA_IMAGE_TYPE_NO_IMAGE = 0; +// private static final int UNCOMPRESSED_COLOR_MAPPED = 1; +// private static final int UNCOMPRESSED_RGB = 2; +// private static final int UNCOMPRESSED_BLACK_AND_WHITE = 3; +// private static final int COMPRESSED_COLOR_MAPPED_RLE = 9; +// private static final int COMPRESSED_RGB_RLE = 10; +// private static final int COMPRESSED_BLACK_AND_WHITE = 11; +// private static final int COMPRESSED_COLOR_MAPPED_DATA_HUFFMAN_DELTA_RLE = 32; +// private static final int COMPRESSED_COLOR_MAPPED_DATA_RLE = 33; +// +// public ImageInfo getImageInfo(ByteSource byteSource) +// throws ImageReadException, IOException +// { +//// int length = (int) byteSource.getLength(); +//// if (length < TGA_FILE_HEADER_LENGTH) +//// return null; +// +// InputStream is = byteSource.getInputStream(); +// +// int id_string_length = this.readByte("id_string_length", is, +// "id_string_length"); +// int color_map_type = this.readByte("color_map_type", is, +// "color_map_type"); +// int image_type = this.readByte("image_type", is, "image_type"); +// +// int color_map_first_entry_index = this.read2Bytes( +// "color_map_first_entry_index", is, +// "color_map_first_entry_index"); +// int color_map_length = this.read2Bytes("color_map_length", is, +// "color_map_length"); +// int color_map_entry_size = this.readByte("color_map_entry_size", is, +// "color_map_entry_size"); +// +// int origin_x = this.read2Bytes("origin_x", is, "origin_x"); +// int origin_y = this.read2Bytes("origin_y", is, "origin_y"); +// +// int width = this.read2Bytes("image width", is, "image width"); +// int height = this.read2Bytes("image height", is, "image height"); +// +// int pixel_depth = this.readByte("pixel_depth", is, "pixel_depth"); +// int image_descriptor = this.readByte("image_descriptor", is, +// "image_descriptor"); +// // charles +// +// switch (image_type) +// { +// case UNCOMPRESSED_COLOR_MAPPED : +// break; +// case UNCOMPRESSED_RGB : +// break; +// case UNCOMPRESSED_BLACK_AND_WHITE : +// break; +// case COMPRESSED_COLOR_MAPPED_RLE : +// break; +// case COMPRESSED_RGB_RLE : +// break; +// case COMPRESSED_BLACK_AND_WHITE : +// break; +// case COMPRESSED_COLOR_MAPPED_DATA_HUFFMAN_DELTA_RLE : +// break; +// case COMPRESSED_COLOR_MAPPED_DATA_RLE : +// break; +// +// default : +// +// } +// String FormatDetails; +// int BitsPerPixel; +// List Comments; +// ImageFormat Format = ImageFormat.IMAGE_FORMAT_TGA; +// String FormatName = Format.name; +// String MimeType = "image/tga"; +// int NumberOfImages = 1; // charles could have thumbnail(s). +// int PhysicalHeightDpi; +// float PhysicalHeightInch; +// int PhysicalWidthDpi; +// float PhysicalWidthInch; +// boolean progressive = false; +// boolean transparent = pixel_depth > 24; +// boolean usesPalette; +// int ColorType; +// +// return null; +// // return new ImageInfo(FormatDetails, BitsPerPixel, Comments, Format, +// // FormatName, height, MimeType, NumberOfImages, +// // PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, +// // PhysicalWidthInch, width, progressive, transparent, +// // usesPalette, ColorType); +// +// // boolean is_new_tga_format = isNewTGAFormat(byteSource); +// // +// // Debug.debug("is_new_tga_format", is_new_tga_format); +// } +// +// public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) +// throws ImageReadException, IOException +// { +// return false; +// } +// +// public BufferedImage getBufferedImage(ByteSource byteSource, Map params) +// throws ImageReadException, IOException +// { +// return null; +// } +// +// // public void writeImage(BufferedImage src, OutputStream os, Map params) +// // throws ImageWriteException, IOException +// // { +// // return false; +// // } +// +// } diff --git a/src/main/java/org/apache/sanselan/formats/tiff/JpegImageData.java b/src/main/java/org/apache/commons/imaging/formats/tiff/JpegImageData.java similarity index 76% rename from src/main/java/org/apache/sanselan/formats/tiff/JpegImageData.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/JpegImageData.java index adb0ff0..04c3e40 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/JpegImageData.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/JpegImageData.java @@ -1,32 +1,30 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.tiff; - -public class JpegImageData extends TiffElement.DataElement -{ - public JpegImageData(int offset, int length, final byte data[]) - { - super(offset, length, data); - } - - public String getElementDescription(boolean verbose) - { - return "Jpeg image data: " + data.length + " bytes"; - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.tiff; + +public class JpegImageData extends TiffElement.DataElement { + public JpegImageData(final long offset, final int length, final byte[] data) { + super(offset, length, data); + } + + @Override + public String getElementDescription(final boolean verbose) { + return "Jpeg image data: " + data.length + " bytes"; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffContents.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffContents.java similarity index 51% rename from src/main/java/org/apache/sanselan/formats/tiff/TiffContents.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/TiffContents.java index 86640d1..13ab1ef 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffContents.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffContents.java @@ -1,114 +1,105 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.util.ArrayList; -import java.util.Collections; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.util.Debug; - -public class TiffContents -{ - public final TiffHeader header; - public final ArrayList directories; - - public TiffContents(TiffHeader tiffHeader, ArrayList directories) - { - this.header = tiffHeader; - this.directories = directories; - } - - public ArrayList getElements() throws ImageReadException - { - ArrayList result = new ArrayList(); - - result.add(header); - - for (int i = 0; i < directories.size(); i++) - { - TiffDirectory directory = (TiffDirectory) directories.get(i); - - result.add(directory); - - ArrayList fields = directory.entries; - for (int j = 0; j < fields.size(); j++) - { - TiffField field = (TiffField) fields.get(j); - TiffElement oversizeValue = field.getOversizeValueElement(); - if (null != oversizeValue) - result.add(oversizeValue); - } - - if (directory.hasTiffImageData()) - result.addAll(directory.getTiffRawImageDataElements()); - if (directory.hasJpegImageData()) - result.add(directory.getJpegRawImageDataElement()); - } - - return result; - } - - public TiffField findField(TagInfo tag) throws ImageReadException - { - for (int i = 0; i < directories.size(); i++) - { - TiffDirectory directory = (TiffDirectory) directories.get(i); - - TiffField field = directory.findField(tag); - if (null != field) - return field; - } - - return null; - } - - public void dissect(boolean verbose) throws ImageReadException - { - ArrayList elements = getElements(); - - Collections.sort(elements, TiffElement.COMPARATOR); - - int lastEnd = 0; - for (int i = 0; i < elements.size(); i++) - { - TiffElement element = (TiffElement) elements.get(i); - - if (element.offset > lastEnd) - Debug.debug("\t" + "gap: " + (element.offset - lastEnd)); - if (element.offset < lastEnd) - Debug.debug("\t" + "overlap"); - - Debug.debug("element, start: " + element.offset + ", length: " - + element.length + ", end: " - + (element.offset + element.length) + ": " - + element.getElementDescription(false)); - if (verbose) - { - String verbosity = element.getElementDescription(true); - if (null != verbosity) - Debug.debug(verbosity); - } - - lastEnd = element.offset + element.length; - } - Debug.debug("end: " + lastEnd); - Debug.debug(); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.util.Debug; + +public class TiffContents { + public final TiffHeader header; + public final List directories; + + public TiffContents(final TiffHeader tiffHeader, final List directories) { + this.header = tiffHeader; + this.directories = directories; + } + + public List getElements() throws ImageReadException { + final List result = new ArrayList(); + + result.add(header); + + for (TiffDirectory directory : directories) { + result.add(directory); + + final List fields = directory.entries; + for (TiffField field : fields) { + final TiffElement oversizeValue = field.getOversizeValueElement(); + if (null != oversizeValue) { + result.add(oversizeValue); + } + } + + if (directory.hasTiffImageData()) { + result.addAll(directory.getTiffRawImageDataElements()); + } + if (directory.hasJpegImageData()) { + result.add(directory.getJpegRawImageDataElement()); + } + } + + return result; + } + + public TiffField findField(final TagInfo tag) throws ImageReadException { + for (TiffDirectory directory : directories) { + final TiffField field = directory.findField(tag); + if (null != field) { + return field; + } + } + + return null; + } + + public void dissect(final boolean verbose) throws ImageReadException { + final List elements = getElements(); + + Collections.sort(elements, TiffElement.COMPARATOR); + + long lastEnd = 0; + for (TiffElement element : elements) { + if (element.offset > lastEnd) { + Debug.debug("\t" + "gap: " + (element.offset - lastEnd)); + } + if (element.offset < lastEnd) { + Debug.debug("\t" + "overlap"); + } + + Debug.debug("element, start: " + element.offset + ", length: " + + element.length + ", end: " + + (element.offset + element.length) + ": " + + element.getElementDescription(false)); + if (verbose) { + final String verbosity = element.getElementDescription(true); + if (null != verbosity) { + Debug.debug(verbosity); + } + } + + lastEnd = element.offset + element.length; + } + Debug.debug("end: " + lastEnd); + Debug.debug(); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java new file mode 100644 index 0000000..01e146f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java @@ -0,0 +1,707 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoXpString; + +public class TiffDirectory extends TiffElement { + public final int type; + public final List entries; + public final long nextDirectoryOffset; + private TiffImageData tiffImageData; + private JpegImageData jpegImageData; + + public TiffDirectory(int type, List entries, long offset, long nextDirectoryOffset) { + super(offset, TiffConstants.TIFF_DIRECTORY_HEADER_LENGTH + + entries.size() * TiffConstants.TIFF_ENTRY_LENGTH + + TiffConstants.TIFF_DIRECTORY_FOOTER_LENGTH); + + this.type = type; + this.entries = Collections.unmodifiableList(entries); + this.nextDirectoryOffset = nextDirectoryOffset; + } + + public String description() { + return TiffDirectory.description(type); + } + + @Override + public String getElementDescription(final boolean verbose) { + if (!verbose) { + return "TIFF Directory (" + description() + ")"; + } + + long entryOffset = offset + TiffConstants.TIFF_DIRECTORY_HEADER_LENGTH; + + final StringBuilder result = new StringBuilder(); + for (TiffField entry : entries) { + result.append(String.format("\t[%d]: %s (%d, 0x%x), %s, %d: %s%n", + entryOffset, entry.getTagInfo().name, + entry.getTag(), entry.getTag(), + entry.getFieldType().getName(), entry.getBytesLength(), + entry.getValueDescription())); + + entryOffset += TiffConstants.TIFF_ENTRY_LENGTH; + } + return result.toString(); + } + + public static String description(final int type) { + switch (type) { + case TiffDirectoryConstants.DIRECTORY_TYPE_UNKNOWN: + return "Unknown"; + case TiffDirectoryConstants.DIRECTORY_TYPE_ROOT: + return "Root"; + case TiffDirectoryConstants.DIRECTORY_TYPE_SUB: + return "Sub"; + case TiffDirectoryConstants.DIRECTORY_TYPE_THUMBNAIL: + return "Thumbnail"; + case TiffDirectoryConstants.DIRECTORY_TYPE_EXIF: + return "Exif"; + case TiffDirectoryConstants.DIRECTORY_TYPE_GPS: + return "Gps"; + case TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY: + return "Interoperability"; + default: + return "Bad Type"; + } + } + + + public List getDirectoryEntries() { + return new ArrayList(entries); + } + + public void dump() { + for (TiffField entry : entries) { + entry.dump(); + } + + } + + public boolean hasJpegImageData() throws ImageReadException { + if (null != findField(TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT)) { + return true; + } + + return false; + } + + public boolean hasTiffImageData() throws ImageReadException { + if (null != findField(TiffTagConstants.TIFF_TAG_TILE_OFFSETS)) { + return true; + } + + if (null != findField(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS)) { + return true; + } + + return false; + } + + public BufferedImage getTiffImage(final ByteOrder byteOrder) throws ImageReadException, + IOException { + final Map params = null; + return getTiffImage(byteOrder, params); + } + + public BufferedImage getTiffImage(final ByteOrder byteOrder, final Map params) + throws ImageReadException, IOException { + if (null == tiffImageData) { + return null; + } + + return new TiffImageParser().getBufferedImage(this, byteOrder, params); + } + + public TiffField findField(final TagInfo tag) throws ImageReadException { + final boolean failIfMissing = false; + return findField(tag, failIfMissing); + } + + public TiffField findField(final TagInfo tag, final boolean failIfMissing) + throws ImageReadException { + if (entries == null) { + return null; + } + + for (TiffField field : entries) { + if (field.getTag() == tag.tag) { + return field; + } + } + + if (failIfMissing) { + throw new ImageReadException("Missing expected field: " + + tag.getDescription()); + } + + return null; + } + + public Object getFieldValue(final TagInfo tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + return field.getValue(); + } + + public byte getSingleFieldValue(final TagInfoByte tag) throws ImageReadException { + final byte[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public String getSingleFieldValue(final TagInfoAscii tag) + throws ImageReadException { + final String[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public short getSingleFieldValue(final TagInfoShort tag) + throws ImageReadException { + final short[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public int getSingleFieldValue(final TagInfoLong tag) throws ImageReadException { + final int[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public int getSingleFieldValue(final TagInfoShortOrLong tag) throws ImageReadException { + final int[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public RationalNumber getSingleFieldValue(final TagInfoRational tag) + throws ImageReadException { + final RationalNumber[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public byte getSingleFieldValue(final TagInfoSByte tag) throws ImageReadException { + final byte[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public short getSingleFieldValue(final TagInfoSShort tag) + throws ImageReadException { + final short[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public int getSingleFieldValue(final TagInfoSLong tag) throws ImageReadException { + final int[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public RationalNumber getSingleFieldValue(final TagInfoSRational tag) + throws ImageReadException { + final RationalNumber[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public float getSingleFieldValue(final TagInfoFloat tag) + throws ImageReadException { + final float[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public double getSingleFieldValue(final TagInfoDouble tag) + throws ImageReadException { + final double[] result = getFieldValue(tag, true); + if (result.length != 1) { + throw new ImageReadException("Field \"" + tag.name + + "\" has incorrect length " + result.length); + } + return result[0]; + } + + public byte[] getFieldValue(final TagInfoByte tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + return field.getByteArrayValue(); + } + + public String[] getFieldValue(final TagInfoAscii tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public short[] getFieldValue(final TagInfoShort tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public int[] getFieldValue(final TagInfoLong tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public int[] getFieldValue(final TagInfoShortOrLong tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + if (field.getFieldType() == FieldType.SHORT) { + return ByteConversions.toUInt16s(bytes, field.getByteOrder()); + } else { + return ByteConversions.toInts(bytes, field.getByteOrder()); + } + } + + public RationalNumber[] getFieldValue(final TagInfoRational tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public byte[] getFieldValue(final TagInfoSByte tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + return field.getByteArrayValue(); + } + + public short[] getFieldValue(final TagInfoSShort tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public int[] getFieldValue(final TagInfoSLong tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public RationalNumber[] getFieldValue(final TagInfoSRational tag, + final boolean mustExist) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public float[] getFieldValue(final TagInfoFloat tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public double[] getFieldValue(final TagInfoDouble tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + if (!tag.dataTypes.contains(field.getFieldType())) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" has incorrect type " + field.getFieldType().getName()); + } else { + return null; + } + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public String getFieldValue(final TagInfoGpsText tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + return tag.getValue(field); + } + + public String getFieldValue(final TagInfoXpString tag, final boolean mustExist) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + if (mustExist) { + throw new ImageReadException("Required field \"" + tag.name + + "\" is missing"); + } else { + return null; + } + } + return tag.getValue(field); + } + + public static final class ImageDataElement extends TiffElement { + public ImageDataElement(final long offset, final int length) { + super(offset, length); + } + + @Override + public String getElementDescription(final boolean verbose) { + if (verbose) { + return null; + } + return "ImageDataElement"; + } + } + + private List getRawImageDataElements( + final TiffField offsetsField, final TiffField byteCountsField) + throws ImageReadException { + final int[] offsets = offsetsField.getIntArrayValue(); + final int[] byteCounts = byteCountsField.getIntArrayValue(); + + if (offsets.length != byteCounts.length) { + throw new ImageReadException("offsets.length(" + offsets.length + + ") != byteCounts.length(" + byteCounts.length + ")"); + } + + final List result = new ArrayList(); + for (int i = 0; i < offsets.length; i++) { + result.add(new ImageDataElement(offsets[i], byteCounts[i])); + } + return result; + } + + public List getTiffRawImageDataElements() + throws ImageReadException { + final TiffField tileOffsets = findField(TiffTagConstants.TIFF_TAG_TILE_OFFSETS); + final TiffField tileByteCounts = findField(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS); + final TiffField stripOffsets = findField(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS); + final TiffField stripByteCounts = findField(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS); + + if ((tileOffsets != null) && (tileByteCounts != null)) { + return getRawImageDataElements(tileOffsets, tileByteCounts); + } else if ((stripOffsets != null) && (stripByteCounts != null)) { + return getRawImageDataElements(stripOffsets, stripByteCounts); + } else { + throw new ImageReadException("Couldn't find image data."); + } + } + + public boolean imageDataInStrips() throws ImageReadException { + final TiffField tileOffsets = findField(TiffTagConstants.TIFF_TAG_TILE_OFFSETS); + final TiffField tileByteCounts = findField(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS); + final TiffField stripOffsets = findField(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS); + final TiffField stripByteCounts = findField(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS); + + if ((tileOffsets != null) && (tileByteCounts != null)) { + return false; + } else if ((stripOffsets != null) && (stripByteCounts != null)) { + return true; + } else { + throw new ImageReadException("Couldn't find image data."); + } + } + + public ImageDataElement getJpegRawImageDataElement() throws ImageReadException { + TiffField jpegInterchangeFormat = findField(TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT); + TiffField jpegInterchangeFormatLength = findField(TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + + if (jpegInterchangeFormat != null && jpegInterchangeFormatLength != null) { + final int offSet = jpegInterchangeFormat.getIntArrayValue()[0]; + final int byteCount = jpegInterchangeFormatLength.getIntArrayValue()[0]; + + return new ImageDataElement(offSet, byteCount); + } else { + throw new ImageReadException("Couldn't find image data."); + } + } + + public void setTiffImageData(final TiffImageData rawImageData) { + this.tiffImageData = rawImageData; + } + + public TiffImageData getTiffImageData() { + return tiffImageData; + } + + public void setJpegImageData(final JpegImageData value) { + this.jpegImageData = value; + } + + public JpegImageData getJpegImageData() { + return jpegImageData; + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffElement.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffElement.java similarity index 56% rename from src/main/java/org/apache/sanselan/formats/tiff/TiffElement.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/TiffElement.java index b9e5984..cbcb0ca 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffElement.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffElement.java @@ -1,76 +1,74 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.util.Comparator; - -public abstract class TiffElement -{ - public final int offset; - public final int length; - - public TiffElement(int offset, int length) - { - this.offset = offset; - this.length = length; - } - - public String getElementDescription() - { - return getElementDescription(false); - } - - public abstract String getElementDescription(boolean verbose); - - public static final Comparator COMPARATOR = new Comparator() - { - public int compare(Object o1, Object o2) - { - TiffElement e1 = (TiffElement) o1; - TiffElement e2 = (TiffElement) o2; - return e1.offset - e2.offset; - } - }; - - public static abstract class DataElement extends TiffElement - { - public final byte data[]; - - public DataElement(int offset, int length, final byte data[]) - { - super(offset, length); - - this.data = data; - } - - } - - public static final class Stub extends TiffElement - { - public Stub(int offset, int length) - { - super(offset, length); - } - - public String getElementDescription(boolean verbose) - { - return "Element, offset: " + offset + ", length: " + length - + ", last: " + (offset + length) + ""; - } - - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import java.util.Comparator; + +public abstract class TiffElement { + public final long offset; + public final int length; + public static final Comparator COMPARATOR = new Comparator() { + public int compare(final TiffElement e1, final TiffElement e2) { + if (e1.offset < e2.offset) { + return -1; + } else if (e1.offset > e2.offset) { + return 1; + } else { + return 0; + } + } + }; + + public TiffElement(final long offset, final int length) { + this.offset = offset; + this.length = length; + } + + public String getElementDescription() { + return getElementDescription(false); + } + + public abstract String getElementDescription(boolean verbose); + + public static abstract class DataElement extends TiffElement { + public final byte[] data; + + public DataElement(final long offset, final int length, final byte[] data) { + super(offset, length); + + this.data = data; + } + + public byte[] getData() { + return data; + } + + } + + public static final class Stub extends TiffElement { + public Stub(final long offset, final int length) { + super(offset, length); + } + + @Override + public String getElementDescription(final boolean verbose) { + return "Element, offset: " + offset + ", length: " + length + + ", last: " + (offset + length); + } + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java new file mode 100644 index 0000000..cae17c1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java @@ -0,0 +1,713 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.formats.tiff.constants.AllTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; + +/** + * A TIFF field in a TIFF directory. Immutable. + */ +public class TiffField { + private final TagInfo tagInfo; + private final int tag; + private final int directoryType; + private final FieldType fieldType; + private final long count; + private final long offset; + private final byte[] value; + private final ByteOrder byteOrder; + private final int sortHint; + private static final Map> ALL_TAG_MAP = makeTagMap(AllTagConstants.ALL_TAGS); + + public TiffField(final int tag, final int directoryType, final FieldType fieldType, + final long count, final long offset, final byte[] value, + final ByteOrder byteOrder, final int sortHint) { + + this.tag = tag; + this.directoryType = directoryType; + this.fieldType = fieldType; + this.count = count; + this.offset = offset; + this.value = value; + this.byteOrder = byteOrder; + this.sortHint = sortHint; + + tagInfo = getTag(directoryType, tag); + } + + public int getDirectoryType() { + return directoryType; + } + + public TagInfo getTagInfo() { + return tagInfo; + } + + /** + * Returns the field's tag, derived from bytes 0-1. + * @return the tag, as an int in which only the lowest 2 bytes are set + */ + public int getTag() { + return tag; + } + + /** + * Returns the field's type, derived from bytes 2-3. + * @return the field's type, as a {@code FieldType} object. + */ + public FieldType getFieldType() { + return fieldType; + } + + /** + * Returns the field's count, derived from bytes 4-7. + * @return the count + */ + public long getCount() { + return count; + } + + /** + * Returns the TIFF field's offset/value field, derived from bytes 8-11. + * @return the field's offset in a long of 4 packed bytes, + * or its inlined value <= 4 bytes long encoded in the field's byte order. + */ + public int getOffset() { + return (int) offset; + } + + /** + * Returns the field's byte order. + * @return the byte order + */ + public ByteOrder getByteOrder() { + return byteOrder; + } + + public int getSortHint() { + return sortHint; + } + + /** + * Indicates whether the field's value is inlined into the offset field. + * @return true if the value is inlined + */ + public boolean isLocalValue() { + return (count * fieldType.getSize()) <= TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH; + } + + /** + * The length of the field's value. + * @return the length, in bytes. + */ + public int getBytesLength() { + return (int) count * fieldType.getSize(); + } + + /** + * Returns a copy of the raw value of the field. + * @return the value of the field, in the byte order of the field. + */ + public byte[] getByteArrayValue() { + return BinaryFunctions.head(value, getBytesLength()); + } + + public final class OversizeValueElement extends TiffElement { + public OversizeValueElement(final int offset, final int length) { + super(offset, length); + } + + @Override + public String getElementDescription(final boolean verbose) { + if (verbose) { + return null; + } + + return "OversizeValueElement, tag: " + getTagInfo().name + + ", fieldType: " + getFieldType().getName(); + } + } + + public TiffElement getOversizeValueElement() { + if (isLocalValue()) { + return null; + } + + return new OversizeValueElement(getOffset(), value.length); + } + + private static TagInfo getTag(final int directoryType, final List possibleMatches) { + // Please keep this method in sync with TiffImageMetadata's findField() + + if (possibleMatches.size() < 1) { + return null; + } + // else if (possibleMatches.size() == 1) + // { + // TagInfo tagInfo = (TagInfo) possibleMatches.get(0); + // return tagInfo; + // } + + // first search for exact match. + for (TagInfo tagInfo : possibleMatches) { + if (tagInfo.directoryType == TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN) { + // pass + continue; + } else if (directoryType == tagInfo.directoryType.directoryType) { + return tagInfo; + } + } + + // accept an inexact match. + for (TagInfo tagInfo : possibleMatches) { + if (tagInfo.directoryType == TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN) { + // pass + continue; + } else if (directoryType >= 0 + && tagInfo.directoryType.isImageDirectory()) { + return tagInfo; + } else if (directoryType < 0 + && !tagInfo.directoryType.isImageDirectory()) { + return tagInfo; + } + } + + // accept a wildcard match. + for (TagInfo tagInfo : possibleMatches) { + if (tagInfo.directoryType == TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN) { + return tagInfo; + } + } + + // // accept a very rough match. + // for (int i = 0; i < possibleMatches.size(); i++) + // { + // TagInfo tagInfo = (TagInfo) possibleMatches.get(i); + // if (tagInfo.exifDirectory == EXIF_DIRECTORY_UNKNOWN) + // return tagInfo; + // else if (directoryType == DIRECTORY_TYPE_EXIF + // && tagInfo.exifDirectory == EXIF_DIRECTORY_EXIF_IFD) + // return tagInfo; + // else if (directoryType == DIRECTORY_TYPE_INTEROPERABILITY + // && tagInfo.exifDirectory == EXIF_DIRECTORY_INTEROP_IFD) + // return tagInfo; + // else if (directoryType == DIRECTORY_TYPE_GPS + // && tagInfo.exifDirectory == EXIF_DIRECTORY_GPS) + // return tagInfo; + // else if (directoryType == DIRECTORY_TYPE_MAKER_NOTES + // && tagInfo.exifDirectory == EXIF_DIRECTORY_MAKER_NOTES) + // return tagInfo; + // else if (directoryType >= 0 + // && tagInfo.exifDirectory.isImageDirectory()) + // return tagInfo; + // else if (directoryType < 0 + // && !tagInfo.exifDirectory.isImageDirectory()) + // return tagInfo; + // } + + return TiffTagConstants.TIFF_TAG_UNKNOWN; + + // if (true) + // throw new Error("Why didn't this algorithm work?"); + // + // { + // TagInfo tagInfo = (TagInfo) possibleMatches.get(0); + // return tagInfo; + // } + + // Object key = new Integer(tag); + // + // if (directoryType == DIRECTORY_TYPE_EXIF + // || directoryType == DIRECTORY_TYPE_INTEROPERABILITY) + // { + // if (EXIF_TAG_MAP.containsKey(key)) + // return (TagInfo) EXIF_TAG_MAP.get(key); + // } + // else if (directoryType == DIRECTORY_TYPE_GPS) + // { + // if (GPS_TAG_MAP.containsKey(key)) + // return (TagInfo) GPS_TAG_MAP.get(key); + // } + // else + // { + // if (TIFF_TAG_MAP.containsKey(key)) + // return (TagInfo) TIFF_TAG_MAP.get(key); + // } + // + // if (ALL_TAG_MAP.containsKey(key)) + // return (TagInfo) ALL_TAG_MAP.get(key); + + // public static final int DIRECTORY_TYPE_EXIF = -2; + // // public static final int DIRECTORY_TYPE_SUB = 5; + // public static final int DIRECTORY_TYPE_GPS = -3; + // public static final int DIRECTORY_TYPE_INTEROPERABILITY = -4; + // + // private static final Map GPS_TAG_MAP = makeTagMap(ALL_GPS_TAGS, + // false); + // private static final Map TIFF_TAG_MAP = makeTagMap(ALL_TIFF_TAGS, + // false); + // private static final Map EXIF_TAG_MAP = makeTagMap(ALL_EXIF_TAGS, + // false); + // private static final Map ALL_TAG_MAP = makeTagMap(ALL_TAGS, true); + // + // for (int i = 0; i < ALL_TAGS.length; i++) + // { + // TagInfo2 tag = ALL_TAGS[i]; + // if (tag.tag == value) + // return tag; + // } + + // return TIFF_TAG_UNKNOWN; + } + + private static TagInfo getTag(final int directoryType, final int tag) { + final List possibleMatches = ALL_TAG_MAP.get(tag); + + if (null == possibleMatches) { + return TiffTagConstants.TIFF_TAG_UNKNOWN; + } + + return getTag(directoryType, possibleMatches); + } + + public String getValueDescription() { + try { + return getValueDescription(getValue()); + } catch (final ImageReadException e) { + return "Invalid value: " + e.getMessage(); + } + } + + private String getValueDescription(final Object o) { + if (o == null) { + return null; + } + + if (o instanceof Number) { + return o.toString(); + } else if (o instanceof String) { + return "'" + o.toString().trim() + "'"; + } else if (o instanceof Date) { + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ENGLISH); + return df.format((Date) o); + } else if (o instanceof Object[]) { + final Object[] objects = (Object[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < objects.length; i++) { + final Object object = objects[i]; + + if (i > 50) { + result.append("... (" + objects.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(object.toString()); + } + return result.toString(); + // } else if (o instanceof Number[]) + // { + // Number numbers[] = (Number[]) o; + // StringBuilder result = new StringBuilder(); + // + // for (int i = 0; i < numbers.length; i++) + // { + // Number number = numbers[i]; + // + // if (i > 0) + // result.append(", "); + // result.append("" + number); + // } + // return result.toString(); + // } + } else if (o instanceof short[]) { + final short[] values = (short[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final short sval = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Short.toString(sval)); + } + return result.toString(); + } else if (o instanceof int[]) { + final int[] values = (int[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final int iVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Integer.toString(iVal)); + } + return result.toString(); + } else if (o instanceof long[]) { + final long[] values = (long[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final long lVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Long.toString(lVal)); + } + return result.toString(); + } else if (o instanceof double[]) { + final double[] values = (double[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final double dVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Double.toString(dVal)); + } + return result.toString(); + } else if (o instanceof byte[]) { + final byte[] values = (byte[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final byte bVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Byte.toString(bVal)); + } + return result.toString(); + } else if (o instanceof char[]) { + final char[] values = (char[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final char cVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Character.toString(cVal)); + } + return result.toString(); + } else if (o instanceof float[]) { + final float[] values = (float[]) o; + final StringBuilder result = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + final float fVal = values[i]; + + if (i > 50) { + result.append("... (" + values.length + ")"); + break; + } + if (i > 0) { + result.append(", "); + } + result.append(Float.toString(fVal)); + } + return result.toString(); + } + // else if (o instanceof short[]) + // { + // short numbers[] = (short[]) o; + // StringBuilder result = new StringBuilder(); + // + // for (int i = 0; i < numbers.length; i++) + // { + // short number = numbers[i]; + // + // if (i > 0) + // result.append(", "); + // result.append("" + number); + // } + // return result.toString(); + // } + + return "Unknown: " + o.getClass().getName(); + } + + public void dump() { + final PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + dump(pw); + pw.flush(); + } + + public void dump(final PrintWriter pw) { + dump(pw, null); + } + + public void dump(final PrintWriter pw, final String prefix) { + if (prefix != null) { + pw.print(prefix + ": "); + } + + pw.println(toString()); + pw.flush(); + } + + public String getDescriptionWithoutValue() { + return getTag() + " (0x" + Integer.toHexString(getTag()) + ": " + getTagInfo().name + + "): "; + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + + result.append(getTag() + " (0x" + Integer.toHexString(getTag()) + ": " + + getTagInfo().name + "): "); + result.append(getValueDescription() + " (" + getCount() + " " + + getFieldType().getName() + ")"); + + return result.toString(); + } + + public String getTagName() { + if (getTagInfo() == TiffTagConstants.TIFF_TAG_UNKNOWN) { + return getTagInfo().name + " (0x" + Integer.toHexString(getTag()) + ")"; + } + return getTagInfo().name; + } + + public String getFieldTypeName() { + return getFieldType().getName(); + } + + public Object getValue() throws ImageReadException { + // System.out.print("getValue"); + return getTagInfo().getValue(this); + } + + public String getStringValue() throws ImageReadException { + final Object o = getValue(); + if (o == null) { + return null; + } + if (!(o instanceof String)) { + throw new ImageReadException("Expected String value(" + + getTagInfo().getDescription() + "): " + o); + } + return (String) o; + } + + private static Map> makeTagMap( + final List tags) { + // make sure to use the thread-safe version; this is shared state. + final Map> map = new Hashtable>(); + + for (TagInfo tag : tags) { + List tagList = map.get(tag.tag); + if (tagList == null) { + tagList = new ArrayList(); + map.put(tag.tag, tagList); + } + tagList.add(tag); + } + + return map; + } + + public int[] getIntArrayValue() throws ImageReadException { + final Object o = getValue(); + // if (o == null) + // return null; + + if (o instanceof Number) { + return new int[] { ((Number) o).intValue() }; + } else if (o instanceof Number[]) { + final Number[] numbers = (Number[]) o; + final int[] result = new int[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = numbers[i].intValue(); + } + return result; + } else if (o instanceof short[]) { + final short[] numbers = (short[]) o; + final int[] result = new int[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = 0xffff & numbers[i]; + } + return result; + } else if (o instanceof int[]) { + final int[] numbers = (int[]) o; + final int[] result = new int[numbers.length]; + System.arraycopy(numbers, 0, result, 0, numbers.length); + return result; + } + + throw new ImageReadException("Unknown value: " + o + " for: " + + getTagInfo().getDescription()); + // return null; + } + + public double[] getDoubleArrayValue() throws ImageReadException { + final Object o = getValue(); + // if (o == null) + // return null; + + if (o instanceof Number) { + return new double[] { ((Number) o).doubleValue() }; + } else if (o instanceof Number[]) { + final Number[] numbers = (Number[]) o; + final double[] result = new double[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = numbers[i].doubleValue(); + } + return result; + } else if (o instanceof short[]) { + final short[] numbers = (short[]) o; + final double[] result = new double[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = numbers[i]; + } + return result; + } else if (o instanceof int[]) { + final int[] numbers = (int[]) o; + final double[] result = new double[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = numbers[i]; + } + return result; + } else if (o instanceof float[]) { + final float[] numbers = (float[]) o; + final double[] result = new double[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + result[i] = numbers[i]; + } + return result; + } else if (o instanceof double[]) { + final double[] numbers = (double[]) o; + final double[] result = new double[numbers.length]; + System.arraycopy(numbers, 0, result, 0, numbers.length); + return result; + } + + throw new ImageReadException("Unknown value: " + o + " for: " + + getTagInfo().getDescription()); + // return null; + } + + public int getIntValueOrArraySum() throws ImageReadException { + final Object o = getValue(); + // if (o == null) + // return -1; + + if (o instanceof Number) { + return ((Number) o).intValue(); + } else if (o instanceof Number[]) { + final Number[] numbers = (Number[]) o; + int sum = 0; + for (final Number number : numbers) { + sum += number.intValue(); + } + return sum; + } else if (o instanceof short[]) { + final short[] numbers = (short[]) o; + int sum = 0; + for (final short number : numbers) { + sum += number; + } + return sum; + } else if (o instanceof int[]) { + final int[] numbers = (int[]) o; + int sum = 0; + for (final int number : numbers) { + sum += number; + } + return sum; + } + + throw new ImageReadException("Unknown value: " + o + " for: " + + getTagInfo().getDescription()); + // return -1; + } + + public int getIntValue() throws ImageReadException { + final Object o = getValue(); + if (o == null) { + throw new ImageReadException("Missing value: " + + getTagInfo().getDescription()); + } + + return ((Number) o).intValue(); + } + + public double getDoubleValue() throws ImageReadException { + final Object o = getValue(); + if (o == null) { + throw new ImageReadException("Missing value: " + + getTagInfo().getDescription()); + } + + return ((Number) o).doubleValue(); + } +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffHeader.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffHeader.java similarity index 68% rename from src/main/java/org/apache/sanselan/formats/tiff/TiffHeader.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/TiffHeader.java index 931a594..074462d 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffHeader.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffHeader.java @@ -1,43 +1,44 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -public class TiffHeader extends TiffElement -{ - public final int byteOrder; - public final int tiffVersion; - public final int offsetToFirstIFD; - - public TiffHeader(final int byteOrder, int tiffVersion, int offsetToFirstIFD) - { - super(0, TiffConstants.TIFF_HEADER_SIZE); - - this.byteOrder = byteOrder; - this.tiffVersion = tiffVersion; - this.offsetToFirstIFD = offsetToFirstIFD; - } - - public String getElementDescription(boolean verbose) - { - if (verbose) - return null; - - return "TIFF Header"; - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; + +public class TiffHeader extends TiffElement { + public final ByteOrder byteOrder; + public final int tiffVersion; + public final long offsetToFirstIFD; + + public TiffHeader(final ByteOrder byteOrder, final int tiffVersion, final long offsetToFirstIFD) { + super(0, TiffConstants.TIFF_HEADER_SIZE); + + this.byteOrder = byteOrder; + this.tiffVersion = tiffVersion; + this.offsetToFirstIFD = offsetToFirstIFD; + } + + @Override + public String getElementDescription(final boolean verbose) { + if (verbose) { + return null; + } + + return "TIFF Header"; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageData.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageData.java new file mode 100644 index 0000000..75746e8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageData.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.tiff; + +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.tiff.datareaders.DataReader; +import org.apache.commons.imaging.formats.tiff.datareaders.DataReaderStrips; +import org.apache.commons.imaging.formats.tiff.datareaders.DataReaderTiled; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; + +public abstract class TiffImageData { + public static class Tiles extends TiffImageData { + public final TiffElement.DataElement[] tiles; + + // public final byte tiles[][]; + private final int tileWidth; + private final int tileLength; + + public Tiles(final TiffElement.DataElement[] tiles, final int tileWidth, final int tileLength) { + this.tiles = tiles; + this.tileWidth = tileWidth; + this.tileLength = tileLength; + } + + @Override + public TiffElement.DataElement[] getImageData() { + return tiles; + } + + @Override + public boolean stripsNotTiles() { + return false; + } + + @Override + public DataReader getDataReader(final TiffDirectory directory, + final PhotometricInterpreter photometricInterpreter, + final int bitsPerPixel, final int[] bitsPerSample, final int predictor, + final int samplesPerPixel, final int width, final int height, final int compression, + final ByteOrder byteOrder) throws IOException, ImageReadException { + return new DataReaderTiled(directory, photometricInterpreter, + tileWidth, tileLength, bitsPerPixel, bitsPerSample, + predictor, samplesPerPixel, width, height, compression, + byteOrder, this); + } + + /** + * Get the width of individual tiles. Note that if the overall + * image width is not a multiple of the tile width, then + * the last column of tiles may extend beyond the image width. + * @return an integer value greater than zero + */ + public int getTileWidth() { + return tileWidth; + } + + /** + * Get the height of individual tiles. Note that if the overall + * image height is not a multiple of the tile height, then + * the last row of tiles may extend beyond the image height. + * @return an integer value greater than zero + */ + public int getTileHeight() { + return tileLength; + } + + // public TiffElement[] getElements() + // { + // return tiles; + // } + } + + public static class Strips extends TiffImageData { + public final TiffElement.DataElement[] strips; + // public final byte strips[][]; + public final int rowsPerStrip; + + public Strips(final TiffElement.DataElement[] strips, final int rowsPerStrip) { + this.strips = strips; + this.rowsPerStrip = rowsPerStrip; + } + + @Override + public TiffElement.DataElement[] getImageData() { + return strips; + } + + @Override + public boolean stripsNotTiles() { + return true; + } + + @Override + public DataReader getDataReader(final TiffDirectory directory, + final PhotometricInterpreter photometricInterpreter, + final int bitsPerPixel, final int[] bitsPerSample, final int predictor, + final int samplesPerPixel, final int width, final int height, final int compression, + final ByteOrder byteorder) throws IOException, ImageReadException { + return new DataReaderStrips(directory, photometricInterpreter, + bitsPerPixel, bitsPerSample, predictor, samplesPerPixel, + width, height, compression, byteorder, rowsPerStrip, this); + } + + // public TiffElement[] getElements() + // { + // return strips; + // } + + } + + // public abstract TiffElement[] getElements(); + + public abstract TiffElement.DataElement[] getImageData(); + + public abstract boolean stripsNotTiles(); + + public abstract DataReader getDataReader(TiffDirectory directory, + PhotometricInterpreter photometricInterpreter, int bitsPerPixel, + int[] bitsPerSample, int predictor, int samplesPerPixel, int width, + int height, int compression, ByteOrder byteOrder) throws IOException, + ImageReadException; + + public static class Data extends TiffElement.DataElement { + public Data(final long offset, final int length, final byte[] data) { + super(offset, length, data); + } + + @Override + public String getElementDescription(final boolean verbose) { + return "Tiff image data: " + data.length + " bytes"; + } + + } + + public static class ByteSourceData extends Data { + ByteSourceFile byteSourceFile; + + public ByteSourceData(final long offset, final int length, final ByteSourceFile byteSource) { + super(offset, length, new byte[0]); + byteSourceFile = byteSource; + } + + @Override + public String getElementDescription(final boolean verbose) { + return "Tiff image data: " + data.length + " bytes"; + } + + @Override + public byte[] getData() { + try { + return byteSourceFile.getBlock(offset, length); + } catch (final IOException ioex) { + return new byte[0]; + } + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageMetadata.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageMetadata.java new file mode 100644 index 0000000..64ca1b8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageMetadata.java @@ -0,0 +1,593 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.AllTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoXpString; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputField; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; + +public class TiffImageMetadata extends ImageMetadata { + public final TiffContents contents; + private static final Map TAG_COUNTS = countTags(AllTagConstants.ALL_TAGS); + + public TiffImageMetadata(final TiffContents contents) { + this.contents = contents; + } + + private static Map countTags(final List tags) { + final Map map = new Hashtable(); + + for (TagInfo tag : tags) { + final Integer count = map.get(tag.tag); + if (count == null) { + map.put(tag.tag, 1); + } else { + map.put(tag.tag, count + 1); + } + } + + return map; + } + + public static class Directory extends ImageMetadata implements + ImageMetadata.IImageMetadataItem { + // private BufferedImage thumbnail = null; + + public final int type; + + private final TiffDirectory directory; + private final ByteOrder byteOrder; + + public Directory(final ByteOrder byteOrder, final TiffDirectory directory) { + this.type = directory.type; + this.directory = directory; + this.byteOrder = byteOrder; + } + + public void add(final TiffField entry) { + add(new TiffImageMetadata.Item(entry)); + } + + public BufferedImage getThumbnail() throws ImageReadException, + IOException { + return directory.getTiffImage(byteOrder); + } + + public TiffImageData getTiffImageData() { + return directory.getTiffImageData(); + } + + public TiffField findField(final TagInfo tagInfo) throws ImageReadException { + return directory.findField(tagInfo); + } + + public List getAllFields() { + return directory.getDirectoryEntries(); + } + + public JpegImageData getJpegImageData() { + return directory.getJpegImageData(); + } + + @Override + public String toString(final String prefix) { + return (prefix != null ? prefix : "") + directory.description() + + ": " // + + (getTiffImageData() != null ? " (tiffImageData)" : "") // + + (getJpegImageData() != null ? " (jpegImageData)" : "") // + + "\n" + super.toString(prefix) + "\n"; + } + + public TiffOutputDirectory getOutputDirectory(final ByteOrder byteOrder) + throws ImageWriteException { + try { + final TiffOutputDirectory dstDir = new TiffOutputDirectory(type, + byteOrder); + + final List entries = getItems(); + for (IImageMetadataItem entry : entries) { + final TiffImageMetadata.Item item = (TiffImageMetadata.Item) entry; + final TiffField srcField = item.getTiffField(); + + if (null != dstDir.findField(srcField.getTag())) { + // ignore duplicate tags in a directory. + continue; + } else if (srcField.getTagInfo().isOffset()) { + // ignore offset fields. + continue; + } + + final TagInfo tagInfo = srcField.getTagInfo(); + final FieldType fieldType = srcField.getFieldType(); + // byte bytes[] = srcField.fieldType.getRawBytes(srcField); + + // Debug.debug("tagInfo", tagInfo); + + final Object value = srcField.getValue(); + + // Debug.debug("value", Debug.getType(value)); + + final byte[] bytes = tagInfo.encodeValue(fieldType, value, + byteOrder); + + // if (tagInfo.isUnknown()) + // Debug.debug( + // "\t" + "unknown tag(0x" + // + Integer.toHexString(srcField.tag) + // + ") bytes", bytes); + + final int count = bytes.length / fieldType.getSize(); + final TiffOutputField dstField = new TiffOutputField( + srcField.getTag(), tagInfo, fieldType, count, bytes); + dstField.setSortHint(srcField.getSortHint()); + dstDir.add(dstField); + } + + dstDir.setTiffImageData(getTiffImageData()); + dstDir.setJpegImageData(getJpegImageData()); + + return dstDir; + } catch (final ImageReadException e) { + throw new ImageWriteException(e.getMessage(), e); + } + } + + } + + public List getDirectories() { + return super.getItems(); + } + + @Override + public List getItems() { + final List result = new ArrayList(); + + final List items = super.getItems(); + for (IImageMetadataItem item : items) { + final Directory dir = (Directory) item; + result.addAll(dir.getItems()); + } + + return result; + } + + public static class Item extends ImageMetadata.Item { + private final TiffField entry; + + public Item(final TiffField entry) { + // super(entry.getTagName() + " (" + entry.getFieldTypeName() + ")", + super(entry.getTagName(), entry.getValueDescription()); + this.entry = entry; + } + + public TiffField getTiffField() { + return entry; + } + + } + + public TiffOutputSet getOutputSet() throws ImageWriteException { + final ByteOrder byteOrder = contents.header.byteOrder; + final TiffOutputSet result = new TiffOutputSet(byteOrder); + + final List srcDirs = getDirectories(); + for (IImageMetadataItem srcDir1 : srcDirs) { + final Directory srcDir = (Directory) srcDir1; + + if (null != result.findDirectory(srcDir.type)) { + // Certain cameras right directories more than once. + // This is a bug. + // Ignore second directory of a given type. + continue; + } + + final TiffOutputDirectory outputDirectory = srcDir + .getOutputDirectory(byteOrder); + result.addDirectory(outputDirectory); + } + + return result; + } + + public TiffField findField(final TagInfo tagInfo) throws ImageReadException { + return findField(tagInfo, false); + } + + public TiffField findField(final TagInfo tagInfo, final boolean exactDirectoryMatch) + throws ImageReadException { + // Please keep this method in sync with TiffField's getTag() + final Integer tagCount = TAG_COUNTS.get(tagInfo.tag); + final int tagsMatching = tagCount == null ? 0 : tagCount; + + final List directories = getDirectories(); + if (exactDirectoryMatch + || tagInfo.directoryType != TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN) { + for (IImageMetadataItem directory1 : directories) { + final Directory directory = (Directory) directory1; + if (directory.type == tagInfo.directoryType.directoryType) { + final TiffField field = directory.findField(tagInfo); + if (field != null) { + return field; + } + } + } + if (exactDirectoryMatch || tagsMatching > 1) { + return null; + } + for (IImageMetadataItem directory1 : directories) { + final Directory directory = (Directory) directory1; + if (tagInfo.directoryType.isImageDirectory() + && directory.type >= 0) { + final TiffField field = directory.findField(tagInfo); + if (field != null) { + return field; + } + } else if (!tagInfo.directoryType.isImageDirectory() + && directory.type < 0) { + final TiffField field = directory.findField(tagInfo); + if (field != null) { + return field; + } + } + } + } + + for (IImageMetadataItem directory1 : directories) { + final Directory directory = (Directory) directory1; + final TiffField field = directory.findField(tagInfo); + if (field != null) { + return field; + } + } + + return null; + } + + public Object getFieldValue(final TagInfo tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + return field.getValue(); + } + + public byte[] getFieldValue(final TagInfoByte tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + return field.getByteArrayValue(); + } + + public String[] getFieldValue(final TagInfoAscii tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public short[] getFieldValue(final TagInfoShort tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public int[] getFieldValue(final TagInfoLong tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public RationalNumber[] getFieldValue(final TagInfoRational tag) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public byte[] getFieldValue(final TagInfoSByte tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + return field.getByteArrayValue(); + } + + public short[] getFieldValue(final TagInfoSShort tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public int[] getFieldValue(final TagInfoSLong tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public RationalNumber[] getFieldValue(final TagInfoSRational tag) + throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public float[] getFieldValue(final TagInfoFloat tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public double[] getFieldValue(final TagInfoDouble tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + if (!tag.dataTypes.contains(field.getFieldType())) { + return null; + } + final byte[] bytes = field.getByteArrayValue(); + return tag.getValue(field.getByteOrder(), bytes); + } + + public String getFieldValue(final TagInfoGpsText tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + return tag.getValue(field); + } + + public String getFieldValue(final TagInfoXpString tag) throws ImageReadException { + final TiffField field = findField(tag); + if (field == null) { + return null; + } + return tag.getValue(field); + } + + public TiffDirectory findDirectory(final int directoryType) { + final List directories = getDirectories(); + for (IImageMetadataItem directory1 : directories) { + final Directory directory = (Directory) directory1; + if (directory.type == directoryType) { + return directory.directory; + } + } + return null; + } + + public List getAllFields() { + final List result = new ArrayList(); + final List directories = getDirectories(); + for (IImageMetadataItem directory1 : directories) { + final Directory directory = (Directory) directory1; + result.addAll(directory.getAllFields()); + } + return result; + } + + public GPSInfo getGPS() throws ImageReadException { + final TiffDirectory gpsDirectory = findDirectory(TiffDirectoryConstants.DIRECTORY_TYPE_GPS); + if (null == gpsDirectory) { + return null; + } + + // more specific example of how to access GPS values. + final TiffField latitudeRefField = gpsDirectory + .findField(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); + final TiffField latitudeField = gpsDirectory + .findField(GpsTagConstants.GPS_TAG_GPS_LATITUDE); + final TiffField longitudeRefField = gpsDirectory + .findField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); + final TiffField longitudeField = gpsDirectory + .findField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE); + + if (latitudeRefField == null || latitudeField == null + || longitudeRefField == null || longitudeField == null) { + return null; + } + + // all of these values are strings. + final String latitudeRef = latitudeRefField.getStringValue(); + final RationalNumber[] latitude = (RationalNumber[]) latitudeField.getValue(); + final String longitudeRef = longitudeRefField.getStringValue(); + final RationalNumber[] longitude = (RationalNumber[]) longitudeField.getValue(); + + if (latitude.length != 3 || longitude.length != 3) { + throw new ImageReadException("Expected three values for latitude and longitude."); + } + + final RationalNumber latitudeDegrees = latitude[0]; + final RationalNumber latitudeMinutes = latitude[1]; + final RationalNumber latitudeSeconds = latitude[2]; + + final RationalNumber longitudeDegrees = longitude[0]; + final RationalNumber longitudeMinutes = longitude[1]; + final RationalNumber longitudeSeconds = longitude[2]; + + return new GPSInfo(latitudeRef, longitudeRef, latitudeDegrees, + latitudeMinutes, latitudeSeconds, longitudeDegrees, + longitudeMinutes, longitudeSeconds); + } + + public static class GPSInfo { + public final String latitudeRef; + public final String longitudeRef; + + public final RationalNumber latitudeDegrees; + public final RationalNumber latitudeMinutes; + public final RationalNumber latitudeSeconds; + public final RationalNumber longitudeDegrees; + public final RationalNumber longitudeMinutes; + public final RationalNumber longitudeSeconds; + + public GPSInfo(final String latitudeRef, final String longitudeRef, + final RationalNumber latitudeDegrees, + final RationalNumber latitudeMinutes, + final RationalNumber latitudeSeconds, + final RationalNumber longitudeDegrees, + final RationalNumber longitudeMinutes, + final RationalNumber longitudeSeconds) { + this.latitudeRef = latitudeRef; + this.longitudeRef = longitudeRef; + this.latitudeDegrees = latitudeDegrees; + this.latitudeMinutes = latitudeMinutes; + this.latitudeSeconds = latitudeSeconds; + this.longitudeDegrees = longitudeDegrees; + this.longitudeMinutes = longitudeMinutes; + this.longitudeSeconds = longitudeSeconds; + } + + @Override + public String toString() { + // This will format the gps info like so: + // + // latitude: 8 degrees, 40 minutes, 42.2 seconds S + // longitude: 115 degrees, 26 minutes, 21.8 seconds E + + final StringBuilder result = new StringBuilder(88); + result.append("[GPS. Latitude: " + latitudeDegrees.toDisplayString() + + " degrees, " + latitudeMinutes.toDisplayString() + + " minutes, " + latitudeSeconds.toDisplayString() + + " seconds " + latitudeRef); + result.append(", Longitude: " + longitudeDegrees.toDisplayString() + + " degrees, " + longitudeMinutes.toDisplayString() + + " minutes, " + longitudeSeconds.toDisplayString() + + " seconds " + longitudeRef); + result.append(']'); + + return result.toString(); + } + + public double getLongitudeAsDegreesEast() throws ImageReadException { + final double result = longitudeDegrees.doubleValue() + + (longitudeMinutes.doubleValue() / 60.0) + + (longitudeSeconds.doubleValue() / 3600.0); + + if (longitudeRef.trim().equalsIgnoreCase("e")) { + return result; + } else if (longitudeRef.trim().equalsIgnoreCase("w")) { + return -result; + } else { + throw new ImageReadException("Unknown longitude ref: \"" + + longitudeRef + "\""); + } + } + + public double getLatitudeAsDegreesNorth() throws ImageReadException { + final double result = latitudeDegrees.doubleValue() + + (latitudeMinutes.doubleValue() / 60.0) + + (latitudeSeconds.doubleValue() / 3600.0); + + if (latitudeRef.trim().equalsIgnoreCase("n")) { + return result; + } else if (latitudeRef.trim().equalsIgnoreCase("s")) { + return -result; + } else { + throw new ImageReadException("Unknown latitude ref: \"" + + latitudeRef + "\""); + } + } + + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java new file mode 100644 index 0000000..02d3ec5 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java @@ -0,0 +1,737 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.Rectangle; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement; +import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.datareaders.DataReader; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCieLab; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCmyk; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLuv; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr; +import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public class TiffImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".tif"; + private static final String[] ACCEPTED_EXTENSIONS = { ".tif", ".tiff", }; + + @Override + public String getName() { + return "Tiff-Custom"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.TIFF, // + }; + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(isStrict(params)) + .readFirstDirectory(byteSource, params, false, formatCompliance); + final TiffDirectory directory = contents.directories.get(0); + + return directory.getFieldValue(TiffEpTagConstants.EXIF_TAG_INTER_COLOR_PROFILE, + false); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(isStrict(params)) + .readFirstDirectory(byteSource, params, false, formatCompliance); + final TiffDirectory directory = contents.directories.get(0); + + final TiffField widthField = directory.findField( + TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true); + final TiffField heightField = directory.findField( + TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true); + + if ((widthField == null) || (heightField == null)) { + throw new ImageReadException("TIFF image missing size info."); + } + + final int height = heightField.getIntValue(); + final int width = widthField.getIntValue(); + + return new Dimension(width, height); + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffReader tiffReader = new TiffReader(isStrict(params)); + final TiffContents contents = tiffReader.readContents(byteSource, params, + formatCompliance); + + final List directories = contents.directories; + + final TiffImageMetadata result = new TiffImageMetadata(contents); + + for (TiffDirectory dir : directories) { + final TiffImageMetadata.Directory metadataDirectory = new TiffImageMetadata.Directory( + tiffReader.getByteOrder(), dir); + + final List entries = dir.getDirectoryEntries(); + + for (TiffField entry : entries) { + metadataDirectory.add(entry); + } + + result.add(metadataDirectory); + } + + return result; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(isStrict(params)) + .readDirectories(byteSource, false, formatCompliance); + final TiffDirectory directory = contents.directories.get(0); + + final TiffField widthField = directory.findField( + TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true); + final TiffField heightField = directory.findField( + TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true); + + if ((widthField == null) || (heightField == null)) { + throw new ImageReadException("TIFF image missing size info."); + } + + final int height = heightField.getIntValue(); + final int width = widthField.getIntValue(); + + // ------------------- + + final TiffField resolutionUnitField = directory + .findField(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT); + int resolutionUnit = 2; // Inch + if ((resolutionUnitField != null) + && (resolutionUnitField.getValue() != null)) { + resolutionUnit = resolutionUnitField.getIntValue(); + } + + double unitsPerInch = -1; + switch (resolutionUnit) { + case 1: + break; + case 2: // Inch + unitsPerInch = 1.0; + break; + case 3: // Centimeter + unitsPerInch = 2.54; + break; + default: + break; + + } + final TiffField xResolutionField = directory + .findField(TiffTagConstants.TIFF_TAG_XRESOLUTION); + final TiffField yResolutionField = directory + .findField(TiffTagConstants.TIFF_TAG_YRESOLUTION); + + int physicalWidthDpi = -1; + float physicalWidthInch = -1; + int physicalHeightDpi = -1; + float physicalHeightInch = -1; + + if (unitsPerInch > 0) { + if ((xResolutionField != null) + && (xResolutionField.getValue() != null)) { + final double xResolutionPixelsPerUnit = xResolutionField + .getDoubleValue(); + physicalWidthDpi = (int) Math + .round((xResolutionPixelsPerUnit * unitsPerInch)); + physicalWidthInch = (float) (width / (xResolutionPixelsPerUnit * unitsPerInch)); + } + if ((yResolutionField != null) + && (yResolutionField.getValue() != null)) { + final double yResolutionPixelsPerUnit = yResolutionField + .getDoubleValue(); + physicalHeightDpi = (int) Math + .round((yResolutionPixelsPerUnit * unitsPerInch)); + physicalHeightInch = (float) (height / (yResolutionPixelsPerUnit * unitsPerInch)); + } + } + + // ------------------- + + final TiffField bitsPerSampleField = directory + .findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); + + int bitsPerSample = 1; + if ((bitsPerSampleField != null) + && (bitsPerSampleField.getValue() != null)) { + bitsPerSample = bitsPerSampleField.getIntValueOrArraySum(); + } + + final int bitsPerPixel = bitsPerSample; // assume grayscale; + // dunno if this handles colormapped images correctly. + + // ------------------- + + final List comments = new ArrayList(); + final List entries = directory.entries; + for (TiffField field : entries) { + final String comment = field.toString(); + comments.add(comment); + } + + final ImageFormat format = ImageFormats.TIFF; + final String formatName = "TIFF Tag-based Image File Format"; + final String mimeType = "image/tiff"; + final int numberOfImages = contents.directories.size(); + // not accurate ... only reflects first + final boolean progressive = false; + // is TIFF ever interlaced/progressive? + + final String formatDetails = "Tiff v." + contents.header.tiffVersion; + + final boolean transparent = false; // TODO: wrong + boolean usesPalette = false; + final TiffField colorMapField = directory + .findField(TiffTagConstants.TIFF_TAG_COLOR_MAP); + if (colorMapField != null) { + usesPalette = true; + } + + final int colorType = ImageInfo.COLOR_TYPE_RGB; + + final int compression = 0xffff & directory + .getSingleFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); + String compressionAlgorithm; + + switch (compression) { + case TIFF_COMPRESSION_UNCOMPRESSED_1: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; + break; + case TIFF_COMPRESSION_CCITT_1D: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_1D; + break; + case TIFF_COMPRESSION_CCITT_GROUP_3: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_3; + break; + case TIFF_COMPRESSION_CCITT_GROUP_4: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_4; + break; + case TIFF_COMPRESSION_LZW: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; + break; + case TIFF_COMPRESSION_JPEG: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; + break; + case TIFF_COMPRESSION_UNCOMPRESSED_2: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; + break; + case TIFF_COMPRESSION_PACKBITS: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PACKBITS; + break; + default: + compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN; + break; + } + + final ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments, + format, formatName, height, mimeType, numberOfImages, + physicalHeightDpi, physicalHeightInch, physicalWidthDpi, + physicalWidthInch, width, progressive, transparent, + usesPalette, colorType, compressionAlgorithm); + + return result; + } + + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(isStrict(params)) + .readDirectories(byteSource, false, formatCompliance); + final TiffDirectory directory = contents.directories.get(0); + + final byte[] bytes = directory.getFieldValue(TiffTagConstants.TIFF_TAG_XMP, + false); + if (bytes == null) { + return null; + } + + try { + // segment data is UTF-8 encoded xml. + final String xml = new String(bytes, "utf-8"); + return xml; + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("Invalid JPEG XMP Segment.", e); + } + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + try { + pw.println("tiff.dumpImageFile"); + + { + final ImageInfo imageData = getImageInfo(byteSource); + if (imageData == null) { + return false; + } + + imageData.toString(pw, ""); + } + + pw.println(""); + + // try + { + final FormatCompliance formatCompliance = FormatCompliance + .getDefault(); + final Map params = null; + final TiffContents contents = new TiffReader(true).readContents( + byteSource, params, formatCompliance); + + final List directories = contents.directories; + + if (directories == null) { + return false; + } + + for (int d = 0; d < directories.size(); d++) { + final TiffDirectory directory = directories.get(d); + + final List entries = directory.entries; + + if (entries == null) { + return false; + } + + // Debug.debug("directory offset", directory.offset); + + for (TiffField field : entries) { + field.dump(pw, Integer.toString(d)); + } + } + + pw.println(""); + } + // catch (Exception e) + // { + // Debug.debug(e); + // pw.println(""); + // return false; + // } + + return true; + } finally { + pw.println(""); + } + } + + @Override + public FormatCompliance getFormatCompliance(final ByteSource byteSource) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final Map params = null; + new TiffReader(isStrict(params)).readContents(byteSource, params, + formatCompliance); + return formatCompliance; + } + + public List collectRawImageData(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(isStrict(params)) + .readDirectories(byteSource, true, formatCompliance); + + final List result = new ArrayList(); + for (int i = 0; i < contents.directories.size(); i++) { + final TiffDirectory directory = contents.directories.get(i); + final List dataElements = directory + .getTiffRawImageDataElements(); + for (ImageDataElement element : dataElements) { + final byte[] bytes = byteSource.getBlock(element.offset, + element.length); + result.add(bytes); + } + } + return result; + } + + /** + * Gets a buffered image specified by the byte source. + * The TiffImageParser class features support for a number of options that + * are unique to the TIFF format. These options can be specified by + * supplying the appropriate parameters using the keys from the + * TiffConstants class and the params argument for this method. + *

Loading Partial Images

+ * The TIFF parser includes support for loading partial images without + * committing significantly more memory resources than are necessary + * to store the image. This feature is useful for conserving memory + * in applications that require a relatively small sub image from a + * very large TIFF file. The specifications for partial images are + * as follows: + *
+     *   HashMap params = new HashMap();
+     *   params.put(TiffConstants.PARAM_KEY_SUBIMAGE_X, new Integer(x));
+     *   params.put(TiffConstants.PARAM_KEY_SUBIMAGE_Y, new Integer(y));
+     *   params.put(TiffConstants.PARAM_KEY_SUBIMAGE_WIDTH, new Integer(width));
+     *   params.put(TiffConstants.PARAM_KEY_SUBIMAGE_HEIGHT, new Integer(height));
+     * 
+ * Note that the arguments x, y, width, and height must specify a + * valid rectangular region that is fully contained within the + * source TIFF image. + * @param byteSource A valid instance of ByteSource + * @param params Optional instructions for special-handling or + * interpretation of the input data (null objects are permitted and + * must be supported by implementations). + * @return A valid instance of BufferedImage. + * @throws ImageReadException In the event that the the specified + * content does not conform to the format of the specific parser + * implementation. + * @throws IOException In the event of unsuccessful read or + * access operation. + */ + @Override + public BufferedImage getBufferedImage(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffReader reader = new TiffReader(isStrict(params)); + final TiffContents contents = reader.readFirstDirectory(byteSource, params, + true, formatCompliance); + final ByteOrder byteOrder = reader.getByteOrder(); + final TiffDirectory directory = contents.directories.get(0); + final BufferedImage result = directory.getTiffImage(byteOrder, params); + if (null == result) { + throw new ImageReadException("TIFF does not contain an image."); + } + return result; + } + + @Override + public List getAllBufferedImages(final ByteSource byteSource) + throws ImageReadException, IOException { + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffReader tiffReader = new TiffReader(true); + final TiffContents contents = tiffReader.readDirectories(byteSource, true, + formatCompliance); + final List results = new ArrayList(); + for (int i = 0; i < contents.directories.size(); i++) { + final TiffDirectory directory = contents.directories.get(i); + final BufferedImage result = directory.getTiffImage( + tiffReader.getByteOrder(), null); + if (result != null) { + results.add(result); + } + } + return results; + } + + private Integer getIntegerParameter( + final String key, final Mapparams) + throws ImageReadException + { + if (params == null) { + return null; + } + + if (!params.containsKey(key)) { + return null; + } + + final Object obj = params.get(key); + + if (obj instanceof Integer) { + return (Integer) obj; + } + throw new ImageReadException("Non-Integer parameter " + key); + } + + private Rectangle checkForSubImage( + final Map params) + throws ImageReadException + { + Integer ix0 = getIntegerParameter(TiffConstants.PARAM_KEY_SUBIMAGE_X, params); + Integer iy0 = getIntegerParameter(TiffConstants.PARAM_KEY_SUBIMAGE_Y, params); + Integer iwidth = getIntegerParameter(TiffConstants.PARAM_KEY_SUBIMAGE_WIDTH, params); + Integer iheight = getIntegerParameter(TiffConstants.PARAM_KEY_SUBIMAGE_HEIGHT, params); + + if (ix0 == null && iy0 == null && iwidth == null && iheight == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(32); + if (ix0 == null) { + sb.append(" x0,"); + } + if (iy0 == null) { + sb.append(" y0,"); + } + if (iwidth == null) { + sb.append(" width,"); + } + if (iheight == null) { + sb.append(" height,"); + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + throw new ImageReadException("Incomplete subimage parameters, missing" + sb.toString()); + } + + return new Rectangle(ix0, iy0, iwidth, iheight); + } + + protected BufferedImage getBufferedImage(final TiffDirectory directory, + final ByteOrder byteOrder, final Map params) + throws ImageReadException, IOException + { + final List entries = directory.entries; + + if (entries == null) { + throw new ImageReadException("TIFF missing entries"); + } + + final int photometricInterpretation = 0xffff & directory.getSingleFieldValue( + TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION); + final int compression = 0xffff & directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); + final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH); + final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); + + Rectangle subImage = checkForSubImage(params); + if (subImage != null) { + // Check for valid subimage specification. The following checks + // are consistent with BufferedImage.getSubimage() + if (subImage.width <= 0) { + throw new ImageReadException("negative or zero subimage width"); + } + if (subImage.height <= 0) { + throw new ImageReadException("negative or zero subimage height"); + } + if (subImage.x < 0 || subImage.x >= width) { + throw new ImageReadException("subimage x is outside raster"); + } + if (subImage.x + subImage.width > width) { + throw new ImageReadException("subimage (x+width) is outside raster"); + } + if (subImage.y < 0 || subImage.y >= height) { + throw new ImageReadException("subimage y is outside raster"); + } + if (subImage.y + subImage.height > height) { + throw new ImageReadException("subimage (y+height) is outside raster"); + } + + // if the subimage is just the same thing as the whole + // image, suppress the subimage processing + if (subImage.x == 0 + && subImage.y == 0 + && subImage.width == width + && subImage.height == height) { + subImage = null; + } + } + + + int samplesPerPixel = 1; + final TiffField samplesPerPixelField = directory.findField( + TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); + if (samplesPerPixelField != null) { + samplesPerPixel = samplesPerPixelField.getIntValue(); + } + int[] bitsPerSample = { 1 }; + int bitsPerPixel = samplesPerPixel; + final TiffField bitsPerSampleField = directory.findField( + TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); + if (bitsPerSampleField != null) { + bitsPerSample = bitsPerSampleField.getIntArrayValue(); + bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum(); + } + + // int bitsPerPixel = getTagAsValueOrArraySum(entries, + // TIFF_TAG_BITS_PER_SAMPLE); + + int predictor = -1; + { + // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER); + // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS); + // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS); + // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); + // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); + final TiffField predictorField = directory.findField( + TiffTagConstants.TIFF_TAG_PREDICTOR); + if (null != predictorField) { + predictor = predictorField.getIntValueOrArraySum(); + } + } + + if (samplesPerPixel != bitsPerSample.length) { + throw new ImageReadException("Tiff: samplesPerPixel (" + + samplesPerPixel + ")!=fBitsPerSample.length (" + + bitsPerSample.length + ")"); + } + + + + final PhotometricInterpreter photometricInterpreter = getPhotometricInterpreter( + directory, photometricInterpretation, bitsPerPixel, + bitsPerSample, predictor, samplesPerPixel, width, height); + + final TiffImageData imageData = directory.getTiffImageData(); + + final DataReader dataReader = imageData.getDataReader(directory, + photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, + samplesPerPixel, width, height, compression, byteOrder); + + BufferedImage result = null; + if (subImage != null) { + result = dataReader.readImageData(subImage); + } else { + final boolean hasAlpha = false; + final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha); + + dataReader.readImageData(imageBuilder); + result = imageBuilder.getBufferedImage(); + } + return result; + } + + private PhotometricInterpreter getPhotometricInterpreter( + final TiffDirectory directory, final int photometricInterpretation, + final int bitsPerPixel, final int[] bitsPerSample, final int predictor, + final int samplesPerPixel, final int width, final int height) + throws ImageReadException { + switch (photometricInterpretation) { + case 0: + case 1: + final boolean invert = photometricInterpretation == 0; + + return new PhotometricInterpreterBiLevel(samplesPerPixel, + bitsPerSample, predictor, width, height, invert); + case 3: // Palette + { + final int[] colorMap = directory.findField( + TiffTagConstants.TIFF_TAG_COLOR_MAP, true) + .getIntArrayValue(); + + final int expectedColormapSize = 3 * (1 << bitsPerPixel); + + if (colorMap.length != expectedColormapSize) { + throw new ImageReadException("Tiff: fColorMap.length (" + + colorMap.length + ")!=expectedColormapSize (" + + expectedColormapSize + ")"); + } + + return new PhotometricInterpreterPalette(samplesPerPixel, + bitsPerSample, predictor, width, height, colorMap); + } + case 2: // RGB + return new PhotometricInterpreterRgb(samplesPerPixel, + bitsPerSample, predictor, width, height); + case 5: // CMYK + return new PhotometricInterpreterCmyk(samplesPerPixel, + bitsPerSample, predictor, width, height); + case 6: // + { +// final double yCbCrCoefficients[] = directory.findField( +// TiffTagConstants.TIFF_TAG_YCBCR_COEFFICIENTS, true) +// .getDoubleArrayValue(); +// +// final int yCbCrPositioning[] = directory.findField( +// TiffTagConstants.TIFF_TAG_YCBCR_POSITIONING, true) +// .getIntArrayValue(); +// final int yCbCrSubSampling[] = directory.findField( +// TiffTagConstants.TIFF_TAG_YCBCR_SUB_SAMPLING, true) +// .getIntArrayValue(); +// +// final double referenceBlackWhite[] = directory.findField( +// TiffTagConstants.TIFF_TAG_REFERENCE_BLACK_WHITE, true) +// .getDoubleArrayValue(); + + return new PhotometricInterpreterYCbCr(samplesPerPixel, + bitsPerSample, predictor, width, + height); + } + + case 8: + return new PhotometricInterpreterCieLab(samplesPerPixel, + bitsPerSample, predictor, width, height); + + case 32844: + case 32845: { +// final boolean yonly = (photometricInterpretation == 32844); + return new PhotometricInterpreterLogLuv(samplesPerPixel, + bitsPerSample, predictor, width, height); + } + + default: + throw new ImageReadException( + "TIFF: Unknown fPhotometricInterpretation: " + + photometricInterpretation); + } + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, final Map params) + throws ImageWriteException, IOException { + new TiffImageWriterLossy().writeImage(src, os, params); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffReader.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffReader.java new file mode 100644 index 0000000..b498698 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffReader.java @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.formats.jpeg.JpegConstants; +import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public class TiffReader extends BinaryFileParser { + + private final boolean strict; + + public TiffReader(final boolean strict) { + this.strict = strict; + } + + private TiffHeader readTiffHeader(final ByteSource byteSource) throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final TiffHeader ret = readTiffHeader(is); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private ByteOrder getTiffByteOrder(final int byteOrderByte) throws ImageReadException { + if (byteOrderByte == 'I') { + return ByteOrder.LITTLE_ENDIAN; // Intel + } else if (byteOrderByte == 'M') { + return ByteOrder.BIG_ENDIAN; // Motorola + } else { + throw new ImageReadException("Invalid TIFF byte order " + (0xff & byteOrderByte)); + } + } + + private TiffHeader readTiffHeader(final InputStream is) throws ImageReadException, IOException { + final int byteOrder1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File"); + final int byteOrder2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File"); + if (byteOrder1 != byteOrder2) { + throw new ImageReadException("Byte Order bytes don't match (" + byteOrder1 + ", " + byteOrder2 + ")."); + } + + final ByteOrder byteOrder = getTiffByteOrder(byteOrder1); + setByteOrder(byteOrder); + + final int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File", getByteOrder()); + if (tiffVersion != 42) { + throw new ImageReadException("Unknown Tiff Version: " + tiffVersion); + } + + final long offsetToFirstIFD = + 0xFFFFffffL & read4Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder()); + + skipBytes(is, offsetToFirstIFD - 8, "Not a Valid TIFF File: couldn't find IFDs"); + + if (getDebug()) { + System.out.println(""); + } + + return new TiffHeader(byteOrder, tiffVersion, offsetToFirstIFD); + } + + private void readDirectories(final ByteSource byteSource, + final FormatCompliance formatCompliance, final Listener listener) + throws ImageReadException, IOException { + final TiffHeader tiffHeader = readTiffHeader(byteSource); + if (!listener.setTiffHeader(tiffHeader)) { + return; + } + + final long offset = tiffHeader.offsetToFirstIFD; + final int dirType = TiffDirectoryConstants.DIRECTORY_TYPE_ROOT; + + final List visited = new ArrayList(); + readDirectory(byteSource, offset, dirType, formatCompliance, listener, visited); + } + + private boolean readDirectory(final ByteSource byteSource, final long offset, + final int dirType, final FormatCompliance formatCompliance, final Listener listener, + final List visited) throws ImageReadException, IOException { + final boolean ignoreNextDirectory = false; + return readDirectory(byteSource, offset, dirType, formatCompliance, + listener, ignoreNextDirectory, visited); + } + + private boolean readDirectory(final ByteSource byteSource, final long directoryOffset, + final int dirType, final FormatCompliance formatCompliance, final Listener listener, + final boolean ignoreNextDirectory, final List visited) + throws ImageReadException, IOException { + + if (visited.contains(directoryOffset)) { + return false; + } + visited.add(directoryOffset); + + InputStream is = null; + boolean canThrow = false; + try { + if (directoryOffset >= byteSource.getLength()) { + canThrow = true; + return true; + } + + is = byteSource.getInputStream(); + skipBytes(is, directoryOffset); + + final List fields = new ArrayList(); + + int entryCount; + try { + entryCount = read2Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder()); + } catch (final IOException e) { + if (strict) { + throw e; + } else { + canThrow = true; + return true; + } + } + + for (int i = 0; i < entryCount; i++) { + final int tag = read2Bytes("Tag", is, "Not a Valid TIFF File", getByteOrder()); + final int type = read2Bytes("Type", is, "Not a Valid TIFF File", getByteOrder()); + final long count = 0xFFFFffffL & read4Bytes("Count", is, "Not a Valid TIFF File", getByteOrder()); + final byte[] offsetBytes = readBytes("Offset", is, 4, "Not a Valid TIFF File"); + final long offset = 0xFFFFffffL & ByteConversions.toInt(offsetBytes, getByteOrder()); + + if (tag == 0) { + // skip invalid fields. + // These are seen very rarely, but can have invalid value + // lengths, + // which can cause OOM problems. + continue; + } + + final FieldType fieldType; + try { + fieldType = FieldType.getFieldType(type); + } catch (final ImageReadException imageReadEx) { + // skip over unknown fields types, since we + // can't calculate their size without + // knowing their type + continue; + } + final long valueLength = count * fieldType.getSize(); + final byte[] value; + if (valueLength > TIFF_ENTRY_MAX_VALUE_LENGTH) { + if ((offset < 0) || (offset + valueLength) > byteSource.getLength()) { + if (strict) { + throw new IOException( + "Attempt to read byte range starting from " + offset + " " + + "of length " + valueLength + " " + + "which is outside the file's size of " + + byteSource.getLength()); + } else { + // corrupt field, ignore it + continue; + } + } + value = byteSource.getBlock(offset, (int) valueLength); + } else { + value = offsetBytes; + } + + final TiffField field = new TiffField(tag, dirType, fieldType, count, + offset, value, getByteOrder(), i); + + fields.add(field); + + if (!listener.addField(field)) { + canThrow = true; + return true; + } + } + + final long nextDirectoryOffset = 0xFFFFffffL & read4Bytes("nextDirectoryOffset", is, + "Not a Valid TIFF File", getByteOrder()); + + final TiffDirectory directory = new TiffDirectory(dirType, fields, + directoryOffset, nextDirectoryOffset); + + if (listener.readImageData()) { + if (directory.hasTiffImageData()) { + final TiffImageData rawImageData = getTiffRawImageData( + byteSource, directory); + directory.setTiffImageData(rawImageData); + } + if (directory.hasJpegImageData()) { + final JpegImageData rawJpegImageData = getJpegRawImageData( + byteSource, directory); + directory.setJpegImageData(rawJpegImageData); + } + } + + if (!listener.addDirectory(directory)) { + canThrow = true; + return true; + } + + if (listener.readOffsetDirectories()) { + final TagInfoLong[] offsetFields = { + EXIF_TAG_EXIF_OFFSET, + EXIF_TAG_GPSINFO, + EXIF_TAG_INTEROP_OFFSET + }; + final int[] directoryTypes = { + TiffDirectoryConstants.DIRECTORY_TYPE_EXIF, + TiffDirectoryConstants.DIRECTORY_TYPE_GPS, + TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY + }; + for (int i = 0; i < offsetFields.length; i++) { + final TagInfoLong offsetField = offsetFields[i]; + final TiffField field = directory.findField(offsetField); + if (field != null) { + long subDirectoryOffset; + int subDirectoryType; + boolean subDirectoryRead = false; + try { + subDirectoryOffset = directory.getSingleFieldValue(offsetField); + subDirectoryType = directoryTypes[i]; + subDirectoryRead = readDirectory(byteSource, + subDirectoryOffset, subDirectoryType, + formatCompliance, listener, true, visited); + + } catch (final ImageReadException imageReadException) { + if (strict) { + throw imageReadException; + } + } + if (!subDirectoryRead) { + fields.remove(field); + } + } + } + } + + if (!ignoreNextDirectory && directory.nextDirectoryOffset > 0) { + // Debug.debug("next dir", directory.nextDirectoryOffset ); + readDirectory(byteSource, directory.nextDirectoryOffset, + dirType + 1, formatCompliance, listener, visited); + } + + canThrow = true; + return true; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + public interface Listener { + boolean setTiffHeader(TiffHeader tiffHeader); + + boolean addDirectory(TiffDirectory directory); + + boolean addField(TiffField field); + + boolean readImageData(); + + boolean readOffsetDirectories(); + } + + private static class Collector implements Listener { + private TiffHeader tiffHeader; + private final List directories = new ArrayList(); + private final List fields = new ArrayList(); + private final boolean readThumbnails; + + public Collector() { + this(null); + } + + public Collector(final Map params) { + boolean tmpReadThumbnails = true; + if (params != null && params.containsKey(PARAM_KEY_READ_THUMBNAILS)) { + tmpReadThumbnails = Boolean.TRUE.equals(params + .get(PARAM_KEY_READ_THUMBNAILS)); + } + this.readThumbnails = tmpReadThumbnails; + } + + public boolean setTiffHeader(final TiffHeader tiffHeader) { + this.tiffHeader = tiffHeader; + return true; + } + + public boolean addDirectory(final TiffDirectory directory) { + directories.add(directory); + return true; + } + + public boolean addField(final TiffField field) { + fields.add(field); + return true; + } + + public boolean readImageData() { + return readThumbnails; + } + + public boolean readOffsetDirectories() { + return true; + } + + public TiffContents getContents() { + return new TiffContents(tiffHeader, directories); + } + } + + private static class FirstDirectoryCollector extends Collector { + private final boolean readImageData; + + public FirstDirectoryCollector(final boolean readImageData) { + this.readImageData = readImageData; + } + + @Override + public boolean addDirectory(final TiffDirectory directory) { + super.addDirectory(directory); + return false; + } + + @Override + public boolean readImageData() { + return readImageData; + } + } + +// NOT USED +// private static class DirectoryCollector extends Collector { +// private final boolean readImageData; +// +// public DirectoryCollector(final boolean readImageData) { +// this.readImageData = readImageData; +// } +// +// @Override +// public boolean addDirectory(final TiffDirectory directory) { +// super.addDirectory(directory); +// return false; +// } +// +// @Override +// public boolean readImageData() { +// return readImageData; +// } +// } + + public TiffContents readFirstDirectory(final ByteSource byteSource, final Map params, + final boolean readImageData, final FormatCompliance formatCompliance) + throws ImageReadException, IOException { + final Collector collector = new FirstDirectoryCollector(readImageData); + read(byteSource, params, formatCompliance, collector); + final TiffContents contents = collector.getContents(); + if (contents.directories.size() < 1) { + throw new ImageReadException( + "Image did not contain any directories."); + } + return contents; + } + + public TiffContents readDirectories(final ByteSource byteSource, + final boolean readImageData, final FormatCompliance formatCompliance) + throws ImageReadException, IOException { + final Collector collector = new Collector(null); + readDirectories(byteSource, formatCompliance, collector); + final TiffContents contents = collector.getContents(); + if (contents.directories.size() < 1) { + throw new ImageReadException( + "Image did not contain any directories."); + } + return contents; + } + + public TiffContents readContents(final ByteSource byteSource, final Map params, + final FormatCompliance formatCompliance) throws ImageReadException, + IOException { + + final Collector collector = new Collector(params); + read(byteSource, params, formatCompliance, collector); + return collector.getContents(); + } + + public void read(final ByteSource byteSource, final Map params, + final FormatCompliance formatCompliance, final Listener listener) + throws ImageReadException, IOException { + // TiffContents contents = + readDirectories(byteSource, formatCompliance, listener); + } + + private TiffImageData getTiffRawImageData(final ByteSource byteSource, + final TiffDirectory directory) throws ImageReadException, IOException { + + final List elements = directory + .getTiffRawImageDataElements(); + final TiffImageData.Data[] data = new TiffImageData.Data[elements.size()]; + + if (byteSource instanceof ByteSourceFile) { + final ByteSourceFile bsf = (ByteSourceFile) byteSource; + for (int i = 0; i < elements.size(); i++) { + final TiffDirectory.ImageDataElement element = elements.get(i); + data[i] = new TiffImageData.ByteSourceData(element.offset, + element.length, bsf); + } + } else { + for (int i = 0; i < elements.size(); i++) { + final TiffDirectory.ImageDataElement element = elements.get(i); + final byte[] bytes = byteSource.getBlock(element.offset, element.length); + data[i] = new TiffImageData.Data(element.offset, element.length, bytes); + } + } + + if (directory.imageDataInStrips()) { + final TiffField rowsPerStripField = directory + .findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP); + /* + * Default value of rowsperstrip is assumed to be infinity + * http://www.awaresystems.be/imaging/tiff/tifftags/rowsperstrip.html + */ + int rowsPerStrip = Integer.MAX_VALUE; + + if (null != rowsPerStripField) { + rowsPerStrip = rowsPerStripField.getIntValue(); + } else { + final TiffField imageHeight = directory + .findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); + /** + * if rows per strip not present then rowsPerStrip is equal to + * imageLength or an infinity value; + */ + if (imageHeight != null) { + rowsPerStrip = imageHeight.getIntValue(); + } + + } + + return new TiffImageData.Strips(data, rowsPerStrip); + } else { + final TiffField tileWidthField = directory + .findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH); + if (null == tileWidthField) { + throw new ImageReadException("Can't find tile width field."); + } + final int tileWidth = tileWidthField.getIntValue(); + + final TiffField tileLengthField = directory + .findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH); + if (null == tileLengthField) { + throw new ImageReadException("Can't find tile length field."); + } + final int tileLength = tileLengthField.getIntValue(); + + return new TiffImageData.Tiles(data, tileWidth, tileLength); + } + } + + private JpegImageData getJpegRawImageData(final ByteSource byteSource, + final TiffDirectory directory) throws ImageReadException, IOException { + final ImageDataElement element = directory.getJpegRawImageDataElement(); + final long offset = element.offset; + int length = element.length; + // In case the length is not correct, adjust it and check if the last read byte actually is the end of the image + if (offset + length > byteSource.getLength()) { + length = (int) (byteSource.getLength() - offset); + } + final byte[] data = byteSource.getBlock(offset, length); + // check if the last read byte is actually the end of the image data + if (strict && + (length < 2 || + (((data[data.length - 2] & 0xff) << 8) | (data[data.length - 1] & 0xff)) != JpegConstants.EOI_MARKER)) { + throw new ImageReadException("JPEG EOI marker could not be found at expected location"); + } + return new JpegImageData(offset, length, data); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePageMaker6TagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePageMaker6TagConstants.java new file mode 100644 index 0000000..ae34997 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePageMaker6TagConstants.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLongOrIFD; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; + +/** + * TIFF specification supplement 1 + *
+ * Enhancements for Adobe PageMaker(R) 6.0 software + *
+ * http://partners.adobe.com/public/developer/en/tiff/TIFFPM6.pdf + */ +public interface AdobePageMaker6TagConstants { + TagInfoLongOrIFD TIFF_TAG_SUB_IFD = new TagInfoLongOrIFD( + "SubIFDs", 0x014a, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN, true); + + TagInfoByte TIFF_TAG_CLIP_PATH = new TagInfoByte( + "ClipPath", 0x0157, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong TIFF_TAG_XCLIP_PATH_UNITS = new TagInfoLong( + "XClipPathUnits", 0x0158, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong TIFF_TAG_YCLIP_PATH_UNITS = new TagInfoLong( + "YClipPathUnits", 0x0159, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort TIFF_TAG_INDEXED = new TagInfoShort( + "Indexed", 0x015a, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int INDEXED_VALUE_NOT_INDEXED = 0; + int INDEXED_VALUE_INDEXED = 1; + + TagInfoShort TIFF_TAG_OPIPROXY = new TagInfoShort( + "OPIProxy", 0x015f, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int OPIPROXY_VALUE_HIGHER_RESOLUTION_IMAGE_DOES_NOT_EXIST = 0; + int OPIPROXY_VALUE_HIGHER_RESOLUTION_IMAGE_EXISTS = 1; + + TagInfoAscii TIFF_TAG_IMAGE_ID = new TagInfoAscii( + "ImageID", 0x800d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_ADOBE_PAGEMAKER_6_TAGS = + Collections.unmodifiableList(Arrays.asList( + TIFF_TAG_SUB_IFD, + TIFF_TAG_CLIP_PATH, + TIFF_TAG_XCLIP_PATH_UNITS, + TIFF_TAG_YCLIP_PATH_UNITS, + TIFF_TAG_INDEXED, + TIFF_TAG_OPIPROXY, + TIFF_TAG_IMAGE_ID)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePhotoshopTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePhotoshopTagConstants.java new file mode 100644 index 0000000..069a273 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AdobePhotoshopTagConstants.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; + +/** + * TIFF specification supplement 2 + *
+ * Adobe Photoshop (R) TIFF Technical Notes + *
+ * http://partners.adobe.com/public/developer/en/tiff/TIFFphotoshop.pdf + */ +public interface AdobePhotoshopTagConstants { + TagInfoUndefined EXIF_TAG_JPEGTABLES = new TagInfoUndefined( + "JPEGTables", 0x015b, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_IMAGE_SOURCE_DATA = new TagInfoUndefined( + "ImageSourceData", 0x935c, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + List ALL_ADOBE_PHOTOSHOP_TAGS = + Collections.unmodifiableList(Arrays.asList(new TagInfo[] { + EXIF_TAG_JPEGTABLES, + EXIF_TAG_IMAGE_SOURCE_DATA + })); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AliasSketchbookProTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AliasSketchbookProTagConstants.java new file mode 100644 index 0000000..e57fa3a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AliasSketchbookProTagConstants.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; + +/** + * Alias Sketchbook Pro multi-layer TIFF + *
+ * http://www.awaresystems.be/imaging/tiff/tifftags/docs/alias.html + */ +public interface AliasSketchbookProTagConstants { + TagInfoAscii EXIF_TAG_ALIAS_LAYER_METADATA = new TagInfoAscii( + "Alias Layer Metadata", 0xc660, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_ALIAS_SKETCHBOOK_PRO_TAGS = + Collections.unmodifiableList(Arrays.asList(new TagInfo[] { + EXIF_TAG_ALIAS_LAYER_METADATA})); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AllTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AllTagConstants.java new file mode 100644 index 0000000..b7adb91 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/AllTagConstants.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; + +public interface AllTagConstants extends + AdobePhotoshopTagConstants, + AdobePageMaker6TagConstants, + AliasSketchbookProTagConstants, + DcfTagConstants, + DngTagConstants, + ExifTagConstants, + GeoTiffTagConstants, + GdalLibraryTagConstants, + GpsTagConstants, + HylaFaxTagConstants, + MicrosoftTagConstants, + MicrosoftHdPhotoTagConstants, + MolecularDynamicsGelTagConstants, + OceScanjobTagConstants, + Rfc2301TagConstants, + Tiff4TagConstants, + TiffEpTagConstants, + TiffTagConstants, + WangTagConstants { + + List ALL_TAGS = Collections.unmodifiableList(TagConstantsUtils.mergeTagLists( + AdobePageMaker6TagConstants.ALL_ADOBE_PAGEMAKER_6_TAGS, + AdobePhotoshopTagConstants.ALL_ADOBE_PHOTOSHOP_TAGS, + AliasSketchbookProTagConstants.ALL_ALIAS_SKETCHBOOK_PRO_TAGS, + DcfTagConstants.ALL_DCF_TAGS, + DngTagConstants.ALL_DNG_TAGS, + ExifTagConstants.ALL_EXIF_TAGS, + GeoTiffTagConstants.ALL_GEO_TIFF_TAGS, + GdalLibraryTagConstants.ALL_GDAL_LIBRARY_TAGS, + GpsTagConstants.ALL_GPS_TAGS, + HylaFaxTagConstants.ALL_HYLAFAX_TAGS, + MicrosoftTagConstants.ALL_MICROSOFT_TAGS, + MicrosoftHdPhotoTagConstants.ALL_MICROSOFT_HD_PHOTO_TAGS, + MolecularDynamicsGelTagConstants.ALL_MOLECULAR_DYNAMICS_GEL_TAGS, + OceScanjobTagConstants.ALL_OCE_SCANJOB_TAGS, + Rfc2301TagConstants.ALL_RFC_2301_TAGS, + Tiff4TagConstants.ALL_TIFF_4_TAGS, + TiffEpTagConstants.ALL_TIFF_EP_TAGS, + TiffTagConstants.ALL_TIFF_TAGS, + WangTagConstants.ALL_WANG_TAGS)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DcfTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DcfTagConstants.java new file mode 100644 index 0000000..40a4762 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DcfTagConstants.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; + +/** + * Design rule for Camera Filesystem + *
+ * http://www.exif.org/dcf.PDF + */ +public interface DcfTagConstants { + TagInfoAscii EXIF_TAG_RELATED_IMAGE_FILE_FORMAT = new TagInfoAscii( + "RelatedImageFileFormat", 0x1000, -1, + TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD); + + TagInfoShortOrLong EXIF_TAG_RELATED_IMAGE_WIDTH = new TagInfoShortOrLong( + "RelatedImageWidth", 0x1001, 1, + TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD); + + TagInfoShortOrLong EXIF_TAG_RELATED_IMAGE_LENGTH = new TagInfoShortOrLong( + "RelatedImageLength", 0x1002, 1, + TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD); + + TagInfoShort EXIF_TAG_COLOR_SPACE = new TagInfoShort( + "ColorSpace", 0xa001, 1, TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int COLOR_SPACE_VALUE_SRGB = 1; + int COLOR_SPACE_VALUE_ADOBE_RGB = 2; + int COLOR_SPACE_VALUE_UNCALIBRATED = 65535; + + List ALL_DCF_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_RELATED_IMAGE_FILE_FORMAT, + EXIF_TAG_RELATED_IMAGE_WIDTH, + EXIF_TAG_RELATED_IMAGE_LENGTH, + EXIF_TAG_COLOR_SPACE)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DngTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DngTagConstants.java new file mode 100644 index 0000000..822ff59 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/DngTagConstants.java @@ -0,0 +1,503 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAsciiOrByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLongOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; + +/** + * Digital Negative (DNG) Specification + *
+ * http://www.adobe.com/products/dng/pdfs/dng_spec_1_3_0_0.pdf + */ +public interface DngTagConstants { + TagInfoByte EXIF_TAG_DNG_VERSION = new TagInfoByte( + "DNGVersion", 0xc612, 4, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoByte EXIF_TAG_DNG_BACKWARD_VERSION = new TagInfoByte( + "DNGBackwardVersion", 0xc613, 4, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAscii EXIF_TAG_UNIQUE_CAMERA_MODEL = new TagInfoAscii( + "UniqueCameraModel", 0xc614, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAsciiOrByte EXIF_TAG_LOCALIZED_CAMERA_MODEL = new TagInfoAsciiOrByte( + "LocalizedCameraModel", 0xc615, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoByte EXIF_TAG_CFAPLANE_COLOR = new TagInfoByte( + "CFAPlaneColor", 0xc616, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_CFALAYOUT = new TagInfoShort( + "CFALayout", 0xc617, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int CFALAYOUT_VALUE_RECTANGULAR = 1; + int CFALAYOUT_VALUE_EVEN_COLUMNS_OFFSET_DOWN_1_2_ROW = 2; + int CFALAYOUT_VALUE_EVEN_COLUMNS_OFFSET_UP_1_2_ROW = 3; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_RIGHT_1_2_COLUMN = 4; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_LEFT_1_2_COLUMN = 5; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_UP_1_2_ROW_EVEN_COLUMNS_OFFSET_LEFT_1_2_COLUMN = 6; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_UP_1_2_ROW_EVEN_COLUMNS_OFFSET_RIGHT_1_2_COLUMN = 7; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_DOWN_1_2_ROW_EVEN_COLUMNS_OFFSET_LEFT_1_2_COLUMN = 8; + int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_DOWN_1_2_ROW_EVEN_COLUMNS_OFFSET_RIGHT_1_2_COLUMN = 9; + + TagInfoShort EXIF_TAG_LINEARIZATION_TABLE = new TagInfoShort( + "LinearizationTable", 0xc618, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_BLACK_LEVEL_REPEAT_DIM = new TagInfoShort( + "BlackLevelRepeatDim", 0xc619, + 2, TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLongOrRational EXIF_TAG_BLACK_LEVEL = new TagInfoShortOrLongOrRational( + "BlackLevel", 0xc61a, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_BLACK_LEVEL_DELTA_H = new TagInfoSRational( + "BlackLevelDeltaH", 0xc61b, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_BLACK_LEVEL_DELTA_V = new TagInfoSRational( + "BlackLevelDeltaV", 0xc61c, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLong EXIF_TAG_WHITE_LEVEL = new TagInfoShortOrLong( + "WhiteLevel", 0xc61d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_DEFAULT_SCALE = new TagInfoRational( + "DefaultScale", 0xc61e, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLongOrRational EXIF_TAG_DEFAULT_CROP_ORIGIN = new TagInfoShortOrLongOrRational( + "DefaultCropOrigin", 0xc61f, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLongOrRational EXIF_TAG_DEFAULT_CROP_SIZE = new TagInfoShortOrLongOrRational( + "DefaultCropSize", 0xc620, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_COLOR_MATRIX_1 = new TagInfoSRational( + "ColorMatrix1", 0xc621, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_COLOR_MATRIX_2 = new TagInfoSRational( + "ColorMatrix2", 0xc622, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_CAMERA_CALIBRATION_1 = new TagInfoSRational( + "CameraCalibration1", 0xc623, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_CAMERA_CALIBRATION_2 = new TagInfoSRational( + "CameraCalibration2", 0xc624, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_REDUCTION_MATRIX_1 = new TagInfoSRational( + "ReductionMatrix1", 0xc625, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_REDUCTION_MATRIX_2 = new TagInfoSRational( + "ReductionMatrix2", 0xc626, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_ANALOG_BALANCE = new TagInfoRational( + "AnalogBalance", 0xc627, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShortOrRational EXIF_TAG_AS_SHOT_NEUTRAL = new TagInfoShortOrRational( + "AsShotNeutral", 0xc628, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_AS_SHOT_WHITE_XY = new TagInfoRational( + "AsShotWhiteXY", 0xc629, 2, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoSRational EXIF_TAG_BASELINE_EXPOSURE = new TagInfoSRational( + "BaselineExposure", 0xc62a, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_BASELINE_NOISE = new TagInfoRational( + "BaselineNoise", 0xc62b, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_BASELINE_SHARPNESS = new TagInfoRational( + "BaselineSharpness", 0xc62c, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoLong EXIF_TAG_BAYER_GREEN_SPLIT = new TagInfoLong( + "BayerGreenSplit", 0xc62d, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_LINEAR_RESPONSE_LIMIT = new TagInfoRational( + "LinearResponseLimit", 0xc62e, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAscii EXIF_TAG_CAMERA_SERIAL_NUMBER = new TagInfoAscii( + "CameraSerialNumber", 0xc62f, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_DNG_LENS_INFO = new TagInfoRational( + "DNGLensInfo", 0xc630, 4, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_CHROMA_BLUR_RADIUS = new TagInfoRational( + "ChromaBlurRadius", 0xc631, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_ANTI_ALIAS_STRENGTH = new TagInfoRational( + "AntiAliasStrength", 0xc632, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_SHADOW_SCALE = new TagInfoRational( + "ShadowScale", 0xc633, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoByte EXIF_TAG_DNG_PRIVATE_DATA = new TagInfoByte( + "DNGPrivateData", 0xc634, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShort EXIF_TAG_MAKER_NOTE_SAFETY = new TagInfoShort( + "MakerNoteSafety", 0xc635, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + int MAKER_NOTE_SAFETY_VALUE_UNSAFE = 0; + int MAKER_NOTE_SAFETY_VALUE_SAFE = 1; + + TagInfoShort EXIF_TAG_CALIBRATION_ILLUMINANT_1 = new TagInfoShort( + "CalibrationIlluminant1", 0xc65a, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + int CALIBRATION_ILLUMINANT_1_VALUE_DAYLIGHT = 1; + int CALIBRATION_ILLUMINANT_1_VALUE_FLUORESCENT = 2; + int CALIBRATION_ILLUMINANT_1_VALUE_TUNGSTEN = 3; + int CALIBRATION_ILLUMINANT_1_VALUE_FLASH = 4; + int CALIBRATION_ILLUMINANT_1_VALUE_FINE_WEATHER = 9; + int CALIBRATION_ILLUMINANT_1_VALUE_CLOUDY = 10; + int CALIBRATION_ILLUMINANT_1_VALUE_SHADE = 11; + int CALIBRATION_ILLUMINANT_1_VALUE_DAYLIGHT_FLUORESCENT = 12; + int CALIBRATION_ILLUMINANT_1_VALUE_DAY_WHITE_FLUORESCENT = 13; + int CALIBRATION_ILLUMINANT_1_VALUE_COOL_WHITE_FLUORESCENT = 14; + int CALIBRATION_ILLUMINANT_1_VALUE_WHITE_FLUORESCENT = 15; + int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_A = 17; + int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_B = 18; + int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_C = 19; + int CALIBRATION_ILLUMINANT_1_VALUE_D55 = 20; + int CALIBRATION_ILLUMINANT_1_VALUE_D65 = 21; + int CALIBRATION_ILLUMINANT_1_VALUE_D75 = 22; + int CALIBRATION_ILLUMINANT_1_VALUE_D50 = 23; + int CALIBRATION_ILLUMINANT_1_VALUE_ISO_STUDIO_TUNGSTEN = 24; + int CALIBRATION_ILLUMINANT_1_VALUE_OTHER = 255; + + TagInfoShort EXIF_TAG_CALIBRATION_ILLUMINANT_2 = new TagInfoShort( + "CalibrationIlluminant2", 0xc65b, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + int CALIBRATION_ILLUMINANT_2_VALUE_DAYLIGHT = 1; + int CALIBRATION_ILLUMINANT_2_VALUE_FLUORESCENT = 2; + int CALIBRATION_ILLUMINANT_2_VALUE_TUNGSTEN = 3; + int CALIBRATION_ILLUMINANT_2_VALUE_FLASH = 4; + int CALIBRATION_ILLUMINANT_2_VALUE_FINE_WEATHER = 9; + int CALIBRATION_ILLUMINANT_2_VALUE_CLOUDY = 10; + int CALIBRATION_ILLUMINANT_2_VALUE_SHADE = 11; + int CALIBRATION_ILLUMINANT_2_VALUE_DAYLIGHT_FLUORESCENT = 12; + int CALIBRATION_ILLUMINANT_2_VALUE_DAY_WHITE_FLUORESCENT = 13; + int CALIBRATION_ILLUMINANT_2_VALUE_COOL_WHITE_FLUORESCENT = 14; + int CALIBRATION_ILLUMINANT_2_VALUE_WHITE_FLUORESCENT = 15; + int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_A = 17; + int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_B = 18; + int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_C = 19; + int CALIBRATION_ILLUMINANT_2_VALUE_D55 = 20; + int CALIBRATION_ILLUMINANT_2_VALUE_D65 = 21; + int CALIBRATION_ILLUMINANT_2_VALUE_D75 = 22; + int CALIBRATION_ILLUMINANT_2_VALUE_D50 = 23; + int CALIBRATION_ILLUMINANT_2_VALUE_ISO_STUDIO_TUNGSTEN = 24; + int CALIBRATION_ILLUMINANT_2_VALUE_OTHER = 255; + + TagInfoRational EXIF_TAG_BEST_QUALITY_SCALE = new TagInfoRational( + "BestQualityScale", 0xc65c, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_RAW_DATA_UNIQUE_ID = new TagInfoByte( + "RawDataUniqueID", 0xc65d, 16, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAsciiOrByte EXIF_TAG_ORIGINAL_RAW_FILE_NAME = new TagInfoAsciiOrByte( + "OriginalRawFileName", 0xc68b, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoUndefined EXIF_TAG_ORIGINAL_RAW_FILE_DATA = new TagInfoUndefined( + "OriginalRawFileData", 0xc68c, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShortOrLong EXIF_TAG_ACTIVE_AREA = new TagInfoShortOrLong( + "ActiveArea", 0xc68d, 4, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLong EXIF_TAG_MASKED_AREAS = new TagInfoShortOrLong( + "MaskedAreas", 0xc68e, 4, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_AS_SHOT_ICCPROFILE = new TagInfoUndefined( + "AsShotICCProfile", 0xc68f, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_AS_SHOT_PRE_PROFILE_MATRIX = new TagInfoSRational( + "AsShotPreProfileMatrix", 0xc690, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoUndefined EXIF_TAG_CURRENT_ICCPROFILE = new TagInfoUndefined( + "CurrentICCProfile", 0xc691, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoSRational EXIF_TAG_CURRENT_PRE_PROFILE_MATRIX = new TagInfoSRational( + "CurrentPreProfileMatrix", 0xc692, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShort EXIF_TAG_COLORIMETRIC_REFERENCE = new TagInfoShort( + "ColorimetricReference", 0xc6bf, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + short COLORIMETRIC_REFERENCE_VALUE_SCENE_REFERRED = 0; + short COLORIMETRIC_REFERENCE_VALUE_OUTPUT_REFERRED = 1; + + TagInfoAsciiOrByte EXIF_TAG_CAMERA_CALIBRATION_SIGNATURE = new TagInfoAsciiOrByte( + "CameraCalibrationSignature", 0xc6f3, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAsciiOrByte EXIF_TAG_PROFILE_CALIBRATION_SIGNATURE = new TagInfoAsciiOrByte( + "ProfileCalibrationSignature", 0xc6f4, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoLong EXIF_TAG_EXTRA_CAMERA_PROFILES = new TagInfoLong( + "ExtraCameraProfiles", 0xc6f5, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoAsciiOrByte EXIF_TAG_AS_SHOT_PROFILE_NAME = new TagInfoAsciiOrByte( + "AsShotProfileName", 0xc6f6, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoRational EXIF_TAG_NOISE_REDUCTION_APPLIED = new TagInfoRational( + "NoiseReductionApplied", 0xc6f7, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_PROFILE_NAME = new TagInfoAscii( + "ProfileName", 0xc6f8, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_PROFILE_HUE_SAT_MAP_DIMS = new TagInfoLong( + "ProfileHueSatMapDims", 0xc6f9, 3, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_PROFILE_HUE_SAT_MAP_DATA1 = new TagInfoFloat( + "ProfileHueSatMapData1", 0xc6fa, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_PROFILE_HUE_SAT_MAP_DATA2 = new TagInfoFloat( + "ProfileHueSatMapData2", 0xc6fb, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_PROFILE_TONE_CURVE = new TagInfoFloat( + "ProfileToneCurve", 0xc6fc, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_PROFILE_EMBED_POLICY = new TagInfoLong( + "ProfileEmbedPolicy", 0xc6fd, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int PROFILE_EMBED_POLICY_VALUE_ALLOW_COPYING = 0; + int PROFILE_EMBED_POLICY_VALUE_EMBED_IF_USED = 1; + int PROFILE_EMBED_POLICY_VALUE_EMBED_NEVER = 2; + int PROFILE_EMBED_POLICY_VALUE_NO_RESTRICTIONS = 3; + + TagInfoAsciiOrByte EXIF_TAG_PROFILE_COPYRIGHT = new TagInfoAsciiOrByte( + "ProfileCopyright", 0xc6fe, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_FORWARD_MATRIX1 = new TagInfoSRational( + "ForwardMatrix1", 0xc714, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSRational EXIF_TAG_FORWARD_MATRIX2 = new TagInfoSRational( + "ForwardMatrix2", 0xc715, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAsciiOrByte EXIF_TAG_PREVIEW_APPLICATION_NAME = new TagInfoAsciiOrByte( + "PreviewApplicationName", 0xc716, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAsciiOrByte EXIF_TAG_PREVIEW_APPLICATION_VERSION = new TagInfoAsciiOrByte( + "PreviewApplicationVersion", 0xc717, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAsciiOrByte EXIF_TAG_PREVIEW_SETTINGS_NAME = new TagInfoAsciiOrByte( + "PreviewSettingsName", 0xc718, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_PREVIEW_SETTINGS_DIGEST = new TagInfoByte( + "PreviewSettingsDigest", 0xc719, 16, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_PREVIEW_COLORSPACE = new TagInfoLong( + "PreviewColorspace", 0xc71a, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int PREVIEW_COLORSPACE_VALUE_UNKNOWN = 0; + int PREVIEW_COLORSPACE_VALUE_GRAY_GAMMA_2_2 = 1; + int PREVIEW_COLORSPACE_VALUE_sRGB = 2; + int PREVIEW_COLORSPACE_VALUE_ADOBE_RGB = 3; + int PREVIEW_COLORSPACE_VALUE_PROPHOTO_RGB = 4; + + TagInfoAscii EXIF_TAG_PREVIEW_DATE_TIME = new TagInfoAscii( + "PreviewDateTime", 0xc71b, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_RAW_IMAGE_DIGEST = new TagInfoByte( + "RawImageDigest", 0xc71c, 16, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoByte EXIF_TAG_ORIGINAL_RAW_FILE_DIGEST = new TagInfoByte( + "OriginalRawFileDigest", 0xc71d, 16, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShortOrLong EXIF_TAG_SUB_TILE_BLOCK_SIZE = new TagInfoShortOrLong( + "SubTileBlockSize", 0xc71e, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLong EXIF_TAG_ROW_INTERLEAVE_FACTOR = new TagInfoShortOrLong( + "RowInterleaveFactor", 0xc71f, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_PROFILE_LOOK_TABLE_DIMS = new TagInfoLong( + "ProfileLookTableDims", 0xc725, 3, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_PROFILE_LOOK_TABLE_DATA = new TagInfoFloat( + "ProfileLookTableData", 0xc726, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_OPCODE_LIST_1 = new TagInfoUndefined( + "OpcodeList1", 0xc740, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_OPCODE_LIST_2 = new TagInfoUndefined( + "OpcodeList2", 0xc741, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_OPCODE_LIST_3 = new TagInfoUndefined( + "OpcodeList3", 0xc74E, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDouble EXIF_TAG_NOISE_PROFILE = new TagInfoDouble( + "NoiseProfile", 0xc761, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_DNG_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_DNG_VERSION, + EXIF_TAG_DNG_BACKWARD_VERSION, + EXIF_TAG_UNIQUE_CAMERA_MODEL, + EXIF_TAG_LOCALIZED_CAMERA_MODEL, + EXIF_TAG_CFAPLANE_COLOR, + EXIF_TAG_CFALAYOUT, + EXIF_TAG_LINEARIZATION_TABLE, + EXIF_TAG_BLACK_LEVEL_REPEAT_DIM, + EXIF_TAG_BLACK_LEVEL, + EXIF_TAG_BLACK_LEVEL_DELTA_H, + EXIF_TAG_BLACK_LEVEL_DELTA_V, + EXIF_TAG_WHITE_LEVEL, + EXIF_TAG_DEFAULT_SCALE, + EXIF_TAG_DEFAULT_CROP_ORIGIN, + EXIF_TAG_DEFAULT_CROP_SIZE, + EXIF_TAG_COLOR_MATRIX_1, + EXIF_TAG_COLOR_MATRIX_2, + EXIF_TAG_CAMERA_CALIBRATION_1, + EXIF_TAG_CAMERA_CALIBRATION_2, + EXIF_TAG_REDUCTION_MATRIX_1, + EXIF_TAG_REDUCTION_MATRIX_2, + EXIF_TAG_ANALOG_BALANCE, + EXIF_TAG_AS_SHOT_NEUTRAL, + EXIF_TAG_AS_SHOT_WHITE_XY, + EXIF_TAG_BASELINE_EXPOSURE, + EXIF_TAG_BASELINE_NOISE, + EXIF_TAG_BASELINE_SHARPNESS, + EXIF_TAG_BAYER_GREEN_SPLIT, + EXIF_TAG_LINEAR_RESPONSE_LIMIT, + EXIF_TAG_CAMERA_SERIAL_NUMBER, + EXIF_TAG_DNG_LENS_INFO, + EXIF_TAG_CHROMA_BLUR_RADIUS, + EXIF_TAG_ANTI_ALIAS_STRENGTH, + EXIF_TAG_SHADOW_SCALE, + EXIF_TAG_DNG_PRIVATE_DATA, + EXIF_TAG_MAKER_NOTE_SAFETY, + EXIF_TAG_CALIBRATION_ILLUMINANT_1, + EXIF_TAG_CALIBRATION_ILLUMINANT_2, + EXIF_TAG_BEST_QUALITY_SCALE, + EXIF_TAG_RAW_DATA_UNIQUE_ID, + EXIF_TAG_ORIGINAL_RAW_FILE_NAME, + EXIF_TAG_ORIGINAL_RAW_FILE_DATA, + EXIF_TAG_ACTIVE_AREA, + EXIF_TAG_MASKED_AREAS, + EXIF_TAG_AS_SHOT_ICCPROFILE, + EXIF_TAG_AS_SHOT_PRE_PROFILE_MATRIX, + EXIF_TAG_CURRENT_ICCPROFILE, + EXIF_TAG_CURRENT_PRE_PROFILE_MATRIX, + EXIF_TAG_COLORIMETRIC_REFERENCE, + EXIF_TAG_CAMERA_CALIBRATION_SIGNATURE, + EXIF_TAG_PROFILE_CALIBRATION_SIGNATURE, + EXIF_TAG_EXTRA_CAMERA_PROFILES, + EXIF_TAG_AS_SHOT_PROFILE_NAME, + EXIF_TAG_NOISE_REDUCTION_APPLIED, + EXIF_TAG_PROFILE_NAME, + EXIF_TAG_PROFILE_HUE_SAT_MAP_DIMS, + EXIF_TAG_PROFILE_HUE_SAT_MAP_DATA1, + EXIF_TAG_PROFILE_HUE_SAT_MAP_DATA2, + EXIF_TAG_PROFILE_TONE_CURVE, + EXIF_TAG_PROFILE_EMBED_POLICY, + EXIF_TAG_PROFILE_COPYRIGHT, + EXIF_TAG_FORWARD_MATRIX1, + EXIF_TAG_FORWARD_MATRIX2, + EXIF_TAG_PREVIEW_APPLICATION_NAME, + EXIF_TAG_PREVIEW_APPLICATION_VERSION, + EXIF_TAG_PREVIEW_SETTINGS_NAME, + EXIF_TAG_PREVIEW_SETTINGS_DIGEST, + EXIF_TAG_PREVIEW_COLORSPACE, + EXIF_TAG_PREVIEW_DATE_TIME, + EXIF_TAG_RAW_IMAGE_DIGEST, + EXIF_TAG_ORIGINAL_RAW_FILE_DIGEST, + EXIF_TAG_SUB_TILE_BLOCK_SIZE, + EXIF_TAG_ROW_INTERLEAVE_FACTOR, + EXIF_TAG_PROFILE_LOOK_TABLE_DIMS, + EXIF_TAG_PROFILE_LOOK_TABLE_DATA, + EXIF_TAG_OPCODE_LIST_1, + EXIF_TAG_OPCODE_LIST_2, + EXIF_TAG_OPCODE_LIST_3, + EXIF_TAG_NOISE_PROFILE)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/ExifTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/ExifTagConstants.java new file mode 100644 index 0000000..556f0ff --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/ExifTagConstants.java @@ -0,0 +1,620 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUnknown; + +/** + * References: + * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html + * http://tiki-lounge.com/~raf/tiff/fields.html + * http://www.awaresystems.be/imaging/tiff/tifftags.html + * + * "Stonits": http://www.anyhere.com/gward/pixformat/tiffluv.html + */ +public interface ExifTagConstants { + TagInfoAscii EXIF_TAG_INTEROPERABILITY_INDEX = new TagInfoAscii( + "InteroperabilityIndex", 0x0001, -1, + TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD); + TagInfoUndefined EXIF_TAG_INTEROPERABILITY_VERSION = new TagInfoUndefined( + "InteroperabilityVersion", 0x0002, 1, + TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD); + TagInfoAscii EXIF_TAG_PROCESSING_SOFTWARE = new TagInfoAscii( + "ProcessingSoftware", 0x000b, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoAscii EXIF_TAG_SOFTWARE = new TagInfoAscii( + "Software", 0x0131, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_START_IFD0 = new TagInfoLong( + "PreviewImageStart", 0x0111, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0, true); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_START_SUB_IFD1 = new TagInfoLong( + "PreviewImageStart", 0x0111, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD1, true); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD2 = new TagInfoLong( + "JpgFromRawStart", 0x0111, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD2, true); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0 = new TagInfoLong( + "PreviewImageLength", 0x0117, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_LENGTH_SUB_IFD1 = new TagInfoLong( + "PreviewImageLength", 0x0117, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD1); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD2 = new TagInfoLong( + "JpgFromRawLength", 0x0117, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD2); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_START_MAKER_NOTES = new TagInfoLong( + "PreviewImageStart", 0x0201, 1, + TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTES); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD = new TagInfoLong( + "JpgFromRawStart", 0x0201, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD, true); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_START_IFD2 = new TagInfoLong( + "JpgFromRawStart", 0x0201, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD2, true); + TagInfoLong EXIF_TAG_OTHER_IMAGE_START = new TagInfoLong( + "OtherImageStart", 0x0201, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN, true); + TagInfoLong EXIF_TAG_PREVIEW_IMAGE_LENGTH_MAKER_NOTES = new TagInfoLong( + "PreviewImageLength", 0x0202, 1, + TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTES); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD = new TagInfoLong( + "JpgFromRawLength", 0x0202, 1, + TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD); + TagInfoLong EXIF_TAG_JPG_FROM_RAW_LENGTH_IFD2 = new TagInfoLong( + "JpgFromRawLength", 0x0202, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD2); + TagInfoLong EXIF_TAG_OTHER_IMAGE_LENGTH = new TagInfoLong( + "OtherImageLength", 0x0202, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_APPLICATION_NOTES = new TagInfoByte( + "ApplicationNotes", 0x02bc, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_MATTEING = new TagInfoUnknown( + "Matteing", 0x80e3, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_DATA_TYPE = new TagInfoUnknown( + "DataType", 0x80e4, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_IMAGE_DEPTH = new TagInfoUnknown( + "ImageDepth", 0x80e5, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_TILE_DEPTH = new TagInfoUnknown( + "TileDepth", 0x80e6, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_MODEL_2 = new TagInfoUnknown( + "Model2", 0x827d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoRational EXIF_TAG_EXPOSURE_TIME = new TagInfoRational( + "ExposureTime", 0x829a, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_FNUMBER = new TagInfoRational( + "FNumber", 0x829d, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + // FIXME: other types? + TagInfoLong EXIF_TAG_IPTC_NAA = new TagInfoLong( + "IPTC-NAA", 0x83bb, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoShort EXIF_TAG_INTERGRAPH_PACKET_DATA = new TagInfoShort( + "IntergraphPacketData", 0x847e, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoLong EXIF_TAG_INTERGRAPH_FLAG_REGISTERS = new TagInfoLong( + "IntergraphFlagRegisters", 0x847f, 16, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoAscii EXIF_TAG_SITE = new TagInfoAscii( + "Site", 0x84e0, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoAscii EXIF_TAG_COLOR_SEQUENCE = new TagInfoAscii( + "ColorSequence", 0x84e1, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoAscii EXIF_TAG_IT8HEADER = new TagInfoAscii( + "IT8Header", 0x84e2, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_RASTER_PADDING = new TagInfoShort( + "RasterPadding", 0x84e3, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_BITS_PER_RUN_LENGTH = new TagInfoShort( + "BitsPerRunLength", 0x84e4, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_BITS_PER_EXTENDED_RUN_LENGTH = new TagInfoShort( + "BitsPerExtendedRunLength", 0x84e5, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_COLOR_TABLE = new TagInfoByte( + "ColorTable", 0x84e6, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_IMAGE_COLOR_INDICATOR = new TagInfoByte( + "ImageColorIndicator", 0x84e7, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_BACKGROUND_COLOR_INDICATOR = new TagInfoByte( + "BackgroundColorIndicator", 0x84e8, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_IMAGE_COLOR_VALUE = new TagInfoByte( + "ImageColorValue", 0x84e9, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_BACKGROUND_COLOR_VALUE = new TagInfoByte( + "BackgroundColorValue", 0x84ea, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_PIXEL_INTENSITY_RANGE = new TagInfoByte( + "PixelIntensityRange", 0x84eb, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_TRANSPARENCY_INDICATOR = new TagInfoByte( + "TransparencyIndicator", 0x84ec, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoAscii EXIF_TAG_COLOR_CHARACTERIZATION = new TagInfoAscii( + "ColorCharacterization", 0x84ed, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShortOrLong EXIF_TAG_HCUSAGE = new TagInfoShortOrLong( + "HCUsage", 0x84ee, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoAscii EXIF_TAG_SEMINFO = new TagInfoAscii( + "SEMInfo", 0x8546, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoLong EXIF_TAG_AFCP_IPTC = new TagInfoLong( + "AFCP_IPTC", 0x8568, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoLong EXIF_TAG_LEAF_DATA = new TagInfoLong( + "LeafData", 0x8606, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoByte EXIF_TAG_PHOTOSHOP_SETTINGS = new TagInfoByte( + "PhotoshopSettings", 0x8649, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoDirectory EXIF_TAG_EXIF_OFFSET = new TagInfoDirectory( + "ExifOffset", 0x8769, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_EXPOSURE_PROGRAM = new TagInfoShort( + "ExposureProgram", 0x8822, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int EXPOSURE_PROGRAM_VALUE_MANUAL = 1; + int EXPOSURE_PROGRAM_VALUE_PROGRAM_AE = 2; + int EXPOSURE_PROGRAM_VALUE_APERTURE_PRIORITY_AE = 3; + int EXPOSURE_PROGRAM_VALUE_SHUTTER_SPEED_PRIORITY_AE = 4; + int EXPOSURE_PROGRAM_VALUE_CREATIVE_SLOW_SPEED = 5; + int EXPOSURE_PROGRAM_VALUE_ACTION_HIGH_SPEED = 6; + int EXPOSURE_PROGRAM_VALUE_PORTRAIT = 7; + int EXPOSURE_PROGRAM_VALUE_LANDSCAPE = 8; + TagInfoAscii EXIF_TAG_SPECTRAL_SENSITIVITY = new TagInfoAscii( + "SpectralSensitivity", 0x8824, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoDirectory EXIF_TAG_GPSINFO = new TagInfoDirectory( + "GPSInfo", 0x8825, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_ISO = new TagInfoShort( + "PhotographicSensitivity", 0x8827, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUndefined EXIF_TAG_OPTO_ELECTRIC_CONV_FACTOR = new TagInfoUndefined( + "Opto - Electric Conv Factor", 0x8828, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoLong EXIF_TAG_LEAF_SUB_IFD = new TagInfoLong( + "LeafSubIFD", 0x888a, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUndefined EXIF_TAG_EXIF_VERSION = new TagInfoUndefined( + "ExifVersion", 0x9000, 4, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_DATE_TIME_ORIGINAL = new TagInfoAscii( + "DateTimeOriginal", 0x9003, 20, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_DATE_TIME_DIGITIZED = new TagInfoAscii( + "DateTimeDigitized", 0x9004, 20, TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUndefined EXIF_TAG_COMPONENTS_CONFIGURATION = new TagInfoUndefined( + "ComponentsConfiguration", 0x9101, 4, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_COMPRESSED_BITS_PER_PIXEL = new TagInfoRational( + "CompressedBitsPerPixel", 0x9102, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoSRational EXIF_TAG_SHUTTER_SPEED_VALUE = new TagInfoSRational( + "ShutterSpeedValue", 0x9201, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_APERTURE_VALUE = new TagInfoRational( + "ApertureValue", 0x9202, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoSRational EXIF_TAG_BRIGHTNESS_VALUE = new TagInfoSRational( + "BrightnessValue", 0x9203, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoSRational EXIF_TAG_EXPOSURE_COMPENSATION = new TagInfoSRational( + "ExposureCompensation", 0x9204, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_MAX_APERTURE_VALUE = new TagInfoRational( + "MaxApertureValue", 0x9205, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_SUBJECT_DISTANCE = new TagInfoRational( + "Subject Distance", 0x9206, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_METERING_MODE = new TagInfoShort( + "MeteringMode", 0x9207, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int METERING_MODE_VALUE_AVERAGE = 1; + int METERING_MODE_VALUE_CENTER_WEIGHTED_AVERAGE = 2; + int METERING_MODE_VALUE_SPOT = 3; + int METERING_MODE_VALUE_MULTI_SPOT = 4; + int METERING_MODE_VALUE_MULTI_SEGMENT = 5; + int METERING_MODE_VALUE_PARTIAL = 6; + int METERING_MODE_VALUE_OTHER = 255; + TagInfoShort EXIF_TAG_LIGHT_SOURCE = new TagInfoShort( + "LightSource", 0x9208, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int LIGHT_SOURCE_VALUE_DAYLIGHT = 1; + int LIGHT_SOURCE_VALUE_FLUORESCENT = 2; + int LIGHT_SOURCE_VALUE_TUNGSTEN = 3; + int LIGHT_SOURCE_VALUE_FLASH = 4; + int LIGHT_SOURCE_VALUE_FINE_WEATHER = 9; + int LIGHT_SOURCE_VALUE_CLOUDY = 10; + int LIGHT_SOURCE_VALUE_SHADE = 11; + int LIGHT_SOURCE_VALUE_DAYLIGHT_FLUORESCENT = 12; + int LIGHT_SOURCE_VALUE_DAY_WHITE_FLUORESCENT = 13; + int LIGHT_SOURCE_VALUE_COOL_WHITE_FLUORESCENT = 14; + int LIGHT_SOURCE_VALUE_WHITE_FLUORESCENT = 15; + int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_A = 17; + int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_B = 18; + int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_C = 19; + int LIGHT_SOURCE_VALUE_D55 = 20; + int LIGHT_SOURCE_VALUE_D65 = 21; + int LIGHT_SOURCE_VALUE_D75 = 22; + int LIGHT_SOURCE_VALUE_D50 = 23; + int LIGHT_SOURCE_VALUE_ISO_STUDIO_TUNGSTEN = 24; + int LIGHT_SOURCE_VALUE_OTHER = 255; + TagInfoShort EXIF_TAG_FLASH = new TagInfoShort( + "Flash", 0x9209, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int FLASH_VALUE_NO_FLASH = 0x0; + int FLASH_VALUE_FIRED = 0x1; + int FLASH_VALUE_FIRED_RETURN_NOT_DETECTED = 0x5; + int FLASH_VALUE_FIRED_RETURN_DETECTED = 0x7; + int FLASH_VALUE_ON_DID_NOT_FIRE = 0x8; + int FLASH_VALUE_ON = 0x9; + int FLASH_VALUE_ON_RETURN_NOT_DETECTED = 0xd; + int FLASH_VALUE_ON_RETURN_DETECTED = 0xf; + int FLASH_VALUE_OFF = 0x10; + int FLASH_VALUE_OFF_DID_NOT_FIRE_RETURN_NOT_DETECTED = 0x14; + int FLASH_VALUE_AUTO_DID_NOT_FIRE = 0x18; + int FLASH_VALUE_AUTO_FIRED = 0x19; + int FLASH_VALUE_AUTO_FIRED_RETURN_NOT_DETECTED = 0x1d; + int FLASH_VALUE_AUTO_FIRED_RETURN_DETECTED = 0x1f; + int FLASH_VALUE_NO_FLASH_FUNCTION = 0x20; + int FLASH_VALUE_OFF_NO_FLASH_FUNCTION = 0x30; + int FLASH_VALUE_FIRED_RED_EYE_REDUCTION = 0x41; + int FLASH_VALUE_FIRED_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x45; + int FLASH_VALUE_FIRED_RED_EYE_REDUCTION_RETURN_DETECTED = 0x47; + int FLASH_VALUE_ON_RED_EYE_REDUCTION = 0x49; + int FLASH_VALUE_ON_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x4d; + int FLASH_VALUE_ON_RED_EYE_REDUCTION_RETURN_DETECTED = 0x4f; + int FLASH_VALUE_OFF_RED_EYE_REDUCTION = 0x50; + int FLASH_VALUE_AUTO_DID_NOT_FIRE_RED_EYE_REDUCTION = 0x58; + int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION = 0x59; + int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x5d; + int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION_RETURN_DETECTED = 0x5f; + TagInfoRational EXIF_TAG_FOCAL_LENGTH = new TagInfoRational( + "FocalLength", 0x920a, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_SUBJECT_AREA = new TagInfoShort( + "SubjectArea", 0x9214, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoDouble EXIF_TAG_STO_NITS = new TagInfoDouble( + "StoNits", 0x923f, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUndefined EXIF_TAG_MAKER_NOTE = new TagInfoUndefined( + "MakerNote", 0x927c, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoGpsText EXIF_TAG_USER_COMMENT = new TagInfoGpsText( + "UserComment", 0x9286, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SUB_SEC_TIME = new TagInfoAscii( + "SubSecTime", 0x9290, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SUB_SEC_TIME_ORIGINAL = new TagInfoAscii( + "SubSecTimeOriginal", 0x9291, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SUB_SEC_TIME_DIGITIZED = new TagInfoAscii( + "SubSecTimeDigitized", 0x9292, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUndefined EXIF_TAG_FLASHPIX_VERSION = new TagInfoUndefined( + "FlashpixVersion", 0xa000, 4, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_EXIF_IMAGE_WIDTH = new TagInfoShort( + "ExifImageWidth", 0xa002, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_EXIF_IMAGE_LENGTH = new TagInfoShort( + "ExifImageLength", 0xa003, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_RELATED_SOUND_FILE = new TagInfoAscii( + "RelatedSoundFile", 0xa004, 13, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoDirectory EXIF_TAG_INTEROP_OFFSET = new TagInfoDirectory( + "InteropOffset", 0xa005, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoRational EXIF_TAG_FLASH_ENERGY_EXIF_IFD = new TagInfoRational( + "FlashEnergy", 0xa20b, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUndefined EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_2 = new TagInfoUndefined( + "SpatialFrequencyResponse", 0xa20c, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_NOISE_2 = new TagInfoUnknown( + "Noise", 0xa20d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoRational EXIF_TAG_FOCAL_PLANE_XRESOLUTION_EXIF_IFD = new TagInfoRational( + "FocalPlaneXResolution", 0xa20e, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_FOCAL_PLANE_YRESOLUTION_EXIF_IFD = new TagInfoRational( + "FocalPlaneYResolution", 0xa20f, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD = new TagInfoShort( + "FocalPlaneResolutionUnit", 0xa210, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_NONE = 1; + int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_INCHES = 2; + int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_CM = 3; + int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_MM = 4; + int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_UM = 5; + TagInfoUnknown EXIF_TAG_IMAGE_NUMBER = new TagInfoUnknown( + "ImageNumber", 0xa211, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_SECURITY_CLASSIFICATION = new TagInfoUnknown( + "SecurityClassification", 0xa212, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUnknown EXIF_TAG_IMAGE_HISTORY = new TagInfoUnknown( + "ImageHistory", 0xa213, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_SUBJECT_LOCATION = new TagInfoShort( + "SubjectLocation", 0xa214, 2, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_EXPOSURE_INDEX_EXIF_IFD = new TagInfoRational( + "ExposureIndex", 0xa215, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUnknown EXIF_TAG_TIFF_EPSTANDARD_ID_2 = new TagInfoUnknown( + "TIFF-EPStandardID", 0xa216, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_SENSING_METHOD_EXIF_IFD = new TagInfoShort( + "SensingMethod", 0xa217, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int SENSING_METHOD_EXIF_IFD_VALUE_NOT_DEFINED = 1; + int SENSING_METHOD_EXIF_IFD_VALUE_ONE_CHIP_COLOR_AREA = 2; + int SENSING_METHOD_EXIF_IFD_VALUE_TWO_CHIP_COLOR_AREA = 3; + int SENSING_METHOD_EXIF_IFD_VALUE_THREE_CHIP_COLOR_AREA = 4; + int SENSING_METHOD_EXIF_IFD_VALUE_COLOR_SEQUENTIAL_AREA = 5; + int SENSING_METHOD_EXIF_IFD_VALUE_TRILINEAR = 7; + int SENSING_METHOD_EXIF_IFD_VALUE_COLOR_SEQUENTIAL_LINEAR = 8; + TagInfoUndefined EXIF_TAG_FILE_SOURCE = new TagInfoUndefined( + "FileSource", 0xa300, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int FILE_SOURCE_VALUE_FILM_SCANNER = 1; + int FILE_SOURCE_VALUE_REFLECTION_PRINT_SCANNER = 2; + int FILE_SOURCE_VALUE_DIGITAL_CAMERA = 3; + TagInfoUndefined EXIF_TAG_SCENE_TYPE = new TagInfoUndefined( + "SceneType", 0xa301, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUndefined EXIF_TAG_CFAPATTERN = new TagInfoUndefined( + "CFAPattern", 0xa302, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_CUSTOM_RENDERED = new TagInfoShort( + "CustomRendered", 0xa401, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int CUSTOM_RENDERED_VALUE_NORMAL = 0; + int CUSTOM_RENDERED_VALUE_CUSTOM = 1; + TagInfoShort EXIF_TAG_EXPOSURE_MODE = new TagInfoShort( + "ExposureMode", 0xa402, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int EXPOSURE_MODE_VALUE_AUTO = 0; + int EXPOSURE_MODE_VALUE_MANUAL = 1; + int EXPOSURE_MODE_VALUE_AUTO_BRACKET = 2; + TagInfoShort EXIF_TAG_WHITE_BALANCE_1 = new TagInfoShort( + "WhiteBalance", 0xa403, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int WHITE_BALANCE_1_VALUE_AUTO = 0; + int WHITE_BALANCE_1_VALUE_MANUAL = 1; + TagInfoRational EXIF_TAG_DIGITAL_ZOOM_RATIO = new TagInfoRational( + "DigitalZoomRatio", 0xa404, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_FOCAL_LENGTH_IN_35MM_FORMAT = new TagInfoShort( + "FocalLengthIn35mmFormat", 0xa405, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoShort EXIF_TAG_SCENE_CAPTURE_TYPE = new TagInfoShort( + "SceneCaptureType", 0xa406, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int SCENE_CAPTURE_TYPE_VALUE_STANDARD = 0; + int SCENE_CAPTURE_TYPE_VALUE_LANDSCAPE = 1; + int SCENE_CAPTURE_TYPE_VALUE_PORTRAIT = 2; + int SCENE_CAPTURE_TYPE_VALUE_NIGHT = 3; + TagInfoShort EXIF_TAG_GAIN_CONTROL = new TagInfoShort( + "GainControl", 0xa407, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int GAIN_CONTROL_VALUE_NONE = 0; + int GAIN_CONTROL_VALUE_LOW_GAIN_UP = 1; + int GAIN_CONTROL_VALUE_HIGH_GAIN_UP = 2; + int GAIN_CONTROL_VALUE_LOW_GAIN_DOWN = 3; + int GAIN_CONTROL_VALUE_HIGH_GAIN_DOWN = 4; + TagInfoShort EXIF_TAG_CONTRAST_1 = new TagInfoShort( + "Contrast", 0xa408, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int CONTRAST_1_VALUE_NORMAL = 0; + int CONTRAST_1_VALUE_LOW = 1; + int CONTRAST_1_VALUE_HIGH = 2; + TagInfoShort EXIF_TAG_SATURATION_1 = new TagInfoShort( + "Saturation", 0xa409, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int SATURATION_1_VALUE_NORMAL = 0; + int SATURATION_1_VALUE_LOW = 1; + int SATURATION_1_VALUE_HIGH = 2; + TagInfoShort EXIF_TAG_SHARPNESS_1 = new TagInfoShort( + "Sharpness", 0xa40a, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int SHARPNESS_1_VALUE_NORMAL = 0; + int SHARPNESS_1_VALUE_SOFT = 1; + int SHARPNESS_1_VALUE_HARD = 2; + TagInfoUndefined EXIF_TAG_DEVICE_SETTING_DESCRIPTION = new TagInfoUndefined( + "DeviceSettingDescription", 0xa40b, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoShort EXIF_TAG_SUBJECT_DISTANCE_RANGE = new TagInfoShort( + "SubjectDistanceRange", 0xa40c, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + int SUBJECT_DISTANCE_RANGE_VALUE_MACRO = 1; + int SUBJECT_DISTANCE_RANGE_VALUE_CLOSE = 2; + int SUBJECT_DISTANCE_RANGE_VALUE_DISTANT = 3; + TagInfoAscii EXIF_TAG_IMAGE_UNIQUE_ID = new TagInfoAscii( + "ImageUniqueID", 0xa420, 33, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoRational EXIF_TAG_GAMMA = new TagInfoRational( + "Gamma", 0xa500, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoUnknown EXIF_TAG_ANNOTATIONS = new TagInfoUnknown( + "Annotations", 0xc44f, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + TagInfoUndefined EXIF_TAG_PRINT_IM = new TagInfoUndefined( + "PrintIM", 0xc4a5, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + TagInfoSLong EXIF_TAG_OFFSET_SCHEMA = new TagInfoSLong( + "OffsetSchema", 0xea1d, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_OWNER_NAME = new TagInfoAscii( + "OwnerName", 0xfde8, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SERIAL_NUMBER = new TagInfoAscii( + "SerialNumber", 0xfde9, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_LENS = new TagInfoAscii( + "Lens", 0xfdea, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_RAW_FILE = new TagInfoAscii( + "RawFile", 0xfe4c, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_CONVERTER = new TagInfoAscii( + "Converter", 0xfe4d, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_WHITE_BALANCE_2 = new TagInfoAscii( + "WhiteBalance", 0xfe4e, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_EXPOSURE = new TagInfoAscii( + "Exposure", 0xfe51, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SHADOWS = new TagInfoAscii( + "Shadows", 0xfe52, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_BRIGHTNESS = new TagInfoAscii( + "Brightness", 0xfe53, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_CONTRAST_2 = new TagInfoAscii( + "Contrast", 0xfe54, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SATURATION_2 = new TagInfoAscii( + "Saturation", 0xfe55, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SHARPNESS_2 = new TagInfoAscii( + "Sharpness", 0xfe56, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_SMOOTHNESS = new TagInfoAscii( + "Smoothness", 0xfe57, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + TagInfoAscii EXIF_TAG_MOIRE_FILTER = new TagInfoAscii( + "MoireFilter", 0xfe58, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + List ALL_EXIF_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_INTEROPERABILITY_INDEX, EXIF_TAG_INTEROPERABILITY_VERSION, + EXIF_TAG_PROCESSING_SOFTWARE, + EXIF_TAG_SOFTWARE, + EXIF_TAG_PREVIEW_IMAGE_START_IFD0, + EXIF_TAG_PREVIEW_IMAGE_START_SUB_IFD1, + EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD2, + EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0, + EXIF_TAG_PREVIEW_IMAGE_LENGTH_SUB_IFD1, + EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD2, + EXIF_TAG_PREVIEW_IMAGE_START_MAKER_NOTES, + EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD, + EXIF_TAG_JPG_FROM_RAW_START_IFD2, EXIF_TAG_OTHER_IMAGE_START, + EXIF_TAG_PREVIEW_IMAGE_LENGTH_MAKER_NOTES, + EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD, + EXIF_TAG_JPG_FROM_RAW_LENGTH_IFD2, EXIF_TAG_OTHER_IMAGE_LENGTH, + EXIF_TAG_APPLICATION_NOTES, + EXIF_TAG_MATTEING, EXIF_TAG_DATA_TYPE, + EXIF_TAG_IMAGE_DEPTH, EXIF_TAG_TILE_DEPTH, EXIF_TAG_MODEL_2, + EXIF_TAG_EXPOSURE_TIME, + EXIF_TAG_FNUMBER, EXIF_TAG_IPTC_NAA, + EXIF_TAG_INTERGRAPH_PACKET_DATA, + EXIF_TAG_INTERGRAPH_FLAG_REGISTERS, + EXIF_TAG_SITE, EXIF_TAG_COLOR_SEQUENCE, + EXIF_TAG_IT8HEADER, EXIF_TAG_RASTER_PADDING, + EXIF_TAG_BITS_PER_RUN_LENGTH, + EXIF_TAG_BITS_PER_EXTENDED_RUN_LENGTH, EXIF_TAG_COLOR_TABLE, + EXIF_TAG_IMAGE_COLOR_INDICATOR, + EXIF_TAG_BACKGROUND_COLOR_INDICATOR, EXIF_TAG_IMAGE_COLOR_VALUE, + EXIF_TAG_BACKGROUND_COLOR_VALUE, EXIF_TAG_PIXEL_INTENSITY_RANGE, + EXIF_TAG_TRANSPARENCY_INDICATOR, EXIF_TAG_COLOR_CHARACTERIZATION, + EXIF_TAG_HCUSAGE, EXIF_TAG_SEMINFO, EXIF_TAG_AFCP_IPTC, + EXIF_TAG_LEAF_DATA, + EXIF_TAG_PHOTOSHOP_SETTINGS, EXIF_TAG_EXIF_OFFSET, + EXIF_TAG_EXPOSURE_PROGRAM, + EXIF_TAG_SPECTRAL_SENSITIVITY, EXIF_TAG_GPSINFO, EXIF_TAG_ISO, + EXIF_TAG_OPTO_ELECTRIC_CONV_FACTOR, + EXIF_TAG_LEAF_SUB_IFD, + EXIF_TAG_EXIF_VERSION, EXIF_TAG_DATE_TIME_ORIGINAL, + EXIF_TAG_DATE_TIME_DIGITIZED, EXIF_TAG_COMPONENTS_CONFIGURATION, + EXIF_TAG_COMPRESSED_BITS_PER_PIXEL, EXIF_TAG_SHUTTER_SPEED_VALUE, + EXIF_TAG_APERTURE_VALUE, EXIF_TAG_BRIGHTNESS_VALUE, + EXIF_TAG_EXPOSURE_COMPENSATION, EXIF_TAG_MAX_APERTURE_VALUE, + EXIF_TAG_SUBJECT_DISTANCE, EXIF_TAG_METERING_MODE, + EXIF_TAG_LIGHT_SOURCE, EXIF_TAG_FLASH, EXIF_TAG_FOCAL_LENGTH, + EXIF_TAG_SUBJECT_AREA, + EXIF_TAG_STO_NITS, EXIF_TAG_SUB_SEC_TIME, + EXIF_TAG_SUB_SEC_TIME_ORIGINAL, EXIF_TAG_SUB_SEC_TIME_DIGITIZED, + EXIF_TAG_FLASHPIX_VERSION, + EXIF_TAG_EXIF_IMAGE_WIDTH, EXIF_TAG_EXIF_IMAGE_LENGTH, + EXIF_TAG_RELATED_SOUND_FILE, EXIF_TAG_INTEROP_OFFSET, + EXIF_TAG_FLASH_ENERGY_EXIF_IFD, + EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_2, EXIF_TAG_NOISE_2, + EXIF_TAG_FOCAL_PLANE_XRESOLUTION_EXIF_IFD, + EXIF_TAG_FOCAL_PLANE_YRESOLUTION_EXIF_IFD, + EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD, + EXIF_TAG_IMAGE_NUMBER, EXIF_TAG_SECURITY_CLASSIFICATION, + EXIF_TAG_IMAGE_HISTORY, EXIF_TAG_SUBJECT_LOCATION, + EXIF_TAG_EXPOSURE_INDEX_EXIF_IFD, EXIF_TAG_TIFF_EPSTANDARD_ID_2, + EXIF_TAG_SENSING_METHOD_EXIF_IFD, EXIF_TAG_FILE_SOURCE, + EXIF_TAG_SCENE_TYPE, EXIF_TAG_CFAPATTERN, EXIF_TAG_CUSTOM_RENDERED, + EXIF_TAG_EXPOSURE_MODE, EXIF_TAG_WHITE_BALANCE_1, + EXIF_TAG_DIGITAL_ZOOM_RATIO, EXIF_TAG_FOCAL_LENGTH_IN_35MM_FORMAT, + EXIF_TAG_SCENE_CAPTURE_TYPE, EXIF_TAG_GAIN_CONTROL, + EXIF_TAG_CONTRAST_1, EXIF_TAG_SATURATION_1, EXIF_TAG_SHARPNESS_1, + EXIF_TAG_DEVICE_SETTING_DESCRIPTION, + EXIF_TAG_SUBJECT_DISTANCE_RANGE, EXIF_TAG_IMAGE_UNIQUE_ID, + EXIF_TAG_GAMMA, + EXIF_TAG_ANNOTATIONS, EXIF_TAG_PRINT_IM, + EXIF_TAG_OFFSET_SCHEMA, EXIF_TAG_OWNER_NAME, + EXIF_TAG_SERIAL_NUMBER, EXIF_TAG_LENS, EXIF_TAG_RAW_FILE, + EXIF_TAG_CONVERTER, EXIF_TAG_WHITE_BALANCE_2, EXIF_TAG_EXPOSURE, + EXIF_TAG_SHADOWS, EXIF_TAG_BRIGHTNESS, EXIF_TAG_CONTRAST_2, + EXIF_TAG_SATURATION_2, EXIF_TAG_SHARPNESS_2, EXIF_TAG_SMOOTHNESS, + EXIF_TAG_MOIRE_FILTER, + + EXIF_TAG_USER_COMMENT, // + + EXIF_TAG_MAKER_NOTE)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GdalLibraryTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GdalLibraryTagConstants.java new file mode 100644 index 0000000..ea19227 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GdalLibraryTagConstants.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; + +/** + * GDAL library. + *
+ * http://www.awaresystems.be/imaging/tiff/tifftags/gdal_metadata.html + *
+ * http://www.awaresystems.be/imaging/tiff/tifftags/gdal_nodata.html + */ +public interface GdalLibraryTagConstants { + TagInfoAscii EXIF_TAG_GDAL_METADATA = new TagInfoAscii( + "GDALMetadata", 0xa480, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_GDAL_NO_DATA = new TagInfoAscii( + "GDALNoData", 0xa481, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_GDAL_LIBRARY_TAGS = + Collections.unmodifiableList(Arrays.asList(new TagInfo[] { + EXIF_TAG_GDAL_METADATA, + EXIF_TAG_GDAL_NO_DATA})); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GeoTiffTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GeoTiffTagConstants.java new file mode 100644 index 0000000..e7379e0 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GeoTiffTagConstants.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; + +/** + * Geo Tiff. + *
+ * http://www.remotesensing.org/geotiff/spec/contents.html + */ +public interface GeoTiffTagConstants { + TagInfoDouble EXIF_TAG_MODEL_PIXEL_SCALE_TAG = new TagInfoDouble( + "ModelPixelScaleTag", 0x830e, 3, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDouble EXIF_TAG_INTERGRAPH_MATRIX_TAG = new TagInfoDouble( + "IntergraphMatrixTag", 0x8480, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDouble EXIF_TAG_MODEL_TIEPOINT_TAG = new TagInfoDouble( + "ModelTiepointTag", 0x8482, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDouble EXIF_TAG_MODEL_TRANSFORMATION_TAG = new TagInfoDouble( + "ModelTransformationTag", 0x85d8, 16, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_GEO_KEY_DIRECTORY_TAG = new TagInfoShort( + "GeoKeyDirectoryTag", 0x87af, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDouble EXIF_TAG_GEO_DOUBLE_PARAMS_TAG = new TagInfoDouble( + "GeoDoubleParamsTag", 0x87b0, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_GEO_ASCII_PARAMS_TAG = new TagInfoAscii( + "GeoAsciiParamsTag", 0x87b1, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_GEO_TIFF_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_MODEL_PIXEL_SCALE_TAG, + EXIF_TAG_INTERGRAPH_MATRIX_TAG, + EXIF_TAG_MODEL_TIEPOINT_TAG, + EXIF_TAG_MODEL_TRANSFORMATION_TAG, + EXIF_TAG_GEO_KEY_DIRECTORY_TAG, + EXIF_TAG_GEO_DOUBLE_PARAMS_TAG, + EXIF_TAG_GEO_ASCII_PARAMS_TAG)); + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GpsTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GpsTagConstants.java new file mode 100644 index 0000000..6b1af0f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/GpsTagConstants.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; + +public interface GpsTagConstants { + TagInfoByte GPS_TAG_GPS_VERSION_ID = new TagInfoByte( + "GPSVersionID", 0x0000, 4, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_LATITUDE_REF = new TagInfoAscii( + "GPSLatitudeRef", 0x0001, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH = "N"; + String GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH = "S"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_LATITUDE = new TagInfoRational( + "GPSLatitude", 0x0002, 3, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_LONGITUDE_REF = new TagInfoAscii( + "GPSLongitudeRef", 0x0003, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST = "E"; + String GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST = "W"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_LONGITUDE = new TagInfoRational( + "GPSLongitude", 0x0004, 3, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoByte GPS_TAG_GPS_ALTITUDE_REF = new TagInfoByte( + "GPSAltitudeRef", 0x0005, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + int GPS_TAG_GPS_ALTITUDE_REF_VALUE_ABOVE_SEA_LEVEL = 0; + int GPS_TAG_GPS_ALTITUDE_REF_VALUE_BELOW_SEA_LEVEL = 1; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_ALTITUDE = new TagInfoRational( + "GPSAltitude", 0x0006, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoRational GPS_TAG_GPS_TIME_STAMP = new TagInfoRational( + "GPSTimeStamp", 0x0007, 3, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_SATELLITES = new TagInfoAscii( + "GPSSatellites", 0x0008, -1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_STATUS = new TagInfoAscii( + "GPSStatus", 0x0009, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_STATUS_VALUE_MEASUREMENT_IN_PROGRESS = "A"; + String GPS_TAG_GPS_STATUS_VALUE_MEASUREMENT_INTEROPERABILITY = "V"; + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_MEASURE_MODE = new TagInfoAscii( + "GPSMeasureMode", 0x000a, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + int GPS_TAG_GPS_MEASURE_MODE_VALUE_2_DIMENSIONAL_MEASUREMENT = 2; + int GPS_TAG_GPS_MEASURE_MODE_VALUE_3_DIMENSIONAL_MEASUREMENT = 3; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_DOP = new TagInfoRational( + "GPSDOP", 0x000b, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_SPEED_REF = new TagInfoAscii( + "GPSSpeedRef", 0x000c, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_SPEED_REF_VALUE_KMPH = "K"; + String GPS_TAG_GPS_SPEED_REF_VALUE_MPH = "M"; + String GPS_TAG_GPS_SPEED_REF_VALUE_KNOTS = "N"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_SPEED = new TagInfoRational( + "GPSSpeed", 0x000d, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_TRACK_REF = new TagInfoAscii( + "GPSTrackRef", 0x000e, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_TRACK_REF_VALUE_MAGNETIC_NORTH = "M"; + String GPS_TAG_GPS_TRACK_REF_VALUE_TRUE_NORTH = "T"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_TRACK = new TagInfoRational( + "GPSTrack", 0x000f, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_IMG_DIRECTION_REF = new TagInfoAscii( + "GPSImgDirectionRef", 0x0010, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_MAGNETIC_NORTH = "M"; + String GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH = "T"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_IMG_DIRECTION = new TagInfoRational( + "GPSImgDirection", 0x0011, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_MAP_DATUM = new TagInfoAscii( + "GPSMapDatum", 0x0012, -1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_DEST_LATITUDE_REF = new TagInfoAscii( + "GPSDestLatitudeRef", 0x0013, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_DEST_LATITUDE_REF_VALUE_NORTH = "N"; + String GPS_TAG_GPS_DEST_LATITUDE_REF_VALUE_SOUTH = "S"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_DEST_LATITUDE = new TagInfoRational( + "GPSDestLatitude", 0x0014, 3, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_DEST_LONGITUDE_REF = new TagInfoAscii( + "GPSDestLongitudeRef", 0x0015, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_DEST_LONGITUDE_REF_VALUE_EAST = "E"; + String GPS_TAG_GPS_DEST_LONGITUDE_REF_VALUE_WEST = "W"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_DEST_LONGITUDE = new TagInfoRational( + "GPSDestLongitude", 0x0016, 3, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_DEST_BEARING_REF = new TagInfoAscii( + "GPSDestBearingRef", 0x0017, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_DEST_BEARING_REF_VALUE_MAGNETIC_NORTH = "M"; + String GPS_TAG_GPS_DEST_BEARING_REF_VALUE_TRUE_NORTH = "T"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_DEST_BEARING = new TagInfoRational( + "GPSDestBearing", 0x0018, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_DEST_DISTANCE_REF = new TagInfoAscii( + "GPSDestDistanceRef", 0x0019, 2, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_KILOMETERS = "K"; + String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_MILES = "M"; + String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_NAUTICAL_MILES = "N"; + // ************************************************************ + TagInfoRational GPS_TAG_GPS_DEST_DISTANCE = new TagInfoRational( + "GPSDestDistance", 0x001a, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoGpsText GPS_TAG_GPS_PROCESSING_METHOD = new TagInfoGpsText( + "GPSProcessingMethod", 0x001b, -1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoGpsText GPS_TAG_GPS_AREA_INFORMATION = new TagInfoGpsText( + "GPSAreaInformation", 0x001c, -1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoAscii GPS_TAG_GPS_DATE_STAMP = new TagInfoAscii( + "GPSDateStamp", 0x001d, 11, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + // ************************************************************ + TagInfoShort GPS_TAG_GPS_DIFFERENTIAL = new TagInfoShort( + "GPSDifferential", 0x001e, 1, + TiffDirectoryType.EXIF_DIRECTORY_GPS); + + int GPS_TAG_GPS_DIFFERENTIAL_VALUE_NO_CORRECTION = 0; + int GPS_TAG_GPS_DIFFERENTIAL_VALUE_DIFFERENTIAL_CORRECTED = 1; + // ************************************************************ + + List ALL_GPS_TAGS = + Collections.unmodifiableList(Arrays.asList( + GPS_TAG_GPS_VERSION_ID, GPS_TAG_GPS_LATITUDE_REF, + GPS_TAG_GPS_LATITUDE, GPS_TAG_GPS_LONGITUDE_REF, + GPS_TAG_GPS_LONGITUDE, GPS_TAG_GPS_ALTITUDE_REF, + GPS_TAG_GPS_ALTITUDE, GPS_TAG_GPS_TIME_STAMP, + GPS_TAG_GPS_SATELLITES, GPS_TAG_GPS_STATUS, + GPS_TAG_GPS_MEASURE_MODE, GPS_TAG_GPS_DOP, GPS_TAG_GPS_SPEED_REF, + GPS_TAG_GPS_SPEED, GPS_TAG_GPS_TRACK_REF, GPS_TAG_GPS_TRACK, + GPS_TAG_GPS_IMG_DIRECTION_REF, GPS_TAG_GPS_IMG_DIRECTION, + GPS_TAG_GPS_MAP_DATUM, GPS_TAG_GPS_DEST_LATITUDE_REF, + GPS_TAG_GPS_DEST_LATITUDE, GPS_TAG_GPS_DEST_LONGITUDE_REF, + GPS_TAG_GPS_DEST_LONGITUDE, GPS_TAG_GPS_DEST_BEARING_REF, + GPS_TAG_GPS_DEST_BEARING, GPS_TAG_GPS_DEST_DISTANCE_REF, + GPS_TAG_GPS_DEST_DISTANCE, GPS_TAG_GPS_PROCESSING_METHOD, + GPS_TAG_GPS_AREA_INFORMATION, GPS_TAG_GPS_DATE_STAMP, + GPS_TAG_GPS_DIFFERENTIAL)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/HylaFaxTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/HylaFaxTagConstants.java new file mode 100644 index 0000000..0e65847 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/HylaFaxTagConstants.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; + +public interface HylaFaxTagConstants { + TagInfoLong EXIF_TAG_FAX_RECV_PARAMS = new TagInfoLong( + "FaxRecvParams", 0x885c, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_FAX_SUB_ADDRESS = new TagInfoAscii( + "FaxSubAddress", 0x885d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_FAX_RECV_TIME = new TagInfoLong( + "FaxRecvTime", 0x885e, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_FAX_DCS = new TagInfoAscii( + "FaxDCS", 0x885f, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_HYLAFAX_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_FAX_RECV_PARAMS, + EXIF_TAG_FAX_SUB_ADDRESS, + EXIF_TAG_FAX_RECV_TIME, + EXIF_TAG_FAX_DCS)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftHdPhotoTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftHdPhotoTagConstants.java new file mode 100644 index 0000000..e746bf9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftHdPhotoTagConstants.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.common.BinaryConstant; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; + +/** + * Microsoft's HDP/WDP file format. + */ +public interface MicrosoftHdPhotoTagConstants { + /* + * The byte order for this GUID field is as follows: + * Data1 (int), Data2 (short), Data3 (short) are little-endian, + * Data4 (char[8]) is endian-independent. + */ + TagInfoByte EXIF_TAG_PIXEL_FORMAT = new TagInfoByte( + "PixelFormat", 0xbc01, 16, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + BinaryConstant PIXEL_FORMAT_VALUE_BLACK_AND_WHITE = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x05); + BinaryConstant PIXEL_FORMAT_VALUE_8_BIT_GRAY = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x08); + BinaryConstant PIXEL_FORMAT_VALUE_16_BIT_BGR555 = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x9); + BinaryConstant PIXEL_FORMAT_VALUE_16_BIT_BGR565 = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xa); + BinaryConstant PIXEL_FORMAT_VALUE_16_BIT_GRAY = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xb); + BinaryConstant PIXEL_FORMAT_VALUE_24_BIT_BGR = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xc); + BinaryConstant PIXEL_FORMAT_VALUE_24_BIT_RGB = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xd); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_BGR = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xe); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_BGRA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0xf); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_PBGRA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x10); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_GRAY_FLOAT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x11); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_RGB_FIXED_POINT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x12); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_BGR101010 = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x13); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_RGB = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x15); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_RGBA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x16); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_PRGBA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x17); + BinaryConstant PIXEL_FORMAT_VALUE_96_BIT_RGB_FIXED_POINT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x18); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_RGBA_FLOAT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x19); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_PRGBA_FLOAT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1a); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_RGB_FLOAT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1b); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_CMYK = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1c); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_RGBA_FIXED_POINT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1d); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_RGBA_FIXED_POINT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1e); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_CMYK = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x1f); + BinaryConstant PIXEL_FORMAT_VALUE_24_BIT_3_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x20); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_4_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x21); + BinaryConstant PIXEL_FORMAT_VALUE_40_BIT_5_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x22); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_6_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x23); + BinaryConstant PIXEL_FORMAT_VALUE_56_BIT_7_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x24); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_8_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x25); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_3_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x26); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_4_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x27); + BinaryConstant PIXEL_FORMAT_VALUE_80_BIT_5_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x28); + BinaryConstant PIXEL_FORMAT_VALUE_96_BIT_6_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x29); + BinaryConstant PIXEL_FORMAT_VALUE_112_BIT_7_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2a); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_8_CHANNELS = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2b); + BinaryConstant PIXEL_FORMAT_VALUE_40_BIT_CMYK_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2c); + BinaryConstant PIXEL_FORMAT_VALUE_80_BIT_CMYK_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2d); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_3_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2e); + BinaryConstant PIXEL_FORMAT_VALUE_40_BIT_4_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x2f); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_5_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x30); + BinaryConstant PIXEL_FORMAT_VALUE_56_BIT_6_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x31); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_7_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x32); + BinaryConstant PIXEL_FORMAT_VALUE_72_BIT_8_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x33); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_3_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x34); + BinaryConstant PIXEL_FORMAT_VALUE_80_BIT_4_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x35); + BinaryConstant PIXEL_FORMAT_VALUE_96_BIT_5_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x36); + BinaryConstant PIXEL_FORMAT_VALUE_112_BIT_6_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x37); + BinaryConstant PIXEL_FORMAT_VALUE_128_BIT_7_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x38); + BinaryConstant PIXEL_FORMAT_VALUE_144_BIT_8_CHANNELS_ALPHA = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x39); + BinaryConstant PIXEL_FORMAT_VALUE_64_BIT_RGBA_HALF = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x3a); + BinaryConstant PIXEL_FORMAT_VALUE_48_BIT_RGB_HALF = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x3b); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_RGBE = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x3d); + BinaryConstant PIXEL_FORMAT_VALUE_16_BIT_GRAY_HALF = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x3e); + BinaryConstant PIXEL_FORMAT_VALUE_32_BIT_GRAY_FIXED_POINT = + TagConstantsUtils.createMicrosoftHdPhotoGuidEndingWith((byte) 0x3f); + + TagInfoLong EXIF_TAG_TRANSFOMATION = new TagInfoLong( + "Transfomation", 0xbc02, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int TRANSFOMATION_VALUE_HORIZONTAL_NORMAL = 0; + int TRANSFOMATION_VALUE_MIRROR_VERTICAL = 1; + int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL = 2; + int TRANSFOMATION_VALUE_ROTATE_180 = 3; + int TRANSFOMATION_VALUE_ROTATE_90_CW = 4; + int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW = 5; + int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW = 6; + int TRANSFOMATION_VALUE_ROTATE_270_CW = 7; + + TagInfoLong EXIF_TAG_UNCOMPRESSED = new TagInfoLong( + "Uncompressed", 0xbc03, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int UNCOMPRESSED_VALUE_NO = 0; + int UNCOMPRESSED_VALUE_YES = 1; + + TagInfoLong EXIF_TAG_IMAGE_TYPE = new TagInfoLong( + "ImageType", 0xbc04, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_IMAGE_WIDTH = new TagInfoLong( + "ImageWidth", 0xbc80, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_IMAGE_HEIGHT = new TagInfoLong( + "ImageHeight", 0xbc81, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_WIDTH_RESOLUTION = new TagInfoFloat( + "WidthResolution", 0xbc82, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoFloat EXIF_TAG_HEIGHT_RESOLUTION = new TagInfoFloat( + "HeightResolution", 0xbc83, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + // FIXME: might be an offset? + TagInfoLong EXIF_TAG_IMAGE_OFFSET = new TagInfoLong( + "ImageOffset", 0xbcc0, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_IMAGE_BYTE_COUNT = new TagInfoLong( + "ImageByteCount", 0xbcc1, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + // FIXME: might be an offset? + TagInfoLong EXIF_TAG_ALPHA_OFFSET = new TagInfoLong( + "AlphaOffset", 0xbcc2, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong EXIF_TAG_ALPHA_BYTE_COUNT = new TagInfoLong( + "AlphaByteCount", 0xbcc3, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_IMAGE_DATA_DISCARD = new TagInfoByte( + "ImageDataDiscard", 0xbcc4, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int IMAGE_DATA_DISCARD_VALUE_FULL_RESOLUTION = 0; + int IMAGE_DATA_DISCARD_VALUE_FLEXBITS_DISCARDED = 1; + int IMAGE_DATA_DISCARD_VALUE_HIGH_PASS_FREQUENCY_DATA_DISCARDED = 2; + int IMAGE_DATA_DISCARD_VALUE_HIGHPASS_AND_LOW_PASS_FREQUENCY_DATA_DISCARDED = 3; + + TagInfoByte EXIF_TAG_ALPHA_DATA_DISCARD = new TagInfoByte( + "AlphaDataDiscard", 0xbcc5, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int ALPHA_DATA_DISCARD_VALUE_FULL_RESOLUTION = 0; + int ALPHA_DATA_DISCARD_VALUE_FLEXBITS_DISCARDED = 1; + int ALPHA_DATA_DISCARD_VALUE_HIGH_PASS_FREQUENCY_DATA_DISCARDED = 2; + int ALPHA_DATA_DISCARD_VALUE_HIGHPASS_AND_LOW_PASS_FREQUENCY_DATA_DISCARDED = 3; + + TagInfoUndefined EXIF_TAG_PADDING = new TagInfoUndefined( + "Padding", 0xea1c, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_MICROSOFT_HD_PHOTO_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_PIXEL_FORMAT, + EXIF_TAG_TRANSFOMATION, + EXIF_TAG_UNCOMPRESSED, + EXIF_TAG_IMAGE_TYPE, + EXIF_TAG_IMAGE_WIDTH, + EXIF_TAG_IMAGE_HEIGHT, + EXIF_TAG_WIDTH_RESOLUTION, + EXIF_TAG_HEIGHT_RESOLUTION, + EXIF_TAG_IMAGE_OFFSET, + EXIF_TAG_IMAGE_BYTE_COUNT, + EXIF_TAG_ALPHA_OFFSET, + EXIF_TAG_ALPHA_BYTE_COUNT, + EXIF_TAG_IMAGE_DATA_DISCARD, + EXIF_TAG_ALPHA_DATA_DISCARD, + EXIF_TAG_PADDING)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftTagConstants.java new file mode 100644 index 0000000..a5e5acd --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MicrosoftTagConstants.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoXpString; + +/** + * Largely undocumented and derived by experimentation. + */ +public interface MicrosoftTagConstants { + TagInfoShort EXIF_TAG_RATING = new TagInfoShort( + "Rating", 0x4746, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoShort EXIF_TAG_RATING_PERCENT = new TagInfoShort( + "RatingPercent", 0x4749, 1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoXpString EXIF_TAG_XPTITLE = new TagInfoXpString( + "XPTitle", 0x9c9b, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoXpString EXIF_TAG_XPCOMMENT = new TagInfoXpString( + "XPComment", 0x9c9c, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoXpString EXIF_TAG_XPAUTHOR = new TagInfoXpString( + "XPAuthor", 0x9c9d, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoXpString EXIF_TAG_XPKEYWORDS = new TagInfoXpString( + "XPKeywords", 0x9c9e, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + TagInfoXpString EXIF_TAG_XPSUBJECT = new TagInfoXpString( + "XPSubject", 0x9c9f, -1, + TiffDirectoryType.EXIF_DIRECTORY_IFD0); + + List ALL_MICROSOFT_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_RATING, + EXIF_TAG_RATING_PERCENT, + EXIF_TAG_XPTITLE, + EXIF_TAG_XPCOMMENT, + EXIF_TAG_XPAUTHOR, + EXIF_TAG_XPKEYWORDS, + EXIF_TAG_XPSUBJECT)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MolecularDynamicsGelTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MolecularDynamicsGelTagConstants.java new file mode 100644 index 0000000..132fe82 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/MolecularDynamicsGelTagConstants.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; + +/** + * Molecular Dynamics GEL file format + *
+ * http://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + */ +public interface MolecularDynamicsGelTagConstants { + TagInfoLong EXIF_TAG_MD_FILE_TAG = new TagInfoLong( + "MD FileTag", 0x82a5, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_MD_SCALE_PIXEL = new TagInfoRational( + "MD ScalePixel", 0x82a6, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_MD_COLOR_TABLE = new TagInfoShort( + "MD ColorTable", 0x82a7, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_MD_LAB_NAME = new TagInfoAscii( + "MD LabName", 0x82a8, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_MD_SAMPLE_INFO = new TagInfoAscii( + "MD SampleInfo", 0x82a9, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_MD_PREP_DATE = new TagInfoAscii( + "MD PrepDate", 0x82aa, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_MD_PREP_TIME = new TagInfoAscii( + "MD PrepTime", 0x82ab, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_MD_FILE_UNITS = new TagInfoAscii( + "MD FileUnits", 0x82ac, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_MOLECULAR_DYNAMICS_GEL_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_MD_FILE_TAG, + EXIF_TAG_MD_SCALE_PIXEL, + EXIF_TAG_MD_COLOR_TABLE, + EXIF_TAG_MD_LAB_NAME, + EXIF_TAG_MD_SAMPLE_INFO, + EXIF_TAG_MD_PREP_DATE, + EXIF_TAG_MD_PREP_TIME, + EXIF_TAG_MD_FILE_UNITS)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/OceScanjobTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/OceScanjobTagConstants.java new file mode 100644 index 0000000..cc827b5 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/OceScanjobTagConstants.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; + +/** + * Oce Scanjob. + *
+ * http://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + */ +public interface OceScanjobTagConstants { + TagInfoAscii EXIF_TAG_OCE_SCANJOB_DESCRIPTION = new TagInfoAscii( + "Oce Scanjob Description", 0xc427, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_OCE_APPLICATION_SELECTOR = new TagInfoAscii( + "Oce Application Selector", 0xc428, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_OCE_IDENTIFICATION_NUMBER = new TagInfoAscii( + "Oce Identification Number", 0xc429, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAscii EXIF_TAG_OCE_IMAGE_LOGIC_CHARACTERISTICS = new TagInfoAscii( + "Oce ImageLogic Characteristics", 0xc42a, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_OCE_SCANJOB_TAGS = + Collections.unmodifiableList(Arrays.asList(new TagInfo[] { + EXIF_TAG_OCE_SCANJOB_DESCRIPTION, + EXIF_TAG_OCE_APPLICATION_SELECTOR, + EXIF_TAG_OCE_IDENTIFICATION_NUMBER, + EXIF_TAG_OCE_IMAGE_LOGIC_CHARACTERISTICS})); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Rfc2301TagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Rfc2301TagConstants.java new file mode 100644 index 0000000..45925de --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Rfc2301TagConstants.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; + +/** + * RFC 2301: File Format for Internet Fax + *
+ * www.ietf.org/rfc/rfc2301.txt + *
+ * Also subsumes "The spirit of TIFF class F" + *
+ * http://cool.conservation-us.org/bytopic/imaging/std/tiff-f.html + */ +public interface Rfc2301TagConstants { + TagInfoShortOrLong TIFF_TAG_BAD_FAX_LINES = new TagInfoShortOrLong( + "BadFaxLines", 0x0146, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort TIFF_TAG_CLEAN_FAX_DATA = new TagInfoShort( + "CleanFaxData", 0x0147, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int CLEAN_FAX_DATA_VALUE_CLEAN = 0; + int CLEAN_FAX_DATA_VALUE_REGENERATED = 1; + int CLEAN_FAX_DATA_VALUE_UNCLEAN = 2; + + TagInfoShortOrLong TIFF_TAG_CONSECUTIVE_BAD_FAX_LINES = new TagInfoShortOrLong( + "ConsecutiveBadFaxLines", 0x0148, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoDirectory TIFF_TAG_GLOBAL_PARAMETERS_IFD = new TagInfoDirectory( + "GlobalParametersIFD", 0x0190, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong TIFF_TAG_PROFILE_TYPE = new TagInfoLong( + "ProfileType", 0x0191, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int PROFILE_TYPE_VALUE_UNSPECIFIED = 0; + int PROFILE_TYPE_VALUE_GROUP_3_FAX = 1; + + TagInfoByte TIFF_TAG_FAX_PROFILE = new TagInfoByte( + "FaxProfile", 0x0192, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int FAX_PROFILE_VALUE_UNKNOWN = 0; + int FAX_PROFILE_VALUE_MINIMAL_B_AND_W_LOSSLESS_S = 1; + int FAX_PROFILE_VALUE_EXTENDED_B_AND_W_LOSSLESS_F = 2; + int FAX_PROFILE_VALUE_LOSSLESS_JBIG_B_AND_W_J = 3; + int FAX_PROFILE_VALUE_LOSSY_COLOR_AND_GRAYSCALE_C = 4; + int FAX_PROFILE_VALUE_LOSSLESS_COLOR_AND_GRAYSCALE_L = 5; + int FAX_PROFILE_VALUE_MIXED_RASTER_CONTENT_M = 6; + + TagInfoLong TIFF_TAG_CODING_METHODS = new TagInfoLong( + "CodingMethods", 0x0193, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int CODING_METHODS_VALUE_T4_1D = 2; + int CODING_METHODS_VALUE_T4_2D = 4; + int CODING_METHODS_VALUE_T6 = 8; + int CODING_METHODS_VALUE_T82_T85 = 16; + int CODING_METHODS_VALUE_T81 = 32; + int CODING_METHODS_VALUE_T82_T43 = 64; + + TagInfoByte TIFF_TAG_VERSION_YEAR = new TagInfoByte( + "VersionYear", 0x0194, 4, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte TIFF_TAG_MODE_NUMBER = new TagInfoByte( + "ModeNumber", 0x0195, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational TIFF_TAG_DECODE = new TagInfoRational( + "Decode", 0x01b1, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort TIFF_TAG_DEFAULT_IMAGE_COLOR = new TagInfoShort( + "DefaultImageColor", 0x01b2, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoLong TIFF_TAG_STRIP_ROW_COUNTS = new TagInfoLong( + "StripRowCounts", 0x022f, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShortOrLong TIFF_TAG_IMAGE_LAYER = new TagInfoShortOrLong( + "ImageLayer", 0x87ac, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_RFC_2301_TAGS = + Collections.unmodifiableList(Arrays.asList( + TIFF_TAG_BAD_FAX_LINES, + TIFF_TAG_CLEAN_FAX_DATA, + TIFF_TAG_CONSECUTIVE_BAD_FAX_LINES, + TIFF_TAG_GLOBAL_PARAMETERS_IFD, + TIFF_TAG_PROFILE_TYPE, + TIFF_TAG_FAX_PROFILE, + TIFF_TAG_CODING_METHODS, + TIFF_TAG_VERSION_YEAR, + TIFF_TAG_MODE_NUMBER, + TIFF_TAG_DECODE, + TIFF_TAG_DEFAULT_IMAGE_COLOR, + TIFF_TAG_STRIP_ROW_COUNTS, + TIFF_TAG_IMAGE_LAYER)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TagConstantsUtils.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TagConstantsUtils.java new file mode 100644 index 0000000..dc906f9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TagConstantsUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.common.BinaryConstant; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; + +public final class TagConstantsUtils { + private static final TiffDirectoryType[] TIFF_DIRECTORY_TYPES = TiffDirectoryType + .values(); + + public static List mergeTagLists(final List... tagLists) { + int count = 0; + for (final List tagList : tagLists) { + count += tagList.size(); + } + + final ArrayList result = new ArrayList(count); + + for (final List tagList : tagLists) { + for (Object tag : tagList) { + result.add((TagInfo) tag); + } + } + + return result; + } + + public static TiffDirectoryType getExifDirectoryType(final int type) { + + for (final TiffDirectoryType tiffDirectoryType : TIFF_DIRECTORY_TYPES) { + if (tiffDirectoryType.directoryType == type) { + return tiffDirectoryType; + } + } + return TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN; + } + + public static BinaryConstant createMicrosoftHdPhotoGuidEndingWith(final byte end) { + return new BinaryConstant(new byte[] { (byte) 0x24, (byte) 0xC3, + (byte) 0xDD, (byte) 0x6F, (byte) 0x03, (byte) 0x4E, + (byte) 0xFE, (byte) 0x4B, (byte) 0xB1, (byte) 0x85, + (byte) 0x3D, (byte) 0x77, (byte) 0x76, (byte) 0x8D, + (byte) 0xC9, end }); + } + + private TagConstantsUtils() { + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Tiff4TagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Tiff4TagConstants.java new file mode 100644 index 0000000..35e91c8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/Tiff4TagConstants.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; + +/** + * Tags in TIFF4 but NOT in TIFF6. + *
+ * http://cool.conservation-us.org/bytopic/imaging/std/tiff4.html + */ +public interface Tiff4TagConstants { + TagInfoShort TIFF_TAG_COLOR_RESPONSE_UNIT = new TagInfoShort( + "ColorResponseUnit", 0x12C, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int COLOR_RESPONSE_UNIT_VALUE_0_1 = 1; + int COLOR_RESPONSE_UNIT_VALUE_0_01 = 2; + int COLOR_RESPONSE_UNIT_VALUE_0_001 = 3; + int COLOR_RESPONSE_UNIT_VALUE_0_0001 = 4; + int COLOR_RESPONSE_UNIT_VALUE_0_00001 = 5; + + List ALL_TIFF_4_TAGS = + Collections.unmodifiableList(Arrays.asList( + new TagInfo[] {TIFF_TAG_COLOR_RESPONSE_UNIT})); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java new file mode 100644 index 0000000..121f4d2 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffConstants.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImagingConstants; + +public interface TiffConstants + extends + ImagingConstants, + TiffDirectoryConstants, + AllTagConstants { + ByteOrder DEFAULT_TIFF_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN; + + int TIFF_HEADER_SIZE = 8; + int TIFF_DIRECTORY_HEADER_LENGTH = 2; + int TIFF_DIRECTORY_FOOTER_LENGTH = 4; + int TIFF_ENTRY_LENGTH = 12; + int TIFF_ENTRY_MAX_VALUE_LENGTH = 4; + + int TIFF_COMPRESSION_UNCOMPRESSED_1 = 1; + int TIFF_COMPRESSION_UNCOMPRESSED = TIFF_COMPRESSION_UNCOMPRESSED_1; + int TIFF_COMPRESSION_CCITT_1D = 2; + int TIFF_COMPRESSION_CCITT_GROUP_3 = 3; + int TIFF_COMPRESSION_CCITT_GROUP_4 = 4; + int TIFF_COMPRESSION_LZW = 5; + int TIFF_COMPRESSION_JPEG = 6; + int TIFF_COMPRESSION_UNCOMPRESSED_2 = 32771; + int TIFF_COMPRESSION_PACKBITS = 32773; + + /** + * Parameter key. Used in write operations to indicate the desired + * T.4 options to use when using TIFF_COMPRESSION_CCITT_GROUP_3. + *

+ * Valid values: any Integer containing a mixture of the + * TIFF_FLAG_T4_OPTIONS_2D, TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE, + * and TIFF_FLAG_T4_OPTIONS_FILL flags. + */ + String PARAM_KEY_T4_OPTIONS = "T4_OPTIONS"; + + /** + * Parameter key. Used in write operations to indicate the desired + * T.6 options to use when using TIFF_COMPRESSION_CCITT_GROUP_4. + *

+ * Valid values: any Integer containing either zero or + * TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE. + */ + String PARAM_KEY_T6_OPTIONS = "T6_OPTIONS"; + + int TIFF_FLAG_T4_OPTIONS_2D = 1; + int TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE = 2; + int TIFF_FLAG_T4_OPTIONS_FILL = 4; + int TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE = 2; + + + String PARAM_KEY_SUBIMAGE_X = "SUBIMAGE_X"; + String PARAM_KEY_SUBIMAGE_Y = "SUBIMAGE_Y"; + String PARAM_KEY_SUBIMAGE_WIDTH = "SUBIMAGE_WIDTH"; + String PARAM_KEY_SUBIMAGE_HEIGHT = "SUBIMAGE_HEIGHT"; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryConstants.java new file mode 100644 index 0000000..637a7e1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryConstants.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +public interface TiffDirectoryConstants { + + int DIRECTORY_TYPE_UNKNOWN = -1; + int DIRECTORY_TYPE_ROOT = 0; + int DIRECTORY_TYPE_SUB = 1; + int DIRECTORY_TYPE_SUB0 = 1; + int DIRECTORY_TYPE_SUB1 = 2; + int DIRECTORY_TYPE_SUB2 = 3; + int DIRECTORY_TYPE_THUMBNAIL = 2; + int DIRECTORY_TYPE_EXIF = -2; + int DIRECTORY_TYPE_GPS = -3; + int DIRECTORY_TYPE_INTEROPERABILITY = -4; + int DIRECTORY_TYPE_MAKER_NOTES = -5; + int DIRECTORY_TYPE_DIR_0 = 0; + int DIRECTORY_TYPE_DIR_1 = 1; + int DIRECTORY_TYPE_DIR_2 = 2; + int DIRECTORY_TYPE_DIR_3 = 3; + int DIRECTORY_TYPE_DIR_4 = 4; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryType.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryType.java new file mode 100644 index 0000000..b400766 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffDirectoryType.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +public enum TiffDirectoryType { + TIFF_DIRECTORY_IFD0( + true, TiffDirectoryConstants.DIRECTORY_TYPE_DIR_0, "IFD0"), + + TIFF_DIRECTORY_IFD1( + true, TiffDirectoryConstants.DIRECTORY_TYPE_DIR_1, "IFD1"), + + TIFF_DIRECTORY_IFD2( + true, TiffDirectoryConstants.DIRECTORY_TYPE_DIR_2, "IFD2"), + + TIFF_DIRECTORY_IFD3( + true, TiffDirectoryConstants.DIRECTORY_TYPE_DIR_3, "IFD3"), + + EXIF_DIRECTORY_INTEROP_IFD( + false, TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY, "Interop IFD"), + EXIF_DIRECTORY_MAKER_NOTES( + false, TiffDirectoryConstants.DIRECTORY_TYPE_MAKER_NOTES, "Maker Notes"), + EXIF_DIRECTORY_EXIF_IFD( + false, TiffDirectoryConstants.DIRECTORY_TYPE_EXIF, "Exif IFD"), + EXIF_DIRECTORY_GPS( + false, TiffDirectoryConstants.DIRECTORY_TYPE_GPS, "GPS IFD"); + + private final boolean isImageDirectory; + public final int directoryType; + public final String name; + + TiffDirectoryType(final boolean isImageDirectory, final int directoryType, final String name) { + this.isImageDirectory = isImageDirectory; + this.directoryType = directoryType; + this.name = name; + } + + public boolean isImageDirectory() { + return isImageDirectory; + } + + public static final TiffDirectoryType EXIF_DIRECTORY_IFD0 = TIFF_DIRECTORY_IFD0; + public static final TiffDirectoryType TIFF_DIRECTORY_ROOT = TIFF_DIRECTORY_IFD0; + public static final TiffDirectoryType EXIF_DIRECTORY_IFD1 = TIFF_DIRECTORY_IFD1; + public static final TiffDirectoryType EXIF_DIRECTORY_IFD2 = TIFF_DIRECTORY_IFD2; + public static final TiffDirectoryType EXIF_DIRECTORY_IFD3 = TIFF_DIRECTORY_IFD3; + public static final TiffDirectoryType EXIF_DIRECTORY_SUB_IFD = TIFF_DIRECTORY_IFD1; + public static final TiffDirectoryType EXIF_DIRECTORY_SUB_IFD1 = TIFF_DIRECTORY_IFD2; + public static final TiffDirectoryType EXIF_DIRECTORY_SUB_IFD2 = TIFF_DIRECTORY_IFD3; + public static final TiffDirectoryType EXIF_DIRECTORY_UNKNOWN = null; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffEpTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffEpTagConstants.java new file mode 100644 index 0000000..a7c002b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffEpTagConstants.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAsciiOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; + +/** + * + */ +public interface TiffEpTagConstants { + TagInfoShort EXIF_TAG_CFAREPEAT_PATTERN_DIM = new TagInfoShort( + "CFARepeatPatternDim", 0x828d, 2, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_CFAPATTERN_2 = new TagInfoByte( + "CFAPattern2", 0x828e, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoAsciiOrRational EXIF_TAG_BATTERY_LEVEL = new TagInfoAsciiOrRational( + "BatteryLevel", 0x828f, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoUndefined EXIF_TAG_INTER_COLOR_PROFILE = new TagInfoUndefined( + "InterColorProfile", 0x8773, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_INTERLACE = new TagInfoShort( + "Interlace", 0x8829, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoSShort EXIF_TAG_TIME_ZONE_OFFSET = new TagInfoSShort( + "TimeZoneOffset", 0x882a, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + TagInfoShort EXIF_TAG_SELF_TIMER_MODE = new TagInfoShort( + "SelfTimerMode", 0x882b, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + TagInfoRational EXIF_TAG_FLASH_ENERGY = new TagInfoRational( + "FlashEnergy", 0x920b, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_1 = new TagInfoUndefined( + "SpatialFrequencyResponse", 0x920c, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoUndefined EXIF_TAG_NOISE_1 = new TagInfoUndefined( + "Noise", 0x920d, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_FOCAL_PLANE_XRESOLUTION = new TagInfoRational( + "FocalPlaneXResolution", 0x920e, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoRational EXIF_TAG_FOCAL_PLANE_YRESOLUTION = new TagInfoRational( + "FocalPlaneYResolution", 0x920f, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT = new TagInfoShort( + "FocalPlaneResolutionUnit", 0x9210, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_NONE = 1; + int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_INCHES = 2; + int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_CM = 3; + int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_MM = 4; + int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_UM = 5; + + TagInfoLong EXIF_TAG_IMAGE_NUMBER_EXIF_IFD = new TagInfoLong( + "ImageNumber", 0x9211, 1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + TagInfoAscii EXIF_TAG_SECURITY_CLASSIFICATION_EXIF_IFD = new TagInfoAscii( + "SecurityClassification", 0x9212, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + TagInfoAscii EXIF_TAG_IMAGE_HISTORY_EXIF_IFD = new TagInfoAscii( + "ImageHistory", 0x9213, -1, + TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD); + + TagInfoRational EXIF_TAG_EXPOSURE_INDEX = new TagInfoRational( + "ExposureIndex", 0x9215, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoByte EXIF_TAG_TIFF_EPSTANDARD_ID_1 = new TagInfoByte( + "TIFF/EPStandardID", 0x9216, 4, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + TagInfoShort EXIF_TAG_SENSING_METHOD = new TagInfoShort( + "SensingMethod", 0x9217, 1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + int SENSING_METHOD_VALUE_MONOCHROME_AREA = 1; + int SENSING_METHOD_VALUE_ONE_CHIP_COLOR_AREA = 2; + int SENSING_METHOD_VALUE_TWO_CHIP_COLOR_AREA = 3; + int SENSING_METHOD_VALUE_THREE_CHIP_COLOR_AREA = 4; + int SENSING_METHOD_VALUE_COLOR_SEQUENTIAL_AREA = 5; + int SENSING_METHOD_VALUE_MONOCHROME_LINEAR = 6; + int SENSING_METHOD_VALUE_TRILINEAR = 7; + int SENSING_METHOD_VALUE_COLOR_SEQUENTIAL_LINEAR = 8; + + List ALL_TIFF_EP_TAGS = + Collections.unmodifiableList(Arrays.asList( + EXIF_TAG_CFAREPEAT_PATTERN_DIM, + EXIF_TAG_CFAPATTERN_2, + EXIF_TAG_BATTERY_LEVEL, + EXIF_TAG_INTER_COLOR_PROFILE, + EXIF_TAG_INTERLACE, + EXIF_TAG_TIME_ZONE_OFFSET, + EXIF_TAG_SELF_TIMER_MODE, + EXIF_TAG_FLASH_ENERGY, + EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_1, + EXIF_TAG_NOISE_1, + EXIF_TAG_FOCAL_PLANE_XRESOLUTION, + EXIF_TAG_FOCAL_PLANE_YRESOLUTION, + EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT, + EXIF_TAG_IMAGE_NUMBER_EXIF_IFD, + EXIF_TAG_SECURITY_CLASSIFICATION_EXIF_IFD, + EXIF_TAG_IMAGE_HISTORY_EXIF_IFD, + EXIF_TAG_EXPOSURE_INDEX, + EXIF_TAG_TIFF_EPSTANDARD_ID_1, + EXIF_TAG_SENSING_METHOD)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java new file mode 100644 index 0000000..9820572 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/TiffTagConstants.java @@ -0,0 +1,474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAny; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByteOrShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUnknown; + +/** + * Tags from the TIFF6 specification. + *
+ * http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf + */ +public interface TiffTagConstants { + TagInfoLong TIFF_TAG_NEW_SUBFILE_TYPE = new TagInfoLong( + "NewSubfileType", 0xFE, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int SUBFILE_TYPE_VALUE_FULL_RESOLUTION_IMAGE = 0; + int SUBFILE_TYPE_VALUE_REDUCED_RESOLUTION_IMAGE = 1; + int SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_IMAGE = 2; + int SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_REDUCED_RESOLUTION_IMAGE = 3; + int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK = 4; + int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_REDUCED_RESOLUTION_IMAGE = 5; + int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_MULTI_PAGE_IMAGE = 6; + int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_REDUCED_RESOLUTION_MULTI_PAGE_IMAGE = 7; + + TagInfoShort TIFF_TAG_SUBFILE_TYPE = new TagInfoShort( + "SubfileType", 0xFF, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int OLD_SUBFILE_TYPE_VALUE_FULL_RESOLUTION_IMAGE = 1; + int OLD_SUBFILE_TYPE_VALUE_REDUCED_RESOLUTION_IMAGE = 2; + int OLD_SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_IMAGE = 3; + + TagInfoShortOrLong TIFF_TAG_IMAGE_WIDTH = new TagInfoShortOrLong( + "ImageWidth", 0x100, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_IMAGE_LENGTH = new TagInfoShortOrLong( + "ImageLength", 0x101, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_BITS_PER_SAMPLE = new TagInfoShort( + "BitsPerSample", 0x102, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_COMPRESSION = new TagInfoShort( + "Compression", 0x103, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int COMPRESSION_VALUE_UNCOMPRESSED = 1; + int COMPRESSION_VALUE_CCITT_1D = 2; + int COMPRESSION_VALUE_T4_GROUP_3_FAX = 3; + int COMPRESSION_VALUE_T6_GROUP_4_FAX = 4; + int COMPRESSION_VALUE_LZW = 5; + int COMPRESSION_VALUE_JPEG_OLD_STYLE = 6; + int COMPRESSION_VALUE_JPEG = 7; + int COMPRESSION_VALUE_ADOBE_DEFLATE = 8; + int COMPRESSION_VALUE_JBIG_B_AND_W = 9; + int COMPRESSION_VALUE_JBIG_COLOR = 10; + int COMPRESSION_VALUE_NEXT = 32766; + int COMPRESSION_VALUE_EPSON_ERF_COMPRESSED = 32769; + int COMPRESSION_VALUE_CCIRLEW = 32771; + int COMPRESSION_VALUE_PACK_BITS = 32773; + int COMPRESSION_VALUE_THUNDERSCAN = 32809; + int COMPRESSION_VALUE_IT8CTPAD = 32895; + int COMPRESSION_VALUE_IT8LW = 32896; + int COMPRESSION_VALUE_IT8MP = 32897; + int COMPRESSION_VALUE_IT8BL = 32898; + int COMPRESSION_VALUE_PIXAR_FILM = 32908; + int COMPRESSION_VALUE_PIXAR_LOG = 32909; + int COMPRESSION_VALUE_DEFLATE = 32946; + int COMPRESSION_VALUE_DCS = 32947; + int COMPRESSION_VALUE_JBIG = 34661; + int COMPRESSION_VALUE_SGILOG = 34676; + int COMPRESSION_VALUE_SGILOG_24 = 34677; + int COMPRESSION_VALUE_JPEG_2000 = 34712; + int COMPRESSION_VALUE_NIKON_NEF_COMPRESSED = 34713; + int COMPRESSION_VALUE_KODAK_DCR_COMPRESSED = 65000; + int COMPRESSION_VALUE_PENTAX_PEF_COMPRESSED = 65535; + + TagInfoShort TIFF_TAG_PHOTOMETRIC_INTERPRETATION = new TagInfoShort( + "PhotometricInterpretation", 0x106, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int PHOTOMETRIC_INTERPRETATION_VALUE_WHITE_IS_ZERO = 0; + int PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO = 1; + int PHOTOMETRIC_INTERPRETATION_VALUE_RGB = 2; + int PHOTOMETRIC_INTERPRETATION_VALUE_RGB_PALETTE = 3; + int PHOTOMETRIC_INTERPRETATION_VALUE_TRANSPARENCY_MASK = 4; + int PHOTOMETRIC_INTERPRETATION_VALUE_CMYK = 5; + int PHOTOMETRIC_INTERPRETATION_VALUE_YCB_CR = 6; + int PHOTOMETRIC_INTERPRETATION_VALUE_CIELAB = 8; + int PHOTOMETRIC_INTERPRETATION_VALUE_ICCLAB = 9; + int PHOTOMETRIC_INTERPRETATION_VALUE_ITULAB = 10; + int PHOTOMETRIC_INTERPRETATION_VALUE_COLOR_FILTER_ARRAY = 32803; + int PHOTOMETRIC_INTERPRETATION_VALUE_PIXAR_LOG_L = 32844; + int PHOTOMETRIC_INTERPRETATION_VALUE_PIXAR_LOG_LUV = 32845; + int PHOTOMETRIC_INTERPRETATION_VALUE_LINEAR_RAW = 34892; + + TagInfoShort TIFF_TAG_THRESHHOLDING = new TagInfoShort( + "Threshholding", 0x107, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int THRESHOLDING_VALUE_NO_DITHERING_OR_HALFTONING = 1; + int THRESHOLDING_VALUE_ORDERED_DITHER_OR_HALFTONE = 2; + int THRESHOLDING_VALUE_RANDOMIZED_DITHER = 3; + + TagInfoShort TIFF_TAG_CELL_WIDTH = new TagInfoShort( + "CellWidth", 0x108, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_CELL_LENGTH = new TagInfoShort( + "CellLength", 0x109, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_FILL_ORDER = new TagInfoShort( + "FillOrder", 0x10A, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int FILL_ORDER_VALUE_NORMAL = 1; + int FILL_ORDER_VALUE_REVERSED = 2; + + TagInfoAscii TIFF_TAG_DOCUMENT_NAME = new TagInfoAscii( + "DocumentName", 0x10D, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_IMAGE_DESCRIPTION = new TagInfoAscii( + "ImageDescription", 0x10E, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_MAKE = new TagInfoAscii( + "Make", 0x10F, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_MODEL = new TagInfoAscii( + "Model", 0x110, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_STRIP_OFFSETS = new TagInfoShortOrLong( + "StripOffsets", 0x111, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT, true); + + TagInfoShort TIFF_TAG_ORIENTATION = new TagInfoShort( + "Orientation", 0x112, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int ORIENTATION_VALUE_HORIZONTAL_NORMAL = 1; + int ORIENTATION_VALUE_MIRROR_HORIZONTAL = 2; + int ORIENTATION_VALUE_ROTATE_180 = 3; + int ORIENTATION_VALUE_MIRROR_VERTICAL = 4; + int ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW = 5; + int ORIENTATION_VALUE_ROTATE_90_CW = 6; + int ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW = 7; + int ORIENTATION_VALUE_ROTATE_270_CW = 8; + + TagInfoShort TIFF_TAG_SAMPLES_PER_PIXEL = new TagInfoShort( + "SamplesPerPixel", 0x115, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_ROWS_PER_STRIP = new TagInfoShortOrLong( + "RowsPerStrip", 0x116, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_STRIP_BYTE_COUNTS = new TagInfoShortOrLong( + "StripByteCounts", 0x117, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_MIN_SAMPLE_VALUE = new TagInfoShort( + "MinSampleValue", 0x118, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_MAX_SAMPLE_VALUE = new TagInfoShort( + "MaxSampleValue", 0x119, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_XRESOLUTION = new TagInfoRational( + "XResolution", 0x11A, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_YRESOLUTION = new TagInfoRational( + "YResolution", 0x11B, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_PLANAR_CONFIGURATION = new TagInfoShort( + "PlanarConfiguration", 0x11C, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int PLANAR_CONFIGURATION_VALUE_CHUNKY = 1; + int PLANAR_CONFIGURATION_VALUE_PLANAR = 2; + + TagInfoAscii TIFF_TAG_PAGE_NAME = new TagInfoAscii( + "PageName", 0x11D, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_XPOSITION = new TagInfoRational( + "XPosition", 0x11E, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_YPOSITION = new TagInfoRational( + "YPosition", 0x11F, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_FREE_OFFSETS = new TagInfoLong( + "FreeOffsets", 0x120, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_FREE_BYTE_COUNTS = new TagInfoLong( + "FreeByteCounts", 0x121, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_GRAY_RESPONSE_UNIT = new TagInfoShort( + "GrayResponseUnit", 0x122, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int GRAY_RESPONSE_UNIT_VALUE_0_1 = 1; + int GRAY_RESPONSE_UNIT_VALUE_0_01 = 2; + int GRAY_RESPONSE_UNIT_VALUE_0_001 = 3; + int GRAY_RESPONSE_UNIT_VALUE_0_0001 = 4; + int GRAY_RESPONSE_UNIT_VALUE_0_00001 = 5; + + TagInfoShort TIFF_TAG_GRAY_RESPONSE_CURVE = new TagInfoShort( + "GrayResponseCurve", 0x123, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_T4_OPTIONS = new TagInfoLong( + "T4Options", 0x124, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_T6_OPTIONS = new TagInfoLong( + "T6Options", 0x125, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_RESOLUTION_UNIT = new TagInfoShort( + "ResolutionUnit", 0x128, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int RESOLUTION_UNIT_VALUE_NONE = 1; + int RESOLUTION_UNIT_VALUE_INCHES = 2; + int RESOLUTION_UNIT_VALUE_CM = 3; + + TagInfoShort TIFF_TAG_PAGE_NUMBER = new TagInfoShort( + "PageNumber", 0x129, 2, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_TRANSFER_FUNCTION = new TagInfoShort( + "TransferFunction", 0x12D, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_SOFTWARE = new TagInfoAscii( + "Software", 0x131, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_DATE_TIME = new TagInfoAscii( + "DateTime", 0x132, 20, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_ARTIST = new TagInfoAscii( + "Artist", 0x13B, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_HOST_COMPUTER = new TagInfoAscii( + "HostComputer", 0x13C, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_PREDICTOR = new TagInfoShort( + "Predictor", 0x13D, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int PREDICTOR_VALUE_NONE = 1; + int PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING = 2; + + TagInfoRational TIFF_TAG_WHITE_POINT = new TagInfoRational( + "WhitePoint", 0x13E, 2, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_PRIMARY_CHROMATICITIES = new TagInfoRational( + "PrimaryChromaticities", 0x13F, 6, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_COLOR_MAP = new TagInfoShort( + "ColorMap", 0x140, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_HALFTONE_HINTS = new TagInfoShort( + "HalftoneHints", 0x141, 2, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_TILE_WIDTH = new TagInfoShortOrLong( + "TileWidth", 0x142, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShortOrLong TIFF_TAG_TILE_LENGTH = new TagInfoShortOrLong( + "TileLength", 0x143, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_TILE_OFFSETS = new TagInfoLong( + "TileOffsets", 0x144, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT, true); + + TagInfoShortOrLong TIFF_TAG_TILE_BYTE_COUNTS = new TagInfoShortOrLong( + "TileByteCounts", 0x145, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_INK_SET = new TagInfoShort( + "InkSet", 0x14C, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int INK_SET_VALUE_CMYK = 1; + int INK_SET_VALUE_NOT_CMYK = 2; + + TagInfoAscii TIFF_TAG_INK_NAMES = new TagInfoAscii( + "InkNames", 0x14D, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_NUMBER_OF_INKS = new TagInfoShort( + "NumberOfInks", 0x14E, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoByteOrShort TIFF_TAG_DOT_RANGE = new TagInfoByteOrShort( + "DotRange", 0x150, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_TARGET_PRINTER = new TagInfoAscii( + "TargetPrinter", 0x151, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_EXTRA_SAMPLES = new TagInfoShort( + "ExtraSamples", 0x152, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_SAMPLE_FORMAT = new TagInfoShort( + "SampleFormat", 0x153, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int SAMPLE_FORMAT_VALUE_UNSIGNED_INTEGER = 1; + int SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER = 2; + int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT = 3; + int SAMPLE_FORMAT_VALUE_UNDEFINED = 4; + int SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER = 5; + int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT_1 = 6; + + TagInfoAny TIFF_TAG_SMIN_SAMPLE_VALUE = new TagInfoAny( + "SMinSampleValue", 0x154, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAny TIFF_TAG_SMAX_SAMPLE_VALUE = new TagInfoAny( + "SMaxSampleValue", 0x155, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_TRANSFER_RANGE = new TagInfoShort( + "TransferRange", 0x156, 6, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_JPEG_PROC = new TagInfoShort( + "JPEGProc", 0x200, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int JPEGPROC_VALUE_BASELINE = 1; + int JPEGPROC_VALUE_LOSSLESS = 14; + + TagInfoLong TIFF_TAG_JPEG_INTERCHANGE_FORMAT = new TagInfoLong( + "JPEGInterchangeFormat", 0x201, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT, true); + + TagInfoLong TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = new TagInfoLong( + "JPEGInterchangeFormatLength", 0x202, + 1, TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_JPEG_RESTART_INTERVAL = new TagInfoShort( + "JPEGRestartInterval", 0x203, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_JPEG_LOSSLESS_PREDICTORS = new TagInfoShort( + "JPEGLosslessPredictors", 0x205, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_JPEG_POINT_TRANSFORMS = new TagInfoShort( + "JPEGPointTransforms", 0x206, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_JPEG_QTABLES = new TagInfoLong( + "JPEGQTables", 0x207, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_JPEG_DCTABLES = new TagInfoLong( + "JPEGDCTables", 0x208, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoLong TIFF_TAG_JPEG_ACTABLES = new TagInfoLong( + "JPEGACTables", 0x209, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoRational TIFF_TAG_YCBCR_COEFFICIENTS = new TagInfoRational( + "YCbCrCoefficients", 0x211, 3, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_YCBCR_SUB_SAMPLING = new TagInfoShort( + "YCbCrSubSampling", 0x212, 2, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoShort TIFF_TAG_YCBCR_POSITIONING = new TagInfoShort( + "YCbCrPositioning", 0x213, 1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + int YCB_CR_POSITIONING_VALUE_CENTERED = 1; + int YCB_CR_POSITIONING_VALUE_CO_SITED = 2; + + TagInfoLong TIFF_TAG_REFERENCE_BLACK_WHITE = new TagInfoLong( + "ReferenceBlackWhite", 0x214, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoAscii TIFF_TAG_COPYRIGHT = new TagInfoAscii( + "Copyright", 0x8298, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + TagInfoByte TIFF_TAG_XMP = new TagInfoByte( + "XMP", 0x2BC, -1, + TiffDirectoryType.TIFF_DIRECTORY_ROOT); + + // TODO: + // TagInfo2 TIFF_TAG_UNKNOWN = null; + TagInfo TIFF_TAG_UNKNOWN = new TagInfoUnknown( + "Unknown Tag", -1, TagInfo.LENGTH_UNKNOWN, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_TIFF_TAGS = + Collections.unmodifiableList(Arrays.asList( + TIFF_TAG_NEW_SUBFILE_TYPE, TIFF_TAG_SUBFILE_TYPE, + TIFF_TAG_IMAGE_WIDTH, TIFF_TAG_IMAGE_LENGTH, + TIFF_TAG_BITS_PER_SAMPLE, TIFF_TAG_COMPRESSION, + TIFF_TAG_PHOTOMETRIC_INTERPRETATION, TIFF_TAG_THRESHHOLDING, + TIFF_TAG_CELL_WIDTH, TIFF_TAG_CELL_LENGTH, TIFF_TAG_FILL_ORDER, + TIFF_TAG_DOCUMENT_NAME, TIFF_TAG_IMAGE_DESCRIPTION, TIFF_TAG_MAKE, + TIFF_TAG_MODEL, TIFF_TAG_STRIP_OFFSETS, TIFF_TAG_ORIENTATION, + TIFF_TAG_SAMPLES_PER_PIXEL, TIFF_TAG_ROWS_PER_STRIP, + TIFF_TAG_STRIP_BYTE_COUNTS, TIFF_TAG_MIN_SAMPLE_VALUE, + TIFF_TAG_MAX_SAMPLE_VALUE, TIFF_TAG_XRESOLUTION, + TIFF_TAG_YRESOLUTION, TIFF_TAG_PLANAR_CONFIGURATION, + TIFF_TAG_PAGE_NAME, TIFF_TAG_XPOSITION, TIFF_TAG_YPOSITION, + TIFF_TAG_FREE_OFFSETS, TIFF_TAG_FREE_BYTE_COUNTS, + TIFF_TAG_GRAY_RESPONSE_UNIT, TIFF_TAG_GRAY_RESPONSE_CURVE, + TIFF_TAG_T4_OPTIONS, TIFF_TAG_T6_OPTIONS, TIFF_TAG_RESOLUTION_UNIT, + TIFF_TAG_PAGE_NUMBER, TIFF_TAG_TRANSFER_FUNCTION, + TIFF_TAG_SOFTWARE, TIFF_TAG_DATE_TIME, TIFF_TAG_ARTIST, + TIFF_TAG_HOST_COMPUTER, TIFF_TAG_PREDICTOR, TIFF_TAG_WHITE_POINT, + TIFF_TAG_PRIMARY_CHROMATICITIES, TIFF_TAG_COLOR_MAP, + TIFF_TAG_HALFTONE_HINTS, TIFF_TAG_TILE_WIDTH, TIFF_TAG_TILE_LENGTH, + TIFF_TAG_TILE_OFFSETS, TIFF_TAG_TILE_BYTE_COUNTS, TIFF_TAG_INK_SET, + TIFF_TAG_INK_NAMES, TIFF_TAG_NUMBER_OF_INKS, TIFF_TAG_DOT_RANGE, + TIFF_TAG_TARGET_PRINTER, TIFF_TAG_EXTRA_SAMPLES, + TIFF_TAG_SAMPLE_FORMAT, TIFF_TAG_SMIN_SAMPLE_VALUE, + TIFF_TAG_SMAX_SAMPLE_VALUE, TIFF_TAG_TRANSFER_RANGE, + TIFF_TAG_JPEG_PROC, TIFF_TAG_JPEG_INTERCHANGE_FORMAT, + TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + TIFF_TAG_JPEG_RESTART_INTERVAL, TIFF_TAG_JPEG_LOSSLESS_PREDICTORS, + TIFF_TAG_JPEG_POINT_TRANSFORMS, TIFF_TAG_JPEG_QTABLES, + TIFF_TAG_JPEG_DCTABLES, TIFF_TAG_JPEG_ACTABLES, + TIFF_TAG_YCBCR_COEFFICIENTS, TIFF_TAG_YCBCR_SUB_SAMPLING, + TIFF_TAG_YCBCR_POSITIONING, TIFF_TAG_REFERENCE_BLACK_WHITE, + TIFF_TAG_COPYRIGHT, + TIFF_TAG_XMP)); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/constants/WangTagConstants.java b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/WangTagConstants.java new file mode 100644 index 0000000..637288f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/constants/WangTagConstants.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; + +/** + * Wang/Eastman Software/Kodac/eiStream/Imaging for Windows tags, + * undocumented and in need of more work. + */ +public interface WangTagConstants { + TagInfoByte EXIF_TAG_WANG_ANNOTATION = new TagInfoByte( + "WangAnnotation", 0x80a4, -1, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + + List ALL_WANG_TAGS = + Collections.unmodifiableList(Arrays.asList(new TagInfo[] { + EXIF_TAG_WANG_ANNOTATION })); +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/BitInputStream.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/BitInputStream.java new file mode 100644 index 0000000..0289fce --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/BitInputStream.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.datareaders; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +/** + * Input stream reading 1-8, 16, 24 or 32 bits, starting from the most + * significant bit, but incapable of reading non-aligned and + * < 8 bit fields across byte boundaries. + */ +class BitInputStream extends InputStream { + private final InputStream is; + private final ByteOrder byteOrder; + private int cache; + private int cacheBitsRemaining; + private long bytesRead; + + public BitInputStream(final InputStream is, final ByteOrder byteOrder) { + this.is = is; + this.byteOrder = byteOrder; + } + + @Override + public int read() throws IOException { + if (cacheBitsRemaining > 0) { + throw new IOException("BitInputStream: incomplete bit read"); + } + return is.read(); + } + + public final int readBits(final int count) throws IOException { + if (count < 8) { + if (cacheBitsRemaining == 0) { + // fill cache + cache = is.read(); + cacheBitsRemaining = 8; + bytesRead++; + } + if (count > cacheBitsRemaining) { + throw new IOException( + "BitInputStream: can't read bit fields across bytes"); + } + + // int bits_to_shift = cache_bits_remaining - count; + cacheBitsRemaining -= count; + final int bits = cache >> cacheBitsRemaining; + + switch (count) { + case 1: + return bits & 1; + case 2: + return bits & 3; + case 3: + return bits & 7; + case 4: + return bits & 15; + case 5: + return bits & 31; + case 6: + return bits & 63; + case 7: + return bits & 127; + } + + } + if (cacheBitsRemaining > 0) { + throw new IOException("BitInputStream: incomplete bit read"); + } + + if (count == 8) { + bytesRead++; + return is.read(); + } + + /** + * Taking default order of the Tiff to be Little Endian and reversing + * the bytes in the end if its Big Endian.This is done because majority + * (may be all) of the files will be of Little Endian. + */ + if (byteOrder == ByteOrder.BIG_ENDIAN) { + if (count == 16) { + bytesRead += 2; + return (is.read() << 8) | (is.read() << 0); + } + + if (count == 24) { + bytesRead += 3; + return (is.read() << 16) | (is.read() << 8) | (is.read() << 0); + } + + if (count == 32) { + bytesRead += 4; + return (is.read() << 24) | (is.read() << 16) | (is.read() << 8) + | (is.read() << 0); + } + } else { + if (count == 16) { + bytesRead += 2; + return ((is.read() << 0) | (is.read() << 8)); + } + + if (count == 24) { + bytesRead += 3; + return ((is.read() << 0) | (is.read() << 8) | (is.read() << 16)); + } + + if (count == 32) { + bytesRead += 4; + return ((is.read() << 0) | (is.read() << 8) | (is.read() << 16) | (is + .read() << 24)); + } + } + + throw new IOException("BitInputStream: unknown error"); + } + + public void flushCache() { + cacheBitsRemaining = 0; + } + + public long getBytesRead() { + return bytesRead; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReader.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReader.java new file mode 100644 index 0000000..61e8d78 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReader.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.datareaders; + +import com.google.code.appengine.awt.Rectangle; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.common.PackBits; +import org.apache.commons.imaging.common.itu_t4.T4AndT6Compression; +import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor; +import org.apache.commons.imaging.formats.tiff.TiffDirectory; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public abstract class DataReader { + protected final TiffDirectory directory; + protected final PhotometricInterpreter photometricInterpreter; + protected final int[] bitsPerSample; + protected final int[] last; + + protected final int predictor; + protected final int samplesPerPixel; + protected final int width; + protected final int height; + + public DataReader(final TiffDirectory directory, + final PhotometricInterpreter photometricInterpreter, final int[] bitsPerSample, + final int predictor, final int samplesPerPixel, final int width, final int height) { + this.directory = directory; + this.photometricInterpreter = photometricInterpreter; + this.bitsPerSample = bitsPerSample; + this.samplesPerPixel = samplesPerPixel; + this.predictor = predictor; + this.width = width; + this.height = height; + last = new int[samplesPerPixel]; + } + + // public abstract void readImageData(BufferedImage bi, ByteSource + // byteSource) + public abstract void readImageData(ImageBuilder imageBuilder) + throws ImageReadException, IOException; + + + public abstract BufferedImage readImageData(Rectangle subImage) + throws ImageReadException, IOException; + + + + /** + * Reads samples and returns them in an int array. + * + * @param bis + * the stream to read from + * @param result + * the samples array to populate, must be the same length as + * bitsPerSample.length + * @throws IOException + */ + void getSamplesAsBytes(final BitInputStream bis, final int[] result) throws IOException { + for (int i = 0; i < bitsPerSample.length; i++) { + final int bits = bitsPerSample[i]; + int sample = bis.readBits(bits); + if (bits < 8) { + final int sign = sample & 1; + sample = sample << (8 - bits); // scale to byte. + if (sign > 0) { + sample = sample | ((1 << (8 - bits)) - 1); // extend to byte + } + } else if (bits > 8) { + sample = sample >> (bits - 8); // extend to byte. + } + result[i] = sample; + } + } + + protected void resetPredictor() { + for (int i = 0; i < last.length; i++) { + last[i] = 0; + } + } + + protected int[] applyPredictor(final int[] samples) { + if (predictor == 2) { + // Horizontal differencing. + for (int i = 0; i < samples.length; i++) { + samples[i] = 0xff & (samples[i] + last[i]); + last[i] = samples[i]; + } + } + + return samples; + } + + protected byte[] decompress(final byte[] compressed, final int compression, + final int expectedSize, final int tileWidth, final int tileHeight) + throws ImageReadException, IOException { + final TiffField fillOrderField = directory + .findField(TiffTagConstants.TIFF_TAG_FILL_ORDER); + int fillOrder = TiffTagConstants.FILL_ORDER_VALUE_NORMAL; + if (fillOrderField != null) { + fillOrder = fillOrderField.getIntValue(); + } + if (fillOrder == TiffTagConstants.FILL_ORDER_VALUE_NORMAL) { + // good + } else if (fillOrder == TiffTagConstants.FILL_ORDER_VALUE_REVERSED) { + for (int i = 0; i < compressed.length; i++) { + compressed[i] = (byte) (Integer.reverse(0xff & compressed[i]) >>> 24); + } + } else { + throw new ImageReadException("TIFF FillOrder=" + fillOrder + + " is invalid"); + } + + switch (compression) { + case TIFF_COMPRESSION_UNCOMPRESSED: // None; + return compressed; + case TIFF_COMPRESSION_CCITT_1D: // CCITT Group 3 1-Dimensional Modified + // Huffman run-length encoding. + return T4AndT6Compression.decompressModifiedHuffman(compressed, + tileWidth, tileHeight); + case TIFF_COMPRESSION_CCITT_GROUP_3: { + int t4Options = 0; + final TiffField field = directory + .findField(TiffTagConstants.TIFF_TAG_T4_OPTIONS); + if (field != null) { + t4Options = field.getIntValue(); + } + final boolean is2D = (t4Options & TIFF_FLAG_T4_OPTIONS_2D) != 0; + final boolean usesUncompressedMode = (t4Options & TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE) != 0; + if (usesUncompressedMode) { + throw new ImageReadException( + "T.4 compression with the uncompressed mode extension is not yet supported"); + } + final boolean hasFillBitsBeforeEOL = (t4Options & TIFF_FLAG_T4_OPTIONS_FILL) != 0; + if (is2D) { + return T4AndT6Compression.decompressT4_2D(compressed, + tileWidth, tileHeight, hasFillBitsBeforeEOL); + } + return T4AndT6Compression.decompressT4_1D(compressed, + tileWidth, tileHeight, hasFillBitsBeforeEOL); + } + case TIFF_COMPRESSION_CCITT_GROUP_4: { + int t6Options = 0; + final TiffField field = directory + .findField(TiffTagConstants.TIFF_TAG_T6_OPTIONS); + if (field != null) { + t6Options = field.getIntValue(); + } + final boolean usesUncompressedMode = (t6Options & TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0; + if (usesUncompressedMode) { + throw new ImageReadException( + "T.6 compression with the uncompressed mode extension is not yet supported"); + } + return T4AndT6Compression.decompressT6(compressed, tileWidth, + tileHeight); + } + case TIFF_COMPRESSION_LZW: // LZW + { + final InputStream is = new ByteArrayInputStream(compressed); + + final int lzwMinimumCodeSize = 8; + + final MyLzwDecompressor myLzwDecompressor = new MyLzwDecompressor( + lzwMinimumCodeSize, ByteOrder.BIG_ENDIAN); + + myLzwDecompressor.setTiffLZWMode(); + + return myLzwDecompressor.decompress(is, expectedSize); + } + + case TIFF_COMPRESSION_PACKBITS: // Packbits + { + return new PackBits().decompress(compressed, expectedSize); + } + + default: + throw new ImageReadException("Tiff: unknown/unsupported compression: " + compression); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java new file mode 100644 index 0000000..128e69c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderStrips.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.datareaders; + +import com.google.code.appengine.awt.Rectangle; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.formats.tiff.TiffDirectory; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; + +public final class DataReaderStrips extends DataReader { + + private final int bitsPerPixel; + private final int compression; + private final int rowsPerStrip; + private final ByteOrder byteOrder; + private int x; + private int y; + private final TiffImageData.Strips imageData; + + public DataReaderStrips(final TiffDirectory directory, + final PhotometricInterpreter photometricInterpreter, final int bitsPerPixel, + final int[] bitsPerSample, final int predictor, final int samplesPerPixel, final int width, + final int height, final int compression, final ByteOrder byteOrder, final int rowsPerStrip, + final TiffImageData.Strips imageData) { + super(directory, photometricInterpreter, bitsPerSample, predictor, + samplesPerPixel, width, height); + + this.bitsPerPixel = bitsPerPixel; + this.compression = compression; + this.rowsPerStrip = rowsPerStrip; + this.imageData = imageData; + this.byteOrder = byteOrder; + } + + private void interpretStrip( + final ImageBuilder imageBuilder, + final byte[] bytes, + final int pixelsPerStrip, + final int yLimit) throws ImageReadException, IOException { + if (y >= yLimit) { + return; + } + + // changes added May 2012 + // In the original implementation, a general-case bit reader called + // getSamplesAsBytes is used to retrieve the samples (raw data values) + // for each pixel in the strip. These samples are then passed into a + // photogrammetric interpreter that converts them to ARGB pixel values + // and stores them in the image. Because the bit-reader must handle + // a large number of formats, it involves several conditional + // branches that must be executed each time a pixel is read. + // Depending on the size of an image, the same evaluations must be + // executed redundantly thousands and perhaps millions of times + // in order to process the complete collection of pixels. + // This code attempts to remove that redundancy by + // evaluating the format up-front and bypassing the general-format + // code for two commonly used data formats: the 8 bits-per-pixel + // and 24 bits-per-pixel cases. For these formats, the + // special case code achieves substantial reductions in image-loading + // time. In other cases, it simply falls through to the original code + // and continues to read the data correctly as it did in previous + // versions of this class. + // In addition to bypassing the getBytesForSample() method, + // the 24-bit case also implements a special block for RGB + // formatted images. To get a sense of the contributions of each + // optimization (removing getSamplesAsBytes and removing the + // photometric interpreter), consider the following results from tests + // conducted with large TIFF images using the 24-bit RGB format + // bypass getSamplesAsBytes: 67.5 % reduction + // bypass both optimizations: 77.2 % reduction + // + // + // Future Changes + // Both of the 8-bit and 24-bit blocks make the assumption that a strip + // always begins on x = 0 and that each strip exactly fills out the rows + // it contains (no half rows). The original code did not make this + // assumption, but the approach is consistent with the TIFF 6.0 spec + // (1992), + // and should probably be considered as an enhancement to the + // original general-case code block that remains from the original + // implementation. Taking this approach saves one conditional + // operation per pixel or about 5 percent of the total run time + // in the 8 bits/pixel case. + + // verify that all samples are one byte in size + boolean allSamplesAreOneByte = true; + for (final int element : bitsPerSample) { + if (element != 8) { + allSamplesAreOneByte = false; + break; + } + } + + if (predictor != 2 && bitsPerPixel == 8 && allSamplesAreOneByte) { + int k = 0; + int nRows = pixelsPerStrip / width; + if (y + nRows > yLimit) { + nRows = yLimit - y; + } + final int i0 = y; + final int i1 = y + nRows; + x = 0; + y += nRows; + final int[] samples = new int[1]; + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++) { + samples[0] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + return; + } else if (predictor != 2 && bitsPerPixel == 24 && allSamplesAreOneByte) { + int k = 0; + int nRows = pixelsPerStrip / width; + if (y + nRows > yLimit) { + nRows = yLimit - y; + } + final int i0 = y; + final int i1 = y + nRows; + x = 0; + y += nRows; + if (photometricInterpreter instanceof PhotometricInterpreterRgb) { + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++, k += 3) { + final int rgb = 0xff000000 + | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8) + | (bytes[k + 2] & 0xff); + imageBuilder.setRGB(j, i, rgb); + } + } + } else { + final int[] samples = new int[3]; + for (int i = i0; i < i1; i++) { + for (int j = 0; j < width; j++) { + samples[0] = bytes[k++] & 0xff; + samples[1] = bytes[k++] & 0xff; + samples[2] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + } + + return; + } + + // ------------------------------------------------------------ + // original code before May 2012 modification + // this logic will handle all cases not conforming to the + // special case handled above + + final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + final BitInputStream bis = new BitInputStream(bais, byteOrder); + + int[] samples = new int[bitsPerSample.length]; + resetPredictor(); + for (int i = 0; i < pixelsPerStrip; i++) { + getSamplesAsBytes(bis, samples); + + if (x < width) { + samples = applyPredictor(samples); + + photometricInterpreter.interpretPixel( + imageBuilder, samples, x, y); + } + + x++; + if (x >= width) { + x = 0; + resetPredictor(); + y++; + bis.flushCache(); + if (y >= yLimit) { + break; + } + } + } + } + + @Override + public void readImageData(final ImageBuilder imageBuilder) + throws ImageReadException, IOException { + for (int strip = 0; strip < imageData.strips.length; strip++) { + final long rowsPerStripLong = 0xFFFFffffL & rowsPerStrip; + final long rowsRemaining = height - (strip * rowsPerStripLong); + final long rowsInThisStrip = Math.min(rowsRemaining, rowsPerStripLong); + final long bytesPerRow = (bitsPerPixel * width + 7) / 8; + final long bytesPerStrip = rowsInThisStrip * bytesPerRow; + final long pixelsPerStrip = rowsInThisStrip * width; + + final byte[] compressed = imageData.strips[strip].getData(); + + final byte[] decompressed = decompress(compressed, compression, + (int) bytesPerStrip, width, (int) rowsInThisStrip); + + interpretStrip( + imageBuilder, + decompressed, + (int) pixelsPerStrip, + height); + + } + } + + + @Override + public BufferedImage readImageData(final Rectangle subImage) + throws ImageReadException, IOException + { + // the legacy code is optimized to the reading of whole + // strips (except for the last strip in the image, which can + // be a partial). So create a working image with compatible + // dimensions and read that. Later on, the working image + // will be sub-imaged to the proper size. + + // strip0 and strip1 give the indices of the strips containing + // the first and last rows of pixels in the subimage + final int strip0 = subImage.y / rowsPerStrip; + final int strip1 = (subImage.y + subImage.height - 1) / rowsPerStrip; + final int workingHeight = (strip1 - strip0 + 1) * rowsPerStrip; + + + // the legacy code uses a member element "y" to keep track + // of the row index of the output image that is being processed + // by interpretStrip. y is set to zero before the first + // call to interpretStrip. y0 will be the index of the first row + // in the full image (the source image) that will be processed. + + final int y0 = strip0 * rowsPerStrip; + final int yLimit = subImage.y - y0 + subImage.height; + + + // TO DO: we can probably save some processing by using yLimit instead + // or working + final ImageBuilder workingBuilder = + new ImageBuilder(width, workingHeight, false); + + for (int strip = strip0; strip <= strip1; strip++) { + final long rowsPerStripLong = 0xFFFFffffL & rowsPerStrip; + final long rowsRemaining = height - (strip * rowsPerStripLong); + final long rowsInThisStrip = Math.min(rowsRemaining, rowsPerStripLong); + final long bytesPerRow = (bitsPerPixel * width + 7) / 8; + final long bytesPerStrip = rowsInThisStrip * bytesPerRow; + final long pixelsPerStrip = rowsInThisStrip * width; + + final byte[] compressed = imageData.strips[strip].getData(); + + final byte[] decompressed = decompress(compressed, compression, + (int) bytesPerStrip, width, (int) rowsInThisStrip); + + interpretStrip( + workingBuilder, + decompressed, + (int) pixelsPerStrip, + yLimit); + } + + + if (subImage.x == 0 + && subImage.y == y0 + && subImage.width == width + && subImage.height == workingHeight) { + // the subimage exactly matches the ImageBuilder bounds + return workingBuilder.getBufferedImage(); + } + return workingBuilder.getSubimage( + subImage.x, + subImage.y - y0, + subImage.width, + subImage.height); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java new file mode 100644 index 0000000..e14aa2e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/datareaders/DataReaderTiled.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.datareaders; + +import com.google.code.appengine.awt.Rectangle; +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; +import org.apache.commons.imaging.formats.tiff.TiffDirectory; +import org.apache.commons.imaging.formats.tiff.TiffElement.DataElement; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter; +import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; + +public final class DataReaderTiled extends DataReader { + + private final int tileWidth; + private final int tileLength; + + private final int bitsPerPixel; + + private final int compression; + private final ByteOrder byteOrder; + + private final TiffImageData.Tiles imageData; + + public DataReaderTiled(final TiffDirectory directory, + final PhotometricInterpreter photometricInterpreter, final int tileWidth, + final int tileLength, final int bitsPerPixel, final int[] bitsPerSample, + final int predictor, final int samplesPerPixel, final int width, final int height, + final int compression, final ByteOrder byteOrder, final TiffImageData.Tiles imageData) { + super(directory, photometricInterpreter, bitsPerSample, predictor, + samplesPerPixel, width, height); + + this.tileWidth = tileWidth; + this.tileLength = tileLength; + + this.bitsPerPixel = bitsPerPixel; + this.compression = compression; + + this.imageData = imageData; + this.byteOrder = byteOrder; + } + + private void interpretTile(final ImageBuilder imageBuilder, final byte[] bytes, + final int startX, final int startY, final int xLimit, final int yLimit) throws ImageReadException, IOException { + // changes introduced May 2012 + // The following block of code implements changes that + // reduce image loading time by using special-case processing + // instead of the general-purpose logic from the original + // implementation. For a detailed discussion, see the comments for + // a similar treatment in the DataReaderStrip class + // + + // verify that all samples are one byte in size + boolean allSamplesAreOneByte = true; + for (final int element : bitsPerSample) { + if (element != 8) { + allSamplesAreOneByte = false; + break; + } + } + + if (predictor != 2 && bitsPerPixel == 24 && allSamplesAreOneByte) { + int k = 0; + final int i0 = startY; + int i1 = startY + tileLength; + if (i1 > yLimit) { + // the tile is padded past bottom of image + i1 = yLimit; + } + final int j0 = startX; + int j1 = startX + tileWidth; + if (j1 > xLimit) { + // the tile is padded to beyond the tile width + j1 = xLimit; + } + if (photometricInterpreter instanceof PhotometricInterpreterRgb) { + for (int i = i0; i < i1; i++) { + k = (i - i0) * tileWidth * 3; + for (int j = j0; j < j1; j++, k += 3) { + final int rgb = 0xff000000 + | (((bytes[k] << 8) | (bytes[k + 1] & 0xff)) << 8) + | (bytes[k + 2] & 0xff); + imageBuilder.setRGB(j, i, rgb); + } + } + } else { + final int[] samples = new int[3]; + for (int i = i0; i < i1; i++) { + k = (i - i0) * tileWidth * 3; + for (int j = j0; j < j1; j++) { + samples[0] = bytes[k++] & 0xff; + samples[1] = bytes[k++] & 0xff; + samples[2] = bytes[k++] & 0xff; + photometricInterpreter.interpretPixel(imageBuilder, + samples, j, i); + } + } + } + return; + } + + // End of May 2012 changes + + final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + final BitInputStream bis = new BitInputStream(bais, byteOrder); + + final int pixelsPerTile = tileWidth * tileLength; + + int tileX = 0; + int tileY = 0; + + int[] samples = new int[bitsPerSample.length]; + resetPredictor(); + for (int i = 0; i < pixelsPerTile; i++) { + + final int x = tileX + startX; + final int y = tileY + startY; + + getSamplesAsBytes(bis, samples); + + if ((x < xLimit) && (y < yLimit)) { + samples = applyPredictor(samples); + photometricInterpreter.interpretPixel(imageBuilder, samples, x, + y); + } + + tileX++; + + if (tileX >= tileWidth) { + tileX = 0; + resetPredictor(); + tileY++; + bis.flushCache(); + if (tileY >= tileLength) { + break; + } + } + + } + } + + @Override + public void readImageData(final ImageBuilder imageBuilder) + throws ImageReadException, IOException { + final int bitsPerRow = tileWidth * bitsPerPixel; + final int bytesPerRow = (bitsPerRow + 7) / 8; + final int bytesPerTile = bytesPerRow * tileLength; + int x = 0; + int y = 0; + + for (final DataElement tile2 : imageData.tiles) { + final byte[] compressed = tile2.getData(); + + final byte[] decompressed = decompress(compressed, compression, + bytesPerTile, tileWidth, tileLength); + + interpretTile(imageBuilder, decompressed, x, y, width, height); + + x += tileWidth; + if (x >= width) { + x = 0; + y += tileLength; + if (y >= height) { + break; + } + } + + } + } + + @Override + public BufferedImage readImageData(final Rectangle subImage) + throws ImageReadException, IOException + { + final int bitsPerRow = tileWidth * bitsPerPixel; + final int bytesPerRow = (bitsPerRow + 7) / 8; + final int bytesPerTile = bytesPerRow * tileLength; + int x = 0; + int y = 0; + + // tileWidth is the width of the tile + // tileLength is the height of the tile + final int col0 = subImage.x / tileWidth; + final int col1 = (subImage.x + subImage.width - 1) / tileWidth; + final int row0 = subImage.y / tileLength; + final int row1 = (subImage.y + subImage.height - 1) / tileLength; + + final int nCol = col1 - col0 + 1; + final int nRow = row1 - row0 + 1; + final int workingWidth = nCol * tileWidth; + final int workingHeight = nRow * tileLength; + + final int nColumnsOfTiles = (width + tileWidth - 1) / tileWidth; + + final int x0 = col0 * tileWidth; + final int y0 = row0 * tileLength; + + final ImageBuilder workingBuilder = + new ImageBuilder(workingWidth, workingHeight, false); + + for (int iRow = row0; iRow <= row1; iRow++) { + for (int iCol = col0; iCol <= col1; iCol++) { + final int tile = iRow * nColumnsOfTiles + iCol; + final byte[] compressed = imageData.tiles[tile].getData(); + final byte[] decompressed = decompress(compressed, compression, + bytesPerTile, tileWidth, tileLength); + x = iCol * tileWidth - x0; + y = iRow * tileLength - y0; + interpretTile(workingBuilder, decompressed, x, y, workingWidth, workingHeight); + } + } + + if (subImage.x == x0 + && subImage.y == y0 + && subImage.width == workingWidth + && subImage.height == workingHeight) { + return workingBuilder.getBufferedImage(); + } + return workingBuilder.getSubimage( + subImage.x - x0, + subImage.y - y0, + subImage.width, + subImage.height); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldType.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldType.java new file mode 100644 index 0000000..b1708c0 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldType.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.TiffField; + +/** + * TIFF field types. + */ +public abstract class FieldType { + public static final FieldTypeByte BYTE = new FieldTypeByte(1, "Byte"); + public static final FieldTypeAscii ASCII = new FieldTypeAscii(2, "ASCII"); + public static final FieldTypeShort SHORT = new FieldTypeShort(3, "Short"); + public static final FieldTypeLong LONG = new FieldTypeLong(4, "Long"); + public static final FieldTypeRational RATIONAL = new FieldTypeRational(5, "Rational"); + public static final FieldTypeByte SBYTE = new FieldTypeByte(6, "SByte"); + public static final FieldTypeByte UNDEFINED = new FieldTypeByte(7, "Undefined"); + public static final FieldTypeShort SSHORT = new FieldTypeShort(8, "SShort"); + public static final FieldTypeLong SLONG = new FieldTypeLong(9, "SLong"); + public static final FieldTypeRational SRATIONAL = new FieldTypeRational(10, "SRational"); + public static final FieldTypeFloat FLOAT = new FieldTypeFloat(11, "Float"); + public static final FieldTypeDouble DOUBLE = new FieldTypeDouble(12, "Double"); + public static final FieldTypeLong IFD = new FieldTypeLong(13, "IFD"); + + private final int type; + private final String name; + private final int elementSize; + + public static final List ANY = + Collections.unmodifiableList(Arrays.asList( + BYTE, ASCII, SHORT, + LONG, RATIONAL, SBYTE, + UNDEFINED, SSHORT, SLONG, + SRATIONAL, FLOAT, DOUBLE, + IFD)); + + public static final List SHORT_OR_LONG = + Collections.unmodifiableList(Arrays.asList( + SHORT, LONG)); + + public static final List SHORT_OR_RATIONAL = + Collections.unmodifiableList(Arrays.asList( + SHORT, RATIONAL)); + + public static final List SHORT_OR_LONG_OR_RATIONAL = + Collections.unmodifiableList(Arrays.asList( + SHORT, LONG, RATIONAL)); + + public static final List LONG_OR_SHORT = + Collections.unmodifiableList(Arrays.asList( + SHORT, LONG)); + + public static final List BYTE_OR_SHORT = + Collections.unmodifiableList(Arrays.asList( + SHORT, BYTE)); + + public static final List LONG_OR_IFD = + Collections.unmodifiableList(Arrays.asList( + (FieldType) LONG, IFD)); + + public static final List ASCII_OR_RATIONAL = + Collections.unmodifiableList(Arrays.asList( + ASCII, RATIONAL)); + + public static final List ASCII_OR_BYTE = + Collections.unmodifiableList(Arrays.asList( + ASCII, BYTE)); + + protected FieldType(final int type, final String name, final int elementSize) { + this.type = type; + this.name = name; + this.elementSize = elementSize; + } + + + public int getType() { + return type; + } + + public String getName() { + return name; + } + + public int getSize() { + return elementSize; + } + + public static FieldType getFieldType(final int type) throws ImageReadException { + for (final FieldType fieldType : ANY) { + if (fieldType.getType() == type) { + return fieldType; + } + } + throw new ImageReadException("Field type " + type + " is unsupported"); + } + + public abstract Object getValue(final TiffField entry); + public abstract byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException; +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java new file mode 100644 index 0000000..cc7e71d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeAscii extends FieldType { + public FieldTypeAscii(final int type, final String name) { + super(type, name, 1); + } + + @Override + public Object getValue(final TiffField entry) { + // According to EXIF specification + // "2 = ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL." + final byte[] bytes = entry.getByteArrayValue(); + int nullCount = 1; + for (int i = 0; i < bytes.length - 1; i++) { + if (bytes[i] == 0) { + nullCount++; + } + } + final String[] strings = new String[nullCount]; + int stringsAdded = 0; + strings[0] = ""; // if we have a 0 length string + int nextStringPos = 0; + // According to the Exiftool FAQ, http://www.metadataworkinggroup.org + // specifies that the TIFF ASCII fields are actually UTF-8. + // Exiftool however allows you to configure the charset used. + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == 0) { + try { + final String string = new String(bytes, nextStringPos, i + - nextStringPos, "UTF-8"); + strings[stringsAdded++] = string; + } catch (final UnsupportedEncodingException unsupportedEncoding) { // NOPMD + } + nextStringPos = i + 1; + } + } + if (nextStringPos < bytes.length) { + // Buggy file, string wasn't null terminated + try { + final String string = new String(bytes, nextStringPos, bytes.length + - nextStringPos, "UTF-8"); + strings[stringsAdded++] = string; + } catch (final UnsupportedEncodingException unsupportedEncoding) { // NOPMD + } + } + if (strings.length == 1) { + return strings[0]; + } + return strings; + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof byte[]) { + final byte[] bytes = (byte[]) o; + final byte[] result = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, result, 0, bytes.length); + result[result.length - 1] = 0; + return result; + } else if (o instanceof String) { + byte[] bytes; + try { + bytes = ((String) o).getBytes("UTF-8"); + } catch (final UnsupportedEncodingException cannotHappen) { + throw new IllegalArgumentException("Your Java doesn't support UTF-8", cannotHappen); + } + final byte[] result = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, result, 0, bytes.length); + result[result.length - 1] = 0; + return result; + } else if (o instanceof String[]) { + final String[] strings = (String[]) o; + int totalLength = 0; + for (final String string : strings) { + byte[] bytes; + try { + bytes = string.getBytes("UTF-8"); + } catch (final UnsupportedEncodingException cannotHappen) { + throw new IllegalArgumentException("Your Java doesn't support UTF-8", cannotHappen); + } + totalLength += (bytes.length + 1); + } + final byte[] result = new byte[totalLength]; + int position = 0; + for (final String string : strings) { + byte[] bytes; + try { + bytes = string.getBytes("UTF-8"); + } catch (final UnsupportedEncodingException cannotHappen) { + throw new IllegalArgumentException("Your Java doesn't support UTF-8", cannotHappen); + } + System.arraycopy(bytes, 0, result, position, bytes.length); + position += (bytes.length + 1); + } + return result; + } else { + throw new ImageWriteException("Unknown data type: " + o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeByte.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeByte.java new file mode 100644 index 0000000..797855d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeByte.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeByte extends FieldType { + public FieldTypeByte(final int type, final String name) { + super(type, name, 1); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return bytes[0]; + } + return bytes; + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof Byte) { + return new byte[] { ((Byte) o).byteValue(), }; + } else if (o instanceof byte[]) { + return (byte[]) o; + } else { + throw new ImageWriteException("Invalid data", o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java new file mode 100644 index 0000000..c7ca989 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeDouble extends FieldType { + public FieldTypeDouble(final int type, final String name) { + super(type, name, 8); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return ByteConversions.toDouble(bytes, + entry.getByteOrder()); + } + return ByteConversions.toDoubles(bytes, entry.getByteOrder()); + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof Double) { + return ByteConversions.toBytes(((Double) o).doubleValue(), + byteOrder); + } else if (o instanceof double[]) { + final double[] numbers = (double[]) o; + return ByteConversions.toBytes(numbers, byteOrder); + } else if (o instanceof Double[]) { + final Double[] numbers = (Double[]) o; + final double[] values = new double[numbers.length]; + for (int i = 0; i < values.length; i++) { + values[i] = numbers[i].doubleValue(); + } + return ByteConversions.toBytes(values, byteOrder); + } else { + throw new ImageWriteException("Invalid data", o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java new file mode 100644 index 0000000..0be8510 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeFloat extends FieldType { + public FieldTypeFloat(final int type, final String name) { + super(type, name, 4); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return ByteConversions.toFloat(bytes, + entry.getByteOrder()); + } + return ByteConversions.toFloats(bytes, entry.getByteOrder()); + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof Float) { + return ByteConversions.toBytes(((Float) o).floatValue(), byteOrder); + } else if (o instanceof float[]) { + final float[] numbers = (float[]) o; + return ByteConversions.toBytes(numbers, byteOrder); + } else if (o instanceof Float[]) { + final Float[] numbers = (Float[]) o; + final float[] values = new float[numbers.length]; + for (int i = 0; i < values.length; i++) { + values[i] = numbers[i].floatValue(); + } + return ByteConversions.toBytes(values, byteOrder); + } else { + throw new ImageWriteException("Invalid data", o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java new file mode 100644 index 0000000..2ed9435 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeLong extends FieldType { + public FieldTypeLong(final int type, final String name) { + super(type, name, 4); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return ByteConversions.toInt(bytes, + entry.getByteOrder()); + } + return ByteConversions.toInts(bytes, entry.getByteOrder()); + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof Integer) { + return ByteConversions.toBytes((Integer) o, byteOrder); + } else if (o instanceof int[]) { + final int[] numbers = (int[]) o; + return ByteConversions.toBytes(numbers, byteOrder); + } else if (o instanceof Integer[]) { + final Integer[] numbers = (Integer[]) o; + final int[] values = new int[numbers.length]; + for (int i = 0; i < values.length; i++) { + values[i] = numbers[i]; + } + return ByteConversions.toBytes(values, byteOrder); + } else { + throw new ImageWriteException("Invalid data", o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java new file mode 100644 index 0000000..ebda8a2 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeRational extends FieldType { + public FieldTypeRational(final int type, final String name) { + super(type, name, 8); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return ByteConversions.toRational(bytes, + entry.getByteOrder()); + } + return ByteConversions.toRationals(bytes, entry.getByteOrder()); + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof RationalNumber) { + return ByteConversions.toBytes((RationalNumber) o, byteOrder); + } else if (o instanceof RationalNumber[]) { + return ByteConversions.toBytes((RationalNumber[]) o, byteOrder); + } else if (o instanceof Number) { + final Number number = (Number) o; + final RationalNumber rationalNumber = RationalNumber.valueOf(number.doubleValue()); + return ByteConversions.toBytes(rationalNumber, byteOrder); + } else if (o instanceof Number[]) { + final Number[] numbers = (Number[]) o; + final RationalNumber[] rationalNumbers = new RationalNumber[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + final Number number = numbers[i]; + rationalNumbers[i] = RationalNumber.valueOf(number.doubleValue()); + } + return ByteConversions.toBytes(rationalNumbers, byteOrder); + } else if (o instanceof double[]) { + final double[] numbers = (double[]) o; + final RationalNumber[] rationalNumbers = new RationalNumber[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + final double number = numbers[i]; + rationalNumbers[i] = RationalNumber.valueOf(number); + } + return ByteConversions.toBytes(rationalNumbers, byteOrder); + } else { + throw new ImageWriteException("Invalid data", o); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java new file mode 100644 index 0000000..893be90 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.fieldtypes; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.TiffField; + +public class FieldTypeShort extends FieldType { + public FieldTypeShort(final int type, final String name) { + super(type, name, 2); + } + + @Override + public Object getValue(final TiffField entry) { + final byte[] bytes = entry.getByteArrayValue(); + if (entry.getCount() == 1) { + return ByteConversions.toShort(bytes, entry.getByteOrder()); + } + return ByteConversions.toShorts(bytes, entry.getByteOrder()); + } + + @Override + public byte[] writeData(final Object o, final ByteOrder byteOrder) throws ImageWriteException { + if (o instanceof Short) { + return ByteConversions.toBytes(((Short) o).shortValue(), byteOrder); + } else if (o instanceof short[]) { + final short[] numbers = (short[]) o; + return ByteConversions.toBytes(numbers, byteOrder); + } else if (o instanceof Short[]) { + final Short[] numbers = (Short[]) o; + final short[] values = new short[numbers.length]; + for (int i = 0; i < values.length; i++) { + values[i] = numbers[i].shortValue(); + } + return ByteConversions.toBytes(values, byteOrder); + } else { + throw new ImageWriteException("Invalid data", o); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/package-info.java b/src/main/java/org/apache/commons/imaging/formats/tiff/package-info.java new file mode 100644 index 0000000..2e9bd8c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/package-info.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides classes and methods for reading and writing + * Tagged Image File Format (TIFF) files. Tiff files are + * widely used in applications for which supplemental + * metadata is added to images in the form of "tags". + */ +package org.apache.commons.imaging.formats.tiff; + diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreter.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreter.java similarity index 56% rename from src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreter.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreter.java index 0eb7e6a..fa5cef3 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreter.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreter.java @@ -1,52 +1,42 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class PhotometricInterpreter -{ - protected final int samplesPerPixel; - protected final int bitsPerSample[]; - protected final int predictor; - protected final int width; - protected final int height; - - public PhotometricInterpreter(int fSamplesPerPixel, int fBitsPerSample[], - int Predictor, int width, int height) - { - this.samplesPerPixel = fSamplesPerPixel; - this.bitsPerSample = fBitsPerSample; - this.predictor = Predictor; - this.width = width; - this.height = height; - } - - public abstract void interpretPixel(BufferedImage bi, int samples[], int x, - int y) throws ImageReadException, IOException; - - public void dumpstats() throws ImageReadException, IOException - { - - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public abstract class PhotometricInterpreter { + protected final int samplesPerPixel; + protected final int[] bitsPerSample; + protected final int predictor; + protected final int width; + protected final int height; + + public PhotometricInterpreter(final int samplesPerPixel, final int[] bitsPerSample, + final int predictor, final int width, final int height) { + this.samplesPerPixel = samplesPerPixel; + this.bitsPerSample = bitsPerSample; + this.predictor = predictor; + this.width = width; + this.height = height; + } + + public abstract void interpretPixel(ImageBuilder imageBuilder, + int[] samples, int x, int y) throws ImageReadException, IOException; +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java similarity index 50% rename from src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java index b6cc175..4468e2c 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterBiLevel.java @@ -1,59 +1,56 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterBiLevel extends PhotometricInterpreter -{ - private final boolean invert; - - // private final int bitsPerPixel; - - public PhotometricInterpreterBiLevel(int fBitsPerPixel, - int fSamplesPerPixel, int fBitsPerSample[], int Predictor, - int width, int height, boolean invert) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - - this.invert = invert; - // this.bitsPerPixel = fBitsPerPixel; - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - int sample = samples[0]; - - if (invert) - sample = 255 - sample; - - int red = sample; - int green = sample; - int blue = sample; - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - bi.setRGB(x, y, rgb); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterBiLevel extends PhotometricInterpreter { + private final boolean invert; + + // private final int bitsPerPixel; + + public PhotometricInterpreterBiLevel(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, + final int height, final boolean invert) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + + this.invert = invert; + // this.bitsPerPixel = fBitsPerPixel; + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + int sample = samples[0]; + + if (invert) { + sample = 255 - sample; + } + + final int red = sample; + final int green = sample; + final int blue = sample; + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + + imageBuilder.setRGB(x, y, rgb); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCieLab.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCieLab.java new file mode 100644 index 0000000..06cbfc9 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCieLab.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.color.ColorConversions; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterCieLab extends PhotometricInterpreter { + public PhotometricInterpreterCieLab(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, final int height) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + final int cieL = samples[0]; + final int cieA = (byte) samples[1]; + final int cieB = (byte) samples[2]; + + final int rgb = ColorConversions.convertCIELabtoARGBTest(cieL, cieA, cieB); + imageBuilder.setRGB(x, y, rgb); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCmyk.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCmyk.java new file mode 100644 index 0000000..f9ac08b --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterCmyk.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.color.ColorConversions; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterCmyk extends PhotometricInterpreter { + public PhotometricInterpreterCmyk(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, final int height) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + + final int sc = samples[0]; + final int sm = samples[1]; + final int sy = samples[2]; + final int sk = samples[3]; + + final int rgb = ColorConversions.convertCMYKtoRGB(sc, sm, sy, sk); + imageBuilder.setRGB(x, y, rgb); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLuv.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLuv.java new file mode 100644 index 0000000..4f9e570 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLuv.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterLogLuv extends PhotometricInterpreter { + // private final boolean yOnly; + + public PhotometricInterpreterLogLuv(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, final int height) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + + // this.yOnly = yonly; + } + + private float cube(final float f) { + return f * f * f; + } + + // private float function_f(float value, ) + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + float X, Y, Z; + + final int cieL = samples[0]; + final int cieA = (byte) samples[1]; + final int cieB = (byte) samples[2]; + + { + + float var_Y = ((cieL * 100.0f / 255.0f) + 16.0f) / 116.0f; + float var_X = cieA / 500.0f + var_Y; + float var_Z = var_Y - cieB / 200.0f; + + final float var_x_cube = cube(var_X); + final float var_y_cube = cube(var_Y); + final float var_z_cube = cube(var_Z); + + if (var_y_cube > 0.008856f) { + var_Y = var_y_cube; + } else { + var_Y = (var_Y - 16 / 116.0f) / 7.787f; + } + + if (var_x_cube > 0.008856f) { + var_X = var_x_cube; + } else { + var_X = (var_X - 16 / 116.0f) / 7.787f; + } + + if (var_z_cube > 0.008856f) { + var_Z = var_z_cube; + } else { + var_Z = (var_Z - 16 / 116.0f) / 7.787f; + } + + final float ref_X = 95.047f; + final float ref_Y = 100.000f; + final float ref_Z = 108.883f; + + X = ref_X * var_X; // ref_X = 95.047 Observer= 2°, Illuminant= D65 + Y = ref_Y * var_Y; // ref_Y = 100.000 + Z = ref_Z * var_Z; // ref_Z = 108.883 + + } + + // ref_X = 95.047 //Observer = 2°, Illuminant = D65 + // ref_Y = 100.000 + // ref_Z = 108.883 + + int R, G, B; + { + final float var_X = X / 100f; // X = From 0 to ref_X + final float var_Y = Y / 100f; // Y = From 0 to ref_Y + final float var_Z = Z / 100f; // Z = From 0 to ref_Y + + float var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f; + float var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f; + float var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f; + + if (var_R > 0.0031308) { + var_R = 1.055f * (float) Math.pow(var_R, (1 / 2.4)) - 0.055f; + } else { + var_R = 12.92f * var_R; + } + if (var_G > 0.0031308) { + var_G = 1.055f * (float) Math.pow(var_G, (1 / 2.4)) - 0.055f; + } else { + var_G = 12.92f * var_G; + } + + if (var_B > 0.0031308) { + var_B = 1.055f * (float) Math.pow(var_B, (1 / 2.4)) - 0.055f; + } else { + var_B = 12.92f * var_B; + } + + // var_R = (((var_R-))) + // updateMaxMin(new float[]{ + // var_R, var_G, var_B, + // }, maxVarRGB, minVarRGB); + + // var_R = ((var_R + 0.16561039f) / (3.0152583f + 0.16561039f)); + // var_G = ((var_G + 0.06561642f) / (3.0239854f + 0.06561642f)); + // var_B = ((var_B + 0.19393992f) / (3.1043448f + 0.19393992f)); + + R = (int) (var_R * 255f); + G = (int) (var_G * 255f); + B = (int) (var_B * 255f); + } + + // float R = 1.910f * X - 0.532f * Y - 0.288f * Z; + // float G = -0.985f * X + 1.999f * Y - 0.028f * Z; + // float B = 0.058f * X - 0.118f * Y + 0.898f * Z; + + // updateMaxMin(new float[]{ + // R, G, B, + // }, maxRGB, minRGB); + + int red = R; + int green = G; + int blue = B; + + red = Math.min(255, Math.max(0, red)); + green = Math.min(255, Math.max(0, green)); + blue = Math.min(255, Math.max(0, blue)); + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + imageBuilder.setRGB(x, y, rgb); + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java new file mode 100644 index 0000000..14ae0e7 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterPalette extends PhotometricInterpreter { + + /** + * The color map of integer ARGB values tied to the pixel index of the + * palette + */ + private final int[] indexColorMap; + + public PhotometricInterpreterPalette(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, final int height, + final int[] colorMap) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + + final int bitsPerPixel = this.bitsPerSample[0]; + final int colormapScale = (1 << bitsPerPixel); + indexColorMap = new int[colormapScale]; + for (int index = 0; index < colormapScale; index++) { + final int red = (colorMap[index] >> 8) & 0xff; + final int green = (colorMap[index + (colormapScale)] >> 8) & 0xff; + final int blue = (colorMap[index + (2 * colormapScale)] >> 8) & 0xff; + indexColorMap[index] = 0xff000000 | (red << 16) | (green << 8) + | blue; + } + + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + imageBuilder.setRGB(x, y, indexColorMap[samples[0]]); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterRgb.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterRgb.java new file mode 100644 index 0000000..8336021 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterRgb.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterRgb extends PhotometricInterpreter { + public PhotometricInterpreterRgb(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, final int width, final int height) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + final int red = samples[0]; + final int green = samples[1]; + final int blue = samples[2]; + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + imageBuilder.setRGB(x, y, rgb); + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java new file mode 100644 index 0000000..c701b86 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.photometricinterpreters; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.ImageBuilder; + +public class PhotometricInterpreterYCbCr extends PhotometricInterpreter { + + public PhotometricInterpreterYCbCr(final int samplesPerPixel, + final int[] bitsPerSample, final int predictor, + final int width, final int height) { + super(samplesPerPixel, bitsPerSample, predictor, width, height); + } + + public static int limit(final int value, final int min, final int max) { + return Math.min(max, Math.max(min, value)); + } + + /** + * This method converts a YUV (aka YCbCr) colorspace to a RGB colorspace. + * This is handy when trying to reconstruct an image in Java from YCbCr + * transmitted data. This routine expects the data to fall in the standard + * PC 0..255 range per pixel, with the array dimensions corresponding to the + * imageWidth and imageHeight. These variables are either set manually in + * the case of a null constructor, or they are automatically calculated from + * the image parameter constructor. + * + * @param Y + * The Y component set. + * @param Cb + * The Cb component set. + * @param Cr + * The Cr component set. + * @return R The R component. + */ + public static int convertYCbCrtoRGB(final int Y, final int Cb, final int Cr) { + final double r1 = (((1.164 * (Y - 16.0))) + (1.596 * (Cr - 128.0))); + final double g1 = (((1.164 * (Y - 16.0))) - (0.813 * (Cr - 128.0)) - (0.392 * (Cb - 128.0))); + final double b1 = (((1.164 * (Y - 16.0))) + (2.017 * (Cb - 128.0))); + + final int r = limit((int) r1, 0, 255); + final int g = limit((int) g1, 0, 255); + final int b = limit((int) b1, 0, 255); + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (r << 16) | (g << 8) | (b << 0); + return rgb; + } + + @Override + public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, + final int y) throws ImageReadException, IOException { + final int Y = samples[0]; + final int Cb = samples[1]; + final int Cr = samples[2]; + final double R = Y + 1.402 * (Cr - 128.0); + final double G = Y - 0.34414 * (Cb - 128.0) - 0.71414 * (Cr - 128.0); + final double B = Y + 1.772 * (Cb - 128.0); + + final int red = limit((int) R, 0, 255); + final int green = limit((int) G, 0, 255); + final int blue = limit((int) B, 0, 255); + + final int alpha = 0xff; + final int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); + imageBuilder.setRGB(x, y, rgb); + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfo.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfo.java new file mode 100644 index 0000000..a2ca53a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfo.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfo { + public static final int LENGTH_UNKNOWN = -1; + public final String name; + public final int tag; + public final List dataTypes; + public final int length; + public final TiffDirectoryType directoryType; + private final boolean isOffset; + + public TagInfo(final String name, final int tag, final FieldType dataType, final int length, + final TiffDirectoryType exifDirectory) { + this(name, tag, Arrays.asList(dataType), length, exifDirectory); + } + + public TagInfo(final String name, final int tag, final FieldType dataType, final int length, + final TiffDirectoryType exifDirectory, final boolean isOffset) { + this(name, tag, Arrays.asList(dataType), length, exifDirectory, + isOffset); + } + + public TagInfo(final String name, final int tag, final FieldType dataType, final int length) { + this(name, tag, Arrays.asList(dataType), length, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + } + + public TagInfo(final String name, final int tag, final FieldType dataType) { + this(name, tag, dataType, LENGTH_UNKNOWN, + TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN); + } + + public TagInfo(final String name, final int tag, final List dataTypes, final int length, + final TiffDirectoryType exifDirectory) { + this(name, tag, dataTypes, length, exifDirectory, false); + } + + public TagInfo(final String name, final int tag, final List dataTypes, final int length, + final TiffDirectoryType exifDirectory, final boolean isOffset) { + this.name = name; + this.tag = tag; + this.dataTypes = Collections.unmodifiableList(new ArrayList( + dataTypes)); + this.length = length; + this.directoryType = exifDirectory; + this.isOffset = isOffset; + } + + /** + * + * @param entry the TIFF field whose value to return + * @return the value of the TIFF field + * @throws ImageReadException thrown by subclasses + */ + public Object getValue(final TiffField entry) throws ImageReadException { + return entry.getFieldType().getValue(entry); + } + + public byte[] encodeValue(final FieldType fieldType, final Object value, final ByteOrder byteOrder) + throws ImageWriteException { + return fieldType.writeData(value, byteOrder); + } + + public String getDescription() { + return tag + " (0x" + Integer.toHexString(tag) + ": " + name + "): "; + } + + @Override + public String toString() { + return "[TagInfo. tag: " + tag + " (0x" + Integer.toHexString(tag) + + ", name: " + name + "]"; + } + + public boolean isOffset() { + return isOffset; + } + + public boolean isText() { + return false; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAny.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAny.java new file mode 100644 index 0000000..6fbe934 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAny.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoAny extends TagInfo { + public TagInfoAny(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, FieldType.ANY, length, directoryType); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java new file mode 100644 index 0000000..42dc7d1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoAscii extends TagInfo { + public TagInfoAscii(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, FieldType.ASCII, length, directoryType); + } + + public String[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + int nullCount = 0; + for (int i = 0; i < bytes.length - 1; i++) { + if (bytes[i] == 0) { + nullCount++; + } + } + final String[] strings = new String[nullCount + 1]; + int stringsAdded = 0; + strings[0] = ""; // if we have a 0 length string + int nextStringPos = 0; + // According to the Exiftool FAQ, http://www.metadataworkinggroup.org + // specifies that the TIFF ASCII fields are actually UTF-8. + // Exiftool however allows you to configure the charset used. + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == 0) { + try { + final String string = new String(bytes, nextStringPos, i + - nextStringPos, "UTF-8"); + strings[stringsAdded++] = string; + } catch (final UnsupportedEncodingException unsupportedEncoding) { // NOPMD + } + nextStringPos = i + 1; + } + } + if (nextStringPos < bytes.length) { + // Buggy file, string wasn't null terminated + try { + final String string = new String(bytes, nextStringPos, bytes.length + - nextStringPos, "UTF-8"); + strings[stringsAdded++] = string; + } catch (final UnsupportedEncodingException unsupportedEncoding) { // NOPMD + } + } + return strings; + } + + public byte[] encodeValue(final ByteOrder byteOrder, final String... values) + throws ImageWriteException { + return FieldType.ASCII.writeData(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrByte.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrByte.java new file mode 100644 index 0000000..3ebc48c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrByte.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoAsciiOrByte extends TagInfo { + public TagInfoAsciiOrByte(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, FieldType.ASCII_OR_BYTE, length, + directoryType, false); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrRational.java new file mode 100644 index 0000000..900ee81 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAsciiOrRational.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoAsciiOrRational extends TagInfo { + public TagInfoAsciiOrRational(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, FieldType.ASCII_OR_RATIONAL, length, + directoryType, false); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByte.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByte.java new file mode 100644 index 0000000..47da289 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByte.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; +import java.util.List; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoByte extends TagInfo { + public TagInfoByte(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.BYTE, length, directoryType); + } + + public TagInfoByte(final String name, final int tag, final List fieldTypes, + final int length, final TiffDirectoryType directoryType) { + super(name, tag, fieldTypes, length, directoryType); + } + + public TagInfoByte(final String name, final int tag, final FieldType fieldType, + final int length, final TiffDirectoryType directoryType) { + super(name, tag, fieldType, length, directoryType); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final byte... values) { + return values; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByteOrShort.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByteOrShort.java new file mode 100644 index 0000000..56fce73 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoByteOrShort.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoByteOrShort extends TagInfo { + public TagInfoByteOrShort(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.BYTE_OR_SHORT, length, directoryType); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final byte... values) { + return values; + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDirectory.java new file mode 100644 index 0000000..b2d546c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDirectory.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; + +/** + * A LONG representing an offset to a TIFF directory. + */ +public class TagInfoDirectory extends TagInfoLong { + public TagInfoDirectory(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, length, directoryType, true); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDouble.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDouble.java new file mode 100644 index 0000000..fbb45eb --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoDouble.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoDouble extends TagInfo { + public TagInfoDouble(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.DOUBLE, length, directoryType); + } + + public double[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toDoubles(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final double... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoFloat.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoFloat.java new file mode 100644 index 0000000..3b49bfc --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoFloat.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoFloat extends TagInfo { + public TagInfoFloat(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.FLOAT, length, directoryType); + } + + public float[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toFloats(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final float... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java new file mode 100644 index 0000000..b53aeef --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.util.Debug; + +/** + * Used by some GPS tags and the EXIF user comment tag, + * this badly documented value is meant to contain + * the text encoding in the first 8 bytes followed by + * the non-null-terminated text in an unknown byte order. + */ +public final class TagInfoGpsText extends TagInfo { + private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_ASCII = new TextEncoding( + new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, }, + "US-ASCII"); // ITU-T T.50 IA5 + private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_JIS = new TextEncoding( + new byte[] { 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, }, + "JIS"); // JIS X208-1990 + private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_LE = new TextEncoding( + new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00}, + "UTF-16LE"); // Unicode Standard + private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_BE = new TextEncoding( + new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00}, + "UTF-16BE"); // Unicode Standard + private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNDEFINED = new TextEncoding( + new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Try to interpret an undefined text as ISO-8859-1 (Latin) + "ISO-8859-1"); // Undefined + private static final TagInfoGpsText.TextEncoding[] TEXT_ENCODINGS = { + TEXT_ENCODING_ASCII, // + TEXT_ENCODING_JIS, // + TEXT_ENCODING_UNICODE_LE, // + TEXT_ENCODING_UNICODE_BE, // + TEXT_ENCODING_UNDEFINED, // + }; + + public TagInfoGpsText(final String name, final int tag, final int length, + final TiffDirectoryType exifDirectory) { + super(name, tag, FieldType.UNDEFINED, length, exifDirectory); + } + + @Override + public boolean isText() { + return true; + } + + private static final class TextEncoding { + public final byte[] prefix; + public final String encodingName; + + public TextEncoding(final byte[] prefix, final String encodingName) { + this.prefix = prefix; + this.encodingName = encodingName; + } + } + + @Override + public byte[] encodeValue(final FieldType fieldType, final Object value, final ByteOrder byteOrder) + throws ImageWriteException { + if (!(value instanceof String)) { + throw new ImageWriteException("GPS text value not String", value); + } + final String s = (String) value; + + try { + // try ASCII, with NO prefix. + final byte[] asciiBytes = s.getBytes(TEXT_ENCODING_ASCII.encodingName); + final String decodedAscii = new String(asciiBytes, TEXT_ENCODING_ASCII.encodingName); + if (decodedAscii.equals(s)) { + // no unicode/non-ascii values. + final byte[] result = new byte[asciiBytes.length + + TEXT_ENCODING_ASCII.prefix.length]; + System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0, + TEXT_ENCODING_ASCII.prefix.length); + System.arraycopy(asciiBytes, 0, result, + TEXT_ENCODING_ASCII.prefix.length, asciiBytes.length); + return result; + } + // use Unicode + final TextEncoding encoding; + if (byteOrder == ByteOrder.BIG_ENDIAN) { + encoding = TEXT_ENCODING_UNICODE_BE; + } else { + encoding = TEXT_ENCODING_UNICODE_LE; + } + final byte[] unicodeBytes = s.getBytes(encoding.encodingName); + final byte[] result = new byte[unicodeBytes.length + encoding.prefix.length]; + System.arraycopy(encoding.prefix, 0, result, 0, encoding.prefix.length); + System.arraycopy(unicodeBytes, 0, result, encoding.prefix.length, unicodeBytes.length); + return result; + } catch (final UnsupportedEncodingException e) { + throw new ImageWriteException(e.getMessage(), e); + } + } + + @Override + public String getValue(final TiffField entry) throws ImageReadException { + if (entry.getFieldType() == FieldType.ASCII) { + final Object object = FieldType.ASCII.getValue(entry); + if (object instanceof String) { + return (String) object; + } else if (object instanceof String[]) { + // Use of arrays with the ASCII type + // should be extremely rare, and use of + // ASCII type in GPS fields should be + // forbidden. So assume the 2 never happen + // together and return incomplete strings if they do. + return ((String[]) object)[0]; + } else { + throw new ImageReadException("Unexpected ASCII type decoded"); + } + } else if (entry.getFieldType() == FieldType.UNDEFINED) { + /* later */ + } else if (entry.getFieldType() == FieldType.BYTE) { + /* later */ + } else { + Debug.debug("entry.type: " + entry.getFieldType()); + Debug.debug("entry.directoryType: " + entry.getDirectoryType()); + Debug.debug("entry.type: " + entry.getDescriptionWithoutValue()); + Debug.debug("entry.type: " + entry.getFieldType()); + throw new ImageReadException("GPS text field not encoded as bytes."); + } + + final byte[] bytes = entry.getByteArrayValue(); + if (bytes.length < 8) { + try { + // try ASCII, with NO prefix. + return new String(bytes, "US-ASCII"); + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("GPS text field missing encoding prefix.", e); + } + } + + for (final TextEncoding encoding : TEXT_ENCODINGS) { + if (BinaryFunctions.compareBytes(bytes, 0, encoding.prefix, 0, + encoding.prefix.length)) { + try { + final String decodedString = new String( + bytes, encoding.prefix.length, + bytes.length - encoding.prefix.length, + encoding.encodingName); + final byte[] reEncodedBytes = decodedString.getBytes( + encoding.encodingName); + if (BinaryFunctions.compareBytes(bytes, encoding.prefix.length, + reEncodedBytes, 0, + reEncodedBytes.length)) { + return decodedString; + } + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException(e.getMessage(), e); + } + } + } + + try { + // try ASCII, with NO prefix. + return new String(bytes, "US-ASCII"); + } catch (final UnsupportedEncodingException e) { + throw new ImageReadException("Unknown GPS text encoding prefix.", e); + } + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLong.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLong.java new file mode 100644 index 0000000..ca03922 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLong.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + + +public class TagInfoLong extends TagInfo { + public TagInfoLong(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.LONG, length, directoryType); + } + + public TagInfoLong(final String name, final int tag, final int length, final TiffDirectoryType directoryType, final boolean isOffset) { + super(name, tag, FieldType.LONG, length, directoryType, isOffset); + } + + public int[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toInts(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final int... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLongOrIFD.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLongOrIFD.java new file mode 100644 index 0000000..2814898 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoLongOrIFD.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + + +public class TagInfoLongOrIFD extends TagInfo { + public TagInfoLongOrIFD(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.LONG_OR_IFD, length, directoryType); + } + + public TagInfoLongOrIFD(final String name, final int tag, final int length, final TiffDirectoryType directoryType, final boolean isOffset) { + super(name, tag, FieldType.LONG_OR_IFD, length, directoryType, isOffset); + } + + public int[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toInts(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final int... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java new file mode 100644 index 0000000..923e644 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoRational extends TagInfo { + public TagInfoRational(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.RATIONAL, length, directoryType); + } + + public RationalNumber[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toRationals(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCType.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSByte.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCType.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSByte.java index e6a57d5..f0d94e6 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCType.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSByte.java @@ -1,42 +1,32 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -import org.apache.sanselan.formats.jpeg.JpegConstants; - -public class IPTCType implements JpegConstants, IPTCConstants -{ - public final int type; - public final String name; - - public IPTCType(int type, String name) - { - this.type = type; - this.name = name; - } - - public String toString() - { - return name + " (" + type + ")"; - } - - public static IPTCType getUnknown(int type) - { - return new IPTCType(type, "Unknown"); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoSByte extends TagInfo { + public TagInfoSByte(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SBYTE, length, directoryType); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final byte... values) { + return values; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSLong.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSLong.java new file mode 100644 index 0000000..ede1084 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSLong.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoSLong extends TagInfo { + public TagInfoSLong(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SLONG, length, directoryType); + } + + public int[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toInts(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final int... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java new file mode 100644 index 0000000..d57b429 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoSRational extends TagInfo { + public TagInfoSRational(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SRATIONAL, length, directoryType); + } + + public RationalNumber[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toRationals(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSShort.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSShort.java new file mode 100644 index 0000000..32aa821 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSShort.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoSShort extends TagInfo { + public TagInfoSShort(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SSHORT, length, directoryType); + } + + public short[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toShorts(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShort.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShort.java new file mode 100644 index 0000000..a14bb77 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShort.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoShort extends TagInfo { + public TagInfoShort(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SHORT, length, directoryType); + } + + public short[] getValue(final ByteOrder byteOrder, final byte[] bytes) { + return ByteConversions.toShorts(bytes, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLong.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLong.java new file mode 100644 index 0000000..1b62bc7 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLong.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoShortOrLong extends TagInfo { + public TagInfoShortOrLong(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SHORT_OR_LONG, length, directoryType, false); + } + + public TagInfoShortOrLong(final String name, final int tag, final int length, final TiffDirectoryType directoryType, final boolean isOffset) { + super(name, tag, FieldType.SHORT_OR_LONG, length, directoryType, isOffset); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final int... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLongOrRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLongOrRational.java new file mode 100644 index 0000000..b99b428 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrLongOrRational.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoShortOrLongOrRational extends TagInfo { + public TagInfoShortOrLongOrRational(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SHORT_OR_LONG_OR_RATIONAL, length, directoryType); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final int... values) { + return ByteConversions.toBytes(values, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrRational.java new file mode 100644 index 0000000..167749e --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoShortOrRational.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.ByteConversions; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoShortOrRational extends TagInfo { + public TagInfoShortOrRational(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.SHORT_OR_RATIONAL, length, directoryType, false); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final short... values) { + return ByteConversions.toBytes(values, byteOrder); + } + + public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) { + return ByteConversions.toBytes(values, byteOrder); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUndefined.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUndefined.java new file mode 100644 index 0000000..a286a9a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUndefined.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +public class TagInfoUndefined extends TagInfoByte { + public TagInfoUndefined(final String name, final int tag, final int length, final TiffDirectoryType directoryType) { + super(name, tag, FieldType.UNDEFINED, length, directoryType); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUnknown.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUnknown.java new file mode 100644 index 0000000..9dcf100 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoUnknown.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +/** + * A TIFF tag whose definition isn't known. + */ +public final class TagInfoUnknown extends TagInfoByte { + public TagInfoUnknown(final String name, final int tag, final int length, final TiffDirectoryType exifDirectory) { + super(name, tag, FieldType.ANY, length, exifDirectory); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoXpString.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoXpString.java new file mode 100644 index 0000000..25c05ca --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoXpString.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.taginfos; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +/** + * Windows XP onwards store some tags using UTF-16LE, but the field type is byte + * - here we deal with this. + */ +public class TagInfoXpString extends TagInfo { + public TagInfoXpString(final String name, final int tag, final int length, + final TiffDirectoryType directoryType) { + super(name, tag, FieldType.BYTE, length, directoryType); + } + + @Override + public byte[] encodeValue(final FieldType fieldType, final Object value, final ByteOrder byteOrder) + throws ImageWriteException { + if (!(value instanceof String)) { + throw new ImageWriteException("Text value not String", value); + } + final String s = (String) value; + try { + return s.getBytes("UTF-16LE"); + } catch (final UnsupportedEncodingException cannotHappen) { + return null; + } + } + + @Override + public String getValue(final TiffField entry) throws ImageReadException { + if (entry.getFieldType() != FieldType.BYTE) { + throw new ImageReadException("Text field not encoded as bytes."); + } + try { + return new String(entry.getByteArrayValue(), "UTF-16LE"); + } catch (final UnsupportedEncodingException cannotHappen) { + return null; + } + } +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/ImageDataOffsets.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java similarity index 65% rename from src/main/java/org/apache/sanselan/formats/tiff/write/ImageDataOffsets.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java index 45cab08..4481705 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/ImageDataOffsets.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java @@ -1,44 +1,41 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import org.apache.sanselan.formats.tiff.TiffElement; - -class ImageDataOffsets -{ - public final int imageDataOffsets[]; - public final TiffOutputField imageDataOffsetsField; - public final TiffOutputItem outputItems[]; - - public ImageDataOffsets(final TiffElement.DataElement imageData[], - final int[] imageDataOffsets, - final TiffOutputField imageDataOffsetsField) - { - this.imageDataOffsets = imageDataOffsets; - this.imageDataOffsetsField = imageDataOffsetsField; - - outputItems = new TiffOutputItem[imageData.length]; - for (int i = 0; i < imageData.length; i++) - { - TiffOutputItem item = new TiffOutputItem.Value("TIFF image data", - imageData[i].data); - outputItems[i] = item; - } - - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import org.apache.commons.imaging.formats.tiff.TiffElement; + +class ImageDataOffsets { + final int[] imageDataOffsets; + final TiffOutputField imageDataOffsetsField; + final TiffOutputItem[] outputItems; + + ImageDataOffsets(final TiffElement.DataElement[] imageData, + final int[] imageDataOffsets, + final TiffOutputField imageDataOffsetsField) { + this.imageDataOffsets = imageDataOffsets; + this.imageDataOffsetsField = imageDataOffsetsField; + + outputItems = new TiffOutputItem[imageData.length]; + for (int i = 0; i < imageData.length; i++) { + final TiffOutputItem item = new TiffOutputItem.Value("TIFF image data", + imageData[i].data); + outputItems[i] = item; + } + + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java new file mode 100644 index 0000000..4c7616a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterBase.java @@ -0,0 +1,595 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.PixelDensity; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.PackBits; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.common.itu_t4.T4AndT6Compression; +import org.apache.commons.imaging.common.mylzw.MyLzwCompressor; +import org.apache.commons.imaging.formats.tiff.TiffElement; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public abstract class TiffImageWriterBase { + + protected final ByteOrder byteOrder; + + public TiffImageWriterBase() { + this.byteOrder = DEFAULT_TIFF_BYTE_ORDER; + } + + public TiffImageWriterBase(final ByteOrder byteOrder) { + this.byteOrder = byteOrder; + } + + protected static int imageDataPaddingLength(final int dataLength) { + return (4 - (dataLength % 4)) % 4; + } + + public abstract void write(OutputStream os, TiffOutputSet outputSet) + throws IOException, ImageWriteException; + + protected TiffOutputSummary validateDirectories(final TiffOutputSet outputSet) + throws ImageWriteException { + final List directories = outputSet.getDirectories(); + + if (directories.isEmpty()) { + throw new ImageWriteException("No directories."); + } + + TiffOutputDirectory exifDirectory = null; + TiffOutputDirectory gpsDirectory = null; + TiffOutputDirectory interoperabilityDirectory = null; + TiffOutputField exifDirectoryOffsetField = null; + TiffOutputField gpsDirectoryOffsetField = null; + TiffOutputField interoperabilityDirectoryOffsetField = null; + + final List directoryIndices = new ArrayList(); + final Map directoryTypeMap = new HashMap(); + for (TiffOutputDirectory directory : directories) { + final int dirType = directory.type; + directoryTypeMap.put(dirType, directory); + // Debug.debug("validating dirType", dirType + " (" + // + directory.getFields().size() + " fields)"); + + if (dirType < 0) { + switch (dirType) { + case DIRECTORY_TYPE_EXIF: + if (exifDirectory != null) { + throw new ImageWriteException( + "More than one EXIF directory."); + } + exifDirectory = directory; + break; + + case DIRECTORY_TYPE_GPS: + if (gpsDirectory != null) { + throw new ImageWriteException( + "More than one GPS directory."); + } + gpsDirectory = directory; + break; + + case DIRECTORY_TYPE_INTEROPERABILITY: + if (interoperabilityDirectory != null) { + throw new ImageWriteException( + "More than one Interoperability directory."); + } + interoperabilityDirectory = directory; + break; + default: + throw new ImageWriteException("Unknown directory: " + + dirType); + } + } else { + if (directoryIndices.contains(dirType)) { + throw new ImageWriteException( + "More than one directory with index: " + dirType + + "."); + } + directoryIndices.add(dirType); + // dirMap.put(arg0, arg1) + } + + final HashSet fieldTags = new HashSet(); + final List fields = directory.getFields(); + for (TiffOutputField field : fields) { + if (fieldTags.contains(field.tag)) { + throw new ImageWriteException("Tag (" + + field.tagInfo.getDescription() + + ") appears twice in directory."); + } + fieldTags.add(field.tag); + + if (field.tag == ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag) { + if (exifDirectoryOffsetField != null) { + throw new ImageWriteException( + "More than one Exif directory offset field."); + } + exifDirectoryOffsetField = field; + } else if (field.tag == ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag) { + if (interoperabilityDirectoryOffsetField != null) { + throw new ImageWriteException( + "More than one Interoperability directory offset field."); + } + interoperabilityDirectoryOffsetField = field; + } else if (field.tag == ExifTagConstants.EXIF_TAG_GPSINFO.tag) { + if (gpsDirectoryOffsetField != null) { + throw new ImageWriteException( + "More than one GPS directory offset field."); + } + gpsDirectoryOffsetField = field; + } + } + // directory. + } + + if (directoryIndices.isEmpty()) { + throw new ImageWriteException("Missing root directory."); + } + + // "normal" TIFF directories should have continous indices starting with + // 0, ie. 0, 1, 2... + Collections.sort(directoryIndices); + + TiffOutputDirectory previousDirectory = null; + for (int i = 0; i < directoryIndices.size(); i++) { + final Integer index = directoryIndices.get(i); + if (index != i) { + throw new ImageWriteException("Missing directory: " + i + "."); + } + + // set up chain of directory references for "normal" directories. + final TiffOutputDirectory directory = directoryTypeMap.get(index); + if (null != previousDirectory) { + previousDirectory.setNextDirectory(directory); + } + previousDirectory = directory; + } + + final TiffOutputDirectory rootDirectory = directoryTypeMap + .get(DIRECTORY_TYPE_ROOT); + + // prepare results + final TiffOutputSummary result = new TiffOutputSummary(byteOrder, + rootDirectory, directoryTypeMap); + + if (interoperabilityDirectory == null + && interoperabilityDirectoryOffsetField != null) { + // perhaps we should just discard field? + throw new ImageWriteException( + "Output set has Interoperability Directory Offset field, but no Interoperability Directory"); + } else if (interoperabilityDirectory != null) { + if (exifDirectory == null) { + exifDirectory = outputSet.addExifDirectory(); + } + + if (interoperabilityDirectoryOffsetField == null) { + interoperabilityDirectoryOffsetField = + TiffOutputField.createOffsetField( + ExifTagConstants.EXIF_TAG_INTEROP_OFFSET, + byteOrder); + exifDirectory.add(interoperabilityDirectoryOffsetField); + } + + result.add(interoperabilityDirectory, + interoperabilityDirectoryOffsetField); + } + + // make sure offset fields and offset'd directories correspond. + if (exifDirectory == null && exifDirectoryOffsetField != null) { + // perhaps we should just discard field? + throw new ImageWriteException( + "Output set has Exif Directory Offset field, but no Exif Directory"); + } else if (exifDirectory != null) { + if (exifDirectoryOffsetField == null) { + exifDirectoryOffsetField = TiffOutputField.createOffsetField( + ExifTagConstants.EXIF_TAG_EXIF_OFFSET, byteOrder); + rootDirectory.add(exifDirectoryOffsetField); + } + + result.add(exifDirectory, exifDirectoryOffsetField); + } + + if (gpsDirectory == null && gpsDirectoryOffsetField != null) { + // perhaps we should just discard field? + throw new ImageWriteException( + "Output set has GPS Directory Offset field, but no GPS Directory"); + } else if (gpsDirectory != null) { + if (gpsDirectoryOffsetField == null) { + gpsDirectoryOffsetField = TiffOutputField.createOffsetField( + ExifTagConstants.EXIF_TAG_GPSINFO, byteOrder); + rootDirectory.add(gpsDirectoryOffsetField); + } + + result.add(gpsDirectory, gpsDirectoryOffsetField); + } + + return result; + + // Debug.debug(); + } + + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + TiffOutputSet userExif = null; + if (params.containsKey(PARAM_KEY_EXIF)) { + userExif = (TiffOutputSet) params.remove(PARAM_KEY_EXIF); + } + + String xmpXml = null; + if (params.containsKey(PARAM_KEY_XMP_XML)) { + xmpXml = (String) params.get(PARAM_KEY_XMP_XML); + params.remove(PARAM_KEY_XMP_XML); + } + + PixelDensity pixelDensity = (PixelDensity) params + .remove(PARAM_KEY_PIXEL_DENSITY); + if (pixelDensity == null) { + pixelDensity = PixelDensity.createFromPixelsPerInch(72, 72); + } + + final int width = src.getWidth(); + final int height = src.getHeight(); + + int compression = TIFF_COMPRESSION_LZW; // LZW is default + if (params.containsKey(PARAM_KEY_COMPRESSION)) { + final Object value = params.get(PARAM_KEY_COMPRESSION); + if (value != null) { + if (!(value instanceof Number)) { + throw new ImageWriteException( + "Invalid compression parameter: " + value); + } + compression = ((Number) value).intValue(); + } + params.remove(PARAM_KEY_COMPRESSION); + } + final HashMap rawParams = new HashMap(params); + params.remove(PARAM_KEY_T4_OPTIONS); + params.remove(PARAM_KEY_T6_OPTIONS); + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + int samplesPerPixel; + int bitsPerSample; + int photometricInterpretation; + if (compression == TIFF_COMPRESSION_CCITT_1D + || compression == TIFF_COMPRESSION_CCITT_GROUP_3 + || compression == TIFF_COMPRESSION_CCITT_GROUP_4) { + samplesPerPixel = 1; + bitsPerSample = 1; + photometricInterpretation = 0; + } else { + samplesPerPixel = 3; + bitsPerSample = 8; + photometricInterpretation = 2; + } + + int rowsPerStrip = 64000 / (width * bitsPerSample * samplesPerPixel); // TODO: + rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one. + + final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip); + + // System.out.println("width: " + width); + // System.out.println("height: " + height); + // System.out.println("fRowsPerStrip: " + fRowsPerStrip); + // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel); + // System.out.println("stripCount: " + stripCount); + + int t4Options = 0; + int t6Options = 0; + if (compression == TIFF_COMPRESSION_CCITT_1D) { + for (int i = 0; i < strips.length; i++) { + strips[i] = T4AndT6Compression.compressModifiedHuffman( + strips[i], width, strips[i].length / ((width + 7) / 8)); + } + } else if (compression == TIFF_COMPRESSION_CCITT_GROUP_3) { + final Integer t4Parameter = (Integer) rawParams.get(PARAM_KEY_T4_OPTIONS); + if (t4Parameter != null) { + t4Options = t4Parameter.intValue(); + } + t4Options &= 0x7; + final boolean is2D = (t4Options & 1) != 0; + final boolean usesUncompressedMode = (t4Options & 2) != 0; + if (usesUncompressedMode) { + throw new ImageWriteException( + "T.4 compression with the uncompressed mode extension is not yet supported"); + } + final boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0; + for (int i = 0; i < strips.length; i++) { + if (is2D) { + strips[i] = T4AndT6Compression.compressT4_2D(strips[i], + width, strips[i].length / ((width + 7) / 8), + hasFillBitsBeforeEOL, rowsPerStrip); + } else { + strips[i] = T4AndT6Compression.compressT4_1D(strips[i], + width, strips[i].length / ((width + 7) / 8), + hasFillBitsBeforeEOL); + } + } + } else if (compression == TIFF_COMPRESSION_CCITT_GROUP_4) { + final Integer t6Parameter = (Integer) rawParams.get(PARAM_KEY_T6_OPTIONS); + if (t6Parameter != null) { + t6Options = t6Parameter.intValue(); + } + t6Options &= 0x4; + final boolean usesUncompressedMode = (t6Options & TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0; + if (usesUncompressedMode) { + throw new ImageWriteException( + "T.6 compression with the uncompressed mode extension is not yet supported"); + } + for (int i = 0; i < strips.length; i++) { + strips[i] = T4AndT6Compression.compressT6(strips[i], width, + strips[i].length / ((width + 7) / 8)); + } + } else if (compression == TIFF_COMPRESSION_PACKBITS) { + for (int i = 0; i < strips.length; i++) { + strips[i] = new PackBits().compress(strips[i]); + } + } else if (compression == TIFF_COMPRESSION_LZW) { + for (int i = 0; i < strips.length; i++) { + final byte[] uncompressed = strips[i]; + + final int LZW_MINIMUM_CODE_SIZE = 8; + + final MyLzwCompressor compressor = new MyLzwCompressor( + LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN, true); + final byte[] compressed = compressor.compress(uncompressed); + + strips[i] = compressed; + } + } else if (compression == TIFF_COMPRESSION_UNCOMPRESSED) { + // do nothing. + } else { + throw new ImageWriteException( + "Invalid compression parameter (Only CCITT 1D/Group 3/Group 4, LZW, Packbits and uncompressed supported)."); + } + + final TiffElement.DataElement[] imageData = new TiffElement.DataElement[strips.length]; + for (int i = 0; i < strips.length; i++) { + imageData[i] = new TiffImageData.Data(0, strips[i].length, strips[i]); + } + + final TiffOutputSet outputSet = new TiffOutputSet(byteOrder); + final TiffOutputDirectory directory = outputSet.addRootDirectory(); + + // WriteField stripOffsetsField; + + { + + directory.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width); + directory.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height); + directory.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, + (short) photometricInterpretation); + directory.add(TiffTagConstants.TIFF_TAG_COMPRESSION, + (short) compression); + directory.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, + (short) samplesPerPixel); + + if (samplesPerPixel == 3) { + directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, + (short) bitsPerSample, (short) bitsPerSample, + (short) bitsPerSample); + } else if (samplesPerPixel == 1) { + directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, + (short) bitsPerSample); + } + // { + // stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS, + // FIELD_TYPE_LONG, stripOffsets.length, FIELD_TYPE_LONG + // .writeData(stripOffsets, byteOrder)); + // directory.add(stripOffsetsField); + // } + // { + // WriteField field = new WriteField(TIFF_TAG_STRIP_BYTE_COUNTS, + // FIELD_TYPE_LONG, stripByteCounts.length, + // FIELD_TYPE_LONG.writeData(stripByteCounts, + // WRITE_BYTE_ORDER)); + // directory.add(field); + // } + directory.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, + rowsPerStrip); + if (pixelDensity.isUnitless()) { + directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, + (short) 0); + directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, + RationalNumber.valueOf(pixelDensity.getRawHorizontalDensity())); + directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, + RationalNumber.valueOf(pixelDensity.getRawVerticalDensity())); + } else if (pixelDensity.isInInches()) { + directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, + (short) 2); + directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, + RationalNumber.valueOf(pixelDensity.horizontalDensityInches())); + directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, + RationalNumber.valueOf(pixelDensity.verticalDensityInches())); + } else { + directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, + (short) 1); + directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, + RationalNumber.valueOf(pixelDensity.horizontalDensityCentimetres())); + directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, + RationalNumber.valueOf(pixelDensity.verticalDensityCentimetres())); + } + if (t4Options != 0) { + directory.add(TiffTagConstants.TIFF_TAG_T4_OPTIONS, t4Options); + } + if (t6Options != 0) { + directory.add(TiffTagConstants.TIFF_TAG_T6_OPTIONS, t6Options); + } + + if (null != xmpXml) { + final byte[] xmpXmlBytes = xmpXml.getBytes("utf-8"); + directory.add(TiffTagConstants.TIFF_TAG_XMP, xmpXmlBytes); + } + + } + + final TiffImageData tiffImageData = new TiffImageData.Strips(imageData, + rowsPerStrip); + directory.setTiffImageData(tiffImageData); + + if (userExif != null) { + combineUserExifIntoFinalExif(userExif, outputSet); + } + + write(os, outputSet); + } + + private void combineUserExifIntoFinalExif(final TiffOutputSet userExif, + final TiffOutputSet outputSet) throws ImageWriteException { + final List outputDirectories = outputSet + .getDirectories(); + Collections.sort(outputDirectories, TiffOutputDirectory.COMPARATOR); + for (final TiffOutputDirectory userDirectory : userExif.getDirectories()) { + final int location = Collections.binarySearch(outputDirectories, + userDirectory, TiffOutputDirectory.COMPARATOR); + if (location < 0) { + outputSet.addDirectory(userDirectory); + } else { + final TiffOutputDirectory outputDirectory = outputDirectories + .get(location); + for (final TiffOutputField userField : userDirectory.getFields()) { + if (outputDirectory.findField(userField.tagInfo) == null) { + outputDirectory.add(userField); + } + } + } + } + } + + private byte[][] getStrips(final BufferedImage src, final int samplesPerPixel, + final int bitsPerSample, final int rowsPerStrip) { + final int width = src.getWidth(); + final int height = src.getHeight(); + + final int stripCount = (height + rowsPerStrip - 1) / rowsPerStrip; + + byte[][] result; + { // Write Strips + result = new byte[stripCount][]; + + int remainingRows = height; + + for (int i = 0; i < stripCount; i++) { + final int rowsInStrip = Math.min(rowsPerStrip, remainingRows); + remainingRows -= rowsInStrip; + + final int bitsInRow = bitsPerSample * samplesPerPixel * width; + final int bytesPerRow = (bitsInRow + 7) / 8; + final int bytesInStrip = rowsInStrip * bytesPerRow; + + final byte[] uncompressed = new byte[bytesInStrip]; + + int counter = 0; + int y = i * rowsPerStrip; + final int stop = i * rowsPerStrip + rowsPerStrip; + + for (; (y < height) && (y < stop); y++) { + int bitCache = 0; + int bitsInCache = 0; + for (int x = 0; x < width; x++) { + final int rgb = src.getRGB(x, y); + final int red = 0xff & (rgb >> 16); + final int green = 0xff & (rgb >> 8); + final int blue = 0xff & (rgb >> 0); + + if (bitsPerSample == 1) { + int sample = (red + green + blue) / 3; + if (sample > 127) { + sample = 0; + } else { + sample = 1; + } + bitCache <<= 1; + bitCache |= sample; + bitsInCache++; + if (bitsInCache == 8) { + uncompressed[counter++] = (byte) bitCache; + bitCache = 0; + bitsInCache = 0; + } + } else { + uncompressed[counter++] = (byte) red; + uncompressed[counter++] = (byte) green; + uncompressed[counter++] = (byte) blue; + } + } + if (bitsInCache > 0) { + bitCache <<= (8 - bitsInCache); + uncompressed[counter++] = (byte) bitCache; + } + } + + result[i] = uncompressed; + } + + } + + return result; + } + + protected void writeImageFileHeader(final BinaryOutputStream bos) + throws IOException { + final int offsetToFirstIFD = TIFF_HEADER_SIZE; + + writeImageFileHeader(bos, offsetToFirstIFD); + } + + protected void writeImageFileHeader(final BinaryOutputStream bos, + final long offsetToFirstIFD) throws IOException { + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + bos.write('I'); + bos.write('I'); + } else { + bos.write('M'); + bos.write('M'); + } + + bos.write2Bytes(42); // tiffVersion + + bos.write4Bytes((int) offsetToFirstIFD); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossless.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossless.java new file mode 100644 index 0000000..24c44e1 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossless.java @@ -0,0 +1,342 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.FormatCompliance; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.formats.tiff.JpegImageData; +import org.apache.commons.imaging.formats.tiff.TiffContents; +import org.apache.commons.imaging.formats.tiff.TiffDirectory; +import org.apache.commons.imaging.formats.tiff.TiffElement; +import org.apache.commons.imaging.formats.tiff.TiffElement.DataElement; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.TiffReader; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public class TiffImageWriterLossless extends TiffImageWriterBase { + private final byte[] exifBytes; + private static final Comparator ELEMENT_SIZE_COMPARATOR = new Comparator() { + public int compare(final TiffElement e1, final TiffElement e2) { + return e1.length - e2.length; + } + }; + private static final Comparator ITEM_SIZE_COMPARATOR = new Comparator() { + public int compare(final TiffOutputItem e1, final TiffOutputItem e2) { + return e1.getItemLength() - e2.getItemLength(); + } + }; + + public TiffImageWriterLossless(final byte[] exifBytes) { + this.exifBytes = exifBytes; + } + + public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) { + super(byteOrder); + this.exifBytes = exifBytes; + } + + private List analyzeOldTiff(final Map frozenFields) throws ImageWriteException, + IOException { + try { + final ByteSource byteSource = new ByteSourceArray(exifBytes); + final Map params = null; + final FormatCompliance formatCompliance = FormatCompliance.getDefault(); + final TiffContents contents = new TiffReader(false).readContents( + byteSource, params, formatCompliance); + + final List elements = new ArrayList(); + // result.add(contents.header); // ? + + final List directories = contents.directories; + for (TiffDirectory directory : directories) { + elements.add(directory); + + final List fields = directory.getDirectoryEntries(); + for (TiffField field : fields) { + final TiffElement oversizeValue = field.getOversizeValueElement(); + if (oversizeValue != null) { + final TiffOutputField frozenField = frozenFields.get(field.getTag()); + if (frozenField != null + && frozenField.getSeperateValue() != null + && frozenField.bytesEqual(field.getByteArrayValue())) { + frozenField.getSeperateValue().setOffset(field.getOffset()); + } else { + elements.add(oversizeValue); + } + } + } + + final JpegImageData jpegImageData = directory.getJpegImageData(); + if (jpegImageData != null) { + elements.add(jpegImageData); + } + + final TiffImageData tiffImageData = directory.getTiffImageData(); + if (tiffImageData != null) { + final DataElement[] data = tiffImageData.getImageData(); + Collections.addAll(elements, data); + } + } + + Collections.sort(elements, TiffElement.COMPARATOR); + + // dumpElements(byteSource, elements); + + final List rewritableElements = new ArrayList(); + { + final int TOLERANCE = 3; + // int last = TIFF_HEADER_SIZE; + TiffElement start = null; + long index = -1; + for (TiffElement element : elements) { + final long lastElementByte = element.offset + element.length; + if (start == null) { + start = element; + index = lastElementByte; + } else if (element.offset - index > TOLERANCE) { + rewritableElements.add(new TiffElement.Stub(start.offset, + (int) (index - start.offset))); + start = element; + index = lastElementByte; + } else { + index = lastElementByte; + } + } + if (null != start) { + rewritableElements.add(new TiffElement.Stub(start.offset, + (int) (index - start.offset))); + } + } + + // dumpElements(byteSource, result); + + return rewritableElements; + } catch (final ImageReadException e) { + throw new ImageWriteException(e.getMessage(), e); + } + } + + @Override + public void write(final OutputStream os, final TiffOutputSet outputSet) + throws IOException, ImageWriteException { + // There are some fields whose address in the file must not change, + // unless of course their value is changed. + final Map frozenFields = new HashMap(); + final TiffOutputField makerNoteField = outputSet.findField(EXIF_TAG_MAKER_NOTE); + if (makerNoteField != null && makerNoteField.getSeperateValue() != null) { + frozenFields.put(EXIF_TAG_MAKER_NOTE.tag, makerNoteField); + } + final List analysis = analyzeOldTiff(frozenFields); + final int oldLength = exifBytes.length; + if (analysis.isEmpty()) { + throw new ImageWriteException("Couldn't analyze old tiff data."); + } else if (analysis.size() == 1) { + final TiffElement onlyElement = analysis.get(0); + if (onlyElement.offset == TIFF_HEADER_SIZE + && onlyElement.offset + onlyElement.length + + TIFF_HEADER_SIZE == oldLength) { + // no gaps in old data, safe to complete overwrite. + new TiffImageWriterLossy(byteOrder).write(os, outputSet); + return; + } + } + final Map frozenFieldOffsets = new HashMap(); + for (final Map.Entry entry : frozenFields.entrySet()) { + final TiffOutputField frozenField = entry.getValue(); + if (frozenField.getSeperateValue().getOffset() != TiffOutputItem.UNDEFINED_VALUE) { + frozenFieldOffsets.put(frozenField.getSeperateValue().getOffset(), frozenField); + } + } + + final TiffOutputSummary outputSummary = validateDirectories(outputSet); + + final List allOutputItems = outputSet + .getOutputItems(outputSummary); + final List outputItems = new ArrayList(); + for (final TiffOutputItem outputItem : allOutputItems) { + if (!frozenFieldOffsets.containsKey(outputItem.getOffset())) { + outputItems.add(outputItem); + } + } + + final long outputLength = updateOffsetsStep(analysis, outputItems); + + outputSummary.updateOffsets(byteOrder); + + writeStep(os, outputSet, analysis, outputItems, outputLength); + + } + + private long updateOffsetsStep(final List analysis, + final List outputItems) { + // items we cannot fit into a gap, we shall append to tail. + long overflowIndex = exifBytes.length; + + // make copy. + final List unusedElements = new ArrayList(analysis); + + // should already be in order of offset, but make sure. + Collections.sort(unusedElements, TiffElement.COMPARATOR); + Collections.reverse(unusedElements); + // any items that represent a gap at the end of the exif segment, can be + // discarded. + while (!unusedElements.isEmpty()) { + final TiffElement element = unusedElements.get(0); + final long elementEnd = element.offset + element.length; + if (elementEnd == overflowIndex) { + // discarding a tail element. should only happen once. + overflowIndex -= element.length; + unusedElements.remove(0); + } else { + break; + } + } + + Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR); + Collections.reverse(unusedElements); + + // make copy. + final List unplacedItems = new ArrayList( + outputItems); + Collections.sort(unplacedItems, ITEM_SIZE_COMPARATOR); + Collections.reverse(unplacedItems); + + while (!unplacedItems.isEmpty()) { + // pop off largest unplaced item. + final TiffOutputItem outputItem = unplacedItems.remove(0); + final int outputItemLength = outputItem.getItemLength(); + // search for the smallest possible element large enough to hold the + // item. + TiffElement bestFit = null; + for (TiffElement element : unusedElements) { + if (element.length >= outputItemLength) { + bestFit = element; + } else { + break; + } + } + if (null == bestFit) { + // we couldn't place this item. overflow. + outputItem.setOffset(overflowIndex); + overflowIndex += outputItemLength; + } else { + outputItem.setOffset(bestFit.offset); + unusedElements.remove(bestFit); + + if (bestFit.length > outputItemLength) { + // not a perfect fit. + final long excessOffset = bestFit.offset + outputItemLength; + final int excessLength = bestFit.length - outputItemLength; + unusedElements.add(new TiffElement.Stub(excessOffset, + excessLength)); + // make sure the new element is in the correct order. + Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR); + Collections.reverse(unusedElements); + } + } + } + + return overflowIndex; + } + + private static class BufferOutputStream extends OutputStream { + private final byte[] buffer; + private int index; + + public BufferOutputStream(final byte[] buffer, final int index) { + this.buffer = buffer; + this.index = index; + } + + @Override + public void write(final int b) throws IOException { + if (index >= buffer.length) { + throw new IOException("Buffer overflow."); + } + + buffer[index++] = (byte) b; + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + if (index + len > buffer.length) { + throw new IOException("Buffer overflow."); + } + System.arraycopy(b, off, buffer, index, len); + index += len; + } + } + + private void writeStep(final OutputStream os, final TiffOutputSet outputSet, + final List analysis, final List outputItems, + final long outputLength) throws IOException, ImageWriteException { + final TiffOutputDirectory rootDirectory = outputSet.getRootDirectory(); + + final byte[] output = new byte[(int) outputLength]; + + // copy old data (including maker notes, etc.) + System.arraycopy(exifBytes, 0, output, 0, + Math.min(exifBytes.length, output.length)); + + // bos.write(exifBytes, TIFF_HEADER_SIZE, exifBytes.length + // - TIFF_HEADER_SIZE); + + { + final BufferOutputStream tos = new BufferOutputStream(output, 0); + final BinaryOutputStream bos = new BinaryOutputStream(tos, byteOrder); + writeImageFileHeader(bos, rootDirectory.getOffset()); + } + + // zero out the parsed pieces of old exif segment, in case we don't + // overwrite them. + for (TiffElement element : analysis) { + for (int j = 0; j < element.length; j++) { + final int index = (int) (element.offset + j); + if (index < output.length) { + output[index] = 0; + } + } + } + + // write in the new items + for (TiffOutputItem outputItem : outputItems) { + final BufferOutputStream tos = new BufferOutputStream(output, + (int) outputItem.getOffset()); + final BinaryOutputStream bos = new BinaryOutputStream(tos, byteOrder); + outputItem.writeItem(bos); + } + + os.write(output); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossy.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossy.java new file mode 100644 index 0000000..c77e735 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffImageWriterLossy.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public class TiffImageWriterLossy extends TiffImageWriterBase { + + public TiffImageWriterLossy() { + // with default byte order + } + + public TiffImageWriterLossy(final ByteOrder byteOrder) { + super(byteOrder); + } + + @Override + public void write(final OutputStream os, final TiffOutputSet outputSet) + throws IOException, ImageWriteException { + final TiffOutputSummary outputSummary = validateDirectories(outputSet); + + final List outputItems = outputSet + .getOutputItems(outputSummary); + + updateOffsetsStep(outputItems); + + outputSummary.updateOffsets(byteOrder); + + final BinaryOutputStream bos = new BinaryOutputStream(os, byteOrder); + + writeStep(bos, outputItems); + } + + private void updateOffsetsStep(final List outputItems) { + int offset = TIFF_HEADER_SIZE; + + for (TiffOutputItem outputItem : outputItems) { + outputItem.setOffset(offset); + final int itemLength = outputItem.getItemLength(); + offset += itemLength; + + final int remainder = imageDataPaddingLength(itemLength); + offset += remainder; + } + } + + private void writeStep(final BinaryOutputStream bos, + final List outputItems) throws IOException, + ImageWriteException { + writeImageFileHeader(bos); + + for (TiffOutputItem outputItem : outputItems) { + outputItem.writeItem(bos); + + final int length = outputItem.getItemLength(); + + final int remainder = imageDataPaddingLength(length); + for (int j = 0; j < remainder; j++) { + bos.write(0); + } + } + + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java new file mode 100644 index 0000000..7693274 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java @@ -0,0 +1,637 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.JpegImageData; +import org.apache.commons.imaging.formats.tiff.TiffDirectory; +import org.apache.commons.imaging.formats.tiff.TiffElement; +import org.apache.commons.imaging.formats.tiff.TiffImageData; +import org.apache.commons.imaging.formats.tiff.constants.TagConstantsUtils; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAsciiOrByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAsciiOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByteOrShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSByte; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLongOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrRational; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoXpString; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public final class TiffOutputDirectory extends TiffOutputItem { + public final int type; + private final List fields = new ArrayList(); + private final ByteOrder byteOrder; + private TiffOutputDirectory nextDirectory; + public static final Comparator COMPARATOR = new Comparator() { + public int compare(final TiffOutputDirectory o1, final TiffOutputDirectory o2) { + if (o1.type < o2.type) { + return -1; + } else if (o1.type > o2.type) { + return 1; + } else { + return 0; + } + } + }; + private JpegImageData jpegImageData; + private TiffImageData tiffImageData; + + public void setNextDirectory(final TiffOutputDirectory nextDirectory) { + this.nextDirectory = nextDirectory; + } + + public TiffOutputDirectory(final int type, final ByteOrder byteOrder) { + this.type = type; + this.byteOrder = byteOrder; + } + + public void add(final TagInfoByte tagInfo, final byte... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.BYTE, values.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoAscii tagInfo, final String... values) + throws ImageWriteException { + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + if (tagInfo.length > 0 && tagInfo.length != bytes.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " byte(s), not " + values.length); + } + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.ASCII, bytes.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShort tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoLong tagInfo, final int... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.LONG, values.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoRational tagInfo, final RationalNumber... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.RATIONAL, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoSByte tagInfo, final byte... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SBYTE, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoSShort tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SSHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoSLong tagInfo, final int... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SLONG, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoSRational tagInfo, final RationalNumber... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SRATIONAL, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoFloat tagInfo, final float... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.FLOAT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoDouble tagInfo, final double... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.DOUBLE, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoByteOrShort tagInfo, final byte... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.BYTE, values.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoByteOrShort tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrLong tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrLong tagInfo, final int... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.LONG, values.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrLongOrRational tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrLongOrRational tagInfo, final int... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.LONG, values.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrLongOrRational tagInfo, + final RationalNumber... values) throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.RATIONAL, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrRational tagInfo, final short... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.SHORT, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoShortOrRational tagInfo, final RationalNumber... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue(byteOrder, values); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.RATIONAL, + values.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoGpsText tagInfo, final String value) + throws ImageWriteException { + final byte[] bytes = tagInfo.encodeValue( + FieldType.UNDEFINED, value, byteOrder); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, tagInfo.dataTypes.get(0), bytes.length, bytes); + add(tiffOutputField); + } + + public void add(final TagInfoXpString tagInfo, final String value) + throws ImageWriteException { + final byte[] bytes = tagInfo.encodeValue( + FieldType.BYTE, value, byteOrder); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.BYTE, bytes.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoAsciiOrByte tagInfo, final String... values) + throws ImageWriteException { + final byte[] bytes = tagInfo.encodeValue( + FieldType.ASCII, values, byteOrder); + if (tagInfo.length > 0 && tagInfo.length != bytes.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " byte(s), not " + values.length); + } + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.ASCII, bytes.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoAsciiOrRational tagInfo, final String... values) + throws ImageWriteException { + final byte[] bytes = tagInfo.encodeValue( + FieldType.ASCII, values, byteOrder); + if (tagInfo.length > 0 && tagInfo.length != bytes.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " byte(s), not " + values.length); + } + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.ASCII, bytes.length, + bytes); + add(tiffOutputField); + } + + public void add(final TagInfoAsciiOrRational tagInfo, final RationalNumber... values) + throws ImageWriteException { + if (tagInfo.length > 0 && tagInfo.length != values.length) { + throw new ImageWriteException("Tag expects " + tagInfo.length + + " value(s), not " + values.length); + } + final byte[] bytes = tagInfo.encodeValue( + FieldType.RATIONAL, values, byteOrder); + final TiffOutputField tiffOutputField = new TiffOutputField(tagInfo.tag, + tagInfo, FieldType.RATIONAL, + bytes.length, bytes); + add(tiffOutputField); + } + + public void add(final TiffOutputField field) { + fields.add(field); + } + + public List getFields() { + return new ArrayList(fields); + } + + public void removeField(final TagInfo tagInfo) { + removeField(tagInfo.tag); + } + + public void removeField(final int tag) { + final List matches = new ArrayList(); + for (TiffOutputField field : fields) { + if (field.tag == tag) { + matches.add(field); + } + } + fields.removeAll(matches); + } + + public TiffOutputField findField(final TagInfo tagInfo) { + return findField(tagInfo.tag); + } + + public TiffOutputField findField(final int tag) { + for (TiffOutputField field : fields) { + if (field.tag == tag) { + return field; + } + } + return null; + } + + public void sortFields() { + final Comparator comparator = new Comparator() { + public int compare(final TiffOutputField e1, final TiffOutputField e2) { + if (e1.tag != e2.tag) { + return e1.tag - e2.tag; + } + return e1.getSortHint() - e2.getSortHint(); + } + }; + Collections.sort(fields, comparator); + } + + public String description() { + return TiffDirectory.description(type); + } + + @Override + public void writeItem(final BinaryOutputStream bos) throws IOException, + ImageWriteException { + // Write Directory Field Count + bos.write2Bytes(fields.size()); // DirectoryFieldCount + + // Write Fields + for (TiffOutputField field : fields) { + field.writeField(bos); + + // Debug.debug("\t" + "writing field (" + field.tag + ", 0x" + + // Integer.toHexString(field.tag) + ")", field.tagInfo); + // if(field.tagInfo.isOffset()) + // Debug.debug("\t\tOFFSET!", field.bytes); + } + + long nextDirectoryOffset = 0; + if (nextDirectory != null) { + nextDirectoryOffset = nextDirectory.getOffset(); + } + + // Write nextDirectoryOffset + if (nextDirectoryOffset == UNDEFINED_VALUE) { + bos.write4Bytes(0); + } else { + bos.write4Bytes((int) nextDirectoryOffset); + } + } + + public void setJpegImageData(final JpegImageData rawJpegImageData) { + this.jpegImageData = rawJpegImageData; + } + + public JpegImageData getRawJpegImageData() { + return jpegImageData; + } + + public void setTiffImageData(final TiffImageData rawTiffImageData) { + this.tiffImageData = rawTiffImageData; + } + + public TiffImageData getRawTiffImageData() { + return tiffImageData; + } + + @Override + public int getItemLength() { + return TIFF_ENTRY_LENGTH * fields.size() + TIFF_DIRECTORY_HEADER_LENGTH + + TIFF_DIRECTORY_FOOTER_LENGTH; + } + + @Override + public String getItemDescription() { + final TiffDirectoryType dirType = TagConstantsUtils + .getExifDirectoryType(type); + return "Directory: " + dirType.name + " (" + type + ")"; + } + + private void removeFieldIfPresent(final TagInfo tagInfo) { + final TiffOutputField field = findField(tagInfo); + if (null != field) { + fields.remove(field); + } + } + + protected List getOutputItems( + final TiffOutputSummary outputSummary) throws ImageWriteException { + // first validate directory fields. + + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT); + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + + TiffOutputField jpegOffsetField = null; + if (null != jpegImageData) { + jpegOffsetField = new TiffOutputField( + TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT, + FieldType.LONG, 1, new byte[TIFF_ENTRY_MAX_VALUE_LENGTH]); + add(jpegOffsetField); + + final byte[] lengthValue = FieldType.LONG.writeData( + jpegImageData.length, + outputSummary.byteOrder); + + final TiffOutputField jpegLengthField = new TiffOutputField( + TiffTagConstants.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + FieldType.LONG, 1, lengthValue); + add(jpegLengthField); + + } + + // -------------------------------------------------------------- + + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS); + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS); + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_TILE_OFFSETS); + removeFieldIfPresent(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS); + + TiffOutputField imageDataOffsetField; + ImageDataOffsets imageDataInfo = null; + if (null != tiffImageData) { + final boolean stripsNotTiles = tiffImageData.stripsNotTiles(); + + TagInfo offsetTag; + TagInfo byteCountsTag; + if (stripsNotTiles) { + offsetTag = TiffTagConstants.TIFF_TAG_STRIP_OFFSETS; + byteCountsTag = TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS; + } else { + offsetTag = TiffTagConstants.TIFF_TAG_TILE_OFFSETS; + byteCountsTag = TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS; + } + + // -------- + + final TiffElement.DataElement[] imageData = tiffImageData.getImageData(); + + // TiffOutputField imageDataOffsetsField = null; + + int[] imageDataOffsets = new int[imageData.length]; + int[] imageDataByteCounts = new int[imageData.length]; + for (int i = 0; i < imageData.length; i++) { + imageDataByteCounts[i] = imageData[i].length; + } + + // -------- + + // Append imageData-related fields to first directory + imageDataOffsetField = new TiffOutputField(offsetTag, + FieldType.LONG, imageDataOffsets.length, + FieldType.LONG.writeData(imageDataOffsets, + outputSummary.byteOrder)); + add(imageDataOffsetField); + + // -------- + + final byte[] data = FieldType.LONG.writeData(imageDataByteCounts, outputSummary.byteOrder); + final TiffOutputField byteCountsField = new TiffOutputField( + byteCountsTag, FieldType.LONG, imageDataByteCounts.length, + data); + add(byteCountsField); + + // -------- + + imageDataInfo = new ImageDataOffsets(imageData, imageDataOffsets, imageDataOffsetField); + } + + // -------------------------------------------------------------- + + final List result = new ArrayList(); + result.add(this); + sortFields(); + + for (TiffOutputField field : fields) { + if (field.isLocalValue()) { + continue; + } + + final TiffOutputItem item = field.getSeperateValue(); + result.add(item); + // outputSummary.add(item, field); + } + + if (null != imageDataInfo) { + Collections.addAll(result, imageDataInfo.outputItems); + + outputSummary.addTiffImageData(imageDataInfo); + } + + if (null != jpegImageData) { + final TiffOutputItem item = new TiffOutputItem.Value("JPEG image data", + jpegImageData.data); + result.add(item); + outputSummary.add(item, jpegOffsetField); + } + + return result; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputField.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputField.java new file mode 100644 index 0000000..d685f80 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputField.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Arrays; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public class TiffOutputField { + public final int tag; + public final TagInfo tagInfo; + public final FieldType fieldType; + public final int count; + private byte[] bytes; + private final TiffOutputItem.Value separateValueItem; + private int sortHint = -1; + private static final String NEWLINE = System.getProperty("line.separator"); + + public TiffOutputField(final TagInfo tagInfo, final FieldType tagtype, final int count, + final byte[] bytes) { + this(tagInfo.tag, tagInfo, tagtype, count, bytes); + } + + public TiffOutputField(final int tag, final TagInfo tagInfo, final FieldType fieldType, + final int count, final byte[] bytes) { + this.tag = tag; + this.tagInfo = tagInfo; + this.fieldType = fieldType; + this.count = count; + this.bytes = bytes; + + if (isLocalValue()) { + separateValueItem = null; + } else { + final String name = "Field Seperate value (" + tagInfo.getDescription() + + ")"; + separateValueItem = new TiffOutputItem.Value(name, bytes); + } + } + + protected static TiffOutputField createOffsetField(final TagInfo tagInfo, + final ByteOrder byteOrder) throws ImageWriteException { + return new TiffOutputField(tagInfo, FieldType.LONG, 1, + FieldType.LONG.writeData(0, byteOrder)); + } + + protected void writeField(final BinaryOutputStream bos) throws IOException, + ImageWriteException { + bos.write2Bytes(tag); + bos.write2Bytes(fieldType.getType()); + bos.write4Bytes(count); + + if (isLocalValue()) { + if (separateValueItem != null) { + throw new ImageWriteException("Unexpected separate value item."); + } + if (bytes.length > 4) { + throw new ImageWriteException( + "Local value has invalid length: " + bytes.length); + } + + bos.write(bytes); + final int remainder = TIFF_ENTRY_MAX_VALUE_LENGTH - bytes.length; + for (int i = 0; i < remainder; i++) { + bos.write(0); + } + } else { + if (separateValueItem == null) { + throw new ImageWriteException("Missing separate value item."); + } + + bos.write4Bytes((int) separateValueItem.getOffset()); + } + } + + protected TiffOutputItem getSeperateValue() { + return separateValueItem; + } + + protected final boolean isLocalValue() { + return bytes.length <= TIFF_ENTRY_MAX_VALUE_LENGTH; + } + + public boolean bytesEqual(final byte[] data) { + return Arrays.equals(bytes, data); + } + + protected void setData(final byte[] bytes) throws ImageWriteException { + // if(tagInfo.isUnknown()) + // Debug.debug("unknown tag(0x" + Integer.toHexString(tag) + // + ") setData", bytes); + + if (this.bytes.length != bytes.length) { + throw new ImageWriteException("Cannot change size of value."); + } + + // boolean wasLocalValue = isLocalValue(); + this.bytes = bytes; + if (separateValueItem != null) { + separateValueItem.updateValue(bytes); + } + // if (isLocalValue() != wasLocalValue) + // throw new Error("Bug. Locality disrupted! " + // + tagInfo.getDescription()); + } + + @Override + public String toString() { + return toString(null); + } + + public String toString(String prefix) { + if (prefix == null) { + prefix = ""; + } + final StringBuilder result = new StringBuilder(); + + result.append(prefix); + result.append(tagInfo); + result.append(NEWLINE); + + result.append(prefix); + result.append("count: "); + result.append(count); + result.append(NEWLINE); + + result.append(prefix); + result.append(fieldType); + result.append(NEWLINE); + + return result.toString(); + } + + public int getSortHint() { + return sortHint; + } + + public void setSortHint(final int sortHint) { + this.sortHint = sortHint; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputItem.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputItem.java similarity index 59% rename from src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputItem.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputItem.java index 4dfba78..37d486f 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputItem.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputItem.java @@ -1,83 +1,77 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.formats.tiff.constants.AllTagConstants; - -abstract class TiffOutputItem implements AllTagConstants -{ - public static final int UNDEFINED_VALUE = -1; - - private int offset = UNDEFINED_VALUE; - - protected int getOffset() - { - return offset; - } - - protected void setOffset(int offset) - { - this.offset = offset; - } - - public abstract int getItemLength(); - - public abstract String getItemDescription(); - - public abstract void writeItem(BinaryOutputStream bos) throws IOException, - ImageWriteException; - - public static class Value extends TiffOutputItem - { - private final byte bytes[]; - private final String name; - - public Value(final String name, final byte[] bytes) - { - this.name = name; - this.bytes = bytes; - } - - public int getItemLength() - { - return bytes.length; - } - - public String getItemDescription() - { - return name; - } - - public void updateValue(byte bytes[]) throws ImageWriteException - { - if (this.bytes.length != bytes.length) - throw new ImageWriteException("Updated data size mismatch: " - + this.bytes.length + " vs. " + bytes.length); - System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); - } - - public void writeItem(BinaryOutputStream bos) throws IOException, - ImageWriteException - { - bos.write(bytes); - } - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BinaryOutputStream; + +abstract class TiffOutputItem { + public static final long UNDEFINED_VALUE = -1; + + private long offset = UNDEFINED_VALUE; + + protected long getOffset() { + return offset; + } + + protected void setOffset(final long offset) { + this.offset = offset; + } + + public abstract int getItemLength(); + + public abstract String getItemDescription(); + + public abstract void writeItem(BinaryOutputStream bos) throws IOException, + ImageWriteException; + + public static class Value extends TiffOutputItem { + private final byte[] bytes; + private final String name; + + public Value(final String name, final byte[] bytes) { + this.name = name; + this.bytes = bytes; + } + + @Override + public int getItemLength() { + return bytes.length; + } + + @Override + public String getItemDescription() { + return name; + } + + public void updateValue(final byte[] bytes) throws ImageWriteException { + if (this.bytes.length != bytes.length) { + throw new ImageWriteException("Updated data size mismatch: " + + this.bytes.length + " vs. " + bytes.length); + } + System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); + } + + @Override + public void writeItem(final BinaryOutputStream bos) throws IOException, + ImageWriteException { + bos.write(bytes); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSet.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSet.java new file mode 100644 index 0000000..e57c877 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSet.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.RationalNumber; +import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.util.Debug; + +import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.*; + +public final class TiffOutputSet { + public final ByteOrder byteOrder; + private final List directories = new ArrayList(); + private static final String NEWLINE = System.getProperty("line.separator"); + + public TiffOutputSet() { + this(DEFAULT_TIFF_BYTE_ORDER); + } + + public TiffOutputSet(final ByteOrder byteOrder) { + super(); + this.byteOrder = byteOrder; + } + + protected List getOutputItems( + final TiffOutputSummary outputSummary) throws ImageWriteException { + final List result = new ArrayList(); + + for (TiffOutputDirectory directory : directories) { + result.addAll(directory.getOutputItems(outputSummary)); + } + + return result; + } + + public void addDirectory(final TiffOutputDirectory directory) + throws ImageWriteException { + if (null != findDirectory(directory.type)) { + throw new ImageWriteException( + "Output set already contains a directory of that type."); + } + directories.add(directory); + } + + public List getDirectories() { + return new ArrayList(directories); + } + + public TiffOutputDirectory getRootDirectory() { + return findDirectory(DIRECTORY_TYPE_ROOT); + } + + public TiffOutputDirectory getExifDirectory() { + return findDirectory(DIRECTORY_TYPE_EXIF); + } + + public TiffOutputDirectory getOrCreateRootDirectory() + throws ImageWriteException { + final TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_ROOT); + if (null != result) { + return result; + } + return addRootDirectory(); + } + + public TiffOutputDirectory getOrCreateExifDirectory() + throws ImageWriteException { + // EXIF directory requires root directory. + getOrCreateRootDirectory(); + + final TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_EXIF); + if (null != result) { + return result; + } + return addExifDirectory(); + } + + public TiffOutputDirectory getOrCreateGPSDirectory() + throws ImageWriteException { + // GPS directory requires EXIF directory + getOrCreateExifDirectory(); + + final TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_GPS); + if (null != result) { + return result; + } + return addGPSDirectory(); + } + + public TiffOutputDirectory getGPSDirectory() { + return findDirectory(DIRECTORY_TYPE_GPS); + } + + public TiffOutputDirectory getInteroperabilityDirectory() { + return findDirectory(DIRECTORY_TYPE_INTEROPERABILITY); + } + + public TiffOutputDirectory findDirectory(final int directoryType) { + for (TiffOutputDirectory directory : directories) { + if (directory.type == directoryType) { + return directory; + } + } + return null; + } + + /** + * A convenience method to update GPS values in EXIF metadata. + * + * @param longitude + * Longitude in degrees E, negative values are W. + * @param latitude + * latitude in degrees N, negative values are S. + * @throws ImageWriteException + */ + public void setGPSInDegrees(double longitude, double latitude) + throws ImageWriteException { + final TiffOutputDirectory gpsDirectory = getOrCreateGPSDirectory(); + + final String longitudeRef = longitude < 0 ? "W" : "E"; + longitude = Math.abs(longitude); + final String latitudeRef = latitude < 0 ? "S" : "N"; + latitude = Math.abs(latitude); + + gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); + gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF, + longitudeRef); + + gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); + gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF, + latitudeRef); + + { + double value = longitude; + final double longitudeDegrees = (long) value; + value %= 1; + value *= 60.0; + final double longitudeMinutes = (long) value; + value %= 1; + value *= 60.0; + final double longitudeSeconds = value; + + gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE); + gpsDirectory + .add(GpsTagConstants.GPS_TAG_GPS_LONGITUDE, + RationalNumber.valueOf(longitudeDegrees), + RationalNumber.valueOf(longitudeMinutes), + RationalNumber.valueOf(longitudeSeconds)); + } + + { + double value = latitude; + final double latitudeDegrees = (long) value; + value %= 1; + value *= 60.0; + final double latitudeMinutes = (long) value; + value %= 1; + value *= 60.0; + final double latitudeSeconds = value; + + gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_LATITUDE); + gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_LATITUDE, + RationalNumber.valueOf(latitudeDegrees), + RationalNumber.valueOf(latitudeMinutes), + RationalNumber.valueOf(latitudeSeconds)); + } + + } + + public void removeField(final TagInfo tagInfo) { + removeField(tagInfo.tag); + } + + public void removeField(final int tag) { + for (TiffOutputDirectory directory : directories) { + directory.removeField(tag); + } + } + + public TiffOutputField findField(final TagInfo tagInfo) { + return findField(tagInfo.tag); + } + + public TiffOutputField findField(final int tag) { + for (TiffOutputDirectory directory : directories) { + final TiffOutputField field = directory.findField(tag); + if (null != field) { + return field; + } + } + return null; + } + + public TiffOutputDirectory addRootDirectory() throws ImageWriteException { + final TiffOutputDirectory result = new TiffOutputDirectory( + DIRECTORY_TYPE_ROOT, byteOrder); + addDirectory(result); + return result; + } + + public TiffOutputDirectory addExifDirectory() throws ImageWriteException { + final TiffOutputDirectory result = new TiffOutputDirectory( + DIRECTORY_TYPE_EXIF, byteOrder); + addDirectory(result); + return result; + } + + public TiffOutputDirectory addGPSDirectory() throws ImageWriteException { + final TiffOutputDirectory result = new TiffOutputDirectory( + DIRECTORY_TYPE_GPS, byteOrder); + addDirectory(result); + return result; + } + + public TiffOutputDirectory addInteroperabilityDirectory() + throws ImageWriteException { + getOrCreateExifDirectory(); + + final TiffOutputDirectory result = new TiffOutputDirectory( + DIRECTORY_TYPE_INTEROPERABILITY, byteOrder); + addDirectory(result); + return result; + } + + @Override + public String toString() { + return toString(null); + } + + public String toString(String prefix) { + if (prefix == null) { + prefix = ""; + } + + final StringBuilder result = new StringBuilder(39); + + result.append(prefix); + result.append("TiffOutputSet {"); + result.append(NEWLINE); + + result.append(prefix); + result.append("byteOrder: "); + result.append(byteOrder); + result.append(NEWLINE); + + for (int i = 0; i < directories.size(); i++) { + final TiffOutputDirectory directory = directories.get(i); + result.append(String.format("%s\tdirectory %d: %s (%d)%n", + prefix, i, directory.description(), directory.type)); + + final List fields = directory.getFields(); + for (TiffOutputField field : fields) { + result.append(prefix); + result.append("\t\tfield " + i + ": " + field.tagInfo); + result.append(NEWLINE); + } + } + result.append(prefix); + + result.append('}'); + result.append(NEWLINE); + + return result.toString(); + } + + public void dump() { + Debug.debug(this.toString()); + } + +} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSummary.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSummary.java similarity index 53% rename from src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSummary.java rename to src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSummary.java index b42e7af..e5b1108 100644 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSummary.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputSummary.java @@ -1,97 +1,81 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -class TiffOutputSummary implements TiffConstants -{ - public final int byteOrder; - public final TiffOutputDirectory rootDirectory; - public final Map directoryTypeMap; - - public TiffOutputSummary(final int byteOrder, - final TiffOutputDirectory rootDirectory, final Map directoryTypeMap) - { - this.byteOrder = byteOrder; - this.rootDirectory = rootDirectory; - this.directoryTypeMap = directoryTypeMap; - } - - private static class OffsetItem - { - public final TiffOutputItem item; - public final TiffOutputField itemOffsetField; - - public OffsetItem(final TiffOutputItem item, - final TiffOutputField itemOffsetField) - { - super(); - this.itemOffsetField = itemOffsetField; - this.item = item; - } - } - - private List offsetItems = new ArrayList(); - - public void add(final TiffOutputItem item, - final TiffOutputField itemOffsetField) - { - offsetItems.add(new OffsetItem(item, itemOffsetField)); - } - - public void updateOffsets(int byteOrder) throws ImageWriteException - { - for (int i = 0; i < offsetItems.size(); i++) - { - OffsetItem offset = (OffsetItem) offsetItems.get(i); - - byte value[] = FIELD_TYPE_LONG.writeData(new int[]{ - offset.item.getOffset(), - }, byteOrder); - offset.itemOffsetField.setData(value); - } - - for (int i = 0; i < imageDataItems.size(); i++) - { - ImageDataOffsets imageDataInfo = (ImageDataOffsets) imageDataItems - .get(i); - - for (int j = 0; j < imageDataInfo.outputItems.length; j++) - { - TiffOutputItem item = imageDataInfo.outputItems[j]; - imageDataInfo.imageDataOffsets[j] = item.getOffset(); - } - - imageDataInfo.imageDataOffsetsField.setData(FIELD_TYPE_LONG - .writeData(imageDataInfo.imageDataOffsets, byteOrder)); - } - } - - private List imageDataItems = new ArrayList(); - - public void addTiffImageData(final ImageDataOffsets imageDataInfo) - { - imageDataItems.add(imageDataInfo); - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.tiff.write; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; + +class TiffOutputSummary { + public final ByteOrder byteOrder; + public final TiffOutputDirectory rootDirectory; + public final Map directoryTypeMap; + private final List offsetItems = new ArrayList(); + private final List imageDataItems = new ArrayList(); + + public TiffOutputSummary(final ByteOrder byteOrder, + final TiffOutputDirectory rootDirectory, + final Map directoryTypeMap) { + this.byteOrder = byteOrder; + this.rootDirectory = rootDirectory; + this.directoryTypeMap = directoryTypeMap; + } + + private static class OffsetItem { + public final TiffOutputItem item; + public final TiffOutputField itemOffsetField; + + public OffsetItem(final TiffOutputItem item, + final TiffOutputField itemOffsetField) { + super(); + this.itemOffsetField = itemOffsetField; + this.item = item; + } + } + + public void add(final TiffOutputItem item, + final TiffOutputField itemOffsetField) { + offsetItems.add(new OffsetItem(item, itemOffsetField)); + } + + public void updateOffsets(final ByteOrder byteOrder) throws ImageWriteException { + for (OffsetItem offset : offsetItems) { + final byte[] value = FieldType.LONG.writeData( + (int) offset.item.getOffset(), byteOrder); + offset.itemOffsetField.setData(value); + } + + for (ImageDataOffsets imageDataInfo : imageDataItems) { + for (int j = 0; j < imageDataInfo.outputItems.length; j++) { + final TiffOutputItem item = imageDataInfo.outputItems[j]; + imageDataInfo.imageDataOffsets[j] = (int) item.getOffset(); + } + + imageDataInfo.imageDataOffsetsField.setData(FieldType.LONG + .writeData(imageDataInfo.imageDataOffsets, byteOrder)); + } + } + + public void addTiffImageData(final ImageDataOffsets imageDataInfo) { + imageDataItems.add(imageDataInfo); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/formats/wbmp/WbmpImageParser.java b/src/main/java/org/apache/commons/imaging/formats/wbmp/WbmpImageParser.java new file mode 100644 index 0000000..b332724 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/wbmp/WbmpImageParser.java @@ -0,0 +1,294 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.wbmp; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DataBufferByte; +import com.google.code.appengine.awt.image.IndexColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.WritableRaster; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class WbmpImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".wbmp"; + private static final String[] ACCEPTED_EXTENSIONS = { ".wbmp", }; + + @Override + public String getName() { + return "Wireless Application Protocol Bitmap Format"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.WBMP, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final WbmpHeader wbmpHeader = readWbmpHeader(byteSource); + return new ImageInfo("WBMP", 1, new ArrayList(), + ImageFormats.WBMP, + "Wireless Application Protocol Bitmap", wbmpHeader.height, + "image/vnd.wap.wbmp", 1, 0, 0, 0, 0, wbmpHeader.width, false, + false, false, ImageInfo.COLOR_TYPE_BW, + ImageInfo.COMPRESSION_ALGORITHM_NONE); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final WbmpHeader wbmpHeader = readWbmpHeader(byteSource); + return new Dimension(wbmpHeader.width, wbmpHeader.height); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + static class WbmpHeader { + int typeField; + byte fixHeaderField; + int width; + int height; + + public WbmpHeader(final int typeField, final byte fixHeaderField, final int width, + final int height) { + this.typeField = typeField; + this.fixHeaderField = fixHeaderField; + this.width = width; + this.height = height; + } + + public void dump(final PrintWriter pw) { + pw.println("WbmpHeader"); + pw.println("TypeField: " + typeField); + pw.println("FixHeaderField: 0x" + + Integer.toHexString(0xff & fixHeaderField)); + pw.println("Width: " + width); + pw.println("Height: " + height); + } + } + + private int readMultiByteInteger(final InputStream is) throws ImageReadException, + IOException { + int value = 0; + int nextByte; + int totalBits = 0; + do { + nextByte = readByte("Header", is, "Error reading WBMP header"); + value <<= 7; + value |= nextByte & 0x7f; + totalBits += 7; + if (totalBits > 31) { + throw new ImageReadException( + "Overflow reading WBMP multi-byte field"); + } + } while ((nextByte & 0x80) != 0); + return value; + } + + private void writeMultiByteInteger(final OutputStream os, final int value) + throws IOException { + boolean wroteYet = false; + for (int position = 4 * 7; position > 0; position -= 7) { + final int next7Bits = 0x7f & (value >>> position); + if (next7Bits != 0 || wroteYet) { + os.write(0x80 | next7Bits); + wroteYet = true; + } + } + os.write(0x7f & value); + } + + private WbmpHeader readWbmpHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final WbmpHeader ret = readWbmpHeader(is); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private WbmpHeader readWbmpHeader(final InputStream is) + throws ImageReadException, IOException { + final int typeField = readMultiByteInteger(is); + if (typeField != 0) { + throw new ImageReadException("Invalid/unsupported WBMP type " + + typeField); + } + + final byte fixHeaderField = readByte("FixHeaderField", is, + "Invalid WBMP File"); + if ((fixHeaderField & 0x9f) != 0) { + throw new ImageReadException( + "Invalid/unsupported WBMP FixHeaderField 0x" + + Integer.toHexString(0xff & fixHeaderField)); + } + + final int width = readMultiByteInteger(is); + + final int height = readMultiByteInteger(is); + + return new WbmpHeader(typeField, fixHeaderField, width, height); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + readWbmpHeader(byteSource).dump(pw); + return true; + } + + private BufferedImage readImage(final WbmpHeader wbmpHeader, final InputStream is) + throws IOException { + final int rowLength = (wbmpHeader.width + 7) / 8; + final byte[] image = readBytes("Pixels", is, + rowLength * wbmpHeader.height, "Error reading image pixels"); + final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); + final WritableRaster raster = Raster.createPackedRaster(dataBuffer, + wbmpHeader.width, wbmpHeader.height, 1, null); + final int[] palette = { 0x000000, 0xffffff }; + final IndexColorModel colorModel = new IndexColorModel(1, 2, palette, 0, + false, -1, DataBuffer.TYPE_BYTE); + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final WbmpHeader wbmpHeader = readWbmpHeader(is); + final BufferedImage ret = readImage(wbmpHeader, is); + canThrow = true; + return ret; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + writeMultiByteInteger(os, 0); // typeField + os.write(0); // fixHeaderField + writeMultiByteInteger(os, src.getWidth()); + writeMultiByteInteger(os, src.getHeight()); + + for (int y = 0; y < src.getHeight(); y++) { + int pixel = 0; + int nextBit = 0x80; + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + final int sample = (red + green + blue) / 3; + if (sample > 127) { + pixel |= nextBit; + } + nextBit >>>= 1; + if (nextBit == 0) { + os.write(pixel); + pixel = 0; + nextBit = 0x80; + } + } + if (nextBit != 0x80) { + os.write(pixel); + } + } + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/wbmp/package-info.java b/src/main/java/org/apache/commons/imaging/formats/wbmp/package-info.java new file mode 100644 index 0000000..ba11f8a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/wbmp/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Wireless Application Protocol Bitmap Format image format. + */ +package org.apache.commons.imaging.formats.wbmp; + diff --git a/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java b/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java new file mode 100644 index 0000000..65fa55a --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java @@ -0,0 +1,413 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.xbm; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DataBufferByte; +import com.google.code.appengine.awt.image.IndexColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.WritableRaster; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.UUID; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BasicCParser; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; + +public class XbmImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".xbm"; + private static final String[] ACCEPTED_EXTENSIONS = { ".xbm", }; + + @Override + public String getName() { + return "X BitMap"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.XBM, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final XbmHeader xbmHeader = readXbmHeader(byteSource); + return new ImageInfo("XBM", 1, new ArrayList(), + ImageFormats.XBM, "X BitMap", xbmHeader.height, + "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width, false, + false, false, ImageInfo.COLOR_TYPE_BW, + ImageInfo.COMPRESSION_ALGORITHM_NONE); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final XbmHeader xbmHeader = readXbmHeader(byteSource); + return new Dimension(xbmHeader.width, xbmHeader.height); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private static class XbmHeader { + int width; + int height; + int xHot = -1; + int yHot = -1; + + public XbmHeader(final int width, final int height, final int xHot, final int yHot) { + this.width = width; + this.height = height; + this.xHot = xHot; + this.yHot = yHot; + } + + public void dump(final PrintWriter pw) { + pw.println("XbmHeader"); + pw.println("Width: " + width); + pw.println("Height: " + height); + if (xHot != -1 && yHot != -1) { + pw.println("X hot: " + xHot); + pw.println("Y hot: " + yHot); + } + } + } + + private static class XbmParseResult { + XbmHeader xbmHeader; + BasicCParser cParser; + } + + private XbmHeader readXbmHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + final XbmParseResult result = parseXbmHeader(byteSource); + return result.xbmHeader; + } + + private XbmParseResult parseXbmHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final Map defines = new HashMap(); + final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess( + is, null, defines); + int width = -1; + int height = -1; + int xHot = -1; + int yHot = -1; + for (final Entry entry : defines.entrySet()) { + final String name = entry.getKey(); + if (name.endsWith("_width")) { + width = Integer.parseInt(entry.getValue()); + } else if (name.endsWith("_height")) { + height = Integer.parseInt(entry.getValue()); + } else if (name.endsWith("_x_hot")) { + xHot = Integer.parseInt(entry.getValue()); + } else if (name.endsWith("_y_hot")) { + yHot = Integer.parseInt(entry.getValue()); + } + } + if (width == -1) { + throw new ImageReadException("width not found"); + } + if (height == -1) { + throw new ImageReadException("height not found"); + } + + final XbmParseResult xbmParseResult = new XbmParseResult(); + xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream( + preprocessedFile.toByteArray())); + xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot); + canThrow = true; + return xbmParseResult; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser cParser) + throws ImageReadException, IOException { + String token; + token = cParser.nextToken(); + if (!"static".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no 'static' token"); + } + token = cParser.nextToken(); + if (token == null) { + throw new ImageReadException( + "Parsing XBM file failed, no 'unsigned' " + + "or 'char' token"); + } + if ("unsigned".equals(token)) { + token = cParser.nextToken(); + } + if (!"char".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no 'char' token"); + } + final String name = cParser.nextToken(); + if (name == null) { + throw new ImageReadException( + "Parsing XBM file failed, no variable name"); + } + if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) { + throw new ImageReadException( + "Parsing XBM file failed, variable name " + + "doesn't start with letter or underscore"); + } + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_') { + throw new ImageReadException( + "Parsing XBM file failed, variable name " + + "contains non-letter non-digit non-underscore"); + } + } + token = cParser.nextToken(); + if (!"[".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no '[' token"); + } + token = cParser.nextToken(); + if (!"]".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no ']' token"); + } + token = cParser.nextToken(); + if (!"=".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no '=' token"); + } + token = cParser.nextToken(); + if (!"{".equals(token)) { + throw new ImageReadException( + "Parsing XBM file failed, no '{' token"); + } + + final int rowLength = (xbmHeader.width + 7) / 8; + final byte[] imageData = new byte[rowLength * xbmHeader.height]; + for (int i = 0; i < imageData.length; i++) { + token = cParser.nextToken(); + if (token == null || !token.startsWith("0x")) { + throw new ImageReadException("Parsing XBM file failed, " + + "hex value missing"); + } + if (token.length() > 4) { + throw new ImageReadException("Parsing XBM file failed, " + + "hex value too long"); + } + final int value = Integer.parseInt(token.substring(2), 16); + int flipped = 0; + for (int j = 0; j < 8; j++) { + if ((value & (1 << j)) != 0) { + flipped |= (0x80 >>> j); + } + } + imageData[i] = (byte) flipped; + + token = cParser.nextToken(); + if (token == null) { + throw new ImageReadException("Parsing XBM file failed, " + + "premature end of file"); + } + if (!",".equals(token) + && (i < (imageData.length - 1) || !"}".equals(token))) { + throw new ImageReadException("Parsing XBM file failed, " + + "punctuation error"); + } + } + + int[] palette = { 0xffffff, 0x000000 }; + ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE); + DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length); + WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null); + + return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + readXbmHeader(byteSource).dump(pw); + return true; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final XbmParseResult result = parseXbmHeader(byteSource); + return readXbmImage(result.xbmHeader, result.cParser); + } + + private String randomName() { + final UUID uuid = UUID.randomUUID(); + final StringBuilder stringBuilder = new StringBuilder("a"); + long bits = uuid.getMostSignificantBits(); + // Long.toHexString() breaks for very big numbers + for (int i = 64 - 8; i >= 0; i -= 8) { + stringBuilder.append(Integer + .toHexString((int) ((bits >> i) & 0xff))); + } + bits = uuid.getLeastSignificantBits(); + for (int i = 64 - 8; i >= 0; i -= 8) { + stringBuilder.append(Integer + .toHexString((int) ((bits >> i) & 0xff))); + } + return stringBuilder.toString(); + } + + private String toPrettyHex(final int value) { + final String s = Integer.toHexString(0xff & value); + if (s.length() == 2) { + return "0x" + s; + } + return "0x0" + s; + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final String name = randomName(); + + os.write(("#define " + name + "_width " + src.getWidth() + "\n") + .getBytes("US-ASCII")); + os.write(("#define " + name + "_height " + src.getHeight() + "\n") + .getBytes("US-ASCII")); + os.write(("static unsigned char " + name + "_bits[] = {") + .getBytes("US-ASCII")); + + int bitcache = 0; + int bitsInCache = 0; + String separator = "\n "; + int written = 0; + for (int y = 0; y < src.getHeight(); y++) { + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + int sample = (red + green + blue) / 3; + if (sample > 127) { + sample = 0; + } else { + sample = 1; + } + bitcache |= (sample << bitsInCache); + ++bitsInCache; + if (bitsInCache == 8) { + os.write(separator.getBytes("US-ASCII")); + separator = ","; + if (written == 12) { + os.write("\n ".getBytes("US-ASCII")); + written = 0; + } + os.write(toPrettyHex(bitcache).getBytes("US-ASCII")); + bitcache = 0; + bitsInCache = 0; + ++written; + } + } + if (bitsInCache != 0) { + os.write(separator.getBytes("US-ASCII")); + separator = ","; + if (written == 12) { + os.write("\n ".getBytes("US-ASCII")); + written = 0; + } + os.write(toPrettyHex(bitcache).getBytes("US-ASCII")); + bitcache = 0; + bitsInCache = 0; + ++written; + } + } + + os.write("\n};\n".getBytes("US-ASCII")); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/xbm/package-info.java b/src/main/java/org/apache/commons/imaging/formats/xbm/package-info.java new file mode 100644 index 0000000..2ed873d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/xbm/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The X BitMap image format. + */ +package org.apache.commons.imaging.formats.xbm; + diff --git a/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java b/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java new file mode 100644 index 0000000..3bfa952 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java @@ -0,0 +1,750 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * under the License. + */ + +package org.apache.commons.imaging.formats.xpm; + +import com.google.code.appengine.awt.Dimension; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import com.google.code.appengine.awt.image.DataBuffer; +import com.google.code.appengine.awt.image.DirectColorModel; +import com.google.code.appengine.awt.image.IndexColorModel; +import com.google.code.appengine.awt.image.Raster; +import com.google.code.appengine.awt.image.WritableRaster; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.UUID; + +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.common.BasicCParser; +import org.apache.commons.imaging.common.IImageMetadata; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.palette.PaletteFactory; +import org.apache.commons.imaging.palette.SimplePalette; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.ImagingConstants.*; + +public class XpmImageParser extends ImageParser { + private static final String DEFAULT_EXTENSION = ".xpm"; + private static final String[] ACCEPTED_EXTENSIONS = { ".xpm", }; + private static Map colorNames; + private static final char[] WRITE_PALETTE = { ' ', '.', 'X', 'o', 'O', '+', + '@', '#', '$', '%', '&', '*', '=', '-', ';', ':', '>', ',', '<', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'q', 'w', 'e', + 'r', 't', 'y', 'u', 'i', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', + 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'M', 'N', 'B', 'V', + 'C', 'Z', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'P', 'I', + 'U', 'Y', 'T', 'R', 'E', 'W', 'Q', '!', '~', '^', '/', '(', ')', + '_', '`', '\'', ']', '[', '{', '}', '|', }; + + private static void loadColorNames() throws ImageReadException { + synchronized (XpmImageParser.class) { + if (colorNames != null) { + return; + } + + try { + final InputStream rgbTxtStream = + XpmImageParser.class.getResourceAsStream("rgb.txt"); + if (rgbTxtStream == null) { + throw new ImageReadException("Couldn't find rgb.txt in our resources"); + } + final Map colors = new HashMap(); + BufferedReader reader = null; + boolean canThrow = false; + try { + reader = new BufferedReader(new InputStreamReader(rgbTxtStream, + "US-ASCII")); + String line; + while ((line = reader.readLine()) != null) { + if (line.charAt(0) == '!') { + continue; + } + try { + final int red = Integer.parseInt(line.substring(0, 3).trim()); + final int green = Integer.parseInt(line.substring(4, 7).trim()); + final int blue = Integer.parseInt(line.substring(8, 11).trim()); + final String colorName = line.substring(11).trim(); + colors.put(colorName, 0xff000000 | (red << 16) + | (green << 8) | blue); + } catch (final NumberFormatException nfe) { + throw new ImageReadException("Couldn't parse color in rgb.txt", nfe); + } + } + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, reader); + } + colorNames = colors; + } catch (final IOException ioException) { + throw new ImageReadException("Could not parse rgb.txt", ioException); + } + } + } + + @Override + public String getName() { + return "X PixMap"; + } + + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Override + protected String[] getAcceptedExtensions() { + return ACCEPTED_EXTENSIONS; + } + + @Override + protected ImageFormat[] getAcceptedTypes() { + return new ImageFormat[] { ImageFormats.XPM, // + }; + } + + @Override + public IImageMetadata getMetadata(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + @Override + public ImageInfo getImageInfo(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final XpmHeader xpmHeader = readXpmHeader(byteSource); + boolean transparent = false; + int colorType = ImageInfo.COLOR_TYPE_BW; + for (final Entry entry : xpmHeader.palette + .entrySet()) { + final PaletteEntry paletteEntry = entry.getValue(); + if ((paletteEntry.getBestARGB() & 0xff000000) != 0xff000000) { + transparent = true; + } + if (paletteEntry.haveColor) { + colorType = ImageInfo.COLOR_TYPE_RGB; + } else if (colorType != ImageInfo.COLOR_TYPE_RGB + && (paletteEntry.haveGray || paletteEntry.haveGray4Level)) { + colorType = ImageInfo.COLOR_TYPE_GRAYSCALE; + } + } + return new ImageInfo("XPM version 3", xpmHeader.numCharsPerPixel * 8, + new ArrayList(), ImageFormats.XPM, + "X PixMap", xpmHeader.height, "image/x-xpixmap", 1, 0, 0, 0, 0, + xpmHeader.width, false, transparent, true, colorType, + ImageInfo.COMPRESSION_ALGORITHM_NONE); + } + + @Override + public Dimension getImageSize(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + final XpmHeader xpmHeader = readXpmHeader(byteSource); + return new Dimension(xpmHeader.width, xpmHeader.height); + } + + @Override + public byte[] getICCProfileBytes(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } + + private static class XpmHeader { + int width; + int height; + int numColors; + int numCharsPerPixel; + int xHotSpot = -1; + int yHotSpot = -1; + boolean xpmExt; + + Map palette = new HashMap(); + + public XpmHeader(final int width, final int height, final int numColors, + final int numCharsPerPixel, final int xHotSpot, final int yHotSpot, final boolean xpmExt) { + this.width = width; + this.height = height; + this.numColors = numColors; + this.numCharsPerPixel = numCharsPerPixel; + this.xHotSpot = xHotSpot; + this.yHotSpot = yHotSpot; + this.xpmExt = xpmExt; + } + + public void dump(final PrintWriter pw) { + pw.println("XpmHeader"); + pw.println("Width: " + width); + pw.println("Height: " + height); + pw.println("NumColors: " + numColors); + pw.println("NumCharsPerPixel: " + numCharsPerPixel); + if (xHotSpot != -1 && yHotSpot != -1) { + pw.println("X hotspot: " + xHotSpot); + pw.println("Y hotspot: " + yHotSpot); + } + pw.println("XpmExt: " + xpmExt); + } + } + + private static class PaletteEntry { + int index; + boolean haveColor = false; + int colorArgb; + boolean haveGray = false; + int grayArgb; + boolean haveGray4Level = false; + int gray4LevelArgb; + boolean haveMono = false; + int monoArgb; + + int getBestARGB() { + if (haveColor) { + return colorArgb; + } else if (haveGray) { + return grayArgb; + } else if (haveGray4Level) { + return gray4LevelArgb; + } else if (haveMono) { + return monoArgb; + } else { + return 0x00000000; + } + } + } + + private static class XpmParseResult { + XpmHeader xpmHeader; + BasicCParser cParser; + } + + private XpmHeader readXpmHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + final XpmParseResult result = parseXpmHeader(byteSource); + return result.xpmHeader; + } + + private XpmParseResult parseXpmHeader(final ByteSource byteSource) + throws ImageReadException, IOException { + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + final StringBuilder firstComment = new StringBuilder(); + final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess( + is, firstComment, null); + if (!"XPM".equals(firstComment.toString().trim())) { + throw new ImageReadException("Parsing XPM file failed, " + + "signature isn't '/* XPM */'"); + } + + final XpmParseResult xpmParseResult = new XpmParseResult(); + xpmParseResult.cParser = new BasicCParser(new ByteArrayInputStream( + preprocessedFile.toByteArray())); + xpmParseResult.xpmHeader = parseXpmHeader(xpmParseResult.cParser); + canThrow = true; + return xpmParseResult; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + + private boolean parseNextString(final BasicCParser cParser, + final StringBuilder stringBuilder) throws IOException, ImageReadException { + stringBuilder.setLength(0); + String token = cParser.nextToken(); + if (token.charAt(0) != '"') { + throw new ImageReadException("Parsing XPM file failed, " + + "no string found where expected"); + } + BasicCParser.unescapeString(stringBuilder, token); + for (token = cParser.nextToken(); token.charAt(0) == '"'; token = cParser + .nextToken()) { + BasicCParser.unescapeString(stringBuilder, token); + } + if (",".equals(token)) { + return true; + } else if ("}".equals(token)) { + return false; + } else { + throw new ImageReadException("Parsing XPM file failed, " + + "no ',' or '}' found where expected"); + } + } + + private XpmHeader parseXpmValuesSection(final String row) + throws ImageReadException { + final String[] tokens = BasicCParser.tokenizeRow(row); + if (tokens.length < 4 && tokens.length > 7) { + throw new ImageReadException("Parsing XPM file failed, " + + " section has incorrect tokens"); + } + try { + final int width = Integer.parseInt(tokens[0]); + final int height = Integer.parseInt(tokens[1]); + final int numColors = Integer.parseInt(tokens[2]); + final int numCharsPerPixel = Integer.parseInt(tokens[3]); + int xHotSpot = -1; + int yHotSpot = -1; + boolean xpmExt = false; + if (tokens.length >= 6) { + xHotSpot = Integer.parseInt(tokens[4]); + yHotSpot = Integer.parseInt(tokens[5]); + } + if (tokens.length == 5 || tokens.length == 7) { + if ("XPMEXT".equals(tokens[tokens.length - 1])) { + xpmExt = true; + } else { + throw new ImageReadException("Parsing XPM file failed, " + + "can't parse section XPMEXT"); + } + } + return new XpmHeader(width, height, numColors, numCharsPerPixel, + xHotSpot, yHotSpot, xpmExt); + } catch (final NumberFormatException nfe) { + throw new ImageReadException("Parsing XPM file failed, " + + "error parsing section", nfe); + } + } + + private int parseColor(String color) throws ImageReadException { + if (color.charAt(0) == '#') { + color = color.substring(1); + if (color.length() == 3) { + final int red = Integer.parseInt(color.substring(0, 1), 16); + final int green = Integer.parseInt(color.substring(1, 2), 16); + final int blue = Integer.parseInt(color.substring(2, 3), 16); + return 0xff000000 | (red << 20) | (green << 12) | (blue << 4); + } else if (color.length() == 6) { + return 0xff000000 | Integer.parseInt(color, 16); + } else if (color.length() == 9) { + final int red = Integer.parseInt(color.substring(0, 1), 16); + final int green = Integer.parseInt(color.substring(3, 4), 16); + final int blue = Integer.parseInt(color.substring(6, 7), 16); + return 0xff000000 | (red << 16) | (green << 8) | blue; + } else if (color.length() == 12) { + final int red = Integer.parseInt(color.substring(0, 1), 16); + final int green = Integer.parseInt(color.substring(4, 5), 16); + final int blue = Integer.parseInt(color.substring(8, 9), 16); + return 0xff000000 | (red << 16) | (green << 8) | blue; + } else { + return 0x00000000; + } + } else if (color.charAt(0) == '%') { + throw new ImageReadException("HSV colors are not implemented " + + "even in the XPM specification!"); + } else if ("None".equals(color)) { + return 0x00000000; + } else { + loadColorNames(); + if (colorNames.containsKey(color)) { + return colorNames.get(color); + } + return 0x00000000; + } + } + + private void populatePaletteEntry(final PaletteEntry paletteEntry, final String key, final String color) throws ImageReadException { + if ("m".equals(key)) { + paletteEntry.monoArgb = parseColor(color); + paletteEntry.haveMono = true; + } else if ("g4".equals(key)) { + paletteEntry.gray4LevelArgb = parseColor(color); + paletteEntry.haveGray4Level = true; + } else if ("g".equals(key)) { + paletteEntry.grayArgb = parseColor(color); + paletteEntry.haveGray = true; + } else if ("s".equals(key)) { + paletteEntry.colorArgb = parseColor(color); + paletteEntry.haveColor = true; + } else if ("c".equals(key)) { + paletteEntry.colorArgb = parseColor(color); + paletteEntry.haveColor = true; + } + } + + private void parsePaletteEntries(final XpmHeader xpmHeader, final BasicCParser cParser) + throws IOException, ImageReadException { + final StringBuilder row = new StringBuilder(); + for (int i = 0; i < xpmHeader.numColors; i++) { + row.setLength(0); + final boolean hasMore = parseNextString(cParser, row); + if (!hasMore) { + throw new ImageReadException("Parsing XPM file failed, " + "file ended while reading palette"); + } + final String name = row.substring(0, xpmHeader.numCharsPerPixel); + final String[] tokens = BasicCParser.tokenizeRow(row.substring(xpmHeader.numCharsPerPixel)); + final PaletteEntry paletteEntry = new PaletteEntry(); + paletteEntry.index = i; + int previousKeyIndex = Integer.MIN_VALUE; + final StringBuilder colorBuffer = new StringBuilder(); + for (int j = 0; j < tokens.length; j++) { + final String token = tokens[j]; + boolean isKey = false; + if (previousKeyIndex < (j - 1) + && "m".equals(token) + || "g4".equals(token) + || "g".equals(token) + || "c".equals(token) + || "s".equals(token)) { + isKey = true; + } + if (isKey) { + if (previousKeyIndex >= 0) { + final String key = tokens[previousKeyIndex]; + final String color = colorBuffer.toString(); + colorBuffer.setLength(0); + populatePaletteEntry(paletteEntry, key, color); + } + previousKeyIndex = j; + } else { + if (previousKeyIndex < 0) { + break; + } + if (colorBuffer.length() > 0) { + colorBuffer.append(' '); + } + colorBuffer.append(token); + } + } + if (previousKeyIndex >= 0 && colorBuffer.length() > 0) { + final String key = tokens[previousKeyIndex]; + final String color = colorBuffer.toString(); + colorBuffer.setLength(0); + populatePaletteEntry(paletteEntry, key, color); + } + xpmHeader.palette.put(name, paletteEntry); + } + } + + private XpmHeader parseXpmHeader(final BasicCParser cParser) + throws ImageReadException, IOException { + String name; + String token; + token = cParser.nextToken(); + if (!"static".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no 'static' token"); + } + token = cParser.nextToken(); + if (!"char".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no 'char' token"); + } + token = cParser.nextToken(); + if (!"*".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no '*' token"); + } + name = cParser.nextToken(); + if (name == null) { + throw new ImageReadException( + "Parsing XPM file failed, no variable name"); + } + if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) { + throw new ImageReadException( + "Parsing XPM file failed, variable name " + + "doesn't start with letter or underscore"); + } + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_') { + throw new ImageReadException( + "Parsing XPM file failed, variable name " + + "contains non-letter non-digit non-underscore"); + } + } + token = cParser.nextToken(); + if (!"[".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no '[' token"); + } + token = cParser.nextToken(); + if (!"]".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no ']' token"); + } + token = cParser.nextToken(); + if (!"=".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no '=' token"); + } + token = cParser.nextToken(); + if (!"{".equals(token)) { + throw new ImageReadException( + "Parsing XPM file failed, no '{' token"); + } + + final StringBuilder row = new StringBuilder(); + final boolean hasMore = parseNextString(cParser, row); + if (!hasMore) { + throw new ImageReadException("Parsing XPM file failed, " + + "file too short"); + } + final XpmHeader xpmHeader = parseXpmValuesSection(row.toString()); + parsePaletteEntries(xpmHeader, cParser); + return xpmHeader; + } + + private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser cParser) + throws ImageReadException, IOException { + ColorModel colorModel; + WritableRaster raster; + int bpp; + if (xpmHeader.palette.size() <= (1 << 8)) { + final int[] palette = new int[xpmHeader.palette.size()]; + for (final Entry entry : xpmHeader.palette + .entrySet()) { + final PaletteEntry paletteEntry = entry.getValue(); + palette[paletteEntry.index] = paletteEntry.getBestARGB(); + } + colorModel = new IndexColorModel(8, xpmHeader.palette.size(), + palette, 0, true, -1, DataBuffer.TYPE_BYTE); + raster = Raster.createInterleavedRaster( + DataBuffer.TYPE_BYTE, xpmHeader.width, xpmHeader.height, 1, + null); + bpp = 8; + } else if (xpmHeader.palette.size() <= (1 << 16)) { + final int[] palette = new int[xpmHeader.palette.size()]; + for (final Entry entry : xpmHeader.palette + .entrySet()) { + final PaletteEntry paletteEntry = entry.getValue(); + palette[paletteEntry.index] = paletteEntry.getBestARGB(); + } + colorModel = new IndexColorModel(16, xpmHeader.palette.size(), + palette, 0, true, -1, DataBuffer.TYPE_USHORT); + raster = Raster.createInterleavedRaster( + DataBuffer.TYPE_USHORT, xpmHeader.width, xpmHeader.height, + 1, null); + bpp = 16; + } else { + colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, + 0x000000ff, 0xff000000); + raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, + xpmHeader.width, xpmHeader.height, new int[] { 0x00ff0000, + 0x0000ff00, 0x000000ff, 0xff000000 }, null); + bpp = 32; + } + + final BufferedImage image = new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), new Properties()); + final DataBuffer dataBuffer = raster.getDataBuffer(); + final StringBuilder row = new StringBuilder(); + boolean hasMore = true; + for (int y = 0; y < xpmHeader.height; y++) { + row.setLength(0); + hasMore = parseNextString(cParser, row); + if (y < (xpmHeader.height - 1) && !hasMore) { + throw new ImageReadException("Parsing XPM file failed, " + + "insufficient image rows in file"); + } + final int rowOffset = y * xpmHeader.width; + for (int x = 0; x < xpmHeader.width; x++) { + final String index = row.substring(x * xpmHeader.numCharsPerPixel, + (x + 1) * xpmHeader.numCharsPerPixel); + final PaletteEntry paletteEntry = xpmHeader.palette.get(index); + if (paletteEntry == null) { + throw new ImageReadException( + "No palette entry was defined " + "for " + index); + } + if (bpp <= 16) { + dataBuffer.setElem(rowOffset + x, paletteEntry.index); + } else { + dataBuffer.setElem(rowOffset + x, + paletteEntry.getBestARGB()); + } + } + } + + while (hasMore) { + row.setLength(0); + hasMore = parseNextString(cParser, row); + } + + final String token = cParser.nextToken(); + if (!";".equals(token)) { + throw new ImageReadException("Last token wasn't ';'"); + } + + return image; + } + + @Override + public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) + throws ImageReadException, IOException { + readXpmHeader(byteSource).dump(pw); + return true; + } + + @Override + public final BufferedImage getBufferedImage(final ByteSource byteSource, + final Map params) throws ImageReadException, IOException { + final XpmParseResult result = parseXpmHeader(byteSource); + return readXpmImage(result.xpmHeader, result.cParser); + } + + private String randomName() { + final UUID uuid = UUID.randomUUID(); + final StringBuilder stringBuilder = new StringBuilder("a"); + long bits = uuid.getMostSignificantBits(); + // Long.toHexString() breaks for very big numbers + for (int i = 64 - 8; i >= 0; i -= 8) { + stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff))); + } + bits = uuid.getLeastSignificantBits(); + for (int i = 64 - 8; i >= 0; i -= 8) { + stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff))); + } + return stringBuilder.toString(); + } + + private String pixelsForIndex(int index, final int charsPerPixel) { + final StringBuilder stringBuilder = new StringBuilder(); + int highestPower = 1; + for (int i = 1; i < charsPerPixel; i++) { + highestPower *= WRITE_PALETTE.length; + } + for (int i = 0; i < charsPerPixel; i++) { + final int multiple = index / highestPower; + index -= (multiple * highestPower); + highestPower /= WRITE_PALETTE.length; + stringBuilder.append(WRITE_PALETTE[multiple]); + } + return stringBuilder.toString(); + } + + private String toColor(final int color) { + final String hex = Integer.toHexString(color); + if (hex.length() < 6) { + final char[] zeroes = new char[6 - hex.length()]; + Arrays.fill(zeroes, '0'); + return "#" + new String(zeroes) + hex; + } + return "#" + hex; + } + + @Override + public void writeImage(final BufferedImage src, final OutputStream os, Map params) + throws ImageWriteException, IOException { + // make copy of params; we'll clear keys as we consume them. + params = (params == null) ? new HashMap() : new HashMap(params); + + // clear format key. + if (params.containsKey(PARAM_KEY_FORMAT)) { + params.remove(PARAM_KEY_FORMAT); + } + + if (!params.isEmpty()) { + final Object firstKey = params.keySet().iterator().next(); + throw new ImageWriteException("Unknown parameter: " + firstKey); + } + + final PaletteFactory paletteFactory = new PaletteFactory(); + boolean hasTransparency = false; + if (paletteFactory.hasTransparency(src, 1)) { + hasTransparency = true; + } + SimplePalette palette = null; + int maxColors = WRITE_PALETTE.length; + int charsPerPixel = 1; + while (palette == null) { + palette = paletteFactory.makeExactRgbPaletteSimple(src, + hasTransparency ? maxColors - 1 : maxColors); + if (palette == null) { + maxColors *= WRITE_PALETTE.length; + charsPerPixel++; + } + } + int colors = palette.length(); + if (hasTransparency) { + ++colors; + } + + String line = "/* XPM */\n"; + os.write(line.getBytes("US-ASCII")); + line = "static char *" + randomName() + "[] = {\n"; + os.write(line.getBytes("US-ASCII")); + line = "\"" + src.getWidth() + " " + src.getHeight() + " " + colors + + " " + charsPerPixel + "\",\n"; + os.write(line.getBytes("US-ASCII")); + + for (int i = 0; i < colors; i++) { + String color; + if (i < palette.length()) { + color = toColor(palette.getEntry(i)); + } else { + color = "None"; + } + line = "\"" + pixelsForIndex(i, charsPerPixel) + " c " + color + + "\",\n"; + os.write(line.getBytes("US-ASCII")); + } + + String separator = ""; + for (int y = 0; y < src.getHeight(); y++) { + os.write(separator.getBytes("US-ASCII")); + separator = ",\n"; + line = "\""; + os.write(line.getBytes("US-ASCII")); + for (int x = 0; x < src.getWidth(); x++) { + final int argb = src.getRGB(x, y); + if ((argb & 0xff000000) == 0) { + line = pixelsForIndex(palette.length(), charsPerPixel); + } else { + line = pixelsForIndex( + palette.getPaletteIndex(0xffffff & argb), + charsPerPixel); + } + os.write(line.getBytes("US-ASCII")); + } + line = "\""; + os.write(line.getBytes("US-ASCII")); + } + + line = "\n};\n"; + os.write(line.getBytes("US-ASCII")); + } + + /** + * Extracts embedded XML metadata as XML string. + *

+ * + * @param byteSource + * File containing image data. + * @param params + * Map of optional parameters, defined in ImagingConstants. + * @return Xmp Xml as String, if present. Otherwise, returns null. + */ + @Override + public String getXmpXml(final ByteSource byteSource, final Map params) + throws ImageReadException, IOException { + return null; + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/xpm/package-info.java b/src/main/java/org/apache/commons/imaging/formats/xpm/package-info.java new file mode 100644 index 0000000..1d0f613 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/xpm/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The X PixMap image format. + */ +package org.apache.commons.imaging.formats.xpm; diff --git a/src/main/java/org/apache/sanselan/util/CachingInputStream.java b/src/main/java/org/apache/commons/imaging/icc/CachingInputStream.java similarity index 74% rename from src/main/java/org/apache/sanselan/util/CachingInputStream.java rename to src/main/java/org/apache/commons/imaging/icc/CachingInputStream.java index 9cd44df..879919a 100644 --- a/src/main/java/org/apache/sanselan/util/CachingInputStream.java +++ b/src/main/java/org/apache/commons/imaging/icc/CachingInputStream.java @@ -1,56 +1,53 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -public class CachingInputStream extends InputStream -{ - private final InputStream is; - private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - public CachingInputStream(InputStream is) - { - this.is = is; - } - - public byte[] getCache() - { - return baos.toByteArray(); - } - - public int read() throws IOException - { - int result = is.read(); - baos.write(result); - return result; - } - - public int available() throws IOException - { - return is.available(); - } - - public void close() throws IOException - { - is.close(); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.imaging.icc; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +class CachingInputStream extends InputStream { + private final InputStream is; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + public CachingInputStream(final InputStream is) { + this.is = is; + } + + public byte[] getCache() { + return baos.toByteArray(); + } + + @Override + public int read() throws IOException { + final int result = is.read(); + baos.write(result); + return result; + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public void close() throws IOException { + is.close(); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccConstants.java b/src/main/java/org/apache/commons/imaging/icc/IccConstants.java new file mode 100644 index 0000000..787a129 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccConstants.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +public final class IccConstants { + public static final int IEC = (((0xff & 'I') << 24) | ((0xff & 'E') << 16) + | ((0xff & 'C') << 8) | ((0xff & ' ') << 0)); + public static final int sRGB = (((0xff & 's') << 24) | ((0xff & 'R') << 16) + | ((0xff & 'G') << 8) | ((0xff & 'B') << 0)); + + private IccConstants() { + } +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccProfileInfo.java b/src/main/java/org/apache/commons/imaging/icc/IccProfileInfo.java new file mode 100644 index 0000000..8ca9a74 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccProfileInfo.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.apache.commons.imaging.ImageReadException; + +public class IccProfileInfo { + + private final byte[] data; + public final int profileSize; + public final int cmmTypeSignature; + public final int profileVersion; + public final int profileDeviceClassSignature; + public final int colorSpace; + public final int profileConnectionSpace; + public final int profileFileSignature; + public final int primaryPlatformSignature; + public final int variousFlags; + public final int deviceManufacturer; + public final int deviceModel; + public final int renderingIntent; + public final int profileCreatorSignature; + private final byte[] profileId; + private final IccTag[] tags; + + public IccProfileInfo(final byte[] data, final int profileSize, final int cmmTypeSignature, + final int profileVersion, final int profileDeviceClassSignature, + final int colorSpace, final int profileConnectionSpace, + final int profileFileSignature, final int primaryPlatformSignature, + final int variousFlags, final int deviceManufacturer, final int deviceModel, + final int renderingIntent, final int profileCreatorSignature, final byte[] profileId, + final IccTag[] tags) { + this.data = data; + + this.profileSize = profileSize; + this.cmmTypeSignature = cmmTypeSignature; + this.profileVersion = profileVersion; + this.profileDeviceClassSignature = profileDeviceClassSignature; + this.colorSpace = colorSpace; + this.profileConnectionSpace = profileConnectionSpace; + this.profileFileSignature = profileFileSignature; + this.primaryPlatformSignature = primaryPlatformSignature; + this.variousFlags = variousFlags; + this.deviceManufacturer = deviceManufacturer; + this.deviceModel = deviceModel; + this.renderingIntent = renderingIntent; + this.profileCreatorSignature = profileCreatorSignature; + this.profileId = profileId; + + this.tags = tags; + } + + public byte[] getData() { + return data; + } + + public byte[] getProfileId() { + return profileId; + } + + public IccTag[] getTags() { + return tags; + } + + public boolean issRGB() { + return deviceManufacturer == IccConstants.IEC + && deviceModel == IccConstants.sRGB; + } + + private void printCharQuad(final PrintWriter pw, final String msg, final int i) { + pw.println(msg + ": '" + (char) (0xff & (i >> 24)) + + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) + + (char) (0xff & (i >> 0)) + "'"); + } + + public void dump(final String prefix) { + System.out.print(toString()); + } + + @Override + public String toString() { + try { + return toString(""); + } catch (final Exception e) { + return "IccProfileInfo: Error"; + } + } + + public String toString(final String prefix) throws ImageReadException, + IOException { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + + pw.println(prefix + ": " + "data length: " + data.length); + + printCharQuad(pw, prefix + ": " + "ProfileDeviceClassSignature", profileDeviceClassSignature); + printCharQuad(pw, prefix + ": " + "CMMTypeSignature", cmmTypeSignature); + printCharQuad(pw, prefix + ": " + "ProfileDeviceClassSignature", profileDeviceClassSignature); + printCharQuad(pw, prefix + ": " + "ColorSpace", colorSpace); + printCharQuad(pw, prefix + ": " + "ProfileConnectionSpace", profileConnectionSpace); + printCharQuad(pw, prefix + ": " + "ProfileFileSignature", profileFileSignature); + printCharQuad(pw, prefix + ": " + "PrimaryPlatformSignature", primaryPlatformSignature); + printCharQuad(pw, prefix + ": " + "ProfileFileSignature", profileFileSignature); + printCharQuad(pw, prefix + ": " + "DeviceManufacturer", deviceManufacturer); + printCharQuad(pw, prefix + ": " + "DeviceModel", deviceModel); + printCharQuad(pw, prefix + ": " + "RenderingIntent", renderingIntent); + printCharQuad(pw, prefix + ": " + "ProfileCreatorSignature", profileCreatorSignature); + + for (int i = 0; i < tags.length; i++) { + final IccTag tag = tags[i]; + tag.dump(pw, "\t" + i + ": "); + } + + pw.println(prefix + ": " + "issRGB: " + issRGB()); + pw.flush(); + + return sw.getBuffer().toString(); + } + +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java b/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java new file mode 100644 index 0000000..d22bf72 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +import com.google.code.appengine.awt.color.ICC_Profile; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.common.BinaryFileParser; +import org.apache.commons.imaging.common.bytesource.ByteSource; +import org.apache.commons.imaging.common.bytesource.ByteSourceArray; +import org.apache.commons.imaging.common.bytesource.ByteSourceFile; +import org.apache.commons.imaging.util.Debug; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public class IccProfileParser extends BinaryFileParser { + public IccProfileParser() { + this.setByteOrder(ByteOrder.BIG_ENDIAN); + } + + public IccProfileInfo getICCProfileInfo(final ICC_Profile iccProfile) { + if (iccProfile == null) { + return null; + } + + return getICCProfileInfo(new ByteSourceArray(iccProfile.getData())); + } + + public IccProfileInfo getICCProfileInfo(final byte[] bytes) { + if (bytes == null) { + return null; + } + + return getICCProfileInfo(new ByteSourceArray(bytes)); + } + + public IccProfileInfo getICCProfileInfo(final File file) { + if (file == null) { + return null; + } + + return getICCProfileInfo(new ByteSourceFile(file)); + } + + public IccProfileInfo getICCProfileInfo(final ByteSource byteSource) { + + InputStream is = null; + + try { + + IccProfileInfo result; + is = byteSource.getInputStream(); + + result = readICCProfileInfo(is); + + if (result == null) { + return null; + } + + is.close(); + is = null; + + for (final IccTag tag : result.getTags()) { + final byte[] bytes = byteSource.getBlock(tag.offset, tag.length); + // Debug.debug("bytes: " + bytes.length); + tag.setData(bytes); + // tag.dump("\t" + i + ": "); + } + // result.fillInTagData(byteSource); + + return result; + } catch (final Exception e) { + // Debug.debug("Error: " + file.getAbsolutePath()); + Debug.debug(e); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (final Exception e) { + Debug.debug(e); + } + + } + + if (getDebug()) { + Debug.debug(); + } + + return null; + } + + private IccProfileInfo readICCProfileInfo(InputStream is) { + final CachingInputStream cis = new CachingInputStream(is); + is = cis; + + if (getDebug()) { + Debug.debug(); + } + + // setDebug(true); + + // if (getDebug()) + // Debug.debug("length: " + length); + + try { + final int profileSize = read4Bytes("ProfileSize", is, "Not a Valid ICC Profile", getByteOrder()); + + // if (length != ProfileSize) + // { + // // Debug.debug("Unexpected Length data expected: " + + // Integer.toHexString((int) length) + // // + ", encoded: " + Integer.toHexString(ProfileSize)); + // // Debug.debug("Unexpected Length data: " + length + // // + ", length: " + ProfileSize); + // // throw new Error("asd"); + // return null; + // } + + final int cmmTypeSignature = read4Bytes("Signature", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("CMMTypeSignature", cmmTypeSignature); + } + + final int profileVersion = read4Bytes("ProfileVersion", is, "Not a Valid ICC Profile", getByteOrder()); + + final int profileDeviceClassSignature = read4Bytes("ProfileDeviceClassSignature", is, + "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("ProfileDeviceClassSignature", profileDeviceClassSignature); + } + + final int colorSpace = read4Bytes("ColorSpace", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("ColorSpace", colorSpace); + } + + final int profileConnectionSpace = read4Bytes("ProfileConnectionSpace", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("ProfileConnectionSpace", profileConnectionSpace); + } + + skipBytes(is, 12, "Not a Valid ICC Profile"); + + final int profileFileSignature = read4Bytes("ProfileFileSignature", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("ProfileFileSignature", profileFileSignature); + } + + final int primaryPlatformSignature = read4Bytes("PrimaryPlatformSignature", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("PrimaryPlatformSignature", primaryPlatformSignature); + } + + final int variousFlags = read4Bytes("VariousFlags", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("VariousFlags", profileFileSignature); + } + + final int deviceManufacturer = read4Bytes("DeviceManufacturer", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("DeviceManufacturer", deviceManufacturer); + } + + final int deviceModel = read4Bytes("DeviceModel", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("DeviceModel", deviceModel); + } + + skipBytes(is, 8, "Not a Valid ICC Profile"); + + final int renderingIntent = read4Bytes("RenderingIntent", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("RenderingIntent", renderingIntent); + } + + skipBytes(is, 12, "Not a Valid ICC Profile"); + + final int profileCreatorSignature = read4Bytes("ProfileCreatorSignature", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("ProfileCreatorSignature", profileCreatorSignature); + } + + final byte[] profileId = null; + skipBytes(is, 16, "Not a Valid ICC Profile"); + // readByteArray("ProfileID", 16, is, + // "Not a Valid ICC Profile"); + // if (getDebug()) + // System.out + // .println("ProfileID: '" + new String(ProfileID) + "'"); + + skipBytes(is, 28, "Not a Valid ICC Profile"); + + // this.setDebug(true); + + final int tagCount = read4Bytes("TagCount", is, "Not a Valid ICC Profile", getByteOrder()); + + // List tags = new ArrayList(); + final IccTag[] tags = new IccTag[tagCount]; + + for (int i = 0; i < tagCount; i++) { + final int tagSignature = read4Bytes("TagSignature[" + i + "]", is, "Not a Valid ICC Profile", getByteOrder()); + // Debug.debug("TagSignature t " + // + Integer.toHexString(TagSignature)); + + // this.printCharQuad("TagSignature", TagSignature); + final int offsetToData = read4Bytes("OffsetToData[" + i + "]", is, "Not a Valid ICC Profile", getByteOrder()); + final int elementSize = read4Bytes("ElementSize[" + i + "]", is, "Not a Valid ICC Profile", getByteOrder()); + + final IccTagType fIccTagType = getIccTagType(tagSignature); + // if (fIccTagType == null) + // throw new Error("oops."); + + // System.out + // .println("\t[" + // + i + // + "]: " + // + ((fIccTagType == null) + // ? "unknown" + // : fIccTagType.name)); + // Debug.debug(); + + final IccTag tag = new IccTag(tagSignature, offsetToData, + elementSize, fIccTagType); + // tag.dump("\t" + i + ": "); + tags[i] = tag; + // tags .add(tag); + } + + { + // read stream to end, filling cache. + while (is.read() >= 0) { // NOPMD we're doing nothing with the data + } + } + + final byte[] data = cis.getCache(); + + if (data.length < profileSize) { + throw new IOException("Couldn't read ICC Profile."); + } + + final IccProfileInfo result = new IccProfileInfo(data, profileSize, + cmmTypeSignature, profileVersion, + profileDeviceClassSignature, colorSpace, + profileConnectionSpace, profileFileSignature, + primaryPlatformSignature, variousFlags, deviceManufacturer, + deviceModel, renderingIntent, profileCreatorSignature, + profileId, tags); + + if (getDebug()) { + Debug.debug("issRGB: " + result.issRGB()); + } + + return result; + } catch (final Exception e) { + Debug.debug(e); + } + + return null; + } + + private IccTagType getIccTagType(final int quad) { + for (final IccTagType iccTagType : IccTagTypes.values()) { + if (iccTagType.getSignature() == quad) { + return iccTagType; + } + } + + return null; + } + + public boolean issRGB(final ICC_Profile iccProfile) throws IOException { + return issRGB(new ByteSourceArray(iccProfile.getData())); + } + + public boolean issRGB(final byte[] bytes) throws IOException { + return issRGB(new ByteSourceArray(bytes)); + } + + public boolean issRGB(final File file) throws IOException { + return issRGB(new ByteSourceFile(file)); + } + + public boolean issRGB(final ByteSource byteSource) throws IOException { + if (getDebug()) { + Debug.debug(); + } + + // setDebug(true); + + // long length = byteSource.getLength(); + // + // if (getDebug()) + // Debug.debug("length: " + length); + + InputStream is = null; + boolean canThrow = false; + try { + is = byteSource.getInputStream(); + + read4Bytes("ProfileSize", is, "Not a Valid ICC Profile", getByteOrder()); + + // if (length != ProfileSize) + // return null; + + skipBytes(is, 4 * 5); + + skipBytes(is, 12, "Not a Valid ICC Profile"); + + skipBytes(is, 4 * 3); + + final int deviceManufacturer = read4Bytes("ProfileFileSignature", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("DeviceManufacturer", deviceManufacturer); + } + + final int deviceModel = read4Bytes("DeviceModel", is, "Not a Valid ICC Profile", getByteOrder()); + if (getDebug()) { + printCharQuad("DeviceModel", deviceModel); + } + + boolean result = deviceManufacturer == IccConstants.IEC && deviceModel == IccConstants.sRGB; + canThrow = true; + return result; + } finally { + IoUtils.closeQuietly(canThrow, is); + } + } + +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccTag.java b/src/main/java/org/apache/commons/imaging/icc/IccTag.java new file mode 100644 index 0000000..b6713f3 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccTag.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.util.IoUtils; + +public class IccTag { + public final int signature; + public final int offset; + public final int length; + public final IccTagType fIccTagType; + public byte[] data; + private IccTagDataType itdt; + private int dataTypeSignature; + + // public final byte[] data; + + public IccTag(final int signature, final int offset, final int length, final IccTagType fIccTagType) { + this.signature = signature; + this.offset = offset; + this.length = length; + this.fIccTagType = fIccTagType; + } + + public void setData(final byte[] bytes) throws IOException { + data = bytes; + + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + dataTypeSignature = BinaryFunctions.read4Bytes("data type signature", bis, + "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + + itdt = getIccTagDataType(dataTypeSignature); + // if (itdt != null) + // { + // System.out.println("\t\t\t" + "itdt: " + itdt.name); + // } + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + private IccTagDataType getIccTagDataType(final int quad) { + for (final IccTagDataType iccTagDataType : IccTagDataTypes.values()) { + if (iccTagDataType.getSignature() == quad) { + return iccTagDataType; + } + } + + return null; + } + + public void dump(final String prefix) throws ImageReadException, IOException { + final PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + + dump(pw, prefix); + + pw.flush(); + } + + public void dump(final PrintWriter pw, final String prefix) throws ImageReadException, + IOException { + pw.println(prefix + + "tag signature: " + + Integer.toHexString(signature) + + " (" + + new String(new byte[] { + (byte) (0xff & (signature >> 24)), + (byte) (0xff & (signature >> 16)), + (byte) (0xff & (signature >> 8)), + (byte) (0xff & (signature >> 0)), }, "US-ASCII") + + ")"); + + if (data == null) { + pw.println(prefix + "data: " + Arrays.toString(data)); + } else { + pw.println(prefix + "data: " + data.length); + + pw.println(prefix + + "data type signature: " + + Integer.toHexString(dataTypeSignature) + + " (" + + new String(new byte[] { + (byte) (0xff & (dataTypeSignature >> 24)), + (byte) (0xff & (dataTypeSignature >> 16)), + (byte) (0xff & (dataTypeSignature >> 8)), + (byte) (0xff & (dataTypeSignature >> 0)), }, "US-ASCII") + + ")"); + + if (itdt == null) { + pw.println(prefix + "IccTagType : " + "unknown"); + } else { + pw.println(prefix + "IccTagType : " + itdt.getName()); + itdt.dump(prefix, data); + } + + } + + pw.println(""); + pw.flush(); + + } +} diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIDAT.java b/src/main/java/org/apache/commons/imaging/icc/IccTagDataType.java similarity index 74% rename from src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIDAT.java rename to src/main/java/org/apache/commons/imaging/icc/IccTagDataType.java index 761504d..5af00db 100644 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIDAT.java +++ b/src/main/java/org/apache/commons/imaging/icc/IccTagDataType.java @@ -1,28 +1,30 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -public class PNGChunkIDAT extends PNGChunk -{ - public PNGChunkIDAT(int Length, int ChunkType, int CRC, byte bytes[]) - throws IOException - { - super(Length, ChunkType, CRC, bytes); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +import java.io.IOException; + +import org.apache.commons.imaging.ImageReadException; + +interface IccTagDataType { + + String getName(); + + int getSignature(); + + void dump(String prefix, byte[] bytes) throws ImageReadException, IOException; +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccTagDataTypes.java b/src/main/java/org/apache/commons/imaging/icc/IccTagDataTypes.java new file mode 100644 index 0000000..2381ecf --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccTagDataTypes.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.util.IoUtils; + +import static org.apache.commons.imaging.common.BinaryFunctions.*; + +public enum IccTagDataTypes implements IccTagDataType { + DESC_TYPE( + "descType", 0x64657363) { + public void dump(final String prefix, final byte[] bytes) + throws ImageReadException, IOException + { + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + read4Bytes("type_signature", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + + // bis.setDebug(true); + read4Bytes("ignore", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + final int stringLength = read4Bytes("stringLength", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + + // bis.readByteArray("ignore", bytes.length -12, "none"); + final String s = new String(bytes, 12, stringLength - 1, "US-ASCII"); + System.out.println(prefix + "s: '" + s + "'"); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + }, + + DATA_TYPE( + "dataType", 0x64617461) { + public void dump(final String prefix, final byte[] bytes) + throws ImageReadException, IOException + { + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + read4Bytes("type_signature", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + }, + + MULTI_LOCALIZED_UNICODE_TYPE( + "multiLocalizedUnicodeType", (0x6D6C7563)) { + public void dump(final String prefix, final byte[] bytes) + throws ImageReadException, IOException + { + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + read4Bytes("type_signature", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + }, + + SIGNATURE_TYPE( + "signatureType", ((0x73696720))) { + public void dump(final String prefix, final byte[] bytes) + throws ImageReadException, IOException + { + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + read4Bytes("type_signature", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + read4Bytes("ignore", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + final int thesignature = read4Bytes("thesignature ", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + System.out.println(prefix + + "thesignature: " + + Integer.toHexString(thesignature) + + " (" + + new String(new byte[]{ + (byte) (0xff & (thesignature >> 24)), + (byte) (0xff & (thesignature >> 16)), + (byte) (0xff & (thesignature >> 8)), + (byte) (0xff & (thesignature >> 0)), }, "US-ASCII") + + ")"); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + }, + + TEXT_TYPE( + "textType", 0x74657874) { + public void dump(final String prefix, final byte[] bytes) + throws ImageReadException, IOException + { + InputStream bis = null; + boolean canThrow = false; + try { + bis = new ByteArrayInputStream(bytes); + read4Bytes("type_signature", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + read4Bytes("ignore", bis, "ICC: corrupt tag data", ByteOrder.BIG_ENDIAN); + final String s = new String(bytes, 8, bytes.length - 8, "US-ASCII"); + System.out.println(prefix + "s: '" + s + "'"); + canThrow = true; + } finally { + IoUtils.closeQuietly(canThrow, bis); + } + } + + }; + + public final String name; + public final int signature; + + IccTagDataTypes(final String name, final int signature) { + this.name = name; + this.signature = signature; + } + + public String getName() { + return name; + } + + public int getSignature() { + return signature; + } +} diff --git a/src/main/java/org/apache/commons/imaging/icc/IccTagType.java b/src/main/java/org/apache/commons/imaging/icc/IccTagType.java new file mode 100644 index 0000000..b6febd0 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/IccTagType.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +interface IccTagType { + + String getName(); + + String getTypeDescription(); + + int getSignature(); +} diff --git a/src/main/java/org/apache/sanselan/icc/IccConstants.java b/src/main/java/org/apache/commons/imaging/icc/IccTagTypes.java similarity index 61% rename from src/main/java/org/apache/sanselan/icc/IccConstants.java rename to src/main/java/org/apache/commons/imaging/icc/IccTagTypes.java index d7f5e3c..4139be3 100644 --- a/src/main/java/org/apache/sanselan/icc/IccConstants.java +++ b/src/main/java/org/apache/commons/imaging/icc/IccTagTypes.java @@ -1,452 +1,338 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryConstants; -import org.apache.sanselan.common.BinaryInputStream; - -public interface IccConstants -{ - public final static int IEC = (((0xff & 'I') << 24) | ((0xff & 'E') << 16) - | ((0xff & 'C') << 8) | ((0xff & ' ') << 0)); - public final static int sRGB = (((0xff & 's') << 24) | ((0xff & 'R') << 16) - | ((0xff & 'G') << 8) | ((0xff & 'B') << 0)); - - public static final IccTagDataType descType = new IccTagDataType( - "descType", 0x64657363) - { - public void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException - { - BinaryInputStream bis = new BinaryInputStream( - new ByteArrayInputStream(bytes), - BinaryConstants.BYTE_ORDER_NETWORK); - bis.read4Bytes("type_signature", "ICC: corrupt tag data"); - - // bis.setDebug(true); - bis.read4Bytes("ignore", "ICC: corrupt tag data"); - int string_length = bis.read4Bytes("string_length", - "ICC: corrupt tag data"); - - // bis.readByteArray("ignore", bytes.length -12, "none"); - String s = new String(bytes, 12, string_length - 1); - System.out.println(prefix + "s: '" + s + "'"); - } - - }; - - public static final IccTagDataType dataType = new IccTagDataType( - "dataType", 0x64617461) - { - public void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException - { - BinaryInputStream bis = new BinaryInputStream( - new ByteArrayInputStream(bytes), - BinaryConstants.BYTE_ORDER_NETWORK); - bis.read4Bytes("type_signature", "ICC: corrupt tag data"); - } - - }; - - public static final IccTagDataType multiLocalizedUnicodeType = new IccTagDataType( - "multiLocalizedUnicodeType", (0x6D6C7563)) - { - public void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException - { - BinaryInputStream bis = new BinaryInputStream( - new ByteArrayInputStream(bytes), - BinaryConstants.BYTE_ORDER_NETWORK); - bis.read4Bytes("type_signature", "ICC: corrupt tag data"); - } - - }; - - public static final IccTagDataType signatureType = new IccTagDataType( - "signatureType", ((0x73696720))) - { - public void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException - { - BinaryInputStream bis = new BinaryInputStream( - new ByteArrayInputStream(bytes), - BinaryConstants.BYTE_ORDER_NETWORK); - bis.read4Bytes("type_signature", "ICC: corrupt tag data"); - bis.read4Bytes("ignore", "ICC: corrupt tag data"); - int thesignature = bis.read4Bytes("thesignature ", - "ICC: corrupt tag data"); - System.out.println(prefix - + "thesignature: " - + Integer.toHexString(thesignature) - + " (" - + new String(new byte[]{ - (byte) (0xff & (thesignature >> 24)), - (byte) (0xff & (thesignature >> 16)), - (byte) (0xff & (thesignature >> 8)), - (byte) (0xff & (thesignature >> 0)), - }) + ")"); - } - - }; - - public static final IccTagDataType textType = new IccTagDataType( - "textType", 0x74657874) - { - public void dump(String prefix, byte bytes[]) - throws ImageReadException, IOException - { - BinaryInputStream bis = new BinaryInputStream( - new ByteArrayInputStream(bytes), - BinaryConstants.BYTE_ORDER_NETWORK); - bis.read4Bytes("type_signature", "ICC: corrupt tag data"); - bis.read4Bytes("ignore", "ICC: corrupt tag data"); - String s = new String(bytes, 8, bytes.length - 8); - System.out.println(prefix + "s: '" + s + "'"); - } - - }; - - public static final IccTagDataType IccTagDataTypes[] = { - descType, dataType, multiLocalizedUnicodeType, signatureType, - textType, - }; - // public static final IccTagDataType dataType = new IccTagDataType("dataType", - // 0x64617461 - // ); - - public static final IccTagType AToB0Tag = new IccTagType("AToB0Tag", - "lut8Type or lut16Type or lutAtoBType", 0x41324230 - // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing mechanisms are described in lut8Type or lut16Type or lutAtoBType."); - ); - - // public static final IccTagType AToB0Tag = new IccTagType( - // "AToB0Tag", - // "lut8Type or lut16Type or lutAtoBType", - // "A2B0 (41324230h)", - // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", - // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", - // ); - - public static final IccTagType AToB1Tag = new IccTagType("AToB1Tag", - "lut8Type or lut16Type or lutAtoBType", 0x41324231 - // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", - // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", - ); - - public static final IccTagType AToB2Tag = new IccTagType("AToB2Tag", - "lut8Type or lut16Type or lutAtoBType", 0x41324232 - // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", - // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", - ); - - public static final IccTagType blueMatrixColumnTag = new IccTagType( - "blueMatrixColumnTag", "XYZType", 0x6258595A - // "The third column in the matrix used in TRC/matrix transforms.", - ); - - public static final IccTagType blueTRCTag = new IccTagType("blueTRCTag", - "curveType or parametricCurveType", 0x62545243 - // "Blue channel tone reproduction curve. The first element represents no colorant (white) or phosphors", - // "(black) and the last element represents 100 percent colorant (blue) or 100 percent phosphor (blue).", - ); - - public static final IccTagType BToA0Tag = new IccTagType("BToA0Tag", - "lut8Type or lut16Type or lutBtoAType", 0x42324130 - // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", - // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", - ); - - public static final IccTagType BToA1Tag = new IccTagType("BToA1Tag", - "lut8Type or lut16Type or lutBtoAType", 0x42324131 - // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", - // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", - ); - - public static final IccTagType BToA2Tag = new IccTagType("BToA2Tag", - "lut8Type or lut16Type or lutBtoAType", 0x42324132 - // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", - // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", - ); - - public static final IccTagType calibrationDateTimeTag = new IccTagType( - "calibrationDateTimeTag", "dateTimeType", 0x63616C74 - // "Profile calibration date and time. Initially, this tag matches the contents of the profile header's creation", - // "date/time field. This allows applications and utilities to verify if this profile matches a vendor's profile and", - // "how recently calibration has been performed.", - ); - - public static final IccTagType charTargetTag = new IccTagType( - "charTargetTag", "textType", 0x74617267 - // "This tag contains the name of the registered characterization data set, or it contains the measurement data", - // "for a characterization target. This tag is provided so that distributed utilities can identify the underlying", - // "characterization data, create transforms \"on the fly\" or check the current performance against the original", - // "device performance.", - // "The first seven characters of the text shall identify the nature of the characterization data.", - // "If the first seven characters are \"ICCHDAT\", then the remainder of the text shall be a single space followed", - // "by the Reference Name of a characterization data set in the ICC Characterization Data Registry and terminated", - // "with a NULL byte (00h). The Reference Name in the text must match exactly (including case) the", - // "Reference Name in the registry.", - // "If the first seven characters match one of the identifiers defined in an ANSI or ISO standard, then the tag", - // "embeds the exact data file format defined in that standard. Each of these file formats contains an identifying", - // "character string as the first seven characters of the format, allowing an external parser to determine", - // "which data file format is being used. This provides the facilities to include a wide range of targets using a", - // "variety of measurement specifications in a standard manner.", - // "NOTE: It is highly recommended that the profileDescriptionTag also include an identification of the characterization", - // "data that was used in the creation of the profile (e.g. \"Based on CGATS TR 001\").", - ); - - public static final IccTagType chromaticAdaptationTag = new IccTagType( - "chromaticAdaptationTag", "s15Fixed16ArrayType", 0x63686164 - // "This tag converts an XYZ color, measured at a device's specific illumination conditions, to an XYZ color in", - // "the PCS illumination conditions after complete adaptation.", - // "The tag reflects a survey of the currently used methods of conversion, all of which can be formulated as a", - // "matrix transformation (see Annex E). Such a 3 by 3 chromatic adaptation matrix is organized as a 9-element", - // "array of signed 15.16 numbers (s15Fixed16ArrayType tag). Similarly as in the other occurrences of a", - // "3 by 3 matrix in the ICC tags, the dimension corresponding to the matrix rows varies least rapidly while the", - // "one corresponding to the matrix columns varies most rapidly.", - // "(19)", - // "(20)", - // "array a0 a1 a2 a3 a4 a5 a6 a7 a8 =", - // "Xpcs", - // "Ypcs", - // "Zpcs", - // "a0 a1 a2", - // "a3 a4 a5", - // "a6 a7 a8", - // "Xsrc", - // "Ysrc", - // "Zsrc", - // "=", - // "Where XYZsrc represents the measured value in the actual device viewing condition and XYZpcs represents", - // "the chromatically adapted value in the PCS.", - // "The chromatic adaptation matrix is a combination of three separate conversions:", - // "1) Conversion of source CIE XYZ tristimulus values to cone response tristimulus values.", - // "2) Adjustment of the cone response values for an observer's chromatic adaptation.", - // "3) Conversion of the adjusted cone response tristimulus back to CIE XYZ values.", - ); - - public static final IccTagType chromaticityTag = new IccTagType( - "chromaticityTag", "chromaticityType", 0x6368726D - // "The data and type of phosphor/colorant chromaticity set.", - ); - - public static final IccTagType colorantOrderTag = new IccTagType( - "colorantOrderTag", "colorantOrderType", 0x636C726F - // "This tag specifies the laydown order of colorants.", - ); - - public static final IccTagType colorantTableTag = new IccTagType( - "colorantTableTag", "colorantTableType", 0x636C7274 - // "This tag identifies the colorants used in the profile by a unique name and an XYZ or L*a*b* value.", - // "This is a required tag for profiles where the color space defined in the header is xCLR, where x is one of", - // "the allowed numbers from 2 through Fh, per Table 13. See Section 6.3.3.2, Section 6.3.4.1.", - ); - - public static final IccTagType copyrightTag = new IccTagType( - "copyrightTag", "multiLocalizedUnicodeType", 0x63707274 - // "This tag contains the text copyright information for the profile.", - ); - - public static final IccTagType deviceMfgDescTag = new IccTagType( - "deviceMfgDescTag", "multiLocalizedUnicodeType", 0x646D6E64 - // "Structure containing invariant and localizable versions of the device manufacturer for display. The content", - // "of this structure is described in 6.5.12.", - ); - - public static final IccTagType deviceModelDescTag = new IccTagType( - "deviceModelDescTag", "multiLocalizedUnicodeType", 0x646D6464 - // "Structure containing invariant and localizable versions of the device model for display. The content of this", - // "structure is described in 6.5.12.", - ); - - public static final IccTagType gamutTag = new IccTagType("gamutTag", - "lut8Type or lut16Type or lutBtoAType", 0x67616D74 - // "Out of gamut tag. The processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", - // "This tag takes PCS values as its input and produces a single channel of output. If the output value is 0, the", - // "PCS color is in-gamut. If the output is non-zero, the PCS color is out-of-gamut, with the output value �n+1�", - // "being at least as far out of gamut as the output value n.", - ); - - public static final IccTagType grayTRCTag = new IccTagType("grayTRCTag", - "curveType or parametricCurveType", 0x6B545243 - // "Gray tone reproduction curve. The tone reproduction curve provides the necessary information to convert", - // "between a single device channel and the CIEXYZ encoding of the profile connection space. The first element", - // "represents black and the last element represents white.", - ); - - public static final IccTagType greenMatrixColumnTag = new IccTagType( - "greenMatrixColumnTag", "XYZType", 0x6758595A - // "The second column in the matrix used in TRC/matrix transforms.", - ); - - public static final IccTagType greenTRCTag = new IccTagType( - // "6.4.21 ", - "greenTRCTag", "curveType or parametricCurveType", 0x67545243 - // "Green channel tone reproduction curve. The first element represents no colorant (white) or phosphors", - // "(black) and the last element represents 100 percent colorant (green) or 100 percent phosphor (green).", - ); - - public static final IccTagType luminanceTag = new IccTagType( - // "6.4.22 ", - "luminanceTag", "XYZType", 0x6C756D69 - // "Absolute luminance of emissive devices in candelas per square meter as described by the Y channel. The", - // "X and Z channels are ignored in all cases.", - ); - - public static final IccTagType measurementTag = new IccTagType( - // "6.4.23 ", - "measurementTag", "measurementType", 0x6D656173 - // "Alternative measurement specification such as a D65 illuminant instead of the default D50.", - ); - - public static final IccTagType mediaBlackPointTag = new IccTagType( - // "6.4.24 ", - "mediaBlackPointTag", "XYZType", 0x626B7074 - // "This tag specifies the media black point and contains the CIE 1931 XYZ colorimetry of the black point of", - // "the actual medium.", - // "NOTE Previous revisions of this specification contained an error indicating that this tag is used to calculate", - // "ICC-absolute colorimetry. This is not the case.", - ); - - public static final IccTagType mediaWhitePointTag = new IccTagType( - // "6.4.25 ", - "mediaWhitePointTag", "XYZType", 0x77747074 - // "This tag, which is used for generating ICC-absolute colorimetric intent, specifies the XYZ tristimulus values", - // "of the media white point. If the media is measured under an illumination source which has a chromaticity", - // "other than D50, the measured values must be adjusted to D50 using the chromaticAdaptationTag matrix", - // "before recording in the tag. For reflecting and transmitting media, the tag values are specified relative to", - // "the perfect diffuser (which is normalized to a Y value of 1,0) for illuminant D50. For displays, the values", - // "specified must be those of D50 (i.e. 0,9642, 1,0 0,8249) normalized such that Y = 1,0.", - // "See Annex A for a more complete description of the use of the media white point.", - ); - - public static final IccTagType namedColor2Tag = new IccTagType( - // "6.4.26 ", - "namedColor2Tag", "namedColor2Type", 0x6E636C32 - // "Named color information providing a PCS and optional device representation for a list of named colors.", - ); - - public static final IccTagType outputResponseTag = new IccTagType( - // "6.4.27 ", - "outputResponseTag", "responseCurveSet16Type", 0x72657370 - // "Structure containing a description of the device response for which the profile is intended. The content of", - // "this structure is described in 6.5.16.", - // "NOTE The user's attention is called to the possibility that the use of this tag for device calibration may", - // "require use of an invention covered by patent rights. By publication of this specification, no position is", - // "taken with respect to the validity of this claim or of any patent rights in connection therewith. The patent", - // "holder has, however, filed a statement of willingness to grant a license under these rights on reasonable", - // "and nondiscriminatory terms and conditions to applicants desiring to obtain such a license. Details may be", - // "obtained from the publisher.", - ); - - public static final IccTagType preview0Tag = new IccTagType( - // "6.4.28 ", - "preview0Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726530 - // "Preview transformation from PCS to device space and back to the PCS. The processing mechanisms are", - // "described in lut8Type or lut16Type or lutBtoAType.", - // "This tag contains the combination of tag B2A0 and tag A2B1.", - ); - - public static final IccTagType preview1Tag = new IccTagType( - // "6.4.29 ", - "preview1Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726531 - // "Preview transformation from the PCS to device space and back to the PCS. The processing mechanisms", - // "are described in lut8Type or lut16Type or lutBtoAType.", - // "This tag contains the combination of tag B2A1 and tag A2B1.", - ); - - public static final IccTagType preview2Tag = new IccTagType( - // "6.4.30 ", - "preview2Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726532 - // "Preview transformation from PCS to device space and back to the PCS. The processing mechanisms are", - // "described in lut8Type or lut16Type or lutBtoAType.", - // "This tag contains the combination of tag B2A2 and tag A2B1.", - ); - - public static final IccTagType profileDescriptionTag = new IccTagType( - // "6.4.31 ", - "profileDescriptionTag", "multiLocalizedUnicodeType", 0x64657363 - // "Structure containing invariant and localizable versions of the profile description for display. The content of", - // "this structure is described in 6.5.12. This invariant description has no fixed relationship to the actual profile", - // "disk file name.", - ); - - public static final IccTagType profileSequenceDescTag = new IccTagType( - // "6.4.32 ", - "profileSequenceDescTag", "profileSequenceDescType", 0x70736571 - // "Structure containing a description of the profile sequence from source to destination, typically used with", - // "the DeviceLink profile. The content of this structure is described in 6.5.15.", - ); - - public static final IccTagType redMatrixColumnTag = new IccTagType( - // "6.4.33 ", - "redMatrixColumnTag", "XYZType", 0x7258595A - // "The first column in the matrix used in TRC/matrix transforms.", - ); - - public static final IccTagType redTRCTag = new IccTagType( - // "6.4.34 ", - "redTRCTag", "curveType or parametricCurveType", 0x72545243 - // "Red channel tone reproduction curve. The first element represents no colorant (white) or phosphors", - // "(black) and the last element represents 100 percent colorant (red) or 100 percent phosphor (red).", - ); - - public static final IccTagType technologyTag = new IccTagType( - // "6.4.35 ", - "technologyTag", "signatureType", 0x74656368 - // "Device technology information such as CRT, Dye Sublimation, etc. The encoding is such that:", - ); - - public static final IccTagType viewingCondDescTag = new IccTagType( - // "6.4.36 ", - "viewingCondDescTag", "multiLocalizedUnicodeType", 0x76756564 - // "Structure containing invariant and localizable versions of the viewing conditions. The content of this structure", - // "is described in 6.5.12.", - - ); - - public static final IccTagType viewingConditionsTag = new IccTagType( - // "6.4.37 ", - "viewingConditionsTag", "viewingConditionsType", 0x76696577 - // "Viewing conditions parameters. The content of this structure is described in 6.5.25.", - ); - - // public static final IccTagType = new IccTagType( - // // "6.4.37 ", - // "viewingConditionsTag", "viewingConditionsType", 0x76696577 - // // "Viewing conditions parameters. The content of this structure is described in 6.5.25.", - // ); - - public static final IccTagType TagTypes[] = { - AToB0Tag, AToB1Tag, AToB2Tag, blueMatrixColumnTag, blueTRCTag, - BToA0Tag, BToA1Tag, BToA2Tag, calibrationDateTimeTag, - charTargetTag, chromaticAdaptationTag, chromaticityTag, - colorantOrderTag, colorantTableTag, copyrightTag, deviceMfgDescTag, - deviceModelDescTag, gamutTag, grayTRCTag, greenMatrixColumnTag, - greenTRCTag, luminanceTag, measurementTag, mediaBlackPointTag, - mediaWhitePointTag, namedColor2Tag, outputResponseTag, preview0Tag, - preview1Tag, preview2Tag, profileDescriptionTag, - profileSequenceDescTag, redMatrixColumnTag, redTRCTag, - technologyTag, viewingCondDescTag, viewingConditionsTag, - }; - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.icc; + +public enum IccTagTypes implements IccTagType { + A_TO_B0_TAG("AToB0Tag", + "lut8Type or lut16Type or lutAtoBType", 0x41324230 + // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing mechanisms are described in lut8Type or lut16Type or lutAtoBType."); + ), + + // public static final IccTagType AToB0Tag = new IccTagType( + // "AToB0Tag", + // "lut8Type or lut16Type or lutAtoBType", + // "�A2B0� (41324230h)", + // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", + // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", + // ); + + A_TO_B1_TAG("AToB1Tag", + "lut8Type or lut16Type or lutAtoBType", 0x41324231 + // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", + // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", + ), + + A_TO_B2_TAG("AToB2Tag", + "lut8Type or lut16Type or lutAtoBType", 0x41324232 + // "This tag defines a color transform from Device to PCS using lookup table tag element structures. The processing", + // "mechanisms are described in lut8Type or lut16Type or lutAtoBType.", + ), + + BLUE_MATRIX_COLUMN_TAG( + "blueMatrixColumnTag", "XYZType", 0x6258595A + // "The third column in the matrix used in TRC/matrix transforms.", + ), + + BLUE_TRC_TAG("blueTRCTag", + "curveType or parametricCurveType", 0x62545243 + // "Blue channel tone reproduction curve. The first element represents no colorant (white) or phosphors", + // "(black) and the last element represents 100 percent colorant (blue) or 100 percent phosphor (blue).", + ), + + B_TO_A0_TAG("BToA0Tag", + "lut8Type or lut16Type or lutBtoAType", 0x42324130 + // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", + // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", + ), + + B_TO_A1_TAG("BToA1Tag", + "lut8Type or lut16Type or lutBtoAType", 0x42324131 + // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", + // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", + ), + + B_TO_A2_TAG("BToA2Tag", + "lut8Type or lut16Type or lutBtoAType", 0x42324132 + // "This tag defines a color transform from PCS to Device using the lookup table tag element structures. The", + // "processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", + ), + + CALIBRATION_DATE_TIME_TAG( + "calibrationDateTimeTag", "dateTimeType", 0x63616C74 + // "Profile calibration date and time. Initially, this tag matches the contents of the profile header�s creation", + // "date/time field. This allows applications and utilities to verify if this profile matches a vendor�s profile and", + // "how recently calibration has been performed.", + ), + + CHAR_TARGET_TAG( + "charTargetTag", "textType", 0x74617267 + // "This tag contains the name of the registered characterization data set, or it contains the measurement data", + // "for a characterization target. This tag is provided so that distributed utilities can identify the underlying", + // "characterization data, create transforms \"on the fly\" or check the current performance against the original", + // "device performance.", + // "The first seven characters of the text shall identify the nature of the characterization data.", + // "If the first seven characters are \"ICCHDAT\", then the remainder of the text shall be a single space followed", + // "by the Reference Name of a characterization data set in the ICC Characterization Data Registry and terminated", + // "with a NULL byte (00h). The Reference Name in the text must match exactly (including case) the", + // "Reference Name in the registry.", + // "If the first seven characters match one of the identifiers defined in an ANSI or ISO standard, then the tag", + // "embeds the exact data file format defined in that standard. Each of these file formats contains an identifying", + // "character string as the first seven characters of the format, allowing an external parser to determine", + // "which data file format is being used. This provides the facilities to include a wide range of targets using a", + // "variety of measurement specifications in a standard manner.", + // "NOTE: It is highly recommended that the profileDescriptionTag also include an identification of the characterization", + // "data that was used in the creation of the profile (e.g. \"Based on CGATS TR 001\").", + ), + + CHROMATIC_ADAPTATION_TAG( + "chromaticAdaptationTag", "s15Fixed16ArrayType", 0x63686164 + // "This tag converts an XYZ color, measured at a device's specific illumination conditions, to an XYZ color in", + // "the PCS illumination conditions after complete adaptation.", + // "The tag reflects a survey of the currently used methods of conversion, all of which can be formulated as a", + // "matrix transformation (see Annex E). Such a 3 by 3 chromatic adaptation matrix is organized as a 9-element", + // "array of signed 15.16 numbers (s15Fixed16ArrayType tag). Similarly as in the other occurrences of a", + // "3 by 3 matrix in the ICC tags, the dimension corresponding to the matrix rows varies least rapidly while the", + // "one corresponding to the matrix columns varies most rapidly.", + // "(19)", + // "(20)", + // "array a0 a1 a2 a3 a4 a5 a6 a7 a8 =", + // "Xpcs", + // "Ypcs", + // "Zpcs", + // "a0 a1 a2", + // "a3 a4 a5", + // "a6 a7 a8", + // "Xsrc", + // "Ysrc", + // "Zsrc", + // "=", + // "Where XYZsrc represents the measured value in the actual device viewing condition and XYZpcs represents", + // "the chromatically adapted value in the PCS.", + // "The chromatic adaptation matrix is a combination of three separate conversions:", + // "1) Conversion of source CIE XYZ tristimulus values to cone response tristimulus values.", + // "2) Adjustment of the cone response values for an observer�s chromatic adaptation.", + // "3) Conversion of the adjusted cone response tristimulus back to CIE XYZ values.", + ), + + CHROMATICITY_TAG( + "chromaticityTag", "chromaticityType", 0x6368726D + // "The data and type of phosphor/colorant chromaticity set.", + ), + + COLORANT_ORDER_TAG( + "colorantOrderTag", "colorantOrderType", 0x636C726F + // "This tag specifies the laydown order of colorants.", + ), + + COLORANT_TABLE_TAG( + "colorantTableTag", "colorantTableType", 0x636C7274 + // "This tag identifies the colorants used in the profile by a unique name and an XYZ or L*a*b* value.", + // "This is a required tag for profiles where the color space defined in the header is xCLR, where x is one of", + // "the allowed numbers from 2 through Fh, per Table 13. See Section 6.3.3.2, Section 6.3.4.1.", + ), + + COPYRIGHT_TAG( + "copyrightTag", "multiLocalizedUnicodeType", 0x63707274 + // "This tag contains the text copyright information for the profile.", + ), + + DEVICE_MFG_DESC_TAG( + "deviceMfgDescTag", "multiLocalizedUnicodeType", 0x646D6E64 + // "Structure containing invariant and localizable versions of the device manufacturer for display. The content", + // "of this structure is described in 6.5.12.", + ), + + DEVICE_MODEL_DESC_TAG( + "deviceModelDescTag", "multiLocalizedUnicodeType", 0x646D6464 + // "Structure containing invariant and localizable versions of the device model for display. The content of this", + // "structure is described in 6.5.12.", + ), + + GAMUT_TAG("gamutTag", + "lut8Type or lut16Type or lutBtoAType", 0x67616D74 + // "Out of gamut tag. The processing mechanisms are described in lut8Type or lut16Type or lutBtoAType.", + // "This tag takes PCS values as its input and produces a single channel of output. If the output value is 0, the", + // "PCS color is in-gamut. If the output is non-zero, the PCS color is out-of-gamut, with the output value �n+1�", + // "being at least as far out of gamut as the output value �n�.", + ), + + GRAY_TRC_TAG("grayTRCTag", + "curveType or parametricCurveType", 0x6B545243 + // "Gray tone reproduction curve. The tone reproduction curve provides the necessary information to convert", + // "between a single device channel and the CIEXYZ encoding of the profile connection space. The first element", + // "represents black and the last element represents white.", + ), + + GREEN_MATRIX_COLUMN_TAG( + "greenMatrixColumnTag", "XYZType", 0x6758595A + // "The second column in the matrix used in TRC/matrix transforms.", + ), + + GREEN_TRC_TAG( + // "6.4.21 ", + "greenTRCTag", "curveType or parametricCurveType", 0x67545243 + // "Green channel tone reproduction curve. The first element represents no colorant (white) or phosphors", + // "(black) and the last element represents 100 percent colorant (green) or 100 percent phosphor (green).", + ), + + LUMINANCE_TAG( + // "6.4.22 ", + "luminanceTag", "XYZType", 0x6C756D69 + // "Absolute luminance of emissive devices in candelas per square meter as described by the Y channel. The", + // "X and Z channels are ignored in all cases.", + ), + + MEASUREMENT_TAG( + // "6.4.23 ", + "measurementTag", "measurementType", 0x6D656173 + // "Alternative measurement specification such as a D65 illuminant instead of the default D50.", + ), + + MEDIA_BLACK_POINT_TAG( + // "6.4.24 ", + "mediaBlackPointTag", "XYZType", 0x626B7074 + // "This tag specifies the media black point and contains the CIE 1931 XYZ colorimetry of the black point of", + // "the actual medium.", + // "NOTE Previous revisions of this specification contained an error indicating that this tag is used to calculate", + // "ICC-absolute colorimetry. This is not the case.", + ), + + MEDIA_WHITE_POINT_TAG( + // "6.4.25 ", + "mediaWhitePointTag", "XYZType", 0x77747074 + // "This tag, which is used for generating ICC-absolute colorimetric intent, specifies the XYZ tristimulus values", + // "of the media white point. If the media is measured under an illumination source which has a chromaticity", + // "other than D50, the measured values must be adjusted to D50 using the chromaticAdaptationTag matrix", + // "before recording in the tag. For reflecting and transmitting media, the tag values are specified relative to", + // "the perfect diffuser (which is normalized to a Y value of 1,0) for illuminant D50. For displays, the values", + // "specified must be those of D50 (i.e. 0,9642, 1,0 0,8249) normalized such that Y = 1,0.", + // "See Annex A for a more complete description of the use of the media white point.", + ), + + NAMED_COLOR_2_TAG( + // "6.4.26 ", + "namedColor2Tag", "namedColor2Type", 0x6E636C32 + // "Named color information providing a PCS and optional device representation for a list of named colors.", + ), + + OUTPUT_RESPONSE_TAG( + // "6.4.27 ", + "outputResponseTag", "responseCurveSet16Type", 0x72657370 + // "Structure containing a description of the device response for which the profile is intended. The content of", + // "this structure is described in 6.5.16.", + // "NOTE The user�s attention is called to the possibility that the use of this tag for device calibration may", + // "require use of an invention covered by patent rights. By publication of this specification, no position is", + // "taken with respect to the validity of this claim or of any patent rights in connection therewith. The patent", + // "holder has, however, filed a statement of willingness to grant a license under these rights on reasonable", + // "and nondiscriminatory terms and conditions to applicants desiring to obtain such a license. Details may be", + // "obtained from the publisher.", + ), + + PREVIEW_0_TAG( + // "6.4.28 ", + "preview0Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726530 + // "Preview transformation from PCS to device space and back to the PCS. The processing mechanisms are", + // "described in lut8Type or lut16Type or lutBtoAType.", + // "This tag contains the combination of tag B2A0 and tag A2B1.", + ), + + PREVIEW_1_TAG( + // "6.4.29 ", + "preview1Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726531 + // "Preview transformation from the PCS to device space and back to the PCS. The processing mechanisms", + // "are described in lut8Type or lut16Type or lutBtoAType.", + // "This tag contains the combination of tag B2A1 and tag A2B1.", + ), + + PREVIEW_2_TAG( + // "6.4.30 ", + "preview2Tag", "lut8Type or lut16Type or lutBtoAType", 0x70726532 + // "Preview transformation from PCS to device space and back to the PCS. The processing mechanisms are", + // "described in lut8Type or lut16Type or lutBtoAType.", + // "This tag contains the combination of tag B2A2 and tag A2B1.", + ), + + PROFILE_DESCRIPTION_TAG( + // "6.4.31 ", + "profileDescriptionTag", "multiLocalizedUnicodeType", 0x64657363 + // "Structure containing invariant and localizable versions of the profile description for display. The content of", + // "this structure is described in 6.5.12. This invariant description has no fixed relationship to the actual profile", + // "disk file name.", + ), + + PROFILE_SEQUENCE_DESC_TAG( + // "6.4.32 ", + "profileSequenceDescTag", "profileSequenceDescType", 0x70736571 + // "Structure containing a description of the profile sequence from source to destination, typically used with", + // "the DeviceLink profile. The content of this structure is described in 6.5.15.", + ), + + RED_MATRIX_COLUMN_TAG( + // "6.4.33 ", + "redMatrixColumnTag", "XYZType", 0x7258595A + // "The first column in the matrix used in TRC/matrix transforms.", + ), + + RED_TRC_TAG( + // "6.4.34 ", + "redTRCTag", "curveType or parametricCurveType", 0x72545243 + // "Red channel tone reproduction curve. The first element represents no colorant (white) or phosphors", + // "(black) and the last element represents 100 percent colorant (red) or 100 percent phosphor (red).", + ), + + TECHNOLOGY_TAG( + // "6.4.35 ", + "technologyTag", "signatureType", 0x74656368 + // "Device technology information such as CRT, Dye Sublimation, etc. The encoding is such that:", + ), + + VIEWING_COND_DESC_TAG( + // "6.4.36 ", + "viewingCondDescTag", "multiLocalizedUnicodeType", 0x76756564 + // "Structure containing invariant and localizable versions of the viewing conditions. The content of this structure", + // "is described in 6.5.12.", + + ), + + VIEWING_CONDITIONS_TAG( + // "6.4.37 ", + "viewingConditionsTag", "viewingConditionsType", 0x76696577 + // "Viewing conditions parameters. The content of this structure is described in 6.5.25.", + ); + + public final String name; + public final String typeDescription; + public final int signature; + + IccTagTypes(final String name, final String typeDescription, final int signature) { + this.name = name; + this.typeDescription = typeDescription; + this.signature = signature; + } + + public String getName() { + return name; + } + + public String getTypeDescription() { + return typeDescription; + } + + public int getSignature() { + return signature; + } +} diff --git a/src/main/java/org/apache/commons/imaging/icc/package-info.java b/src/main/java/org/apache/commons/imaging/icc/package-info.java new file mode 100644 index 0000000..589cd51 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/icc/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * ICC color profile. + */ +package org.apache.commons.imaging.icc; diff --git a/src/main/java/org/apache/commons/imaging/package-info.java b/src/main/java/org/apache/commons/imaging/package-info.java new file mode 100644 index 0000000..9a97b1c --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * The main package for Apache Commons Imaging. + */ +package org.apache.commons.imaging; + diff --git a/src/main/java/org/apache/sanselan/SanselanException.java b/src/main/java/org/apache/commons/imaging/palette/ColorComponent.java similarity index 72% rename from src/main/java/org/apache/sanselan/SanselanException.java rename to src/main/java/org/apache/commons/imaging/palette/ColorComponent.java index 8005f00..be88d79 100644 --- a/src/main/java/org/apache/sanselan/SanselanException.java +++ b/src/main/java/org/apache/commons/imaging/palette/ColorComponent.java @@ -1,32 +1,34 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -public class SanselanException extends Exception -{ - static final long serialVersionUID = -1L; - - public SanselanException(String s) - { - super(s); - } - - public SanselanException(String s, Exception e) - { - super(s, e); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +enum ColorComponent { + ALPHA(24), + RED(16), + GREEN(8), + BLUE(0); + + private final int shift; + + private ColorComponent(int shift) { + this.shift = shift; + } + + public int argbComponent(int argb) { + return (argb >> shift) & 0xff; + } +} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCTypeLookup.java b/src/main/java/org/apache/commons/imaging/palette/ColorCount.java similarity index 53% rename from src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCTypeLookup.java rename to src/main/java/org/apache/commons/imaging/palette/ColorCount.java index 5008e63..ed48d4b 100644 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCTypeLookup.java +++ b/src/main/java/org/apache/commons/imaging/palette/ColorCount.java @@ -1,43 +1,50 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -import java.util.HashMap; -import java.util.Map; - -public abstract class IPTCTypeLookup implements IPTCConstants -{ - - private static final Map IPTC_TYPE_MAP = new HashMap(); - static - { - for (int i = 0; i < IPTC_TYPES.length; i++) - { - IPTCType iptcType = IPTC_TYPES[i]; - Integer key = new Integer(iptcType.type); - IPTC_TYPE_MAP.put(key, iptcType); - } - } - - public static final IPTCType getIptcType(int type) - { - Integer key = new Integer(type); - if (!IPTC_TYPE_MAP.containsKey(key)) - return IPTCType.getUnknown(type); - return (IPTCType) IPTC_TYPE_MAP.get(key); - } -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +class ColorCount { + public final int argb; + public int count; + public final int alpha; + public final int red; + public final int green; + public final int blue; + + public ColorCount(final int argb) { + this.argb = argb; + + alpha = 0xff & (argb >> 24); + red = 0xff & (argb >> 16); + green = 0xff & (argb >> 8); + blue = 0xff & (argb >> 0); + } + + @Override + public int hashCode() { + return argb; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof ColorCount) { + final ColorCount other = (ColorCount) o; + return other.argb == this.argb; + } + return false; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/palette/ColorGroup.java b/src/main/java/org/apache/commons/imaging/palette/ColorGroup.java new file mode 100644 index 0000000..297f1ac --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/ColorGroup.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; + +class ColorGroup { + // public final ColorGroup parent; + public ColorGroupCut cut; + // public final List children = new ArrayList(); + public int paletteIndex = -1; + + public final List colorCounts; + public final boolean ignoreAlpha; + public int minRed = Integer.MAX_VALUE; + public int maxRed = Integer.MIN_VALUE; + public int minGreen = Integer.MAX_VALUE; + public int maxGreen = Integer.MIN_VALUE; + public int minBlue = Integer.MAX_VALUE; + public int maxBlue = Integer.MIN_VALUE; + public int minAlpha = Integer.MAX_VALUE; + public int maxAlpha = Integer.MIN_VALUE; + + public final int alphaDiff; + public final int redDiff; + public final int greenDiff; + public final int blueDiff; + + public final int maxDiff; + public final int diffTotal; + public final int totalPoints; + + public ColorGroup(final List colorCounts, final boolean ignoreAlpha) throws ImageWriteException { + this.colorCounts = colorCounts; + this.ignoreAlpha = ignoreAlpha; + + if (colorCounts.size() < 1) { + throw new ImageWriteException("empty color_group"); + } + + int total = 0; + for (ColorCount color : colorCounts) { + total += color.count; + + minAlpha = Math.min(minAlpha, color.alpha); + maxAlpha = Math.max(maxAlpha, color.alpha); + minRed = Math.min(minRed, color.red); + maxRed = Math.max(maxRed, color.red); + minGreen = Math.min(minGreen, color.green); + maxGreen = Math.max(maxGreen, color.green); + minBlue = Math.min(minBlue, color.blue); + maxBlue = Math.max(maxBlue, color.blue); + } + this.totalPoints = total; + + alphaDiff = maxAlpha - minAlpha; + redDiff = maxRed - minRed; + greenDiff = maxGreen - minGreen; + blueDiff = maxBlue - minBlue; + maxDiff = Math.max( + ignoreAlpha ? redDiff : Math.max(alphaDiff, redDiff), + Math.max(greenDiff, blueDiff)); + diffTotal = (ignoreAlpha ? 0 : alphaDiff) + redDiff + greenDiff + blueDiff; + } + + public boolean contains(final int argb) { + final int alpha = 0xff & (argb >> 24); + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + + if (!ignoreAlpha && (alpha < minAlpha || alpha > maxAlpha)) { + return false; + } + if (red < minRed || red > maxRed) { + return false; + } + if (green < minGreen || green > maxGreen) { + return false; + } + if (blue < minBlue || blue > maxBlue) { + return false; + } + return true; + } + + public int getMedianValue() { + long countTotal = 0; + long alphaTotal = 0; + long redTotal = 0; + long greenTotal = 0; + long blueTotal = 0; + + for (ColorCount color : colorCounts) { + countTotal += color.count; + alphaTotal += color.count * color.alpha; + redTotal += color.count * color.red; + greenTotal += color.count * color.green; + blueTotal += color.count * color.blue; + } + + final int alpha = ignoreAlpha ? 0xff : (int) Math.round((double) alphaTotal / countTotal); + final int red = (int) Math.round((double) redTotal / countTotal); + final int green = (int) Math.round((double) greenTotal / countTotal); + final int blue = (int) Math.round((double) blueTotal / countTotal); + + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + + @Override + public String toString() { + return "{ColorGroup. minRed: " + Integer.toHexString(minRed) + + ", maxRed: " + Integer.toHexString(maxRed) + + ", minGreen: " + Integer.toHexString(minGreen) + + ", maxGreen: " + Integer.toHexString(maxGreen) + + ", minBlue: " + Integer.toHexString(minBlue) + + ", maxBlue: " + Integer.toHexString(maxBlue) + + ", minAlpha: " + Integer.toHexString(minAlpha) + + ", maxAlpha: " + Integer.toHexString(maxAlpha) + + ", maxDiff: " + Integer.toHexString(maxDiff) + + ", diffTotal: " + diffTotal + "}"; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/palette/ColorGroupCut.java b/src/main/java/org/apache/commons/imaging/palette/ColorGroupCut.java new file mode 100644 index 0000000..f4c3e14 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/ColorGroupCut.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +class ColorGroupCut { + public final ColorGroup less; + public final ColorGroup more; + public final ColorComponent mode; + public final int limit; + + public ColorGroupCut(final ColorGroup less, final ColorGroup more, final ColorComponent mode, final int limit) { + this.less = less; + this.more = more; + this.mode = mode; + this.limit = limit; + } + + public ColorGroup getColorGroup(final int argb) { + final int value = mode.argbComponent(argb); + if (value <= limit) { + return less; + } + return more; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/palette/ColorSpaceSubset.java b/src/main/java/org/apache/commons/imaging/palette/ColorSpaceSubset.java new file mode 100644 index 0000000..a6a2f06 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/ColorSpaceSubset.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.io.Serializable; +import java.util.Comparator; + +class ColorSpaceSubset { + final int[] mins; + final int[] maxs; + final int precision; + final int precisionMask; + final int total; + int rgb; // median + // the index in the palette. + private int index; + public static final RgbComparator RGB_COMPARATOR = new RgbComparator(); + + ColorSpaceSubset(final int total, final int precision) { + this.total = total; + this.precision = precision; + precisionMask = (1 << precision) - 1; + + mins = new int[PaletteFactory.COMPONENTS]; + maxs = new int[PaletteFactory.COMPONENTS]; + for (int i = 0; i < PaletteFactory.COMPONENTS; i++) { + mins[i] = 0; + maxs[i] = precisionMask; + } + + rgb = -1; + } + + ColorSpaceSubset(final int total, final int precision, final int[] mins, final int[] maxs) { + this.total = total; + this.precision = precision; + this.mins = mins; + this.maxs = maxs; + precisionMask = (1 << precision) - 1; + + rgb = -1; + } + + public final boolean contains(int red, int green, int blue) { + red >>= (8 - precision); + if (mins[0] > red) { + return false; + } + if (maxs[0] < red) { + return false; + } + + green >>= (8 - precision); + if (mins[1] > green) { + return false; + } + if (maxs[1] < green) { + return false; + } + + blue >>= (8 - precision); + if (mins[2] > blue) { + return false; + } + if (maxs[2] < blue) { + return false; + } + + return true; + } + + public void dump(final String prefix) { + final int rdiff = maxs[0] - mins[0] + 1; + final int gdiff = maxs[1] - mins[1] + 1; + final int bdiff = maxs[2] - mins[2] + 1; + final int colorArea = rdiff * gdiff * bdiff; + + System.out.println(prefix + ": [" + Integer.toHexString(rgb) + + "] total : " + total + // + " (" + // + (100.0 * (double) total / (double) total_area) + // + " %)" + ); + System.out.println("\t" + "rgb: " + Integer.toHexString(rgb) + ", " + + "red: " + Integer.toHexString(mins[0] << (8 - precision)) + + ", " + Integer.toHexString(maxs[0] << (8 - precision)) + ", " + + "green: " + Integer.toHexString(mins[1] << (8 - precision)) + + ", " + Integer.toHexString(maxs[1] << (8 - precision)) + ", " + + "blue: " + Integer.toHexString(mins[2] << (8 - precision)) + + ", " + Integer.toHexString(maxs[2] << (8 - precision))); + System.out.println("\t" + "red: " + mins[0] + ", " + maxs[0] + ", " + + "green: " + mins[1] + ", " + maxs[1] + ", " + "blue: " + + mins[2] + ", " + maxs[2]); + System.out + .println("\t" + "rdiff: " + rdiff + ", " + "gdiff: " + gdiff + + ", " + "bdiff: " + bdiff + ", " + "colorArea: " + + colorArea); + } + + public void dumpJustRGB(final String prefix) { + System.out.println("\t" + "rgb: " + Integer.toHexString(rgb) + ", " + + "red: " + Integer.toHexString(mins[0] << (8 - precision)) + + ", " + Integer.toHexString(maxs[0] << (8 - precision)) + ", " + + "green: " + Integer.toHexString(mins[1] << (8 - precision)) + + ", " + Integer.toHexString(maxs[1] << (8 - precision)) + ", " + + "blue: " + Integer.toHexString(mins[2] << (8 - precision)) + + ", " + Integer.toHexString(maxs[2] << (8 - precision))); + } + + public int getArea() { + final int rdiff = maxs[0] - mins[0] + 1; + final int gdiff = maxs[1] - mins[1] + 1; + final int bdiff = maxs[2] - mins[2] + 1; + final int colorArea = rdiff * gdiff * bdiff; + + return colorArea; + + } + + public void setAverageRGB(final int[] table) { + long redsum = 0; + long greensum = 0; + long bluesum = 0; + + for (int red = mins[0]; red <= maxs[0]; red++) { + for (int green = mins[1]; green <= maxs[1]; green++) { + for (int blue = mins[2]; blue <= maxs[2]; blue++) { + // note: order reversed + final int idx = (blue << (2 * precision)) + | (green << (1 * precision)) + | (red << (0 * precision)); + final int count = table[idx]; + redsum += count * (red << (8 - precision)); + greensum += count * (green << (8 - precision)); + bluesum += count * (blue << (8 - precision)); + } + } + } + + redsum /= total; + greensum /= total; + bluesum /= total; + rgb = (int) (((redsum & 0xff) << 16) | ((greensum & 0xff) << 8) | ((bluesum & 0xff) << 0)); + } + + public final int getIndex() { + return index; + } + + public final void setIndex(final int i) { + index = i; + } + + public static class RgbComparator implements Comparator, Serializable { + private static final long serialVersionUID = 509214838111679029L; + + public int compare(final ColorSpaceSubset c1, final ColorSpaceSubset c2) { + return c1.rgb - c2.rgb; + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/Dithering.java b/src/main/java/org/apache/commons/imaging/palette/Dithering.java new file mode 100644 index 0000000..da4b210 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/Dithering.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import com.google.code.appengine.awt.image.BufferedImage; + +import org.apache.commons.imaging.ImageWriteException; + +/** + * Dithering algorithms to use when quantizing an image to paletted form. + */ +public final class Dithering { + private Dithering() { + } + + /** + * Changes the given image to only use colors from the given palette, + * applying Floyd-Steinberg dithering in the process. Ensure that + * your alpha values in the image and in the palette are consistent. + * + * @param image the image to change + * @param palette the palette to use + * @throws ImageWriteException + */ + public static void applyFloydSteinbergDithering(final BufferedImage image, final Palette palette) throws ImageWriteException { + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + final int argb = image.getRGB(x, y); + final int index = palette.getPaletteIndex(argb); + final int nextArgb = palette.getEntry(index); + image.setRGB(x, y, nextArgb); + + final int a = (argb >> 24) & 0xff; + final int r = (argb >> 16) & 0xff; + final int g = (argb >> 8) & 0xff; + final int b = argb & 0xff; + + final int na = (nextArgb >> 24) & 0xff; + final int nr = (nextArgb >> 16) & 0xff; + final int ng = (nextArgb >> 8) & 0xff; + final int nb = nextArgb & 0xff; + + final int errA = a - na; + final int errR = r - nr; + final int errG = g - ng; + final int errB = b - nb; + + if (x + 1 < image.getWidth()) { + int update = adjustPixel(image.getRGB(x + 1, y), errA, errR, errG, errB, 7); + image.setRGB(x + 1, y, update); + if (y + 1 < image.getHeight()) { + update = adjustPixel(image.getRGB(x + 1, y + 1), errA, errR, errG, errB, 1); + image.setRGB(x + 1, y + 1, update); + } + } + if (y + 1 < image.getHeight()) { + int update = adjustPixel(image.getRGB(x, y + 1), errA, errR, errG, errB, 5); + image.setRGB(x, y + 1, update); + if (x - 1 >= 0) { + update = adjustPixel(image.getRGB(x - 1, y + 1), errA, errR, errG, errB, 3); + image.setRGB(x - 1, y + 1, update); + } + + } + } + } + } + + private static int adjustPixel(final int argb, final int errA, final int errR, final int errG, final int errB, final int mul) { + int a = (argb >> 24) & 0xff; + int r = (argb >> 16) & 0xff; + int g = (argb >> 8) & 0xff; + int b = argb & 0xff; + + a += errA * mul / 16; + r += errR * mul / 16; + g += errG * mul / 16; + b += errB * mul / 16; + + if (a < 0) { + a = 0; + } else if (a > 0xff) { + a = 0xff; + } + if (r < 0) { + r = 0; + } else if (r > 0xff) { + r = 0xff; + } + if (g < 0) { + g = 0; + } else if (g > 0xff) { + g = 0xff; + } + if (b < 0) { + b = 0; + } else if (b > 0xff) { + b = 0xff; + } + + return (a << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/MedianCutImplementation.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutImplementation.java new file mode 100644 index 0000000..f340a11 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutImplementation.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; + +public abstract class MedianCutImplementation { + public abstract boolean performNextMedianCut(final List colorGroups, final boolean ignoreAlpha) + throws ImageWriteException; +} diff --git a/src/main/java/org/apache/commons/imaging/palette/MedianCutLongestAxisImplementation.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutLongestAxisImplementation.java new file mode 100644 index 0000000..444d280 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutLongestAxisImplementation.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; + +public class MedianCutLongestAxisImplementation extends MedianCutImplementation { + private static final Comparator COMPARATOR = new Comparator() { + public int compare(final ColorGroup cg1, final ColorGroup cg2) { + if (cg1.maxDiff == cg2.maxDiff) { + return cg2.diffTotal - cg1.diffTotal; + } + return cg2.maxDiff - cg1.maxDiff; + } + }; + + @Override + public boolean performNextMedianCut(final List colorGroups, final boolean ignoreAlpha) + throws ImageWriteException { + Collections.sort(colorGroups, COMPARATOR); + final ColorGroup colorGroup = colorGroups.get(0); + + if (colorGroup.maxDiff == 0) { + return false; + } + if (!ignoreAlpha + && colorGroup.alphaDiff > colorGroup.redDiff + && colorGroup.alphaDiff > colorGroup.greenDiff + && colorGroup.alphaDiff > colorGroup.blueDiff) { + doCut(colorGroup, ColorComponent.ALPHA, colorGroups, ignoreAlpha); + } else if (colorGroup.redDiff > colorGroup.greenDiff + && colorGroup.redDiff > colorGroup.blueDiff) { + doCut(colorGroup, ColorComponent.RED, colorGroups, ignoreAlpha); + } else if (colorGroup.greenDiff > colorGroup.blueDiff) { + doCut(colorGroup, ColorComponent.GREEN, colorGroups, ignoreAlpha); + } else { + doCut(colorGroup, ColorComponent.BLUE, colorGroups, ignoreAlpha); + } + return true; + } + + private void doCut(final ColorGroup colorGroup, final ColorComponent mode, + final List colorGroups, final boolean ignoreAlpha) throws ImageWriteException { + + final Comparator comp = new Comparator() { + public int compare(final ColorCount c1, final ColorCount c2) { + switch (mode) { + case ALPHA: + return c1.alpha - c2.alpha; + case RED: + return c1.red - c2.red; + case GREEN: + return c1.green - c2.green; + case BLUE: + return c1.blue - c2.blue; + default: + return 0; + } + } + }; + + Collections.sort(colorGroup.colorCounts, comp); + final int countHalf = (int) Math.round((double) colorGroup.totalPoints / 2); + int oldCount = 0; + int newCount = 0; + int medianIndex; + for (medianIndex = 0; medianIndex < colorGroup.colorCounts.size(); medianIndex++) { + final ColorCount colorCount = colorGroup.colorCounts.get(medianIndex); + + newCount += colorCount.count; + + if (newCount < countHalf) { + oldCount = newCount; + } else { + break; + } + } + + if (medianIndex == colorGroup.colorCounts.size() - 1) { + medianIndex--; + } else if (medianIndex > 0) { + final int newDiff = Math.abs(newCount - countHalf); + final int oldDiff = Math.abs(countHalf - oldCount); + if (oldDiff < newDiff) { + medianIndex--; + } + } + + colorGroups.remove(colorGroup); + final List colorCounts1 = new ArrayList( + colorGroup.colorCounts.subList(0, medianIndex + 1)); + final List colorCounts2 = new ArrayList( + colorGroup.colorCounts.subList(medianIndex + 1, + colorGroup.colorCounts.size())); + + ColorGroup less = new ColorGroup(new ArrayList(colorCounts1), ignoreAlpha); + colorGroups.add(less); + ColorGroup more = new ColorGroup(new ArrayList(colorCounts2), ignoreAlpha); + colorGroups.add(more); + + final ColorCount medianValue = colorGroup.colorCounts.get(medianIndex); + int limit; + switch (mode) { + case ALPHA: + limit = medianValue.alpha; + break; + case RED: + limit = medianValue.red; + break; + case GREEN: + limit = medianValue.green; + break; + case BLUE: + limit = medianValue.blue; + break; + default: + throw new Error("Bad mode."); + } + colorGroup.cut = new ColorGroupCut(less, more, mode, limit); + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/MedianCutMostPopulatedBoxesImplementation.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutMostPopulatedBoxesImplementation.java new file mode 100644 index 0000000..38a69b5 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutMostPopulatedBoxesImplementation.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.imaging.ImageWriteException; + +public class MedianCutMostPopulatedBoxesImplementation extends MedianCutImplementation { + @Override + public boolean performNextMedianCut(final List colorGroups, + final boolean ignoreAlpha) throws ImageWriteException { + int maxPoints = 0; + ColorGroup colorGroup = null; + for (ColorGroup group : colorGroups) { + if (group.maxDiff > 0) { + if (group.totalPoints > maxPoints) { + colorGroup = group; + maxPoints = group.totalPoints; + } + } + } + if (colorGroup == null) { + return false; + } + + + + double bestScore = Double.MAX_VALUE; + ColorComponent bestColorComponent = null; + int bestMedianIndex = -1; + for (final ColorComponent colorComponent : ColorComponent.values()) { + if (ignoreAlpha && colorComponent == ColorComponent.ALPHA) { + continue; + } + Collections.sort(colorGroup.colorCounts, new ColorComparer(colorComponent)); + final int countHalf = (int) Math.round((double) colorGroup.totalPoints / 2); + int oldCount = 0; + int newCount = 0; + int medianIndex; + for (medianIndex = 0; medianIndex < colorGroup.colorCounts.size(); medianIndex++) { + final ColorCount colorCount = colorGroup.colorCounts.get(medianIndex); + + newCount += colorCount.count; + + if (newCount < countHalf) { + oldCount = newCount; + } else { + break; + } + } + if (medianIndex == colorGroup.colorCounts.size() - 1) { + medianIndex--; + } else if (medianIndex > 0) { + final int newDiff = Math.abs(newCount - countHalf); + final int oldDiff = Math.abs(countHalf - oldCount); + if (oldDiff < newDiff) { + medianIndex--; + } + } + + final List lowerColors = new ArrayList( + colorGroup.colorCounts.subList(0, medianIndex + 1)); + final List upperColors = new ArrayList( + colorGroup.colorCounts.subList(medianIndex + 1, + colorGroup.colorCounts.size())); + if (lowerColors.isEmpty() || upperColors.isEmpty()) { + continue; + } + final ColorGroup lowerGroup = new ColorGroup(lowerColors, ignoreAlpha); + final ColorGroup upperGroup = new ColorGroup(upperColors, ignoreAlpha); + final int diff = Math.abs(lowerGroup.totalPoints - upperGroup.totalPoints); + final double score = diff / (double) Math.max(lowerGroup.totalPoints, upperGroup.totalPoints); + if (score < bestScore) { + bestScore = score; + bestColorComponent = colorComponent; + bestMedianIndex = medianIndex; + } + } + + if (bestColorComponent == null) { + return false; + } + + Collections.sort(colorGroup.colorCounts, new ColorComparer(bestColorComponent)); + final List lowerColors = new ArrayList( + colorGroup.colorCounts.subList(0, bestMedianIndex + 1)); + final List upperColors = new ArrayList( + colorGroup.colorCounts.subList(bestMedianIndex + 1, + colorGroup.colorCounts.size())); + final ColorGroup lowerGroup = new ColorGroup(lowerColors, ignoreAlpha); + final ColorGroup upperGroup = new ColorGroup(upperColors, ignoreAlpha); + colorGroups.remove(colorGroup); + colorGroups.add(lowerGroup); + colorGroups.add(upperGroup); + + final ColorCount medianValue = colorGroup.colorCounts.get(bestMedianIndex); + int limit; + switch (bestColorComponent) { + case ALPHA: + limit = medianValue.alpha; + break; + case RED: + limit = medianValue.red; + break; + case GREEN: + limit = medianValue.green; + break; + case BLUE: + limit = medianValue.blue; + break; + default: + throw new Error("Bad mode."); + } + colorGroup.cut = new ColorGroupCut(lowerGroup, upperGroup, bestColorComponent, limit); + return true; + } + + private static class ColorComparer implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + private final ColorComponent colorComponent; + + public ColorComparer(final ColorComponent colorComponent) { + this.colorComponent = colorComponent; + } + + public int compare(final ColorCount c1, final ColorCount c2) { + switch (colorComponent) { + case ALPHA: + return c1.alpha - c2.alpha; + case RED: + return c1.red - c2.red; + case GREEN: + return c1.green - c2.green; + case BLUE: + return c1.blue - c2.blue; + default: + return 0; + } + } + } + +} diff --git a/src/main/java/org/apache/sanselan/util/ParamMap.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutPalette.java similarity index 64% rename from src/main/java/org/apache/sanselan/util/ParamMap.java rename to src/main/java/org/apache/commons/imaging/palette/MedianCutPalette.java index dd95cbf..8bb29b3 100644 --- a/src/main/java/org/apache/sanselan/util/ParamMap.java +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutPalette.java @@ -1,34 +1,37 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.util.Map; - -public class ParamMap { - - public static boolean getParamBoolean(Map params, Object key, - boolean default_value) { - boolean result = default_value; - { - Object o = params == null ? null : params.get(key); - if (o != null && o instanceof Boolean) - result = ((Boolean) o).booleanValue(); - } - return result; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +class MedianCutPalette extends SimplePalette { + private final ColorGroup root; + + public MedianCutPalette(final ColorGroup root, final int[] palette) { + super(palette); + this.root = root; + } + + @Override + public int getPaletteIndex(final int rgb) { + ColorGroup cg = root; + + while (cg.cut != null) { + cg = cg.cut.getColorGroup(rgb); + } + + return cg.paletteIndex; + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java new file mode 100644 index 0000000..89d2815 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import com.google.code.appengine.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.util.Debug; + +public class MedianCutQuantizer { + private final boolean ignoreAlpha; + + public MedianCutQuantizer(final boolean ignoreAlpha) { + this.ignoreAlpha = ignoreAlpha; + } + + private Map groupColors1(final BufferedImage image, final int max, + final int mask) { + final Map colorMap = new HashMap(); + + final int width = image.getWidth(); + final int height = image.getHeight(); + + final int[] row = new int[width]; + for (int y = 0; y < height; y++) { + image.getRGB(0, y, width, 1, row, 0, width); + for (int x = 0; x < width; x++) { + int argb = row[x]; + + if (ignoreAlpha) { + argb &= 0xffffff; + } + argb &= mask; + + ColorCount color = colorMap.get(argb); + if (color == null) { + color = new ColorCount(argb); + colorMap.put(argb, color); + if (colorMap.keySet().size() > max) { + return null; + } + } + color.count++; + } + } + + return colorMap; + } + + public Map groupColors(final BufferedImage image, final int maxColors) { + final int max = Integer.MAX_VALUE; + + for (int i = 0; i < 8; i++) { + int mask = 0xff & (0xff << i); + mask = mask | (mask << 8) | (mask << 16) | (mask << 24); + + Debug.debug("mask(" + i + "): " + mask + " (" + Integer.toHexString(mask) + ")"); + + final Map result = groupColors1(image, max, mask); + if (result != null) { + return result; + } + } + throw new Error(""); + } + + public Palette process(final BufferedImage image, final int maxColors, + final MedianCutImplementation medianCutImplementation, final boolean verbose) + throws ImageWriteException { + final Map colorMap = groupColors(image, maxColors); + + final int discreteColors = colorMap.keySet().size(); + if (discreteColors <= maxColors) { + if (verbose) { + Debug.debug("lossless palette: " + discreteColors); + } + + final int[] palette = new int[discreteColors]; + final List colorCounts = new ArrayList( + colorMap.values()); + + for (int i = 0; i < colorCounts.size(); i++) { + final ColorCount colorCount = colorCounts.get(i); + palette[i] = colorCount.argb; + if (ignoreAlpha) { + palette[i] |= 0xff000000; + } + } + + return new SimplePalette(palette); + } + + if (verbose) { + Debug.debug("discrete colors: " + discreteColors); + } + + final List colorGroups = new ArrayList(); + final ColorGroup root = new ColorGroup(new ArrayList(colorMap.values()), ignoreAlpha); + colorGroups.add(root); + + while (colorGroups.size() < maxColors) { + if (!medianCutImplementation.performNextMedianCut(colorGroups, ignoreAlpha)) { + break; + } + } + + final int paletteSize = colorGroups.size(); + if (verbose) { + Debug.debug("palette size: " + paletteSize); + } + + final int[] palette = new int[paletteSize]; + + for (int i = 0; i < colorGroups.size(); i++) { + final ColorGroup colorGroup = colorGroups.get(i); + + palette[i] = colorGroup.getMedianValue(); + + colorGroup.paletteIndex = i; + + if (colorGroup.colorCounts.size() < 1) { + throw new ImageWriteException("empty color_group: " + + colorGroup); + } + } + + if (paletteSize > discreteColors) { + throw new ImageWriteException("palette_size > discrete_colors"); + } + + return new MedianCutPalette(root, palette); + } +} diff --git a/src/main/java/org/apache/sanselan/palette/Palette.java b/src/main/java/org/apache/commons/imaging/palette/Palette.java similarity index 53% rename from src/main/java/org/apache/sanselan/palette/Palette.java rename to src/main/java/org/apache/commons/imaging/palette/Palette.java index 28a0254..8f8b9f5 100644 --- a/src/main/java/org/apache/sanselan/palette/Palette.java +++ b/src/main/java/org/apache/commons/imaging/palette/Palette.java @@ -1,34 +1,53 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -import org.apache.sanselan.ImageWriteException; - -public abstract class Palette -{ - - public abstract int getPaletteIndex(int rgb) throws ImageWriteException; - - public abstract int getEntry(int index); - - public abstract int length(); - - public void dump() - { - } - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.util.Debug; + +/** + * Color palette. + */ +public abstract class Palette { + + /** + * Looks up the palette index for a given color. + * @param rgb the color to look up + * @return the palette index + * @throws ImageWriteException + */ + public abstract int getPaletteIndex(int rgb) throws ImageWriteException; + + /** + * Looks up the color for a given palette index. + * @param index the palette index to look up + * @return the color in ARGB format + */ + public abstract int getEntry(int index); + + /** + * The number of entries in the palette. + * @return the number of palette entries + */ + public abstract int length(); + + public void dump() { + for (int i = 0; i < length(); i++) { + Debug.debug("\t" + "palette[" + i + "]: " + getEntry(i) + " (0x" + Integer.toHexString(getEntry(i)) + ")"); + } + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java b/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java new file mode 100644 index 0000000..9d726dd --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java @@ -0,0 +1,531 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import com.google.code.appengine.awt.color.ColorSpace; +import com.google.code.appengine.awt.image.BufferedImage; +import com.google.code.appengine.awt.image.ColorModel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.imaging.ImageWriteException; + +/** + * Factory for creating palettes. + */ +public class PaletteFactory { + private static final boolean DEBUG = false; + public static final int COMPONENTS = 3; // in bits + + /** + * Builds an exact complete opaque palette containing all the colors in {@code src}, + * using an algorithm that is faster than {@linkplain #makeExactRgbPaletteSimple} for large images + * but uses 2 mebibytes of working memory. Treats all the colors as opaque. + * @param src the image whose palette to build + * @return the palette + */ + public Palette makeExactRgbPaletteFancy(final BufferedImage src) { + // map what rgb values have been used + + final byte[] rgbmap = new byte[256 * 256 * 32]; + + final int width = src.getWidth(); + final int height = src.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int rggbb = 0x1fffff & argb; + final int highred = 0x7 & (argb >> 21); + final int mask = 1 << highred; + rgbmap[rggbb] |= mask; + } + } + + int count = 0; + for (final byte element : rgbmap) { + final int eight = 0xff & element; + count += Integer.bitCount(eight); + } + + if (DEBUG) { + System.out.println("Used colors: " + count); + } + + final int[] colormap = new int[count]; + int mapsize = 0; + for (int i = 0; i < rgbmap.length; i++) { + final int eight = 0xff & rgbmap[i]; + int mask = 0x80; + for (int j = 0; j < 8; j++) { + final int bit = eight & mask; + mask >>>= 1; + + if (bit > 0) { + final int rgb = i | ((7 - j) << 21); + + colormap[mapsize++] = rgb; + } + } + } + + Arrays.sort(colormap); + return new SimplePalette(colormap); + } + + private int pixelToQuantizationTableIndex(int argb, final int precision) { + int result = 0; + final int precisionMask = (1 << precision) - 1; + + for (int i = 0; i < COMPONENTS; i++) { + int sample = argb & 0xff; + argb >>= 8; + + sample >>= (8 - precision); + result = (result << precision) | (sample & precisionMask); + } + + return result; + } + + private int getFrequencyTotal(final int[] table, final int[] mins, final int[] maxs, + final int precision) { + int sum = 0; + + for (int blue = mins[2]; blue <= maxs[2]; blue++) { + final int b = (blue << (2 * precision)); + for (int green = mins[1]; green <= maxs[1]; green++) { + final int g = (green << (1 * precision)); + for (int red = mins[0]; red <= maxs[0]; red++) { + final int index = b | g | red; + + sum += table[index]; + } + } + } + + return sum; + } + + private DivisionCandidate finishDivision(final ColorSpaceSubset subset, + final int component, final int precision, final int sum, final int slice) { + if (DEBUG) { + subset.dump("trying (" + component + "): "); + } + + final int total = subset.total; + + if ((slice < subset.mins[component]) + || (slice >= subset.maxs[component])) { + return null; + } + + if ((sum < 1) || (sum >= total)) { + return null; + } + + final int remainder = total - sum; + if ((remainder < 1) || (remainder >= total)) { + return null; + } + + final int[] sliceMins = new int[subset.mins.length]; + System.arraycopy(subset.mins, 0, sliceMins, 0, subset.mins.length); + final int[] sliceMaxs = new int[subset.maxs.length]; + System.arraycopy(subset.maxs, 0, sliceMaxs, 0, subset.maxs.length); + + sliceMaxs[component] = slice; + sliceMins[component] = slice + 1; + + if (DEBUG) { + System.out.println("total: " + total); + System.out.println("first total: " + sum); + System.out.println("second total: " + (total - sum)); + // System.out.println("start: " + start); + // System.out.println("end: " + end); + System.out.println("slice: " + slice); + + } + + final ColorSpaceSubset first = new ColorSpaceSubset(sum, precision, subset.mins, sliceMaxs); + final ColorSpaceSubset second = new ColorSpaceSubset(total - sum, precision, sliceMins, subset.maxs); + + return new DivisionCandidate(first, second); + + } + + private List divideSubset2(final int[] table, + final ColorSpaceSubset subset, final int component, final int precision) { + if (DEBUG) { + subset.dump("trying (" + component + "): "); + } + + final int total = subset.total; + + final int[] sliceMins = new int[subset.mins.length]; + System.arraycopy(subset.mins, 0, sliceMins, 0, subset.mins.length); + final int[] sliceMaxs = new int[subset.maxs.length]; + System.arraycopy(subset.maxs, 0, sliceMaxs, 0, subset.maxs.length); + + int sum1 = 0; + int slice1; + int last = 0; + + for (slice1 = subset.mins[component]; slice1 != subset.maxs[component] + 1; slice1++) { + sliceMins[component] = slice1; + sliceMaxs[component] = slice1; + + last = getFrequencyTotal(table, sliceMins, sliceMaxs, precision); + + sum1 += last; + + if (sum1 >= (total / 2)) { + break; + } + } + + int sum2 = sum1 - last; + int slice2 = slice1 - 1; + + final DivisionCandidate dc1 = finishDivision(subset, component, precision, sum1, slice1); + final DivisionCandidate dc2 = finishDivision(subset, component, precision, sum2, slice2); + + final List result = new ArrayList(); + + if (dc1 != null) { + result.add(dc1); + } + if (dc2 != null) { + result.add(dc2); + } + + return result; + } + + private DivisionCandidate divideSubset2(final int[] table, + final ColorSpaceSubset subset, final int precision) { + final List dcs = new ArrayList(); + + dcs.addAll(divideSubset2(table, subset, 0, precision)); + dcs.addAll(divideSubset2(table, subset, 1, precision)); + dcs.addAll(divideSubset2(table, subset, 2, precision)); + + DivisionCandidate bestV = null; + double bestScore = Double.MAX_VALUE; + + for (DivisionCandidate dc : dcs) { + final ColorSpaceSubset first = dc.dst_a; + final ColorSpaceSubset second = dc.dst_b; + final int area1 = first.total; + final int area2 = second.total; + + final int diff = Math.abs(area1 - area2); + final double score = ((double) diff) / ((double) Math.max(area1, area2)); + + if (bestV == null) { + bestV = dc; + bestScore = score; + } else if (score < bestScore) { + bestV = dc; + bestScore = score; + } + + } + + return bestV; + } + + private static class DivisionCandidate { + // private final ColorSpaceSubset src; + private final ColorSpaceSubset dst_a; + private final ColorSpaceSubset dst_b; + + public DivisionCandidate(final ColorSpaceSubset dst_a, final ColorSpaceSubset dst_b) { + // this.src = src; + this.dst_a = dst_a; + this.dst_b = dst_b; + } + } + + private List divide(final List v, + final int desiredCount, final int[] table, final int precision) { + final List ignore = new ArrayList(); + + while (true) { + int maxArea = -1; + ColorSpaceSubset maxSubset = null; + + for (ColorSpaceSubset subset : v) { + if (ignore.contains(subset)) { + continue; + } + final int area = subset.total; + + if (maxSubset == null) { + maxSubset = subset; + maxArea = area; + } else if (area > maxArea) { + maxSubset = subset; + maxArea = area; + } + } + + if (maxSubset == null) { + return v; + } + if (DEBUG) { + System.out.println("\t" + "area: " + maxArea); + } + + final DivisionCandidate dc = divideSubset2(table, maxSubset, + precision); + if (dc != null) { + v.remove(maxSubset); + v.add(dc.dst_a); + v.add(dc.dst_b); + } else { + ignore.add(maxSubset); + } + + if (v.size() == desiredCount) { + return v; + } + } + } + + /** + * Builds an inexact opaque palette of at most {@code max} colors in {@code src} + * using a variation of the Median Cut algorithm. Accurate to 6 bits per component, + * and works by splitting the color bounding box most heavily populated by colors + * along the component which splits the colors in that box most evenly. + * @param src the image whose palette to build + * @param max the maximum number of colors the palette can contain + * @return the palette of at most {@code max} colors + */ + public Palette makeQuantizedRgbPalette(final BufferedImage src, final int max) { + final int precision = 6; // in bits + + final int tableScale = precision * COMPONENTS; + final int tableSize = 1 << tableScale; + final int[] table = new int[tableSize]; + + final int width = src.getWidth(); + final int height = src.getHeight(); + + List subsets = new ArrayList(); + final ColorSpaceSubset all = new ColorSpaceSubset(width * height, precision); + subsets.add(all); + + if (DEBUG) { + final int preTotal = getFrequencyTotal(table, all.mins, all.maxs, precision); + System.out.println("pre total: " + preTotal); + } + + // step 1: count frequency of colors + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + + final int index = pixelToQuantizationTableIndex(argb, precision); + + table[index]++; + } + } + + if (DEBUG) { + final int allTotal = getFrequencyTotal(table, all.mins, all.maxs, precision); + System.out.println("all total: " + allTotal); + System.out.println("width * height: " + (width * height)); + } + + subsets = divide(subsets, max, table, precision); + + if (DEBUG) { + System.out.println("subsets: " + subsets.size()); + System.out.println("width*height: " + width * height); + } + + for (int i = 0; i < subsets.size(); i++) { + final ColorSpaceSubset subset = subsets.get(i); + + subset.setAverageRGB(table); + + if (DEBUG) { + subset.dump(i + ": "); + } + } + + Collections.sort(subsets, ColorSpaceSubset.RGB_COMPARATOR); + + return new QuantizedPalette(subsets, precision); + } + + /** + * Builds an inexact possibly translucent palette of at most {@code max} colors in {@code src} + * using the traditional Median Cut algorithm. Color bounding boxes are split along the + * longest axis, with each step splitting the box. All bits in each component are used. + * The Algorithm is slower and seems exact than {@linkplain #makeQuantizedRgbPalette(BufferedImage, int)}. + * @param src the image whose palette to build + * @param transparent whether to consider the alpha values + * @param max the maximum number of colors the palette can contain + * @return the palette of at most {@code max} colors + */ + public Palette makeQuantizedRgbaPalette(final BufferedImage src, final boolean transparent, final int max) throws ImageWriteException { + return new MedianCutQuantizer(!transparent).process(src, max, + new MedianCutLongestAxisImplementation(), false); + } + + /** + * Builds an exact complete opaque palette containing all the colors in {@code src}, + * and fails by returning {@code null} if there are more than {@code max} colors necessary to do this. + * @param src the image whose palette to build + * @param max the maximum number of colors the palette can contain + * @return the complete palette of {@code max} or less colors, or {@code null} if more than {@code max} colors are necessary + */ + public SimplePalette makeExactRgbPaletteSimple(final BufferedImage src, final int max) { + // This is not efficient for large values of max, say, max > 256; + final Set rgbs = new HashSet(); + + final int width = src.getWidth(); + final int height = src.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int rgb = 0xffffff & argb; + + if (rgbs.add(rgb) && rgbs.size() > max) { + return null; + } + } + } + + final int[] result = new int[rgbs.size()]; + int next = 0; + for (final int rgb : rgbs) { + result[next++] = rgb; + } + Arrays.sort(result); + + return new SimplePalette(result); + } + + public boolean isGrayscale(final BufferedImage src) { + final int width = src.getWidth(); + final int height = src.getHeight(); + + if (ColorSpace.TYPE_GRAY == src.getColorModel().getColorSpace() + .getType()) { + return true; + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + + final int red = 0xff & (argb >> 16); + final int green = 0xff & (argb >> 8); + final int blue = 0xff & (argb >> 0); + + if (red != green || red != blue) { + return false; + } + } + } + return true; + } + + public boolean hasTransparency(final BufferedImage src) { + return hasTransparency(src, 255); + } + + public boolean hasTransparency(final BufferedImage src, final int threshold) { + final int width = src.getWidth(); + final int height = src.getHeight(); + + if (!src.getColorModel().hasAlpha()) { + return false; + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int argb = src.getRGB(x, y); + final int alpha = 0xff & (argb >> 24); + if (alpha < threshold) { + return true; + } + } + } + return false; + } + + public int countTrasparentColors(final int[] rgbs) { + int first = -1; + + for (final int rgb : rgbs) { + final int alpha = 0xff & (rgb >> 24); + if (alpha < 0xff) { + if (first < 0) { + first = rgb; + } else if (rgb != first) { + return 2; // more than one transparent color; + } + } + } + + if (first < 0) { + return 0; + } + return 1; + } + + public int countTransparentColors(final BufferedImage src) { + final ColorModel cm = src.getColorModel(); + if (!cm.hasAlpha()) { + return 0; + } + + final int width = src.getWidth(); + final int height = src.getHeight(); + + int first = -1; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int rgb = src.getRGB(x, y); + final int alpha = 0xff & (rgb >> 24); + if (alpha < 0xff) { + if (first < 0) { + first = rgb; + } else if (rgb != first) { + return 2; // more than one transparent color; + } + } + } + } + + if (first < 0) { + return 0; + } + return 1; + } + +} diff --git a/src/main/java/org/apache/commons/imaging/palette/QuantizedPalette.java b/src/main/java/org/apache/commons/imaging/palette/QuantizedPalette.java new file mode 100644 index 0000000..1a6b982 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/QuantizedPalette.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +import java.util.List; +import org.apache.commons.imaging.ImageWriteException; + +public class QuantizedPalette extends Palette { + private final int precision; + private final List subsets; + private final ColorSpaceSubset[] straight; + + public QuantizedPalette(final List subsets, final int precision) { + this.subsets = subsets; + this.precision = precision; + + straight = new ColorSpaceSubset[1 << (precision * 3)]; + + for (int i = 0; i < subsets.size(); i++) { + final ColorSpaceSubset subset = subsets.get(i); + subset.setIndex(i); + + for (int u = subset.mins[0]; u <= subset.maxs[0]; u++) { + for (int j = subset.mins[1]; j <= subset.maxs[1]; j++) { + for (int k = subset.mins[2]; k <= subset.maxs[2]; k++) { + final int index = (u << (precision * 2)) + | (j << (precision * 1)) + | (k << (precision * 0)); + straight[index] = subset; + } + } + } + } + } + + @Override + public int getPaletteIndex(final int rgb) throws ImageWriteException { + final int precisionMask = (1 << precision) - 1; + + final int index = ((rgb >> (24 - 3 * precision)) & (precisionMask << (precision << 1))) + | ((rgb >> (16 - 2 * precision)) & (precisionMask << precision)) + | ((rgb >> (8 - precision)) & (precisionMask)); + + return straight[index].getIndex(); + } + + @Override + public int getEntry(final int index) { + final ColorSpaceSubset subset = subsets.get(index); + return subset.rgb; + } + + @Override + public int length() { + return subsets.size(); + + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/SimplePalette.java b/src/main/java/org/apache/commons/imaging/palette/SimplePalette.java new file mode 100644 index 0000000..3539d8f --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/SimplePalette.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.palette; + +public class SimplePalette extends Palette { + private final int[] palette; + + public SimplePalette(final int[] palette) { + this.palette = palette; + } + + @Override + public int getPaletteIndex(final int rgb) { + return getPaletteIndex(palette, rgb); + } + + @Override + public int getEntry(final int index) { + return palette[index]; + } + + private int getPaletteIndex(final int[] palette, final int argb) { + for (int i = 0; i < palette.length; i++) { + if (palette[i] == argb) { + return i; + } + } + + return -1; + } + + @Override + public int length() { + return palette.length; + } +} diff --git a/src/main/java/org/apache/commons/imaging/palette/package-info.java b/src/main/java/org/apache/commons/imaging/palette/package-info.java new file mode 100644 index 0000000..f0299a3 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/palette/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Color quantization and palette manipulation tools. + */ +package org.apache.commons.imaging.palette; diff --git a/src/main/java/org/apache/commons/imaging/util/Debug.java b/src/main/java/org/apache/commons/imaging/util/Debug.java new file mode 100644 index 0000000..8d2c8d8 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/util/Debug.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.util; + +import com.google.code.appengine.awt.color.ICC_Profile; +import java.io.File; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public final class Debug { + + private static final boolean DEBUG = false; + // public static String newline = System.getProperty("line.separator"); + private static final String NEWLINE = "\r\n"; + private static long counter; + + public static void debug(final String message) { + if (DEBUG) { + System.out.println(message); + } + } + + public static void debug() { + if (DEBUG) { + System.out.print(NEWLINE); + } + } + + private static String getDebug(final String message, final int[] v) { + final StringBuilder result = new StringBuilder(); + + if (v == null) { + result.append(message + " (" + null + ")" + NEWLINE); + } else { + result.append(message + " (" + v.length + ")" + NEWLINE); + for (final int element : v) { + result.append("\t" + element + NEWLINE); + } + result.append(NEWLINE); + } + return result.toString(); + } + + private static String getDebug(final String message, final byte[] v) { + final int max = 250; + return getDebug(message, v, max); + } + + private static String getDebug(final String message, final byte[] v, final int max) { + + final StringBuilder result = new StringBuilder(); + + if (v == null) { + result.append(message + " (" + null + ")" + NEWLINE); + } else { + result.append(message + " (" + v.length + ")" + NEWLINE); + for (int i = 0; i < max && i < v.length; i++) { + final int b = 0xff & v[i]; + + char c; + if (b == 0 || b == 10 || b == 11 || b == 13) { + c = ' '; + } else { + c = (char) b; + } + + result.append("\t" + i + ": " + b + " (" + c + ", 0x" + + Integer.toHexString(b) + ")" + NEWLINE); + } + if (v.length > max) { + result.append("\t..." + NEWLINE); + } + + result.append(NEWLINE); + } + return result.toString(); + } + + private static String getDebug(final String message, final char[] v) { + final StringBuilder result = new StringBuilder(); + + if (v == null) { + result.append(message + " (" + null + ")" + NEWLINE); + } else { + result.append(message + " (" + v.length + ")" + NEWLINE); + for (final char element : v) { + result.append("\t" + element + " (" + (0xff & element) + ")" + NEWLINE); + } + result.append(NEWLINE); + } + return result.toString(); + } + + private static void debug(final String message, final Map map) { + debug(getDebug(message, map)); + } + + private static String getDebug(final String message, final Map map) { + final StringBuilder result = new StringBuilder(); + + if (map == null) { + return message + " map: " + null; + } + + final List keys = new ArrayList(map.keySet()); + result.append(message + " map: " + keys.size() + NEWLINE); + for (int i = 0; i < keys.size(); i++) { + final Object key = keys.get(i); + final Object value = map.get(key); + result.append("\t" + i + ": '" + key + "' -> '" + value + "'" + NEWLINE); + } + + result.append(NEWLINE); + + return result.toString(); + } + + private static String byteQuadToString(final int bytequad) { + final byte b1 = (byte) ((bytequad >> 24) & 0xff); + final byte b2 = (byte) ((bytequad >> 16) & 0xff); + final byte b3 = (byte) ((bytequad >> 8) & 0xff); + final byte b4 = (byte) ((bytequad >> 0) & 0xff); + + final char c1 = (char) b1; + final char c2 = (char) b2; + final char c3 = (char) b3; + final char c4 = (char) b4; + // return new String(new char[] { c1, c2, c3, c4 }); + final StringBuilder buffer = new StringBuilder(31); + buffer.append(new String(new char[]{c1, c2, c3, c4})); + buffer.append(" bytequad: "); + buffer.append(bytequad); + buffer.append(" b1: "); + buffer.append(b1); + buffer.append(" b2: "); + buffer.append(b2); + buffer.append(" b3: "); + buffer.append(b3); + buffer.append(" b4: "); + buffer.append(b4); + + return buffer.toString(); + } + + public static void debug(final String message, final Object value) { + if (value == null) { + debug(message, "null"); + } else if (value instanceof char[]) { + debug(message, (char[]) value); + } else if (value instanceof byte[]) { + debug(message, (byte[]) value); + } else if (value instanceof int[]) { + debug(message, (int[]) value); + } else if (value instanceof String) { + debug(message, (String) value); + } else if (value instanceof List) { + debug(message, (List) value); + } else if (value instanceof Map) { + debug(message, (Map) value); + } else if (value instanceof ICC_Profile) { + debug(message, (ICC_Profile) value); + } else if (value instanceof File) { + debug(message, (File) value); + } else if (value instanceof Date) { + debug(message, (Date) value); + } else if (value instanceof Calendar) { + debug(message, (Calendar) value); + } else { + debug(message, value.toString()); + } + } + + private static void debug(final String message, final byte[] v) { + debug(getDebug(message, v)); + } + + private static void debug(final String message, final char[] v) { + debug(getDebug(message, v)); + } + + private static void debug(final String message, final Calendar value) { + final DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH); + debug(message, (value == null) ? "null" : df.format(value.getTime())); + } + + private static void debug(final String message, final Date value) { + final DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH); + debug(message, (value == null) ? "null" : df.format(value)); + } + + private static void debug(final String message, final File file) { + debug(message + ": " + ((file == null) ? "null" : file.getPath())); + } + + private static void debug(final String message, final ICC_Profile value) { + debug("ICC_Profile " + message + ": " + ((value == null) ? "null" : value.toString())); + if (value != null) { + debug("\t getProfileClass: " + byteQuadToString(value.getProfileClass())); + debug("\t getPCSType: " + byteQuadToString(value.getPCSType())); + debug("\t getColorSpaceType() : " + byteQuadToString(value.getColorSpaceType())); + } + } + + private static void debug(final String message, final int[] v) { + debug(getDebug(message, v)); + } + + private static void debug(final String message, final List v) { + final String suffix = " [" + counter++ + "]"; + + debug(message + " (" + v.size() + ")" + suffix); + for (Object aV : v) { + debug("\t" + aV.toString() + suffix); + } + debug(); + } + + private static void debug(final String message, final String value) { + debug(message + " " + value); + } + + public static void debug(final Throwable e) { + debug(getDebug(e)); + } + + public static void debug(final Throwable e, final int value) { + debug(getDebug(e, value)); + } + + private static String getDebug(final Throwable e) { + return getDebug(e, -1); + } + + private static String getDebug(final Throwable e, final int max) { + final StringBuilder result = new StringBuilder(35); + + final SimpleDateFormat timestamp = new SimpleDateFormat( + "yyyy-MM-dd kk:mm:ss:SSS", Locale.ENGLISH); + final String datetime = timestamp.format(new Date()).toLowerCase(); + + result.append(NEWLINE); + result.append("Throwable: " + + ((e == null) ? "" : ("(" + e.getClass().getName() + ")")) + + ":" + datetime + NEWLINE); + result.append("Throwable: " + ((e == null) ? "null" : e.getLocalizedMessage()) + NEWLINE); + result.append(NEWLINE); + + result.append(getStackTrace(e, max)); + + result.append("Caught here:" + NEWLINE); + result.append(getStackTrace(new Exception(), max, 1)); + // Debug.dumpStack(); + result.append(NEWLINE); + return result.toString(); + } + + private static String getStackTrace(final Throwable e, final int limit) { + return getStackTrace(e, limit, 0); + } + + private static String getStackTrace(final Throwable e, final int limit, final int skip) { + final StringBuilder result = new StringBuilder(); + + if (e != null) { + final StackTraceElement[] stes = e.getStackTrace(); + if (stes != null) { + for (int i = skip; i < stes.length && (limit < 0 || i < limit); i++) { + final StackTraceElement ste = stes[i]; + + result.append("\tat " + ste.getClassName() + "." + + ste.getMethodName() + "(" + ste.getFileName() + + ":" + ste.getLineNumber() + ")" + NEWLINE); + } + if (limit >= 0 && stes.length > limit) { + result.append("\t..." + NEWLINE); + } + } + + // e.printStackTrace(System.out); + result.append(NEWLINE); + } + + return result.toString(); + } + + private Debug() { + } +} diff --git a/src/main/java/org/apache/commons/imaging/util/IoUtils.java b/src/main/java/org/apache/commons/imaging/util/IoUtils.java new file mode 100644 index 0000000..ce7d2dc --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/util/IoUtils.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.util; + +import java.io.Closeable; +import java.io.IOException; + +public final class IoUtils { + public static void closeQuietly(final boolean mayThrow, final Closeable... closeables) + throws IOException { + IOException firstException = null; + for (final Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (final IOException ioException) { + if (mayThrow && firstException == null) { + firstException = ioException; + } + } + } + } + if (firstException != null) { + throw firstException; + } + } + + /** + * This class should never be instantiated. + */ + private IoUtils() { + } + +} diff --git a/src/main/java/org/apache/commons/imaging/util/package-info.java b/src/main/java/org/apache/commons/imaging/util/package-info.java new file mode 100644 index 0000000..ea3fff6 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/util/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utility classes. + */ +package org.apache.commons.imaging.util; diff --git a/src/main/java/org/apache/harmony/x/imageio/plugins/jpeg/JPEGImageWriter.java b/src/main/java/org/apache/harmony/x/imageio/plugins/jpeg/JPEGImageWriter.java index 6f9a1f3..e5f747d 100644 --- a/src/main/java/org/apache/harmony/x/imageio/plugins/jpeg/JPEGImageWriter.java +++ b/src/main/java/org/apache/harmony/x/imageio/plugins/jpeg/JPEGImageWriter.java @@ -25,13 +25,12 @@ import java.util.Map; - +import org.apache.commons.imaging.ImageFormats; import org.apache.harmony.luni.util.NotImplementedException; import org.apache.harmony.x.imageio.internal.OutputStreamWrapper; import org.apache.harmony.x.imageio.internal.nls.Messages; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.Sanselan; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.Imaging; import com.google.code.appengine.awt.color.ColorSpace; import com.google.code.appengine.awt.image.BufferedImage; @@ -96,9 +95,9 @@ public void write(IIOMetadata iioMetadata, IIOImage iioImage, ImageWriteParam pa Map params = new HashMap(); try { - Sanselan.writeImage((BufferedImage)img, + Imaging.writeImage((BufferedImage)img, wrapOutput(ios),//(OutputStream)ios, - ImageFormat.IMAGE_FORMAT_JPEG, + ImageFormats.JPEG, params); } catch (ImageWriteException e) { // TODO Auto-generated catch block diff --git a/src/main/java/org/apache/harmony/x/imageio/plugins/png/PNGImageWriter.java b/src/main/java/org/apache/harmony/x/imageio/plugins/png/PNGImageWriter.java index 316d2d9..b0227a5 100644 --- a/src/main/java/org/apache/harmony/x/imageio/plugins/png/PNGImageWriter.java +++ b/src/main/java/org/apache/harmony/x/imageio/plugins/png/PNGImageWriter.java @@ -26,13 +26,12 @@ import java.util.Map; - +import org.apache.commons.imaging.ImageFormats; import org.apache.harmony.luni.util.NotImplementedException; import org.apache.harmony.x.imageio.internal.OutputStreamWrapper; import org.apache.harmony.x.imageio.internal.nls.Messages; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.Sanselan; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.Imaging; import com.google.code.appengine.awt.image.BufferedImage; import com.google.code.appengine.awt.image.RenderedImage; @@ -109,9 +108,9 @@ public void write(IIOMetadata streamMetadata, IIOImage iioimage, ImageWriteParam try { Map params = new HashMap(); - Sanselan.writeImage((BufferedImage) image, + Imaging.writeImage((BufferedImage) image, wrapOutput(getOutput()), - ImageFormat.IMAGE_FORMAT_PNG, + ImageFormats.PNG, params); } catch (ImageWriteException e) { diff --git a/src/main/java/org/apache/sanselan/FormatCompliance.java b/src/main/java/org/apache/sanselan/FormatCompliance.java deleted file mode 100644 index e0cb606..0000000 --- a/src/main/java/org/apache/sanselan/FormatCompliance.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; - -public class FormatCompliance -{ - private final boolean failOnError; - private final String description; - private final ArrayList comments = new ArrayList(); - - public FormatCompliance(String description) - { - this.description = description; - failOnError = false; - } - - public FormatCompliance(String description, boolean fail_on_error) - { - this.description = description; - this.failOnError = fail_on_error; - } - - public static final FormatCompliance getDefault() - { - return new FormatCompliance("ignore", false); - } - - public void addComment(String s) throws ImageReadException - { - comments.add(s); - if (failOnError) - throw new ImageReadException(s); - } - - public void addComment(String s, int value) throws ImageReadException - { - addComment(s + ": " + getValueDescription(value)); - } - - public String toString() - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - - dump(pw); - - return sw.getBuffer().toString(); - } - - public void dump() - { - dump(new PrintWriter(new OutputStreamWriter(System.out))); - } - - public void dump(PrintWriter pw) - { - pw.println("Format Compliance: " + description); - - if (comments.size() == 0) - pw.println("\t" + "No comments."); - else - { - for (int i = 0; i < comments.size(); i++) - pw.println("\t" + (i + 1) + ": " + comments.get(i)); - } - pw.println(""); - pw.flush(); - } - - private String getValueDescription(int value) - { - return value + " (" + Integer.toHexString(value) + ")"; - } - - public boolean compare_bytes(String name, byte expected[], byte actual[]) - throws ImageReadException - { - if (expected.length != actual.length) - { - addComment(name + ": " + "Unexpected length: (expected: " - + expected.length + ", actual: " + actual.length + ")"); - return false; - } - else - { - for (int i = 0; i < expected.length; i++) - { - // System.out.println("expected: " - // + getValueDescription(expected[i]) + ", actual: " - // + getValueDescription(actual[i]) + ")"); - if (expected[i] != actual[i]) - { - addComment(name + ": " + "Unexpected value: (expected: " - + getValueDescription(expected[i]) + ", actual: " - + getValueDescription(actual[i]) + ")"); - return false; - } - } - } - - return true; - } - - public boolean checkBounds(String name, int min, int max, int actual) - throws ImageReadException - { - if ((actual < min) || (actual > max)) - { - addComment(name + ": " + "bounds check: " + min + " <= " + actual - + " <= " + max + ": false"); - return false; - } - - return true; - } - - public boolean compare(String name, int valid, int actual) - throws ImageReadException - { - return compare(name, new int[]{ - valid, - }, actual); - } - - public boolean compare(String name, int valid[], int actual) - throws ImageReadException - { - for (int i = 0; i < valid.length; i++) - if (actual == valid[i]) - return true; - - StringBuffer result = new StringBuffer(); - result.append(name + ": " + "Unexpected value: (valid: "); - if (valid.length > 1) - result.append("{"); - for (int i = 0; i < valid.length; i++) - { - if (i > 0) - result.append(", "); - result.append(getValueDescription(valid[i])); - } - if (valid.length > 1) - result.append("}"); - result.append(", actual: " + getValueDescription(actual) + ")"); - addComment(result.toString()); - return false; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/ImageDump.java b/src/main/java/org/apache/sanselan/ImageDump.java deleted file mode 100644 index 001ab9b..0000000 --- a/src/main/java/org/apache/sanselan/ImageDump.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.IOException; - -import org.apache.sanselan.icc.IccProfileInfo; -import org.apache.sanselan.icc.IccProfileParser; - -import com.google.code.appengine.awt.color.ColorSpace; -import com.google.code.appengine.awt.color.ICC_ColorSpace; -import com.google.code.appengine.awt.color.ICC_Profile; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class ImageDump -{ - private String colorSpaceTypeToName(ColorSpace cs) - { - // System.out.println(prefix + ": " + "type: " - // + cs.getType() ); - switch (cs.getType()) - { - case ColorSpace.TYPE_CMYK : - return "TYPE_CMYK"; - case ColorSpace.TYPE_RGB : - return "TYPE_RGB"; - - case ColorSpace.CS_sRGB : - return "CS_sRGB"; - case ColorSpace.CS_GRAY : - return "CS_GRAY"; - case ColorSpace.CS_CIEXYZ : - return "CS_CIEXYZ"; - case ColorSpace.CS_LINEAR_RGB : - return "CS_LINEAR_RGB"; - case ColorSpace.CS_PYCC : - return "CS_PYCC"; - } - - return "unknown"; - } - - public void dumpColorSpace(String prefix, ColorSpace cs) - throws ImageReadException, IOException - { - System.out.println(prefix + ": " + "type: " + cs.getType() + " (" - + colorSpaceTypeToName(cs) + ")"); - - if (!(cs instanceof ICC_ColorSpace)) - { - System.out.println(prefix + ": " + "Unknown ColorSpace: " - + cs.getClass().getName()); - return; - } - - ICC_ColorSpace fICC_ColorSpace = (ICC_ColorSpace) cs; - ICC_Profile fICC_Profile = fICC_ColorSpace.getProfile(); - - byte bytes[] = fICC_Profile.getData(); - - IccProfileParser parser = new IccProfileParser(); - - IccProfileInfo info = parser.getICCProfileInfo(bytes); - info.dump(prefix); - } - - public void dump(BufferedImage src) throws ImageReadException, IOException - { - dump("", src); - } - - public void dump(String prefix, BufferedImage src) - throws ImageReadException, IOException - { - System.out.println(prefix + ": " + "dump"); - dumpColorSpace(prefix, src.getColorModel().getColorSpace()); - dumpBIProps(prefix, src); - } - - public void dumpBIProps(String prefix, BufferedImage src) - { - String keys[] = src.getPropertyNames(); - if (keys == null) - { - System.out.println(prefix + ": no props"); - return; - } - for (int i = 0; i < keys.length; i++) - { - String key = keys[i]; - System.out.println(prefix + ": " + key + ": " - + src.getProperty(key)); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/ImageFormat.java b/src/main/java/org/apache/sanselan/ImageFormat.java deleted file mode 100644 index fc45ffe..0000000 --- a/src/main/java/org/apache/sanselan/ImageFormat.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -public class ImageFormat -{ - public final String name; - public final String extension; - public final boolean actual; - - private ImageFormat(String name, boolean actual) - { - this.name = name; - this.extension = name; - this.actual = actual; - } - - private ImageFormat(String name) - { - this.name = name; - this.extension = name; - actual = true; - } - - public boolean equals(Object o) - { - if (!(o instanceof ImageFormat)) - return false; - - ImageFormat other = (ImageFormat) o; - - return other.name.equals(name); - - } - - public String toString() - { - return "{" + name + ": " + extension + "}"; - } - - public int hashCode() - { - return name.hashCode(); - } - - public static final ImageFormat IMAGE_FORMAT_UNKNOWN = new ImageFormat( - "UNKNOWN", false); - public static final ImageFormat IMAGE_FORMAT_PNG = new ImageFormat("PNG"); - public static final ImageFormat IMAGE_FORMAT_GIF = new ImageFormat("GIF"); - public static final ImageFormat IMAGE_FORMAT_ICO = new ImageFormat("ICO"); - public static final ImageFormat IMAGE_FORMAT_TIFF = new ImageFormat("TIFF"); - public static final ImageFormat IMAGE_FORMAT_JPEG = new ImageFormat("JPEG"); - public static final ImageFormat IMAGE_FORMAT_BMP = new ImageFormat("BMP"); - public static final ImageFormat IMAGE_FORMAT_PSD = new ImageFormat("PSD"); - public static final ImageFormat IMAGE_FORMAT_PBM = new ImageFormat("PBM"); - public static final ImageFormat IMAGE_FORMAT_PGM = new ImageFormat("PGM"); - public static final ImageFormat IMAGE_FORMAT_PPM = new ImageFormat("PPM"); - public static final ImageFormat IMAGE_FORMAT_PNM = new ImageFormat("PNM"); - public static final ImageFormat IMAGE_FORMAT_TGA = new ImageFormat("TGA"); - public static final ImageFormat IMAGE_FORMAT_JBIG2 = new ImageFormat("JBig2"); - public static final ImageFormat IMAGE_FORMAT_ICNS = new ImageFormat("ICNS"); - - public static final ImageFormat[] getAllFormats() - { - ImageFormat result[] = { - IMAGE_FORMAT_UNKNOWN, IMAGE_FORMAT_PNG, IMAGE_FORMAT_GIF, - IMAGE_FORMAT_TIFF, IMAGE_FORMAT_JPEG, IMAGE_FORMAT_BMP, - IMAGE_FORMAT_PSD, IMAGE_FORMAT_PBM, IMAGE_FORMAT_PGM, - IMAGE_FORMAT_PPM, IMAGE_FORMAT_PNM, IMAGE_FORMAT_TGA, - IMAGE_FORMAT_JBIG2, IMAGE_FORMAT_ICNS, - }; - - return result; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/ImageParser.java b/src/main/java/org/apache/sanselan/ImageParser.java deleted file mode 100644 index 786edf7..0000000 --- a/src/main/java/org/apache/sanselan/ImageParser.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Map; - -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.IBufferedImageFactory; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.SimpleBufferedImageFactory; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.formats.bmp.BmpImageParser; -import org.apache.sanselan.formats.gif.GifImageParser; -import org.apache.sanselan.formats.icns.IcnsImageParser; -import org.apache.sanselan.formats.ico.IcoImageParser; -import org.apache.sanselan.formats.jpeg.JpegImageParser; -import org.apache.sanselan.formats.png.PngImageParser; -import org.apache.sanselan.formats.pnm.PNMImageParser; -import org.apache.sanselan.formats.psd.PsdImageParser; -import org.apache.sanselan.formats.tiff.TiffImageParser; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class ImageParser extends BinaryFileParser implements - SanselanConstants -{ - - public static final ImageParser[] getAllImageParsers() - { - ImageParser result[] = { new JpegImageParser(), new TiffImageParser(), - new PngImageParser(), new BmpImageParser(), - new GifImageParser(), new PsdImageParser(), - new PNMImageParser(), new IcoImageParser(), - new IcnsImageParser(), - // new JBig2ImageParser(), - // new TgaImageParser(), - }; - - return result; - } - - public final IImageMetadata getMetadata(ByteSource byteSource) - throws ImageReadException, IOException - { - return getMetadata(byteSource, null); - } - - public abstract IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException; - - public final IImageMetadata getMetadata(byte bytes[]) - throws ImageReadException, IOException - { - return getMetadata(bytes); - } - - public final IImageMetadata getMetadata(byte bytes[], Map params) - throws ImageReadException, IOException - { - return getMetadata(new ByteSourceArray(bytes), params); - } - - public final IImageMetadata getMetadata(File file) - throws ImageReadException, IOException - { - return getMetadata(file, null); - } - - public final IImageMetadata getMetadata(File file, Map params) - throws ImageReadException, IOException - { - if (debug) - System.out.println(getName() + ".getMetadata" + ": " - + file.getName()); - - if (!canAcceptExtension(file)) - return null; - - return getMetadata(new ByteSourceFile(file), params); - } - - public abstract ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException; - - public final ImageInfo getImageInfo(ByteSource byteSource) - throws ImageReadException, IOException - { - return getImageInfo(byteSource, null); - } - - public final ImageInfo getImageInfo(byte bytes[], Map params) - throws ImageReadException, IOException - { - return getImageInfo(new ByteSourceArray(bytes), params); - } - - public final ImageInfo getImageInfo(File file, Map params) - throws ImageReadException, IOException - { - if (!canAcceptExtension(file)) - return null; - - return getImageInfo(new ByteSourceFile(file), params); - } - - public FormatCompliance getFormatCompliance(ByteSource byteSource) - throws ImageReadException, IOException - { - return null; - } - - public final FormatCompliance getFormatCompliance(byte bytes[]) - throws ImageReadException, IOException - { - return getFormatCompliance(new ByteSourceArray(bytes)); - } - - public final FormatCompliance getFormatCompliance(File file) - throws ImageReadException, IOException - { - if (!canAcceptExtension(file)) - return null; - - return getFormatCompliance(new ByteSourceFile(file)); - } - - public ArrayList getAllBufferedImages(ByteSource byteSource) - throws ImageReadException, IOException - { - BufferedImage bi = getBufferedImage(byteSource, null); - - ArrayList result = new ArrayList(); - - result.add(bi); - - return result; - } - - public final ArrayList getAllBufferedImages(byte bytes[]) - throws ImageReadException, IOException - { - return getAllBufferedImages(new ByteSourceArray(bytes)); - } - - public final ArrayList getAllBufferedImages(File file) - throws ImageReadException, IOException - { - if (!canAcceptExtension(file)) - return null; - - return getAllBufferedImages(new ByteSourceFile(file)); - } - - // public boolean extractImages(ByteSource byteSource, File dstDir, - // String dstRoot, ImageParser encoder) throws ImageReadException, - // IOException, ImageWriteException - // { - // ArrayList v = getAllBufferedImages(byteSource); - // - // if (v == null) - // return false; - // - // for (int i = 0; i < v.size(); i++) - // { - // BufferedImage image = (BufferedImage) v.get(i); - // File file = new File(dstDir, dstRoot + "_" + i - // + encoder.getDefaultExtension()); - // encoder.writeImage(image, new FileOutputStream(file), null); - // } - // - // return false; - // } - // - // public final boolean extractImages(byte bytes[], File dstDir, - // String dstRoot, ImageParser encoder) - // - // throws ImageReadException, IOException, ImageWriteException - // { - // return extractImages(new ByteSourceArray(bytes), dstDir, dstRoot, - // encoder); - // } - // - // public final boolean extractImages(File file, File dstDir, - // String dstRoot, ImageParser encoder) - // - // throws ImageReadException, IOException, ImageWriteException - // { - // if (!canAcceptExtension(file)) - // return false; - // - // return extractImages(new ByteSourceFile(file), dstDir, dstRoot, - // encoder); - // } - - public abstract BufferedImage getBufferedImage(ByteSource byteSource, - Map params) throws ImageReadException, IOException; - - public final BufferedImage getBufferedImage(byte bytes[], Map params) - throws ImageReadException, IOException - { - return getBufferedImage(new ByteSourceArray(bytes), params); - } - - public final BufferedImage getBufferedImage(File file, Map params) - throws ImageReadException, IOException - { - if (!canAcceptExtension(file)) - return null; - - return getBufferedImage(new ByteSourceFile(file), params); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - try - { - os.close(); // we are obligated to close stream. - } catch (Exception e) - { -// Debug.debug(e); - throw new ImageWriteException("This image format (" + getName() - + ") cannot be written."); - } - - - } - - public final Dimension getImageSize(byte bytes[]) - throws ImageReadException, IOException - { - return getImageSize(bytes, null); - } - - public final Dimension getImageSize(byte bytes[], Map params) - throws ImageReadException, IOException - { - return getImageSize(new ByteSourceArray(bytes), params); - } - - public final Dimension getImageSize(File file) throws ImageReadException, - IOException - { - - return getImageSize(file, null); - } - - public final Dimension getImageSize(File file, Map params) - throws ImageReadException, IOException - { - - if (!canAcceptExtension(file)) - return null; - - return getImageSize(new ByteSourceFile(file), params); - } - - public abstract Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException; - - public abstract String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException; - - public final byte[] getICCProfileBytes(byte bytes[]) - throws ImageReadException, IOException - { - return getICCProfileBytes(bytes, null); - } - - public final byte[] getICCProfileBytes(byte bytes[], Map params) - throws ImageReadException, IOException - { - return getICCProfileBytes(new ByteSourceArray(bytes), params); - } - - public final byte[] getICCProfileBytes(File file) - throws ImageReadException, IOException - { - return getICCProfileBytes(file, null); - } - - public final byte[] getICCProfileBytes(File file, Map params) - throws ImageReadException, IOException - { - if (!canAcceptExtension(file)) - return null; - - if (debug) - System.out.println(getName() + ": " + file.getName()); - - return getICCProfileBytes(new ByteSourceFile(file), params); - } - - public abstract byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException; - - public final String dumpImageFile(byte bytes[]) throws ImageReadException, - IOException - { - return dumpImageFile(new ByteSourceArray(bytes)); - } - - public final String dumpImageFile(File file) throws ImageReadException, - IOException - { - if (!canAcceptExtension(file)) - return null; - - if (debug) - System.out.println(getName() + ": " + file.getName()); - - return dumpImageFile(new ByteSourceFile(file)); - } - - public final String dumpImageFile(ByteSource byteSource) - throws ImageReadException, IOException - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - - dumpImageFile(pw, byteSource); - - pw.flush(); - - return sw.toString(); - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - return false; - } - - public abstract boolean embedICCProfile(File src, File dst, byte profile[]); - - public abstract String getName(); - - public abstract String getDefaultExtension(); - - protected abstract String[] getAcceptedExtensions(); - - protected abstract ImageFormat[] getAcceptedTypes(); - - public boolean canAcceptType(ImageFormat type) - { - ImageFormat types[] = getAcceptedTypes(); - - for (int i = 0; i < types.length; i++) - if (types[i].equals(type)) - return true; - return false; - } - - protected final boolean canAcceptExtension(File file) - { - return canAcceptExtension(file.getName()); - } - - protected final boolean canAcceptExtension(String filename) - { - String exts[] = getAcceptedExtensions(); - if (exts == null) - return true; - - int index = filename.lastIndexOf('.'); - if (index >= 0) - { - String ext = filename.substring(index); - ext = ext.toLowerCase(); - - for (int i = 0; i < exts.length; i++) - if (exts[i].toLowerCase().equals(ext)) - return true; - } - return false; - } - - protected IBufferedImageFactory getBufferedImageFactory(Map params) - { - if (params == null) - return new SimpleBufferedImageFactory(); - - IBufferedImageFactory result = (IBufferedImageFactory) params - .get(SanselanConstants.BUFFERED_IMAGE_FACTORY); - - if (null != result) - return result; - - return new SimpleBufferedImageFactory(); - } - - public static final boolean isStrict(Map params) - { - if (params == null || !params.containsKey(PARAM_KEY_STRICT)) - return false; - return ((Boolean) params.get(PARAM_KEY_STRICT)).booleanValue(); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/Sanselan.java b/src/main/java/org/apache/sanselan/Sanselan.java deleted file mode 100644 index a2ce1e9..0000000 --- a/src/main/java/org/apache/sanselan/Sanselan.java +++ /dev/null @@ -1,1413 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.common.byteSources.ByteSourceInputStream; -import org.apache.sanselan.icc.IccProfileInfo; -import org.apache.sanselan.icc.IccProfileParser; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.color.ICC_Profile; -import com.google.code.appengine.awt.image.BufferedImage; - - -/** - * The primary interface to the sanselan library. - *

- * Almost all of the Sanselan library's core functionality can be accessed - * through it's methods. - *

- * All of Sanselan's methods are static. - *

- * See the source of the SampleUsage class and other classes in the - * org.apache.sanselan.sampleUsage package for examples. - * - * @see org.apache.sanselan.sampleUsage.SampleUsage - */ -public abstract class Sanselan implements SanselanConstants { - - /** - * Tries to guess whether a file contains an image based on its file - * extension. - *

- * Returns true if the file has a file extension associated with a file - * format, such as .jpg or .gif. - *

- * - * @param file - * File which may contain an image. - * @return true if the file has an image format file extension. - */ - public static boolean hasImageFileExtension(File file) { - if (!file.isFile()) - return false; - return hasImageFileExtension(file.getName()); - } - - /** - * Tries to guess whether a filename represents an image based on its file - * extension. - *

- * Returns true if the filename has a file extension associated with a file - * format, such as .jpg or .gif. - *

- * - * @param filename - * String representing name of file which may contain an image. - * @return true if the filename has an image format file extension. - */ - public static boolean hasImageFileExtension(String filename) { - filename = filename.toLowerCase(); - - ImageParser imageParsers[] = ImageParser.getAllImageParsers(); - for (int i = 0; i < imageParsers.length; i++) { - ImageParser imageParser = imageParsers[i]; - String exts[] = imageParser.getAcceptedExtensions(); - - for (int j = 0; j < exts.length; j++) { - String ext = exts[j]; - if (filename.endsWith(ext.toLowerCase())) - return true; - } - } - - return false; - } - - /** - * Tries to guess what the image type (if any) of data based on the file's - * "magic numbers," the first bytes of the data. - *

- * - * @param bytes - * Byte array containing an image file. - * @return An ImageFormat, such as ImageFormat.IMAGE_FORMAT_JPEG. Returns - * ImageFormat.IMAGE_FORMAT_UNKNOWN if the image type cannot be - * guessed. - */ - public static ImageFormat guessFormat(byte bytes[]) - throws ImageReadException, IOException { - return guessFormat(new ByteSourceArray(bytes)); - } - - /** - * Tries to guess what the image type (if any) of a file based on the file's - * "magic numbers," the first bytes of the file. - *

- * - * @param file - * File containing image data. - * @return An ImageFormat, such as ImageFormat.IMAGE_FORMAT_JPEG. Returns - * ImageFormat.IMAGE_FORMAT_UNKNOWN if the image type cannot be - * guessed. - */ - public static ImageFormat guessFormat(File file) throws ImageReadException, - IOException { - return guessFormat(new ByteSourceFile(file)); - } - - private static final int[] MAGIC_NUMBERS_GIF = { 0x47, 0x49, }; - private static final int[] MAGIC_NUMBERS_PNG = { 0x89, 0x50, }; - private static final int[] MAGIC_NUMBERS_JPEG = { 0xff, 0xd8, }; - private static final int[] MAGIC_NUMBERS_BMP = { 0x42, 0x4d, }; - private static final int[] MAGIC_NUMBERS_TIFF_MOTOROLA = { 0x4D, 0x4D, }; - private static final int[] MAGIC_NUMBERS_TIFF_INTEL = { 0x49, 0x49, }; - private static final int[] MAGIC_NUMBERS_PSD = { 0x38, 0x42, }; - private static final int[] MAGIC_NUMBERS_PBM_A = { 0x50, 0x31, }; - private static final int[] MAGIC_NUMBERS_PBM_B = { 0x50, 0x34, }; - private static final int[] MAGIC_NUMBERS_PGM_A = { 0x50, 0x32, }; - private static final int[] MAGIC_NUMBERS_PGM_B = { 0x50, 0x35, }; - private static final int[] MAGIC_NUMBERS_PPM_A = { 0x50, 0x33, }; - private static final int[] MAGIC_NUMBERS_PPM_B = { 0x50, 0x36, }; - private static final int[] MAGIC_NUMBERS_JBIG2_1 = { 0x97, 0x4A, }; - private static final int[] MAGIC_NUMBERS_JBIG2_2 = { 0x42, 0x32, }; - private static final int[] MAGIC_NUMBERS_ICNS = { 0x69, 0x63, }; - - private static boolean compareBytePair(int[] a, int b[]) { - if (a.length != 2 && b.length != 2) { - throw new RuntimeException("Invalid Byte Pair."); - } - return (a[0] == b[0]) && (a[1] == b[1]); - } - - public static ImageFormat guessFormat(ByteSource byteSource) - throws ImageReadException, IOException { - InputStream is = null; - - try { - is = byteSource.getInputStream(); - - int i1 = is.read(); - int i2 = is.read(); - if ((i1 < 0) || (i2 < 0)) - throw new ImageReadException( - "Couldn't read magic numbers to guess format."); - - int b1 = i1 & 0xff; - int b2 = i2 & 0xff; - int bytePair[] = { b1, b2, }; - - if (compareBytePair(MAGIC_NUMBERS_GIF, bytePair)) { - return ImageFormat.IMAGE_FORMAT_GIF; - } - // else if (b1 == 0x00 && b2 == 0x00) // too similar to TGA - // { - // return ImageFormat.IMAGE_FORMAT_ICO; - // } - else if (compareBytePair(MAGIC_NUMBERS_PNG, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PNG; - } else if (compareBytePair(MAGIC_NUMBERS_JPEG, bytePair)) { - return ImageFormat.IMAGE_FORMAT_JPEG; - } else if (compareBytePair(MAGIC_NUMBERS_BMP, bytePair)) { - return ImageFormat.IMAGE_FORMAT_BMP; - } else if (compareBytePair(MAGIC_NUMBERS_TIFF_MOTOROLA, bytePair)) { - return ImageFormat.IMAGE_FORMAT_TIFF; - } else if (compareBytePair(MAGIC_NUMBERS_TIFF_INTEL, bytePair)) { - return ImageFormat.IMAGE_FORMAT_TIFF; - } else if (compareBytePair(MAGIC_NUMBERS_PSD, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PSD; - } else if (compareBytePair(MAGIC_NUMBERS_PBM_A, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PBM; - } else if (compareBytePair(MAGIC_NUMBERS_PBM_B, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PBM; - } else if (compareBytePair(MAGIC_NUMBERS_PGM_A, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PGM; - } else if (compareBytePair(MAGIC_NUMBERS_PGM_B, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PGM; - } else if (compareBytePair(MAGIC_NUMBERS_PPM_A, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PPM; - } else if (compareBytePair(MAGIC_NUMBERS_PPM_B, bytePair)) { - return ImageFormat.IMAGE_FORMAT_PPM; - } else if (compareBytePair(MAGIC_NUMBERS_JBIG2_1, bytePair)) { - int i3 = is.read(); - int i4 = is.read(); - if ((i3 < 0) || (i4 < 0)) - throw new ImageReadException( - "Couldn't read magic numbers to guess format."); - - int b3 = i3 & 0xff; - int b4 = i4 & 0xff; - int bytePair2[] = { b3, b4, }; - if (compareBytePair(MAGIC_NUMBERS_JBIG2_2, bytePair2)) { - return ImageFormat.IMAGE_FORMAT_JBIG2; - } - } else if (compareBytePair(MAGIC_NUMBERS_ICNS, bytePair)) { - return ImageFormat.IMAGE_FORMAT_ICNS; - } - - return ImageFormat.IMAGE_FORMAT_UNKNOWN; - } finally { - if (is != null) { - try { - is.close(); - - } catch (IOException e) { - Debug.debug(e); - - } - } - } - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param bytes - * Byte array containing an image file. - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(byte bytes[]) - throws ImageReadException, IOException { - return getICCProfile(bytes, null); - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(byte bytes[], Map params) - throws ImageReadException, IOException { - return getICCProfile(new ByteSourceArray(bytes), params); - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(InputStream is, String filename) - throws ImageReadException, IOException { - return getICCProfile(is, filename, null); - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(InputStream is, String filename, - Map params) throws ImageReadException, IOException { - return getICCProfile(new ByteSourceInputStream(is, filename), params); - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param file - * File containing image data. - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(File file) - throws ImageReadException, IOException { - return getICCProfile(file, null); - } - - /** - * Extracts an ICC Profile (if present) from JPEG, PNG, PSD (Photoshop) and - * TIFF images. - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ICC_Profile or null if the image contains no ICC - * profile.. - */ - public static ICC_Profile getICCProfile(File file, Map params) - throws ImageReadException, IOException { - return getICCProfile(new ByteSourceFile(file), params); - } - - protected static ICC_Profile getICCProfile(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - byte bytes[] = getICCProfileBytes(byteSource, params); - if (bytes == null) - return null; - - IccProfileParser parser = new IccProfileParser(); - IccProfileInfo info = parser.getICCProfileInfo(bytes); - if (info == null) - return null; - if (info.issRGB()) - return null; - - ICC_Profile icc = ICC_Profile.getInstance(bytes); - return icc; - } - - /** - * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD - * (Photoshop) and TIFF images. - *

- * To parse the result use IccProfileParser or - * ICC_Profile.getInstance(bytes). - *

- * - * @param bytes - * Byte array containing an image file. - * @return A byte array. - * @see IccProfileParser - * @see ICC_Profile - */ - public static byte[] getICCProfileBytes(byte bytes[]) - throws ImageReadException, IOException { - return getICCProfileBytes(bytes, null); - } - - /** - * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD - * (Photoshop) and TIFF images. - *

- * To parse the result use IccProfileParser or - * ICC_Profile.getInstance(bytes). - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A byte array. - * @see IccProfileParser - * @see ICC_Profile - */ - public static byte[] getICCProfileBytes(byte bytes[], Map params) - throws ImageReadException, IOException { - return getICCProfileBytes(new ByteSourceArray(bytes), params); - } - - /** - * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD - * (Photoshop) and TIFF images. - *

- * To parse the result use IccProfileParser or - * ICC_Profile.getInstance(bytes). - *

- * - * @param file - * File containing image data. - * @return A byte array. - * @see IccProfileParser - * @see ICC_Profile - */ - public static byte[] getICCProfileBytes(File file) - throws ImageReadException, IOException { - return getICCProfileBytes(file, null); - } - - /** - * Extracts the raw bytes of an ICC Profile (if present) from JPEG, PNG, PSD - * (Photoshop) and TIFF images. - *

- * To parse the result use IccProfileParser or - * ICC_Profile.getInstance(bytes). - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A byte array. - * @see IccProfileParser - * @see ICC_Profile - */ - public static byte[] getICCProfileBytes(File file, Map params) - throws ImageReadException, IOException { - return getICCProfileBytes(new ByteSourceFile(file), params); - } - - private static byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getICCProfileBytes(byteSource, params); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param filename - * String. - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(String filename, byte bytes[], - Map params) throws ImageReadException, IOException { - return getImageInfo(new ByteSourceArray(filename, bytes), params); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param filename - * String. - * @param bytes - * Byte array containing an image file. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(String filename, byte bytes[]) - throws ImageReadException, IOException { - return getImageInfo(new ByteSourceArray(filename, bytes), null); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(InputStream is, String filename) - throws ImageReadException, IOException { - return getImageInfo(new ByteSourceInputStream(is, filename), null); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(InputStream is, String filename, - Map params) throws ImageReadException, IOException { - return getImageInfo(new ByteSourceInputStream(is, filename), params); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param bytes - * Byte array containing an image file. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(byte bytes[]) - throws ImageReadException, IOException { - return getImageInfo(new ByteSourceArray(bytes), null); - } - - /** - * Parses the "image info" of an image. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(byte bytes[], Map params) - throws ImageReadException, IOException { - return getImageInfo(new ByteSourceArray(bytes), params); - } - - /** - * Parses the "image info" of an image file. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(File file, Map params) - throws ImageReadException, IOException { - return getImageInfo(new ByteSourceFile(file), params); - } - - /** - * Parses the "image info" of an image file. - *

- * "Image info" is a summary of basic information about the image such as: - * width, height, file format, bit depth, color type, etc. - *

- * Not to be confused with "image metadata." - *

- * - * @param file - * File containing image data. - * @return An instance of ImageInfo. - * @see ImageInfo - */ - public static ImageInfo getImageInfo(File file) throws ImageReadException, - IOException { - return getImageInfo(file, null); - } - - private static ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - ImageInfo imageInfo = imageParser.getImageInfo(byteSource, params); - - return imageInfo; - } - - private static final ImageParser getImageParser(ByteSource byteSource) - throws ImageReadException, IOException { - ImageFormat format = guessFormat(byteSource); - if (!format.equals(ImageFormat.IMAGE_FORMAT_UNKNOWN)) { - - ImageParser imageParsers[] = ImageParser.getAllImageParsers(); - - for (int i = 0; i < imageParsers.length; i++) { - ImageParser imageParser = imageParsers[i]; - - if (imageParser.canAcceptType(format)) - return imageParser; - } - } - - String filename = byteSource.getFilename(); - if (filename != null) { - ImageParser imageParsers[] = ImageParser.getAllImageParsers(); - - for (int i = 0; i < imageParsers.length; i++) { - ImageParser imageParser = imageParsers[i]; - - if (imageParser.canAcceptExtension(filename)) - return imageParser; - } - } - - throw new ImageReadException("Can't parse this format."); - } - - /** - * Determines the width and height of an image. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return The width and height of the image. - */ - public static Dimension getImageSize(InputStream is, String filename) - throws ImageReadException, IOException { - return getImageSize(is, filename, null); - } - - /** - * Determines the width and height of an image. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return The width and height of the image. - */ - public static Dimension getImageSize(InputStream is, String filename, - Map params) throws ImageReadException, IOException { - return getImageSize(new ByteSourceInputStream(is, filename), params); - } - - /** - * Determines the width and height of an image. - *

- * - * @param bytes - * Byte array containing an image file. - * @return The width and height of the image. - */ - public static Dimension getImageSize(byte bytes[]) - throws ImageReadException, IOException { - return getImageSize(bytes, null); - } - - /** - * Determines the width and height of an image. - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return The width and height of the image. - */ - public static Dimension getImageSize(byte bytes[], Map params) - throws ImageReadException, IOException { - return getImageSize(new ByteSourceArray(bytes), params); - } - - /** - * Determines the width and height of an image file. - *

- * - * @param file - * File containing image data. - * @return The width and height of the image. - */ - public static Dimension getImageSize(File file) throws ImageReadException, - IOException { - return getImageSize(file, null); - } - - /** - * Determines the width and height of an image file. - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return The width and height of the image. - */ - public static Dimension getImageSize(File file, Map params) - throws ImageReadException, IOException { - return getImageSize(new ByteSourceFile(file), params); - } - - public static Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getImageSize(byteSource, params); - } - - /** - * Determines the width and height of an image. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(InputStream is, String filename) - throws ImageReadException, IOException { - return getXmpXml(is, filename, null); - } - - /** - * Determines the width and height of an image. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(InputStream is, String filename, Map params) - throws ImageReadException, IOException { - return getXmpXml(new ByteSourceInputStream(is, filename), params); - } - - /** - * Determines the width and height of an image. - *

- * - * @param bytes - * Byte array containing an image file. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(byte bytes[]) throws ImageReadException, - IOException { - return getXmpXml(bytes, null); - } - - /** - * Determines the width and height of an image. - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(byte bytes[], Map params) - throws ImageReadException, IOException { - return getXmpXml(new ByteSourceArray(bytes), params); - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param file - * File containing image data. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(File file) throws ImageReadException, - IOException { - return getXmpXml(file, null); - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(File file, Map params) - throws ImageReadException, IOException { - return getXmpXml(new ByteSourceFile(file), params); - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public static String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getXmpXml(byteSource, params); - } - - /** - * Parses the metadata of an image. This metadata depends on the format of - * the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param bytes - * Byte array containing an image file. - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(byte bytes[]) - throws ImageReadException, IOException { - return getMetadata(bytes, null); - } - - /** - * Parses the metadata of an image. This metadata depends on the format of - * the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(byte bytes[], Map params) - throws ImageReadException, IOException { - return getMetadata(new ByteSourceArray(bytes), params); - } - - /** - * Parses the metadata of an image file. This metadata depends on the format - * of the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(InputStream is, String filename) - throws ImageReadException, IOException { - return getMetadata(is, filename, null); - } - - /** - * Parses the metadata of an image file. This metadata depends on the format - * of the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(InputStream is, String filename, - Map params) throws ImageReadException, IOException { - return getMetadata(new ByteSourceInputStream(is, filename), params); - } - - /** - * Parses the metadata of an image file. This metadata depends on the format - * of the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param file - * File containing image data. - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(File file) - throws ImageReadException, IOException { - return getMetadata(file, null); - } - - /** - * Parses the metadata of an image file. This metadata depends on the format - * of the image. - *

- * JPEG/JFIF files may contain EXIF and/or IPTC metadata. PNG files may - * contain comments. TIFF files may contain metadata. - *

- * The instance of IImageMetadata returned by getMetadata() should be upcast - * (depending on image format). - *

- * Not to be confused with "image info." - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return An instance of IImageMetadata. - * @see IImageMetadata - */ - public static IImageMetadata getMetadata(File file, Map params) - throws ImageReadException, IOException { - return getMetadata(new ByteSourceFile(file), params); - } - - private static IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getMetadata(byteSource, params); - } - - /** - * Returns a description of the image's structure. - *

- * Useful for exploring format-specific details of image files. - *

- * - * @param bytes - * Byte array containing an image file. - * @return A description of the image file's structure. - */ - public static String dumpImageFile(byte bytes[]) throws ImageReadException, - IOException { - return dumpImageFile(new ByteSourceArray(bytes)); - } - - /** - * Returns a description of the image file's structure. - *

- * Useful for exploring format-specific details of image files. - *

- * - * @param file - * File containing image data. - * @return A description of the image file's structure. - */ - public static String dumpImageFile(File file) throws ImageReadException, - IOException { - return dumpImageFile(new ByteSourceFile(file)); - } - - private static String dumpImageFile(ByteSource byteSource) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.dumpImageFile(byteSource); - } - - public static FormatCompliance getFormatCompliance(byte bytes[]) - throws ImageReadException, IOException { - return getFormatCompliance(new ByteSourceArray(bytes)); - } - - public static FormatCompliance getFormatCompliance(File file) - throws ImageReadException, IOException { - return getFormatCompliance(new ByteSourceFile(file)); - } - - private static FormatCompliance getFormatCompliance(ByteSource byteSource) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getFormatCompliance(byteSource); - } - - /** - * Returns all images contained in an image. - *

- * Useful for image formats such as GIF and ICO in which a single file may - * contain multiple images. - *

- * - * @param is - * InputStream from which to read image data. - * @param filename - * Filename associated with image data (optional). - * @return A vector of BufferedImages. - */ - public static ArrayList getAllBufferedImages(InputStream is, String filename) - throws ImageReadException, IOException { - return getAllBufferedImages(new ByteSourceInputStream(is, filename)); - } - - /** - * Returns all images contained in an image. - *

- * Useful for image formats such as GIF and ICO in which a single file may - * contain multiple images. - *

- * - * @param bytes - * Byte array containing an image file. - * @return A vector of BufferedImages. - */ - public static ArrayList getAllBufferedImages(byte bytes[]) - throws ImageReadException, IOException { - return getAllBufferedImages(new ByteSourceArray(bytes)); - } - - /** - * Returns all images contained in an image file. - *

- * Useful for image formats such as GIF and ICO in which a single file may - * contain multiple images. - *

- * - * @param file - * File containing image data. - * @return A vector of BufferedImages. - */ - public static ArrayList getAllBufferedImages(File file) - throws ImageReadException, IOException { - return getAllBufferedImages(new ByteSourceFile(file)); - } - - private static ArrayList getAllBufferedImages(ByteSource byteSource) - throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - - return imageParser.getAllBufferedImages(byteSource); - } - - // public static boolean extractImages(byte bytes[], File dstDir, - // String dstRoot, ImageParser encoder) throws ImageReadException, - // IOException, ImageWriteException - // { - // return extractImages(new ByteSourceArray(bytes), dstDir, dstRoot, - // encoder); - // } - // - // public static boolean extractImages(File file, File dstDir, String - // dstRoot, - // ImageParser encoder) throws ImageReadException, IOException, - // ImageWriteException - // { - // return extractImages(new ByteSourceFile(file), dstDir, dstRoot, encoder); - // } - // - // public static boolean extractImages(ByteSource byteSource, File dstDir, - // String dstRoot, ImageParser encoder) throws ImageReadException, - // IOException, ImageWriteException - // { - // ImageParser imageParser = getImageParser(byteSource); - // - // return imageParser.extractImages(byteSource, dstDir, dstRoot, encoder); - // } - - /** - * Reads the first image from an InputStream as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param is - * InputStream to read image data from. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(InputStream is) - throws ImageReadException, IOException { - return getBufferedImage(is, null); - } - - /** - * Reads the first image from an InputStream as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param is - * InputStream to read image data from. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(InputStream is, Map params) - throws ImageReadException, IOException { - String filename = null; - if (params != null && params.containsKey(PARAM_KEY_FILENAME)) - filename = (String) params.get(PARAM_KEY_FILENAME); - return getBufferedImage(new ByteSourceInputStream(is, filename), params); - } - - /** - * Reads the first image from an image file as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param bytes - * Byte array containing an image file. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(byte bytes[]) - throws ImageReadException, IOException { - return getBufferedImage(new ByteSourceArray(bytes), null); - } - - /** - * Reads the first image from an image file as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param bytes - * Byte array containing an image file. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(byte bytes[], Map params) - throws ImageReadException, IOException { - return getBufferedImage(new ByteSourceArray(bytes), params); - } - - /** - * Reads the first image from an image file as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param file - * File containing image data. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(File file) - throws ImageReadException, IOException { - return getBufferedImage(new ByteSourceFile(file), null); - } - - /** - * Reads the first image from an image file as a BufferedImage. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param file - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A BufferedImage. - * @see SanselanConstants - */ - public static BufferedImage getBufferedImage(File file, Map params) - throws ImageReadException, IOException { - return getBufferedImage(new ByteSourceFile(file), params); - } - - private static BufferedImage getBufferedImage(ByteSource byteSource, - Map params) throws ImageReadException, IOException { - ImageParser imageParser = getImageParser(byteSource); - if (null == params) - params = new HashMap(); - - return imageParser.getBufferedImage(byteSource, params); - } - - /** - * Writes a BufferedImage to a file. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param src - * The BufferedImage to be written. - * @param file - * File to write to. - * @param format - * The ImageFormat to use. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @see SanselanConstants - */ - public static void writeImage(BufferedImage src, File file, - ImageFormat format, Map params) throws ImageWriteException, - IOException { - OutputStream os = null; - - try { - os = new FileOutputStream(file); - os = new BufferedOutputStream(os); - - writeImage(src, os, format, params); - } finally { - try { - if (os != null) - os.close(); - } catch (Exception e) { - Debug.debug(e); - } - } - } - - /** - * Writes a BufferedImage to a byte array. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param src - * The BufferedImage to be written. - * @param format - * The ImageFormat to use. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return A byte array containing the image file. - * @see SanselanConstants - */ - public static byte[] writeImageToBytes(BufferedImage src, - ImageFormat format, Map params) throws ImageWriteException, - IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - writeImage(src, os, format, params); - - return os.toByteArray(); - } - - /** - * Writes a BufferedImage to an OutputStream. - *

- * (TODO: elaborate here.) - *

- * Sanselan can only read image info, metadata and ICC profiles from all - * image formats. However, note that the library cannot currently read or - * write JPEG image data. PSD (Photoshop) files can only be partially read - * and cannot be written. All other formats (PNG, GIF, TIFF, BMP, etc.) are - * fully supported. - *

- * - * @param src - * The BufferedImage to be written. - * @param os - * The OutputStream to write to. - * @param format - * The ImageFormat to use. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @see SanselanConstants - */ - public static void writeImage(BufferedImage src, OutputStream os, - ImageFormat format, Map params) throws ImageWriteException, - IOException { - ImageParser imageParsers[] = ImageParser.getAllImageParsers(); - - // make sure params are non-null - if (params == null) - params = new HashMap(); - - params.put(PARAM_KEY_FORMAT, format); - - for (int i = 0; i < imageParsers.length; i++) { - ImageParser imageParser = imageParsers[i]; - - if (!imageParser.canAcceptType(format)) - continue; - - imageParser.writeImage(src, os, params); - return; - } - - throw new ImageWriteException("Unknown Format: " + format); - } - -} diff --git a/src/main/java/org/apache/sanselan/color/ColorConversions.java b/src/main/java/org/apache/sanselan/color/ColorConversions.java deleted file mode 100644 index 444a81f..0000000 --- a/src/main/java/org/apache/sanselan/color/ColorConversions.java +++ /dev/null @@ -1,918 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.color; - -import org.apache.sanselan.util.Debug; - -public abstract class ColorConversions -{ - public final static void main(String args[]) - { - try - { - { - for (int C = 0; C <= 256; C += 64) - for (int M = 0; M <= 256; M += 64) - for (int Y = 0; Y <= 256; Y += 64) - for (int K = 0; K <= 256; K += 64) - { - - int rgb1 = ColorConversions.convertCMYKtoRGB( - Math.min(255, C), Math.min(255, M), - Math.min(255, Y), Math.min(255, K)); - int rgb2 = ColorConversions - .convertCMYKtoRGB_old(Math.min(255, C), - Math.min(255, M), Math.min(255, - Y), Math.min(255, K)); - - if (rgb1 != rgb2) - { - Debug.debug(); - Debug.debug("C", C); - Debug.debug("M", M); - Debug.debug("Y", Y); - Debug.debug("K", K); - Debug.debug("rgb1", rgb1 + " (" - + Integer.toHexString(rgb1) + ")"); - Debug.debug("rgb2", rgb2 + " (" - + Integer.toHexString(rgb2) + ")"); - } - } - } - int sample_rgbs[] = { - 0xffffffff, // - 0xff000000, // - 0xffff0000, // - 0xff00ff00, // - 0xff0000ff, // - 0xffff00ff, // - 0xfff0ff00, // - 0xff00ffff, // - 0x00000000, // - 0xff7f7f7f, // - }; - for (int i = 0; i < sample_rgbs.length; i++) - { - int rgb = sample_rgbs[i]; - - ColorXYZ xyz; - { - xyz = ColorConversions.convertRGBtoXYZ(rgb); - int xyz_rgb = ColorConversions.convertXYZtoRGB(xyz); - - Debug.debug(); - Debug.debug("rgb", rgb + " (" + Integer.toHexString(rgb) - + ")"); - Debug.debug("xyz", xyz); - Debug.debug("xyz_rgb", xyz_rgb + " (" - + Integer.toHexString(xyz_rgb) + ")"); - if ((0xffffff & xyz_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - ColorCIELab cielab; - { - cielab = ColorConversions.convertXYZtoCIELab(xyz); - ColorXYZ cielab_xyz = ColorConversions - .convertCIELabtoXYZ(cielab); - int cielab_xyz_rgb = ColorConversions - .convertXYZtoRGB(cielab_xyz); - - Debug.debug("cielab", cielab); - Debug.debug("cielab_xyz", cielab_xyz); - Debug.debug("cielab_xyz_rgb", cielab_xyz_rgb + " (" - + Integer.toHexString(cielab_xyz_rgb) + ")"); - if ((0xffffff & cielab_xyz_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - - { - ColorHunterLab hunterlab = ColorConversions - .convertXYZtoHunterLab(xyz); - ColorXYZ hunterlab_xyz = ColorConversions - .convertHunterLabtoXYZ(hunterlab); - int hunterlab_xyz_rgb = ColorConversions - .convertXYZtoRGB(hunterlab_xyz); - - Debug.debug("hunterlab", hunterlab); - Debug.debug("hunterlab_xyz", hunterlab_xyz); - Debug.debug("hunterlab_xyz_rgb", hunterlab_xyz_rgb + " (" - + Integer.toHexString(hunterlab_xyz_rgb) + ")"); - if ((0xffffff & hunterlab_xyz_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - - { - ColorCMY cmy = ColorConversions.convertRGBtoCMY(rgb); - ColorCMYK cmyk = ColorConversions.convertCMYtoCMYK(cmy); - ColorCMY cmyk_cmy = ColorConversions.convertCMYKtoCMY(cmyk); - int cmyk_cmy_rgb = ColorConversions - .convertCMYtoRGB(cmyk_cmy); - - Debug.debug("cmy", cmy); - Debug.debug("cmyk", cmyk); - Debug.debug("cmyk_cmy", cmyk_cmy); - Debug.debug("cmyk_cmy_rgb", cmyk_cmy_rgb + " (" - + Integer.toHexString(cmyk_cmy_rgb) + ")"); - if ((0xffffff & cmyk_cmy_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - - { - ColorHSL hsl = ColorConversions.convertRGBtoHSL(rgb); - int hsl_rgb = ColorConversions.convertHSLtoRGB(hsl); - - Debug.debug("hsl", hsl); - Debug.debug("hsl_rgb", hsl_rgb + " (" - + Integer.toHexString(hsl_rgb) + ")"); - if ((0xffffff & hsl_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - { - ColorHSV hsv = ColorConversions.convertRGBtoHSV(rgb); - int hsv_rgb = ColorConversions.convertHSVtoRGB(hsv); - - Debug.debug("hsv", hsv); - Debug.debug("hsv_rgb", hsv_rgb + " (" - + Integer.toHexString(hsv_rgb) + ")"); - if ((0xffffff & hsv_rgb) != (0xffffff & rgb)) - Debug.debug("!!!!!!!!!!!!!!!!!!!!!!!"); - } - - { - ColorCIELCH cielch = ColorConversions - .convertCIELabtoCIELCH(cielab); - ColorCIELab cielch_cielab = ColorConversions - .convertCIELCHtoCIELab(cielch); - - Debug.debug("cielch", cielch); - Debug.debug("cielch_cielab", cielch_cielab); - } - - { - ColorCIELuv cieluv = ColorConversions - .convertXYZtoCIELuv(xyz); - ColorXYZ cieluv_xyz = ColorConversions - .convertCIELuvtoXYZ(cieluv); - - Debug.debug("cieluv", cieluv); - Debug.debug("cieluv_xyz", cieluv_xyz); - } - - } - } - catch (Throwable e) - { - Debug.debug(e); - } - } - - public static final ColorCIELab convertXYZtoCIELab(ColorXYZ xyz) - { - return convertXYZtoCIELab(xyz.X, xyz.Y, xyz.Z); - } - - private static final double ref_X = 95.047; - private static final double ref_Y = 100.000; - private static final double ref_Z = 108.883; - - public static final ColorCIELab convertXYZtoCIELab(double X, double Y, - double Z) - { - - double var_X = X / ref_X; //ref_X = 95.047 Observer= 2, Illuminant= D65 - double var_Y = Y / ref_Y; //ref_Y = 100.000 - double var_Z = Z / ref_Z; //ref_Z = 108.883 - - if (var_X > 0.008856) - var_X = Math.pow(var_X, (1 / 3.0)); - else - var_X = (7.787 * var_X) + (16 / 116.0); - if (var_Y > 0.008856) - var_Y = Math.pow(var_Y, 1 / 3.0); - else - var_Y = (7.787 * var_Y) + (16 / 116.0); - if (var_Z > 0.008856) - var_Z = Math.pow(var_Z, 1 / 3.0); - else - var_Z = (7.787 * var_Z) + (16 / 116.0); - - double L = (116 * var_Y) - 16; - double a = 500 * (var_X - var_Y); - double b = 200 * (var_Y - var_Z); - return new ColorCIELab(L, a, b); - } - - public static final ColorXYZ convertCIELabtoXYZ(ColorCIELab cielab) - { - return convertCIELabtoXYZ(cielab.L, cielab.a, cielab.b); - } - - public static final ColorXYZ convertCIELabtoXYZ(double L, double a, double b) - { - double var_Y = (L + 16) / 116.0; - double var_X = a / 500 + var_Y; - double var_Z = var_Y - b / 200.0; - - if (Math.pow(var_Y, 3) > 0.008856) - var_Y = Math.pow(var_Y, 3); - else - var_Y = (var_Y - 16 / 116.0) / 7.787; - if (Math.pow(var_X, 3) > 0.008856) - var_X = Math.pow(var_X, 3); - else - var_X = (var_X - 16 / 116.0) / 7.787; - if (Math.pow(var_Z, 3) > 0.008856) - var_Z = Math.pow(var_Z, 3); - else - var_Z = (var_Z - 16 / 116.0) / 7.787; - - double X = ref_X * var_X; //ref_X = 95.047 Observer= 2, Illuminant= D65 - double Y = ref_Y * var_Y; //ref_Y = 100.000 - double Z = ref_Z * var_Z; //ref_Z = 108.883 - - return new ColorXYZ(X, Y, Z); - } - - public static final ColorHunterLab convertXYZtoHunterLab(ColorXYZ xyz) - { - return convertXYZtoHunterLab(xyz.X, xyz.Y, xyz.Z); - } - - public static final ColorHunterLab convertXYZtoHunterLab(double X, - double Y, double Z) - { - double L = 10 * Math.sqrt(Y); - double a = 17.5 * (((1.02 * X) - Y) / Math.sqrt(Y)); - double b = 7 * ((Y - (0.847 * Z)) / Math.sqrt(Y)); - - return new ColorHunterLab(L, a, b); - } - - public static final ColorXYZ convertHunterLabtoXYZ(ColorHunterLab cielab) - { - return convertHunterLabtoXYZ(cielab.L, cielab.a, cielab.b); - } - - public static final ColorXYZ convertHunterLabtoXYZ(double L, double a, - double b) - { - double var_Y = L / 10; - double var_X = a / 17.5 * L / 10; - double var_Z = b / 7 * L / 10; - - double Y = Math.pow(var_Y, 2); - double X = (var_X + Y) / 1.02; - double Z = -(var_Z - Y) / 0.847; - - return new ColorXYZ(X, Y, Z); - } - - public static final int convertXYZtoRGB(ColorXYZ xyz) - { - return convertXYZtoRGB(xyz.X, xyz.Y, xyz.Z); - } - - public static final int convertXYZtoRGB(double X, double Y, double Z) - { - //Observer = 2�, Illuminant = D65 - double var_X = X / 100.0; //Where X = 0 95.047 - double var_Y = Y / 100.0; //Where Y = 0 100.000 - double var_Z = Z / 100.0; //Where Z = 0 108.883 - - double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986; - double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415; - double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570; - - if (var_R > 0.0031308) - var_R = 1.055 * Math.pow(var_R, (1 / 2.4)) - 0.055; - else - var_R = 12.92 * var_R; - if (var_G > 0.0031308) - var_G = 1.055 * Math.pow(var_G, (1 / 2.4)) - 0.055; - else - var_G = 12.92 * var_G; - if (var_B > 0.0031308) - var_B = 1.055 * Math.pow(var_B, (1 / 2.4)) - 0.055; - else - var_B = 12.92 * var_B; - - double R = (var_R * 255); - double G = (var_G * 255); - double B = (var_B * 255); - - return convertRGBtoRGB(R, G, B); - } - - public static final ColorXYZ convertRGBtoXYZ(int rgb) - { - int r = 0xff & (rgb >> 16); - int g = 0xff & (rgb >> 8); - int b = 0xff & (rgb >> 0); - - double var_R = r / 255.0; //Where R = 0 - 255 - double var_G = g / 255.0; //Where G = 0 - 255 - double var_B = b / 255.0; //Where B = 0 - 255 - - if (var_R > 0.04045) - var_R = Math.pow((var_R + 0.055) / 1.055, 2.4); - else - var_R = var_R / 12.92; - if (var_G > 0.04045) - var_G = Math.pow((var_G + 0.055) / 1.055, 2.4); - else - var_G = var_G / 12.92; - if (var_B > 0.04045) - var_B = Math.pow((var_B + 0.055) / 1.055, 2.4); - else - var_B = var_B / 12.92; - - var_R = var_R * 100; - var_G = var_G * 100; - var_B = var_B * 100; - - // Observer. = 2, Illuminant = D65 - double X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805; - double Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722; - double Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505; - - return new ColorXYZ(X, Y, Z); - } - - public static final ColorCMY convertRGBtoCMY(int rgb) - { - int R = 0xff & (rgb >> 16); - int G = 0xff & (rgb >> 8); - int B = 0xff & (rgb >> 0); - - // RGB values = 0 - 255 - // CMY values = 0 - 1 - - double C = 1 - (R / 255.0); - double M = 1 - (G / 255.0); - double Y = 1 - (B / 255.0); - - return new ColorCMY(C, M, Y); - } - - public static final int convertCMYtoRGB(ColorCMY cmy) - { - // CMY values = 0 - 1 - // RGB values = 0 - 255 - - double R = (1 - cmy.C) * 255.0; - double G = (1 - cmy.M) * 255.0; - double B = (1 - cmy.Y) * 255.0; - - return convertRGBtoRGB(R, G, B); - } - - public static final ColorCMYK convertCMYtoCMYK(ColorCMY cmy) - { - // Where CMYK and CMY values = 0 - 1 - - double C = cmy.C; - double M = cmy.M; - double Y = cmy.Y; - - double var_K = 1.0; - - if (C < var_K) - var_K = C; - if (M < var_K) - var_K = M; - if (Y < var_K) - var_K = Y; - if (var_K == 1) - { //Black - C = 0; - M = 0; - Y = 0; - } - else - { - C = (C - var_K) / (1 - var_K); - M = (M - var_K) / (1 - var_K); - Y = (Y - var_K) / (1 - var_K); - } - return new ColorCMYK(C, M, Y, var_K); - } - - public static final ColorCMY convertCMYKtoCMY(ColorCMYK cmyk) - { - return convertCMYKtoCMY(cmyk.C, cmyk.M, cmyk.Y, cmyk.K); - } - - public static final ColorCMY convertCMYKtoCMY(double C, double M, double Y, - double K) - { - // Where CMYK and CMY values = 0 - 1 - - C = (C * (1 - K) + K); - M = (M * (1 - K) + K); - Y = (Y * (1 - K) + K); - - return new ColorCMY(C, M, Y); - } - - public static final int convertCMYKtoRGB(int c, int m, int y, int k) - // throws ImageReadException, IOException - { - double C = c / 255.0; - double M = m / 255.0; - double Y = y / 255.0; - double K = k / 255.0; - - return convertCMYtoRGB(convertCMYKtoCMY(C, M, Y, K)); - } - - public static final ColorHSL convertRGBtoHSL(int rgb) - { - - int R = 0xff & (rgb >> 16); - int G = 0xff & (rgb >> 8); - int B = 0xff & (rgb >> 0); - - double var_R = (R / 255.0); //Where RGB values = 0 - 255 - double var_G = (G / 255.0); - double var_B = (B / 255.0); - - double var_Min = Math.min(var_R, Math.min(var_G, var_B)); //Min. value of RGB - double var_Max = Math.max(var_R, Math.max(var_G, var_B)); //Max. value of RGB - double del_Max = var_Max - var_Min; //Delta RGB value - - double L = (var_Max + var_Min) / 2.0; - - double H, S; - // Debug.debug("del_Max", del_Max); - if (del_Max == 0) //This is a gray, no chroma... - { - H = 0; //HSL results = 0 - 1 - S = 0; - } - else - //Chromatic data... - { - // Debug.debug("L", L); - - if (L < 0.5) - S = del_Max / (var_Max + var_Min); - else - S = del_Max / (2 - var_Max - var_Min); - - // Debug.debug("S", S); - - double del_R = (((var_Max - var_R) / 6) + (del_Max / 2)) / del_Max; - double del_G = (((var_Max - var_G) / 6) + (del_Max / 2)) / del_Max; - double del_B = (((var_Max - var_B) / 6) + (del_Max / 2)) / del_Max; - - if (var_R == var_Max) - H = del_B - del_G; - else if (var_G == var_Max) - H = (1 / 3.0) + del_R - del_B; - else if (var_B == var_Max) - H = (2 / 3.0) + del_G - del_R; - else - { - Debug.debug("uh oh"); - H = 0; // cmc - } - - // Debug.debug("H1", H); - - if (H < 0) - H += 1; - if (H > 1) - H -= 1; - - // Debug.debug("H2", H); - } - - return new ColorHSL(H, S, L); - } - - public static int convertHSLtoRGB(ColorHSL hsl) - { - return convertHSLtoRGB(hsl.H, hsl.S, hsl.L); - } - - public static int convertHSLtoRGB(double H, double S, double L) - { - double R, G, B; - - if (S == 0) //HSL values = 0 - 1 - { - R = L * 255; //RGB results = 0 - 255 - G = L * 255; - B = L * 255; - } - else - { - double var_2; - - if (L < 0.5) - var_2 = L * (1 + S); - else - var_2 = (L + S) - (S * L); - - double var_1 = 2 * L - var_2; - - R = 255 * convertHuetoRGB(var_1, var_2, H + (1 / 3.0)); - G = 255 * convertHuetoRGB(var_1, var_2, H); - B = 255 * convertHuetoRGB(var_1, var_2, H - (1 / 3.0)); - } - - return convertRGBtoRGB(R, G, B); - } - - private static double convertHuetoRGB(double v1, double v2, double vH) //Function Hue_2_RGB - { - if (vH < 0) - vH += 1; - if (vH > 1) - vH -= 1; - if ((6 * vH) < 1) - return (v1 + (v2 - v1) * 6 * vH); - if ((2 * vH) < 1) - return (v2); - if ((3 * vH) < 2) - return (v1 + (v2 - v1) * ((2 / 3.0) - vH) * 6); - return (v1); - } - - public static final ColorHSV convertRGBtoHSV(int rgb) - { - int R = 0xff & (rgb >> 16); - int G = 0xff & (rgb >> 8); - int B = 0xff & (rgb >> 0); - - double var_R = (R / 255.0); //RGB values = 0 - 255 - double var_G = (G / 255.0); - double var_B = (B / 255.0); - - double var_Min = Math.min(var_R, Math.min(var_G, var_B)); //Min. value of RGB - double var_Max = Math.max(var_R, Math.max(var_G, var_B)); //Max. value of RGB - double del_Max = var_Max - var_Min; //Delta RGB value - - double V = var_Max; - - double H, S; - if (del_Max == 0) //This is a gray, no chroma... - { - H = 0; //HSV results = 0 - 1 - S = 0; - } - else - //Chromatic data... - { - S = del_Max / var_Max; - - double del_R = (((var_Max - var_R) / 6) + (del_Max / 2)) / del_Max; - double del_G = (((var_Max - var_G) / 6) + (del_Max / 2)) / del_Max; - double del_B = (((var_Max - var_B) / 6) + (del_Max / 2)) / del_Max; - - if (var_R == var_Max) - H = del_B - del_G; - else if (var_G == var_Max) - H = (1 / 3.0) + del_R - del_B; - else if (var_B == var_Max) - H = (2 / 3.0) + del_G - del_R; - else - { - Debug.debug("uh oh"); - H = 0; // cmc; - } - - if (H < 0) - H += 1; - if (H > 1) - H -= 1; - } - - return new ColorHSV(H, S, V); - } - - public static int convertHSVtoRGB(ColorHSV HSV) - { - return convertHSVtoRGB(HSV.H, HSV.S, HSV.V); - } - - public static int convertHSVtoRGB(double H, double S, double V) - { - double R, G, B; - - if (S == 0) //HSV values = 0 - 1 - { - R = V * 255; - G = V * 255; - B = V * 255; - } - else - { - double var_h = H * 6; - if (var_h == 6) - var_h = 0; //H must be < 1 - double var_i = Math.floor(var_h); //Or ... var_i = floor( var_h ) - double var_1 = V * (1 - S); - double var_2 = V * (1 - S * (var_h - var_i)); - double var_3 = V * (1 - S * (1 - (var_h - var_i))); - - double var_r, var_g, var_b; - - if (var_i == 0) - { - var_r = V; - var_g = var_3; - var_b = var_1; - } - else if (var_i == 1) - { - var_r = var_2; - var_g = V; - var_b = var_1; - } - else if (var_i == 2) - { - var_r = var_1; - var_g = V; - var_b = var_3; - } - else if (var_i == 3) - { - var_r = var_1; - var_g = var_2; - var_b = V; - } - else if (var_i == 4) - { - var_r = var_3; - var_g = var_1; - var_b = V; - } - else - { - var_r = V; - var_g = var_1; - var_b = var_2; - } - - R = var_r * 255; //RGB results = 0 - 255 - G = var_g * 255; - B = var_b * 255; - } - - return convertRGBtoRGB(R, G, B); - } - - public static final int convertCMYKtoRGB_old(int sc, int sm, int sy, int sk) - // throws ImageReadException, IOException - { - int red = 255 - (sc + sk); - int green = 255 - (sm + sk); - int blue = 255 - (sy + sk); - - return convertRGBtoRGB(red, green, blue); - } - - private static double cube(double f) - { - return f * f * f; - } - - private static double square(double f) - { - return f * f; - } - - public static final int convertCIELabtoARGBTest(int cieL, int cieA, int cieB) - { - double X, Y, Z; - - { - - double var_Y = ((cieL * 100.0 / 255.0) + 16.0) / 116.0; - double var_X = cieA / 500.0 + var_Y; - double var_Z = var_Y - cieB / 200.0; - - double var_x_cube = cube(var_X); - double var_y_cube = cube(var_Y); - double var_z_cube = cube(var_Z); - - if (var_y_cube > 0.008856) - var_Y = var_y_cube; - else - var_Y = (var_Y - 16 / 116.0) / 7.787; - - if (var_x_cube > 0.008856) - var_X = var_x_cube; - else - var_X = (var_X - 16 / 116.0) / 7.787; - - if (var_z_cube > 0.008856) - var_Z = var_z_cube; - else - var_Z = (var_Z - 16 / 116.0) / 7.787; - - // double ref_X = 95.047; - // double ref_Y = 100.000; - // double ref_Z = 108.883; - - X = ref_X * var_X; //ref_X = 95.047 Observer= 2, Illuminant= D65 - Y = ref_Y * var_Y; //ref_Y = 100.000 - Z = ref_Z * var_Z; //ref_Z = 108.883 - - } - - double R, G, B; - { - double var_X = X / 100; //X = From 0 to ref_X - double var_Y = Y / 100; //Y = From 0 to ref_Y - double var_Z = Z / 100; //Z = From 0 to ref_Y - - double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986; - double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415; - double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570; - - if (var_R > 0.0031308) - var_R = 1.055 * Math.pow(var_R, (1 / 2.4)) - 0.055; - else - var_R = 12.92 * var_R; - if (var_G > 0.0031308) - var_G = 1.055 * Math.pow(var_G, (1 / 2.4)) - 0.055; - else - var_G = 12.92 * var_G; - - if (var_B > 0.0031308) - var_B = 1.055 * Math.pow(var_B, (1 / 2.4)) - 0.055; - else - var_B = 12.92 * var_B; - - R = (var_R * 255); - G = (var_G * 255); - B = (var_B * 255); - } - - return convertRGBtoRGB(R, G, B); - } - - private static final int convertRGBtoRGB(double R, double G, double B) - { - int red = (int) Math.round(R); - int green = (int) Math.round(G); - int blue = (int) Math.round(B); - - red = Math.min(255, Math.max(0, red)); - green = Math.min(255, Math.max(0, green)); - blue = Math.min(255, Math.max(0, blue)); - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - return rgb; - } - - private static final int convertRGBtoRGB(int red, int green, int blue) - { - red = Math.min(255, Math.max(0, red)); - green = Math.min(255, Math.max(0, green)); - blue = Math.min(255, Math.max(0, blue)); - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - return rgb; - } - - public static ColorCIELCH convertCIELabtoCIELCH(ColorCIELab cielab) - { - return convertCIELabtoCIELCH(cielab.L, cielab.a, cielab.b); - } - - public static ColorCIELCH convertCIELabtoCIELCH(double L, double a, double b) - { - double var_H = Math.atan2(b, a); //Quadrant by signs - - if (var_H > 0) - var_H = (var_H / Math.PI) * 180.0; - else - var_H = 360 - radian_2_degree(Math.abs(var_H)); - - // L = L; - double C = Math.sqrt(square(a) + square(b)); - double H = var_H; - - return new ColorCIELCH(L, C, H); - } - - public static ColorCIELab convertCIELCHtoCIELab(ColorCIELCH cielch) - { - return convertCIELCHtoCIELab(cielch.L, cielch.C, cielch.H); - } - - public static ColorCIELab convertCIELCHtoCIELab(double L, double C, double H) - { - // Where CIE-H = 0 - 360 - - // CIE-L* = CIE-L; - double a = Math.cos(degree_2_radian(H)) * C; - double b = Math.sin(degree_2_radian(H)) * C; - - return new ColorCIELab(L, a, b); - } - - public static double degree_2_radian(double degree) - { - return degree * Math.PI / 180.0; - } - - public static double radian_2_degree(double radian) - { - return radian * 180.0 / Math.PI; - } - - public static ColorCIELuv convertXYZtoCIELuv(ColorXYZ xyz) - { - return convertXYZtoCIELuv(xyz.X, xyz.Y, xyz.Z); - } - - public static ColorCIELuv convertXYZtoCIELuv(double X, double Y, double Z) - { - // problems here with div by zero - - double var_U = (4 * X) / (X + (15 * Y) + (3 * Z)); - double var_V = (9 * Y) / (X + (15 * Y) + (3 * Z)); - - // Debug.debug("var_U", var_U); - // Debug.debug("var_V", var_V); - - double var_Y = Y / 100.0; - // Debug.debug("var_Y", var_Y); - - if (var_Y > 0.008856) - var_Y = Math.pow(var_Y, (1 / 3.0)); - else - var_Y = (7.787 * var_Y) + (16 / 116.0); - - double ref_X = 95.047; //Observer= 2, Illuminant= D65 - double ref_Y = 100.000; - double ref_Z = 108.883; - - // Debug.debug("var_Y", var_Y); - - double ref_U = (4 * ref_X) / (ref_X + (15 * ref_Y) + (3 * ref_Z)); - double ref_V = (9 * ref_Y) / (ref_X + (15 * ref_Y) + (3 * ref_Z)); - - // Debug.debug("ref_U", ref_U); - // Debug.debug("ref_V", ref_V); - - double L = (116 * var_Y) - 16; - double u = 13 * L * (var_U - ref_U); - double v = 13 * L * (var_V - ref_V); - - return new ColorCIELuv(L, u, v); - } - - public static ColorXYZ convertCIELuvtoXYZ(ColorCIELuv cielch) - { - return convertCIELuvtoXYZ(cielch.L, cielch.u, cielch.v); - } - - public static ColorXYZ convertCIELuvtoXYZ(double L, double u, double v) - { - // problems here with div by zero - - double var_Y = (L + 16) / 116; - if (Math.pow(var_Y, 3) > 0.008856) - var_Y = Math.pow(var_Y, 3); - else - var_Y = (var_Y - 16 / 116) / 7.787; - - double ref_X = 95.047; //Observer= 2, Illuminant= D65 - double ref_Y = 100.000; - double ref_Z = 108.883; - - double ref_U = (4 * ref_X) / (ref_X + (15 * ref_Y) + (3 * ref_Z)); - double ref_V = (9 * ref_Y) / (ref_X + (15 * ref_Y) + (3 * ref_Z)); - double var_U = u / (13 * L) + ref_U; - double var_V = v / (13 * L) + ref_V; - - double Y = var_Y * 100; - double X = -(9 * Y * var_U) / ((var_U - 4) * var_V - var_U * var_V); - double Z = (9 * Y - (15 * var_V * Y) - (var_V * X)) / (3 * var_V); - - return new ColorXYZ(X, Y, Z); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BinaryConstants.java b/src/main/java/org/apache/sanselan/common/BinaryConstants.java deleted file mode 100644 index 5cdc135..0000000 --- a/src/main/java/org/apache/sanselan/common/BinaryConstants.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -public interface BinaryConstants -{ - public static final int BYTE_ORDER_INTEL = 'I'; - public static final int BYTE_ORDER_LEAST_SIGNIFICANT_BYTE = BYTE_ORDER_INTEL; - public static final int BYTE_ORDER_LSB = BYTE_ORDER_INTEL; - public static final int BYTE_ORDER_LITTLE_ENDIAN = BYTE_ORDER_INTEL; - - public static final int BYTE_ORDER_MOTOROLA = 'M'; - public static final int BYTE_ORDER_MOST_SIGNIFICANT_BYTE = BYTE_ORDER_MOTOROLA; - public static final int BYTE_ORDER_MSB = BYTE_ORDER_MOTOROLA; - public static final int BYTE_ORDER_NETWORK = BYTE_ORDER_MOTOROLA; - public static final int BYTE_ORDER_BIG_ENDIAN = BYTE_ORDER_MOTOROLA; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BinaryFileFunctions.java b/src/main/java/org/apache/sanselan/common/BinaryFileFunctions.java deleted file mode 100644 index 043f179..0000000 --- a/src/main/java/org/apache/sanselan/common/BinaryFileFunctions.java +++ /dev/null @@ -1,1108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.RandomAccessFile; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; - -public class BinaryFileFunctions implements BinaryConstants -{ - protected boolean debug = false; - - public final void setDebug(boolean b) - { - debug = b; - } - - public final boolean getDebug() - { - return debug; - } - - protected final void readRandomBytes(InputStream is) - throws ImageReadException, IOException - { - - for (int counter = 0; counter < 100; counter++) - { - readByte("" + counter, is, "Random Data"); - } - } - - public final void debugNumber(String msg, int data) - { - debugNumber(msg, data, 1); - } - - public final void debugNumber(String msg, int data, int bytes) - { - PrintWriter pw = new PrintWriter(System.out); - debugNumber(pw, msg, - data, bytes); - pw.flush(); - } - - - public final void debugNumber(PrintWriter pw, String msg, int data) - { - debugNumber(pw, msg, data, 1); - } - - public final void debugNumber(PrintWriter pw, String msg, int data, - int bytes) - { - pw.print(msg + ": " + data + " ("); - int byteData = data; - for (int i = 0; i < bytes; i++) - { - if (i > 0) - pw.print(","); - int singleByte = 0xff & byteData; - pw.print((char) singleByte + " [" + singleByte + "]"); - byteData >>= 8; - } - pw.println(") [0x" + Integer.toHexString(data) + ", " - + Integer.toBinaryString(data) + "]"); - pw.flush(); - } - - public final boolean startsWith(byte haystack[], byte needle[]) - { - if (needle == null) - return false; - if (haystack == null) - return false; - if (needle.length > haystack.length) - return false; - - for (int i = 0; i < needle.length; i++) - { - if (needle[i] != haystack[i]) - return false; - } - - return true; - } - - public final byte[] readBytes(InputStream is, int count) - throws ImageReadException, IOException - { - byte result[] = new byte[count]; - for (int i = 0; i < count; i++) - { - int data = is.read(); - result[i] = (byte) data; - } - return result; - } - - public final void readAndVerifyBytes(InputStream is, byte expected[], - String exception) throws ImageReadException, IOException - { - for (int i = 0; i < expected.length; i++) - { - int data = is.read(); - byte b = (byte) (0xff & data); - - if (data < 0) - throw new ImageReadException("Unexpected EOF."); - - if (b != expected[i]) - { - // System.out.println("i" + ": " + i); - - // this.debugByteArray("expected", expected); - // debugNumber("data[" + i + "]", b); - // debugNumber("expected[" + i + "]", expected[i]); - - throw new ImageReadException(exception); - } - } - } - - protected final void readAndVerifyBytes(String name, InputStream is, - byte expected[], String exception) throws ImageReadException, - IOException - { - byte bytes[] = readByteArray(name, expected.length, is, exception); - - for (int i = 0; i < expected.length; i++) - { - if (bytes[i] != expected[i]) - { - // System.out.println("i" + ": " + i); - // debugNumber("bytes[" + i + "]", bytes[i]); - // debugNumber("expected[" + i + "]", expected[i]); - - throw new ImageReadException(exception); - } - } - } - - public final void skipBytes(InputStream is, int length, String exception) - throws IOException - { - long total = 0; - while (length != total) - { - long skipped = is.skip(length - total); - if (skipped < 1) - throw new IOException(exception + " (" + skipped + ")"); - total += skipped; - } - } - - protected final void scanForByte(InputStream is, byte value) - throws IOException - { - int count = 0; - for (int i = 0; count < 3; i++) - // while(count<3) - { - int b = is.read(); - if (b < 0) - return; - if ((0xff & b) == value) - { - System.out.println("\t" + i + ": match."); - count++; - } - } - } - - public final byte readByte(String name, InputStream is, String exception) - throws ImageReadException, IOException - { - int result = is.read(); - - if ((result < 0)) - { - System.out.println(name + ": " + result); - throw new IOException(exception); - } - - if (debug) - debugNumber(name, result); - - return (byte) (0xff & result); - } - - protected final RationalNumber[] convertByteArrayToRationalArray( - String name, byte bytes[], int start, int length, int byteOrder) - { - int expectedLength = start + length * 8; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - RationalNumber result[] = new RationalNumber[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToRational(name, bytes, start + i * 8, - byteOrder); - } - - return result; - } - - protected final RationalNumber convertByteArrayToRational(String name, - byte bytes[], int byteOrder) - { - return convertByteArrayToRational(name, bytes, 0, byteOrder); - } - - protected final RationalNumber convertByteArrayToRational(String name, - byte bytes[], int start, int byteOrder) - { - int numerator = convertByteArrayToInt(name, bytes, start + 0, byteOrder); - int divisor = convertByteArrayToInt(name, bytes, start + 4, byteOrder); - - return new RationalNumber(numerator, divisor); - } - - protected final int convertByteArrayToInt(String name, byte bytes[], - int byteOrder) - { - return convertByteArrayToInt(name, bytes, 0, byteOrder); - } - - protected final int convertByteArrayToInt(String name, byte bytes[], - int start, int byteOrder) - { - byte byte0 = bytes[start + 0]; - byte byte1 = bytes[start + 1]; - byte byte2 = bytes[start + 2]; - byte byte3 = bytes[start + 3]; - - int result; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result = ((0xff & byte0) << 24) | ((0xff & byte1) << 16) - | ((0xff & byte2) << 8) | ((0xff & byte3) << 0); - } else - { - // intel, little endian - result = ((0xff & byte3) << 24) | ((0xff & byte2) << 16) - | ((0xff & byte1) << 8) | ((0xff & byte0) << 0); - } - - if (debug) - debugNumber(name, result, 4); - - return result; - } - - protected final int[] convertByteArrayToIntArray(String name, byte bytes[], - int start, int length, int byteOrder) - { - int expectedLength = start + length * 4; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToInt(name, bytes, start + i * 4, - byteOrder); - } - - return result; - } - - protected final void writeIntInToByteArray(int value, byte bytes[], - int start, int byteOrder) - { - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - bytes[start + 0] = (byte) (value >> 24); - bytes[start + 1] = (byte) (value >> 16); - bytes[start + 2] = (byte) (value >> 8); - bytes[start + 3] = (byte) (value >> 0); - } else - { - bytes[start + 3] = (byte) (value >> 24); - bytes[start + 2] = (byte) (value >> 16); - bytes[start + 1] = (byte) (value >> 8); - bytes[start + 0] = (byte) (value >> 0); - } - } - - protected static final byte[] int2ToByteArray(int value, int byteOrder) - { - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - return new byte[] { (byte) (value >> 8), (byte) (value >> 0), }; - else - return new byte[] { (byte) (value >> 0), (byte) (value >> 8), }; - } - - protected final byte[] convertIntArrayToByteArray(int values[], - int byteOrder) - { - byte result[] = new byte[values.length * 4]; - - for (int i = 0; i < values.length; i++) - { - writeIntInToByteArray(values[i], result, i * 4, byteOrder); - } - - return result; - } - - protected final byte[] convertShortArrayToByteArray(int values[], - int byteOrder) - { - byte result[] = new byte[values.length * 2]; - - for (int i = 0; i < values.length; i++) - { - int value = values[i]; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[i * 2 + 0] = (byte) (value >> 8); - result[i * 2 + 1] = (byte) (value >> 0); - } else - { - result[i * 2 + 1] = (byte) (value >> 8); - result[i * 2 + 0] = (byte) (value >> 0); - } - } - - return result; - } - - protected final byte[] convertShortToByteArray(int value, int byteOrder) - { - byte result[] = new byte[2]; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[0] = (byte) (value >> 8); - result[1] = (byte) (value >> 0); - } else - { - result[1] = (byte) (value >> 8); - result[0] = (byte) (value >> 0); - } - - return result; - } - - protected final byte[] convertIntArrayToRationalArray(int numerators[], - int denominators[], int byteOrder) throws ImageWriteException - { - if (numerators.length != denominators.length) - throw new ImageWriteException("numerators.length (" - + numerators.length + " != denominators.length (" - + denominators.length + ")"); - - byte result[] = new byte[numerators.length * 8]; - - for (int i = 0; i < numerators.length; i++) - { - writeIntInToByteArray(numerators[i], result, i * 8, byteOrder); - writeIntInToByteArray(denominators[i], result, i * 8 + 4, byteOrder); - } - - return result; - } - - protected final byte[] convertRationalArrayToByteArray( - RationalNumber numbers[], int byteOrder) throws ImageWriteException - { - // Debug.debug("convertRationalArrayToByteArray 2"); - byte result[] = new byte[numbers.length * 8]; - - for (int i = 0; i < numbers.length; i++) - { - writeIntInToByteArray(numbers[i].numerator, result, i * 8, - byteOrder); - writeIntInToByteArray(numbers[i].divisor, result, i * 8 + 4, - byteOrder); - } - - return result; - } - - protected final byte[] convertRationalToByteArray(RationalNumber number, - int byteOrder) throws ImageWriteException - { - byte result[] = new byte[8]; - - writeIntInToByteArray(number.numerator, result, 0, byteOrder); - writeIntInToByteArray(number.divisor, result, 4, byteOrder); - - return result; - } - - protected final int convertByteArrayToShort(String name, byte bytes[], - int byteOrder) throws ImageReadException - { - return convertByteArrayToShort(name, 0, bytes, byteOrder); - } - - protected final int convertByteArrayToShort(String name, int index, - byte bytes[], int byteOrder) throws ImageReadException - { - if (index + 1 >= bytes.length) - throw new ImageReadException("Index out of bounds. Array size: " - + bytes.length + ", index: " + index); - - int byte0 = 0xff & bytes[index + 0]; - int byte1 = 0xff & bytes[index + 1]; - - int result; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - result = (byte0 << 8) | byte1; - else - // intel, little endian - result = (byte1 << 8) | byte0; - - if (debug) - debugNumber(name, result, 2); - - return result; - } - - protected final int[] convertByteArrayToShortArray(String name, - byte bytes[], int start, int length, int byteOrder) - throws ImageReadException - - { - int expectedLength = start + length * 2; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToShort(name, start + i * 2, bytes, - byteOrder); - } - - return result; - } - - public final byte[] readByteArray(String name, int length, InputStream is) - throws IOException - { - String exception = name + " could not be read."; - return readByteArray(name, length, is, exception); - } - - public final byte[] readByteArray(String name, int length, InputStream is, - String exception) throws IOException - { - byte result[] = new byte[length]; - - int read = 0; - while (read < length) - { - int count = is.read(result, read, length - read); - // Debug.debug("count", count); - if (count < 1) - throw new IOException(exception+" count: "+ count + " read: "+read+" length: "+length); - - read += count; - } - - if (debug) - { - for (int i = 0; ((i < length) && (i < 50)); i++) - { - debugNumber(name + " (" + i + ")", 0xff & result[i]); - } - } - return result; - } - - public final void debugByteArray(String name, byte bytes[]) - { - System.out.println(name + ": " + bytes.length); - - for (int i = 0; ((i < bytes.length) && (i < 50)); i++) - { - debugNumber("\t" + " (" + i + ")", 0xff & bytes[i]); - } - } - - protected final void debugNumberArray(String name, int numbers[], int length) - { - System.out.println(name + ": " + numbers.length); - - for (int i = 0; ((i < numbers.length) && (i < 50)); i++) - { - debugNumber(name + " (" + i + ")", numbers[i], length); - } - } - - public final byte[] readBytearray(String name, byte bytes[], int start, - int count) throws ImageReadException - { - if (bytes.length < (start + count)) - { - throw new ImageReadException("Invalid read. bytes.length: " + bytes.length+ ", start: " + start + ", count: " + count); - // return null; - } - - byte result[] = new byte[count]; - System.arraycopy(bytes, start, result, 0, count); - - if (debug) - debugByteArray(name, result); - - return result; - } - - protected final byte[] getByteArrayTail(String name, byte bytes[], int count) throws ImageReadException - { - return readBytearray(name, bytes, count, bytes.length - count); - } - - protected final byte[] getBytearrayHead(String name, byte bytes[], int count) throws ImageReadException - { - return readBytearray(name, bytes, 0, bytes.length - count); - } - - public static final byte[] slice(byte bytes[], int start, int count) - { - if (bytes.length < (start + count)) - return null; - - byte result[] = new byte[count]; - System.arraycopy(bytes, start, result, 0, count); - - return result; - } - - public static final byte[] tail(byte bytes[], int count) - { - if (count > bytes.length) - count = bytes.length; - return slice(bytes, bytes.length - count, count); - } - - public static final byte[] head(byte bytes[], int count) - { - if (count > bytes.length) - count = bytes.length; - return slice(bytes, 0, count); - } - - public final boolean compareByteArrays(byte a[], byte b[]) - { - if (a.length != b.length) - return false; - - return compareByteArrays(a, 0, b, 0, a.length); - } - - public final boolean compareByteArrays(byte a[], int aStart, byte b[], - int bStart, int length) - { - if (a.length < (aStart + length)) - { - return false; - } - if (b.length < (bStart + length)) - return false; - - for (int i = 0; i < length; i++) - { - if (a[aStart + i] != b[bStart + i]) - { - // debugNumber("\t" + "a[" + (aStart + i) + "]", a[aStart + i]); - // debugNumber("\t" + "b[" + (bStart + i) + "]", b[bStart + i]); - - return false; - } - } - - return true; - } - - public static final boolean compareBytes(byte a[], byte b[]) - { - if (a.length != b.length) - return false; - - return compareBytes(a, 0, b, 0, a.length); - } - - public static final boolean compareBytes(byte a[], int aStart, byte b[], - int bStart, int length) - { - if (a.length < (aStart + length)) - return false; - if (b.length < (bStart + length)) - return false; - - for (int i = 0; i < length; i++) - { - if (a[aStart + i] != b[bStart + i]) - return false; - } - - return true; - } - - protected final int read4Bytes(String name, InputStream is, - String exception, int byteOrder) throws ImageReadException, - IOException - { - int size = 4; - byte bytes[] = new byte[size]; - - int read = 0; - while (read < size) - { - int count = is.read(bytes, read, size - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return convertByteArrayToInt(name, bytes, byteOrder); - } - - protected final int read3Bytes(String name, InputStream is, - String exception, int byteOrder) throws ImageReadException, - IOException - { - byte byte0 = (byte) is.read(); - byte byte1 = (byte) is.read(); - byte byte2 = (byte) is.read(); - - int result; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - result = ((0xff & byte0) << 16) | ((0xff & byte1) << 8) - | ((0xff & byte2) << 0); - else - // intel, little endian - result = ((0xff & byte2) << 16) | ((0xff & byte1) << 8) - | ((0xff & byte0) << 0); - - if (debug) - debugNumber(name, result, 3); - - return result; - // - // - // int size = 3; - // byte bytes[] = new byte[size]; - // - // int read = 0; - // while (read < size) - // { - // int count = is.read(bytes, read, size - read); - // if (count < 1) - // throw new IOException(exception); - // - // read += count; - // } - // - // return convertByteArrayToInt(name, bytes, 0, 3, byteOrder); - } - - protected final int read2Bytes(String name, InputStream is, - String exception, int byteOrder) throws ImageReadException, - IOException - { - int size = 2; - byte bytes[] = new byte[size]; - - int read = 0; - while (read < size) - { - int count = is.read(bytes, read, size - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return convertByteArrayToShort(name, bytes, byteOrder); - } - - protected final void printCharQuad(String msg, int i) - { - System.out.println(msg + ": '" + (char) (0xff & (i >> 24)) - + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) - + (char) (0xff & (i >> 0)) + "'"); - - } - - protected final void printCharQuad(PrintWriter pw, String msg, int i) - { - pw.println(msg + ": '" + (char) (0xff & (i >> 24)) - + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) - + (char) (0xff & (i >> 0)) + "'"); - - } - - protected final void printByteBits(String msg, byte i) - { - System.out.println(msg + ": '" + Integer.toBinaryString(0xff & i)); - } - - public final static int CharsToQuad(char c1, char c2, char c3, char c4) - { - return (((0xff & c1) << 24) | ((0xff & c2) << 16) | ((0xff & c3) << 8) | ((0xff & c4) << 0)); - } - - public final int findNull(byte src[]) - { - return findNull(src, 0); - } - - public final int findNull(byte src[], int start) - { - for (int i = start; i < src.length; i++) - { - if (src[i] == 0) - return i; - - } - return -1; - } - - protected final byte[] getRAFBytes(RandomAccessFile raf, long pos, - int length, String exception) throws IOException - { - if (debug) - { - System.out.println("getRAFBytes pos" + ": " + pos); - System.out.println("getRAFBytes length" + ": " + length); - } - - byte result[] = new byte[length]; - - raf.seek(pos); - - int read = 0; - while (read < length) - { - int count = raf.read(result, read, length - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return result; - - } - - protected final float convertByteArrayToFloat(String name, byte bytes[], - int byteOrder) - { - return convertByteArrayToFloat(name, bytes, 0, byteOrder); - } - - protected final float convertByteArrayToFloat(String name, byte bytes[], - int start, int byteOrder) - { - // TODO: not tested; probably wrong. - - byte byte0 = bytes[start + 0]; - byte byte1 = bytes[start + 1]; - byte byte2 = bytes[start + 2]; - byte byte3 = bytes[start + 3]; - - int bits; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - bits = ((0xff & byte0) << 24) | ((0xff & byte1) << 16) - | ((0xff & byte2) << 8) | ((0xff & byte3) << 0); - } else - { - // intel, little endian - bits = ((0xff & byte3) << 24) | ((0xff & byte2) << 16) - | ((0xff & byte1) << 8) | ((0xff & byte0) << 0); - } - - float result = Float.intBitsToFloat(bits); - - // if (debug) - // debugNumber(name, result, 4); - - return result; - } - - protected final float[] convertByteArrayToFloatArray(String name, - byte bytes[], int start, int length, int byteOrder) - { - int expectedLength = start + length * 4; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - float result[] = new float[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToFloat(name, bytes, start + i * 4, - byteOrder); - } - - return result; - } - - protected final byte[] convertFloatToByteArray(float value, int byteOrder) - { - // TODO: not tested; probably wrong. - byte result[] = new byte[4]; - - int bits = Float.floatToRawIntBits(value); - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[0] = (byte) (0xff & (bits >> 0)); - result[1] = (byte) (0xff & (bits >> 8)); - result[2] = (byte) (0xff & (bits >> 16)); - result[3] = (byte) (0xff & (bits >> 24)); - } else - { - result[3] = (byte) (0xff & (bits >> 0)); - result[2] = (byte) (0xff & (bits >> 8)); - result[1] = (byte) (0xff & (bits >> 16)); - result[0] = (byte) (0xff & (bits >> 24)); - } - - return result; - } - - protected final byte[] convertFloatArrayToByteArray(float values[], - int byteOrder) - { - // TODO: not tested; probably wrong. - byte result[] = new byte[values.length * 4]; - for (int i = 0; i < values.length; i++) - { - float value = values[i]; - int bits = Float.floatToRawIntBits(value); - - int start = i * 4; - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[start + 0] = (byte) (0xff & (bits >> 0)); - result[start + 1] = (byte) (0xff & (bits >> 8)); - result[start + 2] = (byte) (0xff & (bits >> 16)); - result[start + 3] = (byte) (0xff & (bits >> 24)); - } else - { - result[start + 3] = (byte) (0xff & (bits >> 0)); - result[start + 2] = (byte) (0xff & (bits >> 8)); - result[start + 1] = (byte) (0xff & (bits >> 16)); - result[start + 0] = (byte) (0xff & (bits >> 24)); - } - } - return result; - } - - protected final byte[] convertDoubleToByteArray(double value, int byteOrder) - { - // TODO: not tested; probably wrong. - byte result[] = new byte[8]; - - long bits = Double.doubleToRawLongBits(value); - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[0] = (byte) (0xff & (bits >> 0)); - result[1] = (byte) (0xff & (bits >> 8)); - result[2] = (byte) (0xff & (bits >> 16)); - result[3] = (byte) (0xff & (bits >> 24)); - result[4] = (byte) (0xff & (bits >> 32)); - result[5] = (byte) (0xff & (bits >> 40)); - result[6] = (byte) (0xff & (bits >> 48)); - result[7] = (byte) (0xff & (bits >> 56)); - } else - { - result[7] = (byte) (0xff & (bits >> 0)); - result[6] = (byte) (0xff & (bits >> 8)); - result[5] = (byte) (0xff & (bits >> 16)); - result[4] = (byte) (0xff & (bits >> 24)); - result[3] = (byte) (0xff & (bits >> 32)); - result[2] = (byte) (0xff & (bits >> 40)); - result[1] = (byte) (0xff & (bits >> 48)); - result[0] = (byte) (0xff & (bits >> 56)); - } - - return result; - } - - protected final byte[] convertDoubleArrayToByteArray(double values[], - int byteOrder) - { - // TODO: not tested; probably wrong. - byte result[] = new byte[values.length * 8]; - for (int i = 0; i < values.length; i++) - { - double value = values[i]; - long bits = Double.doubleToRawLongBits(value); - - int start = i * 8; - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - result[start + 0] = (byte) (0xff & (bits >> 0)); - result[start + 1] = (byte) (0xff & (bits >> 8)); - result[start + 2] = (byte) (0xff & (bits >> 16)); - result[start + 3] = (byte) (0xff & (bits >> 24)); - result[start + 4] = (byte) (0xff & (bits >> 32)); - result[start + 5] = (byte) (0xff & (bits >> 40)); - result[start + 6] = (byte) (0xff & (bits >> 48)); - result[start + 7] = (byte) (0xff & (bits >> 56)); - } else - { - result[start + 7] = (byte) (0xff & (bits >> 0)); - result[start + 6] = (byte) (0xff & (bits >> 8)); - result[start + 5] = (byte) (0xff & (bits >> 16)); - result[start + 4] = (byte) (0xff & (bits >> 24)); - result[start + 3] = (byte) (0xff & (bits >> 32)); - result[start + 2] = (byte) (0xff & (bits >> 40)); - result[start + 1] = (byte) (0xff & (bits >> 48)); - result[start + 0] = (byte) (0xff & (bits >> 56)); - } - } - return result; - } - - protected final double convertByteArrayToDouble(String name, byte bytes[], - int byteOrder) - { - return convertByteArrayToDouble(name, bytes, 0, byteOrder); - } - - protected final double convertByteArrayToDouble(String name, byte bytes[], - int start, int byteOrder) - { - // TODO: not tested; probably wrong. - - byte byte0 = bytes[start + 0]; - byte byte1 = bytes[start + 1]; - byte byte2 = bytes[start + 2]; - byte byte3 = bytes[start + 3]; - byte byte4 = bytes[start + 4]; - byte byte5 = bytes[start + 5]; - byte byte6 = bytes[start + 6]; - byte byte7 = bytes[start + 7]; - - long bits; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - { - bits = ((0xff & byte0) << 56) | ((0xff & byte1) << 48) - | ((0xff & byte2) << 40) | ((0xff & byte3) << 32) - | ((0xff & byte4) << 24) | ((0xff & byte5) << 16) - | ((0xff & byte6) << 8) | ((0xff & byte7) << 0); - - } else - { - // intel, little endian - bits = ((0xff & byte7) << 56) | ((0xff & byte6) << 48) - | ((0xff & byte5) << 40) | ((0xff & byte4) << 32) - | ((0xff & byte3) << 24) | ((0xff & byte2) << 16) - | ((0xff & byte1) << 8) | ((0xff & byte0) << 0); - } - - double result = Double.longBitsToDouble(bits); - - // if (debug) - // debugNumber(name, result, 4); - - return result; - - // byte array[]; - // if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - // // ?? dunno byte order very likely wrong here. - // array = new byte[]{ - // bytes[start + 0], bytes[start + 1], bytes[start + 2], - // bytes[start + 3], bytes[start + 4], bytes[start + 5], - // bytes[start + 6], bytes[start + 7], - // - // }; - // else - // // ?? dunno byte order very likely wrong here. - // array = new byte[]{ - // bytes[start + 3], bytes[start + 2], bytes[start + 1], - // bytes[start + 0], bytes[start + 7], bytes[start + 6], - // bytes[start + 5], bytes[start + 4], - // }; - // - // double result = Double.NaN; - // - // try - // { - // ByteArrayInputStream bais = new ByteArrayInputStream(array); - // if (start > 0) - // { - // skipBytes(bais, start); - // // bais.skip(start); - // } - // DataInputStream dis = new DataInputStream(bais); - // result = dis.readDouble(); - // - // dis.close(); - // } - // catch (Exception e) - // { - // Debug.debug(e); - // } - // - // return result; - } - - protected final double[] convertByteArrayToDoubleArray(String name, - byte bytes[], int start, int length, int byteOrder) - { - int expectedLength = start + length * 8; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - double result[] = new double[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToDouble(name, bytes, start + i * 8, - byteOrder); - } - - return result; - } - - protected void skipBytes(InputStream is, int length) throws IOException - { - this.skipBytes(is, length, "Couldn't skip bytes"); - } - - public final void copyStreamToStream(InputStream is, OutputStream os) - throws IOException - { - byte buffer[] = new byte[1024]; - int read; - while ((read = is.read(buffer)) > 0) - { - os.write(buffer, 0, read); - } - } - - public final byte[] getStreamBytes(InputStream is) throws IOException - { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - copyStreamToStream(is, os); - return os.toByteArray(); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BinaryFileParser.java b/src/main/java/org/apache/sanselan/common/BinaryFileParser.java deleted file mode 100644 index df3fb43..0000000 --- a/src/main/java/org/apache/sanselan/common/BinaryFileParser.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; - -public class BinaryFileParser extends BinaryFileFunctions -{ - public BinaryFileParser(int byteOrder) - { - this.byteOrder = byteOrder; - } - - public BinaryFileParser() - { - - } - - // default byte order for Java, many file formats. - private int byteOrder = BYTE_ORDER_NETWORK; - - // protected boolean BYTE_ORDER_reversed = true; - - protected void setByteOrder(int a, int b) throws ImageReadException, - IOException - { - if (a != b) - throw new ImageReadException("Byte Order bytes don't match (" + a - + ", " + b + ")."); - - if (a == BYTE_ORDER_MOTOROLA) - byteOrder = a; - else if (a == BYTE_ORDER_INTEL) - byteOrder = a; - else - throw new ImageReadException("Unknown Byte Order hint: " + a); - } - - protected void setByteOrder(int byteOrder) - { - this.byteOrder = byteOrder; - } - - protected int getByteOrder() - { - return byteOrder; - } - - protected final int convertByteArrayToInt(String name, int start, - byte bytes[]) - { - return convertByteArrayToInt(name, bytes, start, byteOrder); - } - - protected final int convertByteArrayToInt(String name, byte bytes[]) - { - return convertByteArrayToInt(name, bytes, byteOrder); - } - - public final int convertByteArrayToShort(String name, byte bytes[]) - throws ImageReadException - { - return convertByteArrayToShort(name, bytes, byteOrder); - } - - public final int convertByteArrayToShort(String name, int start, - byte bytes[]) throws ImageReadException - { - return convertByteArrayToShort(name, start, bytes, byteOrder); - } - - public final int read4Bytes(String name, InputStream is, String exception) - throws ImageReadException, IOException - { - return read4Bytes(name, is, exception, byteOrder); - } - - public final int read3Bytes(String name, InputStream is, String exception) - throws ImageReadException, IOException - { - return read3Bytes(name, is, exception, byteOrder); - } - - public final int read2Bytes(String name, InputStream is, String exception) - throws ImageReadException, IOException - { - return read2Bytes(name, is, exception, byteOrder); - } - - public static boolean byteArrayHasPrefix(byte bytes[], byte prefix[]) - { - if ((bytes == null) || (bytes.length < prefix.length)) - return false; - - for (int i = 0; i < prefix.length; i++) - if (bytes[i] != prefix[i]) - return false; - - return true; - } - - protected final byte[] int2ToByteArray(int value) - { - return int2ToByteArray(value, byteOrder); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BinaryInputStream.java b/src/main/java/org/apache/sanselan/common/BinaryInputStream.java deleted file mode 100644 index b507329..0000000 --- a/src/main/java/org/apache/sanselan/common/BinaryInputStream.java +++ /dev/null @@ -1,688 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; - -import org.apache.sanselan.ImageReadException; - -public class BinaryInputStream extends InputStream implements BinaryConstants -{ - protected boolean debug = false; - - public final void setDebug(boolean b) - { - debug = b; - } - - public final boolean getDebug() - { - return debug; - } - - private final InputStream is; - - public BinaryInputStream(byte bytes[], int byteOrder) - { - this.byteOrder = byteOrder; - this.is = new ByteArrayInputStream(bytes); - } - - public BinaryInputStream(InputStream is, int byteOrder) - { - this.byteOrder = byteOrder; - this.is = is; - } - - public BinaryInputStream(InputStream is) - { - this.is = is; - } - - // default byte order for Java, many file formats. - private int byteOrder = BYTE_ORDER_NETWORK; - - protected void setByteOrder(int a, int b) throws ImageReadException, - IOException - { - if (a != b) - throw new ImageReadException("Byte Order bytes don't match (" + a - + ", " + b + ")."); - - if (a == BYTE_ORDER_MOTOROLA) - byteOrder = a; - else if (a == BYTE_ORDER_INTEL) - byteOrder = a; - else - throw new ImageReadException("Unknown Byte Order hint: " + a); - } - - protected void setByteOrder(int byteOrder) - { - this.byteOrder = byteOrder; - } - - protected int getByteOrder() - { - return byteOrder; - } - - public int read() throws IOException - { - return is.read(); - } - - protected final int convertByteArrayToInt(String name, byte bytes[]) - { - return convertByteArrayToInt(name, bytes, byteOrder); - } - - public final int convertByteArrayToShort(String name, byte bytes[]) - { - return convertByteArrayToShort(name, bytes, byteOrder); - } - - public final int convertByteArrayToShort(String name, int start, - byte bytes[]) - { - return convertByteArrayToShort(name, start, bytes, byteOrder); - } - - public final int read4Bytes(String name, String exception) - throws ImageReadException, IOException - { - return read4Bytes(name, exception, byteOrder); - } - - public final int read3Bytes(String name, String exception) - throws ImageReadException, IOException - { - return read3Bytes(name, exception, byteOrder); - } - - public final int read2Bytes(String name, String exception) - throws ImageReadException, IOException - { - return read2Bytes(name, exception, byteOrder); - } - - protected final void readRandomBytes() throws ImageReadException, - IOException - { - - for (int counter = 0; counter < 100; counter++) - { - readByte("" + counter, "Random Data"); - } - } - - public final void debugNumber(String msg, int data) - { - debugNumber(msg, data, 1); - } - - public final void debugNumber(String msg, int data, int bytes) - { - System.out.print(msg + ": " + data + " ("); - int byteData = data; - for (int i = 0; i < bytes; i++) - { - if (i > 0) - System.out.print(","); - int singleByte = 0xff & byteData; - System.out.print((char) singleByte + " [" + singleByte + "]"); - byteData >>= 8; - } - System.out.println(") [0x" + Integer.toHexString(data) + ", " - + Integer.toBinaryString(data) + "]"); - } - - public final void readAndVerifyBytes(byte expected[], String exception) - throws ImageReadException, IOException - { - for (int i = 0; i < expected.length; i++) - { - int data = is.read(); - byte b = (byte) (0xff & data); - - if ((data < 0) || (b != expected[i])) - { - System.out.println("i" + ": " + i); - - this.debugByteArray("expected", expected); - debugNumber("data[" + i + "]", b); - // debugNumber("expected[" + i + "]", expected[i]); - - throw new ImageReadException(exception); - } - } - } - - protected final void readAndVerifyBytes(String name, byte expected[], - String exception) throws ImageReadException, IOException - { - byte bytes[] = readByteArray(name, expected.length, exception); - - for (int i = 0; i < expected.length; i++) - { - if (bytes[i] != expected[i]) - { - System.out.println("i" + ": " + i); - debugNumber("bytes[" + i + "]", bytes[i]); - debugNumber("expected[" + i + "]", expected[i]); - - throw new ImageReadException(exception); - } - } - } - - public final void skipBytes(int length, String exception) - throws IOException - { - long total = 0; - while (length != total) - { - long skipped = is.skip(length - total); - if (skipped < 1) - throw new IOException(exception + " (" + skipped + ")"); - total += skipped; - } - } - - protected final void scanForByte(byte value) throws IOException - { - int count = 0; - for (int i = 0; count < 3; i++) - // while(count<3) - { - int b = is.read(); - if (b < 0) - return; - if ((0xff & b) == value) - { - System.out.println("\t" + i + ": match."); - count++; - } - } - } - - public final byte readByte(String name, String exception) - throws IOException - { - int result = is.read(); - - if ((result < 0)) - { - System.out.println(name + ": " + result); - throw new IOException(exception); - } - - if (debug) - debugNumber(name, result); - - return (byte) (0xff & result); - } - - protected final RationalNumber[] convertByteArrayToRationalArray( - String name, byte bytes[], int start, int length, int byteOrder) - { - int expectedLength = start + length * 8; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - RationalNumber result[] = new RationalNumber[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToRational(name, bytes, start + i * 8, - byteOrder); - } - - return result; - } - - protected final RationalNumber convertByteArrayToRational(String name, - byte bytes[], int byteOrder) - { - return convertByteArrayToRational(name, bytes, 0, byteOrder); - } - - protected final RationalNumber convertByteArrayToRational(String name, - byte bytes[], int start, int byteOrder) - { - int numerator = convertByteArrayToInt(name, bytes, start + 0, 4, - byteOrder); - int divisor = convertByteArrayToInt(name, bytes, start + 4, 4, - byteOrder); - - return new RationalNumber(numerator, divisor); - } - - protected final int convertByteArrayToInt(String name, byte bytes[], - int byteOrder) - { - return convertByteArrayToInt(name, bytes, 0, 4, byteOrder); - } - - protected final int convertByteArrayToInt(String name, byte bytes[], - int start, int length, int byteOrder) - { - byte byte0 = bytes[start + 0]; - byte byte1 = bytes[start + 1]; - byte byte2 = bytes[start + 2]; - byte byte3 = 0; - if (length == 4) - byte3 = bytes[start + 3]; - - // return convert4BytesToInt(name, byte0, byte1, byte2, byte3, - // byteOrder); - - int result; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - result = ((0xff & byte0) << 24) + ((0xff & byte1) << 16) - + ((0xff & byte2) << 8) + ((0xff & byte3) << 0); - // result = (( byte0) << 24) + ((byte1) << 16) - // + (( byte2) << 8) + (( byte3) << 0); - else - // intel, little endian - result = ((0xff & byte3) << 24) + ((0xff & byte2) << 16) - + ((0xff & byte1) << 8) + ((0xff & byte0) << 0); - // result = (( byte3) << 24) + (( byte2) << 16) - // + (( byte1) << 8) + (( byte0) << 0); - - if (debug) - debugNumber(name, result, 4); - - return result; - } - - protected final int[] convertByteArrayToIntArray(String name, byte bytes[], - int start, int length, int byteOrder) - { - int expectedLength = start + length * 4; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToInt(name, bytes, start + i * 4, 4, - byteOrder); - } - - return result; - } - - protected final int convertByteArrayToShort(String name, byte bytes[], - int byteOrder) - { - return convertByteArrayToShort(name, 0, bytes, byteOrder); - } - - protected final int convertByteArrayToShort(String name, int start, - byte bytes[], int byteOrder) - { - byte byte0 = bytes[start + 0]; - byte byte1 = bytes[start + 1]; - - // return convert2BytesToShort(name, byte0, byte1, byteOrder); - - int result; - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - result = ((0xff & byte0) << 8) + ((0xff & byte1) << 0); - else - // intel, little endian - result = ((0xff & byte1) << 8) + ((0xff & byte0) << 0); - - if (debug) - debugNumber(name, result, 2); - - return result; - } - - protected final int[] convertByteArrayToShortArray(String name, - byte bytes[], int start, int length, int byteOrder) - { - int expectedLength = start + length * 2; - - if (bytes.length < expectedLength) - { - System.out.println(name + ": expected length: " + expectedLength - + ", actual length: " + bytes.length); - return null; - } - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - result[i] = convertByteArrayToShort(name, start + i * 2, bytes, - byteOrder); - - // byte byte0 = bytes[start + i * 2]; - // byte byte1 = bytes[start + i * 2 + 1]; - // result[i] = convertBytesToShort(name, byte0, byte1, byteOrder); - } - - return result; - } - - public final byte[] readByteArray(String name, int length, String exception) - throws ImageReadException, IOException - { - byte result[] = new byte[length]; - - int read = 0; - while (read < length) - { - int count = is.read(result, read, length - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - if (debug) - { - for (int i = 0; ((i < length) && (i < 150)); i++) - { - debugNumber(name + " (" + i + ")", 0xff & result[i]); - } - } - return result; - } - - protected final void debugByteArray(String name, byte bytes[]) - { - System.out.println(name + ": " + bytes.length); - - for (int i = 0; ((i < bytes.length) && (i < 50)); i++) - { - debugNumber(name + " (" + i + ")", bytes[i]); - } - } - - protected final void debugNumberArray(String name, int numbers[], int length) - { - System.out.println(name + ": " + numbers.length); - - for (int i = 0; ((i < numbers.length) && (i < 50)); i++) - { - debugNumber(name + " (" + i + ")", numbers[i], length); - } - } - - public final byte[] readBytearray(String name, byte bytes[], int start, - int count) - { - if (bytes.length < (start + count)) - return null; - - byte result[] = new byte[count]; - System.arraycopy(bytes, start, result, 0, count); - - if (debug) - debugByteArray(name, result); - - return result; - } - - public final byte[] readByteArray(int length, String error) - throws ImageReadException, IOException - { - boolean verbose = false; - boolean strict = true; - return readByteArray(length, error, verbose, strict); - } - - public final byte[] readByteArray(int length, String error, - boolean verbose, boolean strict) throws ImageReadException, - IOException - { - byte bytes[] = new byte[length]; - int total = 0; - int read; - while ((read = read(bytes, total, length - total)) > 0) - total += read; - if (total < length) - { - if (strict) - throw new ImageReadException(error); - else if(verbose) - System.out.println(error); - return null; - } - return bytes; - } - - protected final byte[] getBytearrayTail(String name, byte bytes[], int count) - { - return readBytearray(name, bytes, count, bytes.length - count); - } - - protected final byte[] getBytearrayHead(String name, byte bytes[], int count) - { - return readBytearray(name, bytes, 0, bytes.length - count); - } - - public final boolean compareByteArrays(byte a[], int aStart, byte b[], - int bStart, int length) - { - if (a.length < (aStart + length)) - return false; - if (b.length < (bStart + length)) - return false; - - for (int i = 0; i < length; i++) - { - if (a[aStart + i] != b[bStart + i]) - { - debugNumber("a[" + (aStart + i) + "]", a[aStart + i]); - debugNumber("b[" + (bStart + i) + "]", b[bStart + i]); - - return false; - } - } - - return true; - } - - protected final int read4Bytes(String name, String exception, int byteOrder) - throws ImageReadException, IOException - { - int size = 4; - byte bytes[] = new byte[size]; - - int read = 0; - while (read < size) - { - int count = is.read(bytes, read, size - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return convertByteArrayToInt(name, bytes, byteOrder); - } - - protected final int read3Bytes(String name, String exception, int byteOrder) - throws ImageReadException, IOException - { - int size = 3; - byte bytes[] = new byte[size]; - - int read = 0; - while (read < size) - { - int count = is.read(bytes, read, size - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return convertByteArrayToInt(name, bytes, 0, 3, byteOrder); - - } - - protected final int read2Bytes(String name, String exception, int byteOrder) - throws ImageReadException, IOException - { - int size = 2; - byte bytes[] = new byte[size]; - - int read = 0; - while (read < size) - { - int count = is.read(bytes, read, size - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return convertByteArrayToShort(name, bytes, byteOrder); - } - - public final int read1ByteInteger(String exception) - throws ImageReadException, IOException - { - int byte0 = is.read(); - if (byte0 < 0) - throw new ImageReadException(exception); - - return 0xff & byte0; - } - - public final int read2ByteInteger(String exception) - throws ImageReadException, IOException - { - int byte0 = is.read(); - int byte1 = is.read(); - if (byte0 < 0 || byte1 < 0) - throw new ImageReadException(exception); - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - return ((0xff & byte0) << 8) + ((0xff & byte1) << 0); - else - // intel, little endian - return ((0xff & byte1) << 8) + ((0xff & byte0) << 0); - } - - public final int read4ByteInteger(String exception) - throws ImageReadException, IOException - { - int byte0 = is.read(); - int byte1 = is.read(); - int byte2 = is.read(); - int byte3 = is.read(); - if (byte0 < 0 || byte1 < 0 || byte2 < 0 || byte3 < 0) - throw new ImageReadException(exception); - - if (byteOrder == BYTE_ORDER_MOTOROLA) // motorola, big endian - return ((0xff & byte0) << 24) + ((0xff & byte1) << 16) - + ((0xff & byte2) << 8) + ((0xff & byte3) << 0); - else - // intel, little endian - return ((0xff & byte3) << 24) + ((0xff & byte2) << 16) - + ((0xff & byte1) << 8) + ((0xff & byte0) << 0); - } - - protected final void printCharQuad(String msg, int i) - { - System.out.println(msg + ": '" + (char) (0xff & (i >> 24)) - + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) - + (char) (0xff & (i >> 0)) + "'"); - - } - - protected final void printByteBits(String msg, byte i) - { - System.out.println(msg + ": '" + Integer.toBinaryString(0xff & i)); - } - - protected final static int CharsToQuad(char c1, char c2, char c3, char c4) - { - return (((0xff & c1) << 24) | ((0xff & c2) << 16) | ((0xff & c3) << 8) | ((0xff & c4) << 0)); - } - - public final int findNull(byte src[]) - { - return findNull(src, 0); - } - - public final int findNull(byte src[], int start) - { - for (int i = start; i < src.length; i++) - { - if (src[i] == 0) - return i; - - } - return -1; - } - - protected final byte[] getRAFBytes(RandomAccessFile raf, long pos, - int length, String exception) throws IOException - { - byte result[] = new byte[length]; - - if (debug) - { - System.out.println("getRAFBytes pos" + ": " + pos); - System.out.println("getRAFBytes length" + ": " + length); - } - - raf.seek(pos); - - int read = 0; - while (read < length) - { - int count = raf.read(result, read, length - read); - if (count < 1) - throw new IOException(exception); - - read += count; - } - - return result; - - } - - protected void skipBytes(int length) throws IOException - { - skipBytes(length, "Couldn't skip bytes"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BinaryOutputStream.java b/src/main/java/org/apache/sanselan/common/BinaryOutputStream.java deleted file mode 100644 index ae2010e..0000000 --- a/src/main/java/org/apache/sanselan/common/BinaryOutputStream.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.IOException; -import java.io.OutputStream; - -import org.apache.sanselan.ImageWriteException; - -public class BinaryOutputStream extends OutputStream implements BinaryConstants -{ - protected boolean debug = false; - private int count = 0; - - public final void setDebug(boolean b) - { - debug = b; - } - - public final boolean getDebug() - { - return debug; - } - - private final OutputStream os; - - public BinaryOutputStream(OutputStream os, int byteOrder) - { - this.byteOrder = byteOrder; - this.os = os; - } - - public BinaryOutputStream(OutputStream os) - { - this.os = os; - } - - // default byte order for Java, many file formats. - private int byteOrder = BYTE_ORDER_NETWORK; - - protected void setByteOrder(int a, int b) throws ImageWriteException, - IOException - { - if (a != b) - throw new ImageWriteException("Byte Order bytes don't match (" + a - + ", " + b + ")."); - - if (a == BYTE_ORDER_MOTOROLA) - byteOrder = a; - else if (a == BYTE_ORDER_INTEL) - byteOrder = a; - else - throw new ImageWriteException("Unknown Byte Order hint: " + a); - } - - protected void setByteOrder(int byteOrder) - { - this.byteOrder = byteOrder; - } - - public int getByteOrder() - { - return byteOrder; - } - - public void write(int i) throws IOException - { - os.write(i); - count++; - } - - public int getByteCount() - { - return count; - } - - public final void write4Bytes(int value) throws ImageWriteException, - IOException - { - writeNBytes(value, 4); - } - - public final void write3Bytes(int value) throws ImageWriteException, - IOException - { - writeNBytes(value, 3); - } - - public final void write2Bytes(int value) throws ImageWriteException, - IOException - { - writeNBytes(value, 2); - } - - public final void write4ByteInteger(int value) throws ImageWriteException, - IOException - { - if (byteOrder == BYTE_ORDER_MOTOROLA) - { - write(0xff & (value >> 24)); - write(0xff & (value >> 16)); - write(0xff & (value >> 8)); - write(0xff & value); - } else - { - write(0xff & value); - write(0xff & (value >> 8)); - write(0xff & (value >> 16)); - write(0xff & (value >> 24)); - } - } - - public final void write2ByteInteger(int value) throws ImageWriteException, - IOException - { - if (byteOrder == BYTE_ORDER_MOTOROLA) - { - write(0xff & (value >> 8)); - write(0xff & value); - } else - { - write(0xff & value); - write(0xff & (value >> 8)); - } - } - - public final void writeByteArray(byte bytes[]) throws IOException - { - os.write(bytes, 0, bytes.length); - count += bytes.length; - } - - private byte[] convertValueToByteArray(int value, int n) - { - byte result[] = new byte[n]; - - if (byteOrder == BYTE_ORDER_MOTOROLA) - { - for (int i = 0; i < n; i++) - { - int b = 0xff & (value >> (8 * (n - i - 1))); - result[i] = (byte) b; - } - } else - { - for (int i = 0; i < n; i++) - { - int b = 0xff & (value >> (8 * i)); - result[i] = (byte) b; - } - } - - return result; - } - - private final void writeNBytes(int value, int n) - throws ImageWriteException, IOException - { - write(convertValueToByteArray(value, n)); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/BitInputStream.java b/src/main/java/org/apache/sanselan/common/BitInputStream.java deleted file mode 100644 index dbe34fa..0000000 --- a/src/main/java/org/apache/sanselan/common/BitInputStream.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.IOException; -import java.io.InputStream; - -public class BitInputStream extends InputStream implements BinaryConstants -{ - // TODO should be byte order conscious, ie TIFF for reading - // samples size<8 - shuoldn't that effect their order within byte? - private final InputStream is; - - public BitInputStream(InputStream is) - { - this.is = is; - // super(is); - } - - public int read() throws IOException - { - if (cacheBitsRemaining > 0) - throw new IOException("BitInputStream: incomplete bit read"); - return is.read(); - } - - private int cache; - private int cacheBitsRemaining = 0; - private long bytes_read = 0; - - public final int readBits(int count) throws IOException - { - if (count < 8) - { - if (cacheBitsRemaining == 0) - { - // fill cache - cache = is.read(); - cacheBitsRemaining = 8; - bytes_read++; - } - if (count > cacheBitsRemaining) - throw new IOException( - "BitInputStream: can't read bit fields across bytes"); - - // int bits_to_shift = cache_bits_remaining - count; - cacheBitsRemaining -= count; - int bits = cache >> cacheBitsRemaining; - - switch (count) - { - case 1 : - return bits & 1; - case 2 : - return bits & 3; - case 3 : - return bits & 7; - case 4 : - return bits & 15; - case 5 : - return bits & 31; - case 6 : - return bits & 63; - case 7 : - return bits & 127; - } - - } - if (cacheBitsRemaining > 0) - throw new IOException("BitInputStream: incomplete bit read"); - - if (count == 8) - { - bytes_read++; - return is.read(); - } - - if (count == 16) - { - bytes_read += 2; - return (is.read() << 8) | (is.read() << 0); - } - - if (count == 24) - { - bytes_read += 3; - return (is.read() << 16) | (is.read() << 8) | (is.read() << 0); - } - - if (count == 32) - { - bytes_read += 4; - return (is.read() << 24) | (is.read() << 16) | (is.read() << 8) - | (is.read() << 0); - } - - throw new IOException("BitInputStream: unknown error"); - } - - public void flushCache() - { - cacheBitsRemaining = 0; - } - - public long getBytesRead() - { - return bytes_read; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/Compression.java b/src/main/java/org/apache/sanselan/common/Compression.java deleted file mode 100644 index ef7bee5..0000000 --- a/src/main/java/org/apache/sanselan/common/Compression.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.mylzw.MyLZWCompressor; -import org.apache.sanselan.common.mylzw.MyLZWDecompressor; - -public class Compression -{ - - public byte[] decompressLZW(byte compressed[], int LZWMinimumCodeSize, - int expectedSize, int byteOrder) throws IOException - { - InputStream is = new ByteArrayInputStream(compressed); - - MyLZWDecompressor decompressor = new MyLZWDecompressor( - LZWMinimumCodeSize, byteOrder); - byte[] result = decompressor.decompress(is, expectedSize); - - return result; - } - - public byte[] decompressPackBits(byte compressed[], int expectedSize, - int byteOrder) throws ImageReadException, IOException - { - byte unpacked[] = new PackBits().decompress(compressed, expectedSize); - return unpacked; - } - - public byte[] compressLZW(byte src[], int LZWMinimumCodeSize, - int byteOrder, boolean earlyLimit) throws IOException - - { - MyLZWCompressor compressor = new MyLZWCompressor(LZWMinimumCodeSize, - byteOrder, earlyLimit); - - byte compressed[] = compressor.compress(src); - - return compressed; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/ImageMetadata.java b/src/main/java/org/apache/sanselan/common/ImageMetadata.java deleted file mode 100644 index a78c7de..0000000 --- a/src/main/java/org/apache/sanselan/common/ImageMetadata.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.util.ArrayList; - -public class ImageMetadata implements IImageMetadata -{ - - private final ArrayList items = new ArrayList(); - - public void add(String keyword, String text) - { - add(new Item(keyword, text)); - } - - public void add(IImageMetadataItem item) - { - items.add(item); - } - - public ArrayList getItems() - { - return new ArrayList(items); - } - - protected static final String newline = System - .getProperty("line.separator"); - - public String toString() - { - return toString(null); - } - - public String toString(String prefix) - { - if (null == prefix) - prefix = ""; - - StringBuffer result = new StringBuffer(); - for (int i = 0; i < items.size(); i++) - { - if (i > 0) - result.append(newline); - // if (null != prefix) - // result.append(prefix); - - ImageMetadata.IImageMetadataItem item = (ImageMetadata.IImageMetadataItem) items - .get(i); - result.append(item.toString(prefix + "\t")); - - // Debug.debug("prefix", prefix); - // Debug.debug("item", items.get(i)); - // Debug.debug(); - } - return result.toString(); - } - - public static class Item implements IImageMetadataItem - { - private final String keyword, text; - - public Item(String keyword, String text) - { - this.keyword = keyword; - this.text = text; - } - - public String getKeyword() - { - return keyword; - } - - public String getText() - { - return text; - } - - public String toString() - { - return toString(null); - } - - public String toString(String prefix) - { - String result = keyword + ": " + text; - if (null != prefix) - result = prefix + result; - return result; - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/PackBits.java b/src/main/java/org/apache/sanselan/common/PackBits.java deleted file mode 100644 index 8518cf8..0000000 --- a/src/main/java/org/apache/sanselan/common/PackBits.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class PackBits -{ - - public byte[] decompress(byte bytes[], int expected) - throws ImageReadException, IOException - { - int total = 0; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // Loop until you get the number of unpacked bytes you are expecting: - int i = 0; - while (total < expected) - - { - // Read the next source byte into n. - if (i >= bytes.length) - throw new ImageReadException( - "Tiff: Unpack bits source exhausted: " + i - + ", done + " + total + ", expected + " - + expected); - - int n = bytes[i++]; - // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. - if ((n >= 0) && (n <= 127)) - { - - int count = n + 1; - - total += count; - for (int j = 0; j < count; j++) - baos.write(bytes[i++]); - } - // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 - // times. - else if ((n >= -127) && (n <= -1)) - { - int b = bytes[i++]; - int count = -n + 1; - - total += count; - for (int j = 0; j < count; j++) - baos.write(b); - } - else if (n == -128) - throw new ImageReadException("Packbits: " + n); - // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 - // times. - // else - // Else if n is -128, noop. - } - byte result[] = baos.toByteArray(); - - return result; - - } - - private int findNextDuplicate(byte bytes[], int start) - { - // int last = -1; - if (start >= bytes.length) - return -1; - - byte prev = bytes[start]; - - for (int i = start + 1; i < bytes.length; i++) - { - byte b = bytes[i]; - - if (b == prev) - return i - 1; - - prev = b; - } - - return -1; - } - - private int findRunLength(byte bytes[], int start) - { - byte b = bytes[start]; - - int i; - - for (i = start + 1; (i < bytes.length) && (bytes[i] == b); i++) - ; - - return i - start; - } - - public byte[] compress(byte bytes[]) throws IOException - { - MyByteArrayOutputStream baos = new MyByteArrayOutputStream( - bytes.length * 2); // max length 1 extra byte for every 128 - - int ptr = 0; - int count = 0; - while (ptr < bytes.length) - { - count++; - int dup = findNextDuplicate(bytes, ptr); - - if (dup == ptr) // write run length - { - int len = findRunLength(bytes, dup); - int actual_len = Math.min(len, 128); - baos.write(-(actual_len - 1)); - baos.write(bytes[ptr]); - ptr += actual_len; - } - else - { // write literals - int len = dup - ptr; - - if (dup > 0) - { - int runlen = findRunLength(bytes, dup); - if (runlen < 3) // may want to discard next run. - { - int nextptr = ptr + len + runlen; - int nextdup = findNextDuplicate(bytes, nextptr); - if (nextdup != nextptr) // discard 2-byte run - { - dup = nextdup; - len = dup - ptr; - } - } - } - - if (dup < 0) - len = bytes.length - ptr; - int actual_len = Math.min(len, 128); - - baos.write(actual_len - 1); - for (int i = 0; i < actual_len; i++) - { - baos.write(bytes[ptr]); - ptr++; - } - } - } - byte result[] = baos.toByteArray(); - - return result; - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/RationalNumber.java b/src/main/java/org/apache/sanselan/common/RationalNumber.java deleted file mode 100644 index 8207478..0000000 --- a/src/main/java/org/apache/sanselan/common/RationalNumber.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -import java.text.DecimalFormat; -import java.text.NumberFormat; - -public class RationalNumber extends Number -{ - private static final long serialVersionUID = -1; - - public final int numerator; - public final int divisor; - - public RationalNumber(int numerator, int divisor) - { - this.numerator = numerator; - this.divisor = divisor; - } - - public static final RationalNumber factoryMethod(long n, long d) - { - // safer than constructor - handles values outside min/max range. - // also does some simple finding of common denominators. - - if (n > Integer.MAX_VALUE || n < Integer.MIN_VALUE - || d > Integer.MAX_VALUE || d < Integer.MIN_VALUE) - { - while ((n > Integer.MAX_VALUE || n < Integer.MIN_VALUE - || d > Integer.MAX_VALUE || d < Integer.MIN_VALUE) - && (Math.abs(n) > 1) && (Math.abs(d) > 1)) - { - // brutal, inprecise truncation =( - // use the sign-preserving right shift operator. - n >>= 1; - d >>= 1; - } - - if (d == 0) - throw new NumberFormatException("Invalid value, numerator: " - + n + ", divisor: " + d); - } - - long gcd = gcd(n, d); - d = d / gcd; - n = n / gcd; - - return new RationalNumber((int) n, (int) d); - } - - /** - * Return the greatest common divisor - */ - private static long gcd(long a, long b) - { - - if (b == 0) - return a; - else - return gcd(b, a % b); - } - - public RationalNumber negate() - { - return new RationalNumber(-numerator, divisor); - } - - public double doubleValue() - { - return (double) numerator / (double) divisor; - } - - public float floatValue() - { - return (float) numerator / (float) divisor; - } - - public int intValue() - { - return numerator / divisor; - } - - public long longValue() - { - return (long) numerator / (long) divisor; - } - - public boolean isValid() - { - return divisor != 0; - } - - private static final NumberFormat nf = DecimalFormat.getInstance(); - - public String toString() - { - if (divisor == 0) - return "Invalid rational (" + numerator + "/" + divisor + ")"; - if ((numerator % divisor) == 0) - return nf.format(numerator / divisor); - return numerator + "/" + divisor + " (" - + nf.format((double) numerator / divisor) + ")"; - } - - public String toDisplayString() - { - if ((numerator % divisor) == 0) - return "" + (numerator / divisor); - NumberFormat nf = DecimalFormat.getInstance(); - nf.setMaximumFractionDigits(3); - return nf.format((double) numerator / (double) divisor); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/RationalNumberUtilities.java b/src/main/java/org/apache/sanselan/common/RationalNumberUtilities.java deleted file mode 100644 index ebd0409..0000000 --- a/src/main/java/org/apache/sanselan/common/RationalNumberUtilities.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common; - -public abstract class RationalNumberUtilities extends Number -{ - - private static class Option - { - public final RationalNumber rationalNumber; - public final double error; - - private Option(final RationalNumber rationalNumber, final double error) - { - this.rationalNumber = rationalNumber; - this.error = error; - } - - public static final Option factory(final RationalNumber rationalNumber, - final double value) - { - return new Option(rationalNumber, Math.abs(rationalNumber - .doubleValue() - - value)); - } - - public String toString() - { - return rationalNumber.toString(); - } - } - - // int-precision tolerance - private static final double TOLERANCE = 1E-8; - - // - // calculate rational number using successive approximations. - // - public static final RationalNumber getRationalNumber(double value) - { - if (value >= Integer.MAX_VALUE) - return new RationalNumber(Integer.MAX_VALUE, 1); - else if (value <= -Integer.MAX_VALUE) - return new RationalNumber(-Integer.MAX_VALUE, 1); - - boolean negative = false; - if (value < 0) - { - negative = true; - value = Math.abs(value); - } - - Option low; - Option high; - { - RationalNumber l, h; - - if (value == 0) - return new RationalNumber(0, 1); - else if (value >= 1) - { - int approx = (int) value; - if (approx < value) - { - l = new RationalNumber(approx, 1); - h = new RationalNumber(approx + 1, 1); - } - else - { - l = new RationalNumber(approx - 1, 1); - h = new RationalNumber(approx, 1); - } - } - else - { - int approx = (int) (1.0 / value); - if ((1.0 / approx) < value) - { - l = new RationalNumber(1, approx); - h = new RationalNumber(1, approx - 1); - } - else - { - l = new RationalNumber(1, approx + 1); - h = new RationalNumber(1, approx); - } - } - low = Option.factory(l, value); - high = Option.factory(h, value); - } - - Option bestOption = (low.error < high.error) ? low : high; - - final int MAX_ITERATIONS = 100; // value is quite high, actually. shouldn't matter. - for (int count = 0; bestOption.error > TOLERANCE - && count < MAX_ITERATIONS; count++) - { - // Debug.debug("bestOption: " + bestOption + ", left: " + low - // + ", right: " + high + ", value: " + value + ", error: " - // + bestOption.error); - - RationalNumber mediant = RationalNumber.factoryMethod( - (long) low.rationalNumber.numerator - + (long) high.rationalNumber.numerator, - (long) low.rationalNumber.divisor - + (long) high.rationalNumber.divisor); - Option mediantOption = Option.factory(mediant, value); - - if (value < mediant.doubleValue()) - { - if (high.error <= mediantOption.error) - break; - - high = mediantOption; - } - else - { - if (low.error <= mediantOption.error) - break; - - low = mediantOption; - } - - if (mediantOption.error < bestOption.error) - bestOption = mediantOption; - } - - return negative - ? bestOption.rationalNumber.negate() - : bestOption.rationalNumber; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceInputStream.java b/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceInputStream.java deleted file mode 100644 index 69b31d1..0000000 --- a/src/main/java/org/apache/sanselan/common/byteSources/ByteSourceInputStream.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.byteSources; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -public class ByteSourceInputStream extends ByteSource -{ - private final InputStream is; - private CacheBlock cacheHead = null; - private static final int BLOCK_SIZE = 1024; - - public ByteSourceInputStream(InputStream is, String filename) - { - super(filename); - this.is = new BufferedInputStream(is); - } - - private class CacheBlock - { - public final byte bytes[]; - private CacheBlock next = null; - private boolean triedNext = false; - - public CacheBlock(final byte[] bytes) - { - this.bytes = bytes; - } - - public CacheBlock getNext() throws IOException - { - if (null != next) - return next; - if (triedNext) - return null; - triedNext = true; - next = readBlock(); - return next; - } - - } - - private byte readBuffer[] = null; - - private CacheBlock readBlock() throws IOException - { - if (null == readBuffer) - readBuffer = new byte[BLOCK_SIZE]; - - int read = is.read(readBuffer); - if (read < 1) - return null; - else if (read < BLOCK_SIZE) - { - // return a copy. - byte result[] = new byte[read]; - System.arraycopy(readBuffer, 0, result, 0, read); - return new CacheBlock(result); - } - else - { - // return current buffer. - byte result[] = readBuffer; - readBuffer = null; - return new CacheBlock(result); - } - } - - private CacheBlock getFirstBlock() throws IOException - { - if (null == cacheHead) - cacheHead = readBlock(); - return cacheHead; - } - - private class CacheReadingInputStream extends InputStream - { - private CacheBlock block = null; - private boolean readFirst = false; - private int blockIndex = 0; - - public int read() throws IOException - { - if (null == block) - { - if (readFirst) - return -1; - block = getFirstBlock(); - readFirst = true; - } - - if (block != null && blockIndex >= block.bytes.length) - { - block = block.getNext(); - blockIndex = 0; - } - - if (null == block) - return -1; - - if (blockIndex >= block.bytes.length) - return -1; - - return 0xff & block.bytes[blockIndex++]; - } - - public int read(byte b[], int off, int len) throws IOException - { - // first section copied verbatim from InputStream - if (b == null) - throw new NullPointerException(); - else if ((off < 0) || (off > b.length) || (len < 0) - || ((off + len) > b.length) || ((off + len) < 0)) - throw new IndexOutOfBoundsException(); - else if (len == 0) - return 0; - - // optimized block read - - if (null == block) - { - if (readFirst) - return -1; - block = getFirstBlock(); - readFirst = true; - } - - if (block != null && blockIndex >= block.bytes.length) - { - block = block.getNext(); - blockIndex = 0; - } - - if (null == block) - return -1; - - if (blockIndex >= block.bytes.length) - return -1; - - int readSize = Math.min(len, block.bytes.length - blockIndex); - System.arraycopy(block.bytes, blockIndex, b, off, readSize); - blockIndex += readSize; - return readSize; - } - - } - - public InputStream getInputStream() throws IOException - { - return new CacheReadingInputStream(); - } - - public byte[] getBlock(int blockStart, int blockLength) throws IOException - { - // We include a separate check for int overflow. - if ((blockStart < 0) - || (blockLength < 0) - || (blockStart + blockLength < 0) - || (blockStart + blockLength > streamLength.longValue())) { - throw new IOException("Could not read block (block start: " + blockStart - + ", block length: " + blockLength + ", data length: " - + streamLength + ")."); - } - - InputStream is = getInputStream(); - is.skip(blockStart); - - byte bytes[] = new byte[blockLength]; - int total = 0; - while (true) - { - int read = is.read(bytes, total, bytes.length - total); - if (read < 1) - throw new IOException("Could not read block."); - total += read; - if (total >= blockLength) - return bytes; - } - } - - private Long streamLength = null; - - public long getLength() throws IOException - { - if (streamLength != null) - return streamLength.longValue(); - - InputStream is = getInputStream(); - long result = 0; - long skipped; - while ((skipped = is.skip(1024)) > 0) - result += skipped; - streamLength = new Long(result); - return result; - } - - public byte[] getAll() throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - CacheBlock block = getFirstBlock(); - while (block != null) - { - baos.write(block.bytes); - block = block.getNext(); - } - return baos.toByteArray(); - } - - public String getDescription() - { - return "Inputstream: '" + filename + "'"; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/mylzw/MyBitInputStream.java b/src/main/java/org/apache/sanselan/common/mylzw/MyBitInputStream.java deleted file mode 100644 index 342605f..0000000 --- a/src/main/java/org/apache/sanselan/common/mylzw/MyBitInputStream.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.mylzw; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.common.BinaryConstants; - -public class MyBitInputStream extends InputStream implements BinaryConstants -{ - private final InputStream is; - private final int byteOrder; - private boolean tiffLZWMode = false; - - public MyBitInputStream(InputStream is, int byteOrder) - { - this.byteOrder = byteOrder; - this.is = is; - } - - public int read() throws IOException - { - return readBits(8); - } - - private long bytesRead = 0; - private int bitsInCache = 0; - private int bitCache = 0; - - public void setTiffLZWMode() - { - tiffLZWMode = true; - } - - public int readBits(int SampleBits) throws IOException - { - while (bitsInCache < SampleBits) - { - int next = is.read(); - - if (next < 0) - { - if (tiffLZWMode) - { - // pernicious special case! - return 257; - } - return -1; - } - - int newByte = (0xff & next); - - if (byteOrder == BYTE_ORDER_NETWORK) // MSB, so add to right - bitCache = (bitCache << 8) | newByte; - else if (byteOrder == BYTE_ORDER_INTEL) // LSB, so add to left - bitCache = (newByte << bitsInCache) | bitCache; - else - throw new IOException("Unknown byte order: " + byteOrder); - - bytesRead++; - bitsInCache += 8; - } - int sampleMask = (1 << SampleBits) - 1; - - int sample; - - if (byteOrder == BYTE_ORDER_NETWORK) // MSB, so read from left - { - sample = sampleMask & (bitCache >> (bitsInCache - SampleBits)); - } - else if (byteOrder == BYTE_ORDER_INTEL) // LSB, so read from right - { - sample = sampleMask & bitCache; - bitCache >>= SampleBits; - } - else - throw new IOException("Unknown byte order: " + byteOrder); - - int result = sample; - - bitsInCache -= SampleBits; - int remainderMask = (1 << bitsInCache) - 1; - bitCache &= remainderMask; - - return result; - } - - public void flushCache() - { - bitsInCache = 0; - bitCache = 0; - } - - public long getBytesRead() - { - return bytesRead; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/mylzw/MyBitOutputStream.java b/src/main/java/org/apache/sanselan/common/mylzw/MyBitOutputStream.java deleted file mode 100644 index 8d56f8f..0000000 --- a/src/main/java/org/apache/sanselan/common/mylzw/MyBitOutputStream.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.mylzw; - -import java.io.IOException; -import java.io.OutputStream; - -import org.apache.sanselan.common.BinaryConstants; - -public class MyBitOutputStream extends OutputStream implements BinaryConstants -{ - private final OutputStream os; - private final int byteOrder; - - public MyBitOutputStream(OutputStream os, int byteOrder) - { - this.byteOrder = byteOrder; - this.os = os; - } - - public void write(int value) throws IOException - { - writeBits(value, 8); - } - - private int bitsInCache = 0; - private int bitCache = 0; - - // TODO: in and out streams CANNOT accurately read/write 32bits at a time, - // as int will overflow. should have used a long - public void writeBits(int value, int SampleBits) throws IOException - { - int sampleMask = (1 << SampleBits) - 1; - value &= sampleMask; - - if (byteOrder == BYTE_ORDER_NETWORK) // MSB, so add to right - { - bitCache = (bitCache << SampleBits) | value; - } - else if (byteOrder == BYTE_ORDER_INTEL) // LSB, so add to left - { - bitCache = bitCache | (value << bitsInCache); - } - else - throw new IOException("Unknown byte order: " + byteOrder); - bitsInCache += SampleBits; - - while (bitsInCache >= 8) - { - if (byteOrder == BYTE_ORDER_NETWORK) // MSB, so write from left - { - int b = 0xff & (bitCache >> (bitsInCache - 8)); - actualWrite(b); - - bitsInCache -= 8; - } - else if (byteOrder == BYTE_ORDER_INTEL) // LSB, so write from right - { - int b = 0xff & bitCache; - actualWrite(b); - - bitCache >>= 8; - bitsInCache -= 8; - } - int remainderMask = (1 << bitsInCache) - 1; // unneccesary - bitCache &= remainderMask; // unneccesary - } - - } - - private int bytesWritten = 0; - - private void actualWrite(int value) throws IOException - { - os.write(value); - bytesWritten++; - } - - public void flushCache() throws IOException - { - if (bitsInCache > 0) - { - int bitMask = (1 << bitsInCache) - 1; - int b = bitMask & bitCache; - - if (byteOrder == BYTE_ORDER_NETWORK) // MSB, so write from left - { - b <<= 8 - bitsInCache; // left align fragment. - os.write(b); - } - else if (byteOrder == BYTE_ORDER_INTEL) // LSB, so write from right - { - os.write(b); - } - } - - bitsInCache = 0; - bitCache = 0; - } - - public int getBytesWritten() - { - return bytesWritten + ((bitsInCache > 0) ? 1 : 0); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/common/mylzw/MyLZWCompressor.java b/src/main/java/org/apache/sanselan/common/mylzw/MyLZWCompressor.java deleted file mode 100644 index 5dc2f8c..0000000 --- a/src/main/java/org/apache/sanselan/common/mylzw/MyLZWCompressor.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.common.mylzw; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class MyLZWCompressor -{ - - // private static final int MAX_TABLE_SIZE = 1 << 12; - - private int codeSize; - private final int initialCodeSize; - private int codes = -1; - - private final int byteOrder; - private final boolean earlyLimit; - private final int clearCode; - private final int eoiCode; - private final Listener listener; - - public MyLZWCompressor(int initialCodeSize, int byteOrder, - boolean earlyLimit) - { - this(initialCodeSize, byteOrder, earlyLimit, null); - } - - public MyLZWCompressor(int initialCodeSize, int byteOrder, - boolean earlyLimit, Listener listener) - { - this.listener = listener; - this.byteOrder = byteOrder; - this.earlyLimit = earlyLimit; - - this.initialCodeSize = initialCodeSize; - - clearCode = 1 << initialCodeSize; - eoiCode = clearCode + 1; - - if (null != listener) - listener.init(clearCode, eoiCode); - - InitializeStringTable(); - } - - private final Map map = new HashMap(); - - private final void InitializeStringTable() - { - codeSize = initialCodeSize; - - int intial_entries_count = (1 << codeSize) + 2; - - map.clear(); - for (codes = 0; codes < intial_entries_count; codes++) - { - if ((codes != clearCode) && (codes != eoiCode)) - { - Object key = arrayToKey((byte) codes); - - map.put(key, new Integer(codes)); - } - } - } - - private final void clearTable() - { - InitializeStringTable(); - incrementCodeSize(); - } - - private final void incrementCodeSize() - { - if (codeSize != 12) - codeSize++; - } - - private final Object arrayToKey(byte b) - { - return arrayToKey(new byte[] { b, }, 0, 1); - } - - private final static class ByteArray - { - private final byte bytes[]; - private final int start; - private final int length; - private final int hash; - - public ByteArray(byte bytes[]) - { - this(bytes, 0, bytes.length); - } - - public ByteArray(byte bytes[], int start, int length) - { - this.bytes = bytes; - this.start = start; - this.length = length; - - int tempHash = length; - - for (int i = 0; i < length; i++) - { - int b = 0xff & bytes[i + start]; - tempHash = tempHash + (tempHash << 8) ^ b ^ i; - } - - hash = tempHash; - } - - public final int hashCode() - { - return hash; - } - - public final boolean equals(Object o) - { - ByteArray other = (ByteArray) o; - if (other.hash != hash) - return false; - if (other.length != length) - return false; - - for (int i = 0; i < length; i++) - { - if (other.bytes[i + other.start] != bytes[i + start]) - return false; - } - - return true; - } - } - - private final Object arrayToKey(byte bytes[], int start, int length) - { - return new ByteArray(bytes, start, length); - } - - private final void writeDataCode(MyBitOutputStream bos, int code) - throws IOException - { - if (null != listener) - listener.dataCode(code); - writeCode(bos, code); - } - - - private final void writeClearCode(MyBitOutputStream bos) throws IOException - { - if (null != listener) - listener.dataCode(clearCode); - writeCode(bos, clearCode); - } - - private final void writeEoiCode(MyBitOutputStream bos) throws IOException - { - if (null != listener) - listener.eoiCode(eoiCode); - writeCode(bos, eoiCode); - } - - private final void writeCode(MyBitOutputStream bos, int code) - throws IOException - { - bos.writeBits(code, codeSize); - } - - private final boolean isInTable(byte bytes[], int start, int length) - { - Object key = arrayToKey(bytes, start, length); - - return map.containsKey(key); - } - - private final int codeFromString(byte bytes[], int start, int length) - throws IOException - { - Object key = arrayToKey(bytes, start, length); - Object o = map.get(key); - if (o == null) - throw new IOException("CodeFromString"); - return ((Integer) o).intValue(); - } - - private final boolean addTableEntry(MyBitOutputStream bos, byte bytes[], - int start, int length) throws IOException - { - Object key = arrayToKey(bytes, start, length); - return addTableEntry(bos, key); - } - - private final boolean addTableEntry(MyBitOutputStream bos, Object key) - throws IOException - { - boolean cleared = false; - - { - int limit = (1 << codeSize); - if (earlyLimit) - limit--; - - if (codes == limit) - { - if (codeSize < 12) - incrementCodeSize(); - else - { - writeClearCode(bos); - clearTable(); - cleared = true; - } - } - } - - if (!cleared) - { - map.put(key, new Integer(codes)); - codes++; - } - - return cleared; - } - - public static interface Listener - { - public void dataCode(int code); - - public void eoiCode(int code); - - public void clearCode(int code); - - public void init(int clearCode, int eoiCode); - } - - public byte[] compress(byte bytes[]) throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length); - MyBitOutputStream bos = new MyBitOutputStream(baos, byteOrder); - - InitializeStringTable(); - clearTable(); - writeClearCode(bos); - boolean cleared = false; - - int w_start = 0; - int w_length = 0; - - for (int i = 0; i < bytes.length; i++) - { - if (isInTable(bytes, w_start, w_length + 1)) - { - w_length++; - - cleared = false; - } else - { - int code = codeFromString(bytes, w_start, w_length); - writeDataCode(bos, code); - cleared = addTableEntry(bos, bytes, w_start, w_length + 1); - - w_start = i; - w_length = 1; - } - } /* end of for loop */ - - int code = codeFromString(bytes, w_start, w_length); - writeDataCode(bos, code); - - writeEoiCode(bos); - - bos.flushCache(); - - return baos.toByteArray(); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/BmpHeaderInfo.java b/src/main/java/org/apache/sanselan/formats/bmp/BmpHeaderInfo.java deleted file mode 100644 index 77c797b..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/BmpHeaderInfo.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp; - -public class BmpHeaderInfo -{ - // BM - Windows 3.1x, 95, NT, - // BA - OS/2 Bitmap Array - // CI - OS/2 Color Icon - // CP - OS/2 Color Pointer - // IC - OS/2 Icon - // PT - OS/2 Pointer - public final byte identifier1; - public final byte identifier2; - - public final int fileSize; - public final int reserved; - public final int bitmapDataOffset; - - public final int bitmapHeaderSize; - public final int width; - public final int height; - public final int planes; - public final int bitsPerPixel; - public final int compression; - public final int bitmapDataSize; - public final int hResolution; - public final int vResolution; - public final int colorsUsed; - public final int colorsImportant; - - public BmpHeaderInfo(byte identifier1, byte identifier2, int fileSize, - int reserved, int bitmapDataOffset, - int bitmapHeaderSize, int width, int height, int planes, - int bitsPerPixel, int compression, int bitmapDataSize, - int hResolution, int vResolution, int colorsUsed, - int colorsImportant) - { - this.identifier1 = identifier1; - this.identifier2 = identifier2; - this.fileSize = fileSize; - this.reserved = reserved; - this.bitmapDataOffset = bitmapDataOffset; - - this.bitmapHeaderSize = bitmapHeaderSize; - this.width = width; - this.height = height; - this.planes = planes; - this.bitsPerPixel = bitsPerPixel; - this.compression = compression; - this.bitmapDataSize = bitmapDataSize; - this.hResolution = hResolution; - this.vResolution = vResolution; - this.colorsUsed = colorsUsed; - this.colorsImportant = colorsImportant; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/BmpImageParser.java b/src/main/java/org/apache/sanselan/formats/bmp/BmpImageParser.java deleted file mode 100644 index ef0b5e0..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/BmpImageParser.java +++ /dev/null @@ -1,723 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.FormatCompliance; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.bmp.pixelparsers.PixelParser; -import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserBitFields; -import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserRgb; -import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserRle; -import org.apache.sanselan.formats.bmp.writers.BMPWriter; -import org.apache.sanselan.formats.bmp.writers.BMPWriterPalette; -import org.apache.sanselan.formats.bmp.writers.BMPWriterRGB; -import org.apache.sanselan.palette.PaletteFactory; -import org.apache.sanselan.palette.SimplePalette; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class BmpImageParser extends ImageParser -{ - - public BmpImageParser() - { - super.setByteOrder(BYTE_ORDER_INTEL); - } - - public String getName() - { - return "Bmp-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".bmp"; - - private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_BMP, // - }; - } - - private static final byte BMP_HEADER_SIGNATURE[] = { 0x42, 0x4d, }; - - private BmpHeaderInfo readBmpHeaderInfo(InputStream is, - FormatCompliance formatCompliance, boolean verbose) - throws ImageReadException, IOException - { - byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File"); - byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File"); - - if (formatCompliance != null) - { - formatCompliance.compare_bytes("Signature", BMP_HEADER_SIGNATURE, - new byte[] { identifier1, identifier2, }); - } - - int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File"); - int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File"); - int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, - "Not a Valid BMP File"); - - int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, - "Not a Valid BMP File"); - int width = read4Bytes("Width", is, "Not a Valid BMP File"); - int height = read4Bytes("Height", is, "Not a Valid BMP File"); - int planes = read2Bytes("Planes", is, "Not a Valid BMP File"); - int bitsPerPixel = read2Bytes("Bits Per Pixel", is, - "Not a Valid BMP File"); - int compression = read4Bytes("Compression", is, "Not a Valid BMP File"); - int bitmapDataSize = read4Bytes("Bitmap Data Size", is, - "Not a Valid BMP File"); - int hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File"); - int vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File"); - int colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File"); - int colorsImportant = read4Bytes("ColorsImportant", is, - "Not a Valid BMP File"); - - if (verbose) - { - this.debugNumber("identifier1", identifier1, 1); - this.debugNumber("identifier2", identifier2, 1); - this.debugNumber("fileSize", fileSize, 4); - this.debugNumber("reserved", reserved, 4); - this.debugNumber("bitmapDataOffset", bitmapDataOffset, 4); - this.debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4); - this.debugNumber("width", width, 4); - this.debugNumber("height", height, 4); - this.debugNumber("planes", planes, 2); - this.debugNumber("bitsPerPixel", bitsPerPixel, 2); - this.debugNumber("compression", compression, 4); - this.debugNumber("bitmapDataSize", bitmapDataSize, 4); - this.debugNumber("hResolution", hResolution, 4); - this.debugNumber("vResolution", vResolution, 4); - this.debugNumber("colorsUsed", colorsUsed, 4); - this.debugNumber("colorsImportant", colorsImportant, 4); - } - - BmpHeaderInfo result = new BmpHeaderInfo(identifier1, identifier2, - fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width, - height, planes, bitsPerPixel, compression, bitmapDataSize, - hResolution, vResolution, colorsUsed, colorsImportant); - return result; - } - - private static final int BI_RGB = 0; - private static final int BI_RLE4 = 2; - private static final int BI_RLE8 = 1; - private static final int BI_BITFIELDS = 3; - - private byte[] getRLEBytes(InputStream is, int RLESamplesPerByte) - throws ImageReadException, IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // this.setDebug(true); - - boolean done = false; - while (!done) - { - int a = 0xff & this.readByte("RLE a", is, "BMP: Bad RLE"); - baos.write(a); - int b = 0xff & this.readByte("RLE b", is, "BMP: Bad RLE"); - baos.write(b); - - if (a == 0) - { - switch (b) - { - case 0: // EOL - break; - case 1: // EOF - // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - // ); - done = true; - break; - case 2: { - // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - // ); - int c = 0xff & this.readByte("RLE c", is, "BMP: Bad RLE"); - baos.write(c); - int d = 0xff & this.readByte("RLE d", is, "BMP: Bad RLE"); - baos.write(d); - - } - break; - default: { - int size = b / RLESamplesPerByte; - if ((b % RLESamplesPerByte) > 0) - size++; - if ((size % 2) != 0) - size++; - - // System.out.println("b: " + b); - // System.out.println("size: " + size); - // System.out.println("RLESamplesPerByte: " + - // RLESamplesPerByte); - // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - // ); - byte bytes[] = this.readByteArray("bytes", size, is, - "RLE: Absolute Mode"); - baos.write(bytes); - } - break; - } - } - } - - return baos.toByteArray(); - } - - private ImageContents readImageContents(InputStream is, - FormatCompliance formatCompliance, boolean verbose) - throws ImageReadException, IOException - { - BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance, verbose); - - int colorTableSize = bhi.colorsUsed; - if (colorTableSize == 0) - colorTableSize = (1 << bhi.bitsPerPixel); - - if (verbose) - { - this.debugNumber("ColorsUsed", bhi.colorsUsed, 4); - this.debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4); - this.debugNumber("ColorTableSize", colorTableSize, 4); - this.debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4); - this.debugNumber("Compression", bhi.compression, 4); - } - - int paletteLength; - int rleSamplesPerByte = 0; - boolean rle = false; - - switch (bhi.compression) - { - case BI_RGB: - if (verbose) - System.out.println("Compression: BI_RGB"); - if (bhi.bitsPerPixel <= 8) - paletteLength = 4 * colorTableSize; - else - paletteLength = 0; - // BytesPerPaletteEntry = 0; - // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel); - // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel - // <= 16)); - break; - - case BI_RLE4: - if (verbose) - System.out.println("Compression: BI_RLE4"); - paletteLength = 4 * colorTableSize; - rleSamplesPerByte = 2; - // ExtraBitsPerPixel = 4; - rle = true; - // // BytesPerPixel = 2; - // // BytesPerPaletteEntry = 0; - break; - // - case BI_RLE8: - if (verbose) - System.out.println("Compression: BI_RLE8"); - paletteLength = 4 * colorTableSize; - rleSamplesPerByte = 1; - // ExtraBitsPerPixel = 8; - rle = true; - // BytesPerPixel = 2; - // BytesPerPaletteEntry = 0; - break; - // - case BI_BITFIELDS: - if (verbose) - System.out.println("Compression: BI_BITFIELDS"); - paletteLength = 3 * 4; // TODO: is this right? are the masks always - // LONGs? - // BytesPerPixel = 2; - // BytesPerPaletteEntry = 4; - break; - - default: - throw new ImageReadException("BMP: Unknown Compression: " - + bhi.compression); - } - - byte colorTable[] = null; - if (paletteLength > 0) - colorTable = this.readByteArray("ColorTable", paletteLength, is, - "Not a Valid BMP File"); - - if (verbose) - { - this.debugNumber("paletteLength", paletteLength, 4); - System.out.println("ColorTable: " - + ((colorTable == null) ? "null" : "" + colorTable.length)); - } - - int pixelCount = bhi.width * bhi.height; - - int imageLineLength = ((((bhi.bitsPerPixel) * bhi.width) + 7) / 8); - - if (verbose) - { - // this.debugNumber("Total BitsPerPixel", - // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4); - // this.debugNumber("Total Bit Per Line", - // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4); - // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4); - this.debugNumber("bhi.Width", bhi.width, 4); - this.debugNumber("bhi.Height", bhi.height, 4); - this.debugNumber("ImageLineLength", imageLineLength, 4); - // this.debugNumber("imageDataSize", imageDataSize, 4); - this.debugNumber("PixelCount", pixelCount, 4); - } - // int ImageLineLength = BytesPerPixel * bhi.Width; - while ((imageLineLength % 4) != 0) - imageLineLength++; - - final int headerSize = BITMAP_FILE_HEADER_SIZE - + BITMAP_INFO_HEADER_SIZE; - int expectedDataOffset = headerSize + paletteLength; - - if (verbose) - { - this.debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4); - this.debugNumber("expectedDataOffset", expectedDataOffset, 4); - } - int extraBytes = bhi.bitmapDataOffset - expectedDataOffset; - if (extraBytes < 0) - throw new ImageReadException("BMP has invalid image data offset: " - + bhi.bitmapDataOffset + " (expected: " - + expectedDataOffset + ", paletteLength: " + paletteLength - + ", headerSize: " + headerSize + ")"); - else if (extraBytes > 0) - this.readByteArray("BitmapDataOffset", extraBytes, is, - "Not a Valid BMP File"); - - int imageDataSize = bhi.height * imageLineLength; - - if (verbose) - this.debugNumber("imageDataSize", imageDataSize, 4); - - byte imageData[]; - if (rle) - imageData = getRLEBytes(is, rleSamplesPerByte); - else - imageData = this.readByteArray("ImageData", imageDataSize, is, - "Not a Valid BMP File"); - - if (verbose) - this.debugNumber("ImageData.length", imageData.length, 4); - - PixelParser pixelParser; - - switch (bhi.compression) - { - case BI_RLE4: - case BI_RLE8: - pixelParser = new PixelParserRle(bhi, colorTable, imageData); - break; - case BI_RGB: - pixelParser = new PixelParserRgb(bhi, colorTable, imageData); - break; - case BI_BITFIELDS: - pixelParser = new PixelParserBitFields(bhi, colorTable, imageData); - break; - default: - throw new ImageReadException("BMP: Unknown Compression: " - + bhi.compression); - } - - return new ImageContents(bhi, colorTable, imageData, pixelParser); - } - - private BmpHeaderInfo readBmpHeaderInfo(ByteSource byteSource, - boolean verbose) throws ImageReadException, IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - - // readSignature(is); - return readBmpHeaderInfo(is, null, verbose); - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageReadException("Unknown parameter: " + firstKey); - } - - BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource, verbose); - - if (bhi == null) - throw new ImageReadException("BMP: couldn't read header"); - - return new Dimension(bhi.width, bhi.height); - - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - private String getBmpTypeDescription(int Identifier1, int Identifier2) - { - if ((Identifier1 == 'B') && (Identifier2 == 'M')) - return "Windows 3.1x, 95, NT,"; - if ((Identifier1 == 'B') && (Identifier2 == 'A')) - return "OS/2 Bitmap Array"; - if ((Identifier1 == 'C') && (Identifier2 == 'I')) - return "OS/2 Color Icon"; - if ((Identifier1 == 'C') && (Identifier2 == 'P')) - return "OS/2 Color Pointer"; - if ((Identifier1 == 'I') && (Identifier2 == 'C')) - return "OS/2 Icon"; - if ((Identifier1 == 'P') && (Identifier2 == 'T')) - return "OS/2 Pointer"; - - return "Unknown"; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageReadException("Unknown parameter: " + firstKey); - } - - ImageContents ic = readImageContents(byteSource.getInputStream(), - FormatCompliance.getDefault(), verbose); - - if (ic == null) - throw new ImageReadException("Couldn't read BMP Data"); - - BmpHeaderInfo bhi = ic.bhi; - byte colorTable[] = ic.colorTable; - - if (bhi == null) - throw new ImageReadException("BMP: couldn't read header"); - - int height = bhi.height; - int width = bhi.width; - - ArrayList comments = new ArrayList(); - // TODO: comments... - - int bitsPerPixel = bhi.bitsPerPixel; - ImageFormat format = ImageFormat.IMAGE_FORMAT_BMP; - String name = "BMP Windows Bitmap"; - String mimeType = "image/x-ms-bmp"; - // we ought to count images, but don't yet. - int numberOfImages = -1; - // not accurate ... only reflects first - boolean isProgressive = false; - // boolean isProgressive = (fPNGChunkIHDR.InterlaceMethod != 0); - // - // pixels per meter - int physicalWidthDpi = (int) (bhi.hResolution * 1000.0 / 2.54); - float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); - // int physicalHeightDpi = 72; - int physicalHeightDpi = (int) (bhi.vResolution * 1000.0 / 2.54); - float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); - - String formatDetails = "Bmp (" + (char) bhi.identifier1 - + (char) bhi.identifier2 + ": " - + getBmpTypeDescription(bhi.identifier1, bhi.identifier2) + ")"; - - boolean isTransparent = false; - - boolean usesPalette = colorTable != null; - int colorType = ImageInfo.COLOR_TYPE_RGB; - String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_RLE; - - ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments, - format, name, height, mimeType, numberOfImages, - physicalHeightDpi, physicalHeightInch, physicalWidthDpi, - physicalWidthInch, width, isProgressive, isTransparent, - usesPalette, colorType, compressionAlgorithm); - - return result; - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - pw.println("bmp.dumpImageFile"); - - ImageInfo imageData = getImageInfo(byteSource, null); - if (imageData == null) - return false; - - imageData.toString(pw, ""); - - pw.println(""); - - return true; - } - - public FormatCompliance getFormatCompliance(ByteSource byteSource) - throws ImageReadException, IOException - { - boolean verbose = false; - - FormatCompliance result = new FormatCompliance(byteSource - .getDescription()); - - readImageContents(byteSource.getInputStream(), result, verbose); - - return result; - } - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return getBufferedImage(byteSource.getInputStream(), params); - } - - public BufferedImage getBufferedImage(InputStream inputStream, Map params) - throws ImageReadException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - if (params.containsKey(BUFFERED_IMAGE_FACTORY)) - params.remove(BUFFERED_IMAGE_FACTORY); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageReadException("Unknown parameter: " + firstKey); - } - - ImageContents ic = readImageContents(inputStream, - FormatCompliance.getDefault(), verbose); - if (ic == null) - throw new ImageReadException("Couldn't read BMP Data"); - - BmpHeaderInfo bhi = ic.bhi; - // byte colorTable[] = ic.colorTable; - // byte imageData[] = ic.imageData; - - int width = bhi.width; - int height = bhi.height; - - boolean hasAlpha = false; - BufferedImage result = getBufferedImageFactory(params) - .getColorBufferedImage(width, height, hasAlpha); - - if (verbose) - { - System.out.println("width: " + width); - System.out.println("height: " + height); - System.out.println("width*height: " + width * height); - System.out.println("width*height*4: " + width * height * 4); - } - - PixelParser pixelParser = ic.pixelParser; - - pixelParser.processImage(result); - - return result; - - } - - private static final int BITMAP_FILE_HEADER_SIZE = 14; - private static final int BITMAP_INFO_HEADER_SIZE = 40; - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - final SimplePalette palette = new PaletteFactory().makePaletteSimple( - src, 256); - - BMPWriter writer = null; - if (palette == null) - writer = new BMPWriterRGB(); - else - writer = new BMPWriterPalette(palette); - - byte imagedata[] = writer.getImageData(src); - BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_INTEL); - - { - // write BitmapFileHeader - os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap - os.write(0x4d); // M - - int filesize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header - // size - 4 * writer.getPaletteSize() + // palette size in bytes - imagedata.length; - bos.write4Bytes(filesize); - - bos.write4Bytes(0); // reserved - bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE - + 4 * writer.getPaletteSize()); // Bitmap Data Offset - } - - int width = src.getWidth(); - int height = src.getHeight(); - - { // write BitmapInfoHeader - bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size - bos.write4Bytes(width); // width - bos.write4Bytes(height); // height - bos.write2Bytes(1); // Number of Planes - bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel - - bos.write4Bytes(BI_RGB); // Compression - bos.write4Bytes(imagedata.length); // Bitmap Data Size - bos.write4Bytes(0); // HResolution - bos.write4Bytes(0); // VResolution - if (palette == null) - bos.write4Bytes(0); // Colors - else - bos.write4Bytes(palette.length()); // Colors - bos.write4Bytes(0); // Important Colors - // bos.write_4_bytes(0); // Compression - } - - { // write Palette - writer.writePalette(bos); - } - { // write Image Data - bos.writeByteArray(imagedata); - } - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParser.java b/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParser.java deleted file mode 100644 index 6caccfd..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParser.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.pixelparsers; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.formats.bmp.BmpHeaderInfo; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class PixelParser -{ - public final BmpHeaderInfo bhi; - public final byte colorTable[]; - public final byte imageData[]; - - protected final BinaryFileParser bfp; - protected final ByteArrayInputStream is; - - public PixelParser(BmpHeaderInfo bhi, byte ColorTable[], byte ImageData[]) - { - this.bhi = bhi; - this.colorTable = ColorTable; - this.imageData = ImageData; - - bfp = new BinaryFileParser(BinaryFileParser.BYTE_ORDER_INTEL); - is = new ByteArrayInputStream(ImageData); - } - - public abstract void processImage(BufferedImage bi) - throws ImageReadException, IOException; - - protected int getColorTableRGB(int index) - { - index *= 4; - int blue = 0xff & colorTable[index + 0]; - int green = 0xff & colorTable[index + 1]; - int red = 0xff & colorTable[index + 2]; - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - return rgb; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRgb.java b/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRgb.java deleted file mode 100644 index 7d6e718..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRgb.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.pixelparsers; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.bmp.BmpHeaderInfo; - -public class PixelParserRgb extends PixelParserSimple -{ - public PixelParserRgb(BmpHeaderInfo bhi, byte ColorTable[], - byte ImageData[]) - { - super(bhi, ColorTable, ImageData); - - } - - private int bytecount = 0; - private int cached_bit_count = 0; - private int cached_byte = 0; - - int pixelCount = 0; - - public int getNextRGB() throws ImageReadException, IOException - { - pixelCount++; - - if ((bhi.bitsPerPixel == 1) // always grayscale? - || (bhi.bitsPerPixel == 4)) // always grayscale? - { - if (cached_bit_count < bhi.bitsPerPixel) - { - if (cached_bit_count != 0) - throw new ImageReadException("Unexpected leftover bits: " - + cached_bit_count + "/" + bhi.bitsPerPixel); - - cached_bit_count += 8; - cached_byte = - (0xff & imageData[bytecount]); - bytecount++; - } - int cache_mask = (1 << bhi.bitsPerPixel) - 1; - int sample = cache_mask & (cached_byte >> (8 - bhi.bitsPerPixel)); - cached_byte = 0xff & (cached_byte << bhi.bitsPerPixel); - cached_bit_count -= bhi.bitsPerPixel; - - int rgb = getColorTableRGB(sample); - - return rgb; - } else if (bhi.bitsPerPixel == 8) // always grayscale? - { - int sample = 0xff & imageData[bytecount + 0]; - - int rgb = getColorTableRGB(sample); - - bytecount += 1; - - return rgb; - } else if (bhi.bitsPerPixel == 16) - { - int data = bfp.read2Bytes("Pixel", is, "BMP Image Data"); - - int blue = (0x1f & (data >> 0)) << 3; - int green = (0x1f & (data >> 5)) << 3; - int red = (0x1f & (data >> 10)) << 3; - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - bytecount += 2; - - return rgb; - } else if (bhi.bitsPerPixel == 24) - { - int blue = 0xff & imageData[bytecount + 0]; - int green = 0xff & imageData[bytecount + 1]; - int red = 0xff & imageData[bytecount + 2]; - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - bytecount += 3; - - return rgb; - } else if (bhi.bitsPerPixel == 32) - { - int blue = 0xff & imageData[bytecount + 0]; - int green = 0xff & imageData[bytecount + 1]; - int red = 0xff & imageData[bytecount + 2]; - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - - bytecount += 4; - - return rgb; - } - - throw new ImageReadException("Unknown BitsPerPixel: " - + bhi.bitsPerPixel); - } - - public void newline() throws ImageReadException, IOException - { - cached_bit_count = 0; - - while (((bytecount) % 4) != 0) - { - bfp.readByte("Pixel", is, "BMP Image Data"); - bytecount++; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRle.java b/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRle.java deleted file mode 100644 index b60df8e..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/pixelparsers/PixelParserRle.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.pixelparsers; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.bmp.BmpHeaderInfo; - -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.DataBuffer; - - -public class PixelParserRle extends PixelParser -{ - - public PixelParserRle(BmpHeaderInfo bhi, byte ColorTable[], - byte ImageData[]) - { - super(bhi, ColorTable, ImageData); - - } - - private int getSamplesPerByte() throws ImageReadException, IOException - { - if (bhi.bitsPerPixel == 8) - return 1; - else if (bhi.bitsPerPixel == 4) - return 2; - else - throw new ImageReadException("BMP RLE: bad BitsPerPixel: " - + bhi.bitsPerPixel); - } - - private int[] convertDataToSamples(int data) throws ImageReadException, - IOException - { - int rgbs[]; - if (bhi.bitsPerPixel == 8) - { - rgbs = new int[1]; - rgbs[0] = getColorTableRGB(data); - // pixels_written = 1; - } - else if (bhi.bitsPerPixel == 4) - { - rgbs = new int[2]; - int sample1 = data >> 4; - int sample2 = 0x0f & data; - rgbs[0] = getColorTableRGB(sample1); - rgbs[1] = getColorTableRGB(sample2); - // pixels_written = 2; - } - else - throw new ImageReadException("BMP RLE: bad BitsPerPixel: " - + bhi.bitsPerPixel); - - return rgbs; - } - - private int processByteOfData(int rgbs[], int repeat, int x, int y, - int width, int height, DataBuffer db, BufferedImage bi) - throws ImageReadException - { - // int rbg - int pixels_written = 0; - for (int i = 0; i < repeat; i++) - { - - if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) - { - // int rgb = 0xff000000; - // rgb = getNextRGB(); - int rgb = rgbs[i % rgbs.length]; - // bi.setRGB(x, y, rgb); - db.setElem(y * bhi.width + x, rgb); - // bi.setRGB(x, y, 0xff00ff00); - } - else - { - System.out.println("skipping bad pixel (" + x + "," + y + ")"); - } - - x++; - pixels_written++; - } - - return pixels_written; - } - - public void processImage(BufferedImage bi) throws ImageReadException, - IOException - { - DataBuffer db = bi.getRaster().getDataBuffer(); - - int count = 0; - int width = bhi.width; - int height = bhi.height; - int x = 0, y = height - 1; - - // bfp.setDebug(true); - - boolean done = false; - while (!done) - { - count++; - // if (count > 100) - // return; - - int a = 0xff & bfp.readByte("RLE (" + x + "," + y + ") a", is, - "BMP: Bad RLE"); - // baos.write(a); - int b = 0xff & bfp.readByte("RLE (" + x + "," + y + ") b", is, - "BMP: Bad RLE"); - // baos.write(b); - - if (a == 0) - { - switch (b) - { - case 0 : // EOL - { - // System.out.println("EOL"); - y--; - x = 0; - } - break; - case 1 : // EOF - // System.out - // .println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); - done = true; - break; - case 2 : { - // System.out - // .println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); - int c = 0xff & bfp - .readByte("RLE c", is, "BMP: Bad RLE"); - // baos.write(c); - int d = 0xff & bfp - .readByte("RLE d", is, "BMP: Bad RLE"); - // baos.write(d); - - } - break; - default : { - // System.out - // .println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); - - int SamplesPerByte = getSamplesPerByte(); - int size = b / SamplesPerByte; - if ((b % SamplesPerByte) > 0) - size++; - if ((size % 2) != 0) - size++; - - // System.out.println("b: " + b); - // System.out.println("size: " + size); - // System.out.println("SamplesPerByte: " + SamplesPerByte); - - byte bytes[] = bfp.readByteArray("bytes", size, is, - "RLE: Absolute Mode"); - - int remaining = b; - - // while(true) - for (int i = 0; remaining > 0; i++) - // for (int i = 0; i < bytes.length; i++) - { - int samples[] = convertDataToSamples(0xff & bytes[i]); - int towrite = Math.min(remaining, SamplesPerByte); - // System.out.println("remaining: " + remaining); - // System.out.println("SamplesPerByte: " - // + SamplesPerByte); - // System.out.println("towrite: " + towrite); - int written = processByteOfData(samples, towrite, - x, y, width, height, db, bi); - // System.out.println("written: " + written); - // System.out.println(""); - x += written; - remaining -= written; - } - // baos.write(bytes); - } - break; - } - } - else - { - int rgbs[] = convertDataToSamples(b); - - x += processByteOfData(rgbs, a, x, y, width, height, db, bi); - // x += processByteOfData(b, a, x, y, width, height, bi); - - } - } - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterPalette.java b/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterPalette.java deleted file mode 100644 index 526854b..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterPalette.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.writers; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.palette.SimplePalette; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class BMPWriterPalette extends BMPWriter -{ - private final SimplePalette palette; - private final int bitsPerSample; - - public BMPWriterPalette(SimplePalette palette) - { - this.palette = palette; - - if (palette.length() <= 2) - bitsPerSample = 1; - else if (palette.length() <= 16) - bitsPerSample = 4; - else - bitsPerSample = 8; - } - - public int getPaletteSize() - { - return palette.length(); - } - - public int getBitsPerPixel() - { - return bitsPerSample; - } - - public void writePalette(BinaryOutputStream bos) throws IOException - { - for (int i = 0; i < palette.length(); i++) - { - int rgb = palette.getEntry(i); - - int red = 0xff & (rgb >> 16); - int green = 0xff & (rgb >> 8); - int blue = 0xff & (rgb >> 0); - - bos.write(blue); - bos.write(green); - bos.write(red); - bos.write(0); - } - } - - public byte[] getImageData(BufferedImage src) - { - int width = src.getWidth(); - int height = src.getHeight(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - int bit_cache = 0; - int bits_in_cache = 0; - - int bytecount = 0; - for (int y = height - 1; y >= 0; y--) - { - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int rgb = 0xffffff & argb; - - int index = palette.getPaletteIndex(rgb); - - if (bitsPerSample == 8) - { - baos.write(0xff & index); - bytecount++; - } else - // 4 or 1 - { - bit_cache = (bit_cache << bitsPerSample) | index; - bits_in_cache += bitsPerSample; - if (bits_in_cache >= 8) - { - baos.write(0xff & bit_cache); - bytecount++; - bit_cache = 0; - bits_in_cache = 0; - } - } - } - - if (bits_in_cache > 0) - { - bit_cache = (bit_cache << (8 - bits_in_cache)); - - baos.write(0xff & bit_cache); - bytecount++; - bit_cache = 0; - bits_in_cache = 0; - } - - while ((bytecount % 4) != 0) - { - baos.write(0); - bytecount++; - } - } - - return baos.toByteArray(); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterRGB.java b/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterRGB.java deleted file mode 100644 index 59a0a76..0000000 --- a/src/main/java/org/apache/sanselan/formats/bmp/writers/BMPWriterRGB.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.bmp.writers; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import org.apache.sanselan.common.BinaryOutputStream; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class BMPWriterRGB extends BMPWriter -{ - // private final boolean alpha; - // - // public BMPWriterRGB(boolean alpha) - // { - // this.alpha = alpha; - // } - - public int getPaletteSize() - { - return 0; - } - - public int getBitsPerPixel() - { - // return alpha ? 32 : 24; - return 24; - } - - public void writePalette(BinaryOutputStream bos) throws IOException - { - } - - public byte[] getImageData(BufferedImage src) - { - int width = src.getWidth(); - int height = src.getHeight(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - // BinaryOutputStream bos = new BinaryOutputStream(baos, BYTE_ORDER_Network); - - int bytecount = 0; - for (int y = height - 1; y >= 0; y--) - { - // for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int rgb = 0xffffff & argb; - - int red = 0xff & (rgb >> 16); - int green = 0xff & (rgb >> 8); - int blue = 0xff & (rgb >> 0); - - baos.write(blue); - baos.write(green); - baos.write(red); - bytecount += 3; - } - while ((bytecount % 4) != 0) - { - baos.write(0); - bytecount++; - } - } - - return baos.toByteArray(); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/gif/GifImageParser.java b/src/main/java/org/apache/sanselan/formats/gif/GifImageParser.java deleted file mode 100644 index dcbcbe5..0000000 --- a/src/main/java/org/apache/sanselan/formats/gif/GifImageParser.java +++ /dev/null @@ -1,1191 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.gif; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.FormatCompliance; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.mylzw.MyLZWCompressor; -import org.apache.sanselan.common.mylzw.MyLZWDecompressor; -import org.apache.sanselan.palette.Palette; -import org.apache.sanselan.palette.PaletteFactory; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.DataBuffer; - - -public class GifImageParser extends ImageParser -{ - - public GifImageParser() - { - super.setByteOrder(BYTE_ORDER_LSB); - } - - public String getName() - { - return "Gif-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".gif"; - - private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_GIF, // - }; - } - - private static final byte GIF_HEADER_SIGNATURE[] = { 71, 73, 70 }; - - private GIFHeaderInfo readHeader(InputStream is, - FormatCompliance formatCompliance) throws ImageReadException, - IOException - { - byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File"); - byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File"); - byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File"); - - byte version1 = readByte("version1", is, "Not a Valid GIF File"); - byte version2 = readByte("version2", is, "Not a Valid GIF File"); - byte version3 = readByte("version3", is, "Not a Valid GIF File"); - - if (formatCompliance != null) - { - formatCompliance.compare_bytes("Signature", GIF_HEADER_SIGNATURE, - new byte[] { identifier1, identifier2, identifier3, }); - formatCompliance.compare("version", 56, version1); - formatCompliance - .compare("version", new int[] { 55, 57, }, version2); - formatCompliance.compare("version", 97, version3); - } - - if (debug) - printCharQuad("identifier: ", ((identifier1 << 16) - | (identifier2 << 8) | (identifier3 << 0))); - if (debug) - printCharQuad("version: ", - ((version1 << 16) | (version2 << 8) | (version3 << 0))); - - int logicalScreenWidth = read2Bytes("Logical Screen Width", is, - "Not a Valid GIF File"); - int logicalScreenHeight = read2Bytes("Logical Screen Height", is, - "Not a Valid GIF File"); - - if (formatCompliance != null) - { - formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, - logicalScreenWidth); - formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, - logicalScreenHeight); - } - - byte packedFields = readByte("Packed Fields", is, - "Not a Valid GIF File"); - byte backgroundColorIndex = readByte("Background Color Index", is, - "Not a Valid GIF File"); - byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, - "Not a Valid GIF File"); - - if (debug) - printByteBits("PackedFields bits", packedFields); - - boolean globalColorTableFlag = ((packedFields & 128) > 0); - if (debug) - System.out.println("GlobalColorTableFlag: " + globalColorTableFlag); - byte colorResolution = (byte) ((packedFields >> 4) & 7); - if (debug) - System.out.println("ColorResolution: " + colorResolution); - boolean sortFlag = ((packedFields & 8) > 0); - if (debug) - System.out.println("SortFlag: " + sortFlag); - byte sizeofGlobalColorTable = (byte) (packedFields & 7); - if (debug) - System.out.println("SizeofGlobalColorTable: " - + sizeofGlobalColorTable); - - if (formatCompliance != null) - { - if (globalColorTableFlag && backgroundColorIndex != -1) - formatCompliance.checkBounds("Background Color Index", 0, - convertColorTableSize(sizeofGlobalColorTable), - backgroundColorIndex); - } - - return new GIFHeaderInfo(identifier1, identifier2, identifier3, - version1, version2, version3, logicalScreenWidth, - logicalScreenHeight, packedFields, backgroundColorIndex, - pixelAspectRatio, globalColorTableFlag, colorResolution, - sortFlag, sizeofGlobalColorTable); - } - - private GraphicControlExtension readGraphicControlExtension(int code, - InputStream is) throws ImageReadException, IOException - { - readByte("block_size", is, "GIF: corrupt GraphicControlExt"); - int packed = readByte("packed fields", is, - "GIF: corrupt GraphicControlExt"); - - int dispose = (packed & 0x1c) >> 2; // disposal method - boolean transparency = (packed & 1) != 0; - - int delay = read2Bytes("delay in milliseconds", is, - "GIF: corrupt GraphicControlExt"); - int transparentColorIndex = 0xff & readByte("transparent color index", - is, "GIF: corrupt GraphicControlExt"); - readByte("block terminator", is, "GIF: corrupt GraphicControlExt"); - - return new GraphicControlExtension(code, packed, dispose, transparency, - delay, transparentColorIndex); - } - - private byte[] readSubBlock(InputStream is) throws ImageReadException, - IOException - { - int block_size = 0xff & readByte("block_size", is, "GIF: corrupt block"); - - byte bytes[] = readByteArray("block", block_size, is, - "GIF: corrupt block"); - - return bytes; - } - - protected GenericGIFBlock readGenericGIFBlock(InputStream is, int code) - throws ImageReadException, IOException - { - return readGenericGIFBlock(is, code, null); - } - - protected GenericGIFBlock readGenericGIFBlock(InputStream is, int code, - byte first[]) throws ImageReadException, IOException - { - ArrayList subblocks = new ArrayList(); - - if (first != null) - subblocks.add(first); - - while (true) - { - byte bytes[] = readSubBlock(is); - if (bytes.length < 1) - break; - subblocks.add(bytes); - } - - return new GenericGIFBlock(code, subblocks); - } - - private final static int EXTENSION_CODE = 0x21; - private final static int IMAGE_SEPARATOR = 0x2C; - private final static int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9; - private final static int COMMENT_EXTENSION = 0xfe; - private final static int PLAIN_TEXT_EXTENSION = 0x01; - private final static int XMP_EXTENSION = 0xff; - private final static int TERMINATOR_BYTE = 0x3b; - private final static int APPLICATION_EXTENSION_LABEL = 0xff; - private final static int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8) - | XMP_EXTENSION; - - private ArrayList readBlocks(GIFHeaderInfo ghi, InputStream is, - boolean stopBeforeImageData, FormatCompliance formatCompliance) - throws ImageReadException, IOException - { - ArrayList result = new ArrayList(); - - while (true) - { - int code = is.read(); - - switch (code) - { - case -1: - throw new ImageReadException("GIF: unexpected end of data"); - - case IMAGE_SEPARATOR: - ImageDescriptor id = readImageDescriptor(ghi, code, is, - stopBeforeImageData, formatCompliance); - result.add(id); - // if(stopBeforeImageData) - // return result; - - break; - - case EXTENSION_CODE: // extension - { - int extensionCode = is.read(); - int completeCode = ((0xff & code) << 8) - | (0xff & extensionCode); - - switch (extensionCode) - { - case 0xf9: - GraphicControlExtension gce = readGraphicControlExtension( - completeCode, is); - result.add(gce); - break; - - case COMMENT_EXTENSION: - case PLAIN_TEXT_EXTENSION: { - GenericGIFBlock block = readGenericGIFBlock(is, - completeCode); - result.add(block); - break; - } - - case APPLICATION_EXTENSION_LABEL: // 255 (hex 0xFF) Application - // Extension Label - { - byte label[] = readSubBlock(is); - - if (formatCompliance != null) - formatCompliance - .addComment("Unknown Application Extension (" - + new String(label) + ")", completeCode); - - // if (label == new String("ICCRGBG1")) - { - // GIF's can have embedded ICC Profiles - who knew? - } - - if ((label != null) && (label.length > 0)) - { - GenericGIFBlock block = readGenericGIFBlock(is, - completeCode, label); - result.add(block); - } - break; - } - - default: { - - if (formatCompliance != null) - formatCompliance.addComment("Unknown block", - completeCode); - - GenericGIFBlock block = readGenericGIFBlock(is, - completeCode); - result.add(block); - break; - } - } - } - break; - - case TERMINATOR_BYTE: - return result; - - case 0x00: // bad byte, but keep going and see what happens - break; - - default: - throw new ImageReadException("GIF: unknown code: " + code); - } - } - } - - private ImageDescriptor readImageDescriptor(GIFHeaderInfo ghi, - int blockCode, InputStream is, boolean stopBeforeImageData, - FormatCompliance formatCompliance) throws ImageReadException, - IOException - { - int ImageLeftPosition = read2Bytes("Image Left Position", is, - "Not a Valid GIF File"); - int ImageTopPosition = read2Bytes("Image Top Position", is, - "Not a Valid GIF File"); - int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File"); - int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File"); - byte PackedFields = readByte("Packed Fields", is, - "Not a Valid GIF File"); - - if (formatCompliance != null) - { - formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth, - imageWidth); - formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight, - imageHeight); - formatCompliance.checkBounds("Left Position", 0, - ghi.logicalScreenWidth - imageWidth, ImageLeftPosition); - formatCompliance.checkBounds("Top Position", 0, - ghi.logicalScreenHeight - imageHeight, ImageTopPosition); - } - - if (debug) - printByteBits("PackedFields bits", PackedFields); - - boolean LocalColorTableFlag = (((PackedFields >> 7) & 1) > 0); - if (debug) - System.out.println("LocalColorTableFlag: " + LocalColorTableFlag); - boolean InterlaceFlag = (((PackedFields >> 6) & 1) > 0); - if (debug) - System.out.println("Interlace Flag: " + InterlaceFlag); - boolean SortFlag = (((PackedFields >> 5) & 1) > 0); - if (debug) - System.out.println("Sort Flag: " + SortFlag); - - byte SizeofLocalColorTable = (byte) (PackedFields & 7); - if (debug) - System.out.println("SizeofLocalColorTable: " - + SizeofLocalColorTable); - - byte LocalColorTable[] = null; - if (LocalColorTableFlag) - LocalColorTable = readColorTable(is, SizeofLocalColorTable, - formatCompliance); - - byte imageData[] = null; - if (!stopBeforeImageData) - { - int LZWMinimumCodeSize = is.read(); - - GenericGIFBlock block = readGenericGIFBlock(is, -1); - byte bytes[] = block.appendSubBlocks(); - InputStream bais = new ByteArrayInputStream(bytes); - - int size = imageWidth * imageHeight; - MyLZWDecompressor myLzwDecompressor = new MyLZWDecompressor( - LZWMinimumCodeSize, BYTE_ORDER_LSB); - imageData = myLzwDecompressor.decompress(bais, size); - } else - { - int LZWMinimumCodeSize = is.read(); - if (debug) - System.out.println("LZWMinimumCodeSize: " + LZWMinimumCodeSize); - - readGenericGIFBlock(is, -1); - } - - ImageDescriptor result = new ImageDescriptor(blockCode, - ImageLeftPosition, ImageTopPosition, imageWidth, imageHeight, - PackedFields, LocalColorTableFlag, InterlaceFlag, SortFlag, - SizeofLocalColorTable, LocalColorTable, imageData); - - return result; - } - - private int simple_pow(int base, int power) - { - int result = 1; - - for (int i = 0; i < power; i++) - result *= base; - - return result; - } - - private int convertColorTableSize(int ct_size) - { - return 3 * simple_pow(2, ct_size + 1); - } - - private byte[] readColorTable(InputStream is, int ct_size, - FormatCompliance formatCompliance) throws IOException - { - int actual_size = convertColorTableSize(ct_size); - - byte bytes[] = readByteArray("block", actual_size, is, - "GIF: corrupt Color Table"); - - return bytes; - } - - // TODO - unused - private GIFHeaderInfo readHeader(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - - return readHeader(is, FormatCompliance.getDefault()); - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } - - private GIFBlock findBlock(ArrayList v, int code) - { - for (int i = 0; i < v.size(); i++) - { - GIFBlock gifBlock = (GIFBlock) v.get(i); - if (gifBlock.blockCode == code) - return gifBlock; - } - return null; - } - - private ImageContents readFile(ByteSource byteSource, - boolean stopBeforeImageData) throws ImageReadException, IOException - { - return readFile(byteSource, stopBeforeImageData, FormatCompliance - .getDefault()); - } - - private ImageContents readFile(ByteSource byteSource, - boolean stopBeforeImageData, FormatCompliance formatCompliance) - throws ImageReadException, IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - - GIFHeaderInfo ghi = readHeader(is, formatCompliance); - - byte globalColorTable[] = null; - if (ghi.globalColorTableFlag) - globalColorTable = readColorTable(is, - ghi.sizeOfGlobalColorTable, formatCompliance); - - ArrayList blocks = readBlocks(ghi, is, stopBeforeImageData, - formatCompliance); - - ImageContents result = new ImageContents(ghi, globalColorTable, - blocks); - - return result; - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ImageContents blocks = readFile(byteSource, false); - - if (blocks == null) - throw new ImageReadException("GIF: Couldn't read blocks"); - - GIFHeaderInfo bhi = blocks.gifHeaderInfo; - if (bhi == null) - throw new ImageReadException("GIF: Couldn't read Header"); - - ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, - IMAGE_SEPARATOR); - if (id == null) - throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); - - // Prefer the size information in the ImageDescriptor; it is more reliable - // than the size information in the header. - return new Dimension(id.imageWidth, id.imageHeight); - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - private ArrayList getComments(ArrayList v) throws IOException - { - ArrayList result = new ArrayList(); - int code = 0x21fe; - - for (int i = 0; i < v.size(); i++) - { - GIFBlock block = (GIFBlock) v.get(i); - if (block.blockCode == code) - { - byte bytes[] = ((GenericGIFBlock) block).appendSubBlocks(); - result.add(new String(bytes)); - } - } - - return result; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ImageContents blocks = readFile(byteSource, false); - - if (blocks == null) - throw new ImageReadException("GIF: Couldn't read blocks"); - - GIFHeaderInfo bhi = blocks.gifHeaderInfo; - if (bhi == null) - throw new ImageReadException("GIF: Couldn't read Header"); - - ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, - IMAGE_SEPARATOR); - if (id == null) - throw new ImageReadException("GIF: Couldn't read ImageDescriptor"); - - GraphicControlExtension gce = (GraphicControlExtension) findBlock( - blocks.blocks, GRAPHIC_CONTROL_EXTENSION); - - // Prefer the size information in the ImageDescriptor; it is more reliable - // than the size information in the header. - int height = id.imageWidth; - int width = id.imageHeight; - - ArrayList Comments; - - Comments = getComments(blocks.blocks); - - int BitsPerPixel = (bhi.colorResolution + 1) * 3; - ImageFormat Format = ImageFormat.IMAGE_FORMAT_GIF; - String FormatName = "GIF Graphics Interchange Format"; - String MimeType = "image/gif"; - // we ought to count images, but don't yet. - int NumberOfImages = -1; - - boolean isProgressive = id.interlaceFlag; - - int PhysicalWidthDpi = 72; - float PhysicalWidthInch = (float) ((double) width / (double) PhysicalWidthDpi); - int PhysicalHeightDpi = 72; - float PhysicalHeightInch = (float) ((double) height / (double) PhysicalHeightDpi); - - String FormatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1) - + ((char) blocks.gifHeaderInfo.version2) - + ((char) blocks.gifHeaderInfo.version3); - - boolean isTransparent = false; - if (gce != null && gce.transparency) - isTransparent = true; - - boolean usesPalette = true; - int colorType = ImageInfo.COLOR_TYPE_RGB; - String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; - - ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, - Format, FormatName, height, MimeType, NumberOfImages, - PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, - PhysicalWidthInch, width, isProgressive, isTransparent, - usesPalette, colorType, compressionAlgorithm); - - return result; - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - pw.println("gif.dumpImageFile"); - - { - ImageInfo imageData = getImageInfo(byteSource); - if (imageData == null) - return false; - - imageData.toString(pw, ""); - } - { - ImageContents blocks = readFile(byteSource, false); - - if (blocks == null) - return false; - - pw.println("gif.blocks: " + blocks.blocks.size()); - for (int i = 0; i < blocks.blocks.size(); i++) - { - GIFBlock gifBlock = (GIFBlock) blocks.blocks.get(i); - this.debugNumber(pw, "\t" + i + " (" - + gifBlock.getClass().getName() + ")", - gifBlock.blockCode, 4); - } - - } - - pw.println(""); - - return true; - } - - private int[] getColorTable(byte bytes[]) throws ImageReadException - { - if ((bytes.length % 3) != 0) - throw new ImageReadException("Bad Color Table Length: " - + bytes.length); - int length = bytes.length / 3; - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - int red = 0xff & bytes[(i * 3) + 0]; - int green = 0xff & bytes[(i * 3) + 1]; - int blue = 0xff & bytes[(i * 3) + 2]; - - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - result[i] = rgb; - } - - return result; - } - - public FormatCompliance getFormatCompliance(ByteSource byteSource) - throws ImageReadException, IOException - { - FormatCompliance result = new FormatCompliance(byteSource - .getDescription()); - - readFile(byteSource, false, result); - - return result; - } - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ImageContents imageContents = readFile(byteSource, false); - - if (imageContents == null) - throw new ImageReadException("GIF: Couldn't read blocks"); - - GIFHeaderInfo ghi = imageContents.gifHeaderInfo; - if (ghi == null) - throw new ImageReadException("GIF: Couldn't read Header"); - - ImageDescriptor id = (ImageDescriptor) findBlock(imageContents.blocks, - IMAGE_SEPARATOR); - if (id == null) - throw new ImageReadException("GIF: Couldn't read Image Descriptor"); - GraphicControlExtension gce = (GraphicControlExtension) findBlock( - imageContents.blocks, GRAPHIC_CONTROL_EXTENSION); - - // Prefer the size information in the ImageDescriptor; it is more reliable - // than the size information in the header. - int width = id.imageWidth; - int height = id.imageHeight; - - boolean hasAlpha = false; - if (gce != null && gce.transparency) - hasAlpha = true; - - BufferedImage result = getBufferedImageFactory(params) - .getColorBufferedImage(width, height, hasAlpha); - - { - int colorTable[]; - if (id.localColorTable != null) - colorTable = getColorTable(id.localColorTable); - else if (imageContents.globalColorTable != null) - colorTable = getColorTable(imageContents.globalColorTable); - else - throw new ImageReadException("Gif: No Color Table"); - - int transparentIndex = -1; - if (hasAlpha) - transparentIndex = gce.transparentColorIndex; - - int counter = 0; - - int rowsInPass1 = (height + 7) / 8; - int rowsInPass2 = (height + 3) / 8; - int rowsInPass3 = (height + 1) / 4; - int rowsInPass4 = (height) / 2; - - DataBuffer db = result.getRaster().getDataBuffer(); - - for (int row = 0; row < height; row++) - { - int y; - if (id.interlaceFlag) - { - int the_row = row; - if (the_row < rowsInPass1) - y = the_row * 8; - else - { - the_row -= rowsInPass1; - if (the_row < (rowsInPass2)) - y = 4 + (the_row * 8); - else - { - the_row -= rowsInPass2; - if (the_row < (rowsInPass3)) - y = 2 + (the_row * 4); - else - { - the_row -= rowsInPass3; - if (the_row < (rowsInPass4)) - y = 1 + (the_row * 2); - else - throw new ImageReadException( - "Gif: Strange Row"); - } - } - } - } else - y = row; - - for (int x = 0; x < width; x++) - { - int index = 0xff & id.imageData[counter++]; - int rgb = colorTable[index]; - - if (transparentIndex == index) - rgb = 0x00; - - db.setElem(y * width + x, rgb); - } - - } - } - - return result; - - } - - private void writeAsSubBlocks(OutputStream os, byte bytes[]) - throws IOException - { - int index = 0; - - while (index < bytes.length) - { - int block_size = Math.min(bytes.length - index, 255); - os.write(block_size); - os.write(bytes, index, block_size); - index += block_size; - } - os.write(0); // last block - } - - private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7; - private static final int INTERLACE_FLAG_MASK = 1 << 6; - private static final int SORT_FLAG_MASK = 1 << 5; - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - String xmpXml = null; - if (params.containsKey(PARAM_KEY_XMP_XML)) - { - xmpXml = (String) params.get(PARAM_KEY_XMP_XML); - params.remove(PARAM_KEY_XMP_XML); - } - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - int width = src.getWidth(); - int height = src.getHeight(); - - boolean hasAlpha = new PaletteFactory().hasTransparency(src); - - int max_colors = hasAlpha ? 255 : 256; - - Palette palette2 = new PaletteFactory().makePaletteSimple(src, - max_colors); - // int palette[] = new PaletteFactory().makePaletteSimple(src, 256); - // Map palette_map = paletteToMap(palette); - - if (palette2 == null) - { - palette2 = new PaletteFactory().makePaletteQuantized(src, - max_colors); - if (verbose) - System.out.println("quantizing"); - } else if (verbose) - System.out.println("exact palette"); - - if (palette2 == null) - throw new ImageWriteException( - "Gif: can't write images with more than 256 colors"); - int palette_size = palette2.length() + (hasAlpha ? 1 : 0); - - BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_LSB); - - { - // write Header - os.write(0x47); // G magic numbers - os.write(0x49); // I - os.write(0x46); // F - - os.write(0x38); // 8 version magic numbers - os.write(0x39); // 9 - os.write(0x61); // a - - // Logical Screen Descriptor. - - bos.write2Bytes(width); - bos.write2Bytes(height); - - int colorTableScaleLessOne = (palette_size > 128) ? 7 - : (palette_size > 64) ? 6 : (palette_size > 32) ? 5 - : (palette_size > 16) ? 4 : (palette_size > 8) ? 3 - : (palette_size > 4) ? 2 - : (palette_size > 2) ? 1 : 0; - - int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1); - int actual_size = 3 * simple_pow(2, colorTableScaleLessOne + 1); - { - byte colorResolution = (byte) colorTableScaleLessOne; // TODO: - - boolean globalColorTableFlag = false; - boolean sortFlag = false; - int globalColorTableFlagMask = 1 << 7; - int sortFlagMask = 8; - int sizeOfGlobalColorTable = 0; - - int packedFields = ((globalColorTableFlag ? globalColorTableFlagMask - : 0) - | (sortFlag ? sortFlagMask : 0) - | ((7 & colorResolution) << 4) | (7 & sizeOfGlobalColorTable)); - bos.write(packedFields); // one byte - } - { - byte BackgroundColorIndex = 0; - bos.write(BackgroundColorIndex); - } - { - byte PixelAspectRatio = 0; - bos.write(PixelAspectRatio); - } - - { // write Global Color Table. - - } - - { // ALWAYS write GraphicControlExtension - bos.write(EXTENSION_CODE); - bos.write((byte) 0xf9); - // bos.write(0xff & (kGraphicControlExtension >> 8)); - // bos.write(0xff & (kGraphicControlExtension >> 0)); - - bos.write((byte) 4); // block size; - int packedFields = hasAlpha ? 1 : 0; // transparency flag - bos.write((byte) packedFields); - bos.write((byte) 0); // Delay Time - bos.write((byte) 0); // Delay Time - bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent - // Color - // Index - bos.write((byte) 0); // terminator - } - - if (null != xmpXml) - { - bos.write(EXTENSION_CODE); - bos.write(APPLICATION_EXTENSION_LABEL); - - bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B - bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE); - - byte xmpXmlBytes[] = xmpXml.getBytes("utf-8"); - bos.write(xmpXmlBytes); - - // write "magic trailer" - for (int magic = 0; magic <= 0xff; magic++) - bos.write(0xff - magic); - - bos.write((byte) 0); // terminator - - } - - { // Image Descriptor. - bos.write(IMAGE_SEPARATOR); - bos.write2Bytes(0); // Image Left Position - bos.write2Bytes(0); // Image Top Position - bos.write2Bytes(width); // Image Width - bos.write2Bytes(height); // Image Height - - { - boolean LocalColorTableFlag = true; - // boolean LocalColorTableFlag = false; - boolean InterlaceFlag = false; - boolean SortFlag = false; - int SizeOfLocalColorTable = colorTableScaleLessOne; - - // int SizeOfLocalColorTable = 0; - - int PackedFields = ((LocalColorTableFlag ? LOCAL_COLOR_TABLE_FLAG_MASK - : 0) - | (InterlaceFlag ? INTERLACE_FLAG_MASK : 0) - | (SortFlag ? SORT_FLAG_MASK : 0) | (7 & SizeOfLocalColorTable)); - bos.write(PackedFields); // one byte - } - } - - { // write Local Color Table. - for (int i = 0; i < colorTableSizeInFormat; i++) - { - if (i < palette2.length()) - { - int rgb = palette2.getEntry(i); - - int red = 0xff & (rgb >> 16); - int green = 0xff & (rgb >> 8); - int blue = 0xff & (rgb >> 0); - - bos.write(red); - bos.write(green); - bos.write(blue); - } else - { - bos.write(0); - bos.write(0); - bos.write(0); - } - } - } - - { // get Image Data. - int image_data_total = 0; - - int LZWMinimumCodeSize = colorTableScaleLessOne + 1; -// LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize); - if (LZWMinimumCodeSize < 2) - LZWMinimumCodeSize = 2; - - // TODO: - // make - // better - // choice - // here. - bos.write(LZWMinimumCodeSize); - - MyLZWCompressor compressor = new MyLZWCompressor( - LZWMinimumCodeSize, BYTE_ORDER_LSB, false); // GIF - // Mode); - - byte imagedata[] = new byte[width * height]; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int rgb = 0xffffff & argb; - int index; - - if (hasAlpha) - { - int alpha = 0xff & (argb >> 24); - final int alphaThreshold = 255; - if (alpha < alphaThreshold) - index = palette2.length(); // is transparent - else - index = palette2.getPaletteIndex(rgb); - } else - { - index = palette2.getPaletteIndex(rgb); - } - - imagedata[y * width + x] = (byte) index; - } - } - - byte compressed[] = compressor.compress(imagedata); - writeAsSubBlocks(bos, compressed); - image_data_total += compressed.length; - } - - // palette2.dump(); - - bos.write(TERMINATOR_BYTE); - } - - bos.close(); - os.close(); - } - - private static final byte XMP_APPLICATION_ID_AND_AUTH_CODE[] = { 0x58, // X - 0x4D, // M - 0x50, // P - 0x20, // - 0x44, // D - 0x61, // a - 0x74, // t - 0x61, // a - 0x58, // X - 0x4D, // M - 0x50, // P - }; - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - - InputStream is = null; - try - { - is = byteSource.getInputStream(); - - FormatCompliance formatCompliance = null; - GIFHeaderInfo ghi = readHeader(is, formatCompliance); - - if (ghi.globalColorTableFlag) - readColorTable(is, ghi.sizeOfGlobalColorTable, formatCompliance); - - ArrayList blocks = readBlocks(ghi, is, true, formatCompliance); - - List result = new ArrayList(); - for (int i = 0; i < blocks.size(); i++) - { - GIFBlock block = (GIFBlock) blocks.get(i); - if (block.blockCode != XMP_COMPLETE_CODE) - continue; - - GenericGIFBlock genericBlock = (GenericGIFBlock) block; - - byte blockBytes[] = genericBlock.appendSubBlocks(true); - if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) - continue; - - if (!compareByteArrays(blockBytes, 0, - XMP_APPLICATION_ID_AND_AUTH_CODE, 0, - XMP_APPLICATION_ID_AND_AUTH_CODE.length)) - continue; - - byte GIF_MAGIC_TRAILER[] = new byte[256]; - for (int magic = 0; magic <= 0xff; magic++) - GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic); - - if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length - + GIF_MAGIC_TRAILER.length) - continue; - if (!compareByteArrays(blockBytes, blockBytes.length - - GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0, - GIF_MAGIC_TRAILER.length)) - throw new ImageReadException( - "XMP block in GIF missing magic trailer."); - - try - { - // XMP is UTF-8 encoded xml. - String xml = new String( - blockBytes, - XMP_APPLICATION_ID_AND_AUTH_CODE.length, - blockBytes.length - - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length), - "utf-8"); - result.add(xml); - } catch (UnsupportedEncodingException e) - { - throw new ImageReadException("Invalid XMP Block in GIF."); - } - } - - if (result.size() < 1) - return null; - if (result.size() > 1) - throw new ImageReadException("More than one XMP Block in GIF."); - return (String) result.get(0); - - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/icns/IcnsDecoder.java b/src/main/java/org/apache/sanselan/formats/icns/IcnsDecoder.java deleted file mode 100644 index 1c1410b..0000000 --- a/src/main/java/org/apache/sanselan/formats/icns/IcnsDecoder.java +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.icns; - -import java.io.IOException; -import java.util.ArrayList; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class IcnsDecoder -{ - private static final int[] palette_4bpp = - { - 0xffffffff, - 0xfffcf305, - 0xffff6402, - 0xffdd0806, - 0xfff20884, - 0xff4600a5, - 0xff0000d4, - 0xff02abea, - 0xff1fb714, - 0xff006411, - 0xff562c05, - 0xff90713a, - 0xffc0c0c0, - 0xff808080, - 0xff404040, - 0xff000000 - }; - - private static final int[] palette_8bpp = - { - 0xFFFFFFFF, - 0xFFFFFFCC, - 0xFFFFFF99, - 0xFFFFFF66, - 0xFFFFFF33, - 0xFFFFFF00, - 0xFFFFCCFF, - 0xFFFFCCCC, - 0xFFFFCC99, - 0xFFFFCC66, - 0xFFFFCC33, - 0xFFFFCC00, - 0xFFFF99FF, - 0xFFFF99CC, - 0xFFFF9999, - 0xFFFF9966, - 0xFFFF9933, - 0xFFFF9900, - 0xFFFF66FF, - 0xFFFF66CC, - 0xFFFF6699, - 0xFFFF6666, - 0xFFFF6633, - 0xFFFF6600, - 0xFFFF33FF, - 0xFFFF33CC, - 0xFFFF3399, - 0xFFFF3366, - 0xFFFF3333, - 0xFFFF3300, - 0xFFFF00FF, - 0xFFFF00CC, - 0xFFFF0099, - 0xFFFF0066, - 0xFFFF0033, - 0xFFFF0000, - 0xFFCCFFFF, - 0xFFCCFFCC, - 0xFFCCFF99, - 0xFFCCFF66, - 0xFFCCFF33, - 0xFFCCFF00, - 0xFFCCCCFF, - 0xFFCCCCCC, - 0xFFCCCC99, - 0xFFCCCC66, - 0xFFCCCC33, - 0xFFCCCC00, - 0xFFCC99FF, - 0xFFCC99CC, - 0xFFCC9999, - 0xFFCC9966, - 0xFFCC9933, - 0xFFCC9900, - 0xFFCC66FF, - 0xFFCC66CC, - 0xFFCC6699, - 0xFFCC6666, - 0xFFCC6633, - 0xFFCC6600, - 0xFFCC33FF, - 0xFFCC33CC, - 0xFFCC3399, - 0xFFCC3366, - 0xFFCC3333, - 0xFFCC3300, - 0xFFCC00FF, - 0xFFCC00CC, - 0xFFCC0099, - 0xFFCC0066, - 0xFFCC0033, - 0xFFCC0000, - 0xFF99FFFF, - 0xFF99FFCC, - 0xFF99FF99, - 0xFF99FF66, - 0xFF99FF33, - 0xFF99FF00, - 0xFF99CCFF, - 0xFF99CCCC, - 0xFF99CC99, - 0xFF99CC66, - 0xFF99CC33, - 0xFF99CC00, - 0xFF9999FF, - 0xFF9999CC, - 0xFF999999, - 0xFF999966, - 0xFF999933, - 0xFF999900, - 0xFF9966FF, - 0xFF9966CC, - 0xFF996699, - 0xFF996666, - 0xFF996633, - 0xFF996600, - 0xFF9933FF, - 0xFF9933CC, - 0xFF993399, - 0xFF993366, - 0xFF993333, - 0xFF993300, - 0xFF9900FF, - 0xFF9900CC, - 0xFF990099, - 0xFF990066, - 0xFF990033, - 0xFF990000, - 0xFF66FFFF, - 0xFF66FFCC, - 0xFF66FF99, - 0xFF66FF66, - 0xFF66FF33, - 0xFF66FF00, - 0xFF66CCFF, - 0xFF66CCCC, - 0xFF66CC99, - 0xFF66CC66, - 0xFF66CC33, - 0xFF66CC00, - 0xFF6699FF, - 0xFF6699CC, - 0xFF669999, - 0xFF669966, - 0xFF669933, - 0xFF669900, - 0xFF6666FF, - 0xFF6666CC, - 0xFF666699, - 0xFF666666, - 0xFF666633, - 0xFF666600, - 0xFF6633FF, - 0xFF6633CC, - 0xFF663399, - 0xFF663366, - 0xFF663333, - 0xFF663300, - 0xFF6600FF, - 0xFF6600CC, - 0xFF660099, - 0xFF660066, - 0xFF660033, - 0xFF660000, - 0xFF33FFFF, - 0xFF33FFCC, - 0xFF33FF99, - 0xFF33FF66, - 0xFF33FF33, - 0xFF33FF00, - 0xFF33CCFF, - 0xFF33CCCC, - 0xFF33CC99, - 0xFF33CC66, - 0xFF33CC33, - 0xFF33CC00, - 0xFF3399FF, - 0xFF3399CC, - 0xFF339999, - 0xFF339966, - 0xFF339933, - 0xFF339900, - 0xFF3366FF, - 0xFF3366CC, - 0xFF336699, - 0xFF336666, - 0xFF336633, - 0xFF336600, - 0xFF3333FF, - 0xFF3333CC, - 0xFF333399, - 0xFF333366, - 0xFF333333, - 0xFF333300, - 0xFF3300FF, - 0xFF3300CC, - 0xFF330099, - 0xFF330066, - 0xFF330033, - 0xFF330000, - 0xFF00FFFF, - 0xFF00FFCC, - 0xFF00FF99, - 0xFF00FF66, - 0xFF00FF33, - 0xFF00FF00, - 0xFF00CCFF, - 0xFF00CCCC, - 0xFF00CC99, - 0xFF00CC66, - 0xFF00CC33, - 0xFF00CC00, - 0xFF0099FF, - 0xFF0099CC, - 0xFF009999, - 0xFF009966, - 0xFF009933, - 0xFF009900, - 0xFF0066FF, - 0xFF0066CC, - 0xFF006699, - 0xFF006666, - 0xFF006633, - 0xFF006600, - 0xFF0033FF, - 0xFF0033CC, - 0xFF003399, - 0xFF003366, - 0xFF003333, - 0xFF003300, - 0xFF0000FF, - 0xFF0000CC, - 0xFF000099, - 0xFF000066, - 0xFF000033, - 0xFFEE0000, - 0xFFDD0000, - 0xFFBB0000, - 0xFFAA0000, - 0xFF880000, - 0xFF770000, - 0xFF550000, - 0xFF440000, - 0xFF220000, - 0xFF110000, - 0xFF00EE00, - 0xFF00DD00, - 0xFF00BB00, - 0xFF00AA00, - 0xFF008800, - 0xFF007700, - 0xFF005500, - 0xFF004400, - 0xFF002200, - 0xFF001100, - 0xFF0000EE, - 0xFF0000DD, - 0xFF0000BB, - 0xFF0000AA, - 0xFF000088, - 0xFF000077, - 0xFF000055, - 0xFF000044, - 0xFF000022, - 0xFF000011, - 0xFFEEEEEE, - 0xFFDDDDDD, - 0xFFBBBBBB, - 0xFFAAAAAA, - 0xFF888888, - 0xFF777777, - 0xFF555555, - 0xFF444444, - 0xFF222222, - 0xFF111111, - 0xFF000000 - }; - - private static void decode1BPPImage(IcnsType imageType, byte[] imageData, - BufferedImage bufferedImage) - { - int position = 0; - int bitsLeft = 0; - int value = 0; - for (int y = 0; y < imageType.getHeight(); y++) - { - for (int x = 0; x < imageType.getWidth(); x++) - { - if (bitsLeft == 0) - { - value = 0xff & imageData[position++]; - bitsLeft = 8; - } - int argb; - if ((value & 0x80) != 0) - argb = 0xff000000; - else - argb = 0xffffffff; - value <<= 1; - bitsLeft--; - bufferedImage.setRGB(x, y, argb); - } - } - } - - private static void decode4BPPImage(IcnsType imageType, byte[] imageData, - BufferedImage bufferedImage) - { - int i = 0; - boolean visited = false; - for (int y = 0; y < imageType.getHeight(); y++) - { - for (int x = 0; x < imageType.getWidth(); x++) - { - int index; - if (!visited) - index = 0xf & (imageData[i] >> 4); - else - index = 0xf & imageData[i++]; - visited = !visited; - bufferedImage.setRGB(x, y, palette_4bpp[index]); - } - } - } - - private static void decode8BPPImage(IcnsType imageType, byte[] imageData, - BufferedImage bufferedImage) - { - for (int y = 0; y < imageType.getHeight(); y++) - { - for (int x = 0; x < imageType.getWidth(); x++) - { - int index = 0xff & imageData[y*imageType.getWidth() + x]; - bufferedImage.setRGB(x, y, palette_8bpp[index]); - } - } - } - - private static void decode32BPPImage(IcnsType imageType, byte[] imageData, - BufferedImage bufferedImage) - { - for (int y = 0; y < imageType.getHeight(); y++) - { - for (int x = 0; x < imageType.getWidth(); x++) - { - int argb = 0xff000000 /* the "alpha" is ignored */ | - ((0xff & imageData[4*(y*imageType.getWidth() + x) + 1]) << 16) | - ((0xff & imageData[4*(y*imageType.getWidth() + x) + 2]) << 8) | - (0xff & imageData[4*(y*imageType.getWidth() + x) + 3]); - bufferedImage.setRGB(x, y, argb); - } - } - } - - private static void apply1BPPMask(byte[] maskData, BufferedImage bufferedImage) throws ImageReadException - { - int position = 0; - int bitsLeft = 0; - int value = 0; - - // 1 bit icon types have image data followed by mask data in the same entry - int totalBytes = (bufferedImage.getWidth() * bufferedImage.getHeight() + 7) / 8; - if (maskData.length >= 2*totalBytes) - position = totalBytes; - else - throw new ImageReadException("1 BPP mask underrun parsing ICNS file"); - - for (int y = 0; y < bufferedImage.getHeight(); y++) - { - for (int x = 0; x < bufferedImage.getWidth(); x++) - { - if (bitsLeft == 0) - { - value = 0xff & maskData[position++]; - bitsLeft = 8; - } - int alpha; - if ((value & 0x80) != 0) - alpha = 0xff; - else - alpha = 0x00; - value <<= 1; - bitsLeft--; - bufferedImage.setRGB(x, y, (alpha << 24) | - (0xffffff & bufferedImage.getRGB(x, y))); - } - } - } - - private static void apply8BPPMask(byte[] maskData, BufferedImage bufferedImage) - { - for (int y = 0; y < bufferedImage.getHeight(); y++) - { - for (int x = 0; x < bufferedImage.getWidth(); x++) - { - int alpha = 0xff & maskData[y*bufferedImage.getWidth() + x]; - bufferedImage.setRGB(x, y, (alpha << 24) | - (0xffffff & bufferedImage.getRGB(x, y))); - } - } - } - - public static ArrayList decodeAllImages(IcnsImageParser.IcnsElement[] icnsElements) - throws ImageReadException, IOException - { - ArrayList result = new ArrayList(); - for (int i = 0; i < icnsElements.length; i++) - { - IcnsImageParser.IcnsElement imageElement = icnsElements[i]; - IcnsType imageType = IcnsType.findImageType(imageElement.type); - if (imageType == null) - continue; - - IcnsType maskType = null; - IcnsImageParser.IcnsElement maskElement = null; - if (imageType.hasMask()) - { - maskType = imageType; - maskElement = imageElement; - } - else - { - maskType = IcnsType.find8BPPMaskType(imageType); - if (maskType != null) - { - for (int j = 0; j < icnsElements.length; j++) - { - if (icnsElements[j].type == maskType.getType()) - { - maskElement = icnsElements[j]; - break; - } - } - } - if (maskElement == null) - { - maskType = IcnsType.find1BPPMaskType(imageType); - if (maskType != null) - { - for (int j = 0; j < icnsElements.length; j++) - { - if (icnsElements[j].type == maskType.getType()) - { - maskElement = icnsElements[j]; - break; - } - } - } - } - } - - // FIXME: don't skip these when JPEG 2000 support is added: - if (imageType == IcnsType.ICNS_256x256_32BIT_ARGB_IMAGE || - imageType == IcnsType.ICNS_512x512_32BIT_ARGB_IMAGE) - continue; - - int expectedSize = (imageType.getWidth()*imageType.getHeight()* - imageType.getBitsPerPixel() + 7) / 8; - byte[] imageData; - if (imageElement.data.length < expectedSize) - { - if (imageType.getBitsPerPixel() == 32) - { - imageData = Rle24Compression.decompress(imageType.getWidth(), - imageType.getHeight(), imageElement.data); - } - else - throw new ImageReadException( - "Short image data but not a 32 bit compressed type"); - } - else - imageData = imageElement.data; - - BufferedImage bufferedImage = new BufferedImage(imageType.getWidth(), - imageType.getHeight(), BufferedImage.TYPE_INT_ARGB); - switch (imageType.getBitsPerPixel()) - { - case 1: - decode1BPPImage(imageType, imageData, bufferedImage); - break; - case 4: - decode4BPPImage(imageType, imageData, bufferedImage); - break; - case 8: - decode8BPPImage(imageType, imageData, bufferedImage); - break; - case 32: - decode32BPPImage(imageType, imageData, bufferedImage); - break; - default: - throw new ImageReadException( - "Unsupported bit depth " + imageType.getBitsPerPixel()); - } - - if (maskElement != null) - { - if (maskType.getBitsPerPixel() == 1) - apply1BPPMask(maskElement.data, bufferedImage); - else if (maskType.getBitsPerPixel() == 8) - apply8BPPMask(maskElement.data, bufferedImage); - else - throw new ImageReadException("Unsupport mask bit depth " + - maskType.getBitsPerPixel()); - } - - result.add(bufferedImage); - } - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/icns/IcnsImageParser.java b/src/main/java/org/apache/sanselan/formats/icns/IcnsImageParser.java deleted file mode 100644 index 6e04248..0000000 --- a/src/main/java/org/apache/sanselan/formats/icns/IcnsImageParser.java +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.icns; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class IcnsImageParser extends ImageParser -{ - public static final int ICNS_MAGIC = IcnsType.typeAsInt("icns"); - - public IcnsImageParser() - { - super.setByteOrder(BYTE_ORDER_MSB); - } - - public String getName() - { - return "icns-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".icns"; - - private static final String ACCEPTED_EXTENSIONS[] = { - ".icns", - }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[]{ - ImageFormat.IMAGE_FORMAT_ICNS - }; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageReadException("Unknown parameter: " + firstKey); - } - - IcnsContents contents = readImage(byteSource); - ArrayList images = IcnsDecoder.decodeAllImages(contents.icnsElements); - if (images.isEmpty()) - throw new ImageReadException("No icons in ICNS file"); - BufferedImage image0 = (BufferedImage) images.get(0); - return new ImageInfo("Icns", 32, new ArrayList(), ImageFormat.IMAGE_FORMAT_ICNS, - "ICNS Apple Icon Image", image0.getHeight(), "image/x-icns", images.size(), - 0, 0, 0, 0, image0.getWidth(), false, true, false, ImageInfo.COLOR_TYPE_RGB, - ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN); - } - - public Dimension getImageSize(ByteSource byteSource, - Map params) - throws ImageReadException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageReadException("Unknown parameter: " + firstKey); - } - - IcnsContents contents = readImage(byteSource); - ArrayList images = IcnsDecoder.decodeAllImages(contents.icnsElements); - if (images.isEmpty()) - throw new ImageReadException("No icons in ICNS file"); - BufferedImage image0 = (BufferedImage) images.get(0); - return new Dimension(image0.getWidth(), image0.getHeight()); - } - - public byte[] getICCProfileBytes(ByteSource byteSource, - Map params) - throws ImageReadException, IOException - { - return null; - } - - private static class IcnsHeader - { - public final int magic; // Magic literal (4 bytes), always "icns" - public final int fileSize; // Length of file (4 bytes), in bytes. - - public IcnsHeader(final int magic, final int fileSize) - { - this.magic = magic; - this.fileSize = fileSize; - } - - public void dump(PrintWriter pw) - { - pw.println("IcnsHeader"); - pw.println("Magic: 0x" + Integer.toHexString(magic) + - " (" + IcnsType.describeType(magic) + ")"); - pw.println("FileSize: " + fileSize); - pw.println(""); - } - } - - private IcnsHeader readIcnsHeader(InputStream is) - throws ImageReadException, IOException - { - int Magic = read4Bytes("Magic", is, "Not a Valid ICNS File"); - int FileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File"); - - if (Magic != ICNS_MAGIC) - throw new ImageReadException("Not a Valid ICNS File: " + - "magic is 0x" + Integer.toHexString(Magic)); - - return new IcnsHeader(Magic, FileSize); - } - - public static class IcnsElement - { - public final int type; - public final int elementSize; - public final byte[] data; - - public IcnsElement(final int type, final int elementSize, byte[] data) - { - this.type = type; - this.elementSize = elementSize; - this.data = data; - } - - public void dump(PrintWriter pw) - { - pw.println("IcnsElement"); - IcnsType icnsType = IcnsType.findAnyType(type); - String typeDescription; - if (icnsType == null) - typeDescription = ""; - else - typeDescription = " " + icnsType.toString(); - pw.println("Type: 0x" + Integer.toHexString(type) + - " (" + IcnsType.describeType(type) + ")" + - typeDescription); - pw.println("ElementSize: " + elementSize); - pw.println(""); - } - } - - private IcnsElement readIcnsElement(InputStream is) throws ImageReadException, - IOException - { - int type = read4Bytes("Type", is, "Not a Valid ICNS File"); // Icon type (4 bytes) - int elementSize = read4Bytes("ElementSize", is, "Not a Valid ICNS File"); // Length of data (4 bytes), in bytes, including this header - byte[] data = readByteArray("Data", elementSize - 8, is, "Not a Valid ICNS File"); - - return new IcnsElement(type, elementSize, data); - } - - private static class IcnsContents - { - public final IcnsHeader icnsHeader; - public final IcnsElement icnsElements[]; - - public IcnsContents(final IcnsHeader icnsHeader, - final IcnsElement[] icnsElements) - { - super(); - this.icnsHeader = icnsHeader; - this.icnsElements = icnsElements; - } - } - - private IcnsContents readImage(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - IcnsHeader icnsHeader = readIcnsHeader(is); - - ArrayList icnsElementList = new ArrayList(); - for (int remainingSize = icnsHeader.fileSize - 8; - remainingSize > 0; ) - { - IcnsElement icnsElement = readIcnsElement(is); - icnsElementList.add(icnsElement); - remainingSize -= icnsElement.elementSize; - } - - IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()]; - for (int i = 0; i < icnsElements.length; i++) - icnsElements[i] = (IcnsElement) icnsElementList.get(i); - - return new IcnsContents(icnsHeader, icnsElements); - } - finally - { - try - { - is.close(); - } - catch (Exception e) - { - Debug.debug(e); - } - } - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - IcnsContents icnsContents = readImage(byteSource); - icnsContents.icnsHeader.dump(pw); - for (int i = 0; i < icnsContents.icnsElements.length; i++) - icnsContents.icnsElements[i].dump(pw); - return true; - } - - public final BufferedImage getBufferedImage(ByteSource byteSource, - Map params) throws ImageReadException, IOException - { - IcnsContents icnsContents = readImage(byteSource); - ArrayList result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements); - if (result.size() > 0) - return (BufferedImage) result.get(0); - else - throw new ImageReadException("No icons in ICNS file"); - } - - public ArrayList getAllBufferedImages(ByteSource byteSource) - throws ImageReadException, IOException - { - IcnsContents icnsContents = readImage(byteSource); - return IcnsDecoder.decodeAllImages(icnsContents.icnsElements); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - IcnsType imageType; - if (src.getWidth() == 16 && src.getHeight() == 16) - imageType = IcnsType.ICNS_16x16_32BIT_IMAGE; - else if (src.getWidth() == 32 && src.getHeight() == 32) - imageType = IcnsType.ICNS_32x32_32BIT_IMAGE; - else if (src.getWidth() == 48 && src.getHeight() == 48) - imageType = IcnsType.ICNS_48x48_32BIT_IMAGE; - else if (src.getWidth() == 128 && src.getHeight() == 128) - imageType = IcnsType.ICNS_128x128_32BIT_IMAGE; - else - throw new ImageWriteException("Invalid/unsupported source width " + - src.getWidth() + " and height " + src.getHeight()); - - BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_BIG_ENDIAN); - bos.write4Bytes(ICNS_MAGIC); - bos.write4Bytes(4 + 4 + 4 + 4 + 4*imageType.getWidth()*imageType.getHeight() + - 4 + 4 + imageType.getWidth()*imageType.getHeight()); - - bos.write4Bytes(imageType.getType()); - bos.write4Bytes(4 + 4 + 4*imageType.getWidth()*imageType.getHeight()); - for (int y = 0; y < src.getHeight(); y++) - { - for (int x = 0; x < src.getWidth(); x++) - { - int argb = src.getRGB(x, y); - bos.write(0); - bos.write(argb >> 16); - bos.write(argb >> 8); - bos.write(argb); - } - } - - IcnsType maskType = IcnsType.find8BPPMaskType(imageType); - bos.write4Bytes(maskType.getType()); - bos.write4Bytes(4 + 4 + imageType.getWidth()*imageType.getWidth()); - for (int y = 0; y < src.getHeight(); y++) - { - for (int x = 0; x < src.getWidth(); x++) - { - int argb = src.getRGB(x, y); - bos.write(argb >> 24); - } - } - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } -} diff --git a/src/main/java/org/apache/sanselan/formats/icns/IcnsType.java b/src/main/java/org/apache/sanselan/formats/icns/IcnsType.java deleted file mode 100644 index c1edfe7..0000000 --- a/src/main/java/org/apache/sanselan/formats/icns/IcnsType.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.icns; - -import java.io.UnsupportedEncodingException; - - -public class IcnsType -{ - private final int type; - private final int width; - private final int height; - private final int bitsPerPixel; - private final boolean hasMask; - - public static final IcnsType ICNS_16x12_1BIT_IMAGE_AND_MASK = - new IcnsType("icm#", 16, 12, 1, true); - public static final IcnsType ICNS_16x12_4BIT_IMAGE = - new IcnsType("icm4", 16, 12, 4, false); - public static final IcnsType ICNS_16x12_8BIT_IMAGE = - new IcnsType("icm8", 16, 12, 8, false); - - public static final IcnsType ICNS_16x16_8BIT_MASK = - new IcnsType("s8mk", 16, 16, 8, true); - public static final IcnsType ICNS_16x16_1BIT_IMAGE_AND_MASK = - new IcnsType("ics#", 16, 16, 1, true); - public static final IcnsType ICNS_16x16_4BIT_IMAGE = - new IcnsType("ics4", 16, 16, 4, false); - public static final IcnsType ICNS_16x16_8BIT_IMAGE = - new IcnsType("ics8", 16, 16, 8, false); - public static final IcnsType ICNS_16x16_32BIT_IMAGE = - new IcnsType("is32", 16, 16, 32, false); - - public static final IcnsType ICNS_32x32_8BIT_MASK = - new IcnsType("l8mk", 32, 32, 8, true); - public static final IcnsType ICNS_32x32_1BIT_IMAGE_AND_MASK = - new IcnsType("ICN#", 32, 32, 1, true); - public static final IcnsType ICNS_32x32_4BIT_IMAGE = - new IcnsType("icl4", 32, 32, 4, false); - public static final IcnsType ICNS_32x32_8BIT_IMAGE = - new IcnsType("icl8", 32, 32, 8, false); - public static final IcnsType ICNS_32x32_32BIT_IMAGE = - new IcnsType("il32", 32, 32, 32, false); - - public static final IcnsType ICNS_48x48_8BIT_MASK = - new IcnsType("h8mk", 48, 48, 8, true); - public static final IcnsType ICNS_48x48_1BIT_IMAGE_AND_MASK = - new IcnsType("ich#", 48, 48, 1, true); - public static final IcnsType ICNS_48x48_4BIT_IMAGE = - new IcnsType("ich4", 48, 48, 4, false); - public static final IcnsType ICNS_48x48_8BIT_IMAGE = - new IcnsType("ich8", 48, 48, 8, false); - public static final IcnsType ICNS_48x48_32BIT_IMAGE = - new IcnsType("ih32", 48, 48, 32, false); - - public static final IcnsType ICNS_128x128_8BIT_MASK = - new IcnsType("t8mk", 128, 128, 8, true); - public static final IcnsType ICNS_128x128_32BIT_IMAGE = - new IcnsType("it32", 128, 128, 32, false); - - public static final IcnsType ICNS_256x256_32BIT_ARGB_IMAGE = - new IcnsType("ic08", 256, 256, 32, false); - - public static final IcnsType ICNS_512x512_32BIT_ARGB_IMAGE = - new IcnsType("ic09", 512, 512, 32, false); - - private static final IcnsType[] allImageTypes = - { - ICNS_16x12_1BIT_IMAGE_AND_MASK, ICNS_16x12_4BIT_IMAGE, ICNS_16x12_8BIT_IMAGE, - ICNS_16x16_1BIT_IMAGE_AND_MASK, ICNS_16x16_4BIT_IMAGE, ICNS_16x16_8BIT_IMAGE, ICNS_16x16_32BIT_IMAGE, - ICNS_32x32_1BIT_IMAGE_AND_MASK, ICNS_32x32_4BIT_IMAGE, ICNS_32x32_8BIT_IMAGE, ICNS_32x32_32BIT_IMAGE, - ICNS_48x48_1BIT_IMAGE_AND_MASK, ICNS_48x48_4BIT_IMAGE, ICNS_48x48_8BIT_IMAGE, ICNS_48x48_32BIT_IMAGE, - ICNS_128x128_32BIT_IMAGE, - ICNS_256x256_32BIT_ARGB_IMAGE, - ICNS_512x512_32BIT_ARGB_IMAGE - }; - - private static final IcnsType[] allMaskTypes = - { - ICNS_16x12_1BIT_IMAGE_AND_MASK, - ICNS_16x16_1BIT_IMAGE_AND_MASK, ICNS_16x16_8BIT_MASK, - ICNS_32x32_1BIT_IMAGE_AND_MASK, ICNS_32x32_8BIT_MASK, - ICNS_48x48_1BIT_IMAGE_AND_MASK, ICNS_48x48_8BIT_MASK, - ICNS_128x128_8BIT_MASK - }; - - private IcnsType(String type, int width, int height, int bitsPerPixel, boolean hasMask) - { - this.type = typeAsInt(type); - this.width = width; - this.height = height; - this.bitsPerPixel = bitsPerPixel; - this.hasMask = hasMask; - } - - public int getType() - { - return type; - } - - public int getWidth() - { - return width; - } - - public int getHeight() - { - return height; - } - - public int getBitsPerPixel() - { - return bitsPerPixel; - } - - public boolean hasMask() - { - return hasMask; - } - - public String toString() - { - return getClass().getName() + "[" + - "width=" + width + "," + - "height=" + height + "," + - "bpp=" + bitsPerPixel + "," + - "hasMask=" + hasMask + "]"; - } - - public static IcnsType findAnyType(int type) - { - for (int i = 0; i < allImageTypes.length; i++) - { - if (allImageTypes[i].getType() == type) - return allImageTypes[i]; - } - for (int i = 0; i < allMaskTypes.length; i++) - { - if (allMaskTypes[i].getType() == type) - return allMaskTypes[i]; - } - return null; - } - - public static IcnsType findImageType(int type) - { - for (int i = 0; i < allImageTypes.length; i++) - { - if (allImageTypes[i].getType() == type) - return allImageTypes[i]; - } - return null; - } - - public static IcnsType find8BPPMaskType(IcnsType imageType) - { - for (int i = 0; i < allMaskTypes.length; i++) - { - if (allMaskTypes[i].getBitsPerPixel() == 8 && - allMaskTypes[i].getWidth() == imageType.getWidth() && - allMaskTypes[i].getHeight() == imageType.getHeight()) - { - return allMaskTypes[i]; - } - } - return null; - } - - public static IcnsType find1BPPMaskType(IcnsType imageType) - { - for (int i = 0; i < allMaskTypes.length; i++) - { - if (allMaskTypes[i].getBitsPerPixel() == 1 && - allMaskTypes[i].getWidth() == imageType.getWidth() && - allMaskTypes[i].getHeight() == imageType.getHeight()) - { - return allMaskTypes[i]; - } - } - return null; - } - - public static int typeAsInt(String type) - { - byte[] bytes = null; - try - { - bytes = type.getBytes("US-ASCII"); - } - catch (UnsupportedEncodingException cannotHappen) - { - } - if (bytes.length != 4) - throw new IllegalArgumentException("Invalid ICNS type"); - return ((0xff & bytes[0]) << 24) | - ((0xff & bytes[1]) << 16) | - ((0xff & bytes[2]) << 8) | - (0xff & bytes[3]); - } - - public static String describeType(int type) - { - byte[] bytes = new byte[4]; - bytes[0] = (byte)(0xff & (type >> 24)); - bytes[1] = (byte)(0xff & (type >> 16)); - bytes[2] = (byte)(0xff & (type >> 8)); - bytes[3] = (byte)(0xff & type); - try - { - return new String(bytes, "US-ASCII"); - } - catch (UnsupportedEncodingException cannotHappen) - { - } - return null; - } -} diff --git a/src/main/java/org/apache/sanselan/formats/icns/Rle24Compression.java b/src/main/java/org/apache/sanselan/formats/icns/Rle24Compression.java deleted file mode 100644 index 5fee4e6..0000000 --- a/src/main/java/org/apache/sanselan/formats/icns/Rle24Compression.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.icns; - -class Rle24Compression -{ - public static byte[] decompress(int width, int height, byte[] data) - { - final int pixelCount = width * height; - final byte[] result = new byte[4 * pixelCount]; - - // Several ICNS parsers advance by 4 bytes here: - // http://code.google.com/p/icns2png/ - when the width is >= 128 - // http://icns.sourceforge.net/ - when those 4 bytes are all zero - // - // A scan of all .icns files on MacOS shows that - // all 128x128 images indeed start with 4 zeroes, - // while all smaller images don't. - // However it is dangerous to assume - // that 4 initial zeroes always need to be skipped, - // because they could encode valid pixels on smaller images. - // So always skip on 128x128, and never skip on anything else. - int dataPos = 0; - if (width >= 128 && height >= 128) - dataPos = 4; - - // argb, band by band in 3 passes, with no alpha - for (int band = 1; band <= 3; band++) - { - int remaining = pixelCount; - int resultPos = 0; - while (remaining > 0) - { - if ((data[dataPos] & 0x80) != 0) - { - int count = (0xff & data[dataPos]) - 125; - for (int i = 0; i < count; i++) - result[band + 4*(resultPos++)] = data[dataPos + 1]; - dataPos += 2; - remaining -= count; - } - else - { - int count = (0xff & data[dataPos]) + 1; - dataPos++; - for (int i = 0; i < count; i++) - result[band + 4*(resultPos++)] = data[dataPos++]; - remaining -= count; - } - } - } - return result; - } -} diff --git a/src/main/java/org/apache/sanselan/formats/ico/IcoImageParser.java b/src/main/java/org/apache/sanselan/formats/ico/IcoImageParser.java deleted file mode 100644 index 2b98a59..0000000 --- a/src/main/java/org/apache/sanselan/formats/ico/IcoImageParser.java +++ /dev/null @@ -1,793 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.ico; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.Sanselan; -import org.apache.sanselan.formats.bmp.BmpImageParser; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.palette.PaletteFactory; -import org.apache.sanselan.palette.SimplePalette; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class IcoImageParser extends ImageParser -{ - - public IcoImageParser() - { - super.setByteOrder(BYTE_ORDER_LSB); - } - - public String getName() - { - return "ico-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".ico"; - - private static final String ACCEPTED_EXTENSIONS[] = { - ".ico", ".cur", - }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[]{ - ImageFormat.IMAGE_FORMAT_ICO, // - }; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public Dimension getImageSize(ByteSource byteSource, - Map params) - throws ImageReadException, IOException - { - return null; - } - - public byte[] getICCProfileBytes(ByteSource byteSource, - Map params) - throws ImageReadException, IOException - { - return null; - } - - private static class FileHeader - { - public final int reserved; // Reserved (2 bytes), always 0 - public final int iconType; // IconType (2 bytes), if the image is an icon it?s 1, for cursors the value is 2. - public final int iconCount; //IconCount (2 bytes), number of icons in this file. - - public FileHeader(final int reserved, final int iconType, - final int iconCount) - { - this.reserved = reserved; - this.iconType = iconType; - this.iconCount = iconCount; - } - - public void dump(PrintWriter pw) - { - pw.println("FileHeader"); - pw.println("Reserved: " + reserved); - pw.println("IconType: " + iconType); - pw.println("IconCount: " + iconCount); - pw.println(); - } - } - - private FileHeader readFileHeader(InputStream is) - throws ImageReadException, IOException - { - int Reserved = read2Bytes("Reserved", is, "Not a Valid ICO File"); - int IconType = read2Bytes("IconType", is, "Not a Valid ICO File"); - int IconCount = read2Bytes("IconCount", is, "Not a Valid ICO File"); - - if (Reserved != 0) - throw new ImageReadException("Not a Valid ICO File: reserved is " + Reserved); - if (IconType != 1 && IconType != 2) - throw new ImageReadException("Not a Valid ICO File: icon type is " + IconType); - - return new FileHeader(Reserved, IconType, IconCount); - - } - - private static class IconInfo - { - public final byte Width; - public final byte Height; - public final byte ColorCount; - public final byte Reserved; - public final int Planes; - public final int BitCount; - public final int ImageSize; - public final int ImageOffset; - - public IconInfo(final byte width, final byte height, - final byte colorCount, final byte reserved, final int planes, - final int bitCount, final int imageSize, final int imageOffset) - { - Width = width; - Height = height; - ColorCount = colorCount; - Reserved = reserved; - Planes = planes; - BitCount = bitCount; - ImageSize = imageSize; - ImageOffset = imageOffset; - } - - public void dump(PrintWriter pw) - { - pw.println("IconInfo"); - pw.println("Width: " + Width); - pw.println("Height: " + Height); - pw.println("ColorCount: " + ColorCount); - pw.println("Reserved: " + Reserved); - pw.println("Planes: " + Planes); - pw.println("BitCount: " + BitCount); - pw.println("ImageSize: " + ImageSize); - pw.println("ImageOffset: " + ImageOffset); - } - } - - private IconInfo readIconInfo(InputStream is) throws ImageReadException, - IOException - { - byte Width = readByte("Width", is, "Not a Valid ICO File"); // Width (1 byte), Width of Icon (1 to 255) - byte Height = readByte("Height", is, "Not a Valid ICO File"); // Height (1 byte), Height of Icon (1 to 255) - byte ColorCount = readByte("ColorCount", is, "Not a Valid ICO File"); // ColorCount (1 byte), Number of colors, either 0 for 24 bit or higher, 2 for monochrome or 16 for 16 color images. - byte Reserved = readByte("Reserved", is, "Not a Valid ICO File"); // Reserved (1 byte), Not used (always 0) - int Planes = read2Bytes("Planes", is, "Not a Valid ICO File"); // Planes (2 bytes), always 1 - int BitCount = read2Bytes("BitCount", is, "Not a Valid ICO File"); // BitCount (2 bytes), number of bits per pixel (1 for monochrome, 4 for 16 colors, 8 for 256 colors, 24 for true colors, 32 for true colors + alpha channel) - int ImageSize = read4Bytes("ImageSize", is, "Not a Valid ICO File"); // ImageSize (4 bytes), Length of resource in bytes - int ImageOffset = read4Bytes("ImageOffset", is, "Not a Valid ICO File"); // ImageOffset (4 bytes), start of the image in the file. - - return new IconInfo(Width, Height, ColorCount, Reserved, Planes, - BitCount, ImageSize, ImageOffset); - } - - private static class BitmapHeader - { - public final int Size; - public final int Width; - public final int Height; - public final int Planes; - public final int BitCount; - public final int Compression; - public final int SizeImage; - public final int XPelsPerMeter; - public final int YPelsPerMeter; - public final int ColorsUsed; - public final int ColorsImportant; - - public BitmapHeader(final int size, final int width, final int height, - final int planes, final int bitCount, final int compression, - final int sizeImage, final int pelsPerMeter, - final int pelsPerMeter2, final int colorsUsed, - final int colorsImportant) - { - Size = size; - Width = width; - Height = height; - Planes = planes; - BitCount = bitCount; - Compression = compression; - SizeImage = sizeImage; - XPelsPerMeter = pelsPerMeter; - YPelsPerMeter = pelsPerMeter2; - ColorsUsed = colorsUsed; - ColorsImportant = colorsImportant; - } - - public void dump(PrintWriter pw) - { - pw.println("BitmapHeader"); - - pw.println("Size: " + Size); - pw.println("Width: " + Width); - pw.println("Height: " + Height); - pw.println("Planes: " + Planes); - pw.println("BitCount: " + BitCount); - pw.println("Compression: " + Compression); - pw.println("SizeImage: " + SizeImage); - pw.println("XPelsPerMeter: " + XPelsPerMeter); - pw.println("YPelsPerMeter: " + YPelsPerMeter); - pw.println("ColorsUsed: " + ColorsUsed); - pw.println("ColorsImportant: " + ColorsImportant); - } - } - - private static abstract class IconData - { - public final IconInfo iconInfo; - - public IconData(final IconInfo iconInfo) - { - this.iconInfo = iconInfo; - } - - public void dump(PrintWriter pw) - { - iconInfo.dump(pw); - pw.println(); - dumpSubclass(pw); - } - - protected abstract void dumpSubclass(PrintWriter pw); - public abstract BufferedImage readBufferedImage() throws ImageReadException; - } - - private static class BitmapIconData extends IconData - { - public final BitmapHeader header; - public final BufferedImage bufferedImage; - - public BitmapIconData(final IconInfo iconInfo, final BitmapHeader header, - final BufferedImage bufferedImage) - { - super(iconInfo); - this.header = header; - this.bufferedImage = bufferedImage; - } - - public BufferedImage readBufferedImage() throws ImageReadException - { - return bufferedImage; - } - - protected void dumpSubclass(PrintWriter pw) - { - pw.println("BitmapIconData"); - header.dump(pw); - pw.println(); - } - } - - private static class PNGIconData extends IconData - { - public final BufferedImage bufferedImage; - - public PNGIconData(final IconInfo iconInfo, final BufferedImage bufferedImage) - { - super(iconInfo); - this.bufferedImage = bufferedImage; - } - - public BufferedImage readBufferedImage() - { - return bufferedImage; - } - - protected void dumpSubclass(PrintWriter pw) - { - pw.println("PNGIconData"); - pw.println(); - } - } - - private IconData readBitmapIconData(byte[] iconData, IconInfo fIconInfo) throws ImageReadException, ImageWriteException, IOException - { - ByteArrayInputStream is = new ByteArrayInputStream(iconData); - int Size = read4Bytes("Size", is, "Not a Valid ICO File"); // Size (4 bytes), size of this structure (always 40) - int Width = read4Bytes("Width", is, "Not a Valid ICO File"); // Width (4 bytes), width of the image (same as iconinfo.width) - int Height = read4Bytes("Height", is, "Not a Valid ICO File"); // Height (4 bytes), scanlines in the color map + transparent map (iconinfo.height * 2) - int Planes = read2Bytes("Planes", is, "Not a Valid ICO File"); // Planes (2 bytes), always 1 - int BitCount = read2Bytes("BitCount", is, "Not a Valid ICO File"); // BitCount (2 bytes), 1,4,8,16,24,32 (see iconinfo for details) - int Compression = read4Bytes("Compression", is, "Not a Valid ICO File"); // Compression (4 bytes), we don?t use this (0) - int SizeImage = read4Bytes("SizeImage", is, "Not a Valid ICO File"); // SizeImage (4 bytes), we don?t use this (0) - int XPelsPerMeter = read4Bytes("XPelsPerMeter", is, - "Not a Valid ICO File"); // XPelsPerMeter (4 bytes), we don?t use this (0) - int YPelsPerMeter = read4Bytes("YPelsPerMeter", is, - "Not a Valid ICO File"); // YPelsPerMeter (4 bytes), we don?t use this (0) - int ColorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid ICO File"); // ColorsUsed (4 bytes), we don?t use this (0) - int ColorsImportant = read4Bytes("ColorsImportant", is, - "Not a Valid ICO File"); // ColorsImportant (4 bytes), we don?t use this (0) - - if (Size != 40) - throw new ImageReadException("Not a Valid ICO File: Wrong bitmap header size " + Size); - if (Planes != 1) - throw new ImageReadException("Not a Valid ICO File: Planes can't be " + Planes); - - BitmapHeader header = new BitmapHeader(Size, Width, Height, Planes, - BitCount, Compression, SizeImage, XPelsPerMeter, YPelsPerMeter, - ColorsUsed, ColorsImportant); - - int bitmapPixelsOffset = 14 + 40 + ((Compression == 3) ? 3*4 : 0) + - 4 * ((ColorsUsed == 0 && BitCount <= 8) ? (1 << BitCount) : ColorsUsed); - int bitmapSize = 14 + iconData.length; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(14 + iconData.length); - BinaryOutputStream bos = new BinaryOutputStream(baos, - BinaryOutputStream.BYTE_ORDER_LITTLE_ENDIAN); - - bos.write('B'); - bos.write('M'); - bos.write4Bytes(bitmapSize); - bos.write4Bytes(0); - bos.write4Bytes(bitmapPixelsOffset); - - bos.write4Bytes(Size); - bos.write4Bytes(Width); - bos.write4Bytes(Height / 2); - bos.write2Bytes(Planes); - bos.write2Bytes(BitCount); - bos.write4Bytes(Compression); - bos.write4Bytes(SizeImage); - bos.write4Bytes(XPelsPerMeter); - bos.write4Bytes(YPelsPerMeter); - bos.write4Bytes(ColorsUsed); - bos.write4Bytes(ColorsImportant); - bos.write(iconData, 40, iconData.length - 40); - bos.flush(); - - ByteArrayInputStream bmpInputStream = new ByteArrayInputStream(baos.toByteArray()); - BufferedImage bmpImage = new BmpImageParser().getBufferedImage(bmpInputStream, null); - - // Transparency map is optional with 32 BPP icons, because they already have - // an alpha channel, and Windows only uses the transparency map when it has to - // display the icon on a < 32 BPP screen. But it's still used instead of alpha - // if the image would be completely transparent with alpha... - int t_scanline_size = (Width + 7) / 8; - if ((t_scanline_size % 4) != 0) - t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 byte size. - int tcolor_map_size_bytes = t_scanline_size * (Height/2); - byte[] transparency_map = null; - try - { - transparency_map = this.readByteArray("transparency_map", - tcolor_map_size_bytes, bmpInputStream, "Not a Valid ICO File"); - } - catch (IOException ioEx) - { - if (BitCount != 32) - throw ioEx; - } - - // FIXME: get BmpImageParser to support alpha, then uncomment below -// boolean allAlphasZero = true; -// if (BitCount == 32) -// { -// for (int y = 0; allAlphasZero && y < bmpImage.getHeight(); y++) -// { -// for (int x = 0; x < bmpImage.getWidth(); x++) -// { -// if ((bmpImage.getRGB(x, y) & 0xff000000) != 0) -// { -// allAlphasZero = false; -// break; -// } -// } -// } -// } - BufferedImage resultImage = new BufferedImage(bmpImage.getWidth(), bmpImage.getHeight(), - BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < resultImage.getHeight(); y++) - { - for (int x = 0; x < resultImage.getWidth(); x++) - { - int alpha = 0xff; - if (transparency_map != null) - { - int alpha_byte = 0xff & transparency_map[t_scanline_size - * (bmpImage.getHeight() - y - 1) + (x / 8)]; - alpha = 0x01 & (alpha_byte >> (7 - (x % 8))); - alpha = (alpha == 0) ? 0xff : 0x00; - } - // FIXME: get the BMP decoder to support alpha, then uncomment below - //if (BitCount < 32 || allAlphasZero) - resultImage.setRGB(x, y, (alpha << 24) | (0xffffff & bmpImage.getRGB(x, y))); - } - } - return new BitmapIconData(fIconInfo, header, resultImage); - } - - private IconData readIconData(byte[] iconData, IconInfo fIconInfo) - throws ImageReadException, IOException - { - ImageFormat imageFormat = Sanselan.guessFormat(iconData); - if (imageFormat.equals(ImageFormat.IMAGE_FORMAT_PNG)) - { - BufferedImage bufferedImage = Sanselan.getBufferedImage(iconData); - PNGIconData pngIconData = new PNGIconData(fIconInfo, bufferedImage); - return pngIconData; - } - else - { - try - { - return readBitmapIconData(iconData, fIconInfo); - } - catch (ImageWriteException imageWriteException) - { - IOException ioe = new IOException(); - ioe.initCause(imageWriteException); - throw ioe; - } - } - } - - private static class ImageContents - { - public final FileHeader fileHeader; - public final IconData iconDatas[]; - - public ImageContents(final FileHeader fileHeader, - final IconData[] iconDatas) - { - super(); - this.fileHeader = fileHeader; - this.iconDatas = iconDatas; - } - } - - private ImageContents readImage(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - FileHeader fileHeader = readFileHeader(is); - - IconInfo fIconInfos[] = new IconInfo[fileHeader.iconCount]; - for (int i = 0; i < fileHeader.iconCount; i++) - { - fIconInfos[i] = readIconInfo(is); - } - - IconData fIconDatas[] = new IconData[fileHeader.iconCount]; - for (int i = 0; i < fileHeader.iconCount; i++) - { - byte[] iconData = byteSource.getBlock(fIconInfos[i].ImageOffset, - fIconInfos[i].ImageSize); - fIconDatas[i] = readIconData(iconData, fIconInfos[i]); - } - - return new ImageContents(fileHeader, fIconDatas); - } - finally - { - try - { - if (is != null) { - is.close(); - } - } - catch (Exception e) - { - Debug.debug(e); - } - - } - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - ImageContents contents = readImage(byteSource); - contents.fileHeader.dump(pw); - for (int i = 0; i < contents.iconDatas.length; i++) - contents.iconDatas[i].dump(pw); - return true; - } - - public final BufferedImage getBufferedImage(ByteSource byteSource, - Map params) throws ImageReadException, IOException - { - ImageContents contents = readImage(byteSource); - FileHeader fileHeader = contents.fileHeader; - if (fileHeader.iconCount > 0) - return contents.iconDatas[0].readBufferedImage(); - else - throw new ImageReadException("No icons in ICO file"); - } - - public ArrayList getAllBufferedImages(ByteSource byteSource) - throws ImageReadException, IOException - { - ArrayList result = new ArrayList(); - ImageContents contents = readImage(byteSource); - - FileHeader fileHeader = contents.fileHeader; - for (int i = 0; i < fileHeader.iconCount; i++) - { - IconData iconData = contents.iconDatas[i]; - - BufferedImage image = iconData.readBufferedImage(); - - result.add(image); - } - - return result; - } - - // public boolean extractImages(ByteSource byteSource, File dst_dir, - // String dst_root, ImageParser encoder) throws ImageReadException, - // IOException, ImageWriteException - // { - // ImageContents contents = readImage(byteSource); - // - // FileHeader fileHeader = contents.fileHeader; - // for (int i = 0; i < fileHeader.iconCount; i++) - // { - // IconData iconData = contents.iconDatas[i]; - // - // BufferedImage image = readBufferedImage(iconData); - // - // int size = Math.max(iconData.iconInfo.Width, - // iconData.iconInfo.Height); - // File file = new File(dst_dir, dst_root + "_" + size + "_" - // + iconData.iconInfo.BitCount - // + encoder.getDefaultExtension()); - // encoder.writeImage(image, new FileOutputStream(file), null); - // } - // - // return true; - // } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = (params == null) ? new HashMap() : new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - final PaletteFactory paletteFactory = new PaletteFactory(); - final SimplePalette palette = paletteFactory.makePaletteSimple(src, 256); - final int bitCount; - final boolean hasTransparency = paletteFactory.hasTransparency(src); - if (palette == null) - { - if (hasTransparency) - bitCount = 32; - else - bitCount = 24; - } - else if (palette.length() <= 2) - bitCount = 1; - else if (palette.length() <= 16) - bitCount = 4; - else - bitCount = 8; - - BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_INTEL); - - int scanline_size = (bitCount * src.getWidth() + 7) / 8; - if ((scanline_size % 4) != 0) - scanline_size += 4 - (scanline_size % 4); // pad scanline to 4 byte size. - int t_scanline_size = (src.getWidth() + 7) / 8; - if ((t_scanline_size % 4) != 0) - t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 byte size. - int imageSize = 40 + 4 * (bitCount <= 8 ? (1 << bitCount) : 0) + - src.getHeight() * scanline_size + - src.getHeight() * t_scanline_size; - - // ICONDIR - bos.write2Bytes(0); // reserved - bos.write2Bytes(1); // 1=ICO, 2=CUR - bos.write2Bytes(1); // count - - // ICONDIRENTRY - int iconDirEntryWidth = src.getWidth(); - int iconDirEntryHeight = src.getHeight(); - if (iconDirEntryWidth > 255 || iconDirEntryHeight > 255) - { - iconDirEntryWidth = 0; - iconDirEntryHeight = 0; - } - bos.write(iconDirEntryWidth); - bos.write(iconDirEntryHeight); - bos.write((bitCount >= 8) ? 0 : (1 << bitCount)); - bos.write(0); // reserved - bos.write2Bytes(1); // color planes - bos.write2Bytes(bitCount); - bos.write4Bytes(imageSize); - bos.write4Bytes(22); // image offset - - // BITMAPINFOHEADER - bos.write4Bytes(40); // size - bos.write4Bytes(src.getWidth()); - bos.write4Bytes(2 * src.getHeight()); - bos.write2Bytes(1); // planes - bos.write2Bytes(bitCount); - bos.write4Bytes(0); // compression - bos.write4Bytes(0); // image size - bos.write4Bytes(0); // x pixels per meter - bos.write4Bytes(0); // y pixels per meter - bos.write4Bytes(0); // colors used, 0 = (1 << bitCount) (ignored) - bos.write4Bytes(0); // colors important - - if (palette != null) - { - for (int i = 0; i < (1 << bitCount); i++) - { - if (i < palette.length()) - { - int argb = palette.getEntry(i); - bos.write(0xff & argb); - bos.write(0xff & (argb >> 8)); - bos.write(0xff & (argb >> 16)); - bos.write(0); - } - else - { - bos.write(0); - bos.write(0); - bos.write(0); - bos.write(0); - } - } - } - - int bit_cache = 0; - int bits_in_cache = 0; - int row_padding = scanline_size - (bitCount * src.getWidth() + 7) / 8; - for (int y = src.getHeight() - 1; y >= 0; y--) - { - for (int x = 0; x < src.getWidth(); x++) - { - int argb = src.getRGB(x, y); - if (bitCount < 8) - { - int rgb = 0xffffff & argb; - int index = palette.getPaletteIndex(rgb); - bit_cache <<= bitCount; - bit_cache |= index; - bits_in_cache += bitCount; - if (bits_in_cache >= 8) - { - bos.write(0xff & bit_cache); - bit_cache = 0; - bits_in_cache = 0; - } - } - else if (bitCount == 8) - { - int rgb = 0xffffff & argb; - int index = palette.getPaletteIndex(rgb); - bos.write(0xff & index); - } - else if (bitCount == 24) - { - bos.write(0xff & argb); - bos.write(0xff & (argb >> 8)); - bos.write(0xff & (argb >> 16)); - } - else if (bitCount == 32) - { - bos.write(0xff & argb); - bos.write(0xff & (argb >> 8)); - bos.write(0xff & (argb >> 16)); - bos.write(0xff & (argb >> 24)); - } - } - - if (bits_in_cache > 0) - { - bit_cache <<= (8 - bits_in_cache); - bos.write(0xff & bit_cache); - bit_cache = 0; - bits_in_cache = 0; - } - - for (int x = 0; x < row_padding; x++) - bos.write(0); - } - - int t_row_padding = t_scanline_size - (src.getWidth() + 7) / 8; - for (int y = src.getHeight() - 1; y >= 0; y--) - { - for (int x = 0; x < src.getWidth(); x++) - { - int argb = src.getRGB(x, y); - int alpha = 0xff & (argb >> 24); - bit_cache <<= 1; - if (alpha == 0) - bit_cache |= 1; - bits_in_cache++; - if (bits_in_cache >= 8) - { - bos.write(0xff & bit_cache); - bit_cache = 0; - bits_in_cache = 0; - } - } - - if (bits_in_cache > 0) - { - bit_cache <<= (8 - bits_in_cache); - bos.write(0xff & bit_cache); - bit_cache = 0; - bits_in_cache = 0; - } - - for (int x = 0; x < t_row_padding; x++) - bos.write(0); - } - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - return null; - } -} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/JpegConstants.java b/src/main/java/org/apache/sanselan/formats/jpeg/JpegConstants.java deleted file mode 100644 index 2f73822..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/JpegConstants.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg; - -public interface JpegConstants -{ - public static final int MAX_SEGMENT_SIZE = 0xffff; - - public static final byte JFIF0_SIGNATURE[] = new byte[] { // - 0x4a, // J - 0x46, // F - 0x49, // I - 0x46, // F - 0x0, // - }; - public static final byte JFIF0_SIGNATURE_ALTERNATIVE[] = new byte[] { // - 0x4a, // J - 0x46, // F - 0x49, // I - 0x46, // F - 0x20, // - }; - - public static final byte EXIF_IDENTIFIER_CODE[] = { // - 0x45, // E - 0x78, // x - 0x69, // i - 0x66, // f - }; - - public static final byte XMP_IDENTIFIER[] = { // - 0x68, // h - 0x74, // t - 0x74, // t - 0x70, // p - 0x3A, // : - 0x2F, // / - 0x2F, // / - 0x6E, // n - 0x73, // s - 0x2E, // . - 0x61, // a - 0x64, // d - 0x6F, // o - 0x62, // b - 0x65, // e - 0x2E, // . - 0x63, // c - 0x6F, // o - 0x6D, // m - 0x2F, // / - 0x78, // x - 0x61, // a - 0x70, // p - 0x2F, // / - 0x31, // 1 - 0x2E, // . - 0x30, // 0 - 0x2F, // / - 0, // 0-terminated us-ascii string. - }; - - public static final byte SOI[] = new byte[] { (byte) 0xff, (byte) 0xd8 }; - public static final byte EOI[] = new byte[] { (byte) 0xff, (byte) 0xd9 }; - - public static final int SOS_Marker = (0xff00) | (0xda); - - public static final int JPEG_APP0 = 0xE0; - // public static final int JPEG_APP1 = JPEG_APP0 + 1; - // public static final int JPEG_APP1_Marker = (0xff00) | JPEG_APP1; - public static final int JPEG_APP0_Marker = (0xff00) | (JPEG_APP0); - public static final int JPEG_APP1_Marker = (0xff00) | (JPEG_APP0 + 1); - // public static final int JPEG_APP2 = ; - public static final int JPEG_APP2_Marker = (0xff00) | (JPEG_APP0 + 2); - public static final int JPEG_APP13_Marker = (0xff00) | (JPEG_APP0 + 13); - public static final int JPEG_APP14_Marker = (0xff00) | (JPEG_APP0 + 14); - public static final int JPEG_APP15_Marker = (0xff00) | (JPEG_APP0 + 15); - - public static final int JFIFMarker = 0xFFE0; - public static final int SOF0Marker = 0xFFc0; - public static final int SOF1Marker = 0xFFc0 + 0x1; - public static final int SOF2Marker = 0xFFc0 + 0x2; - public static final int SOF3Marker = 0xFFc0 + 0x3; - public static final int SOF4Marker = 0xFFc0 + 0x4; - public static final int SOF5Marker = 0xFFc0 + 0x5; - public static final int SOF6Marker = 0xFFc0 + 0x6; - public static final int SOF7Marker = 0xFFc0 + 0x7; - public static final int SOF8Marker = 0xFFc0 + 0x8; - public static final int SOF9Marker = 0xFFc0 + 0x9; - public static final int SOF10Marker = 0xFFc0 + 0xa; - public static final int SOF11Marker = 0xFFc0 + 0xb; - public static final int SOF12Marker = 0xFFc0 + 0xc; - public static final int SOF13Marker = 0xFFc0 + 0xd; - public static final int SOF14Marker = 0xFFc0 + 0xe; - public static final int SOF15Marker = 0xFFc0 + 0xf; - - public static final int MARKERS[] = { SOS_Marker, JPEG_APP0, - JPEG_APP0_Marker, JPEG_APP1_Marker, JPEG_APP2_Marker, - JPEG_APP13_Marker, JPEG_APP14_Marker, JPEG_APP15_Marker, - JFIFMarker, SOF0Marker, SOF1Marker, SOF2Marker, SOF3Marker, - SOF4Marker, SOF5Marker, SOF6Marker, SOF7Marker, SOF8Marker, - SOF9Marker, SOF10Marker, SOF11Marker, SOF12Marker, SOF13Marker, - SOF14Marker, SOF15Marker, }; - - public static final byte icc_profile_label[] = { 0x49, 0x43, 0x43, 0x5F, - 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x0 }; - - public static final byte PHOTOSHOP_IDENTIFICATION_STRING[] = { // - 0x50, // P - 0x68, // h - 0x6F, // o - 0x74, // t - 0x6F, // o - 0x73, // s - 0x68, // h - 0x6F, // o - 0x70, // p - 0x20, // - 0x33, // 3 - 0x2E, // . - 0x30, // 0 - 0, - }; - public static final byte CONST_8BIM[] = { // - 0x38, // 8 - 0x42, // B - 0x49, // I - 0x4D, // M - }; - -} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageMetadata.java b/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageMetadata.java deleted file mode 100644 index 7211772..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageMetadata.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg; - -import java.io.IOException; -import java.util.ArrayList; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.Sanselan; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.TiffImageMetadata; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class JpegImageMetadata implements IImageMetadata { - private final JpegPhotoshopMetadata photoshop; - private final TiffImageMetadata exif; - - public JpegImageMetadata(final JpegPhotoshopMetadata photoshop, - final TiffImageMetadata exif) { - this.photoshop = photoshop; - this.exif = exif; - } - - public TiffImageMetadata getExif() { - return exif; - } - - public JpegPhotoshopMetadata getPhotoshop() { - return photoshop; - } - - public TiffField findEXIFValue(TagInfo tagInfo) { - TiffField field = findEXIFValue(tagInfo, true); - if (field == null) { - // In some cases, we want an exact directory match (such as GPS values). - // In other cases, we are more permissive (ie. with tags that may appear - // in a number of different directories, depending on the camera manufacturer, etc. - // TODO: Modify TagInfo class to include a "permissive/exact" flag. - field = findEXIFValue(tagInfo, false); - } - return field; - } - - public TiffField findEXIFValueWithExactMatch(TagInfo tagInfo) { - return findEXIFValue(tagInfo, true); - } - - private TiffField findEXIFValue(TagInfo tagInfo, boolean requireDirectoryMatch) { - ArrayList items = getItems(); - for (int i = 0; i < items.size(); i++) { - Object o = items.get(i); - if (!(o instanceof TiffImageMetadata.Item)) - continue; - - TiffImageMetadata.Item item = (TiffImageMetadata.Item) o; - TiffField field = item.getTiffField(); - if (requireDirectoryMatch && - (field.directoryType != tagInfo.directoryType.directoryType)) { - continue; - } - if (field.tag == tagInfo.tag) - return field; - } - - return null; - } - - /** - * Returns the size of the first JPEG thumbnail found in the EXIF metadata. - * - * @return Thumbnail width and height or null if no thumbnail. - * @throws ImageReadException - * @throws IOException - */ - public Dimension getEXIFThumbnailSize() throws ImageReadException, IOException { - byte[] data = getEXIFThumbnailData(); - - if( data != null ){ - return Sanselan.getImageSize(data); - } - return null; - } - - /** - * Returns the data of the first JPEG thumbnail found in the EXIF metadata. - * - * @return JPEG data or null if no thumbnail. - * @throws ImageReadException - * @throws IOException - */ - public byte[] getEXIFThumbnailData() throws ImageReadException, IOException { - ArrayList dirs = exif.getDirectories(); - for (int i = 0; i < dirs.size(); i++) { - TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) dirs - .get(i); - - byte[] data = null; - if( dir.getJpegImageData() != null ){ - data = dir.getJpegImageData().data; - } - // Support other image formats here. - - if( data != null ){ - return data; - } - } - return null; - } - - /** - * Get the thumbnail image if available. - * - * @return the thumbnail image. - * May be null if no image could be found. - * @throws ImageReadException - * @throws IOException - */ - public BufferedImage getEXIFThumbnail() throws ImageReadException, - IOException { - - if (exif == null) { - return null; - } - - ArrayList dirs = exif.getDirectories(); - for (int i = 0; i < dirs.size(); i++) { - TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) dirs - .get(i); - // Debug.debug("dir", dir); - BufferedImage image = dir.getThumbnail(); - if (null != image) { - return image; - } - -// JpegImageData jpegImageData = dir.getJpegImageData(); -// if (jpegImageData != null) { -// ByteArrayInputStream input = new ByteArrayInputStream(jpegImageData.data); -// // JPEG thumbnail as JPEG or other format; try to parse. -// image = ImageIO.read(input); -// if (image != null) { -// return image; -// } -// } - } - - return null; - } - - public TiffImageData getRawImageData() { - ArrayList dirs = exif.getDirectories(); - for (int i = 0; i < dirs.size(); i++) { - TiffImageMetadata.Directory dir = (TiffImageMetadata.Directory) dirs - .get(i); - // Debug.debug("dir", dir); - TiffImageData rawImageData = dir.getTiffImageData(); - if (null != rawImageData) - return rawImageData; - } - - return null; - } - - public ArrayList getItems() { - ArrayList result = new ArrayList(); - - if (null != exif) - result.addAll(exif.getItems()); - - if (null != photoshop) - result.addAll(photoshop.getItems()); - - return result; - } - - private static final String newline = System.getProperty("line.separator"); - - public String toString() { - return toString(null); - } - - public String toString(String prefix) { - if (prefix == null) - prefix = ""; - - StringBuffer result = new StringBuffer(); - - result.append(prefix); - if (null == exif) - result.append("No Exif metadata."); - else { - result.append("Exif metadata:"); - result.append(newline); - result.append(exif.toString("\t")); - } - - // if (null != exif && null != photoshop) - result.append(newline); - - result.append(prefix); - if (null == photoshop) - result.append("No Photoshop (IPTC) metadata."); - else { - result.append("Photoshop (IPTC) metadata:"); - result.append(newline); - result.append(photoshop.toString("\t")); - } - - return result.toString(); - } - - public void dump() { - Debug.debug(this.toString()); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageParser.java b/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageParser.java deleted file mode 100644 index 666758a..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/JpegImageParser.java +++ /dev/null @@ -1,1039 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg; - -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.jpeg.iptc.IPTCParser; -import org.apache.sanselan.formats.jpeg.iptc.PhotoshopApp13Data; -import org.apache.sanselan.formats.jpeg.segments.App13Segment; -import org.apache.sanselan.formats.jpeg.segments.App2Segment; -import org.apache.sanselan.formats.jpeg.segments.GenericSegment; -import org.apache.sanselan.formats.jpeg.segments.JFIFSegment; -import org.apache.sanselan.formats.jpeg.segments.SOFNSegment; -import org.apache.sanselan.formats.jpeg.segments.Segment; -import org.apache.sanselan.formats.jpeg.segments.UnknownSegment; -import org.apache.sanselan.formats.jpeg.xmp.JpegXmpParser; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.formats.tiff.TiffImageMetadata; -import org.apache.sanselan.formats.tiff.TiffImageParser; -import org.apache.sanselan.formats.tiff.constants.TiffTagConstants; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class JpegImageParser extends ImageParser implements JpegConstants, - TiffTagConstants -{ - public JpegImageParser() - { - setByteOrder(BYTE_ORDER_NETWORK); - // setDebug(true); - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_JPEG, // - }; - } - - public String getName() - { - return "Jpeg-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".jpg"; - - public static final String AcceptedExtensions[] = { ".jpg", ".jpeg", }; - - protected String[] getAcceptedExtensions() - { - return AcceptedExtensions; - } - - public final BufferedImage getBufferedImage(ByteSource byteSource, - Map params) throws ImageReadException, IOException - { - throw new ImageReadException( - "Sanselan cannot read or write JPEG images."); - } - - private boolean keepMarker(int marker, int markers[]) - { - if (markers == null) - return true; - - for (int i = 0; i < markers.length; i++) - { - if (markers[i] == marker) - return true; - } - - return false; - } - - public ArrayList readSegments(ByteSource byteSource, final int markers[], - final boolean returnAfterFirst, boolean readEverything) - throws ImageReadException, IOException - { - final ArrayList result = new ArrayList(); - final JpegImageParser parser = this; - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return false; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - if (marker == 0xffd9) - return false; - - // Debug.debug("visitSegment marker", marker); - // // Debug.debug("visitSegment keepMarker(marker, markers)", - // keepMarker(marker, markers)); - // Debug.debug("visitSegment keepMarker(marker, markers)", - // keepMarker(marker, markers)); - - if (!keepMarker(marker, markers)) - return true; - - if (marker == JPEG_APP13_Marker) - { - // Debug.debug("app 13 segment data", segmentData.length); - result.add(new App13Segment(parser, marker, segmentData)); - } else if (marker == JPEG_APP2_Marker) - { - result.add(new App2Segment(marker, segmentData)); - } else if (marker == JFIFMarker) - { - result.add(new JFIFSegment(marker, segmentData)); - } else if ((marker >= SOF0Marker) && (marker <= SOF15Marker)) - { - result.add(new SOFNSegment(marker, segmentData)); - } else if ((marker >= JPEG_APP1_Marker) - && (marker <= JPEG_APP15_Marker)) - { - result.add(new UnknownSegment(marker, segmentData)); - } - - if (returnAfterFirst) - return false; - - return true; - } - }; - - new JpegUtils().traverseJFIF(byteSource, visitor); - - return result; - } - - public static final boolean permissive = true; - - private byte[] assembleSegments(ArrayList v) throws ImageReadException - { - try - { - return assembleSegments(v, false); - } catch (ImageReadException e) - { - return assembleSegments(v, true); - } - } - - private byte[] assembleSegments(ArrayList v, boolean start_with_zero) - throws ImageReadException - { - if (v.size() < 1) - throw new ImageReadException("No App2 Segments Found."); - - int markerCount = ((App2Segment) v.get(0)).num_markers; - - // if (permissive && (markerCount == 0)) - // markerCount = v.size(); - - if (v.size() != markerCount) - throw new ImageReadException("App2 Segments Missing. Found: " - + v.size() + ", Expected: " + markerCount + "."); - - Collections.sort(v); - - int offset = start_with_zero ? 0 : 1; - - int total = 0; - for (int i = 0; i < v.size(); i++) - { - App2Segment segment = (App2Segment) v.get(i); - - if ((i + offset) != segment.cur_marker) - { - dumpSegments(v); - throw new ImageReadException( - "Incoherent App2 Segment Ordering. i: " + i - + ", segment[" + i + "].cur_marker: " - + segment.cur_marker + "."); - } - - if (markerCount != segment.num_markers) - { - dumpSegments(v); - throw new ImageReadException( - "Inconsistent App2 Segment Count info. markerCount: " - + markerCount + ", segment[" + i - + "].num_markers: " + segment.num_markers + "."); - } - - total += segment.icc_bytes.length; - } - - byte result[] = new byte[total]; - int progress = 0; - - for (int i = 0; i < v.size(); i++) - { - App2Segment segment = (App2Segment) v.get(i); - - System.arraycopy(segment.icc_bytes, 0, result, progress, - segment.icc_bytes.length); - progress += segment.icc_bytes.length; - } - - return result; - } - - private void dumpSegments(ArrayList v) - { - Debug.debug(); - Debug.debug("dumpSegments", v.size()); - - for (int i = 0; i < v.size(); i++) - { - App2Segment segment = (App2Segment) v.get(i); - - Debug.debug((i) + ": " + segment.cur_marker + " / " - + segment.num_markers); - } - Debug.debug(); - } - - public ArrayList readSegments(ByteSource byteSource, int markers[], - boolean returnAfterFirst) throws ImageReadException, IOException - { - return readSegments(byteSource, markers, returnAfterFirst, false); - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList segments = readSegments(byteSource, - new int[] { JPEG_APP2_Marker, }, false); - - if (segments != null) - { - // throw away non-icc profile app2 segments. - ArrayList filtered = new ArrayList(); - for (int i = 0; i < segments.size(); i++) - { - App2Segment segment = (App2Segment) segments.get(i); - if (segment.icc_bytes != null) - filtered.add(segment); - } - segments = filtered; - } - - if ((segments == null) || (segments.size() < 1)) - return null; - - byte bytes[] = assembleSegments(segments); - - if (debug) - System.out.println("bytes" + ": " - + ((bytes == null) ? null : "" + bytes.length)); - - if (debug) - System.out.println(""); - - return (bytes); - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - TiffImageMetadata exif = getExifMetadata(byteSource, params); - - JpegPhotoshopMetadata photoshop = getPhotoshopMetadata(byteSource, - params); - - if (null == exif && null == photoshop) - return null; - - JpegImageMetadata result = new JpegImageMetadata(photoshop, exif); - - return result; - } - - public static boolean isExifAPP1Segment(GenericSegment segment) - { - return byteArrayHasPrefix(segment.bytes, EXIF_IDENTIFIER_CODE); - } - - private ArrayList filterAPP1Segments(ArrayList v) - { - ArrayList result = new ArrayList(); - - for (int i = 0; i < v.size(); i++) - { - GenericSegment segment = (GenericSegment) v.get(i); - if (isExifAPP1Segment(segment)) - result.add(segment); - } - - return result; - } - - // TODO unused - private ArrayList filterSegments(ArrayList v, List markers) - { - ArrayList result = new ArrayList(); - - for (int i = 0; i < v.size(); i++) - { - Segment segment = (Segment) v.get(i); - Integer marker = new Integer(segment.marker); - if (markers.contains(marker)) - result.add(segment); - } - - return result; - } - - public TiffImageMetadata getExifMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - byte bytes[] = getExifRawData(byteSource); - if (null == bytes) - return null; - - if (params == null) - params = new HashMap(); - if (!params.containsKey(PARAM_KEY_READ_THUMBNAILS)) - params.put(PARAM_KEY_READ_THUMBNAILS, Boolean.TRUE); - - return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, - params); - } - - public byte[] getExifRawData(ByteSource byteSource) - throws ImageReadException, IOException - { - ArrayList segments = readSegments(byteSource, - new int[] { JPEG_APP1_Marker, }, false); - - if ((segments == null) || (segments.size() < 1)) - return null; - - ArrayList exifSegments = filterAPP1Segments(segments); - if (debug) - System.out.println("exif_segments.size" + ": " - + exifSegments.size()); - - // Debug.debug("segments", segments); - // Debug.debug("exifSegments", exifSegments); - - // TODO: concatenate if multiple segments, need example. - if (exifSegments.size() < 1) - return null; - if (exifSegments.size() > 1) - throw new ImageReadException( - "Sanselan currently can't parse EXIF metadata split across multiple APP1 segments. " - + "Please send this image to the Sanselan project."); - - GenericSegment segment = (GenericSegment) exifSegments.get(0); - byte bytes[] = segment.bytes; - - // byte head[] = readBytearray("exif head", bytes, 0, 6); - // - // Debug.debug("head", head); - - return getByteArrayTail("trimmed exif bytes", bytes, 6); - } - - public boolean hasExifSegment(ByteSource byteSource) - throws ImageReadException, IOException - { - final boolean result[] = { false, }; - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return false; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - if (marker == 0xffd9) - return false; - - if (marker == JPEG_APP1_Marker) - { - if (byteArrayHasPrefix(segmentData, EXIF_IDENTIFIER_CODE)) - { - result[0] = true; - return false; - } - } - - return true; - } - }; - - new JpegUtils().traverseJFIF(byteSource, visitor); - - return result[0]; - } - - public boolean hasIptcSegment(ByteSource byteSource) - throws ImageReadException, IOException - { - final boolean result[] = { false, }; - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return false; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - if (marker == 0xffd9) - return false; - - if (marker == JPEG_APP13_Marker) - { - if (new IPTCParser().isPhotoshopJpegSegment(segmentData)) - { - result[0] = true; - return false; - } - } - - return true; - } - }; - - new JpegUtils().traverseJFIF(byteSource, visitor); - - return result[0]; - } - - public boolean hasXmpSegment(ByteSource byteSource) - throws ImageReadException, IOException - { - final boolean result[] = { false, }; - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return false; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - if (marker == 0xffd9) - return false; - - if (marker == JPEG_APP1_Marker) - { - if (new JpegXmpParser().isXmpJpegSegment(segmentData)) - { - result[0] = true; - return false; - } - } - - return true; - } - }; - new JpegUtils().traverseJFIF(byteSource, visitor); - - return result[0]; - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - - final List result = new ArrayList(); - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return false; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - if (marker == 0xffd9) - return false; - - if (marker == JPEG_APP1_Marker) - { - if (new JpegXmpParser().isXmpJpegSegment(segmentData)) - { - result.add(new JpegXmpParser() - .parseXmpJpegSegment(segmentData)); - return false; - } - } - - return true; - } - }; - new JpegUtils().traverseJFIF(byteSource, visitor); - - if (result.size() < 1) - return null; - if (result.size() > 1) - throw new ImageReadException( - "Jpeg file contains more than one XMP segment."); - return (String) result.get(0); - } - - public JpegPhotoshopMetadata getPhotoshopMetadata(ByteSource byteSource, - Map params) throws ImageReadException, IOException - { - ArrayList segments = readSegments(byteSource, - new int[] { JPEG_APP13_Marker, }, false); - - if ((segments == null) || (segments.size() < 1)) - return null; - - PhotoshopApp13Data photoshopApp13Data = null; - - for (int i = 0; i < segments.size(); i++) - { - App13Segment segment = (App13Segment) segments.get(i); - - PhotoshopApp13Data data = segment.parsePhotoshopSegment(params); - if (data != null && photoshopApp13Data != null) - throw new ImageReadException( - "Jpeg contains more than one Photoshop App13 segment."); - - photoshopApp13Data = data; - } - - if(null==photoshopApp13Data) - return null; - return new JpegPhotoshopMetadata(photoshopApp13Data); - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList segments = readSegments(byteSource, new int[] { - // kJFIFMarker, - SOF0Marker, - - SOF1Marker, SOF2Marker, SOF3Marker, SOF5Marker, SOF6Marker, - SOF7Marker, SOF9Marker, SOF10Marker, SOF11Marker, SOF13Marker, - SOF14Marker, SOF15Marker, - - }, true); - - if ((segments == null) || (segments.size() < 1)) - throw new ImageReadException("No JFIF Data Found."); - - if (segments.size() > 1) - throw new ImageReadException("Redundant JFIF Data Found."); - - SOFNSegment fSOFNSegment = (SOFNSegment) segments.get(0); - - return new Dimension(fSOFNSegment.width, fSOFNSegment.height); - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - // ArrayList allSegments = readSegments(byteSource, null, false); - - ArrayList SOF_segments = readSegments(byteSource, new int[] { - // kJFIFMarker, - - SOF0Marker, SOF1Marker, SOF2Marker, SOF3Marker, SOF5Marker, - SOF6Marker, SOF7Marker, SOF9Marker, SOF10Marker, SOF11Marker, - SOF13Marker, SOF14Marker, SOF15Marker, - - }, false); - - if (SOF_segments == null) - throw new ImageReadException("No SOFN Data Found."); - - // if (SOF_segments.size() != 1) - // System.out.println("Incoherent SOFN Data Found: " - // + SOF_segments.size()); - - ArrayList jfifSegments = readSegments(byteSource, - new int[] { JFIFMarker, }, true); - - SOFNSegment fSOFNSegment = (SOFNSegment) SOF_segments.get(0); - // SOFNSegment fSOFNSegment = (SOFNSegment) findSegment(segments, - // SOFNmarkers); - - if (fSOFNSegment == null) - throw new ImageReadException("No SOFN Data Found."); - - int Width = fSOFNSegment.width; - int Height = fSOFNSegment.height; - - JFIFSegment jfifSegment = null; - - if ((jfifSegments != null) && (jfifSegments.size() > 0)) - jfifSegment = (JFIFSegment) jfifSegments.get(0); - - // JFIFSegment fTheJFIFSegment = (JFIFSegment) findSegment(segments, - // kJFIFMarker); - - double x_density = -1.0; - double y_density = -1.0; - double units_per_inch = -1.0; - // int JFIF_major_version; - // int JFIF_minor_version; - String FormatDetails; - - if (jfifSegment != null) - { - x_density = jfifSegment.xDensity; - y_density = jfifSegment.yDensity; - int density_units = jfifSegment.densityUnits; - // JFIF_major_version = fTheJFIFSegment.JFIF_major_version; - // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version; - - FormatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + "." - + jfifSegment.jfifMinorVersion; - - switch (density_units) - { - case 0: - break; - case 1: // inches - units_per_inch = 1.0; - break; - case 2: // cms - units_per_inch = 2.54; - break; - default: - break; - } - } else - { - JpegImageMetadata metadata = (JpegImageMetadata) getMetadata( - byteSource, params); - - if (metadata != null) - { - { - TiffField field = metadata - .findEXIFValue(TIFF_TAG_XRESOLUTION); - if (field != null) - x_density = ((Number) field.getValue()).doubleValue(); - } - { - TiffField field = metadata - .findEXIFValue(TIFF_TAG_YRESOLUTION); - if (field != null) - y_density = ((Number) field.getValue()).doubleValue(); - } - { - TiffField field = metadata - .findEXIFValue(TIFF_TAG_RESOLUTION_UNIT); - if (field != null) - { - int density_units = ((Number) field.getValue()) - .intValue(); - - switch (density_units) - { - case 1: - break; - case 2: // inches - units_per_inch = 1.0; - break; - case 3: // cms - units_per_inch = 2.54; - break; - default: - break; - } - } - - } - } - - FormatDetails = "Jpeg/DCM"; - - } - - int PhysicalHeightDpi = -1; - float PhysicalHeightInch = -1; - int PhysicalWidthDpi = -1; - float PhysicalWidthInch = -1; - - if (units_per_inch > 0) - { - PhysicalWidthDpi = (int) Math.round(x_density / units_per_inch); - PhysicalWidthInch = (float) (Width / (x_density * units_per_inch)); - PhysicalHeightDpi = (int) Math.round(y_density * units_per_inch); - PhysicalHeightInch = (float) (Height / (y_density * units_per_inch)); - } - - ArrayList Comments = new ArrayList(); - // TODO: comments... - - int Number_of_components = fSOFNSegment.numberOfComponents; - int Precision = fSOFNSegment.precision; - - int BitsPerPixel = Number_of_components * Precision; - ImageFormat Format = ImageFormat.IMAGE_FORMAT_JPEG; - String FormatName = "JPEG (Joint Photographic Experts Group) Format"; - String MimeType = "image/jpeg"; - // we ought to count images, but don't yet. - int NumberOfImages = 1; - // not accurate ... only reflects first - boolean isProgressive = fSOFNSegment.marker == SOF2Marker; - - boolean isTransparent = false; // TODO: inaccurate. - boolean usesPalette = false; // TODO: inaccurate. - int ColorType; - if (Number_of_components == 1) - ColorType = ImageInfo.COLOR_TYPE_BW; - else if (Number_of_components == 3) - ColorType = ImageInfo.COLOR_TYPE_RGB; - else if (Number_of_components == 4) - ColorType = ImageInfo.COLOR_TYPE_CMYK; - else - ColorType = ImageInfo.COLOR_TYPE_UNKNOWN; - - String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; - - ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, - Format, FormatName, Height, MimeType, NumberOfImages, - PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, - PhysicalWidthInch, Width, isProgressive, isTransparent, - usesPalette, ColorType, compressionAlgorithm); - - return result; - } - - // public ImageInfo getImageInfo(ByteSource byteSource, Map params) - // throws ImageReadException, IOException - // { - // - // ArrayList allSegments = readSegments(byteSource, null, false); - // - // final int SOF_MARKERS[] = new int[]{ - // SOF0Marker, SOF1Marker, SOF2Marker, SOF3Marker, SOF5Marker, - // SOF6Marker, SOF7Marker, SOF9Marker, SOF10Marker, SOF11Marker, - // SOF13Marker, SOF14Marker, SOF15Marker, - // }; - // - // ArrayList sofMarkers = new ArrayList(); - // for(int i=0;i 0) - // jfifSegment = (JFIFSegment) jfifSegments.get(0); - // - // double x_density = -1.0; - // double y_density = -1.0; - // double units_per_inch = -1.0; - // // int JFIF_major_version; - // // int JFIF_minor_version; - // String FormatDetails; - // - // if (jfifSegment != null) - // { - // x_density = jfifSegment.xDensity; - // y_density = jfifSegment.yDensity; - // int density_units = jfifSegment.densityUnits; - // // JFIF_major_version = fTheJFIFSegment.JFIF_major_version; - // // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version; - // - // FormatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion - // + "." + jfifSegment.jfifMinorVersion; - // - // switch (density_units) - // { - // case 0 : - // break; - // case 1 : // inches - // units_per_inch = 1.0; - // break; - // case 2 : // cms - // units_per_inch = 2.54; - // break; - // default : - // break; - // } - // } - // else - // { - // JpegImageMetadata metadata = (JpegImageMetadata) getMetadata(byteSource, - // params); - // - // { - // TiffField field = metadata - // .findEXIFValue(TiffField.TIFF_TAG_XRESOLUTION); - // if (field == null) - // throw new ImageReadException("No XResolution"); - // - // x_density = ((Number) field.getValue()).doubleValue(); - // } - // { - // TiffField field = metadata - // .findEXIFValue(TiffField.TIFF_TAG_YRESOLUTION); - // if (field == null) - // throw new ImageReadException("No YResolution"); - // - // y_density = ((Number) field.getValue()).doubleValue(); - // } - // { - // TiffField field = metadata - // .findEXIFValue(TiffField.TIFF_TAG_RESOLUTION_UNIT); - // if (field == null) - // throw new ImageReadException("No ResolutionUnits"); - // - // int density_units = ((Number) field.getValue()).intValue(); - // - // switch (density_units) - // { - // case 1 : - // break; - // case 2 : // inches - // units_per_inch = 1.0; - // break; - // case 3 : // cms - // units_per_inch = 2.54; - // break; - // default : - // break; - // } - // - // } - // - // FormatDetails = "Jpeg/DCM"; - // - // } - // - // int PhysicalHeightDpi = -1; - // float PhysicalHeightInch = -1; - // int PhysicalWidthDpi = -1; - // float PhysicalWidthInch = -1; - // - // if (units_per_inch > 0) - // { - // PhysicalWidthDpi = (int) Math.round((double) x_density - // / units_per_inch); - // PhysicalWidthInch = (float) ((double) Width / (x_density * - // units_per_inch)); - // PhysicalHeightDpi = (int) Math.round((double) y_density - // * units_per_inch); - // PhysicalHeightInch = (float) ((double) Height / (y_density * - // units_per_inch)); - // } - // - // ArrayList Comments = new ArrayList(); - // // TODO: comments... - // - // int Number_of_components = firstSOFNSegment.numberOfComponents; - // int Precision = firstSOFNSegment.precision; - // - // int BitsPerPixel = Number_of_components * Precision; - // ImageFormat Format = ImageFormat.IMAGE_FORMAT_JPEG; - // String FormatName = "JPEG (Joint Photographic Experts Group) Format"; - // String MimeType = "image/jpeg"; - // // we ought to count images, but don't yet. - // int NumberOfImages = -1; - // // not accurate ... only reflects first - // boolean isProgressive = firstSOFNSegment.marker == SOF2Marker; - // - // boolean isTransparent = false; // TODO: inaccurate. - // boolean usesPalette = false; // TODO: inaccurate. - // int ColorType; - // if (Number_of_components == 1) - // ColorType = ImageInfo.COLOR_TYPE_BW; - // else if (Number_of_components == 3) - // ColorType = ImageInfo.COLOR_TYPE_RGB; - // else if (Number_of_components == 4) - // ColorType = ImageInfo.COLOR_TYPE_CMYK; - // else - // ColorType = ImageInfo.COLOR_TYPE_UNKNOWN; - // - // String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; - // - // ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, - // Format, FormatName, Height, MimeType, NumberOfImages, - // PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, - // PhysicalWidthInch, Width, isProgressive, isTransparent, - // usesPalette, ColorType, compressionAlgorithm); - // - // return result; - // } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - pw.println("tiff.dumpImageFile"); - - { - ImageInfo imageInfo = getImageInfo(byteSource); - if (imageInfo == null) - return false; - - imageInfo.toString(pw, ""); - } - - pw.println(""); - - { - ArrayList segments = readSegments(byteSource, null, false); - - if (segments == null) - throw new ImageReadException("No Segments Found."); - - for (int d = 0; d < segments.size(); d++) - { - - Segment segment = (Segment) segments.get(d); - - NumberFormat nf = NumberFormat.getIntegerInstance(); - // this.debugNumber("found, marker: ", marker, 4); - pw.println(d + ": marker: " - + Integer.toHexString(segment.marker) + ", " - + segment.getDescription() + " (length: " - + nf.format(segment.length) + ")"); - segment.dump(pw); - } - - pw.println(""); - } - - return true; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/JpegUtils.java b/src/main/java/org/apache/sanselan/formats/jpeg/JpegUtils.java deleted file mode 100644 index ab2d7f0..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/JpegUtils.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.util.Debug; - -public class JpegUtils extends BinaryFileParser implements JpegConstants -{ - public JpegUtils() - { - setByteOrder(BYTE_ORDER_NETWORK); - } - - public static interface Visitor - { - // return false to exit before reading image data. - public boolean beginSOS(); - - public void visitSOS(int marker, byte markerBytes[], byte imageData[]); - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int segmentLength, byte segmentLengthBytes[], - byte segmentData[]) throws ImageReadException, - // ImageWriteException, - IOException; - } - - public void traverseJFIF(ByteSource byteSource, Visitor visitor) - throws ImageReadException, - // ImageWriteException, - IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - readAndVerifyBytes(is, SOI, - "Not a Valid JPEG File: doesn't begin with 0xffd8"); - - int byteOrder = getByteOrder(); - - for (int markerCount = 0; true; markerCount++) - { - byte markerBytes[] = readByteArray("markerBytes", 2, is, - "markerBytes"); - int marker = convertByteArrayToShort("marker", markerBytes, - byteOrder); - -// Debug.debug("marker", marker + " (0x" + Integer.toHexString(marker) + ")"); -// Debug.debug("markerBytes", markerBytes); - - if (marker == 0xffd9 || marker == SOS_Marker) - { - if (!visitor.beginSOS()) - return; - - byte imageData[] = getStreamBytes(is); - visitor.visitSOS(marker, markerBytes, imageData); - break; - } - - byte segmentLengthBytes[] = readByteArray("segmentLengthBytes", - 2, is, "segmentLengthBytes"); - int segmentLength = convertByteArrayToShort("segmentLength", - segmentLengthBytes, byteOrder); - -// Debug.debug("segmentLength", segmentLength + " (0x" + Integer.toHexString(segmentLength) + ")"); -// Debug.debug("segmentLengthBytes", segmentLengthBytes); - - byte segmentData[] = readByteArray("Segment Data", - segmentLength - 2, is, - "Invalid Segment: insufficient data"); - - // Debug.debug("segmentLength", segmentLength); - - if (!visitor.visitSegment(marker, markerBytes, segmentLength, - segmentLengthBytes, segmentData)) - return; - } - - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public static String getMarkerName(int marker) - { - switch (marker) - { - case SOS_Marker: - return "SOS_Marker"; - // case JPEG_APP0 : - // return "JPEG_APP0"; - // case JPEG_APP0_Marker : - // return "JPEG_APP0_Marker"; - case JPEG_APP1_Marker: - return "JPEG_APP1_Marker"; - case JPEG_APP2_Marker: - return "JPEG_APP2_Marker"; - case JPEG_APP13_Marker: - return "JPEG_APP13_Marker"; - case JPEG_APP14_Marker: - return "JPEG_APP14_Marker"; - case JPEG_APP15_Marker: - return "JPEG_APP15_Marker"; - case JFIFMarker: - return "JFIFMarker"; - case SOF0Marker: - return "SOF0Marker"; - case SOF1Marker: - return "SOF1Marker"; - case SOF2Marker: - return "SOF2Marker"; - case SOF3Marker: - return "SOF3Marker"; - case SOF4Marker: - return "SOF4Marker"; - case SOF5Marker: - return "SOF5Marker"; - case SOF6Marker: - return "SOF6Marker"; - case SOF7Marker: - return "SOF7Marker"; - case SOF8Marker: - return "SOF8Marker"; - case SOF9Marker: - return "SOF9Marker"; - case SOF10Marker: - return "SOF10Marker"; - case SOF11Marker: - return "SOF11Marker"; - case SOF12Marker: - return "SOF12Marker"; - case SOF13Marker: - return "SOF13Marker"; - case SOF14Marker: - return "SOF14Marker"; - case SOF15Marker: - return "SOF15Marker"; - default: - return "Unknown"; - } - } - - public void dumpJFIF(ByteSource byteSource) throws ImageReadException, - IOException, ImageWriteException - { - Visitor visitor = new Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return true; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - Debug.debug("SOS marker. " + imageData.length - + " bytes of image data."); - Debug.debug(""); - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int segmentLength, byte segmentLengthBytes[], - byte segmentData[]) - { - Debug.debug("Segment marker: " + Integer.toHexString(marker) - + " (" + getMarkerName(marker) + "), " - + segmentData.length + " bytes of segment data."); - return true; - } - }; - - traverseJFIF(byteSource, visitor); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/exifRewrite/ExifRewriter.java b/src/main/java/org/apache/sanselan/formats/jpeg/exifRewrite/ExifRewriter.java deleted file mode 100644 index 518ef61..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/exifRewrite/ExifRewriter.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.exifRewrite; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.common.byteSources.ByteSourceInputStream; -import org.apache.sanselan.formats.jpeg.JpegConstants; -import org.apache.sanselan.formats.jpeg.JpegUtils; -import org.apache.sanselan.formats.tiff.write.TiffImageWriterBase; -import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossless; -import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossy; -import org.apache.sanselan.formats.tiff.write.TiffOutputSet; -import org.apache.sanselan.util.Debug; - -/** - * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. - *

- *

- * See the source of the ExifMetadataUpdateExample class for example usage. - * - * @see org.apache.sanselan.sampleUsage.WriteExifMetadataExample - */ -public class ExifRewriter extends BinaryFileParser implements JpegConstants -{ - /** - * Constructor. to guess whether a file contains an image based on its file extension. - */ - public ExifRewriter() - { - setByteOrder(BYTE_ORDER_NETWORK); - } - - /** - * Constructor. - *

- * @param byteOrder byte order of EXIF segment. Optional. See BinaryConstants class. - * - * @see org.apache.sanselan.common.BinaryConstants - */ - public ExifRewriter(int byteOrder) - { - setByteOrder(byteOrder); - } - - private static class JFIFPieces - { - public final List pieces; - public final List exifPieces; - - public JFIFPieces(final List pieces, final List exifPieces) - { - this.pieces = pieces; - this.exifPieces = exifPieces; - } - - } - - private abstract static class JFIFPiece - { - protected abstract void write(OutputStream os) throws IOException; - } - - private static class JFIFPieceSegment extends JFIFPiece - { - public final int marker; - public final byte markerBytes[]; - public final byte markerLengthBytes[]; - public final byte segmentData[]; - - public JFIFPieceSegment(final int marker, final byte[] markerBytes, - final byte[] markerLengthBytes, final byte[] segmentData) - { - this.marker = marker; - this.markerBytes = markerBytes; - this.markerLengthBytes = markerLengthBytes; - this.segmentData = segmentData; - } - - protected void write(OutputStream os) throws IOException - { - os.write(markerBytes); - os.write(markerLengthBytes); - os.write(segmentData); - } - } - - private static class JFIFPieceSegmentExif extends JFIFPieceSegment - { - - public JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, - final byte[] markerLengthBytes, final byte[] segmentData) - { - super(marker, markerBytes, markerLengthBytes, segmentData); - } - } - - private static class JFIFPieceImageData extends JFIFPiece - { - public final byte markerBytes[]; - public final byte imageData[]; - - public JFIFPieceImageData(final byte[] markerBytes, - final byte[] imageData) - { - super(); - this.markerBytes = markerBytes; - this.imageData = imageData; - } - - protected void write(OutputStream os) throws IOException - { - os.write(markerBytes); - os.write(imageData); - } - } - - private JFIFPieces analyzeJFIF(ByteSource byteSource) - throws ImageReadException, IOException - // , ImageWriteException - { - final ArrayList pieces = new ArrayList(); - final List exifPieces = new ArrayList(); - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() - { - // return false to exit before reading image data. - public boolean beginSOS() - { - return true; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - pieces.add(new JFIFPieceImageData(markerBytes, imageData)); - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int markerLength, byte markerLengthBytes[], - byte segmentData[]) throws - // ImageWriteException, - ImageReadException, IOException - { - if (marker != JPEG_APP1_Marker) - { - pieces.add(new JFIFPieceSegment(marker, markerBytes, - markerLengthBytes, segmentData)); - } - else if (!byteArrayHasPrefix(segmentData, EXIF_IDENTIFIER_CODE)) - { - pieces.add(new JFIFPieceSegment(marker, markerBytes, - markerLengthBytes, segmentData)); - } - // else if (exifSegmentArray[0] != null) - // { - // // TODO: add support for multiple segments - // throw new ImageReadException( - // "More than one APP1 EXIF segment."); - // } - else - { - JFIFPiece piece = new JFIFPieceSegmentExif(marker, - markerBytes, markerLengthBytes, segmentData); - pieces.add(piece); - exifPieces.add(piece); - } - return true; - } - }; - - new JpegUtils().traverseJFIF(byteSource, visitor); - - // GenericSegment exifSegment = exifSegmentArray[0]; - // if (exifSegments.size() < 1) - // { - // // TODO: add support for adding, not just replacing. - // throw new ImageReadException("No APP1 EXIF segment found."); - // } - - return new JFIFPieces(pieces, exifPieces); - } - - /** - * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), - * and writes the result to a stream. - *

- * @param src Image file. - * @param os OutputStream to write the image to. - * - * @see java.io.File - * @see java.io.OutputStream - * @see java.io.File - * @see java.io.OutputStream - */ - public void removeExifMetadata(File src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - removeExifMetadata(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), - * and writes the result to a stream. - *

- * @param src Byte array containing Jpeg image data. - * @param os OutputStream to write the image to. - */ - public void removeExifMetadata(byte src[], OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - removeExifMetadata(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), - * and writes the result to a stream. - *

- * @param src InputStream containing Jpeg image data. - * @param os OutputStream to write the image to. - */ - public void removeExifMetadata(InputStream src, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - removeExifMetadata(byteSource, os); - } - - /** - * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), - * and writes the result to a stream. - *

- * @param byteSource ByteSource containing Jpeg image data. - * @param os OutputStream to write the image to. - */ - public void removeExifMetadata(ByteSource byteSource, OutputStream os) - throws ImageReadException, IOException, ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List pieces = jfifPieces.pieces; - - // Debug.debug("pieces", pieces); - - // pieces.removeAll(jfifPieces.exifSegments); - - // Debug.debug("pieces", pieces); - - writeSegmentsReplacingExif(os, pieces, null); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF - * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting - * any part of the original segment that it couldn't parse. This can cause the EXIF segment to - * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 - * segment of the Jpeg image. - *

- * @param src Image file. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossless(File src, OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - updateExifMetadataLossless(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF - * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting - * any part of the original segment that it couldn't parse. This can cause the EXIF segment to - * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 - * segment of the Jpeg image. - *

- * @param src Byte array containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossless(byte src[], OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - updateExifMetadataLossless(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF - * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting - * any part of the original segment that it couldn't parse. This can cause the EXIF segment to - * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 - * segment of the Jpeg image. - *

- * @param src InputStream containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossless(InputStream src, OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - updateExifMetadataLossless(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF - * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting - * any part of the original segment that it couldn't parse. This can cause the EXIF segment to - * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 - * segment of the Jpeg image. - *

- * @param byteSource ByteSource containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossless(ByteSource byteSource, - OutputStream os, TiffOutputSet outputSet) - throws ImageReadException, IOException, ImageWriteException - { - // List outputDirectories = outputSet.getDirectories(); - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List pieces = jfifPieces.pieces; - - TiffImageWriterBase writer; - // Just use first APP1 segment for now. - // Multiple APP1 segments are rare and poorly supported. - if (jfifPieces.exifPieces.size() > 0) - { - JFIFPieceSegment exifPiece = null; - exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0); - - byte exifBytes[] = exifPiece.segmentData; - exifBytes = getByteArrayTail("trimmed exif bytes", exifBytes, 6); - - writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes); - - } - else - writer = new TiffImageWriterLossy(outputSet.byteOrder); - - boolean includeEXIFPrefix = true; - byte newBytes[] = writeExifSegment(writer, outputSet, includeEXIFPrefix); - - writeSegmentsReplacingExif(os, pieces, newBytes); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, - * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). - *

- * @param src Byte array containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossy(byte src[], OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceArray(src); - updateExifMetadataLossy(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, - * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). - *

- * @param src InputStream containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossy(InputStream src, OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceInputStream(src, null); - updateExifMetadataLossy(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, - * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). - *

- * @param src Image file. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossy(File src, OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - ByteSource byteSource = new ByteSourceFile(src); - updateExifMetadataLossy(byteSource, os, outputSet); - } - - /** - * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. - *

- * Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, - * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). - *

- * @param byteSource ByteSource containing Jpeg image data. - * @param os OutputStream to write the image to. - * @param outputSet TiffOutputSet containing the EXIF data to write. - */ - public void updateExifMetadataLossy(ByteSource byteSource, OutputStream os, - TiffOutputSet outputSet) throws ImageReadException, IOException, - ImageWriteException - { - JFIFPieces jfifPieces = analyzeJFIF(byteSource); - List pieces = jfifPieces.pieces; - - TiffImageWriterBase writer = new TiffImageWriterLossy( - outputSet.byteOrder); - - boolean includeEXIFPrefix = true; - byte newBytes[] = writeExifSegment(writer, outputSet, includeEXIFPrefix); - - writeSegmentsReplacingExif(os, pieces, newBytes); - } - - private void writeSegmentsReplacingExif(OutputStream os, List segments, - byte newBytes[]) throws ImageWriteException, IOException - { - int byteOrder = getByteOrder(); - - try - { - os.write(SOI); - - boolean hasExif = false; - - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - if (piece instanceof JFIFPieceSegmentExif) - hasExif = true; - } - - if (!hasExif && newBytes != null) - { - byte markerBytes[] = convertShortToByteArray(JPEG_APP1_Marker, - byteOrder); - if (newBytes.length > 0xffff) - throw new ExifOverflowException( - "APP1 Segment is too long: " + newBytes.length); - int markerLength = newBytes.length + 2; - byte markerLengthBytes[] = convertShortToByteArray( - markerLength, byteOrder); - - int index = 0; - JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments - .get(index); - if (firstSegment.marker == JFIFMarker) - index = 1; - segments.add(0, new JFIFPieceSegmentExif(JPEG_APP1_Marker, - markerBytes, markerLengthBytes, newBytes)); - } - - boolean APP1Written = false; - - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - if (piece instanceof JFIFPieceSegmentExif) - { - // only replace first APP1 segment; skips others. - if (APP1Written) - continue; - APP1Written = true; - - if (newBytes == null) - continue; - - byte markerBytes[] = convertShortToByteArray( - JPEG_APP1_Marker, byteOrder); - if (newBytes.length > 0xffff) - throw new ExifOverflowException( - "APP1 Segment is too long: " + newBytes.length); - int markerLength = newBytes.length + 2; - byte markerLengthBytes[] = convertShortToByteArray( - markerLength, byteOrder); - - os.write(markerBytes); - os.write(markerLengthBytes); - os.write(newBytes); - } - else - { - piece.write(os); - } - } - } - finally - { - try - { - os.close(); - } - catch (Exception e) - { - Debug.debug(e); - } - } - } - - public static class ExifOverflowException extends ImageWriteException - { - public ExifOverflowException(String s) - { - super(s); - } - } - - private byte[] writeExifSegment(TiffImageWriterBase writer, - TiffOutputSet outputSet, boolean includeEXIFPrefix) - throws IOException, ImageWriteException - { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - if (includeEXIFPrefix) - { - os.write(EXIF_IDENTIFIER_CODE); - os.write(0); - os.write(0); - } - - writer.write(os, outputSet); - - return os.toByteArray(); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCConstants.java b/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCConstants.java deleted file mode 100644 index 27707f8..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCConstants.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -import org.apache.sanselan.formats.jpeg.JpegConstants; - -public interface IPTCConstants extends JpegConstants -{ - - public static final byte IPTC_PREFIX[] = { 0x1C, 0x02, }; - - public static final int IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE = 32767; - - public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP2_INFO = 0x03e8; - public static final int IMAGE_RESOURCE_BLOCK_MACINTOSH_PRINT_INFO = 0x03e9; - public static final int IMAGE_RESOURCE_BLOCK_XML_DATA = 0x03ea; - public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP2_COLOR_TABLE = 0x03eb; - public static final int IMAGE_RESOURCE_BLOCK_RESOLUTION_INFO = 0x03ed; - public static final int IMAGE_RESOURCE_BLOCK_ALPHA_CHANNELS_NAMES = 0x03ee; - public static final int IMAGE_RESOURCE_BLOCK_DISPLAY_INFO = 0x03ef; - public static final int IMAGE_RESOURCE_BLOCK_PSTRING_CAPTION = 0x03f0; - public static final int IMAGE_RESOURCE_BLOCK_BORDER_INFORMATION = 0x03f1; - public static final int IMAGE_RESOURCE_BLOCK_BACKGROUND_COLOR = 0x03f2; - public static final int IMAGE_RESOURCE_BLOCK_PRINT_FLAGS = 0x03f3; - public static final int IMAGE_RESOURCE_BLOCK_BW_HALFTONING_INFO = 0x03f4; - public static final int IMAGE_RESOURCE_BLOCK_COLOR_HALFTONING_INFO = 0x03f5; - public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_HALFTONING_INFO = 0x03f6; - public static final int IMAGE_RESOURCE_BLOCK_BW_TRANSFER_FUNC = 0x03f7; - public static final int IMAGE_RESOURCE_BLOCK_COLOR_TRANSFER_FUNCS = 0x03f8; - public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_TRANSFER_FUNCS = 0x03f9; - public static final int IMAGE_RESOURCE_BLOCK_DUOTONE_IMAGE_INFO = 0x03fa; - public static final int IMAGE_RESOURCE_BLOCK_EFFECTIVE_BW = 0x03fb; - public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG1 = 0x03fc; - public static final int IMAGE_RESOURCE_BLOCK_EPS_OPTIONS = 0x03fd; - public static final int IMAGE_RESOURCE_BLOCK_QUICK_MASK_INFO = 0x03fe; - public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG2 = 0x03ff; - public static final int IMAGE_RESOURCE_BLOCK_LAYER_STATE_INFO = 0x0400; - public static final int IMAGE_RESOURCE_BLOCK_WORKING_PATH = 0x0401; - public static final int IMAGE_RESOURCE_BLOCK_LAYERS_GROUP_INFO = 0x0402; - public static final int IMAGE_RESOURCE_BLOCK_OBSOLETE_PHOTOSHOP_TAG3 = 0x0403; - public static final int IMAGE_RESOURCE_BLOCK_IPTC_DATA = 0x0404; - public static final int IMAGE_RESOURCE_BLOCK_RAW_IMAGE_MODE = 0x0405; - public static final int IMAGE_RESOURCE_BLOCK_JPEG_QUALITY = 0x0406; - public static final int IMAGE_RESOURCE_BLOCK_GRID_GUIDES_INFO = 0x0408; - public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP_BGR_THUMBNAIL = 0x0409; - public static final int IMAGE_RESOURCE_BLOCK_COPYRIGHT_FLAG = 0x040a; - public static final int IMAGE_RESOURCE_BLOCK_URL = 0x040b; - public static final int IMAGE_RESOURCE_BLOCK_PHOTOSHOP_THUMBNAIL = 0x040c; - public static final int IMAGE_RESOURCE_BLOCK_GLOBAL_ANGLE = 0x040d; - public static final int IMAGE_RESOURCE_BLOCK_COLOR_SAMPLERS_RESOURCE = 0x040e; - public static final int IMAGE_RESOURCE_BLOCK_ICC_PROFILE = 0x040f; - public static final int IMAGE_RESOURCE_BLOCK_WATERMARK = 0x0410; - public static final int IMAGE_RESOURCE_BLOCK_ICC_UNTAGGED = 0x0411; - public static final int IMAGE_RESOURCE_BLOCK_EFFECTS_VISIBLE = 0x0412; - public static final int IMAGE_RESOURCE_BLOCK_SPOT_HALFTONE = 0x0413; - public static final int IMAGE_RESOURCE_BLOCK_IDS_BASE_VALUE = 0x0414; - public static final int IMAGE_RESOURCE_BLOCK_UNICODE_ALPHA_NAMES = 0x0415; - public static final int IMAGE_RESOURCE_BLOCK_INDEXED_COLOUR_TABLE_COUNT = 0x0416; - public static final int IMAGE_RESOURCE_BLOCK_TRANSPARENT_INDEX = 0x0417; - public static final int IMAGE_RESOURCE_BLOCK_GLOBAL_ALTITUDE = 0x0419; - public static final int IMAGE_RESOURCE_BLOCK_SLICES = 0x041a; - public static final int IMAGE_RESOURCE_BLOCK_WORKFLOW_URL = 0x041b; - public static final int IMAGE_RESOURCE_BLOCK_JUMP_TO_XPEP = 0x041c; - public static final int IMAGE_RESOURCE_BLOCK_ALPHA_IDENTIFIERS = 0x041d; - public static final int IMAGE_RESOURCE_BLOCK_URL_LIST = 0x041e; - public static final int IMAGE_RESOURCE_BLOCK_VERSION_INFO = 0x0421; - public static final int IMAGE_RESOURCE_BLOCK_EXIFINFO = 0x0422; - public static final int IMAGE_RESOURCE_BLOCK_EXIF_INFO2 = 0x0423; - public static final int IMAGE_RESOURCE_BLOCK_XMP = 0x0424; - public static final int IMAGE_RESOURCE_BLOCK_CAPTION_DIGEST = 0x0425; - public static final int IMAGE_RESOURCE_BLOCK_PRINT_SCALE = 0x0426; - public static final int IMAGE_RESOURCE_BLOCK_PIXEL_ASPECT_RATIO = 0x0428; - public static final int IMAGE_RESOURCE_BLOCK_LAYER_COMPS = 0x0429; - public static final int IMAGE_RESOURCE_BLOCK_ALTERNATE_DUOTONE_COLORS = 0x042a; - public static final int IMAGE_RESOURCE_BLOCK_ALTERNATE_SPOT_COLORS = 0x042b; - public static final int IMAGE_RESOURCE_BLOCK_CLIPPING_PATH_NAME = 0x0bb7; - public static final int IMAGE_RESOURCE_BLOCK_PRINT_FLAGS_INFO = 0x2710; - - // public static final int IPTC_RECORD_PREFIX = 0x1c02; - public static final int IPTC_RECORD_TAG_MARKER = 0x1c; - public static final int IPTC_ENVELOPE_RECORD_NUMBER = 0x01; - public static final int IPTC_APPLICATION_2_RECORD_NUMBER = 0x02; - - public static final IPTCType IPTC_TYPE_RECORD_VERSION = new IPTCType(0, - "Record Version"); - public static final IPTCType IPTC_TYPE_OBJECT_TYPE_REFERENCE = new IPTCType( - 3, "Object Type Reference"); - public static final IPTCType IPTC_TYPE_OBJECT_ATTRIBUTE_REFERENCE = new IPTCType( - 4, "Object Attribute Reference"); - public static final IPTCType IPTC_TYPE_OBJECT_NAME = new IPTCType(5, - "Object Name"); - public static final IPTCType IPTC_TYPE_EDIT_STATUS = new IPTCType(7, - "Edit Status"); - public static final IPTCType IPTC_TYPE_EDITORIAL_UPDATE = new IPTCType(8, - "Editorial Update"); - public static final IPTCType IPTC_TYPE_URGENCY = new IPTCType(10, "Urgency"); - public static final IPTCType IPTC_TYPE_SUBJECT_REFERENCE = new IPTCType(12, - "Subject Reference"); - public static final IPTCType IPTC_TYPE_CATEGORY = new IPTCType(15, - "Category"); - public static final IPTCType IPTC_TYPE_SUPPLEMENTAL_CATEGORY = new IPTCType( - 20, "Supplemental Category"); - public static final IPTCType IPTC_TYPE_FIXTURE_IDENTIFIER = new IPTCType( - 22, "Fixture Identifier"); - public static final IPTCType IPTC_TYPE_KEYWORDS = new IPTCType(25, - "Keywords"); - public static final IPTCType IPTC_TYPE_CONTENT_LOCATION_CODE = new IPTCType( - 26, "Content Location Code"); - public static final IPTCType IPTC_TYPE_CONTENT_LOCATION_NAME = new IPTCType( - 27, "Content Location Name"); - public static final IPTCType IPTC_TYPE_RELEASE_DATE = new IPTCType(30, - "Release Date"); - public static final IPTCType IPTC_TYPE_RELEASE_TIME = new IPTCType(35, - "Release Time"); - public static final IPTCType IPTC_TYPE_EXPIRATION_DATE = new IPTCType(37, - "Expiration Date"); - public static final IPTCType IPTC_TYPE_EXPIRATION_TIME = new IPTCType(38, - "Expiration Time"); - public static final IPTCType IPTC_TYPE_SPECIAL_INSTRUCTIONS = new IPTCType( - 40, "Special Instructions"); - public static final IPTCType IPTC_TYPE_ACTION_ADVISED = new IPTCType(42, - "Action Advised"); - public static final IPTCType IPTC_TYPE_REFERENCE_SERVICE = new IPTCType(45, - "Reference Service"); - public static final IPTCType IPTC_TYPE_REFERENCE_DATE = new IPTCType(47, - "Reference Date"); - public static final IPTCType IPTC_TYPE_REFERENCE_NUMBER = new IPTCType(50, - "Reference Number"); - public static final IPTCType IPTC_TYPE_DATE_CREATED = new IPTCType(55, - "Date Created"); - public static final IPTCType IPTC_TYPE_TIME_CREATED = new IPTCType(60, - "Time Created"); - public static final IPTCType IPTC_TYPE_DIGITAL_CREATION_DATE = new IPTCType( - 62, "Digital Creation Date"); - public static final IPTCType IPTC_TYPE_DIGITAL_CREATION_TIME = new IPTCType( - 63, "Digital Creation Time"); - public static final IPTCType IPTC_TYPE_ORIGINATING_PROGRAM = new IPTCType( - 65, "Originating Program"); - public static final IPTCType IPTC_TYPE_PROGRAM_VERSION = new IPTCType(70, - "Program Version"); - public static final IPTCType IPTC_TYPE_OBJECT_CYCLE = new IPTCType(75, - "Object Cycle"); - public static final IPTCType IPTC_TYPE_BYLINE = new IPTCType(80, "By-line"); - public static final IPTCType IPTC_TYPE_BYLINE_TITLE = new IPTCType(85, - "By-line Title"); - public static final IPTCType IPTC_TYPE_CITY = new IPTCType(90, "City"); - public static final IPTCType IPTC_TYPE_SUBLOCATION = new IPTCType(92, - "Sublocation"); - public static final IPTCType IPTC_TYPE_PROVINCE_STATE = new IPTCType(95, - "Province/State"); - public static final IPTCType IPTC_TYPE_COUNTRY_PRIMARY_LOCATION_CODE = new IPTCType( - 100, "Country/Primary Location Code"); - public static final IPTCType IPTC_TYPE_COUNTRY_PRIMARY_LOCATION_NAME = new IPTCType( - 101, "Country/Primary Location Name"); - public static final IPTCType IPTC_TYPE_ORIGINAL_TRANSMISSION_REFERENCE = new IPTCType( - 103, "Original Transmission, Reference"); - public static final IPTCType IPTC_TYPE_HEADLINE = new IPTCType(105, - "Headline"); - public static final IPTCType IPTC_TYPE_CREDIT = new IPTCType(110, "Credit"); - public static final IPTCType IPTC_TYPE_SOURCE = new IPTCType(115, "Source"); - public static final IPTCType IPTC_TYPE_COPYRIGHT_NOTICE = new IPTCType(116, - "Copyright Notice"); - public static final IPTCType IPTC_TYPE_CONTACT = new IPTCType(118, - "Contact"); - public static final IPTCType IPTC_TYPE_CAPTION_ABSTRACT = new IPTCType(120, - "Caption/Abstract"); - public static final IPTCType IPTC_TYPE_WRITER_EDITOR = new IPTCType(122, - "Writer/Editor"); - public static final IPTCType IPTC_TYPE_RASTERIZED_CAPTION = new IPTCType( - 125, "Rasterized Caption"); - public static final IPTCType IPTC_TYPE_IMAGE_TYPE = new IPTCType(130, - "ImageType"); - public static final IPTCType IPTC_TYPE_IMAGE_ORIENTATION = new IPTCType( - 131, "Image Orientation"); - public static final IPTCType IPTC_TYPE_LANGUAGE_IDENTIFIER = new IPTCType( - 135, "Language Identifier"); - public static final IPTCType IPTC_TYPE_AUDIO_TYPE = new IPTCType(150, - "Audio Type"); - public static final IPTCType IPTC_TYPE_AUDIO_SAMPLING_RATE = new IPTCType( - 151, "Audio Sampling Rate"); - public static final IPTCType IPTC_TYPE_AUDIO_SAMPLING_RESOLUTION = new IPTCType( - 152, "Audio Sampling Resolution"); - public static final IPTCType IPTC_TYPE_AUDIO_DURATION = new IPTCType(153, - "Audio Duration"); - public static final IPTCType IPTC_TYPE_AUDIO_OUTCUE = new IPTCType(154, - "Audio Outcue"); - public static final IPTCType IPTC_TYPE_OBJECT_DATA_PREVIEW_FILE_FORMAT = new IPTCType( - 200, "Object Data Preview, File Format"); - public static final IPTCType IPTC_TYPE_OBJECT_DATA_PREVIEW_FILE_FORMAT_VERSION = new IPTCType( - 201, "Object Data Preview, File Format Version"); - public static final IPTCType IPTC_TYPE_OBJECT_DATA_PREVIEW_DATA = new IPTCType( - 202, "Object Data Preview Data"); - // -- - // public static final IPTCType IPTC_TYPE_UNKNOWN = new IPTCType(-1, - // "Unknown"); - - public static final IPTCType IPTC_TYPES[] = { IPTC_TYPE_RECORD_VERSION, - IPTC_TYPE_OBJECT_TYPE_REFERENCE, - IPTC_TYPE_OBJECT_ATTRIBUTE_REFERENCE, IPTC_TYPE_OBJECT_NAME, - IPTC_TYPE_EDIT_STATUS, IPTC_TYPE_EDITORIAL_UPDATE, - IPTC_TYPE_URGENCY, IPTC_TYPE_SUBJECT_REFERENCE, IPTC_TYPE_CATEGORY, - IPTC_TYPE_SUPPLEMENTAL_CATEGORY, IPTC_TYPE_FIXTURE_IDENTIFIER, - IPTC_TYPE_KEYWORDS, IPTC_TYPE_CONTENT_LOCATION_CODE, - IPTC_TYPE_CONTENT_LOCATION_NAME, IPTC_TYPE_RELEASE_DATE, - IPTC_TYPE_RELEASE_TIME, IPTC_TYPE_EXPIRATION_DATE, - IPTC_TYPE_EXPIRATION_TIME, IPTC_TYPE_SPECIAL_INSTRUCTIONS, - IPTC_TYPE_ACTION_ADVISED, IPTC_TYPE_REFERENCE_SERVICE, - IPTC_TYPE_REFERENCE_DATE, IPTC_TYPE_REFERENCE_NUMBER, - IPTC_TYPE_DATE_CREATED, IPTC_TYPE_TIME_CREATED, - IPTC_TYPE_DIGITAL_CREATION_DATE, IPTC_TYPE_DIGITAL_CREATION_TIME, - IPTC_TYPE_ORIGINATING_PROGRAM, IPTC_TYPE_PROGRAM_VERSION, - IPTC_TYPE_OBJECT_CYCLE, IPTC_TYPE_BYLINE, IPTC_TYPE_BYLINE_TITLE, - IPTC_TYPE_CITY, IPTC_TYPE_SUBLOCATION, IPTC_TYPE_PROVINCE_STATE, - IPTC_TYPE_COUNTRY_PRIMARY_LOCATION_CODE, - IPTC_TYPE_COUNTRY_PRIMARY_LOCATION_NAME, - IPTC_TYPE_ORIGINAL_TRANSMISSION_REFERENCE, IPTC_TYPE_HEADLINE, - IPTC_TYPE_CREDIT, IPTC_TYPE_SOURCE, IPTC_TYPE_COPYRIGHT_NOTICE, - IPTC_TYPE_CONTACT, IPTC_TYPE_CAPTION_ABSTRACT, - IPTC_TYPE_WRITER_EDITOR, IPTC_TYPE_RASTERIZED_CAPTION, - IPTC_TYPE_IMAGE_TYPE, IPTC_TYPE_IMAGE_ORIENTATION, - IPTC_TYPE_LANGUAGE_IDENTIFIER, IPTC_TYPE_AUDIO_TYPE, - IPTC_TYPE_AUDIO_SAMPLING_RATE, IPTC_TYPE_AUDIO_SAMPLING_RESOLUTION, - IPTC_TYPE_AUDIO_DURATION, IPTC_TYPE_AUDIO_OUTCUE, - IPTC_TYPE_OBJECT_DATA_PREVIEW_FILE_FORMAT, - IPTC_TYPE_OBJECT_DATA_PREVIEW_FILE_FORMAT_VERSION, - IPTC_TYPE_OBJECT_DATA_PREVIEW_DATA, }; - -} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCParser.java b/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCParser.java deleted file mode 100644 index b776f76..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCParser.java +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.jpeg.iptc; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.SanselanConstants; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.BinaryInputStream; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; - -public class IPTCParser extends BinaryFileParser implements IPTCConstants -{ - private static final int APP13_BYTE_ORDER = BYTE_ORDER_NETWORK; - - public IPTCParser() - { - setByteOrder(BYTE_ORDER_NETWORK); - } - - public boolean isPhotoshopJpegSegment(byte segmentData[]) - { - if (!compareByteArrays(segmentData, 0, PHOTOSHOP_IDENTIFICATION_STRING, - 0, PHOTOSHOP_IDENTIFICATION_STRING.length)) - return false; - - int index = PHOTOSHOP_IDENTIFICATION_STRING.length; - if (index + CONST_8BIM.length > segmentData.length) - return false; - - if (!compareByteArrays(segmentData, index, CONST_8BIM, 0, - CONST_8BIM.length)) - return false; - - return true; - } - - /* - * In practice, App13 segments are only used for Photoshop/IPTC metadata. - * However, we should not treat App13 signatures without Photoshop's - * signature as Photoshop/IPTC segments. - * - * A Photoshop/IPTC App13 segment begins with the Photoshop Identification - * string. - * - * There follows 0-N blocks (Photoshop calls them "Image Resource Blocks"). - * - * Each block has the following structure: - * - * 1. 4-byte type. This is always "8BIM" for blocks in a Photoshop App13 - * segment. 2. 2-byte id. IPTC data is stored in blocks with id 0x0404, aka. - * IPTC_NAA_RECORD_IMAGE_RESOURCE_ID 3. Block name as a Pascal String. This - * is padded to have an even length. 4. 4-byte size (in bytes). 5. Block - * data. This is also padded to have an even length. - * - * The block data consists of a 0-N records. A record has the following - * structure: - * - * 1. 2-byte prefix. The value is always 0x1C02 2. 1-byte record type. The - * record types are documented by the IPTC. See IPTCConstants. 3. 2-byte - * record size (in bytes). 4. Record data, "record size" bytes long. - * - * Record data (unlike block data) is NOT padded to have an even length. - * - * Record data, for IPTC record, should always be ISO-8859-1. - * - * The exception is the first record in the block, which must always be a - * record version record, whose value is a two-byte number; the value is - * 0x02. - * - * Some IPTC blocks are missing this first "record version" record, so we - * don't require it. - */ - public PhotoshopApp13Data parsePhotoshopSegment(byte bytes[], Map params) - throws ImageReadException, IOException - { - boolean strict = ParamMap.getParamBoolean(params, - SanselanConstants.PARAM_KEY_STRICT, false); - boolean verbose = ParamMap.getParamBoolean(params, - SanselanConstants.PARAM_KEY_VERBOSE, false); - - return parsePhotoshopSegment(bytes, verbose, strict); - } - - public PhotoshopApp13Data parsePhotoshopSegment(byte bytes[], - boolean verbose, boolean strict) throws ImageReadException, - IOException - { - ArrayList records = new ArrayList(); - - List allBlocks = parseAllBlocks(bytes, verbose, strict); - - for (int i = 0; i < allBlocks.size(); i++) - { - IPTCBlock block = (IPTCBlock) allBlocks.get(i); - - // Ignore everything but IPTC data. - if (!block.isIPTCBlock()) - continue; - - records.addAll(parseIPTCBlock(block.blockData, verbose)); - } - - return new PhotoshopApp13Data(records, allBlocks); - } - - protected List parseIPTCBlock(byte bytes[], boolean verbose) - throws ImageReadException, IOException - { - ArrayList elements = new ArrayList(); - - int index = 0; - // Integer recordVersion = null; - while (index + 1 < bytes.length) - { - int tagMarker = 0xff & bytes[index++]; - if (verbose) - Debug.debug("tagMarker", tagMarker + " (0x" - + Integer.toHexString(tagMarker) + ")"); - - if (tagMarker != IPTC_RECORD_TAG_MARKER) - { - if (verbose) - System.out - .println("Unexpected record tag marker in IPTC data."); - return elements; - } - - int recordNumber = 0xff & bytes[index++]; - if (verbose) - Debug.debug("recordNumber", recordNumber + " (0x" - + Integer.toHexString(recordNumber) + ")"); - - if (recordNumber != IPTC_APPLICATION_2_RECORD_NUMBER) - continue; - - // int recordPrefix = convertByteArrayToShort("recordPrefix", index, - // bytes); - // if (verbose) - // Debug.debug("recordPrefix", recordPrefix + " (0x" - // + Integer.toHexString(recordPrefix) + ")"); - // index += 2; - // - // if (recordPrefix != IPTC_RECORD_PREFIX) - // { - // if (verbose) - // System.out - // .println("Unexpected record prefix in IPTC data!"); - // return elements; - // } - - // throw new ImageReadException( - // "Unexpected record prefix in IPTC data."); - - int recordType = 0xff & bytes[index]; - if (verbose) - Debug.debug("recordType", recordType + " (0x" - + Integer.toHexString(recordType) + ")"); - index++; - - int recordSize = convertByteArrayToShort("recordSize", index, bytes); - index += 2; - - boolean extendedDataset = recordSize > IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE; - int dataFieldCountLength = recordSize & 0x7fff; - if (extendedDataset && verbose) - Debug.debug("extendedDataset. dataFieldCountLength: " - + dataFieldCountLength); - if (extendedDataset) // ignore extended dataset and everything - // after. - return elements; - - byte recordData[] = readBytearray("recordData", bytes, index, - recordSize); - index += recordSize; - - // Debug.debug("recordSize", recordSize + " (0x" - // + Integer.toHexString(recordSize) + ")"); - - if (recordType == 0) - { - if (verbose) - System.out.println("ignore record version record! " - + elements.size()); - // ignore "record version" record; - continue; - } - // if (recordVersion == null) - // { - // // The first record in a JPEG/Photoshop IPTC block must be - // // the record version. - // if (recordType != 0) - // throw new ImageReadException("Missing record version: " - // + recordType); - // recordVersion = new Integer(convertByteArrayToShort( - // "recordNumber", recordData)); - // - // if (recordSize != 2) - // throw new ImageReadException( - // "Invalid record version record size: " + recordSize); - // - // // JPEG/Photoshop IPTC metadata is always in Record version - // // 2 - // if (recordVersion.intValue() != 2) - // throw new ImageReadException( - // "Invalid IPTC record version: " + recordVersion); - // - // // Debug.debug("recordVersion", recordVersion); - // continue; - // } - - String value = new String(recordData, "ISO-8859-1"); - - IPTCType iptcType = IPTCTypeLookup.getIptcType(recordType); - - // Debug.debug("iptcType", iptcType); - // debugByteArray("iptcData", iptcData); - // Debug.debug(); - - // if (recordType == IPTC_TYPE_CREDIT.type - // || recordType == IPTC_TYPE_OBJECT_NAME.type) - // { - // this.debugByteArray("recordData", recordData); - // Debug.debug("index", IPTC_TYPE_CREDIT.name); - // } - - IPTCRecord element = new IPTCRecord(iptcType, value); - elements.add(element); - } - - return elements; - } - - protected List parseAllBlocks(byte bytes[], boolean verbose, boolean strict) - throws ImageReadException, IOException - { - List blocks = new ArrayList(); - - BinaryInputStream bis = new BinaryInputStream(bytes, APP13_BYTE_ORDER); - - // Note that these are unsigned quantities. Name is always an even - // number of bytes (including the 1st byte, which is the size.) - - byte[] idString = bis.readByteArray( - PHOTOSHOP_IDENTIFICATION_STRING.length, - "App13 Segment missing identification string"); - if (!compareByteArrays(idString, PHOTOSHOP_IDENTIFICATION_STRING)) - throw new ImageReadException("Not a Photoshop App13 Segment"); - - // int index = PHOTOSHOP_IDENTIFICATION_STRING.length; - - while (true) - { - byte[] imageResourceBlockSignature = bis - .readByteArray(CONST_8BIM.length, - "App13 Segment missing identification string", - false, false); - if (null == imageResourceBlockSignature) - break; - if (!compareByteArrays(imageResourceBlockSignature, CONST_8BIM)) - throw new ImageReadException( - "Invalid Image Resource Block Signature"); - - int blockType = bis - .read2ByteInteger("Image Resource Block missing type"); - if (verbose) - Debug.debug("blockType", blockType + " (0x" - + Integer.toHexString(blockType) + ")"); - - int blockNameLength = bis - .read1ByteInteger("Image Resource Block missing name length"); - if (verbose && blockNameLength > 0) - Debug.debug("blockNameLength", blockNameLength + " (0x" - + Integer.toHexString(blockNameLength) + ")"); - byte[] blockNameBytes; - if (blockNameLength == 0) - { - bis.read1ByteInteger("Image Resource Block has invalid name"); - blockNameBytes = new byte[0]; - } else - { - blockNameBytes = bis.readByteArray(blockNameLength, - "Invalid Image Resource Block name", verbose, strict); - if (null == blockNameBytes) - break; - - if (blockNameLength % 2 == 0) - bis - .read1ByteInteger("Image Resource Block missing padding byte"); - } - - int blockSize = bis - .read4ByteInteger("Image Resource Block missing size"); - if (verbose) - Debug.debug("blockSize", blockSize + " (0x" - + Integer.toHexString(blockSize) + ")"); - - /* - * doesn't catch cases where blocksize is invalid but is still less than bytes.length - * but will at least prevent OutOfMemory errors - */ - if(blockSize > bytes.length) { - throw new ImageReadException("Invalid Block Size : "+blockSize+ " > "+bytes.length); - } - - byte[] blockData = bis.readByteArray(blockSize, - "Invalid Image Resource Block data", verbose, strict); - if (null == blockData) - break; - - blocks.add(new IPTCBlock(blockType, blockNameBytes, blockData)); - - if ((blockSize % 2) != 0) - bis - .read1ByteInteger("Image Resource Block missing padding byte"); - } - - return blocks; - } - - // private void writeIPTCRecord(BinaryOutputStream bos, ) - - public byte[] writePhotoshopApp13Segment(PhotoshopApp13Data data) - throws IOException, ImageWriteException - { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - BinaryOutputStream bos = new BinaryOutputStream(os); - - bos.write(PHOTOSHOP_IDENTIFICATION_STRING); - - List blocks = data.getRawBlocks(); - for (int i = 0; i < blocks.size(); i++) - { - IPTCBlock block = (IPTCBlock) blocks.get(i); - - bos.write(CONST_8BIM); - - if (block.blockType < 0 || block.blockType > 0xffff) - throw new ImageWriteException("Invalid IPTC block type."); - bos.write2ByteInteger(block.blockType); - - if (block.blockNameBytes.length > 255) - throw new ImageWriteException("IPTC block name is too long: " - + block.blockNameBytes.length); - bos.write(block.blockNameBytes.length); - bos.write(block.blockNameBytes); - if (block.blockNameBytes.length % 2 == 0) - bos.write(0); // pad to even size, including length byte. - - if (block.blockData.length > IPTC_NON_EXTENDED_RECORD_MAXIMUM_SIZE) - throw new ImageWriteException("IPTC block data is too long: " - + block.blockData.length); - bos.write4ByteInteger(block.blockData.length); - bos.write(block.blockData); - if (block.blockData.length % 2 == 1) - bos.write(0); // pad to even size - - } - - bos.flush(); - return os.toByteArray(); - } - - public byte[] writeIPTCBlock(List elements) throws ImageWriteException, - IOException - { - byte blockData[]; - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BinaryOutputStream bos = new BinaryOutputStream(baos, - getByteOrder()); - - // first, right record version record - bos.write(IPTC_RECORD_TAG_MARKER); - bos.write(IPTC_APPLICATION_2_RECORD_NUMBER); - bos.write(IPTC_TYPE_RECORD_VERSION.type); // record version record - // type. - bos.write2Bytes(2); // record version record size - bos.write2Bytes(2); // record version value - - // make a copy of the list. - elements = new ArrayList(elements); - - // sort the list. Records must be in numerical order. - Comparator comparator = new Comparator() { - public int compare(Object o1, Object o2) - { - IPTCRecord e1 = (IPTCRecord) o1; - IPTCRecord e2 = (IPTCRecord) o2; - return e2.iptcType.type - e1.iptcType.type; - } - }; - Collections.sort(elements, comparator); - // TODO: make sure order right - - // write the list. - for (int i = 0; i < elements.size(); i++) - { - IPTCRecord element = (IPTCRecord) elements.get(i); - - if (element.iptcType.type == IPTC_TYPE_RECORD_VERSION.type) - continue; // ignore - - bos.write(IPTC_RECORD_TAG_MARKER); - bos.write(IPTC_APPLICATION_2_RECORD_NUMBER); - if (element.iptcType.type < 0 || element.iptcType.type > 0xff) - throw new ImageWriteException("Invalid record type: " - + element.iptcType.type); - bos.write(element.iptcType.type); - - byte recordData[] = element.value.getBytes("ISO-8859-1"); - if (!new String(recordData, "ISO-8859-1").equals(element.value)) - throw new ImageWriteException( - "Invalid record value, not ISO-8859-1"); - - bos.write2Bytes(recordData.length); - bos.write(recordData); - } - - blockData = baos.toByteArray(); - } - - return blockData; - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCRecord.java b/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCRecord.java deleted file mode 100644 index 4cc7182..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/iptc/IPTCRecord.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.iptc; - -import java.util.Comparator; - -/* - * Represents an IPTC record, a single key-value pair of Photoshop IPTC data. - */ -public class IPTCRecord -{ - public final IPTCType iptcType; - public final String value; - - public IPTCRecord(IPTCType iptcType, String value) - { - this.iptcType = iptcType; - this.value = value; - } - - public String getValue() - { - return value; - } - - public String getIptcTypeName() - { - return iptcType.name; - } - - public static final Comparator COMPARATOR = new Comparator() { - public int compare(Object o1, Object o2) - { - IPTCRecord e1 = (IPTCRecord) o1; - IPTCRecord e2 = (IPTCRecord) o2; - return e1.iptcType.type - e2.iptcType.type; - } - }; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/App2Segment.java b/src/main/java/org/apache/sanselan/formats/jpeg/segments/App2Segment.java deleted file mode 100644 index 78db809..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/App2Segment.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.jpeg.JpegImageParser; - -public class App2Segment extends APPNSegment implements Comparable -{ - public final byte icc_bytes[]; - public final int cur_marker, num_markers; - - public App2Segment(int marker, byte segmentData[]) - throws ImageReadException, IOException - { - this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); - } - - public App2Segment(int marker, int marker_length, InputStream is2) - throws ImageReadException, IOException - { - super(marker, marker_length, is2); - - if (startsWith(bytes, JpegImageParser.icc_profile_label)) - { - InputStream is = new ByteArrayInputStream(bytes); - - readAndVerifyBytes(is, JpegImageParser.icc_profile_label, - "Not a Valid App2 Segment: missing ICC Profile label"); - - cur_marker = readByte("cur_marker", is, "Not a valid App2 Marker"); - num_markers = readByte("num_markers", is, "Not a valid App2 Marker"); - - marker_length -= JpegImageParser.icc_profile_label.length; - marker_length -= (1 + 1); - - icc_bytes = readByteArray("App2 Data", marker_length, is, - "Invalid App2 Segment: insufficient data"); - } - else - { - // debugByteArray("Unknown APP2 Segment Type", bytes); - cur_marker = -1; - num_markers = -1; - icc_bytes = null; - } - } - - public int compareTo(Object o) - { - App2Segment other = (App2Segment) o; - return cur_marker - other.cur_marker; - } - - // public String getDescription() - // { - // return "APPN (APP" - // + (marker - JpegImageParser.JPEG_APP0) - // + ") (" + getDescription() + ")"; - // } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/GenericSegment.java b/src/main/java/org/apache/sanselan/formats/jpeg/segments/GenericSegment.java deleted file mode 100644 index 7887602..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/GenericSegment.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; - -import org.apache.sanselan.ImageReadException; - -public abstract class GenericSegment extends Segment -{ - public final byte bytes[]; - - public GenericSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length); - - bytes = readByteArray("Segment Data", marker_length, is, - "Invalid Segment: insufficient data"); - } - - public GenericSegment(int marker, byte bytes[]) throws ImageReadException, - IOException - { - super(marker, bytes.length); - - this.bytes = bytes; - } - - public void dump(PrintWriter pw) - { - dump(pw, 0); - } - - public void dump(PrintWriter pw, int start) - { - for (int i = 0; (i < 50) && ((i + start) < bytes.length); i++) - { - debugNumber(pw, "\t" + (i + start), bytes[i + start]); - } - } - - // public String getDescription() - // { - // return "Unknown"; - // } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOFNSegment.java b/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOFNSegment.java deleted file mode 100644 index 3ef372b..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOFNSegment.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.jpeg.JpegImageParser; - -public class SOFNSegment extends Segment -{ - public final int width, height; - public final int numberOfComponents; - public final int precision; - - public SOFNSegment(int marker, byte segmentData[]) - throws ImageReadException, IOException - { - this(marker, segmentData.length, new ByteArrayInputStream(segmentData)); - } - - public SOFNSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length); - - if (getDebug()) - System.out.println("SOF0Segment marker_length: " + marker_length); - - { - precision = readByte("Data_precision", is, "Not a Valid JPEG File"); - height = read2Bytes("Image_height", is, "Not a Valid JPEG File"); - width = read2Bytes("Image_Width", is, "Not a Valid JPEG File"); - numberOfComponents = readByte("Number_of_components", is, - "Not a Valid JPEG File"); - - // ignore the rest of the segment for now... - skipBytes(is, marker_length - 6, - "Not a Valid JPEG File: SOF0 Segment"); - - // int Each_component1 = read_byte("Each_component1", is, - // "Not a Valid JPEG File"); - // int Each_component2 = read_byte("Each_component2", is, - // "Not a Valid JPEG File"); - // int Each_component3 = read_byte("Each_component3", is, - // "Not a Valid JPEG File"); - } - - if (getDebug()) - System.out.println(""); - } - - public String getDescription() - { - return "SOFN (SOF" + (marker - JpegImageParser.SOF0Marker) + ") (" - + getSegmentType() + ")"; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOSSegment.java b/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOSSegment.java deleted file mode 100644 index 28c6135..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/SOSSegment.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.util.Debug; - -public class SOSSegment extends Segment -{ - // public final int width, height; - // public final int Number_of_components; - // public final int Precision; - - // public final byte bytes[]; - // public final int cur_marker, num_markers; - - public SOSSegment(int marker, int marker_length, InputStream is) - throws ImageReadException, IOException - { - super(marker, marker_length); - - if (getDebug()) - System.out.println("SOSSegment marker_length: " + marker_length); - - Debug.debug("SOS", marker_length); - // { - int number_of_components_in_scan = readByte( - "number_of_components_in_scan", is, "Not a Valid JPEG File"); - Debug.debug("number_of_components_in_scan", - number_of_components_in_scan); - - for (int i = 0; i < number_of_components_in_scan; i++) - { - int scan_component_selector = readByte("scan_component_selector", - is, "Not a Valid JPEG File"); - Debug.debug("scan_component_selector", scan_component_selector); - - int ac_dc_entrooy_coding_table_selector = readByte( - "ac_dc_entrooy_coding_table_selector", is, - "Not a Valid JPEG File"); - Debug.debug("ac_dc_entrooy_coding_table_selector", - ac_dc_entrooy_coding_table_selector); - } - - int start_of_spectral_selection = readByte( - "start_of_spectral_selection", is, "Not a Valid JPEG File"); - Debug.debug("start_of_spectral_selection", start_of_spectral_selection); - int end_of_spectral_selection = readByte("end_of_spectral_selection", - is, "Not a Valid JPEG File"); - Debug.debug("end_of_spectral_selection", end_of_spectral_selection); - int successive_approximation_bit_position = readByte( - "successive_approximation_bit_position", is, - "Not a Valid JPEG File"); - Debug.debug("successive_approximation_bit_position", - successive_approximation_bit_position); - - // height = read2Bytes("Image_height", is, "Not a Valid JPEG File"); - // width = read2Bytes("Image_Width", is, "Not a Valid JPEG File"); - // Number_of_components = read_byte("Number_of_components", is, - // "Not a Valid JPEG File"); - // - // // ignore the rest of the segment for now... - // skipBytes(is, marker_length - 6, - // "Not a Valid JPEG File: SOF0 Segment"); - // - // // int Each_component1 = read_byte("Each_component1", is, - // // "Not a Valid JPEG File"); - // // int Each_component2 = read_byte("Each_component2", is, - // // "Not a Valid JPEG File"); - // // int Each_component3 = read_byte("Each_component3", is, - // // "Not a Valid JPEG File"); - // } - - if (getDebug()) - System.out.println(""); - } - - public String getDescription() - { - return "SOS (" + getSegmentType() + ")"; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/segments/Segment.java b/src/main/java/org/apache/sanselan/formats/jpeg/segments/Segment.java deleted file mode 100644 index 612a1ba..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/segments/Segment.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.segments; - -import java.io.PrintWriter; - -import org.apache.sanselan.common.BinaryFileParser; - -public abstract class Segment extends BinaryFileParser -{ - public final int marker; - public final int length; - - public Segment(int marker, int length) - { - // super(); - - this.marker = marker; - this.length = length; - } - - public void dump(PrintWriter pw) - { - } - - public abstract String getDescription(); - - public String toString() - { - return "[Segment: " + getDescription() + "]"; - } - - public String getSegmentType() - { - - switch (marker) - { - case 0xffc0 : - return "Start Of Frame, Baseline DCT, Huffman coding"; - case 0xffc1 : - return "Start Of Frame, Extended sequential DCT, Huffman coding"; - case 0xffc2 : - return "Start Of Frame, Progressive DCT, Huffman coding"; - case 0xffc3 : - return "Start Of Frame, Lossless (sequential), Huffman coding"; - - case 0xffc5 : - return "Start Of Frame, Differential sequential DCT, Huffman coding"; - case 0xffc6 : - return "Start Of Frame, Differential progressive DCT, Huffman coding"; - case 0xffc7 : - return "Start Of Frame, Differential lossless (sequential), Huffman coding"; - - case 0xffc8 : - return "Start Of Frame, Reserved for JPEG extensions, arithmetic coding"; - case 0xffc9 : - return "Start Of Frame, Extended sequential DCT, arithmetic coding"; - case 0xffca : - return "Start Of Frame, Progressive DCT, arithmetic coding"; - case 0xffcb : - return "Start Of Frame, Lossless (sequential), arithmetic coding"; - - case 0xffcd : - return "Start Of Frame, Differential sequential DCT, arithmetic coding"; - case 0xffce : - return "Start Of Frame, Differential progressive DCT, arithmetic coding"; - case 0xffcf : - return "Start Of Frame, Differential lossless (sequential), arithmetic coding"; - - case 0xffc4 : - return "Define Huffman table(s)"; - case 0xffcc : - return "Define arithmetic coding conditioning(s)"; - - case 0xffd0 : - return "Restart with modulo 8 count 0"; - case 0xffd1 : - return "Restart with modulo 8 count 1"; - case 0xffd2 : - return "Restart with modulo 8 count 2"; - case 0xffd3 : - return "Restart with modulo 8 count 3"; - case 0xffd4 : - return "Restart with modulo 8 count 4"; - case 0xffd5 : - return "Restart with modulo 8 count 5"; - case 0xffd6 : - return "Restart with modulo 8 count 6"; - case 0xffd7 : - return "Restart with modulo 8 count 7"; - - case 0xffd8 : - return "Start of image"; - case 0xffd9 : - return "End of image"; - case 0xffda : - return "Start of scan"; - case 0xffdb : - return "Define quantization table(s)"; - case 0xffdc : - return "Define number of lines"; - case 0xffdd : - return "Define restart interval"; - case 0xffde : - return "Define hierarchical progression"; - case 0xffdf : - return "Expand reference component(s)"; - // case 0xffd8 : - // return "Reserved for application segments"; - // case 0xffd8 : - // return "Reserved for JPEG extensions"; - case 0xfffe : - return "Comment"; - case 0xff01 : - return "For temporary private use in arithmetic coding"; - // case 0xffd8 : - // return "Reserved"; - - default : - } - - if ((marker >= 0xff02) && (marker <= 0xffbf)) - return "Reserved"; - if ((marker >= 0xffe0) && (marker <= 0xffef)) - return "APP" + (marker - 0xffe0); - if ((marker >= 0xfff0) && (marker <= 0xfffd)) - return "JPG" + (marker - 0xffe0); - - return "Unknown"; - - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegRewriter.java b/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegRewriter.java deleted file mode 100644 index ad32c65..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegRewriter.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.jpeg.xmp; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.jpeg.JpegConstants; -import org.apache.sanselan.formats.jpeg.JpegUtils; -import org.apache.sanselan.formats.jpeg.iptc.IPTCParser; - -/** - * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. - *

- *

- * See the source of the XmpXmlUpdateExample class for example usage. - * - * @see org.apache.sanselan.sampleUsage.WriteXmpXmlExample - */ -public class JpegRewriter extends BinaryFileParser implements JpegConstants -{ - private static final int JPEG_BYTE_ORDER = BYTE_ORDER_NETWORK; - - /** - * Constructor. to guess whether a file contains an image based on its file - * extension. - */ - public JpegRewriter() - { - setByteOrder(JPEG_BYTE_ORDER); - } - - protected static class JFIFPieces - { - public final List pieces; - public final List segmentPieces; - - public JFIFPieces(final List pieces, final List segmentPieces) - { - this.pieces = pieces; - this.segmentPieces = segmentPieces; - } - - } - - protected abstract static class JFIFPiece - { - protected abstract void write(OutputStream os) throws IOException; - - public String toString() - { - return "[" + this.getClass().getName() + "]"; - } - } - - protected static class JFIFPieceSegment extends JFIFPiece - { - public final int marker; - public final byte markerBytes[]; - public final byte segmentLengthBytes[]; - public final byte segmentData[]; - - public JFIFPieceSegment(final int marker, final byte[] segmentData) - { - this(marker, int2ToByteArray(marker, JPEG_BYTE_ORDER), - int2ToByteArray(segmentData.length + 2, JPEG_BYTE_ORDER), - segmentData); - } - - public JFIFPieceSegment(final int marker, final byte[] markerBytes, - final byte[] segmentLengthBytes, final byte[] segmentData) - { - this.marker = marker; - this.markerBytes = markerBytes; - this.segmentLengthBytes = segmentLengthBytes; - this.segmentData = segmentData; - } - - public String toString() - { - return "[" + this.getClass().getName() + " (0x" + Integer.toHexString(marker) + ")]"; - } - - protected void write(OutputStream os) throws IOException - { - os.write(markerBytes); - os.write(segmentLengthBytes); - os.write(segmentData); - } - - public boolean isApp1Segment() - { - return marker == JPEG_APP1_Marker; - } - - public boolean isAppSegment() - { - return marker >= JPEG_APP0_Marker && marker <= JPEG_APP15_Marker; - } - - public boolean isExifSegment() - { - if (marker != JPEG_APP1_Marker) - return false; - if (!byteArrayHasPrefix(segmentData, EXIF_IDENTIFIER_CODE)) - return false; - return true; - } - - public boolean isPhotoshopApp13Segment() - { - if (marker != JPEG_APP13_Marker) - return false; - if (!new IPTCParser().isPhotoshopJpegSegment(segmentData)) - return false; - return true; - } - - public boolean isXmpSegment() - { - if (marker != JPEG_APP1_Marker) - return false; - if (!byteArrayHasPrefix(segmentData, XMP_IDENTIFIER)) - return false; - return true; - } - - } - - protected static class JFIFPieceImageData extends JFIFPiece - { - public final byte markerBytes[]; - public final byte imageData[]; - - public JFIFPieceImageData(final byte[] markerBytes, - final byte[] imageData) - { - super(); - this.markerBytes = markerBytes; - this.imageData = imageData; - } - - protected void write(OutputStream os) throws IOException - { - os.write(markerBytes); - os.write(imageData); - } - } - - protected JFIFPieces analyzeJFIF(ByteSource byteSource) - throws ImageReadException, IOException - // , ImageWriteException - { - final ArrayList pieces = new ArrayList(); - final List segmentPieces = new ArrayList(); - - JpegUtils.Visitor visitor = new JpegUtils.Visitor() { - // return false to exit before reading image data. - public boolean beginSOS() - { - return true; - } - - public void visitSOS(int marker, byte markerBytes[], - byte imageData[]) - { - pieces.add(new JFIFPieceImageData(markerBytes, imageData)); - } - - // return false to exit traversal. - public boolean visitSegment(int marker, byte markerBytes[], - int segmentLength, byte segmentLengthBytes[], - byte segmentData[]) throws ImageReadException, IOException - { - JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes, - segmentLengthBytes, segmentData); - pieces.add(piece); - segmentPieces.add(piece); - - return true; - } - }; - - new JpegUtils().traverseJFIF(byteSource, visitor); - - return new JFIFPieces(pieces, segmentPieces); - } - - private static interface SegmentFilter - { - public boolean filter(JFIFPieceSegment segment); - } - - private static final SegmentFilter EXIF_SEGMENT_FILTER = new SegmentFilter() { - public boolean filter(JFIFPieceSegment segment) - { - return segment.isExifSegment(); - } - }; - - private static final SegmentFilter XMP_SEGMENT_FILTER = new SegmentFilter() { - public boolean filter(JFIFPieceSegment segment) - { - return segment.isXmpSegment(); - } - }; - - private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = new SegmentFilter() { - public boolean filter(JFIFPieceSegment segment) - { - return segment.isPhotoshopApp13Segment(); - } - }; - - protected List removeXmpSegments(List segments) - { - return filterSegments(segments, XMP_SEGMENT_FILTER); - } - - protected List removePhotoshopApp13Segments(List segments) - { - return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER); - } - - protected List findPhotoshopApp13Segments(List segments) - { - return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true); - } - - protected List removeExifSegments(List segments) - { - return filterSegments(segments, EXIF_SEGMENT_FILTER); - } - - protected List filterSegments(List segments, SegmentFilter filter) - { - return filterSegments(segments, filter, false); - } - - protected List filterSegments(List segments, SegmentFilter filter, - boolean reverse) - { - List result = new ArrayList(); - - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - if (piece instanceof JFIFPieceSegment) - { - if (filter.filter((JFIFPieceSegment) piece) ^ !reverse) - result.add(piece); - } else if(!reverse) - result.add(piece); - } - - return result; - } - - protected List insertBeforeFirstAppSegments(List segments, List newSegments) - throws ImageWriteException - { - int firstAppIndex = -1; - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - if (!(piece instanceof JFIFPieceSegment)) - continue; - - JFIFPieceSegment segment = (JFIFPieceSegment) piece; - if (segment.isAppSegment()) - { - if (firstAppIndex == -1) - firstAppIndex = i; - } - } - - List result = new ArrayList(segments); - if (firstAppIndex == -1) - throw new ImageWriteException("JPEG file has no APP segments."); - result.addAll(firstAppIndex, newSegments); - return result; - } - - protected List insertAfterLastAppSegments(List segments, List newSegments) - throws ImageWriteException - { - int lastAppIndex = -1; - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - if (!(piece instanceof JFIFPieceSegment)) - continue; - - JFIFPieceSegment segment = (JFIFPieceSegment) piece; - if (segment.isAppSegment()) - lastAppIndex = i; - } - - List result = new ArrayList(segments); - if (lastAppIndex == -1) - { - if(segments.size()<1) - throw new ImageWriteException("JPEG file has no APP segments."); - result.addAll(1, newSegments); - } - else - result.addAll(lastAppIndex + 1, newSegments); - - return result; - } - - protected void writeSegments(OutputStream os, List segments) - throws ImageWriteException, IOException - { - try - { - os.write(SOI); - - for (int i = 0; i < segments.size(); i++) - { - JFIFPiece piece = (JFIFPiece) segments.get(i); - piece.write(os); - } - os.close(); - os = null; - } finally - { - try - { - if (os != null) - os.close(); - } catch (Exception e) - { - // swallow exception; already in the context of an exception. - } - } - } - - // private void writeSegment(OutputStream os, JFIFPieceSegment piece) - // throws ImageWriteException, IOException - // { - // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_Marker, - // JPEG_BYTE_ORDER); - // if (piece.segmentData.length > 0xffff) - // throw new JpegSegmentOverflowException("Jpeg segment is too long: " - // + piece.segmentData.length); - // int segmentLength = piece.segmentData.length + 2; - // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength, - // JPEG_BYTE_ORDER); - // - // os.write(markerBytes); - // os.write(segmentLengthBytes); - // os.write(piece.segmentData); - // } - - public static class JpegSegmentOverflowException extends - ImageWriteException - { - public JpegSegmentOverflowException(String s) - { - super(s); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpParser.java b/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpParser.java deleted file mode 100644 index d1dbe69..0000000 --- a/src/main/java/org/apache/sanselan/formats/jpeg/xmp/JpegXmpParser.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.jpeg.xmp; - -import java.io.UnsupportedEncodingException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.formats.jpeg.JpegConstants; - -public class JpegXmpParser extends BinaryFileParser implements JpegConstants -{ - - public JpegXmpParser() - { - setByteOrder(BYTE_ORDER_NETWORK); - } - - - public boolean isXmpJpegSegment(byte segmentData[]) - { - int index = 0; - - if (segmentData.length < XMP_IDENTIFIER.length) - return false; - for (; index < XMP_IDENTIFIER.length; index++) - if (segmentData[index] < XMP_IDENTIFIER[index]) - return false; - - return true; - } - - public String parseXmpJpegSegment(byte segmentData[]) - throws ImageReadException - { - int index = 0; - - if (segmentData.length < XMP_IDENTIFIER.length) - throw new ImageReadException("Invalid JPEG XMP Segment."); - for (; index < XMP_IDENTIFIER.length; index++) - if (segmentData[index] < XMP_IDENTIFIER[index]) - throw new ImageReadException("Invalid JPEG XMP Segment."); - - try - { - // segment data is UTF-8 encoded xml. - String xml = new String(segmentData, index, segmentData.length - - index, "utf-8"); - return xml; - } catch (UnsupportedEncodingException e) - { - throw new ImageReadException("Invalid JPEG XMP Segment."); - } - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/png/PngConstants.java b/src/main/java/org/apache/sanselan/formats/png/PngConstants.java deleted file mode 100644 index 9589e0b..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/PngConstants.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import org.apache.sanselan.SanselanConstants; - -public interface PngConstants extends SanselanConstants -{ - - public static final int COMPRESSION_DEFLATE_INFLATE = 0; - - public final static byte[] IHDR_CHUNK_TYPE = new byte[] { 73, 72, 68, 82 }; - public final static byte[] PLTE_CHUNK_TYPE = new byte[] { 80, 76, 84, 69 }; - public final static byte[] IEND_CHUNK_TYPE = new byte[] { 73, 69, 78, 68 }; - public final static byte[] IDAT_CHUNK_TYPE = new byte[] { 73, 68, 65, 84 }; - public final static byte[] iTXt_CHUNK_TYPE = new byte[] { // - 105, // - 84, // - 88, // - 116, // - }; - public final static byte[] tEXt_CHUNK_TYPE = new byte[] { // - 0x74, // - 0x45, // - 0x58, // - 0x74, // - }; - public final static byte[] zTXt_CHUNK_TYPE = new byte[] { // - 0x7A, // - 0x54, // - 0x58, // - 0x74, // - }; - - public final static int IEND = PngImageParser.CharsToQuad('I', 'E', 'N', - 'D'); - public final static int IHDR = PngImageParser.CharsToQuad('I', 'H', 'D', - 'R'); - public final static int iCCP = PngImageParser.CharsToQuad('i', 'C', 'C', - 'P'); - public final static int tEXt = PngImageParser.CharsToQuad('t', 'E', 'X', - 't'); - public final static int zTXt = PngImageParser.CharsToQuad('z', 'T', 'X', - 't'); - public final static int pHYs = PngImageParser.CharsToQuad('p', 'H', 'Y', - 's'); - public final static int PLTE = PngImageParser.CharsToQuad('P', 'L', 'T', - 'E'); - public final static int IDAT = PngImageParser.CharsToQuad('I', 'D', 'A', - 'T'); - public final static int tRNS = PngImageParser.CharsToQuad('t', 'R', 'N', - 'S'); - public final static int gAMA = PngImageParser.CharsToQuad('g', 'A', 'M', - 'A'); - public final static int sRGB = PngImageParser.CharsToQuad('s', 'R', 'G', - 'B'); - - // XMP chunk type. - public final static int iTXt = PngImageParser.CharsToQuad('i', 'T', 'X', - 't'); - - public static final byte PNG_Signature[] = { - (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, }; - - public static final String PARAM_KEY_PNG_BIT_DEPTH = "PNG_BIT_DEPTH"; - public static final String PARAM_KEY_PNG_FORCE_INDEXED_COLOR = "PNG_FORCE_INDEXED_COLOR"; - public static final String PARAM_KEY_PNG_FORCE_TRUE_COLOR = "PNG_FORCE_TRUE_COLOR"; - - // public static final Object PARAM_KEY_PNG_BIT_DEPTH_YES = "YES"; - // public static final Object PARAM_KEY_PNG_BIT_DEPTH_NO = "NO"; - - public static final int COLOR_TYPE_GREYSCALE = 0; - public static final int COLOR_TYPE_TRUE_COLOR = 2; - public static final int COLOR_TYPE_INDEXED_COLOR = 3; - public static final int COLOR_TYPE_GREYSCALE_WITH_ALPHA = 4; - public static final int COLOR_TYPE_TRUE_COLOR_WITH_ALPHA = 6; - - public static final byte COMPRESSION_TYPE_INFLATE_DEFLATE = 0; - public static final byte FILTER_METHOD_ADAPTIVE = 0; - - public static final byte INTERLACE_METHOD_NONE = 0; - public static final byte INTERLACE_METHOD_ADAM7 = 1; - - public static final byte FILTER_TYPE_NONE = 0; - public static final byte FILTER_TYPE_SUB = 1; - public static final byte FILTER_TYPE_UP = 2; - public static final byte FILTER_TYPE_AVERAGE = 3; - public static final byte FILTER_TYPE_PAETH = 4; - - /* - * Background colour Solid background colour to be used when presenting the - * image if no better option is available. Gamma and chromaticity Gamma - * characteristic of the image with respect to the desired output intensity, - * and chromaticity characteristics of the RGB values used in the image. ICC - * profile Description of the colour space (in the form of an International - * Color Consortium (ICC) profile) to which the samples in the image - * conform. Image histogram Estimates of how frequently the image uses each - * palette entry. Physical pixel dimensions Intended pixel size and aspect - * ratio to be used in presenting the PNG image. Significant bits The number - * of bits that are significant in the samples. sRGB colour space A - * rendering intent (as defined by the International Color Consortium) and - * an indication that the image samples conform to this colour space. - * Suggested palette A reduced palette that may be used when the display - * device is not capable of displaying the full range of colours in the - * image. Textual data Textual information (which may be compressed) - * associated with the image. Time The time when the PNG image was last - * modified. Transparency Alpha information that allows the reference image - * to be reconstructed when the alpha channel is not retained in the PNG - * image. - */ - - public final String XMP_KEYWORD = "XML:com.adobe.xmp"; - - /** - * Parameter key. - * - * Only used when writing Png images. - *

- * Valid values: a list of WriteTexts. - *

- */ - public static final String PARAM_KEY_PNG_TEXT_CHUNKS = "PNG_TEXT_CHUNKS"; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/PngImageParser.java b/src/main/java/org/apache/sanselan/formats/png/PngImageParser.java deleted file mode 100644 index dacdc16..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/PngImageParser.java +++ /dev/null @@ -1,941 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.zip.InflaterInputStream; - -import org.apache.sanselan.ColorTools; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.ImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.png.chunks.PNGChunk; -import org.apache.sanselan.formats.png.chunks.PNGChunkIDAT; -import org.apache.sanselan.formats.png.chunks.PNGChunkIHDR; -import org.apache.sanselan.formats.png.chunks.PNGChunkPLTE; -import org.apache.sanselan.formats.png.chunks.PNGChunkgAMA; -import org.apache.sanselan.formats.png.chunks.PNGChunkiCCP; -import org.apache.sanselan.formats.png.chunks.PNGChunkiTXt; -import org.apache.sanselan.formats.png.chunks.PNGChunkpHYs; -import org.apache.sanselan.formats.png.chunks.PNGChunktEXt; -import org.apache.sanselan.formats.png.chunks.PNGChunkzTXt; -import org.apache.sanselan.formats.png.chunks.PNGTextChunk; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilter; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilterGrayscale; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilterIndexedColor; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilterTrueColor; -import org.apache.sanselan.icc.IccProfileParser; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.color.ColorSpace; -import com.google.code.appengine.awt.color.ICC_ColorSpace; -import com.google.code.appengine.awt.color.ICC_Profile; -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.ColorModel; - - -public class PngImageParser extends ImageParser implements PngConstants -{ - - public PngImageParser() - { - // setDebug(true); - } - - public String getName() - { - return "Png-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".png"; - - private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_PNG, // - }; - } - - // private final static int tRNS = CharsToQuad('t', 'R', 'N', 's'); - - public static final String getChunkTypeName(int chunkType) { - StringBuffer result = new StringBuffer(); - result.append((char) (0xff & (chunkType >> 24))); - result.append((char) (0xff & (chunkType >> 16))); - result.append((char) (0xff & (chunkType >> 8))); - result.append((char) (0xff & (chunkType >> 0))); - return result.toString(); - } - - /** - * @return List of String-formatted chunk types, ie. "tRNs". - */ - public List getChuckTypes(InputStream is) throws ImageReadException, IOException { - List chunks = readChunks(is, null, false); - List chunkTypes = new ArrayList(); - for (int i=0; i 0; - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - - private boolean keepChunk(int ChunkType, int chunkTypes[]) - { - // System.out.println("keepChunk: "); - if (chunkTypes == null) - return true; - - for (int i = 0; i < chunkTypes.length; i++) - { - if (chunkTypes[i] == ChunkType) - return true; - } - return false; - } - - private ArrayList readChunks(InputStream is, int chunkTypes[], - boolean returnAfterFirst) throws ImageReadException, IOException - { - ArrayList result = new ArrayList(); - - while (true) - { - if (debug) - System.out.println(""); - - int length = read4Bytes("Length", is, "Not a Valid PNG File"); - int chunkType = read4Bytes("ChunkType", is, "Not a Valid PNG File"); - - if (debug) - { - printCharQuad("ChunkType", chunkType); - debugNumber("Length", length, 4); - } - boolean keep = keepChunk(chunkType, chunkTypes); - - byte bytes[] = null; - if (keep) - { - bytes = readByteArray("Chunk Data", length, is, - "Not a Valid PNG File: Couldn't read Chunk Data."); - } else - skipBytes(is, length, "Not a Valid PNG File"); - - if (debug) - if (bytes != null) - debugNumber("bytes", bytes.length, 4); - - int CRC = read4Bytes("CRC", is, "Not a Valid PNG File"); - - if (keep) - { - if (chunkType == iCCP) - result.add(new PNGChunkiCCP(length, chunkType, CRC, bytes)); - else if (chunkType == tEXt) - result.add(new PNGChunktEXt(length, chunkType, CRC, bytes)); - else if (chunkType == zTXt) - result.add(new PNGChunkzTXt(length, chunkType, CRC, bytes)); - else if (chunkType == IHDR) - result.add(new PNGChunkIHDR(length, chunkType, CRC, bytes)); - else if (chunkType == PLTE) - result.add(new PNGChunkPLTE(length, chunkType, CRC, bytes)); - else if (chunkType == pHYs) - result.add(new PNGChunkpHYs(length, chunkType, CRC, bytes)); - else if (chunkType == IDAT) - result.add(new PNGChunkIDAT(length, chunkType, CRC, bytes)); - else if (chunkType == gAMA) - result.add(new PNGChunkgAMA(length, chunkType, CRC, bytes)); - else if (chunkType == iTXt) - result.add(new PNGChunkiTXt(length, chunkType, CRC, bytes)); - else - result.add(new PNGChunk(length, chunkType, CRC, bytes)); - - if (returnAfterFirst) - return result; - } - - if (chunkType == IEND) - break; - - } - - return result; - - } - - public void readSignature(InputStream is) throws ImageReadException, - IOException - { - readAndVerifyBytes(is, PNG_Signature, - "Not a Valid PNG Segment: Incorrect Signature"); - - } - - private ArrayList readChunks(ByteSource byteSource, int chunkTypes[], - boolean returnAfterFirst) throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - ArrayList chunks = null; - - readSignature(is); - chunks = readChunks(is, chunkTypes, returnAfterFirst); - return chunks; - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList chunks = readChunks(byteSource, new int[] { iCCP, }, true); - - if ((chunks == null) || (chunks.size() < 1)) - { - // throw new ImageReadException("Png: No chunks"); - return null; - } - - if (chunks.size() > 1) - throw new ImageReadException( - "PNG contains more than one ICC Profile "); - - PNGChunkiCCP pngChunkiCCP = (PNGChunkiCCP) chunks.get(0); - byte bytes[] = pngChunkiCCP.UncompressedProfile; - - return (bytes); - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList chunks = readChunks(byteSource, new int[] { IHDR, }, true); - - if ((chunks == null) || (chunks.size() < 1)) - throw new ImageReadException("Png: No chunks"); - - if (chunks.size() > 1) - throw new ImageReadException("PNG contains more than one Header"); - - PNGChunkIHDR pngChunkIHDR = (PNGChunkIHDR) chunks.get(0); - - return new Dimension(pngChunkIHDR.width, pngChunkIHDR.height); - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList chunks = readChunks(byteSource, new int[] { tEXt, zTXt, }, - true); - - if ((chunks == null) || (chunks.size() < 1)) - return null; - - ImageMetadata result = new ImageMetadata(); - - for (int i = 0; i < chunks.size(); i++) - { - PNGTextChunk chunk = (PNGTextChunk) chunks.get(i); - - result.add(chunk.getKeyword(), chunk.getText()); - } - - return result; - } - - private boolean isGrayscale(int colorType) throws ImageReadException - { - // Color type is a single-byte integer that describes the interpretation - // of the - // image data. Color type codes represent sums of the following values: - // 1 (palette used), 2 (color used), and 4 (alpha channel used). - // Valid values are 0, 2, 3, 4, and 6. - // - // Bit depth restrictions for each color type are imposed to simplify - // implementations - // and to prohibit combinations that do not compress well. Decoders must - // support all - // valid combinations of bit depth and color type. The allowed - // combinations are: - // - // Color Allowed Interpretation - // Type Bit Depths - // - // 0 1,2,4,8,16 Each pixel is a grayscale sample. - // - // 2 8,16 Each pixel is an R,G,B triple. - // - // 3 1,2,4,8 Each pixel is a palette index; - // a PLTE chunk must appear. - // - // 4 8,16 Each pixel is a grayscale sample, - // followed by an alpha sample. - // - // 6 8,16 Each pixel is an R,G,B triple, - // followed by an alpha sample. - switch (colorType) - { - case COLOR_TYPE_GREYSCALE: - return true; - case COLOR_TYPE_TRUE_COLOR: - return false; - case COLOR_TYPE_INDEXED_COLOR: - return false; - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: - return true; - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: - return false; - } - - // return -1; - throw new ImageReadException("PNG: unknown color type: " + colorType); - } - - private int samplesPerPixel(int colorType) throws ImageReadException - { - // Color type is a single-byte integer that describes the interpretation - // of the - // image data. Color type codes represent sums of the following values: - // 1 (palette used), 2 (color used), and 4 (alpha channel used). - // Valid values are 0, 2, 3, 4, and 6. - // - // Bit depth restrictions for each color type are imposed to simplify - // implementations - // and to prohibit combinations that do not compress well. Decoders must - // support all - // valid combinations of bit depth and color type. The allowed - // combinations are: - // - // Color Allowed Interpretation - // Type Bit Depths - // - // 0 1,2,4,8,16 Each pixel is a grayscale sample. - // - // 2 8,16 Each pixel is an R,G,B triple. - // - // 3 1,2,4,8 Each pixel is a palette index; - // a PLTE chunk must appear. - // - // 4 8,16 Each pixel is a grayscale sample, - // followed by an alpha sample. - // - // 6 8,16 Each pixel is an R,G,B triple, - // followed by an alpha sample. - switch (colorType) - { - case COLOR_TYPE_GREYSCALE: - return 1; - case COLOR_TYPE_TRUE_COLOR: - return 3; - case COLOR_TYPE_INDEXED_COLOR: - return 1; // is this accurate ? how may bits per index? - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: - return 2; - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: - return 4; - } - - // return -1; - throw new ImageReadException("PNG: unknown color type: " + colorType); - } - - private ArrayList filterChunks(ArrayList v, int type) - { - ArrayList result = new ArrayList(); - - for (int i = 0; i < v.size(); i++) - { - PNGChunk chunk = (PNGChunk) v.get(i); - if (chunk.chunkType == type) - result.add(chunk); - } - - return result; - } - - private boolean hasAlphaChannel(int ColorType) throws ImageReadException - { - switch (ColorType) - { - case COLOR_TYPE_GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale - // sample. - case COLOR_TYPE_TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple. - case COLOR_TYPE_INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette index; - return false; - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a grayscale - // sample, - // followed by an alpha sample. - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an R,G,B - // triple, - // followed by an alpha sample. - return true; - default: - throw new ImageReadException("PNG: unknown color type: " - + ColorType); - } - } - - private String getColorTypeDescription(int ColorType) - { - switch (ColorType) - { - case COLOR_TYPE_GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale - // sample. - return "grayscale"; - case COLOR_TYPE_TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple. - return "rgb"; - case COLOR_TYPE_INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette index; - return "indexed rgb"; - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a grayscale - // sample, - // followed by an alpha sample. - return "grayscale w/ alpha"; - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an R,G,B - // triple, - // followed by an alpha sample. - return "RGB w/ alpha"; - default: - return "Unknown Color Type"; - } - } - - // TODO: I have been too casual about making inner classes subclass of - // BinaryFileParser - // I may not have always preserved byte order correctly. - - private TransparencyFilter getTransparencyFilter(int ColorType, - PNGChunk pngChunktRNS) throws ImageReadException, IOException - { - // this.printCharQuad("pngChunktRNS.ChunkType", pngChunktRNS.ChunkType); - // this.debugNumber("pngChunktRNS.Length", pngChunktRNS.Length); - - switch (ColorType) - { - case COLOR_TYPE_GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale - // sample. - return new TransparencyFilterGrayscale(pngChunktRNS.bytes); - case COLOR_TYPE_TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple. - return new TransparencyFilterTrueColor(pngChunktRNS.bytes); - case COLOR_TYPE_INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette index; - return new TransparencyFilterIndexedColor(pngChunktRNS.bytes); - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a grayscale - // sample, - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an R,G,B - // triple, - default: - throw new ImageReadException( - "Simple Transparency not compatible with ColorType: " - + ColorType); - } - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList chunks = readChunks(byteSource, new int[] { IHDR, pHYs, tEXt, - zTXt, tRNS, PLTE, iTXt, }, false); - - // if(chunks!=null) - // System.out.println("chunks: " + chunks.size()); - - if ((chunks == null) || (chunks.size() < 1)) - throw new ImageReadException("PNG: no chunks"); - - ArrayList IHDRs = filterChunks(chunks, IHDR); - if (IHDRs.size() != 1) - throw new ImageReadException("PNG contains more than one Header"); - - PNGChunkIHDR pngChunkIHDR = (PNGChunkIHDR) IHDRs.get(0); - PNGChunk pngChunktRNS = null; - - boolean isTransparent = false; - - ArrayList tRNSs = filterChunks(chunks, tRNS); - if (tRNSs.size() > 0) - { - isTransparent = true; - pngChunktRNS = (PNGChunk) IHDRs.get(0); - } else - hasAlphaChannel(pngChunkIHDR.colorType); - - PNGChunkpHYs pngChunkpHYs = null; - - ArrayList pHYss = filterChunks(chunks, pHYs); - if (pHYss.size() > 1) - throw new ImageReadException("PNG contains more than one pHYs: " - + pHYss.size()); - else if (pHYss.size() == 1) - pngChunkpHYs = (PNGChunkpHYs) pHYss.get(0); - - ArrayList tEXts = filterChunks(chunks, tEXt); - ArrayList zTXts = filterChunks(chunks, zTXt); - ArrayList iTXts = filterChunks(chunks, iTXt); - - { - ArrayList comments = new ArrayList(); - List textChunks = new ArrayList(); - - for (int i = 0; i < tEXts.size(); i++) - { - PNGChunktEXt pngChunktEXt = (PNGChunktEXt) tEXts.get(i); - comments.add(pngChunktEXt.keyword + ": " + pngChunktEXt.text); - textChunks.add(pngChunktEXt.getContents()); - } - for (int i = 0; i < zTXts.size(); i++) - { - PNGChunkzTXt pngChunkzTXt = (PNGChunkzTXt) zTXts.get(i); - comments.add(pngChunkzTXt.keyword + ": " + pngChunkzTXt.text); - textChunks.add(pngChunkzTXt.getContents()); - } - for (int i = 0; i < iTXts.size(); i++) - { - PNGChunkiTXt pngChunkiTXt = (PNGChunkiTXt) iTXts.get(i); - comments.add(pngChunkiTXt.keyword + ": " + pngChunkiTXt.text); - textChunks.add(pngChunkiTXt.getContents()); - } - - int BitsPerPixel = pngChunkIHDR.bitDepth - * samplesPerPixel(pngChunkIHDR.colorType); - ImageFormat Format = ImageFormat.IMAGE_FORMAT_PNG; - String FormatName = "PNG Portable Network Graphics"; - int Height = pngChunkIHDR.height; - String MimeType = "image/png"; - int NumberOfImages = 1; - int Width = pngChunkIHDR.width; - boolean isProgressive = (pngChunkIHDR.interlaceMethod != 0); - - int PhysicalHeightDpi = -1; - float PhysicalHeightInch = -1; - int PhysicalWidthDpi = -1; - float PhysicalWidthInch = -1; - - // if (pngChunkpHYs != null) - // { - // System.out.println("\t" + "pngChunkpHYs.UnitSpecifier: " + - // pngChunkpHYs.UnitSpecifier ); - // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitYAxis: " + - // pngChunkpHYs.PixelsPerUnitYAxis ); - // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitXAxis: " + - // pngChunkpHYs.PixelsPerUnitXAxis ); - // } - if ((pngChunkpHYs != null) && (pngChunkpHYs.UnitSpecifier == 1)) // meters - { - double meters_per_inch = 0.0254; - - PhysicalWidthDpi = (int) Math - .round(pngChunkpHYs.PixelsPerUnitXAxis - * meters_per_inch); - PhysicalWidthInch = (float) ((double) Width - * (double) pngChunkpHYs.PixelsPerUnitXAxis * meters_per_inch); - PhysicalHeightDpi = (int) Math - .round(pngChunkpHYs.PixelsPerUnitYAxis - * meters_per_inch); - PhysicalHeightInch = (float) ((double) Height - * (double) pngChunkpHYs.PixelsPerUnitYAxis * meters_per_inch); - } - - String FormatDetails = "Png"; - - boolean usesPalette = false; - - ArrayList PLTEs = filterChunks(chunks, PLTE); - if (PLTEs.size() > 1) - usesPalette = true; - - int ColorType; - switch (pngChunkIHDR.colorType) - { - case COLOR_TYPE_GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale - // sample. - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a - // grayscale sample, - // followed by an alpha sample. - ColorType = ImageInfo.COLOR_TYPE_GRAYSCALE; - break; - case COLOR_TYPE_TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple. - case COLOR_TYPE_INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette - // index; - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an - // R,G,B triple, - // followed by an alpha sample. - ColorType = ImageInfo.COLOR_TYPE_RGB; - break; - default: - throw new ImageReadException("Png: Unknown ColorType: " - + pngChunkIHDR.colorType); - } - - String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PNG_FILTER; - - ImageInfo result = new PngImageInfo(FormatDetails, BitsPerPixel, - comments, Format, FormatName, Height, MimeType, - NumberOfImages, PhysicalHeightDpi, PhysicalHeightInch, - PhysicalWidthDpi, PhysicalWidthInch, Width, isProgressive, - isTransparent, usesPalette, ColorType, - compressionAlgorithm, textChunks); - - return result; - } - } - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - // if (params.size() > 0) { - // Object firstKey = params.keySet().iterator().next(); - // throw new ImageWriteException("Unknown parameter: " + firstKey); - // } - - ArrayList chunks = readChunks(byteSource, new int[] { IHDR, PLTE, IDAT, - tRNS, iCCP, gAMA, sRGB, }, false); - - if ((chunks == null) || (chunks.size() < 1)) - throw new ImageReadException("PNG: no chunks"); - - ArrayList IHDRs = filterChunks(chunks, IHDR); - if (IHDRs.size() != 1) - throw new ImageReadException("PNG contains more than one Header"); - - PNGChunkIHDR pngChunkIHDR = (PNGChunkIHDR) IHDRs.get(0); - - ArrayList PLTEs = filterChunks(chunks, PLTE); - if (PLTEs.size() > 1) - throw new ImageReadException("PNG contains more than one Palette"); - - PNGChunkPLTE pngChunkPLTE = null; - if (PLTEs.size() == 1) - pngChunkPLTE = (PNGChunkPLTE) PLTEs.get(0); - - // ----- - - ArrayList IDATs = filterChunks(chunks, IDAT); - if (IDATs.size() < 1) - throw new ImageReadException("PNG missing image data"); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - for (int i = 0; i < IDATs.size(); i++) - { - PNGChunkIDAT pngChunkIDAT = (PNGChunkIDAT) IDATs.get(i); - byte bytes[] = pngChunkIDAT.bytes; - // System.out.println(i + ": bytes: " + bytes.length); - baos.write(bytes); - } - - byte compressed[] = baos.toByteArray(); - - baos = null; - - TransparencyFilter transparencyFilter = null; - - ArrayList tRNSs = filterChunks(chunks, tRNS); - if (tRNSs.size() > 0) - { - PNGChunk pngChunktRNS = (PNGChunk) tRNSs.get(0); - transparencyFilter = getTransparencyFilter(pngChunkIHDR.colorType, - pngChunktRNS); - } - - ICC_Profile icc_profile = null; - GammaCorrection gammaCorrection = null; - { - ArrayList sRGBs = filterChunks(chunks, sRGB); - ArrayList gAMAs = filterChunks(chunks, gAMA); - ArrayList iCCPs = filterChunks(chunks, iCCP); - if (sRGBs.size() > 1) - throw new ImageReadException("PNG: unexpected sRGB chunk"); - if (gAMAs.size() > 1) - throw new ImageReadException("PNG: unexpected gAMA chunk"); - if (iCCPs.size() > 1) - throw new ImageReadException("PNG: unexpected iCCP chunk"); - - if (sRGBs.size() == 1) - { - // no color management neccesary. - if (debug) - System.out.println("sRGB, no color management neccesary."); - } else if (iCCPs.size() == 1) - { - if (debug) - System.out.println("iCCP."); - - PNGChunkiCCP pngChunkiCCP = (PNGChunkiCCP) iCCPs.get(0); - byte bytes[] = pngChunkiCCP.UncompressedProfile; - - // TODO(maybe): ICC color profile - //icc_profile = ICC_Profile.getInstance(bytes); - icc_profile = null; - } else if (gAMAs.size() == 1) - { - PNGChunkgAMA pngChunkgAMA = (PNGChunkgAMA) gAMAs.get(0); - double gamma = pngChunkgAMA.getGamma(); - - // charles: what is the correct target value here? - // double targetGamma = 2.2; - double targetGamma = 1.0; - double diff = Math.abs(targetGamma - gamma); - if (diff >= 0.5) - gammaCorrection = new GammaCorrection(gamma, targetGamma); - - if (gammaCorrection != null) - if (pngChunkPLTE != null) - pngChunkPLTE.correct(gammaCorrection); - - } - } - - { - int width = pngChunkIHDR.width; - int height = pngChunkIHDR.height; - int colorType = pngChunkIHDR.colorType; - int bitDepth = pngChunkIHDR.bitDepth; - - int bitsPerSample = bitDepth; - - if (pngChunkIHDR.filterMethod != 0) - throw new ImageReadException("PNG: unknown FilterMethod: " - + pngChunkIHDR.filterMethod); - - int samplesPerPixel = samplesPerPixel(pngChunkIHDR.colorType); - boolean isGrayscale = isGrayscale(pngChunkIHDR.colorType); - - int bitsPerPixel = bitsPerSample * samplesPerPixel; - - boolean hasAlpha = colorType == COLOR_TYPE_GREYSCALE_WITH_ALPHA - || colorType == COLOR_TYPE_TRUE_COLOR_WITH_ALPHA; - - BufferedImage result; - if (isGrayscale) - result = getBufferedImageFactory(params) - .getGrayscaleBufferedImage(width, height, hasAlpha); - else - result = getBufferedImageFactory(params).getColorBufferedImage( - width, height, hasAlpha); - - ByteArrayInputStream bais = new ByteArrayInputStream(compressed); - InflaterInputStream iis = new InflaterInputStream(bais); - - ScanExpediter scanExpediter; - - if (pngChunkIHDR.interlaceMethod == 0) - scanExpediter = new ScanExpediterSimple(width, height, iis, - result, colorType, bitDepth, bitsPerPixel, - pngChunkPLTE, gammaCorrection, transparencyFilter); - else if (pngChunkIHDR.interlaceMethod == 1) - scanExpediter = new ScanExpediterInterlaced(width, height, iis, - result, colorType, bitDepth, bitsPerPixel, - pngChunkPLTE, gammaCorrection, transparencyFilter); - else - throw new ImageReadException("Unknown InterlaceMethod: " - + pngChunkIHDR.interlaceMethod); - - scanExpediter.drive(); - - if (icc_profile != null) - { - Boolean is_srgb = new IccProfileParser().issRGB(icc_profile); - if (is_srgb == null || !is_srgb.booleanValue()) - { - ICC_ColorSpace cs = new ICC_ColorSpace(icc_profile); - - ColorModel srgbCM = ColorModel.getRGBdefault(); - ColorSpace cs_sRGB = srgbCM.getColorSpace(); - - result = new ColorTools().convertBetweenColorSpaces(result, - cs, cs_sRGB); - } - } - - return result; - - } - - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - ImageInfo imageInfo = getImageInfo(byteSource); - if (imageInfo == null) - return false; - - imageInfo.toString(pw, ""); - - { - ArrayList chunks = readChunks(byteSource, null, false); - { - ArrayList IHDRs = filterChunks(chunks, IHDR); - if (IHDRs.size() != 1) - { - if (debug) - System.out.println("PNG contains more than one Header"); - return false; - } - PNGChunkIHDR pngChunkIHDR = (PNGChunkIHDR) IHDRs.get(0); - pw.println("Color: " - + getColorTypeDescription(pngChunkIHDR.colorType)); - } - - pw.println("chunks: " + chunks.size()); - - if ((chunks.size() < 1)) - return false; - - for (int i = 0; i < chunks.size(); i++) - { - PNGChunk chunk = (PNGChunk) chunks.get(i); - printCharQuad(pw, "\t" + i + ": ", chunk.chunkType); - } - } - - pw.println(""); - - pw.flush(); - - return true; - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - new PngWriter(params).writeImage(src, os, params); - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - - List chunks = readChunks(byteSource, new int[] { iTXt, }, false); - - if ((chunks == null) || (chunks.size() < 1)) - return null; - - List xmpChunks = new ArrayList(); - for (int i = 0; i < chunks.size(); i++) - { - PNGChunkiTXt chunk = (PNGChunkiTXt) chunks.get(i); - if (!chunk.getKeyword().equals(XMP_KEYWORD)) - continue; - xmpChunks.add(chunk); - } - - if (xmpChunks.size() < 1) - return null; - if (xmpChunks.size() > 1) - throw new ImageReadException( - "PNG contains more than one XMP chunk."); - - PNGChunkiTXt chunk = (PNGChunkiTXt) xmpChunks.get(0); - return chunk.getText(); - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/png/PngWriter.java b/src/main/java/org/apache/sanselan/formats/png/PngWriter.java deleted file mode 100644 index 6ba9887..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/PngWriter.java +++ /dev/null @@ -1,629 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.DeflaterOutputStream; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.ZLibUtils; -import org.apache.sanselan.palette.MedianCutQuantizer; -import org.apache.sanselan.palette.Palette; -import org.apache.sanselan.palette.PaletteFactory; -import org.apache.sanselan.util.Debug; -import org.apache.sanselan.util.ParamMap; -import org.apache.sanselan.util.UnicodeUtils; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PngWriter implements PngConstants -{ - private final boolean verbose; - - public PngWriter(boolean verbose) - { - this.verbose = verbose; - } - - public PngWriter(Map params) - { - this.verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE, - false); - } - - /* - * 1. IHDR: image header, which is the first chunk in a PNG datastream. 2. - * PLTE: palette table associated with indexed PNG images. 3. IDAT: image - * data chunks. 4. IEND: image trailer, which is the last chunk in a PNG - * datastream. - * - * The remaining 14 chunk types are termed ancillary chunk types, which - * encoders may generate and decoders may interpret. - * - * 1. Transparency information: tRNS (see 11.3.2: Transparency information). - * 2. Colour space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: - * Colour space information). 3. Textual information: iTXt, tEXt, zTXt (see - * 11.3.4: Textual information). 4. Miscellaneous information: bKGD, hIST, - * pHYs, sPLT (see 11.3.5: Miscellaneous information). 5. Time information: - * tIME (see 11.3.6: Time stamp information). - */ - - private final void writeInt(OutputStream os, int value) throws IOException - { - os.write(0xff & (value >> 24)); - os.write(0xff & (value >> 16)); - os.write(0xff & (value >> 8)); - os.write(0xff & (value >> 0)); - } - - private final void writeChunk(OutputStream os, byte chunkType[], - byte data[]) throws IOException - { - int dataLength = data == null ? 0 : data.length; - writeInt(os, dataLength); - os.write(chunkType); - if (data != null) - os.write(data); - - // Debug.debug("writeChunk chunkType", chunkType); - // Debug.debug("writeChunk data", data); - - { - PngCrc png_crc = new PngCrc(); - - long crc1 = png_crc.start_partial_crc(chunkType, chunkType.length); - long crc2 = data == null ? crc1 : png_crc.continue_partial_crc( - crc1, data, data.length); - int crc = (int) png_crc.finish_partial_crc(crc2); - - // Debug.debug("crc1", crc1 + " (" + Long.toHexString(crc1) - // + ")"); - // Debug.debug("crc2", crc2 + " (" + Long.toHexString(crc2) - // + ")"); - // Debug.debug("crc3", crc + " (" + Integer.toHexString(crc) - // + ")"); - - writeInt(os, crc); - } - } - - private static class ImageHeader - { - public final int width; - public final int height; - public final byte bit_depth; - public final byte colorType; - public final byte compressionMethod; - public final byte filterMethod; - public final byte interlaceMethod; - - public ImageHeader(int width, int height, byte bit_depth, - byte colorType, byte compressionMethod, byte filterMethod, - byte interlaceMethod) - { - this.width = width; - this.height = height; - this.bit_depth = bit_depth; - this.colorType = colorType; - this.compressionMethod = compressionMethod; - this.filterMethod = filterMethod; - this.interlaceMethod = interlaceMethod; - } - - } - - private void writeChunkIHDR(OutputStream os, ImageHeader value) - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeInt(baos, value.width); - writeInt(baos, value.height); - baos.write(0xff & value.bit_depth); - baos.write(0xff & value.colorType); - baos.write(0xff & value.compressionMethod); - baos.write(0xff & value.filterMethod); - baos.write(0xff & value.interlaceMethod); - - // Debug.debug("baos", baos.toByteArray()); - - writeChunk(os, IHDR_CHUNK_TYPE, baos.toByteArray()); - } - - private void writeChunkiTXt(OutputStream os, PngText.iTXt text) - throws IOException, ImageWriteException - { - if (!UnicodeUtils.isValidISO_8859_1(text.keyword)) - throw new ImageWriteException( - "Png tEXt chunk keyword is not ISO-8859-1: " + text.keyword); - if (!UnicodeUtils.isValidISO_8859_1(text.languageTag)) - throw new ImageWriteException( - "Png tEXt chunk language tag is not ISO-8859-1: " - + text.languageTag); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // keyword - baos.write(text.keyword.getBytes("ISO-8859-1")); - baos.write(0); - - baos.write(1); // compressed flag, true - baos.write(COMPRESSION_DEFLATE_INFLATE); // compression method - - // language tag - baos.write(text.languageTag.getBytes("ISO-8859-1")); - baos.write(0); - - // translated keyword - baos.write(text.translatedKeyword.getBytes("utf-8")); - baos.write(0); - - baos.write(new ZLibUtils().deflate(text.text.getBytes("utf-8"))); - - writeChunk(os, iTXt_CHUNK_TYPE, baos.toByteArray()); - } - - private void writeChunkzTXt(OutputStream os, PngText.zTXt text) - throws IOException, ImageWriteException - { - if (!UnicodeUtils.isValidISO_8859_1(text.keyword)) - throw new ImageWriteException( - "Png zTXt chunk keyword is not ISO-8859-1: " + text.keyword); - if (!UnicodeUtils.isValidISO_8859_1(text.text)) - throw new ImageWriteException( - "Png zTXt chunk text is not ISO-8859-1: " + text.text); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // keyword - baos.write(text.keyword.getBytes("ISO-8859-1")); - baos.write(0); - - // compression method - baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); - - // text - baos - .write(new ZLibUtils().deflate(text.text - .getBytes("ISO-8859-1"))); - - writeChunk(os, zTXt_CHUNK_TYPE, baos.toByteArray()); - } - - private void writeChunktEXt(OutputStream os, PngText.tEXt text) - throws IOException, ImageWriteException - { - if (!UnicodeUtils.isValidISO_8859_1(text.keyword)) - throw new ImageWriteException( - "Png tEXt chunk keyword is not ISO-8859-1: " + text.keyword); - if (!UnicodeUtils.isValidISO_8859_1(text.text)) - throw new ImageWriteException( - "Png tEXt chunk text is not ISO-8859-1: " + text.text); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // keyword - baos.write(text.keyword.getBytes("ISO-8859-1")); - baos.write(0); - - // text - baos.write(text.text.getBytes("ISO-8859-1")); - - writeChunk(os, tEXt_CHUNK_TYPE, baos.toByteArray()); - } - - private void writeChunkXmpiTXt(OutputStream os, String xmpXml) - throws IOException - { - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // keyword - baos.write(XMP_KEYWORD.getBytes("ISO-8859-1")); - baos.write(0); - - baos.write(1); // compressed flag, true - baos.write(COMPRESSION_DEFLATE_INFLATE); // compression method - - baos.write(0); // language tag (ignore). TODO - - // translated keyword - baos.write(XMP_KEYWORD.getBytes("utf-8")); - baos.write(0); - - baos.write(new ZLibUtils().deflate(xmpXml.getBytes("utf-8"))); - - writeChunk(os, iTXt_CHUNK_TYPE, baos.toByteArray()); - } - - private void writeChunkPLTE(OutputStream os, Palette palette) - throws IOException - { - int length = palette.length(); - byte bytes[] = new byte[length * 3]; - - // Debug.debug("length", length); - for (int i = 0; i < length; i++) - { - int rgb = palette.getEntry(i); - int index = i * 3; - // Debug.debug("index", index); - bytes[index + 0] = (byte) (0xff & (rgb >> 16)); - bytes[index + 1] = (byte) (0xff & (rgb >> 8)); - bytes[index + 2] = (byte) (0xff & (rgb >> 0)); - } - - writeChunk(os, PLTE_CHUNK_TYPE, bytes); - } - - private void writeChunkIEND(OutputStream os) throws IOException - { - writeChunk(os, IEND_CHUNK_TYPE, null); - } - - private void writeChunkIDAT(OutputStream os, byte bytes[]) - throws IOException - { - writeChunk(os, IDAT_CHUNK_TYPE, bytes); - } - - private byte getColourType(boolean hasAlpha, boolean isGrayscale) - { - byte result; - - boolean index = false; // charles - - if (index) - result = COLOR_TYPE_INDEXED_COLOR; - else if (isGrayscale) - { - if (hasAlpha) - result = COLOR_TYPE_GREYSCALE_WITH_ALPHA; - else - result = COLOR_TYPE_GREYSCALE; - } else if (hasAlpha) - result = COLOR_TYPE_TRUE_COLOR_WITH_ALPHA; - else - result = COLOR_TYPE_TRUE_COLOR; - - return result; - } - - private byte getBitDepth(final byte colorType, Map params) - { - byte result = 8; - - Object o = params.get(PARAM_KEY_PNG_BIT_DEPTH); - if (o != null && o instanceof Number) - { - int value = ((Number) o).intValue(); - switch (value) - { - case 1: - case 2: - case 4: - case 8: - case 16: - result = (byte) value; - break; - default: - } - switch (colorType) - { - case COLOR_TYPE_GREYSCALE: - break; - case COLOR_TYPE_INDEXED_COLOR: - result = (byte) Math.min(8, result); - break; - case COLOR_TYPE_GREYSCALE_WITH_ALPHA: - case COLOR_TYPE_TRUE_COLOR: - case COLOR_TYPE_TRUE_COLOR_WITH_ALPHA: - result = (byte) Math.max(8, result); - break; - default: - } - } - - return result; - } - - /* - * between two chunk types indicates alternatives. Table 5.3 Chunk - * ordering rules Critical chunks (shall appear in this order, except PLTE - * is optional) Chunk name Multiple allowed Ordering constraints IHDR No - * Shall be first PLTE No Before first IDAT IDAT Yes Multiple IDAT chunks - * shall be consecutive IEND No Shall be last Ancillary chunks (need not - * appear in this order) Chunk name Multiple allowed Ordering constraints - * cHRM No Before PLTE and IDAT gAMA No Before PLTE and IDAT iCCP No Before - * PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be - * present. sBIT No Before PLTE and IDAT sRGB No Before PLTE and IDAT. If - * the sRGB chunk is present, the iCCP chunk should not be present. bKGD No - * After PLTE; before IDAT hIST No After PLTE; before IDAT tRNS No After - * PLTE; before IDAT pHYs No Before IDAT sPLT Yes Before IDAT tIME No None - * iTXt Yes None tEXt Yes None zTXt Yes None - */ - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // make copy of params; we'll clear keys as we consume them. - params = new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - // clear verbose key. - if (params.containsKey(PARAM_KEY_VERBOSE)) - params.remove(PARAM_KEY_VERBOSE); - - Map rawParams = new HashMap(params); - if (params.containsKey(PARAM_KEY_PNG_FORCE_TRUE_COLOR)) - params.remove(PARAM_KEY_PNG_FORCE_TRUE_COLOR); - if (params.containsKey(PARAM_KEY_PNG_FORCE_INDEXED_COLOR)) - params.remove(PARAM_KEY_PNG_FORCE_INDEXED_COLOR); - if (params.containsKey(PARAM_KEY_PNG_BIT_DEPTH)) - params.remove(PARAM_KEY_PNG_BIT_DEPTH); - if (params.containsKey(PARAM_KEY_XMP_XML)) - params.remove(PARAM_KEY_XMP_XML); - if (params.containsKey(PARAM_KEY_PNG_TEXT_CHUNKS)) - params.remove(PARAM_KEY_PNG_TEXT_CHUNKS); - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - params = rawParams; - - int width = src.getWidth(); - int height = src.getHeight(); - - boolean hasAlpha = new PaletteFactory().hasTransparency(src); - if (verbose) - Debug.debug("hasAlpha", hasAlpha); - // int transparency = new PaletteFactory().getTransparency(src); - - boolean isGrayscale = new PaletteFactory().isGrayscale(src); - if (verbose) - Debug.debug("isGrayscale", isGrayscale); - - byte colorType; - { - boolean forceIndexedColor = ParamMap.getParamBoolean(params, - PARAM_KEY_PNG_FORCE_INDEXED_COLOR, false); - boolean forceTrueColor = ParamMap.getParamBoolean(params, - PARAM_KEY_PNG_FORCE_TRUE_COLOR, false); - - if (forceIndexedColor && forceTrueColor) - throw new ImageWriteException( - "Params: Cannot force both indexed and true color modes"); - else if (forceIndexedColor) - { - colorType = COLOR_TYPE_INDEXED_COLOR; - } else if (forceTrueColor) - { - colorType = (byte) (hasAlpha ? COLOR_TYPE_TRUE_COLOR_WITH_ALPHA - : COLOR_TYPE_TRUE_COLOR); - isGrayscale = false; - } else - colorType = getColourType(hasAlpha, isGrayscale); - if (verbose) - Debug.debug("colorType", colorType); - } - - byte bitDepth = getBitDepth(colorType, params); - if (verbose) - Debug.debug("bit_depth", bitDepth); - - int sampleDepth; - if (colorType == COLOR_TYPE_INDEXED_COLOR) - sampleDepth = 8; - else - sampleDepth = bitDepth; - if (verbose) - Debug.debug("sample_depth", sampleDepth); - - { - os.write(PNG_Signature); - } - { - // IHDR must be first - - byte compressionMethod = COMPRESSION_TYPE_INFLATE_DEFLATE; - byte filterMethod = FILTER_METHOD_ADAPTIVE; - byte interlaceMethod = INTERLACE_METHOD_NONE; - - ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, - colorType, compressionMethod, filterMethod, interlaceMethod); - - writeChunkIHDR(os, imageHeader); - } - - { - // sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the - // iCCP chunk should not be present. - - // charles - } - - Palette palette = null; - if (colorType == COLOR_TYPE_INDEXED_COLOR) - { - // PLTE No Before first IDAT - - int max_colors = hasAlpha ? 255 : 256; - - palette = new MedianCutQuantizer(true).process(src, max_colors, - verbose); - // Palette palette2 = new PaletteFactory().makePaletteSimple(src, - // max_colors); - - // palette.dump(); - - writeChunkPLTE(os, palette); - } - - if (params.containsKey(PARAM_KEY_XMP_XML)) - { - String xmpXml = (String) params.get(PARAM_KEY_XMP_XML); - writeChunkXmpiTXt(os, xmpXml); - } - - if (params.containsKey(PARAM_KEY_PNG_TEXT_CHUNKS)) - { - List outputTexts = (List) params.get(PARAM_KEY_PNG_TEXT_CHUNKS); - for (int i = 0; i < outputTexts.size(); i++) - { - PngText text = (PngText) outputTexts.get(i); - if (text instanceof PngText.tEXt) - writeChunktEXt(os, (PngText.tEXt) text); - else if (text instanceof PngText.zTXt) - writeChunkzTXt(os, (PngText.zTXt) text); - else if (text instanceof PngText.iTXt) - writeChunkiTXt(os, (PngText.iTXt) text); - else - throw new ImageWriteException( - "Unknown text to embed in PNG: " + text); - } - } - - { - // Debug.debug("writing IDAT"); - - // IDAT Yes Multiple IDAT chunks shall be consecutive - - byte uncompressed[]; - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - boolean useAlpha = colorType == COLOR_TYPE_GREYSCALE_WITH_ALPHA - || colorType == COLOR_TYPE_TRUE_COLOR_WITH_ALPHA; - - int row[] = new int[width]; - for (int y = 0; y < height; y++) - { - // Debug.debug("y", y + "/" + height); - src.getRGB(0, y, width, 1, row, 0, width); - - byte filter_type = FILTER_TYPE_NONE; - baos.write(filter_type); - for (int x = 0; x < width; x++) - { - int argb = row[x]; - - if (palette != null) - { - int index = palette.getPaletteIndex(argb); - baos.write(0xff & index); - } else - { - int alpha = 0xff & (argb >> 24); - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - - if (isGrayscale) - { - int gray = (red + green + blue) / 3; - // if(y==0) - // { - // Debug.debug("gray: " + x + ", " + y + - // " argb: 0x" - // + Integer.toHexString(argb) + " gray: 0x" - // + Integer.toHexString(gray)); - // // Debug.debug(x + ", " + y + " gray", gray); - // // Debug.debug(x + ", " + y + " gray", gray); - // Debug.debug(x + ", " + y + " gray", gray + - // " " + Integer.toHexString(gray)); - // Debug.debug(); - // } - baos.write(gray); - } else - { - baos.write(red); - baos.write(green); - baos.write(blue); - } - if (useAlpha) - baos.write(alpha); - } - } - } - uncompressed = baos.toByteArray(); - } - - // Debug.debug("uncompressed", uncompressed.length); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DeflaterOutputStream dos = new DeflaterOutputStream(baos); - int chunk_size = 256 * 1024; - for (int index = 0; index < uncompressed.length; index += chunk_size) - { - int end = Math.min(uncompressed.length, index + chunk_size); - int length = end - index; - - dos.write(uncompressed, index, length); - dos.flush(); - baos.flush(); - - byte compressed[] = baos.toByteArray(); - baos.reset(); - if (compressed.length > 0) - { - // Debug.debug("compressed", compressed.length); - writeChunkIDAT(os, compressed); - } - - } - { - dos.finish(); - byte compressed[] = baos.toByteArray(); - if (compressed.length > 0) - { - // Debug.debug("compressed final", compressed.length); - writeChunkIDAT(os, compressed); - } - } - } - - { - // IEND No Shall be last - - writeChunkIEND(os); - } - - /* - * Ancillary chunks (need not appear in this order) Chunk name Multiple - * allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No - * Before PLTE and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk - * is present, the sRGB chunk should not be present. sBIT No Before PLTE - * and IDAT sRGB No Before PLTE and IDAT. If the sRGB chunk is present, - * the iCCP chunk should not be present. bKGD No After PLTE; before IDAT - * hIST No After PLTE; before IDAT tRNS No After PLTE; before IDAT pHYs - * No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt - * Yes None zTXt Yes None - */ - - os.close(); - } // todo: filter types - // proper colour types - // srgb, etc. -} diff --git a/src/main/java/org/apache/sanselan/formats/png/ScanExpediter.java b/src/main/java/org/apache/sanselan/formats/png/ScanExpediter.java deleted file mode 100644 index 97fa3f3..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/ScanExpediter.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.formats.png.chunks.PNGChunkPLTE; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilter; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilterAverage; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilterNone; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilterPaeth; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilterSub; -import org.apache.sanselan.formats.png.scanlinefilters.ScanlineFilterUp; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class ScanExpediter extends BinaryFileParser -{ - protected final int width; - protected final int height; - protected final InputStream is; - protected final BufferedImage bi; - protected final int colorType; - protected final int bitDepth; - protected final int bytesPerPixel; - protected final int bitsPerPixel; - protected final PNGChunkPLTE pngChunkPLTE; - protected final GammaCorrection gammaCorrection; - protected final TransparencyFilter transparencyFilter; - - public ScanExpediter(int width, int height, InputStream is, - BufferedImage bi, int color_type, int bitDepth, int bitsPerPixel, - PNGChunkPLTE pngChunkPLTE, GammaCorrection gammaCorrection, - TransparencyFilter transparencyFilter) - - { - this.width = width; - this.height = height; - this.is = is; - this.bi = bi; - this.colorType = color_type; - this.bitDepth = bitDepth; - this.bytesPerPixel = this.getBitsToBytesRoundingUp(bitsPerPixel); - this.bitsPerPixel = bitsPerPixel; - this.pngChunkPLTE = pngChunkPLTE; - this.gammaCorrection = gammaCorrection; - this.transparencyFilter = transparencyFilter; - } - - protected int getBitsToBytesRoundingUp(int bits) - { - int bytes = bits / 8; - if ((bits % 8 > 0)) - bytes++; - return bytes; - } - - protected final int getPixelARGB(int alpha, int red, int green, int blue) - { - int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) - | ((0xff & green) << 8) | ((0xff & blue) << 0); - - return rgb; - } - - protected final int getPixelRGB(int red, int green, int blue) - { - return getPixelARGB(0xff, red, green, blue); - } - - public abstract void drive() throws ImageReadException, IOException; - - protected int getRGB(BitParser bitParser, int pixelIndexInScanline) - throws ImageReadException, IOException - { - - switch (colorType) - { - case 0: // 1,2,4,8,16 Each pixel is a grayscale sample. - { - int sample = bitParser.getSampleAsByte(pixelIndexInScanline, 0); - - if (gammaCorrection != null) - { - sample = gammaCorrection.correctSample(sample); - } - - int rgb = getPixelRGB(sample, sample, sample); - - if (transparencyFilter != null) - rgb = transparencyFilter.filter(rgb, sample); - - return rgb; - - } - case 2: // 8,16 Each pixel is an R,G,B triple. - { - int red = bitParser.getSampleAsByte(pixelIndexInScanline, 0); - int green = bitParser.getSampleAsByte(pixelIndexInScanline, 1); - int blue = bitParser.getSampleAsByte(pixelIndexInScanline, 2); - - int rgb = getPixelRGB(red, green, blue); - - if (transparencyFilter != null) - rgb = transparencyFilter.filter(rgb, -1); - - if (gammaCorrection != null) - { - int alpha = (0xff000000 & rgb) >> 24; // make sure to preserve - // transparency - red = gammaCorrection.correctSample(red); - green = gammaCorrection.correctSample(green); - blue = gammaCorrection.correctSample(blue); - rgb = getPixelARGB(alpha, red, green, blue); - } - - return rgb; - } - // - case 3: // 1,2,4,8 Each pixel is a palette index; - // a PLTE chunk must appear. - { - int index = bitParser.getSample(pixelIndexInScanline, 0); - - int rgb = pngChunkPLTE.getRGB(index); - - if (transparencyFilter != null) - rgb = transparencyFilter.filter(rgb, index); - - return rgb; - } - case 4: // 8,16 Each pixel is a grayscale sample, - // followed by an alpha sample. - { - int sample = bitParser.getSampleAsByte(pixelIndexInScanline, 0); - int alpha = bitParser.getSampleAsByte(pixelIndexInScanline, 1); - - if (gammaCorrection != null) - sample = gammaCorrection.correctSample(sample); - - int rgb = getPixelARGB(alpha, sample, sample, sample); - return rgb; - - } - case 6: // 8,16 Each pixel is an R,G,B triple, - { - int red = bitParser.getSampleAsByte(pixelIndexInScanline, 0); - int green = bitParser.getSampleAsByte(pixelIndexInScanline, 1); - int blue = bitParser.getSampleAsByte(pixelIndexInScanline, 2); - int alpha = bitParser.getSampleAsByte(pixelIndexInScanline, 3); - - if (gammaCorrection != null) - { - red = gammaCorrection.correctSample(red); - green = gammaCorrection.correctSample(green); - blue = gammaCorrection.correctSample(blue); - } - - int rgb = getPixelARGB(alpha, red, green, blue); - return rgb; - } - default: - throw new ImageReadException("PNG: unknown color type: " - + colorType); - } - } - - protected ScanlineFilter getScanlineFilter(int filter_type, - int BytesPerPixel) throws ImageReadException, IOException - { - ScanlineFilter filter; - - switch (filter_type) - { - case 0: // None - filter = new ScanlineFilterNone(); - break; - - case 1: // Sub - filter = new ScanlineFilterSub(BytesPerPixel); - break; - - case 2: // Up - filter = new ScanlineFilterUp(BytesPerPixel); - break; - - case 3: // Average - filter = new ScanlineFilterAverage(BytesPerPixel); - break; - - case 4: // Paeth - filter = new ScanlineFilterPaeth(BytesPerPixel); - break; - - default: - throw new ImageReadException("PNG: unknown filter_type: " - + filter_type); - - } - - return filter; - } - - protected byte[] unfilterScanline(int filter_type, byte src[], byte prev[], - int BytesPerPixel) throws ImageReadException, IOException - { - ScanlineFilter filter = getScanlineFilter(filter_type, BytesPerPixel); - - byte dst[] = new byte[src.length]; - filter.unfilter(src, dst, prev); - return dst; - } - - protected byte[] getNextScanline(InputStream is, int length, byte prev[], - int BytesPerPixel) throws ImageReadException, IOException - { - int filterType = is.read(); - if (filterType < 0) - throw new ImageReadException("PNG: missing filter type"); - - byte scanline[] = this.readByteArray("scanline", length, is, - "PNG: missing image data"); - - byte unfiltered[] = unfilterScanline(filterType, scanline, prev, - BytesPerPixel); - - return unfiltered; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/ScanExpediterInterlaced.java b/src/main/java/org/apache/sanselan/formats/png/ScanExpediterInterlaced.java deleted file mode 100644 index 980b8c5..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/ScanExpediterInterlaced.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.png.chunks.PNGChunkPLTE; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class ScanExpediterInterlaced extends ScanExpediter -{ - public ScanExpediterInterlaced(int width, int height, InputStream is, - BufferedImage bi, int color_type, int BitDepth, int bits_per_pixel, - PNGChunkPLTE fPNGChunkPLTE, GammaCorrection fGammaCorrection, - TransparencyFilter fTransparencyFilter) - - { - super(width, height, is, bi, color_type, BitDepth, bits_per_pixel, - fPNGChunkPLTE, fGammaCorrection, fTransparencyFilter); - } - - private void visit(int x, int y, BufferedImage bi, BitParser fBitParser, - int color_type, int pixel_index_in_scanline, - PNGChunkPLTE fPNGChunkPLTE, GammaCorrection fGammaCorrection) - throws ImageReadException, IOException - { - int rgb = getRGB(fBitParser, - // color_type, - pixel_index_in_scanline - // , - // fPNGChunkPLTE, fGammaCorrection - ); - - bi.setRGB(x, y, rgb); - - // buffer.setElem(y * width +x , rgb); - - } - - public static final int Starting_Row[] = { - 0, 0, 4, 0, 2, 0, 1 - }; - public static final int Starting_Col[] = { - 0, 4, 0, 2, 0, 1, 0 - }; - public static final int Row_Increment[] = { - 8, 8, 8, 4, 4, 2, 2 - }; - public static final int Col_Increment[] = { - 8, 8, 4, 4, 2, 2, 1 - }; - public static final int Block_Height[] = { - 8, 8, 4, 4, 2, 2, 1 - }; - public static final int Block_Width[] = { - 8, 4, 4, 2, 2, 1, 1 - }; - - public void drive() throws ImageReadException, IOException - { - - int pass = 1; - while (pass <= 7) - { - byte prev[] = null; - - int y = Starting_Row[pass - 1]; - // int y_stride = Row_Increment[pass - 1]; - boolean rows_in_pass = (y < height); - while (y < height) - { - int x = Starting_Col[pass - 1]; - int pixel_index_in_scanline = 0; - - if (x < width) - { - // only get data if there are pixels in this scanline/pass - int ColumnsInRow = 1 + ((width - Starting_Col[pass - 1] - 1) / Col_Increment[pass - 1]); - int bitsPerScanLine = bitsPerPixel * ColumnsInRow; - int pixel_bytes_per_scan_line = getBitsToBytesRoundingUp(bitsPerScanLine); - - byte unfiltered[] = getNextScanline(is, - pixel_bytes_per_scan_line, prev, bytesPerPixel); - - prev = unfiltered; - - BitParser fBitParser = new BitParser(unfiltered, - bitsPerPixel, bitDepth); - - while (x < width) - { - visit(x, y, bi, fBitParser, colorType, - pixel_index_in_scanline, pngChunkPLTE, - gammaCorrection); - - x = x + Col_Increment[pass - 1]; - pixel_index_in_scanline++; - } - } - y = y + Row_Increment[pass - 1]; - } - pass = pass + 1; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/ScanExpediterSimple.java b/src/main/java/org/apache/sanselan/formats/png/ScanExpediterSimple.java deleted file mode 100644 index a3d0a6a..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/ScanExpediterSimple.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.png.chunks.PNGChunkPLTE; -import org.apache.sanselan.formats.transparencyfilters.TransparencyFilter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class ScanExpediterSimple extends ScanExpediter -{ - public ScanExpediterSimple(int width, int height, InputStream is, - BufferedImage bi, int color_type, int BitDepth, int bitsPerPixel, - PNGChunkPLTE pngChunkPLTE, GammaCorrection gammaCorrection, - TransparencyFilter transparencyFilter) - - { - super(width, height, is, bi, color_type, BitDepth, bitsPerPixel, - pngChunkPLTE, gammaCorrection, transparencyFilter); - } - - public void drive() throws ImageReadException, IOException - { - int bitsPerScanLine = bitsPerPixel * width; - int pixelBytesPerScanLine = getBitsToBytesRoundingUp(bitsPerScanLine); - byte prev[] = null; - - for (int y = 0; y < height; y++) - { - byte unfiltered[] = getNextScanline(is, pixelBytesPerScanLine, - prev, bytesPerPixel); - - prev = unfiltered; - - BitParser bitParser = new BitParser(unfiltered, bitsPerPixel, - bitDepth); - - for (int x = 0; x < width; x++) - { - int rgb = getRGB(bitParser, x); - - bi.setRGB(x, y, rgb); - } - } - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIHDR.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIHDR.java deleted file mode 100644 index 8c4af7e..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkIHDR.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class PNGChunkIHDR extends PNGChunk -{ - public final int width; - public final int height; - public final int bitDepth; - public final int colorType; - public final int compressionMethod; - public final int filterMethod; - public final int interlaceMethod; - - public PNGChunkIHDR(int Length, int ChunkType, int CRC, byte bytes[]) - throws ImageReadException, IOException - { - super(Length, ChunkType, CRC, bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - width = read4Bytes("Width", is, "Not a Valid Png File: IHDR Corrupt"); - height = read4Bytes("Height", is, "Not a Valid Png File: IHDR Corrupt"); - bitDepth = readByte("BitDepth", is, - "Not a Valid Png File: IHDR Corrupt"); - colorType = readByte("ColorType", is, - "Not a Valid Png File: IHDR Corrupt"); - compressionMethod = readByte("CompressionMethod", is, - "Not a Valid Png File: IHDR Corrupt"); - filterMethod = readByte("FilterMethod", is, - "Not a Valid Png File: IHDR Corrupt"); - interlaceMethod = readByte("InterlaceMethod", is, - "Not a Valid Png File: IHDR Corrupt"); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiCCP.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiCCP.java deleted file mode 100644 index 8bc22ac..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiCCP.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.ZLibUtils; - -public class PNGChunkiCCP extends PNGChunk -{ - // private final PngImageParser parser; - public final String ProfileName; - public final int CompressionMethod; - public final byte CompressedProfile[]; - public final byte UncompressedProfile[]; - - public PNGChunkiCCP( - // PngImageParser parser, - int Length, int ChunkType, int CRC, byte bytes[]) - throws ImageReadException, IOException - { - super(Length, ChunkType, CRC, bytes); - // this.parser = parser; - - { - int index = findNull(bytes); - if (index < 0) - throw new ImageReadException("PNGChunkiCCP: No Profile Name"); - byte name_bytes[] = new byte[index]; - System.arraycopy(bytes, 0, name_bytes, 0, index); - ProfileName = new String(name_bytes); - - CompressionMethod = bytes[index + 1]; - - int CompressedProfileLength = bytes.length - (index + 1 + 1); - CompressedProfile = new byte[CompressedProfileLength]; - System.arraycopy(bytes, index + 1 + 1, CompressedProfile, 0, - CompressedProfileLength); - - if (getDebug()) - { - System.out.println("ProfileName: " + ProfileName); - System.out.println("ProfileName.length(): " - + ProfileName.length()); - System.out.println("CompressionMethod: " + CompressionMethod); - System.out.println("CompressedProfileLength: " - + CompressedProfileLength); - System.out.println("bytes.length: " + bytes.length); - } - - UncompressedProfile = new ZLibUtils() - .inflate(CompressedProfile); - - if (getDebug()) - { - System.out.println("UncompressedProfile: " - + ((UncompressedProfile == null) ? "null" : "" - + bytes.length)); - } - - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiTXt.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiTXt.java deleted file mode 100644 index 08220f2..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkiTXt.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.ZLibUtils; -import org.apache.sanselan.formats.png.PngConstants; -import org.apache.sanselan.formats.png.PngText; - -public class PNGChunkiTXt extends PNGTextChunk -{ - public final String keyword, text; - - /* - * The language tag defined in [RFC-3066] indicates the human language used - * by the translated keyword and the text. Unlike the keyword, the language - * tag is case-insensitive. It is an ISO 646.IRV:1991 [ISO 646] string - * consisting of hyphen-separated words of 1-8 alphanumeric characters each - * (for example cn, en-uk, no-bok, x-klingon, x-KlInGoN). If the first word - * is two or three letters long, it is an ISO language code [ISO-639]. If - * the language tag is empty, the language is unspecified. - */ - public final String languageTag; - - public final String translatedKeyword; - - public PNGChunkiTXt(int length, int chunkType, int crc, byte bytes[]) - throws ImageReadException, IOException - { - super(length, chunkType, crc, bytes); - { - int terminator = findNull(bytes); - if (terminator < 0) - throw new ImageReadException( - "PNG iTXt chunk keyword is not terminated."); - - keyword = new String(bytes, 0, terminator, "ISO-8859-1"); - int index = terminator + 1; - - int compressionFlag = bytes[index++]; - if (compressionFlag != 0 && compressionFlag != 1) - throw new ImageReadException( - "PNG iTXt chunk has invalid compression flag: " - + compressionFlag); - - boolean compressed = compressionFlag == 1; - - int compressionMethod = bytes[index++]; - if (compressed) - if (compressionMethod != PngConstants.COMPRESSION_DEFLATE_INFLATE) - throw new ImageReadException( - "PNG iTXt chunk has unexpected compression method: " - + compressionMethod); - else if (compressionMethod != 0) - throw new ImageReadException( - "PNG iTXt chunk has unexpected compression method: " - + compressionMethod); - - terminator = findNull(bytes, index); - if (terminator < 0) - throw new ImageReadException( - "PNG iTXt chunk language tag is not terminated."); - - languageTag = new String(bytes, index, terminator - index, - "ISO-8859-1"); - index = terminator + 1; - - terminator = findNull(bytes, index); - if (terminator < 0) - throw new ImageReadException( - "PNG iTXt chunk translated keyword is not terminated."); - - translatedKeyword = new String(bytes, index, terminator - index, - "utf-8"); - index = terminator + 1; - - if (compressed) - { - int compressedTextLength = bytes.length - index; - - byte compressedText[] = new byte[compressedTextLength]; - System.arraycopy(bytes, index, compressedText, 0, - compressedTextLength); - - text = new String(new ZLibUtils().inflate(compressedText), - "utf-8"); - - } else - text = new String(bytes, index, bytes.length - index, "utf-8"); - } - } - - /** - * @return Returns the keyword. - */ - public String getKeyword() - { - return keyword; - } - - /** - * @return Returns the text. - */ - public String getText() - { - return text; - } - - public PngText getContents() - { - return new PngText.iTXt(keyword, text, languageTag, translatedKeyword); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkpHYs.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkpHYs.java deleted file mode 100644 index 7c461a6..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkpHYs.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class PNGChunkpHYs extends PNGChunk -{ - public final int PixelsPerUnitXAxis; - public final int PixelsPerUnitYAxis; - public final int UnitSpecifier; - - public PNGChunkpHYs(int Length, int ChunkType, int CRC, byte bytes[]) - throws ImageReadException, IOException - { - super(Length, ChunkType, CRC, bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - - PixelsPerUnitXAxis = read4Bytes("PixelsPerUnitXAxis", is, - "Not a Valid Png File: pHYs Corrupt"); - PixelsPerUnitYAxis = read4Bytes("PixelsPerUnitYAxis", is, - "Not a Valid Png File: pHYs Corrupt"); - UnitSpecifier = readByte("Unit specifier", is, - "Not a Valid Png File: pHYs Corrupt"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunktEXt.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunktEXt.java deleted file mode 100644 index 9ce48e2..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunktEXt.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.png.PngText; - -public class PNGChunktEXt extends PNGTextChunk -{ - public final String keyword, text; - - public PNGChunktEXt(int length, int chunkType, int crc, byte bytes[]) - throws ImageReadException, IOException - { - super(length, chunkType, crc, bytes); - { - int index = findNull(bytes); - if (index < 0) - throw new ImageReadException( - "PNG tEXt chunk keyword is not terminated."); - - keyword = new String(bytes, 0, index, "ISO-8859-1"); - - int textLength = bytes.length - (index + 1); - text = new String(bytes, index + 1, textLength, "ISO-8859-1"); - - if (getDebug()) - { - System.out.println("Keyword: " + keyword); - System.out.println("Text: " + text); - } - - } - } - - /** - * @return Returns the keyword. - */ - public String getKeyword() - { - return keyword; - } - - /** - * @return Returns the text. - */ - public String getText() - { - return text; - } - - public PngText getContents() - { - return new PngText.tEXt(keyword, text); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkzTXt.java b/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkzTXt.java deleted file mode 100644 index bce6b1a..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/chunks/PNGChunkzTXt.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.chunks; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.ZLibUtils; -import org.apache.sanselan.formats.png.PngConstants; -import org.apache.sanselan.formats.png.PngText; - -public class PNGChunkzTXt extends PNGTextChunk -{ - - public final String keyword, text; - - public PNGChunkzTXt(int length, int chunkType, int crc, byte bytes[]) - throws ImageReadException, IOException - { - super(length, chunkType, crc, bytes); - - { - int index = findNull(bytes); - if (index < 0) - throw new ImageReadException( - "PNG zTXt chunk keyword is unterminated."); - - keyword = new String(bytes, 0, index, "ISO-8859-1"); - index++; - - int compressionMethod = bytes[index++]; - if (compressionMethod != PngConstants.COMPRESSION_DEFLATE_INFLATE) - throw new ImageReadException( - "PNG zTXt chunk has unexpected compression method: " - + compressionMethod); - - int compressedTextLength = bytes.length - index; - byte compressedText[] = new byte[compressedTextLength]; - System.arraycopy(bytes, index, compressedText, 0, - compressedTextLength); - - text = new String(new ZLibUtils().inflate(compressedText), - "ISO-8859-1"); - } - } - - /** - * @return Returns the keyword. - */ - public String getKeyword() - { - return keyword; - } - - /** - * @return Returns the text. - */ - public String getText() - { - return text; - } - - public PngText getContents() - { - return new PngText.zTXt(keyword, text); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterAverage.java b/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterAverage.java deleted file mode 100644 index 80f5221..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterAverage.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ScanlineFilterAverage extends ScanlineFilter -{ - private final int BytesPerPixel; - - public ScanlineFilterAverage(int BytesPerPixel) - { - this.BytesPerPixel = BytesPerPixel; - } - - public void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException - { - for (int i = 0; i < src.length; i++) - { - int Raw = 0; - int prev_index = i - BytesPerPixel; - if (prev_index >= 0) - Raw = dst[prev_index]; - - int Prior = 0; - if (up != null) - Prior = up[i]; - - int Average = ((0xff & Raw) + (0xff & Prior)) / 2; - - dst[i] = (byte) ((src[i] + Average) % 256); - // dst[i] = src[i]; - // dst[i] = (byte) 255; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterPaeth.java b/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterPaeth.java deleted file mode 100644 index 51c9642..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterPaeth.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ScanlineFilterPaeth extends ScanlineFilter -{ - private final int BytesPerPixel; - - public ScanlineFilterPaeth(int BytesPerPixel) - { - this.BytesPerPixel = BytesPerPixel; - } - - private int PaethPredictor(int a, int b, int c) - { - // ; a = left, b = above, c = upper left - int p = a + b - c; // ; initial estimate - int pa = Math.abs(p - a); // ; distances to a, b, c - int pb = Math.abs(p - b); - int pc = Math.abs(p - c); - // ; return nearest of a,b,c, - // ; breaking ties in order a,b,c. - if ((pa <= pb) && (pa <= pc)) - return a; - else if (pb <= pc) - return b; - else - return c; - } - - public void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException - { - for (int i = 0; i < src.length; i++) - { - int left = 0; - int prev_index = i - BytesPerPixel; - if (prev_index >= 0) - left = dst[prev_index]; - - int above = 0; - if (up != null) - above = up[i]; - // above = 255; - - int upperleft = 0; - if ((prev_index >= 0) && (up != null)) - upperleft = up[prev_index]; - // upperleft = 255; - - int PaethPredictor = PaethPredictor(0xff & left, 0xff & above, - 0xff & upperleft); - - dst[i] = (byte) ((src[i] + PaethPredictor) % 256); - // dst[i] = (byte) ((src[i] + PaethPredictor) ); - // dst[i] = src[i]; - - // dst[i] = (byte) 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterSub.java b/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterSub.java deleted file mode 100644 index 06a9b78..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterSub.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ScanlineFilterSub extends ScanlineFilter -{ - private final int BytesPerPixel; - - public ScanlineFilterSub(int BytesPerPixel) - { - this.BytesPerPixel = BytesPerPixel; - } - - public void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException - { - for (int i = 0; i < src.length; i++) - { - int prev_index = i - BytesPerPixel; - if (prev_index >= 0) - dst[i] = (byte) ((src[i] + dst[prev_index]) % 256); - // dst[i] = 0xff & (src[i] + src[prev_index]); - else - dst[i] = src[i]; - - // if(i<10) - // System.out.println("\t" + i + ": " + dst[i] + " (" + src[i] + ", " + prev_index + ")"); - - // dst[i] = src[i]; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterUp.java b/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterUp.java deleted file mode 100644 index 9d2aaff..0000000 --- a/src/main/java/org/apache/sanselan/formats/png/scanlinefilters/ScanlineFilterUp.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.png.scanlinefilters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ScanlineFilterUp extends ScanlineFilter -{ - private final int BytesPerPixel; - - public ScanlineFilterUp(int BytesPerPixel) - { - this.BytesPerPixel = BytesPerPixel; - } - - public void unfilter(byte src[], byte dst[], byte up[]) - throws ImageReadException, IOException - { - for (int i = 0; i < src.length; i++) - { - // byte b; - - if (up != null) - dst[i] = (byte) ((src[i] + up[i]) % 256); - else - dst[i] = src[i]; - - // if(i<10) - // System.out.println("\t" + i + ": " + dst[i]); - // dst[i] = b; - // dst[i] = src[i]; - // dst[i] = (byte) 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/FileInfo.java b/src/main/java/org/apache/sanselan/formats/pnm/FileInfo.java deleted file mode 100644 index c0a7205..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/FileInfo.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageFormat; - -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.DataBuffer; - - -public abstract class FileInfo -{ - protected final int width, height; - protected final boolean RAWBITS; - - public FileInfo(int width, int height, boolean RAWBITS) - { - this.width = width; - this.height = height; - this.RAWBITS = RAWBITS; - } - - public abstract int getNumComponents(); - - public abstract int getBitDepth(); - - public abstract ImageFormat getImageType(); - - public abstract String getImageTypeDescription(); - - public abstract String getMIMEType(); - - public abstract int getColorType(); - - public abstract int getRGB(WhiteSpaceReader wsr) throws IOException; - - public abstract int getRGB(InputStream is) throws IOException; - - protected void newline() - { - // do nothing by default. - } - - public void readImage(BufferedImage bi, InputStream is) throws IOException - { - // is = new BufferedInputStream(is); - // int count = 0; - // - // try - // { - DataBuffer buffer = bi.getRaster().getDataBuffer(); - - if (!RAWBITS) - { - WhiteSpaceReader wsr = new WhiteSpaceReader(is); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int rgb = getRGB(wsr); - - buffer.setElem(y * width + x, rgb); - // count++; - } - newline(); - } - } else - { - for (int y = 0; y < height; y++) - { - // System.out.println("y: " + y); - for (int x = 0; x < width; x++) - { - int rgb = getRGB(is); - buffer.setElem(y * width + x, rgb); - // count++; - } - newline(); - } - } - // } - // finally - // { - // System.out.println("count: " + count); - // dump(); - // } - } - - public void dump() - { - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PBMWriter.java b/src/main/java/org/apache/sanselan/formats/pnm/PBMWriter.java deleted file mode 100644 index 83f97fc..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PBMWriter.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PBMWriter extends PNMWriter implements PNMConstants -{ - public PBMWriter(boolean RAWBITS) - { - super(RAWBITS); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - os.write(PNM_PREFIX_BYTE); - os.write(RAWBITS ? PBM_RAW_CODE : PBM_TEXT_CODE); - os.write(PNM_SEPARATOR); - - int width = src.getWidth(); - int height = src.getHeight(); - - os.write(("" + width).getBytes()); - os.write(PNM_SEPARATOR); - - os.write(("" + height).getBytes()); - os.write(PNM_SEPARATOR); - - int bitcache = 0; - int bits_in_cache = 0; - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - int sample = (red + green + blue) / 3; - if (sample > 127) - sample = 0; - else - sample = 1; - - if (RAWBITS) - { - bitcache = (bitcache << 1) | (0x1 & sample); - bits_in_cache++; - - if (bits_in_cache >= 8) - { - os.write((byte) bitcache); - bitcache = 0; - bits_in_cache = 0; - } - } else - { - os.write(("" + sample).getBytes()); // max component value - os.write(PNM_SEPARATOR); - } - } - - if ((RAWBITS) && (bits_in_cache > 0)) - { - bitcache = bitcache << (8-bits_in_cache); - os.write((byte) bitcache); - bitcache = 0; - bits_in_cache = 0; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PGMFileInfo.java b/src/main/java/org/apache/sanselan/formats/pnm/PGMFileInfo.java deleted file mode 100644 index 955b1f1..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PGMFileInfo.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; - -public class PGMFileInfo extends FileInfo -{ - private final int max; // TODO: handle max - - public PGMFileInfo(int width, int height, boolean RAWBITS, int max) - { - super(width, height, RAWBITS); - - this.max = max; - } - - public int getNumComponents() - { - return 1; - } - - public int getBitDepth() - { - return 8; - } - - public ImageFormat getImageType() - { - return ImageFormat.IMAGE_FORMAT_PPM; - } - - public String getImageTypeDescription() - { - return "PGM: portable pixmap file format"; - } - - public String getMIMEType() - { - return "image/x-portable-pixmap"; - } - - public int getColorType() - { - return ImageInfo.COLOR_TYPE_RGB; - } - - public int getRGB(InputStream is) throws IOException - { - int sample = is.read(); - if (sample < 0) - throw new IOException("PGM: Unexpected EOF"); - - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & sample) << 16) - | ((0xff & sample) << 8) | ((0xff & sample) << 0); - - return rgb; - } - - public int getRGB(WhiteSpaceReader wsr) throws IOException - { - int sample = Integer.parseInt(wsr.readtoWhiteSpace()); - - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & sample) << 16) - | ((0xff & sample) << 8) | ((0xff & sample) << 0); - - return rgb; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PGMWriter.java b/src/main/java/org/apache/sanselan/formats/pnm/PGMWriter.java deleted file mode 100644 index 7ad5c93..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PGMWriter.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PGMWriter extends PNMWriter -{ - public PGMWriter(boolean RAWBITS) - { - super(RAWBITS); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // System.out.println - // (b1 == 0x50 && b2 == 0x36) - os.write(0x50); - os.write(RAWBITS ? 0x35 : 0x32); - os.write(' '); - - int width = src.getWidth(); - int height = src.getHeight(); - - os.write(("" + width).getBytes()); - os.write(' '); - - os.write(("" + height).getBytes()); - os.write(' '); - - os.write(("" + 255).getBytes()); // max component value - os.write('\n'); - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - int sample = (red + green + blue) / 3; - - if (RAWBITS) - { - os.write((byte) sample); - } - else - { - os.write(("" + sample).getBytes()); // max component value - os.write(' '); - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PNMConstants.java b/src/main/java/org/apache/sanselan/formats/pnm/PNMConstants.java deleted file mode 100644 index 20e61df..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PNMConstants.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -public interface PNMConstants -{ - public static final byte PNM_PREFIX_BYTE = 0x50; // P - - public static final byte PBM_TEXT_CODE = 0x31; // Textual Bitmap - public static final byte PGM_TEXT_CODE = 0x32; // Textual GrayMap - public static final byte PPM_TEXT_CODE = 0x33; // Textual Pixmap - public static final byte PGM_RAW_CODE = 0x35; // RAW GrayMap - public static final byte PBM_RAW_CODE = 0x34; // RAW Bitmap - public static final byte PPM_RAW_CODE = 0x36; // RAW Pixmap - - public static final byte PNM_SEPARATOR = 0x20; // Space - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PNMImageParser.java b/src/main/java/org/apache/sanselan/formats/pnm/PNMImageParser.java deleted file mode 100644 index ec438ef..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PNMImageParser.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PNMImageParser extends ImageParser implements PNMConstants -{ - - public PNMImageParser() - { - super.setByteOrder(BYTE_ORDER_LSB); - // setDebug(true); - } - - public String getName() - { - return "Pbm-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".pnm"; - - private static final String ACCEPTED_EXTENSIONS[] = { ".pbm", ".pgm", - ".ppm", ".pnm", }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_PBM, // - ImageFormat.IMAGE_FORMAT_PGM, // - ImageFormat.IMAGE_FORMAT_PPM, // - ImageFormat.IMAGE_FORMAT_PNM, }; - } - - private FileInfo readHeader(InputStream is) throws ImageReadException, - IOException - { - byte identifier1 = readByte("Identifier1", is, "Not a Valid PNM File"); - byte identifier2 = readByte("Identifier2", is, "Not a Valid PNM File"); - - WhiteSpaceReader wsr = new WhiteSpaceReader(is); - - int width = Integer.parseInt(wsr.readtoWhiteSpace()); - int height = Integer.parseInt(wsr.readtoWhiteSpace()); - - // System.out.println("width: " + width); - // System.out.println("height: " + height); - // System.out.println("width*height: " + width * height); - // System.out.println("3*width*height: " + 3 * width * height); - // System.out.println("((width*height+7)/8): " - // + ((width * height + 7) / 8)); - - if (identifier1 != PNM_PREFIX_BYTE) - throw new ImageReadException("PNM file has invalid header."); - - if (identifier2 == PBM_TEXT_CODE) - return new PBMFileInfo(width, height, false); - else if (identifier2 == PBM_RAW_CODE) - return new PBMFileInfo(width, height, true); - else if (identifier2 == PGM_TEXT_CODE) - { - int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); - return new PGMFileInfo(width, height, false, maxgray); - } else if (identifier2 == PGM_RAW_CODE) - { - int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); - return new PGMFileInfo(width, height, true, maxgray); - } else if (identifier2 == PPM_TEXT_CODE) - { - int max = Integer.parseInt(wsr.readtoWhiteSpace()); - return new PPMFileInfo(width, height, false, max); - } else if (identifier2 == PPM_RAW_CODE) - { - int max = Integer.parseInt(wsr.readtoWhiteSpace()); - // System.out.println("max: " + max); - return new PPMFileInfo(width, height, true, max); - } else - throw new ImageReadException("PNM file has invalid header."); - } - - private FileInfo readHeader(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - return readHeader(is); - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FileInfo info = readHeader(byteSource); - - if (info == null) - throw new ImageReadException("PNM: Couldn't read Header"); - - return new Dimension(info.width, info.height); - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FileInfo info = readHeader(byteSource); - - if (info == null) - throw new ImageReadException("PNM: Couldn't read Header"); - - ArrayList Comments = new ArrayList(); - - int BitsPerPixel = info.getBitDepth() * info.getNumComponents(); - ImageFormat Format = info.getImageType(); - String FormatName = info.getImageTypeDescription(); - String MimeType = info.getMIMEType(); - int NumberOfImages = 1; - boolean isProgressive = false; - - // boolean isProgressive = (fPNGChunkIHDR.InterlaceMethod != 0); - // - int PhysicalWidthDpi = 72; - float PhysicalWidthInch = (float) ((double) info.width / (double) PhysicalWidthDpi); - int PhysicalHeightDpi = 72; - float PhysicalHeightInch = (float) ((double) info.height / (double) PhysicalHeightDpi); - - String FormatDetails = info.getImageTypeDescription(); - - boolean isTransparent = false; - boolean usesPalette = false; - - int ColorType = info.getColorType(); - String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; - - ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, - Format, FormatName, info.height, MimeType, NumberOfImages, - PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, - PhysicalWidthInch, info.width, isProgressive, isTransparent, - usesPalette, ColorType, compressionAlgorithm); - - return result; - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - pw.println("pnm.dumpImageFile"); - - { - ImageInfo imageData = getImageInfo(byteSource); - if (imageData == null) - return false; - - imageData.toString(pw, ""); - } - - pw.println(""); - - return true; - } - - private int[] getColorTable(byte bytes[]) throws ImageReadException, - IOException - { - if ((bytes.length % 3) != 0) - throw new ImageReadException("Bad Color Table Length: " - + bytes.length); - int length = bytes.length / 3; - - int result[] = new int[length]; - - for (int i = 0; i < length; i++) - { - int red = 0xff & bytes[(i * 3) + 0]; - int green = 0xff & bytes[(i * 3) + 1]; - int blue = 0xff & bytes[(i * 3) + 2]; - - int alpha = 0xff; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - result[i] = rgb; - } - - return result; - } - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - FileInfo info = readHeader(is); - - int width = info.width; - int height = info.height; - - boolean hasAlpha = false; - BufferedImage result = getBufferedImageFactory(params) - .getColorBufferedImage(width, height, hasAlpha); - - info.readImage(result, is); - - return result; - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public static final String PARAM_KEY_PNM_RAWBITS = "PNM_RAWBITS"; - public static final String PARAM_VALUE_PNM_RAWBITS_YES = "YES"; - public static final String PARAM_VALUE_PNM_RAWBITS_NO = "NO"; - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - PNMWriter writer = null; - boolean useRawbits = true; - - if (params != null) - { - Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS); - if (useRawbitsParam != null) - { - if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) - useRawbits = false; - } - - Object subtype = params.get(PARAM_KEY_FORMAT); - if (subtype != null) - { - if (subtype.equals(ImageFormat.IMAGE_FORMAT_PBM)) - writer = new PBMWriter(useRawbits); - else if (subtype.equals(ImageFormat.IMAGE_FORMAT_PGM)) - writer = new PGMWriter(useRawbits); - else if (subtype.equals(ImageFormat.IMAGE_FORMAT_PPM)) - writer = new PPMWriter(useRawbits); - } - } - - if (writer == null) - writer = new PPMWriter(useRawbits); - - // make copy of params; we'll clear keys as we consume them. - params = new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - writer.writeImage(src, os, params); - } - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PPMFileInfo.java b/src/main/java/org/apache/sanselan/formats/pnm/PPMFileInfo.java deleted file mode 100644 index f3d7795..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PPMFileInfo.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; - -public class PPMFileInfo extends FileInfo -{ - private final int max; // TODO: handle max - - public PPMFileInfo(int width, int height, boolean RAWBITS, int max) - { - super(width, height, RAWBITS); - - this.max = max; - } - - public int getNumComponents() - { - return 3; - } - - public int getBitDepth() - { - return 8; - } - - public ImageFormat getImageType() - { - return ImageFormat.IMAGE_FORMAT_PGM; - } - - public String getImageTypeDescription() - { - return "PGM: portable graymap file format"; - } - - public String getMIMEType() - { - return "image/x-portable-graymap"; - } - - public int getColorType() - { - return ImageInfo.COLOR_TYPE_GRAYSCALE; - } - - public int getRGB(InputStream is) throws IOException - { - int red = is.read(); - int green = is.read(); - int blue = is.read(); - - if ((red < 0) || (green < 0) || (blue < 0)) - throw new IOException("PPM: Unexpected EOF"); - - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) - | ((0xff & green) << 8) | ((0xff & blue) << 0); - - return rgb; - } - - public int getRGB(WhiteSpaceReader wsr) throws IOException - { - int red = Integer.parseInt(wsr.readtoWhiteSpace()); - int green = Integer.parseInt(wsr.readtoWhiteSpace()); - int blue = Integer.parseInt(wsr.readtoWhiteSpace()); - - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) - | ((0xff & green) << 8) | ((0xff & blue) << 0); - - return rgb; - } - - public void dump() - { - // System.out.println("count: " + count); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/pnm/PPMWriter.java b/src/main/java/org/apache/sanselan/formats/pnm/PPMWriter.java deleted file mode 100644 index eaf3240..0000000 --- a/src/main/java/org/apache/sanselan/formats/pnm/PPMWriter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.pnm; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PPMWriter extends PNMWriter -{ - public PPMWriter(boolean RAWBITS) - { - super(RAWBITS); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // System.out.println - // (b1 == 0x50 && b2 == 0x36) - os.write(0x50); - os.write(RAWBITS ? 0x36 : 0x33); - os.write(' '); - - int width = src.getWidth(); - int height = src.getHeight(); - - os.write(("" + width).getBytes()); - os.write(' '); - - os.write(("" + height).getBytes()); - os.write(' '); - - os.write(("" + 255).getBytes()); // max component value - os.write('\n'); - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - - if (RAWBITS) - { - os.write((byte) red); - os.write((byte) green); - os.write((byte) blue); - } - else - { - os.write(("" + red).getBytes()); // max component value - os.write(' '); - os.write(("" + green).getBytes()); // max component value - os.write(' '); - os.write(("" + blue).getBytes()); // max component value - os.write(' '); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/ImageResourceType.java b/src/main/java/org/apache/sanselan/formats/psd/ImageResourceType.java deleted file mode 100644 index d50c4bb..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/ImageResourceType.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class ImageResourceType -{ - public final int ID; - public final String Description; - - public ImageResourceType(int ID, String Description) - { - this.ID = ID; - this.Description = Description; - } - - public ImageResourceType(int ID, int ID2, String Description) - throws ImageReadException, IOException - { - this(ID, Description); - if (ID != ID2) - throw new ImageReadException("Mismatch ID: " + ID + " ID2: " + ID2); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/PSDConstants.java b/src/main/java/org/apache/sanselan/formats/psd/PSDConstants.java deleted file mode 100644 index 8410040..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/PSDConstants.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import org.apache.sanselan.util.Debug; - -public class PSDConstants -{ - public static final ImageResourceType fImageResourceTypes[]; - - public String getDescription(int id) - { - for (int i = 0; i < fImageResourceTypes.length; i++) - { - if (fImageResourceTypes[i].ID == id) - return fImageResourceTypes[i].Description; - } - return "Unknown"; - } - - static - { - ImageResourceType temp[] = null; - - try - { - temp = new ImageResourceType[]{ - new ImageResourceType( - 0x03E8, - 1000, - " Contains five 2 byte values: number of channels, rows, columns, depth, and mode."), - new ImageResourceType(0x03E9, 1001, - "Optional. Macintosh print manager print info record."), - new ImageResourceType(0x03EB, 1003, - " Contains the indexed color table."), - new ImageResourceType(0x03ED, 1005, - "ResolutionInfo structure. See Appendix A in Photoshop SDK Guide.pdf."), - new ImageResourceType(0x03EE, 1006, - "Names of the alpha channels as a series of Pascal strings."), - new ImageResourceType(0x03EF, 1007, - "DisplayInfo structure. See Appendix A in Photoshop SDK Guide.pdf ."), - new ImageResourceType(0x03F0, 1008, - "Optional. The caption as a Pascal string."), - new ImageResourceType( - 0x03F1, - 1009, - "Border information. Contains a fixed-number for the border width, and 2 bytes for border units (1=inches, 2=cm, 3=points, 4=picas, 5=columns)."), - new ImageResourceType(0x03F2, 1010, - "Background color. See the Colors additional file information."), - new ImageResourceType( - 0x03F3, - 1011, - "Print flags. A series of one byte boolean values (see Page Setup dialog): labels, crop marks, color bars, registration marks, negative, flip, interpolate, caption."), - new ImageResourceType(0x03F4, 1012, - "Grayscale and multichannel halftoning information."), - new ImageResourceType(0x03F5, 1013, - "Color halftoning information."), - new ImageResourceType(0x03F6, 1014, - "Duotone halftoning information."), - new ImageResourceType(0x03F7, 1015, - "Grayscale and multichannel transfer function."), - new ImageResourceType(0x03F8, 1016, - "Color transfer functions."), - new ImageResourceType(0x03F9, 1017, - "Duotone transfer functions."), - new ImageResourceType(0x03FA, 1018, - "Duotone image information."), - new ImageResourceType(0x03FB, 1019, - "Two bytes for the effective black and white values for the dot range."), - new ImageResourceType(0x03FC, 1020, "Obsolete."), - new ImageResourceType(0x03FD, 1021, "EPS options."), - new ImageResourceType( - 0x03FE, - 1022, - "Quick Mask information. 2 bytes containing Quick Mask channel ID, 1 byte boolean indicating whether the mask was initially empty."), - new ImageResourceType(0x03FF, 1023, "Obsolete."), - new ImageResourceType( - 0x0400, - 1024, - "Layer state information. 2 bytes containing the index of target layer. 0=bottom layer."), - new ImageResourceType(0x0401, 1025, - "Working path (not saved). See path resource format later in this chapter."), - new ImageResourceType( - 0x0402, - 1026, - "Layers group information. 2 bytes per layer containing a group ID for the dragging groups. Layers in a group have the same group ID."), - new ImageResourceType(0x0403, 1027, "Obsolete."), - new ImageResourceType( - 0x0404, - 1028, - "IPTC-NAA record. This contains the File Info... information. See the IIMV4.pdf document."), - new ImageResourceType(0x0405, 1029, - "Image mode for raw format files."), - new ImageResourceType(0x0406, 1030, - "JPEG quality. Private."), - new ImageResourceType( - 0x0408, - 1032, - "Grid and guides information. See grid and guides resource format later in this chapter."), - new ImageResourceType(0x0409, 1033, - "Thumbnail resource. See thumbnail resource format later in this chapter."), - new ImageResourceType( - 0x040A, - 1034, - "Copyright flag. Boolean indicating whether image is copyrighted. Can be set via Property suite or by user in File Info..."), - new ImageResourceType( - 0x040B, - 1035, - "URL. Handle of a text string with uniform resource locator. Can be set via Property suite or by user in File Info..."), - new ImageResourceType(0x040C, 1036, - "Thumbnail resource. See thumbnail resource format later in this chapter."), - new ImageResourceType( - 0x040D, - 1037, - "Global Angle. 4 bytes that contain an integer between 0..359 which is the global lighting angle for effects layer. If not present assumes 30."), - new ImageResourceType( - 0x040E, - 1038, - "Color samplers resource. See color samplers resource format later in this chapter."), - new ImageResourceType( - 0x040F, - 1039, - "ICC Profile. The raw bytes of an ICC format profile, see the ICC34.pdf and ICC34.h files from the Internation Color Consortium located in the documentation section."), - new ImageResourceType(0x0410, 1040, - "One byte for Watermark."), - new ImageResourceType( - 0x0411, - 1041, - "ICC Untagged. 1 byte that disables any assumed profile handling when opening the file. 1 = intentionally untagged."), - new ImageResourceType( - 0x0412, - 1042, - "Effects visible. 1 byte global flag to show/hide all the effects layer. Only present when they are hidden."), - new ImageResourceType( - 0x0413, - 1043, - "Spot Halftone. 4 bytes for version, 4 bytes for length, and the variable length data."), - new ImageResourceType( - 0x0414, - 1044, - "Document specific IDs, layer IDs will be generated starting at this base value or a greater value if we find existing IDs to already exceed it. It's purpose is to avoid the case where we add layers, flatten, save, open, and then add more layers that end up with the same IDs as the first set. 4 bytes."), - new ImageResourceType(0x0415, 1045, - "Unicode Alpha Names. 4 bytes for length and the string as a unicode string."), - new ImageResourceType( - 0x0416, - 1046, - "Indexed Color Table Count. 2 bytes for the number of colors in table that are actually defined"), - new ImageResourceType(0x0417, 1047, - "Tansparent Index. 2 bytes for the index of transparent color, if any."), - new ImageResourceType(0x0419, 1049, - "Global Altitude. 4 byte entry for altitude"), - new ImageResourceType(0x041A, 1050, - "Slices. See description later in this chapter"), - new ImageResourceType(0x041B, 1051, - "Workflow URL. Unicode string, 4 bytes of length followed by unicode string."), - new ImageResourceType( - 0x041C, - 1052, - "Jump To XPEP. 2 bytes major version, 2 bytes minor version, 4 bytes count. Following is repeated for count: 4 bytes block size, 4 bytes key, if key = 'jtDd' then next is a Boolean for the dirty flag otherwise it's a 4 byte entry for the mod date."), - new ImageResourceType( - 0x041D, - 1053, - "Alpha Identifiers. 4 bytes of length, followed by 4 bytes each for every alpha identifier."), - new ImageResourceType( - 0x041E, - 1054, - "URL List. 4 byte count of URLs, followed by 4 byte long, 4 byte ID, and unicode string for each count."), - new ImageResourceType( - 0x0421, - 1057, - "Version Info. 4 byte version, 1 byte HasRealMergedData, unicode string of writer name, unicode string of reader name, 4 bytes of file version."), - new ImageResourceType( - 0x07D0 - 0x0BB6, - 2000 - 2998, - "Path Information (saved paths). See path resource format later in this chapter."), - new ImageResourceType(0x0BB7, 2999, - "Name of clipping path. See path resource format later in this chapter."), - new ImageResourceType( - 0x2710, - 10000, - "Print flags information. 2 bytes version (=1), 1 byte center crop marks, 1 byte (=0), 4 bytes bleed width value, 2 bytes bleed width scale."), - }; - } - catch (Exception e) - { - Debug.debug(PSDConstants.class, e); - } - fImageResourceTypes = temp; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/PSDHeaderInfo.java b/src/main/java/org/apache/sanselan/formats/psd/PSDHeaderInfo.java deleted file mode 100644 index b33e80e..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/PSDHeaderInfo.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import java.io.PrintWriter; - -public class PSDHeaderInfo -{ - public final int Version; - public final byte Reserved[]; - public final int Channels; - public final int Rows; - public final int Columns; - public final int Depth; - public final int Mode; - - public PSDHeaderInfo(int Version, byte Reserved[], int Channels, int Rows, - int Columns, int Depth, int Mode) - { - this.Version = Version; - this.Reserved = Reserved; - this.Channels = Channels; - this.Rows = Rows; - this.Columns = Columns; - this.Depth = Depth; - this.Mode = Mode; - - } - - public void dump() - { - PrintWriter pw = new PrintWriter(System.out); - dump(pw); - pw.flush(); - } - - public void dump(PrintWriter pw) - { - pw.println(""); - pw.println("Header"); - pw.println("Version: " + Version + " (" + Integer.toHexString(Version) - + ")"); - pw.println("Channels: " + Channels + " (" - + Integer.toHexString(Channels) + ")"); - pw.println("Rows: " + Rows + " (" + Integer.toHexString(Rows) + ")"); - pw.println("Columns: " + Columns + " (" + Integer.toHexString(Columns) - + ")"); - pw.println("Depth: " + Depth + " (" + Integer.toHexString(Depth) + ")"); - pw.println("Mode: " + Mode + " (" + Integer.toHexString(Mode) + ")"); - pw.println("Reserved: " + Reserved.length); - pw.println(""); - pw.flush(); - - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/PsdImageParser.java b/src/main/java/org/apache/sanselan/formats/psd/PsdImageParser.java deleted file mode 100644 index cf55adf..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/PsdImageParser.java +++ /dev/null @@ -1,852 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.psd.dataparsers.DataParser; -import org.apache.sanselan.formats.psd.dataparsers.DataParserBitmap; -import org.apache.sanselan.formats.psd.dataparsers.DataParserCMYK; -import org.apache.sanselan.formats.psd.dataparsers.DataParserGrayscale; -import org.apache.sanselan.formats.psd.dataparsers.DataParserIndexed; -import org.apache.sanselan.formats.psd.dataparsers.DataParserLab; -import org.apache.sanselan.formats.psd.dataparsers.DataParserRGB; -import org.apache.sanselan.formats.psd.datareaders.CompressedDataReader; -import org.apache.sanselan.formats.psd.datareaders.DataReader; -import org.apache.sanselan.formats.psd.datareaders.UncompressedDataReader; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PsdImageParser extends ImageParser -{ - - public PsdImageParser() - { - super.setByteOrder(BYTE_ORDER_MSB); - // setDebug(true); - } - - public String getName() - { - return "PSD-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".psd"; - - private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_PSD, // - }; - } - - private PSDHeaderInfo readHeader(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - return readHeader(is); - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } - - private PSDHeaderInfo readHeader(InputStream is) throws ImageReadException, - IOException - { - readAndVerifyBytes(is, new byte[] { 56, 66, 80, 83 }, - "Not a Valid PSD File"); - - int Version = read2Bytes("Version", is, "Not a Valid PSD File"); - - byte Reserved[] = readByteArray("Reserved", 6, is, - "Not a Valid PSD File"); - - int Channels = read2Bytes("Channels", is, "Not a Valid PSD File"); - int Rows = read4Bytes("Rows", is, "Not a Valid PSD File"); - int Columns = read4Bytes("Columns", is, "Not a Valid PSD File"); - int Depth = read2Bytes("Depth", is, "Not a Valid PSD File"); - int Mode = read2Bytes("Mode", is, "Not a Valid PSD File"); - - PSDHeaderInfo result = new PSDHeaderInfo(Version, Reserved, Channels, - Rows, Columns, Depth, Mode); - - return result; - } - - private ImageContents readImageContents(InputStream is) - throws ImageReadException, IOException - { - PSDHeaderInfo header = readHeader(is); - - int ColorModeDataLength = read4Bytes("ColorModeDataLength", is, - "Not a Valid PSD File"); - skipBytes(is, ColorModeDataLength); - // is.skip(ColorModeDataLength); - // byte ColorModeData[] = readByteArray("ColorModeData", - // ColorModeDataLength, is, "Not a Valid PSD File"); - - int ImageResourcesLength = read4Bytes("ImageResourcesLength", is, - "Not a Valid PSD File"); - skipBytes(is, ImageResourcesLength); - // long skipped = is.skip(ImageResourcesLength); - // byte ImageResources[] = readByteArray("ImageResources", - // ImageResourcesLength, is, "Not a Valid PSD File"); - - int LayerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", is, - "Not a Valid PSD File"); - skipBytes(is, LayerAndMaskDataLength); - // is.skip(LayerAndMaskDataLength); - // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", - // LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - int Compression = read2Bytes("Compression", is, "Not a Valid PSD File"); - - // skip_bytes(is, LayerAndMaskDataLength); - // byte ImageData[] = readByteArray("ImageData", LayerAndMaskDataLength, - // is, "Not a Valid PSD File"); - - // System.out.println("Compression: " + Compression); - - ImageContents result = new ImageContents(header, ColorModeDataLength, - // ColorModeData, - ImageResourcesLength, - // ImageResources, - LayerAndMaskDataLength, - // LayerAndMaskData, - Compression); - - return result; - } - - private ArrayList readImageResourceBlocks(byte bytes[], - int imageResourceIDs[], int maxBlocksToRead) - throws ImageReadException, IOException - { - return readImageResourceBlocks(new ByteArrayInputStream(bytes), - imageResourceIDs, maxBlocksToRead, bytes.length); - } - - private boolean keepImageResourceBlock(int ID, int imageResourceIDs[]) - { - if (imageResourceIDs == null) - return true; - - for (int i = 0; i < imageResourceIDs.length; i++) - if (ID == imageResourceIDs[i]) - return true; - - return false; - } - - private ArrayList readImageResourceBlocks(InputStream is, - int imageResourceIDs[], int maxBlocksToRead, int available) - throws ImageReadException, IOException - { - ArrayList result = new ArrayList(); - - while (available > 0) - { - readAndVerifyBytes(is, new byte[] { 56, 66, 73, 77 }, - "Not a Valid PSD File"); - available -= 4; - - int id = read2Bytes("ID", is, "Not a Valid PSD File"); - available -= 2; - - int nameLength = readByte("NameLength", is, "Not a Valid PSD File"); - - available -= 1; - byte nameBytes[] = readByteArray("NameData", nameLength, is, - "Not a Valid PSD File"); - available -= nameLength; - if (((nameLength + 1) % 2) != 0) - { - int NameDiscard = readByte("NameDiscard", is, - "Not a Valid PSD File"); - available -= 1; - } - // String Name = readPString("Name", 6, is, "Not a Valid PSD File"); - int DataSize = read4Bytes("Size", is, "Not a Valid PSD File"); - available -= 4; - // int ActualDataSize = ((DataSize % 2) == 0) - // ? DataSize - // : DataSize + 1; // pad to make even - - byte Data[] = readByteArray("Data", DataSize, is, - "Not a Valid PSD File"); - available -= DataSize; - - if ((DataSize % 2) != 0) - { - int DataDiscard = readByte("DataDiscard", is, - "Not a Valid PSD File"); - available -= 1; - } - - if (keepImageResourceBlock(id, imageResourceIDs)) - { - result.add(new ImageResourceBlock(id, nameBytes, Data)); - - if ((maxBlocksToRead >= 0) - && (result.size() >= maxBlocksToRead)) - return result; - } - // debugNumber("ID", ID, 2); - - } - - return result; - } - - private ArrayList readImageResourceBlocks(ByteSource byteSource, - int imageResourceIDs[], int maxBlocksToRead) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - ImageContents imageContents = readImageContents(is); - - is.close(); - - is = this.getInputStream(byteSource, PSD_SECTION_IMAGE_RESOURCES); - byte ImageResources[] = readByteArray("ImageResources", - imageContents.ImageResourcesLength, is, - "Not a Valid PSD File"); - - return readImageResourceBlocks(ImageResources, imageResourceIDs, - maxBlocksToRead); - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - } - - private static final int PSD_SECTION_HEADER = 0; - private static final int PSD_SECTION_COLOR_MODE = 1; - private static final int PSD_SECTION_IMAGE_RESOURCES = 2; - private static final int PSD_SECTION_LAYER_AND_MASK_DATA = 3; - private static final int PSD_SECTION_IMAGE_DATA = 4; - - private static final int PSD_HEADER_LENGTH = 26; - - private InputStream getInputStream(ByteSource byteSource, int section) - throws ImageReadException, IOException - { - InputStream is = byteSource.getInputStream(); - - if (section == PSD_SECTION_HEADER) - return is; - - skipBytes(is, PSD_HEADER_LENGTH); - // is.skip(kHeaderLength); - - int ColorModeDataLength = read4Bytes("ColorModeDataLength", is, - "Not a Valid PSD File"); - - if (section == PSD_SECTION_COLOR_MODE) - return is; - - skipBytes(is, ColorModeDataLength); - // byte ColorModeData[] = readByteArray("ColorModeData", - // ColorModeDataLength, is, "Not a Valid PSD File"); - - int ImageResourcesLength = read4Bytes("ImageResourcesLength", is, - "Not a Valid PSD File"); - - if (section == PSD_SECTION_IMAGE_RESOURCES) - return is; - - skipBytes(is, ImageResourcesLength); - // byte ImageResources[] = readByteArray("ImageResources", - // ImageResourcesLength, is, "Not a Valid PSD File"); - - int LayerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", is, - "Not a Valid PSD File"); - - if (section == PSD_SECTION_LAYER_AND_MASK_DATA) - return is; - - skipBytes(is, LayerAndMaskDataLength); - // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", - // LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - int Compression = read2Bytes("Compression", is, "Not a Valid PSD File"); - - // byte ImageData[] = readByteArray("ImageData", - // LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - if (section == PSD_SECTION_IMAGE_DATA) - return is; - - throw new ImageReadException("getInputStream: Unknown Section: " - + section); - } - - private byte[] getData(ByteSource byteSource, int section) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - // PSDHeaderInfo header = readHeader(is); - if (section == PSD_SECTION_HEADER) - return readByteArray("Header", PSD_HEADER_LENGTH, is, - "Not a Valid PSD File"); - skipBytes(is, PSD_HEADER_LENGTH); - - int ColorModeDataLength = read4Bytes("ColorModeDataLength", is, - "Not a Valid PSD File"); - - if (section == PSD_SECTION_COLOR_MODE) - return readByteArray("ColorModeData", ColorModeDataLength, is, - "Not a Valid PSD File"); - - skipBytes(is, ColorModeDataLength); - // byte ColorModeData[] = readByteArray("ColorModeData", - // ColorModeDataLength, is, "Not a Valid PSD File"); - - int ImageResourcesLength = read4Bytes("ImageResourcesLength", is, - "Not a Valid PSD File"); - - if (section == PSD_SECTION_IMAGE_RESOURCES) - return readByteArray("ImageResources", ImageResourcesLength, - is, "Not a Valid PSD File"); - - skipBytes(is, ImageResourcesLength); - // byte ImageResources[] = readByteArray("ImageResources", - // ImageResourcesLength, is, "Not a Valid PSD File"); - - int LayerAndMaskDataLength = read4Bytes("LayerAndMaskDataLength", - is, "Not a Valid PSD File"); - - if (section == PSD_SECTION_LAYER_AND_MASK_DATA) - return readByteArray("LayerAndMaskData", - LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - skipBytes(is, LayerAndMaskDataLength); - // byte LayerAndMaskData[] = readByteArray("LayerAndMaskData", - // LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - int Compression = read2Bytes("Compression", is, - "Not a Valid PSD File"); - - // byte ImageData[] = readByteArray("ImageData", - // LayerAndMaskDataLength, is, "Not a Valid PSD File"); - - // if (section == kPSD_SECTION_IMAGE_DATA) - // return readByteArray("LayerAndMaskData", LayerAndMaskDataLength, - // is, - // "Not a Valid PSD File"); - - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - throw new ImageReadException("getInputStream: Unknown Section: " - + section); - } - - private ImageContents readImageContents(ByteSource byteSource) - throws ImageReadException, IOException - { - InputStream is = null; - - try - { - is = byteSource.getInputStream(); - - ImageContents imageContents = readImageContents(is); - return imageContents; - } finally - { - try - { - if (is != null) { - is.close(); - } - } catch (Exception e) - { - Debug.debug(e); - } - - } - - } - - public final static int IMAGE_RESOURCE_ID_ICC_PROFILE = 0x040F; - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ArrayList blocks = readImageResourceBlocks(byteSource, - new int[] { IMAGE_RESOURCE_ID_ICC_PROFILE, }, 1); - - if ((blocks == null) || (blocks.size() < 1)) - return null; - - ImageResourceBlock irb = (ImageResourceBlock) blocks.get(0); - byte bytes[] = irb.data; - if ((bytes == null) || (bytes.length < 1)) - return null; - return bytes; - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - PSDHeaderInfo bhi = readHeader(byteSource); - if (bhi == null) - throw new ImageReadException("PSD: couldn't read header"); - - return new Dimension(bhi.Columns, bhi.Rows); - - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - private int getChannelsPerMode(int mode) - { - switch (mode) - { - case 0: // Bitmap - return 1; - case 1: // Grayscale - return 1; - case 2: // Indexed - return -1; - case 3: // RGB - return 3; - case 4: // CMYK - return 4; - case 7: // Multichannel - return -1; - case 8: // Duotone - return -1; - case 9: // Lab - return 4; - default: - return -1; - - } - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ImageContents imageContents = readImageContents(byteSource); - // ImageContents imageContents = readImage(byteSource, false); - - if (imageContents == null) - throw new ImageReadException("PSD: Couldn't read blocks"); - - PSDHeaderInfo header = imageContents.header; - if (header == null) - throw new ImageReadException("PSD: Couldn't read Header"); - - int Width = header.Columns; - int Height = header.Rows; - - ArrayList Comments = new ArrayList(); - // TODO: comments... - - int BitsPerPixel = header.Depth * getChannelsPerMode(header.Mode); - // System.out.println("header.Depth: " + header.Depth); - // System.out.println("header.Mode: " + header.Mode); - // System.out.println("getChannelsPerMode(header.Mode): " + - // getChannelsPerMode(header.Mode)); - if (BitsPerPixel < 0) - BitsPerPixel = 0; - ImageFormat Format = ImageFormat.IMAGE_FORMAT_PSD; - String FormatName = "Photoshop"; - String MimeType = "image/x-photoshop"; - // we ought to count images, but don't yet. - int NumberOfImages = -1; - // not accurate ... only reflects first - boolean isProgressive = false; - - int PhysicalWidthDpi = 72; - float PhysicalWidthInch = (float) ((double) Width / (double) PhysicalWidthDpi); - int PhysicalHeightDpi = 72; - float PhysicalHeightInch = (float) ((double) Height / (double) PhysicalHeightDpi); - - String FormatDetails = "Psd"; - - boolean isTransparent = false; // TODO: inaccurate. - boolean usesPalette = header.Mode == COLOR_MODE_INDEXED; - int ColorType = ImageInfo.COLOR_TYPE_UNKNOWN; - - String compressionAlgorithm; - switch (imageContents.Compression) - { - case 0: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; - break; - case 1: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PSD; - break; - default: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN; - } - - ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments, - Format, FormatName, Height, MimeType, NumberOfImages, - PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, - PhysicalWidthInch, Width, isProgressive, isTransparent, - usesPalette, ColorType, compressionAlgorithm); - - return result; - } - - // TODO not used - private ImageResourceBlock findImageResourceBlock(ArrayList blocks, int ID) - { - for (int i = 0; i < blocks.size(); i++) - { - ImageResourceBlock block = (ImageResourceBlock) blocks.get(i); - - if (block.id == ID) - return block; - } - return null; - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - pw.println("gif.dumpImageFile"); - - { - ImageInfo fImageData = getImageInfo(byteSource); - if (fImageData == null) - return false; - - fImageData.toString(pw, ""); - } - { - ImageContents imageContents = readImageContents(byteSource); - - imageContents.dump(pw); - imageContents.header.dump(pw); - - ArrayList blocks = readImageResourceBlocks(byteSource, - // fImageContents.ImageResources, - null, -1); - - pw.println("blocks.size(): " + blocks.size()); - - // System.out.println("gif.blocks: " + blocks.blocks.size()); - for (int i = 0; i < blocks.size(); i++) - { - ImageResourceBlock block = (ImageResourceBlock) blocks.get(i); - pw.println("\t" + i + " (" + Integer.toHexString(block.id) - + ", " + "'" - + new String(block.nameData) - + "' (" - + block.nameData.length - + "), " - // + block.getClass().getName() - // + ", " - + " data: " + block.data.length + " type: '" - + new PSDConstants().getDescription(block.id) + "' " - + ")"); - - } - - } - - pw.println(""); - - return true; - } - - private static final int COLOR_MODE_INDEXED = 2; - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - ImageContents imageContents = readImageContents(byteSource); - // ImageContents imageContents = readImage(byteSource, false); - - if (imageContents == null) - throw new ImageReadException("PSD: Couldn't read blocks"); - - PSDHeaderInfo header = imageContents.header; - if (header == null) - throw new ImageReadException("PSD: Couldn't read Header"); - - // ImageDescriptor id = (ImageDescriptor) - // findBlock(fImageContents.blocks, - // kImageSeperator); - // if (id == null) - // throw new ImageReadException("PSD: Couldn't read Image Descriptor"); - // GraphicControlExtension gce = (GraphicControlExtension) findBlock( - // fImageContents.blocks, kGraphicControlExtension); - - ArrayList blocks = readImageResourceBlocks(byteSource, - // fImageContents.ImageResources, - null, -1); - - int width = header.Columns; - int height = header.Rows; - // int height = header.Columns; - - // int transfer_type; - - // transfer_type = DataBuffer.TYPE_BYTE; - - boolean hasAlpha = false; - BufferedImage result = getBufferedImageFactory(params) - .getColorBufferedImage(width, height, hasAlpha); - - DataParser dataParser; - switch (imageContents.header.Mode) - { - case 0: // bitmap - dataParser = new DataParserBitmap(); - break; - case 1: - case 8: // Duotone=8; - dataParser = new DataParserGrayscale(); - break; - case 3: - dataParser = new DataParserRGB(); - break; - case 4: - dataParser = new DataParserCMYK(); - break; - case 9: - dataParser = new DataParserLab(); - break; - case COLOR_MODE_INDEXED: - // case 2 : // Indexed=2; - { - - byte ColorModeData[] = getData(byteSource, PSD_SECTION_COLOR_MODE); - - // ImageResourceBlock block = findImageResourceBlock(blocks, - // 0x03EB); - // if (block == null) - // throw new ImageReadException( - // "Missing: Indexed Color Image Resource Block"); - - dataParser = new DataParserIndexed(ColorModeData); - break; - } - case 7: // Multichannel=7; - // fDataParser = new DataParserStub(); - // break; - - // case 1 : - // fDataReader = new CompressedDataReader(); - // break; - default: - throw new ImageReadException("Unknown Mode: " - + imageContents.header.Mode); - } - DataReader fDataReader; - switch (imageContents.Compression) - { - case 0: - fDataReader = new UncompressedDataReader(dataParser); - break; - case 1: - fDataReader = new CompressedDataReader(dataParser); - break; - default: - throw new ImageReadException("Unknown Compression: " - + imageContents.Compression); - } - { - InputStream is = null; - - try - { - is = getInputStream(byteSource, PSD_SECTION_IMAGE_DATA); - fDataReader.readData(is, result, imageContents, this); - - fDataReader.dump(); - // is. - // ImageContents imageContents = readImageContents(is); - // return imageContents; - } finally - { - try - { - if (is != null) - is.close(); - } catch (Exception e) - { - Debug.debug(e); - } - - } - - } - - return result; - - } - - public final static int IMAGE_RESOURCE_ID_XMP = 0x0424; - - public final static String BLOCK_NAME_XMP = "XMP"; - - /** - * Extracts embedded XML metadata as XML string. - *

- * - * @param byteSource - * File containing image data. - * @param params - * Map of optional parameters, defined in SanselanConstants. - * @return Xmp Xml as String, if present. Otherwise, returns null. - */ - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - - ImageContents imageContents = readImageContents(byteSource); - - if (imageContents == null) - throw new ImageReadException("PSD: Couldn't read blocks"); - - PSDHeaderInfo header = imageContents.header; - if (header == null) - throw new ImageReadException("PSD: Couldn't read Header"); - - ArrayList blocks = readImageResourceBlocks(byteSource, - new int[] { IMAGE_RESOURCE_ID_XMP, }, -1); - - if ((blocks == null) || (blocks.size() < 1)) - return null; - - List xmpBlocks = new ArrayList(); - if (false) - { - // TODO: for PSD 7 and later, verify "XMP" name. - for (int i = 0; i < blocks.size(); i++) - { - ImageResourceBlock block = (ImageResourceBlock) blocks.get(i); - if (!block.getName().equals(BLOCK_NAME_XMP)) - continue; - xmpBlocks.add(block); - } - } else - xmpBlocks.addAll(blocks); - - if (xmpBlocks.size() < 1) - return null; - if (xmpBlocks.size() > 1) - throw new ImageReadException( - "PSD contains more than one XMP block."); - - ImageResourceBlock block = (ImageResourceBlock) xmpBlocks.get(0); - - try - { - // segment data is UTF-8 encoded xml. - String xml = new String(block.data, 0, block.data.length, "utf-8"); - return xml; - } catch (UnsupportedEncodingException e) - { - throw new ImageReadException("Invalid JPEG XMP Segment."); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserIndexed.java b/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserIndexed.java deleted file mode 100644 index 9574583..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserIndexed.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserIndexed extends DataParser -{ - private final int ColorTable[]; - - public DataParserIndexed(byte ColorModeData[]) - { - ColorTable = new int[256]; - for (int i = 0; i < 256; i++) - { - int red = 0xff & ColorModeData[0 * 256 + i]; - int green = 0xff & ColorModeData[1 * 256 + i]; - int blue = 0xff & ColorModeData[2 * 256 + i]; - int alpha = 0xff; - - int rgb = ((0xff & alpha) << 24) | ((0xff & red) << 16) - | ((0xff & green) << 8) | ((0xff & blue) << 0); - - ColorTable[i] = rgb; - } - } - - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int sample = 0xff & data[0][y][x]; - int rgb = ColorTable[sample]; - - return rgb; - } - - public int getBasicChannelsCount() - { - return 1; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserLab.java b/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserLab.java deleted file mode 100644 index 7e3fa5f..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/dataparsers/DataParserLab.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.dataparsers; - -import org.apache.sanselan.color.ColorConversions; -import org.apache.sanselan.formats.psd.ImageContents; - -public class DataParserLab extends DataParser -{ - - public DataParserLab() - { - - } - - protected int getRGB(int data[][][], int x, int y, - ImageContents imageContents) - { - int cieL = 0xff & data[0][y][x]; - int cieA = 0xff & data[1][y][x]; - int cieB = 0xff & data[2][y][x]; - - cieA -= 128; - cieB -= 128; - - int rgb = ColorConversions.convertCIELabtoARGBTest(cieL, cieA, cieB); - - return rgb; - } - - public int getBasicChannelsCount() - { - return 3; - } - - public void dump() - { - // for(int i=0;i<3;i++) - // { - // System.out.println("CIE: " + i + ": min: " + mins[i] + ", max: " + maxs[i]); - // } - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/datareaders/CompressedDataReader.java b/src/main/java/org/apache/sanselan/formats/psd/datareaders/CompressedDataReader.java deleted file mode 100644 index 6e99264..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/datareaders/CompressedDataReader.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.datareaders; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.PackBits; -import org.apache.sanselan.common.mylzw.BitsToByteInputStream; -import org.apache.sanselan.common.mylzw.MyBitInputStream; -import org.apache.sanselan.formats.psd.ImageContents; -import org.apache.sanselan.formats.psd.PSDHeaderInfo; -import org.apache.sanselan.formats.psd.dataparsers.DataParser; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class CompressedDataReader extends DataReader -{ - - public CompressedDataReader(DataParser fDataParser) - { - super(fDataParser); - } - - public void readData(InputStream is, BufferedImage bi, - ImageContents imageContents, BinaryFileParser bfp) - throws ImageReadException, IOException - { - PSDHeaderInfo header = imageContents.header; - int width = header.Columns; - int height = header.Rows; - - // this.setDebug(true); - int scanline_count = height * header.Channels; - int scanline_bytecounts[] = new int[scanline_count]; - for (int i = 0; i < scanline_count; i++) - scanline_bytecounts[i] = bfp.read2Bytes("scanline_bytecount[" + i - + "]", is, "PSD: bad Image Data"); - bfp.setDebug(false); - // System.out.println("fImageContents.Compression: " - // + imageContents.Compression); - - int depth = header.Depth; - - int channel_count = dataParser.getBasicChannelsCount(); - int data[][][] = new int[channel_count][height][]; - // channels[0] = - for (int channel = 0; channel < channel_count; channel++) - for (int y = 0; y < height; y++) - { - int index = channel * height + y; - byte packed[] = bfp.readByteArray("scanline", - scanline_bytecounts[index], is, - "PSD: Missing Image Data"); - - byte unpacked[] = new PackBits().decompress(packed, width); - InputStream bais = new ByteArrayInputStream(unpacked); - MyBitInputStream mbis = new MyBitInputStream(bais, - BYTE_ORDER_MSB); - BitsToByteInputStream bbis = new BitsToByteInputStream(mbis, 8); // we want all samples to be bytes - int scanline[] = bbis.readBitsArray(depth, width); - data[channel][y] = scanline; - - } - - dataParser.parseData(data, bi, imageContents); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/psd/datareaders/UncompressedDataReader.java b/src/main/java/org/apache/sanselan/formats/psd/datareaders/UncompressedDataReader.java deleted file mode 100644 index a861553..0000000 --- a/src/main/java/org/apache/sanselan/formats/psd/datareaders/UncompressedDataReader.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.psd.datareaders; - -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.mylzw.BitsToByteInputStream; -import org.apache.sanselan.common.mylzw.MyBitInputStream; -import org.apache.sanselan.formats.psd.ImageContents; -import org.apache.sanselan.formats.psd.PSDHeaderInfo; -import org.apache.sanselan.formats.psd.dataparsers.DataParser; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class UncompressedDataReader extends DataReader -{ - public UncompressedDataReader(DataParser fDataParser) - { - super(fDataParser); - } - - public void readData(InputStream is, BufferedImage bi, - ImageContents imageContents, BinaryFileParser bfp) - throws ImageReadException, IOException - { - PSDHeaderInfo header = imageContents.header; - int width = header.Columns; - int height = header.Rows; - - bfp.setDebug(false); - - int channel_count = dataParser.getBasicChannelsCount(); - int depth = header.Depth; - MyBitInputStream mbis = new MyBitInputStream(is, BYTE_ORDER_MSB); - BitsToByteInputStream bbis = new BitsToByteInputStream(mbis, 8); // we want all samples to be bytes - - int data[][][] = new int[channel_count][height][width]; - for (int channel = 0; channel < channel_count; channel++) - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int b = bbis.readBits(depth); - - data[channel][y][x] = (byte) b; - } - - dataParser.parseData(data, bi, imageContents); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tga/TgaImageParser.java b/src/main/java/org/apache/sanselan/formats/tga/TgaImageParser.java deleted file mode 100644 index 97d6c6a..0000000 --- a/src/main/java/org/apache/sanselan/formats/tga/TgaImageParser.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tga; - -/*import java.nawt.Dimension; -import java.nawt.image.BufferedImage;*/ -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.util.Map; -import java.util.ArrayList; - -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - -/* - * This class is just a placeholder. TGA format is not yet supported. - */ -public class TgaImageParser extends ImageParser implements TgaConstants -{ - public TgaImageParser() - { - this.setByteOrder(BYTE_ORDER_INTEL); - setDebug(true); - } - - public String getName() - { - return "Tga"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".tga"; - - private static final String ACCEPTED_EXTENSIONS[] = { - ".tga", ".tpic", - }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[]{ - ImageFormat.IMAGE_FORMAT_TGA, // - }; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - } - - public byte[] getICCProfileBytes(ByteSource byteSource) - throws ImageReadException, IOException - { - return null; - } - - private static final int TGA_FILE_HEADER_LENGTH = 18; - - public Dimension getImageSize(ByteSource byteSource) - throws ImageReadException, IOException - { -// int length = (int) byteSource.getLength(); -// if (length < TGA_FILE_HEADER_LENGTH) -// return null; - - InputStream is = byteSource.getInputStream(); - - is.skip(12); - - int width = this.read2Bytes("image width", is, "image width"); - int height = this.read2Bytes("image height", is, "image height"); - - return new Dimension(width, height); - } - - private static final int TGA_FILE_FOOTER_LENGTH = 26; - private static final String TGA_FILE_FOOTER_SIGNATURE = "TRUEVISION-XFILE"; - - private final boolean isNewTGAFormat(ByteSource byteSource) - throws ImageReadException, IOException - { - int length = (int) byteSource.getLength(); - if (length < TGA_FILE_FOOTER_LENGTH) - return true; - - InputStream is = byteSource.getInputStream(length - - TGA_FILE_FOOTER_LENGTH); - - byte bytes[] = this.readByteArray("tga_file_footer", - TGA_FILE_FOOTER_LENGTH, is, "tga_file_footer"); - - Debug.debug("bytes", bytes); - - Debug.debug("kTGA_FILE_FOOTER_SIGNATURE", TGA_FILE_FOOTER_SIGNATURE); - Debug.debug("kTGA_FILE_FOOTER_SIGNATURE", TGA_FILE_FOOTER_SIGNATURE - .length()); - - return this.compareByteArrays(bytes, 8, TGA_FILE_FOOTER_SIGNATURE - .getBytes(), 0, TGA_FILE_FOOTER_SIGNATURE.length()); - } - - private static final int TGA_IMAGE_TYPE_NO_IMAGE = 0; - private static final int UNCOMPRESSED_COLOR_MAPPED = 1; - private static final int UNCOMPRESSED_RGB = 2; - private static final int UNCOMPRESSED_BLACK_AND_WHITE = 3; - private static final int COMPRESSED_COLOR_MAPPED_RLE = 9; - private static final int COMPRESSED_RGB_RLE = 10; - private static final int COMPRESSED_BLACK_AND_WHITE = 11; - private static final int COMPRESSED_COLOR_MAPPED_DATA_HUFFMAN_DELTA_RLE = 32; - private static final int COMPRESSED_COLOR_MAPPED_DATA_RLE = 33; - -// @Override -// public final ImageInfo getImageInfo(ByteSource byteSource) -// throws ImageReadException, IOException -// { -//// int length = (int) byteSource.getLength(); -//// if (length < TGA_FILE_HEADER_LENGTH) -//// return null; -// -// InputStream is = byteSource.getInputStream(); -// -// int id_string_length = this.readByte("id_string_length", is, -// "id_string_length"); -// int color_map_type = this.readByte("color_map_type", is, -// "color_map_type"); -// int image_type = this.readByte("image_type", is, "image_type"); -// -// int color_map_first_entry_index = this.read2Bytes( -// "color_map_first_entry_index", is, -// "color_map_first_entry_index"); -// int color_map_length = this.read2Bytes("color_map_length", is, -// "color_map_length"); -// int color_map_entry_size = this.readByte("color_map_entry_size", is, -// "color_map_entry_size"); -// -// int origin_x = this.read2Bytes("origin_x", is, "origin_x"); -// int origin_y = this.read2Bytes("origin_y", is, "origin_y"); -// -// int width = this.read2Bytes("image width", is, "image width"); -// int height = this.read2Bytes("image height", is, "image height"); -// -// int pixel_depth = this.readByte("pixel_depth", is, "pixel_depth"); -// int image_descriptor = this.readByte("image_descriptor", is, -// "image_descriptor"); -// // charles -// -// switch (image_type) -// { -// case UNCOMPRESSED_COLOR_MAPPED : -// break; -// case UNCOMPRESSED_RGB : -// break; -// case UNCOMPRESSED_BLACK_AND_WHITE : -// break; -// case COMPRESSED_COLOR_MAPPED_RLE : -// break; -// case COMPRESSED_RGB_RLE : -// break; -// case COMPRESSED_BLACK_AND_WHITE : -// break; -// case COMPRESSED_COLOR_MAPPED_DATA_HUFFMAN_DELTA_RLE : -// break; -// case COMPRESSED_COLOR_MAPPED_DATA_RLE : -// break; -// -// default : -// -// } -// String FormatDetails; -// int BitsPerPixel; -// ArrayList Comments; -// ImageFormat Format = ImageFormat.IMAGE_FORMAT_TGA; -// String FormatName = Format.name; -// String MimeType = "image/tga"; -// int NumberOfImages = 1; // charles could have thumbnail(s). -// int PhysicalHeightDpi; -// float PhysicalHeightInch; -// int PhysicalWidthDpi; -// float PhysicalWidthInch; -// boolean isProgressive = false; -// boolean isTransparent = pixel_depth > 24; -// boolean usesPalette; -// int ColorType; -// -// return null; -//// return new ImageInfo(FormatDetails, BitsPerPixel, Comments, Format, -//// FormatName, height, MimeType, NumberOfImages, -//// PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi, -//// PhysicalWidthInch, width, isProgressive, isTransparent, -//// usesPalette, ColorType); -// -// // boolean is_new_tga_format = isNewTGAFormat(byteSource); -// // -// // Debug.debug("is_new_tga_format", is_new_tga_format); -// } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - return false; - } - -/* public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - return null; - }*/ - - // public void writeImage(BufferedImage src, OutputStream os, Map params) - // throws ImageWriteException, IOException - // { - // return false; - // } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - @Override - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException { - // TODO Auto-generated method stub - return null; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffDirectory.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffDirectory.java deleted file mode 100644 index 942ac05..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffDirectory.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Map; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class TiffDirectory extends TiffElement implements TiffConstants -//extends BinaryFileFunctions -{ - - public String description() - { - return TiffDirectory.description(type); - } - - public String getElementDescription(boolean verbose) - { - if (!verbose) - return "TIFF Directory (" + description() + ")"; - - int entryOffset = offset + TIFF_DIRECTORY_HEADER_LENGTH; - - StringBuffer result = new StringBuffer(); - for (int i = 0; i < entries.size(); i++) - { - TiffField entry = (TiffField) entries.get(i); - - result.append("\t"); - result.append("[" + entryOffset + "]: "); - result.append(entry.tagInfo.name); - result.append(" (" + entry.tag + ", 0x" - + Integer.toHexString(entry.tag) + ")"); - result.append(", " + entry.fieldType.name); - result.append(", " + entry.fieldType.getRawBytes(entry).length); - result.append(": " + entry.getValueDescription()); - - result.append("\n"); - - entryOffset += TIFF_ENTRY_LENGTH; - // entry.fillInValue(byteSource); - } - return result.toString(); - } - - public static final String description(int type) - { - switch (type) - { - case DIRECTORY_TYPE_UNKNOWN : - return "Unknown"; - case DIRECTORY_TYPE_ROOT : - return "Root"; - case DIRECTORY_TYPE_SUB : - return "Sub"; - case DIRECTORY_TYPE_THUMBNAIL : - return "Thumbnail"; - case DIRECTORY_TYPE_EXIF : - return "Exif"; - case DIRECTORY_TYPE_GPS : - return "Gps"; - case DIRECTORY_TYPE_INTEROPERABILITY : - return "Interoperability"; - default : - return "Bad Type"; - } - } - - public final int type; - public final ArrayList entries; - // public final int offset; - public final int nextDirectoryOffset; - - public TiffDirectory(int type, ArrayList entries, final int offset, - int nextDirectoryOffset) - { - super(offset, TIFF_DIRECTORY_HEADER_LENGTH + entries.size() - * TIFF_ENTRY_LENGTH + TIFF_DIRECTORY_FOOTER_LENGTH); - - this.type = type; - this.entries = entries; - this.nextDirectoryOffset = nextDirectoryOffset; - } - - public ArrayList getDirectoryEntrys() - { - return new ArrayList(entries); - } - - protected void fillInValues(ByteSource byteSource) - throws ImageReadException, IOException - { - for (int i = 0; i < entries.size(); i++) - { - TiffField entry = (TiffField) entries.get(i); - - entry.fillInValue(byteSource); - } - } - - public void dump() - { - for (int i = 0; i < entries.size(); i++) - { - TiffField entry = (TiffField) entries.get(i); - entry.dump(); - } - - } - - public boolean hasJpegImageData() throws ImageReadException - { - if (null != findField(TIFF_TAG_JPEG_INTERCHANGE_FORMAT)) - return true; - - return false; - } - - public boolean hasTiffImageData() throws ImageReadException - { - if (null != findField(TIFF_TAG_TILE_OFFSETS)) - return true; - - if (null != findField(TIFF_TAG_STRIP_OFFSETS)) - return true; - - return false; - } - - public BufferedImage getTiffImage() throws ImageReadException, IOException - { - Map params = null; - return getTiffImage(params); - } - - public BufferedImage getTiffImage(Map params) throws ImageReadException, - IOException - { - if (null == tiffImageData) - return null; - - return new TiffImageParser().getBufferedImage(this, params); - } - - public TiffField findField(TagInfo tag) throws ImageReadException - { - boolean failIfMissing = false; - return findField(tag, failIfMissing); - } - - public TiffField findField(TagInfo tag, boolean failIfMissing) - throws ImageReadException - { - if (entries == null) - return null; - - for (int i = 0; i < entries.size(); i++) - { - TiffField field = (TiffField) entries.get(i); - if (field.tag == tag.tag) - return field; - } - - if (failIfMissing) - throw new ImageReadException("Missing expected field: " - + tag.getDescription()); - - return null; - } - - public final class ImageDataElement extends TiffElement - { - public ImageDataElement(int offset, int length) - { - super(offset, length); - } - - public String getElementDescription(boolean verbose) - { - if (verbose) - return null; - return "ImageDataElement"; - } - } - - private ArrayList getRawImageDataElements(TiffField offsetsField, - TiffField byteCountsField) throws ImageReadException - { - int offsets[] = offsetsField.getIntArrayValue(); - int byteCounts[] = byteCountsField.getIntArrayValue(); - - if (offsets.length != byteCounts.length) - throw new ImageReadException("offsets.length(" + offsets.length - + ") != byteCounts.length(" + byteCounts.length + ")"); - - ArrayList result = new ArrayList(); - for (int i = 0; i < offsets.length; i++) - { - result.add(new ImageDataElement(offsets[i], byteCounts[i])); - } - return result; - } - - public ArrayList getTiffRawImageDataElements() throws ImageReadException - { - TiffField tileOffsets = findField(TIFF_TAG_TILE_OFFSETS); - TiffField tileByteCounts = findField(TIFF_TAG_TILE_BYTE_COUNTS); - TiffField stripOffsets = findField(TIFF_TAG_STRIP_OFFSETS); - TiffField stripByteCounts = findField(TIFF_TAG_STRIP_BYTE_COUNTS); - - if ((tileOffsets != null) && (tileByteCounts != null)) - { - return getRawImageDataElements(tileOffsets, tileByteCounts); - } - else if ((stripOffsets != null) && (stripByteCounts != null)) - { - return getRawImageDataElements(stripOffsets, stripByteCounts); - } - else - throw new ImageReadException("Couldn't find image data."); - } - - public boolean imageDataInStrips() throws ImageReadException - { - TiffField tileOffsets = findField(TIFF_TAG_TILE_OFFSETS); - TiffField tileByteCounts = findField(TIFF_TAG_TILE_BYTE_COUNTS); - TiffField stripOffsets = findField(TIFF_TAG_STRIP_OFFSETS); - TiffField stripByteCounts = findField(TIFF_TAG_STRIP_BYTE_COUNTS); - - if ((tileOffsets != null) && (tileByteCounts != null)) - return false; - else if ((stripOffsets != null) && (stripByteCounts != null)) - return true; - else if ((stripOffsets != null) && (stripByteCounts != null)) - return true; - else - throw new ImageReadException("Couldn't find image data."); - } - - public ImageDataElement getJpegRawImageDataElement() - throws ImageReadException - { - TiffField jpegInterchangeFormat = findField(TIFF_TAG_JPEG_INTERCHANGE_FORMAT); - TiffField jpegInterchangeFormatLength = findField(TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - - if ((jpegInterchangeFormat != null) - && (jpegInterchangeFormatLength != null)) - { - int offset = jpegInterchangeFormat.getIntArrayValue()[0]; - int byteCount = jpegInterchangeFormatLength.getIntArrayValue()[0]; - - return new ImageDataElement(offset, byteCount); - } - else - throw new ImageReadException("Couldn't find image data."); - } - - private TiffImageData tiffImageData = null; - - public void setTiffImageData(TiffImageData rawImageData) - { - this.tiffImageData = rawImageData; - } - - public TiffImageData getTiffImageData() - { - return tiffImageData; - } - - private JpegImageData jpegImageData = null; - - public void setJpegImageData(JpegImageData value) - { - this.jpegImageData = value; - } - - public JpegImageData getJpegImageData() - { - return jpegImageData; - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffField.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffField.java deleted file mode 100644 index 37c6fd4..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffField.java +++ /dev/null @@ -1,796 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.io.IOException; -import java.io.PrintWriter; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; - -public class TiffField implements TiffConstants -{ - public final TagInfo tagInfo; - public final FieldType fieldType; - - public final int tag; - public final int directoryType; - public final int type; - public final int length; - public final int valueOffset; - public final byte valueOffsetBytes[]; - - public byte oversizeValue[] = null; - public final int byteOrder; - - public TiffField(int tag, int directoryType, int type, int Length, - int ValueOffset, byte ValueOffsetBytes[], int byteOrder) - { - - this.tag = tag; - this.directoryType = directoryType; - this.type = type; - this.length = Length; - this.valueOffset = ValueOffset; - this.valueOffsetBytes = ValueOffsetBytes; - this.byteOrder = byteOrder; - - fieldType = getFieldType(type); - tagInfo = getTag(directoryType, tag); - } - - private int sortHint = -1; - - public boolean isLocalValue() - { - return fieldType.isLocalValue(this); - } - - public int getBytesLength() throws ImageReadException - { - return fieldType.getBytesLength(this); - } - - public final class OversizeValueElement extends TiffElement - { - public OversizeValueElement(int offset, int length) - { - super(offset, length); - } - - public String getElementDescription(boolean verbose) - { - if (verbose) - return null; - - return "OversizeValueElement, tag: " + tagInfo.name - + ", fieldType: " + fieldType.name; - } - } - - public TiffElement getOversizeValueElement() - { - if (fieldType.isLocalValue(this)) - return null; - - return new OversizeValueElement(valueOffset, oversizeValue.length); - } - - public void setOversizeValue(byte bytes[]) - { - this.oversizeValue = bytes; - } - - private static FieldType getFieldType(int value) - { - for (int i = 0; i < FIELD_TYPES.length; i++) - { - FieldType fieldType = FIELD_TYPES[i]; - if (fieldType.type == value) - return fieldType; - } - - return FIELD_TYPE_UNKNOWN; - } - - private static TagInfo getTag(int directoryType, int tag, - List possibleMatches) - { - if (possibleMatches.size() < 1) - return null; - // else if (possibleMatches.size() == 1) - // { - // TagInfo tagInfo = (TagInfo) possibleMatches.get(0); - // return tagInfo; - // } - - // first search for exact match. - for (int i = 0; i < possibleMatches.size(); i++) - { - TagInfo tagInfo = (TagInfo) possibleMatches.get(i); - if (tagInfo.directoryType == EXIF_DIRECTORY_UNKNOWN) - // pass - continue; - else if (directoryType == DIRECTORY_TYPE_EXIF - && tagInfo.directoryType == EXIF_DIRECTORY_EXIF_IFD) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_INTEROPERABILITY - && tagInfo.directoryType == EXIF_DIRECTORY_INTEROP_IFD) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_GPS - && tagInfo.directoryType == EXIF_DIRECTORY_GPS) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_MAKER_NOTES - && tagInfo.directoryType == EXIF_DIRECTORY_MAKER_NOTES) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_DIR_0 - && tagInfo.directoryType == TIFF_DIRECTORY_IFD0) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_DIR_1 - && tagInfo.directoryType == TIFF_DIRECTORY_IFD1) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_DIR_2 - && tagInfo.directoryType == TIFF_DIRECTORY_IFD2) - return tagInfo; - else if (directoryType == DIRECTORY_TYPE_DIR_3 - && tagInfo.directoryType == TIFF_DIRECTORY_IFD3) - return tagInfo; - } - - // accept an inexact match. - for (int i = 0; i < possibleMatches.size(); i++) - { - TagInfo tagInfo = (TagInfo) possibleMatches.get(i); - - if (tagInfo.directoryType == EXIF_DIRECTORY_UNKNOWN) - // pass - continue; - else if (directoryType >= 0 - && tagInfo.directoryType.isImageDirectory()) - return tagInfo; - else if (directoryType < 0 - && !tagInfo.directoryType.isImageDirectory()) - return tagInfo; - } - - // accept a wildcard match. - for (int i = 0; i < possibleMatches.size(); i++) - { - TagInfo tagInfo = (TagInfo) possibleMatches.get(i); - - if (tagInfo.directoryType == EXIF_DIRECTORY_UNKNOWN) - return tagInfo; - } - - // // accept a very rough match. - // for (int i = 0; i < possibleMatches.size(); i++) - // { - // TagInfo tagInfo = (TagInfo) possibleMatches.get(i); - // if (tagInfo.exifDirectory == EXIF_DIRECTORY_UNKNOWN) - // return tagInfo; - // else if (directoryType == DIRECTORY_TYPE_EXIF - // && tagInfo.exifDirectory == EXIF_DIRECTORY_EXIF_IFD) - // return tagInfo; - // else if (directoryType == DIRECTORY_TYPE_INTEROPERABILITY - // && tagInfo.exifDirectory == EXIF_DIRECTORY_INTEROP_IFD) - // return tagInfo; - // else if (directoryType == DIRECTORY_TYPE_GPS - // && tagInfo.exifDirectory == EXIF_DIRECTORY_GPS) - // return tagInfo; - // else if (directoryType == DIRECTORY_TYPE_MAKER_NOTES - // && tagInfo.exifDirectory == EXIF_DIRECTORY_MAKER_NOTES) - // return tagInfo; - // else if (directoryType >= 0 - // && tagInfo.exifDirectory.isImageDirectory()) - // return tagInfo; - // else if (directoryType < 0 - // && !tagInfo.exifDirectory.isImageDirectory()) - // return tagInfo; - // } - - return TIFF_TAG_UNKNOWN; - - // if (true) - // throw new Error("Why didn't this algorithm work?"); - // - // { - // TagInfo tagInfo = (TagInfo) possibleMatches.get(0); - // return tagInfo; - // } - - // Object key = new Integer(tag); - // - // if (directoryType == DIRECTORY_TYPE_EXIF - // || directoryType == DIRECTORY_TYPE_INTEROPERABILITY) - // { - // if (EXIF_TAG_MAP.containsKey(key)) - // return (TagInfo) EXIF_TAG_MAP.get(key); - // } - // else if (directoryType == DIRECTORY_TYPE_GPS) - // { - // if (GPS_TAG_MAP.containsKey(key)) - // return (TagInfo) GPS_TAG_MAP.get(key); - // } - // else - // { - // if (TIFF_TAG_MAP.containsKey(key)) - // return (TagInfo) TIFF_TAG_MAP.get(key); - // } - // - // if (ALL_TAG_MAP.containsKey(key)) - // return (TagInfo) ALL_TAG_MAP.get(key); - - // public static final int DIRECTORY_TYPE_EXIF = -2; - // // public static final int DIRECTORY_TYPE_SUB = 5; - // public static final int DIRECTORY_TYPE_GPS = -3; - // public static final int DIRECTORY_TYPE_INTEROPERABILITY = -4; - // - // private static final Map GPS_TAG_MAP = makeTagMap(ALL_GPS_TAGS, - // false); - // private static final Map TIFF_TAG_MAP = makeTagMap(ALL_TIFF_TAGS, - // false); - // private static final Map EXIF_TAG_MAP = makeTagMap(ALL_EXIF_TAGS, - // false); - // private static final Map ALL_TAG_MAP = makeTagMap(ALL_TAGS, true); - // - // for (int i = 0; i < ALL_TAGS.length; i++) - // { - // TagInfo2 tag = ALL_TAGS[i]; - // if (tag.tag == value) - // return tag; - // } - - // return TIFF_TAG_UNKNOWN; - } - - private static TagInfo getTag(int directoryType, int tag) - { - Object key = new Integer(tag); - - List possibleMatches = (List) EXIF_TAG_MAP.get(key); - - if (null == possibleMatches) - { - return TIFF_TAG_UNKNOWN; - } - - TagInfo result = getTag(directoryType, tag, possibleMatches); - return result; - } - - private int getValueLengthInBytes() - { - int unit_length = fieldType.length; - int valueLength = unit_length * length; - - // Debug.debug("getValueLengthInBytes unit_length", unit_length); - // Debug.debug("getValueLengthInBytes length", length); - - return valueLength; - } - - public void fillInValue(ByteSource byteSource) throws ImageReadException, - IOException - { - if (fieldType.isLocalValue(this)) - return; - - int valueLength = getValueLengthInBytes(); - - // Debug.debug("fillInValue tag", tag); - // Debug.debug("fillInValue tagInfo", tagInfo); - // Debug.debug("fillInValue valueOffset", valueOffset); - // Debug.debug("fillInValue valueLength", valueLength); - - byte bytes[] = byteSource.getBlock(valueOffset, valueLength); - setOversizeValue(bytes); - } - - public String getValueDescription() - { - try - { - return getValueDescription(getValue()); - } catch (ImageReadException e) - { - return "Invalid value: " + e.getMessage(); - } - } - - private String getValueDescription(Object o) - { - if (o == null) - return null; - - if (o instanceof Number) - { - return o.toString(); - } else if (o instanceof String) - { - return "'" + o.toString().trim() + "'"; - } else if (o instanceof Date) - { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - return df.format((Date) o); - } else if (o instanceof Object[]) - { - Object objects[] = (Object[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < objects.length; i++) - { - Object object = objects[i]; - - if (i > 50) - { - result.append("... (" + objects.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + object); - } - return result.toString(); - } - // else if (o instanceof Number[]) - // { - // Number numbers[] = (Number[]) o; - // StringBuffer result = new StringBuffer(); - // - // for (int i = 0; i < numbers.length; i++) - // { - // Number number = numbers[i]; - // - // if (i > 0) - // result.append(", "); - // result.append("" + number); - // } - // return result.toString(); - // } - else if (o instanceof int[]) - { - int values[] = (int[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - int value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } else if (o instanceof long[]) - { - long values[] = (long[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - long value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } else if (o instanceof double[]) - { - double values[] = (double[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - double value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } else if (o instanceof byte[]) - { - byte values[] = (byte[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - byte value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } else if (o instanceof char[]) - { - char values[] = (char[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - char value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } else if (o instanceof float[]) - { - float values[] = (float[]) o; - StringBuffer result = new StringBuffer(); - - for (int i = 0; i < values.length; i++) - { - float value = values[i]; - - if (i > 50) - { - result.append("... (" + values.length + ")"); - break; - } - if (i > 0) - result.append(", "); - result.append("" + value); - } - return result.toString(); - } - // else if (o instanceof short[]) - // { - // short numbers[] = (short[]) o; - // StringBuffer result = new StringBuffer(); - // - // for (int i = 0; i < numbers.length; i++) - // { - // short number = numbers[i]; - // - // if (i > 0) - // result.append(", "); - // result.append("" + number); - // } - // return result.toString(); - // } - - return "Unknown: " + o.getClass().getName(); - } - - public void dump() - { - PrintWriter pw = new PrintWriter(System.out); - dump(pw); - pw.flush(); - } - - public void dump(PrintWriter pw) - { - dump(pw, null); - } - - public void dump(PrintWriter pw, String prefix) - { - if (prefix != null) - pw.print(prefix + ": "); - - pw.println(toString()); - pw.flush(); - } - - // private void errorDump() - // { - // Debug.debug("tagInfo", tagInfo); - // Debug.debug("fieldType", fieldType); - // Debug.debug("tag", tag); - // Debug.debug("type", type); - // Debug.debug("length", length); - // Debug.debug("valueOffset", valueOffset); - // Debug.debug("valueOffsetBytes", valueOffsetBytes); - // Debug.debug("oversizeValue", oversizeValue); - // Debug.debug("byteOrder", byteOrder); - // } - - public String getDescriptionWithoutValue() - { - return tag + " (0x" + Integer.toHexString(tag) + ": " + tagInfo.name - + "): "; - } - - public String toString() - { - StringBuffer result = new StringBuffer(); - - result.append(tag + " (0x" + Integer.toHexString(tag) + ": " - + tagInfo.name + "): "); - result.append(getValueDescription() + " (" + length + " " - + fieldType.name + ")"); - - return result.toString(); - } - - public String getTagName() - { - if (tagInfo == TIFF_TAG_UNKNOWN) - return tagInfo.name + " (0x" + Integer.toHexString(tag) + ")"; - return tagInfo.name; - } - - public String getFieldTypeName() - { - return fieldType.name; - } - - public static final String Attribute_Tag = "Tag"; - - public Object getValue() throws ImageReadException - { - // System.out.print("getValue"); - return tagInfo.getValue(this); - } - - public String getStringValue() throws ImageReadException - { - Object o = getValue(); - if (o == null) - return null; - if (!(o instanceof String)) - throw new ImageReadException("Expected String value(" - + tagInfo.getDescription() + "): " + o); - return (String) o; - } - - private static final Map makeTagMap(TagInfo tags[], - boolean ignoreDuplicates, String name) - { - // make sure to use the thread-safe version; this is shared state. - Map map = new Hashtable(); - - for (int i = 0; i < tags.length; i++) - { - TagInfo tag = tags[i]; - Object key = new Integer(tag.tag); - - List tagList = (List) map.get(key); - if (tagList == null) - { - tagList = new ArrayList(); - map.put(key, tagList); - } - tagList.add(tag); - - // if (map.get(key) == null) - // map.put(key, tag); - // else if (!ignoreDuplicates) - // { - // System.out.println("Duplicate tag in " + name + ": " + tag.tag - // + " (0x" + Integer.toHexString(tag.tag) + ")"); - // System.out.println("\t" + "New name: " + tag.name); - // System.out.println("\t" + "Old name: " - // + ((TagInfo) map.get(key)).name); - // } - } - - return map; - } - - private static final Map GPS_TAG_MAP = makeTagMap(ALL_GPS_TAGS, false, - "GPS"); - private static final Map TIFF_TAG_MAP = makeTagMap(ALL_TIFF_TAGS, false, - "TIFF"); - private static final Map EXIF_TAG_MAP = makeTagMap(ALL_EXIF_TAGS, true, - "EXIF"); - private static final Map ALL_TAG_MAP = makeTagMap(ALL_TAGS, true, "All"); - - // static - // { - // Map map = new HashMap(); - // - // for (int i = 0; i < ALL_TAGS.length; i++) - // { - // TagInfo2 tag = ALL_TAGS[i]; - // Object o = map.get("" + tag.tag); - // if (o == null) - // map.put("" + tag.tag, tag); - // else - // { - // System.out.println("Duplicate tag: " + tag.tag); - // System.out.println("\t" + "New name: " + tag.name); - // System.out.println("\t" + "Old name: " + ((TagInfo2) o).name); - // } - // } - // - // } - - // public static final TagInfo2 ALL_TAGS[] = TagConstantsUtils - // .mergeTagLists(new TagInfo2[][]{ - // ALL_EXIF_TAGS, ALL_TIFF_TAGS, ALL_GPS_TAGS, - // }); - // - // - - public int[] getIntArrayValue() throws ImageReadException - { - Object o = getValue(); - // if (o == null) - // return null; - - if (o instanceof Number) - return new int[] { ((Number) o).intValue() }; - else if (o instanceof Number[]) - { - Number numbers[] = (Number[]) o; - int result[] = new int[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i].intValue(); - return result; - } else if (o instanceof int[]) - { - int numbers[] = (int[]) o; - int result[] = new int[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i]; - return result; - } - - throw new ImageReadException("Unknown value: " + o + " for: " - + tagInfo.getDescription()); - // return null; - } - - public double[] getDoubleArrayValue() throws ImageReadException - { - Object o = getValue(); - // if (o == null) - // return null; - - if (o instanceof Number) - { - return new double[] { ((Number) o).doubleValue() }; - } else if (o instanceof Number[]) - { - Number numbers[] = (Number[]) o; - double result[] = new double[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i].doubleValue(); - return result; - } else if (o instanceof int[]) - { - int numbers[] = (int[]) o; - double result[] = new double[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i]; - return result; - } else if (o instanceof float[]) - { - float numbers[] = (float[]) o; - double result[] = new double[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i]; - return result; - } else if (o instanceof double[]) - { - double numbers[] = (double[]) o; - double result[] = new double[numbers.length]; - for (int i = 0; i < numbers.length; i++) - result[i] = numbers[i]; - return result; - } - - throw new ImageReadException("Unknown value: " + o + " for: " - + tagInfo.getDescription()); - // return null; - } - - public int getIntValueOrArraySum() throws ImageReadException - { - Object o = getValue(); - // if (o == null) - // return -1; - - if (o instanceof Number) - return ((Number) o).intValue(); - else if (o instanceof Number[]) - { - Number numbers[] = (Number[]) o; - int sum = 0; - for (int i = 0; i < numbers.length; i++) - sum += numbers[i].intValue(); - return sum; - } else if (o instanceof int[]) - { - int numbers[] = (int[]) o; - int sum = 0; - for (int i = 0; i < numbers.length; i++) - sum += numbers[i]; - return sum; - } - - throw new ImageReadException("Unknown value: " + o + " for: " - + tagInfo.getDescription()); - // return -1; - } - - public int getIntValue() throws ImageReadException - { - Object o = getValue(); - if (o == null) - throw new ImageReadException("Missing value: " - + tagInfo.getDescription()); - - return ((Number) o).intValue(); - } - - public double getDoubleValue() throws ImageReadException - { - Object o = getValue(); - if (o == null) - throw new ImageReadException("Missing value: " - + tagInfo.getDescription()); - - return ((Number) o).doubleValue(); - } - - public byte[] getByteArrayValue() throws ImageReadException - { - return fieldType.getRawBytes(this); - } - - public int getSortHint() - { - return sortHint; - } - - public void setSortHint(int sortHint) - { - this.sortHint = sortHint; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageData.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffImageData.java deleted file mode 100644 index 076b967..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageData.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.tiff; - -import java.io.IOException; -import java.util.ArrayList; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.formats.tiff.datareaders.DataReader; -import org.apache.sanselan.formats.tiff.datareaders.DataReaderStrips; -import org.apache.sanselan.formats.tiff.datareaders.DataReaderTiled; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; - -public abstract class TiffImageData -{ - public static class Tiles extends TiffImageData - { - public final TiffElement.DataElement tiles[]; - - // public final byte tiles[][]; - private final int tileWidth, tileLength; - - public Tiles(final TiffElement.DataElement tiles[], - final int tileWidth, final int tileLength) - { - this.tiles = tiles; - this.tileWidth = tileWidth; - this.tileLength = tileLength; - } - - public TiffElement.DataElement[] getImageData() - { - return tiles; - } - - public boolean stripsNotTiles() - { - return false; - } - - public DataReader getDataReader(ArrayList entries, - PhotometricInterpreter photometricInterpreter, - int bitsPerPixel, int bitsPerSample[], int predictor, - int samplesPerPixel, int width, int height, int compression) - throws IOException, ImageReadException - { - return new DataReaderTiled(photometricInterpreter, tileWidth, - tileLength, bitsPerPixel, bitsPerSample, predictor, - samplesPerPixel, width, height, compression, this); - } - - // public TiffElement[] getElements() - // { - // return tiles; - // } - } - - public static class Strips extends TiffImageData - { - public final TiffElement.DataElement strips[]; - // public final byte strips[][]; - public final int rowsPerStrip; - - public Strips(final TiffElement.DataElement strips[], - final int rowsPerStrip) - { - this.strips = strips; - this.rowsPerStrip = rowsPerStrip; - } - - public TiffElement.DataElement[] getImageData() - { - return strips; - } - - public boolean stripsNotTiles() - { - return true; - } - - public DataReader getDataReader(ArrayList entries, - PhotometricInterpreter photometricInterpreter, - int bitsPerPixel, int bitsPerSample[], int predictor, - int samplesPerPixel, int width, int height, int compression) - throws IOException, ImageReadException - { - return new DataReaderStrips(photometricInterpreter, bitsPerPixel, - bitsPerSample, predictor, samplesPerPixel, width, height, - compression, rowsPerStrip, this); - } - - // public TiffElement[] getElements() - // { - // return strips; - // } - - } - - // public abstract TiffElement[] getElements(); - - public abstract TiffElement.DataElement[] getImageData(); - - public abstract boolean stripsNotTiles(); - - public abstract DataReader getDataReader(ArrayList entries, - PhotometricInterpreter photometricInterpreter, int bitsPerPixel, - int bitsPerSample[], int predictor, int samplesPerPixel, int width, - int height, int compression) throws IOException, ImageReadException; - - public static class Data extends TiffElement.DataElement - { - public Data(int offset, int length, final byte data[]) - { - super(offset, length, data); - } - - public String getElementDescription(boolean verbose) - { - return "Tiff image data: " + data.length + " bytes"; - } - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageMetadata.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffImageMetadata.java deleted file mode 100644 index 814fb24..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageMetadata.java +++ /dev/null @@ -1,398 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.ImageMetadata; -import org.apache.sanselan.common.RationalNumber; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.constants.TiffDirectoryConstants; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; -import org.apache.sanselan.formats.tiff.write.TiffOutputDirectory; -import org.apache.sanselan.formats.tiff.write.TiffOutputField; -import org.apache.sanselan.formats.tiff.write.TiffOutputSet; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class TiffImageMetadata extends ImageMetadata - implements - TiffDirectoryConstants -{ - public final TiffContents contents; - - public TiffImageMetadata(final TiffContents contents) - { - this.contents = contents; - } - - public static class Directory extends ImageMetadata - implements - ImageMetadata.IImageMetadataItem - { - // private BufferedImage thumbnail = null; - - public final int type; - - private final TiffDirectory directory; - - public Directory(final TiffDirectory directory) - { - this.type = directory.type; - this.directory = directory; - } - - public void add(TiffField entry) - { - add(new TiffImageMetadata.Item(entry)); - } - - public BufferedImage getThumbnail() throws ImageReadException, - IOException - { - return directory.getTiffImage(); - } - - public TiffImageData getTiffImageData() - { - return directory.getTiffImageData(); - } - - public TiffField findField(TagInfo tagInfo) throws ImageReadException - { - return directory.findField(tagInfo); - } - - public List getAllFields() throws ImageReadException - { - return directory.getDirectoryEntrys(); - } - - public JpegImageData getJpegImageData() - { - return directory.getJpegImageData(); - } - - public String toString(String prefix) - { - return (prefix != null ? prefix : "") + directory.description() - + ": " // - + (getTiffImageData() != null ? " (tiffImageData)" : "") // - + (getJpegImageData() != null ? " (jpegImageData)" : "") // - + "\n" + super.toString(prefix) + "\n"; - } - - public TiffOutputDirectory getOutputDirectory(int byteOrder) - throws ImageWriteException - { - try - { - TiffOutputDirectory dstDir = new TiffOutputDirectory(type); - - ArrayList entries = getItems(); - for (int i = 0; i < entries.size(); i++) - { - TiffImageMetadata.Item item = (TiffImageMetadata.Item) entries - .get(i); - TiffField srcField = item.getTiffField(); - - if (null != dstDir.findField(srcField.tag)) - { - // ignore duplicate tags in a directory. - continue; - } - else if (srcField.tagInfo instanceof TagInfo.Offset) - { - // ignore offset fields. - continue; - } - - TagInfo tagInfo = srcField.tagInfo; - FieldType fieldType = srcField.fieldType; - int count = srcField.length; - // byte bytes[] = srcField.fieldType.getRawBytes(srcField); - - // Debug.debug("tagInfo", tagInfo); - - Object value = srcField.getValue(); - - // Debug.debug("value", Debug.getType(value)); - - byte bytes[] = tagInfo.encodeValue(fieldType, value, - byteOrder); - - // if (tagInfo.isUnknown()) - // Debug.debug( - // "\t" + "unknown tag(0x" - // + Integer.toHexString(srcField.tag) - // + ") bytes", bytes); - - TiffOutputField dstField = new TiffOutputField( - srcField.tag, tagInfo, fieldType, count, bytes); - dstField.setSortHint(srcField.getSortHint()); - dstDir.add(dstField); - } - - dstDir.setTiffImageData(getTiffImageData()); - dstDir.setJpegImageData(getJpegImageData()); - - return dstDir; - } - catch (ImageReadException e) - { - throw new ImageWriteException(e.getMessage(), e); - } - } - - } - - public ArrayList getDirectories() - { - return super.getItems(); - } - - public ArrayList getItems() - { - ArrayList result = new ArrayList(); - - ArrayList items = super.getItems(); - for (int i = 0; i < items.size(); i++) - { - Directory dir = (Directory) items.get(i); - result.addAll(dir.getItems()); - } - - return result; - } - - public static class Item extends ImageMetadata.Item - { - private final TiffField entry; - - public Item(TiffField entry) - { - // super(entry.getTagName() + " (" + entry.getFieldTypeName() + ")", - super(entry.getTagName(), entry.getValueDescription()); - this.entry = entry; - } - - public TiffField getTiffField() - { - return entry; - } - - } - - public TiffOutputSet getOutputSet() throws ImageWriteException - { - int byteOrder = contents.header.byteOrder; - TiffOutputSet result = new TiffOutputSet(byteOrder); - - ArrayList srcDirs = getDirectories(); - for (int i = 0; i < srcDirs.size(); i++) - { - TiffImageMetadata.Directory srcDir = (TiffImageMetadata.Directory) srcDirs - .get(i); - - if (null != result.findDirectory(srcDir.type)) - { - // Certain cameras right directories more than once. - // This is a bug. - // Ignore second directory of a given type. - continue; - } - - TiffOutputDirectory outputDirectory = srcDir - .getOutputDirectory(byteOrder); - result.addDirectory(outputDirectory); - } - - return result; - } - - public TiffField findField(TagInfo tagInfo) throws ImageReadException - { - ArrayList directories = getDirectories(); - for (int i = 0; i < directories.size(); i++) - { - Directory directory = (Directory) directories.get(i); - TiffField field = directory.findField(tagInfo); - if (null != field) - return field; - } - return null; - } - - public TiffDirectory findDirectory(int directoryType) - { - ArrayList directories = getDirectories(); - for (int i = 0; i < directories.size(); i++) - { - Directory directory = (Directory) directories.get(i); - if (directory.type == directoryType) - return directory.directory; - } - return null; - } - - public List getAllFields() throws ImageReadException - { - List result = new ArrayList(); - ArrayList directories = getDirectories(); - for (int i = 0; i < directories.size(); i++) - { - Directory directory = (Directory) directories.get(i); - result.addAll(directory.getAllFields()); - } - return result; - } - - public GPSInfo getGPS() throws ImageReadException - { - TiffDirectory gpsDirectory = findDirectory(DIRECTORY_TYPE_GPS); - if (null == gpsDirectory) - return null; - - // more specific example of how to access GPS values. - TiffField latitudeRefField = gpsDirectory - .findField(TiffConstants.GPS_TAG_GPS_LATITUDE_REF); - TiffField latitudeField = gpsDirectory - .findField(TiffConstants.GPS_TAG_GPS_LATITUDE); - TiffField longitudeRefField = gpsDirectory - .findField(TiffConstants.GPS_TAG_GPS_LONGITUDE_REF); - TiffField longitudeField = gpsDirectory - .findField(TiffConstants.GPS_TAG_GPS_LONGITUDE); - - if (latitudeRefField == null || latitudeField == null - || longitudeRefField == null || longitudeField == null) - return null; - - // all of these values are strings. - String latitudeRef = latitudeRefField.getStringValue(); - RationalNumber latitude[] = (RationalNumber[]) latitudeField.getValue(); - String longitudeRef = longitudeRefField.getStringValue(); - RationalNumber longitude[] = (RationalNumber[]) longitudeField - .getValue(); - - if (latitude.length != 3 || longitude.length != 3) - throw new ImageReadException( - "Expected three values for latitude and longitude."); - - RationalNumber latitudeDegrees = latitude[0]; - RationalNumber latitudeMinutes = latitude[1]; - RationalNumber latitudeSeconds = latitude[2]; - - RationalNumber longitudeDegrees = longitude[0]; - RationalNumber longitudeMinutes = longitude[1]; - RationalNumber longitudeSeconds = longitude[2]; - - return new GPSInfo(latitudeRef, longitudeRef, latitudeDegrees, - latitudeMinutes, latitudeSeconds, longitudeDegrees, - longitudeMinutes, longitudeSeconds); - } - - public static class GPSInfo - { - public final String latitudeRef; - public final String longitudeRef; - - public final RationalNumber latitudeDegrees; - public final RationalNumber latitudeMinutes; - public final RationalNumber latitudeSeconds; - public final RationalNumber longitudeDegrees; - public final RationalNumber longitudeMinutes; - public final RationalNumber longitudeSeconds; - - public GPSInfo(final String latitudeRef, final String longitudeRef, - final RationalNumber latitudeDegrees, - final RationalNumber latitudeMinutes, - final RationalNumber latitudeSeconds, - final RationalNumber longitudeDegrees, - final RationalNumber longitudeMinutes, - final RationalNumber longitudeSeconds) - { - this.latitudeRef = latitudeRef; - this.longitudeRef = longitudeRef; - this.latitudeDegrees = latitudeDegrees; - this.latitudeMinutes = latitudeMinutes; - this.latitudeSeconds = latitudeSeconds; - this.longitudeDegrees = longitudeDegrees; - this.longitudeMinutes = longitudeMinutes; - this.longitudeSeconds = longitudeSeconds; - } - - public String toString() - { - // This will format the gps info like so: - // - // latitude: 8 degrees, 40 minutes, 42.2 seconds S - // longitude: 115 degrees, 26 minutes, 21.8 seconds E - - StringBuffer result = new StringBuffer(); - result.append("[GPS. "); - result.append("Latitude: " + latitudeDegrees.toDisplayString() - + " degrees, " + latitudeMinutes.toDisplayString() - + " minutes, " + latitudeSeconds.toDisplayString() - + " seconds " + latitudeRef); - result.append(", Longitude: " + longitudeDegrees.toDisplayString() - + " degrees, " + longitudeMinutes.toDisplayString() - + " minutes, " + longitudeSeconds.toDisplayString() - + " seconds " + longitudeRef); - result.append("]"); - - return result.toString(); - } - - public double getLongitudeAsDegreesEast() throws ImageReadException - { - double result = longitudeDegrees.doubleValue() - + (longitudeMinutes.doubleValue() / 60.0) - + (longitudeSeconds.doubleValue() / 3600.0); - - if (longitudeRef.trim().equalsIgnoreCase("e")) - return result; - else if (longitudeRef.trim().equalsIgnoreCase("w")) - return -result; - else - throw new ImageReadException("Unknown longitude ref: \"" - + longitudeRef + "\""); - } - - public double getLatitudeAsDegreesNorth() throws ImageReadException - { - double result = latitudeDegrees.doubleValue() - + (latitudeMinutes.doubleValue() / 60.0) - + (latitudeSeconds.doubleValue() / 3600.0); - - if (latitudeRef.trim().equalsIgnoreCase("n")) - return result; - else if (latitudeRef.trim().equalsIgnoreCase("s")) - return -result; - else - throw new ImageReadException("Unknown latitude ref: \"" - + latitudeRef + "\""); - } - - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageParser.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffImageParser.java deleted file mode 100644 index 8fefdd9..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffImageParser.java +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.FormatCompliance; -import org.apache.sanselan.ImageFormat; -import org.apache.sanselan.ImageInfo; -import org.apache.sanselan.ImageParser; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.datareaders.DataReader; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterCIELAB; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterCMYK; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLUV; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterRGB; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr; -import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossy; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.image.BufferedImage; - - -public class TiffImageParser extends ImageParser implements TiffConstants -{ - public TiffImageParser() - { - // setDebug(true); - } - - public String getName() - { - return "Tiff-Custom"; - } - - public String getDefaultExtension() - { - return DEFAULT_EXTENSION; - } - - private static final String DEFAULT_EXTENSION = ".tif"; - - private static final String ACCEPTED_EXTENSIONS[] = { ".tif", ".tiff", }; - - protected String[] getAcceptedExtensions() - { - return ACCEPTED_EXTENSIONS; - } - - protected ImageFormat[] getAcceptedTypes() - { - return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_TIFF, // - }; - } - - public byte[] getICCProfileBytes(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readFirstDirectory(byteSource, params, false, formatCompliance); - TiffDirectory directory = (TiffDirectory) contents.directories.get(0); - - TiffField field = directory.findField(EXIF_TAG_ICC_PROFILE); - if (null == field) - return null; - return field.oversizeValue; - } - - public Dimension getImageSize(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readFirstDirectory(byteSource, params, false, formatCompliance); - TiffDirectory directory = (TiffDirectory) contents.directories.get(0); - - int width = directory.findField(TIFF_TAG_IMAGE_WIDTH).getIntValue(); - int height = directory.findField(TIFF_TAG_IMAGE_LENGTH).getIntValue(); - - return new Dimension(width, height); - } - - public byte[] embedICCProfile(byte image[], byte profile[]) - { - return null; - } - - public boolean embedICCProfile(File src, File dst, byte profile[]) - { - return false; - } - - public IImageMetadata getMetadata(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)).readContents( - byteSource, params, formatCompliance); - - ArrayList directories = contents.directories; - - TiffImageMetadata result = new TiffImageMetadata(contents); - - for (int i = 0; i < directories.size(); i++) - { - TiffDirectory dir = (TiffDirectory) directories.get(i); - - TiffImageMetadata.Directory metadataDirectory = new TiffImageMetadata.Directory( - dir); - - ArrayList entries = dir.getDirectoryEntrys(); - - for (int j = 0; j < entries.size(); j++) - { - TiffField entry = (TiffField) entries.get(j); - metadataDirectory.add(entry); - } - - result.add(metadataDirectory); - } - - return result; - } - - public ImageInfo getImageInfo(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readDirectories(byteSource, false, formatCompliance); - TiffDirectory directory = (TiffDirectory) contents.directories.get(0); - - TiffField widthField = directory.findField(TIFF_TAG_IMAGE_WIDTH, true); - TiffField heightField = directory - .findField(TIFF_TAG_IMAGE_LENGTH, true); - - if ((widthField == null) || (heightField == null)) - throw new ImageReadException("TIFF image missing size info."); - - int height = heightField.getIntValue(); - int width = widthField.getIntValue(); - - // ------------------- - - TiffField resolutionUnitField = directory - .findField(TIFF_TAG_RESOLUTION_UNIT); - int resolutionUnit = 2; // Inch - if ((resolutionUnitField != null) - && (resolutionUnitField.getValue() != null)) - resolutionUnit = resolutionUnitField.getIntValue(); - - double unitsPerInch = -1; - switch (resolutionUnit) - { - case 1: - break; - case 2: // Inch - unitsPerInch = 1.0; - break; - case 3: // Meter - unitsPerInch = 0.0254; - break; - default: - break; - - } - TiffField xResolutionField = directory.findField(TIFF_TAG_XRESOLUTION); - TiffField yResolutionField = directory.findField(TIFF_TAG_YRESOLUTION); - - int physicalWidthDpi = -1; - float physicalWidthInch = -1; - int physicalHeightDpi = -1; - float physicalHeightInch = -1; - - if (unitsPerInch > 0) - { - if ((xResolutionField != null) - && (xResolutionField.getValue() != null)) - { - double XResolutionPixelsPerUnit = xResolutionField - .getDoubleValue(); - physicalWidthDpi = (int) (XResolutionPixelsPerUnit / unitsPerInch); - physicalWidthInch = (float) (width / (XResolutionPixelsPerUnit * unitsPerInch)); - } - if ((yResolutionField != null) - && (yResolutionField.getValue() != null)) - { - double YResolutionPixelsPerUnit = yResolutionField - .getDoubleValue(); - physicalHeightDpi = (int) (YResolutionPixelsPerUnit / unitsPerInch); - physicalHeightInch = (float) (height / (YResolutionPixelsPerUnit * unitsPerInch)); - } - } - - // ------------------- - - TiffField bitsPerSampleField = directory - .findField(TIFF_TAG_BITS_PER_SAMPLE); - - int bitsPerSample = -1; - if ((bitsPerSampleField != null) - && (bitsPerSampleField.getValue() != null)) - bitsPerSample = bitsPerSampleField.getIntValueOrArraySum(); - - int bitsPerPixel = bitsPerSample; // assume grayscale; - // dunno if this handles colormapped images correctly. - - // ------------------- - - ArrayList comments = new ArrayList(); - ArrayList entries = directory.entries; - for (int i = 0; i < entries.size(); i++) - { - TiffField field = (TiffField) entries.get(i); - String comment = field.toString(); - comments.add(comment); - } - - ImageFormat format = ImageFormat.IMAGE_FORMAT_TIFF; - String formatName = "TIFF Tag-based Image File Format"; - String mimeType = "image/tiff"; - int numberOfImages = contents.directories.size(); - // not accurate ... only reflects first - boolean isProgressive = false; - // is TIFF ever interlaced/progressive? - - String formatDetails = "Tiff v." + contents.header.tiffVersion; - - boolean isTransparent = false; // TODO: wrong - boolean usesPalette = false; - TiffField colorMapField = directory.findField(TIFF_TAG_COLOR_MAP); - if (colorMapField != null) - usesPalette = true; - - int colorType = ImageInfo.COLOR_TYPE_RGB; - - int compression = directory.findField(TIFF_TAG_COMPRESSION) - .getIntValue(); - String compressionAlgorithm; - - switch (compression) - { - case TIFF_COMPRESSION_UNCOMPRESSED_1: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; - break; - case TIFF_COMPRESSION_CCITT_1D: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_1D; - break; - case TIFF_COMPRESSION_CCITT_GROUP_3: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_3; - break; - case TIFF_COMPRESSION_CCITT_GROUP_4: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_4; - break; - case TIFF_COMPRESSION_LZW: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; - break; - case TIFF_COMPRESSION_JPEG: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; - break; - case TIFF_COMPRESSION_UNCOMPRESSED_2: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; - break; - case TIFF_COMPRESSION_PACKBITS: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PACKBITS; - break; - default: - compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN; - break; - } - - ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments, - format, formatName, height, mimeType, numberOfImages, - physicalHeightDpi, physicalHeightInch, physicalWidthDpi, - physicalWidthInch, width, isProgressive, isTransparent, - usesPalette, colorType, compressionAlgorithm); - - return result; - } - - public String getXmpXml(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readDirectories(byteSource, false, formatCompliance); - TiffDirectory directory = (TiffDirectory) contents.directories.get(0); - - TiffField xmpField = directory.findField(TIFF_TAG_XMP, false); - if (xmpField == null) - return null; - - byte bytes[] = xmpField.getByteArrayValue(); - - try - { - // segment data is UTF-8 encoded xml. - String xml = new String(bytes, "utf-8"); - return xml; - } catch (UnsupportedEncodingException e) - { - throw new ImageReadException("Invalid JPEG XMP Segment."); - } - } - - public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) - throws ImageReadException, IOException - { - try - { - pw.println("tiff.dumpImageFile"); - - { - ImageInfo imageData = getImageInfo(byteSource); - if (imageData == null) - return false; - - imageData.toString(pw, ""); - } - - pw.println(""); - - // try - { - FormatCompliance formatCompliance = FormatCompliance - .getDefault(); - Map params = null; - TiffContents contents = new TiffReader(true).readContents( - byteSource, params, formatCompliance); - - ArrayList directories = contents.directories; - - if (directories == null) - return false; - - for (int d = 0; d < directories.size(); d++) - { - TiffDirectory directory = (TiffDirectory) directories - .get(d); - - ArrayList entries = directory.entries; - - if (entries == null) - return false; - - // Debug.debug("directory offset", directory.offset); - - for (int i = 0; i < entries.size(); i++) - { - TiffField field = (TiffField) entries.get(i); - - field.dump(pw, d + ""); - } - } - - pw.println(""); - } - // catch (Exception e) - // { - // Debug.debug(e); - // pw.println(""); - // return false; - // } - - return true; - } finally - { - pw.println(""); - } - } - - public FormatCompliance getFormatCompliance(ByteSource byteSource) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - Map params = null; - new TiffReader(isStrict(params)).readContents(byteSource, params, - formatCompliance); - return formatCompliance; - } - - public List collectRawImageData(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readDirectories(byteSource, true, formatCompliance); - - List result = new ArrayList(); - for (int i = 0; i < contents.directories.size(); i++) - { - TiffDirectory directory = (TiffDirectory) contents.directories - .get(i); - List dataElements = directory.getTiffRawImageDataElements(); - for (int j = 0; j < dataElements.size(); j++) - { - TiffDirectory.ImageDataElement element = (TiffDirectory.ImageDataElement) dataElements - .get(j); - byte bytes[] = byteSource.getBlock(element.offset, - element.length); - result.add(bytes); - } - } - return result; - } - - public BufferedImage getBufferedImage(ByteSource byteSource, Map params) - throws ImageReadException, IOException - { - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(isStrict(params)) - .readFirstDirectory(byteSource, params, true, formatCompliance); - TiffDirectory directory = (TiffDirectory) contents.directories.get(0); - BufferedImage result = directory.getTiffImage(params); - if (null == result) - throw new ImageReadException("TIFF does not contain an image."); - return result; - } - - protected BufferedImage getBufferedImage(TiffDirectory directory, Map params) - throws ImageReadException, IOException - { - ArrayList entries = directory.entries; - - if (entries == null) - throw new ImageReadException("TIFF missing entries"); - - int photometricInterpretation = directory.findField( - TIFF_TAG_PHOTOMETRIC_INTERPRETATION, true).getIntValue(); - int compression = directory.findField(TIFF_TAG_COMPRESSION, true) - .getIntValue(); - int width = directory.findField(TIFF_TAG_IMAGE_WIDTH, true) - .getIntValue(); - int height = directory.findField(TIFF_TAG_IMAGE_LENGTH, true) - .getIntValue(); - int samplesPerPixel = directory.findField(TIFF_TAG_SAMPLES_PER_PIXEL, - true).getIntValue(); - int bitsPerSample[] = directory.findField(TIFF_TAG_BITS_PER_SAMPLE, - true).getIntArrayValue(); - // TODO: why are we using bits per sample twice? because one is a sum. - int bitsPerPixel = directory.findField(TIFF_TAG_BITS_PER_SAMPLE, true) - .getIntValueOrArraySum(); - - // int bitsPerPixel = getTagAsValueOrArraySum(entries, - // TIFF_TAG_BITS_PER_SAMPLE); - - int predictor = -1; - { - // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER); - // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS); - // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS); - // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); - // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); - TiffField predictorField = directory.findField(TIFF_TAG_PREDICTOR); - if (null != predictorField) - predictor = predictorField.getIntValueOrArraySum(); - } - - if (samplesPerPixel != bitsPerSample.length) - throw new ImageReadException("Tiff: samplesPerPixel (" - + samplesPerPixel + ")!=fBitsPerSample.length (" - + bitsPerSample.length + ")"); - - boolean hasAlpha = false; - BufferedImage result = getBufferedImageFactory(params) - .getColorBufferedImage(width, height, hasAlpha); - - PhotometricInterpreter photometricInterpreter = getPhotometricInterpreter( - directory, photometricInterpretation, bitsPerPixel, - bitsPerSample, predictor, samplesPerPixel, width, height); - - TiffImageData imageData = directory.getTiffImageData(); - - DataReader dataReader = imageData.getDataReader(entries, - photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, - samplesPerPixel, width, height, compression); - - dataReader.readImageData(result); - - photometricInterpreter.dumpstats(); - - return result; - } - - private PhotometricInterpreter getPhotometricInterpreter( - TiffDirectory directory, int photometricInterpretation, - int bitsPerPixel, int bitsPerSample[], int predictor, - int samplesPerPixel, int width, int height) throws IOException, - ImageReadException - { - switch (photometricInterpretation) - { - case 0: - case 1: - boolean invert = photometricInterpretation == 0; - - return new PhotometricInterpreterBiLevel(bitsPerPixel, - samplesPerPixel, bitsPerSample, predictor, width, height, - invert); - case 3: // Palette - { - int colorMap[] = directory.findField(TIFF_TAG_COLOR_MAP, true) - .getIntArrayValue(); - - int expected_colormap_size = 3 * (1 << bitsPerPixel); - - if (colorMap.length != expected_colormap_size) - throw new ImageReadException("Tiff: fColorMap.length (" - + colorMap.length + ")!=expected_colormap_size (" - + expected_colormap_size + ")"); - - return new PhotometricInterpreterPalette(samplesPerPixel, - bitsPerSample, predictor, width, height, colorMap); - } - case 2: // RGB - return new PhotometricInterpreterRGB(samplesPerPixel, - bitsPerSample, predictor, width, height); - case 5: // CMYK - return new PhotometricInterpreterCMYK(samplesPerPixel, - bitsPerSample, predictor, width, height); - case 6: // - { - double yCbCrCoefficients[] = directory.findField( - TIFF_TAG_YCBCR_COEFFICIENTS, true).getDoubleArrayValue(); - - int yCbCrPositioning[] = directory.findField( - TIFF_TAG_YCBCR_POSITIONING, true).getIntArrayValue(); - int yCbCrSubSampling[] = directory.findField( - TIFF_TAG_YCBCR_SUB_SAMPLING, true).getIntArrayValue(); - - double referenceBlackWhite[] = directory.findField( - TIFF_TAG_REFERENCE_BLACK_WHITE, true).getDoubleArrayValue(); - - return new PhotometricInterpreterYCbCr(yCbCrCoefficients, - yCbCrPositioning, yCbCrSubSampling, referenceBlackWhite, - samplesPerPixel, bitsPerSample, predictor, width, height); - } - - case 8: - return new PhotometricInterpreterCIELAB(samplesPerPixel, - bitsPerSample, predictor, width, height); - - case 32844: - case 32845: { - boolean yonly = (photometricInterpretation == 32844); - return new PhotometricInterpreterLogLUV(samplesPerPixel, - bitsPerSample, predictor, width, height, yonly); - } - - default: - throw new ImageReadException( - "TIFF: Unknown fPhotometricInterpretation: " - + photometricInterpretation); - } - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - new TiffImageWriterLossy().writeImage(src, os, params); - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/TiffReader.java b/src/main/java/org/apache/sanselan/formats/tiff/TiffReader.java deleted file mode 100644 index d03be22..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/TiffReader.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.FormatCompliance; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.formats.tiff.TiffDirectory.ImageDataElement; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.util.Debug; - -public class TiffReader extends BinaryFileParser implements TiffConstants -{ - - private final boolean strict; - - public TiffReader(boolean strict) - { - this.strict = strict; - } - - private TiffHeader readTiffHeader(ByteSource byteSource, - FormatCompliance formatCompliance) throws ImageReadException, - IOException - { - InputStream is = null; - try - { - is = byteSource.getInputStream(); - return readTiffHeader(is, formatCompliance); - } finally - { - try - { - if (is != null) - is.close(); - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - private TiffHeader readTiffHeader(InputStream is, - FormatCompliance formatCompliance) throws ImageReadException, - IOException - { - int BYTE_ORDER_1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File"); - int BYTE_ORDER_2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File"); - setByteOrder(BYTE_ORDER_1, BYTE_ORDER_2); - - int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File"); - if (tiffVersion != 42) - throw new ImageReadException("Unknown Tiff Version: " + tiffVersion); - - int offsetToFirstIFD = read4Bytes("offsetToFirstIFD", is, - "Not a Valid TIFF File"); - - skipBytes(is, offsetToFirstIFD - 8, - "Not a Valid TIFF File: couldn't find IFDs"); - - if (debug) - System.out.println(""); - - return new TiffHeader(BYTE_ORDER_1, tiffVersion, offsetToFirstIFD); - } - - private void readDirectories(ByteSource byteSource, - FormatCompliance formatCompliance, Listener listener) - throws ImageReadException, IOException - { - TiffHeader tiffHeader = readTiffHeader(byteSource, formatCompliance); - if (!listener.setTiffHeader(tiffHeader)) - return; - - int offset = tiffHeader.offsetToFirstIFD; - int dirType = TiffDirectory.DIRECTORY_TYPE_ROOT; - - List visited = new ArrayList(); - readDirectory(byteSource, offset, dirType, formatCompliance, listener, - visited); - } - - private boolean readDirectory(ByteSource byteSource, int offset, - int dirType, FormatCompliance formatCompliance, Listener listener, - List visited) throws ImageReadException, IOException - { - boolean ignoreNextDirectory = false; - return readDirectory(byteSource, offset, dirType, formatCompliance, - listener, ignoreNextDirectory, visited); - } - - private boolean readDirectory(ByteSource byteSource, int offset, - int dirType, FormatCompliance formatCompliance, Listener listener, - boolean ignoreNextDirectory, List visited) - throws ImageReadException, IOException - { - Number key = new Integer(offset); - - // Debug.debug(); - // Debug.debug("dir offset", offset + " (0x" + - // Integer.toHexString(offset) - // + ")"); - // Debug.debug("dir key", key); - // Debug.debug("dir visited", visited); - // Debug.debug("dirType", dirType); - // Debug.debug(); - - if (visited.contains(key)) - return false; - visited.add(key); - - InputStream is = null; - try - { - is = byteSource.getInputStream(); - if (offset > 0) - is.skip(offset); - - ArrayList fields = new ArrayList(); - - if (offset >= byteSource.getLength()) - { - // Debug.debug("skipping invalid directory!"); - return true; - } - - int entryCount; - try - { - entryCount = read2Bytes("DirectoryEntryCount", is, - "Not a Valid TIFF File"); - } catch (IOException e) - { - if (strict) - throw e; - else - return true; - } - - // Debug.debug("entryCount", entryCount); - - for (int i = 0; i < entryCount; i++) - { - int tag = read2Bytes("Tag", is, "Not a Valid TIFF File"); - int type = read2Bytes("Type", is, "Not a Valid TIFF File"); - int length = read4Bytes("Length", is, "Not a Valid TIFF File"); - - // Debug.debug("tag*", tag + " (0x" + Integer.toHexString(tag) - // + ")"); - - byte valueOffsetBytes[] = readByteArray("ValueOffset", 4, is, - "Not a Valid TIFF File"); - int valueOffset = convertByteArrayToInt("ValueOffset", - valueOffsetBytes); - - if (tag == 0) - { - // skip invalid fields. - // These are seen very rarely, but can have invalid value - // lengths, - // which can cause OOM problems. - continue; - } - - // if (keepField(tag, tags)) - // { - TiffField field = new TiffField(tag, dirType, type, length, - valueOffset, valueOffsetBytes, getByteOrder()); - field.setSortHint(i); - - // Debug.debug("tagInfo", field.tagInfo); - - field.fillInValue(byteSource); - - // Debug.debug("\t" + "value", field.getValueDescription()); - - fields.add(field); - - if (!listener.addField(field)) - return true; - } - - int nextDirectoryOffset = read4Bytes("nextDirectoryOffset", is, - "Not a Valid TIFF File"); - // Debug.debug("nextDirectoryOffset", nextDirectoryOffset); - - TiffDirectory directory = new TiffDirectory(dirType, fields, - offset, nextDirectoryOffset); - - if (listener.readImageData()) - { - if (directory.hasTiffImageData()) - { - TiffImageData rawImageData = getTiffRawImageData( - byteSource, directory); - directory.setTiffImageData(rawImageData); - } - if (directory.hasJpegImageData()) - { - JpegImageData rawJpegImageData = getJpegRawImageData( - byteSource, directory); - directory.setJpegImageData(rawJpegImageData); - } - } - - if (!listener.addDirectory(directory)) - return true; - - if (listener.readOffsetDirectories()) - { - List fieldsToRemove = new ArrayList(); - for (int j = 0; j < fields.size(); j++) - { - TiffField entry = (TiffField) fields.get(j); - - if (entry.tag == TiffConstants.EXIF_TAG_EXIF_OFFSET.tag - || entry.tag == TiffConstants.EXIF_TAG_GPSINFO.tag - || entry.tag == TiffConstants.EXIF_TAG_INTEROP_OFFSET.tag) - ; - else - continue; - - int subDirectoryOffset = ((Number) entry.getValue()) - .intValue(); - int subDirectoryType; - if (entry.tag == TiffConstants.EXIF_TAG_EXIF_OFFSET.tag) - subDirectoryType = TiffDirectory.DIRECTORY_TYPE_EXIF; - else if (entry.tag == TiffConstants.EXIF_TAG_GPSINFO.tag) - subDirectoryType = TiffDirectory.DIRECTORY_TYPE_GPS; - else if (entry.tag == TiffConstants.EXIF_TAG_INTEROP_OFFSET.tag) - subDirectoryType = TiffDirectory.DIRECTORY_TYPE_INTEROPERABILITY; - else - throw new ImageReadException( - "Unknown subdirectory type."); - - // Debug.debug("sub dir", subDirectoryOffset); - boolean subDirectoryRead = readDirectory(byteSource, - subDirectoryOffset, subDirectoryType, - formatCompliance, listener, true, visited); - - if (!subDirectoryRead) - { - // Offset field pointed to invalid location. - // This is a bug in certain cameras. Ignore offset - // field. - fieldsToRemove.add(entry); - } - - } - fields.removeAll(fieldsToRemove); - } - - if (!ignoreNextDirectory && directory.nextDirectoryOffset > 0) - { - // Debug.debug("next dir", directory.nextDirectoryOffset ); - readDirectory(byteSource, directory.nextDirectoryOffset, - dirType + 1, formatCompliance, listener, visited); - } - - return true; - } finally - { - try - { - if (is != null) - is.close(); - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public static interface Listener - { - public boolean setTiffHeader(TiffHeader tiffHeader); - - public boolean addDirectory(TiffDirectory directory); - - public boolean addField(TiffField field); - - public boolean readImageData(); - - public boolean readOffsetDirectories(); - } - - private static class Collector implements Listener - { - private TiffHeader tiffHeader = null; - private ArrayList directories = new ArrayList(); - private ArrayList fields = new ArrayList(); - private final boolean readThumbnails; - - public Collector() - { - this(null); - } - - public Collector(Map params) - { - boolean readThumbnails = true; - if (params != null && params.containsKey(PARAM_KEY_READ_THUMBNAILS)) - readThumbnails = Boolean.TRUE.equals(params - .get(PARAM_KEY_READ_THUMBNAILS)); - this.readThumbnails = readThumbnails; - } - - public boolean setTiffHeader(TiffHeader tiffHeader) - { - this.tiffHeader = tiffHeader; - return true; - } - - public boolean addDirectory(TiffDirectory directory) - { - directories.add(directory); - return true; - } - - public boolean addField(TiffField field) - { - fields.add(field); - return true; - } - - public boolean readImageData() - { - return readThumbnails; - } - - public boolean readOffsetDirectories() - { - return true; - } - - public TiffContents getContents() - { - return new TiffContents(tiffHeader, directories); - } - } - - private static class FirstDirectoryCollector extends Collector - { - private final boolean readImageData; - - public FirstDirectoryCollector(final boolean readImageData) - { - this.readImageData = readImageData; - } - - public boolean addDirectory(TiffDirectory directory) - { - super.addDirectory(directory); - return false; - } - - public boolean readImageData() - { - return readImageData; - } - } - - private static class DirectoryCollector extends Collector - { - private final boolean readImageData; - - public DirectoryCollector(final boolean readImageData) - { - this.readImageData = readImageData; - } - - public boolean addDirectory(TiffDirectory directory) - { - super.addDirectory(directory); - return false; - } - - public boolean readImageData() - { - return readImageData; - } - } - - public TiffContents readFirstDirectory(ByteSource byteSource, Map params, - boolean readImageData, FormatCompliance formatCompliance) - throws ImageReadException, IOException - { - Collector collector = new FirstDirectoryCollector(readImageData); - read(byteSource, params, formatCompliance, collector); - TiffContents contents = collector.getContents(); - if (contents.directories.size() < 1) - throw new ImageReadException( - "Image did not contain any directories."); - return contents; - } - - public TiffContents readDirectories(ByteSource byteSource, - boolean readImageData, FormatCompliance formatCompliance) - throws ImageReadException, IOException - { - Collector collector = new FirstDirectoryCollector(readImageData); - readDirectories(byteSource, formatCompliance, collector); - TiffContents contents = collector.getContents(); - if (contents.directories.size() < 1) - throw new ImageReadException( - "Image did not contain any directories."); - return contents; - } - - public TiffContents readContents(ByteSource byteSource, Map params, - FormatCompliance formatCompliance) throws ImageReadException, - IOException - { - - Collector collector = new Collector(params); - read(byteSource, params, formatCompliance, collector); - TiffContents contents = collector.getContents(); - return contents; - } - - public void read(ByteSource byteSource, Map params, - FormatCompliance formatCompliance, Listener listener) - throws ImageReadException, IOException - { - // TiffContents contents = - readDirectories(byteSource, formatCompliance, listener); - } - - private TiffImageData getTiffRawImageData(ByteSource byteSource, - TiffDirectory directory) throws ImageReadException, IOException - { - - ArrayList elements = directory.getTiffRawImageDataElements(); - TiffImageData.Data data[] = new TiffImageData.Data[elements.size()]; - for (int i = 0; i < elements.size(); i++) - { - TiffDirectory.ImageDataElement element = (TiffDirectory.ImageDataElement) elements - .get(i); - byte bytes[] = byteSource.getBlock(element.offset, element.length); - data[i] = new TiffImageData.Data(element.offset, element.length, - bytes); - } - - if (directory.imageDataInStrips()) - { - TiffField rowsPerStripField = directory - .findField(TIFF_TAG_ROWS_PER_STRIP); - if (null == rowsPerStripField) - throw new ImageReadException("Can't find rows per strip field."); - int rowsPerStrip = rowsPerStripField.getIntValue(); - - return new TiffImageData.Strips(data, rowsPerStrip); - } else - { - TiffField tileWidthField = directory.findField(TIFF_TAG_TILE_WIDTH); - if (null == tileWidthField) - throw new ImageReadException("Can't find tile width field."); - int tileWidth = tileWidthField.getIntValue(); - - TiffField tileLengthField = directory - .findField(TIFF_TAG_TILE_LENGTH); - if (null == tileLengthField) - throw new ImageReadException("Can't find tile length field."); - int tileLength = tileLengthField.getIntValue(); - - return new TiffImageData.Tiles(data, tileWidth, tileLength); - } - } - - private JpegImageData getJpegRawImageData(ByteSource byteSource, - TiffDirectory directory) throws ImageReadException, IOException - { - ImageDataElement element = directory.getJpegRawImageDataElement(); - int offset = element.offset; - int length = element.length; - // Sony DCR-PC110 has an off-by-one error. - if (offset + length == byteSource.getLength() + 1) - length--; - byte data[] = byteSource.getBlock(offset, length); - return new JpegImageData(offset, length, data); - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/AllTagConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/AllTagConstants.java deleted file mode 100644 index 8484ba1..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/AllTagConstants.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -import org.apache.sanselan.SanselanConstants; - -public interface AllTagConstants - extends - SanselanConstants, - TiffTagConstants, - ExifTagConstants, - GPSTagConstants -{ - public static final TagInfo ALL_TAGS[] = TagConstantsUtils - .mergeTagLists(new TagInfo[][]{ - ALL_TIFF_TAGS, ALL_EXIF_TAGS, ALL_GPS_TAGS, - }); - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/ExifTagConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/ExifTagConstants.java deleted file mode 100644 index 2477c76..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/ExifTagConstants.java +++ /dev/null @@ -1,1477 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -public interface ExifTagConstants - extends - TiffDirectoryConstants, - TiffFieldTypeConstants -{ /**/ - public static final TagInfo EXIF_TAG_INTEROP_INDEX = new TagInfo( - "Interop Index", 0x0001, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_INTEROP_IFD); - public static final TagInfo EXIF_TAG_INTEROP_VERSION = new TagInfo( - "Interop Version", 0x0002, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_INTEROP_IFD); - public static final TagInfo EXIF_TAG_PROCESSING_SOFTWARE = new TagInfo( - "Processing Software", 0x000b, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_SUBFILE_TYPE = new TagInfo( - "Subfile Type", 0x00fe, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int SUBFILE_TYPE_VALUE_FULL_RESOLUTION_IMAGE = 0; - public static final int SUBFILE_TYPE_VALUE_REDUCED_RESOLUTION_IMAGE = 1; - public static final int SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_IMAGE = 2; - public static final int SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_REDUCED_RESOLUTION_IMAGE = 3; - public static final int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK = 4; - public static final int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_REDUCED_RESOLUTION_IMAGE = 5; - public static final int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_MULTI_PAGE_IMAGE = 6; - public static final int SUBFILE_TYPE_VALUE_TRANSPARENCY_MASK_OF_REDUCED_RESOLUTION_MULTI_PAGE_IMAGE = 7; - public static final TagInfo EXIF_TAG_OLD_SUBFILE_TYPE = new TagInfo( - "Old Subfile Type", 0x00ff, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int OLD_SUBFILE_TYPE_VALUE_FULL_RESOLUTION_IMAGE = 1; - public static final int OLD_SUBFILE_TYPE_VALUE_REDUCED_RESOLUTION_IMAGE = 2; - public static final int OLD_SUBFILE_TYPE_VALUE_SINGLE_PAGE_OF_MULTI_PAGE_IMAGE = 3; - public static final TagInfo EXIF_TAG_IMAGE_WIDTH_IFD0 = new TagInfo( - "Image Width", 0x0100, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_IMAGE_HEIGHT_IFD0 = new TagInfo( - "Image Height", 0x0101, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_BITS_PER_SAMPLE = new TagInfo( - "Bits Per Sample", 0x0102, FIELD_TYPE_SHORT, -1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_COMPRESSION = new TagInfo( - "Compression", 0x0103, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int COMPRESSION_VALUE_UNCOMPRESSED = 1; - public static final int COMPRESSION_VALUE_CCITT_1D = 2; - public static final int COMPRESSION_VALUE_T4_GROUP_3_FAX = 3; - public static final int COMPRESSION_VALUE_T6_GROUP_4_FAX = 4; - public static final int COMPRESSION_VALUE_LZW = 5; - public static final int COMPRESSION_VALUE_JPEG_OLD_STYLE = 6; - public static final int COMPRESSION_VALUE_JPEG = 7; - public static final int COMPRESSION_VALUE_ADOBE_DEFLATE = 8; - public static final int COMPRESSION_VALUE_JBIG_B_AND_W = 9; - public static final int COMPRESSION_VALUE_JBIG_COLOR = 10; - public static final int COMPRESSION_VALUE_NEXT = 32766; - public static final int COMPRESSION_VALUE_EPSON_ERF_COMPRESSED = 32769; - public static final int COMPRESSION_VALUE_CCIRLEW = 32771; - public static final int COMPRESSION_VALUE_PACK_BITS = 32773; - public static final int COMPRESSION_VALUE_THUNDERSCAN = 32809; - public static final int COMPRESSION_VALUE_IT8CTPAD = 32895; - public static final int COMPRESSION_VALUE_IT8LW = 32896; - public static final int COMPRESSION_VALUE_IT8MP = 32897; - public static final int COMPRESSION_VALUE_IT8BL = 32898; - public static final int COMPRESSION_VALUE_PIXAR_FILM = 32908; - public static final int COMPRESSION_VALUE_PIXAR_LOG = 32909; - public static final int COMPRESSION_VALUE_DEFLATE = 32946; - public static final int COMPRESSION_VALUE_DCS = 32947; - public static final int COMPRESSION_VALUE_JBIG = 34661; - public static final int COMPRESSION_VALUE_SGILOG = 34676; - public static final int COMPRESSION_VALUE_SGILOG_24 = 34677; - public static final int COMPRESSION_VALUE_JPEG_2000 = 34712; - public static final int COMPRESSION_VALUE_NIKON_NEF_COMPRESSED = 34713; - public static final int COMPRESSION_VALUE_KODAK_DCR_COMPRESSED = 65000; - public static final int COMPRESSION_VALUE_PENTAX_PEF_COMPRESSED = 65535; - public static final TagInfo EXIF_TAG_PHOTOMETRIC_INTERPRETATION = new TagInfo( - "Photometric Interpretation", 0x0106, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int PHOTOMETRIC_INTERPRETATION_VALUE_WHITE_IS_ZERO = 0; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO = 1; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_RGB = 2; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_RGB_PALETTE = 3; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_TRANSPARENCY_MASK = 4; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_CMYK = 5; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_YCB_CR = 6; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_CIELAB = 8; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_ICCLAB = 9; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_ITULAB = 10; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_COLOR_FILTER_ARRAY = 32803; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_PIXAR_LOG_L = 32844; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_PIXAR_LOG_LUV = 32845; - public static final int PHOTOMETRIC_INTERPRETATION_VALUE_LINEAR_RAW = 34892; - public static final TagInfo EXIF_TAG_THRESHOLDING = new TagInfo( - "Thresholding", 0x0107, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int THRESHOLDING_VALUE_NO_DITHERING_OR_HALFTONING = 1; - public static final int THRESHOLDING_VALUE_ORDERED_DITHER_OR_HALFTONE = 2; - public static final int THRESHOLDING_VALUE_RANDOMIZED_DITHER = 3; - public static final TagInfo EXIF_TAG_CELL_WIDTH = new TagInfo("Cell Width", - 0x0108, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_CELL_LENGTH = new TagInfo( - "Cell Length", 0x0109, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_FILL_ORDER = new TagInfo("Fill Order", - 0x010a, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int FILL_ORDER_VALUE_NORMAL = 1; - public static final int FILL_ORDER_VALUE_REVERSED = 2; - public static final TagInfo EXIF_TAG_DOCUMENT_NAME = new TagInfo( - "Document Name", 0x010d, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_IMAGE_DESCRIPTION = new TagInfo( - "Image Description", 0x010e, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_MAKE = new TagInfo("Make", 0x010f, - FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_MODEL = new TagInfo("Model", 0x0110, - FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - // poly tag public static final TagInfo2 EXIF_TAG_STRIP_OFFSETS = new TagInfo2( "StripOffsets", 0x0111, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_START_IFD0 = new TagInfo.Offset( - "Preview Image Start", 0x0111, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_START_SUB_IFD1 = new TagInfo.Offset( - "Preview Image Start", 0x0111, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD1); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD2 = new TagInfo.Offset( - "Jpg From Raw Start", 0x0111, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD2); - public static final TagInfo EXIF_TAG_ORIENTATION = new TagInfo( - "Orientation", 0x0112, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int ORIENTATION_VALUE_HORIZONTAL_NORMAL = 1; - public static final int ORIENTATION_VALUE_MIRROR_HORIZONTAL = 2; - public static final int ORIENTATION_VALUE_ROTATE_180 = 3; - public static final int ORIENTATION_VALUE_MIRROR_VERTICAL = 4; - public static final int ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW = 5; - public static final int ORIENTATION_VALUE_ROTATE_90_CW = 6; - public static final int ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW = 7; - public static final int ORIENTATION_VALUE_ROTATE_270_CW = 8; - public static final TagInfo EXIF_TAG_SAMPLES_PER_PIXEL = new TagInfo( - "Samples Per Pixel", 0x0115, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_ROWS_PER_STRIP = new TagInfo( - "Rows Per Strip", 0x0116, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - // poly tag public static final TagInfo2 EXIF_TAG_STRIP_BYTE_COUNTS = new TagInfo2( "StripByteCounts", 0x0117, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0 = new TagInfo( - "Preview Image Length", 0x0117, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_LENGTH_SUB_IFD1 = new TagInfo( - "Preview Image Length", 0x0117, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD1); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD2 = new TagInfo( - "Jpg From Raw Length", 0x0117, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD2); - public static final TagInfo EXIF_TAG_MIN_SAMPLE_VALUE = new TagInfo( - "Min Sample Value", 0x0118, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_MAX_SAMPLE_VALUE = new TagInfo( - "Max Sample Value", 0x0119, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XRESOLUTION = new TagInfo( - "XResolution", 0x011a, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_YRESOLUTION = new TagInfo( - "YResolution", 0x011b, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_PLANAR_CONFIGURATION = new TagInfo( - "Planar Configuration", 0x011c, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int PLANAR_CONFIGURATION_VALUE_CHUNKY = 1; - public static final int PLANAR_CONFIGURATION_VALUE_PLANAR = 2; - public static final TagInfo EXIF_TAG_PAGE_NAME = new TagInfo("Page Name", - 0x011d, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPOSITION = new TagInfo("XPosition", - 0x011e, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_YPOSITION = new TagInfo("YPosition", - 0x011f, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_FREE_OFFSETS = new TagInfo( - "Free Offsets", 0x0120, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FREE_BYTE_COUNTS = new TagInfo( - "Free Byte Counts", 0x0121, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GRAY_RESPONSE_UNIT = new TagInfo( - "Gray Response Unit", 0x0122, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int GRAY_RESPONSE_UNIT_VALUE_0_1 = 1; - public static final int GRAY_RESPONSE_UNIT_VALUE_0_001 = 2; - public static final int GRAY_RESPONSE_UNIT_VALUE_0_0001 = 3; - public static final int GRAY_RESPONSE_UNIT_VALUE_1E_05 = 4; - public static final int GRAY_RESPONSE_UNIT_VALUE_1E_06 = 5; - public static final TagInfo EXIF_TAG_GRAY_RESPONSE_CURVE = new TagInfo( - "Gray Response Curve", 0x0123, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_T4OPTIONS = new TagInfo("T4 Options", - 0x0124, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_T6OPTIONS = new TagInfo("T6 Options", - 0x0125, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_RESOLUTION_UNIT = new TagInfo( - "Resolution Unit", 0x0128, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int RESOLUTION_UNIT_VALUE_NONE = 1; - public static final int RESOLUTION_UNIT_VALUE_INCHES = 2; - public static final int RESOLUTION_UNIT_VALUE_CM = 3; - public static final TagInfo EXIF_TAG_PAGE_NUMBER = new TagInfo( - "Page Number", 0x0129, FIELD_TYPE_SHORT, 2, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_COLOR_RESPONSE_UNIT = new TagInfo( - "Color Response Unit", 0x012c, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TRANSFER_FUNCTION = new TagInfo( - "Transfer Function", 0x012d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SOFTWARE = new TagInfo("Software", - 0x0131, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_MODIFY_DATE = new TagInfo( - "Modify Date", 0x0132, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_ARTIST = new TagInfo("Artist", 0x013b, - FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_HOST_COMPUTER = new TagInfo( - "Host Computer", 0x013c, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_PREDICTOR = new TagInfo("Predictor", - 0x013d, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int PREDICTOR_VALUE_NONE = 1; - public static final int PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING = 2; - public static final TagInfo EXIF_TAG_WHITE_POINT = new TagInfo( - "White Point", 0x013e, FIELD_TYPE_RATIONAL, 2, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_PRIMARY_CHROMATICITIES = new TagInfo( - "Primary Chromaticities", 0x013f, FIELD_TYPE_RATIONAL, 6, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_COLOR_MAP = new TagInfo("Color Map", - 0x0140, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_HALFTONE_HINTS = new TagInfo( - "Halftone Hints", 0x0141, FIELD_TYPE_SHORT, 2, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_TILE_WIDTH = new TagInfo("Tile Width", - 0x0142, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_TILE_LENGTH = new TagInfo( - "Tile Length", 0x0143, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_TILE_OFFSETS = new TagInfo( - "Tile Offsets", 0x0144, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TILE_BYTE_COUNTS = new TagInfo( - "Tile Byte Counts", 0x0145, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BAD_FAX_LINES = new TagInfo( - "Bad Fax Lines", 0x0146, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CLEAN_FAX_DATA = new TagInfo( - "Clean Fax Data", 0x0147, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int CLEAN_FAX_DATA_VALUE_CLEAN = 0; - public static final int CLEAN_FAX_DATA_VALUE_REGENERATED = 1; - public static final int CLEAN_FAX_DATA_VALUE_UNCLEAN = 2; - public static final TagInfo EXIF_TAG_CONSECUTIVE_BAD_FAX_LINES = new TagInfo( - "Consecutive Bad Fax Lines", 0x0148, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SUB_IFD = new TagInfo("Sub IFD", - 0x014a, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_INK_SET = new TagInfo("Ink Set", - 0x014c, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - // tag constants public static final int INK_SET_VALUE_CMYK = 1; - public static final int INK_SET_VALUE_NOT_CMYK = 2; - public static final TagInfo EXIF_TAG_INK_NAMES = new TagInfo("Ink Names", - 0x014d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_NUMBEROF_INKS = new TagInfo( - "Numberof Inks", 0x014e, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DOT_RANGE = new TagInfo("Dot Range", - 0x0150, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_TARGET_PRINTER = new TagInfo( - "Target Printer", 0x0151, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_EXTRA_SAMPLES = new TagInfo( - "Extra Samples", 0x0152, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SAMPLE_FORMAT = new TagInfo( - "Sample Format", 0x0153, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int SAMPLE_FORMAT_VALUE_UNSIGNED_INTEGER = 1; - public static final int SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER = 2; - public static final int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT = 3; - public static final int SAMPLE_FORMAT_VALUE_UNDEFINED = 4; - public static final int SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER = 5; - public static final int SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT_1 = 6; - public static final TagInfo EXIF_TAG_SMIN_SAMPLE_VALUE = new TagInfo( - "SMin Sample Value", 0x0154, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SMAX_SAMPLE_VALUE = new TagInfo( - "SMax Sample Value", 0x0155, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TRANSFER_RANGE = new TagInfo( - "Transfer Range", 0x0156, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CLIP_PATH = new TagInfo("Clip Path", - 0x0157, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_XCLIP_PATH_UNITS = new TagInfo( - "XClip Path Units", 0x0158, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_YCLIP_PATH_UNITS = new TagInfo( - "YClip Path Units", 0x0159, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_INDEXED = new TagInfo("Indexed", - 0x015a, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int INDEXED_VALUE_NOT_INDEXED = 0; - public static final int INDEXED_VALUE_INDEXED = 1; - public static final TagInfo EXIF_TAG_JPEGTABLES = new TagInfo("JPEGTables", - 0x015b, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_OPIPROXY = new TagInfo("OPIProxy", - 0x015f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int OPIPROXY_VALUE_HIGHER_RESOLUTION_IMAGE_DOES_NOT_EXIST = 0; - public static final int OPIPROXY_VALUE_HIGHER_RESOLUTION_IMAGE_EXISTS = 1; - public static final TagInfo EXIF_TAG_GLOBAL_PARAMETERS_IFD = new TagInfo( - "Global Parameters IFD", 0x0190, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PROFILE_TYPE = new TagInfo( - "Profile Type", 0x0191, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int PROFILE_TYPE_VALUE_UNSPECIFIED = 0; - public static final int PROFILE_TYPE_VALUE_GROUP_3_FAX = 1; - public static final TagInfo EXIF_TAG_FAX_PROFILE = new TagInfo( - "Fax Profile", 0x0192, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int FAX_PROFILE_VALUE_UNKNOWN = 0; - public static final int FAX_PROFILE_VALUE_MINIMAL_B_AND_W_LOSSLESS_S = 1; - public static final int FAX_PROFILE_VALUE_EXTENDED_B_AND_W_LOSSLESS_F = 2; - public static final int FAX_PROFILE_VALUE_LOSSLESS_JBIG_B_AND_W_J = 3; - public static final int FAX_PROFILE_VALUE_LOSSY_COLOR_AND_GRAYSCALE_C = 4; - public static final int FAX_PROFILE_VALUE_LOSSLESS_COLOR_AND_GRAYSCALE_L = 5; - public static final int FAX_PROFILE_VALUE_MIXED_RASTER_CONTENT_M = 6; - public static final TagInfo EXIF_TAG_CODING_METHODS = new TagInfo( - "Coding Methods", 0x0193, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_VERSION_YEAR = new TagInfo( - "Version Year", 0x0194, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MODE_NUMBER = new TagInfo( - "Mode Number", 0x0195, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DECODE = new TagInfo("Decode", 0x01b1, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DEFAULT_IMAGE_COLOR = new TagInfo( - "Default Image Color", 0x01b2, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGPROC = new TagInfo("JPEGProc", - 0x0200, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int JPEGPROC_VALUE_BASELINE = 1; - public static final int JPEGPROC_VALUE_LOSSLESS = 14; - // poly tag public static final TagInfo2 EXIF_TAG_THUMBNAIL_OFFSET = new TagInfo2( "ThumbnailOffset", 0x0201, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD1); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_START_MAKER_NOTES = new TagInfo( - "Preview Image Start", 0x0201, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_MAKER_NOTES); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD = new TagInfo.Offset( - "Jpg From Raw Start", 0x0201, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_START_IFD2 = new TagInfo.Offset( - "Jpg From Raw Start", 0x0201, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_IFD2); - public static final TagInfo EXIF_TAG_OTHER_IMAGE_START = new TagInfo.Offset( - "Other Image Start", 0x0201, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // poly tag public static final TagInfo2 EXIF_TAG_THUMBNAIL_LENGTH = new TagInfo2( "ThumbnailLength", 0x0202, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD1); - public static final TagInfo EXIF_TAG_PREVIEW_IMAGE_LENGTH_MAKER_NOTES = new TagInfo( - "Preview Image Length", 0x0202, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_MAKER_NOTES); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD = new TagInfo( - "Jpg From Raw Length", 0x0202, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_JPG_FROM_RAW_LENGTH_IFD2 = new TagInfo( - "Jpg From Raw Length", 0x0202, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_IFD2); - public static final TagInfo EXIF_TAG_OTHER_IMAGE_LENGTH = new TagInfo( - "Other Image Length", 0x0202, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGRESTART_INTERVAL = new TagInfo( - "JPEGRestart Interval", 0x0203, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGLOSSLESS_PREDICTORS = new TagInfo( - "JPEGLossless Predictors", 0x0205, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGPOINT_TRANSFORMS = new TagInfo( - "JPEGPoint Transforms", 0x0206, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGQTABLES = new TagInfo( - "JPEGQTables", 0x0207, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGDCTABLES = new TagInfo( - "JPEGDCTables", 0x0208, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_JPEGACTABLES = new TagInfo( - "JPEGACTables", 0x0209, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_YCBCR_COEFFICIENTS = new TagInfo( - "YCbCr Coefficients", 0x0211, FIELD_TYPE_RATIONAL, 3, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_YCBCR_SUB_SAMPLING = new TagInfo( - "YCbCr Sub Sampling", 0x0212, FIELD_TYPE_SHORT, 2, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_YCBCR_POSITIONING = new TagInfo( - "YCbCr Positioning", 0x0213, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int YCB_CR_POSITIONING_VALUE_CENTERED = 1; - public static final int YCB_CR_POSITIONING_VALUE_CO_SITED = 2; - public static final TagInfo EXIF_TAG_REFERENCE_BLACK_WHITE = new TagInfo( - "Reference Black White", 0x0214, FIELD_TYPE_RATIONAL, 6, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_STRIP_ROW_COUNTS = new TagInfo( - "Strip Row Counts", 0x022f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_APPLICATION_NOTES = new TagInfo( - "Application Notes", 0x02bc, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_RELATED_IMAGE_FILE_FORMAT = new TagInfo( - "Related Image File Format", 0x1000, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_INTEROP_IFD); - public static final TagInfo EXIF_TAG_RELATED_IMAGE_WIDTH = new TagInfo( - "Related Image Width", 0x1001, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_INTEROP_IFD); - public static final TagInfo EXIF_TAG_RELATED_IMAGE_LENGTH = new TagInfo( - "Related Image Length", 0x1002, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_INTEROP_IFD); - public static final TagInfo EXIF_TAG_RATING = new TagInfo("Rating", 0x4746, - FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_RATING_PERCENT = new TagInfo( - "Rating Percent", 0x4749, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_IMAGE_ID = new TagInfo("Image ID", - 0x800d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_WANG_ANNOTATION = new TagInfo( - "Wang Annotation", 0x80a4, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MATTEING = new TagInfo("Matteing", - 0x80e3, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DATA_TYPE = new TagInfo("Data Type", - 0x80e4, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_DEPTH = new TagInfo( - "Image Depth", 0x80e5, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TILE_DEPTH = new TagInfo("Tile Depth", - 0x80e6, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MODEL_2 = new TagInfo("Model 2", - 0x827d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CFAREPEAT_PATTERN_DIM = new TagInfo( - "CFARepeat Pattern Dim", 0x828d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CFAPATTERN_2 = new TagInfo( - "CFAPattern 2", 0x828e, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BATTERY_LEVEL = new TagInfo( - "Battery Level", 0x828f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_COPYRIGHT = new TagInfo("Copyright", - 0x8298, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_EXPOSURE_TIME = new TagInfo( - "Exposure Time", 0x829a, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FNUMBER = new TagInfo("FNumber", - 0x829d, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_MDFILE_TAG = new TagInfo("MDFile Tag", - 0x82a5, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDSCALE_PIXEL = new TagInfo( - "MDScale Pixel", 0x82a6, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDCOLOR_TABLE = new TagInfo( - "MDColor Table", 0x82a7, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDLAB_NAME = new TagInfo("MDLab Name", - 0x82a8, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDSAMPLE_INFO = new TagInfo( - "MDSample Info", 0x82a9, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDPREP_DATE = new TagInfo( - "MDPrep Date", 0x82aa, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDPREP_TIME = new TagInfo( - "MDPrep Time", 0x82ab, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MDFILE_UNITS = new TagInfo( - "MDFile Units", 0x82ac, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PIXEL_SCALE = new TagInfo( - "Pixel Scale", 0x830e, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IPTC_NAA = new TagInfo("IPTC- NAA", - 0x83bb, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_INTERGRAPH_PACKET_DATA = new TagInfo( - "Intergraph Packet Data", 0x847e, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_INTERGRAPH_FLAG_REGISTERS = new TagInfo( - "Intergraph Flag Registers", 0x847f, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_INTERGRAPH_MATRIX = new TagInfo( - "Intergraph Matrix", 0x8480, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MODEL_TIE_POINT = new TagInfo( - "Model Tie Point", 0x8482, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SITE = new TagInfo("Site", 0x84e0, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_COLOR_SEQUENCE = new TagInfo( - "Color Sequence", 0x84e1, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IT8HEADER = new TagInfo("IT8 Header", - 0x84e2, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_RASTER_PADDING = new TagInfo( - "Raster Padding", 0x84e3, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BITS_PER_RUN_LENGTH = new TagInfo( - "Bits Per Run Length", 0x84e4, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BITS_PER_EXTENDED_RUN_LENGTH = new TagInfo( - "Bits Per Extended Run Length", 0x84e5, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_COLOR_TABLE = new TagInfo( - "Color Table", 0x84e6, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_COLOR_INDICATOR = new TagInfo( - "Image Color Indicator", 0x84e7, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BACKGROUND_COLOR_INDICATOR = new TagInfo( - "Background Color Indicator", 0x84e8, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_COLOR_VALUE = new TagInfo( - "Image Color Value", 0x84e9, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BACKGROUND_COLOR_VALUE = new TagInfo( - "Background Color Value", 0x84ea, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PIXEL_INTENSITY_RANGE = new TagInfo( - "Pixel Intensity Range", 0x84eb, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TRANSPARENCY_INDICATOR = new TagInfo( - "Transparency Indicator", 0x84ec, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_COLOR_CHARACTERIZATION = new TagInfo( - "Color Characterization", 0x84ed, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_HCUSAGE = new TagInfo("HCUsage", - 0x84ee, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SEMINFO = new TagInfo("SEMInfo", - 0x8546, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_AFCP_IPTC = new TagInfo("AFCP_ IPTC", - 0x8568, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MODEL_TRANSFORM = new TagInfo( - "Model Transform", 0x85d8, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_LEAF_DATA = new TagInfo("Leaf Data", - 0x8606, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PHOTOSHOP_SETTINGS = new TagInfo( - "Photoshop Settings", 0x8649, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_EXIF_OFFSET = new TagInfo.Offset( - "Exif Offset", 0x8769, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ICC_PROFILE = new TagInfo( - "ICC_ Profile", 0x8773, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_LAYER = new TagInfo( - "Image Layer", 0x87ac, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GEO_TIFF_DIRECTORY = new TagInfo( - "Geo Tiff Directory", 0x87af, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GEO_TIFF_DOUBLE_PARAMS = new TagInfo( - "Geo Tiff Double Params", 0x87b0, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GEO_TIFF_ASCII_PARAMS = new TagInfo( - "Geo Tiff Ascii Params", 0x87b1, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_EXPOSURE_PROGRAM = new TagInfo( - "Exposure Program", 0x8822, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int EXPOSURE_PROGRAM_VALUE_MANUAL = 1; - public static final int EXPOSURE_PROGRAM_VALUE_PROGRAM_AE = 2; - public static final int EXPOSURE_PROGRAM_VALUE_APERTURE_PRIORITY_AE = 3; - public static final int EXPOSURE_PROGRAM_VALUE_SHUTTER_SPEED_PRIORITY_AE = 4; - public static final int EXPOSURE_PROGRAM_VALUE_CREATIVE_SLOW_SPEED = 5; - public static final int EXPOSURE_PROGRAM_VALUE_ACTION_HIGH_SPEED = 6; - public static final int EXPOSURE_PROGRAM_VALUE_PORTRAIT = 7; - public static final int EXPOSURE_PROGRAM_VALUE_LANDSCAPE = 8; - public static final TagInfo EXIF_TAG_SPECTRAL_SENSITIVITY = new TagInfo( - "Spectral Sensitivity", 0x8824, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_GPSINFO = new TagInfo.Offset( - "GPSInfo", 0x8825, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ISO = new TagInfo("ISO", 0x8827, - FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_OPTO__ELECTRIC_CONV_FACTOR = new TagInfo( - "Opto - Electric Conv Factor", 0x8828, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_INTERLACE = new TagInfo("Interlace", - 0x8829, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TIME_ZONE_OFFSET = new TagInfo( - "Time Zone Offset", 0x882a, FIELD_TYPE_SSHORT, -1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SELF_TIMER_MODE = new TagInfo( - "Self Timer Mode", 0x882b, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FAX_RECV_PARAMS = new TagInfo( - "Fax Recv Params", 0x885c, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FAX_SUB_ADDRESS = new TagInfo( - "Fax Sub Address", 0x885d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FAX_RECV_TIME = new TagInfo( - "Fax Recv Time", 0x885e, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_LEAF_SUB_IFD = new TagInfo( - "Leaf Sub IFD", 0x888a, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_EXIF_VERSION = new TagInfo( - "Exif Version", 0x9000, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_DATE_TIME_ORIGINAL = new TagInfo( - "Date Time Original", 0x9003, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_CREATE_DATE = new TagInfo( - "Create Date", 0x9004, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_COMPONENTS_CONFIGURATION = new TagInfo( - "Components Configuration", 0x9101, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_COMPRESSED_BITS_PER_PIXEL = new TagInfo( - "Compressed Bits Per Pixel", 0x9102, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SHUTTER_SPEED_VALUE = new TagInfo( - "Shutter Speed Value", 0x9201, FIELD_TYPE_SRATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_APERTURE_VALUE = new TagInfo( - "Aperture Value", 0x9202, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_BRIGHTNESS_VALUE = new TagInfo( - "Brightness Value", 0x9203, FIELD_TYPE_SRATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_EXPOSURE_COMPENSATION = new TagInfo( - "Exposure Compensation", 0x9204, FIELD_TYPE_SRATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_MAX_APERTURE_VALUE = new TagInfo( - "Max Aperture Value", 0x9205, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SUBJECT_DISTANCE = new TagInfo( - "Subject Distance", 0x9206, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_METERING_MODE = new TagInfo( - "Metering Mode", 0x9207, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int METERING_MODE_VALUE_AVERAGE = 1; - public static final int METERING_MODE_VALUE_CENTER_WEIGHTED_AVERAGE = 2; - public static final int METERING_MODE_VALUE_SPOT = 3; - public static final int METERING_MODE_VALUE_MULTI_SPOT = 4; - public static final int METERING_MODE_VALUE_MULTI_SEGMENT = 5; - public static final int METERING_MODE_VALUE_PARTIAL = 6; - public static final int METERING_MODE_VALUE_OTHER = 255; - public static final TagInfo EXIF_TAG_LIGHT_SOURCE = new TagInfo( - "Light Source", 0x9208, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int LIGHT_SOURCE_VALUE_DAYLIGHT = 1; - public static final int LIGHT_SOURCE_VALUE_FLUORESCENT = 2; - public static final int LIGHT_SOURCE_VALUE_TUNGSTEN = 3; - public static final int LIGHT_SOURCE_VALUE_FLASH = 4; - public static final int LIGHT_SOURCE_VALUE_FINE_WEATHER = 9; - public static final int LIGHT_SOURCE_VALUE_CLOUDY = 10; - public static final int LIGHT_SOURCE_VALUE_SHADE = 11; - public static final int LIGHT_SOURCE_VALUE_DAYLIGHT_FLUORESCENT = 12; - public static final int LIGHT_SOURCE_VALUE_DAY_WHITE_FLUORESCENT = 13; - public static final int LIGHT_SOURCE_VALUE_COOL_WHITE_FLUORESCENT = 14; - public static final int LIGHT_SOURCE_VALUE_WHITE_FLUORESCENT = 15; - public static final int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_A = 17; - public static final int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_B = 18; - public static final int LIGHT_SOURCE_VALUE_STANDARD_LIGHT_C = 19; - public static final int LIGHT_SOURCE_VALUE_D55 = 20; - public static final int LIGHT_SOURCE_VALUE_D65 = 21; - public static final int LIGHT_SOURCE_VALUE_D75 = 22; - public static final int LIGHT_SOURCE_VALUE_D50 = 23; - public static final int LIGHT_SOURCE_VALUE_ISO_STUDIO_TUNGSTEN = 24; - public static final int LIGHT_SOURCE_VALUE_OTHER = 255; - public static final TagInfo EXIF_TAG_FLASH = new TagInfo("Flash", 0x9209, - FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int FLASH_VALUE_NO_FLASH = 0x0; - public static final int FLASH_VALUE_FIRED = 0x1; - public static final int FLASH_VALUE_FIRED_RETURN_NOT_DETECTED = 0x5; - public static final int FLASH_VALUE_FIRED_RETURN_DETECTED = 0x7; - public static final int FLASH_VALUE_ON_DID_NOT_FIRE = 0x8; - public static final int FLASH_VALUE_ON = 0x9; - public static final int FLASH_VALUE_ON_RETURN_NOT_DETECTED = 0xd; - public static final int FLASH_VALUE_ON_RETURN_DETECTED = 0xf; - public static final int FLASH_VALUE_OFF = 0x10; - public static final int FLASH_VALUE_OFF_DID_NOT_FIRE_RETURN_NOT_DETECTED = 0x14; - public static final int FLASH_VALUE_AUTO_DID_NOT_FIRE = 0x18; - public static final int FLASH_VALUE_AUTO_FIRED = 0x19; - public static final int FLASH_VALUE_AUTO_FIRED_RETURN_NOT_DETECTED = 0x1d; - public static final int FLASH_VALUE_AUTO_FIRED_RETURN_DETECTED = 0x1f; - public static final int FLASH_VALUE_NO_FLASH_FUNCTION = 0x20; - public static final int FLASH_VALUE_OFF_NO_FLASH_FUNCTION = 0x30; - public static final int FLASH_VALUE_FIRED_RED_EYE_REDUCTION = 0x41; - public static final int FLASH_VALUE_FIRED_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x45; - public static final int FLASH_VALUE_FIRED_RED_EYE_REDUCTION_RETURN_DETECTED = 0x47; - public static final int FLASH_VALUE_ON_RED_EYE_REDUCTION = 0x49; - public static final int FLASH_VALUE_ON_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x4d; - public static final int FLASH_VALUE_ON_RED_EYE_REDUCTION_RETURN_DETECTED = 0x4f; - public static final int FLASH_VALUE_OFF_RED_EYE_REDUCTION = 0x50; - public static final int FLASH_VALUE_AUTO_DID_NOT_FIRE_RED_EYE_REDUCTION = 0x58; - public static final int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION = 0x59; - public static final int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION_RETURN_NOT_DETECTED = 0x5d; - public static final int FLASH_VALUE_AUTO_FIRED_RED_EYE_REDUCTION_RETURN_DETECTED = 0x5f; - public static final TagInfo EXIF_TAG_FOCAL_LENGTH = new TagInfo( - "Focal Length", 0x920a, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FLASH_ENERGY = new TagInfo( - "Flash Energy", 0x920b, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_1 = new TagInfo( - "Spatial Frequency Response", 0x920c, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_NOISE_1 = new TagInfo("Noise", 0x920d, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_XRESOLUTION = new TagInfo( - "Focal Plane XResolution", 0x920e, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_YRESOLUTION = new TagInfo( - "Focal Plane YResolution", 0x920f, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT = new TagInfo( - "Focal Plane Resolution Unit", 0x9210, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_NONE = 1; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_INCHES = 2; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_CM = 3; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_MM = 4; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_VALUE_UM = 5; - public static final TagInfo EXIF_TAG_IMAGE_NUMBER_EXIF_IFD = new TagInfo( - "Image Number", 0x9211, FIELD_TYPE_LONG, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SECURITY_CLASSIFICATION_EXIF_IFD = new TagInfo( - "Security Classification", 0x9212, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_IMAGE_HISTORY_EXIF_IFD = new TagInfo( - "Image History", 0x9213, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SUBJECT_LOCATION_1 = new TagInfo( - "Subject Location", 0x9214, FIELD_TYPE_SHORT, 4, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_EXPOSURE_INDEX = new TagInfo( - "Exposure Index", 0x9215, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_TIFF_EPSTANDARD_ID_1 = new TagInfo( - "TIFF- EPStandard ID", 0x9216, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SENSING_METHOD = new TagInfo( - "Sensing Method", 0x9217, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int SENSING_METHOD_VALUE_MONOCHROME_AREA = 1; - public static final int SENSING_METHOD_VALUE_ONE_CHIP_COLOR_AREA = 2; - public static final int SENSING_METHOD_VALUE_TWO_CHIP_COLOR_AREA = 3; - public static final int SENSING_METHOD_VALUE_THREE_CHIP_COLOR_AREA = 4; - public static final int SENSING_METHOD_VALUE_COLOR_SEQUENTIAL_AREA = 5; - public static final int SENSING_METHOD_VALUE_MONOCHROME_LINEAR = 6; - public static final int SENSING_METHOD_VALUE_TRILINEAR = 7; - public static final int SENSING_METHOD_VALUE_COLOR_SEQUENTIAL_LINEAR = 8; - public static final TagInfo EXIF_TAG_STO_NITS = new TagInfo("Sto Nits", - 0x923f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // skipping Maker Note! - public static final TagInfo EXIF_TAG_MAKER_NOTE = new TagInfo("Maker Note", - 0x927c, FIELD_TYPE_UNDEFINED, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_USER_COMMENT = new TagInfo.Text( - "UserComment", 0x9286, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SUB_SEC_TIME = new TagInfo( - "Sub Sec Time", 0x9290, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SUB_SEC_TIME_ORIGINAL = new TagInfo( - "Sub Sec Time Original", 0x9291, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SUB_SEC_TIME_DIGITIZED = new TagInfo( - "Sub Sec Time Digitized", 0x9292, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_IMAGE_SOURCE_DATA = new TagInfo( - "Image Source Data", 0x935c, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPTITLE = new TagInfo("XPTitle", - 0x9c9b, FIELD_TYPE_BYTE, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPCOMMENT = new TagInfo("XPComment", - 0x9c9c, FIELD_TYPE_BYTE, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPAUTHOR = new TagInfo("XPAuthor", - 0x9c9d, FIELD_TYPE_BYTE, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPKEYWORDS = new TagInfo("XPKeywords", - 0x9c9e, FIELD_TYPE_BYTE, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_XPSUBJECT = new TagInfo("XPSubject", - 0x9c9f, FIELD_TYPE_BYTE, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_FLASHPIX_VERSION = new TagInfo( - "Flashpix Version", 0xa000, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_COLOR_SPACE = new TagInfo( - "Color Space", 0xa001, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int COLOR_SPACE_VALUE_SRGB = 1; - public static final int COLOR_SPACE_VALUE_ADOBE_RGB = 2; - public static final int COLOR_SPACE_VALUE_UNCALIBRATED = 65535; - public static final TagInfo EXIF_TAG_EXIF_IMAGE_WIDTH = new TagInfo( - "Exif Image Width", 0xa002, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_EXIF_IMAGE_LENGTH = new TagInfo( - "Exif Image Length", 0xa003, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_RELATED_SOUND_FILE = new TagInfo( - "Related Sound File", 0xa004, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_INTEROP_OFFSET = new TagInfo.Offset( - "Interop Offset", 0xa005, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FLASH_ENERGY_EXIF_IFD = new TagInfo( - "Flash Energy", 0xa20b, FIELD_TYPE_RATIONAL, -1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_2 = new TagInfo( - "Spatial Frequency Response", 0xa20c, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_NOISE_2 = new TagInfo("Noise", 0xa20d, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_XRESOLUTION_EXIF_IFD = new TagInfo( - "Focal Plane XResolution", 0xa20e, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_YRESOLUTION_EXIF_IFD = new TagInfo( - "Focal Plane YResolution", 0xa20f, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD = new TagInfo( - "Focal Plane Resolution Unit", 0xa210, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_NONE = 1; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_INCHES = 2; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_CM = 3; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_MM = 4; - public static final int FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD_VALUE_UM = 5; - public static final TagInfo EXIF_TAG_IMAGE_NUMBER = new TagInfo( - "Image Number", 0xa211, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SECURITY_CLASSIFICATION = new TagInfo( - "Security Classification", 0xa212, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_HISTORY = new TagInfo( - "Image History", 0xa213, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SUBJECT_LOCATION_2 = new TagInfo( - "Subject Location", 0xa214, FIELD_TYPE_SHORT, 2, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_EXPOSURE_INDEX_EXIF_IFD = new TagInfo( - "Exposure Index", 0xa215, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_TIFF_EPSTANDARD_ID_2 = new TagInfo( - "TIFF- EPStandard ID", 0xa216, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SENSING_METHOD_EXIF_IFD = new TagInfo( - "Sensing Method", 0xa217, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int SENSING_METHOD_EXIF_IFD_VALUE_NOT_DEFINED = 1; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_ONE_CHIP_COLOR_AREA = 2; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_TWO_CHIP_COLOR_AREA = 3; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_THREE_CHIP_COLOR_AREA = 4; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_COLOR_SEQUENTIAL_AREA = 5; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_TRILINEAR = 7; - public static final int SENSING_METHOD_EXIF_IFD_VALUE_COLOR_SEQUENTIAL_LINEAR = 8; - public static final TagInfo EXIF_TAG_FILE_SOURCE = new TagInfo( - "File Source", 0xa300, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int FILE_SOURCE_VALUE_FILM_SCANNER = 1; - public static final int FILE_SOURCE_VALUE_REFLECTION_PRINT_SCANNER = 2; - public static final int FILE_SOURCE_VALUE_DIGITAL_CAMERA = 3; - public static final TagInfo EXIF_TAG_SCENE_TYPE = new TagInfo("Scene Type", - 0xa301, FIELD_TYPE_UNDEFINED, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_CFAPATTERN = new TagInfo("CFAPattern", - 0xa302, FIELD_TYPE_UNDEFINED, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_CUSTOM_RENDERED = new TagInfo( - "Custom Rendered", 0xa401, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int CUSTOM_RENDERED_VALUE_NORMAL = 0; - public static final int CUSTOM_RENDERED_VALUE_CUSTOM = 1; - public static final TagInfo EXIF_TAG_EXPOSURE_MODE = new TagInfo( - "Exposure Mode", 0xa402, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int EXPOSURE_MODE_VALUE_AUTO = 0; - public static final int EXPOSURE_MODE_VALUE_MANUAL = 1; - public static final int EXPOSURE_MODE_VALUE_AUTO_BRACKET = 2; - public static final TagInfo EXIF_TAG_WHITE_BALANCE_1 = new TagInfo( - "White Balance", 0xa403, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int WHITE_BALANCE_1_VALUE_AUTO = 0; - public static final int WHITE_BALANCE_1_VALUE_MANUAL = 1; - public static final TagInfo EXIF_TAG_DIGITAL_ZOOM_RATIO = new TagInfo( - "Digital Zoom Ratio", 0xa404, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_FOCAL_LENGTH_IN_35MM_FORMAT = new TagInfo( - "Focal Length In 3 5mm Format", 0xa405, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SCENE_CAPTURE_TYPE = new TagInfo( - "Scene Capture Type", 0xa406, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int SCENE_CAPTURE_TYPE_VALUE_STANDARD = 0; - public static final int SCENE_CAPTURE_TYPE_VALUE_LANDSCAPE = 1; - public static final int SCENE_CAPTURE_TYPE_VALUE_PORTRAIT = 2; - public static final int SCENE_CAPTURE_TYPE_VALUE_NIGHT = 3; - public static final TagInfo EXIF_TAG_GAIN_CONTROL = new TagInfo( - "Gain Control", 0xa407, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int GAIN_CONTROL_VALUE_NONE = 0; - public static final int GAIN_CONTROL_VALUE_LOW_GAIN_UP = 1; - public static final int GAIN_CONTROL_VALUE_HIGH_GAIN_UP = 2; - public static final int GAIN_CONTROL_VALUE_LOW_GAIN_DOWN = 3; - public static final int GAIN_CONTROL_VALUE_HIGH_GAIN_DOWN = 4; - public static final TagInfo EXIF_TAG_CONTRAST_1 = new TagInfo("Contrast", - 0xa408, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int CONTRAST_1_VALUE_NORMAL = 0; - public static final int CONTRAST_1_VALUE_LOW = 1; - public static final int CONTRAST_1_VALUE_HIGH = 2; - public static final TagInfo EXIF_TAG_SATURATION_1 = new TagInfo( - "Saturation", 0xa409, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int SATURATION_1_VALUE_NORMAL = 0; - public static final int SATURATION_1_VALUE_LOW = 1; - public static final int SATURATION_1_VALUE_HIGH = 2; - public static final TagInfo EXIF_TAG_SHARPNESS_1 = new TagInfo("Sharpness", - 0xa40a, FIELD_TYPE_SHORT, 1, EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int SHARPNESS_1_VALUE_NORMAL = 0; - public static final int SHARPNESS_1_VALUE_SOFT = 1; - public static final int SHARPNESS_1_VALUE_HARD = 2; - public static final TagInfo EXIF_TAG_DEVICE_SETTING_DESCRIPTION = new TagInfo( - "Device Setting Description", 0xa40b, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_SUBJECT_DISTANCE_RANGE = new TagInfo( - "Subject Distance Range", 0xa40c, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_EXIF_IFD); - // tag constants public static final int SUBJECT_DISTANCE_RANGE_VALUE_MACRO = 1; - public static final int SUBJECT_DISTANCE_RANGE_VALUE_CLOSE = 2; - public static final int SUBJECT_DISTANCE_RANGE_VALUE_DISTANT = 3; - public static final TagInfo EXIF_TAG_IMAGE_UNIQUE_ID = new TagInfo( - "Image Unique ID", 0xa420, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_GDALMETADATA = new TagInfo( - "GDALMetadata", 0xa480, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GDALNO_DATA = new TagInfo( - "GDALNo Data", 0xa481, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_GAMMA = new TagInfo("Gamma", 0xa500, - FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_PIXEL_FORMAT = new TagInfo( - "Pixel Format", 0xbc01, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int PIXEL_FORMAT_VALUE_BLACK_AND_WHITE = 0x5; - public static final int PIXEL_FORMAT_VALUE_8_BIT_GRAY = 0x8; - public static final int PIXEL_FORMAT_VALUE_16_BIT_BGR555 = 0x9; - public static final int PIXEL_FORMAT_VALUE_16_BIT_BGR565 = 0xa; - public static final int PIXEL_FORMAT_VALUE_16_BIT_GRAY = 0xb; - public static final int PIXEL_FORMAT_VALUE_24_BIT_BGR = 0xc; - public static final int PIXEL_FORMAT_VALUE_24_BIT_RGB = 0xd; - public static final int PIXEL_FORMAT_VALUE_32_BIT_BGR = 0xe; - public static final int PIXEL_FORMAT_VALUE_32_BIT_BGRA = 0xf; - public static final int PIXEL_FORMAT_VALUE_32_BIT_PBGRA = 0x10; - public static final int PIXEL_FORMAT_VALUE_32_BIT_GRAY_FLOAT = 0x11; - public static final int PIXEL_FORMAT_VALUE_48_BIT_RGB_FIXED_POINT = 0x12; - public static final int PIXEL_FORMAT_VALUE_32_BIT_BGR101010 = 0x13; - public static final int PIXEL_FORMAT_VALUE_48_BIT_RGB = 0x15; - public static final int PIXEL_FORMAT_VALUE_64_BIT_RGBA = 0x16; - public static final int PIXEL_FORMAT_VALUE_64_BIT_PRGBA = 0x17; - public static final int PIXEL_FORMAT_VALUE_96_BIT_RGB_FIXED_POINT = 0x18; - public static final int PIXEL_FORMAT_VALUE_128_BIT_RGBA_FLOAT = 0x19; - public static final int PIXEL_FORMAT_VALUE_128_BIT_PRGBA_FLOAT = 0x1a; - public static final int PIXEL_FORMAT_VALUE_128_BIT_RGB_FLOAT = 0x1b; - public static final int PIXEL_FORMAT_VALUE_32_BIT_CMYK = 0x1c; - public static final int PIXEL_FORMAT_VALUE_64_BIT_RGBA_FIXED_POINT = 0x1d; - public static final int PIXEL_FORMAT_VALUE_128_BIT_RGBA_FIXED_POINT = 0x1e; - public static final int PIXEL_FORMAT_VALUE_64_BIT_CMYK = 0x1f; - public static final int PIXEL_FORMAT_VALUE_24_BIT_3_CHANNELS = 0x20; - public static final int PIXEL_FORMAT_VALUE_32_BIT_4_CHANNELS = 0x21; - public static final int PIXEL_FORMAT_VALUE_40_BIT_5_CHANNELS = 0x22; - public static final int PIXEL_FORMAT_VALUE_48_BIT_6_CHANNELS = 0x23; - public static final int PIXEL_FORMAT_VALUE_56_BIT_7_CHANNELS = 0x24; - public static final int PIXEL_FORMAT_VALUE_64_BIT_8_CHANNELS = 0x25; - public static final int PIXEL_FORMAT_VALUE_48_BIT_3_CHANNELS = 0x26; - public static final int PIXEL_FORMAT_VALUE_64_BIT_4_CHANNELS = 0x27; - public static final int PIXEL_FORMAT_VALUE_80_BIT_5_CHANNELS = 0x28; - public static final int PIXEL_FORMAT_VALUE_96_BIT_6_CHANNELS = 0x29; - public static final int PIXEL_FORMAT_VALUE_112_BIT_7_CHANNELS = 0x2a; - public static final int PIXEL_FORMAT_VALUE_128_BIT_8_CHANNELS = 0x2b; - public static final int PIXEL_FORMAT_VALUE_40_BIT_CMYK_ALPHA = 0x2c; - public static final int PIXEL_FORMAT_VALUE_80_BIT_CMYK_ALPHA = 0x2d; - public static final int PIXEL_FORMAT_VALUE_32_BIT_3_CHANNELS_ALPHA = 0x2e; - public static final int PIXEL_FORMAT_VALUE_40_BIT_4_CHANNELS_ALPHA = 0x2f; - public static final int PIXEL_FORMAT_VALUE_48_BIT_5_CHANNELS_ALPHA = 0x30; - public static final int PIXEL_FORMAT_VALUE_56_BIT_6_CHANNELS_ALPHA = 0x31; - public static final int PIXEL_FORMAT_VALUE_64_BIT_7_CHANNELS_ALPHA = 0x32; - public static final int PIXEL_FORMAT_VALUE_72_BIT_8_CHANNELS_ALPHA = 0x33; - public static final int PIXEL_FORMAT_VALUE_64_BIT_3_CHANNELS_ALPHA = 0x34; - public static final int PIXEL_FORMAT_VALUE_80_BIT_4_CHANNELS_ALPHA = 0x35; - public static final int PIXEL_FORMAT_VALUE_96_BIT_5_CHANNELS_ALPHA = 0x36; - public static final int PIXEL_FORMAT_VALUE_112_BIT_6_CHANNELS_ALPHA = 0x37; - public static final int PIXEL_FORMAT_VALUE_128_BIT_7_CHANNELS_ALPHA = 0x38; - public static final int PIXEL_FORMAT_VALUE_144_BIT_8_CHANNELS_ALPHA = 0x39; - public static final int PIXEL_FORMAT_VALUE_64_BIT_RGBA_HALF = 0x3a; - public static final int PIXEL_FORMAT_VALUE_48_BIT_RGB_HALF = 0x3b; - public static final int PIXEL_FORMAT_VALUE_32_BIT_RGBE = 0x3d; - public static final int PIXEL_FORMAT_VALUE_16_BIT_GRAY_HALF = 0x3e; - public static final int PIXEL_FORMAT_VALUE_32_BIT_GRAY_FIXED_POINT = 0x3f; - public static final TagInfo EXIF_TAG_TRANSFOMATION = new TagInfo( - "Transfomation", 0xbc02, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int TRANSFOMATION_VALUE_HORIZONTAL_NORMAL = 0; - public static final int TRANSFOMATION_VALUE_MIRROR_VERTICAL = 1; - public static final int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL = 2; - public static final int TRANSFOMATION_VALUE_ROTATE_180 = 3; - public static final int TRANSFOMATION_VALUE_ROTATE_90_CW = 4; - public static final int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW = 5; - public static final int TRANSFOMATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW = 6; - public static final int TRANSFOMATION_VALUE_ROTATE_270_CW = 7; - public static final TagInfo EXIF_TAG_UNCOMPRESSED = new TagInfo( - "Uncompressed", 0xbc03, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int UNCOMPRESSED_VALUE_NO = 0; - public static final int UNCOMPRESSED_VALUE_YES = 1; - public static final TagInfo EXIF_TAG_IMAGE_TYPE = new TagInfo("Image Type", - 0xbc04, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_WIDTH = new TagInfo( - "Image Width", 0xbc80, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_HEIGHT = new TagInfo( - "Image Height", 0xbc81, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_WIDTH_RESOLUTION = new TagInfo( - "Width Resolution", 0xbc82, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_HEIGHT_RESOLUTION = new TagInfo( - "Height Resolution", 0xbc83, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // might be an offset? - public static final TagInfo EXIF_TAG_IMAGE_OFFSET = new TagInfo( - "Image Offset", 0xbcc0, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_BYTE_COUNT = new TagInfo( - "Image Byte Count", 0xbcc1, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // might be an offset? - public static final TagInfo EXIF_TAG_ALPHA_OFFSET = new TagInfo( - "Alpha Offset", 0xbcc2, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ALPHA_BYTE_COUNT = new TagInfo( - "Alpha Byte Count", 0xbcc3, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_IMAGE_DATA_DISCARD = new TagInfo( - "Image Data Discard", 0xbcc4, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int IMAGE_DATA_DISCARD_VALUE_FULL_RESOLUTION = 0; - public static final int IMAGE_DATA_DISCARD_VALUE_FLEXBITS_DISCARDED = 1; - public static final int IMAGE_DATA_DISCARD_VALUE_HIGH_PASS_FREQUENCY_DATA_DISCARDED = 2; - public static final int IMAGE_DATA_DISCARD_VALUE_HIGHPASS_AND_LOW_PASS_FREQUENCY_DATA_DISCARDED = 3; - public static final TagInfo EXIF_TAG_ALPHA_DATA_DISCARD = new TagInfo( - "Alpha Data Discard", 0xbcc5, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int ALPHA_DATA_DISCARD_VALUE_FULL_RESOLUTION = 0; - public static final int ALPHA_DATA_DISCARD_VALUE_FLEXBITS_DISCARDED = 1; - public static final int ALPHA_DATA_DISCARD_VALUE_HIGH_PASS_FREQUENCY_DATA_DISCARDED = 2; - public static final int ALPHA_DATA_DISCARD_VALUE_HIGHPASS_AND_LOW_PASS_FREQUENCY_DATA_DISCARDED = 3; - public static final TagInfo EXIF_TAG_OCE_SCANJOB_DESC = new TagInfo( - "Oce Scanjob Desc", 0xc427, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_OCE_APPLICATION_SELECTOR = new TagInfo( - "Oce Application Selector", 0xc428, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_OCE_IDNUMBER = new TagInfo( - "Oce IDNumber", 0xc429, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_OCE_IMAGE_LOGIC = new TagInfo( - "Oce Image Logic", 0xc42a, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ANNOTATIONS = new TagInfo( - "Annotations", 0xc44f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_PRINT_IM = new TagInfo("Print IM", - 0xc4a5, FIELD_TYPE_UNDEFINED, 1, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_DNG_VERSION = new TagInfo( - "DNG Version", 0xc612, FIELD_TYPE_BYTE, 4, EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_DNG_BACKWARD_VERSION = new TagInfo( - "DNG Backward Version", 0xc613, FIELD_TYPE_BYTE, 4, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_UNIQUE_CAMERA_MODEL = new TagInfo( - "Unique Camera Model", 0xc614, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_LOCALIZED_CAMERA_MODEL = new TagInfo( - "Localized Camera Model", 0xc615, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_CFAPLANE_COLOR = new TagInfo( - "CFAPlane Color", 0xc616, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CFALAYOUT = new TagInfo("CFALayout", - 0xc617, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - // tag constants public static final int CFALAYOUT_VALUE_RECTANGULAR = 1; - public static final int CFALAYOUT_VALUE_EVEN_COLUMNS_OFFSET_DOWN_1_2_ROW = 2; - public static final int CFALAYOUT_VALUE_EVEN_COLUMNS_OFFSET_UP_1_2_ROW = 3; - public static final int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_RIGHT_1_2_COLUMN = 4; - public static final int CFALAYOUT_VALUE_EVEN_ROWS_OFFSET_LEFT_1_2_COLUMN = 5; - public static final TagInfo EXIF_TAG_LINEARIZATION_TABLE = new TagInfo( - "Linearization Table", 0xc618, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BLACK_LEVEL_REPEAT_DIM = new TagInfo( - "Black Level Repeat Dim", 0xc619, FIELD_TYPE_DESCRIPTION_UNKNOWN, - 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BLACK_LEVEL = new TagInfo( - "Black Level", 0xc61a, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BLACK_LEVEL_DELTA_H = new TagInfo( - "Black Level Delta H", 0xc61b, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_BLACK_LEVEL_DELTA_V = new TagInfo( - "Black Level Delta V", 0xc61c, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_WHITE_LEVEL = new TagInfo( - "White Level", 0xc61d, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DEFAULT_SCALE = new TagInfo( - "Default Scale", 0xc61e, FIELD_TYPE_RATIONAL, 2, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_DEFAULT_CROP_ORIGIN = new TagInfo( - "Default Crop Origin", 0xc61f, FIELD_TYPE_LONG, 2, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_DEFAULT_CROP_SIZE = new TagInfo( - "Default Crop Size", 0xc620, FIELD_TYPE_LONG, 2, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_COLOR_MATRIX_1 = new TagInfo( - "Color Matrix 1", 0xc621, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_COLOR_MATRIX_2 = new TagInfo( - "Color Matrix 2", 0xc622, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CAMERA_CALIBRATION_1 = new TagInfo( - "Camera Calibration 1", 0xc623, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CAMERA_CALIBRATION_2 = new TagInfo( - "Camera Calibration 2", 0xc624, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_REDUCTION_MATRIX_1 = new TagInfo( - "Reduction Matrix 1", 0xc625, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_REDUCTION_MATRIX_2 = new TagInfo( - "Reduction Matrix 2", 0xc626, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ANALOG_BALANCE = new TagInfo( - "Analog Balance", 0xc627, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_AS_SHOT_NEUTRAL = new TagInfo( - "As Shot Neutral", 0xc628, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_AS_SHOT_WHITE_XY = new TagInfo( - "As Shot White XY", 0xc629, FIELD_TYPE_RATIONAL, 2, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_BASELINE_EXPOSURE = new TagInfo( - "Baseline Exposure", 0xc62a, FIELD_TYPE_SRATIONAL, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_BASELINE_NOISE = new TagInfo( - "Baseline Noise", 0xc62b, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_BASELINE_SHARPNESS = new TagInfo( - "Baseline Sharpness", 0xc62c, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_BAYER_GREEN_SPLIT = new TagInfo( - "Bayer Green Split", 0xc62d, FIELD_TYPE_LONG, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_LINEAR_RESPONSE_LIMIT = new TagInfo( - "Linear Response Limit", 0xc62e, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_CAMERA_SERIAL_NUMBER = new TagInfo( - "Camera Serial Number", 0xc62f, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_DNG_LENS_INFO = new TagInfo( - "DNG Lens Info", 0xc630, FIELD_TYPE_RATIONAL, 4, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_CHROMA_BLUR_RADIUS = new TagInfo( - "Chroma Blur Radius", 0xc631, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_ANTI_ALIAS_STRENGTH = new TagInfo( - "Anti Alias Strength", 0xc632, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_SHADOW_SCALE = new TagInfo( - "Shadow Scale", 0xc633, FIELD_TYPE_RATIONAL, 1, EXIF_DIRECTORY_IFD0); - // poly tag public static final TagInfo2 EXIF_TAG_SR2PRIVATE = new TagInfo2( "SR2Private", 0xc634, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DNG_ADOBE_DATA = new TagInfo( - "DNG Adobe Data", 0xc634, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DNG_PENTAX_DATA = new TagInfo( - "DNG Pentax Data", 0xc634, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_DNG_PRIVATE_DATA = new TagInfo( - "DNG Private Data", 0xc634, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_MAKER_NOTE_SAFETY = new TagInfo( - "Maker Note Safety", 0xc635, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int MAKER_NOTE_SAFETY_VALUE_UNSAFE = 0; - public static final int MAKER_NOTE_SAFETY_VALUE_SAFE = 1; - public static final TagInfo EXIF_TAG_CALIBRATION_ILLUMINANT_1 = new TagInfo( - "Calibration Illuminant 1", 0xc65a, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int CALIBRATION_ILLUMINANT_1_VALUE_DAYLIGHT = 1; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_FLUORESCENT = 2; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_TUNGSTEN = 3; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_FLASH = 4; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_FINE_WEATHER = 9; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_CLOUDY = 10; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_SHADE = 11; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_DAYLIGHT_FLUORESCENT = 12; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_DAY_WHITE_FLUORESCENT = 13; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_COOL_WHITE_FLUORESCENT = 14; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_WHITE_FLUORESCENT = 15; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_A = 17; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_B = 18; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_STANDARD_LIGHT_C = 19; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_D55 = 20; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_D65 = 21; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_D75 = 22; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_D50 = 23; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_ISO_STUDIO_TUNGSTEN = 24; - public static final int CALIBRATION_ILLUMINANT_1_VALUE_OTHER = 255; - public static final TagInfo EXIF_TAG_CALIBRATION_ILLUMINANT_2 = new TagInfo( - "Calibration Illuminant 2", 0xc65b, FIELD_TYPE_SHORT, 1, - EXIF_DIRECTORY_IFD0); - // tag constants public static final int CALIBRATION_ILLUMINANT_2_VALUE_DAYLIGHT = 1; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_FLUORESCENT = 2; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_TUNGSTEN = 3; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_FLASH = 4; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_FINE_WEATHER = 9; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_CLOUDY = 10; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_SHADE = 11; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_DAYLIGHT_FLUORESCENT = 12; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_DAY_WHITE_FLUORESCENT = 13; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_COOL_WHITE_FLUORESCENT = 14; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_WHITE_FLUORESCENT = 15; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_A = 17; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_B = 18; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_STANDARD_LIGHT_C = 19; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_D55 = 20; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_D65 = 21; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_D75 = 22; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_D50 = 23; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_ISO_STUDIO_TUNGSTEN = 24; - public static final int CALIBRATION_ILLUMINANT_2_VALUE_OTHER = 255; - public static final TagInfo EXIF_TAG_BEST_QUALITY_SCALE = new TagInfo( - "Best Quality Scale", 0xc65c, FIELD_TYPE_RATIONAL, 1, - EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_RAW_DATA_UNIQUE_ID = new TagInfo( - "Raw Data Unique ID", 0xc65d, FIELD_TYPE_BYTE, 16, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_ALIAS_LAYER_METADATA = new TagInfo( - "Alias Layer Metadata", 0xc660, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_ORIGINAL_RAW_FILE_NAME = new TagInfo( - "Original Raw File Name", 0xc68b, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_ORIGINAL_RAW_FILE_DATA = new TagInfo( - "Original Raw File Data", 0xc68c, FIELD_TYPE_UNDEFINED, 1, - EXIF_DIRECTORY_IFD0); - public static final TagInfo EXIF_TAG_ACTIVE_AREA = new TagInfo( - "Active Area", 0xc68d, FIELD_TYPE_LONG, 4, EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_MASKED_AREAS = new TagInfo( - "Masked Areas", 0xc68e, FIELD_TYPE_LONG, 4, EXIF_DIRECTORY_SUB_IFD); - public static final TagInfo EXIF_TAG_AS_SHOT_ICCPROFILE = new TagInfo( - "As Shot ICCProfile", 0xc68f, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_AS_SHOT_PRE_PROFILE_MATRIX = new TagInfo( - "As Shot Pre Profile Matrix", 0xc690, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CURRENT_ICCPROFILE = new TagInfo( - "Current ICCProfile", 0xc691, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_CURRENT_PRE_PROFILE_MATRIX = new TagInfo( - "Current Pre Profile Matrix", 0xc692, - FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, EXIF_DIRECTORY_UNKNOWN); - public static final TagInfo EXIF_TAG_OFFSET_SCHEMA = new TagInfo( - "Offset Schema", 0xea1d, FIELD_TYPE_SLONG, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_OWNER_NAME = new TagInfo("Owner Name", - 0xfde8, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SERIAL_NUMBER = new TagInfo( - "Serial Number", 0xfde9, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_LENS = new TagInfo("Lens", 0xfdea, - FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_RAW_FILE = new TagInfo("Raw File", - 0xfe4c, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_CONVERTER = new TagInfo("Converter", - 0xfe4d, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_WHITE_BALANCE_2 = new TagInfo( - "White Balance", 0xfe4e, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_EXPOSURE = new TagInfo("Exposure", - 0xfe51, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SHADOWS = new TagInfo("Shadows", - 0xfe52, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_BRIGHTNESS = new TagInfo("Brightness", - 0xfe53, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_CONTRAST_2 = new TagInfo("Contrast", - 0xfe54, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SATURATION_2 = new TagInfo( - "Saturation", 0xfe55, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SHARPNESS_2 = new TagInfo("Sharpness", - 0xfe56, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_SMOOTHNESS = new TagInfo("Smoothness", - 0xfe57, FIELD_TYPE_ASCII, 1, EXIF_DIRECTORY_EXIF_IFD); - public static final TagInfo EXIF_TAG_MOIRE_FILTER = new TagInfo( - "Moire Filter", 0xfe58, FIELD_TYPE_ASCII, 1, - EXIF_DIRECTORY_EXIF_IFD); - - public static final TagInfo ALL_EXIF_TAGS[] = { - EXIF_TAG_INTEROP_INDEX, EXIF_TAG_INTEROP_VERSION, - EXIF_TAG_PROCESSING_SOFTWARE, EXIF_TAG_SUBFILE_TYPE, - EXIF_TAG_OLD_SUBFILE_TYPE, EXIF_TAG_IMAGE_WIDTH_IFD0, - EXIF_TAG_IMAGE_HEIGHT_IFD0, EXIF_TAG_BITS_PER_SAMPLE, - EXIF_TAG_COMPRESSION, EXIF_TAG_PHOTOMETRIC_INTERPRETATION, - EXIF_TAG_THRESHOLDING, EXIF_TAG_CELL_WIDTH, EXIF_TAG_CELL_LENGTH, - EXIF_TAG_FILL_ORDER, EXIF_TAG_DOCUMENT_NAME, - EXIF_TAG_IMAGE_DESCRIPTION, EXIF_TAG_MAKE, EXIF_TAG_MODEL, - EXIF_TAG_PREVIEW_IMAGE_START_IFD0, - EXIF_TAG_PREVIEW_IMAGE_START_SUB_IFD1, - EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD2, EXIF_TAG_ORIENTATION, - EXIF_TAG_SAMPLES_PER_PIXEL, EXIF_TAG_ROWS_PER_STRIP, - EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0, - EXIF_TAG_PREVIEW_IMAGE_LENGTH_SUB_IFD1, - EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD2, EXIF_TAG_MIN_SAMPLE_VALUE, - EXIF_TAG_MAX_SAMPLE_VALUE, EXIF_TAG_XRESOLUTION, - EXIF_TAG_YRESOLUTION, EXIF_TAG_PLANAR_CONFIGURATION, - EXIF_TAG_PAGE_NAME, EXIF_TAG_XPOSITION, EXIF_TAG_YPOSITION, - EXIF_TAG_FREE_OFFSETS, EXIF_TAG_FREE_BYTE_COUNTS, - EXIF_TAG_GRAY_RESPONSE_UNIT, EXIF_TAG_GRAY_RESPONSE_CURVE, - EXIF_TAG_T4OPTIONS, EXIF_TAG_T6OPTIONS, EXIF_TAG_RESOLUTION_UNIT, - EXIF_TAG_PAGE_NUMBER, EXIF_TAG_COLOR_RESPONSE_UNIT, - EXIF_TAG_TRANSFER_FUNCTION, EXIF_TAG_SOFTWARE, - EXIF_TAG_MODIFY_DATE, EXIF_TAG_ARTIST, EXIF_TAG_HOST_COMPUTER, - EXIF_TAG_PREDICTOR, EXIF_TAG_WHITE_POINT, - EXIF_TAG_PRIMARY_CHROMATICITIES, EXIF_TAG_COLOR_MAP, - EXIF_TAG_HALFTONE_HINTS, EXIF_TAG_TILE_WIDTH, EXIF_TAG_TILE_LENGTH, - EXIF_TAG_TILE_OFFSETS, EXIF_TAG_TILE_BYTE_COUNTS, - EXIF_TAG_BAD_FAX_LINES, EXIF_TAG_CLEAN_FAX_DATA, - EXIF_TAG_CONSECUTIVE_BAD_FAX_LINES, EXIF_TAG_SUB_IFD, - EXIF_TAG_INK_SET, EXIF_TAG_INK_NAMES, EXIF_TAG_NUMBEROF_INKS, - EXIF_TAG_DOT_RANGE, EXIF_TAG_TARGET_PRINTER, - EXIF_TAG_EXTRA_SAMPLES, EXIF_TAG_SAMPLE_FORMAT, - EXIF_TAG_SMIN_SAMPLE_VALUE, EXIF_TAG_SMAX_SAMPLE_VALUE, - EXIF_TAG_TRANSFER_RANGE, EXIF_TAG_CLIP_PATH, - EXIF_TAG_XCLIP_PATH_UNITS, EXIF_TAG_YCLIP_PATH_UNITS, - EXIF_TAG_INDEXED, EXIF_TAG_JPEGTABLES, EXIF_TAG_OPIPROXY, - EXIF_TAG_GLOBAL_PARAMETERS_IFD, EXIF_TAG_PROFILE_TYPE, - EXIF_TAG_FAX_PROFILE, EXIF_TAG_CODING_METHODS, - EXIF_TAG_VERSION_YEAR, EXIF_TAG_MODE_NUMBER, EXIF_TAG_DECODE, - EXIF_TAG_DEFAULT_IMAGE_COLOR, EXIF_TAG_JPEGPROC, - EXIF_TAG_PREVIEW_IMAGE_START_MAKER_NOTES, - EXIF_TAG_JPG_FROM_RAW_START_SUB_IFD, - EXIF_TAG_JPG_FROM_RAW_START_IFD2, EXIF_TAG_OTHER_IMAGE_START, - EXIF_TAG_PREVIEW_IMAGE_LENGTH_MAKER_NOTES, - EXIF_TAG_JPG_FROM_RAW_LENGTH_SUB_IFD, - EXIF_TAG_JPG_FROM_RAW_LENGTH_IFD2, EXIF_TAG_OTHER_IMAGE_LENGTH, - EXIF_TAG_JPEGRESTART_INTERVAL, EXIF_TAG_JPEGLOSSLESS_PREDICTORS, - EXIF_TAG_JPEGPOINT_TRANSFORMS, EXIF_TAG_JPEGQTABLES, - EXIF_TAG_JPEGDCTABLES, EXIF_TAG_JPEGACTABLES, - EXIF_TAG_YCBCR_COEFFICIENTS, EXIF_TAG_YCBCR_SUB_SAMPLING, - EXIF_TAG_YCBCR_POSITIONING, EXIF_TAG_REFERENCE_BLACK_WHITE, - EXIF_TAG_STRIP_ROW_COUNTS, EXIF_TAG_APPLICATION_NOTES, - EXIF_TAG_RELATED_IMAGE_FILE_FORMAT, EXIF_TAG_RELATED_IMAGE_WIDTH, - EXIF_TAG_RELATED_IMAGE_LENGTH, EXIF_TAG_RATING, - EXIF_TAG_RATING_PERCENT, EXIF_TAG_IMAGE_ID, - EXIF_TAG_WANG_ANNOTATION, EXIF_TAG_MATTEING, EXIF_TAG_DATA_TYPE, - EXIF_TAG_IMAGE_DEPTH, EXIF_TAG_TILE_DEPTH, EXIF_TAG_MODEL_2, - EXIF_TAG_CFAREPEAT_PATTERN_DIM, EXIF_TAG_CFAPATTERN_2, - EXIF_TAG_BATTERY_LEVEL, EXIF_TAG_COPYRIGHT, EXIF_TAG_EXPOSURE_TIME, - EXIF_TAG_FNUMBER, EXIF_TAG_MDFILE_TAG, EXIF_TAG_MDSCALE_PIXEL, - EXIF_TAG_MDCOLOR_TABLE, EXIF_TAG_MDLAB_NAME, - EXIF_TAG_MDSAMPLE_INFO, EXIF_TAG_MDPREP_DATE, EXIF_TAG_MDPREP_TIME, - EXIF_TAG_MDFILE_UNITS, EXIF_TAG_PIXEL_SCALE, EXIF_TAG_IPTC_NAA, - EXIF_TAG_INTERGRAPH_PACKET_DATA, - EXIF_TAG_INTERGRAPH_FLAG_REGISTERS, EXIF_TAG_INTERGRAPH_MATRIX, - EXIF_TAG_MODEL_TIE_POINT, EXIF_TAG_SITE, EXIF_TAG_COLOR_SEQUENCE, - EXIF_TAG_IT8HEADER, EXIF_TAG_RASTER_PADDING, - EXIF_TAG_BITS_PER_RUN_LENGTH, - EXIF_TAG_BITS_PER_EXTENDED_RUN_LENGTH, EXIF_TAG_COLOR_TABLE, - EXIF_TAG_IMAGE_COLOR_INDICATOR, - EXIF_TAG_BACKGROUND_COLOR_INDICATOR, EXIF_TAG_IMAGE_COLOR_VALUE, - EXIF_TAG_BACKGROUND_COLOR_VALUE, EXIF_TAG_PIXEL_INTENSITY_RANGE, - EXIF_TAG_TRANSPARENCY_INDICATOR, EXIF_TAG_COLOR_CHARACTERIZATION, - EXIF_TAG_HCUSAGE, EXIF_TAG_SEMINFO, EXIF_TAG_AFCP_IPTC, - EXIF_TAG_MODEL_TRANSFORM, EXIF_TAG_LEAF_DATA, - EXIF_TAG_PHOTOSHOP_SETTINGS, EXIF_TAG_EXIF_OFFSET, - EXIF_TAG_ICC_PROFILE, EXIF_TAG_IMAGE_LAYER, - EXIF_TAG_GEO_TIFF_DIRECTORY, EXIF_TAG_GEO_TIFF_DOUBLE_PARAMS, - EXIF_TAG_GEO_TIFF_ASCII_PARAMS, EXIF_TAG_EXPOSURE_PROGRAM, - EXIF_TAG_SPECTRAL_SENSITIVITY, EXIF_TAG_GPSINFO, EXIF_TAG_ISO, - EXIF_TAG_OPTO__ELECTRIC_CONV_FACTOR, EXIF_TAG_INTERLACE, - EXIF_TAG_TIME_ZONE_OFFSET, EXIF_TAG_SELF_TIMER_MODE, - EXIF_TAG_FAX_RECV_PARAMS, EXIF_TAG_FAX_SUB_ADDRESS, - EXIF_TAG_FAX_RECV_TIME, EXIF_TAG_LEAF_SUB_IFD, - EXIF_TAG_EXIF_VERSION, EXIF_TAG_DATE_TIME_ORIGINAL, - EXIF_TAG_CREATE_DATE, EXIF_TAG_COMPONENTS_CONFIGURATION, - EXIF_TAG_COMPRESSED_BITS_PER_PIXEL, EXIF_TAG_SHUTTER_SPEED_VALUE, - EXIF_TAG_APERTURE_VALUE, EXIF_TAG_BRIGHTNESS_VALUE, - EXIF_TAG_EXPOSURE_COMPENSATION, EXIF_TAG_MAX_APERTURE_VALUE, - EXIF_TAG_SUBJECT_DISTANCE, EXIF_TAG_METERING_MODE, - EXIF_TAG_LIGHT_SOURCE, EXIF_TAG_FLASH, EXIF_TAG_FOCAL_LENGTH, - EXIF_TAG_FLASH_ENERGY, EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_1, - EXIF_TAG_NOISE_1, EXIF_TAG_FOCAL_PLANE_XRESOLUTION, - EXIF_TAG_FOCAL_PLANE_YRESOLUTION, - EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT, - EXIF_TAG_IMAGE_NUMBER_EXIF_IFD, - EXIF_TAG_SECURITY_CLASSIFICATION_EXIF_IFD, - EXIF_TAG_IMAGE_HISTORY_EXIF_IFD, EXIF_TAG_SUBJECT_LOCATION_1, - EXIF_TAG_EXPOSURE_INDEX, EXIF_TAG_TIFF_EPSTANDARD_ID_1, - EXIF_TAG_SENSING_METHOD, EXIF_TAG_STO_NITS, EXIF_TAG_SUB_SEC_TIME, - EXIF_TAG_SUB_SEC_TIME_ORIGINAL, EXIF_TAG_SUB_SEC_TIME_DIGITIZED, - EXIF_TAG_IMAGE_SOURCE_DATA, EXIF_TAG_XPTITLE, EXIF_TAG_XPCOMMENT, - EXIF_TAG_XPAUTHOR, EXIF_TAG_XPKEYWORDS, EXIF_TAG_XPSUBJECT, - EXIF_TAG_FLASHPIX_VERSION, EXIF_TAG_COLOR_SPACE, - EXIF_TAG_EXIF_IMAGE_WIDTH, EXIF_TAG_EXIF_IMAGE_LENGTH, - EXIF_TAG_RELATED_SOUND_FILE, EXIF_TAG_INTEROP_OFFSET, - EXIF_TAG_FLASH_ENERGY_EXIF_IFD, - EXIF_TAG_SPATIAL_FREQUENCY_RESPONSE_2, EXIF_TAG_NOISE_2, - EXIF_TAG_FOCAL_PLANE_XRESOLUTION_EXIF_IFD, - EXIF_TAG_FOCAL_PLANE_YRESOLUTION_EXIF_IFD, - EXIF_TAG_FOCAL_PLANE_RESOLUTION_UNIT_EXIF_IFD, - EXIF_TAG_IMAGE_NUMBER, EXIF_TAG_SECURITY_CLASSIFICATION, - EXIF_TAG_IMAGE_HISTORY, EXIF_TAG_SUBJECT_LOCATION_2, - EXIF_TAG_EXPOSURE_INDEX_EXIF_IFD, EXIF_TAG_TIFF_EPSTANDARD_ID_2, - EXIF_TAG_SENSING_METHOD_EXIF_IFD, EXIF_TAG_FILE_SOURCE, - EXIF_TAG_SCENE_TYPE, EXIF_TAG_CFAPATTERN, EXIF_TAG_CUSTOM_RENDERED, - EXIF_TAG_EXPOSURE_MODE, EXIF_TAG_WHITE_BALANCE_1, - EXIF_TAG_DIGITAL_ZOOM_RATIO, EXIF_TAG_FOCAL_LENGTH_IN_35MM_FORMAT, - EXIF_TAG_SCENE_CAPTURE_TYPE, EXIF_TAG_GAIN_CONTROL, - EXIF_TAG_CONTRAST_1, EXIF_TAG_SATURATION_1, EXIF_TAG_SHARPNESS_1, - EXIF_TAG_DEVICE_SETTING_DESCRIPTION, - EXIF_TAG_SUBJECT_DISTANCE_RANGE, EXIF_TAG_IMAGE_UNIQUE_ID, - EXIF_TAG_GDALMETADATA, EXIF_TAG_GDALNO_DATA, EXIF_TAG_GAMMA, - EXIF_TAG_PIXEL_FORMAT, EXIF_TAG_TRANSFOMATION, - EXIF_TAG_UNCOMPRESSED, EXIF_TAG_IMAGE_TYPE, EXIF_TAG_IMAGE_WIDTH, - EXIF_TAG_IMAGE_HEIGHT, EXIF_TAG_WIDTH_RESOLUTION, - EXIF_TAG_HEIGHT_RESOLUTION, EXIF_TAG_IMAGE_OFFSET, - EXIF_TAG_IMAGE_BYTE_COUNT, EXIF_TAG_ALPHA_OFFSET, - EXIF_TAG_ALPHA_BYTE_COUNT, EXIF_TAG_IMAGE_DATA_DISCARD, - EXIF_TAG_ALPHA_DATA_DISCARD, EXIF_TAG_OCE_SCANJOB_DESC, - EXIF_TAG_OCE_APPLICATION_SELECTOR, EXIF_TAG_OCE_IDNUMBER, - EXIF_TAG_OCE_IMAGE_LOGIC, EXIF_TAG_ANNOTATIONS, EXIF_TAG_PRINT_IM, - EXIF_TAG_DNG_VERSION, EXIF_TAG_DNG_BACKWARD_VERSION, - EXIF_TAG_UNIQUE_CAMERA_MODEL, EXIF_TAG_LOCALIZED_CAMERA_MODEL, - EXIF_TAG_CFAPLANE_COLOR, EXIF_TAG_CFALAYOUT, - EXIF_TAG_LINEARIZATION_TABLE, EXIF_TAG_BLACK_LEVEL_REPEAT_DIM, - EXIF_TAG_BLACK_LEVEL, EXIF_TAG_BLACK_LEVEL_DELTA_H, - EXIF_TAG_BLACK_LEVEL_DELTA_V, EXIF_TAG_WHITE_LEVEL, - EXIF_TAG_DEFAULT_SCALE, EXIF_TAG_DEFAULT_CROP_ORIGIN, - EXIF_TAG_DEFAULT_CROP_SIZE, EXIF_TAG_COLOR_MATRIX_1, - EXIF_TAG_COLOR_MATRIX_2, EXIF_TAG_CAMERA_CALIBRATION_1, - EXIF_TAG_CAMERA_CALIBRATION_2, EXIF_TAG_REDUCTION_MATRIX_1, - EXIF_TAG_REDUCTION_MATRIX_2, EXIF_TAG_ANALOG_BALANCE, - EXIF_TAG_AS_SHOT_NEUTRAL, EXIF_TAG_AS_SHOT_WHITE_XY, - EXIF_TAG_BASELINE_EXPOSURE, EXIF_TAG_BASELINE_NOISE, - EXIF_TAG_BASELINE_SHARPNESS, EXIF_TAG_BAYER_GREEN_SPLIT, - EXIF_TAG_LINEAR_RESPONSE_LIMIT, EXIF_TAG_CAMERA_SERIAL_NUMBER, - EXIF_TAG_DNG_LENS_INFO, EXIF_TAG_CHROMA_BLUR_RADIUS, - EXIF_TAG_ANTI_ALIAS_STRENGTH, EXIF_TAG_SHADOW_SCALE, - EXIF_TAG_DNG_ADOBE_DATA, EXIF_TAG_DNG_PENTAX_DATA, - EXIF_TAG_DNG_PRIVATE_DATA, EXIF_TAG_MAKER_NOTE_SAFETY, - EXIF_TAG_CALIBRATION_ILLUMINANT_1, - EXIF_TAG_CALIBRATION_ILLUMINANT_2, EXIF_TAG_BEST_QUALITY_SCALE, - EXIF_TAG_RAW_DATA_UNIQUE_ID, EXIF_TAG_ALIAS_LAYER_METADATA, - EXIF_TAG_ORIGINAL_RAW_FILE_NAME, EXIF_TAG_ORIGINAL_RAW_FILE_DATA, - EXIF_TAG_ACTIVE_AREA, EXIF_TAG_MASKED_AREAS, - EXIF_TAG_AS_SHOT_ICCPROFILE, EXIF_TAG_AS_SHOT_PRE_PROFILE_MATRIX, - EXIF_TAG_CURRENT_ICCPROFILE, EXIF_TAG_CURRENT_PRE_PROFILE_MATRIX, - EXIF_TAG_OFFSET_SCHEMA, EXIF_TAG_OWNER_NAME, - EXIF_TAG_SERIAL_NUMBER, EXIF_TAG_LENS, EXIF_TAG_RAW_FILE, - EXIF_TAG_CONVERTER, EXIF_TAG_WHITE_BALANCE_2, EXIF_TAG_EXPOSURE, - EXIF_TAG_SHADOWS, EXIF_TAG_BRIGHTNESS, EXIF_TAG_CONTRAST_2, - EXIF_TAG_SATURATION_2, EXIF_TAG_SHARPNESS_2, EXIF_TAG_SMOOTHNESS, - EXIF_TAG_MOIRE_FILTER, - - EXIF_TAG_USER_COMMENT, // - - EXIF_TAG_MAKER_NOTE, // - }; -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/GPSTagConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/GPSTagConstants.java deleted file mode 100644 index 65bd033..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/GPSTagConstants.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -public interface GPSTagConstants - extends - TiffDirectoryConstants, - TiffFieldTypeConstants -{ - public static final TagInfo GPS_TAG_GPS_VERSION_ID = new TagInfo( - "GPS Version ID", 0x0000, FIELD_TYPE_DESCRIPTION_BYTE, 4, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_LATITUDE_REF = new TagInfo( - "GPS Latitude Ref", 0x0001, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH = "N"; - public static final String GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH = "S"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_LATITUDE = new TagInfo( - "GPS Latitude", 0x0002, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_LONGITUDE_REF = new TagInfo( - "GPS Longitude Ref", 0x0003, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST = "E"; - public static final String GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST = "W"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_LONGITUDE = new TagInfo( - "GPS Longitude", 0x0004, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_ALTITUDE_REF = new TagInfo( - "GPS Altitude Ref", 0x0005, FIELD_TYPE_DESCRIPTION_BYTE, -1, - EXIF_DIRECTORY_GPS); - - public static final int GPS_TAG_GPS_ALTITUDE_REF_VALUE_ABOVE_SEA_LEVEL = 0; - public static final int GPS_TAG_GPS_ALTITUDE_REF_VALUE_BELOW_SEA_LEVEL = 1; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_ALTITUDE = new TagInfo( - "GPS Altitude", 0x0006, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_TIME_STAMP = new TagInfo( - "GPS Time Stamp", 0x0007, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_SATELLITES = new TagInfo( - "GPS Satellites", 0x0008, FIELD_TYPE_DESCRIPTION_ASCII, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_STATUS = new TagInfo("GPS Status", - 0x0009, FIELD_TYPE_DESCRIPTION_ASCII, 2, EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_STATUS_VALUE_MEASUREMENT_IN_PROGRESS = "A"; - public static final String GPS_TAG_GPS_STATUS_VALUE_MEASUREMENT_INTEROPERABILITY = "V"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_MEASURE_MODE = new TagInfo( - "GPS Measure Mode", 0x000a, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final int GPS_TAG_GPS_MEASURE_MODE_VALUE_2_DIMENSIONAL_MEASUREMENT = 2; - public static final int GPS_TAG_GPS_MEASURE_MODE_VALUE_3_DIMENSIONAL_MEASUREMENT = 3; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DOP = new TagInfo("GPS DOP", - 0x000b, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_SPEED_REF = new TagInfo( - "GPS Speed Ref", 0x000c, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_SPEED_REF_VALUE_KMPH = "K"; - public static final String GPS_TAG_GPS_SPEED_REF_VALUE_MPH = "M"; - public static final String GPS_TAG_GPS_SPEED_REF_VALUE_KNOTS = "N"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_SPEED = new TagInfo("GPS Speed", - 0x000d, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_TRACK_REF = new TagInfo( - "GPS Track Ref", 0x000e, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_TRACK_REF_VALUE_MAGNETIC_NORTH = "M"; - public static final String GPS_TAG_GPS_TRACK_REF_VALUE_TRUE_NORTH = "T"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_TRACK = new TagInfo("GPS Track", - 0x000f, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_IMG_DIRECTION_REF = new TagInfo( - "GPS Img Direction Ref", 0x0010, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_MAGNETIC_NORTH = "M"; - public static final String GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH = "T"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_IMG_DIRECTION = new TagInfo( - "GPS Img Direction", 0x0011, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_MAP_DATUM = new TagInfo( - "GPS Map Datum", 0x0012, FIELD_TYPE_DESCRIPTION_ASCII, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_LATITUDE_REF = new TagInfo( - "GPS Dest Latitude Ref", 0x0013, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_DEST_LATITUDE_REF_VALUE_NORTH = "N"; - public static final String GPS_TAG_GPS_DEST_LATITUDE_REF_VALUE_SOUTH = "S"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_LATITUDE = new TagInfo( - "GPS Dest Latitude", 0x0014, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_LONGITUDE_REF = new TagInfo( - "GPS Dest Longitude Ref", 0x0015, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_DEST_LONGITUDE_REF_VALUE_EAST = "E"; - public static final String GPS_TAG_GPS_DEST_LONGITUDE_REF_VALUE_WEST = "W"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_LONGITUDE = new TagInfo( - "GPS Dest Longitude", 0x0016, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_BEARING_REF = new TagInfo( - "GPS Dest Bearing Ref", 0x0017, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_DEST_BEARING_REF_VALUE_MAGNETIC_NORTH = "M"; - public static final String GPS_TAG_GPS_DEST_BEARING_REF_VALUE_TRUE_NORTH = "T"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_BEARING = new TagInfo( - "GPS Dest Bearing", 0x0018, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_DISTANCE_REF = new TagInfo( - "GPS Dest Distance Ref", 0x0019, FIELD_TYPE_DESCRIPTION_ASCII, 2, - EXIF_DIRECTORY_GPS); - - public static final String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_KILOMETERS = "K"; - public static final String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_MILES = "M"; - public static final String GPS_TAG_GPS_DEST_DISTANCE_REF_VALUE_NAUTICAL_MILES = "N"; - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DEST_DISTANCE = new TagInfo( - "GPS Dest Distance", 0x001a, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_PROCESSING_METHOD = new TagInfo.Text( - "GPS Processing Method", 0x001b, FIELD_TYPE_DESCRIPTION_UNKNOWN, - -1, EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_AREA_INFORMATION = new TagInfo.Text( - "GPS Area Information", 0x001c, FIELD_TYPE_DESCRIPTION_UNKNOWN, -1, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DATE_STAMP = new TagInfo( - "GPS Date Stamp", 0x001d, FIELD_TYPE_DESCRIPTION_ASCII, 11, - EXIF_DIRECTORY_GPS); - - // ************************************************************ - public static final TagInfo GPS_TAG_GPS_DIFFERENTIAL = new TagInfo( - "GPS Differential", 0x001e, FIELD_TYPE_DESCRIPTION_SHORT, -1, - EXIF_DIRECTORY_GPS); - - public static final int GPS_TAG_GPS_DIFFERENTIAL_VALUE_NO_CORRECTION = 0; - public static final int GPS_TAG_GPS_DIFFERENTIAL_VALUE_DIFFERENTIAL_CORRECTED = 1; - // ************************************************************ - - public static final TagInfo ALL_GPS_TAGS[] = { - GPS_TAG_GPS_VERSION_ID, GPS_TAG_GPS_LATITUDE_REF, - GPS_TAG_GPS_LATITUDE, GPS_TAG_GPS_LONGITUDE_REF, - GPS_TAG_GPS_LONGITUDE, GPS_TAG_GPS_ALTITUDE_REF, - GPS_TAG_GPS_ALTITUDE, GPS_TAG_GPS_TIME_STAMP, - GPS_TAG_GPS_SATELLITES, GPS_TAG_GPS_STATUS, - GPS_TAG_GPS_MEASURE_MODE, GPS_TAG_GPS_DOP, GPS_TAG_GPS_SPEED_REF, - GPS_TAG_GPS_SPEED, GPS_TAG_GPS_TRACK_REF, GPS_TAG_GPS_TRACK, - GPS_TAG_GPS_IMG_DIRECTION_REF, GPS_TAG_GPS_IMG_DIRECTION, - GPS_TAG_GPS_MAP_DATUM, GPS_TAG_GPS_DEST_LATITUDE_REF, - GPS_TAG_GPS_DEST_LATITUDE, GPS_TAG_GPS_DEST_LONGITUDE_REF, - GPS_TAG_GPS_DEST_LONGITUDE, GPS_TAG_GPS_DEST_BEARING_REF, - GPS_TAG_GPS_DEST_BEARING, GPS_TAG_GPS_DEST_DISTANCE_REF, - GPS_TAG_GPS_DEST_DISTANCE, GPS_TAG_GPS_PROCESSING_METHOD, - GPS_TAG_GPS_AREA_INFORMATION, GPS_TAG_GPS_DATE_STAMP, - GPS_TAG_GPS_DIFFERENTIAL, - }; -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TagConstantsUtils.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TagConstantsUtils.java deleted file mode 100644 index 74d1b13..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TagConstantsUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -public class TagConstantsUtils implements TiffDirectoryConstants -{ - - public static TagInfo[] mergeTagLists(TagInfo lists[][]) - { - int count = 0; - for (int i = 0; i < lists.length; i++) - count += lists[i].length; - - TagInfo result[] = new TagInfo[count]; - - int index = 0; - for (int i = 0; i < lists.length; i++) - { - System.arraycopy(lists[i], 0, result, index, lists[i].length); - index += lists[i].length; - } - - return result; - } - - public static ExifDirectoryType getExifDirectoryType(int type) - { - for (int i = 0; i < EXIF_DIRECTORIES.length; i++) - if (EXIF_DIRECTORIES[i].directoryType == type) - return EXIF_DIRECTORIES[i]; - return EXIF_DIRECTORY_UNKNOWN; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TagInfo.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TagInfo.java deleted file mode 100644 index a72af2c..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TagInfo.java +++ /dev/null @@ -1,447 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.formats.tiff.constants; - -import java.io.UnsupportedEncodingException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileFunctions; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; -import org.apache.sanselan.util.Debug; - -public class TagInfo implements TiffDirectoryConstants, TiffFieldTypeConstants -{ - protected static final int LENGTH_UNKNOWN = -1; - - public TagInfo(String name, int tag, FieldType dataType, int length, - ExifDirectoryType exifDirectory) - { - this(name, tag, new FieldType[]{ - dataType - }, length, exifDirectory); - } - - public TagInfo(String name, int tag, FieldType dataType, int length) - { - this(name, tag, new FieldType[]{ - dataType - }, length, EXIF_DIRECTORY_UNKNOWN); - } - - public TagInfo(String name, int tag, FieldType dataType, - String lengthDescription) - { - this(name, tag, new FieldType[]{ - dataType - }, LENGTH_UNKNOWN, EXIF_DIRECTORY_UNKNOWN); - } - - public TagInfo(String name, int tag, FieldType dataTypes[], - String lengthDescription) - { - this(name, tag, dataTypes, LENGTH_UNKNOWN, EXIF_DIRECTORY_UNKNOWN); - } - - public TagInfo(String name, int tag, FieldType dataType) - { - this(name, tag, dataType, LENGTH_UNKNOWN, EXIF_DIRECTORY_UNKNOWN); - } - - public TagInfo(String name, int tag, FieldType dataTypes[], int length, - String lengthDescription) - { - this(name, tag, dataTypes, length, EXIF_DIRECTORY_UNKNOWN); - } - - public final String name; - public final int tag; - public final FieldType dataTypes[]; - public final int length; - public final ExifDirectoryType directoryType; - - // public final String lengthDescription; - - public TagInfo(String name, int tag, FieldType dataTypes[], int length, - ExifDirectoryType exifDirectory - // , String lengthDescription - ) - { - this.name = name; - this.tag = tag; - this.dataTypes = dataTypes; - this.length = length; - // this.lengthDescription = lengthDescription; - this.directoryType = exifDirectory; - } - - public Object getValue(TiffField entry) throws ImageReadException - { - Object o = entry.fieldType.getSimpleValue(entry); - return o; - } - - public byte[] encodeValue(FieldType fieldType, Object value, int byteOrder) - throws ImageWriteException - { - return fieldType.writeData(value, byteOrder); - } - - public String getDescription() - { - return tag + " (0x" + Integer.toHexString(tag) + ": " + name + "): "; - } - - public String toString() - { - return "[TagInfo. tag: " + tag + " (0x" + Integer.toHexString(tag) - + ", name: " + name + "]"; - } - - public boolean isDate() - { - return false; - } - - public boolean isOffset() - { - return false; - } - - public boolean isText() - { - return false; - } - - public boolean isUnknown() - { - return false; - } - - public static class Offset extends TagInfo - { - public Offset(String name, int tag, FieldType dataTypes[], int length, - ExifDirectoryType exifDirectory) - { - super(name, tag, dataTypes, length, exifDirectory); - } - - public Offset(String name, int tag, FieldType dataType, int length, - ExifDirectoryType exifDirectory) - { - super(name, tag, dataType, length, exifDirectory); - } - - public Offset(String name, int tag, FieldType dataType, int length) - { - super(name, tag, dataType, length); - } - - // "Exif Offset", 0x8769, FIELD_TYPE_DESCRIPTION_UNKNOWN, 1, - // EXIF_DIRECTORY_UNKNOWN); - public boolean isOffset() - { - return true; - } - } - - public static class Date extends TagInfo - { - public Date(String name, int tag, FieldType dataType, int length) - { - super(name, tag, dataType, length); - } - - private static final DateFormat DATE_FORMAT_1 = new SimpleDateFormat( - "yyyy:MM:dd HH:mm:ss"); - private static final DateFormat DATE_FORMAT_2 = new SimpleDateFormat( - "yyyy:MM:dd:HH:mm:ss"); - - public Object getValue(TiffField entry) throws ImageReadException - { - Object o = entry.fieldType.getSimpleValue(entry); - - String s = (String) o; - try - { - java.util.Date date = DATE_FORMAT_1.parse(s); - return date; - } - catch (Exception e) - { - // Debug.debug(e); - } - try - { - java.util.Date date = DATE_FORMAT_2.parse(s); - return date; - } - catch (Exception e) - { - Debug.debug(e); - } - - return o; - } - - public byte[] encodeValue(FieldType fieldType, Object value, - int byteOrder) throws ImageWriteException - { - throw new ImageWriteException("date encode value: " + value + " (" - + Debug.getType(value) + ")"); - // return fieldType.writeData(value, byteOrder); - // Object o = entry.fieldType.getSimpleValue(entry); - // byte bytes2[]; - // if (tagInfo.isDate()) - // bytes2 = fieldType.getRawBytes(srcField); - // else - // bytes2 = fieldType.writeData(value, byteOrder); - // return o; - } - - public String toString() - { - return "[TagInfo. tag: " + tag + ", name: " + name + " (data)" - + "]"; - } - - // TODO: use polymorphism - public boolean isDate() - { - return true; - } - - } - - public static final class Text extends TagInfo - { - public Text(String name, int tag, FieldType dataType, int length, - ExifDirectoryType exifDirectory) - { - super(name, tag, dataType, length, exifDirectory); - } - - public Text(String name, int tag, FieldType dataTypes[], int length, - ExifDirectoryType exifDirectory) - { - super(name, tag, dataTypes, length, exifDirectory); - } - - public boolean isText() - { - return true; - } - - private static final class TextEncoding - { - public final byte prefix[]; - public final String encodingName; - - public TextEncoding(final byte[] prefix, final String encodingName) - { - this.prefix = prefix; - this.encodingName = encodingName; - } - } - - private static final TextEncoding TEXT_ENCODING_ASCII = new TextEncoding( - new byte[]{ - 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, - }, "US-ASCII"); // ITU-T T.50 IA5 - private static final TextEncoding TEXT_ENCODING_JIS = new TextEncoding( - new byte[]{ - 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, - }, "JIS"); // JIS X208-1990 - private static final TextEncoding TEXT_ENCODING_UNICODE = new TextEncoding( - new byte[]{ - 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00, - // Which Unicode encoding to use, UTF-8? - }, "UTF-8"); // Unicode Standard - private static final TextEncoding TEXT_ENCODING_UNDEFINED = new TextEncoding( - new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Try to interpret an undefined text as ISO-8859-1 (Latin) - }, "ISO-8859-1"); // Undefined - private static final TextEncoding TEXT_ENCODINGS[] = { - TEXT_ENCODING_ASCII, // - TEXT_ENCODING_JIS, // - TEXT_ENCODING_UNICODE, // - TEXT_ENCODING_UNDEFINED, // - }; - - public byte[] encodeValue(FieldType fieldType, Object value, - int byteOrder) throws ImageWriteException - { - if (!(value instanceof String)) - throw new ImageWriteException("Text value not String: " + value - + " (" + Debug.getType(value) + ")"); - String s = (String) value; - - try - { - // try ASCII, with NO prefix. - byte asciiBytes[] = s - .getBytes(TEXT_ENCODING_ASCII.encodingName); - String decodedAscii = new String(asciiBytes, - TEXT_ENCODING_ASCII.encodingName); - if (decodedAscii.equals(s)) - { - // no unicode/non-ascii values. - byte result[] = new byte[asciiBytes.length - + TEXT_ENCODING_ASCII.prefix.length]; - System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0, - TEXT_ENCODING_ASCII.prefix.length); - System.arraycopy(asciiBytes, 0, result, - TEXT_ENCODING_ASCII.prefix.length, - asciiBytes.length); - return result; - } - else - { - // use unicode - byte unicodeBytes[] = s - .getBytes(TEXT_ENCODING_UNICODE.encodingName); - byte result[] = new byte[unicodeBytes.length - + TEXT_ENCODING_UNICODE.prefix.length]; - System.arraycopy(TEXT_ENCODING_UNICODE.prefix, 0, result, - 0, TEXT_ENCODING_UNICODE.prefix.length); - System.arraycopy(unicodeBytes, 0, result, - TEXT_ENCODING_UNICODE.prefix.length, - unicodeBytes.length); - return result; - } - } - catch (UnsupportedEncodingException e) - { - throw new ImageWriteException(e.getMessage(), e); - } - } - - public Object getValue(TiffField entry) throws ImageReadException - { - // Debug.debug("entry.type", entry.type); - // Debug.debug("entry.type", entry.getDescriptionWithoutValue()); - // Debug.debug("entry.type", entry.fieldType); - - if (entry.type == FIELD_TYPE_ASCII.type) - return FIELD_TYPE_ASCII.getSimpleValue(entry); - else if (entry.type == FIELD_TYPE_UNDEFINED.type) - ; - else if (entry.type == FIELD_TYPE_BYTE.type) - ; - else - { - Debug.debug("entry.type", entry.type); - Debug.debug("entry.directoryType", entry.directoryType); - Debug.debug("entry.type", entry.getDescriptionWithoutValue()); - Debug.debug("entry.type", entry.fieldType); - throw new ImageReadException("Text field not encoded as bytes."); - } - - byte bytes[] = entry.fieldType.getRawBytes(entry); - if (bytes.length < 8) - { - try - { - // try ASCII, with NO prefix. - return new String(bytes, "US-ASCII"); - } - catch (UnsupportedEncodingException e) - { - throw new ImageReadException( - "Text field missing encoding prefix."); - } - } - - for (int i = 0; i < TEXT_ENCODINGS.length; i++) - { - TextEncoding encoding = TEXT_ENCODINGS[i]; - if (BinaryFileFunctions.compareBytes(bytes, 0, encoding.prefix, - 0, encoding.prefix.length)) - { - try - { - // Debug.debug("encodingName", encoding.encodingName); - return new String(bytes, encoding.prefix.length, - bytes.length - encoding.prefix.length, - encoding.encodingName); - } - catch (UnsupportedEncodingException e) - { - throw new ImageReadException(e.getMessage(), e); - } - } - } - - // Debug.debug("entry.tag", entry.tag + " (0x" + Integer.toHexString(entry.tag ) +")"); - // Debug.debug("entry.type", entry.type); - // Debug.debug("bytes", bytes, 10); - // throw new ImageReadException( - // "Unknown Text encoding prefix."); - - try - { - // try ASCII, with NO prefix. - return new String(bytes, "US-ASCII"); - } - catch (UnsupportedEncodingException e) - { - throw new ImageReadException("Unknown text encoding prefix."); - } - - } - } - - public static final class Unknown extends TagInfo - { - - public Unknown(String name, int tag, FieldType dataTypes[], int length, - ExifDirectoryType exifDirectory) - { - super(name, tag, dataTypes, length, exifDirectory); - } - - public boolean isUnknown() - { - return true; - } - - public byte[] encodeValue(FieldType fieldType, Object value, - int byteOrder) throws ImageWriteException - { - // Debug.debug(); - // Debug.debug("unknown tag(0x" + Integer.toHexString(tag) + ") ", - // this); - // Debug.debug("unknown tag fieldType", fieldType); - // Debug.debug("unknown tag value", value); - // Debug.debug("unknown tag value", Debug.getType(value)); - byte result[] = super.encodeValue(fieldType, value, byteOrder); - // Debug.debug("unknown tag result", result); - return result; - } - - public Object getValue(TiffField entry) throws ImageReadException - { - return super.getValue(entry); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffConstants.java deleted file mode 100644 index 2e11042..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffConstants.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -import org.apache.sanselan.SanselanConstants; -import org.apache.sanselan.common.BinaryConstants; - -public interface TiffConstants - extends - SanselanConstants, - TiffFieldTypeConstants, - TiffDirectoryConstants, - AllTagConstants, - BinaryConstants -{ - public static final int DEFAULT_TIFF_BYTE_ORDER = BYTE_ORDER_INTEL; - - public static final int TIFF_HEADER_SIZE = 8; - public static final int TIFF_DIRECTORY_HEADER_LENGTH = 2; - public static final int TIFF_DIRECTORY_FOOTER_LENGTH = 4; - public static final int TIFF_ENTRY_LENGTH = 12; - public static final int TIFF_ENTRY_MAX_VALUE_LENGTH = 4; - - public static final int TIFF_COMPRESSION_UNCOMPRESSED_1 = 1; - public static final int TIFF_COMPRESSION_UNCOMPRESSED = TIFF_COMPRESSION_UNCOMPRESSED_1; - public static final int TIFF_COMPRESSION_CCITT_1D = 2; - public static final int TIFF_COMPRESSION_CCITT_GROUP_3 = 3; - public static final int TIFF_COMPRESSION_CCITT_GROUP_4 = 4; - public static final int TIFF_COMPRESSION_LZW = 5; - public static final int TIFF_COMPRESSION_JPEG = 6; - public static final int TIFF_COMPRESSION_UNCOMPRESSED_2 = 32771; - public static final int TIFF_COMPRESSION_PACKBITS = 32773; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffDirectoryConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffDirectoryConstants.java deleted file mode 100644 index f3fe1cc..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffDirectoryConstants.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -public interface TiffDirectoryConstants -{ - - public static final int DIRECTORY_TYPE_UNKNOWN = -1; - public static final int DIRECTORY_TYPE_ROOT = 0; - public static final int DIRECTORY_TYPE_SUB = 1; - public static final int DIRECTORY_TYPE_SUB0 = 1; - public static final int DIRECTORY_TYPE_SUB1 = 2; - public static final int DIRECTORY_TYPE_SUB2 = 3; - public static final int DIRECTORY_TYPE_THUMBNAIL = 2; - public static final int DIRECTORY_TYPE_EXIF = -2; - // public static final int DIRECTORY_TYPE_SUB = 5; - public static final int DIRECTORY_TYPE_GPS = -3; - public static final int DIRECTORY_TYPE_INTEROPERABILITY = -4; - public static final int DIRECTORY_TYPE_MAKER_NOTES = -5; - public static final int DIRECTORY_TYPE_DIR_0 = 0; - public static final int DIRECTORY_TYPE_DIR_1 = 1; - public static final int DIRECTORY_TYPE_DIR_2 = 2; - public static final int DIRECTORY_TYPE_DIR_3 = 3; - public static final int DIRECTORY_TYPE_DIR_4 = 4; - - public abstract static class ExifDirectoryType - { - public final int directoryType; - public final String name; - - public ExifDirectoryType(final int directoryType, final String name) - { - this.directoryType = directoryType; - this.name = name; - } - - public abstract boolean isImageDirectory(); - - public static class Image extends ExifDirectoryType - { - public Image(final int directoryType, final String name) - { - super(directoryType, name); - } - - public boolean isImageDirectory() - { - return true; - } - } - - public static class Special extends ExifDirectoryType - { - public Special(final int directoryType, final String name) - { - super(directoryType, name); - } - - public boolean isImageDirectory() - { - return false; - } - } - - } - - public static final ExifDirectoryType TIFF_DIRECTORY_IFD0 = new ExifDirectoryType.Image( - DIRECTORY_TYPE_DIR_0, "IFD0"); - public static final ExifDirectoryType EXIF_DIRECTORY_IFD0 = TIFF_DIRECTORY_IFD0; - public static final ExifDirectoryType TIFF_DIRECTORY_ROOT = TIFF_DIRECTORY_IFD0; - - public static final ExifDirectoryType TIFF_DIRECTORY_IFD1 = new ExifDirectoryType.Image( - DIRECTORY_TYPE_DIR_1, "IFD1"); - public static final ExifDirectoryType EXIF_DIRECTORY_IFD1 = TIFF_DIRECTORY_IFD1; - - public static final ExifDirectoryType TIFF_DIRECTORY_IFD2 = new ExifDirectoryType.Image( - DIRECTORY_TYPE_DIR_2, "IFD2"); - public static final ExifDirectoryType EXIF_DIRECTORY_IFD2 = TIFF_DIRECTORY_IFD2; - - public static final ExifDirectoryType TIFF_DIRECTORY_IFD3 = new ExifDirectoryType.Image( - DIRECTORY_TYPE_DIR_3, "IFD3"); - public static final ExifDirectoryType EXIF_DIRECTORY_IFD3 = TIFF_DIRECTORY_IFD3; - - public static final ExifDirectoryType EXIF_DIRECTORY_SUB_IFD = TIFF_DIRECTORY_IFD1; - public static final ExifDirectoryType EXIF_DIRECTORY_SUB_IFD1 = TIFF_DIRECTORY_IFD2; - public static final ExifDirectoryType EXIF_DIRECTORY_SUB_IFD2 = TIFF_DIRECTORY_IFD3; - - public static final ExifDirectoryType EXIF_DIRECTORY_INTEROP_IFD = new ExifDirectoryType.Special( - DIRECTORY_TYPE_INTEROPERABILITY, "Interop IFD"); - public static final ExifDirectoryType EXIF_DIRECTORY_MAKER_NOTES = new ExifDirectoryType.Special( - DIRECTORY_TYPE_MAKER_NOTES, "Maker Notes"); - public static final ExifDirectoryType EXIF_DIRECTORY_EXIF_IFD = new ExifDirectoryType.Special( - DIRECTORY_TYPE_EXIF, "Exif IFD"); - public static final ExifDirectoryType EXIF_DIRECTORY_GPS = new ExifDirectoryType.Special( - DIRECTORY_TYPE_GPS, "GPS IFD"); - - public static final ExifDirectoryType EXIF_DIRECTORY_UNKNOWN = null; - - public static final ExifDirectoryType EXIF_DIRECTORIES[] = { - TIFF_DIRECTORY_ROOT, EXIF_DIRECTORY_EXIF_IFD, TIFF_DIRECTORY_IFD0, - EXIF_DIRECTORY_IFD0, TIFF_DIRECTORY_IFD1, EXIF_DIRECTORY_IFD1, - TIFF_DIRECTORY_IFD2, EXIF_DIRECTORY_IFD2, - EXIF_DIRECTORY_INTEROP_IFD, EXIF_DIRECTORY_MAKER_NOTES, - EXIF_DIRECTORY_SUB_IFD, EXIF_DIRECTORY_SUB_IFD1, - EXIF_DIRECTORY_SUB_IFD2, - //EXIF_DIRECTORY_UNKNOWN, - EXIF_DIRECTORY_GPS, - }; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffFieldTypeConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffFieldTypeConstants.java deleted file mode 100644 index 8be67a7..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffFieldTypeConstants.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -import org.apache.sanselan.SanselanConstants; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeASCII; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeByte; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeDouble; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeFloat; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeLong; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeRational; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeShort; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldTypeUnknown; - -public interface TiffFieldTypeConstants extends SanselanConstants -{ - - public static final FieldTypeByte FIELD_TYPE_BYTE = new FieldTypeByte(1, - "Byte"); - - public static final FieldTypeASCII FIELD_TYPE_ASCII = new FieldTypeASCII(2, - "ASCII"); - - public static final FieldTypeShort FIELD_TYPE_SHORT = new FieldTypeShort(3, - "Short"); - - public static final FieldTypeLong FIELD_TYPE_LONG = new FieldTypeLong(4, - "Long"); - - public static final FieldTypeRational FIELD_TYPE_RATIONAL = new FieldTypeRational( - 5, "Rational"); - - public static final FieldType FIELD_TYPE_SBYTE = new FieldTypeByte(6, - "SByte"); - public static final FieldType FIELD_TYPE_UNDEFINED = new FieldTypeByte(7, - "Undefined"); - public static final FieldType FIELD_TYPE_SSHORT = new FieldTypeShort(8, - "SShort"); - - public static final FieldType FIELD_TYPE_SLONG = new FieldTypeLong(9, - "SLong"); - - public static final FieldType FIELD_TYPE_SRATIONAL = new FieldTypeRational( - 10, "SRational"); - - public static final FieldType FIELD_TYPE_FLOAT = new FieldTypeFloat(); - - public static final FieldType FIELD_TYPE_DOUBLE = new FieldTypeDouble(); - - public static final FieldType FIELD_TYPE_UNKNOWN = new FieldTypeUnknown(); - - public static final FieldType FIELD_TYPES[] = { - FIELD_TYPE_BYTE, FIELD_TYPE_ASCII, FIELD_TYPE_SHORT, - FIELD_TYPE_LONG, FIELD_TYPE_RATIONAL, FIELD_TYPE_SBYTE, - FIELD_TYPE_UNDEFINED, FIELD_TYPE_SSHORT, FIELD_TYPE_SLONG, - FIELD_TYPE_SRATIONAL, FIELD_TYPE_FLOAT, FIELD_TYPE_DOUBLE, - }; - - public static final FieldType FIELD_TYPE_ANY[] = FIELD_TYPES; - - public static final FieldType FIELD_TYPE_DESCRIPTION_LONG[] = { - FIELD_TYPE_LONG, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_SHORT[] = { - FIELD_TYPE_SHORT, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG[] = { - FIELD_TYPE_SHORT, FIELD_TYPE_LONG, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_ASCII[] = { - FIELD_TYPE_ASCII, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_LONG_OR_SHORT[] = { - FIELD_TYPE_SHORT, FIELD_TYPE_LONG, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_RATIONAL[] = { - FIELD_TYPE_RATIONAL, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_BYTE_OR_SHORT[] = { - FIELD_TYPE_SHORT, FIELD_TYPE_BYTE - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_BYTE[] = { - FIELD_TYPE_BYTE, - }; - public static final FieldType FIELD_TYPE_DESCRIPTION_ANY[] = FIELD_TYPE_ANY; - public static final FieldType FIELD_TYPE_DESCRIPTION_UNKNOWN[] = null; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffTagConstants.java b/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffTagConstants.java deleted file mode 100644 index 0f08487..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/constants/TiffTagConstants.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.constants; - -public interface TiffTagConstants - extends - TiffDirectoryConstants, - TiffFieldTypeConstants -{ - - public static final TagInfo TIFF_TAG_NEW_SUBFILE_TYPE = new TagInfo( - "New Subfile Type", 0xFE, FIELD_TYPE_DESCRIPTION_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SUBFILE_TYPE = new TagInfo( - "Subfile Type", 0xFF, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_IMAGE_WIDTH = new TagInfo( - "Image Width", 0x100, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_IMAGE_LENGTH = new TagInfo( - "Image Length", 0x101, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_BITS_PER_SAMPLE = new TagInfo( - "Bits Per Sample", 0x102, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_COMPRESSION = new TagInfo( - "Compression", 0x103, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PHOTOMETRIC_INTERPRETATION = new TagInfo( - "Photometric Interpretation", 0x106, FIELD_TYPE_DESCRIPTION_SHORT, - 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_THRESHHOLDING = new TagInfo( - "Threshholding", 0x107, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_CELL_WIDTH = new TagInfo("Cell Width", - 0x108, FIELD_TYPE_DESCRIPTION_SHORT, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_CELL_LENGTH = new TagInfo( - "Cell Length", 0x109, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_FILL_ORDER = new TagInfo("Fill Order", - 0x10A, FIELD_TYPE_DESCRIPTION_SHORT, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_DOCUMENT_NAME = new TagInfo( - "Document Name", 0x10D, FIELD_TYPE_DESCRIPTION_ASCII, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_IMAGE_DESCRIPTION = new TagInfo( - "Image Description", 0x10E, FIELD_TYPE_DESCRIPTION_ASCII, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_MAKE = new TagInfo("Make", 0x10F, - FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_MODEL = new TagInfo("Model", 0x110, - FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_STRIP_OFFSETS = new TagInfo.Offset( - "Strip Offsets", 0x111, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_ORIENTATION = new TagInfo( - "Orientation", 0x112, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SAMPLES_PER_PIXEL = new TagInfo( - "Samples Per Pixel", 0x115, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_ROWS_PER_STRIP = new TagInfo( - "Rows Per Strip", 0x116, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_STRIP_BYTE_COUNTS = new TagInfo( - "Strip Byte Counts", 0x117, FIELD_TYPE_DESCRIPTION_LONG_OR_SHORT, - -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_MIN_SAMPLE_VALUE = new TagInfo( - "Min Sample Value", 0x118, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_MAX_SAMPLE_VALUE = new TagInfo( - "Max Sample Value", 0x119, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_XRESOLUTION = new TagInfo( - "XResolution", 0x11A, FIELD_TYPE_DESCRIPTION_RATIONAL, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_YRESOLUTION = new TagInfo( - "YResolution", 0x11B, FIELD_TYPE_DESCRIPTION_RATIONAL, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PLANAR_CONFIGURATION = new TagInfo( - "Planar Configuration", 0x11C, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PAGE_NAME = new TagInfo("Page Name", - 0x11D, FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_XPOSITION = new TagInfo("XPosition", - 0x11E, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_YPOSITION = new TagInfo("YPosition", - 0x11F, FIELD_TYPE_DESCRIPTION_RATIONAL, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_FREE_OFFSETS = new TagInfo( - "Free Offsets", 0x120, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_FREE_BYTE_COUNTS = new TagInfo( - "Free Byte Counts", 0x121, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_GRAY_RESPONSE_UNIT = new TagInfo( - "Gray Response Unit", 0x122, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_GRAY_RESPONSE_CURVE = new TagInfo( - "Gray Response Curve", 0x123, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_T4_OPTIONS = new TagInfo("T4 Options", - 0x124, FIELD_TYPE_DESCRIPTION_LONG, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_T6_OPTIONS = new TagInfo("T6 Options", - 0x125, FIELD_TYPE_DESCRIPTION_LONG, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_RESOLUTION_UNIT = new TagInfo( - "Resolution Unit", 0x128, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PAGE_NUMBER = new TagInfo( - "Page Number", 0x129, FIELD_TYPE_DESCRIPTION_SHORT, 2, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TRANSFER_FUNCTION = new TagInfo( - "Transfer Function", 0x12D, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SOFTWARE = new TagInfo("Software", - 0x131, FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_DATE_TIME = new TagInfo("Date Time", - 0x132, FIELD_TYPE_DESCRIPTION_ASCII, 20, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_ARTIST = new TagInfo("Artist", 0x13B, - FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_HOST_COMPUTER = new TagInfo( - "Host Computer", 0x13C, FIELD_TYPE_DESCRIPTION_ASCII, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PREDICTOR = new TagInfo("Predictor", - 0x13D, FIELD_TYPE_DESCRIPTION_SHORT, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_WHITE_POINT = new TagInfo( - "White Point", 0x13E, FIELD_TYPE_DESCRIPTION_RATIONAL, 2, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_PRIMARY_CHROMATICITIES = new TagInfo( - "Primary Chromaticities", 0x13F, FIELD_TYPE_DESCRIPTION_RATIONAL, - 6, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_COLOR_MAP = new TagInfo("Color Map", - 0x140, FIELD_TYPE_DESCRIPTION_SHORT, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_HALFTONE_HINTS = new TagInfo( - "Halftone Hints", 0x141, FIELD_TYPE_DESCRIPTION_SHORT, 2, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TILE_WIDTH = new TagInfo("Tile Width", - 0x142, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TILE_LENGTH = new TagInfo( - "Tile Length", 0x143, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TILE_OFFSETS = new TagInfo.Offset( - "Tile Offsets", 0x144, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TILE_BYTE_COUNTS = new TagInfo( - "Tile Byte Counts", 0x145, FIELD_TYPE_DESCRIPTION_SHORT_OR_LONG, - -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_INK_SET = new TagInfo("Ink Set", - 0x14C, FIELD_TYPE_DESCRIPTION_SHORT, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_INK_NAMES = new TagInfo("Ink Names", - 0x14D, FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_NUMBER_OF_INKS = new TagInfo( - "Number Of Inks", 0x14E, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_DOT_RANGE = new TagInfo("Dot Range", - 0x150, FIELD_TYPE_DESCRIPTION_BYTE_OR_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TARGET_PRINTER = new TagInfo( - "Target Printer", 0x151, FIELD_TYPE_DESCRIPTION_ASCII, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_EXTRA_SAMPLES = new TagInfo( - "Extra Samples", 0x152, FIELD_TYPE_DESCRIPTION_BYTE, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SAMPLE_FORMAT = new TagInfo( - "Sample Format", 0x153, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SMIN_SAMPLE_VALUE = new TagInfo( - "SMin Sample Value", 0x154, FIELD_TYPE_DESCRIPTION_ANY, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_SMAX_SAMPLE_VALUE = new TagInfo( - "SMax Sample Value", 0x155, FIELD_TYPE_DESCRIPTION_ANY, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_TRANSFER_RANGE = new TagInfo( - "Transfer Range", 0x156, FIELD_TYPE_DESCRIPTION_SHORT, 6, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_PROC = new TagInfo("JPEGProc", - 0x200, FIELD_TYPE_DESCRIPTION_SHORT, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_INTERCHANGE_FORMAT = new TagInfo.Offset( - "JPEGInterchange Format", 0x201, FIELD_TYPE_DESCRIPTION_LONG, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = new TagInfo( - "JPEGInterchange Format Length", 0x202, - FIELD_TYPE_DESCRIPTION_LONG, 1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_RESTART_INTERVAL = new TagInfo( - "JPEGRestart Interval", 0x203, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_LOSSLESS_PREDICTORS = new TagInfo( - "JPEGLossless Predictors", 0x205, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_POINT_TRANSFORMS = new TagInfo( - "JPEGPoint Transforms", 0x206, FIELD_TYPE_DESCRIPTION_SHORT, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_QTABLES = new TagInfo( - "JPEGQTables", 0x207, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_DCTABLES = new TagInfo( - "JPEGDCTables", 0x208, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_JPEG_ACTABLES = new TagInfo( - "JPEGACTables", 0x209, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_YCBCR_COEFFICIENTS = new TagInfo( - "YCbCr Coefficients", 0x211, FIELD_TYPE_DESCRIPTION_RATIONAL, 3, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_YCBCR_SUB_SAMPLING = new TagInfo( - "YCbCr Sub Sampling", 0x212, FIELD_TYPE_DESCRIPTION_SHORT, 2, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_YCBCR_POSITIONING = new TagInfo( - "YCbCr Positioning", 0x213, FIELD_TYPE_DESCRIPTION_SHORT, 1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_REFERENCE_BLACK_WHITE = new TagInfo( - "Reference Black White", 0x214, FIELD_TYPE_DESCRIPTION_LONG, -1, - TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_COPYRIGHT = new TagInfo("Copyright", - 0x8298, FIELD_TYPE_DESCRIPTION_ASCII, -1, TIFF_DIRECTORY_ROOT); - - public static final TagInfo TIFF_TAG_XMP = new TagInfo("XMP", - 0x2BC, FIELD_TYPE_DESCRIPTION_BYTE, -1, TIFF_DIRECTORY_ROOT); - - // TODO: - // public static final TagInfo2 TIFF_TAG_UNKNOWN = null; - public static final TagInfo TIFF_TAG_UNKNOWN = new TagInfo.Unknown( - "Unknown Tag", -1, FIELD_TYPE_DESCRIPTION_UNKNOWN, - TagInfo.LENGTH_UNKNOWN, EXIF_DIRECTORY_UNKNOWN); - - public static final TagInfo ALL_TIFF_TAGS[] = { - TIFF_TAG_NEW_SUBFILE_TYPE, TIFF_TAG_SUBFILE_TYPE, - TIFF_TAG_IMAGE_WIDTH, TIFF_TAG_IMAGE_LENGTH, - TIFF_TAG_BITS_PER_SAMPLE, TIFF_TAG_COMPRESSION, - TIFF_TAG_PHOTOMETRIC_INTERPRETATION, TIFF_TAG_THRESHHOLDING, - TIFF_TAG_CELL_WIDTH, TIFF_TAG_CELL_LENGTH, TIFF_TAG_FILL_ORDER, - TIFF_TAG_DOCUMENT_NAME, TIFF_TAG_IMAGE_DESCRIPTION, TIFF_TAG_MAKE, - TIFF_TAG_MODEL, TIFF_TAG_STRIP_OFFSETS, TIFF_TAG_ORIENTATION, - TIFF_TAG_SAMPLES_PER_PIXEL, TIFF_TAG_ROWS_PER_STRIP, - TIFF_TAG_STRIP_BYTE_COUNTS, TIFF_TAG_MIN_SAMPLE_VALUE, - TIFF_TAG_MAX_SAMPLE_VALUE, TIFF_TAG_XRESOLUTION, - TIFF_TAG_YRESOLUTION, TIFF_TAG_PLANAR_CONFIGURATION, - TIFF_TAG_PAGE_NAME, TIFF_TAG_XPOSITION, TIFF_TAG_YPOSITION, - TIFF_TAG_FREE_OFFSETS, TIFF_TAG_FREE_BYTE_COUNTS, - TIFF_TAG_GRAY_RESPONSE_UNIT, TIFF_TAG_GRAY_RESPONSE_CURVE, - TIFF_TAG_T4_OPTIONS, TIFF_TAG_T6_OPTIONS, TIFF_TAG_RESOLUTION_UNIT, - TIFF_TAG_PAGE_NUMBER, TIFF_TAG_TRANSFER_FUNCTION, - TIFF_TAG_SOFTWARE, TIFF_TAG_DATE_TIME, TIFF_TAG_ARTIST, - TIFF_TAG_HOST_COMPUTER, TIFF_TAG_PREDICTOR, TIFF_TAG_WHITE_POINT, - TIFF_TAG_PRIMARY_CHROMATICITIES, TIFF_TAG_COLOR_MAP, - TIFF_TAG_HALFTONE_HINTS, TIFF_TAG_TILE_WIDTH, TIFF_TAG_TILE_LENGTH, - TIFF_TAG_TILE_OFFSETS, TIFF_TAG_TILE_BYTE_COUNTS, TIFF_TAG_INK_SET, - TIFF_TAG_INK_NAMES, TIFF_TAG_NUMBER_OF_INKS, TIFF_TAG_DOT_RANGE, - TIFF_TAG_TARGET_PRINTER, TIFF_TAG_EXTRA_SAMPLES, - TIFF_TAG_SAMPLE_FORMAT, TIFF_TAG_SMIN_SAMPLE_VALUE, - TIFF_TAG_SMAX_SAMPLE_VALUE, TIFF_TAG_TRANSFER_RANGE, - TIFF_TAG_JPEG_PROC, TIFF_TAG_JPEG_INTERCHANGE_FORMAT, - TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, - TIFF_TAG_JPEG_RESTART_INTERVAL, TIFF_TAG_JPEG_LOSSLESS_PREDICTORS, - TIFF_TAG_JPEG_POINT_TRANSFORMS, TIFF_TAG_JPEG_QTABLES, - TIFF_TAG_JPEG_DCTABLES, TIFF_TAG_JPEG_ACTABLES, - TIFF_TAG_YCBCR_COEFFICIENTS, TIFF_TAG_YCBCR_SUB_SAMPLING, - TIFF_TAG_YCBCR_POSITIONING, TIFF_TAG_REFERENCE_BLACK_WHITE, - TIFF_TAG_COPYRIGHT, - // - TIFF_TAG_XMP, - }; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReader.java b/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReader.java deleted file mode 100644 index 77ddbd9..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReader.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.datareaders; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryConstants; -import org.apache.sanselan.common.BitInputStream; -import org.apache.sanselan.common.PackBits; -import org.apache.sanselan.common.mylzw.MyLZWDecompressor; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class DataReader implements TiffConstants, BinaryConstants -{ - protected final PhotometricInterpreter photometricInterpreter; - protected final int bitsPerSample[]; - protected final int last[]; - - protected final int predictor; - protected final int samplesPerPixel; - - public DataReader(PhotometricInterpreter photometricInterpreter, - int bitsPerSample[], int predictor, int samplesPerPixel) - { - this.photometricInterpreter = photometricInterpreter; - this.bitsPerSample = bitsPerSample; - this.samplesPerPixel = samplesPerPixel; - this.predictor = predictor; - last = new int[samplesPerPixel]; - } - - // public abstract void readImageData(BufferedImage bi, ByteSource byteSource) - public abstract void readImageData(BufferedImage bi) - throws ImageReadException, IOException; - - protected int[] getSamplesAsBytes(BitInputStream bis) - throws ImageReadException, IOException - { - int result[] = new int[bitsPerSample.length]; - for (int i = 0; i < bitsPerSample.length; i++) - { - int bits = bitsPerSample[i]; - int sample = bis.readBits(bits); - if (bits < 8) - { - int sign = sample & 1; - sample = sample << (8 - bits); // scale to byte. - if (sign > 0) - sample = sample | ((1 << (8 - bits)) - 1); // extend to byte - } - else if (bits > 8) - { - sample = sample >> (bits - 8); // extend to byte. - } - result[i] = sample; - } - - return result; - } - - protected int[] applyPredictor(int samples[], int x) - { - if (predictor == 2) // Horizontal differencing. - { - for (int i = 0; i < samples.length; i++) - { - if (x > 0) - { - samples[i] = 0xff & (samples[i] + last[i]); - } - last[i] = samples[i]; - } - } - - return samples; - } - - private int count = 0; - - protected byte[] decompress(byte compressed[], int compression, - int expected_size) throws ImageReadException, IOException - { - switch (compression) - { - case 1 : // None; - return compressed; - case 2 : // CCITT Group 3 1-Dimensional Modified Huffman run-length encoding. - throw new ImageReadException("Tiff: unknown compression: " - + compression); - case TIFF_COMPRESSION_LZW : // LZW - { - InputStream is = new ByteArrayInputStream(compressed); - - int LZWMinimumCodeSize = 8; - - MyLZWDecompressor myLzwDecompressor = new MyLZWDecompressor( - LZWMinimumCodeSize, BYTE_ORDER_NETWORK); - - myLzwDecompressor.setTiffLZWMode(); - - byte[] result = myLzwDecompressor.decompress(is, expected_size); - - return result; - } - - case TIFF_COMPRESSION_PACKBITS : // Packbits - { - byte unpacked[] = new PackBits().decompress(compressed, - expected_size); - count++; - - return unpacked; - } - - default : - throw new ImageReadException("Tiff: unknown compression: " - + compression); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderStrips.java b/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderStrips.java deleted file mode 100644 index 20c7fa6..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderStrips.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.datareaders; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BitInputStream; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public final class DataReaderStrips extends DataReader -{ - - private final int bitsPerPixel; - private final int width, height; - private final int compression; - private final int rowsPerStrip; - - private final TiffImageData.Strips imageData; - - public DataReaderStrips(PhotometricInterpreter photometricInterpreter, - int bitsPerPixel, int bitsPerSample[], int predictor, - int samplesPerPixel, int width, int height, int compression, - int rowsPerStrip, TiffImageData.Strips imageData) - { - super(photometricInterpreter, bitsPerSample, predictor, samplesPerPixel); - - this.bitsPerPixel = bitsPerPixel; - this.width = width; - this.height = height; - this.compression = compression; - this.rowsPerStrip = rowsPerStrip; - this.imageData = imageData; - } - - private void interpretStrip(BufferedImage bi, byte bytes[], - int pixels_per_strip) throws ImageReadException, IOException - { - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - BitInputStream bis = new BitInputStream(bais); - - for (int i = 0; i < pixels_per_strip; i++) - { - int samples[] = getSamplesAsBytes(bis); - - if ((x < width) && (y < height)) - { - samples = applyPredictor(samples, x); - - photometricInterpreter.interpretPixel(bi, samples, x, y); - } - - x++; - if (x >= width) - { - x = 0; - y++; - bis.flushCache(); - if (y >= height) - break; - } - } - } - - private int x = 0, y = 0; - - public void readImageData(BufferedImage bi) throws ImageReadException, - IOException - { - for (int strip = 0; strip < imageData.strips.length; strip++) - { - int rowsRemaining = height - (strip * rowsPerStrip); - int rowsInThisStrip = Math.min(rowsRemaining, rowsPerStrip); - int pixelsPerStrip = rowsInThisStrip * width; - int bytesPerStrip = ((pixelsPerStrip * bitsPerPixel) + 7) / 8; - - byte compressed[] = imageData.strips[strip].data; - - byte decompressed[] = decompress(compressed, compression, - bytesPerStrip); - - interpretStrip(bi, decompressed, pixelsPerStrip); - - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderTiled.java b/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderTiled.java deleted file mode 100644 index d79b269..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/datareaders/DataReaderTiled.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.datareaders; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BitInputStream; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public final class DataReaderTiled extends DataReader -{ - - private final int tileWidth; - private final int tileLength; - - private final int bitsPerPixel; - - private final int width, height; - - private final int compression; - - private final TiffImageData.Tiles imageData; - - public DataReaderTiled(PhotometricInterpreter photometricInterpreter, - int tileWidth, int tileLength, int bitsPerPixel, - int bitsPerSample[], int predictor, int samplesPerPixel, int width, - int height, int compression, TiffImageData.Tiles imageData) - { - super(photometricInterpreter, bitsPerSample, predictor, samplesPerPixel); - - this.tileWidth = tileWidth; - this.tileLength = tileLength; - - this.bitsPerPixel = bitsPerPixel; - this.width = width; - this.height = height; - this.compression = compression; - - this.imageData = imageData; - } - - private void interpretTile(BufferedImage bi, byte bytes[], int startX, - int startY) throws ImageReadException, IOException - { - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - BitInputStream bis = new BitInputStream(bais); - - int pixelsPerTile = tileWidth * tileLength; - - int tileX = 0, tileY = 0; - - for (int i = 0; i < pixelsPerTile; i++) - { - - int x = tileX + startX; - int y = tileY + startY; - - int samples[] = getSamplesAsBytes(bis); - - if ((x < width) && (y < height)) - { - samples = applyPredictor(samples, x); - photometricInterpreter.interpretPixel(bi, samples, x, y); - } - - tileX++; - - if (tileX >= tileWidth) - { - tileX = 0; - tileY++; - bis.flushCache(); - if (tileY >= tileLength) - break; - } - - } - } - - public void readImageData(BufferedImage bi) throws ImageReadException, - IOException - { - int bitsPerRow = tileWidth * bitsPerPixel; - int bytesPerRow = (bitsPerRow + 7) / 8; - int bytesPerTile = bytesPerRow * tileLength; - int x = 0, y = 0; - - for (int tile = 0; tile < imageData.tiles.length; tile++) - { - byte compressed[] = imageData.tiles[tile].data; - - byte decompressed[] = decompress(compressed, compression, - bytesPerTile); - - interpretTile(bi, decompressed, x, y); - - x += tileWidth; - if (x >= width) - { - x = 0; - y += tileLength; - if (y >= height) - break; - } - - } - } -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldType.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldType.java deleted file mode 100644 index d1ee4fb..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldType.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileFunctions; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -public abstract class FieldType extends BinaryFileFunctions implements - TiffConstants -{ - public final int type, length; - public final String name; - - public FieldType(int type, int length, String name) - { - this.type = type; - this.length = length; - this.name = name; - } - - public boolean isLocalValue(TiffField entry) - { - return ((length > 0) && ((length * entry.length) <= TIFF_ENTRY_MAX_VALUE_LENGTH)); - } - - public int getBytesLength(TiffField entry) throws ImageReadException - { - if (length < 1) - throw new ImageReadException("Unknown field type"); - - return length * entry.length; - } - - // public static final byte[] STUB_LOCAL_VALUE = new - // byte[TIFF_ENTRY_MAX_VALUE_LENGTH]; - - public static final byte[] getStubLocalValue() - { - return new byte[TIFF_ENTRY_MAX_VALUE_LENGTH]; - } - - public final byte[] getStubValue(int count) - { - return new byte[count * length]; - } - - public String getDisplayValue(TiffField entry) throws ImageReadException - { - Object o = getSimpleValue(entry); - if (o == null) - return "NULL"; - return o.toString(); - } - - public final byte[] getRawBytes(TiffField entry) - { - if (isLocalValue(entry)) - { - int rawLength = length * entry.length; - byte result[] = new byte[rawLength]; - System.arraycopy(entry.valueOffsetBytes, 0, result, 0, rawLength); - return result; -// return readBytearray(name, entry.valueOffsetBytes, 0, length -// * entry.length); - // return getBytearrayHead(name + " (" + entry.tagInfo.name + ")", - // entry.valueOffsetBytes, length * entry.length); - } - - return entry.oversizeValue; - } - - public abstract Object getSimpleValue(TiffField entry) - throws ImageReadException; - - // public final Object getSimpleValue(TiffField entry) - // { - // Object array[] = getValueArray(entry); - // if (null == array) - // return null; - // if (array.length == 1) - // return array[0]; - // return array; - // } - // - // public abstract Object[] getValueArray(TiffField entry); - - public String toString() - { - return "[" + getClass().getName() + ". type: " + type + ", name: " - + name + ", length: " + length + "]"; - } - - public abstract byte[] writeData(Object o, int byteOrder) - throws ImageWriteException; - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeASCII.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeASCII.java deleted file mode 100644 index 72015ef..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeASCII.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; - -public class FieldTypeASCII extends FieldType -{ - public FieldTypeASCII(int type, String name) - { - super(type, 1, name); - } - - public Object getSimpleValue(TiffField entry) { - // According to EXIF specification "2 = ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL." - byte bytes[] = getRawBytes(entry); - // Ignore last (should be NULL) byte. - return new String(bytes, 0, bytes.length-1); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof byte[]) { - byte bytes[] = (byte[]) o; - byte result[] = new byte[bytes.length + 1]; - System.arraycopy(bytes, 0, result, 0, bytes.length); - result[result.length - 1] = 0; - return result; - } else if (o instanceof String) { - byte bytes[] = ((String) o).getBytes(); - byte result[] = new byte[bytes.length + 1]; - System.arraycopy(bytes, 0, result, 0, bytes.length); - result[result.length - 1] = 0; - return result; - } - else - throw new ImageWriteException("Unknown data type: " + o); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeByte.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeByte.java deleted file mode 100644 index a55ff0d..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeByte.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeByte extends FieldType -{ - public FieldTypeByte(int type, String name) - { - super(type, 1, name); - } - - public Object getSimpleValue(TiffField entry) - { - if (entry.length == 1) - return new Byte(entry.valueOffsetBytes[0]); - - return getRawBytes(entry); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Byte) - return new byte[]{ - ((Byte) o).byteValue(), - }; - else if (o instanceof byte[]) - return (byte[]) o; - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeDouble.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeDouble.java deleted file mode 100644 index d8cb53b..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeDouble.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeDouble extends FieldType -{ - public FieldTypeDouble() - { - super(12, 8, "Double"); - } - - public Object getSimpleValue(TiffField entry) - { - return "?"; - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Double) - return convertDoubleToByteArray(((Double) o).doubleValue(), - byteOrder); - else if (o instanceof double[]) - { - double numbers[] = (double[]) o; - return convertDoubleArrayToByteArray(numbers, byteOrder); - } - else if (o instanceof Double[]) - { - Double numbers[] = (Double[]) o; - double values[] = new double[numbers.length]; - for (int i = 0; i < values.length; i++) - values[i] = numbers[i].doubleValue(); - return convertDoubleArrayToByteArray(values, byteOrder); - } - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeFloat.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeFloat.java deleted file mode 100644 index 8fe2209..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeFloat.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeFloat extends FieldType -{ - public FieldTypeFloat() - { - super(11, 4, "Float"); - } - - // = new FieldType(11, 4, "Float") - - public Object getSimpleValue(TiffField entry) - { - if (entry.length == 1) - return new Float(convertByteArrayToFloat(name + " (" - + entry.tagInfo.name + ")", entry.valueOffsetBytes, - entry.byteOrder)); - - return convertByteArrayToFloatArray(name + " (" + entry.tagInfo.name - + ")", getRawBytes(entry), 0, entry.length, entry.byteOrder); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Float) - return convertFloatToByteArray(((Float) o).floatValue(), byteOrder); - else if (o instanceof float[]) - { - float numbers[] = (float[]) o; - return convertFloatArrayToByteArray(numbers, byteOrder); - } - else if (o instanceof Float[]) - { - Float numbers[] = (Float[]) o; - float values[] = new float[numbers.length]; - for (int i = 0; i < values.length; i++) - values[i] = numbers[i].floatValue(); - return convertFloatArrayToByteArray(values, byteOrder); - } - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeLong.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeLong.java deleted file mode 100644 index b8e65eb..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeLong.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeLong extends FieldType -{ - public FieldTypeLong(int type, String name) - { - super(type, 4, name); - } - - public Object getSimpleValue(TiffField entry) - { - if (entry.length == 1) - return new Integer(convertByteArrayToInt(name + " (" - + entry.tagInfo.name + ")", entry.valueOffsetBytes, - entry.byteOrder)); - - return convertByteArrayToIntArray(name + " (" + entry.tagInfo.name - + ")", getRawBytes(entry), 0, entry.length, entry.byteOrder); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Integer) - return convertIntArrayToByteArray(new int[]{ - ((Integer) o).intValue(), - }, byteOrder); - else if (o instanceof int[]) - { - int numbers[] = (int[]) o; - return convertIntArrayToByteArray(numbers, byteOrder); - } - else if (o instanceof Integer[]) - { - Integer numbers[] = (Integer[]) o; - int values[] = new int[numbers.length]; - for (int i = 0; i < values.length; i++) - values[i] = numbers[i].intValue(); - return convertIntArrayToByteArray(values, byteOrder); - } - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeRational.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeRational.java deleted file mode 100644 index 33748af..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeRational.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.RationalNumber; -import org.apache.sanselan.common.RationalNumberUtilities; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeRational extends FieldType -{ - public FieldTypeRational(int type, String name) - { - super(type, 8, name); - } - - public Object getSimpleValue(TiffField entry) - { - if (entry.length == 1) - return convertByteArrayToRational(name + " (" + entry.tagInfo.name - + ")", entry.oversizeValue, entry.byteOrder); - - return convertByteArrayToRationalArray(name + " (" + entry.tagInfo.name - + ")", getRawBytes(entry), 0, entry.length, entry.byteOrder); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof RationalNumber) - return convertRationalToByteArray((RationalNumber) o, byteOrder); - else if (o instanceof RationalNumber[]) - { - return convertRationalArrayToByteArray((RationalNumber[]) o, - byteOrder); - } - else if (o instanceof Number) - { - Number number = (Number) o; - RationalNumber rationalNumber = RationalNumberUtilities - .getRationalNumber(number.doubleValue()); - return convertRationalToByteArray(rationalNumber, byteOrder); - } - else if (o instanceof Number[]) - { - Number numbers[] = (Number[]) o; - RationalNumber rationalNumbers[] = new RationalNumber[numbers.length]; - for (int i = 0; i < numbers.length; i++) - { - Number number = numbers[i]; - rationalNumbers[i] = RationalNumberUtilities - .getRationalNumber(number.doubleValue()); - } - return convertRationalArrayToByteArray(rationalNumbers, byteOrder); - } - else if (o instanceof double[]) - { - double numbers[] = (double[]) o; - RationalNumber rationalNumbers[] = new RationalNumber[numbers.length]; - for (int i = 0; i < numbers.length; i++) - { - double number = numbers[i]; - rationalNumbers[i] = RationalNumberUtilities - .getRationalNumber(number); - } - return convertRationalArrayToByteArray(rationalNumbers, byteOrder); - } - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - - public byte[] writeData(int numerator, int denominator, int byteOrder) - throws ImageWriteException - { - return writeData(new int[]{ - numerator - }, new int[]{ - denominator - }, byteOrder); - } - - public byte[] writeData(int numerators[], int denominators[], int byteOrder) - throws ImageWriteException - { - return convertIntArrayToRationalArray(numerators, denominators, - byteOrder); - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeShort.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeShort.java deleted file mode 100644 index b07fb99..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeShort.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeShort extends FieldType -{ - public FieldTypeShort(int type, String name) - { - super(type, 2, name); - } - - // public Object[] getValueArray(TiffField entry) - // { - // if(isLocalValue(entry)) - // return convertByteArrayToShortArray(name + " (" + entry.tagInfo.name - // + ")", entry.valueOffsetBytes, 0, entry.length, entry.byteOrder); - // - //// return new Integer(convertByteArrayToShort(name + " (" - //// + entry.tagInfo.name + ")", entry.valueOffsetBytes, - //// entry.byteOrder)); - // - // return convertByteArrayToShortArray(name + " (" + entry.tagInfo.name - // + ")", getRawBytes(entry), 0, entry.length, entry.byteOrder); - // } - - public Object getSimpleValue(TiffField entry) throws ImageReadException - { - if (entry.length == 1) - return new Integer(convertByteArrayToShort(name + " (" - + entry.tagInfo.name + ")", entry.valueOffsetBytes, - entry.byteOrder)); - - return convertByteArrayToShortArray(name + " (" + entry.tagInfo.name - + ")", getRawBytes(entry), 0, entry.length, entry.byteOrder); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Integer) - return convertShortArrayToByteArray(new int[]{ - ((Integer) o).intValue(), - }, byteOrder); - else if (o instanceof int[]) - { - int numbers[] = (int[]) o; - return convertShortArrayToByteArray(numbers, byteOrder); - } - else if (o instanceof Integer[]) - { - Integer numbers[] = (Integer[]) o; - int values[] = new int[numbers.length]; - for (int i = 0; i < values.length; i++) - values[i] = numbers[i].intValue(); - return convertShortArrayToByteArray(values, byteOrder); - } - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeUnknown.java b/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeUnknown.java deleted file mode 100644 index 755f5f9..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/fieldtypes/FieldTypeUnknown.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.fieldtypes; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.util.Debug; - -public class FieldTypeUnknown extends FieldType -{ - public FieldTypeUnknown() - { - super(-1, 1, "Unknown"); - } - - public Object getSimpleValue(TiffField entry) - { - // Debug.debug("unknown field type. entry", entry.tagInfo.name); - // Debug.debug("unknown field type. entry.type", entry.type); - // Debug.debug("unknown field type. entry.length", entry.length); - // Debug.debug("unknown field type. entry.oversizeValue", entry.oversizeValue); - // Debug.debug("unknown field type. entry.isLocalValue()", entry.isLocalValue()); - // Debug.debug("unknown field type. entry.oversizeValue", entry.oversizeValue); - - if (entry.length == 1) - return new Byte(entry.valueOffsetBytes[0]); - - return getRawBytes(entry); - } - - public byte[] writeData(Object o, int byteOrder) throws ImageWriteException - { - if (o instanceof Byte) - return new byte[]{ - ((Byte) o).byteValue(), - }; - else if (o instanceof byte[]) - return (byte[]) o; - else - throw new ImageWriteException("Invalid data: " + o + " (" - + Debug.getType(o) + ")"); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCIELAB.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCIELAB.java deleted file mode 100644 index 34a24a8..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCIELAB.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.color.ColorConversions; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterCIELAB extends PhotometricInterpreter -{ - public PhotometricInterpreterCIELAB(int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - } - - public void dumpstats() throws ImageReadException, IOException - { - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - int cieL = samples[0]; - int cieA = (byte) samples[1]; - int cieB = (byte) samples[2]; - - int rgb = ColorConversions.convertCIELabtoARGBTest(cieL, cieA, cieB); - bi.setRGB(x, y, rgb); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCMYK.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCMYK.java deleted file mode 100644 index 03a6003..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterCMYK.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.color.ColorConversions; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterCMYK extends PhotometricInterpreter -{ - public PhotometricInterpreterCMYK(int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - - int sc = samples[0]; - int sm = samples[1]; - int sy = samples[2]; - int sk = samples[3]; - - int rgb = ColorConversions.convertCMYKtoRGB(sc, sm, sy, sk); - bi.setRGB(x, y, rgb); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLUV.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLUV.java deleted file mode 100644 index 089da01..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterLogLUV.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterLogLUV extends PhotometricInterpreter -{ - // private final boolean yOnly; - - public PhotometricInterpreterLogLUV(int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height, - boolean yonly) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - - // this.yOnly = yonly; - } - - public void dumpstats() throws ImageReadException, IOException - { - } - - private float cube(float f) - { - return f * f * f; - } - - // private float function_f(float value, ) - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - float X, Y, Z; - - int cieL = samples[0]; - int cieA = (byte) samples[1]; - int cieB = (byte) samples[2]; - - { - - float var_Y = ((cieL * 100.0f / 255.0f) + 16.0f) / 116.0f; - float var_X = cieA / 500.0f + var_Y; - float var_Z = var_Y - cieB / 200.0f; - - float var_x_cube = cube(var_X); - float var_y_cube = cube(var_Y); - float var_z_cube = cube(var_Z); - - if (var_y_cube > 0.008856f) - var_Y = var_y_cube; - else - var_Y = (var_Y - 16 / 116.0f) / 7.787f; - - if (var_x_cube > 0.008856f) - var_X = var_x_cube; - else - var_X = (var_X - 16 / 116.0f) / 7.787f; - - if (var_z_cube > 0.008856f) - var_Z = var_z_cube; - else - var_Z = (var_Z - 16 / 116.0f) / 7.787f; - - float ref_X = 95.047f; - float ref_Y = 100.000f; - float ref_Z = 108.883f; - - X = ref_X * var_X; //ref_X = 95.047 Observer= 2, Illuminant= D65 - Y = ref_Y * var_Y; //ref_Y = 100.000 - Z = ref_Z * var_Z; //ref_Z = 108.883 - - } - - // ref_X = 95.047 //Observer = 2, Illuminant = D65 - // ref_Y = 100.000 - // ref_Z = 108.883 - - int R, G, B; - { - float var_X = X / 100f; //X = From 0 to ref_X - float var_Y = Y / 100f; //Y = From 0 to ref_Y - float var_Z = Z / 100f; //Z = From 0 to ref_Y - - float var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f; - float var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f; - float var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f; - - if (var_R > 0.0031308) - var_R = 1.055f * (float) Math.pow(var_R, (1 / 2.4)) - 0.055f; - else - var_R = 12.92f * var_R; - if (var_G > 0.0031308) - var_G = 1.055f * (float) Math.pow(var_G, (1 / 2.4)) - 0.055f; - else - var_G = 12.92f * var_G; - - if (var_B > 0.0031308) - var_B = 1.055f * (float) Math.pow(var_B, (1 / 2.4)) - 0.055f; - else - var_B = 12.92f * var_B; - - // var_R = (((var_R-))) - // updateMaxMin(new float[]{ - // var_R, var_G, var_B, - // }, maxVarRGB, minVarRGB); - - // var_R = ((var_R + 0.16561039f) / (3.0152583f + 0.16561039f)); - // var_G = ((var_G + 0.06561642f) / (3.0239854f + 0.06561642f)); - // var_B = ((var_B + 0.19393992f) / (3.1043448f + 0.19393992f)); - - R = (int) (var_R * 255f); - G = (int) (var_G * 255f); - B = (int) (var_B * 255f); - } - - // float R = 1.910f * X - 0.532f * Y - 0.288f * Z; - // float G = -0.985f * X + 1.999f * Y - 0.028f * Z; - // float B = 0.058f * X - 0.118f * Y + 0.898f * Z; - - // updateMaxMin(new float[]{ - // R, G, B, - // }, maxRGB, minRGB); - - int red = R; - int green = G; - int blue = B; - - red = Math.min(255, Math.max(0, red)); - green = Math.min(255, Math.max(0, green)); - blue = Math.min(255, Math.max(0, blue)); - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - bi.setRGB(x, y, rgb); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java deleted file mode 100644 index 4330b88..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterPalette.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterPalette extends PhotometricInterpreter -{ - private final int[] fColorMap; - - public PhotometricInterpreterPalette(int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height, - int[] fColorMap) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - - this.fColorMap = fColorMap; - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - int fBitsPerPixel = bitsPerSample[0]; - int colormap_scale = (1 << fBitsPerPixel); - // int expected_colormap_size = 3 * (1 << fBitsPerPixel); - - int index = samples[0]; - int red = fColorMap[index] >> 8; - int green = fColorMap[index + (colormap_scale)] >> 8; - int blue = fColorMap[index + (2 * colormap_scale)] >> 8; - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - bi.setRGB(x, y, rgb); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterRGB.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterRGB.java deleted file mode 100644 index d7f1f2f..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterRGB.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterRGB extends PhotometricInterpreter -{ - public PhotometricInterpreterRGB(int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - int red = samples[0]; - int green = samples[1]; - int blue = samples[2]; - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - bi.setRGB(x, y, rgb); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java b/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java deleted file mode 100644 index 44f90c9..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/photometricinterpreters/PhotometricInterpreterYCbCr.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.photometricinterpreters; - -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class PhotometricInterpreterYCbCr extends PhotometricInterpreter -{ - - public PhotometricInterpreterYCbCr(double[] fYCbCrCoefficients, - int[] fYCbCrPositioning, int[] fYCbCrSubSampling, - double[] fReferenceBlackWhite, int fSamplesPerPixel, - int fBitsPerSample[], int Predictor, int width, int height) - { - super(fSamplesPerPixel, fBitsPerSample, Predictor, width, height); - } - - public int limit(int value, int min, int max) - { - return Math.min(max, Math.max(min, value)); - } - - /** - * This method converts a YUV (aka YCbCr) colorspace to a RGB colorspace. - * This is handy when trying to reconstruct an image in Java from YCbCr transmitted - * data. This routine expects the data to fall in the standard PC 0..255 range - * per pixel, with the array dimensions corresponding to the imageWidth and imageHeight. - * These variables are either set manually in the case of a null constructor, - * or they are automatically calculated from the image parameter constructor. - * @param Y The Y component set. - * @param Cb The Cb component set. - * @param Cr The Cr component set. - * @return R The R component. - */ - public int convertYCbCrtoRGB(int Y, int Cb, int Cr) - { - double r1 = (((1.164 * (Y - 16.0))) + (1.596 * (Cr - 128.0))); - double g1 = (((1.164 * (Y - 16.0))) - - (0.813 * (Cr - 128.0)) - (0.392 * (Cb - 128.0))); - double b1 = (((1.164 * (Y - 16.0))) + (2.017 * (Cb - 128.0))); - - int r = limit((int) r1, 0, 255); - int g = limit((int) g1, 0, 255); - int b = limit((int) b1, 0, 255); - - int alpha = 0xff; - int rgb = (alpha << 24) | (r << 16) | (g << 8) | (b << 0); - return rgb; - } - - public void interpretPixel(BufferedImage bi, int samples[], int x, int y) - throws ImageReadException, IOException - { - int Y = samples[0]; - int Cb = samples[1]; - int Cr = samples[2]; - double R = Y + 1.402 * (Cr - 128.0); - double G = Y - 0.34414 * (Cb - 128.0) - 0.71414 * (Cr - 128.0); - double B = Y + 1.772 * (Cb - 128.0); - - int red = limit((int) R, 0, 255); - int green = limit((int) G, 0, 255); - int blue = limit((int) B, 0, 255); - - int alpha = 0xff; - int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0); - bi.setRGB(x, y, rgb); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterBase.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterBase.java deleted file mode 100644 index 1d4049e..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterBase.java +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryConstants; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.PackBits; -import org.apache.sanselan.common.mylzw.MyLZWCompressor; -import org.apache.sanselan.formats.tiff.TiffElement; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public abstract class TiffImageWriterBase implements TiffConstants, - BinaryConstants -{ - - protected final int byteOrder; - - public TiffImageWriterBase() - { - this.byteOrder = DEFAULT_TIFF_BYTE_ORDER; - } - - public TiffImageWriterBase(int byteOrder) - { - this.byteOrder = byteOrder; - } - - protected final static int imageDataPaddingLength(int dataLength) - { - return (4 - (dataLength % 4)) % 4; - } - - public abstract void write(OutputStream os, TiffOutputSet outputSet) - throws IOException, ImageWriteException; - - protected TiffOutputSummary validateDirectories(TiffOutputSet outputSet) - throws ImageWriteException - { - List directories = outputSet.getDirectories(); - - if (1 > directories.size()) - throw new ImageWriteException("No directories."); - - TiffOutputDirectory exifDirectory = null; - TiffOutputDirectory gpsDirectory = null; - TiffOutputDirectory interoperabilityDirectory = null; - TiffOutputField exifDirectoryOffsetField = null; - TiffOutputField gpsDirectoryOffsetField = null; - TiffOutputField interoperabilityDirectoryOffsetField = null; - - ArrayList directoryIndices = new ArrayList(); - Map directoryTypeMap = new HashMap(); - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - int dirType = directory.type; - Integer key = new Integer(dirType); - directoryTypeMap.put(key, directory); - // Debug.debug("validating dirType", dirType + " (" - // + directory.getFields().size() + " fields)"); - - if (dirType < 0) - { - switch (dirType) - { - case DIRECTORY_TYPE_EXIF: - if (exifDirectory != null) - throw new ImageWriteException( - "More than one EXIF directory."); - exifDirectory = directory; - break; - - case DIRECTORY_TYPE_GPS: - if (gpsDirectory != null) - throw new ImageWriteException( - "More than one GPS directory."); - gpsDirectory = directory; - break; - - case DIRECTORY_TYPE_INTEROPERABILITY: - if (interoperabilityDirectory != null) - throw new ImageWriteException( - "More than one Interoperability directory."); - interoperabilityDirectory = directory; - break; - default: - throw new ImageWriteException("Unknown directory: " - + dirType); - } - } else - { - if (directoryIndices.contains(key)) - throw new ImageWriteException( - "More than one directory with index: " + dirType - + "."); - directoryIndices.add(new Integer(dirType)); - // dirMap.put(arg0, arg1) - } - - HashSet fieldTags = new HashSet(); - ArrayList fields = directory.getFields(); - for (int j = 0; j < fields.size(); j++) - { - TiffOutputField field = (TiffOutputField) fields.get(j); - - Integer fieldKey = new Integer(field.tag); - if (fieldTags.contains(fieldKey)) - throw new ImageWriteException("Tag (" - + field.tagInfo.getDescription() - + ") appears twice in directory."); - fieldTags.add(fieldKey); - - if (field.tag == EXIF_TAG_EXIF_OFFSET.tag) - { - if (exifDirectoryOffsetField != null) - throw new ImageWriteException( - "More than one Exif directory offset field."); - exifDirectoryOffsetField = field; - } else if (field.tag == EXIF_TAG_INTEROP_OFFSET.tag) - { - if (interoperabilityDirectoryOffsetField != null) - throw new ImageWriteException( - "More than one Interoperability directory offset field."); - interoperabilityDirectoryOffsetField = field; - } else if (field.tag == EXIF_TAG_GPSINFO.tag) - { - if (gpsDirectoryOffsetField != null) - throw new ImageWriteException( - "More than one GPS directory offset field."); - gpsDirectoryOffsetField = field; - } - } - // directory. - } - - if (directoryIndices.size() < 1) - throw new ImageWriteException("Missing root directory."); - - // "normal" TIFF directories should have continous indices starting with - // 0, ie. 0, 1, 2... - Collections.sort(directoryIndices); - - TiffOutputDirectory previousDirectory = null; - for (int i = 0; i < directoryIndices.size(); i++) - { - Integer index = (Integer) directoryIndices.get(i); - if (index.intValue() != i) - throw new ImageWriteException("Missing directory: " + i + "."); - - // set up chain of directory references for "normal" directories. - TiffOutputDirectory directory = (TiffOutputDirectory) directoryTypeMap - .get(index); - if (null != previousDirectory) - previousDirectory.setNextDirectory(directory); - previousDirectory = directory; - } - - TiffOutputDirectory rootDirectory = (TiffOutputDirectory) directoryTypeMap - .get(new Integer(DIRECTORY_TYPE_ROOT)); - - // prepare results - TiffOutputSummary result = new TiffOutputSummary(byteOrder, - rootDirectory, directoryTypeMap); - - if (interoperabilityDirectory == null - && interoperabilityDirectoryOffsetField != null) - { - // perhaps we should just discard field? - throw new ImageWriteException( - "Output set has Interoperability Directory Offset field, but no Interoperability Directory"); - } else if (interoperabilityDirectory != null) - { - if (exifDirectory == null) - { - exifDirectory = outputSet.addExifDirectory(); - } - - if (interoperabilityDirectoryOffsetField == null) - { - interoperabilityDirectoryOffsetField = TiffOutputField - .createOffsetField(EXIF_TAG_INTEROP_OFFSET, byteOrder); - exifDirectory.add(interoperabilityDirectoryOffsetField); - } - - result.add(interoperabilityDirectory, - interoperabilityDirectoryOffsetField); - } - - // make sure offset fields and offset'd directories correspond. - if (exifDirectory == null && exifDirectoryOffsetField != null) - { - // perhaps we should just discard field? - throw new ImageWriteException( - "Output set has Exif Directory Offset field, but no Exif Directory"); - } else if (exifDirectory != null) - { - if (exifDirectoryOffsetField == null) - { - exifDirectoryOffsetField = TiffOutputField.createOffsetField( - EXIF_TAG_EXIF_OFFSET, byteOrder); - rootDirectory.add(exifDirectoryOffsetField); - } - - result.add(exifDirectory, exifDirectoryOffsetField); - } - - if (gpsDirectory == null && gpsDirectoryOffsetField != null) - { - // perhaps we should just discard field? - throw new ImageWriteException( - "Output set has GPS Directory Offset field, but no GPS Directory"); - } else if (gpsDirectory != null) - { - if (gpsDirectoryOffsetField == null) - { - gpsDirectoryOffsetField = TiffOutputField.createOffsetField( - EXIF_TAG_GPSINFO, byteOrder); - rootDirectory.add(gpsDirectoryOffsetField); - } - - result.add(gpsDirectory, gpsDirectoryOffsetField); - } - - return result; - - // Debug.debug(); - } - - public void writeImage(BufferedImage src, OutputStream os, Map params) - throws ImageWriteException, IOException - { - // writeImageNew(src, os, params); - // } - // - // public void writeImageNew(BufferedImage src, OutputStream os, Map - // params) - // throws ImageWriteException, IOException - // { - - // make copy of params; we'll clear keys as we consume them. - params = new HashMap(params); - - // clear format key. - if (params.containsKey(PARAM_KEY_FORMAT)) - params.remove(PARAM_KEY_FORMAT); - - String xmpXml = null; - if (params.containsKey(PARAM_KEY_XMP_XML)) - { - xmpXml = (String) params.get(PARAM_KEY_XMP_XML); - params.remove(PARAM_KEY_XMP_XML); - } - - int width = src.getWidth(); - int height = src.getHeight(); - - // BinaryOutputStream bos = new BinaryOutputStream(os, - // WRITE_BYTE_ORDER); - // - // writeImageFileHeader(bos, WRITE_BYTE_ORDER); - - // ArrayList directoryFields = new ArrayList(); - - final int photometricInterpretation = 2; // TODO: - - int compression = TIFF_COMPRESSION_LZW; // LZW is default - if (params.containsKey(PARAM_KEY_COMPRESSION)) - { - Object value = params.get(PARAM_KEY_COMPRESSION); - if (value != null) - { - if (!(value instanceof Number)) - throw new ImageWriteException( - "Invalid compression parameter: " + value); - compression = ((Number) value).intValue(); - } - params.remove(PARAM_KEY_COMPRESSION); - } - - final int samplesPerPixel = 3; // TODO: - final int bitsPerSample = 8; // TODO: - - // int fRowsPerStrip; // TODO: - int rowsPerStrip = 8000 / (width * samplesPerPixel); // TODO: - rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one. - - byte strips[][] = getStrips(src, samplesPerPixel, bitsPerSample, - rowsPerStrip); - - // int stripCount = (height + fRowsPerStrip - 1) / fRowsPerStrip; - // int stripCount = strips.length; - - if (params.size() > 0) - { - Object firstKey = params.keySet().iterator().next(); - throw new ImageWriteException("Unknown parameter: " + firstKey); - } - - // System.out.println("width: " + width); - // System.out.println("height: " + height); - // System.out.println("fRowsPerStrip: " + fRowsPerStrip); - // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel); - // System.out.println("stripCount: " + stripCount); - - if (compression == TIFF_COMPRESSION_PACKBITS) - { - for (int i = 0; i < strips.length; i++) - strips[i] = new PackBits().compress(strips[i]); - } else if (compression == TIFF_COMPRESSION_LZW) - { - for (int i = 0; i < strips.length; i++) - { - byte uncompressed[] = strips[i]; - - int LZW_MINIMUM_CODE_SIZE = 8; - - MyLZWCompressor compressor = new MyLZWCompressor( - LZW_MINIMUM_CODE_SIZE, BYTE_ORDER_MSB, true); - byte compressed[] = compressor.compress(uncompressed); - - strips[i] = compressed; - } - } else if (compression == TIFF_COMPRESSION_UNCOMPRESSED) - { - // do nothing. - } else - throw new ImageWriteException( - "Invalid compression parameter (Only LZW, Packbits and uncompressed supported)."); - - TiffElement.DataElement imageData[] = new TiffElement.DataElement[strips.length]; - for (int i = 0; i < strips.length; i++) - imageData[i] = new TiffImageData.Data(0, strips[i].length, - strips[i]); - - // int stripOffsets[] = new int[stripCount]; - // int stripByteCounts[] = new int[stripCount]; - // - // for (int i = 0; i < strips.length; i++) - // stripByteCounts[i] = strips[i].length; - - TiffOutputSet outputSet = new TiffOutputSet(byteOrder); - TiffOutputDirectory directory = outputSet.addRootDirectory(); - - // WriteField stripOffsetsField; - - { - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_IMAGE_WIDTH, FIELD_TYPE_LONG, 1, - FIELD_TYPE_LONG.writeData(new int[] { width, }, - byteOrder)); - directory.add(field); - } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_IMAGE_LENGTH, FIELD_TYPE_LONG, 1, - FIELD_TYPE_LONG.writeData(new int[] { height, }, - byteOrder)); - directory.add(field); - } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_PHOTOMETRIC_INTERPRETATION, FIELD_TYPE_SHORT, - 1, FIELD_TYPE_SHORT.writeData( - new int[] { photometricInterpretation, }, - byteOrder)); - directory.add(field); - } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_COMPRESSION, FIELD_TYPE_SHORT, 1, - FIELD_TYPE_SHORT.writeData(new int[] { compression, }, - byteOrder)); - directory.add(field); - } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_SAMPLES_PER_PIXEL, FIELD_TYPE_SHORT, 1, - FIELD_TYPE_SHORT.writeData( - new int[] { samplesPerPixel, }, byteOrder)); - directory.add(field); - } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_BITS_PER_SAMPLE, FIELD_TYPE_SHORT, 3, - FIELD_TYPE_SHORT.writeData(new int[] { bitsPerSample, - bitsPerSample, bitsPerSample, }, byteOrder)); - directory.add(field); - } - // { - // stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS, - // FIELD_TYPE_LONG, stripOffsets.length, FIELD_TYPE_LONG - // .writeData(stripOffsets, byteOrder)); - // directory.add(stripOffsetsField); - // } - // { - // WriteField field = new WriteField(TIFF_TAG_STRIP_BYTE_COUNTS, - // FIELD_TYPE_LONG, stripByteCounts.length, - // FIELD_TYPE_LONG.writeData(stripByteCounts, - // WRITE_BYTE_ORDER)); - // directory.add(field); - // } - { - TiffOutputField field = new TiffOutputField( - TIFF_TAG_ROWS_PER_STRIP, FIELD_TYPE_LONG, 1, - FIELD_TYPE_LONG.writeData(new int[] { rowsPerStrip, }, - byteOrder)); - directory.add(field); - } - - { - int resolutionUnit = 2;// inches. - TiffOutputField field = new TiffOutputField( - TIFF_TAG_RESOLUTION_UNIT, FIELD_TYPE_SHORT, 1, - FIELD_TYPE_SHORT.writeData( - new int[] { resolutionUnit, }, byteOrder)); - directory.add(field); - } - - { - int xResolution = 72; - TiffOutputField field = new TiffOutputField( - TIFF_TAG_XRESOLUTION, FIELD_TYPE_RATIONAL, 1, - FIELD_TYPE_RATIONAL - .writeData(xResolution, 1, byteOrder)); - directory.add(field); - } - - { - int yResolution = 72; - TiffOutputField field = new TiffOutputField( - TIFF_TAG_YRESOLUTION, FIELD_TYPE_RATIONAL, 1, - FIELD_TYPE_RATIONAL - .writeData(yResolution, 1, byteOrder)); - directory.add(field); - } - - if (null != xmpXml) - { - byte xmpXmlBytes[] = xmpXml.getBytes("utf-8"); - - TiffOutputField field = new TiffOutputField(TIFF_TAG_XMP, - FIELD_TYPE_BYTE, xmpXmlBytes.length, xmpXmlBytes); - directory.add(field); - } - - } - - TiffImageData tiffImageData = new TiffImageData.Strips(imageData, - rowsPerStrip); - directory.setTiffImageData(tiffImageData); - - write(os, outputSet); - } - - private byte[][] getStrips(BufferedImage src, int samplesPerPixel, - int bitsPerSample, int rowsPerStrip) - { - int width = src.getWidth(); - int height = src.getHeight(); - - int stripCount = (height + rowsPerStrip - 1) / rowsPerStrip; - - byte result[][] = null; - { // Write Strips - result = new byte[stripCount][]; - - int remaining_rows = height; - - for (int i = 0; i < stripCount; i++) - { - int rowsInStrip = Math.min(rowsPerStrip, remaining_rows); - remaining_rows -= rowsInStrip; - - int bitsInStrip = bitsPerSample * rowsInStrip * width - * samplesPerPixel; - int bytesInStrip = (bitsInStrip + 7) / 8; - - byte uncompressed[] = new byte[bytesInStrip]; - - int counter = 0; - int y = i * rowsPerStrip; - int stop = i * rowsPerStrip + rowsPerStrip; - - for (; (y < height) && (y < stop); y++) - { - for (int x = 0; x < width; x++) - { - int rgb = src.getRGB(x, y); - int red = 0xff & (rgb >> 16); - int green = 0xff & (rgb >> 8); - int blue = 0xff & (rgb >> 0); - - uncompressed[counter++] = (byte) red; - uncompressed[counter++] = (byte) green; - uncompressed[counter++] = (byte) blue; - } - } - - result[i] = uncompressed; - } - - } - - return result; - } - - protected void writeImageFileHeader(BinaryOutputStream bos) - throws IOException, ImageWriteException - { - int offsetToFirstIFD = TIFF_HEADER_SIZE; - - writeImageFileHeader(bos, offsetToFirstIFD); - } - - protected void writeImageFileHeader(BinaryOutputStream bos, - int offsetToFirstIFD) throws IOException, ImageWriteException - { - bos.write(byteOrder); - bos.write(byteOrder); - - bos.write2Bytes(42); // tiffVersion - - bos.write4Bytes(offsetToFirstIFD); - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossless.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossless.java deleted file mode 100644 index b6a67fc..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossless.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -import org.apache.sanselan.FormatCompliance; -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryFileFunctions; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.formats.tiff.JpegImageData; -import org.apache.sanselan.formats.tiff.TiffContents; -import org.apache.sanselan.formats.tiff.TiffDirectory; -import org.apache.sanselan.formats.tiff.TiffElement; -import org.apache.sanselan.formats.tiff.TiffField; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.TiffReader; -import org.apache.sanselan.util.Debug; - -public class TiffImageWriterLossless extends TiffImageWriterBase -{ - private final byte exifBytes[]; - - public TiffImageWriterLossless(byte exifBytes[]) - { - this.exifBytes = exifBytes; - } - - public TiffImageWriterLossless(int byteOrder, byte exifBytes[]) - { - super(byteOrder); - this.exifBytes = exifBytes; - } - - // private static class TiffPiece - // { - // public final int offset; - // public final int length; - // - // public TiffPiece(final int offset, final int length) - // { - // this.offset = offset; - // this.length = length; - // } - // } - - private void dumpElements(List elements) throws IOException - { - // try - // { - ByteSource byteSource = new ByteSourceArray(exifBytes); - - dumpElements(byteSource, elements); - // } - // catch (ImageReadException e) - // { - // throw new ImageWriteException(e.getMessage(), e); - // } - } - - private void dumpElements(ByteSource byteSource, List elements) - throws IOException - { - int last = TIFF_HEADER_SIZE; - for (int i = 0; i < elements.size(); i++) - { - TiffElement element = (TiffElement) elements.get(i); - if (element.offset > last) - { - final int SLICE_SIZE = 32; - int gepLength = element.offset - last; - Debug.debug("gap of " + gepLength + " bytes."); - byte bytes[] = byteSource.getBlock(last, gepLength); - if (bytes.length > 2 * SLICE_SIZE) - { - Debug.debug("\t" + "head", BinaryFileFunctions.head(bytes, - SLICE_SIZE)); - Debug.debug("\t" + "tail", BinaryFileFunctions.tail(bytes, - SLICE_SIZE)); - } - else - Debug.debug("\t" + "bytes", bytes); - } - - Debug.debug("element[" + i + "]:" + element.getElementDescription() - + " (" + element.offset + " + " + element.length + " = " - + (element.offset + element.length) + ")"); - if (element instanceof TiffDirectory) - { - TiffDirectory dir = (TiffDirectory) element; - Debug.debug("\t" + "next Directory Offset: " - + dir.nextDirectoryOffset); - } - last = element.offset + element.length; - } - Debug.debug(); - } - - private List analyzeOldTiff() throws ImageWriteException, IOException - { - try - { - ByteSource byteSource = new ByteSourceArray(exifBytes); - Map params = null; - FormatCompliance formatCompliance = FormatCompliance.getDefault(); - TiffContents contents = new TiffReader(false).readContents(byteSource, - params, formatCompliance); - - ArrayList elements = new ArrayList(); - // result.add(contents.header); // ? - - List directories = contents.directories; - for (int d = 0; d < directories.size(); d++) - { - TiffDirectory directory = (TiffDirectory) directories.get(d); - elements.add(directory); - - List fields = directory.getDirectoryEntrys(); - for (int f = 0; f < fields.size(); f++) - { - TiffField field = (TiffField) fields.get(f); - TiffElement oversizeValue = field.getOversizeValueElement(); - if (oversizeValue != null) - elements.add(oversizeValue); - - } - - JpegImageData jpegImageData = directory.getJpegImageData(); - if (jpegImageData != null) - elements.add(jpegImageData); - - TiffImageData tiffImageData = directory.getTiffImageData(); - if (tiffImageData != null) - { - TiffElement.DataElement data[] = tiffImageData - .getImageData(); - for (int i = 0; i < data.length; i++) - elements.add(data[i]); - } - } - - Collections.sort(elements, TiffElement.COMPARATOR); - - // dumpElements(byteSource, elements); - - List result = new ArrayList(); - { - final int TOLERANCE = 3; - // int last = TIFF_HEADER_SIZE; - TiffElement start = null; - int index = -1; - for (int i = 0; i < elements.size(); i++) - { - TiffElement element = (TiffElement) elements.get(i); - int lastElementByte = element.offset + element.length; - if (start == null) - { - start = element; - index = lastElementByte; - } - else if (element.offset - index > TOLERANCE) - { - result.add(new TiffElement.Stub(start.offset, index - - start.offset)); - start = element; - index = lastElementByte; - } - else - { - index = lastElementByte; - } - } - if (null != start) - result.add(new TiffElement.Stub(start.offset, index - - start.offset)); - } - - // dumpElements(byteSource, result); - - return result; - } - catch (ImageReadException e) - { - throw new ImageWriteException(e.getMessage(), e); - } - } - - public void write(OutputStream os, TiffOutputSet outputSet) - throws IOException, ImageWriteException - { - List analysis = analyzeOldTiff(); - int oldLength = exifBytes.length; - if (analysis.size() < 1) - throw new ImageWriteException("Couldn't analyze old tiff data."); - else if (analysis.size() == 1) - { - TiffElement onlyElement = (TiffElement) analysis.get(0); - // Debug.debug("onlyElement", onlyElement.getElementDescription()); - if (onlyElement.offset == TIFF_HEADER_SIZE - && onlyElement.offset + onlyElement.length - + TIFF_HEADER_SIZE == oldLength) - { - // no gaps in old data, safe to complete overwrite. - new TiffImageWriterLossy(byteOrder).write(os, outputSet); - return; - } - } - - // if (true) - // throw new ImageWriteException("hahah"); - - // List directories = outputSet.getDirectories(); - - TiffOutputSummary outputSummary = validateDirectories(outputSet); - - List outputItems = outputSet.getOutputItems(outputSummary); - - int outputLength = updateOffsetsStep(analysis, outputItems); - // Debug.debug("outputLength", outputLength); - - outputSummary.updateOffsets(byteOrder); - - writeStep(os, outputSet, analysis, outputItems, outputLength); - - } - - private static final Comparator ELEMENT_SIZE_COMPARATOR = new Comparator() - { - public int compare(Object o1, Object o2) - { - TiffElement e1 = (TiffElement) o1; - TiffElement e2 = (TiffElement) o2; - return e1.length - e2.length; - } - }; - - private static final Comparator ITEM_SIZE_COMPARATOR = new Comparator() - { - public int compare(Object o1, Object o2) - { - TiffOutputItem e1 = (TiffOutputItem) o1; - TiffOutputItem e2 = (TiffOutputItem) o2; - return e1.getItemLength() - e2.getItemLength(); - } - }; - - private int updateOffsetsStep(List analysis, List outputItems) - throws IOException, ImageWriteException - { - // items we cannot fit into a gap, we shall append to tail. - int overflowIndex = exifBytes.length; - - // make copy. - List unusedElements = new ArrayList(analysis); - - // should already be in order of offset, but make sure. - Collections.sort(unusedElements, TiffElement.COMPARATOR); - Collections.reverse(unusedElements); - // any items that represent a gap at the end of the exif segment, can be discarded. - while (unusedElements.size() > 0) - { - TiffElement element = (TiffElement) unusedElements.get(0); - int elementEnd = element.offset + element.length; - if (elementEnd == overflowIndex) - { - // discarding a tail element. should only happen once. - overflowIndex -= element.length; - unusedElements.remove(0); - } - else - break; - } - - Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR); - Collections.reverse(unusedElements); - - // Debug.debug("unusedElements"); - // dumpElements(unusedElements); - - // make copy. - List unplacedItems = new ArrayList(outputItems); - Collections.sort(unplacedItems, ITEM_SIZE_COMPARATOR); - Collections.reverse(unplacedItems); - - while (unplacedItems.size() > 0) - { - // pop off largest unplaced item. - TiffOutputItem outputItem = (TiffOutputItem) unplacedItems - .remove(0); - int outputItemLength = outputItem.getItemLength(); - // Debug.debug("largest unplaced item: " - // + outputItem.getItemDescription() + " (" + outputItemLength - // + ")"); - - // search for the smallest possible element large enough to hold the item. - TiffElement bestFit = null; - for (int i = 0; i < unusedElements.size(); i++) - { - TiffElement element = (TiffElement) unusedElements.get(i); - if (element.length >= outputItemLength) - bestFit = element; - else - break; - } - if (null == bestFit) - { - // we couldn't place this item. overflow. - outputItem.setOffset(overflowIndex); - overflowIndex += outputItemLength; - } - else - { - outputItem.setOffset(bestFit.offset); - unusedElements.remove(bestFit); - - if (bestFit.length > outputItemLength) - { - // not a perfect fit. - int excessOffset = bestFit.offset + outputItemLength; - int excessLength = bestFit.length - outputItemLength; - unusedElements.add(new TiffElement.Stub(excessOffset, - excessLength)); - // make sure the new element is in the correct order. - Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR); - Collections.reverse(unusedElements); - } - } - } - - return overflowIndex; - // - // if (true) - // throw new IOException("mew"); - // - // // int offset = TIFF_HEADER_SIZE; - // int offset = exifBytes.length; - // - // for (int i = 0; i < outputItems.size(); i++) - // { - // TiffOutputItem outputItem = (TiffOutputItem) outputItems.get(i); - // - // outputItem.setOffset(offset); - // int itemLength = outputItem.getItemLength(); - // offset += itemLength; - // - // int remainder = imageDataPaddingLength(itemLength); - // offset += remainder; - // } - } - private static class BufferOutputStream extends OutputStream - { - private final byte buffer[]; - private int index; - - public BufferOutputStream(final byte[] buffer, final int index) - { - this.buffer = buffer; - this.index = index; - } - - public void write(int b) throws IOException - { - if (index >= buffer.length) - throw new IOException("Buffer overflow."); - - buffer[index++] = (byte) b; - } - - public void write(byte b[], int off, int len) throws IOException - { - if (index + len > buffer.length) - throw new IOException("Buffer overflow."); - System.arraycopy(b, off, buffer, index, len); - index += len; - } - } - - private void writeStep(OutputStream os, TiffOutputSet outputSet, - List analysis, List outputItems, int outputLength) - throws IOException, ImageWriteException - { - TiffOutputDirectory rootDirectory = outputSet.getRootDirectory(); - - byte output[] = new byte[outputLength]; - - // copy old data (including maker notes, etc.) - System.arraycopy(exifBytes, 0, output, 0, Math.min(exifBytes.length, - output.length)); - - // bos.write(exifBytes, TIFF_HEADER_SIZE, exifBytes.length - // - TIFF_HEADER_SIZE); - - { - BufferOutputStream tos = new BufferOutputStream(output, 0); - BinaryOutputStream bos = new BinaryOutputStream(tos, byteOrder); - writeImageFileHeader(bos, rootDirectory.getOffset()); - } - - // zero out the parsed pieces of old exif segment, in case we don't overwrite them. - for (int i = 0; i < analysis.size(); i++) - { - TiffElement element = (TiffElement) analysis.get(i); - for (int j = 0; j < element.length; j++) - { - int index = element.offset + j; - if (index < output.length) - output[index] = 0; - } - } - - // write in the new items - for (int i = 0; i < outputItems.size(); i++) - { - TiffOutputItem outputItem = (TiffOutputItem) outputItems.get(i); - - BufferOutputStream tos = new BufferOutputStream(output, outputItem - .getOffset()); - BinaryOutputStream bos = new BinaryOutputStream(tos, byteOrder); - outputItem.writeItem(bos); - } - - os.write(output); - } - -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossy.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossy.java deleted file mode 100644 index 6a5a76a..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffImageWriterLossy.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; - -public class TiffImageWriterLossy extends TiffImageWriterBase -{ - - public TiffImageWriterLossy() - { - } - - public TiffImageWriterLossy(int byteOrder) - { - super(byteOrder); - } - - public void write(OutputStream os, TiffOutputSet outputSet) - throws IOException, ImageWriteException - { - TiffOutputSummary outputSummary = validateDirectories(outputSet); - - List outputItems = outputSet.getOutputItems(outputSummary); - - updateOffsetsStep(outputItems); - - outputSummary.updateOffsets(byteOrder); - - BinaryOutputStream bos = new BinaryOutputStream(os, byteOrder); - - writeStep(bos, outputItems); - } - - private void updateOffsetsStep(List outputItems) throws IOException, - ImageWriteException - { - int offset = TIFF_HEADER_SIZE; - - for (int i = 0; i < outputItems.size(); i++) - { - TiffOutputItem outputItem = (TiffOutputItem) outputItems.get(i); - - outputItem.setOffset(offset); - int itemLength = outputItem.getItemLength(); - offset += itemLength; - - int remainder = imageDataPaddingLength(itemLength); - offset += remainder; - } - } - - private void writeStep(BinaryOutputStream bos, List outputItems) - throws IOException, ImageWriteException - { - writeImageFileHeader(bos); - - for (int i = 0; i < outputItems.size(); i++) - { - TiffOutputItem outputItem = (TiffOutputItem) outputItems.get(i); - - outputItem.writeItem(bos); - - int length = outputItem.getItemLength(); - - int remainder = imageDataPaddingLength(length); - for (int j = 0; j < remainder; j++) - bos.write(0); - } - - } -} diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputDirectory.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputDirectory.java deleted file mode 100644 index 6937b18..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputDirectory.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.formats.tiff.JpegImageData; -import org.apache.sanselan.formats.tiff.TiffDirectory; -import org.apache.sanselan.formats.tiff.TiffElement; -import org.apache.sanselan.formats.tiff.TiffImageData; -import org.apache.sanselan.formats.tiff.constants.TagConstantsUtils; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; - -public final class TiffOutputDirectory extends TiffOutputItem implements - TiffConstants -{ - public final int type; - private final ArrayList fields = new ArrayList(); - - private TiffOutputDirectory nextDirectory = null; - - public void setNextDirectory(TiffOutputDirectory nextDirectory) - { - this.nextDirectory = nextDirectory; - } - - public TiffOutputDirectory(final int type) - { - this.type = type; - } - - public void add(TiffOutputField field) - { - fields.add(field); - } - - public ArrayList getFields() - { - return new ArrayList(fields); - } - - public void removeField(TagInfo tagInfo) - { - removeField(tagInfo.tag); - } - - public void removeField(int tag) - { - ArrayList matches = new ArrayList(); - for (int i = 0; i < fields.size(); i++) - { - TiffOutputField field = (TiffOutputField) fields.get(i); - if (field.tag == tag) - matches.add(field); - } - fields.removeAll(matches); - } - - public TiffOutputField findField(TagInfo tagInfo) - { - return findField(tagInfo.tag); - } - - public TiffOutputField findField(int tag) - { - for (int i = 0; i < fields.size(); i++) - { - TiffOutputField field = (TiffOutputField) fields.get(i); - if (field.tag == tag) - return field; - } - return null; - } - - public void sortFields() - { - Comparator comparator = new Comparator() { - public int compare(Object o1, Object o2) - { - TiffOutputField e1 = (TiffOutputField) o1; - TiffOutputField e2 = (TiffOutputField) o2; - - if (e1.tag != e2.tag) - return e1.tag - e2.tag; - return e1.getSortHint() - e2.getSortHint(); - } - }; - Collections.sort(fields, comparator); - } - - public String description() - { - return TiffDirectory.description(type); - } - - public void writeItem(BinaryOutputStream bos) throws IOException, - ImageWriteException - { - // Write Directory Field Count - bos.write2Bytes(fields.size()); // DirectoryFieldCount - - // Write Fields - for (int i = 0; i < fields.size(); i++) - { - TiffOutputField field = (TiffOutputField) fields.get(i); - field.writeField(bos); - -// Debug.debug("\t" + "writing field (" + field.tag + ", 0x" + -// Integer.toHexString(field.tag) + ")", field.tagInfo); -// if(field.tagInfo.isOffset()) -// Debug.debug("\t\tOFFSET!", field.bytes); - } - - int nextDirectoryOffset = 0; - if (nextDirectory != null) - nextDirectoryOffset = nextDirectory.getOffset(); - - // Write nextDirectoryOffset - if (nextDirectoryOffset == UNDEFINED_VALUE) - bos.write4Bytes(0); - else - bos.write4Bytes(nextDirectoryOffset); - } - - private JpegImageData jpegImageData = null; - - public void setJpegImageData(JpegImageData rawJpegImageData) - { - this.jpegImageData = rawJpegImageData; - } - - public JpegImageData getRawJpegImageData() - { - return jpegImageData; - } - - private TiffImageData tiffImageData = null; - - public void setTiffImageData(TiffImageData rawTiffImageData) - { - this.tiffImageData = rawTiffImageData; - } - - public TiffImageData getRawTiffImageData() - { - return tiffImageData; - } - - public int getItemLength() - { - return TIFF_ENTRY_LENGTH * fields.size() + TIFF_DIRECTORY_HEADER_LENGTH - + TIFF_DIRECTORY_FOOTER_LENGTH; - } - - public String getItemDescription() - { - ExifDirectoryType dirType = TagConstantsUtils - .getExifDirectoryType(type); - return "Directory: " + dirType.name + " (" + type + ")"; - } - - private void removeFieldIfPresent(TagInfo tagInfo) - { - TiffOutputField field = findField(tagInfo); - if (null != field) - fields.remove(field); - } - - protected List getOutputItems(TiffOutputSummary outputSummary) - throws ImageWriteException - { - // first validate directory fields. - - removeFieldIfPresent(TIFF_TAG_JPEG_INTERCHANGE_FORMAT); - removeFieldIfPresent(TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - - TiffOutputField jpegOffsetField = null; - if (null != jpegImageData) - { - jpegOffsetField = new TiffOutputField( - TIFF_TAG_JPEG_INTERCHANGE_FORMAT, FIELD_TYPE_LONG, 1, - FieldType.getStubLocalValue()); - add(jpegOffsetField); - - byte lengthValue[] = FIELD_TYPE_LONG.writeData( - new int[] { jpegImageData.length, }, - outputSummary.byteOrder); - - TiffOutputField jpegLengthField = new TiffOutputField( - TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, FIELD_TYPE_LONG, - 1, lengthValue); - add(jpegLengthField); - - } - - // -------------------------------------------------------------- - - removeFieldIfPresent(TIFF_TAG_STRIP_OFFSETS); - removeFieldIfPresent(TIFF_TAG_STRIP_BYTE_COUNTS); - removeFieldIfPresent(TIFF_TAG_TILE_OFFSETS); - removeFieldIfPresent(TIFF_TAG_TILE_BYTE_COUNTS); - - TiffOutputField imageDataOffsetField; - ImageDataOffsets imageDataInfo = null; - if (null != tiffImageData) - { - boolean stripsNotTiles = tiffImageData.stripsNotTiles(); - - TagInfo offsetTag; - TagInfo byteCountsTag; - if (stripsNotTiles) - { - offsetTag = TIFF_TAG_STRIP_OFFSETS; - byteCountsTag = TIFF_TAG_STRIP_BYTE_COUNTS; - } else - { - offsetTag = TIFF_TAG_TILE_OFFSETS; - byteCountsTag = TIFF_TAG_TILE_BYTE_COUNTS; - } - - // -------- - - TiffElement.DataElement imageData[] = tiffImageData.getImageData(); - - int imageDataOffsets[] = null; - int imageDataByteCounts[] = null; - // TiffOutputField imageDataOffsetsField = null; - - imageDataOffsets = new int[imageData.length]; - imageDataByteCounts = new int[imageData.length]; - for (int i = 0; i < imageData.length; i++) - { - imageDataByteCounts[i] = imageData[i].length; - } - - // -------- - - // Append imageData-related fields to first directory - imageDataOffsetField = new TiffOutputField(offsetTag, - FIELD_TYPE_LONG, imageDataOffsets.length, FIELD_TYPE_LONG - .writeData(imageDataOffsets, - outputSummary.byteOrder)); - add(imageDataOffsetField); - - // -------- - - byte data[] = FIELD_TYPE_LONG.writeData(imageDataByteCounts, - outputSummary.byteOrder); - TiffOutputField byteCountsField = new TiffOutputField( - byteCountsTag, FIELD_TYPE_LONG, imageDataByteCounts.length, - data); - add(byteCountsField); - - // -------- - - imageDataInfo = new ImageDataOffsets(imageData, imageDataOffsets, - imageDataOffsetField); - } - - // -------------------------------------------------------------- - - List result = new ArrayList(); - result.add(this); - sortFields(); - - for (int i = 0; i < fields.size(); i++) - { - TiffOutputField field = (TiffOutputField) fields.get(i); - if (field.isLocalValue()) - continue; - - TiffOutputItem item = field.getSeperateValue(); - result.add(item); - // outputSummary.add(item, field); - } - - if (null != imageDataInfo) - { - for (int i = 0; i < imageDataInfo.outputItems.length; i++) - result.add(imageDataInfo.outputItems[i]); - - outputSummary.addTiffImageData(imageDataInfo); - } - - if (null != jpegImageData) - { - TiffOutputItem item = new TiffOutputItem.Value("JPEG image data", - jpegImageData.data); - result.add(item); - outputSummary.add(item, jpegOffsetField); - } - - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputField.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputField.java deleted file mode 100644 index 95c3a12..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputField.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.io.IOException; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.common.BinaryOutputStream; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.fieldtypes.FieldType; - -public class TiffOutputField implements TiffConstants -{ - public final int tag; - public final TagInfo tagInfo; - public final FieldType fieldType; - public final int count; - - private byte bytes[]; - - private final TiffOutputItem.Value separateValueItem; - - public TiffOutputField(TagInfo tagInfo, FieldType tagtype, int count, - byte bytes[]) - { - this(tagInfo.tag, tagInfo, tagtype, count, bytes); - } - - public TiffOutputField(final int tag, TagInfo tagInfo, FieldType fieldType, - int count, byte bytes[]) - { - this.tag = tag; - this.tagInfo = tagInfo; - this.fieldType = fieldType; - this.count = count; - this.bytes = bytes; - - if (isLocalValue()) - separateValueItem = null; - else - { - String name = "Field Seperate value (" + tagInfo.getDescription() - + ")"; - separateValueItem = new TiffOutputItem.Value(name, bytes); - } - } - - private int sortHint = -1; - - public static TiffOutputField create(TagInfo tagInfo, int byteOrder, - Number number) throws ImageWriteException - { - if (tagInfo.dataTypes == null || tagInfo.dataTypes.length < 1) - throw new ImageWriteException("Tag has no default data type."); - FieldType fieldType = tagInfo.dataTypes[0]; - - if (tagInfo.length != 1) - throw new ImageWriteException("Tag does not expect a single value."); - - byte bytes[] = fieldType.writeData(number, byteOrder); - - return new TiffOutputField(tagInfo.tag, tagInfo, fieldType, 1, bytes); - } - - public static TiffOutputField create(TagInfo tagInfo, int byteOrder, - Number value[]) throws ImageWriteException - { - if (tagInfo.dataTypes == null || tagInfo.dataTypes.length < 1) - throw new ImageWriteException("Tag has no default data type."); - FieldType fieldType = tagInfo.dataTypes[0]; - - if (tagInfo.length != value.length) - throw new ImageWriteException("Tag does not expect a single value."); - - byte bytes[] = fieldType.writeData(value, byteOrder); - - return new TiffOutputField(tagInfo.tag, tagInfo, fieldType, - value.length, bytes); - } - - public static TiffOutputField create(TagInfo tagInfo, int byteOrder, - String value) throws ImageWriteException - { - FieldType fieldType; - if (tagInfo.dataTypes == null) - fieldType = FIELD_TYPE_ASCII; - else if (tagInfo.dataTypes == FIELD_TYPE_DESCRIPTION_ASCII) - fieldType = FIELD_TYPE_ASCII; - else - throw new ImageWriteException("Tag has unexpected data type."); - - byte bytes[] = fieldType.writeData(value, byteOrder); - - return new TiffOutputField(tagInfo.tag, tagInfo, fieldType, 1, bytes); - } - - protected static final TiffOutputField createOffsetField(TagInfo tagInfo, - int byteOrder) throws ImageWriteException - { - return new TiffOutputField(tagInfo, FIELD_TYPE_LONG, 1, FIELD_TYPE_LONG - .writeData(new int[] { 0, }, byteOrder)); - } - - protected void writeField(BinaryOutputStream bos) throws IOException, - ImageWriteException - { - bos.write2Bytes(tag); - bos.write2Bytes(fieldType.type); - bos.write4Bytes(count); - - if (isLocalValue()) - { - if (separateValueItem != null) - throw new ImageWriteException("Unexpected separate value item."); - if (bytes.length > 4) - throw new ImageWriteException( - "Local value has invalid length: " + bytes.length); - - bos.writeByteArray(bytes); - int remainder = TIFF_ENTRY_MAX_VALUE_LENGTH - bytes.length; - for (int i = 0; i < remainder; i++) - bos.write(0); - } else - { - if (separateValueItem == null) - throw new ImageWriteException("Missing separate value item."); - - bos.write4Bytes(separateValueItem.getOffset()); - } - } - - protected TiffOutputItem getSeperateValue() - { - return separateValueItem; - } - - protected boolean isLocalValue() - { - return bytes.length <= TIFF_ENTRY_MAX_VALUE_LENGTH; - } - - protected void setData(byte bytes[]) throws ImageWriteException - { - // if(tagInfo.isUnknown()) - // Debug.debug("unknown tag(0x" + Integer.toHexString(tag) - // + ") setData", bytes); - - if (this.bytes.length != bytes.length) - throw new ImageWriteException("Cannot change size of value."); - - // boolean wasLocalValue = isLocalValue(); - this.bytes = bytes; - if (separateValueItem != null) - separateValueItem.updateValue(bytes); - // if (isLocalValue() != wasLocalValue) - // throw new Error("Bug. Locality disrupted! " - // + tagInfo.getDescription()); - } - - private static final String newline = System.getProperty("line.separator"); - - public String toString() - { - return toString(null); - } - - public String toString(String prefix) - { - if (prefix == null) - prefix = ""; - StringBuffer result = new StringBuffer(); - - result.append(prefix); - result.append(tagInfo); - result.append(newline); - - result.append(prefix); - result.append("count: " + count); - result.append(newline); - - result.append(prefix); - result.append(fieldType); - result.append(newline); - - return result.toString(); - } - - public int getSortHint() - { - return sortHint; - } - - public void setSortHint(int sortHint) - { - this.sortHint = sortHint; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSet.java b/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSet.java deleted file mode 100644 index 7386d5a..0000000 --- a/src/main/java/org/apache/sanselan/formats/tiff/write/TiffOutputSet.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.tiff.write; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.formats.tiff.constants.TagInfo; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.util.Debug; - -public final class TiffOutputSet implements TiffConstants -{ - public final int byteOrder; - private final ArrayList directories = new ArrayList(); - - public TiffOutputSet() - { - this(TiffConstants.DEFAULT_TIFF_BYTE_ORDER); - } - - public TiffOutputSet(final int byteOrder) - { - super(); - this.byteOrder = byteOrder; - } - - protected List getOutputItems(TiffOutputSummary outputSummary) - throws ImageWriteException - { - List result = new ArrayList(); - - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - - result.addAll(directory.getOutputItems(outputSummary)); - } - - return result; - } - - public void addDirectory(TiffOutputDirectory directory) - throws ImageWriteException - { - if (null != findDirectory(directory.type)) - throw new ImageWriteException( - "Output set already contains a directory of that type."); - directories.add(directory); - } - - public List getDirectories() - { - return new ArrayList(directories); - } - - public TiffOutputDirectory getRootDirectory() - { - return findDirectory(DIRECTORY_TYPE_ROOT); - } - - public TiffOutputDirectory getExifDirectory() - { - return findDirectory(DIRECTORY_TYPE_EXIF); - } - - public TiffOutputDirectory getOrCreateRootDirectory() - throws ImageWriteException - { - TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_ROOT); - if (null != result) - return result; - return addRootDirectory(); - } - - public TiffOutputDirectory getOrCreateExifDirectory() - throws ImageWriteException - { - // EXIF directory requires root directory. - getOrCreateRootDirectory(); - - TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_EXIF); - if (null != result) - return result; - return addExifDirectory(); - } - - public TiffOutputDirectory getOrCreateGPSDirectory() - throws ImageWriteException - { - // GPS directory requires EXIF directory - getOrCreateExifDirectory(); - - TiffOutputDirectory result = findDirectory(DIRECTORY_TYPE_GPS); - if (null != result) - return result; - return addGPSDirectory(); - } - - public TiffOutputDirectory getGPSDirectory() - { - return findDirectory(DIRECTORY_TYPE_GPS); - } - - public TiffOutputDirectory getInteroperabilityDirectory() - { - return findDirectory(DIRECTORY_TYPE_INTEROPERABILITY); - } - - public TiffOutputDirectory findDirectory(int directoryType) - { - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - if (directory.type == directoryType) - return directory; - } - return null; - } - - /** - * A convenience method to update GPS values in EXIF metadata. - * - * @param longitude Longitude in degrees E, negative values are W. - * @param latitude latitude in degrees N, negative values are S. - * @throws ImageWriteException - */ - public void setGPSInDegrees(double longitude, double latitude) - throws ImageWriteException - { - TiffOutputDirectory gpsDirectory = getOrCreateGPSDirectory(); - - String longitudeRef = longitude < 0 ? "W" : "E"; - longitude = Math.abs(longitude); - String latitudeRef = latitude < 0 ? "S" : "N"; - latitude = Math.abs(latitude); - - { - TiffOutputField longitudeRefField = TiffOutputField.create( - TiffConstants.GPS_TAG_GPS_LONGITUDE_REF, byteOrder, - longitudeRef); - gpsDirectory.removeField(TiffConstants.GPS_TAG_GPS_LONGITUDE_REF); - gpsDirectory.add(longitudeRefField); - } - - { - TiffOutputField latitudeRefField = TiffOutputField.create( - TiffConstants.GPS_TAG_GPS_LATITUDE_REF, byteOrder, - latitudeRef); - gpsDirectory.removeField(TiffConstants.GPS_TAG_GPS_LATITUDE_REF); - gpsDirectory.add(latitudeRefField); - } - - { - double value = longitude; - double longitudeDegrees = (long) value; - value %= 1; - value *= 60.0; - double longitudeMinutes = (long) value; - value %= 1; - value *= 60.0; - double longitudeSeconds = value; - Double values[] = { - new Double(longitudeDegrees), new Double(longitudeMinutes), - new Double(longitudeSeconds), - }; - - TiffOutputField longitudeField = TiffOutputField.create( - TiffConstants.GPS_TAG_GPS_LONGITUDE, byteOrder, values); - gpsDirectory.removeField(TiffConstants.GPS_TAG_GPS_LONGITUDE); - gpsDirectory.add(longitudeField); - } - - { - double value = latitude; - double latitudeDegrees = (long) value; - value %= 1; - value *= 60.0; - double latitudeMinutes = (long) value; - value %= 1; - value *= 60.0; - double latitudeSeconds = value; - Double values[] = { - new Double(latitudeDegrees), new Double(latitudeMinutes), - new Double(latitudeSeconds), - }; - - TiffOutputField latitudeField = TiffOutputField.create( - TiffConstants.GPS_TAG_GPS_LATITUDE, byteOrder, values); - gpsDirectory.removeField(TiffConstants.GPS_TAG_GPS_LATITUDE); - gpsDirectory.add(latitudeField); - } - - } - - public void removeField(TagInfo tagInfo) - { - removeField(tagInfo.tag); - } - - public void removeField(int tag) - { - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - directory.removeField(tag); - } - } - - public TiffOutputField findField(TagInfo tagInfo) - { - return findField(tagInfo.tag); - } - - public TiffOutputField findField(int tag) - { - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - TiffOutputField field = directory.findField(tag); - if (null != field) - return field; - } - return null; - } - - public TiffOutputDirectory addRootDirectory() throws ImageWriteException - { - TiffOutputDirectory result = new TiffOutputDirectory( - DIRECTORY_TYPE_ROOT); - addDirectory(result); - return result; - } - - public TiffOutputDirectory addExifDirectory() throws ImageWriteException - { - TiffOutputDirectory result = new TiffOutputDirectory( - DIRECTORY_TYPE_EXIF); - addDirectory(result); - return result; - } - - public TiffOutputDirectory addGPSDirectory() throws ImageWriteException - { - TiffOutputDirectory result = new TiffOutputDirectory(DIRECTORY_TYPE_GPS); - addDirectory(result); - return result; - } - - public TiffOutputDirectory addInteroperabilityDirectory() - throws ImageWriteException - { - getOrCreateExifDirectory(); - - TiffOutputDirectory result = new TiffOutputDirectory( - DIRECTORY_TYPE_INTEROPERABILITY); - addDirectory(result); - return result; - } - - private static final String newline = System.getProperty("line.separator"); - - public String toString() - { - return toString(null); - } - - public String toString(String prefix) - { - if (prefix == null) - prefix = ""; - - StringBuffer result = new StringBuffer(); - - result.append(prefix); - result.append("TiffOutputSet {"); - result.append(newline); - - result.append(prefix); - result.append("byteOrder: " + byteOrder); - result.append(newline); - - for (int i = 0; i < directories.size(); i++) - { - TiffOutputDirectory directory = (TiffOutputDirectory) directories - .get(i); - result.append(prefix); - result.append("\t" + "directory " + i + ": " - + directory.description() + " (" + directory.type + ")"); - result.append(newline); - - ArrayList fields = directory.getFields(); - for (int j = 0; j < fields.size(); j++) - { - TiffOutputField field = (TiffOutputField) fields.get(j); - result.append(prefix); - result.append("\t\t" + "field " + i + ": " + field.tagInfo); - result.append(newline); - } - } - - result.append(prefix); - result.append("}"); - result.append(newline); - - return result.toString(); - } - - public void dump() - { - Debug.debug(this.toString()); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterTrueColor.java b/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterTrueColor.java deleted file mode 100644 index 7fcf8b2..0000000 --- a/src/main/java/org/apache/sanselan/formats/transparencyfilters/TransparencyFilterTrueColor.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.formats.transparencyfilters; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.sanselan.ImageReadException; - -public class TransparencyFilterTrueColor extends TransparencyFilter -{ - private final int transparent_red; - private final int transparent_green; - private final int transparent_blue; - private final int transparent_color; - - public TransparencyFilterTrueColor(byte bytes[]) throws ImageReadException, - IOException - { - super(bytes); - - ByteArrayInputStream is = new ByteArrayInputStream(bytes); - transparent_red = read2Bytes("transparent_red", is, - "tRNS: Missing transparent_color"); - transparent_green = read2Bytes("transparent_green", is, - "tRNS: Missing transparent_color"); - transparent_blue = read2Bytes("transparent_blue", is, - "tRNS: Missing transparent_color"); - - transparent_color = ((0xff & transparent_red) << 16) - | ((0xff & transparent_green) << 8) - | ((0xff & transparent_blue) << 0); - - } - - public int filter(int rgb, int sample) throws ImageReadException, - IOException - { - if ((0x00ffffff & rgb) == transparent_color) - return 0x00; - - return rgb; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/icc/IccProfileInfo.java b/src/main/java/org/apache/sanselan/icc/IccProfileInfo.java deleted file mode 100644 index 6f14fbe..0000000 --- a/src/main/java/org/apache/sanselan/icc/IccProfileInfo.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; - -import org.apache.sanselan.ImageReadException; - -public class IccProfileInfo implements IccConstants -{ - - public final byte data[]; - public final int ProfileSize; - public final int CMMTypeSignature; - public final int ProfileVersion; - public final int ProfileDeviceClassSignature; - public final int ColorSpace; - public final int ProfileConnectionSpace; - public final int ProfileFileSignature; - public final int PrimaryPlatformSignature; - public final int VariousFlags; - public final int DeviceManufacturer; - public final int DeviceModel; - public final int RenderingIntent; - public final int ProfileCreatorSignature; - public final byte ProfileID[]; - public final IccTag tags[]; - - public IccProfileInfo(byte data[], int ProfileSize, int CMMTypeSignature, - int ProfileVersion, int ProfileDeviceClassSignature, - int ColorSpace, int ProfileConnectionSpace, - int ProfileFileSignature, int PrimaryPlatformSignature, - int VariousFlags, int DeviceManufacturer, int DeviceModel, - int RenderingIntent, int ProfileCreatorSignature, byte ProfileID[], - IccTag tags[]) - { - this.data = data; - - this.ProfileSize = ProfileSize; - this.CMMTypeSignature = CMMTypeSignature; - this.ProfileVersion = ProfileVersion; - this.ProfileDeviceClassSignature = ProfileDeviceClassSignature; - this.ColorSpace = ColorSpace; - this.ProfileConnectionSpace = ProfileConnectionSpace; - this.ProfileFileSignature = ProfileFileSignature; - this.PrimaryPlatformSignature = PrimaryPlatformSignature; - this.VariousFlags = VariousFlags; - this.DeviceManufacturer = DeviceManufacturer; - this.DeviceModel = DeviceModel; - this.RenderingIntent = RenderingIntent; - this.ProfileCreatorSignature = ProfileCreatorSignature; - this.ProfileID = ProfileID; - - this.tags = tags; - } - - public boolean issRGB() - { - boolean result = ((DeviceManufacturer == IEC) && (DeviceModel == sRGB)); - return result; - } - - private void printCharQuad(PrintWriter pw, String msg, int i) - { - pw.println(msg + ": '" + (char) (0xff & (i >> 24)) - + (char) (0xff & (i >> 16)) + (char) (0xff & (i >> 8)) - + (char) (0xff & (i >> 0)) + "'"); - } - - public void dump(String prefix) throws IOException - { - System.out.print(toString()); - } - - public String toString() - { - try - { - return toString(""); - } - catch (Exception e) - { - return "IccProfileInfo: Error"; - } - } - - public String toString(String prefix) throws ImageReadException, - IOException - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - - // StringBuffer result = new StringBuffer(); - pw.println(prefix + ": " + "data length: " + data.length); - - printCharQuad(pw, prefix + ": " + "ProfileDeviceClassSignature", - ProfileDeviceClassSignature); - - printCharQuad(pw, prefix + ": " + "CMMTypeSignature", CMMTypeSignature); - - printCharQuad(pw, prefix + ": " + "ProfileDeviceClassSignature", - ProfileDeviceClassSignature); - printCharQuad(pw, prefix + ": " + "ColorSpace", ColorSpace); - printCharQuad(pw, prefix + ": " + "ProfileConnectionSpace", - ProfileConnectionSpace); - - printCharQuad(pw, prefix + ": " + "ProfileFileSignature", - ProfileFileSignature); - - printCharQuad(pw, prefix + ": " + "PrimaryPlatformSignature", - PrimaryPlatformSignature); - - printCharQuad(pw, prefix + ": " + "ProfileFileSignature", - ProfileFileSignature); - - printCharQuad(pw, prefix + ": " + "DeviceManufacturer", - DeviceManufacturer); - - printCharQuad(pw, prefix + ": " + "DeviceModel", DeviceModel); - - printCharQuad(pw, prefix + ": " + "RenderingIntent", RenderingIntent); - - printCharQuad(pw, prefix + ": " + "ProfileCreatorSignature", - ProfileCreatorSignature); - - for (int i = 0; i < tags.length; i++) - { - IccTag tag = tags[i]; - tag.dump(pw, "\t" + i + ": "); - } - - pw.println(prefix + ": " + "issRGB: " + issRGB()); - pw.flush(); - - return sw.getBuffer().toString(); - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/icc/IccProfileParser.java b/src/main/java/org/apache/sanselan/icc/IccProfileParser.java deleted file mode 100644 index 926f49e..0000000 --- a/src/main/java/org/apache/sanselan/icc/IccProfileParser.java +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -import org.apache.sanselan.common.BinaryFileParser; -import org.apache.sanselan.common.byteSources.ByteSource; -import org.apache.sanselan.common.byteSources.ByteSourceArray; -import org.apache.sanselan.common.byteSources.ByteSourceFile; -import org.apache.sanselan.util.CachingInputStream; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.color.ICC_Profile; - - -public class IccProfileParser extends BinaryFileParser implements IccConstants -{ - public IccProfileParser() - { - this.setByteOrder(BYTE_ORDER_NETWORK); - } - - public IccProfileInfo getICCProfileInfo(ICC_Profile icc_profile) - { - if (icc_profile == null) - return null; - - return getICCProfileInfo(new ByteSourceArray(icc_profile.getData())); - } - - public IccProfileInfo getICCProfileInfo(byte bytes[]) - { - if (bytes == null) - return null; - - return getICCProfileInfo(new ByteSourceArray(bytes)); - } - - public IccProfileInfo getICCProfileInfo(File file) - { - if (file == null) - return null; - - return getICCProfileInfo(new ByteSourceFile(file)); - } - - public IccProfileInfo getICCProfileInfo(ByteSource byteSource) - { - - InputStream is = null; - - try - { - - IccProfileInfo result; - { - is = byteSource.getInputStream(); - - result = readICCProfileInfo(is); - } - - if (result == null) - return null; - - is.close(); - is = null; - - for (int i = 0; i < result.tags.length; i++) - { - IccTag tag = result.tags[i]; - byte bytes[] = byteSource.getBlock(tag.offset, tag.length); - // Debug.debug("bytes: " + bytes.length); - tag.setData(bytes); - // tag.dump("\t" + i + ": "); - } - // result.fillInTagData(byteSource); - - return result; - } - catch (Exception e) - { - // Debug.debug("Error: " + file.getAbsolutePath()); - Debug.debug(e); - } - finally - { - try - { - if (is != null) - is.close(); - } - catch (Exception e) - { - Debug.debug(e); - } - - } - - if (debug) - Debug.debug(); - - return null; - } - - private IccProfileInfo readICCProfileInfo(InputStream is) - { - CachingInputStream cis = new CachingInputStream(is); - is = cis; - - if (debug) - Debug.debug(); - - // setDebug(true); - - // if (debug) - // Debug.debug("length: " + length); - - try - { - int ProfileSize = read4Bytes("ProfileSize", is, - "Not a Valid ICC Profile"); - - // if (length != ProfileSize) - // { - // // Debug.debug("Unexpected Length data expected: " + Integer.toHexString((int) length) - // // + ", encoded: " + Integer.toHexString(ProfileSize)); - // // Debug.debug("Unexpected Length data: " + length - // // + ", length: " + ProfileSize); - // // throw new Error("asd"); - // return null; - // } - - int CMMTypeSignature = read4Bytes("Signature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("CMMTypeSignature", CMMTypeSignature); - - int ProfileVersion = read4Bytes("ProfileVersion", is, - "Not a Valid ICC Profile"); - - int ProfileDeviceClassSignature = read4Bytes( - "ProfileDeviceClassSignature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ProfileDeviceClassSignature", - ProfileDeviceClassSignature); - - int ColorSpace = read4Bytes("ColorSpace", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ColorSpace", ColorSpace); - - int ProfileConnectionSpace = read4Bytes("ProfileConnectionSpace", - is, "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ProfileConnectionSpace", ProfileConnectionSpace); - - skipBytes(is, 12, "Not a Valid ICC Profile"); - - int ProfileFileSignature = read4Bytes("ProfileFileSignature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ProfileFileSignature", ProfileFileSignature); - - int PrimaryPlatformSignature = read4Bytes( - "PrimaryPlatformSignature", is, "Not a Valid ICC Profile"); - if (debug) - printCharQuad("PrimaryPlatformSignature", - PrimaryPlatformSignature); - - int VariousFlags = read4Bytes("ProfileFileSignature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ProfileFileSignature", ProfileFileSignature); - - int DeviceManufacturer = read4Bytes("ProfileFileSignature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("DeviceManufacturer", DeviceManufacturer); - - int DeviceModel = read4Bytes("DeviceModel", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("DeviceModel", DeviceModel); - - skipBytes(is, 8, "Not a Valid ICC Profile"); - - int RenderingIntent = read4Bytes("RenderingIntent", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("RenderingIntent", RenderingIntent); - - skipBytes(is, 12, "Not a Valid ICC Profile"); - - int ProfileCreatorSignature = read4Bytes("ProfileCreatorSignature", - is, "Not a Valid ICC Profile"); - if (debug) - printCharQuad("ProfileCreatorSignature", - ProfileCreatorSignature); - - byte ProfileID[] = null; - skipBytes(is, 16, "Not a Valid ICC Profile"); - // readByteArray("ProfileID", 16, is, - // "Not a Valid ICC Profile"); - // if (debug) - // System.out - // .println("ProfileID: '" + new String(ProfileID) + "'"); - - skipBytes(is, 28, "Not a Valid ICC Profile"); - - // this.setDebug(true); - - int TagCount = read4Bytes("TagCount", is, "Not a Valid ICC Profile"); - - // ArrayList tags = new ArrayList(); - IccTag tags[] = new IccTag[TagCount]; - - for (int i = 0; i < TagCount; i++) - { - int TagSignature = read4Bytes("TagSignature[" + i + "]", is, - "Not a Valid ICC Profile"); - // Debug.debug("TagSignature t " - // + Integer.toHexString(TagSignature)); - - // this.printCharQuad("TagSignature", TagSignature); - int OffsetToData = read4Bytes("OffsetToData[" + i + "]", is, - "Not a Valid ICC Profile"); - int ElementSize = read4Bytes("ElementSize[" + i + "]", is, - "Not a Valid ICC Profile"); - - IccTagType fIccTagType = getIccTagType(TagSignature); - // if (fIccTagType == null) - // throw new Error("oops."); - - // System.out - // .println("\t[" - // + i - // + "]: " - // + ((fIccTagType == null) - // ? "unknown" - // : fIccTagType.name)); - // Debug.debug(); - - IccTag tag = new IccTag(TagSignature, OffsetToData, - ElementSize, fIccTagType); - // tag.dump("\t" + i + ": "); - tags[i] = tag; - // tags .add(tag); - } - - { - // read stream to end, filling cache. - while (is.read() >= 0) - ; - } - - byte data[] = cis.getCache(); - - if (data.length < ProfileSize) - throw new IOException("Couldn't read ICC Profile."); - - IccProfileInfo result = new IccProfileInfo(data, ProfileSize, - CMMTypeSignature, ProfileVersion, - ProfileDeviceClassSignature, ColorSpace, - ProfileConnectionSpace, ProfileFileSignature, - PrimaryPlatformSignature, VariousFlags, DeviceManufacturer, - DeviceModel, RenderingIntent, ProfileCreatorSignature, - ProfileID, tags); - - if (debug) - Debug.debug("issRGB: " + result.issRGB()); - - return result; - } - catch (Exception e) - { - Debug.debug(e); - } - - return null; - } - - private IccTagType getIccTagType(int quad) - { - for (int i = 0; i < TagTypes.length; i++) - if (TagTypes[i].signature == quad) - return TagTypes[i]; - - return null; - } - - public Boolean issRGB(ICC_Profile icc_profile) - { - if (icc_profile == null) - return null; - - return issRGB(new ByteSourceArray(icc_profile.getData())); - } - - public Boolean issRGB(byte bytes[]) - { - if (bytes == null) - return null; - - return issRGB(new ByteSourceArray(bytes)); - } - - public Boolean issRGB(File file) - { - if (file == null) - return null; - - return issRGB(new ByteSourceFile(file)); - } - - public Boolean issRGB(ByteSource byteSource) - { - try - { - if (debug) - Debug.debug(); - - // setDebug(true); - - // long length = byteSource.getLength(); - // - // if (debug) - // Debug.debug("length: " + length); - - InputStream is = byteSource.getInputStream(); - - int ProfileSize = read4Bytes("ProfileSize", is, - "Not a Valid ICC Profile"); - - // if (length != ProfileSize) - // return null; - - this.skipBytes(is, 4 * 5); - - skipBytes(is, 12, "Not a Valid ICC Profile"); - - this.skipBytes(is, 4 * 3); - - int DeviceManufacturer = read4Bytes("ProfileFileSignature", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("DeviceManufacturer", DeviceManufacturer); - - int DeviceModel = read4Bytes("DeviceModel", is, - "Not a Valid ICC Profile"); - if (debug) - printCharQuad("DeviceModel", DeviceModel); - - boolean result = ((DeviceManufacturer == IEC) && (DeviceModel == sRGB)); - - return new Boolean(result); - } - catch (Exception e) - { - Debug.debug(e); - } - - return null; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/icc/IccTag.java b/src/main/java/org/apache/sanselan/icc/IccTag.java deleted file mode 100644 index 925e33c..0000000 --- a/src/main/java/org/apache/sanselan/icc/IccTag.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; - -import org.apache.sanselan.ImageReadException; -import org.apache.sanselan.common.BinaryConstants; -import org.apache.sanselan.common.BinaryInputStream; - -public class IccTag implements BinaryConstants, IccConstants -{ - public final int signature; - public final int offset, length; - public final IccTagType fIccTagType; - - // public final byte data[]; - - public IccTag(int signature, int offset, int length, IccTagType fIccTagType) - { - this.signature = signature; - this.offset = offset; - this.length = length; - this.fIccTagType = fIccTagType; - } - - public byte data[] = null; - private IccTagDataType itdt = null; - private int data_type_signature; - - public void setData(byte bytes[]) throws ImageReadException, IOException - { - data = bytes; - - BinaryInputStream bis = new BinaryInputStream(new ByteArrayInputStream( - bytes), BYTE_ORDER_NETWORK); - data_type_signature = bis.read4Bytes("data type signature", - "ICC: corrupt tag data"); - - itdt = getIccTagDataType(data_type_signature); - // if (itdt != null) - // { - // System.out.println("\t\t\t" + "itdt: " + itdt.name); - // } - - } - - private IccTagDataType getIccTagDataType(int quad) - { - for (int i = 0; i < IccTagDataTypes.length; i++) - if (IccTagDataTypes[i].signature == quad) - return IccTagDataTypes[i]; - - return null; - } - - public void dump(String prefix) throws ImageReadException, IOException - { - PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out)); - - dump(pw, prefix); - - pw.flush(); - } - - public void dump(PrintWriter pw, String prefix) throws ImageReadException, - IOException - { - pw.println(prefix - + "tag signature: " - + Integer.toHexString(signature) - + " (" - + new String(new byte[]{ - (byte) (0xff & (signature >> 24)), - (byte) (0xff & (signature >> 16)), - (byte) (0xff & (signature >> 8)), - (byte) (0xff & (signature >> 0)), - }) + ")"); - - if (data == null) - pw.println(prefix + "data: " + data); - else - { - pw.println(prefix + "data: " + data.length); - - pw.println(prefix - + "data type signature: " - + Integer.toHexString(data_type_signature) - + " (" - + new String(new byte[]{ - (byte) (0xff & (data_type_signature >> 24)), - (byte) (0xff & (data_type_signature >> 16)), - (byte) (0xff & (data_type_signature >> 8)), - (byte) (0xff & (data_type_signature >> 0)), - }) + ")"); - - if (itdt == null) - pw.println(prefix + "IccTagType : " + "unknown"); - else - { - pw.println(prefix + "IccTagType : " + itdt.name); - itdt.dump(prefix, data); - } - - } - - pw.println(""); - pw.flush(); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/icc/IccTagType.java b/src/main/java/org/apache/sanselan/icc/IccTagType.java deleted file mode 100644 index ca855ae..0000000 --- a/src/main/java/org/apache/sanselan/icc/IccTagType.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.icc; - -public class IccTagType -{ - public final String name; - public final String type_description; - public final int signature; - - public IccTagType(String name, String type_description, int signature) - { - this.name = name; - this.type_description = type_description; - this.signature = signature; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/palette/ColorSpaceSubset.java b/src/main/java/org/apache/sanselan/palette/ColorSpaceSubset.java deleted file mode 100644 index 4f067f0..0000000 --- a/src/main/java/org/apache/sanselan/palette/ColorSpaceSubset.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -class ColorSpaceSubset implements Comparable -{ - public final int mins[], maxs[]; - public final int precision; - public final int precision_mask; - public final int total; - public int rgb; // median - - public ColorSpaceSubset(int total, int precision) - { - this.total = total; - this.precision = precision; - precision_mask = (1 << precision) - 1; - - mins = new int[PaletteFactory.components]; - maxs = new int[PaletteFactory.components]; - for (int i = 0; i < PaletteFactory.components; i++) - { - mins[i] = 0; - // maxs[i] = 255; - maxs[i] = precision_mask; - } - - rgb = -1; - } - - public ColorSpaceSubset(int total, int precision, int mins[], int maxs[], - int table[]) - { - this.total = total; - this.precision = precision; - this.mins = mins; - this.maxs = maxs; - precision_mask = (1 << precision) - 1; - - rgb = -1; - } - - public static long compares = 0; - - public final boolean contains(int red, int green, int blue) - { - compares++; - - red >>= (8 - precision); - if (mins[0] > red) - return false; - if (maxs[0] < red) - return false; - - green >>= (8 - precision); - if (mins[1] > green) - return false; - if (maxs[1] < green) - return false; - - blue >>= (8 - precision); - if (mins[2] > blue) - return false; - if (maxs[2] < blue) - return false; - - return true; - } - - public void dump(String prefix) - { - int rdiff = maxs[0] - mins[0] + 1; - int gdiff = maxs[1] - mins[1] + 1; - int bdiff = maxs[2] - mins[2] + 1; - int color_area = rdiff * gdiff * bdiff; - - System.out.println(prefix + ": [" + Integer.toHexString(rgb) - + "] total : " + total - // + " (" - // + (100.0 * (double) total / (double) total_area) - // + " %)" - ); - System.out.println("\t" + "rgb: " + Integer.toHexString(rgb) + ", " - + "red: " + Integer.toHexString(mins[0] << (8 - precision)) - + ", " + Integer.toHexString(maxs[0] << (8 - precision)) + ", " - + "green: " + Integer.toHexString(mins[1] << (8 - precision)) - + ", " + Integer.toHexString(maxs[1] << (8 - precision)) + ", " - + "blue: " + Integer.toHexString(mins[2] << (8 - precision)) - + ", " + Integer.toHexString(maxs[2] << (8 - precision))); - System.out.println("\t" + "red: " + mins[0] + ", " + maxs[0] + ", " - + "green: " + mins[1] + ", " + maxs[1] + ", " + "blue: " - + mins[2] + ", " + maxs[2]); - System.out - .println("\t" + "rdiff: " + rdiff + ", " + "gdiff: " + gdiff - + ", " + "bdiff: " + bdiff + ", " + "color_area: " - + color_area); - } - - public void dumpJustRGB(String prefix) - { - System.out.println("\t" + "rgb: " + Integer.toHexString(rgb) + ", " - + "red: " + Integer.toHexString(mins[0] << (8 - precision)) - + ", " + Integer.toHexString(maxs[0] << (8 - precision)) + ", " - + "green: " + Integer.toHexString(mins[1] << (8 - precision)) - + ", " + Integer.toHexString(maxs[1] << (8 - precision)) + ", " - + "blue: " + Integer.toHexString(mins[2] << (8 - precision)) - + ", " + Integer.toHexString(maxs[2] << (8 - precision))); - } - - public int getArea() - { - int rdiff = maxs[0] - mins[0] + 1; - int gdiff = maxs[1] - mins[1] + 1; - int bdiff = maxs[2] - mins[2] + 1; - int color_area = rdiff * gdiff * bdiff; - - return color_area; - - } - - public void setAverageRGB(int table[]) - { - - { - long redsum = 0, greensum = 0, bluesum = 0; - - for (int red = mins[0]; red <= maxs[0]; red++) - for (int green = mins[1]; green <= maxs[1]; green++) - for (int blue = mins[2]; blue <= maxs[2]; blue++) - // for (int red = 0; red <= precision_mask; red++) - // for (int green = 0; green <= precision_mask; green++) - // for (int blue = 0; blue <= precision_mask; blue++) - { - int index = (blue << (2 * precision)) // note: order reversed - | (green << (1 * precision)) - | (red << (0 * precision)); - int count = table[index]; - redsum += count * (red << (8 - precision)); - greensum += count * (green << (8 - precision)); - bluesum += count * (blue << (8 - precision)); - - } - - redsum /= total; - greensum /= total; - bluesum /= total; - rgb = (int) (((redsum & 0xff) << 16) | ((greensum & 0xff) << 8) | ((bluesum & 0xff) << 0)); - } - } - - public int compareTo(Object o) - { - ColorSpaceSubset other = (ColorSpaceSubset) o; - return rgb - other.rgb; - } - - public int index; - - public final void setIndex(int i) - { - index = i; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/palette/MedianCutQuantizer.java b/src/main/java/org/apache/sanselan/palette/MedianCutQuantizer.java deleted file mode 100644 index 606f64c..0000000 --- a/src/main/java/org/apache/sanselan/palette/MedianCutQuantizer.java +++ /dev/null @@ -1,528 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; - -import org.apache.sanselan.ImageWriteException; -import org.apache.sanselan.util.Debug; - -import com.google.code.appengine.awt.image.BufferedImage; - - -public class MedianCutQuantizer -{ - private final boolean ignoreAlpha; - - public MedianCutQuantizer(boolean ignore_alpha) - { - this.ignoreAlpha = ignore_alpha; - } - - private static class ColorCount - { - public final int argb; - public int count = 0; - public final int alpha, red, green, blue; - - public ColorCount(int argb) - { - this.argb = argb; - - alpha = 0xff & (argb >> 24); - red = 0xff & (argb >> 16); - green = 0xff & (argb >> 8); - blue = 0xff & (argb >> 0); - - // super.hashCode(); - } - - public int hashCode() - { - return argb; - } - - public boolean equals(Object o) - { - ColorCount other = (ColorCount) o; - return other.argb == this.argb; - } - - } - - private class ColorGroup - { - // public final ColorGroup parent; - public ColorGroupCut cut = null; - // public final ArrayList children = new ArrayList(); - public int palette_index = -1; - - public final ArrayList color_counts; - public int min_red = Integer.MAX_VALUE; - public int max_red = Integer.MIN_VALUE; - public int min_green = Integer.MAX_VALUE; - public int max_green = Integer.MIN_VALUE; - public int min_blue = Integer.MAX_VALUE; - public int max_blue = Integer.MIN_VALUE; - public int min_alpha = Integer.MAX_VALUE; - public int max_alpha = Integer.MIN_VALUE; - - public final int alpha_diff; - public final int red_diff; - public final int green_diff; - public final int blue_diff; - - public final int max_diff; - public final int diff_total; - - public ColorGroup(final ArrayList color_counts) - throws ImageWriteException - { - this.color_counts = color_counts; - - if (color_counts.size() < 1) - throw new ImageWriteException("empty color_group"); - - for (int i = 0; i < color_counts.size(); i++) - { - ColorCount color = (ColorCount) color_counts.get(i); - - min_alpha = Math.min(min_alpha, color.alpha); - max_alpha = Math.max(max_alpha, color.alpha); - min_red = Math.min(min_red, color.red); - max_red = Math.max(max_red, color.red); - min_green = Math.min(min_green, color.green); - max_green = Math.max(max_green, color.green); - min_blue = Math.min(min_blue, color.blue); - max_blue = Math.max(max_blue, color.blue); - } - - alpha_diff = max_alpha - min_alpha; - red_diff = max_red - min_red; - green_diff = max_green - min_green; - blue_diff = max_blue - min_blue; - max_diff = Math.max(ignoreAlpha ? red_diff : Math.max(alpha_diff, - red_diff), Math.max(green_diff, blue_diff)); - diff_total = (ignoreAlpha ? 0 : alpha_diff) + red_diff + green_diff - + blue_diff; - - } - - public boolean contains(int argb) - { - int alpha = 0xff & (argb >> 24); - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - - if (!ignoreAlpha && (alpha < min_alpha || alpha > max_alpha)) - return false; - if (red < min_red || red > max_red) - return false; - if (green < min_green || green > max_green) - return false; - if (blue < min_blue || blue > max_blue) - return false; - return true; - } - - public int getMedianValue() - { - long count_total = 0; - long alpha_total = 0, red_total = 0, green_total = 0, blue_total = 0; - - for (int i = 0; i < color_counts.size(); i++) - { - ColorCount color = (ColorCount) color_counts.get(i); - - count_total += color.count; - alpha_total += color.count * color.alpha; - red_total += color.count * color.red; - green_total += color.count * color.green; - blue_total += color.count * color.blue; - } - - int alpha = ignoreAlpha ? 0xff : (int) Math - .round((double) alpha_total / count_total); - int red = (int) Math.round((double) red_total / count_total); - int green = (int) Math.round((double) green_total / count_total); - int blue = (int) Math.round((double) blue_total / count_total); - - return (alpha << 24) | (red << 16) | (green << 8) | blue; - } - - public String toString() - { - return "{ColorGroup. min_red: " + Integer.toHexString(min_red) - + ", max_red: " + Integer.toHexString(max_red) - + ", min_green: " + Integer.toHexString(min_green) - + ", max_green: " + Integer.toHexString(max_green) - + ", min_blue: " + Integer.toHexString(min_blue) - + ", max_blue: " + Integer.toHexString(max_blue) - + ", min_alpha: " + Integer.toHexString(min_alpha) - + ", max_alpha: " + Integer.toHexString(max_alpha) - + ", max_diff: " + Integer.toHexString(max_diff) - + ", diff_total: " + diff_total + "}"; - } - - } - - public Map groupColors1(BufferedImage image, int max, int mask) - throws ImageWriteException - { - Map color_map = new HashMap(); - - int width = image.getWidth(); - int height = image.getHeight(); - - int row[] = new int[width]; - for (int y = 0; y < height; y++) - { - image.getRGB(0, y, width, 1, row, 0, width); - for (int x = 0; x < width; x++) - { - int argb = row[x]; - - if (ignoreAlpha) - argb &= 0xffffff; - argb &= mask; - - ColorCount color = (ColorCount) color_map - .get(new Integer(argb)); - if (color == null) - { - color = new ColorCount(argb); - color_map.put(new Integer(argb), color); - if (color_map.keySet().size() > max) - return null; - } - color.count++; - } - } - - return color_map; - } - - public Map groupColors(BufferedImage image, int max_colors) - throws ImageWriteException - { - int max = Integer.MAX_VALUE; - - for (int i = 0; i < 8; i++) - { - int mask = 0xff & (0xff << i); - mask = mask | (mask << 8) | (mask << 16) | (mask << 24); - - Debug.debug("mask(" + i + ")", mask + " (" - + Integer.toHexString(mask) + ")"); - - Map result = groupColors1(image, max, mask); - if (result != null) - return result; - } - throw new Error(""); - } - - public Palette process(BufferedImage image, int max_colors, boolean verbose) - throws ImageWriteException - { - Map color_map = groupColors(image, max_colors); - - int discrete_colors = color_map.keySet().size(); - if (discrete_colors <= max_colors) - { - if (verbose) - Debug.debug("lossless palette: " + discrete_colors); - - int palette[] = new int[discrete_colors]; - ArrayList color_counts = new ArrayList(color_map.values()); - - for (int i = 0; i < color_counts.size(); i++) - { - ColorCount color_count = (ColorCount) color_counts.get(i); - palette[i] = color_count.argb; - if (ignoreAlpha) - palette[i] |= 0xff000000; - } - - return new SimplePalette(palette); - } - - if (verbose) - Debug.debug("discrete colors: " + discrete_colors); - - ArrayList color_groups = new ArrayList(); - ColorGroup root = new ColorGroup(new ArrayList(color_map.values())); - { - color_groups.add(root); - - final Comparator comparator = new Comparator() - { - public int compare(Object o1, Object o2) - { - ColorGroup cg1 = (ColorGroup) o1; - ColorGroup cg2 = (ColorGroup) o2; - - if (cg1.max_diff == cg2.max_diff) - return cg2.diff_total - cg1.diff_total; - return cg2.max_diff - cg1.max_diff; - } - }; - - while (color_groups.size() < max_colors) - { - Collections.sort(color_groups, comparator); - - ColorGroup color_group = (ColorGroup) color_groups.get(0); - - if (color_group.max_diff == 0) - break; - if (!ignoreAlpha - && color_group.alpha_diff > color_group.red_diff - && color_group.alpha_diff > color_group.green_diff - && color_group.alpha_diff > color_group.blue_diff) - { - doCut(color_group, ALPHA, color_groups); - } - else if (color_group.red_diff > color_group.green_diff - && color_group.red_diff > color_group.blue_diff) - { - doCut(color_group, RED, color_groups); - } - else if (color_group.green_diff > color_group.blue_diff) - { - doCut(color_group, GREEN, color_groups); - } - else - { - doCut(color_group, BLUE, color_groups); - } - } - } - - { - int palette_size = color_groups.size(); - if (verbose) - Debug.debug("palette size: " + palette_size); - - int palette[] = new int[palette_size]; - - for (int i = 0; i < color_groups.size(); i++) - { - ColorGroup color_group = (ColorGroup) color_groups.get(i); - - palette[i] = color_group.getMedianValue(); - - color_group.palette_index = i; - - if (color_group.color_counts.size() < 1) - throw new ImageWriteException("empty color_group: " - + color_group); - - // if(color_group.) - // Debug.debug("color_group", color_group); - // Debug.debug("palette[" + i + "]", palette[i] + " (" - // + Integer.toHexString(palette[i]) + ")"); - } - - if (palette_size > discrete_colors) - throw new ImageWriteException("palette_size>discrete_colors"); - - return new MedianCutPalette(root, palette); - } - } - - private static final int ALPHA = 0; - private static final int RED = 1; - private static final int GREEN = 2; - private static final int BLUE = 3; - - private void doCut(ColorGroup color_group, final int mode, - final ArrayList color_groups) throws ImageWriteException - { - int count_total = 0; - for (int i = 0; i < color_group.color_counts.size(); i++) - { - ColorCount color_count = (ColorCount) color_group.color_counts - .get(i); - count_total += color_count.count; - } - - Comparator comparator = new Comparator() - { - public int compare(Object o1, Object o2) - { - ColorCount c1 = (ColorCount) o1; - ColorCount c2 = (ColorCount) o2; - - switch (mode) - { - case ALPHA : - return c1.alpha - c2.alpha; - case RED : - return c1.red - c2.red; - case GREEN : - return c1.green - c2.green; - case BLUE : - return c1.blue - c2.blue; - default : - return 0; - } - } - }; - - Collections.sort(color_group.color_counts, comparator); - int count_half = (int) Math.round((double) count_total / 2); - int old_count = 0, new_count = 0; - int median_index; - for (median_index = 0; median_index < color_group.color_counts.size(); median_index++) - { - ColorCount color_count = (ColorCount) color_group.color_counts - .get(median_index); - - new_count += color_count.count; - - if (new_count < count_half) - { - old_count = new_count; - continue; - } - break; - } - - if (median_index == color_group.color_counts.size() - 1) - median_index--; - else if (median_index > 0) - { - int new_diff = Math.abs(new_count - count_half); - int old_diff = Math.abs(count_half - old_count); - if (old_diff < new_diff) - median_index--; - } - - color_groups.remove(color_group); - { - ArrayList color_counts1 = new ArrayList(color_group.color_counts - .subList(0, median_index + 1)); - ArrayList color_counts2 = new ArrayList(color_group.color_counts - .subList(median_index + 1, color_group.color_counts.size())); - - ColorGroup less, more; - { - less = new ColorGroup(new ArrayList(color_counts1)); - color_groups.add(less); - } - { - more = new ColorGroup(new ArrayList(color_counts2)); - color_groups.add(more); - } - - ColorCount median_value = (ColorCount) color_group.color_counts - .get(median_index); - int limit; - switch (mode) - { - case ALPHA : - limit = median_value.alpha; - break; - case RED : - limit = median_value.red; - break; - case GREEN : - limit = median_value.green; - break; - case BLUE : - limit = median_value.blue; - break; - default : - throw new Error("Bad mode."); - } - color_group.cut = new ColorGroupCut(less, more, mode, limit); - - } - } - private class ColorGroupCut - { - public final ColorGroup less, more; - public final int mode, limit; - - public ColorGroupCut(ColorGroup less, ColorGroup more, int mode, - int limit) - { - this.less = less; - this.more = more; - this.mode = mode; - this.limit = limit; - } - - public ColorGroup getColorGroup(int argb) - { - int value; - switch (mode) - { - case ALPHA : - value = 0xff & (argb >> 24); - break; - case RED : - value = 0xff & (argb >> 16); - break; - case GREEN : - value = 0xff & (argb >> 8); - break; - case BLUE : - value = 0xff & (argb >> 0); - break; - default : - throw new Error("bad mode."); - } - if (value <= limit) - return less; - return more; - } - - } - - public class MedianCutPalette extends SimplePalette - { - private final ColorGroup root; - - public MedianCutPalette(ColorGroup root, int palette[]) - { - super(palette); - this.root = root; - } - - public int getPaletteIndex(int rgb) - { - ColorGroup cg = root; - - while (cg.cut != null) - { - ColorGroup next = cg.cut.getColorGroup(rgb); - - cg = next; - } - - return cg.palette_index; - } - } - -} diff --git a/src/main/java/org/apache/sanselan/palette/PaletteFactory.java b/src/main/java/org/apache/sanselan/palette/PaletteFactory.java deleted file mode 100644 index 2d80fc0..0000000 --- a/src/main/java/org/apache/sanselan/palette/PaletteFactory.java +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.google.code.appengine.awt.color.ColorSpace; -import com.google.code.appengine.awt.image.BufferedImage; -import com.google.code.appengine.awt.image.ColorModel; - - -public class PaletteFactory -{ - private static final boolean debug = false; - - public void makePaletteFancy(BufferedImage src) - { - // map what rgb values have been used - - byte rgbmap[] = new byte[256 * 256 * 32]; - for (int i = 0; i < rgbmap.length; i++) - rgbmap[i] = 0; - - int width = src.getWidth(); - int height = src.getHeight(); - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int rggbb = 0x1fffff & argb; - int highred = 0x7 & (argb >> 21); - int mask = 1 << highred; - rgbmap[rggbb] |= mask; - } - - int count = 0; - for (int i = 0; i < rgbmap.length; i++) - { - int eight = 0xff & rgbmap[i]; - if ((i < 3) || ((i - rgbmap.length) > -3)) - { - } - for (int j = 0; j < 8; j++) - { - int mask = 1 << (7 - j); - int bit = eight & mask; - if (bit > 0) - count++; - - } - } - - if (debug) - System.out.println("Used colors: " + count); - - int colormap[] = new int[count]; - int mapsize = 0; - for (int i = 0; i < rgbmap.length; i++) - { - int eight = 0xff & rgbmap[i]; - - for (int j = 0; j < 8; j++) - { - int mask = 1 << (7 - j); - int bit = eight & mask; - - if (bit > 0) - { - int rgb = i | ((7 - j) << 21); - - if (mapsize < colormap.length) - colormap[mapsize] = rgb; - mapsize++; - } - } - } - - if (debug) - System.out.println("mapsize: " + mapsize); - - // for (int i = 0; i < colormap.length; i++) - // { - // int rgb = colormap[i]; - // } - - } - - private int pixelToQuantizationTableIndex(int argb, int precision) - { - int result = 0; - int precision_mask = (1 << precision) - 1; - - for (int i = 0; i < components; i++) - { - int sample = argb & 0xff; - argb >>= 8; - - sample >>= (8 - precision); - result = (result << precision) | (sample & precision_mask); - } - - return result; - } - - private int getFrequencyTotal(int table[], int mins[], int maxs[], - int precision) - { - int sum = 0; - - for (int blue = mins[2]; blue <= maxs[2]; blue++) - { - int b = (blue << (2 * precision)); - for (int green = mins[1]; green <= maxs[1]; green++) - { - int g = (green << (1 * precision)); - - for (int red = mins[0]; red <= maxs[0]; red++) - { - int index = b | g | red; - - sum += table[index]; - } - } - } - - return sum; - } - - private DivisionCandidate finishDivision(int table[], - ColorSpaceSubset subset, int component, int precision, int sum, - int slice) - { - if (debug) - subset.dump("trying (" + component + "): "); - - int total = subset.total; - - if ((slice < subset.mins[component]) - || (slice >= subset.maxs[component])) - { - return null; - } - - if ((sum < 1) || (sum >= total)) - { - return null; - } - - int remainder = total - sum; - if ((remainder < 1) || (remainder >= total)) - { - return null; - } - - // ArrayList result = new ArrayList(); - - int slice_mins[] = new int[subset.mins.length]; - System.arraycopy(subset.mins, 0, slice_mins, 0, subset.mins.length); - int slice_maxs[] = new int[subset.maxs.length]; - System.arraycopy(subset.maxs, 0, slice_maxs, 0, subset.maxs.length); - - slice_maxs[component] = slice; - slice_mins[component] = slice + 1; - - if (debug) - { - System.out.println("total: " + total); - System.out.println("first total: " + sum); - System.out.println("second total: " + (total - sum)); - // System.out.println("start: " + start); - // System.out.println("end: " + end); - System.out.println("slice: " + slice); - - } - - ColorSpaceSubset first = new ColorSpaceSubset(sum, precision, - subset.mins, slice_maxs, table); - ColorSpaceSubset second = new ColorSpaceSubset(total - sum, precision, - slice_mins, subset.maxs, table); - - return new DivisionCandidate(subset, first, second); - - } - - private ArrayList divideSubset2(int table[], ColorSpaceSubset subset, - int component, int precision) - { - if (debug) - subset.dump("trying (" + component + "): "); - - int total = subset.total; - - int slice_mins[] = new int[subset.mins.length]; - System.arraycopy(subset.mins, 0, slice_mins, 0, subset.mins.length); - int slice_maxs[] = new int[subset.maxs.length]; - System.arraycopy(subset.maxs, 0, slice_maxs, 0, subset.maxs.length); - - int sum1 = 0, sum2; - int slice1, slice2; - int last = 0; - - { - for (slice1 = subset.mins[component]; slice1 != subset.maxs[component] + 1; slice1++) - { - - slice_mins[component] = slice1; - slice_maxs[component] = slice1; - - last = getFrequencyTotal(table, slice_mins, slice_maxs, - precision); - - sum1 += last; - - if (sum1 >= (total / 2)) - break; - } - - sum2 = sum1 - last; - slice2 = slice1 - 1; - - } - - DivisionCandidate dc1 = finishDivision(table, subset, component, - precision, sum1, slice1); - DivisionCandidate dc2 = finishDivision(table, subset, component, - precision, sum2, slice2); - - ArrayList result = new ArrayList(); - - if (dc1 != null) - result.add(dc1); - if (dc2 != null) - result.add(dc2); - - return result; - } - - private DivisionCandidate divideSubset2(int table[], - ColorSpaceSubset subset, int precision) - { - ArrayList dcs = new ArrayList(); - - dcs.addAll(divideSubset2(table, subset, 0, precision)); - dcs.addAll(divideSubset2(table, subset, 1, precision)); - dcs.addAll(divideSubset2(table, subset, 2, precision)); - - DivisionCandidate best_v = null; - // int best_split - double best_score = Double.MAX_VALUE; - - for (int i = 0; i < dcs.size(); i++) - { - DivisionCandidate dc = (DivisionCandidate) dcs.get(i); - - ColorSpaceSubset first = dc.dst_a; - ColorSpaceSubset second = dc.dst_b; - int area1 = first.total; - int area2 = second.total; - - int diff = Math.abs(area1 - area2); - double split = ((double) diff) / ((double) Math.max(area1, area2)); - - double score = split; - - if (best_v == null) - { - best_v = dc; - best_score = score; - } - else if (score < best_score) - { - best_v = dc; - best_score = score; - } - - } - - return best_v; - } - - public static final int components = 3; // in bits - - private static class DivisionCandidate - { - // private final ColorSpaceSubset src; - private final ColorSpaceSubset dst_a, dst_b; - - public DivisionCandidate(ColorSpaceSubset src, ColorSpaceSubset dst_a, - ColorSpaceSubset dst_b) - { - // this.src = src; - this.dst_a = dst_a; - this.dst_b = dst_b; - } - } - - private ArrayList divide(ArrayList v, int desired_count, int table[], - int precision) - { - ArrayList ignore = new ArrayList(); - - int count = 0; - while (true) - { - count++; - - if (debug) - System.out.println("cycle(" + count + "): " + v.size() - + " done"); - - int max_area = -1; - ColorSpaceSubset max_subset = null; - - for (int i = 0; i < v.size(); i++) - { - ColorSpaceSubset subset = (ColorSpaceSubset) v.get(i); - if (ignore.contains(subset)) - continue; - int area = subset.total; - - if (max_subset == null) - { - max_subset = subset; - max_area = area; - } - else if (area > max_area) - { - max_subset = subset; - max_area = area; - } - } - - if (max_subset == null) - { - return v; - } - if (debug) - System.out.println("\t" + "area: " + max_area); - - { - - DivisionCandidate dc = divideSubset2(table, max_subset, - precision); - if (dc != null) - { - v.remove(max_subset); - v.add(dc.dst_a); - v.add(dc.dst_b); - } - else - ignore.add(max_subset); - } - - if (v.size() == desired_count) - return v; - } - - // return result; - } - - public Palette makePaletteQuantized(BufferedImage src, int max) - { - int precision = 6; // in bits - - int table_scale = precision * components; - int table_size = 1 << table_scale; - int table[] = new int[table_size]; - - int width = src.getWidth(); - int height = src.getHeight(); - - ArrayList subsets = new ArrayList(); - ColorSpaceSubset all = new ColorSpaceSubset(width * height, precision); - subsets.add(all); - - int pre_total = getFrequencyTotal(table, all.mins, all.maxs, precision); - if (debug) - System.out.println("pre total: " + pre_total); - - { // step 1: count frequency of colors - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - - int index = pixelToQuantizationTableIndex(argb, precision); - - table[index]++; - } - } - - int all_total = getFrequencyTotal(table, all.mins, all.maxs, precision); - if (debug) - { - System.out.println("all total: " + all_total); - - System.out.println("width * height: " + (width * height)); - } - - subsets = divide(subsets, 256, table, precision); - - if (debug) - { - System.out.println("subsets: " + subsets.size()); - System.out.println("width*height: " + width * height); - } - - for (int i = 0; i < subsets.size(); i++) - { - ColorSpaceSubset subset = (ColorSpaceSubset) subsets.get(i); - - subset.setAverageRGB(table); - - if (debug) - subset.dump(i + ": "); - } - - Collections.sort(subsets); - - return new QuantizedPalette(subsets, precision); - } - - public SimplePalette makePaletteSimple(BufferedImage src, int max) - // This is not efficient for large values of max, say, max > 256; - { - Map map = new HashMap(); - int rgbs[] = new int[max]; - int rgb_count = 0; - - int width = src.getWidth(); - int height = src.getHeight(); - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int rgb = 0xffffff & argb; - - String key = "" + rgb; - if (null == map.get(key)) - { - if (rgb_count == max) - return null; - - rgbs[rgb_count] = rgb; - map.put(key, key); - rgb_count++; - } - } - - int result[] = new int[rgb_count]; - System.arraycopy(rgbs, 0, result, 0, rgb_count); - Arrays.sort(result); - - // return result; - return new SimplePalette(result); - } - - public boolean isGrayscale(BufferedImage src) - { - int width = src.getWidth(); - int height = src.getHeight(); - - if (ColorSpace.TYPE_GRAY == src.getColorModel().getColorSpace() - .getType()) - return true; - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - - int red = 0xff & (argb >> 16); - int green = 0xff & (argb >> 8); - int blue = 0xff & (argb >> 0); - - if (red != green || red != blue) - return false; - } - - return true; - } - - public boolean hasTransparency(BufferedImage src) - { - return hasTransparency(src, 255); - } - - public boolean hasTransparency(BufferedImage src, int threshold) - { - int width = src.getWidth(); - int height = src.getHeight(); - - if (!src.getColorModel().hasAlpha()) - return false; - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int argb = src.getRGB(x, y); - int alpha = 0xff & (argb >> 24); - if (alpha < threshold) - return true; - } - - return false; - } - - public int countTrasparentColors(int rgbs[]) - { - int first = -1; - - for (int i = 0; i < rgbs.length; i++) - { - int rgb = rgbs[i]; - int alpha = 0xff & (rgb >> 24); - if (alpha < 0xff) - { - if (first < 0) - { - first = rgb; - } - else if (rgb != first) - return 2; // more than one transparent color; - } - } - - if (first < 0) - return 0; - return 1; - } - - public int countTransparentColors(BufferedImage src) - { - ColorModel cm = src.getColorModel(); - if (!cm.hasAlpha()) - return 0; - - int width = src.getWidth(); - int height = src.getHeight(); - - int first = -1; - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - { - int rgb = src.getRGB(x, y); - int alpha = 0xff & (rgb >> 24); - if (alpha < 0xff) - { - if (first < 0) - { - first = rgb; - } - else if (rgb != first) - return 2; // more than one transparent color; - } - } - - if (first < 0) - return 0; - return 1; - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/palette/QuantizedPalette.java b/src/main/java/org/apache/sanselan/palette/QuantizedPalette.java deleted file mode 100644 index 801eb91..0000000 --- a/src/main/java/org/apache/sanselan/palette/QuantizedPalette.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -import java.util.ArrayList; - -import org.apache.sanselan.ImageWriteException; - -public class QuantizedPalette extends Palette -{ - private final int precision; - private final ArrayList subsets; - private final ColorSpaceSubset straight[]; - - public QuantizedPalette(ArrayList subsets, int precision) - { - this.subsets = subsets; - this.precision = precision; - - { - straight = new ColorSpaceSubset[1 << (precision * 3)]; - - for (int i = 0; i < subsets.size(); i++) - { - ColorSpaceSubset subset = (ColorSpaceSubset) subsets.get(i); - subset.setIndex(i); - - for (int u = subset.mins[0]; u <= subset.maxs[0]; u++) - for (int j = subset.mins[1]; j <= subset.maxs[1]; j++) - for (int k = subset.mins[2]; k <= subset.maxs[2]; k++) - { - int index = (u << (precision * 2)) - | (j << (precision * 1)) - | (k << (precision * 0)); - straight[index] = subset; - } - } - } - - } - - public int getPaletteIndex(int rgb) throws ImageWriteException - { - int precisionMask = (1 << precision) - 1; - - int index = ((rgb >> (24 - 3 * precision)) & (precisionMask << (precision << 1))) - | ((rgb >> (16 - 2 * precision)) & (precisionMask << precision)) - | ((rgb >> (8 - precision)) & (precisionMask)); - - return straight[index].index; - } - - public void dump() - { - // System.out.println("ColorSpaceSubset.compares: " - // + ColorSpaceSubset.compares); - // System.out.println("converted: " + converted); - // System.out.println("avg. distance: " + (distance / converted)); - } - - public int getEntry(int index) - { - ColorSpaceSubset subset = (ColorSpaceSubset) subsets.get(index); - return subset.rgb; - } - - public int length() - { - return subsets.size(); - - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/palette/SimplePalette.java b/src/main/java/org/apache/sanselan/palette/SimplePalette.java deleted file mode 100644 index 0afe34c..0000000 --- a/src/main/java/org/apache/sanselan/palette/SimplePalette.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.palette; - -import org.apache.sanselan.util.Debug; - -public class SimplePalette extends Palette -{ - private final int palette[]; - - public SimplePalette(int palette[]) - { - this.palette = palette; - } - - public int getPaletteIndex(int rgb) - { - return getPaletteIndex(palette, rgb); - } - - public int getEntry(int index) - { - return palette[index]; - // return getPaletteIndex(palette, rgb); - } - - private int getPaletteIndex(int palette[], int argb) - { - - // Debug.debug("getPaletteIndex argb", argb + " (" - // + Integer.toHexString(argb) + ")"); - - for (int i = 0; i < palette.length; i++) - { - // Debug.debug("\t" + "palette[" + i + "]", palette[i] + " (" - // + Integer.toHexString(palette[i]) + ")"); - - if (palette[i] == argb) - return i; - } - - return -1; - } - - public void dump() - { - for (int i = 0; i < palette.length; i++) - { - Debug.debug("\t" + "palette[" + i + "]", palette[i] + " (0x" - + Integer.toHexString(palette[i]) + ")"); - } - } - - public int length() - { - return palette.length; - } -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/util/Debug.java b/src/main/java/org/apache/sanselan/util/Debug.java deleted file mode 100644 index 0a19176..0000000 --- a/src/main/java/org/apache/sanselan/util/Debug.java +++ /dev/null @@ -1,916 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.util; - -import java.io.File; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.Map; - -import com.google.code.appengine.awt.Dimension; -import com.google.code.appengine.awt.Point; -import com.google.code.appengine.awt.Rectangle; -import com.google.code.appengine.awt.color.ICC_Profile; - - -public final class Debug -{ - - public static void debug(String message) - { - System.out.println(message); - } - - public static void debug(Object o) - { - System.out.println(o == null ? "null" : o.toString()); - } - - public static String getDebug(String message) - { - return message; - } - - public static void debug() - { - newline(); - } - - public static void newline() - { - System.out.print(newline); - } - - public static String getDebug(String message, int value) - { - return getDebug(message + ": " + (value)); - } - - public static String getDebug(String message, double value) - { - return getDebug(message + ": " + (value)); - } - - public static String getDebug(String message, String value) - { - return getDebug(message + " " + value); - } - - public static String getDebug(String message, long value) - { - return getDebug(message + " " + Long.toString(value)); - } - - public static String getDebug(String message, int v[]) - { - StringBuffer result = new StringBuffer(); - - if (v == null) - result.append(message + " (" + null + ")" + newline); - else - { - result.append(message + " (" + v.length + ")" + newline); - for (int i = 0; i < v.length; i++) - result.append("\t" + v[i] + newline); - result.append(newline); - } - return result.toString(); - } - - public static String getDebug(String message, byte v[]) - { - final int max = 250; - return getDebug(message, v, max); - } - - public static String getDebug(String message, byte v[], int max) - { - - StringBuffer result = new StringBuffer(); - - if (v == null) - result.append(message + " (" + null + ")" + newline); - else - { - result.append(message + " (" + v.length + ")" + newline); - for (int i = 0; i < max && i < v.length; i++) - { - int b = 0xff & v[i]; - - char c; - if (b == 0 || b == 10 || b == 11 || b == 13) - c = ' '; - else - c = (char) b; - - result.append("\t" + i + ": " + b + " (" + c + ", 0x" - + Integer.toHexString(b) + ")" + newline); - } - if (v.length > max) - result.append("\t" + "..." + newline); - - result.append(newline); - } - return result.toString(); - } - - public static String getDebug(String message, char v[]) - { - StringBuffer result = new StringBuffer(); - - if (v == null) - result.append(getDebug(message + " (" + null + ")") + newline); - else - { - result.append(getDebug(message + " (" + v.length + ")") + newline); - for (int i = 0; i < v.length; i++) - result.append(getDebug("\t" + v[i] + " (" + (0xff & v[i])) - + ")" + newline); - result.append(newline); - } - return result.toString(); - } - - private static long counter = 0; - - public static String getDebug(String message, java.util.List v) - { - StringBuffer result = new StringBuffer(); - - String suffix = " [" + counter++ + "]"; - - result.append(getDebug(message + " (" + v.size() + ")" + suffix) - + newline); - for (int i = 0; i < v.size(); i++) - result.append(getDebug("\t" + v.get(i).toString() + suffix) - + newline); - result.append(newline); - - return result.toString(); - } - - public static void debug(String message, Map map) - { - debug(getDebug(message, map)); - } - - public static String getDebug(String message, Map map) - { - StringBuffer result = new StringBuffer(); - - if (map == null) - return getDebug(message + " map: " + null); - - ArrayList keys = new ArrayList(map.keySet()); - result.append(getDebug(message + " map: " + keys.size()) + newline); - for (int i = 0; i < keys.size(); i++) - { - Object key = keys.get(i); - Object value = map.get(key); - result.append(getDebug("\t" + i + ": '" + key + "' -> '" + value - + "'") - + newline); - } - - result.append(newline); - - return result.toString(); - } - - public static boolean compare(String prefix, Map a, Map b) - { - return compare(prefix, a, b, null, null); - } - - // public static String newline = System.getProperty("line.separator"); - public static String newline = "\r\n"; - - private static void log(StringBuffer buffer, String s) - { - Debug.debug(s); - if (buffer != null) - buffer.append(s + newline); - } - - public static boolean compare(String prefix, Map a, Map b, - ArrayList ignore, StringBuffer buffer) - { - if ((a == null) && (b == null)) - { - log(buffer, prefix + " both maps null"); - return true; - } - if (a == null) - { - log(buffer, prefix + " map a: null, map b: map"); - return false; - } - if (b == null) - { - log(buffer, prefix + " map a: map, map b: null"); - return false; - } - - ArrayList keys_a = new ArrayList(a.keySet()); - ArrayList keys_b = new ArrayList(b.keySet()); - - if (ignore != null) - { - keys_a.removeAll(ignore); - keys_b.removeAll(ignore); - } - - boolean result = true; - - for (int i = 0; i < keys_a.size(); i++) - { - Object key = keys_a.get(i); - if (!keys_b.contains(key)) - { - log(buffer, prefix + "b is missing key '" + key + "' from a"); - result = false; - } - else - { - keys_b.remove(key); - Object value_a = a.get(key); - Object value_b = b.get(key); - if (!value_a.equals(value_b)) - { - log(buffer, prefix + "key(" + key + ") value a: " + value_a - + ") != b: " + value_b + ")"); - result = false; - } - } - } - for (int i = 0; i < keys_b.size(); i++) - { - Object key = keys_b.get(i); - - log(buffer, prefix + "a is missing key '" + key + "' from b"); - result = false; - } - - if (result) - log(buffer, prefix + "a is the same as b"); - - return result; - } - - private static final String byteQuadToString(int bytequad) - { - byte b1 = (byte) ((bytequad >> 24) & 0xff); - byte b2 = (byte) ((bytequad >> 16) & 0xff); - byte b3 = (byte) ((bytequad >> 8) & 0xff); - byte b4 = (byte) ((bytequad >> 0) & 0xff); - - char c1 = (char) b1; - char c2 = (char) b2; - char c3 = (char) b3; - char c4 = (char) b4; - // return new String(new char[] { c1, c2, c3, c4 }); - StringBuffer fStringBuffer = new StringBuffer(); - fStringBuffer.append(new String(new char[]{ - c1, c2, c3, c4 - })); - fStringBuffer.append(" bytequad: " + bytequad); - fStringBuffer.append(" b1: " + b1); - fStringBuffer.append(" b2: " + b2); - fStringBuffer.append(" b3: " + b3); - fStringBuffer.append(" b4: " + b4); - - return fStringBuffer.toString(); - } - - public static String getDebug(String message, ICC_Profile value) - { - - StringBuffer result = new StringBuffer(); - - result.append(getDebug("ICC_Profile " + message + ": " - + ((value == null) ? "null" : value.toString())) - + newline); - if (value != null) - { - result.append(getDebug("\t getProfileClass: " - + byteQuadToString(value.getProfileClass())) - + newline); - result.append(getDebug("\t getPCSType: " - + byteQuadToString(value.getPCSType())) - + newline); - result.append(getDebug("\t getColorSpaceType() : " - + byteQuadToString(value.getColorSpaceType())) - + newline); - } - - return result.toString(); - - } - - public static String getDebug(String message, boolean value) - { - return getDebug(message + " " + ((value) ? ("true") : ("false"))); - } - - public static String getDebug(String message, File file) - { - return getDebug(message + ": " - + ((file == null) ? "null" : file.getPath())); - } - - public static String getDebug(String message, Date value) - { - DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); - return getDebug(message, (value == null) ? "null" : df.format(value)); - } - - public static String getDebug(String message, Calendar value) - { - DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); - return getDebug(message, (value == null) ? "null" : df.format(value - .getTime())); - } - - public static void debug(String message, Object value) - { - if (value == null) - debug(message, "null"); - else if (value instanceof char[]) - debug(message, (char[]) value); - else if (value instanceof byte[]) - debug(message, (byte[]) value); - else if (value instanceof int[]) - debug(message, (int[]) value); - else if (value instanceof String) - debug(message, (String) value); - else if (value instanceof java.util.List) - debug(message, (java.util.List) value); - else if (value instanceof Map) - debug(message, (Map) value); - // else if (value instanceof Object) - // debug(message, (Object) value); - else if (value instanceof ICC_Profile) - debug(message, (ICC_Profile) value); - else if (value instanceof File) - debug(message, (File) value); - else if (value instanceof Date) - debug(message, (Date) value); - else if (value instanceof Calendar) - debug(message, (Calendar) value); - else - debug(message, value.toString()); - } - - public static void debug(String message, Object value[]) - { - if (value == null) { - debug(message, "null"); - } else { - debug(message, value.length); - final int max = 10; - for (int i = 0; i < value.length && i < max; i++) { - debug("\t" + i, value[i]); - } - if (value.length > max) { - debug("\t..."); - } - } - debug(); - } - - public static String getDebug(String message, Object value) - { - if (value == null) - return getDebug(message, "null"); - else if (value instanceof Calendar) - return getDebug(message, (Calendar) value); - else if (value instanceof Date) - return getDebug(message, (Date) value); - else if (value instanceof File) - return getDebug(message, (File) value); - else if (value instanceof ICC_Profile) - return getDebug(message, (ICC_Profile) value); - else if (value instanceof Map) - return getDebug(message, (Map) value); - else if (value instanceof Map) - return getDebug(message, (Map) value); // - // else if (value instanceof Object) // getDebug(message, (Object) value); - else if (value instanceof String) - return getDebug(message, (String) value); - else if (value instanceof byte[]) - return getDebug(message, (byte[]) value); - else if (value instanceof char[]) - return getDebug(message, (char[]) value); - else if (value instanceof int[]) - return getDebug(message, (int[]) value); - else if (value instanceof java.util.List) - return getDebug(message, (java.util.List) value); - else - return getDebug(message, value.toString()); - } - - public static String getType(Object value) - { - if (value == null) - return "null"; - else if (value instanceof Object[]) - return "[Object[]: " + ((Object[]) value).length + "]"; - else if (value instanceof char[]) - return "[char[]: " + ((char[]) value).length + "]"; - else if (value instanceof byte[]) - return "[byte[]: " + ((byte[]) value).length + "]"; - else if (value instanceof short[]) - return "[short[]: " + ((short[]) value).length + "]"; - else if (value instanceof int[]) - return "[int[]: " + ((int[]) value).length + "]"; - else if (value instanceof long[]) - return "[long[]: " + ((long[]) value).length + "]"; - else if (value instanceof float[]) - return "[float[]: " + ((float[]) value).length + "]"; - else if (value instanceof double[]) - return "[double[]: " + ((double[]) value).length + "]"; - else if (value instanceof boolean[]) - return "[boolean[]: " + ((boolean[]) value).length + "]"; - else - return value.getClass().getName(); - } - - public static boolean isArray(Object value) - { - if (value == null) - return false; - else if (value instanceof Object[]) - return true; - else if (value instanceof char[]) - return true; - else if (value instanceof byte[]) - return true; - else if (value instanceof short[]) - return true; - else if (value instanceof int[]) - return true; - else if (value instanceof long[]) - return true; - else if (value instanceof float[]) - return true; - else if (value instanceof double[]) - return true; - else if (value instanceof boolean[]) - return true; - else - return false; - } - - public static String getDebug(String message, Object value[]) - { - StringBuffer result = new StringBuffer(); - - if (value == null) { - result.append(getDebug(message, "null") + newline); - } else { - result.append(getDebug(message, value.length)); - final int max = 10; - for (int i = 0; i < value.length && i < max; i++) { - result.append(getDebug("\t" + i, value[i]) + newline); - } - if (value.length > max) { - result.append(getDebug("\t...") + newline); - } - } - result.append(newline); - - return result.toString(); - } - - public static String getDebug(Class fClass, Throwable e) - { - return getDebug(fClass == null ? "[Unknown]" : fClass.getName(), e); - } - - public static void debug(Class fClass, Throwable e) - { - debug(fClass.getName(), e); - } - - private static final SimpleDateFormat timestamp = new SimpleDateFormat( - "yyyy-MM-dd kk:mm:ss:SSS"); - - public static void debug(String message, boolean value) - { - debug(message + " " + ((value) ? ("true") : ("false"))); - } - - public static void debug(String message, byte v[]) - { - debug(getDebug(message, v)); - } - - public static void debug(String message, char v[]) - { - debug(getDebug(message, v)); - } - - public static void debug(String message, Calendar value) - { - DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); - debug(message, (value == null) ? "null" : df.format(value.getTime())); - } - - public static void debug(String message, Date value) - { - DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); - debug(message, (value == null) ? "null" : df.format(value)); - } - - public static void debug(String message, double value) - { - debug(message + ": " + (value)); - } - - public static void debug(String message, File file) - { - debug(message + ": " + ((file == null) ? "null" : file.getPath())); - } - - // public static void debug(String message, Object value) - // { - // debug("Unknown Object " + message + ": " - // + ((value == null) ? "null" : value.toString())); - // } - - public static void debug(String message, ICC_Profile value) - { - debug("ICC_Profile " + message + ": " - + ((value == null) ? "null" : value.toString())); - if (value != null) - { - debug("\t getProfileClass: " - + byteQuadToString(value.getProfileClass())); - debug("\t getPCSType: " + byteQuadToString(value.getPCSType())); - debug("\t getColorSpaceType() : " - + byteQuadToString(value.getColorSpaceType())); - } - } - - public static void debug(String message, int value) - { - debug(message + ": " + (value)); - } - - public static void debug(String message, int v[]) - { - debug(getDebug(message, v)); - } - - public static void debug(String message, byte v[], int max) - { - debug(getDebug(message, v, max)); - } - - public static void debug(String message, java.util.List v) - { - String suffix = " [" + counter++ + "]"; - - debug(message + " (" + v.size() + ")" + suffix); - for (int i = 0; i < v.size(); i++) - debug("\t" + v.get(i).toString() + suffix); - debug(); - } - - public static void debug(String message, long value) - { - debug(message + " " + Long.toString(value)); - } - - public static void debug(String prefix, Point p) - { - System.out.println(prefix + ": " - + ((p == null) ? "null" : (p.x + ", " + p.y))); - } - - public static void debug(String prefix, Rectangle r) - { - debug(getDebug(prefix, r)); - } - - public static void debug(String message, String value) - { - debug(message + " " + value); - } - - public static void debug(String message, Throwable e) - { - debug(getDebug(message, e)); - } - - public static void debug(Throwable e) - { - debug(getDebug(e)); - } - - public static void debug(Throwable e, int value) - { - debug(getDebug(e, value)); - } - - public static void dumpStack() - { - debug(getStackTrace(new Exception("Stack trace"), -1, 1)); - } - - public static void dumpStack(int limit) - { - debug(getStackTrace(new Exception("Stack trace"), limit, 1)); - } - - public static String getDebug(String message, Throwable e) - { - return message + newline + getDebug(e); - } - - public static String getDebug(Throwable e) - { - return getDebug(e, -1); - } - - public static String getDebug(Throwable e, int max) - { - StringBuffer result = new StringBuffer(); - - String datetime = timestamp.format(new Date()).toLowerCase(); - - result.append(newline); - result.append("Throwable: " - + ((e == null) ? "" : ("(" + e.getClass().getName() + ")")) - + ":" + datetime + newline); - result.append("Throwable: " - + ((e == null) ? "null" : e.getLocalizedMessage()) + newline); - result.append(newline); - - result.append(getStackTrace(e, max)); - - result.append("Caught here:" + newline); - result.append(getStackTrace(new Exception(), max, 1)); - // Debug.dumpStack(); - result.append(newline); - return result.toString(); - } - - public static String getStackTrace(Throwable e) - { - return getStackTrace(e, -1); - } - - public static String getStackTrace(Throwable e, int limit) - { - return getStackTrace(e, limit, 0); - } - - public static String getStackTrace(Throwable e, int limit, int skip) - { - StringBuffer result = new StringBuffer(); - - if (e != null) - { - StackTraceElement stes[] = e.getStackTrace(); - if (stes != null) - { - for (int i = skip; i < stes.length && (limit < 0 || i < limit); i++) - { - StackTraceElement ste = stes[i]; - - result.append("\tat " + ste.getClassName() + "." - + ste.getMethodName() + "(" + ste.getFileName() - + ":" + ste.getLineNumber() + ")" + newline); - } - if (limit >= 0 && stes.length > limit) - result.append("\t..." + newline); - } - - // e.printStackTrace(System.out); - result.append(newline); - } - - return result.toString(); - } - - public static void debugByteQuad(String message, int i) - { - int alpha = (i >> 24) & 0xff; - int red = (i >> 16) & 0xff; - int green = (i >> 8) & 0xff; - int blue = (i >> 0) & 0xff; - - System.out.println(message + ": " + "alpha: " + alpha + ", " + "red: " - + red + ", " + "green: " + green + ", " + "blue: " + blue); - } - - public static void debugIPQuad(String message, int i) - { - int b1 = (i >> 24) & 0xff; - int b2 = (i >> 16) & 0xff; - int b3 = (i >> 8) & 0xff; - int b4 = (i >> 0) & 0xff; - - System.out.println(message + ": " + "b1: " + b1 + ", " + "b2: " + b2 - + ", " + "b3: " + b3 + ", " + "b4: " + b4); - } - - public static void debugIPQuad(String message, byte bytes[]) - { - System.out.print(message + ": "); - if (bytes == null) - System.out.print("null"); - else - { - for (int i = 0; i < bytes.length; i++) - { - if (i > 0) - System.out.print("."); - System.out.print(0xff & bytes[i]); - } - } - System.out.println(); - } - - public static String getDebug(String prefix, Dimension r) - { - String s_ar1 = "null"; - String s_ar2 = "null"; - - if (r != null) - { - double aspect_ratio = ((double) r.width) / ((double) r.height); - double aspect_ratio2 = 1.0 / aspect_ratio; - - s_ar1 = "" + aspect_ratio; - s_ar2 = "" + aspect_ratio2; - - if (s_ar1.length() > 7) - s_ar1 = s_ar1.substring(0, 7); - if (s_ar2.length() > 7) - s_ar2 = s_ar2.substring(0, 7); - } - - return (prefix + ": " - + ((r == null) ? "null" : (r.width + "x" + r.height)) - + " aspect_ratio: " + s_ar1 + " (" + s_ar2 + ")"); - } - - public static void debug(String prefix, Dimension r) - { - debug(getDebug(prefix, r)); - } - - public static String getDebug(String prefix, Rectangle r) - { - String s_ar1 = "null"; - String s_ar2 = "null"; - - if (r != null) - { - double aspect_ratio = ((double) r.width) / ((double) r.height); - double aspect_ratio2 = 1.0 / aspect_ratio; - - s_ar1 = "" + aspect_ratio; - s_ar2 = "" + aspect_ratio2; - - if (s_ar1.length() > 7) - s_ar1 = s_ar1.substring(0, 7); - if (s_ar2.length() > 7) - s_ar2 = s_ar2.substring(0, 7); - } - - return (prefix - + ": " - + ((r == null) ? "null" : (r.x + "x" + r.y + "," + r.width - + "x" + r.height)) + " aspect_ratio: " + s_ar1 + " (" - + s_ar2 + ")"); - } - - public static String getDebug(String prefix, Point p) - { - return (prefix + ": " + ((p == null) ? "null" : (p.x + ", " + p.y))); - } - - public static void dump(String prefix, Object value) - { - if (value == null) - debug(prefix, "null"); - else if (value instanceof Object[]) - { - Object[] array = (Object[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - dump(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof int[]) - { - int[] array = (int[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof char[]) - { - char[] array = (char[]) value; - debug(prefix, "[" + new String(array) + "]"); - } - else if (value instanceof long[]) - { - long[] array = (long[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof boolean[]) - { - boolean[] array = (boolean[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof byte[]) - { - byte[] array = (byte[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof float[]) - { - float[] array = (float[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof byte[]) - { - double[] array = (double[]) value; - debug(prefix, array); - for (int i = 0; i < array.length; i++) - debug(prefix + "\t" + i + ": ", array[i]); - } - else if (value instanceof java.util.List) - { - java.util.List list = (java.util.List) value; - debug(prefix, "list"); - for (int i = 0; i < list.size(); i++) - dump(prefix + "\t" + "list: " + i + ": ", list.get(i)); - } - else if (value instanceof Map) - { - java.util.Map map = (java.util.Map) value; - debug(prefix, "map"); - ArrayList keys = new ArrayList(map.keySet()); - Collections.sort(keys); - for (int i = 0; i < keys.size(); i++) - { - Object key = keys.get(i); - dump(prefix + "\t" + "map: " + key + " -> ", map.get(key)); - } - } - // else if (value instanceof String) - // debug(prefix, value); - else - { - debug(prefix, value.toString()); - debug(prefix + "\t", value.getClass().getName()); - } - } - - public static final void purgeMemory() - { - try - { - // Thread.sleep(50); - System.runFinalization(); - Thread.sleep(50); - System.gc(); - Thread.sleep(50); - } - catch (Throwable e) - { - Debug.debug(e); - } - } - -} diff --git a/src/main/java/org/apache/sanselan/util/DebugOutputStream.java b/src/main/java/org/apache/sanselan/util/DebugOutputStream.java deleted file mode 100644 index 95945ed..0000000 --- a/src/main/java/org/apache/sanselan/util/DebugOutputStream.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.io.IOException; -import java.io.OutputStream; - -public class DebugOutputStream extends OutputStream -{ - private final OutputStream os; - private long count = 0; - - public DebugOutputStream(final OutputStream os) - { - super(); - this.os = os; - } - - public void write(int b) throws IOException - { - os.write(b); - count++; - } - - public void write(byte b[]) throws IOException - { - os.write(b); - count += b.length; - } - - public void write(byte b[], int off, int len) throws IOException - { - os.write(b, off, len); - count += len; - } - - public void flush() throws IOException - { - os.flush(); - } - - public void close() throws IOException - { - os.close(); - } - - public long count() - { - return count; - } -} diff --git a/src/main/java/org/apache/sanselan/util/IOUtils.java b/src/main/java/org/apache/sanselan/util/IOUtils.java deleted file mode 100644 index 45b921a..0000000 --- a/src/main/java/org/apache/sanselan/util/IOUtils.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sanselan.util; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.FileChannel; - -import org.apache.sanselan.SanselanConstants; - -public class IOUtils implements SanselanConstants -{ - /** - * This class should never be instantiated. - */ - private IOUtils() - { - } - - /** - * Reads an InputStream to the end. - *

- * - * @param is - * The InputStream to read. - * @return A byte array containing the contents of the InputStream - * @see InputStream - */ - public static byte[] getInputStreamBytes(InputStream is) throws IOException - { - ByteArrayOutputStream os = null; - - try - { - os = new ByteArrayOutputStream(4096); - - is = new BufferedInputStream(is); - - int count; - byte[] buffer = new byte[4096]; - while ((count = is.read(buffer, 0, 4096)) > 0) - { - os.write(buffer, 0, count); - } - - os.flush(); - - return os.toByteArray(); - } finally - { - try - { - if (os != null) - os.close(); - } catch (IOException e) - { - Debug.debug(e); - } - } - } - - /** - * Reads a File into memory. - *

- * - * @param file - * The File to read. - * @return A byte array containing the contents of the File - * @see InputStream - */ - public static byte[] getFileBytes(File file) throws IOException - { - InputStream is = null; - - try - { - is = new FileInputStream(file); - - return getInputStreamBytes(is); - } finally - { - try - { - if (is != null) - is.close(); - } catch (IOException e) - { - Debug.debug(e); - } - } - } - - public static void writeToFile(byte[] src, File file) throws IOException - { - ByteArrayInputStream stream = null; - - try - { - stream = new ByteArrayInputStream(src); - - putInputStreamToFile(stream, file); - } finally - { - try - { - if (stream != null) - stream.close(); - } catch (Exception e) - { - Debug.debug(e); - - } - } - } - - public static void putInputStreamToFile(InputStream src, File file) - throws IOException - { - FileOutputStream stream = null; - - try - { - if (file.getParentFile() != null) - file.getParentFile().mkdirs(); - stream = new FileOutputStream(file); - - copyStreamToStream(src, stream); - } finally - { - try - { - if (stream != null) - stream.close(); - } catch (Exception e) - { - Debug.debug(e); - } - } - } - - public static void copyStreamToStream(InputStream src, OutputStream dst) - throws IOException - { - copyStreamToStream(src, dst, true); - } - - public static void copyStreamToStream(InputStream src, OutputStream dst, - boolean close_streams) throws IOException - { - BufferedInputStream bis = null; - BufferedOutputStream bos = null; - - try - { - bis = new BufferedInputStream(src); - bos = new BufferedOutputStream(dst); - - int count; - byte[] buffer = new byte[4096]; - while ((count = bis.read(buffer, 0, buffer.length)) > 0) - dst.write(buffer, 0, count); - - bos.flush(); - } finally - { - if (close_streams) - { - try - { - if (bis != null) - bis.close(); - } catch (IOException e) - { - Debug.debug(e); - } - try - { - if (bos != null) - bos.close(); - } catch (IOException e) - { - Debug.debug(e); - } - } - } - - } - - public static final boolean copyFileNio(File src, File dst) - throws IOException - { - FileChannel srcChannel = null, dstChannel = null; - try - { - // Create channel on the source - srcChannel = new FileInputStream(src).getChannel(); - - // Create channel on the destination - dstChannel = new FileOutputStream(dst).getChannel(); - - // // Copy file contents from source to destination - // dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); - - { - // long theoretical_max = (64 * 1024 * 1024) - (32 * 1024); - int safe_max = (64 * 1024 * 1024) / 4; - long size = srcChannel.size(); - long position = 0; - while (position < size) - { - position += srcChannel.transferTo(position, safe_max, - dstChannel); - } - } - - // Close the channels - srcChannel.close(); - srcChannel = null; - dstChannel.close(); - dstChannel = null; - - return true; - } - finally - { - try - { - if (srcChannel != null) - srcChannel.close(); - } catch (IOException e) - { - Debug.debug(e); - - } - try - { - if (dstChannel != null) - dstChannel.close(); - } catch (IOException e) - { - Debug.debug(e); - - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/apache/sanselan/util/UnicodeUtils.java b/src/main/java/org/apache/sanselan/util/UnicodeUtils.java deleted file mode 100644 index cd8466d..0000000 --- a/src/main/java/org/apache/sanselan/util/UnicodeUtils.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sanselan.util; - -import java.io.UnsupportedEncodingException; - -import org.apache.sanselan.common.BinaryConstants; - -public abstract class UnicodeUtils implements BinaryConstants -{ - /** - * This class should never be instantiated. - */ - private UnicodeUtils() - { - } - - public static class UnicodeException extends Exception - { - public UnicodeException(String message) - { - super(message); - } - } - - // A default single-byte charset. - public static final int CHAR_ENCODING_CODE_ISO_8859_1 = 0; - public static final int CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_WITH_BOM = 1; - public static final int CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_WITH_BOM = 2; - public static final int CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_NO_BOM = 3; - public static final int CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_NO_BOM = 4; - public static final int CHAR_ENCODING_CODE_UTF_8 = 5; - public static final int CHAR_ENCODING_CODE_AMBIGUOUS = -1; - - // /* - // * Guess the character encoding of arbitrary character data in a data - // * buffer. - // * - // * The data may not run to the end of the buffer; it may be terminated. - // This - // * makes the problem much harder, since the character data may be followed - // * by arbitrary data. - // */ - // public static int guessCharacterEncoding(byte bytes[], int index) - // { - // int length = bytes.length - index; - // - // if (length < 1) - // return CHAR_ENCODING_CODE_AMBIGUOUS; - // - // if (length >= 2) - // { - // // look for BOM. - // - // int c1 = 0xff & bytes[index]; - // int c2 = 0xff & bytes[index + 1]; - // if (c1 == 0xFF && c2 == 0xFE) - // return CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_WITH_BOM; - // else if (c1 == 0xFE && c2 == 0xFF) - // return CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_WITH_BOM; - // } - // - // } - // - // /* - // * Guess the character encoding of arbitrary character data in a data - // * buffer. - // * - // * The data fills the entire buffer. If it is terminated, the terminator - // * byte(s) will be the last bytes in the buffer. - // * - // * This makes the problem a bit easier. - // */ - // public static int guessCharacterEncodingSimple(byte bytes[], int index) - // throws UnicodeException - // { - // int length = bytes.length - index; - // - // if (length < 1) - // return CHAR_ENCODING_CODE_AMBIGUOUS; - // - // if (length >= 2) - // { - // // identify or eliminate UTF-16 with a BOM. - // - // int c1 = 0xff & bytes[index]; - // int c2 = 0xff & bytes[index + 1]; - // if (c1 == 0xFF && c2 == 0xFE) - // return CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_WITH_BOM; - // else if (c1 == 0xFE && c2 == 0xFF) - // return CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_WITH_BOM; - // } - // - // if (length >= 2) - // { - // // look for optional double-byte terminator. - // - // int c1 = 0xff & bytes[bytes.length - 2]; - // int c2 = 0xff & bytes[bytes.length - 1]; - // if (c1 == 0 && c2 == 0) - // { - // // definitely a flavor of UTF-16. - // if (length % 2 != 0) - // throw new UnicodeException( - // "Character data with double-byte terminator has an odd length."); - // - // boolean mayHaveTerminator = true; - // boolean mustHaveTerminator = false; - // boolean possibleBigEndian = new UnicodeMetricsUTF16NoBOM( - // BYTE_ORDER_BIG_ENDIAN).isValid(bytes, index, - // mayHaveTerminator, mustHaveTerminator); - // boolean possibleLittleEndian = new UnicodeMetricsUTF16NoBOM( - // BYTE_ORDER_LITTLE_ENDIAN).isValid(bytes, index, - // mayHaveTerminator, mustHaveTerminator); - // if ((!possibleBigEndian) && (!possibleLittleEndian)) - // throw new UnicodeException( - // "Invalid character data, possibly UTF-16."); - // if (possibleBigEndian && possibleLittleEndian) - // return CHAR_ENCODING_CODE_AMBIGUOUS; - // if (possibleBigEndian) - // return CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_NO_BOM; - // if (possibleLittleEndian) - // return CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_NO_BOM; - // } - // } - // - // List possibleEncodings = new ArrayList(); - // if (length % 2 == 0) - // { - // boolean mayHaveTerminator = true; - // boolean mustHaveTerminator = false; - // boolean possibleBigEndian = new UnicodeMetricsUTF16NoBOM( - // BYTE_ORDER_BIG_ENDIAN).isValid(bytes, index, - // mayHaveTerminator, mustHaveTerminator); - // boolean possibleLittleEndian = new UnicodeMetricsUTF16NoBOM( - // BYTE_ORDER_LITTLE_ENDIAN).isValid(bytes, index, - // mayHaveTerminator, mustHaveTerminator); - // - // if (possibleBigEndian) - // return CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_NO_BOM; - // if (possibleLittleEndian) - // return CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_NO_BOM; - // } - // - // } - - public static final boolean isValidISO_8859_1(String s) - { - try - { - String roundtrip = new String(s.getBytes("ISO-8859-1"), - "ISO-8859-1"); - return s.equals(roundtrip); - } catch (UnsupportedEncodingException e) - { - // should never be thrown. - throw new RuntimeException("Error parsing string.", e); - } - } - - /* - * Return the index of the first utf-16 terminator (ie. two even-aligned - * nulls). If not found, return -1. - */ - private static int findFirstDoubleByteTerminator(byte bytes[], int index) - { - for (int i = index; i < bytes.length - 1; i += 2) - { - int c1 = 0xff & bytes[index]; - int c2 = 0xff & bytes[index + 1]; - if (c1 == 0 && c2 == 0) - return i; - } - return -1; - } - - public final int findEndWithTerminator(byte bytes[], int index) - throws UnicodeException - { - return findEnd(bytes, index, true); - } - - public final int findEndWithoutTerminator(byte bytes[], int index) - throws UnicodeException - { - return findEnd(bytes, index, false); - } - - protected abstract int findEnd(byte bytes[], int index, - boolean includeTerminator) throws UnicodeException; - - public static UnicodeUtils getInstance(int charEncodingCode) - throws UnicodeException - { - switch (charEncodingCode) - { - case CHAR_ENCODING_CODE_ISO_8859_1: - return new UnicodeMetricsASCII(); - case CHAR_ENCODING_CODE_UTF_8: - // Debug.debug("CHAR_ENCODING_CODE_UTF_8"); - return new UnicodeMetricsUTF8(); - case CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_WITH_BOM: - case CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_WITH_BOM: - // Debug.debug("CHAR_ENCODING_CODE_UTF_16_WITH_BOM"); - return new UnicodeMetricsUTF16WithBOM(); - case CHAR_ENCODING_CODE_UTF_16_BIG_ENDIAN_NO_BOM: - return new UnicodeMetricsUTF16NoBOM(BYTE_ORDER_BIG_ENDIAN); - case CHAR_ENCODING_CODE_UTF_16_LITTLE_ENDIAN_NO_BOM: - return new UnicodeMetricsUTF16NoBOM(BYTE_ORDER_LITTLE_ENDIAN); - default: - throw new UnicodeException("Unknown char encoding code: " - + charEncodingCode); - } - } - - private static class UnicodeMetricsASCII extends UnicodeUtils - { - public int findEnd(byte bytes[], int index, boolean includeTerminator) - throws UnicodeException - { - for (int i = index; i < bytes.length; i++) - { - if (bytes[i] == 0) - return includeTerminator ? i + 1 : i; - } - return bytes.length; - // throw new UnicodeException("Terminator not found."); - } - } - - // private static class UnicodeMetricsISO_8859_1 extends UnicodeUtils - // { - // public int findEnd(byte bytes[], int index, boolean includeTerminator) - // throws UnicodeException - // { - // for (int i = index; i < bytes.length; i++) - // { - // if (bytes[i] == 0) - // return includeTerminator ? i + 1 : i; - // } - // return bytes.length; - // // throw new UnicodeException("Terminator not found."); - // } - // } - - private static class UnicodeMetricsUTF8 extends UnicodeUtils - { - - public int findEnd(byte bytes[], int index, boolean includeTerminator) - throws UnicodeException - { - // http://en.wikipedia.org/wiki/UTF-8 - - while (true) - { - if (index == bytes.length) - return bytes.length; - if (index > bytes.length) - throw new UnicodeException("Terminator not found."); - - int c1 = 0xff & bytes[index++]; - if (c1 == 0) - return includeTerminator ? index : index - 1; - else if (c1 <= 0x7f) - continue; - else if (c1 <= 0xDF) - { - if (index >= bytes.length) - throw new UnicodeException("Invalid unicode."); - - int c2 = 0xff & bytes[index++]; - if (c2 < 0x80 || c2 > 0xBF) - throw new UnicodeException("Invalid code point."); - } else if (c1 <= 0xEF) - { - if (index >= bytes.length - 1) - throw new UnicodeException("Invalid unicode."); - - int c2 = 0xff & bytes[index++]; - if (c2 < 0x80 || c2 > 0xBF) - throw new UnicodeException("Invalid code point."); - int c3 = 0xff & bytes[index++]; - if (c3 < 0x80 || c3 > 0xBF) - throw new UnicodeException("Invalid code point."); - } else if (c1 <= 0xF4) - { - if (index >= bytes.length - 2) - throw new UnicodeException("Invalid unicode."); - - int c2 = 0xff & bytes[index++]; - if (c2 < 0x80 || c2 > 0xBF) - throw new UnicodeException("Invalid code point."); - int c3 = 0xff & bytes[index++]; - if (c3 < 0x80 || c3 > 0xBF) - throw new UnicodeException("Invalid code point."); - int c4 = 0xff & bytes[index++]; - if (c4 < 0x80 || c4 > 0xBF) - throw new UnicodeException("Invalid code point."); - } else - throw new UnicodeException("Invalid code point."); - } - } - } - - private abstract static class UnicodeMetricsUTF16 extends UnicodeUtils - { - protected static final int BYTE_ORDER_BIG_ENDIAN = 0; - protected static final int BYTE_ORDER_LITTLE_ENDIAN = 1; - protected int byteOrder = BYTE_ORDER_BIG_ENDIAN; - - public UnicodeMetricsUTF16(int byteOrder) - { - this.byteOrder = byteOrder; - } - - public boolean isValid(byte bytes[], int index, - boolean mayHaveTerminator, boolean mustHaveTerminator) - throws UnicodeException - { - // http://en.wikipedia.org/wiki/UTF-16/UCS-2 - - while (true) - { - if (index == bytes.length) - { - // end of buffer, no terminator found. - return !mustHaveTerminator; - } - - if (index >= bytes.length - 1) - { - // end of odd-length buffer, no terminator found. - return false; - } - - int c1 = 0xff & bytes[index++]; - int c2 = 0xff & bytes[index++]; - int msb1 = byteOrder == BYTE_ORDER_BIG_ENDIAN ? c1 : c2; - - if (c1 == 0 && c2 == 0) - { - // terminator found. - return mayHaveTerminator; - } - - if (msb1 >= 0xD8) - { - // Surrogate pair found. - - if (msb1 >= 0xDC) - { - // invalid first surrogate. - return false; - } - - if (index >= bytes.length - 1) - { - // missing second surrogate. - return false; - } - - // second word. - int c3 = 0xff & bytes[index++]; - int c4 = 0xff & bytes[index++]; - int msb2 = byteOrder == BYTE_ORDER_BIG_ENDIAN ? c3 : c4; - if (msb2 < 0xDC) - { - // invalid second surrogate. - return false; - } - } - } - } - - public int findEnd(byte bytes[], int index, boolean includeTerminator) - throws UnicodeException - { - // http://en.wikipedia.org/wiki/UTF-16/UCS-2 - - while (true) - { - if (index == bytes.length) - return bytes.length; - if (index > bytes.length - 1) - throw new UnicodeException("Terminator not found."); - - int c1 = 0xff & bytes[index++]; - int c2 = 0xff & bytes[index++]; - int msb1 = byteOrder == BYTE_ORDER_BIG_ENDIAN ? c1 : c2; - - if (c1 == 0 && c2 == 0) - { - return includeTerminator ? index : index - 2; - } else if (msb1 >= 0xD8) - { - if (index > bytes.length - 1) - throw new UnicodeException("Terminator not found."); - - // second word. - int c3 = 0xff & bytes[index++]; - int c4 = 0xff & bytes[index++]; - int msb2 = byteOrder == BYTE_ORDER_BIG_ENDIAN ? c3 : c4; - if (msb2 < 0xDC) - throw new UnicodeException("Invalid code point."); - } - } - } - } - - private static class UnicodeMetricsUTF16NoBOM extends UnicodeMetricsUTF16 - { - - public UnicodeMetricsUTF16NoBOM(final int byteOrder) - { - super(byteOrder); - } - - } - - private static class UnicodeMetricsUTF16WithBOM extends UnicodeMetricsUTF16 - { - - public UnicodeMetricsUTF16WithBOM() - { - super(BYTE_ORDER_BIG_ENDIAN); - } - - public int findEnd(byte bytes[], int index, boolean includeTerminator) - throws UnicodeException - { - // http://en.wikipedia.org/wiki/UTF-16/UCS-2 - - if (index >= bytes.length - 1) - throw new UnicodeException("Missing BOM."); - - int c1 = 0xff & bytes[index++]; - int c2 = 0xff & bytes[index++]; - if (c1 == 0xFF && c2 == 0xFE) - byteOrder = BYTE_ORDER_LITTLE_ENDIAN; - else if (c1 == 0xFE && c2 == 0xFF) - byteOrder = BYTE_ORDER_BIG_ENDIAN; - else - throw new UnicodeException("Invalid byte order mark."); - - return super.findEnd(bytes, index, includeTerminator); - } - } - -} From 641beeb55883d5bbd1223dec8576552e47d40e2b Mon Sep 17 00:00:00 2001 From: mjohnsonengr Date: Thu, 8 Jan 2015 13:36:23 -0700 Subject: [PATCH 4/4] Created project for Windward --- .idea/.name | 1 + .idea/compiler.xml | 29 +++ .idea/copyright/profiles_settings.xml | 3 + .idea/encodings.xml | 6 + ...om_github_sfntly_sfntly_0_0_1_SNAPSHOT.xml | 13 ++ .../Maven__com_ibm_icu_icu4j_4_8_1_1.xml | 13 ++ ...ven__com_newatlanta_commons_gaevfs_0_3.xml | 13 ++ ...pache_commons_commons_vfs_2_0_SNAPSHOT.xml | 13 ++ .idea/misc.xml | 219 ++++++++++++++++++ .idea/modules.xml | 8 + .idea/scopes/scope_settings.xml | 5 + .idea/vcs.xml | 6 + .settings/org.eclipse.core.resources.prefs | 4 - .settings/org.eclipse.jdt.core.prefs | 5 - .settings/org.eclipse.m2e.core.prefs | 4 - FILEIO_INFO.txt | 30 +++ LICENSE.txt | 201 ++++++++++++++++ README.md | 9 - README.txt | 80 +++++++ appengine-awt.iml | 18 ++ pom.xml | 22 +- 21 files changed, 679 insertions(+), 23 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/libraries/Maven__com_github_sfntly_sfntly_0_0_1_SNAPSHOT.xml create mode 100644 .idea/libraries/Maven__com_ibm_icu_icu4j_4_8_1_1.xml create mode 100644 .idea/libraries/Maven__com_newatlanta_commons_gaevfs_0_3.xml create mode 100644 .idea/libraries/Maven__org_apache_commons_commons_vfs_2_0_SNAPSHOT.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml delete mode 100644 .settings/org.eclipse.core.resources.prefs delete mode 100644 .settings/org.eclipse.jdt.core.prefs delete mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 FILEIO_INFO.txt create mode 100644 LICENSE.txt delete mode 100644 README.md create mode 100644 README.txt create mode 100644 appengine-awt.iml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c388548 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +appengine-awt \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..51f84f0 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..d44269f --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_github_sfntly_sfntly_0_0_1_SNAPSHOT.xml b/.idea/libraries/Maven__com_github_sfntly_sfntly_0_0_1_SNAPSHOT.xml new file mode 100644 index 0000000..843d718 --- /dev/null +++ b/.idea/libraries/Maven__com_github_sfntly_sfntly_0_0_1_SNAPSHOT.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_ibm_icu_icu4j_4_8_1_1.xml b/.idea/libraries/Maven__com_ibm_icu_icu4j_4_8_1_1.xml new file mode 100644 index 0000000..ca8361d --- /dev/null +++ b/.idea/libraries/Maven__com_ibm_icu_icu4j_4_8_1_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_newatlanta_commons_gaevfs_0_3.xml b/.idea/libraries/Maven__com_newatlanta_commons_gaevfs_0_3.xml new file mode 100644 index 0000000..a45b400 --- /dev/null +++ b/.idea/libraries/Maven__com_newatlanta_commons_gaevfs_0_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_commons_commons_vfs_2_0_SNAPSHOT.xml b/.idea/libraries/Maven__org_apache_commons_commons_vfs_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..3bdf3f0 --- /dev/null +++ b/.idea/libraries/Maven__org_apache_commons_commons_vfs_2_0_SNAPSHOT.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f05dffc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + + + + + + + + 1.6 + + + + + + + + 1.7 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3b954a2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index abdea9a..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index abec6ca..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,5 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.5 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f..0000000 --- a/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/FILEIO_INFO.txt b/FILEIO_INFO.txt new file mode 100644 index 0000000..6e6ba35 --- /dev/null +++ b/FILEIO_INFO.txt @@ -0,0 +1,30 @@ +==================== +FILE I/O INFORMATION +==================== +This library has a basic issue: it still uses regular File I/O which is not +supported on Google App Engine. As a remedy, we are changing references as +needed. +See com/google/code/appengine/imageio/stream/FileCacheImageInputStream.java +for an example of a changed reference. + + +=============== +IMPORTANT NOTES +=============== +The new file system comes with some quirks. But it isn't just the file system, it's the entirety of working with files on Google App Engine. + +For more information, please read the gae vfs documentation here: https://code.google.com/p/gaevfs/wiki/UsingGaeVFS + +Here are the basics. Since Google App Engine run servlet code, everything discussed here is in the context of servlets (e.g. the init/destroy methods) + +1. You must set a root path for the entire servlet to use. The best place to do this is in the servlet's init method. You can do this with something like this: + + GaeVFS.setRootPath(getServletContext().getRealPath("/")); + +2. You *should* (in good practice) close GaeVFS in the destroy method: + + GaeVFS.close(); + +3. Now, it's important to clear the thread local cache. All GAE operations should be placed in a try/finally clause with the clear cache operation appearing in the finally clause. We should investigate putting this somewhere else (such as in the appengine-awt code in destroy code or something) because it seems like a bad practice to require library users (which means OEM customers) to call this in their code. + + GaeVFS.clearFilesCache(); \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ad410e1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index faadad4..0000000 --- a/README.md +++ /dev/null @@ -1,9 +0,0 @@ -appengine-awt -============= - fork from appengine-export:https://github.com/bedatadriven/appengine-export - -appengine-awt is a pure java implementation of the java.awt and javax.imageio packages for use in the Google AppEngine environment. - -The code is based mainly on the Apache Harmony and Apache Sanselan projects. - -http://code.google.com/p/appengine-awt/ diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..94b69c5 --- /dev/null +++ b/README.txt @@ -0,0 +1,80 @@ +appengine-awt +============= + +Original location: https://github.com/witwall/appengine-awt +Brought here by marcusj + +This project replaces java.awt and javax.imagio since we can't use either of +these classes in Google App Engine. Specifically, the source code is here +because modifications are needed in order to actually remove the File I/O +dependencies. + +========= +CHANGELOG +========= + + 1.0.1 - changing a few File references to use gaevfs instead + -- see FILEIO_INFO.txt for more information on the change. + + 1.0.0 - initial import from github + + +============ +DEPENDENCIES +============ + +IMPORTANT: You need to add the included jars to the local Maven repository in + order to build appengine-awt. To do this, run the .bat script + located in the dep directory. + + - Maven + In order to build sfntly and appengine-awt, you need Maven (it was already + set up well for both projects) -- Maven will basically generate all the + jars without a second thought. + + - sfntly (jar included in dep) + Located at: https://code.google.com/p/sfntly/ + + - gaevfs (jar included in dep) + Located at: https://code.google.com/p/gaevfs/ + + - commons-vfs (jar included in dep) + Although the original project originates from Apache Commons, the jar included + is a patched copy that came with gaevfs. + +======== +BUILDING +======== + + - This project uses Maven (I didn't want to set up ant and Maven worked + perfectly well and was already set up). You should check the Dependencies + section before building to make sure dependencies are satisfied!! + + mvn clean - clean up generated files + mvn compile - compile + mvn test - run tests (haha j/k; there are none) + mvn package - generate a jar <-- This is most likely what you want + mvn install - install to the local Maven repository + + - any output files generated are usually placed in target\ such as the jar and + class files. + + - The version number can be updated in pom.xml + + +===== +ABOUT +===== + + - This project was downloaded from https://github.com/witwall/appengine-awt + Original README below: + +appengine-awt +============= + fork from appengine-export:https://github.com/bedatadriven/appengine-export + +appengine-awt is a pure java implementation of the java.awt and javax.imageio packages for use in the Google AppEngine environment. + +The code is based mainly on the Apache Harmony and Apache Sanselan projects. + +http://code.google.com/p/appengine-awt/ diff --git a/appengine-awt.iml b/appengine-awt.iml new file mode 100644 index 0000000..b0c1c26 --- /dev/null +++ b/appengine-awt.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 09b600f..d11be9a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.github.appengine-awt appengine-awt - 0.0.1-SNAPSHOT + 1.0.1 https://github.com/pascalleclercq/appengine-awt org.sonatype.oss @@ -36,6 +36,16 @@ gaevfs 0.3 + + org.apache.commons + commons-vfs + 2.0-SNAPSHOT + + + com.newatlanta.commons + gaevfs + 0.3 + https://github.com/pascalleclercq/appengine-awt @@ -74,6 +84,16 @@ + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + target + appengine-awt-1.0.1 + false + + \ No newline at end of file