From 419801afa26b5c5bbf5b3cf5a4615133881f6dc5 Mon Sep 17 00:00:00 2001 From: treeform Date: Wed, 18 May 2022 10:57:20 -0700 Subject: [PATCH] Add JPEG exif orientation. --- pixie.nimble | 2 +- src/pixie/fileformats/jpeg.nim | 94 ++++++++++++++++++++- src/pixie/images.nim | 10 +++ tests/fileformats/jpeg/masters/f1-exif.jpg | Bin 0 -> 992 bytes tests/fileformats/jpeg/masters/f2-exif.jpg | Bin 0 -> 994 bytes tests/fileformats/jpeg/masters/f3-exif.jpg | Bin 0 -> 992 bytes tests/fileformats/jpeg/masters/f4-exif.jpg | Bin 0 -> 994 bytes tests/fileformats/jpeg/masters/f5-exif.jpg | Bin 0 -> 980 bytes tests/fileformats/jpeg/masters/f6-exif.jpg | Bin 0 -> 982 bytes tests/fileformats/jpeg/masters/f7-exif.jpg | Bin 0 -> 980 bytes tests/fileformats/jpeg/masters/f8-exif.jpg | Bin 0 -> 982 bytes tests/jpegsuite.nim | 9 ++ 12 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 tests/fileformats/jpeg/masters/f1-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f2-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f3-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f4-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f5-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f6-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f7-exif.jpg create mode 100644 tests/fileformats/jpeg/masters/f8-exif.jpg diff --git a/pixie.nimble b/pixie.nimble index 3a9c108..470b9f9 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -9,7 +9,7 @@ requires "nim >= 1.4.8" requires "vmath >= 1.1.4" requires "chroma >= 0.2.5" requires "zippy >= 0.9.7" -requires "flatty >= 0.3.0" +requires "flatty >= 0.3.2" requires "nimsimd >= 1.0.0" requires "bumpy >= 1.1.1" diff --git a/src/pixie/fileformats/jpeg.nim b/src/pixie/fileformats/jpeg.nim index a4d684a..32dd347 100644 --- a/src/pixie/fileformats/jpeg.nim +++ b/src/pixie/fileformats/jpeg.nim @@ -1,4 +1,5 @@ -import pixie/common, pixie/images, pixie/masks, sequtils, strutils, chroma, std/decls +import pixie/common, pixie/images, pixie/masks, sequtils, strutils, chroma, + std/decls, flatty/binny # This JPEG decoder is loosely based on stb_image which is public domain. @@ -81,6 +82,7 @@ type restartInterval: int todoBeforeRestart: int eobRun: int + orientation: int when defined(release): {.push checks: off.} @@ -108,6 +110,14 @@ proc readUint16be(state: var DecoderState): uint16 = ## Reads uint16 big-endian from the input stream. (state.readUint8().uint16 shl 8) or state.readUint8() +proc readUint32be(state: var DecoderState): uint32 = + ## Reads uint32 big-endian from the input stream. + return + (state.readUint8().uint32 shl 24) or + (state.readUint8().uint32 shl 16) or + (state.readUint8().uint32 shl 8) or + state.readUint8().uint32 + proc skipBytes(state: var DecoderState, n: int) = ## Skips a number of bytes. if state.pos + n > state.buffer.len: @@ -312,6 +322,57 @@ proc decodeSOF2(state: var DecoderState) = state.decodeSOF0() state.progressive = true +proc decodeExif(state: var DecoderState) = + ## Decode Exif header + let + len = state.readUint16be().int - 2 + endOffset = state.pos + len + + let exifHeader = state.buffer[state.pos ..< state.pos + 6] + state.skipBytes(6) + if exifHeader != "Exif\0\0": + # Happens with progressive images, just ignore instead of error. + # Skip to the end. + state.pos = endOffset + return + + # Read the endianess of the exif header + let + tiffHeader = state.readUint16be().int + littleEndian = + if tiffHeader == 0x4D4D: + false + elif tiffHeader == 0x4949: + true + else: + failInvalid("invalid Tiff header") + + # Verify we got the endianess right. + if state.readUint16be().maybeSwap(littleEndian) != 0x002A.uint16: + failInvalid("invalid Tiff header endianess") + + # Skip any other tiff header data. + let offsetToFirstIFD = state.readUint32be().maybeSwap(littleEndian).int + state.skipBytes(offsetToFirstIFD - 8) + + # Read the IFD0 (main image) tags. + let numTags = state.readUint16be().maybeSwap(littleEndian).int + for i in 0 ..< numTags: + let + tagNumber = state.readUint16be().maybeSwap(littleEndian) + dataFormat = state.readUint16be().maybeSwap(littleEndian) + numberComponents = state.readUint32be().maybeSwap(littleEndian) + dataOffset = state.readUint32be().maybeSwap(littleEndian).int + # For now we only care about orientation tag. + case tagNumber: + of 0x0112: # Orientation + state.orientation = dataOffset shr 16 + else: + discard + + # Skip all of the data we do not want to read, IFD1, thumbnail, etc. + state.pos = endOffset + proc reset(state: var DecoderState) = ## Rests the decoder state need for restart markers. state.bitBuffer = 0 @@ -913,6 +974,32 @@ proc buildImage(state: var DecoderState): Image = else: failInvalid() + # Do any of the orientation flips from the Exif header. + case state.orientation: + of 0, 1: + discard + of 2: + result.flipHorizontal() + of 3: + result.flipVertical() + result.flipHorizontal() + of 4: + result.flipVertical() + of 5: + result.rotate90() + of 6: + result.rotate90() + result.flipHorizontal() + of 7: + result.rotate90() + result.flipVertical() + result.flipHorizontal() + of 8: + result.rotate90() + result.flipVertical() + else: + failInvalid("invalid orientation") + proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = ## Decodes the JPEG into an Image. @@ -962,9 +1049,8 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = # state.decodeAPP0(data, at) state.skipChunk() of 0xE1: - # Exif - # state.decodeExif(data, at) - state.skipChunk() + # Exif/APP1 + state.decodeExif() of 0xE2..0xEF: # Application-specific state.skipChunk() diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 0368570..d4eb875 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -170,6 +170,16 @@ proc flipVertical*(image: Image) {.raises: [].} = image.data[image.dataIndex(x, image.height - y - 1)] ) +proc rotate90*(image: Image) {.raises: [PixieError].} = + ## Rotates the image 90 degrees. + var copy = newImage(image.height, image.width) + for y in 0 ..< copy.height: + for x in 0 ..< copy.width: + copy.data[copy.dataIndex(x, y)] = image.data[image.dataIndex(y, x)] + image.width = copy.width + image.height = copy.height + image.data = copy.data + proc subImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError].} = ## Gets a sub image from this image. if x < 0 or x + w > image.width: diff --git a/tests/fileformats/jpeg/masters/f1-exif.jpg b/tests/fileformats/jpeg/masters/f1-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff003e394915961ec2576628bf882b6b49a3407d GIT binary patch literal 992 zcmbV~Nl+6(6oy|<&txVu31pHY9z;cv6SzZJj6F#Vr6=@Fzv;hv-s?Bt>()lK33!#)nV;DAL|z(gVP07?KRqyehY@~Cb=&PWVJVpJe+kcfLJ+hT5q2k1y{JDGJWBu;h> z&#J7bZbfm+u0%Q0?NZ&c3Sc+_z>)}BRu#Dmg*Bou1b(k8vK&C+Dw+lmprTbEfEP8{ z1GLv*Db6p-2ZRu*NvdlLV9WK^R{3gc68#=uePTgTNvZY{asgA6K8Vp|FdW0sG{dv3 zfiv+YlfVmtImT)+$A~e4U~yPPn`F1!O|fzD4k_L$*(H4wgh>v~7#YSWnFX`-&!*i1 zk%LljVk813A}k_p7*>#`F$6)Uh#2Vz%@|nD$O|N3&;k^~n4+=%*CcwM-Z5HatVv3? z!B$qqCO1lIYgZ4Kk~1)5FCUpn&8YUa8F@!s{IZ0!m8+bq(_NWs)~;Lc&fSogUr<<7 zym3>-=E^Nwx9zB@t=n1eY1rl4y~p3Qw|W18gY6xM4tE|scD(z<$y2A#^qxI;{=&t+ z{@|s{R|cmZ z=5;P|jA5)O*FOC6GNxpd&)BQI zBfKNkHIX)}qlL(p2G;gpWKpn1u5mD7L=F#&kOe=30jh-uC%L9AS;5YT=x&CEU}%kp hg^DRW-xrR;bCn?x`WQR8eTgZt)4kyuf2L?-zW}!h3XT8( literal 0 HcmV?d00001 diff --git a/tests/fileformats/jpeg/masters/f2-exif.jpg b/tests/fileformats/jpeg/masters/f2-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e0f170e36674d695e53c5b480e57d59ca2ecab9 GIT binary patch literal 994 zcmbW0OHk8L6o${uO`0@mp=r~CxR9q-R!Jy6x?uo$&M*TgqAr}F0#bHVw8)GzfC|b= zRD7)pzOXw~e62+Bxx*)*3#+2Y3Vg#j#2Z?R!%94JPwqb_=iHO;+=(`#h2V8wleZS2 zqy*9dfCCm;0TYFQB98=M<~4u^EsyGYYh?M8CJOF0r7fq*QwaxqvB3U&Ls#7>;3Rn&Da2 zz?pcHN#F&+9AmYZW5gIiusAHDO|skVrr5Z6hZJv>?2>*6!lZ^~j0|Ix%z{~3vT3(L zPiM2#ZJ?f)!+F3_;K-B1R@cGX|D3@&ZX1umFWHrf95xHObzmPmC5BYm$;} zu$5J@$qka)+SS9Q@wy=Tvzzi_ed zQtxK+R8~eT|NDS#*~cmDZ9rv z%sWzDp|oioZJzAkz}o(cEDE;3H3lY3{v8pEkOjYj0jh-uCj??Adn00edrMZZQ;$(C fS>4Sr7o3+o%vDU_*}h10i6x{5o~c~aMH~GM?`I42 literal 0 HcmV?d00001 diff --git a/tests/fileformats/jpeg/masters/f3-exif.jpg b/tests/fileformats/jpeg/masters/f3-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ed7b16a79c93f0d6f75c5060cbfb7bcd48d8fd6 GIT binary patch literal 992 zcmbV}NlesG7{|ZY*G@a_3`}P_1L8qeMNVxQT;@UmSrZZ#5%pjK0y5kfQINz0P(e9~ zircB+hI2#3?Ieop4Oc)9qN2zN+#!+rTE-#agnoI;Kd=Aqef@n;AJNC*O?H#N2B5GI zQUHJh7C8YEg^U%&0Ap%^O0+Vp>risYLlKV(WCe-L0ObmuBY+M+(4pLRvKx^{Ci$|k ztFof{6vZcdW92lTSM{Z;0G6WwEOF4Xs>oevK_d!-_V-+oWzGCIK)Mc~QQ<0};YCgE z0R1gE3Ucyu03k$blIr?AxUvG(6%Ex@v5o$Q+SuIu!Xo`OWC5lqW00W9U^s@MX@+N6 z3uohPHh~uedxX3WGm$fN7CpRy@ zpmcNDmh!FJw(qQ}uGv-VuiM?QXK!QEzUBi554Cq3KGJ#Y_=)b5r%s@+0RlHT16^FdScFkd!_=lYhWkWp02v4h+W-In literal 0 HcmV?d00001 diff --git a/tests/fileformats/jpeg/masters/f4-exif.jpg b/tests/fileformats/jpeg/masters/f4-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e081f9198454aae18ddb9525a6b987b6191824d GIT binary patch literal 994 zcmbV}Nl+6}5Qh8Bdy|>WB#=oa0r4QKMou$BaLI)NWUZnKP((dg1p*RoL=>dTDnJG0 zBr0yFfE(n7irYyP*Bh>Y9z;cv6SzZJjBf&l(v#Tr`prK*{krGtrw{96@G7gpR|QZ| z0LcKr1BdK@sX{>L76WWZ1C*oXVO@jV3potU(SYtCQ};8Dg*XCe^BEOr9e`HqhUHQ> z-a}7KQ8llsdKFKslIrzn-V_bMbOeB-8AQ=kr31}t#9#>gj;o3iK=WO69U#DjyFdUh zYFY#I*I>)f&dUab5ba63>T}@8^jDVERaV5-`|7G=bMgub^_P$dm|=`XjnKvNEK3M0 zaGZ&^2o{Sdh@v&ZZnH*65u#{w*(8VTbUH1OQPD0r+Acd~qX@$E3}MYIYnH8|RbH~` zw?N{d5Zo9^z(@#7NFRh1bZ9I^Feny`PK2-~jyDS;P3X4)gD_?YHvXDs?=vPwB-S3M zrkfl^Wn6r%tTlIZ@d+7y1J2^1sl=3We~VdgMMW=*Nm{wey*k;Gx@PUV_1?@4S=l+c zdHEYRm258EvUS^zipr{;)xMftb-VY}H|%ZPf8bzi+o8klM~@xvJaO{W=`-DD&z-+; zv8Ok9>GGBStJkjIxOw}|;N5%oA3S{Y_{q~}&tD9WjE=o|`)>UGhl!7q)1N+n`TFhq zkC|D6i(cbAR+#KBE;s+1(_78v>Ez+ndsY?S^EU k(mESqE*O#m%#}>w*&Z6Dd3dJu&m%@G^l?c?LOt}6Uy(TsK>z>% literal 0 HcmV?d00001 diff --git a/tests/fileformats/jpeg/masters/f5-exif.jpg b/tests/fileformats/jpeg/masters/f5-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8d875479fcf6f9f443434323edc5aab142f2500 GIT binary patch literal 980 zcmbVJNl?^K5bd9zOfty~OlC48;z3qLPDvPC=0X9oR#64Gpx!`0h8rUaQe~B(f_f4a zx6^<-H&onCqPX601@s{9asqcOi`aoUASbo!_x~^dzwXZKHU^Cmc%IW3tOh75f>Zzy zfG2LChzM}F#hN=}fC{o8f@?{N#bNMK2i!qH4>2C=9wXX? znif#~acX+NuLsg}0NZ{59vQ5vYic{0#)P2|{(Y~hYM4w{F${n(6R84WI)gC>7%#w8 zm|Ktwgb=KWRgFpTWQVHC>#Hi`8iMsTad`zr#l|zp2FftzLSu1p0>`l|C-S^aaEK0v zBubJq%I$JS$x)Kz^0{P>;`MqR(eq+_N{n0aDrOQwF^1*r9A{UYl2iF-Gp>UyKnfI7 zA_F56Difn07Gl#llwhi`3_HSdHeRrc5)yh{zz|9qmYRQ!?Coa9SebLjYne7rNjaZT zr|8Y?okC((PoKARU@R%EBGh6Ree+`G$0jdYoU$a4~jg?i^n`(l!o9nl1ZD`!qv~$<)*0w!+_Z>KRsN?XFqsNYSoj7^w^qI5gy3b#@ z*n8>nm8;io-0Z(~`_A2a_a8ib^!Uls!J*-im#<#GdHe4DhtctmpFV&2`tAF~4^tP8 zaavYH*&JQi1j4W^WqDH%yRLX&7E1om)Cg`^kzZHYVYjnvnQsNj(ICW1EMd< zKaxCdN}E#lcfwl!OIbwNjILpDP=dpwGGxHd?l7}M#EH&6CcCG;Gdh~Ug2F1b*EtW{J2E~qyUkZ>cSAXQcgDySz> zaXSUvxuN2A62SS&VN;2q{nMtLg5)y8qX2zSqWxF%B>CTl{qZrKOMo z00QvD2^19p3r-2Z&1!&ZvM{0>NP5`AphpLsLBSDVT;Y8J(CG&@oZEq`8AUkoB(&

ECwfnwI(_Esx$}J& zE?yeAeC6u3>o;!=-M(}8-u(v;A3c8Z^x4Sh*!Zj0Z{EIp|Ka1r)Ths1zJB}uWBRAb zh0hqmije)mg-sv~%Tks%xd_vYgUT$Iq*X2qntIxsAH zQoZA8QzqIh+24V+{})*VY@TZjY?R>RQ5mw~S6?u8qIai=ZyIFgmhA3UnCXk&!E-uY XIcJTMW>%CvN`_M+mxfDZ8>7Dg!SMnV literal 0 HcmV?d00001 diff --git a/tests/fileformats/jpeg/masters/f7-exif.jpg b/tests/fileformats/jpeg/masters/f7-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b5dddea44bf215079b3027016b56d4a902affe95 GIT binary patch literal 980 zcmbV}Nl+6(6oy|<_hcqB31pHF1b*EtW{L8h}*#`B0<89h=NvG1*o8$ z#9dFp6}X||b`r()hAW^4qoU{u+@(s!9teS=JgBewb-(KVyI+6r=|g%5UgkFXs{x9O zAq@a{;E)|KjSz5jiUW2!2JoWAk+>G6hBXY;Xh3I>sRtNGcpm|@`GJJ*w$jzG0_yZ+ zX4Mo`^QfvvamOne9=GO6*8ofh!N?0l(Nv{l`VT-5gCRJ(uPRCq&1BJafFKje0zo_% z(>FkW1-7ER!aP6-(VR59J_U}PfUmONR~6sjudj*EFDx$6UqB9EhA{>;LI=yUEFrAG zaVFj(SS+F-iq|iK5MAlN_?s>9j=0#Jc2IyX=&WM-ZlW2y13pvuqWu@;{q? z6C@r|p#&od7ztqs=>xEY7LBC{21VFtMF?x+c(WiO9M=d~Frv-F5@f=!@*vYB(2mY7(W9H2GJD$R?HK1% Y6=Qg^>kpa*@_9NMMjnl%$kc~_19kTSF1b*EtW{J2irc{|5Rh;qqCu;y0#r~= z;;yIQ3fxd}JBi|Y!xhkjQBm{+?ot+GPZ9!pQeXA*Pxs&b=6kJ==s|dq-|VdeC@qBy z0N{Z`cED62ssSYc8`1#PXkk=0p!A4`p&kwB2r}(H#t~UZ03BW+k+bb|Hlm1jx)Yu? zMb$j2>QUT@N|wj1c``Ks(*XdEI*6jFN*4-i#9#=_uB(a?K;bO94iI3XSs;MtG`$1# zmtZR?C@uhm5Y0)m>oefU^VL>0)z&2Xy-oFrg~g?1`g6zw%rHiyL1<%nmL-H0IL^dd z1dBx!M9~^!w^?JP7*VvjY?4EEI-Qo-xOkTwZw!Jvo)tq5UF9B&pxY8bEqgD_?YHvXErcNrBU5^GOVb4-r% zDlWNE)>^x|`IOxLL1)GAWNK!$ugxsD;^G%1q%B^OzBI#~wQTu{m7cs+`2~eV#U-oP zRIXjOe#6GiHMMnH>b(tHo3?HDH}BZFd(YnXj(z((4<0&vECr+L^edg@B^F6(N z7cO2JxP0a6wd*%;4c)$T_ul;n4<9{#^7Prr=veU8>o?