From c6e8c8bea68e150903674f9a7ab4b4b8414d0ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=F8=EDk?= Date: Sun, 16 Mar 2014 12:51:33 +0100 Subject: [PATCH] exporting frames - PNGs, AVI, GIF format --- trunk/lib/avi.jar | Bin 0 -> 215818 bytes trunk/lib/gif.jar | Bin 0 -> 6306 bytes trunk/libsrc/avi/build.xml | 73 + trunk/libsrc/avi/nbproject/build-impl.xml | 1407 +++++++++++++ .../libsrc/avi/nbproject/genfiles.properties | 8 + trunk/libsrc/avi/nbproject/project.properties | 72 + trunk/libsrc/avi/nbproject/project.xml | 15 + .../src/org/monte/media/AbortException.java | 39 + .../src/org/monte/media/AbstractCodec.java | 99 + .../org/monte/media/AbstractVideoCodec.java | 226 +++ .../src/org/monte/media/AudioFormatKeys.java | 126 ++ .../avi/src/org/monte/media/Buffer.java | 164 ++ .../avi/src/org/monte/media/BufferFlag.java | 42 + .../libsrc/avi/src/org/monte/media/Codec.java | 73 + .../src/org/monte/media/DefaultRegistry.java | 395 ++++ .../avi/src/org/monte/media/Format.java | 325 +++ .../avi/src/org/monte/media/FormatKey.java | 112 ++ .../avi/src/org/monte/media/FormatKeys.java | 61 + .../avi/src/org/monte/media/MovieReader.java | 93 + .../avi/src/org/monte/media/MovieWriter.java | 82 + .../avi/src/org/monte/media/Multiplexer.java | 34 + .../src/org/monte/media/ParseException.java | 30 + .../avi/src/org/monte/media/Registry.java | 310 +++ .../src/org/monte/media/VideoFormatKeys.java | 77 + .../src/org/monte/media/avi/AVIBMPDIB.java | 106 + .../org/monte/media/avi/AVIOutputStream.java | 1117 +++++++++++ .../src/org/monte/media/avi/AVIWriter.java | 517 +++++ .../monte/media/avi/AbstractAVIStream.java | 1734 +++++++++++++++++ .../avi/src/org/monte/media/avi/DIBCodec.java | 343 ++++ .../avi/src/org/monte/media/color/Colors.java | 213 ++ .../media/io/ByteArrayImageInputStream.java | 216 ++ .../media/io/ByteArrayImageOutputStream.java | 336 ++++ .../media/io/ImageInputStreamAdapter.java | 189 ++ .../monte/media/io/ImageInputStreamImpl2.java | 65 + .../io/SeekableByteArrayOutputStream.java | 163 ++ .../monte/media/io/SubImageOutputStream.java | 151 ++ .../src/org/monte/media/jpeg/JPEGCodec.java | 155 ++ .../org/monte/media/jpeg/MJPGImageReader.java | 119 ++ .../monte/media/jpeg/MJPGImageReaderSpi.java | 88 + .../avi/src/org/monte/media/math/IntMath.java | 250 +++ .../src/org/monte/media/math/Rational.java | 492 +++++ .../avi/src/org/monte/media/png/PNGCodec.java | 118 ++ .../src/org/monte/media/riff/RIFFChunk.java | 188 ++ .../src/org/monte/media/riff/RIFFParser.java | 841 ++++++++ .../media/riff/RIFFPrimitivesInputStream.java | 265 +++ .../src/org/monte/media/riff/RIFFVisitor.java | 43 + .../avi/src/org/monte/media/util/Methods.java | 463 +++++ trunk/libsrc/gif/build.xml | 73 + trunk/libsrc/gif/nbproject/build-impl.xml | 1407 +++++++++++++ .../libsrc/gif/nbproject/genfiles.properties | 8 + trunk/libsrc/gif/nbproject/project.properties | 71 + trunk/libsrc/gif/nbproject/project.xml | 15 + .../net/kroo/elliot/GifSequenceWriter.java | 191 ++ trunk/nbproject/project.xml | 2 +- trunk/src/com/jpexs/decompiler/flash/SWF.java | 188 +- .../console/CommandLineArgumentParser.java | 79 +- .../exporters/modes/FramesExportMode.java | 28 + .../exporters/modes/MorphshapeExportMode.java | 29 + .../exporters/modes/ShapeExportMode.java | 3 +- .../decompiler/flash/gui/ExportDialog.java | 14 +- .../decompiler/flash/gui/ImagePanel.java | 8 +- .../jpexs/decompiler/flash/gui/MainPanel.java | 70 +- .../decompiler/flash/gui/PreviewImage.java | 2 +- .../decompiler/flash/gui/TagTreeModel.java | 8 +- .../flash/gui/locales/ExportDialog.properties | 9 + .../decompiler/flash/tags/ShowFrameTag.java | 4 +- .../decompiler/flash/treenodes/FrameNode.java | 5 +- .../com/jpexs/helpers/SerializableImage.java | 4 +- 68 files changed, 14139 insertions(+), 114 deletions(-) create mode 100644 trunk/lib/avi.jar create mode 100644 trunk/lib/gif.jar create mode 100644 trunk/libsrc/avi/build.xml create mode 100644 trunk/libsrc/avi/nbproject/build-impl.xml create mode 100644 trunk/libsrc/avi/nbproject/genfiles.properties create mode 100644 trunk/libsrc/avi/nbproject/project.properties create mode 100644 trunk/libsrc/avi/nbproject/project.xml create mode 100644 trunk/libsrc/avi/src/org/monte/media/AbortException.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/Buffer.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/BufferFlag.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/Codec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/Format.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/FormatKey.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/FormatKeys.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/MovieReader.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/MovieWriter.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/Multiplexer.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/ParseException.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/Registry.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/color/Colors.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/math/IntMath.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/math/Rational.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java create mode 100644 trunk/libsrc/avi/src/org/monte/media/util/Methods.java create mode 100644 trunk/libsrc/gif/build.xml create mode 100644 trunk/libsrc/gif/nbproject/build-impl.xml create mode 100644 trunk/libsrc/gif/nbproject/genfiles.properties create mode 100644 trunk/libsrc/gif/nbproject/project.properties create mode 100644 trunk/libsrc/gif/nbproject/project.xml create mode 100644 trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java create mode 100644 trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java create mode 100644 trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java diff --git a/trunk/lib/avi.jar b/trunk/lib/avi.jar new file mode 100644 index 0000000000000000000000000000000000000000..5409c76787f478c7f2fd74eff04b4dc5308c2d68 GIT binary patch literal 215818 zcmdpf349dCmG|p8)H9=&7$gJ;1c+Pc;6BA+bO8b+VI;r;+jx)$G`5h04ls_L_>6No zj-A8_wv*USVv;zAjcs8Eo8a9f-Z;BCHrd^HH|KsyHpyPuJMlW<`~RzYdb;NjN9^6- z_hGBMtEP@uuU@^XdiCC`9ozhYh)76eYTH1gxc-GDAT3QDbv4bcn-=~46_FiI(kV## zpT8}6{yrq<-vsRJJcPwn#)Dr7U9E=YQ*X)iD4kh~gYRi`_TwD;T>vP4_ z4aB;7;^m0Lk82h#DTp)-#$&_r?wb1JOuuU3;{D54)Ero{y1b%oFxJ%@FK_4{9Oxg6 z4Wq;b1(B9mqOYc*H#Rg>Ti!o-XyM`hzTx=7!}0D!Y~kV9aL>Z#zTp-R#${!$%mTZk5=)2TP1``Jlj!{WAxJHH(y=hrK`pj2mpEpGKHz!N$ z>kjk}4maJ{6(1noE$kw}z4H4no<-7SG9`k+GLer-rc5@a)R5>sR(HCzWIeZ{K z*bzI>iL^4h|AX&h8 zJXyd7q-5H>%Bs`iOD|zADxarHn5a|PB_O>NNioVW6u|sTq%$OY!$8A6KCb5DnuuI0`%Rf^%5@PSA{LPXIl!+jKDteb z^L;Raw1@ax4|63Lyq;e-u-M*+9G1R_^vi%LMlC}iUqj~VLy5En}}&uLfzWWj{>6~6nsansV<3$u7kTK>X35pk4m6GQaX59CcaiZ}QYlpyX1KW)*c=!O z-nYnGlRVkmUzSwM+bp?NwpwzV+-}JoawoblowI4OvE(j!yCth+wIz4UJ1m(m6_%`# zT1yu2>z(p0Q{HXK4!P2l_gHd|yw{LP=?JJFN%VHd2Q9f*-p88n1A)E8%Y@c5{(kgL zb$P#QNU7-eThbv{8Zs>%Mh|*09#ZwT9k@Q;1$>vJXHkqA{n6Fi-v@5w?Jk`QR2Zme z=5Wo64GhHly3uX1Zah8HtetkjUZfsM+=Q|)$79r(!~JS#7*YXVkPgvW48x2#PSmks zESSNGr{<<=)D!}psZ}w$)g_(F(R9aeY~#R3TLE`?XoSoU zO9wWzIV%;{WLsyr+M_G5dmGSr(kBr%M*6afhRk}C#RXe?b5)X2A!^O2aEe{w zR8&}o3a63BHD^}_{40Yc!IHpZGP|ZExU@E}y6R;KmFAt4 zaA{uEs6<93f6tvkR6Jicf^QM5EvR}P zTD%tVT#zl4b!ej>T~I0Ov9t>S`Saxpe6eHN99*=agbFmEKwR&5z~a2 z>m+aX$&}5eY%#?&fR*#8!SqJrcQKISJgUm_lTT&dd|txhcQT>|Lz-o)My32I zAkRbG#;UK%>dH|W*HV3UgMVe9BrxqUnO|KJSXvt_4FYS?6H-*Ez9P@cc;I^clXnJy z!&fT}gbj9~U*T>R)`Nnt0I)R`t)BzpTL{8ihVU9$3B0UQc-(+`r-E3w0V_d(xLD!Q zhiS0w{@=?ug`nBLK`Dl8cUpE9_+-mgRs#AWceBd9 z&C2JnDlQG`_WYRbDR#AIx+1l8fOI_wa|8PR3QQ;K(878Qq6Vield-(lZOK!=b9jZ3 znl#}@t7bcGS?RQ8WqMmI)s`~dmKJHXdub8g92h}`cYL@<%N7Mfa<`$!x&gw=mbo~6 zsI_Fxj2SXZ&HnVvJF{lqDeZPB}w+R$EVH8Wb zs_4Q|nYjLpOzJ!>ldH-C*NsZ)s7xuE%10R<#LTEnE1Isqi}4-3WmIO2N_h%w&zfgf`al>g_()g=Wyq8zrVK|w$Jh@Ym18EiLLqL2umih^@!SuYa!Ule z!?&99wg`5XZ)N^l`M51Cx62(7xl`^k@UEYYg0AWCN$@UD_^bLv6P8-uQsGCbM>S#g>@Iu5>&{;O6>qKmAQ4b)28o2 z^tx;3=K3Wo#;yVjbANZ&VMAu7_g7-`Cu%U^1>pH3<$Qpv~WZ5Om_lmKc!- zuv9*vA_`n}u+DH-|G;t8Mng8(OXOTb)n4D``WdxhB~QyUhKyG=D1j=7 zJAR0}*JowalGE~(h2__CmOL+CH06vXFUX6QoRu$Ga*hdJ;^U8GG!sRsW}`^L_{Wxf zS-xV*S1oy2{scQ4cct8(jSROPtnVM`>mG8b-jFH~Vfni9#pO5OTn^GqeloU7MQkWf zEW{6ohI?ib)UTQHiY3>}*DWkf|BNx;Fl5$+r`8K^xV6X^R1mNH(FNJS3C9}-b&zwN*^xO}txq=0x##`0` zQa_^rW&{6E6MAOOF^*hyMtTpZMUDP>Tl_d=0iivdA@EIBke=E}h`KW{u73nGK_e6w z%$?i4;dXi>l(5e-P=u0dgtw>(O2Lv>Ve`tuE_JAvoTMQ5aU(eyjkzl+h+==HqQN-3 z2V=)<#tF%#GpJS6+UDj|OhT=CRD9b39Kv8>w3eb5vpX>~&_5JcaP9{CM}zBe2GbH7 zx&fVm(B{E-yiW%s2=0t`>j3);EIyBMo3S>q*s5kV4KRKlfw}Oc#A%!pARwvOSFfiK zkC;e{MAT_asnb5)niTmc=qcXeL_)GOWTOimI=Pp^*Jey4X}=rNo>4@q#q;LY62 z@xLU26H>^pa}vP+PwL3v37MxOgDsFt~Jn8%dsY0rG(NPE$u#YQx z8JqTJr1+HD`<;`CqcUlOj+l(ippIA>s5~RV&S@oqX!x|0J`3pta$@t>T2t-!8>2FH zPtB;5=}6-8oT&0$=xfDcnm{fULD!X_(`KN<=A+A&09k8*oJ~MY2T&3N()uB4ISSd& z-H@G}fQ;eOkTg6cE%HTdv(I9){Y~s$iEc`VDCKB`e8P{^vrx)re4W5nZ415vsNqv~ zXFP=tq$Gxf@olGDzU}IkVhNV~PBxgb&6Mr0$&@v(LDU4Li2oORDf9>R(k}GUZuHVs z=%r5Ja4&jkA25A2@Ov$KX+L@?hF`5`!@&Ot@P7>WzY+L94*cH){J#a#lAGlK zPH+a~HsJd%h2KgvcMT|7kAnG-LL}MG6Aqm};i7X({3Wl;UeNh#7>@y*(ENXdx_1F} z?*{7b0qWih)ZGizy$|TQ52(8zsCz$9_aUI}p*I9|SISOJbr>etr=aU5XsQdIkbIlK z+}*3^wnqTtqY!3)T*08l_!FG2n&9dxzZga6p>~WppPD@1f)F`a4Jga!FN;QRzORFy zfwE}G{tTA+@l~Y~r3JyKT);Ms~0Ast+ zDpUrM+-8(;6}|(A>BMtyc94QuLGoV^q%Q!ZCjrt^0O=G!dipXz+6Vrmuw<;pWEuvr zw@iCTCYJ;rl48tU*HxeWxJ3BzuoRq;d7VY`PfNv@N&_qm0_> zX(i@$3s;6pgC%))<{2d+h3Hg>xOPHB_njuB^Ixed4fc@Bc< zGgv>ofVIPm7<^~3COC&l?InQzM_`{Xf~kH5L;S0%`B$S68zJ5%_Cso-yar!9i@C+= zrCXd{x`oOkKmdQe4(UiHw}4G^8mt1H9zY2})Z2!`uFY6))ZzDW5-1J-UJ6VJ7{+Uu zjon?`{U+j*ICBuFNH-NCOgbZp4(MrW^}nNq1uyGu2J}dP@YWt(zmD zKS<2iQg%LFZjIn{3S#rMayx^NozIs$8N7?ZxAW_6e!YWV5TQ?(ck%1p{CZCqExw13 z_cHF@FiL(Oi--JtCgtZodA})8tMbVQ`FOyj5Phb6m=8a5Wx~h!^>KcEf?uCB*(+hHV|qElvA7_O!J#Lx=*bYzf-i_H^h3&t^~1+`4qRP5|*$ zwi23Kmo3!^PD4p13k*B!_UPZA!v9vqWp5vkm~sEMXodu zVWe{+&4f^%|KMQ=uJkd=SVdrLq$!Mjz@%6f(%%9o;-nax0w_myT9h(&H8}-&iZQ+j z+0DAHUR&AMBU7K?0s>t!WDC?Wi;p3iEjoti`K$7> zh1D8j{zSfJ$tx7}V##VtdtZ@nSXezE_)Q9bck}C8{Q5S({+v1X@asGBT}%E#{?d}a z;$t72GaD?drt$a%;oZouU-GL?pm|-Iz0=j`h-#`#u_KoF zE6U+)o&gc%!6d`LHxF4Aw*bpm!aNT=!F;0`2vuwmSZG=_k!_eRXYnc z^+A77seD<@V3LDMXN+oS6Tx*Vx*-n_r5wXwOb6^f5~y7Uk*sw~*v+|jYU$w?M*FLNqYo-xM08?kxu zBO|e1kdj9no>T$^I1?-O+A?f4XD4Sd{*Z-Osg9Z71uGyJ2I;AMdgh_EGf33gz~vr( zSO@fks*E{+Xrdsse2X0H?T=|g56F4Pg2A;QNj5t(3KW<+Yq8HjE(*h&a9XixuhM&Sp!KF+hH{tBr!5xc6@ilPg4W7)hG>w4}xO36X-RZ1qR-e95X4F;;-^>`afx6TLo zQz_)IsqiL*H@m`H5N>vbw<5gF72b|;i!0oUaGNW<17R(2X-C~Y2p~#1eWCtT7?YSM zj0wyMUxoCT44v>^gfW>p;j0k_6Ly6u?BJ%_z=oPyb~r=F+hvIoLr`l`PR9N^sYMyY z4!awnu)XEG4oX?2*tawD=o#^KMvGn)f2aSH1ll_TrzF_k89XIG@y%J47 zKO@sSD*+Ca%1%hJ{whBwGe-r2R{%Xl0X-W(gdv(eCv!$+E&^7Rs2G)b=VbnL=R%1@Mf2RFT3`W62&hbXi8MF$%w{RIWYaW zn1PcL@}HDI;Iu4AN9%|Stv?4^`)#!TwM5Qiy9)dbh)?5>TBzQz!ta3illVPy9-~bq zaZrB5XP_e%=vdQ9h*(6IW{<|<9jgs_QlporXc%#WF_)!z#GCSZ1nVsS2)125TukH5Bn{>HKt1>@0A{c{u2+gj``O$i5XP zS3AL-c7s#wMZaAuZ$)4FWC(eEkmtXEcM-mBK{uX3{^Q`V=Qfslr7`zDaL-$B9Vf!S zGGs_!FKKOwG$G9ADB8FlLo7m3PZ$ERK0f*xgu3Hwsyoh>K|Y4~80KSykE47*a5I~l zkF(`CzhDN|9Kkt*R(vd$w?^b`l-v}{ZG7Br${iuOGeX-Blj53QYCO)SxTcpHkG&Mv zyoVZ(z0`Q@m3vKjAG92C7=Exfb_jERv7T|%@o`h%$l>-l&4k=L`6ZbtGPlPX6`N3| z-rTmfb?k57(bUkqshMII8Yy{2BmVRF2QX-phx31^)y1&4_X=nnB?ns_B@c=-$~GrY z>&(#9NNrXdM+Vi-9P9Org=zZwEna+sH^#ypwoOMy z;@GxpqfoSLD~fpoY)3P9ts155479l=J{;@lhomQS8DpnY>xLxHlfQkxA zZ%}Q6)LO&Ft81+>)~bi$_c0!PWOar4Z+f-44If_!qJS5daIAmX@z!dM2Kxkdo?9O!|IDseY@3pVw! zL7lpM%=*fY|8x>jt@UzK~g@?!eK+DB_EUrXhDImWwPADVGkZF zWtD9Qw}d||SWaZB0qV+ZT>sa5DHAHtY!t z=YqgUh6b4iGOJCUR*iJ?cF=ksK;&-Nv;D9)8(ByIImkkoqasU(C$*d0m0-xZ#A5Bo zq2Y?*7lzqt?6_t}mF}&N&3u|O`H|Mrx>WtX{~xrsK#S?C4TWLN+%RzR5`9PeZ-DR$ z8sRDW%jE1yt+vcW6rS0;^xRv2ZBCg7#pV8^s7!aOf<&245|G#sZHo<=n^hR#qA1%0 zxynsN&~xjBx;>=oIS2{E2;32S;MrJK=?xu*JJx^m)k>}c%j5-E^+DHGg{$#}@sDrX z5%L@kM-lcL!(WBL0n{5TLcPHy)EkUKy}?P;n<{PEE|L3h1AA}SX;8`iCEsgY%w};6uL{wu{>XlBkmLkOB@iE*dX}GrU3{*Fs{em-UJAD zSG^?TPsqxub21+Pw^plQv|0zx)|`DzLN#Y!2({Gs?KpqU+2^F_q_iSu@d;UooW-rs zA(~JdC=EcXZe_3r=e-ka^Ug?dXKA1`uOx^w-$^Yer3^VHW5-)nRQjAuK}3FQ6`sMW zmg*+~))4f7ZUJunkc$W=2I|!91CUqaDE}JZ`&t-BT?e{~0iRuHN;fp;;*gIV#IEEJ zw&*=5;d+#D7^U2RJxZ_Kitjt*2v+pN3NchV7>81}VE5=p3A3@k*@my}usYoi`3$vD zCZgsxY(V!SC9T);P?8V(pLV1Jx*Vk54a8Gw!nj@d@}UC+Fq^N-Y*WSuUX$@lc9^m! z$gaOi>Cmw2*CQdjK8#Kc;B>s~vk3T9i?y)jxT_1PB0&nLxMeSJ!J@7PF7)`h0uZq% z5H|2WEkzqV3l@N#hy6{89!#e&(<$uQY=!x=6m|y+yYm8tVY;;;>jMD9)|9dnGR|)3 z^gSm*=ep4|_7GUz&sqcE9l-rNLCEh054uOyqg8gMI6zOafkvsIf;QAZd}AC%L(mtC zVMnU5(t}^v{OMI^AH>jc*vd&Nig!KCFLyzB&UF-)y2@km1Io+v$I+jLV zsKc(ch_aj*Xwav4*-^XJDXe!hU_*PO4Q|(gm>pY z`zL>L&mGXl;9oh4O`l6stH`w7`+Djs)ICdG;G28sT~RSH`a9^+MKhyZu_pL_6slT^VD_$^Yn~t4oXsZ;5Lb7nm8cN z@OhLvL0RlxY&c7c+}K5_*&QZbjE9-zj>Ih=SC(J4yCt`fLLZvDPIec%7Ce+|Twr68 zwi6kbLX5o|DHTw|6bnlNv?=4vFO}S)BDL+{LwWfqmI)U0AMu#T$0R=BqyVCX)Uu5L zLKZT%r;8PC-0StatBp=)cuQToaS6z=2ty!mV<|X2Tz=sVaMT+RsW*U9Z#qwq=oF0Z zDaa832cHVT7l;g@S!#TWL{15&O5Zv0Q(6iGFP5!-D@OVm7;ZZK3ehX2{9>$To|E7? zsQM$R&y!&$V)TH{M+LLh?{pX8Q&lbjyCqA;WKnin@-$Cy*O$t!y%LDCDhy#&NUlT3 z-;b8?-=Q-S=qxh*rzx(4j1hqdq`nB`tAG_$0q7#)m%=<1fZP&+sHpgiZx;bFtK1pcZ648&vK5!Y9>U z26n~ckMI8#rUd>?MIq@!ESp93OjBlr zm3)%hIhRZ@kFzjH4kM-%LyzZeG=rfp2_BfG!Xbn?(ItP6Ae`?CTM`CE6kzuQO;)T* zqv}1LoCsmXhwv`3!-@|fJPDkUzbpQOH@Odqm7^Gp!#hN-d+NbmrvNMemLWlU8yn*X zW3aZ}89#*W>EQ7>$&aQ!gwm${@(5ap89d18J1C`mfZvyyGL0wnDUV`fii*=L3f&H) z+?JF!tH#zjDK~D)6#Loo=M3^l#;4ePlJi5fmU9NrJ_&2Dg#{kOfHQdYOBQ8-FeYL# zJ9_AFLRxBjI<;iF1%e8JPWXU(K`<|D-7l#VvkTn}R3 zK=OxBLFLEt8IvsjO;Gh3>WejV*wKqoK45}(Lxg;oJGi0bvOS2wP$X#{R14qM_Ju~c zOX6QPb>Xx|dx_pS9>Fp1x5Ie+c?8gYhmY@?@)!B?m-1Jpe2;DlUp3{g8UHGi{S6=A z=i>*a{4fmF=O2gVC-S%a`adE0Dc?T}$B)?)Ue;<;62+2Q&xhUe+|jMh2-Bu@*g4j&yf6ANPZKN-?GftSk`}sF*}0YwThXO zZ_G932@@v``6rt)nX7HmjpzZ)NPAmOru zeqEgiXI8KkfvKtrP6WrE^@;9;n${tgdxK_-%bnXIinP=&#CvAzOl~0nYy%sNbIOHv z`z22$1@Pvl3Q?1`A&u@mP?99mdp-^AX~f4PDE|$#)DQG3x%EUBeWhp`ot5+8Yjf5d zmp%$I&LWL+3=7~*6RYydm)R{#`RcI(m|=M$f!;anlS`o!~KImGkv%O?f!MUEydu6mLdoT zX?!#(Fz|=NLpXlsq8JdSGqrZ5yI_*q68wF@)lW`o*?5OqOewfh1aRryNmeu8RVA4f zK*~}fQ#NKxncmxDlkx`40`NBU7nqk!V?AadH>-2{5w0E6i?xX;M}J|<=o^G8Mvy`p zqS>wF>2xbk3m`7jEP_t6bL7TyPKy$|Z*Hl^a>6)6|0G_39c2Q1O&P@StE$0V{GC6gA$ zjr0wwZ3^_dIc2Av$JsdN2>12&Vr06iOb(<=C)bNxsNXJcv0(E&WEmTbD=cH90qJAL z5wxZ!)2O$M2BUE-C)KtOTi3y~J*8VvI~%fKzmCT-0oA!qkoBpiX*5~JCS$W{G+V|t zW4mRv7@NVxx6=K&(aLY_H4JqaZD`$%HS|o6I}jjS3p8)9Z!x4Co#45lfWIz;dDG$8 zfWkce;@%BnIZC&6i*OSFvNkr?gD>+u)|>TcVj+IAv%Q_!0xes201P@{_avq3{)Jn2 zG;OwfHvljD*@{H;!X2$ns-4@Lkc$3)Jf#7ug*!+3wky9u$r6W$xs zhI^7}_qNo7l<1>3qg?)^TV(7Yim#OSK-%nBG01T z(RBlLu7DnG=a2!UlVst}*s&y#gScq|TqV82RN6XpoznVB<{*|$qunw(NDGtM>>)P0 zydBozHX4_&c&RzDE1iAx2dm4roWSVbS#x7%f4r~H5dMdZ*JJ65?7g1w! z?&l8Hi)xZ8F863jrpVfXs8Tad3nw%eS!HrXkh{uc3O{I%Hcej!vxY_h5Tb|IrS03v z1n#zs-3DwbUBg_nIScLq?&FVFVa1SbVz!JyMwo^37T8sl2i*l!uv z88OU?7Sz|eETh|qoAjNob#+t*hgn<3AtP?dJLH{~(PJbmELd)^BbG5}fE5liIAUCF!Ob1$S}fxjJ#$}3=m4Tbr+V%hNy=W5W=fvL5hkkS>CNP% zzb2Kyr8PmCbosP)VJwvhBlvP$U1u~W!}iH?OoNnp&seUnOasb#mt%3XyScFm-2CD# zj#@Bm9UCXF{ZUSPm84x$cBA23_Q4ql%J2&|zM=o{036B>^$&8*s<0G*(7Xxb2TPC3 zaros?+Zs!6(~kwy1=WMbEMQDm>LD((k7Ip{QRpE~y$UZ#4PBKc#7!?({M0nGt%*Fu zj%AS96B}w(Yd~!V73fm|ueFyAG4@WW8ZeY08v;UYL!gUw(w>JF)^SBunoB1M)=zFR zuBGa<&8R4kQq-eQo&wH? z5=Zh-FGyu|gbcau;xbof&5SjuU2`KKr^J*FkA0&Mzp7W@MF~DHc|JLp^roHyXNjxC zDGoTeNWm^~8Qv_BMnoZf!pO{89i9MjuWHmI1%-V$(vZiQdPs7Ip*G^QSIu7^id6EjqKdjm1hpDE1|uy3@@{2 zc@DxFqYHBmz#O!D5yQg;-3rw!D|7UFjw&uJMNTv{tuz4@)UtST<86|0l2c!OD$ zt1m9Aic4$7nm4}{msQ23wL)`TjEgs~FVd(p{6XSy1dZv6$* zO;qVNTri!k_li;BvNPCufqLsMm`>MQ&vZo%W0t6D9)+<&QPY^|REt%*O=G50H6vYW z?fp@u8n+3G5SyU@u?5?hW_TcMf&0h~`L%Q!JLOvA8R_y(lpf!Wa@hCRGU#6~NBxh; zEdih09_W_417DSQ2P@=#!FS0AgTIxB@~)PT<$Vc`?q|uT%)8`s<}c)_&^8$jeO|s8 z9xq=C56d5i|4P0VJ}<9C=F8V3ZStp)WAbN_kI6S8FUz+g|196mpCo^te+9hnIQyIY zjn0Mw;$M41u^)Q|Dte5AJ7riHlvY+(@s(Q}(>-z#vI>p~rHY8-HC9B^uEa4huCd*RlTn@mXVY`{LLw8umi zV275E1>W{^($ZSKs(2u#ZBGph$#*E_`heO-YOTqC0#N@9y_sJ_CG20I z8~Sfh=lpjUt`<1JEr1gIdlaq~C_s3L(limWR|k_doXm9{eKM_h(IFo%o({7gt!wf9@V=x zfIGk*wnxs(GU)Ew{84FtUOEh^%`1fm*IlHVR(|0W<(%xPHA~Is1>$ND#;A1GhDt+r zqIvvMi8j}Ut4hPq$lg)e2UoPE!BRM;*s+7WiDEz;;%t` zHRE;d1vUKMKPuM&IZxJLD8*ist)p__oODrtT=R&xLeX4|DbNR-itE?Pq{NsG^}!iZ zVU){uW2PK2X2~taY`M#rEAKGo;i}6DdDN(s$Bioays=PD8H?owV~KplSSr6XmKlY{ za$}0I!kA^Of?C&VoTS&{ymK8+GuPv&aU+fe>WmK~^qA3PeA3vYXcs1AFw>dY!O-Vb0%J_?OO^@Y)q%=;TB?=PLa-*NJO zC7IXQ;NKSIW5;am8#p#Jg3$8nL$XMss_ z$DIzup9jPmPO=USC%-gI{8dAOI2#iFs>}U?Q|^-}muzQHO3niTxR{SuwW$A}HUzb5q^D5EcF|cjg{zi-VylB>rw;GHt979qYRAjQF!%L zGD^b)M6?y$P1bC)aZS7})h~+)k1r)pD=2Y^e`Nr{P8ShRcN1|khXjO7f)4x4{--vG*o4!t!Eo^2D-g={vf~@+EZ(a%*Z>i?BatAj|^L6*|8Y=Usb2M*DEj`ZY4$pl_9=dTXWAgi829{1WO7qN(Q;D~Hanyo9FeNjNOV z@1#^+p=Q9>a<+aec+>l>3;ouOev6~u4x--%9}HZh+8$Ml20U7^MW*Sl>F_-*nvwGc{DzE1xN(v$MxKL2%k z%zkFA<^f;F-AS4U(7=^E-~bxt%;8SMS~DBgqRoMOU2I^kn++%i0Q{r15ynHY`witJSp6--=#@n%Vxm#k!I~>~Df>}iKgr!)1 ze;l(#5FBBd{24}lK+2&z&bfq`>2NS}z`@J`wFoy9Phh9(GQQ!I@qkmt=WT3Fm7vDf z?`;lDMqBY4#espYZ=xYScpW{0_Y`E=j3cB0ESUCS@;sSG-RdpI)^tBfmH!%9dkzya z|FZc>(nnHW?Q{7T`YFPtic6F~=zK7|hx1`r`q6yoEj+@Zi$2ch_v=xYSF6sITa$bhV)np9XK|H zavbdosj%tM5w(x#P1%j1d-<9UETB;gg}|NTL_^KqLUTV@nJUp-(>r z&Y}_9KFoWRyLD3d_OZF2d)VhJk?h~!LvL~J;c07CL#Mi{2mRfEk9~L~{i~Tnx<+_Q z#5A9#ES;}$X&W=HlsdW@`+9~xB1RL~HQl=>OgcgThI~`q@cAwIHXXWuCx<>Fu1djA zx!s}D`}=n+TInTSmMJtSRe2~3L8HAZK0u#Ju&7n@rux}~Whz;J5Ac>B9vK=gKM*hP zKUh8t$Kd7sA?ZpUP9@nc_>)&Y)No}Wa8!#VKad~Nk3F2nchP?B$CkhfkiO!f9QsrF znI%7G1OJZj<AfT~v@dT*_=NwX!!!am`WUa%rqtxFUFnZs@i1xLlsN zVat)~P=`OHqC9p8l6TEvGOHa=UL(OE0mbhqgr&f_D)dO9!RF1JfRO9fXbsI}k z%fXbIE9M^?QrI`+CT)E4ltsr}fv@HG_Q^*4fj8=Ej7IE5c~*5lH0^w44@#u! zCGkHfw^Th(lmF)-SQ1JY;DX?UbmGILEFr`l`SY;A#dm~X`4sY556bd0QqXw@8Xj;m z=EO&fa9UH8$}mdD(75<=0yL|>bn^Tsa8~N?tU4_dpQV#+xH&pU?`@Ox?wu{xPSbcA zSB3MoYRKto;BsprxYJ_jKQ4h3Vi}IORw!)nkZci-C+i{CqocR|NU3l6y5GUj{WgZs zYURL7JIx{vclZ`lXwMxkkAwoPsW zW_KVDJMcfyLoBC{m{n~&EtAnM|N62j_(JQfDq9NdwboRX1+H7Fu(#BOJ&4$&Meyrg zWdE#@h*%fyAV8Uv%syq$oj%{m^CweuqOrCXFsuXU>miHWfOcE~i}#I~CFa6@q($uvG-F93XOHsz|{DjM#De=SYJ z^sPmq5nYr`f`n$O7F@DVwR4&+E(+#7QJR9o3Jx~~GlovfGzDLcuTHtfuHQ%sxdEjM260->yS_qzO}e6E~U? zvw=DQpy<$EcfQ*>8zHfsv!$T8a?F++$|t@?;!+gJQA{UXlb+LF=6$aAT%}6VBRyod zN8{-b^6|1WUCs)-oP3sp8#|wq8AOe{sQs>@@;yaekIXl~-GXYJF$=l!katKIRj$|s zMR3Pk0`}AoVnR3z1fA*S_sqx%H%U~ zwon6M=z54Wo8?w8gWJFoZU@S41!3L=of+3&_t*E_&??}1SCy$a3ripcvZ$UO3p zYPLdn9@1)rn<%v#=-mm_&qOzACS8D%l1%ym2lWrws0Y@dAW!A^WGCpHWYX+}l@d~- zQ*;6@B}OM)WfZ+8!QavAlIoB1=!Ae`+~D1~gs)XZi=>aif>Mvxe??iG1ov?!Vjp2L zY9aPUMboy^S0vfw6j*I{5PIZ6nV1aO*vGBUI4b@68JXQ#4c}HuAz#z2)}(kCbMr@l zj*p_j9|KH}g2&)WO#s0etOn@O$SK8Kl0?ov2RZv}=O!Rvxu(x!N`W6!6%IZ1#o9;R?e}k(#(8RA)ilmiobju09HnO}?>z5JRr>ej zeD9g7fF3(LW-32ss!wJ?jn>I{5}-eYW7ty=TR)9M2%J)5b#V}Jy*Q^zqNq%^DwZ7p z&0ec0j+4Mq2SrEi>B@p9&lHNtCUr~_F-1fSrtq7feX>o>VKl3_IXi8TCP*7u$MG9R z8!rRIKLHJV4UKsPwDG5C=$|-L^hSRH`%_Ji+e(ChV@^_%QLjC$xf+i ztvS0g;9nUm3G!;*s+yAE(%L+tvNTW47{$=$NCu~pIo%n=T=Cs3gHZFQe*tX&iZ$WC zVZHe8z{9^{)%afyKBi-6l8xu#@MR??i4n|*g;+1&3dSG6!f+9IR)=D#TOE9C#UwHU z_(R}q-T|p&l319k*OpKu(EJ)4>!^VR5RSuwDB|`c5^!`!1d1H-uTN`!c}DX?f%PXP ztlIn|aO+^@lP*@*T1D=5b|r5&E`gKb$AGl}rqx=rhBM`yJ!)S41#wm5<>f1}ZV)Js z`2gQ!067QBWAh9@&a7dNW#mCLW6BC6B6UW->@_T$G3Lu5uv;>*n=r~J16LZ4+}mh8 zhOkc2c+7{QfX3rq^fuF(Sg+4k=>nLTaa*jy@^-95n2t-bBM!cg*!aeEwy^AN2Hn+R zzeLPyce!MRI%b8vo>|6SPCivQhzj$XZ$OR;v!udbW}!lTI$Z*#hKbMwm;{VY1V&383Y=wQBoA~q)5cs8Gz>N7 zN&r}r0@pYgSz}{_ml1d=P{$-Ga0>5dDoNr2%Z?Lp6C=S!3&?p1mxNI2Uj<;y; zX_&(X)@Nm3O|E4FmTw_Hs4-F%g~Gu6Jyq^?UIli1d#a+f{J~yH!0YdVljonSir%79 zW8a+G(I}o~OqYl;1DGn88e^s`F=ok1V~(sh=1H3|A8G;>pt}XqZB*ihsVap*y~bGX ztTDC$TY8r~U}GzS3HNR+?>Jp{gJY3?k_)gJtWoO05%mIWq6B_Vm63OWU|L_NcfNtV z-(t_s8|L3*W2yQR7!nIjNd+?~*R{4@tpOCJ~bG*2yrltDq2I%~h1g1SE zZPg`#<>Y74a%eqOm0;mJAC8}wmIR-Zg;?7b!M3Mp52;w9-PiS+V$X~?KYVw)}*V%#lOjMypH=V$HZlTC*WoC>s= z>*N0omz7sHTOFV~vV5n|9h( zm|`Q8`%{^JJ|7GCXtS_^E1`F7Jm5gq!V(2D!w%l8ym;(Wv{h1T^77 z13BrHL^A*f=HyAzxYS{cqEj>_G1S~QG#taFVV+tvjimL0O*l$=9|mXiVcAy*eq4&l zBnmD(9_-8qN1#47T79G4>W!x3_fqwZWd*T-?|j5f!}86i*z-Y5q};pWIuY78*rNV& z#Ati>g_vX(VUZ)Eg_a>qDzgzn4pkr4n)qT(J}elM_l7XlTqaV#v2G2;JXQC6yY3@s z1t%n2VN%VttxwOC<%rUiFGuzjs`5&m(5XD^R32tGqd@vn6vUN)xJs(=vJ@HP0pr+!P$lH1le=g=-W)x9UJ-S<@dSDw;a^+lC#+; zC52l4Myg7V^K}>w3~lhP4J%$PSX6-l!oNx+$f@+>x-mR$nb<@Z<4Ys#Lgm{Y{%|MA zDn3@@0*(j=CR5bH8#&)=Lo$W$bs?F`_xg~O@x37=)A)uJ)J81y>^sce_nN1v{#~@Q zp`h)ZDA3Z}$T_j2=_;5B1zMVTNr&y|Cv{iH{4unMTu9(SE>vqq=zDQNwp%-#>Cl@g zr-oz!QsMbzi<+~uC#iMU=pyVoer;>TG7|3No-tahy>vft#96(7_j^LQd3 zmr7>q?Z9RTWQFaA)SH5X_lt5*#k)NkEeMNoji7pzaLrN5#}vK#z*iZ+ra@41$+vur z;m^mAKi~?G2^fLqMqd2^7yEbvVD$#f>OIwd19BAx@aifM;0QT+6-YVQ13U~OGz)M3 zOTP+)yjag86Oo?xcK#TKN!5RUqUee(_|LS+4@gp54UU3D_}1 zv4|S1Di-3YA$bBV;lH=2Yd!vut3Xr%MIm)BNHIP6T;O7mPTUS&oVp%_pijqYi-tP| z0GD^J%*S&%772AApH?g&{B{#KJF+QDkXG--aq*B$OxMESx$Pexp9H}i|H_dtH@{8e zvhBP{c#TOm3N_VrT#exxq$09Sa!h{KL2$}te z1c;HZvRt{VxnWxeGKZ+mp^ItRf%~H%#0aYtTkCcMy>M}-gY@tif>20`)vwJhb(@>^ zw>Mq6tEsgCg!WI+z^*9vwy=ceqFioThmEO)g-sKRyI(*+nkM$lM0L7@I{ygL!2*T9RO z1r~AVI1v&(Di3x+>8$TimhBxU1tvLcJ0-Nh(GU(FOm>>~9P052>FE^<+zc6_H>CAT zHZ?KImXUVMDa55wsW;{T#^vlb(`l>y>*0arSkY|7_8Zfx+p}&#^WoT`c)NWqcG|VF z_ENw#gD2%am`sYmcDO?=B^tq?$+WqqB1$#?{XCgey6`qao z99MWQ!t-3=`3P6I!V3^apE&iiKA_wQS0fCxI^l(Y5q=xhj%5iTb~}($8)_=sCHJ%? zm~SQ`Npflng2AsqD1=P}w>&Ih*q)QNpAlbYkssG(otEI!lBa@Zv0#ot5cgH#*GMvm z8>8P&;^{>N!K1e-??$t_sKmjkW z37e2Uschf$?AP!0i|}tIGNex`E&WFmaqW;cZC06WO1UXBA(Mp&tb@0_+gGSlHxV@C zC@Bw_ICjB^Nc459WYB4@XwEV6)wM|ZC@^*7`%I{)rbFc!LEsd#s4sb;L>8SCcl5Wb z8|4j|odpPQxeFk0w)E}r9sP6`tTJuAa81OB+m-p0EKSf85?~KAvF)yWYPNPbDOqeo z&q!Yq(E+3eh=D;|eUTlh_8nA|!iMQ<)5bq9qNCeM&AK6KA9lJ~H*;2|jYAr0P$IjD zl%cbDICRu0FFvT&;<%6_y?b0qJbX3DwX`Q+2P>qUv7#@KLddjfvLLA$Ue!pPC$buZVs zD2@UFNm+8twvaBx6~ ziXlOLGz<=VkeG-GVe{sVQ5Rz27Y+m}Yisg%=h5Wvj#0Ba(keMM-XwKSO7<-8CCq1E z|K9>C+hF`hsYK&z5Q975-L3hMp5a#g4G`y8jdN!F49H?_>1N=)oQ+1a3o^zz+ z2?l0>%Ubkj9!+}Nfhp0%wIoAWbmrKLRb~-#xmKVZJ%jzn)G-t1yj9Td&+5~YT()nbPcStXP@Yz@P;_6cVaWYD$&ZOsi zFI^g;2=MVJPg@?tjn6d8e>8%?C-{H|fLeK+kCVy=Kv+HRhmf4pP$UrVe1Fs=T>^1LbU%SY3{$Omp>tCbf_dC{b~|8)71 zDd!?^TJlFGjr_;Sm-+iw_<*f{t+Ms!`)e%dQJD6H1>Pqj*gyOkzrMk*Z!*ERSopUY z{Bx5R?N#yaw<`Gy=KD(~{VRTbkB?Vz;~sc$!Z|XWlCg44q7FmB8H0boHk&hB>V&c^ z%VN}3VL|&amhv#1>fjK<7}wX6xo-Qq^n0URgVV)5#sjRzICj?Xg3e2yC=S2WyNsNR ze#UTQzQ=G}&+mJzyDXwMkN!kK3U>Yfwyx%bYtND58ZPv%}yWl)h*T&n$m zDD4hYmTn_QFUeFcq}!Uari_Y`i@Mw`^3gWe&4C@}h~_5U3?vnFJgx>(XASJYE&%3s z+o0^~ctatI$*But-xRLwLY)kmvNDY>T)8Ft!F<4FFi3$M6>UiKrK2MQPr3$9N%{_` zNjXx0(}72T+n`7OfXm{T*I8PkC3+X{47Ds*aB)elQ;m5$oDvAycO+!F2F$gwL$^eD z7g%zCo9kjQR-6PiY0`Ru3UqSqc3}*TbTP2nMWw81FHp5c5vu-U?3I9I_g*Opy+^Am zeQHb&7%WfHMFkc1+9o#$c4jQ+oZiw=7#m~XdYS5RCqu^Nlz-TTX%mJ<4jqaQc`nWd zSeq|n@TiXSj#*HX@9;rPN!o=$X(qpMaAUhSP}dR~UD%XlrcI7U-@)s_gYf4&m;h4M zUC`>7Ll=f9c$!G-W%HmC*q~|okc)KnB$F1X5G2L6C~T3Q`Mk}@WIZ<6Z7Wr~u98j* z_M>pn;JUfV;nEf~5B|oI@5>J?m|4J|K=zByE!bZCBw2!Y@okFoTkzA|o)U1UV@p2{ zHp-KWpoQg%G|IcUC@JsjA1*)GKhoDtzX(5*pVRHZFHAZ!_?3K=hF5s}z3s0+xhk;W z&i2<9wC5q3%TyM#+ziQrEyTZpz*L-qTMNHT`FFPWKXM5SGy}4*n+6(hmD_lMA6zU9 zvdT<}0VTMa#aYn&hZTDcmkp-;mnFZE-&!zg`)><&xcFKxzq6ore5D1~w&$@4H;l2) zSuDe6_=zR7|HnqaGJ>i!BhNBS1G2j@ZpWek1?-RmOJBG%H_Il^Z4S{p zTd$U;#wOT?f6SYXT=;K4J~SLZj0|yJkZCV9z>9Yb!v6}^BJtQ^6zBMY$;R#7K? z%JGRyN7M_cv&?ZsI<6XlmI2n^+Xvp~z6|WLZv85RxVPDT;r$qE)Qkm+&8a}yD5XvF zMO${wsidJFyPCm~E_DsmGLN$x-Cu6x48{-k!k^6|S0>AzLR&BAZm+aM@|@neI8(c^i-~4g&Y1wWyyjS7 zP?Dk@CUI$yD-{KoZ=hqPi8xnD^4bf;!}52IkJ+Ch}P;Ulb~afI+Cl$aTpa!8({08Gi&2)sBE4R-^)_0P2U1fix1&Z z3F1k0Kc8&GQ~OZY0+T321`{DNX+k_4wNDJdxi@WDAm!T({|^EYL^BK-bR}z%gyI~t zvdS10r9h}l)G)45FeY7YXc#GU*#^63>H)W8Dae4vWJW=aV-*C*FwZK;;IB3-WM- z_H{|qmFCjI*Fn71Nfa9TRNr-F{Jwm zG}0RUtKB4z> zt278_4`m3H2A`}AW?(Sj!5~AEltI>~U=PpXvKl(vuuNAd)AqR^L!%zSFOOlsejKW1 zpTODpCvh%+5-C3gp7I%3UOj;(KLSStpLbAK4k2p-hTcJ>&`1oT1NJE-Qn&1ogTzA) z5)U~@yv{}<5w$=9$_BR^7FboL+yL8737XPt3Y@{=|86X&fWFtH3WHQ#!)B6bKa2ri7+1?h;T)1m*o zOvK&-3Vtih7D>t2pQ!ikFdb(2PWyeA{pQUoq;NX4$9);--C_(}npUpT7cN$U_g3)+ z98fZEhwBXBt^(S4s#|&%;yy)qsfMR>UP!YD*LaYjF0Q2oc4?4TH=JD=@~;e+giAt? z$%jhKlJL^nNNL1)UY7WzHor9g>`KeOvY@1(#ClAQl;)QdEUhgpEo3Q6PsrY?(n1WP zWxDE7S$+cS++SJ1veB3T`|%PushB8t8k6KhMkzdN zPJt)Jsq&IBO}=H6%irMlpCH}e8S~}ejS4w$EHENQrBPy388eJ(W1dlim<7fnV?ORw zSgMGGw7YN4Dntth4Iq|={3dUEl% zi*+7Z;7h?_+;PRer+|4+!FCuB%^*X`{xkSKJ%1wE>r}oyJpVO`oJX}|ln3+N*Tsa% zE+*N-#_ZopkQ3}ffW;>dV*o+qfNul7UKsO;_f+og`Q78X^F{bK7YWjL25C1Kdk^R6 zA$yjfc?UysC?q|mB*qqPF%8sU+Zvl&H}9|CwP{n+ z&ZfrwN<}uuS#Y{u?%mksIVCu{x%(TMTbp*&Z42SD5SiJ~*0N(~Q+s>c&i$=*(E81i z?452vQonEA(X=@P^K_Y>y9}M?-4L^6fn!H2@(fb>l>qByXdhE{a&|QANA#*)xJDM7{ zw6`>OY}w!5u(PSDb$>(Mj*eYBk@N$GRHuW_(sSqdAk_Rkg*Udf*SB4jQSt*=4SLeG zY~2B*e<&Sd;29DgCuMzeM@!ufO=}_fFxbGJME7uyQjZ>67~q!IUWOudcuPESs7K%W z9BPDLS6!z?JtZYHQ6MbLv3tjlZjE*=Wubm{wwuW>T`j(NN8-kKZ{5%U1T|1WPxLEb za*2}_FzFn}t2n4Y+k`^9y=v_jiMaay{@!@3PidgT00AoOy^!oEpdwd}#Cj9M$5r4W z(Y2|!KQ{b^sk6__=%X}li*CuK(>@_BsV!8;zGEzbE98OQiNjanNbwkn6}_I4yx7Y6 z7MAHJEbJm+zQM$uSP=IpilYSHj)Vj#*iF)-r!U%hbSH@~Q-;j~AGEOTM7ly6n-4-+ zCn>dci&BRnvpmGq4_VkLA$4>hjhHU18{eU7Iq0DmPb+PLu&@Io+>nxLlHkb#BFrP2 zC_K$jy-xqeNRV+>k=73v5E=#(RE`H`yN)KhQC&l#FFp{v0TI^^#9=RB?da(r?k}$? z=d6I(fxbiK5FaYu6Bz7`BM`3ZLm}Fmb6_xb3{ipBw$>)Rz-k!+Z{=%vsEa3A9r3Q7 zA*e6*l(%;c#^ZhE4Y2`zSQOaXQV%&>fD8|>=I!;cNcjUCH(a!5i_7=WfSG@$vi~Z> zQgt>q+?YYsvSuE_^IhQzgcrENl?Ye4!dik)2ng&PrWa)mb`yxA2d{99b% zW`r@OQU7*@uNH(coIPO-WltDG)(KyU^ca**xE)~(HYdCbVGJoJya!>7C{Gvz$O-So ziqF7Ur$g0i08tk4?6rv20>%>Mv4(4H8nsi{M2m?7PUf#69sk{@tRDPDfno`smb|CM zR1u+~a55sIBJyb{p;Hv7h{9qScUqz_lu*GUh~cC-a9Soj%?m1FSTV7f-uI9IS3DwE zS}apkaHeq5@cEN@uUE+?%R$>-$X^Zr0BQi-=M<9r%O&>MrR>Ng8 zJtx4lFDUC7vw=bQ-@*RIlsd-LQ_rv=h`RXCmyfL%Bp{opHK(2M6nmWTK({4qPR#|Sh*Xc3%zhUASjVY-iDzXJzt%@Tue@nqBM*Z zldf{o;J$cJtVEw1{i(xKcgu3GfC^Le&P9xJ^P)TvVBw%vH7*omcch{KYyI)zxY8GJ z)k34ZQWgLO1Dtx$Ii3pc1W_dOB@Nm<+2DqY*>>S(8w=85EUYTiM<&t3Ip3A9t{XFO ze2_V@x?0Q{GCH6I*J%tMZ=l6NdUod9Ysx;0(u^!iBF~seBEzLEkW_piWGPb_tmrsa zupSuu@Y~d*1nENY-Zy5?qWe0qzqI2nq^ zxk{?o%3QWWcLvhZ^Qn8TCZ{x{Tz#N^at_a3Kd`PF5(B!ftypbnI|V4)lPbUpBP15Q zIW=j+n`sDTnZKsniO8{M;5#{gm7`D|FVa^Dx)0w=!EZUYg)04(XP-+RCZ_VYxblZE zsk3_R&qU{klN!jVM7G%nldZV^IoNqh@=gncgP}ckFzgOiPQ~wMuPfuT8^=^kcSiC% zAzZXhOM#Z|5(I59*p3Oe1M|o(%w)SUf9+8qX~#U)r|ex0wHvJEzs^z;+L97n5t zKUQD1xJ<=VBZf_=K^mgq)0ldEc(>V2*k0vrf*njYp>l#0J}*&}%5yWg?kUpn7vl!Ue(qrA zE17R6e{N^$j_7zicd>}w(IPzeL}%i8RdflSs2K^w)UyN9W)<5M74nk>~+D2cv^{4n=Rrb66p8L?Q5KbRuGoMJM5T zV{|f}$D^fq-V~jJ=Ubw4@w_=Y56@eo^YMIZv;xn!MHh5*j+a}bm3-Y6UC!6-(G`5% z5najGozYc%-4$KU*W06O__{k<%hx-i4Sc;b+Q`?tqD_3gJGzOl_e3}Ibx(8)U+;}J z^L1}@D_`%6w)1sgw1cnvqr3Qee{?rrABgVZ>x0p&_JztMRhxmFlI?UH&(Gk8r9zDv}C!)vr`egJ* zz8;Sr=j&wjCcZuuy@Rh$NAKk8Gts;FdLsIEzCIhho3GDB-@(`CqwnPF3(i zkYExHFGKcsJi33*N)G$Adfg_N(ASe0w6elW$K3ck%71 zU>o0_4({gLGr>K4dp5Y2Z_foE=G(7>kMr#}!C&$1dFEyz`fcz@zP%89if_LQKFzlm zgU|5orQoxCdpY7Dc`(yB3zP%lMk8ghpzR$OJf*?%(oAMpYZL&;Gg;SQSdK( z`#AUo-#!U`$+tfT|G~Gv1pmpmzXt!sw@-ur=G$k%|M2be;8%S6TkvbX{XO^%-@ah> z5TY*)zW&3**MFLP{g-n8+hX^B)b%SbyT4ZMZiWG(=ny9XfswC5 z5ucF!LKy0Wsa|+EJ>gXu5EjQ4K2A^gnL~v{fLT;Xq%e0137eT$NTf2y3W+Yvra~f3 zWkUp+U4=x5c~nTGGb0O$uFSVWB7>P#NMtg%3W+RcQ6bTd`B+GFXC@UA+04U2q6agv zkm$)g3rEB=(+Y{+%zr|n4>PrpIE=YeNQ9Y*g+vbXuaM}=j4dSkG4BeA{>;2WVgPfv zkQm5}D3o0zEhnwX^cnwYHknmAnXHIb|MnwX;anm9u7H8EB3 zHF2ckYvL%y*Tgi%*F>J;Yht?MYa(CqH8Df+HBq4WnkZC!P0Un$P0Uh!O%y4@6K5&*CN?VeCN?SdCN?YfCeBvuO`N0Hn>bgoH*ua~Z{mE#-oyop zy@?AIdlMHa_9iY?>`h#v*qgXiu{Uvdd1Mh4T_r|QncSu4`&WupF2 z2X3uvNmx-+AcI2!nASi~D+@=<@Nv*1`7+8!X>3D1n&Ab<_^2!2#{w*s@8h6ohc*9- z%DnQ*f*Dvv(urf$KOP!uct^`zNpgk>wy&_Lw5Vd1N-@8OO+^y+s-(@m2W!adWO|)oIlGUV+zrm|QlWoG{f^$iubQ?N0 zxMWZ|w%*i2Ry6wkY)o!WV;tjjVgN#9Ro#(W1vH4d z;1HN($B>NsqHFVCBG6zDJkwhRV-a4j%J3pdhd_hw2)z8D*Je_nuFX?w$<>G}Cy^9r zFnM&~7IEIy0zKm)`>;2~J3O4s@hA!Z<#iHhFgbwWL)y$Ai)u(pbG-3bX5sZC-W9+J zAn@^hFqY{#1eUQd^$$+#70(GLU3Gkqm!T-izaa?NiMEbsfkw9gERpWz7c5)@w`1z#jz4CF$Zmh^CzzPF?oC6ksV0i>`<`U7;l44~O$iY0BIObt+tcpN#9 zUHr6Ardcu=pk*?|X`~z2Zdm3y^LQwX$i+FbB>F&}p(S$LPnd{-6Lk0PQLV2Izh{h@TC%Hhd{D$et%hu?r*YW;xV=IvpxuvmC=g#|Gp$Ii5c|fm=cG3QJD($w>ja zT0HKPlYJ6n_d@YK8bs#$>iDvRFH3!L8E32BCzo4tg-URyuAtB`5avS%rKts2P|kyi z$&*@)s;sVQMO#*=UAO-6@i3zMxt^Uw{M@dNJ6-83Rn(tSr*yIyfelAZcJeFj691h1|@TUEse?m%j3S5}=!yrxqqwvoaX>}m(4Ly<4Jl%`=k z2PChGc!`Kh%}@YnFkKLASEAL zoGxA`q96yb4^@gk3T8|h%f-YikP8%TN)1#TV9~I01JC$0o1x zcI_UEGX+DgE@&uW_&%Dyy;UAB0>CslRMV{<7-E#<~VH zL{Q@)a%U)NoL97xMn$*swbF9DdKU>(XZ3qXuadfg)>Rx<;KboD+kuyR&6vkIBX+I^rZ)>!k@Xm-{fm8-da z$&yh#{3$Mo!epZ8u5T40wfuavKMX!$g4(XXpicD%5hNL;izNVwY@xNZy?4CXsFQKt z9CnUx%0$QrnW5GlWlt1fa8V%#^T7^$E`#dpj3x`pHs*yOPPE(V<~lH)=2i#(5AnO` zr-gt!YEh_w_FBdljFOUna1pDMs6&Aa=#HRq8|u*bIQ<10r;6Uu@f|AS{<1VO9{F9P z#+XhvaWo3*J*uf+g{NX1J|g$c8tIo0APiyilx11m#{FKU(UJOxtOW&HD?x&)FvjB5ODwq#0;MHShOCI5 z?aCku7#Lg>tN0YneH?TRwvtvz9Wh4ZK%)9;S8II*CUA{)>IMERS{4cFhAvyht@i_X zkZ#PPc-0D2z{E&J&Ta-j0jm_~OJ=PacNlL(0gq(p+EF8R5fntKwJMRn1!7=C%wRE6 zxgl&?@{~xj1*_^+vPvYW(+I1A*MS;U=SRH+(dlbe=yoh>tYv^Dxe$Ys`?*t3av_Es zKZ_jBSG%yF!>~zZjt*3+N-S25p@!;a#&Oa3v6^qoR?uGRVyN4+%&u!zdf_NG>QJuX z&H#*TII9sRvvzHThJmT8bthosxx(J%^el0eC9dH~pUgy?R4{9*V|f4z7)jSsghwo} z<*D?Q4TGJpB04Ye(vXb|2}`cF`PA7Y7wsY>4%&ns$Zs9Rdykk`BvsxpmW zlQqz$)`LSVkY;KQ8U%D@Vj@F$jEIHTAW1O{2hRY`H~VZp*EFxsET_%NuNYBfDsnT-9utNz7an`UxUs4Q9)#IYqA(PVGbY^B z93BgN7&O%msHSNq#Z(>*GMR&nnV1{ybo<4m9gJ-UHt2s`f`@FmlS{lyUSr8NTke*7 zEVi5J;|=8`0})T27-2C^CTZm z6c+;fj8=G5>fv;rjZ@K&7$V5$K!GU0uWk7o?!kw}dWO?|FeJCe08Mc?j_#M?WlFed z2`__q8m?|YU#&eEy#>w0V?}FSV~#DKmzUV`w~SBIC$R|el8h~1;GXb1`3z^~MO(fk zUk2NZWkz>XTU3Z%w*0-AV2iU^rX4wt<$>mKeM`8$FdP4 zB0QoxyuuMU5cdyE_oEU^oV?MZLWM0~LxZFAuiNqs@HF|Re9M-9ly5WY&$rw;-nGRU zTk`IRld=6ByPrF_e-gY5cFe$O<6z9SE8fSi?AkzZ6~%R>ghnrl&3L&y!b zAq~SeOtHW+JhtIAEWY?`!;iitdZIFn0AEr#l+CVGj@gBYT3Nr?MjA6}#{_0jtdq`x z48Lks6HD-r#q{F;2XOcy_iAQ@f=%^0gcEkt74Zmt7&LzaZzPualxX3 z(iug0rI6{gC>$3GQyUwTI#;Z2&WcogFrMAnElNU*$g4aH*F~GnD}PGR0zfXrTwEap z|1Sb`b%cfORcsy=;~;eTTXEP-WfKNJNW{r@Ns-MTH$F=MjQSx%RLg)M-Ki7KAi%lK zaf%#q*Xc9y;(@Z_QJoU3R$VrT^**Q8EfV>W| zz@2WyR20M$AJ;SZXr*=DA^O&g_E1?#2h=G=9Uo%?n$?S5#dS+S0Y0@rP}u}M^nh}f zv&}DNd&)DtskOBUsB=DWC}+;*U4_bFL}aYcVgAeixF3Xd#dRp+q85Kx-M<$!?p>9S zPK3417(g9PX8EDl!#iIyoZG-!J+n}YiIHO&+Yx+fMf1~r$er4n8sR5rnV2haHNqg%S_NG!bT^-UEug%@;q0aW=kJ#i4_Rjc)N zwfa?*n<;M;o;p>Fb3AjdsKZ3r$3lf(QSDN_W+}&tz6&gFXsTI}*8mhyo^WH0m18-l z)@bCpgi47H38UJ#PqapW)EgTancJi*uT6Z6)wV*X%5(HV`R#f`(^FL-kTlw{>k(e!g1q z@6^aTMddjgxhbz}fQ`x;EbQ@QJ~l8g*0+0_kFnS>Z~5^75rh^wcOqgaV;Na^=&YoS zZeQU^Sw(*pUadI$Yk0*~A4TRCl3kJQCSL=li|ltbW&NYc`~H*Jv}olMA5kSZIW?ouY;Q?pR>J zn4C4Y>cum^n(DWrfZ?co{#5vyimvEiBlzU(hxFZEvs%R zMQ=d0HmX|(DmT2)7uiGPwaSXha=n<&tV3n5Ir3RPMqWQ&@ zF09*ViV72$UNkc*Yx&dysuDaLRMn6!r&{qYTcT5U=?|4u^J=oOvU(l&SoC4YQHmLW zQYgtmH7@H+Hfl#B!CB7B^FIjM*~#S!)wqyJs_OThEv92 ztz>N>A8x*Rc&FziGl2smLGeU*XWILx?0Tk!Fb?iose# zY}<6TDnPnZfK#Dg#6=sGya0S^ddg5h^Q8$wHi53CGtK#xD!&>hZlNo`$3aW$c<4u+ zfc!3U<#SXtpQCtT71?lSDH9-6s!rt#fJMXcB;rQcPX&iPMJcGhkaZLF*+=QH0qcso z%}8hXL_E&|_TO&m{^&l+t{RG3?a@X(hwY(W`=~bzlN9{j4#3|r5LZ}XgyjGuEWK|< zm9>NVRaE(gRlxRX2ldAd$L|fm4QICw#Lb`S-9v-mfIiw_+)^?v?YDPeEeh69nLh3B zvV(@gpJwhMPAsTk$$~I8A5oQMhIi9Qh9cVy6fyvfg?G`I>_C>ei^gULGSTkivIE90 z8n3Ps@W{K1CgNu8qDi>9cO8qk_TxzT6aJqMd02s_a(E0zw(w*1Al4 z!)2A5YygOTk_|KX+$t*=%m$oUcFJy=vWt$`MtPaOpzmhNg=&9%8X=vA6a6zCJ?KL? zcLH3q3^g`&o>RwW?RHRrrr1Wbw?5i%HCk>B%<9(B3$%e=p)-LQcow}6bhkeP>+Nr} z87OjRV|C#i^rLe{0O(0Ux&ZBdp~#_NwMVkwG%MN~nTi3NZbs-r8!O59uMDseub zc&?-^ilR1AD!|Hmif*Di));W?qR0ktS&BHGvYclNa1N+?#*`GHJh*xawvOXxSWjls zhYO3!pkH={ksbgC;L3rE;lyqfec?jSgbVA=py(5HdO!)=u3 z5PGK9yom;6dObJMVebAg9ljH9!(#wDd;)lLPXedyX<*ttE6VBjVm`eh7SXF> z8T~;t(QD##dR<(E(|@i6!rqPamLlp^VECYiD6!rJoW1UfSnrAw>s?V|y-N(kTPD`K zfWxPW^)8_Cja0;Xml&mpmEVqrD}WBvRg4ki@QvFrfQ=Uu&~cc6E6AJPW6+R8w3kva zSP0X^$~pGV#6&=K;_9P2@ZS=X@E=18PH{~PWhI7+=j@Zk;o$W=TGpe#L9LH^41bJr zw*b@=Y^7%#EjU2g!w*m{I2Dg8nyYkCA>T(I`~YL_N2-FtI(DRjfVr28qOmSu4>3jH zpi@;Ks3r0sJyif7T7C8ahI!V0D~^3{VF|L3y@kf&yV<29cG7XV<~};UDjNgEz&1MJ z04-{x6WKR7>f24#4{#X6U{^Gw+WPoY=oxm}(*iDVO-fS-{$eUEiY=oRTFV&R)WXK35sD}A)pMcgmNu<$QaQ(9cjKxrhaujA94O2XufnGL7t7K!eZ8wUU z;MpvsY!tJ=A#DbV_#5R7o5fht%iUK$_+hK$BKR>J^Gb&F&Df@qW^fEJ|e_C@Jn4D z^acEbIP3b}PHMhCS{!8?fX*J1htr}TNpn78^45~mH^WX1+x2M z$nG(UR#@-oiUD-LE~0=kL52JcZBCG-zY*a#nXc9_SuMc%*lZ9he~dXDJ|^#Q`f4lp z)!4Umg_Yu4W#~P}y1tVc|D6oicZ4_&;*JuT#d8o#0Fz5E#%gIH_gC<^t<=vgC#=Tk z50;ysz6Oqntl-&7-Lt(rX>A*=+u~&LWX@u?7wi$hx!$UsbV~ZE8+Oqt`{=Z)q1o0> zIz6;fIPjm||B<$$265{=bSDVQsvz^UQ{w8bI-xK=|;ele9nMw}~_ zfFmrU)uNtG70bb0RzRL?fMWPcx=u9G?HF?RidA$#oP?9KS^#X@s_Gb0Bv)FLUS5n6 z<%^2bFyXx@P5^)5QR_vq2ref67X@}H!8lUs3DsAwXP@PwW zlN`?nz;VxPvv(U^kZnE%0Uf;Q!flieWN&zV%A4&0nem{TiMb%**I(T=v+AoPg-u{E zXQNurLA9PoJs@WH#eE3wW5h+Ons@}`FGofzqMCypcvKXT)#TDh|Gx@y~bC#p#!<2OZx`{%j9~#Y>GNHGOlf zUdF`jQAF%vBF1Gyu2<2e6_PxU@(<4j7W3s*69e)lN(Tx1jm&@|{cVRnR`hPwO-kX2 zn-tOGgJ+vrR#r-HrukF={sZb?(>xqg=VhDdXIBKOchMCbJAR}G zi+Y%D68mYpc!c(eM?u2}Af!GSP%o6 z(;)`99AbcL1^O6>0WR(z1`L(HcgE?n&>v4iukZl6`E=2YzTgGa@>iY&SlRw_(IKH5%m94>o+@*IkJg9f=MW-u8CsR-0D`4F8>wjvkI z)FEF)35JOTd2}d{rj8Hm04wmSRL^1)O{7$-Q2i<%pZFPo4mY^@o^r!~(sRoRU ze;acBT=$a#*?!29nI1htatj79CaMuKIxzOi3SeOLa`MdQFv{`>X(f!1*%ot)UYk2) z7dU^!nCQ#&^}3b9Lwl;P_G;i9-ohE^)o|iObCVB?M(@N%*#!j4OdtZ9(x#A1#UUJB zXf!&?;W9|mWQYo7I+e?=FaXM+6J;i?msv11>PDBy?sT2ZraNU1dO-G~hh%TsFZ2sMGF^$t&gS+sug`S4gH>5(T{b`<*F6=c#S#E2)W+r_g$#Xk(haX^oTdw$dPWX&>fWk%1ghT6AuF!w zf*ee?9D?c|iUUxFp}L3DQF3H-dP=M)|W#i}S0g&GZ=qCU=a0mr(3 zM@OMh7+hJqO4BlCNm2=e%oJL3dmHMNnGC?F?O z7nw`lGKN z7-#Q|A7_9S6JjzPe9wSJ@6q{3&o3g{;zZJL;7iOmQA)kIx5xfcrrn5&3WvFC7q z)zI``q(88m9^6GcwD+O(o$&H%$PO%_Z$zM72-KEAyLZ#xUGy+sJlsb6hUq)6GhkVL ze+kayc|>6S)rRAjf)3C^oW{Y)Ka~C`(#ti6vBD$`<%k4$ z42ubms}KGPFMbsX0mnx8m*R^oqUmqqcqO>hN0;S#oxu`I7rYGdgc>ZJF%l@W*<6oGIsLh8&o31R#|(-` zUhsMTJSPc1CyCp}ZQp<4RPRISzgE?I7S||#px})$vOOEi6(rYDP@W8H-P34*Je?-W z^)ywU0h8Xdz(zMwquflb@*J?zbLm2P9$h6bpl$L(n0Q`9yX7VHs=So`A}^;ePpCa)JWY6 zzoBEkrm}B9zOf-#vPz^T=#x6S`3(APHUFk} zIh+M|-VNfjRQ3n>1eN_M7=kgsWXi&3j6+#1>{67qIX4CSTfbAAa>ICy_~O0vqNM1a zmwpzVFkp*>*PuDzrP$*O2HCekN8yJr<l^Zb)c zo!|XDZFjPE2D@}m%eDNIe2zTe&V6rp?z4RSU5qi4eA^EFDa*{l7S*eh&;iu&55KIfSeQ{-BZP;ewqr0}tJ$&F@lVQxyMD%qQyzqj_#AMqb@jmqxZ{OnK8 zGyk?jj}OcA>Ca~R<38`?BJSpVHNx49KgBp3_yd^M@Ad7K;uvAz$xS=y9qg0n>o=*#!^()%DHJo_N(`p~I2JbXRod?V((HRilRJ3$~=hMnX^6x8hgTwk^? z${OKv2-u(PM_Xo@XiUhNa85F_yoh(@R!Y@=?=}i$VWNt6S8gG%f07rhF%pKo!?MiS z$3Jq4@fa0gU1-DsD%eT$w~;r?8w>Mhv{-LPU2jEQe^Rayyx14@I~x*N-b$ zJy$SpsNmpW{jF@jdj&rBY;bfPUOzMSCph>J*DKUDKkBC*t>S*3{&5?9g4_?&O}~v! z#xd51K07!pRW--cbJxTGz9hfzSio4AkuQkDMK-Sp|BG0NaIt>dDfjuJ?O zERy8ply;vOHjEz)j24J}v63(IG&mWUX2wUw<`s8}lpl_snX*h*UR*C2#u-22X|jxM zl%m{i^qD)HdiE4m8WW?hu?LO4=UZu-j+}|Dvd{O@-$3XpF#G?W;QvDV@o5Cxl!AH3 zquXdXCUd9~f zPqb5hNC)J{^eg#SdfJfmTf?AN4KKZBSoEghqi+m9eFug1-bRWT40ZNl&}JVGW%f!V zO`Ko^MJ=?-ml|Ega-+LgYxEEo8GXc6Mt^a=F<9JT%n%P5GsVM3rFhzyFJ3TC6fYUo z;x(g2{Ku%3DMlTzHYo*Wn3ZG8CS|P zjjQBk#?|sV;~IIpu|?i*Y?FJ8yX51>-STFykR(tg*|OVze0r#%^P_vB#(|9yaD0`;0}#eq*umsIkI$(pYW$#<;}zgR#YU z&A82Y-MG(q1ED`KerbGYJYjrfJZF4uyl8xbH{TlX8V8Mk8Q+6Sqc@=DN5)GK=0vNahbRrq6%vlD+ldv&$A26A}{ z?HAWV0Anbho5U71W$O*y1)k)kV5Zq$rza{Y=sY`;mt!H61)w{zTx>-QP#6vb#kaOh+@QfAVQ#*hy*c7(VU-!(yLlf-Rsc|o%;XhBlWH9l0ihal)}T%P~kD5OibAcrXzlbC1?~CR1+Tz9QG?d)-ztR zzYDc5v0VLc+!vrj{1fVU|C-hdXRrPP1v6_S?w?J|jz`9t+Uu69KORHGzkc{|Qa#eSMcTG6 z9urH>4bXj}4_^-RW8pK*mz;p;EBaYtfFC=TfH~UE6$dyWuK>)ER{-Y7FNXP{n*ofY z8^lOIG%`kWDBu}w7i0OwIDRqS64>~e7{JtOl1~6PC{rBH7aZr2DS%>>A&%g%Q+?t{ zz8~ci)A*j}6Vv&g?_;U}Y>{6S@@1waW(8=FC}Pjie1U7bIEKRlg=my0;g6OAa)CXw z`Q5R8Q7$TYfee2)1N_J@D*4eIPG>H==J91dU#bG&PYc+!kT1va<#?YsA%I1;6AgxJ z;uNd70v2=q)UX#MSBEt|Jb`D4C6@ZcGWMg=iUdw7St{!JvfL+D@MAy;Wr~$P(dZLR zK7qpp3&ly6Xb#X^F^N4b03Z^r0kN8U{#w4{fWf)qWWJn|f~n_e9N=_*v7Wzph9x%e zyEFN67C+f&iA|hEq~sOAVVWy8TjFefa}Ik3`oy`GIFBppeD+_!pT3Y^U*r=Pa~$O7 z5}&vDta73{i_FINSa;q$X=EwRN9zI|N)OP@Gcaf7&lFE{e#CcfOv zms|J)w+6rsZwDXa_PP_Rvz&>$_~G6B`W_$sli%FS#oW%W`vT&A@e6+U06%-sCwBP6 zLq4(7CwBQn8@Iu33$SdUzP+5*!#=T(BtxvmhUZ}&aB7uqvU<{@M#UBX>jo%|! zefUV&4m)bjj{TcbZ~s}qL>-0nqfP0o6dQI|j!gpj+;P zse>}QRX|G}6oA#iW9BnZU5_C|!73&JuSr2r@I$xNd?K2WHUVI3f6%G6Kw;Y;*nTHk z_T!O$&uZ#F6Xr+*VkAL=QeA5gxVKh!XN*-C1OZE>eOsz`XbYdhzER=O=IxqX2V;XP zPmNBkTL)oKbD#uO_l}*JwwH|9xb%x4y2K33G=fK6t+ExJqRczuAa7FY+0ImlO2#Ji z8x$fn4jTt~RR*{)*H1KQi~k}h@qYsS3VaM$Oc;_d2w+&T#B(qtPg0jocDXXWKy!f? zF&6`)28vS-G))u|=tpa7!4R0x4`^Sf3AhwfR5+N5lSkS9gDo{wx1fVt^(&W_&zugk z2?gz917mAVebed|{VagD(Y$oRFwlqaGx(54l&F>MVA>xC>KO6r^OV>=+HE%l1hKO* zrdh%dhJvF9(RUo;O=7R)u%V7=+A&S(Qr|MSu)HE-%Z;oJ=1Pm%g>g7MaGjfp5NuoM zhK%8}qZ|=#c{QIM)keGda=@lv&;vG5Ie%k`=WX#@@q#6O_tQ`=Y38h`e6OT1}|KMEi$yh1P8^aotTpZMK7;@$reNrpexlg}$| zMDK^W9xM;TE9+ZY;AHp+Tf8UU=gSAS_)vVrp+C08C*n2qR%NK;7)Zv)`7^)Ih#Y|% z|L!mRE;D*;ZT!o>s)<5;Bq$WaCrunJqpSpCv^Mft~^0{#|^5S`%N|;vZaL09WN2 z{dbZhJ)wI5WQDKAHcbxwBIv$V<2reo3BI!L_8q&0-hh1L2So9tUuw*un zKYh0J%K#cmrtt0wU>%_9Ys*xw>7S0k11)SAXLlaHphsy_Sh4aLvd9uO=rhPn7tTl; zp~^C%s4GAoSrS$Zb)hYT;x}k-iM&N(VyUylr1}dD;@gG?eT&%leD1{@Qf`pWG1+&Ewf}d3}cC-xei(g+fG|{m)W-L zA$!`gm+WmxU}795@r@jrY0JK72_c2StZuq3`*F(s`7%HbjGXq?!JuTzL9%xzz<*%K zg-|*}Bb~A;OclmtSzH%xT-^Yy#q7iqXif%Nw&h?s#Kwj_ay3j2x8(?I*h7)LgQg0* zu^8OvvQ1T7GrXi4GG8s@0@XCFZm3nrFaXiQ01h;hy08{NH2w>BWx$=ZwAQTxPRb}b z+LmKD7h~l(TaFjDEhh*Ypkz$YlWc6&gEmGamg5I`yCn~|Wv-k8?jVn_$WX9awk+bVceFglmc@W- zmL=%UvecGka<(mxmB(PvRl*7C?l7kV)Hc;=)Eo!MgL?#IRfO=3)ImVcmJ=8R815j# zNHD_gSh{LY&ANeYSV_XYtzfAUyy#ABkzx4&~;fd~JG2xfyN5 zrk%>o$Mo7*%s}{dSXau5H!@Up7Gb1kVoR0)y{%3UcM-O!OjP-tzKS}?4N)u#2ZT?( zbpy<5=#cow;Czq`+Zo(PEyX(vbkZ>@UjzX)f`01YFtoGeSI}kAZU^b-C!kSO< zCXmPyU^ZOX9~}UDbm3WPU1QVA`bKSW4apAELmQ7v^@)>ghV4cuSklnMkNPr*MPjc= zoWS->Vk{lQ@QphyK88(_F}Toe2d~eE7nJuy&EvAkPGU+Q?LPrzzB+=nfRYxXCe^%7 zL+2*LG;P;2eiY-v&zOM@o$q_FSFfLLHO7qKR6qU|glvm8_JW+e@IB1(l z@jE2NS33A!4OC#XLJ zTg!_sF&Ik71rcP4m_!?Ujn;MH05Kedp6&y?LqVOSMOFc#bAQWOS&2wwv2h)31K5%x zL_`kbpcnod)UJ+Uy8LSyBazW6@HyqK&>#^J6yB89CVs1A5y(>-;i7vYftA-S1;|)4 zkS2KHvZxU_9*s3fvR9Io(MVWSTRboo$g>U@zqO2Z;)bV=FsXK==}?HSC^c3ypmqYH zRTr>qg=DBh1DT{fWjG17AEKgP_^%oM{AgUw)g2PDx*z+at&|!enU`-7PJ1IWwhM(qPXMH+MHY2zfUYK6*hY#Ulfc;CP^eEgW z?rT)42N0`WsxmRvqmk+{9i;jgJ)V&2uV9y=QyqynY}yACVS`JebE>QnsZ6D(6{1Zi zm+rhhNlzgz=Pe)cxNL0RXu>YfbAX^bXC)?B>B?YQwAg8|FMmd5<5_ynNw5`_!8>{V zT?v*9MasVEj6aeC<@7*3E-Aaoj(tVx9n*2xX+0jg#wVz+X^YMN(XaJ)zQ#wW|C)Y_ zY^USj3-mh}`UrZFUcwVD(g{wHvY{CT+Y{hpq}@q=ZqeX&zJ|6VkfGXDf3_ZWbjxAg z*WfB-cJ#B^yyk`v{9eTpU0sbVc>zs-6(&zM5+yMfU&VpZUAHodLI#X>G65HomENt5 zx{)vJ9V*nA}f>eD{ZVeJBi+Xu>N7pQOhK+sWhe{QJGd6xs!UuO?4 zSeFfOdj`UTJmJltHW~~Mz8&-68!->c#|Z@5SUnEP=fkEB?S%-#?L7p=>yZa&R2z+M zqcOc=%`s7#L#{=4TL%NGQ*c`LsdO|<`?GFz;tAGER$>o zjlAbHzW3u zJv1#nZ#^0!C(Dy*WkQ8DGr+2H(_?LL0(C1{LalTw+T%8~%MYOwfnme)#JWbPhp>tiGO%5jU`H zA{7>NSRvBj1!1Ho=u=dgPJb-!JTkET7Yrh1aO1j1hi6rqdUWtPX$k;G{0*VNK;_q9 z;Q@4}3(#u|VtFjo7OOKsr9*q}rde!H1W(_6R1{@%M}wvC^J5~cuJ}>qWu;Xm=^QD>)n#>m&S?KRV}WO3Q{t z#auApc?`dTXY;ozEz@jvbDHYeDm*k49Ei?cm++%7sk z+lQt)f#c1Dtp|T*FD){FrFv5l&?kojQjNfn+AX7u zcrEzv_-dVdo%(d9Ka#CBTEe8Ehb(0v2kJLL^*8|KzZ#e@WZdgaQa3}1w`|abV!tfR_4PN@qCPI+&7OyU-}Y# z|L;iOLaa-W#+Ojpn@?k*6U99Zgb^V`xU>Yo&;CjO0s*m0kI5l%3cmXldTy~e3gBiB zfULM5LKXZVkTK|+fd^?cdg8Z~-!&y;8kDzskPn>`8i?P*g7zEkq!z4b`Em%UBzuQ# zl7kj~H=OnD{O#jn7gIm1VcjrF9>g0B5--4g4h=BXA&kl3G(bENH*kl5?BZ(VSoiRj z>Nl#f8LQ)j$7tymrFg$=3-yn^gvNP2NDsGVQOese5!C02M(Pc%y`M5Wj;bpRR$yu^q5s! zXey`q7}ac{aVm+E*slo@u3G%+hJ)_gs2LrA4Uu){Xrq<~ky`%KG(vTM?ap!c{#Nut z{$QjZ7C#E#>Zhp-OtPF_xSF5jvK2x8aJDW%*HM5EYpT-M?xuABd{7+-pEe&vH-RjA z5?>-61*+8w)?rY~3&Vyu2e>T#sgD>)L&YE*k~^5Dh#{~~9|3H* zk+A<9MH?YRTqMRoH)#Tp8Ybd!y~#jmI2@e=6FD&*>W=w<8JK~MvjQ;&DlC(sIFc`l z;VMDbDHV&wY_Srk8I6D|xn9gw-DnnBq6pANhtPSvOghmnb3?St+yEy1B}lA~I711v z26EV;L8!>rAiRhvV&?F0BEq0u$Ts<@B5vfw}4TlTCCpAAWBF z7Y_zr!`X)VROR+_AUDO^@PzHP6dKQDuxdK9$LRuOf@H3*(QdR(~OA$ zRKXcQNdnf{hN>NO=59KR$IZ+DjLdLL$qdw*7+l~sV8WFdfDHt!Aa0|hU}W#hv_}Sm zr-kUydKE%H@FhHmzD5EYH!)t{fo=3KWAa7wuRXc|PjLZIkapRIjsO4kYy;(hF-I`y zZ)Qf(1~?bKo^vi{QRik_F$#K>CZOR?1Upy^c2EN}a8S3+IA2}>P8H2`CKT>Bp^IK9E`fsmrBIo_jP3;?JP7Qx{ZOrcUR+IY zL$Ur-aV>VJuf!O89mdw1LGQN+AC%0~pgY-3+%9s&ozQ{YCdL90ZL-)2=CTWHXnn*( zim6$uR~C|eh>mcEb;f3aRAK>^i(q5`t!5~pr+51vEhNNOH2rIm-_Zd5G}ZmIXI#ft zu%2oapcLQZj@n}noj1($2%XO(-~|BUOurDr;6-h8ae@FZ-cmKn*3O6(DJ@(qxnN10 z^(Jr!(}2E)59Pwyi>}e14e_ng;X_C4rc2NwBVrRfmId;k5=Y`ubUET~em25#vX#!+ zx>TVVUIuv3uC$c1xR)-MG*76IL7x-!3IxT}^GX#M0EJ#&TDb~SYK16@^7&9c1%HT? z7*e!GCv~;N{Aizhu)ao%&)MnM@~Dj=JADg3LB3+cH~Yl@d_Kk{&BxZ*=hFdcqVlAc zONuq_45pJrYKcI+L$-Jqo%ucLA>PM$_W@9iJ_Pp6M;P7y4D$V)-Vt8_2jok1wEv(} zeXE*Q!3d$lJF_UbG$GqSbIjS5rq*k!py5efADUUC5*sefTY%gV7g2 zpPC+MIPN^*l4{&?O|=Bf8Sq?BwWRZ5fxUm=11c&7rk}{QneOHp$cXvAWJxOBU%kwdz z2vVaj4#j%aFsh-ESQ#0ES=o3%?o9&EU=cyW{XFgr90-It`VH(x3UU7rMx;^V4e?f@ zok+p5tP>}r4b=}36SpLie?1|kFbGRl#K@~>-j+s7I)X%En1sjcBWQ{g&u{cV`uyCb%0;Z@s%}IMQscM`}MmIf_4acBhR&FV6() z@lMO;!V%$)f~h+SrS2#YLS%52U>&jtCf#B9Jy;>ifyE&-%HZQW{{d|oAaL@4-l~F* z6Q1d1)6ED!0IOFzJmYY3jFhfq4&6hZ17vD3%^hJ70_Y(%1s1IMIHC*zz^6`P6z-e^ z<8OEqBVS6f$R2QPE`j7W#Fg_q3p|>qcJ|O=fT6aAjsS+fUY6wy=VyO{-7@5tgbf1xqeTBe7g!vx4wwa!)yj_$x z&66HfULd*SZMrwTtMX=GL^oNMO;ICy7d2LgFrx3EjEaZ!rlqdmEcLZ>u!wvva&#Wb zcs|7K&C$G#qwy+_6R<c}289deyilX!>?+w%Oh(`(Y^<;^Wv zR9IGCl2^G1ib1dn$!}8I`&u!ISLm_I5^@L2n}LA*LZa)qUlbJ-Ag$uEyh?rx&Ebr= zx6?}&<%`D1a6jx-Z&GvSOQn{a7R8dq`8p}ZUgzQ)4 zmE_HGWj~Cw;@pW;l$GX{=W`}?^1gY``)1|FAH z@o;Mq%*Dbe0X`dS^b!w6X{zL4;md013yHIufEVucA_OfXPTnVhqUnk#LHBC5D4I`aE_{ zUU_9f`HX^mrXCqiC~#rPqRO&)r4@@RRGF}N9Upyq z-oy<Qx5p(X&A0DRk z8#|oKC7*Wb*;K%7_?s{r9^1Jr_m3rk+y|5ilrq$G&AXDE;-z)gfB5t@sF7Bj+=!0L z$NhG0_H-3pZP8BHqp%_4HMEDZ!(oXA8Q7wSJ8gm%E7DK1dBhw^+2Q=GSzEi+iO8bG z&s>%`vAu?6(nWzO?Jc8|hKoBTP9Gl_IdrgdQ$Naq=*-B9Mc@Ph%u1Le*v>r_GZyxz zIrpIRJf!Zuuy>GxE(lnA%ziR3LCtjev*7RM@^|M6tjpg6{+=#>FZcl>6iu%W{D-;x zVfb@g{=V?{OYrx1`3K(j+~pqu|45gA6#S!I{xR^6b@|7^ zKi=h^0RKdne-iwYUH-%2&vp5yz<-3xKNbEXUH+rspXT!C!9U&Q&xe18%U=M0q02uL z{#h=65&TEH{Kvpw?DChuU+VIg!9UyOKNkLSm%jr3N|%2Q{BvFYdGOD7`K#b(~L{v|H|Quvp-{PplJcllSq-{A7Egul_{ zZ-Sro&La6e3I1l6zXkqQmwz?#m;Y4wPjmTChabvj(eIrB{|1-; zO!&`o`8UG9$>rY+|Jg48Iq;wB@}CF)`7Zwj@L%ZiUj+ZfF8?L)U+VH-2LI(Q{}tc? z;OH33gIL+P8a#mok87X_1jt?ZmS8o3S4MjAiF^XfBRfc9uioH8@66EJ#T#~!N3WD@ zMJRQ6i3hu69-Ir}2~bau4IM(l3K0f&eEE7n?(<(Sx*6Z(9k*U|3taroTcK5`zc~_b zcqzvOTK|2xOXvqefSN>48XnNE5Fx`=l;adG66N|%>`JbE)F7CB2r)=M{@Xl6Xv zBHN~--K9QuH0v1mK-3Pbw%1G2yh1Ws;8v$uprw2F&KKNPkk;ggS zG(j}#ERc(e%~Dapx*8xzz*eor0!$PZqygE}!hjMAlfkjD(jrmklDelAbAS=727s zoQAGrI--Ch2%saQMCuLJ#vlYpObLkQAtZOgZn`7G^kHZ(J~R~C={SJI>BAue(0zCm z`8}hli)RdFdB);d#Q32bjXrAP`zpSi6hM6eL2Lm5@@o{evTL;;Bg|UhRnn=m@@wm=fX?;U&Gap}@rPuwHDOPcPArE8y59{Vh-1ZO^{-=BxqtMCvNfaxQ=UL zM{K)T7T6RZqA+>tB)@eX+U==XhL(jXS!c7v89+`_6M(A{7^bZ6k4e0%xr?0BrxDd! z)r7Oz*#u1-ZP5lW{uXs?wC1i+^T^Jz*<1p&!x40*4S9TwMQ7Qt=-6b@W*hR(IW~lx z^Nzor3leCT;CFIZE6ZPR5vfJ19}ObUuHw#x`GRFr413Q z!iJPJn33QPkvCQtpFaaG<6k`AQR()KCBl$rM zbSxR5FcAZ0n-5)&$PW=}Xmn6m%9<5b2d2BuZFl%UTo7av>8SJHohTg}-=-E9RaRoh zqo5R9OE59`X-C!S`GU~lR zf13)q<4E|UFVwjz%*Bz9zm-?b+wHG(1 zrX#QW#l2P^@6TsgaBD=uBqq^4{&h47)Z4()5qo)N07D8m1`9$x=0YJGb;-x$Aw`ur z^_?Ln1#m_ITV!!gk*{I+CgkBXZhJ#jwmNm$(?FJIC8c{B=?KqBG{e(OWu8_#!Lyng zJ!>GVt)*yBG$@HM-6guEJmEQ84M(=t~rw=^q=~K@c^tES$@OsV?DV|NLu~?h^ zEz~!xGL%As*=OFPH;*gE*UiBr0VKvID+Fao^k7vD@_8=xnz3I1Bsna zV?7tp1kZ)h+=nqQ(7ER+lg>Rp>*U!)53N+?;M_xB_&c(7<_ClB=HQvcN)xLVq{C%H zFrI)Z_9)z^Q^LI9`As)Zco1^}{z0j`lk`QeCAh)EB0T@GP(PRDa!e3L^8}%a z768JRj!S_(@(KK65xX!uSje-3#Z<%f%6~q7S<9C?%n*D$KL`=9g+~z(g-6kH6^=h) z(F%(IESya%ebmTzOb)VXm5-+e**rDKrj`IyG4$jDNt*#=qt!>NeY7U>IZOeL=PASS zw9cZFeRK+=zAxj;sTQ4P(dl5d@shS3@iKlW%Wwak@cq+dhM%=Q;E%Q!2ArbC#=2&u z(8OAKV>&7FMpM9;Wol(Oo)g8%-<_XEXi0-nNpu~?3(~6IF$qh9$l->l#0h2&w8oFi zHpnJ}>6SToBaT>|bU{PbqZPAVq6CDWy5`2}hJxnire-80>(*h1R0oJ(ZkPfZ?ChS; zB+AFhZZy%>N~34kSiC$lnXsN`L$yfB;*jAXrCvlA+vxD)ZPt5&E=5e{DQak_TUy zERbId7@5A|>YCQo)eT{u>BJ)xF0Efv*QlQ7x&!5cT*Iz+EYhQ5wsP)WlZm+YLKa=? zK}!AyYFOuF{Ak&J+8hY`X^I&F;HOjH!A$k1O7uwqT{X{|g3Y>OjHF}eW(c!>2JCebpa`ykTvxkHUfHQlFiPqR@2miseqON-7Y+m=kvMP8I zgP`YEuWkX$0lSQK<;rEvO>3(cH`GM{p43uj?5UE}M`H}*MM>6w7^3_h4#pFQx_$KpuY((}Eo|+Vc<{<-y@Wo?R;Y zVO7w1(d^H2vd=5CGcb|S(}O{L^hPPp?a<2hm@U9{2(=k|0^8?8Xz062t%ZCpbtRJF zos(=7EvO7^L(n1JO%&MPg9b$>5nRQXL=4fB zh@hTCaQ)=+R=np~)b?|jMEn|)$L9&B0bvsHA|??pQH$qgI@|LKCK0dFGoIJ!70(;= zp64C<*z=yNPCs$AObU6vrLu|NQeJjEHDK6u+Uk1H4fn1wC?u$XaUs&~!-aFX7e>ci zCmD73;b_WXRp~g`eMHG{Of*N%8~!K~P&)6BOEtCV3GheA|Hou|KBYdM&*(7E=QPCg zH%x&49;NUhn0xT#MM4Q6osPvq8@KluSH3Nh_OTWwD-zVo{9%rowD24N%k!AC^Blj5 z)+g=_+&rN9!u2q_{>SjpoOjD74$Dn>w&{$ix#F4Y23+>LTdFPhNp-y9C zy=e|xl=MWwY)q2pSv229r&wUoLYw!lZ0J}lvgt&AQf>3Z)P@#CosI2!Jf29CY+6b^ zu}?(x44VZDl*vE0zx;1YnaNk-f7*QW|Id=@&yr5-(F-EdY2@Gxpe#-=QtFFt`7SD> zCKClAuVty-T9(&9CSa>3lbu>gWxXy&&VqJ6n7O1d)}y9F6@sA;u8C-a@Jeb9wvl=L zbO%9{z?vPqaY(79T|3AZeVT)z7>_fE1+0{F_xo}^IQcWnn{(y$G?Jrbdkj7UIVa0o z#{u?Y#fWffXzWonpBwsO4u*9#=B9Ea47h0+aIv1J$J-oaa60dKdkyk2_%#%gO)&=EVR+`H;dtMT3}Uba=VXW1zb7CSj_;PsLRv11Megs4?1@GO zo6yaQ@l?xYY8@0pK`4XLAtPmIVVu)Z!nk)ZrFnnOh;^n7b%5Mg~gA^^PUWJC1Dc z1Xr@DPO{uo5OR4_GS)1)(P|xs4#DX|{AXKjqdKu_4|k>MchZzf9Fh=v1|TX+Hdzn^ z`Xi}sxR)nNHs6ie|1RwGiGMsr>Kvyo0dV&$1wNY~(S!pi5JryP7JwHhxU#;cxv8SA zW_De(I%-1;8i1=^$0(>Rc#N0W8(M{ZX`mU;Y_7&31l1U7PX>!w0_0h&zwt;6*wohQ z29*S!r1BK8KIXSt)Ep9Ty0)xdy0i}G1k`GDTD^AW41v!65@XIr&yB#SAJx;w3fRw8 zFABseq@768S@r8!63A5NhU*}OEF5g!*#kLZUqI8uCe{&cq0TMMfGubwIX22R)il=x z*I}{KM4!`ltscxeLE4Y`f_iT|_f+T38baXgh>4=$&Z<5!TgoULDr*a?NuKux@NZtv z=WgMFJ`YUcaZI=^ts|f(!Dm^XvE}^|kd%6+D5^aVDRi z`f{i1A3!k&uy-I|20@9-M?(V0@KB3}TR@3OYEi&s{r|c~ia%QVc2wQjRq<#FOw|9E zZ3>{8j)71u65Aj=XmpGx+lB_H4ZDt5f0)zg9JNtbTehmMxtiCdTx-Yar8q7n8X|mh zUF%3rZGuG;JH3n=t(8s-xQ;o(|D;)h19$NMzEwg;OQ4-*Sbp?S<`e1M)=IwARtmgc zl?K*>9*Xy!*EVK0r8E`2c@XlZLomswp56lNtrO{`wqD>MQI z-t;%eD?db>edMi52U4T&QH122MmS!PN>y__R_fzaq4@#%Sf_9#QsC4(PN7t#5Lv;D zgYvz?<#Aa!aH7gk}#1BC-6D*l`_(oz^v>)!H6BUo2owso1yZA2uc&W8>azjP+ z%2f?@39NKj0AhXEkHI5{FJP(`YaZiFXud%Am|1H3*?Dwwp|%$r>Kd2w;<@Xn#kgbo zSOHKUfY^#Wfb~YO^SwGx?HJt1POc>A=x#Z}H@K+i=%W|>Y+=Gu%MeA-wu{)+73dvw-&s~lp4bK71p;=h!_6*ImqTmTE z6sVO$dL6|426FHwzV;S6!ynNR-^R4^Pat<*&tvGKFuEjzPp}K8cH_J3;@olP3afnc za%NcNoTZsCx?u4CWA9DitSZjD|2lW+bMNib&@?n#H_+_RjevrH?9C@WKfmGJ-Ak3+<_r7 zV>|4}1+X6%+GYNgW0bXv>xxaAQHGT5qvUom%Iw#nONVY94*0UI8srM= z(H#t|2FdqgkU6ZwmA*VLh8q&M50@#`AoFUyB|9qLT*sQfC(Ld=ycYxFBGWLF6>>B6 zmEbP!WziRh6L+()32Ee^(O)l%knNG1MM=gd3ATBE;_UwRwvJXgh*+PIlBjJjap>m= z$hNw*tF2RZ@p&y#=0HzJw^JNtWEgOmcEeh<}XLn(&*FxD{tCzP; zMFsh7p3u;&Y-GB$a%rMn>pK~CN$RfN6z8;P^l@>P>ntW4ZDDMH01l<6)hPqfz|D0Q z8-)~1<%ufFXJ!#)qpH0eWypVni~LL4vdoL?{dJqM6Vxlss$@~F=7pRxnX1dj2nffw zgcyPYo70wtinmGotI)(+L#B5(1lsA4TAag~I#~ak*;3h@t-~B0=IOy2oi!>R-c(&q zm!w!OyJvuai#gtTI^1Y(Qru$a{NtP3F*NGm`r(iw<3lRFL-pYeB(uL+r^`r{QNO-~ z_<%2KXVr;vg+$|A%K<{Bhjw3C8{pVy$pURn+>Io3Tg~`#4l^oUI-$&)KA8UJmKTED~>H?lA%f z4w2(XPB$tW6OR#3kh3a<6CBW3J+r<#*SWKL_z`<{U}tsp5xnOIHrBFdRgK%pAS|6~ z9wWg+bQ*5Jolos{Lo-oD6z|K;K<{od#Ji7e{bOwLe4NN*pTzp%Q)Y(uRWlp&uyYav z%;LmAvpq4$T%4#e`x1lA!Nd@ZaST0#?wyDSo2>=D&bEO=3D}2h?@7Qugh|>IlS+9+ ze%JvzJZ$nW;=U)-gAgnCBC_kNjqHjI#tQ0gb9R$X-R00!8xA7Rn&FRHQ?%Z9N8W-J zovhRbV{ufsy>Z8*ras55`cF`162l?+8-w*k9kyFU*hq{vD-&bPhD3vHg=1wmMy=Q= zdk7%gX}|Us0$MAl2`nQf@;i=34>wn_1*d?GSK$^h0-eKE7!sx+@~h02(k;0y4=i4b zAFq6QPh(5YCJipf;KHERm=#%wah3dm&rK~ka{jM%Z?z6ao^Mwu8J93N0f@MzhE<=vg#JuR>$ zoh)*_6FCO?)Q--$CC=4EvrgLPb9dW(rz73tFA7-_IG#oY&|G{;()U?c7CKjsRQ)JyHs-@VL25q&Z)|b% zR#_I(?ewyS(T%ToFe*$eeDkW%?y1S_3|MVy14j8=eD(vg+q7BRsCI+7SZrl?bjF%$ z)2U+=wJ$J)kO`RIIAHvO8-HLfH=Vv=<(=6-9;ATU)8#qx%xM3#wlEks#hH8MEyIhYaNcMLH_VY+ly~E%Pp;!SPh5o1`7tv|TbOm6hk1 z3NrU-yOY`H#0x9VxM^uU5RI5Ma8@-|NmI>|1Z6YRWBFuvnzVGeh#HEnp?@x>hGFSa zYz?W{-S3$2w}I01kLWS>cS86(G5nnr{!X^P35^B2BPTN^YYoF9IgQm+yxjLg?q_Y@ z;wvBjrv|Pi-8h0{1;;O#u_IF0)|2n^S(B~F;k|kkp65{$3~JkT{V}*9d%7Ejr^+qy z(<$5r(N9oC;_oowy3*7peqibnKQ!kheuND|o>^Io5F&QXU|%&GP{nqvMF^>fQP7#} znB)L#6wdi1i+UOsh-mjp+ps_e^yi#pKcMKQm?Uj%$Vb?3IbQL&0Ji%IrzQLUjN9)s*KTJ z6CajNBxOa;k{yl@DAdT!glNzX%*|OgU~bjnwn_zS(Agb2dwq@oR44)8VBX}LJALzJ z-`oX-$1Poo-1&U-7MA@;|Lw~$t6=5eo43I!^tKK5&D(h`vf}p5I|y!59FV~`?_>yu zY9D;LVgVU_bCm1CyzQI2dDXINzPX2y?Zisoyqonl9&8?dYSWrkt1ex$!m&Fgv7+^i zula&ElKeFA0Y4Xnmltrkju6PY)GwDU8{EtLyRYova`lQ$>-2PATTiF^cC{|)9(GcF z)>iuzb(5ZQX`gZ#K3%Vuxx^k+PX(whSp;>tBD{oK^4F}m;7nb1v>hQVaK)yT%XQV$ zz8jEhmapEre(j~3w=UbVW8=z8Tbeg3La73ArZ?Kx$lZw+E4^ZwzA>h4moCC5ZF;gj zd~y{}27`drM8$4C_tMqP#N=JSVxfb zRjTnitnfW?nJHlT+Dip8k25?TS$MpinwK|k+_H5O0lEpoY)3aUcj1lY8#eCPwC3E^ zTU0u83TUwSwSW_Nwt3k)Td$&lk10H{DJd@9*t~A#mMtqUUB0?`{kh6KaoQ%x0o{bx zRo?g0Iq3GZ?!H{>OF0c53s+pFyrSGY{Z$=1C6_eEuY~Dc{{ym~Dr$ve9E(i&$&^>l zY?;-*tNp5&zuFHd9#x;6d6RLf*vj>f~0No53t+Ob!bbs3#C+|a0v@y zHtt|7k+KVygys6;;3_O6H%mp6#^k_iZ)7tG!@0lpAYQk{oHKLKX3}TuZeP{WiH4`o zycL#Suef)_0>FFD}O>BWodr@Rplq1(?-3QOJTiYTY0hkc2`dCU8J^xF%# zoX=`X{x@Yszlz=aT6>xqp(&K84BA3Io}p7NS{_%chNB$Kr?pYDeDx%BoP;&9EE{0~ z;gAd0F7|BU+QptNT)Wt_h3hl<{;b&dF7|BUxmemcvF9%KY~i_!JzKb5!21hh@4MKu zh378zY~i|@_m{=qcd=&+&t2@+`wZ z6uaKc^_JN6R<5_juD5f2LF{@5*B8dFFXH-DvFq(H#Y~O#6^T^MPW#JzN&oG({nutK zn3-I;vzI%|LY}|c8Z$j<5-4D$)Rlmez;`O2`9+rdceBNRFRFvQ<#RPrXfNj~Wjf7% z-d1_%a3vW+PONF1UaW(q-!8eq6=w~oi8fj&W?)!Mmo3U8-F=F1wO51%u11!CwXyMO z(qkyqZ^cJBX$k;3+o*@J-TH(r)kzU~;P{eYw>trBe;aBEv;Rcyay0Iw!;kN6t0m5p!n6Cu1PYGKYz6D9eBZ3RclbKpfqx z!-}t?nW}h>|5SQiQ*vbWaGSC8EnAyXBg1s2oTo=tyG*8>SBw;RoSZPZhxyVW)C!Ol zkDC(#mR#1dfT)ccpIs&a;e0%8@WiBo9Ge)?aBPF5+k;NuBgP) zAcgo=iRdONzY;lLQhp`!y`=m~=vn?(m8K4{uAbjfT#x3Lq{c=DaAUbYj^FW=H-X=Y zrDEULE53O5fd<%h{>A}7Ik68_*m7-47fXBIT3?~(-kUY|n+LK)oqNB|9?X~z==T_5 zF7^AtocWOXuq^C8k}(ful*i|O^D*CGW^upycn+W+mV){dSyc0n=(SJgID1rw$8w4! zpXG2OOC5{BK$IVl|`Lw=#S{Kjw=Gh$WeNF{_MrWVZ+2?fjc^$r>x1QJGiz@Z6 zboi1l6OYa2%f9(a4%Nk9>+n^*{k5F=y7@-VeA7Iqvu|Y?q~F%rcXak$o&Am8`<@E? zTi<*?$L+sUSw9ebKh#@4(&5Lt_xC#dM2Da1@H4gJ=c?;}>F^8R{6mhwUjL|4f9acF z`R1Q8=GQsq%-`hLCH-yA{ImHN-~2Ae-qOG3NQwGyI{dqD{=+9r`zykVkM5=TlAxdO zark}zKu`DX_Jar+vwADILtBGhWrX*|M_&i~Nf9N8-%Pb{Aw!cTT34uDxLA7R0m=Cb8J`+^Ey0uU{vKe8B zen+`Ba-(bQm@u{oeOJ^~@?&+Ltlkljc!UE<*7NafOW&qr!~LjeFDkuDu5CzLkk|w?VI68cNG@^29>7`Rs#zt1TvdL6*}Q|NP}}zswtVx4!2Hqt$v1z- z1I8oYe1as>CUqXMF#|7Y{uFpAo_guPtML55Y$Zjr$zG{lzZs0|{Kkft)tfmTDm-#@ zZ1sY`tMUc}WTCE>e>fiF`^?3pSq{7!Z*brZ@do%_ZJ=bff%4J@-bn473@W(bC|=Rt z_MX72^Xda{l!q^2eTiDTh%N(fj9zRI3}baRPG{pe1DXi|3-6wQwU@)y=9+*dl!J}= z+DoE&v7xL2Cvq^`0&kLsXTt9TZ;Ch7_ZkDQ$(yE_t9t_P4Ap$S&ZY~R8Q#pmJ5#N2 zQHn&z(QsCYX9eCYZ+76F?GgOyErRo{I-uHAz}7bg-dt~<4H6Z2=Ll)@y#;}{&?D7T zIcX;F77M&39xTDA60{XYg~sIR_&wFy?d-sHQlRsSNYz#fwvlba$v@)KNZ8^nQ^6$^O-lTylJ_L-t8@(RT*!kh1ied zn>j&C-e!@B!ZWAZ&GDr;n^0O}p|A;2QH-0|d67z*DJ0A^JE_;&SFR}L(aILg6}3cJ z2#a^023hs>Dp@UVBIucYBQ~;@b$55Rw@PDc6;B%&cSEOK7#BptH>d^?HKwx48|N_B z2I@*1AkP+`SwT$vefD%{v8_-zjw$PG$|{ej1${oyv>uu$Iz*IP{M{33c&Qs}y|y{s z%tDV5cosu|)-ByoL7)7JT@m+$HoGHBo9N1to(_zo*InMc31xnLFH{uXqb0cgxGz{7 z`Yv#R6n>Qltw+`J?!z)e$aHkMJd$9;h9pk4*(f1#&0}OpkzpHfdq8~j0)x-0p|3BpZ7a|u!Pp8AX zz>>GrPYV-IYd@beYf+GRro`wH(nrI^5@Egi}rLv9EM-704W;&muO+^}h? z|1mSX=@B#HxEXnr2nL(~iTc8m|(v4~%_gHV$e z&VkRGft*d^Z!$1UIc8>;e=_x$85urleAehJ|4mb}(2G5&#-vG1qs6Ww%GWT{j-GHI zdch8Kf}PBfU1-U>>EkQtzaHK^#2bfs<0>@NujcGEM%vCt%?)U@ZZ@AH7x>T3ZRVHc zm8dtb_hykzV~%-~*JAGUHe#Exm(WHcLmT?Y>&R0IXCb9<<|w7XTO-3e5ORSxh&uPs zy{&vX!c>iT!OZu~-inI!3ntFb4dODOgXLYZGN0#)^l`Thw1xS4s}oH-}0v$GTh9!uj{ zx|rSe7$>?sTbFaVJbRixoBNoVM{vj|%{e=g$rmG-$Iblt3M&bz6i9MTdYVTe#D2xR5{5&s&v?X@OvW-UQR^&{~*$sPT*u6(drQnngM+)&Zyix;DLUXD!XMt&KqD<1AmA5};CMO?q-EnT% zq|`%@^51y%-rt*5=oPlw-)-KT?C+i4llJ#1@5lNTX@Qw9S=*<3`0m{dfqNMO7{8Mp zWjt!?d8B?@Y92z#`cW8$kAcmPLy`|e`kz4U{|JsEpG2I0)V$q1W}amLeA7H`{>D7% zWz46&f#zv%h_u#Rtfqg@?# z_?~SBj2Qib@qKgUpUjp&n`Yl!<;{G-4DrptKbrByCym_6?8c#zL5*U^4Q<@|6~n@q z79|&$aonta)~uPIwik{V?4a~D+|HVx8IiGrk_$H|b$Pa)*+KcFId4bJ+Q-d07nVcx z;9?5=3R?Ilwo!;iYJP*A|8LD~6zt2*?<|gA_R^z#WVuXXv6>=yZlc3Q%>X zi7Z8lOYLNtrrdLEUsO=~42vBCVXzUbUXiU43M|)HdOdA7d+vJ$8ml74&B^MgVNR}o zPUVP<20VweYq3GB&8FJ)J0}>y&~KSvm7Fu6c0g^Gq0m%YIijjIsLh=)>uW1(b35l$ za^<(p32q+X9Xt60@f&V5tzsL>n183O|3NGNllJ|dmi+BSs-g+i){}=!4@FBiL*++JBx*F51i9MxQd&X z(hS6e5!#hK=w9F4>YLj*yUOFA?i!jsiLOK$*>{I-c&XHTS{=IaWppSi;3_o0L%d3S zf&w$qt1`2_0cN>ZZO&tLKHsY$BI;l|bO;?0?Ib;~G+WAkPJL;&DBM-g(;uT3$C zD)!CE)MoB68*0;YvdKBQ+Vm)mm~3t31Ey(&Uz?j&o2kw2oK-#WI{%0}PuZ*M@7i*6 zHgR)~G4*X;JZ{eC#fr8$nPVp(DU2~272KQ5tUbjH_NJ1KrjgZds@dR8LrpuyTnbEL zvaSKHVa)odtfB3ZtO|Ep+HPRZd9$VM0Z_a~mpBFDy}Egwvz??gP-pD4Yl3F8ol;wy zq&-7-ZF%jTwyWekw9}3$MTxtJC@wv$F|FKX1NzSj$u-dXiy6d#_`R9_M<%nk^GLP~ ztN26t?a;`=nzCTPRXYs&wf;Rw0n=*z+Vsv*$IT{AK!tzoXe zc&jb3P2WUWYO)&VQ{RNBaT5U6HEu%GxCv3?CP*=F8#f_p+=Q@kYEsrDUcgQ6k8lX_ zW+R3mT(G~{m zdeh`>V8CuP3%&Er8gG-?#=T47K-ziU?K#QtX5gO9zWz}VAaB~mmTq#OZUF`=nhA%& zFWtkL7T*IALJvd;m2JsEyY?N35PBd&=z$2K2O?^@9G*)ZR0)GGy2<3}<}YTLoj*zI z<9UBD^NZ;w0h=Z}i2GPXoCfE8EG>U2wO6=d=#4$UpfmJ0ju%UHt-Ix|<}`c?F$uzMwj+M#jDM zz1+LCOVTb|@N0Z#%KxHW_;@?e$_?C&w(n4TOY3f{yhLp}uD4#Zx4WK)g`0M1yvz9O z&~Ex=y2B>5-E;QPaLYC|-O<&io#zxg5Nui|J9eYf>xVaB@vc2#z))PG-#pa4WBGEc zwOe)3DruXOJ$tPM>?%&L-o2YjH}S`ncP@V@G(8lz9xuY&o5XUxyBruZ6mzJX0@0as zoM+2DbB--}%|%vKYj@4jZo8{WJ~(rZ{RG8rjw`;=ms~^z=pt@ZTj?-rcVYt!ZHA!S zF40|=AXaa!^_3u!6jlMY8SBgJgT*_rNN4I)qBIG8^F_<_pHS3iMiKO+bYOCSM;GIF z5EQI`Oj&}I$ySa(N^GZ@JuR~|O&W?H8`@bhsmVfU7}w76B#r1ef-B9Wb*_NK|6;f@ zqk3BhdFH9iT6)i??>tLj;`seNsC zZD|TG(z=)yUR3Df=A?IX1#{e2ZccN2>gE*3hi*=utj;}^*^#XN?Uo&>>K|^|k*I*02qP2~5lQENMIB z!^mjTVkzEpfbUTazB@yZ)<(mPPN5giVy<4oioVXwVZob=sp2e@GiRXzITNmSAv5t3 zWY(p~qs`_Sj1fNz_x(J11-`_+uj8oxecaW5gx%0D;B$Y4pVM!+_rEc){4-nxuIXMH zrmo6tg27u4Gq=@iV9l6fwjcuxDxWEzaE2|KLntq`eWeN)gx6NShn@Uh{Q;3TQlMY^!_AS%=0!tV5`E zBJU+>-3MW<@xBIzq*3JTG@$|vZ5(itYH`VGbpO*HR)?(T7VbHJG->{~l4m0e`55Vt zN`EFZl{+^zjhVktK5>Z5`o`qUm5r&Hs~XcY=cQ^=6E3SsPrPhbO~tU(WxM93Ck#vP zna8<){9%4oSB<}GUS?QDO~#&Oo07xQ=QpK>WzKI(56hlk7<_icrsq<~&KackDkSvL zu(uOnaGU7O*+_+pSu>Z@tE*WhTUcdPmC7hmOuWTf9S*EqNNBI z6KniERhP|64@+@Uler8uR7}_f7SbFu6KYb!{LA)$rwqq**F2%Z7b>zsMWz=jt^~}h z0O1H=U(K|24TSS*y8Sf(dM#dc*Fn&)g~YCh{BMY%qQxRWsA#cB5h~W$#tIcHZNr3$ zrM6)yN}gw%ou>ThAu1$>3wb_ujk74rP-C@4qfnt7pmr)nCUU6Q(U_dRs4+EtabtS= zoK$s7{k}ow=%{@=t4A`^?VOXYXNd2sO}8-*rt+t7hUw0l>`vr_ip;+1j6DY}wdoB_ zsoKnjrgUv~Ln(#aLUV4VA-B46%KBmg|#1OO_1SubZL2=*^ltGYj2&(2ghoE5*f~H0YniU~vafBdQ zwmSrEjv;7qL^qQ_tOo9tMRYT&CfkbSt}!Jn^B;()d=XVvM3w19RPUq1-w)m%gaAIkh&aaN`$0PW zLv-(lnPNV|Wb_b|&qq=JeJqBvD=nskvnwrmiKu!k*=VeFSqzA%Iv8Ujs#?=(2`EkZ zTSKBcn@LGnhCQYX5nqNHHP#$aEs1cZcpVZ_Z!E&upyWR?*wWW9uFUmq`*w2OIVW8+ zpnhM?KsX9+wbfLI58G-62&49kieI~@?gPIm>5%GB46YpB^OtFR>4%qvLD^_~@sqUe zQDDRO*gVNJ`V)^^elOX*tJ3VXD<&|UjSFngM}}GtFN;Be9bZ=uQK0&#=glc|1GAp zZ^v+ThsBU+?2c&Sx-}e^88DfjxULN+t_e_&o4A&;1ZoUcz!*)pnDm*RnkfmF=i<2W zdrtn_GoMAI)t|heOQl3C4I-moYeKxH>%igAq@qM>B$^<>ft0HWxpAmjsmxESl%nZe z9ahUu`#hbk)xo86(tGQDg=HayJc*ogXgZ1GLUJoT*{m10WXx9GCYkI|8=A$3|1z=U zMFfJg=0TF)Dy?-z-tjYXz|7n7yrd{+y@P|D3MWXQBd{EDmSx4szO6Np%xA+~-+g2R zuZtNf_b#Bfd2&jYi(tf>L>0@Kkv)h6+g-dv_HvFylV-j;czFjBtnwBjn#%}S=6>1M zD_c7{WvGp;ySuYnc?u~Rv8!)OnwTlD4fPTFi-j2xlLxwy!Lxfh4pMKa~+ zzNt&1Wtf#|l2WE&={m|x)`1*Mh-Pxuob8)SLO)G}w1BmT!$iaTOU&H7cxsJl)*tTd z+}MrDPkT>fx=eg3E5F(otmuld^oBjo-YR-4o(GDW+tDSNLA0u2VS{Z|dmH|;V+9vR zt>lI(6L9TR6oU&~nL$@N)(FSbc~*IY*u33s5wU>gc=maPZFELZWd-OCy5D-)LMSmq z>di0LXsL4dzV_XhFBcs_qPYUP`nI0R?&=7$_OevEFde;8C?OBh(acs7)D=n%%U7KI(5#w2%5x zJDZ*+apYs9+o^wyhA0R~u28Mpol|VBrBvOFI(Zp*SZ*4S`zFESt7$H;(&Pw~$q^`$ z66D&cG@8u-XzdK(~ax8OKkilhl)dK!PYb4^`}F(oyg5!QH;n<*Y0R*g;1%l;t1 z{-Ela`g_d4QTLe2QB6q(dTqv(*#qCY?qfX%Ur$+DvjTlvc-KLU6=1Q>|XOOw7k+~X>xuzg*osGOTmsR&% zHpVt%=dm4OWCw!CK3466tk6QeivV{xLj7S@Uq=OpS-~CZ53_;`^}aIrCQHDc~!#okA&lIfa4#B z8~ZeKzlGA#z7jz>+7~x9o4N%NzJnvlR6uL<#4v|@B8H$){q4e7f^x%fE$m*X^{%Ib zuv`y*cW&4sMM+T^UV)M;?2S*yK15M#0V3=pgH#dc4TCH1_DP@^p@YBdE$zh-ng&qmLMw_BKZY>3Jz3ebt+!8;~qvOJR$y!Q_UV72slm3M4erx!{s?f3n8YbD#Y|u)0IQZ zb3kWT=z!1TRQVlFHHUl!mL8xeVprxEkexYnBS-Xv5Yto5H4IbTevQ7rR&%O(?a7lT zi8Y;r_IY`vd2PmAuiqPVxKZ!jltW>6i^{kab%Ji+o-wb>m^*SP6K<_Uo4`i)8+{D7 zPLt1coYG6;GH3L%h3~~P#L*0!DW7}zvVmc`u;r}>T6eqDVxi^we={FCEVO1WVnY@P z#8YypMcm_^uMEf9H?LZbH_h8lXNY_f$`S=L>H&wk<9HW$?|6Hh5 zQW0N1Ci=7&9O3@YOtIlJ6%Opp0UXUjUCh^Ekq&1n94HQBZ&BpUw+8HQzFmrZl1ZVZ z4VaO6{~hMcSByn}XMn!zXuz^Z&G(p=@+|sH^X`BZ@jk*G6&QbcnRe}6cDSRn4W~R* zWAEkThQ$r!@oTX1WP<`}>cRa1mT~V>32*n!`vbH_ACR-$cCd?NMZ4c)j&HcHJh?KA~;{1HY6VWw@3$ZLGFK8Qb^K45?ju*5^P25 zfdlPbNE!7=v@yr7qTXfNJR=;xR_1dL7o6(-%pr0Ti$L(b$ZsUazs(Cwg_!V&xD^^@ z@;;<8b3%IKvH8WlG`EUuUAmM$%?1p#Wk zEiW&-xh4t)t~{MFPgzVctA!=UWw#2+ZoZY>kk_2-MpWvYWVcEyy9I@6o$R)ejya#I zH^H87hI`xsXSo#)a+?JuFT3SInH+&K8Abkdi|@&>j?+WgP2h>aok?%FUQyr`^-7M8 zeJayb29Z0c{lXX`a}gq4b0wKI)B6eA%LQ5RxXlDB_A-~31%1gM87m8BN@c-9&s`3P zowmZfEa;lI956#!a0RDoha}i$sK(V@kP4~WNrg`O>n#;lBNYykR2agSmkKjZDg(s+2N7bpqNkJ-%qlGxuVHd~c z!;F;=ZSy1_hRrLI4|xT3LMR`qJx+}II0X1G=a0~qPa+UJ225B9^E@vf=HZ>r&QA+< z3%0y`xE=y=1Q(YN^VoIrq3UwN$LC_buN((%D%lXtMkVrL;lr=TKE%s5{BR>9$WcgK zK2#)h^?qeuK2$MoI{P+oe-_eObLO%R4BmAd~?w`dFJ2|Xex|b~xVy%2wfJeJTN+GR@ z_J8;Za_jM* z)&LmUs8f5HI?{0a3Da=GjMW6DyRng6ch{E22K)=G;>k5_5|f6(96W`o#9++9>o5nO zgE@G!`B!!o|BW4te`kCCKWsDIeA*K=vj=+DbU&EAoNY8%0ap!+1Ga>7Qe$JCphipb zSEHM$O7j@HJd|%(ZWks}*QX~9;TmFSjjso8d=+DJyDQi`i#bPf;)B8_6G>ft)6?%W zwJ4be*%|zOCd==;&6zQj*5Ld!8kegHH4`}jw-ah6aU!@TgjgPRH%k_X%-lX|CqI~q zH^>^uBK!Ttn6FJmm~X1LxVN93APu%U!OcH5bwR8HoLyspy+@U4@CIU+QjLAeAhX#U zj2+4lv)`*VH+#c~_&OZ+c7%DqH_{@YP$C3Z z!P>Bb!9^83VWy6 zvyNZeoF@4w(UUbzE-aR+vyOk>*;wab58%y>RXHZCQZ{d`Aw#k&s;H3nnZgUj+4<31 zXp3>HV>&FxW0^lcgfh>qSp-`sL;X%PQ+my@A}Yfo# zo$dUUu=AH|TMGkgJr>K@%<{eiswM)eje7~xUaT>?I{84h9q$g)dC$B=3vYg>uiY*OLb`W%`zYB ze3R_xfES-^g@N)SR16?QgJ{1nbj*J7Z>)Ahu{qGgtRrC2`osHowfAgs`)4)lx_7sB zZX@BluEU$DL;D2Ov~ud6K5!&mPkfKBNMo|1RAXYzRozCPc0~KT*;l%8SiJd1$kiUq zVMCRZ4PeySvA^RG;#W^QZv8mV>y6Ea4;&zAg!Xz+g%QA^NTpEUNBbgs(v^YCHjx?v z_!9Qp^zb%WwaOUUeD#ern$;Sw#7V2E#cmntY7W0XGKr5sg`4xO2&1L2duPP zM_gi}u3{x)$rd(&J&V>}T}#w>)FhrTNe@dIO%5v4@qK66vfLEncEptP$g%pPY+0kj zvbHkSi^mavlVFSH33BB0Dz3#A=h*402(&7c=nWQ-D)1mHNktEoTIl!0j zB*|NvP7`CEV0#Gro?^(i_kjf$voziYx1}b0>!A*>^Yu%@tuvg!=Qq#MujpVZT=OF7Hj0cV`Sk`SMhs_LU?;G9t^vH=uWUAEdkwof1BYxLV2t z-!y(jcZpOfy?rWHMj}L?TjyEeikGbK{q>g~9tp+iPwJ6=OdWO$zGT5VBL^c(;>-#& zCdZ*cXJfOVU_1%F@oHpUShj;;=F7IOx^&&j_2+I`4eU(ICBg2ErC@&-GXnukK)+&_ zM9b03*j8xl&pKlp#wugy_09X3!R`FL88B^p6w+ygt5?y|Gl``~>k?s*?E5sUXucm= zI^R14YN}KY?|}i#MOBy!&*q@9lX>H5*+We2*Tj1&?Wk?^DvU!2$Wl=IXVn#_w$pLJ z&Wd(BKtRFh0@0=d;?TS@5(!Qdvg{rc%%x(h@XBH6V~xSojSi73r)gq7W>3;<9Mph= zK{QVf;<+2kbJgLvfaP=( zNxN}hojIcCDSMGTa{XJiOCDZ^&)kdl8RaEe1*B2bOg$BNnlQ?r#>kuzgJ)d`k5&=d zHR&oUio zN}$ZLv&<<&YnIG5?Kq)|<*haBCoJ~Ok{noElrc;7+pNPf9k9VmDks_yvr-3a@P^1S zYqeRUfT{Aw5u3dzhb+Ec0aG{n=6oM%(l}c!%}=ky-1CwmiH0m@n5eOM{sBzs`<)w5 z+yXERek2pOvYU8uySF^UTD%h|+qa_0jmfVZ8^%=3o3G@iV0Eg%aoAAk#tdiB*4f#aZ<9$9aJ&+o3qg@aEWP1<2T>`?@H=-t^+UMc(Nd*ZHO`Kv6c8g}%&Ed`cW(S!Ej? z-!L!UVl5X&5*p9%7@h8x(2H#4y!eh$Uy;M|rmSU{eYe%+T-@Bg+ly^Dy^_0gWp}4w z@?1JCv5U@9PvOLz;hWNF<<%B!GBre-OfuVZE7?TW$4Qn&cPp81P5HEvLC$uukiEE5 z1IiRFQTJ{sI+G|mQz$yqH5JE=Uq@OjLZ?4&a?fZVihUO`ze$7?2{BR_%%F&wEWl?X z6U@fwM5zGs0!~v{rF_jAU>?P|N_y~4@^ZquX*zhuhwx}?!tutyBRR@em$212IqDp$ zo6l;m3ACzYp8>_zHG zJx@-Uf!bcuP4tDqg{hrnLzT{!U8gVum|E|&(fEV0tAZgm?EPv~N^4Mroo7L7aFs@& ztwbL!3=Cl9o5ESb;sUwk1#^Z2c&bB!$0Teqd*l@AQ!Mq|o=jdb8d$xkS{CX>)i$e< z&bLi^(Xeq=FRJ?i#(GvQ%IC`YTAM0r<8;eRZKFLGfY=?teIbm=tJq(>7%}D&?(D=O zw-wd&F3g5@+jcvR=#i-1N30E`*rg*-huW>PCeD(Sv((~3_K}jQQ}sR?(HTO=G;7%> zGM8PMKChJhR+SFnuAh3r5k)unIzZ=NVIgqjt`PJh3Z*gACXkBCOQwF~81(P4V%&(i zoe}doTZEI(^CL#u^q~;Cq$I~|Ikn%cu-KU)gfgECLe-&wb3*6aVn9`dK*h_MZ3nWz zTNRGR0ZpPlF>F=G&0q~UHv$}#E|5@x`Uga)9sm@+JMjtX@^ z-X8-p9bs<1eQQB6)7AFtn{NFROt${c;DvJQ$>CjAIEgP4CA?NorDKmq$U9-AlE#iu z(pb`L=&(7*VT)eDIbnp^rnBujyFh0{WXCVqIRM%S%<%PMx*tuuF&CI<)D~ zuEQQ1No60*SodM%s?)IdiChD0!YgNYg3$ueTd!*EX8 z;tVT_gq{T#zGN-v)N<14<}!VQaGKafpxQoMCW<(>4m@4@-JbLMY*5@2!f}0$J1%BP zOH{L=04W9Yjb4yd(@c+nB%`o^kn zZLZgaW`?YP&(L9Mz?w*yaOSwcG?=kA6Ko)5eSlvtPsixx={k_H+&5i;3_x`;ThI9D z3SBPItIawr)2qwPih$*e!zvxlHLG<1!;||t$X^kIs(u>ffNoUd()NamguX2#i~S_Z zuzv+zFO-1Y6AO?_St|C(B`rnxwCjnCoVBhbgm!)hF|Eq7I>8EQn20Kv#HD0tYI?$W z>_m)$lW|k{MDDvOe4N#wPU5=Jz0Ddqg)6ORs!hw?09M*YO4XTaO0s6U#YYM{$RMoo z4Lo6Ah&YmXP>5wpH8#lOp*Ag#2L*BRn;K%dcLasAm6=H0+IXH!4O8GXr!sjqQd1Lm z^L`HFBFM%?kd3pL5M<*ljB4*V3u8J4qw<8#2w{|Qhgh)rYzk*teo_`SOSLPQX5+tB zw{&j7)X7pEboU8^KDTvDyA;StSi3ymrCrn+z&#T_;!H4q7Ld%gaJ!{@ehBvft3Z(l z%b9#@xvi-(;VNK~;SoY9+aY1TRz~aDx8c`dJs*Gpov%}42!K2vnuQOoM5`oZIhLa~ ztW;H@o2e|RXLeZ6dtfJ|xRQHH<=oWJRIDy^^S2CzQLDQB2{RyN6y2gwvtziI(TwE~ z&4ad5^HwR!5#>5ion(6^VSC1L5-l-=3=DyYF0YKo0xPylWf+v#{^Fe0qHoEz35ua1hN>`{babK}KdO7T0Ppk1-zt-2O4 z%2E(hG{tI`Dd~#AcJHHiSL}(EobL+R8LbG5iSIChf+6`GCQ-TT%W17d<` zui0U_{G}+7V@k%{pj$U)%uV_wBXqZZ zaZb6_+?KI^DLE9)cVx`#v&ijl(1C=|o=pfXdE?De>E5NoTXMAgZCMU)&th8tPF=oB z502{WZauh%EX>kC+^fTV8S|c;d9S&@Qbz6Av&$pr!5m=_jtK1Z;_Sk<2jZy5|TBVPw4Q7ivOhQB_Y`a^O!D=>x`^qZ#5@;^F)qm^;5oiQa=0l z`eA0W<>tN|oZZtof(1OQ!*e=3qIVwm&1Zaq#ulWMm$41j&`Z!L?mpy;|Do(%#mTo$ z-wPpKe^_AgJVzf4Qu`CcaQvb5UEk>ZO?VO&b16Z!Bfinz%<&_y*JMOHDJC z>H?%bQndfqr=Y56*P?>OThX>xnD0hbg8wI^qN+yWR7~@`SA-&l>#Wk=Et%thB zIyf7bqPwhEUF}CuY!-XNh26<*Wj|`)(~9!bav&k=kx!i!n~eo);T3AL+qiwc05o7N zKg>{gD!&%cI5+6r`Hpe118>NbRq+927YE8mEnGT3MYG8Q*JZIKy~rytY9296SZTJk zb{?i~I8wNz+G4OmKm?f$=_=}W`wV@I0Mxo$!!9XM%qDyp+k5gnk^|26!@7=xha9j4 zOzkI+TN@dtW~^DWK{?%YZtYU@3f@af72;*T=!?%QyF|?r2Rg1hOTJW zqHlAH$UL@p&aR224Lf+C!+B1-#gtXjBEnkx77$jn+-B$?yG+w%vS(v=FY?tabV%tC zK6QRAd28vPG*ZrcYCSuBe?~Gz6X1QhvEtK2@=e+#Q=FF1k5!ak>v5&SO4$$k%u1Cj zB-A4NR&~WAh7?=XevPtgkxxt6wX!a|Rz96pJ|`7rr)}A}zJRGIls(v$U7V~-*}1ZX z;{I=MA2C82Yib`eL#2R7U_+HxyUz4JXU_G$g#KnD_pI90`!bFko4E3DG26lsQXn** zvq6?_ahT0FHT+pKq6S+Aaz44NQ*|!uRK3%$7OGf>{Z|W0cnr5F$Zg|~534Y!dt5#@ zoo&aRC&AXZ)%Hqj54-Q`PXy?9&Hu3Np!+YCxOYcHi^(Y@|6t32JoyAHj~M$C(Y~~XL#7EpG^uA!sBLa$`Tbb zS7~tO0ns@?G)=n)^B*(gl@@1#y;{h=`6BY-*gBU)kkjg|xBz|^B}CHp)-`sIWD8i> zimG}WTKes1Q+BX-av_Mj2$jaG(9Bjm-C;n>^IcdkWGH0-@3!+EJr{NPFQP90McCzIkVW?Z zZIUN{GwP|%Y;3Kxrg1pdwVrsF@&^=#dN9i%}=8Qy;2k%IoVJ}HBwP+`XxlgE>0 z>5el9xjdhFY8h5K%OQppkDHb14E5BiX(Q6ft z|Ar{IX`(V!nd|Ag8|b>5P&VC+g7FsguD7C}zMXx8H!usok)ih{kb5VnybGkfg{Hoh zPI((8zMal^2PM6euij<5Wi{wtOUEm_Mw+Jubl(=C`?d((w}t4QK<6lVO_Dm-f$jsK zQ0T@50d%7b9C#9)G%=a4@J)~XpS&p~{1ECB5Q(Oc!{#geO5b*k`786~(rEr~eP3hx z7Ho#~*T{>dj7&KZu~;c71>tyeK@O;P$U^-9^~_v=Y(j=`}j2fgjq z?KU0SeJL#m=$*YPVxLS%ZDAd>f~-n8;Y_Bfgz_10x_xs%chq4063b`2jpY+h7ckl? zNl9=7CuL*Tk9B-bVbNXD(L-K%8+P7iQ-z7^r&cMZH|#Hu@RH*8^v-Hrt|x$daCc9~ z-i|JeE4%h??CF*s2%&FW32#+~eLOg0qrf4SWsp&(<#AN_c5)t?7yUToE|*MMQ?AWoSe+U+lwAtLpp^Ga zA8`6yr&%r38?m){b#AG#N>2RkYO6xE(hHos_oABCLPu|V52 zp>5C1V)#(15pZ*cv%Im6e)i*-KF~L}2kgz=A!uJ8uvop(GyQWU)MsysD<%sudG+W{ zIZVD8ZJfnsfs9ILQ&oId85LW(8LYgPRy3<=th|AB%mUfT&jjZw0jY?xElDy<)wvWd z67`ub@trNoh}}RokX|Ds_6C#1{9fX8Yzc9*Pys9PPbJDGTiXx;`_ffqw5cNP2oqgC z98l@>IFw0ZL%G?0La%A2?0m0zI}}L!P_m3FUH)6I?3mHKZOyWljVsnH!*2^!q@p3M zSiNO)JCTbN3NFMM8tNDgf~-@xwZ`T7A2@G?Ypb2ybGhsaRv=@V`U_wGUT5xV~DeP zPbvj{FQXY(noJc`T7VTt0!a=YJ0+npVfXeOe2NpaC|0%&#>P96QO{xTy}0ZJrz$&L zTJ|!^Uf%Dr6^JuwvG8V?ZCPE-7Tiyo3d9A!W!jTuGa~sn|A$R+KL`vt%#n_pU^^L^ zt7ISSEY94<|7u`ZLtD-R+hX8NC>YOa}TH$&e#Kp3gjXU9WAlHCkz(cU|DELRm`oCJ*eQp6^x0)))r!WvG_ky@MlH zSlT{*g~8go`(m+iLni58H2DUTF39;mTAwKAxBMJ)03ul$Fyh75dJ@5hCXhF z9XG>|A-6wgMjSPRns_wggc)gX4$xKI38lCmPVg7B6OXyb%dWd!-T|de+lxcqK72*? zBeW~yf=dpDkLGSyCG#ylAe24rbV#%d+&o9mm+zS%M)_Xk9u zLW2oFPlMega0d12U+t4-^p5J`HDhWTlpo=7Gxn1vD_xMYr*Tvvhw5$IBR5{LZOOFd zcfW%$yGDO!@IvWqUTy(KlUX9qotH5)GG?Z4&divzGGPE?t=3x z(en^1BWo7wun5ONiEc~q8uV?bjF_bKGAt~5n{xNj2v(c{yIZ?fwC_fJAFgs`7Z(H? z%Xywg{Ema&hkJG-Ae3QNOMRKaQHj8`9QM;O9*(P7({=DrYnO_f5iUr*HwOx0KOsIws#k2 zL8jC9ce7tPeSdpfN9**<_V1bA>a0hCP}7nCXd!rtHYzEkGAGPhpAqO4Dei#pRgEJ5Cfw||-)I6Or{ z(LeD{a+627!PQ~zYdt@=vBs-Oq&{tuJCZf2&EZKpe^Rlz#%Di+?0bXlCd^>O+#Tt` zrgF2F#LmPW9?yGg$=%(tTvMUWg=i+jZ2(vRxxZ_PP|~-A1>rt^wSdlOdd_5znyF3C znrzFoC&)K!I1XTle(ycRk$pnRl#>s+-_%24+z?ge0<$e;S!!l!UxvT>N(x_vwvE6! z@Ci*U+tAaTio$>vZy5Y?nb=LC;RYPVQHBW<)KunEXqgLl{?vraLuxXE$i5u zw@Gm`Md{s8)GWOnDGhr+NFG!oRqrr=7|w1+yV=uzWqZ%Tc9$|HR9`AFtKW5ae|U3$ zYX_RCVw0S3@wC%NYtLTRo55u!s*=?+$diGVOdEU(Hc*kBXX>yGfNYrO0HuDTZ00Tu zq&gQT&EXQ=yHscF)&y+Sa5lq?^Udx+do>WZ)p^9G_$bsgTQqb?!x;?$8x#8iww0tw z52Q(Fk~kH?7OmZR<%-jul^_>(t5AOnv`&q!yTD7B`Db`ui&63&q>{RD3zLX{zE|{eM@q~`jw~>4<6oSjnU}m$Q)`!6wtXhh!t%e zVmyk_kpTqWiz@BRNG-u&ej?U25EcPZt*xc7ggEQ71e;pRE_iBPBcXGSn~M9)BzDN%S-tkh{k8cg zCx6_;>;Af9C%>x)87F)f@T5SKuL8JB0O(TiuoHoGR}8pC04M!`+{q`}H8Bs|G=Nk3 z90%NI0Zw>YjG2I6cehg8#JX)!-QtmT?=hoC);(Yb)QxOgII4c<;$tU&JF>2A0Z~mC zk?5D7bZ4O#UG~*OKz1eWT35j*T@CuKf!1C_FJDW&*MX+j!b9AE{^~~bLbqF}olnQ& zsCShBQoTDA#7^T(;kPF7m0Z5ml1h`#VED9c_ObK011Cs$p)$=my@N$no7~P{Zi<=AjisQ>}9=j-CAF=!W(sLsLU5 zTIZ&Yo%}m{HpFE27pTR>b3V#u<~=Zg?*>qpBWH?vFNnS$Qhq7 zH(wo+(NW@DI|94g_?$dkAVKb<{Hg#IjSus7c;57IV@uO|QCtPwqy2q1@k;qDSa$|` zvdsEpCx5Qn!P;qOaM^O~dl$k#C#_y#NBx7hpsHY?WmW4L*BM9j)tD+X^N3b$#TB`9GLxM>W@ zVvzBNBx@`GY?8j2&i|tafFGwX)lgIxUvcRE$ZDp9fg)SEx#ro-pV&Ix%m%{>v1w*w z@eK#dogsS1Bo`$Yk77&=t4|$SR6jCx-Svw`re3pvyw%4}{wg%EQL)bN^p{lfD+>B2 zy6)GI$ZufPe?_(b95pe;Of0WXvF>A{F))U1*BB6p8VzoOVIuxBVSCB4Je)F}9#?I< zKLl=0a$#L{<^eM^RR?SpG;{GIrus=U2z6);O0dB-Lyi-ln3LmX=y5abW)Gmh?i`;A z^UJ?6i2j{P{y&(9{}UAa8^rjB2;Kk~g`NaY9c*IYl@VF+N+0b4VvAhKnC_B7D<9Qd zZY0dSV$S;M=f=Kz6pC{Fjpv23jLIgqZo0c#JBwLs4<5MId{&3g<+tt__!HIqQ=R=RV}72KAB1oIK?&SGufsoP%rEu( zD;@qRV}7mQ-(<{h_505``84R;e^nL#yQ2BWl>e+EbNm}UqgVbTWByZKQ@_vAGk?@u zf700t`smN9`Y*b7vSP^-NA>S+?!%{Y@N`J%kklciLptYGc)ku90QIth_Pe^w>1n0! z1(fYo=`g_eBt2RNYfO3ZAm3R9rmy;7J8^3}x50zcja%2OfHOx9D2`ib@dyEU6c1a@ zu}!HZBRM8a0r`>)E3y=r-)Xaw2%Aln6yLpHMiknAt041-jXmMPMGwGM_VDiZRq|x8 zYBw1~6iYOa1p!j}U{A|)r?&Xe(xEr=nUW|1n+#O^WJz4OuDxroIBai??^PpC_cl1T zr9|kKn4%5%(843%UgSS;?G%Tx20KofU>&C%aOe_G{Tzr7?Q6w?yNo?_F?bigcu^jb zL=@{H#=ci{DUcY9xeUKw3?9Fu%L;qQrI=T%Z^wz}f3eRzJzC zIKoCdkBUe*=m$@Js5{cpUfG^khB{d}LxBZ@Zv>reXLr}$@J<#^tXUrzi^S?*AGe#%gzR;<7P;zb7sR(0txNO#T|Eif-a5e&9N zi1Up9>>kaQ?mX4mzK2`s9+D|DtP*<|RE(cJdscCh*qy&x&PlH0U_MHqXJ&Iw$S$I zBC|`Xn>HPu^}RuXxdZn(G&;mKywGfA(hjiEMg#K}?Nz) zMP8HdO$)p;yy<~A!-{=7j`-xQ+{<^`na!Kx{?x8uky_u+}E+^(58;P`xx&d^2UoZO3-2lpLDWC%ED8r z;$ju9V||n!t4ty`%8hB>%DfONM|tmbHE%DX$g3}Q3Ud=lp|%*kR@rnt8W% zebINWDm$mQwI5N!xlmJ{MUi?iOj|b;Q>WhWf#3;c=k7v7)Q!AW8_`I7l9$nJuSOT` zW4lHzKp|LbZR^tr&CU^q_<7bLh7fw_*CQhdgi~f>qa72tF1Z~2uG$l#XjJ_tfY^;8VaCM*YbH_}@ z5R+PZ%naoAyG?fN;bmhEFRe`Zl393sDhr_~@|jz^fEisU{GW!((yYGl z1VOTy`I!)c5-W=x7phA%SS@(xxT{g_esG`6zABH|BQ{>^j+tTZoq(8(n;HKQ!_s` zA|0Lcb{-V`Q?8qHP?vL*5sF{v9CoLflg>g2A{L9^n#9j4x$1+li906-DH3f_&{^ zmE4E&t&=bh`^hcVWxH?>1hCb1;pKGU#b{eQDJMmlN#45LQu@!LZv7dhT~7DVDU^9R z-K8__Je+3^g+<1rEBR9L zCcs->L3r2}li&Jq^MOaZ%6*(E{rKv#AIlTT%|{FD(ARv_#0)aTZCV%VzBbl`_*`_v zHX$KyF)Zk5dQSsyGgfpNA%)oj54+kOTnkCQ%>iG}EcI~y4UFiP<=+^qoBKQT;#hsX zzoT`mUKSZU?XG}wvDt0_w>P05y$!AI?GWM{ERdQ3s6N8;W{5y|);*DhLgh_lQ$Um5 zM*d#SQ#S)(8z^Nfj9l0j1;Z6Rk+%(9B_@2OXt_6rmTXj`A`ZEpg*6U~nhOC72IYkD z537+U%!tzB?vHg+o=^y_6f1;S{b3=>4(KPbGb}CS!Lo(GqK1%s9!P|tixmV;y`vCw z%Ijhw((f2`$k;JFQW`>kY=>^{=kADyZTI9KjEOzai}7nJNLtz*XIGGqqKW+&OQ$v+ z9zjq0Nhb0~nZ_Su9(mjX>{h0WBP3qT{*0E>(M!wWR2Ip@BuWOWOG??R<`QeipbtPdmTBV)Q&4d0%AS_)@H$ zJyAP*qIPcS)y|12g$}l;dpJ0UGDWO=AHbJ5hTH z5=LRI<)x(SC)1#B;c_e4f)Fm@m7VmU8Zcg$2Zk>koBT0Vqzd3El@QiNlC%ttY$^d!`0u ziCN|iHfy~h=wOD%`sQOH4wRAoj)=;b>s~N*sY&u@cs+}o>z=d2?y)_gO84LHb`qn- z+N+PDw=yi+lU64zVs=tAUD^wHxi>lsDf^$>EhE5U89?N3*`sn;qB`3{AOObW&77Y$|DSmBi+f_1aAj zoIQomvCUc#)8I9N$lzLmu z>%8se_1-1sUhh)#aj(^)R3URa*JC<2T<*iq&hz?l%ZWDSto?(ue#ydo76)6>HMZ%<77{ay6Qq8 zve;3)+w~$|7LNTRyfjpF55+=7HhrpNZLE2y*J(>}Yu^B@FPxY@fSlRl0X41pOlwOa z5_p66^ilY|eH4D6|Al)7ig1Km)=qPH=>Tw`ABrJhV2A*>z9p)b3cU~XRU+_~ z3@KOQkyDqLmFhEuk+9j|*O*Hu4S46>CYvQU?TCjboiJw(F=xH=J~LinGs20t=4@u! zNyp8cnz?R9e%#D^hF8zQ#Dt&ucVhlwzg>XQO;`}6RJWZl3kz$JwJ!Ev!+^gQihivb z=3S59#EoXLcavG|-C{O)x0iK`(tgINSj=>6RmPV1AbyC)#Vw8DnLO=AA<5jFk8i2tD%#{=cFeMPwzd)A2{6a zJ|{aoedpZD;n^wgyw_xEQ}8xJD|1$$3V(St#4NeIHkA?`9XHK*Mp?nwjeg5SdLN?) zJ`S`Gle75~kklh)y!T1+w?1Z}Rs;^}M%5uQ(%DBj6V0_6P`W72$jI?l3;72rqeK7%y^z zNs)QrjAO)MiOJ^H^2+-ZEFW@(_cUzJGiIFk91F&0%rx)wW|oIgF+Iifpbr)8^ai@2jFzX&}X*^-pA2%Cx zPJxQGe>QTmBRj{s{M7i~&mg;>6PD;7%-P;QnmOJtqk%F9f_cJx$`V?$d6F1DqLumP z(~z87lF%~0z^A0odm5RVc*P;$b1en2ib^KT<)HY)C(R;VWW$TGQZ9Q|w0ElN(Tp#N zsn2mgVLnrAU3JZeMt$S~Eb;U=ju%RIrw(-Oow0F!xvAasqwU5@R1Y`%ifcX5+^WNE zL_f%(;D4Pi?$Ch%*HVYn{VFEw#dRT=EN)N zED=X8TOR|X*nRoPzi0I}pKy=L?0Sa};0oB78QyEhK6;GCZhyOr%~*D;yVQ&cV~Up> ztvY#VKdRaP@o>{GFTk{Q5ABUt7gK`Xa}EIg3>f5_cR;yN8S8R9ib+*8*0eE(fph72 z%u`0M4Md-vb$IflzPTrWwSAAax{tC$cb;`Jx!)WOShOC1$gJz+j3_3wHoN)g{Q>LQ z2Y4TvYzW|c_|`_d&bs#r`Je(m7{Eh(xXi9#z=Hlz!0P!in|6ng?E#DBCjyqgr2$LA zQr|opunZj6#`}ptvtOWzFJR7l`t)Nx&oR#k9K33?9SRUR1F(JbY``S<8REDH=5yxr z>^BREC0?^GPEiz>p^!5x+7BGsX9Kam#5l(# zfs3Z7DKpVUl??%Oy=Y$ zn6P|W3xVD?b{`B=@R;~3>x3*Td+jjrkR@<%ivEBb}+ikahYx)1q_uYHv-kCeeV7q}kbMKvd z&v#zm>wM>&uO{+A()#8kU&@$%*QE9(t8Ywl>`GeST$0A}m7d+#lPNeOII{IdadgNQ z91-5eKjvbnG*g%<>@XpdM<=lGU%9%3D~lD%S$qm#usy;lrA|mpQ*l0C7tcvbn3vSX z$;Cw{)Ud;9RZVSCMkGDyj3t9H1p8ACOO9imayY(*0GX4j^s)Y+8IoaHOy@(+0vecy zNr>ZYct4v-+Z@c`b1^J5Xv&Sz=!8uk&H~(&hlVFv*l{)|KC@25!Xvx1jisZZe$0TIu z-yO{gQx*Al$DC4QpHkHpv($0o2XKO{yjUp2Ffrk@nrM>iPO6Vea$R!ovH&B`)Z%S} z^}lj5wwOAdPIn4PfhD=_RDE?08CuVElIu?6VS^SVtz%UAB-1m)uuh%lpa~LJ!i-he zWLD$*wT`*mdWUc&JGl-A)^_h!m@vNu#^oq|t5uqvTM% zls7i;YY2k)gvFh^3fMnyCu+)d3uq$hE$FDSj(8I=gGXDEvZPnC~1JM zlMCkSc`{#T3r_jCUOo;Km}&b#*_f(G-6$W2qB3_MDNl`8H_6$}F`2*@$poI|wwTP^ zi_{%4nYWLWDgMWqvUIdD0j4>Q^pUKQuilPp`I2CwNH2+i_>y$>_S~khyQizWy{B)V zCLM2eJA7NGzFABtLthJ_6^u%}`9Pb^D9-=PC26A?KU-%E^t)soEo$w;6Iw?U1tB!I zr>A9~o%ZxX4-)SE0g;eG26eZP=%8l>VG+!mGPt|D<__(~%b@QA1y!Z*gO$P*#)Wu` z5?etDBsS%2AU4U9c_Ot%gbrX+NHtb`2**{)XUIU4&KEt1a%GGia<)cmgXxy_wN{Cu z?%jPP$xUj4M0{j}NlvR&88Z@10}v23kw$)*qPEre%x)S8VK~y>iDR$5tFbQ8v-yT(yU1w~6(d+83t$-&?@srqrUO3<&5VlC zt(n=lk0lNO_#wNPEYNk=8DfhB&7H`^Nt@;|c^GK#7dsR!+S1w8w?i^h1rFgoQA`9T z_O0|+m`o1VvCj5u>?kamnWVhsPpxS@sUgY}E9SEU6($>Gu};Jp;oPlv!;E8XCXS*; z)yC=6_#k}QF+TX4BzcFh5<-A|zq_r4{QikJtLFFO!_|MMAdg-gXYu^LIK5+2oNk85 zmQSh65L%oC(+6ye6R8Mot4!uBB)EI(C=429lJPdEg;Dj89dKbyj!XVSe)+J^!er)F z2uoNo)u*_+n$so}+uq^&#}zHzW*ZLg&19|7zlEvudQ%+@Q@fhmQ@AWA>1Mj`^Gde? z1kULrSGDh>rkbEiPg>U~5p#`thZ|ZYCbj)#oNcs1ntjM^ij1C0FhJYdF2}<^ z*a@F0NfCXq3^GH?Y_Ay-b=0E}G*W+~l=|DGr3{IFGBP<=-;;!-CM4ZlU%<6YY!uE0 zGElvkPZ?cFZhINZSK`HO)l?iO&*(_fGS=Y9F1tAEuju0%E%B1Y4u@B9 z7$#A24JFsIj4_$vXPqwDxVo0RC6~J;m)j)^_QE64^a5p7U)meKspau z6(XyljIGO%)F`H);~B?_jAnsUqdj^xtA9-{SxqikO)gnYE?G@BSrEZQWfshxM~TB^ zt(47~qpUMp5&XLu>JEo!Nq}ldmmO_INV1b{$hc4m!rG4kKq6VQ)|X==BAHKvUe*cB zLYi9h^5VdkWi0QMEGl72&NZ%A$&#A_UtSXUvh53=+t8Xe<|ysDU{lq`xSC-$ zOs7{zRDwcC%N`W+D10gkVNj4sk1$+(djrTO;2~MGacu3N=qo{Er-z|VJ@Zhx(&elK zCkSUND5Ah{HV$WpQ5ob8j4erHY&V5mO(DI^FZw);t#Ik9&{SCtRl-;aI?_~G%P3MW zlba%A;jh9%IVZKoitsG*>kYT)Pb)bYVKH|ul68M{edH09tCNMiRmmImk?<_%CFhl~ z$ybiP9l z#DlcHAE6PvfvR;Qv)MzC@ljOjCg!>~(<0x>?Du1u2jVkPW6Lwb6UCYbGP~XF@@2Pf z2N-M;{2|*Ag(y*G-VMrQ)&jF!DME_rIV^lUf?5v1rv~$`&tTMw7o9>T)}1OBEU< z?yci#nCFm9Msag{VC$OJG(Q?h5v%_;J3Oh)kE`-j)?&QF;u6f-4*DTs?UmSBOvB-2 zt?k~GWbN@lL?qtGyu1b0N~?pz@kpA23c6Sl-;658<7O0|s^kEwlFKpXa;$w>&E*L- zPBhHhr#$T`>;jvg7FHyAs?+atv2)K)Uz;>ZDLKaRK*qth8Jqb{G8Wd4E7d!BxX8R4 zRlmqbl)sdVU&+U>^Z6iCOo#eSR5HcnNv4<%^`G+8f5`)+i|J6mle6E;*?-H~AK3CX zDtTgts&``QPx4K^m=5(`OtQtW$!$!XDS+)3zmOP)45Z|Q8A?tVJgRbmOkx;5NDR|q z<;AT0m{q`AWQ37TZAk``uO?VU^5c*KtJoSUAHyitDv^7o@-aMSjSz@sF{@mFj+9p_ z1Zbr^J&IWLeB5UeF{>({kn^$fQ600!#jNr2`UGiuTmkcLf2+F~t|n~P)C5_w~3%vu(+mdC6WF{?3Z zHAStJOu!c`oVRWfyk;igPktj!0LmJbdEpkFA4iNE%a^QKtFNMs>y|IqX9fC<4|9L% zy2fSZY)=Qv5<1&SW=p|(^9&Qyd@++bR;Zz#DZQA**0lHWh}S4~LHZL}%=yVWlWKtr zuz?l!_H}h@RK$?OB+5Ijv(YiT@l4h8IAZA~Ba7b1r{D^)6FGEf7mjl>lTV7oT`b+~ zY47!KS`qG$+#=bh29J02u5oY%-#D?+?9S4(eLL0!go`mU+|kAl!#!MUZg06th>^)F z%NoJIc*(SF2+u|GPyr=;w^k-2O~Mmw(#^q z$gcD}O+rJ*PRP)PXTLR)`YGJeCPosMWNzgHwNA*h3us!sLhNE5m2ebAWE<{oC;3Xc ztyOm(E)PT}&^e7vuEhNJO5!LGxARTG{)J5q-p6yKYNDj6mES0+ZCJO>>|T>>Iwr2I zLDxetaqOle>^7;nrL(VhZP!97ZjUzX(rwV_N3AuwVO;BQLYh02*-$Z9b6;=&rQzxf z0~wiYie92CgJ%UG7a;AORLZ!9wy33No3@H$*BJdnxit(7VMM-m&{Zvs~d+Sb+^_1oq=ZDxod4UeI@1;O4wQr|^cb<3KEn_s71P|z# zQ-b>|dI9C`B?7W(lCDW!f=nLJ7}=0IdgZrJE9S~j>$&oAo_tKB6Cg`d7aJ33O68#} zY*8eHF!ce&UDw&WySuxqr?0(jVJ90su;-mmky|?H641JB`}*2@qw1@4%alRg=fSmI zQedD>S31}h8Zn;o8;Z?6^w^UQ$Y;GBEWNa@e?4|8pj&0ntwZGVQByMNRbcNUiGWBK zM09YR>D3-Fv;yg~;>lN&3Q3w!afb7>WOLhEx~WtGWxbt!r`fucr8m{BjjnO#wVXYz zPTPBzrCEYC87@0Brvu`bC7pX(c6PKSg!+Uui-{!*>Jww))&`3l);~}`q_Ikzvo~sO zid&b-Rc@k_4FwZXYjd1nq`T$8Em3PLzgjJEl6tiY-fcoIzqN}6{JZLPri)HN4YZ62 zQ7=lNHKU~M))i4}ZQR-+%Nc33_!}YtPS&Fr@EEkNid#F?*W=bMt254&AC{aFm&or+ zaYasXT0d=qaFdFUNwoAOx^%BzAGf+e*SZ>XEhfrwGSx9;FgdbQ+Sa($NAQ&@S;x-i z>^g|nCTMySE!zXR%*wk@> zj9b@PAC6o5t?T2~0SndnhTQvz5ci^--5_T-s-5E0`1QE>R>v_2@bMw}_%I*Vjn*Ma zp2X#?>SbDhr0v}va>cEW$|G;6*TpN6j~-!LuOwmcIHx-GCui=O`Ae1vhBwhrYhVM^ zT%W4Rn68KXo(p?=y0kXK=9{fsXlq2-;)Dsm#kt)-B>1CpK6k+U;|Cc9lh7%(!npci z>Z%WMXVD=|iRNyjrSVh_kS5v`4v2 z0~wx>9JTWb-IpR}eQjT4G2VdM0a8`vL!Ijm5U}8xL!*8LjamNA_Le>EcFBWv0{Jz4 zEv;9rC9fa4t=*`n_OX^V)ucc(zHE#GfS=snsN7lKO?y&?sE#vfUhh>M-3_~U>J*#2 zZ^jTwZvlIA(~)k;EZT%Oux%rYl{qz&W!aFe25T_V=A2Y^-XFnG zuc26Rg2V<9-(E;8-e6spSwk6xy;?|>G~ovb><}J}Vzpoor5)y+(^|`m`!0WO%5oyA+pTrf2&Hvi6h~k1*$Da*mf@ zsXYD^fr&UdR_Gty8>hrXOJed-C)Ma@vjAs5DUuE2;b(7QWF?4U9is+YV_AZ!R->$O znD!>9Db_?1Kh&sWR-H%ckJucFsUw7kcf+A0z$s7>He0s_zo*tsN)bp%#^*dH>OBjA z$T~tk3rq>92(_dN;~Y;>`K~bXb){m0M0n0o!}M@7_S_*T{}JH8^s2dzPF)x=cjA(I zjKJW-;&z$j^jJ71_cg0AUpwrpeVHIdGQ-u%kK=IKd`?ZMkDgW&Hk9NdPEr$;MNg`U z^|A2uyqYpnhfKO(r=7_yi#?@ka6g>RCQ^kIt#8z_#uG`jT$t}*RhmzVgPWYI3nh(& zsLJ%hl0sWq&`*?Qqg+oG<>8uO)ukncr_>aITox-S)cSLwmmC5%OG^rgZ7n{Rv(h*^ zVx(L@xGYvS_=Gy|1PkJWpEc#3f9%ZFCE&Ts!SjL;Du^ZQ6fI-i9u`MwpI&9|=6 z-?v(~$+xYHci^$0teGg|EbQL1(fGN9C(l!h$lL*sSde^#C{TAbKnYGCp%PeZVwT1MkTft$6bpcbN z`OGO6GO=1}wSvzMW(ghEt$g3b_k+xUq@}%)Z8OSP{2?5SEgDWRIXFgBCsU11t^SfY z;coQ^_wvE=Jc$RRwMHu)-Q|o}F!VoW-A6#!N73G5>ki#uMxgKzfz{nKpqt^(mx&RR zv~B0$X!{sVS}zsv2|ABFUB#|Yg?~|t$n_T`4ED*ts7v_dKMu}0k@wlbz#3dU?SE9> zYC_BYs2XszJwx?ix$s0(os_rC(~!iUxZ$cZpb`uC-QS*uWbS8Ixu{*`B%#v)Rj%5r zqeP=qJseQ!?sT*`rP6sS*|zjb)}-rOsat>B9VjOyz&P2QWL5v% z%GYsBH|IHtVQLmtYL5Lw_H`*l{OwAs6%A{}UdgvzcRr#L+}neeU4wq@MYGrlgzOOu ztm|1hKES3{A3?KjV8(eP5N=Y3$sBRSxbvOX53M`Zzmcfn zjCD74<{s+H-&q%0_gVGU{nkwDu(ibc6gYgwT1PEu<@-v$_gW9>`f@ED_;_l7sV}D( z9GUv^JYBVP7lr8ZVN+kgcb{8J_PMp>YPXhLL0sfVX+1__4l%WaHE_F@>|&^3YDqWV z6UkcACbeWiP%X(fwWRnxwTx;KDyE|R&8i3%BU43)a*t6-lzP*yBJ&)zxiCk%66s{Z z$4G%geuj#wQieeD>Pcc5CC$P8YI4=h&&2A*wI!!47OuLP+y>1=2+-9maDNPSRW@x!U>e_q}pzsBH_+cq~?B_WILxZ_&r06?^kKOzotG< zo`o-2P@=hbm?Ej(+zpkA`Rq291w){1l)Y^+DApCKfc=y=BY=IlQA3?@73!`atp zGG$cwrdBgrHeyw!xs`F@^IDN|a3_02D@{~4s28c+GYlegbQAa_URNpiJ$l}=K2w6rn%QK33J${4A z`X`3t|4d?&@2iCM1661J5K;e&nq&PRwcPqwwc7d-Yq>v0)IUMgZ>hc3+vHaIDJH$2 zA?kla)ITRx`#XsHUD}#osHd%8yW&l_;$7_ub-#P2s8i0M|FkRK)3$i!S0i3IlXfD9 z?(XkB;?+aa`C4M5QY01;tDrq3y#>k1khC7Qtaw?~@zwaUs^+WdW$k#j@+CEltCdn^ z^}uiyd4=s1SdtTWl{n*>(X`13^Jm79@589SsHp%xHx$+cn>kaxOH#c?)r_7=8dqmA zwH52DjaIXbRxw4dCRC~)D+6GLNIjUX7TE@3)(d8RV742~t^u>Xe>G;` z8~`))z=K(nC`2G;cY@i+!R#(Dy9dlZ@mFK^>HwIbJRZzMzKwyHF#r#J5zM{>W{-l| zm;Y+a$a-Mw*$!kat%c+)TIs`QrQBf#KhCdU+N=@!I-320Dh|D@uP6lArGl4K!|^vVn$VIm5=g-EL0Cf#tHp zI_n(m+6cj!nrOFcqy~>96Aw!D&}O61_dFPnb1@$0X<3Lh9UY#7C0W)!>>`)iNDnJ# zKKr1`=L?%ia0x&6pg&AkE~Tp!a0?Awmbz@XD{Z(V*?Y11epSNPrH54^;5R&&0sJ34 z;75AEW3?&MShH+nL;q)>=Ro=9@}d#fUQ%9EbnOszX8+9cqL@CJRW6s)Bb2uBL3P2% zqQqy_w2?*oKd0&{6(ehnI*dZ-{0lOPh8ST3Z`|tMZ)#Q{_9iJj~Z=wP{0b@nt8~=Eqi?R$De;soQ!|wLA-` zvBVEd!57tZvN}U^c(RIwr>J;%su~tPM^%K+Rb#{Ft100N)YR~Fjbei5B{BaO*d9@; z>C1Z zsA%tj+MH=cF}`xAh4&tiAwWnQ)U>5GJP&~`&{&vyo=|U-FJ96LkF?8+i|M+x7Q`;w$v`#^N3Nd$6tJ0N{nqp?UBbj<>MMDmr&bnYF|{x*fOB%)gC@N-qq}9?|Opuv(ET|+`~SA zAD#BU%jC0V*5u}GSi8_!z6@5^a#;l+^%bXe36~o(hZwH9P9D0`$jqjVYUd9y*IY}Qd zd(FxYK-TX$D_rg}mh5Eay2!bSBXU~VCyNzGck4UbEl0z!ciqbAY1iI`5m`BtMWdzS zK_*TJQMuFI-UhCoRW;imFQ>1ouZ3;4!mYarhxB=|jM82#LC$HmQ_Dof{z}Amk;y7f z^gC<~_IJSVd~qC4=kazN;c|sPTR%Pm6J^V$OVoLBMVNp<=J8Acg?JBXsmW4dg4M%> zI4vBIkAv)vWxOXQCw2$9TEH3eJ3)&dQS;)Ud4t4Qu8|LLU8feGwY!d;b80G!`8dF^ zaO+vpcfv4bp+DVKz`y(t!5NnZKk%yl0PDb><#gYwrS0xCX+3L@X4hMpKO#wG z^PVJ%CT#H%5SU3%HX9V@fXY0$xIm*R-MMt)CXzA+k_OEptUC1|9MTArV)T77w)-)j z;j&NID1B{={yeWZUvnq~0DGiM7?^Ox45MRqQC`}qhPEJn2JO*5zzEV zNhE+91p(aHTB+Gmw0CPsM4ye=yEVxlvoXbb_|pu3E~Tg6fCg?t<1Ry}m&2K@8hJBn z+~V@7+2vD{&8HD$)0hdz!n{9=f0AEagsq1{Q^oQ%+l(81VxRLjCEZXM-fKvX?cHir z0@}6aGR#UNy${lNgXA7azXsIzLHf0jejTLm&p^5mN8`$XSqPSW(HChZ7}7<7vLW5% zlacDThUcaF4VM9JO>Gzx>!9Y-s*sT@iC}a@piw6imUkDA-;FwbLZfHu_i$S>5dur* zquYK}l_WK$atUgps`;qukVC^%@%3h@G^wl)c$76iKv^Y%)4COgN(W_-| z1X)%NvFbPM(qyw?OuSNr329{aD@>-PjG+_Q=aY4#$}UdShS_SurzuOI!L)o{X9PQP zG>Ja5Lf$rYEyspuNylj;-2&3WYuN?FZt5OnKbm9{m#iCx+fSh&PctVv?cuhB9!~*C zj5c|hRtC3OBihhgwdscKf**$SQnfE3;WNwYB~pbeA5?YH%AQuGw6w!z6~2t$%8SX7 zTUHX0GnQRhv9CO-Mm<{+N#bT0`E{lhFR+044dM!4*7%uvdWnr6r4fND+8}35T3O|6 zC1+vYokk#~qpZ47zSxgFqXHZISHL|NC^L1_Q~8jZDEzCfD2Z&XEQz$SGs^Uwk{p_( zT!dQ^DaqM7J#y^K4>Vs(B8Q}1@}h5=oo|E0cR=HH<}cr6)c*!;=9@6~pQugW)8t4H z^hj#jYS0ws$wu?ScsX+jo9PlZ(5Tcf(ZIJXyPBob)y3G4UM&rf^xnxdnPTVXZMVjny!m-U}0 z2`ge6Z(}`eaPe+%@t#SKC0ZRv_?um=W-=FjUqvh{AxSfm!wuQkA*8nW=ds&i2g3ga_dMV1rATHk z4l~5x*iu<|TY6?C86ALAJ>m!O5YCkvB?GY#6*rH2vN2og zy#n1~c%I{lLA(`Gg?KFOXFq@Pum?77`wDs$f4@_wWoc%2 zcQ#dSZ6(u}>#h!%I?qE+8ALdAwd%xEYg>BoJoP?r_KMFpC^ok2q8xA5B%3ajQac0V z(!N%F-L>9)0+n__;mNu*D;9b1>yK%Jdv$0IwGo_{X-iy{76o|pb+j^d^Oj~d(ajQc z4v|*ZVjUXddENj>5wne-=du1U_KHV?YwYU7+|=3T@@NJ%#!?%zLEKYj8f}iBvF90A zPwa@j971(=Bg)INDa)H<*c9fy(_d*heI{hL*!zOR%3O^DdNI`dtBdJoFKQV%?S1v^ zaO?Wp!B1zjH;}-ksLJzPW7nG9tvgKRPRbu8ZfoD(LN(W^#Ate5wO-WTr?q^d7i}A~ z2d>R1?G%sSlFIIJV=TmAnBC(WCB9FB)X%y`_opny-umm1JHbnCnx@;-YJ4vCKADj* z8v`ZdN#2$n7+<7_E8y(jZ3slcYGCi$tVr-kJB=az@z+Os&K zxJC1JD+2_Qmy+CteKm00l;}_0YA@NoaBoL%AFY8;;u+Bt+j@4&+K~OD8|Bfn*6EBw z09dJ?y5^3)9g^nJQp?U()EQeq!8xxTpc)VRTsI$jxNqM#JvY%ryLai|%(ZXG95<~| zuWjDPSfqDsAj&_PBu|&2_i*r;@Qs+F>M5DxJk3x$`9|ID%0~ zqN6uKBF99F9^-T{>d?DMNY2A&EmfO@Kj1LOSC&s@FrH09?}?ZmtJ2oVj;TpSUZVH{RR~yNx<^1&MPEz^CjQo^SZj2Q4z`c zvU(-1zA1?rACoOB9#jA5lUc*gu9iOGir&fM+jh8Sy_zAy+9e-^!PYB6T6K|AWCT0K zXsHt%C%A5s@Z0kPwzKe#h@C}_)W}DPXR`Hks1pIEpV2oNmP&qgqkoo~)$5lQCxf3s9jBkhIWUlaO)*Iqn}D;d@lgV$Wp7|j?vTW^#lE>m;I1@Oqr z5r?m_j!rX{a8_0GcA`vn^I#S%;c)D5l{ zc*|}{@iE*da5o%Qgf6=(%{Na?_7aqQ$V*f5F8ZAkfT*gN0gA;*Ve@vh%_*ozddS!avKLY53nJWu6dapXHNQ1B* z4abaFhMB*dWvdHV37ZZHf}t232Vo7k1OuaZ!i2apoA=8xL(Wlixt`9ib67!=Qs!Z6 zpUt7g>uOSkS$yxE#Z` z!R>PRCKa!=rq<};ek4PeEcFR@2q6**X0@`Y^lqQQdb2^v46{kv9dP2PFEa% zsq5`he?ghKJT<4LmTDUjHMh{>T-n&*vHD0q3NJqvZIE3Lk}5wG`HJ+1Qjz`Aza2Z( z9W2t^$rB$3r@MIKZlwPSO~-0}joH$Vhu(v_?#>6IT1k&a4!&HHlv-KF^(pLG+j= zN)}26*-9aTt#g&4&PBV!65>uG)=%n7$Lr^#V;2Hvd#>0*7hS@WgDMY_aAy2M4g#70```{ig^ z#NoQ2zd-g?&tuuT#YX%c;0mv6yj=^;tlid#?megX==f4&h5es56fCTQ3i+`VVivE$V=36}Z8cpW6dGb3nuCMdtcX8Et!_~4u=$~m=M}V8OF%g~*LHTCN z8mG_ZayC()$%>KGVW)F(YO~zC98WaU#L^M+dTPG(Ek}vvI(d9IZwWW z#=YwjPQTJSAW2d@=Pj!Vaz6fne!tn~+G9SV@4B_o( zBWzMjjy`K){~lrgH^Tk{!u}&X`5%P+CusS9(Ec8R`?E{)-%{AylQgFZ`wE-pw~(4B zc(qs9eg%K5akBkiOISuTBH?m-OjHg{J_%cS1gPedF}Hv)IkK2qgNWZQRHLoIs>&*2 zC)*(&Y71S}cE$`9TAHHTqxG4nwo!MZ+SRUvt6d3K>#FRkHlru6YVRO3gK39;W>Y)C z9XB-AIpYRXmjlFG386x8C4`DfS0X$=!J~+@9gR~`0z#`GbPR-!h0toY+!@DG=Ezfeu9}V!a8Yqe?(1|YOz9#*L2cP zsDy8@SV7)Y884pYSW!I9gdJn4g$_H+*1G4BDs&p8pAYF5s5#bjHP^aGvp`13L+LC{ zb2iQ$9nSTP4p+GHUEz|uf{Yptxr~t8f`&}>MCA2H^q5ql$No);UI5VxA-Vyg7eVx5 zh+g_v6YaFunTg(I6a6+iDw1C1lXSI@XxR+D`lK5F0SK4f1Vq}Rf_`bQhwx@Kk-+vz z)&_!mH?rP-nTK%GhKFgdow>k5kF;00(q83Cdlk~|ge-eFgkN_0TmWl`P58U$l(b-8 z6_@#s9vIs5p*l^mZ;+Ha4GbC^N-r)%C#b1@qIa-d-a#$Dl8EN3)GTW!67SSR%T%bC zr$xnOzXNA9IEOC484U)jxXceiLI~u`w9zJ?raN^scni}(3AUhyTQa_s-g=XkyE7n` zUCML)qr(0Pmcg^$4(ugWpuc5Lat6v1o6B;{thkt(E6gZn*6T1sdtO-AssignXu)+f zfgfhKll^!aAAlhT)un`Z%V13g%LN>UDJ34Jj3g&c5Ar(?{czZ`$<>KXu1;*C5j2D4 zO|ZuqEH6}%H8y+BAgkP{>W%8XuA*uWy?Yd|Jd2MrH2*=rh4@X9u6ySh`|#j1orSca zGL|Ck#VIwZnJ}akkMomEXY!!M3{g#vnA=sZ^>KVp?w}*MOH*v7R|=Xo14gouI0Lc* zn#@ZaY`d$rZR>P!p%aL4~4E$73!W< z#6!r3TjxNBxxf~TUpR4IZ_q*PQC-cJO=cR-j(T;)Q*y98i0LJ~W9<(*0(jSQoVAimIr7-A{eP;kM-GbZw80Vk0Q+M-IB(JsegHuE$%$8mhl;>@2E{vVu*6IOq3K!p7#Z{U69amwlMy&~r|P3A?u_ja|n zb+q(bYLZeZl{ghospf4q^X?adD2(|N2=@g*XzkkB)zdGGp$-U5!?yxp5b@9-6H0sl z-VXq9Wp{i3{P%-62mMQRn`7tV9sOeAXJ%^v2)&uNNAXhC#*f4&10d)$l-aZ5l>iWW z2__Z9XTI{{*fa1=j%t^iN*z7;UH}MxUQ6R4mJ0DdfBESLrr>=h$56|zaU?HiX$`JMOPzW=~rrR1pafxMFqCsGkLTUrN`JofEZZoh%HM2>_H(voGF zAjzKB!O&(u{P31vAx$|(`k>{nOo!GF&$d2(c-<2Kkz<+ugN0SgEQS@YZc8PAPYN)EqJPTrUf(ug=1(z%PD zyz?h{O348*_wVSz$B$-$gt7z!oA|>)Qzud%5z11tEZI@J+!EjeE z`_03{8H~%3aG^?lp81vj;08lm-f;XM?_>ZfN0krS-A|`OThX z9MwKzr+hsfoZrJY7{T)oAAfZtHBOFEK8WA?Mmj{Negwn1X+qoJHb&KQ_^ZaYx6)zh z)^DKp>YIZv|2u|_N)CTZo%iqQAZ0)>cm0xiD^@O8GA|g%x)*PFpWIyj=2BMPI41d^Uy5IJYV&HN>xCYS^vsIS-uv#k zO3C5pLQhjhu>Gm!!X2esFQWSC!-wbLgBbvBqk(K`?Ss8GA2=JJqQ5@)n-6@CL7*Jd zeWgsiIqOoSSCiDd_Lk169a?u)$B92v8{~jH?%&aatM1RX44qsznDd+GzjfPlsIwgP zE^uk9KK<#e!FTQI?qbuL_I}EF?ZzV)JWd&MV6SlhjvoBVBiWYYZmAGV%8_wBx9w*# zB8S-}%an5T;3?eK0(CP@<_#Tt+uLOK;Q`8e|J-NZz88te;g|f~U(L3x6=WV>+)qor zv+H+*-=h}G;b+-(&t+XkM_Wff%Jt3WO}pNP1Ub%gWg^ml^~G$9SktnL7Cl%b-=22$ z3&AA3_e!?#*OIBa9}<2r<-I%DU|cpCPpKbEzMXXunXBL0hh{D7M4{yH3wj(IBV?yk zx+Cb1gclAx@hBs8Icj}Uc=zor%Lr!w>$$HS?oKEr#~2^rAOGKAz;rA9j6DyQex&Ce zkd(s@?~bBS8Z*q?lO0UDdNTAT+t2*|*%uhe$>CSOuZ&8E$2?RrZR?GZQ}@pZrvJ7_ zKGyv95Y(H)KURKuVixcR?zi@ZAIfW|n~}rc+5V|Muz*y&?R`Ij(f`3~uRc~z<(GrO zxchhX;FsoR1$_xM^Sof#PyAPVUKMPY!>`xH4S}%Nw6|X+0VA#m2IfxNzu)lD_h|3s zsB^KG*5#(A;4*e^(+nCI^h2HN+g32=m!sMT`n65zptS*F>B@zR`qAxsK39`-2)&l0 z#s~4T?hFuDEM2)sUby} zv^JsOGrIy)N-(^yJzw>qO2)Nv_(x&C_;?0*t939^%g$hc*M6b$iw`jom&1=>!-;f& z+CbCTpNw9-=5qxrsmpTsr9Sbcba?KrA^i+XQnz??1b_74)8Fq0qS=QenCR3@MoEz4 zt#Dc+=_1)rUZd0@VQP$NvV{K_+)oKkkfnBUPKB8om6(KCRGbde>q+czA%`{)s{#KC MOX(ucXS%BXKV~oJBqTr%lPKhn5Qy7wRY(+Lw8JvM#${)n9Zb+x zYinEW;aRoX+oq=WNWB6?G}>xyo8GOxTYG8`YwyE5{eR!=0=q0}_XjiIym{|^?|yH- zjvYRK0MLQRhU7L(`4z*D)~2q8>PTC2?d!*YjtSIri5k80lGXi0m;Yt_pFnMGXp1yA zb#~RXHn*CISf7=0t9MyWDrP6@!mDdml>{0RQy>jVGdf^}3FFJNYgU&88Xe1Yt={Ub z!xDc(&8kbzTvy$*`mAteyJJSI-dC@0|9&z_vkpN<5N2B)$wT^pV9GI9iNk^hje^i$9+2P*YSV^JSZ;@ zN#2L$ek1ZSs$ncZjgH8Nhvj8l$0Gqeilae1hQ~F0LB|&ZsKFum{Dg)lC6_PB%Tqyo z8D9zDt2n0NYXNM)AxZdk4c`c$4&T)9tpL7_??@HCD=*)Zm#2gHKAtYd5AZ`BKMLT- z_=$#}3KaLp`aIJKoZPb4yxiPh8yhtHt(aXKk$3N9y})b*R*xN~V-5;~?zJduODtiv4Gs2KPM6seCnVHjN6q*y(}~IFG|=x3#8LuFTVyz^U`2%HLrLAUCxrOno;b$6tuHhLbqd=f(f7D9K!j#hR3p#h7 zO!Y21llcORJ!jOK`&=)0YtOS1y_RG3D#O&v$neDm`vp$%OfVCoKw&g)r>KRmQW6#S zTW%&h7F90KXZ4Bv@XPumQ0DD0zi~Oo!=L8QW@+bF9lg3R0z=8e$(ma&U5o@rQv!FztZq)GU>F3 zoT$|tla*lJwB)XlX*_$U5l;NSR{1Kodbj7fL0hy+TO;+l1C&Nj}K7~^m3-30o z&m4+-ogqC7Ej~ea$V@{gN7WGC?AU`@TX~bg>6MY(a;&_Zr7}Co{7C`SdsYiIHwpCT zc7i zrQ3RT$FiON+I?%IwY~OWt?We<$4HUL4ihqPNO22I2x@%`* zsXXJ793ji|Y#_&l63VfYH8~UNS$G~$%_2coI}Rv~-V{VNr|GnOI!GW$_A+=p>*UpP zIwRR|N$1rH_J*Wm${-h*w92#=mWleDfXf#uif^6A zcT$2((az9oVWDYC^%^t%HJpI-oSR^$hiimz!dqdK`T`d%i}#K<+5 zGq&_%mmTR(a6)J_<;~Ne&e@tb1Oud)uQ-&bMMY;b@XugB+ z`_~&4!JZlAxa+h1ZqH%j#@yoV7isR&dAN3isKPwonB1gCN7TQP#u zANxW9lf>k^*Sb=$1iG`>|HP&K1)?pjYq8&G47dGJ%Y{mc@a5Bwc}8ozIm|tu8<}Wm~9t?M5MGdKwpD4?VgKPvK&6)hOSicoSDTC3+i7u8JvB z53{haGG3}}dH*gN$PI7i@8q8CxL8pJ6o z2O%%8sDy+971FNbIHP-okVYRxC2tI)<>|yvv|u)K=y4wP}T<;%j(#X}(f$k6S=~ucWyR)3%b^($I=AtlUv` z1l58DD;Y;kcc^w0s~#h0wV-j<+@}~w1wv#5ets0;O3cQaDezSj8x~;^h8PyWKK@G) zJji}NNmJ_(ucAW3u(G5SsD=jNrwmt8y+>3k1DmQwuy&Jg^P+`!V`jW6N z>+gc00L;PruJwt7$DffYvw|7ZR#h#dy;_RuQ?_0r&r@*&qvb{la}$NT1_ zx1%0+DACU(hKAFrjEXi!_*)n!^0$e~?V)gmjQ*=tRIVe>xALt|DHMe7HE2Sd@-k71 zaSg*xIZD)!VG$WzS#{KZ0QNXGbXWP-jN+^@oZT{xx^4yG%U%f_MSbfyHg>P1hMWAp zQEYCj=5mXFy}#Tqk?zK+ilE$|=22HzUBSTGa;;yQfQp~9@(3CP4kGvfwuTxoAL=9H2n=@QnCgM*Jb_^8mGbkeT}sGwU!ldXO58A%-K2z=s)e<8;hXWuMKY?B@U5 znYD#DCv7P|dF{qK2ovPkjCV4B3&?i^-o>0|*3zxl;dleX)oTP;s-mT$1Of;q& zWAA00_M%L^On| + + + + + + + + + + Builds, tests, and runs the project avi. + + + diff --git a/trunk/libsrc/avi/nbproject/build-impl.xml b/trunk/libsrc/avi/nbproject/build-impl.xml new file mode 100644 index 000000000..675a2b235 --- /dev/null +++ b/trunk/libsrc/avi/nbproject/build-impl.xml @@ -0,0 +1,1407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/trunk/libsrc/avi/nbproject/genfiles.properties b/trunk/libsrc/avi/nbproject/genfiles.properties new file mode 100644 index 000000000..4b63314ae --- /dev/null +++ b/trunk/libsrc/avi/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=505ab359 +build.xml.script.CRC32=fa68b8cb +build.xml.stylesheet.CRC32=8064a381@1.68.1.46 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=505ab359 +nbproject/build-impl.xml.script.CRC32=b5283dd3 +nbproject/build-impl.xml.stylesheet.CRC32=5a01deb7@1.68.1.46 diff --git a/trunk/libsrc/avi/nbproject/project.properties b/trunk/libsrc/avi/nbproject/project.properties new file mode 100644 index 000000000..b26bc8e4c --- /dev/null +++ b/trunk/libsrc/avi/nbproject/project.properties @@ -0,0 +1,72 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/avi.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.7 +javac.target=1.7 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +main.class=org.monte.media.math.IntMath +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=true +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/trunk/libsrc/avi/nbproject/project.xml b/trunk/libsrc/avi/nbproject/project.xml new file mode 100644 index 000000000..53a131c45 --- /dev/null +++ b/trunk/libsrc/avi/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + avi + + + + + + + + + diff --git a/trunk/libsrc/avi/src/org/monte/media/AbortException.java b/trunk/libsrc/avi/src/org/monte/media/AbortException.java new file mode 100644 index 000000000..cd20a6ecf --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbortException.java @@ -0,0 +1,39 @@ +/* + * @(#)AbortException.java + * + * Copyright (c) 1999-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +/** + * This exception is thrown when the production of an image + * has been aborted. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * + * @version $Id: AbortException.java 299 2013-01-03 07:40:18Z werner $ + */ +public class AbortException extends Exception { + + public static final long serialVersionUID = 1L; + + /** + Creates a new exception. + */ + public AbortException() { + super(); + } + + /** + Creates a new exception. + + */ + public AbortException(String message) { + super(message); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java new file mode 100644 index 000000000..6a2a5e816 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java @@ -0,0 +1,99 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +import java.util.ArrayList; +/** + * {@code AbstractCodec}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public abstract class AbstractCodec implements Codec { + + protected Format[] inputFormats; + protected Format[] outputFormats; + protected Format inputFormat; + protected Format outputFormat; + protected String name="unnamed codec"; + + public AbstractCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) { + this.inputFormats = supportedInputFormats; + this.outputFormats = supportedOutputFormats; + } + public AbstractCodec(Format[] supportedInputOutputFormats) { + this.inputFormats = supportedInputOutputFormats; + this.outputFormats = supportedInputOutputFormats; + } + + @Override + public Format[] getInputFormats() { + return inputFormats.clone(); + } + + @Override + public Format[] getOutputFormats(Format input) { + ArrayListof=new ArrayList(outputFormats.length); + for (Format f:outputFormats) { + of.add(input==null?f:f.append(input)); + } + return of.toArray(new Format[of.size()]); + } + + @Override + public Format setInputFormat(Format f) { + if (f!=null) + for (Format sf : getInputFormats()) { + if (sf.matches(f)) { + this.inputFormat = sf.append(f); + return inputFormat; + } + } + this.inputFormat=null; + return null; + } + + @Override + public Format setOutputFormat(Format f) { + for (Format sf : getOutputFormats(f)) { + if (sf.matches(f)) { + this.outputFormat = f; + return sf; + } + } + this.outputFormat=null; + return null; + } + + @Override + public Format getInputFormat() { + return inputFormat; + } + + @Override + public Format getOutputFormat() { + return outputFormat; + } + + @Override + public String getName() { + return name; + } + + /** Empty implementation of the reset method. Don't call super. */ + @Override + public void reset() { + // empty + } + + @Override + public String toString() { + String className=getClass().getName(); + int p=className.lastIndexOf('.'); + return className.substring(p+1)+"{" + "inputFormat=" + inputFormat + ", outputFormat=" + outputFormat+'}'; + } + + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java new file mode 100644 index 000000000..f180e7233 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java @@ -0,0 +1,226 @@ +/* + * @(#)AbstractVideoCodec.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.DirectColorModel; +import java.awt.image.WritableRaster; +import java.io.IOException; +import javax.imageio.stream.ImageOutputStream; +import static org.monte.media.VideoFormatKeys.*; + +/** + * {@code AbstractVideoCodec}. + * + * @author Werner Randelshofer + * @version $Id: AbstractVideoCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class AbstractVideoCodec extends AbstractCodec { + + private BufferedImage imgConverter; + + public AbstractVideoCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) { + super(supportedInputFormats, supportedOutputFormats); + } + + /** Gets 8-bit indexed pixels from a buffer. Returns null if conversion failed. */ + protected byte[] getIndexed8(Buffer buf) { + if (buf.data instanceof byte[]) { + return (byte[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getRaster().getDataBuffer() instanceof DataBufferByte) { + return ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + } + } + return null; + } + + /** Gets 15-bit RGB pixels from a buffer. Returns null if conversion failed. */ + protected short[] getRGB15(Buffer buf) { + if (buf.data instanceof int[]) { + return (short[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (image.getRaster().getDataBuffer() instanceof DataBufferShort) { + // FIXME - Implement additional checks + return ((DataBufferShort) image.getRaster().getDataBuffer()).getData(); + } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) { + // FIXME - Implement additional checks + return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData(); + } + } + if (imgConverter == null) { + int width = outputFormat.get(WidthKey); + int height = outputFormat.get(HeightKey); + imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB); + } + Graphics2D g = imgConverter.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData(); + } + return null; + } + /** Gets 16-bit RGB-5-6-5 pixels from a buffer. Returns null if conversion failed. */ + protected short[] getRGB16(Buffer buf) { + if (buf.data instanceof int[]) { + return (short[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (image.getRaster().getDataBuffer() instanceof DataBufferShort) { + // FIXME - Implement additional checks + return ((DataBufferShort) image.getRaster().getDataBuffer()).getData(); + } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) { + // FIXME - Implement additional checks + return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData(); + } + } + if (imgConverter == null) { + int width = outputFormat.get(WidthKey); + int height = outputFormat.get(HeightKey); + imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_565_RGB); + } + Graphics2D g = imgConverter.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData(); + } + return null; + } + + + /** Gets 24-bit RGB pixels from a buffer. Returns null if conversion failed. */ + protected int[] getRGB24(Buffer buf) { + if (buf.data instanceof int[]) { + return (int[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) { + if (image.getRaster().getDataBuffer() instanceof DataBufferInt) { + return ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + } + } + return image.getRGB(0, 0, // + outputFormat.get(WidthKey), outputFormat.get(HeightKey), // + null, 0, outputFormat.get(WidthKey)); + } + return null; + } + + /** Gets 32-bit ARGB pixels from a buffer. Returns null if conversion failed. */ + protected int[] getARGB32(Buffer buf) { + if (buf.data instanceof int[]) { + return (int[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) { + if (image.getRaster().getDataBuffer() instanceof DataBufferInt) { + return ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + } + } + return image.getRGB(0, 0, // + outputFormat.get(WidthKey), outputFormat.get(HeightKey), // + null, 0, outputFormat.get(WidthKey)); + } + return null; + } + + /** Gets a buffered image from a buffer. Returns null if conversion failed. */ + protected BufferedImage getBufferedImage(Buffer buf) { + if (buf.data instanceof BufferedImage) { + return (BufferedImage) buf.data; + } + return null; + } + private byte[] byteBuf = new byte[4]; + + protected void writeInt24(ImageOutputStream out, int v) throws IOException { + byteBuf[0] = (byte) (v >>> 16); + byteBuf[1] = (byte) (v >>> 8); + byteBuf[2] = (byte) (v >>> 0); + out.write(byteBuf, 0, 3); + } + + protected void writeInt24LE(ImageOutputStream out, int v) throws IOException { + byteBuf[2] = (byte) (v >>> 16); + byteBuf[1] = (byte) (v >>> 8); + byteBuf[0] = (byte) (v >>> 0); + out.write(byteBuf, 0, 3); + } + + protected void writeInts24(ImageOutputStream out, int[] i, int off, int len) throws IOException { + // Fix 4430357 - if off + len < 0, overflow occurred + if (off < 0 || len < 0 || off + len > i.length || off + len < 0) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!"); + } + + byte[] b = new byte[len * 3]; + int boff = 0; + for (int j = 0; j < len; j++) { + int v = i[off + j]; + //b[boff++] = (byte)(v >>> 24); + b[boff++] = (byte) (v >>> 16); + b[boff++] = (byte) (v >>> 8); + b[boff++] = (byte) (v >>> 0); + } + + out.write(b, 0, len * 3); + } + + protected void writeInts24LE(ImageOutputStream out, int[] i, int off, int len) throws IOException { + // Fix 4430357 - if off + len < 0, overflow occurred + if (off < 0 || len < 0 || off + len > i.length || off + len < 0) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!"); + } + + byte[] b = new byte[len * 3]; + int boff = 0; + for (int j = 0; j < len; j++) { + int v = i[off + j]; + b[boff++] = (byte) (v >>> 0); + b[boff++] = (byte) (v >>> 8); + b[boff++] = (byte) (v >>> 16); + //b[boff++] = (byte)(v >>> 24); + } + + out.write(b, 0, len * 3); + } + + /** Copies a buffered image. */ + protected static BufferedImage copyImage(BufferedImage img) { + ColorModel cm = img.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = img.copyData(null); + return new BufferedImage(cm, raster, isAlphaPremultiplied, null); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java new file mode 100644 index 000000000..4d5e2558c --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java @@ -0,0 +1,126 @@ +/* + * @(#)AudioFormatKeys.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; +import java.nio.ByteOrder; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioFormat.Encoding; + +/** + * Defines common format keys for audio media. + * + * @author Werner Randelshofer + * @version $Id: AudioFormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class AudioFormatKeys extends FormatKeys { + // Standard video EncodingKey strings for use onlyWith FormatKey.Encoding. + + /** + * Specifies SignedKey, linear PCM data. + */ + public static final String ENCODING_PCM_SIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED.toString(); + /** + * Specifies unsigned, linear PCM data. + */ + public static final String ENCODING_PCM_UNSIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_UNSIGNED.toString(); + /** + * Specifies u-law encoded data. + */ + public static final String ENCODING_ULAW = javax.sound.sampled.AudioFormat.Encoding.ULAW.toString(); + /** + * Specifies a-law encoded data. + */ + public static final String ENCODING_ALAW = javax.sound.sampled.AudioFormat.Encoding.ALAW.toString(); + /** + * AVI PCM encoding. + */ + public static final String ENCODING_AVI_PCM = "\u0000\u0000\u0000\u0001"; + /** + * QuickTime 16-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_TWOS_PCM = "twos"; + /** + * QuickTime 16-bit little endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_SOWT_PCM = "sowt"; + /** + * QuickTime 24-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_IN24_PCM = "in24"; + /** + * QuickTime 32-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_IN32_PCM = "in32"; + /** + * QuickTime 8-bit unsigned PCM encoding. + */ + public static final String ENCODING_QUICKTIME_RAW_PCM = "raw "; + /** + * Specifies MP3 encoded data. + */ + public static final String ENCODING_MP3 = "MP3"; + /** + * The sample size in bits. + */ + public final static FormatKey SampleSizeInBitsKey = new FormatKey("sampleSizeInBits", Integer.class); + /** + * The numer of ChannelsKey. + */ + public final static FormatKey ChannelsKey = new FormatKey("channels", Integer.class); + /** + * The size of a frame. + */ + public final static FormatKey FrameSizeKey = new FormatKey("frameSize", Integer.class); + /** + * The compressor name. + */ + public final static FormatKey ByteOrderKey = new FormatKey("byteOrder", ByteOrder.class); + /** + * The number of frames per second. + */ + public final static FormatKey SampleRateKey = new FormatKey("sampleRate", Rational.class); + /** + * Whether values are signed. + */ + public final static FormatKey SignedKey = new FormatKey("signed", Boolean.class); + /** + * Whether silence is encoded as -128 instead of 0. + */ + public final static FormatKey SilenceBugKey = new FormatKey("silenceBug", Boolean.class); + + public static Format fromAudioFormat(javax.sound.sampled.AudioFormat fmt) { + return new Format( + MediaTypeKey, MediaType.AUDIO, + EncodingKey, fmt.getEncoding().toString(), + SampleRateKey, Rational.valueOf(fmt.getSampleRate()), + SampleSizeInBitsKey, fmt.getSampleSizeInBits(), + ChannelsKey, fmt.getChannels(), + FrameSizeKey, fmt.getFrameSize(), + FrameRateKey, Rational.valueOf(fmt.getFrameRate()), + ByteOrderKey, fmt.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN, + SignedKey, AudioFormat.Encoding.PCM_SIGNED.equals(fmt.getEncoding())//, + // + ); + } + + public static javax.sound.sampled.AudioFormat toAudioFormat(Format fmt) { + // We always use PCM_SIGNED or PCM_UNSIGNED + return new javax.sound.sampled.AudioFormat( + !fmt.containsKey(SignedKey) || fmt.get(SignedKey) ? Encoding.PCM_SIGNED : Encoding.PCM_UNSIGNED, + fmt.get(SampleRateKey).floatValue(), + fmt.get(SampleSizeInBitsKey, 16), + fmt.get(ChannelsKey, 1), + fmt.containsKey(FrameSizeKey) ? fmt.get(FrameSizeKey) : (fmt.get(SampleSizeInBitsKey, 16) + 7) / 8 * fmt.get(ChannelsKey, 1), + fmt.containsKey(FrameRateKey) ? fmt.get(FrameRateKey).floatValue() : fmt.get(SampleRateKey).floatValue(), + fmt.containsKey(ByteOrderKey) ? fmt.get(ByteOrderKey) == ByteOrder.BIG_ENDIAN : true); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Buffer.java b/trunk/libsrc/avi/src/org/monte/media/Buffer.java new file mode 100644 index 000000000..94c6da20e --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Buffer.java @@ -0,0 +1,164 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.WritableRaster; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.monte.media.math.Rational; +import org.monte.media.util.Methods; + +/** + * A {@code Buffer} carries media data from one media processing unit to another. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public class Buffer { + + /** A flag mask that describes the boolean attributes for this buffer. + */ + public EnumSet flags = EnumSet.noneOf(BufferFlag.class); + /** Values which are not specified must have this value. */ + public static final int NOT_SPECIFIED = -1; + /** The track number. + * This can be set to NOT_SPECIFIED or to a number >= 0. + */ + public int track; + /** Header information, such as RTP header for this chunk. */ + public Object header; + /** The media data. */ + public Object data; + /** The data offset. This field is only used if {@code data} is an array. */ + public int offset; + /** The data length. This field is only used if {@code data} is an array. */ + public int length; + /** Duration of a sample in seconds. + * Multiply this with {@code sampleCount} to get the buffer duration. + */ + public Rational sampleDuration; + /** The time stamp of this buffer in seconds. */ + public Rational timeStamp; + /** The format of the data in this buffer. */ + public Format format; + /** The number of samples in the data field. */ + public int sampleCount = 1; + + /** Sequence number of the buffer. This can be used for debugging. */ + public long sequenceNumber; + + /** Sets all variables of this buffer to that buffer except for {@code data}, + * {@code offset}, {@code length} and {@code header}. + */ + public void setMetaTo(Buffer that) { + this.flags = EnumSet.copyOf(that.flags); + //this.data=that.data; + //this.offset=that.offset; + //this.length=that.length; + //this.header=that.header; + this.track = that.track; + this.sampleDuration = that.sampleDuration; + this.timeStamp = that.timeStamp; + this.format = that.format; + this.sampleCount = that.sampleCount; + this.format = that.format; + this.sequenceNumber=that.sequenceNumber; + } + + /** Sets {@code data}, {@code offset}, {@code length} and {@code header} + * of this buffer to that buffer. + * Note that this method creates copies of the {@code data} and + * {@code header}, so that these fields in that buffer can be discarded + * without affecting the contents of this buffer. + *

+ * FIXME - This method does not always create a copy!! + */ + public void setDataTo(Buffer that) { + this.offset = that.offset; + this.length = that.length; + this.data = copy(that.data, this.data); + this.header = copy(that.header, this.header); + + } + + private Object copy(Object from, Object into) { + if (from instanceof byte[]) { + byte[] b=(byte[])from; + if (!(into instanceof byte[]) || ((byte[]) into).length < b.length) { + into = new byte[b.length]; + } + System.arraycopy(b, 0, (byte[])into, 0, b.length); + } else if (from instanceof BufferedImage) { + // FIXME - Try to reuse BufferedImage in output! + BufferedImage img = (BufferedImage) from; + ColorModel cm = img.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = img.copyData(null); + into = new BufferedImage(cm, raster, isAlphaPremultiplied, null); + } else if (from instanceof Cloneable) { + try { + into=Methods.invoke(from, "clone"); + } catch (NoSuchMethodException ex) { + into=from; + } + } else { + // FIXME - This is very fragile, since we do not know, if the + // input data stays valid until the output data is processed! + into = from; + } + + return into; + } + + /** Returns true if the specified flag is set. */ + public boolean isFlag(BufferFlag flag) { + return flags.contains(flag); + } + + /** Convenience method for setting a flag. */ + public void setFlag(BufferFlag flag) { + setFlag(flag, true); + } + + /** Convenience method for clearing a flag. */ + public void clearFlag(BufferFlag flag) { + setFlag(flag, false); + } + + /** Sets or clears the specified flag. */ + public void setFlag(BufferFlag flag, boolean value) { + if (value) { + flags.add(flag); + } else { + flags.remove(flag); + } + } + + /** Clears all flags, and then sets the specified flag. */ + public void setFlagsTo(BufferFlag... flags) { + if (flags.length == 0) { + this.flags = EnumSet.noneOf(BufferFlag.class); + } else { + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + } + } + + /** Clears all flags, and then sets the specified flag. */ + public void setFlagsTo(EnumSet flags) { + if (flags == null) { + this.flags = EnumSet.noneOf(BufferFlag.class); + } else { + this.flags = EnumSet.copyOf(flags); + } + } + + public void clearFlags() { + flags.clear(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java new file mode 100644 index 000000000..92c1e730d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java @@ -0,0 +1,42 @@ +/* + * @(#)BufferFlag.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +/** + * {@code BufferFlag}. + * + * @author Werner Randelshofer + * @version $Id: BufferFlag.java 299 2013-01-03 07:40:18Z werner $ + */ +public enum BufferFlag { + + /** Indicates that the data in this buffer should be ignored. */ + DISCARD, + /** Indicates that this Buffer holds an intra-coded picture, which can be + * decoded independently. */ + KEYFRAME, + /** Indicates that the data in this buffer is at the end of the media. */ + END_OF_MEDIA, + /** Indicates that the data in this buffer is used for initializing the + * decoding queue. + *

+ * This flag is used when the media time of a track is set to a non-keyframe + * sample. Thus decoding must start at a keyframe at an earlier time. + *

+ * Decoders should decode the buffer. + * Encoders and Multiplexers should discard the buffer. + */ + PREFETCH, + /** Indicates that this buffer is known to have the same data as the + * previous buffer. This may improve encoding performance. + */ + SAME_DATA; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Codec.java b/trunk/libsrc/avi/src/org/monte/media/Codec.java new file mode 100644 index 000000000..908707a8e --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Codec.java @@ -0,0 +1,73 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +/** + * A {@code Codec} processes a {@code Buffer} and stores the result in another + * {@code Buffer}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public interface Codec { + + /** The codec successfully converted the input to output. */ + public final static int CODEC_OK = 0; + /** The codec could not handle the input. */ + public final static int CODEC_FAILED = 1; + /** The codec did not fully consume the input buffer. + * The codec has updated the input buffer to + * reflect the amount of data that it has processed. + * The codec must be called again with the same input buffer. + */ + public final static int CODEC_INPUT_NOT_CONSUMED = 2; + /** The codec did not fully fill the output buffer. + * The codec has updated the output buffer to + * reflect the amount of data that it has processed. + * The codec must be called again with the same output buffer. + */ + public final static int CODEC_OUTPUT_NOT_FILLED = 4; + + /** Lists all of the input formats that this codec accepts. */ + public Format[] getInputFormats(); + + /** Lists all of the output formats that this codec can generate + * with the provided input format. If the input format is null, returns + * all supported output formats. + */ + public Format[] getOutputFormats(Format input); + + /** Sets the input format. + * Returns the format that was actually set. This is the closest format + * that the Codec supports. Returns null if the specified format is not + * supported and no reasonable match could be found. + */ + public Format setInputFormat(Format input); + + public Format getInputFormat(); + + /** Sets the output format. + * Returns the format that was actually set. This is the closest format + * that the Codec supports. Returns null if the specified format is not + * supported and no reasonable match could be found. + */ + public Format setOutputFormat(Format output); + + public Format getOutputFormat(); + + /** Performs the media processing defined by this codec. + *

+ * Copies the data from the input buffer into the output buffer. + * + * @return A combination of processing flags. + */ + public int process(Buffer in, Buffer out); + + /** Returns a human readable name of the codec. */ + public String getName(); + + /** Resets the state of the codec. */ + public void reset(); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java new file mode 100644 index 000000000..16496597b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java @@ -0,0 +1,395 @@ +/* + * @(#)DefaultRegistry.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.AudioFormatKeys.*; + +/** + * {@code DefaultRegistry}. + *

+ * FIXME - The registry should be read from a file. + * + * @author Werner Randelshofer + * @version $Id: DefaultRegistry.java 299 2013-01-03 07:40:18Z werner $ + */ +public class DefaultRegistry extends Registry { + + private HashMap> codecMap; + private HashMap> readerMap; + private HashMap> writerMap; + private HashMap fileFormatMap; + + @Override + public Format[] getReaderFormats() { + return getFileFormats(); + } + + @Override + public Format[] getWriterFormats() { + return getFileFormats(); + } + + @Override + public Format[] getFileFormats() { + return fileFormatMap.values().toArray(new Format[fileFormatMap.size()]); + } + + private static class RegistryEntry { + + Format inputFormat; + Format outputFormat; + String className; + + public RegistryEntry(Format inputFormat, Format outputFormat, String className) { + this.inputFormat = inputFormat; + this.outputFormat = outputFormat; + this.className = className; + } + } + + public DefaultRegistry() { + } + + @Override + protected void init() { + codecMap = new HashMap>(); + readerMap = new HashMap>(); + writerMap = new HashMap>(); + fileFormatMap = new HashMap(); + + // IFF ANIM + // -------- + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_ANIM, EncodingKey, ENCODING_BITMAP_IMAGE), + "org.monte.media.anim.BitmapCodec"); + + // AVI + // -------- + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DIB), + "org.monte.media.avi.DIBCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG), + "org.monte.media.jpeg.JPEGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PNG), + "org.monte.media.png.PNGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_RLE), + "org.monte.media.avi.RunLengthCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.TechSmithCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DOSBOX_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.ZMBVCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM), + "org.monte.media.avi.AVIPCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM), + "org.monte.media.avi.AVIPCMAudioCodec"); + + // QuickTime + // -------- + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW), + "org.monte.media.quicktime.RawCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_ANIMATION), + "org.monte.media.quicktime.AnimationCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_JPEG), + "org.monte.media.jpeg.JPEGCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG), + "org.monte.media.jpeg.JPEGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_PNG), + "org.monte.media.png.PNGCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.TechSmithCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIReader"); + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeReader"); + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMReader"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIWriter"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeWriter"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMWriter"); + + putFileFormat("avi", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI)); + putFileFormat("mov", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME)); + putFileFormat("anim", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM)); + } + + /** + * + * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param codecClass + */ + public void putBidiCodec(Format inputFormat, Format outputFormat, String codecClass) { + putCodec(inputFormat, outputFormat, codecClass); + putCodec(outputFormat, inputFormat, codecClass); + } + + /** + * + * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param codecClass + */ + @Override + public void putCodec(Format inputFormat, Format outputFormat, String codecClass) { + RegistryEntry entry = new RegistryEntry(inputFormat, outputFormat, codecClass); + addCodecEntry(inputFormat.get(EncodingKey), entry); + addCodecEntry(outputFormat.get(EncodingKey), entry); + } + + private void addCodecEntry(String key, RegistryEntry entry) { + LinkedList list = codecMap.get(key); + if (list == null) { + list = new LinkedList(); + codecMap.put(key, list); + } + list.add(entry); + } + + /** + * + * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}. + * @param readerClass + */ + @Override + public void putReader(Format fileFormat, String readerClass) { + RegistryEntry entry = new RegistryEntry(null, fileFormat, readerClass); + String key = fileFormat.get(MimeTypeKey); + LinkedList list = readerMap.get(key); + if (list == null) { + list = new LinkedList(); + readerMap.put(key, list); + } + list.add(entry); + } + + /** + * + * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}. + * @param writerClass + */ + @Override + public void putWriter(Format fileFormat, String writerClass) { + RegistryEntry entry = new RegistryEntry(fileFormat, null, writerClass); + String key = fileFormat.get(MimeTypeKey); + LinkedList list = writerMap.get(key); + if (list == null) { + list = new LinkedList(); + writerMap.put(key, list); + } + list.add(entry); + } + + @Override + public String[] getCodecClasses(Format inputFormat, Format outputFormat) { + HashSet classNames = new HashSet(); + HashSet entries = new HashSet(); + if (inputFormat != null) { + LinkedList re; + if (inputFormat.get(EncodingKey) == null) { + re = new LinkedList(); + for (Map.Entry> i : codecMap.entrySet()) { + for (RegistryEntry j : i.getValue()) { + if (inputFormat.matches(j.inputFormat)) { + re.add(j); + } + } + } + } else { + re = codecMap.get(inputFormat.get(EncodingKey)); + } + if (re != null) { + entries.addAll(re); + } + } + if (outputFormat != null) { + LinkedList re; + if (outputFormat.get(EncodingKey) == null) { + re = new LinkedList(); + for (Map.Entry> i : codecMap.entrySet()) { + for (RegistryEntry j : i.getValue()) { + if (outputFormat.matches(j.outputFormat)) { + re.add(j); + } + } + } + } else { + re = codecMap.get(outputFormat.get(EncodingKey)); + } + if (re != null) { + entries.addAll(re); + } + } + for (RegistryEntry e : entries) { + if ((inputFormat == null || e.inputFormat == null || inputFormat.matches(e.inputFormat)) + && (outputFormat == null || e.outputFormat == null || outputFormat.matches(e.outputFormat))) { + classNames.add(e.className); + } + } + return classNames.toArray(new String[classNames.size()]); + } + + @Override + public String[] getReaderClasses(Format fileFormat) { + LinkedList rr = readerMap.get(fileFormat.get(MimeTypeKey)); + String[] names = new String[rr == null ? 0 : rr.size()]; + if (rr != null) { + int i = 0; + for (RegistryEntry e : rr) { + names[i++] = e.className; + } + } + return names; + } + + @Override + public Format getFileFormat(File file) { + String ext = file.getName(); + int p = ext.lastIndexOf('.'); + if (p != -1) { + ext = ext.substring(p + 1); + } + ext = ext.toLowerCase(); + return fileFormatMap.get(ext); + } + + @Override + public String[] getWriterClasses(Format fileFormat) { + LinkedList rr = writerMap.get(fileFormat.get(MimeTypeKey)); + String[] names = new String[rr == null ? 0 : rr.size()]; + if (rr != null) { + int i = 0; + for (RegistryEntry e : rr) { + names[i++] = e.className; + } + } + return names; + } + + @Override + public void putFileFormat(String extension, Format format) { + fileFormatMap.put(extension.toLowerCase(), format); + } + + @Override + public String getExtension(Format ff) { + for (Map.Entry e : fileFormatMap.entrySet()) { + if (e.getValue().get(MimeTypeKey).equals(ff.get(MimeTypeKey))) { + return e.getKey(); + } + } + return ""; + } + + @Override + public void unregisterCodec(String codecClass) { + for (Map.Entry> i:codecMap.entrySet()) { + LinkedList ll=i.getValue(); + for (Iterator j=ll.iterator();j.hasNext();) { + RegistryEntry e=j.next(); + if (e.className.equals(codecClass)) { + j.remove(); + } + } + } + } + + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Format.java b/trunk/libsrc/avi/src/org/monte/media/Format.java new file mode 100644 index 000000000..edabcfc04 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Format.java @@ -0,0 +1,325 @@ +/* + * @(#)Format.java + * + * Copyright (c) 2011-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Specifies the format of a media, for example of audio and video. + * + * @author Werner Randelshofer + * @version $Id: Format.java 299 2013-01-03 07:40:18Z werner $ + */ +public class Format { + + /** + * Holds the properties of the format. + */ + private HashMap properties; + + /** + * Creates a new format onlyWith the specified properties. + */ + public Format(Map properties) { + this(properties, true); + } + + /** + * Creates a new format onlyWith the specified properties. + */ + private Format(Map properties, boolean copy) { + if (copy || ! (properties instanceof HashMap)) { + for (Map.Entry e : properties.entrySet()) { + if (!e.getKey().isAssignable(e.getValue())) { + throw new ClassCastException(e.getValue() + " must be of type " + e.getKey().getValueClass()); + } + } + this.properties = new HashMap< FormatKey, Object>(properties); + } else { + this.properties = (HashMap< FormatKey, Object>) properties; + } + } + + /** + * Creates a new format onlyWith the specified properties. The properties + * must be given as key value pairs. + */ + public Format(Object... p) { + this.properties = new HashMap< FormatKey, Object>(); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + this.properties.put(key, p[i + 1]); + } + } + + @SuppressWarnings("unchecked") + public T get(FormatKey key) { + return (T) properties.get(key); + } + + @SuppressWarnings("unchecked") + public T get(FormatKey key, T defaultValue) { + return (properties.containsKey(key)) ? (T) properties.get(key) : defaultValue; + } + + public boolean containsKey(FormatKey key) { + return properties.containsKey(key); + } + + /** + * Gets the properties of the format as an unmodifiable map. + */ + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Gets the keys of the format as an unmodifiable set. + */ + public Set getKeys() { + return Collections.unmodifiableSet(properties.keySet()); + } + + /** + * Returns true if that format matches this format. That is iff all + * properties defined in both format objects are identical. Properties which + * are only defined in one of the format objects are not considered. + * + * @param that Another format. + * @return True if the other format matches this format. + */ + public boolean matches(Format that) { + for (Map.Entry e : properties.entrySet()) { + if (!e.getKey().isComment()) { + if (that.properties.containsKey(e.getKey())) { + Object a = e.getValue(); + Object b = that.properties.get(e.getKey()); + if (a != b && a == null || !a.equals(b)) { + return false; + } + + } + } + } + return true; + } + + public boolean matchesWithout(Format that, FormatKey... without) { + OuterLoop: + for (Map.Entry e : properties.entrySet()) { + FormatKey k = e.getKey(); + if (!e.getKey().isComment()) { + if (that.properties.containsKey(k)) { + for (int i = 0; i < without.length; i++) { + if (without[i] == k) { + continue OuterLoop; + } + } + Object a = e.getValue(); + Object b = that.properties.get(k); + if (a != b && a == null || !a.equals(b)) { + return false; + } + + } + } + } + return true; + } + + /** + * Creates a new format which contains all properties from this format and + * additional properties from that format.

If a property is specified in + * both formats, then the property value from this format is used. It + * overwrites that format.

If one of the format has more properties than + * the other, then the new format is more specific than this format. + * + * @param that + * @return That format with properties overwritten by this format. + */ + public Format append(Format that) { + HashMap m = new HashMap(this.properties); + for (Map.Entry e : that.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all properties from this format and + * additional properties listed.

If a property is specified in both + * formats, then the property value from this format is used. It overwrites + * that format.

If one of the format has more properties than the other, + * then the new format is more specific than this format. + * + * @param p The properties must be given as key value pairs. + * @return That format with properties overwritten by this format. + */ + public Format append(Object... p) { + HashMap m = new HashMap(this.properties); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + m.put(key, p[i + 1]); + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all properties from the specified + * format and additional properties from this format. + *

If a property is specified in both formats, then the property value + * from this format is used. It overwrites that format. + *

If one of the format has more properties than the other, then the new + * format is more specific than this format. + * + * @param that + * @return That format with properties overwritten by this format. + */ + public Format prepend(Format that) { + HashMap m = new HashMap(that.properties); + for (Map.Entry e : this.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all specified properties and + * additional properties from this format. + *

If a property is specified in both formats, then the property value + * from this format is used. It overwrites that format. + *

If one of the format has more properties than the other, then the new + * format is more specific than this format. + * + * @param p The properties must be given as key value pairs. + * @return That format with properties overwritten by this format. + */ + public Format prepend(Object... p) { + HashMap m = new HashMap(); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + m.put(key, p[i + 1]); + } + for (Map.Entry e : this.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + /** + * Creates a new format which only has the specified keys (or less).

If + * the keys are reduced, then the new format is less specific than this + * format. + */ + public Format intersectKeys(FormatKey... keys) { + HashMap m = new HashMap(); + for (FormatKey k : keys) { + if (properties.containsKey(k)) { + m.put(k, properties.get(k)); + } + } + return new Format(m,false); + } + + /** + * Creates a new format without the specified keys.

If the keys are + * reduced, then the new format is less specific than this format. + */ + public Format removeKeys(FormatKey... keys) { + boolean needsRemoval = false; + for (FormatKey k : keys) { + if (properties.containsKey(k)) { + needsRemoval = true; + break; + } + } + if (!needsRemoval) { + return this; + } + + HashMap m = new HashMap(properties); + for (FormatKey k : keys) { + m.remove(k); + } + return new Format(m,false); + } + + /** + * Returns true if the format has the specified keys. + */ + public Format containsKeys(FormatKey... keys) { + HashMap m = new HashMap(properties); + for (FormatKey k : keys) { + m.remove(k); + } + return new Format(m,false); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("Format{"); + boolean isFirst = true; + for (Map.Entry e : properties.entrySet()) { + if (isFirst) { + isFirst = false; + } else { + buf.append(','); + } + buf.append(e.getKey().toString()); + buf.append(':'); + appendStuffedString(e.getValue(), buf); + } + buf.append('}'); + return buf.toString(); + } + + /** + * This method is used by #toString. + */ + private static void appendStuffedString(Object value, StringBuilder stuffed) { + if (value == null) { + stuffed.append("null"); + } + value = value.toString(); + if (value instanceof String) { + for (char ch : ((String) value).toCharArray()) { + if (ch >= ' ') { + stuffed.append(ch); + } else { + String hex = Integer.toHexString(ch); + stuffed.append("\\u"); + for (int i = hex.length(); i < 4; i++) { + stuffed.append('0'); + } + stuffed.append(hex); + } + } + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKey.java b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java new file mode 100644 index 000000000..374d76ca0 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java @@ -0,0 +1,112 @@ +/* + * @(#)FormatKey.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.io.Serializable; + +/** + * A FormatKey provides type-safe access to an attribute of + * a {@link Format}. + *

+ * A format key has a name, a type and a value. + * + * @author Werner Randelshofer + * @version $Id: FormatKey.java 299 2013-01-03 07:40:18Z werner $ + */ +public class FormatKey implements Serializable, Comparable { + + public static final long serialVersionUID = 1L; + /** + * Holds a String representation of the attribute key. + */ + private String key; + /** + * Holds a pretty name. This can be null, if the value is self-explaining. + */ + private String name; + /** This variable is used as a "type token" so that we can check for + * assignability of attribute values at runtime. + */ + private Class clazz; + + /** Comment keys are ignored when matching two media formats with each other. */ + private boolean comment; + + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, Class clazz) { + this(key, key, clazz); + } + + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, String name, Class clazz) { + this(key,name,clazz,false); + } + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, String name, Class clazz, boolean comment) { + this.key = key; + this.name = name; + this.clazz = clazz; + this.comment=comment; + } + + /** + * Returns the key string. + * @return key string. + */ + public String getKey() { + return key; + } + + /** + * Returns the pretty name string. + * @return name string. + */ + public String getName() { + return name; + } + + /** Returns the key string. */ + @Override + public String toString() { + return key; + } + + /** + * Returns true if the specified value is assignable with this key. + * + * @param value + * @return True if assignable. + */ + public boolean isAssignable(Object value) { + return clazz.isInstance(value); + } + + public boolean isComment() { + return comment; + } + + + public Class getValueClass() { + return clazz; + } + + @Override + public int compareTo(Object o) { + return compareTo((FormatKey) o); + } + + public int compareTo(FormatKey that) { + return this.key.compareTo(that.key); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java new file mode 100644 index 000000000..71dff637f --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java @@ -0,0 +1,61 @@ +/* + * @(#)FormatKeys.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; + +/** + * Defines common {@code FormatKey}'s. + * + * @author Werner Randelshofer + * @version $Id: FormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class FormatKeys { + public static enum MediaType { + AUDIO, + VIDEO, + MIDI, + TEXT, + META, + FILE + } + /** + * The media MediaTypeKey. + */ + public final static FormatKey MediaTypeKey = new FormatKey("mediaType", MediaType.class); + /** + * The EncodingKey. + */ + public final static FormatKey EncodingKey = new FormatKey("encoding", String.class); + + // + public final static String MIME_AVI = "video/avi"; + public final static String MIME_QUICKTIME = "video/quicktime"; + public final static String MIME_MP4 = "video/mp4"; + public final static String MIME_JAVA = "Java"; + public final static String MIME_ANIM = "x-iff/anim"; + public final static String MIME_IMAGE_SEQUENCE = "ImageSequence"; + /** + * The mime type. + */ + public final static FormatKey MimeTypeKey = new FormatKey("mimeType", String.class); + /** + * The number of frames per second. + */ + public final static FormatKey FrameRateKey = new FormatKey("frameRate", Rational.class); + + /** + * The interval between key frames. + * If this value is not specified, most codecs will use {@code FrameRateKey} + * as a hint and try to produce one key frame per second. + */ + public final static FormatKey KeyFrameIntervalKey = new FormatKey("keyFrameInterval", Integer.class); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieReader.java b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java new file mode 100644 index 000000000..0370eea0c --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java @@ -0,0 +1,93 @@ +/* + * @(#)MovieReader.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; +import java.io.IOException; + +/** + * A simple API for reading movie data (audio and video) from a file. + * + *

+ * FIXME - MovieReader should extend Demultiplexer + * + * @author Werner Randelshofer + * @version $Id: MovieReader.java 299 2013-01-03 07:40:18Z werner $ + */ +public interface MovieReader { + /** Returns the number of tracks. */ + public int getTrackCount() throws IOException; + + /** Finds a track with the specified format. + * + * @param fromTrack the start track number. + * @param format A format specification. + * @return The track number >= fromTrack or -1 if no track has been found. + */ + public int findTrack(int fromTrack, Format format) throws IOException; + + /** Returns the total duration of the movie . */ + public Rational getDuration() throws IOException; + /** Returns the duration of the specified track. */ + public Rational getDuration(int track) throws IOException; + + /** Returns the sample number for the specified time. */ + public long timeToSample(int track, Rational seconds) throws IOException; + /** Returns the time for the specified sample number. */ + public Rational sampleToTime(int track, long sample) throws IOException; + + /** Returns the file format. */ + public Format getFileFormat() throws IOException; + + /** Returns the media format of the specified track. + * + * @param track Track number. + * @return The media format of the track. + */ + public Format getFormat(int track) throws IOException; + + /** Returns the number of media data chunks in the specified track. + * A chunk contains one or more samples. + */ + public long getChunkCount(int track) throws IOException; + + /** Reads the next sample chunk from the specified track. + * + * @param track Track number. + * @param buffer The buffer into which to store the sample data. + */ + public void read(int track, Buffer buffer) throws IOException; + /** Reads the next sample chunk from the next track in playback sequence. + * The variable buffer.track contains the track number. + * + * @param buf The buffer into which to store the sample data. + */ + //public void read(Buffer buffer) throws IOException; + + /** Returns the index of the next track in playback sequence. + * + * @return Index of next track or -1 if end of media reached. + */ + public int nextTrack() throws IOException; + + public void close() throws IOException; + + /** Sets the read time of all tracks to the closest sync sample before or + * at the specified time. + * + * @param newValue Time in seconds. + */ + public void setMovieReadTime(Rational newValue) throws IOException; + + /** Returns the current time of the track. */ + public Rational getReadTime(int track) throws IOException; + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java new file mode 100644 index 000000000..e4fd559cf --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java @@ -0,0 +1,82 @@ +/* + * @(#)MovieWriter.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; +import java.io.IOException; + +/** + * A simple API for writing movie data (audio and video) into a file. + * + * @author Werner Randelshofer + * @version $Id: MovieWriter.java 299 2013-01-03 07:40:18Z werner $ + */ +public interface MovieWriter extends Multiplexer { + /** Returns the file format. */ + public Format getFileFormat() throws IOException; + + /** Adds a track to the writer for a suggested input format. + *

+ * The format should at least specify the desired {@link FormatKeys.MediaType}. + * The actual input format is a refined version of the suggested format. For + * example, if a MovieWriter only supports fixed frame rate video, then the + * MovieWriter will extend the format with that information. + *

+ * If the suggested input format is not compatible, then an IOException is + * thrown. For example, if a MovieWriter only supports fixed frame rate video, + * but a format with variable frame rate was requested. + * + * @param format The desired input format of the track. The actual input + * format may be a refined version of the specified format. + * @return The track number. + */ + public int addTrack(Format format) throws IOException; + + /** Returns the media format of the specified track. + * This is a refined version of the format that was requested when the + * track was added. See {@link #addTrack}. + * + * @param track Track number. + * @return The media format of the track. + */ + public Format getFormat(int track); + + /** Returns the number of tracks. */ + public int getTrackCount(); + + /** Writes a sample into the specified track. + * Does nothing if the discard-flag in the buffer is set to true. + * + * @param track The track number. + * @param buf The buffer containing the sample data. + */ + @Override + public void write(int track, Buffer buf) throws IOException; + + /** Closes the writer. */ + @Override + public void close() throws IOException; + + /** Returns true if the limit for media data has been reached. + * If this limit is reached, no more samples should be added to the movie. + *

+ * This limit is imposed by data structures of the movie file + * which will overflow if more samples are added to the movie. + *

+ * FIXME - Maybe replace by getCapacity():long. + */ + public boolean isDataLimitReached(); + + /** Returns the duration of the track in seconds. */ + public Rational getDuration(int track); + /** Returns true if the specified track has no samples. */ + public boolean isEmpty(int track); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java new file mode 100644 index 000000000..04311c5d9 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java @@ -0,0 +1,34 @@ +/* + * @(#)Multiplexer.java 1.0 2011-02-19 + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ + +package org.monte.media; + +import java.io.IOException; + +/** + * A {@code Multiplexer} can write multiple media tracks into a + * single output stream. + * + * @author Werner Randelshofer + * @version 1.0 2011-02-19 Created. + */ +public interface Multiplexer { + /** Writes a sample. + * Does nothing if the discard-flag in the buffer is set to true. + * + * @param track The track number. + * @param buf The buffer containing the sample data. + */ + public void write(int track, Buffer buf) throws IOException; + + /** Closes the Multiplexer. */ + public void close() throws IOException; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/ParseException.java b/trunk/libsrc/avi/src/org/monte/media/ParseException.java new file mode 100644 index 000000000..7aee64949 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/ParseException.java @@ -0,0 +1,30 @@ +/* + * @(#)ParseException.java + * + * Copyright (c) 1999-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +/** + * Exception thrown by IFFParse. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version $Id: ParseException.java 299 2013-01-03 07:40:18Z werner $ + */ +public class ParseException extends Exception { + + public static final long serialVersionUID = 1L; + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Registry.java b/trunk/libsrc/avi/src/org/monte/media/Registry.java new file mode 100644 index 000000000..e0f660a93 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Registry.java @@ -0,0 +1,310 @@ +/* + * @(#)Registry.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.io.File; +import java.util.ArrayList; +import static org.monte.media.FormatKeys.*; + +/** + * The {@code Registry} for audio and video codecs. + * + * @author Werner Randelshofer + * @version $Id: Registry.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class Registry { + + private static Registry instance; + + public static Registry getInstance() { + if (instance == null) { + instance = new DefaultRegistry(); + instance.init(); + } + return instance; + } + + /** + * Initializes the registry. + */ + protected abstract void init(); + + /** + * Puts a codec into the registry. + * + * @param inputFormat The input format. Must not be null. + * @param outputFormat The output format. Must not be null. + * @param codecClass The codec class name. Must not be null. + */ + public abstract void putCodec(Format inputFormat, Format outputFormat, String codecClass); + + /** + * Gets all codecs which can decode the specified format. + * + * @param format The format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final String[] getDecoderClasses(Format format) { + return getCodecClasses(format, null); + } + + /** + * Gets all codecs which can decode the specified format. + * + * @param format The format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final String[] getEncoderClasses(Format format) { + return getCodecClasses(null, format); + } + + /** + * Gets all codecs which can transcode from the specified input format to + * the specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public abstract String[] getCodecClasses(// + Format inputFormat, + Format outputFormat); + + /** + * Gets all codecs which can decode the specified format. + * + * @param inputFormat The input format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final Codec[] getDecoders(Format inputFormat) { + return getCodecs(inputFormat, null); + } + + /** + * Gets the first codec which can decode the specified format. + * + * @param inputFormat The output format. + * @return A codec. Returns null if no codec was found. + */ + public Codec getDecoder(Format inputFormat) { + return getCodec(inputFormat, null); + } + + /** + * Gets all codecs which can encode the specified format. + * + * @param outputFormat The output format. + * @return An array of codecs. If no codec was found, an empty array is + * returned. + */ + public final Codec[] getEncoders(Format outputFormat) { + return getCodecs(null, outputFormat); + } + + /** + * Gets the first codec which can encode the specified foramt. + * + * @param outputFormat The output format. + * @return A codec. Returns null if no codec was found. + */ + public Codec getEncoder(Format outputFormat) { + return getCodec(null, outputFormat); + } + + /** + * Gets all codecs which can transcode from the specified input format to + * the specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public Codec[] getCodecs(Format inputFormat, Format outputFormat) { + String[] clazz = getCodecClasses(inputFormat, outputFormat); + ArrayList codecs = new ArrayList(clazz.length); + for (int i = 0; i < clazz.length; i++) { + try { + codecs.add((Codec) Class.forName(clazz[i]).newInstance()); + } catch (Exception ex) { + //ex.printStackTrace(); + System.err.println("Monte Registry. Codec class not found: " + clazz[i]); + unregisterCodec(clazz[i]); + } + } + return codecs.toArray(new Codec[codecs.size()]); + } + + /** + * Gets a codec which can transcode from the specified input format to the + * specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return A codec or null. + */ + public Codec getCodec(Format inputFormat, Format outputFormat) { + String[] clazz = getCodecClasses(inputFormat, outputFormat); + for (int i = 0; i < clazz.length; i++) { + try { + Codec codec = ((Codec) Class.forName(clazz[i]).newInstance()); + codec.setInputFormat(inputFormat); + if (outputFormat != null) { + codec.setOutputFormat(outputFormat); + } + return codec; + } catch (Exception ex) { + //ex.printStackTrace(); + System.err.println("Monte Registry. Codec class not found: " + clazz[i]); + unregisterCodec(clazz[i]); + } + } + return null; + } + + /** + * Puts a reader into the registry. + * + * @param fileFormat The file format, e.g."video/avi", "video/quicktime". + * Use "Java" for formats which are not tied to a file format. Must not be + * null. + * @param readerClass The reader class name. Must not be null. + */ + public abstract void putReader(Format fileFormat, String readerClass); + + /** + * Puts a writer into the registry. + * + * @param fileFormat The file format, e.g."video/avi", "video/quicktime". + * Use "Java" for formats which are not tied to a file format. Must not be + * null. + * @param writerClass The writer class name. Must not be null. + */ + public abstract void putWriter(Format fileFormat, String writerClass); + + /** + * Gets all reader class names from the registry for the specified file + * format. + * + * @param fileFormat The file format, e.g."AVI", "QuickTime". + * @return The reader class names. + */ + public abstract String[] getReaderClasses(Format fileFormat); + + /** + * Gets all writer class names from the registry for the specified file + * format. + * + * @param fileFormat The file format, e.g."AVI", "QuickTime". + * @return The writer class names. + */ + public abstract String[] getWriterClasses(Format fileFormat); + + public MovieReader getReader(Format fileFormat, File file) { + String[] clazz = getReaderClasses(fileFormat); + for (int i = 0; i < clazz.length; i++) { + try { + return ((MovieReader) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return null; + } + + public MovieWriter getWriter(File file) { + Format format = getFileFormat(file); + return format == null ? null : getWriter(format, file); + } + + public MovieWriter getWriter(Format fileFormat, File file) { + String[] clazz = getWriterClasses(fileFormat); + for (int i = 0; i < clazz.length; i++) { + try { + return ((MovieWriter) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return null; + } + + public MovieReader getReader(File file) { + Format format = getFileFormat(file); + return format == null ? null : getReader(format, file); + } + + public abstract void putFileFormat(String extension, Format format); + + public abstract Format getFileFormat(File file); + + public abstract Format[] getReaderFormats(); + + public abstract Format[] getWriterFormats(); + + public abstract Format[] getFileFormats(); + + public abstract String getExtension(Format ff); + + /** + * Suggests output formats for the given input media format and specified + * file format. + * + * @param inputMediaFormat + * @param outputFileFormat + * @return List of output media formats. + */ + public ArrayList suggestOutputFormats(Format inputMediaFormat, Format outputFileFormat) { + ArrayList formats = new ArrayList(); + Format matchFormat = new Format(// + MimeTypeKey, outputFileFormat.get(MimeTypeKey),// + MediaTypeKey, inputMediaFormat.get(MediaTypeKey)); + Codec[] codecs = getEncoders(matchFormat); + int matchingCount = 0; + for (Codec c : codecs) { + for (Format mf : c.getOutputFormats(null)) { + if (mf.matches(matchFormat)) { + if (inputMediaFormat.matchesWithout(mf, MimeTypeKey)) { + // add matching formats first + formats.add(0, mf.append(inputMediaFormat)); + matchingCount++; + } else if (inputMediaFormat.matchesWithout(mf, MimeTypeKey, EncodingKey)) { + // add formats which match everything but the encoding second + formats.add(matchingCount, mf.append(inputMediaFormat)); + } else { + // add remaining formats last + formats.add(mf.append(inputMediaFormat)); + } + } + } + } + + // remove duplicates + for (int i = formats.size() - 1; i >= 0; i--) { + Format fi = formats.get(i); + for (int j = i - 1; j >= 0; j--) { + Format fj = formats.get(j); + if (fi.matches(fj)) { + formats.remove(i); + break; + } + } + } + + return formats; + } + + public abstract void unregisterCodec(String codecClass); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java new file mode 100644 index 000000000..2bb943c3b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java @@ -0,0 +1,77 @@ +/* + * @(#)VideoFormatKeys.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; + +/** + * Defines common format keys for video media. + * + * @author Werner Randelshofer + * @version $Id: VideoFormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class VideoFormatKeys extends FormatKeys { + // Standard video ENCODING strings for use with FormatKey.Encoding. + public static final String ENCODING_BUFFERED_IMAGE = "image"; + /** Cinepak format. */ + public static final String ENCODING_QUICKTIME_CINEPAK = "cvid"; + public static final String COMPRESSOR_NAME_QUICKTIME_CINEPAK = "Cinepak"; + /** JPEG format. */ + public static final String ENCODING_QUICKTIME_JPEG = "jpeg"; + public static final String COMPRESSOR_NAME_QUICKTIME_JPEG = "Photo - JPEG"; + /** PNG format. */ + public static final String ENCODING_QUICKTIME_PNG = "png "; + public static final String COMPRESSOR_NAME_QUICKTIME_PNG = "PNG"; + /** Animation format. */ + public static final String ENCODING_QUICKTIME_ANIMATION = "rle "; + public static final String COMPRESSOR_NAME_QUICKTIME_ANIMATION = "Animation"; + /** Raw format. */ + public static final String ENCODING_QUICKTIME_RAW = "raw "; + public static final String COMPRESSOR_NAME_QUICKTIME_RAW = "NONE"; + // AVI Formats + /** Microsoft Device Independent Bitmap (DIB) format. */ + public static final String ENCODING_AVI_DIB = "DIB "; + /** Microsoft Run Length format. */ + public static final String ENCODING_AVI_RLE = "RLE "; + /** Techsmith Screen Capture format. */ + public static final String ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE = "tscc"; + public static final String COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE = "Techsmith Screen Capture"; + /** DosBox Screen Capture format. */ + public static final String ENCODING_AVI_DOSBOX_SCREEN_CAPTURE = "ZMBV"; + /** JPEG format. */ + public static final String ENCODING_AVI_MJPG = "MJPG"; + /** PNG format. */ + public static final String ENCODING_AVI_PNG = "png "; + /** Interleaved planar bitmap format. */ + public static final String ENCODING_BITMAP_IMAGE = "ILBM"; + + // + + /** The WidthKey of a video frame. */ + public final static FormatKey WidthKey = new FormatKey("dimX","width", Integer.class); + /** The HeightKey of a video frame. */ + public final static FormatKey HeightKey = new FormatKey("dimY","height", Integer.class); + /** The number of bits per pixel. */ + public final static FormatKey DepthKey = new FormatKey("dimZ","depth", Integer.class); + /** The data class. */ + public final static FormatKey DataClassKey = new FormatKey("dataClass", Class.class); + /** The compressor name. */ + public final static FormatKey CompressorNameKey = new FormatKey("compressorName", "compressorName",String.class, true); + /** The pixel aspect ratio WidthKey : HeightKey; + */ + public final static FormatKey PixelAspectRatioKey = new FormatKey("pixelAspectRatio", Rational.class); + /** Whether the frame rate must be fixed. False means variable frame rate. */ + public final static FormatKey FixedFrameRateKey = new FormatKey("fixedFrameRate", Boolean.class); + /** Whether the video is interlaced. */ + public final static FormatKey InterlaceKey = new FormatKey("interlace", Boolean.class); + /** Encoding quality. Value between 0 and 1. */ + public final static FormatKey QualityKey = new FormatKey("quality", Float.class); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java new file mode 100644 index 000000000..4f9a68bee --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java @@ -0,0 +1,106 @@ +/* + * @(#)AVIBMPDIB.java 1.0 2009-12-30 + * + * Copyright (c) 2009 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.avi; + +import org.monte.media.io.ImageInputStreamAdapter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.*; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +/** + * This class defines the JPEG Huffman table, which is omitted in AVI MJPEG + * files. + *

+ * Source: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + * + * @author Werner Randelshofer + * @version 1.0 2009-12-30 Created. + */ + public class AVIBMPDIB { + + /** MJPG DHT Segment */ + private static byte[] MJPGDHTSeg = { + /* JPEG DHT Segment for YCrCb omitted from MJPG data */ + (byte) 0xFF, (byte) 0xC4, (byte) 0x01, (byte) 0xA2, + (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x01, + (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x10, (byte) 0x00, + (byte) 0x02, (byte) 0x01, (byte) 0x03, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x03, (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x7D, + (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x11, (byte) 0x05, (byte) 0x12, (byte) 0x21, (byte) 0x31, (byte) 0x41, (byte) 0x06, (byte) 0x13, (byte) 0x51, (byte) 0x61, + (byte) 0x07, (byte) 0x22, (byte) 0x71, (byte) 0x14, (byte) 0x32, (byte) 0x81, (byte) 0x91, (byte) 0xA1, (byte) 0x08, (byte) 0x23, (byte) 0x42, (byte) 0xB1, (byte) 0xC1, (byte) 0x15, (byte) 0x52, + (byte) 0xD1, (byte) 0xF0, (byte) 0x24, (byte) 0x33, (byte) 0x62, (byte) 0x72, (byte) 0x82, (byte) 0x09, (byte) 0x0A, (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x25, + (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, + (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, + (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x83, + (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, + (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, + (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, (byte) 0xD3, + (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, + (byte) 0xE9, (byte) 0xEA, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x11, (byte) 0x00, (byte) 0x02, + (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x03, (byte) 0x04, (byte) 0x07, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x77, (byte) 0x00, + (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x11, (byte) 0x04, (byte) 0x05, (byte) 0x21, (byte) 0x31, (byte) 0x06, (byte) 0x12, (byte) 0x41, (byte) 0x51, (byte) 0x07, (byte) 0x61, (byte) 0x71, + (byte) 0x13, (byte) 0x22, (byte) 0x32, (byte) 0x81, (byte) 0x08, (byte) 0x14, (byte) 0x42, (byte) 0x91, (byte) 0xA1, (byte) 0xB1, (byte) 0xC1, (byte) 0x09, (byte) 0x23, (byte) 0x33, (byte) 0x52, + (byte) 0xF0, (byte) 0x15, (byte) 0x62, (byte) 0x72, (byte) 0xD1, (byte) 0x0A, (byte) 0x16, (byte) 0x24, (byte) 0x34, (byte) 0xE1, (byte) 0x25, (byte) 0xF1, (byte) 0x17, (byte) 0x18, (byte) 0x19, + (byte) 0x1A, (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, + (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, + (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x82, + (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, + (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, + (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, + (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, + (byte) 0xE9, (byte) 0xEA, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA + }; + /** JFIF Start of Image (SOI) segment. */ + private static byte[] JFIFSOISeg = { + (byte) 0xff, + (byte) 0xd8 + }; + + public static InputStream prependDHTSeg(byte[] jpgWithoutDHT) { + return prependDHTSeg(jpgWithoutDHT, 0, jpgWithoutDHT.length); + } + public static InputStream prependDHTSeg(byte[] jpgWithoutDHT, int offset, int length) { + + // FIXME - Only add DHT Segment if none is present + + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + v.add(new ByteArrayInputStream(jpgWithoutDHT,offset+JFIFSOISeg.length,length-JFIFSOISeg.length)); + return new SequenceInputStream(v.elements()); + } + + public static ImageInputStream prependDHTSeg(ImageInputStream iisWithoutDHT) throws IOException { + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + iisWithoutDHT.seek(2);// skip JFIF SOI + v.add(new ImageInputStreamAdapter(iisWithoutDHT)); + return new MemoryCacheImageInputStream(new SequenceInputStream(v.elements())); + } + public static ImageInputStream prependDHTSeg(InputStream inWithoutDHT) throws IOException { + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + inWithoutDHT.skip(2);// skip JFIF SOI + v.add(inWithoutDHT); + return new MemoryCacheImageInputStream(new SequenceInputStream(v.elements())); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java new file mode 100644 index 000000000..6207d2013 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java @@ -0,0 +1,1117 @@ +/** + * @(#)AVIOutputStream.java + * + * Copyright (c) 2011-2012 Werner Randelshofer, Goldau, Switzerland. All + * rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. For details see + * accompanying license terms. + */ +package org.monte.media.avi; + +import java.awt.image.ColorModel; +import org.monte.media.riff.RIFFChunk; +import org.monte.media.math.Rational; +import java.util.ArrayList; +import org.monte.media.Format; +import org.monte.media.riff.RIFFParser; +import java.awt.Dimension; +import java.awt.image.IndexColorModel; +import java.io.*; +import java.nio.ByteOrder; +import javax.imageio.stream.*; +import static java.lang.Math.*; +import static org.monte.media.FormatKeys.*; +import static org.monte.media.AudioFormatKeys.*; +import static org.monte.media.VideoFormatKeys.*; + +/** + * Provides low-level support for writing already encoded audio and video + * samples into an AVI 1.0 file.

The length of an AVI 1.0 file is limited to + * 1 GB. This class supports lengths of up to 4 GB, but such files may not work + * on all players.

For detailed information about the AVI 1.0 file format + * see:
msdn.microsoft.com + * AVI RIFF
www.microsoft.com + * FOURCC for Video Compression
www.saettler.com + * RIFF
+ * + * @author Werner Randelshofer + * @version $Id: AVIOutputStream.java 306 2013-01-04 16:19:29Z werner $ + */ +public class AVIOutputStream extends AbstractAVIStream { + + /** + * The states of the movie output stream. + */ + protected static enum States { + + STARTED, FINISHED, CLOSED; + } + /** + * The current state of the movie output stream. + */ + protected States state = States.FINISHED; + /** + * This chunk holds the whole AVI content. + */ + protected CompositeChunk aviChunk; + /** + * This chunk holds the movie frames. + */ + protected CompositeChunk moviChunk; + /** + * This chunk holds the AVI Main Header. + */ + protected FixedSizeDataChunk avihChunk; + ArrayList idx1 = new ArrayList(); + + /** + * Creates a new instance. + * + * @param file the output file + */ + public AVIOutputStream(File file) throws IOException { + if (file.exists()) { + file.delete(); + } + this.out = new FileImageOutputStream(file); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + this.streamOffset = 0; + } + + /** + * Creates a new instance. + * + * @param out the output stream. + */ + public AVIOutputStream(ImageOutputStream out) throws IOException { + this.out = out; + this.streamOffset = out.getStreamPosition(); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + /** + * Adds a video track. + * + * @param fccHandler The 4-character code of the format. + * @param scale The numerator of the sample rate. + * @param rate The denominator of the sample rate. + * @param width The width of a video image. Must be greater than 0. + * @param height The height of a video image. Must be greater than 0. + * @param depth The number of bits per pixel. Must be greater than 0. + * @param syncInterval Interval for sync-samples. 0=automatic. 1=all frames + * are keyframes. Values larger than 1 specify that for every n-th frame is + * a keyframe. + * + * @return Returns the track index. + * + * @throws IllegalArgumentException if the width or the height is smaller + * than 1. + */ + public int addVideoTrack(String fccHandler, long scale, long rate, int width, int height, int depth, int syncInterval) throws IOException { + ensureFinished(); + if (fccHandler == null || fccHandler.length() != 4) { + throw new IllegalArgumentException("fccHandler must be 4 characters long:" + fccHandler); + } + VideoTrack vt = new VideoTrack(tracks.size(), typeToInt(fccHandler),// + new Format(MediaTypeKey, MediaType.VIDEO, + MimeTypeKey, MIME_AVI, + EncodingKey, fccHandler, + DataClassKey, byte[].class, + WidthKey, width, HeightKey, height, DepthKey, depth, + FixedFrameRateKey, true, + FrameRateKey, new Rational(rate, scale))); + vt.scale = scale; + vt.rate = rate; + vt.syncInterval = syncInterval; + vt.frameLeft = 0; + vt.frameTop = 0; + vt.frameRight = width; + vt.frameBottom = height; + vt.bitCount = depth; + vt.planes = 1; // must be 1 + + if (depth == 4) { + byte[] gray = new byte[16]; + for (int i = 0; i < gray.length; i++) { + gray[i] = (byte) ((i << 4) | i); + } + vt.palette = new IndexColorModel(4, 16, gray, gray, gray); + } else if (depth == 8) { + byte[] gray = new byte[256]; + for (int i = 0; i < gray.length; i++) { + gray[i] = (byte) i; + } + vt.palette = new IndexColorModel(8, 256, gray, gray, gray); + } + + tracks.add(vt); + return tracks.size() - 1; + } + + /** + * Adds an audio track. + * + * @param waveFormatTag The format of the audio stream given in MMREG.H, for + * example 0x0001 for WAVE_FORMAT_PCM. + * @param scale The numerator of the sample rate. + * @param rate The denominator of the sample rate. + * @param numberOfChannels The number of channels: 1 for mono, 2 for stereo. + * @param sampleSizeInBits The number of bits in a sample: 8 or 16. + * @param isCompressed Whether the sound is compressed. + * @param frameDuration The frame duration, expressed in the media’s + * timescale, where the timescale is equal to the sample rate. For + * uncompressed formats, this field is always 1. + * @param frameSize For uncompressed audio, the number of bytes in a sample + * for a single channel (sampleSize divided by 8). For compressed audio, the + * number of bytes in a frame. + * + * @throws IllegalArgumentException if the format is not 4 characters long, + * if the time scale is not between 1 and 2^32, if the integer portion of + * the sampleRate is not equal to the scale, if numberOfChannels is not 1 or + * 2. + * @return Returns the track index. + */ + public int addAudioTrack(int waveFormatTag, // + long scale, long rate, // + int numberOfChannels, int sampleSizeInBits, // + boolean isCompressed, // + int frameDuration, int frameSize) throws IOException { + ensureFinished(); + + if (scale < 1 || scale > (2L << 32)) { + throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + scale); + } + if (numberOfChannels != 1 && numberOfChannels != 2) { + throw new IllegalArgumentException("numberOfChannels must be 1 or 2: " + numberOfChannels); + } + if (sampleSizeInBits != 8 && sampleSizeInBits != 16) { + throw new IllegalArgumentException("sampleSize must be 8 or 16: " + numberOfChannels); + } + + AudioTrack t = new AudioTrack(tracks.size(), typeToInt("\u0000\u0000\u0000\u0000")); + t.wFormatTag = waveFormatTag; + + float afSampleRate = (float) rate / (float) scale; + + t.format = new Format(MediaTypeKey, MediaType.AUDIO, + MimeTypeKey, MIME_AVI, + EncodingKey, RIFFParser.idToString(waveFormatTag), + SampleRateKey, Rational.valueOf(afSampleRate), + SampleSizeInBitsKey, sampleSizeInBits, + ChannelsKey, numberOfChannels, + FrameSizeKey, frameSize, + FrameRateKey, Rational.valueOf(afSampleRate), + SignedKey, sampleSizeInBits != 8, + ByteOrderKey, ByteOrder.LITTLE_ENDIAN); + + t.scale = scale; + t.rate = rate; + t.samplesPerSec = rate / scale; + t.channels = numberOfChannels; + t.avgBytesPerSec = t.samplesPerSec * frameSize; + t.blockAlign = t.channels * sampleSizeInBits / 8; + t.bitsPerSample = sampleSizeInBits; + tracks.add(t); + return tracks.size() - 1; + } + + /** + * Sets the global color palette. + */ + public void setPalette(int track, ColorModel palette) { + if (palette instanceof IndexColorModel) { + ((VideoTrack) tracks.get(track)).palette = (IndexColorModel) palette; + } + } + + /** + * Gets the dimension of a track. + */ + public Dimension getVideoDimension(int track) { + Track tr = tracks.get(track); + if (tr instanceof VideoTrack) { + VideoTrack vt = (VideoTrack) tr; + Format fmt = vt.format; + return new Dimension(fmt.get(WidthKey), fmt.get(HeightKey)); + } else { + return new Dimension(0, 0); + } + } + + /** + * Returns the contents of the extra track header. Returns null if the + * header is not present.

Note: this method can only be performed before + * media data has been written into the tracks. + * + * @param track + * @param fourcc + * @param data the extra header as a byte array + * @throws IOException + */ + public void putExtraHeader(int track, String fourcc, byte[] data) throws IOException { + if (state == States.STARTED) { + throw new IllegalStateException("Stream headers have already been written!"); + } + Track tr = tracks.get(track); + int id = RIFFParser.stringToID(fourcc); + // Remove duplicate entries + for (int i = tr.extraHeaders.size() - 1; i >= 0; i--) { + if (tr.extraHeaders.get(i).getID() == id) { + tr.extraHeaders.remove(i); + } + } + + // Add new entry + RIFFChunk chunk = new RIFFChunk(STRH_ID, id, data.length, -1); + chunk.setData(data); + tr.extraHeaders.add(chunk); + } + + /** + * Returns the fourcc's of all extra stream headers. + * + * @param track + * @return An array of fourcc's of all extra stream headers. + * @throws IOException + */ + public String[] getExtraHeaderFourCCs(int track) throws IOException { + Track tr = tracks.get(track); + String[] fourccs = new String[tr.extraHeaders.size()]; + for (int i = 0; i < fourccs.length; i++) { + fourccs[i] = RIFFParser.idToString(tr.extraHeaders.get(i).getID()); + } + return fourccs; + } + + public void setName(int track, String name) { + tracks.get(track).name = name; + } + + /** + * Sets the compression quality of a track.

A value of 0 stands for + * "high compression is important" a value of 1 for "high image quality is + * important".

Changing this value affects the encoding of video frames + * which are subsequently written into the track. Frames which have already + * been written are not changed.

This value has no effect on videos + * encoded with lossless encoders such as the PNG format.

The default + * value is 0.97. + * + * @param newValue + */ + public void setCompressionQuality(int track, float newValue) { + VideoTrack vt = (VideoTrack) tracks.get(track); + vt.videoQuality = newValue; + } + + /** + * Returns the compression quality of a track. + * + * @return compression quality + */ + public float getCompressionQuality(int track) { + return ((VideoTrack) tracks.get(track)).videoQuality; + } /** + * Sets the state of the QuickTimeOutpuStream to started.

If the state + * is changed by this method, the prolog is written. + */ + protected void ensureStarted() throws IOException { + if (state != States.STARTED) { + writeProlog(); + state = States.STARTED; + } + } + + /** + * Sets the state of the QuickTimeOutpuStream to finished.

If the state + * is changed by this method, the prolog is written. + */ + protected void ensureFinished() throws IOException { + if (state != States.FINISHED) { + throw new IllegalStateException("Writer is in illegal state for this operation."); + } + } + + /** + * Writes an already encoded palette change into the specified track.

If + * a track contains palette changes, then all key frames must be immediately + * preceeded by a palette change chunk which also is a key frame. If a key + * frame is not preceeded by a key frame palette change chunk, it will be + * downgraded to a delta frame. + * + * @throws IllegalArgumentException if the track is not a video track. + */ + public void writePalette(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + Track tr = tracks.get(track); + if (!(tr instanceof VideoTrack)) { + throw new IllegalArgumentException("Error: track " + track + " is not a video track."); + } + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe."); + } + + VideoTrack vt = (VideoTrack) tr; + tr.flags |= STRH_FLAG_VIDEO_PALETTE_CHANGES; + + DataChunk paletteChangeChunk = new DataChunk(vt.twoCC | PC_ID); + long offset = getRelativeStreamPosition(); + ImageOutputStream pOut = paletteChangeChunk.getOutputStream(); + pOut.write(data, off, len); + moviChunk.add(paletteChangeChunk); + paletteChangeChunk.finish(); + long length = getRelativeStreamPosition() - offset; + Sample s = new Sample(paletteChangeChunk.chunkType, 0, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + //tr.length+=0; Length is not affected by this chunk! + offset = getRelativeStreamPosition(); + } + + /** + * Writes an already encoded sample from a file to the specified track.

+ * This method does not inspect the contents of the file. For example, Its + * your responsibility to only append JPG files if you have chosen the JPEG + * video format.

If you append all frames from files or from input + * streams, then you have to explicitly set the dimension of the video track + * before you call finish() or close(). + * + * @param file The file which holds the sample data. + * + * @throws IllegalStateException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, File file, boolean isKeyframe) throws IOException { + FileInputStream in = null; + try { + in = new FileInputStream(file); + writeSample(track, in, isKeyframe); + } finally { + if (in != null) { + in.close(); + } + } + } + + /** + * Writes an already encoded sample from an input stream to the specified + * track.

This method does not inspect the contents of the file. For + * example, its your responsibility to only append JPG files if you have + * chosen the JPEG video format.

If you append all frames from files or + * from input streams, then you have to explicitly set the dimension of the + * video track before you call finish() or close(). + * + * @param track The track number. + * @param in The input stream which holds the sample data. + * @param isKeyframe True if the sample is a key frame. + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, InputStream in, boolean isKeyframe) throws IOException { + ensureStarted(); + + Track tr = tracks.get(track); + + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe."); + } + + // If a stream has palette changes, then only palette change samples can + // be marked as keyframe. + if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) { + // If a keyframe sample is immediately preceeded by a palette change + // we can raise the palette change to a keyframe. + if (tr.samples.size() > 0) { + Sample s = tr.samples.get(tr.samples.size() - 1); + if ((s.chunkType & 0xffff) == PC_ID) { + s.isKeyframe = true; + } + } + isKeyframe = false; + } + + + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe)); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + byte[] buf = new byte[512]; + int len; + while ((len = in.read(buf)) != -1) { + mdatOut.write(buf, 0, len); + } + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + tr.length++; + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } + + /** + * Writes an already encoded sample from a byte array into a track.

This + * method does not inspect the contents of the samples. The contents has to + * match the format and dimensions of the media in this track.

If a + * track contains palette changes, then all key frames must be immediately + * preceeded by a palette change chunk. If a key frame is not preceeded by a + * palette change chunk, it will be downgraded to a delta frame. + * + * @param track The track index. + * @param data The encoded sample data. + * @param off The startTime offset in the data. + * @param len The number of bytes to write. + * @param isKeyframe Whether the sample is a sync sample (keyframe). + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + ensureStarted(); + Track tr = tracks.get(track); + + // The first sample in a track is always a key frame + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe.\nTrack="+track+", "+tr.format); + } + + // If a stream has palette changes, then only palette change samples can + // be marked as keyframe. + if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) { + throw new IllegalStateException("Only palette changes can be marked as keyframe.\nTrack="+track+", "+tr.format); + } + + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + mdatOut.write(data, off, len); + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } + + /** + * Writes multiple already encoded samples from a byte array into a track. + *

This method does not inspect the contents of the data. The contents + * has to match the format and dimensions of the media in this track. + * + * @param track The track index. + * @param sampleCount The number of samples. + * @param data The encoded sample data. + * @param off The startTime offset in the data. + * @param len The number of bytes to write. Must be dividable by + * sampleCount. + * @param isKeyframe Whether the samples are sync samples. All samples must + * either be sync samples or non-sync samples. + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + ensureStarted(); + Track tr = tracks.get(track); + if (tr.mediaType == AVIMediaType.AUDIO) { + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + mdatOut.write(data, off, len); + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, sampleCount, offset, length, isKeyframe | tr.samples.isEmpty()); + tr.addSample(s); + idx1.add(s); + tr.length += sampleCount; + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } else { + for (int i = 0; i < sampleCount; i++) { + writeSample(track, data, off, len / sampleCount, isKeyframe); + off += len / sampleCount; + } + } + } + + /** + * Returns the duration of the track in media time scale. + */ + public long getMediaDuration(int track) { + Track tr = tracks.get(track); + long duration = tr.startTime; + if (!tr.samples.isEmpty()) { + Sample s = tr.samples.get(tr.samples.size() - 1); + duration += s.timeStamp + s.duration; + } + return duration; + } + + /** + * Closes the stream. + * + * @exception IOException if an I/O error has occurred + */ + public void close() throws IOException { + if (state == States.STARTED) { + finish(); + } + if (state != States.CLOSED) { + out.close(); + state = States.CLOSED; + } + } + + /** + * Finishes writing the contents of the AVI output stream without closing + * the underlying stream. Use this method when applying multiple filters in + * succession to the same output stream. + * + * @exception IllegalStateException if the dimension of the video track has + * not been specified or determined yet. + * @exception IOException if an I/O exception has occurred + */ + public void finish() throws IOException { + ensureOpen(); + if (state != States.FINISHED) { + moviChunk.finish(); + writeEpilog(); + state = States.FINISHED; + } + } + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (state == States.CLOSED) { + throw new IOException("Stream closed"); + } + } + + /** + * Returns true if the limit for media samples has been reached. If this + * limit is reached, no more samples should be added to the movie.

AVI + * 1.0 files have a file size limit of 2 GB. This method returns true if a + * file size of 1.8 GB has been reached. + */ + public boolean isDataLimitReached() { + try { + return getRelativeStreamPosition() > (long) (1.8 * 1024 * 1024 * 1024); + } catch (IOException ex) { + return true; + } + } + + private void writeProlog() throws IOException { + // The file has the following structure: + // + // .RIFF AVI + // ..avih (AVI Header Chunk) + // ..LIST strl (for each track) + // ...strh (Stream Header Chunk) + // ...strf (Stream Format Chunk) + // ...**** (Extra Stream Header Chunks) + // ...strn (Stream Name Chunk) + // ..LIST movi + // ...00dc (Compressed video data chunk in Track 00, repeated for each frame) + // ..idx1 (List of video data chunks and their location in the file) + + // The RIFF AVI Chunk holds the complete movie + aviChunk = new CompositeChunk(RIFF_ID, AVI_ID); + CompositeChunk hdrlChunk = new CompositeChunk(LIST_ID, HDRL_ID); + + // Write empty AVI Main Header Chunk - we fill the data in later + aviChunk.add(hdrlChunk); + avihChunk = new FixedSizeDataChunk(AVIH_ID, 56); + avihChunk.seekToEndOfChunk(); + hdrlChunk.add(avihChunk); + + // Write empty AVI Stream Header Chunk - we fill the data in later + for (Track tr : tracks) { + + CompositeChunk strlChunk = new CompositeChunk(LIST_ID, STRL_ID); + hdrlChunk.add(strlChunk); + + tr.strhChunk = new FixedSizeDataChunk(STRH_ID, 56); + tr.strhChunk.seekToEndOfChunk(); + strlChunk.add(tr.strhChunk); + + tr.strfChunk = new FixedSizeDataChunk(STRF_ID, tr.getSTRFChunkSize()); + tr.strfChunk.seekToEndOfChunk(); + strlChunk.add(tr.strfChunk); + + for (RIFFChunk c : tr.extraHeaders) { + DataChunk d = new DataChunk(c.getID(), + c.getSize()); + ImageOutputStream dout = d.getOutputStream(); + dout.write(c.getData()); + d.finish(); + strlChunk.add(d); + } + + if (tr.name != null) { + byte[] data = (tr.name + "\u0000").getBytes("ASCII"); + DataChunk d = new DataChunk(STRN_ID, + data.length); + ImageOutputStream dout = d.getOutputStream(); + dout.write(data); + d.finish(); + strlChunk.add(d); + } + } + + moviChunk = new CompositeChunk(LIST_ID, MOVI_ID); + aviChunk.add(moviChunk); + + + } + + private void writeEpilog() throws IOException { + + ImageOutputStream d; + + /* Create Idx1 Chunk and write data + * ------------- + typedef struct _avioldindex { + FOURCC fcc; + DWORD cb; + struct _avioldindex_entry { + DWORD dwChunkId; + DWORD flags; + DWORD dwOffset; + DWORD dwSize; + } aIndex[]; + } AVIOLDINDEX; + */ + { + DataChunk idx1Chunk = new DataChunk(IDX1_ID); + aviChunk.add(idx1Chunk); + d = idx1Chunk.getOutputStream(); + long moviListOffset = moviChunk.offset + 8 + 8; + + { + double movieTime = 0; + int nTracks = tracks.size(); + int[] trackSampleIndex = new int[nTracks]; + long[] trackSampleCount = new long[nTracks]; + for (Sample s : idx1) { + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(s.chunkType); // dwChunkId + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + // Specifies a FOURCC that identifies a stream in the AVI file. The + // FOURCC must have the form 'xxyy' where xx is the stream number and yy + // is a two-character code that identifies the contents of the stream: + // + // Two-character code Description + // db Uncompressed video frame + // dc Compressed video frame + // header Palette change + // wb Audio data + + d.writeInt(((s.chunkType & 0xffff) == PC_ID ? 0x100 : 0x0)// + | (s.isKeyframe ? 0x10 : 0x0)); // flags + // Specifies a bitwise combination of zero or more of the following + // flags: + // + // Value Name Description + // 0x10 AVIIF_KEYFRAME The data chunk is a key frame. + // 0x1 AVIIF_LIST The data chunk is a 'rec ' list. + // 0x100 AVIIF_NO_TIME The data chunk does not affect the timing of the + // stream. For example, this flag should be set for + // palette changes. + + d.writeInt((int) (s.offset - moviListOffset)); // dwOffset + // Specifies the location of the data chunk in the file. The value + // should be specified as an offset, in bytes, from the startTime of the + // 'movi' list; however, in some AVI files it is given as an offset from + // the startTime of the file. + + d.writeInt((int) (s.length)); // dwSize + // Specifies the size of the data chunk, in bytes. + } + + } + + idx1Chunk.finish(); + } + + /* Write Data into AVI Main Header Chunk + * ------------- + * The AVIMAINHEADER structure defines global information in an AVI file. + * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx + typedef struct _avimainheader { + FOURCC fcc; + DWORD cb; + DWORD dwMicroSecPerFrame; + DWORD dwMaxBytesPerSec; + DWORD dwPaddingGranularity; + DWORD flags; + DWORD dwTotalFrames; + DWORD initialFrames; + DWORD dwStreams; + DWORD dwSuggestedBufferSize; + DWORD dwWidth; + DWORD dwHeight; + DWORD dwReserved[4]; + } AVIMAINHEADER; */ + { + avihChunk.seekToStartOfData(); + d = avihChunk.getOutputStream(); + + // compute largest buffer size + long largestBufferSize = 0; + long duration = 0; + for (Track tr : tracks) { + long trackDuration = 0; + for (Sample s : tr.samples) { + trackDuration += s.duration; + } + duration = max(duration, trackDuration); + for (Sample s : tr.samples) { + if (s.length > largestBufferSize) { + largestBufferSize = s.length; + } + } + } + + + + // FIXME compute dwMicroSecPerFrame properly! + Track tt = tracks.get(0); + + d.writeInt((int) ((1000000L * tt.scale) / tt.rate)); // dwMicroSecPerFrame + // Specifies the number of microseconds between frames. + // This value indicates the overall timing for the file. + + d.writeInt((int)largestBufferSize); // dwMaxBytesPerSec + // Specifies the approximate maximum data rate of the file. + // This value indicates the number of bytes per second the system + // must handle to present an AVI sequence as specified by the other + // parameters contained in the main header and stream header chunks. + + d.writeInt(0); // dwPaddingGranularity + // Specifies the alignment for data, in bytes. Pad the data to multiples + // of this value. + + d.writeInt(0x10|0x100|0x800); // flags + // Contains a bitwise combination of zero or more of the following + // flags: + // + // Value Name Description + // 0x10 AVIF_HASINDEX Indicates the AVI file has an index. + // 0x20 AVIF_MUSTUSEINDEX Indicates that application should use the + // index, rather than the physical ordering of the + // chunks in the file, to determine the order of + // presentation of the data. For example, this flag + // could be used to create a list of frames for + // editing. + // 0x100 AVIF_ISINTERLEAVED Indicates the AVI file is interleaved. + // 0x800 AVIF_TRUST_CK_TYPE ??? + // 0x1000 AVIF_WASCAPTUREFILE Indicates the AVI file is a specially + // allocated file used for capturing real-time + // video. Applications should warn the user before + // writing over a file with this flag set because + // the user probably defragmented this file. + // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted + // data and software. When this flag is used, + // software should not permit the data to be + // duplicated. + + /*long dwTotalFrames = 0; + for (Track t : tracks) { + dwTotalFrames += t.samples.size(); + }*/ + d.writeInt(tt.samples.size()); // dwTotalFrames + // Specifies the total number of frames of data in the file. + + d.writeInt(0); // initialFrames + // Specifies the initial frame for interleaved files. Noninterleaved + // files should specify zero. If you are creating interleaved files, + // specify the number of frames in the file prior to the initial frame + // of the AVI sequence in this member. + // To give the audio driver enough audio to work with, the audio data in + // an interleaved file must be skewed from the video data. Typically, + // the audio data should be moved forward enough frames to allow + // approximately 0.75 seconds of audio data to be preloaded. The + // dwInitialRecords member should be set to the number of frames the + // audio is skewed. Also set the same value for the initialFrames + // member of the AVISTREAMHEADER structure in the audio stream header + + d.writeInt(tracks.size()); // dwStreams + // Specifies the number of streams in the file. For example, a file with + // audio and video has two streams. + + d.writeInt((int) largestBufferSize); // dwSuggestedBufferSize + // Specifies the suggested buffer size for reading the file. Generally, + // this size should be large enough to contain the largest chunk in the + // file. If set to zero, or if it is too small, the playback software + // will have to reallocate memory during playback, which will reduce + // performance. For an interleaved file, the buffer size should be large + // enough to read an entire record, and not just a chunk. + { + VideoTrack vt = null; + int width = 0, height = 0; + // FIXME - Maybe we should support a global video dimension property + for (Track tr : tracks) { + width = max(width, max(tr.frameLeft, tr.frameRight)); + height = max(height, max(tr.frameTop, tr.frameBottom)); + } + d.writeInt(width); // dwWidth + // Specifies the width of the AVI file in pixels. + + d.writeInt(height); // dwHeight + // Specifies the height of the AVI file in pixels. + } + d.writeInt(0); // dwReserved[0] + d.writeInt(0); // dwReserved[1] + d.writeInt(0); // dwReserved[2] + d.writeInt(0); // dwReserved[3] + // Reserved. Set this array to zero. + } + + for (Track tr : tracks) { + /* Write Data into AVI Stream Header Chunk + * ------------- + * The AVISTREAMHEADER structure contains information about one stream + * in an AVI file. + * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx + typedef struct _avistreamheader { + FOURCC fcc; + DWORD cb; + FOURCC fccType; + FOURCC fccHandler; + DWORD flags; + WORD priority; + WORD language; + DWORD initialFrames; + DWORD scale; + DWORD rate; + DWORD startTime; + DWORD dwLength; + DWORD dwSuggestedBufferSize; + DWORD quality; + DWORD dwSampleSize; + struct { + short int left; + short int top; + short int right; + short int bottom; + } rcFrame; + } AVISTREAMHEADER; + */ + tr.strhChunk.seekToStartOfData(); + d = tr.strhChunk.getOutputStream(); + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(typeToInt(tr.mediaType.fccType)); // fccType: "vids" for video stream + d.writeInt(tr.fccHandler); // fccHandler: specifies the codec + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + d.writeInt(tr.flags); + // Contains any flags for the data stream. The bits in the high-order + // word of these flags are specific to the type of data contained in the + // stream. The following standard flags are defined: + // + // Value Name Description + // AVISF_DISABLED 0x00000001 Indicates this stream should not + // be enabled by default. + // AVISF_VIDEO_PALCHANGES 0x00010000 + // Indicates this video stream contains + // palette changes. This flag warns the playback + // software that it will need to animate the + // palette. + + d.writeShort(tr.priority); // priority: highest priority denotes default stream + d.writeShort(tr.language); // language: language code (?) + d.writeInt((int) tr.initialFrames); // initialFrames: how far audio data is ahead of the video frames + d.writeInt((int) tr.scale); // scale: time scale + d.writeInt((int) tr.rate); // rate: sample rate in scale units + d.writeInt((int) tr.startTime); // startTime: starting time of stream + d.writeInt((int) tr.length); // dwLength: length of stream ! WRONG + + long dwSuggestedBufferSize = 0; + long dwSampleSize = -1; // => -1 indicates unknown + for (Sample s : tr.samples) { + if (s.length > dwSuggestedBufferSize) { + dwSuggestedBufferSize = s.length; + } + if (dwSampleSize == -1) { + dwSampleSize = s.length; + } else if (dwSampleSize != s.length) { + dwSampleSize = 0; + } + } + if (dwSampleSize == -1) { + dwSampleSize = 0; + } + + d.writeInt((int) dwSuggestedBufferSize); // dwSuggestedBufferSize + // Specifies how large a buffer should be used to read this stream. + // Typically, this contains a value corresponding to the largest chunk + // present in the stream. Using the correct buffer size makes playback + // more efficient. Use zero if you do not know the correct buffer size. + + d.writeInt(tr.quality); // quality + // Specifies an indicator of the quality of the data in the stream. + // Quality is represented as a number between 0 and 10,000. + // For compressed data, this typically represents the value of the + // quality parameter passed to the compression software. If set to –1, + // drivers use the default quality value. + + d.writeInt(tr instanceof AudioTrack ? ((AudioTrack) tr).blockAlign : (int) dwSampleSize); // dwSampleSize + // Specifies the size of a single sample of data. This is set to zero + // if the samples can vary in size. If this number is nonzero, then + // multiple samples of data can be grouped into a single chunk within + // the file. If it is zero, each sample of data (such as a video frame) + // must be in a separate chunk. For video streams, this number is + // typically zero, although it can be nonzero if all video frames are + // the same size. For audio streams, this number should be the same as + // the blockAlign member of the WAVEFORMATEX structure describing the + // audio. + + d.writeShort(tr.frameLeft); // rcFrame.left + d.writeShort(tr.frameTop); // rcFrame.top + d.writeShort(tr.frameRight); // rcFrame.right + d.writeShort(tr.frameBottom); // rcFrame.bottom + // Specifies the destination rectangle for a text or video stream within + // the movie rectangle specified by the dwWidth and dwHeight members of + // the AVI main header structure. The rcFrame member is typically used + // in support of multiple video streams. Set this rectangle to the + // coordinates corresponding to the movie rectangle to update the whole + // movie rectangle. Units for this member are pixels. The upper-left + // corner of the destination rectangle is relative to the upper-left + // corner of the movie rectangle. + + if (tr instanceof VideoTrack) { + VideoTrack vt = (VideoTrack) tr; + Format vf = tr.format; + + /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk + /* ------------- + * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx + typedef struct tagBITMAPINFOHEADER { + DWORD biSize; + LONG width; + LONG height; + WORD planes; + WORD bitCount; + DWORD compression; + DWORD sizeImage; + LONG xPelsPerMeter; + LONG yPelsPerMeter; + DWORD clrUsed; + DWORD clrImportant; + } BITMAPINFOHEADER; + */ + tr.strfChunk.seekToStartOfData(); + d = tr.strfChunk.getOutputStream(); + d.writeInt(40); // biSize: number of bytes required by the structure. + d.writeInt(vf.get(WidthKey)); // width + d.writeInt(vf.get(HeightKey)); // height + d.writeShort(1); // planes + d.writeShort(vf.get(DepthKey)); // bitCount + + String enc = vf.get(EncodingKey); + if (enc.equals(ENCODING_AVI_DIB)) { + d.writeInt(0); // compression - BI_RGB for uncompressed RGB + } else if (enc.equals(ENCODING_AVI_RLE)) { + if (vf.get(DepthKey) == 8) { + d.writeInt(1); // compression - BI_RLE8 + } else if (vf.get(DepthKey) == 4) { + d.writeInt(2); // compression - BI_RLE4 + } else { + throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images"); + } + } else { + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(typeToInt(vt.format.get(EncodingKey))); // compression + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + if (enc.equals(ENCODING_AVI_DIB)) { + d.writeInt(0); // sizeImage + } else { + if (vf.get(DepthKey) == 4) { + d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) / 2); // sizeImage + } else { + int bytesPerPixel = Math.max(1, vf.get(DepthKey) / 8); + d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) * bytesPerPixel); // sizeImage + } + } + + d.writeInt(0); // xPelsPerMeter + d.writeInt(0); // yPelsPerMeter + + d.writeInt(vt.palette == null ? 0 : vt.palette.getMapSize()); // clrUsed + + d.writeInt(0); // clrImportant + + if (vt.palette != null) { + for (int i = 0, n = vt.palette.getMapSize(); i < n; ++i) { + /* + * typedef struct tagRGBQUAD { + BYTE rgbBlue; + BYTE rgbGreen; + BYTE rgbRed; + BYTE rgbReserved; // This member is reserved and must be zero. + } RGBQUAD; + */ + d.write(vt.palette.getBlue(i)); + d.write(vt.palette.getGreen(i)); + d.write(vt.palette.getRed(i)); + d.write(0); + } + } + } else if (tr instanceof AudioTrack) { + AudioTrack at = (AudioTrack) tr; + + /* Write WAVEFORMATEX Data into AVI Stream Format Chunk + /* ------------- + * see http://msdn.microsoft.com/en-us/library/dd757720(v=vs.85).aspx + typedef struct { + WORD wFormatTag; + WORD channels; + DWORD samplesPerSec; + DWORD avgBytesPerSec; + WORD blockAlign; + WORD bitsPerSample; + WORD cbSize; + } WAVEFORMATEX; + */ + tr.strfChunk.seekToStartOfData(); + d = tr.strfChunk.getOutputStream(); + + d.writeShort(at.wFormatTag); // wFormatTag: WAVE_FORMAT_PCM=0x0001 + d.writeShort(at.channels); // channels + d.writeInt((int) at.samplesPerSec);// samplesPerSec + d.writeInt((int) at.avgBytesPerSec); // avgBytesPerSec + d.writeShort(at.blockAlign); // blockAlign + d.writeShort(at.bitsPerSample); // bitsPerSample + + d.writeShort(0); //cbSize + // cbSize: Size, in bytes, of extra format information appended + // to the end of the WAVEFORMATEX structure. This information + // can be used by non-PCM formats to store extra attributes for + // the wFormatTag. If no extra information is required by the + // wFormatTag, this member must be set to zero. If this value is + // 22, the format is most likely described using the + // WAVEFORMATEXTENSIBLE structure, of which WAVEFORMATEX is the + // first member. + } + } + + // ----------------- + aviChunk.finish(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java new file mode 100644 index 000000000..6a41f0c7d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java @@ -0,0 +1,517 @@ +/** + * @(#)AVIWriter.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. All rights + * reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. For details + * see accompanying license terms. + */ +package org.monte.media.avi; + +import java.util.EnumSet; +import org.monte.media.math.Rational; +import org.monte.media.Format; +import org.monte.media.Codec; +import org.monte.media.Buffer; +import org.monte.media.MovieWriter; +import org.monte.media.Registry; +import org.monte.media.io.ByteArrayImageOutputStream; +import org.monte.media.riff.RIFFParser; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.*; +import java.nio.ByteOrder; +import java.util.Arrays; +import javax.imageio.stream.*; +import static org.monte.media.AudioFormatKeys.*; +import static org.monte.media.VideoFormatKeys.*; +import org.monte.media.BufferFlag; +import static org.monte.media.BufferFlag.*; + +/** + * Provides high-level support for encoding and writing audio and video samples + * into an AVI 1.0 file. + * + * @author Werner Randelshofer + * @version $Id: AVIWriter.java 306 2013-01-04 16:19:29Z werner $ + */ +public class AVIWriter extends AVIOutputStream implements MovieWriter { + + public final static Format AVI = new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI); + public final static Format VIDEO_RAW = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_JPEG = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_PNG = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_PNG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_RLE = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_RLE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_SCREEN_CAPTURE = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + + /** + * Creates a new AVI writer. + * + * @param file the output file + */ + public AVIWriter(File file) throws IOException { + super(file); + } + + /** + * Creates a new AVI writer. + * + * @param out the output stream. + */ + public AVIWriter(ImageOutputStream out) throws IOException { + super(out); + } + + @Override + public Format getFileFormat() throws IOException { + return AVI; + } + + @Override + public Format getFormat(int track) { + return tracks.get(track).format; + } + + /** + * Returns the media duration of the track in seconds. + */ + @Override + public Rational getDuration(int track) { + Track tr = tracks.get(track); + long duration = getMediaDuration(track); + return new Rational(duration * tr.scale, tr.rate); + } + + /** + * Adds a track. + * + * @param format The format of the track. + * @return The track number. + */ + @Override + public int addTrack(Format format) throws IOException { + if (format.get(MediaTypeKey) == MediaType.VIDEO) { + return addVideoTrack(format); + } else { + return addAudioTrack(format); + } + } + + /** + * Adds a video track. + * + * @param format The format of the track. + * @return The track number. + */ + private int addVideoTrack(Format vf) throws IOException { + if (!vf.containsKey(EncodingKey)) { + throw new IllegalArgumentException("EncodingKey missing in " + vf); + } + if (!vf.containsKey(FrameRateKey)) { + throw new IllegalArgumentException("FrameRateKey missing in " + vf); + } + if (!vf.containsKey(WidthKey)) { + throw new IllegalArgumentException("WidthKey missing in " + vf); + } + if (!vf.containsKey(HeightKey)) { + throw new IllegalArgumentException("HeightKey missing in " + vf); + } + if (!vf.containsKey(DepthKey)) { + throw new IllegalArgumentException("DepthKey missing in " + vf); + } + int tr = addVideoTrack(vf.get(EncodingKey), + vf.get(FrameRateKey).getDenominator(), vf.get(FrameRateKey).getNumerator(), + vf.get(WidthKey), vf.get(HeightKey), vf.get(DepthKey), + vf.get(FrameRateKey).floor(1).intValue()); + setCompressionQuality(tr, vf.get(QualityKey, 1.0f)); + return tr; + } + + /** + * Adds an audio track. + * + * @param format The format of the track. + * @return The track number. + */ + private int addAudioTrack(Format format) throws IOException { + int waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + + + long timeScale = 1; + long sampleRate = format.get(SampleRateKey, new Rational(41000, 0)).longValue(); + int numberOfChannels = format.get(ChannelsKey, 1); + int sampleSizeInBits = format.get(SampleSizeInBitsKey, 16); // + boolean isCompressed = false; // FIXME + int frameDuration = 1; + int frameSize = format.get(FrameSizeKey, (sampleSizeInBits + 7) / 8 * numberOfChannels); + + + String enc = format.get(EncodingKey); + if (enc == null) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_ALAW)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_PCM_SIGNED)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_PCM_UNSIGNED)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_ULAW)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_MP3)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM - FIXME + } else { + waveFormatTag = RIFFParser.stringToID(format.get(EncodingKey)) & 0xffff; + } + + return addAudioTrack(waveFormatTag, // + timeScale, sampleRate, // + numberOfChannels, sampleSizeInBits, // + isCompressed, // + frameDuration, frameSize); + } + + /** + * Returns the codec of the specified track. + */ + public Codec getCodec(int track) { + return tracks.get(track).codec; + } + + /** + * Sets the codec for the specified track. + */ + public void setCodec(int track, Codec codec) { + tracks.get(track).codec = codec; + } + + @Override + public int getTrackCount() { + return tracks.size(); + } + + /** + * Encodes the provided image and writes its sample data into the specified + * track. + * + * @param track The track index. + * @param image The image of the video frame. + * @param duration Duration given in media time units. + * + * @throws IndexOutofBoundsException if the track index is out of bounds. + * @throws if the duration is less than 1, or if the dimension of the frame + * does not match the dimension of the video. + * @throws UnsupportedOperationException if the {@code MovieWriter} does not + * have a built-in encoder for this video format. + * @throws IOException if writing the sample data failed. + */ + public void write(int track, BufferedImage image, long duration) throws IOException { + ensureStarted(); + + VideoTrack vt = (VideoTrack) tracks.get(track); + if (vt.codec == null) { + createCodec(track); + } + if (vt.codec == null) { + throw new UnsupportedOperationException("No codec for this format: " + vt.format); + } + + // The dimension of the image must match the dimension of the video track + Format fmt = vt.format; + if (fmt.get(WidthKey) != image.getWidth() || fmt.get(HeightKey) != image.getHeight()) { + throw new IllegalArgumentException("Dimensions of image[" + vt.samples.size() + + "] (width=" + image.getWidth() + ", height=" + image.getHeight() + + ") differs from video format of track: " + fmt); + } + + // Encode pixel data + { + if (vt.outputBuffer == null) { + vt.outputBuffer = new Buffer(); + } + + boolean isKeyframe = vt.syncInterval == 0 ? false : vt.samples.size() % vt.syncInterval == 0; + + Buffer inputBuffer = new Buffer(); + inputBuffer.flags = (isKeyframe) ? EnumSet.of(KEYFRAME) : EnumSet.noneOf(BufferFlag.class); + inputBuffer.data = image; + vt.codec.process(inputBuffer, vt.outputBuffer); + if (vt.outputBuffer.flags.contains(DISCARD)) { + return; + } + + // Encode palette data + isKeyframe = vt.outputBuffer.flags.contains(KEYFRAME); + boolean paletteChange = writePalette(track, image, isKeyframe); + writeSample(track, (byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length, isKeyframe && !paletteChange); + /* + long offset = getRelativeStreamPosition(); + + DataChunk videoFrameChunk = new DataChunk(vt.getSampleChunkFourCC(isKeyframe)); + moviChunk.add(videoFrameChunk); + videoFrameChunk.getOutputStream().write((byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length); + videoFrameChunk.finish(); + long length = getRelativeStreamPosition() - offset; + + Sample s=new Sample(videoFrameChunk.chunkType, 1, offset, length, isKeyframe&&!paletteChange); + vt.addSample(s); + idx1.add(s); + + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + }*/ + } + } + + /** + * Encodes the data provided in the buffer and then writes it into the + * specified track.

Does nothing if the discard-flag in the buffer is + * set to true. + * + * @param track The track number. + * @param buf The buffer containing a data sample. + */ + @Override + public void write(int track, Buffer buf) throws IOException { + ensureStarted(); + if (buf.flags.contains(DISCARD)) { + return; + } + + Track tr = tracks.get(track); + + boolean isKeyframe = buf.flags.contains(KEYFRAME); + if (buf.data instanceof BufferedImage) { + if (tr.syncInterval != 0) { + isKeyframe = buf.flags.contains(KEYFRAME) | (tr.samples.size() % tr.syncInterval == 0); + } + } + // Encode palette data + boolean paletteChange = false; + if (buf.data instanceof BufferedImage && tr instanceof VideoTrack) { + paletteChange = writePalette(track, (BufferedImage) buf.data, isKeyframe); + } else if (buf.header instanceof IndexColorModel) { + paletteChange = writePalette(track, (IndexColorModel) buf.header, isKeyframe); + } + // Encode sample data + { + if (buf.format == null) { + throw new IllegalArgumentException("Buffer.format must not be null"); + } + if (buf.format.matchesWithout(tr.format, FrameRateKey) && buf.data instanceof byte[]) { + writeSamples(track, buf.sampleCount, (byte[]) buf.data, buf.offset, buf.length, + buf.isFlag(KEYFRAME) && !paletteChange); + return; + } + + // We got here, because the buffer format does not match the track + // format. Lets see if we can create a codec which can perform the + // encoding for us. + + if (tr.codec == null) { + createCodec(track); + if (tr.codec == null) { + throw new UnsupportedOperationException("No codec for this format " + tr.format); + } + } + + if (tr.outputBuffer == null) { + tr.outputBuffer = new Buffer(); + } + Buffer outBuf = tr.outputBuffer; + if (tr.codec.process(buf, outBuf) != Codec.CODEC_OK) { + throw new IOException("Codec failed or could not encode the sample in a single step."); + } + if (outBuf.isFlag(DISCARD)) { + return; + } + writeSamples(track, outBuf.sampleCount, (byte[]) outBuf.data, outBuf.offset, outBuf.length, + isKeyframe && !paletteChange); + } + } + + private boolean writePalette(int track, BufferedImage image, boolean isKeyframe) throws IOException { + if ((image.getColorModel() instanceof IndexColorModel)) { + return writePalette(track, (IndexColorModel) image.getColorModel(), isKeyframe); + } + return false; + } + + private boolean writePalette(int track, IndexColorModel imgPalette, boolean isKeyframe) throws IOException { + ensureStarted(); + + VideoTrack vt = (VideoTrack) tracks.get(track); + int imgDepth = vt.bitCount; + ByteArrayImageOutputStream tmp = null; + boolean paletteChange = false; + switch (imgDepth) { + case 4: { + //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); + int[] imgRGBs = new int[16]; + imgPalette.getRGBs(imgRGBs); + int[] previousRGBs = new int[16]; + if (vt.previousPalette == null) { + vt.previousPalette = vt.palette; + } + vt.previousPalette.getRGBs(previousRGBs); + if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { + paletteChange = true; + vt.previousPalette = imgPalette; + /* + int first = imgPalette.getMapSize(); + int last = -1; + for (int i = 0; i < 16; i++) { + if (previousRGBs[i] != imgRGBs[i] && i < first) { + first = i; + } + if (previousRGBs[i] != imgRGBs[i] && i > last) { + last = i; + } + }*/ + int first = 0; + int last = imgPalette.getMapSize() - 1; + /* + * typedef struct { + BYTE bFirstEntry; + BYTE bNumEntries; + WORD wFlags; + PALETTEENTRY peNew[]; + } AVIPALCHANGE; + * + * typedef struct tagPALETTEENTRY { + BYTE peRed; + BYTE peGreen; + BYTE peBlue; + BYTE peFlags; + } PALETTEENTRY; + */ + tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); + tmp.writeByte(first);//bFirstEntry + tmp.writeByte(last - first + 1);//bNumEntries + tmp.writeShort(0);//wFlags + + for (int i = first; i <= last; i++) { + tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red + tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green + tmp.writeByte(imgRGBs[i] & 0xff); // blue + tmp.writeByte(0); // reserved*/ + } + + } + break; + } + case 8: { + //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); + int[] imgRGBs = new int[256]; + imgPalette.getRGBs(imgRGBs); + int[] previousRGBs = new int[256]; + if (vt.previousPalette != null) { + vt.previousPalette.getRGBs(previousRGBs); + } + if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { + paletteChange = true; + vt.previousPalette = imgPalette; + /* + int first = imgPalette.getMapSize(); + int last = -1; + for (int i = 0; i < 16; i++) { + if (previousRGBs[i] != imgRGBs[i] && i < first) { + first = i; + } + if (previousRGBs[i] != imgRGBs[i] && i > last) { + last = i; + } + }*/ + int first = 0; + int last = imgPalette.getMapSize() - 1; + /* + * typedef struct { + BYTE bFirstEntry; + BYTE bNumEntries; + WORD wFlags; + PALETTEENTRY peNew[]; + } AVIPALCHANGE; + * + * typedef struct tagPALETTEENTRY { + BYTE peRed; + BYTE peGreen; + BYTE peBlue; + BYTE peFlags; + } PALETTEENTRY; + */ + tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); + tmp.writeByte(first);//bFirstEntry + tmp.writeByte(last - first + 1);//bNumEntries + tmp.writeShort(0);//wFlags + for (int i = first; i <= last; i++) { + tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red + tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green + tmp.writeByte(imgRGBs[i] & 0xff); // blue + tmp.writeByte(0); // reserved*/ + } + } + + break; + } + } + if (tmp != null) { + tmp.close(); + writePalette(track, tmp.toByteArray(), 0, (int) tmp.length(), isKeyframe); + } + return paletteChange; + } + + private Codec createCodec(Format fmt) { + return Registry.getInstance().getEncoder(fmt.prepend(MimeTypeKey, MIME_AVI)); + } + + private void createCodec(int track) { + Track tr = tracks.get(track); + Format fmt = tr.format; + tr.codec = createCodec(fmt); + String enc = fmt.get(EncodingKey); + if (tr.codec != null) { + if (fmt.get(MediaTypeKey) == MediaType.VIDEO) { + tr.codec.setInputFormat(fmt.prepend( + EncodingKey, ENCODING_BUFFERED_IMAGE, + DataClassKey, BufferedImage.class)); + if (null == tr.codec.setOutputFormat( + fmt.prepend(FixedFrameRateKey, true, + QualityKey, getCompressionQuality(track), + MimeTypeKey, MIME_AVI, + DataClassKey, byte[].class))) { + throw new UnsupportedOperationException("Track " + tr + " codec does not support format " + fmt + ". codec=" + tr.codec); + } + } else { + tr.codec.setInputFormat(null); + if (null == tr.codec.setOutputFormat( + fmt.prepend(FixedFrameRateKey, true, + QualityKey, getCompressionQuality(track), + MimeTypeKey, MIME_AVI, + DataClassKey, byte[].class))) { + throw new UnsupportedOperationException("Track " + tr + " codec " + tr.codec + " does not support format. " + fmt); + } + } + } + } + + public boolean isVFRSupported() { + return false; + } + + @Override + public boolean isEmpty(int track) { + return tracks.get(track).samples.isEmpty(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java new file mode 100644 index 000000000..8e2a66df6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java @@ -0,0 +1,1734 @@ +/* + * @(#)AbstractAVIStream.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.avi; + +import org.monte.media.riff.RIFFChunk; +import java.util.Map; +import org.monte.media.Buffer; +import org.monte.media.Codec; +import org.monte.media.Format; +import org.monte.media.io.SubImageOutputStream; +import java.awt.Dimension; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import javax.imageio.stream.ImageOutputStream; +import static org.monte.media.VideoFormatKeys.*; + +/** + * This is the base class for low-level AVI stream IO. + * + * @author Werner Randelshofer + * @version $Id: AbstractAVIStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class AbstractAVIStream { + + /** + * Chunk IDs. + */ + /* + protected final static int RIFF_ID = 0x46464952;//0x52494646;// "RIFF" + protected final static int AVI_ID = 0x20495641; //0x41564920;// "AVI " + protected final static int LIST_ID = 0x5453494c;//0x4c495354;// "LIST" + protected final static int MOVI_ID = 0x69766f6d;//0x6d6f7669;// "movi" + protected final static int HDRL_ID = 0x6c726468;//0x6864726c;// "hdrl" + protected final static int AVIH_ID = 0x68697661;//0x61766968;// "avih" + protected final static int STRL_ID = 0x6c727473;//0x7374726c;// "strl" + protected final static int STRH_ID = 0x68727473;//0x73747268;// "strh" + protected final static int STRN_ID = 0x6e727473;//0x7374726e;// "strn" + protected final static int STRF_ID = 0x66727473;//0x73747266;// "strf" + protected final static int STRD_ID = 0x64727473;//0x73747264;// "strd" + protected final static int IDX1_ID = 0x31786469;//0x69647831;// "idx1" + protected final static int REC_ID = 0x20636572;//0x72656320;// "rec " + protected final static int PC_ID = 0x63700000;//0x00007063;// "??pc" + protected final static int DB_ID = 0x62640000;//0x00006462;// "??db" + protected final static int DC_ID = 0x63640000;//0x00006463;// "??dc" + protected final static int WB_ID = 0x62770000;//0x00007762;// "??wb" + */ + protected final static int RIFF_ID =0x52494646;// "RIFF" + protected final static int AVI_ID = 0x41564920;// "AVI " + protected final static int AVIX_ID = 0x41564958;// "AVIX" + protected final static int LIST_ID = 0x4c495354;// "LIST" + protected final static int MOVI_ID = 0x6d6f7669;// "movi" + protected final static int HDRL_ID = 0x6864726c;// "hdrl" + protected final static int AVIH_ID = 0x61766968;// "avih" + protected final static int STRL_ID = 0x7374726c;// "strl" + protected final static int STRH_ID = 0x73747268;// "strh" + protected final static int STRN_ID = 0x7374726e;// "strn" + protected final static int STRF_ID = 0x73747266;// "strf" + protected final static int STRD_ID = 0x73747264;// "strd" + protected final static int IDX1_ID = 0x69647831;// "idx1" + protected final static int REC_ID = 0x72656320;// "rec " + protected final static int CHUNK_SUBTYPE_MASK = 0xffff;// "??xx" + protected final static int PC_ID = 0x00007063;// "??pc" + protected final static int DB_ID = 0x00006462;// "??db" + protected final static int DC_ID = 0x00006463;// "??dc" + protected final static int WB_ID = 0x00007762;// "??wb" + + /** + * Indicates the AVI file has an index. + */ + public final static int AVIH_FLAG_HAS_INDEX = 0x00000010; + /** + * Indicates that application should use the index, rather than the physical + * ordering of the chunks in the file, to determine the order of + * presentation of the data. For example, this flag could be used to create + * a list of frames for editing. + */ + public final static int AVIH_FLAG_MUST_USE_INDEX = 0x00000020; + /** + * Indicates the AVI file is interleaved. + */ + public final static int AVIH_FLAG_IS_INTERLEAVED = 0x00000100; + /** + * ?? + */ + public final static int AVIH_FLAG_TRUST_CK_TYPE = 0x00000800; + /** + * // Indicates the AVI file is a specially allocated file used for + * capturing real-time video. Applications should warn the user before + * writing over a file with this flag set because the user probably + * defragmented this file. + */ + public final static int AVIH_FLAG_WAS_CAPTURE_FILE = 0x00010000; + /* Indicates the AVI file contains copyrighted data and + * software. When this flag is used, software should not + * permit the data to be duplicated. */ + public final static int AVIH_FLAG_COPYRIGHTED = 0x00020000; + /** + * Indicates this stream should not be enabled by default. + */ + public final static int STRH_FLAG_DISABLED = 0x00000001; + /** + * Indicates this video stream contains palette changes. This flag warns the + * playback software that it will need to animate the palette. + */ + public final static int STRH_FLAG_VIDEO_PALETTE_CHANGES = 0x00010000; + /** + * Underlying output stream. + */ + protected ImageOutputStream out; + /** + * The offset in the underlying ImageOutputStream. Normally this is 0 unless + * the underlying stream already contained data when it was passed to the + * constructor. + */ + protected long streamOffset; + + /** + * Supported media types. + */ + public static enum AVIMediaType { + + AUDIO("auds"),// + MIDI("mids"),// + TEXT("txts"),// + VIDEO("vids")// + ; + protected final String fccType; + + @Override + public String toString() { + return fccType; + } + + AVIMediaType(String fourCC) { + this.fccType = fourCC; + } + } + /** + * The list of tracks in the file. + */ + protected ArrayList tracks = new ArrayList(); + + /** + * Gets the position relative to the beginning of the QuickTime stream.

+ * Usually this value is equal to the stream position of the underlying + * ImageOutputStream, but can be larger if the underlying stream already + * contained data. + * + * @return The relative stream position. + * @throws IOException + */ + protected long getRelativeStreamPosition() throws IOException { + return out.getStreamPosition() - streamOffset; + } + + /** + * Seeks relative to the beginning of the AVI stream.

Usually this equal + * to seeking in the underlying ImageOutputStream, but can be different if + * the underlying stream already contained data. + * + */ + protected void seekRelative(long newPosition) throws IOException { + out.seek(newPosition + streamOffset); + } + + /** + * AVI stores media data in sample chunks. A sample chunk may contain one or + * more media samples. A media sample is a single element in a sequence of + * time-ordered data. + */ + protected static class Sample { + + int chunkType; + /** + * Offset of the sample chunk relative to the startTime of the AVI file. + */ + long offset; + /** + * Data length of the sample chunk. + */ + long length; + /** + * The number of media samples in the sample chunk. + */ + int duration; + /** + * Whether the sample is a sync-sample. + */ + boolean isKeyframe; + long timeStamp; + /** + * Palette change sample. + */ + Sample header; + + /** + * Creates a new sample. + * + * @param duration The number of media samples contained in the sample + * chunk. + * @param offset The offset in the AVI stream. + * @param length The length in the AVI stream. + */ + public Sample(int chunkId, int duration, long offset, long length, boolean isSync) { + this.chunkType = chunkId; + this.duration = duration; + this.offset = offset; + this.length = length; + this.isKeyframe = isSync; + } + } + + /** + * Represents a track (or "stream") in an AVI file.

A track is defined + * by an "strh" chunk, which contains an {@code AVISTREAMHEADER} struct. + * Additional chunks can be provided depending on the media type of the + * track.

See + * http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx

+ *
+     * -----------------
+     * AVI Stream Header
+     * -----------------
+     *
+     * enum {
+     *    audioStream = "auds",
+     *   midiStream = "mids",
+     *   textStream = "txts",
+     *   videoStream = "vids"
+     * } aviStrhType;
+     *
+     * set {
+     *    disabled = 0x00000001, // Indicates this stream should not be enabled by default.
+     *    videoPaletteChanges = 0x00010000
+     *        // Indicates this video stream contains palette changes. This flag
+     *        // warns the playback software that it will need to animate the palette.
+     *} aviStrhFlags;
+     *
+     *typedef struct {
+     *    Int16 left;
+     *    Int16 top;
+     *    Int16 right;
+     *    Int16 bottom;
+     *} aviRectangle;
+     *
+     *typedef struct {
+     *     FOURCC enum aviStrhType type;
+     *        // Contains a FOURCC that specifies the type of the data contained in
+     *        // the stream. The following standard AVI values for video and audio are
+     *        // defined.
+     *     FOURCC handler;
+     *        // Optionally, contains a FOURCC that identifies a specific data
+     *        // handler.
+     *        // The data handler is the preferred handler for the stream. For audio
+     *        // and video streams, this specifies the codec for decoding the stream.
+     *     DWORD  set aviStrhFlags flags;
+     *        // Contains any flags for the data stream. The bits in the high-order
+     *        // word of these flags are specific to the type of data contained in the
+     *        // stream.
+     *     WORD   priority;
+     *        // Specifies priority of a stream type. For example, in a file with
+     *        // multiple audio streams, the one with the highest priority might be
+     *        // the default stream.
+     *     WORD   language;
+     *     DWORD  initialFrames;
+     *        // Specifies how far audio data is skewed ahead of the video frames in
+     *        // interleaved files. Typically, this is about 0.75 seconds. If you are
+     *        // creating interleaved files, specify the number of frames in the file
+     *        // prior to the initial frame of the AVI sequence in this member. For
+     *        // more information about the contents of this member, see "Special
+     *        // Information for Interleaved Files" in the Video for Windows
+     *        // Programmer's Guide.
+     *     DWORD  scale;
+     *        // Used with "rate" to specify the time scale that this stream will use.
+     *        // Dividing "rate" by "scale" gives the number of samples per second.
+     *        // For video streams, this is the frame rate. For audio streams, this
+     *        // rate corresponds to the time needed to play blockAlign bytes of
+     *        // audio, which for PCM audio is the just the sample rate.
+     *     DWORD  rate;
+     *        // See "scale".
+     *     DWORD  startTime;
+     *        // Specifies the starting time for this stream. The units are defined by
+     *        // the "rate" and "scale" members in the main file header. Usually, this
+     *        // is zero, but it can specify a delay time for a stream that does not
+     *        // startTime concurrently with the file.
+     *     DWORD  length;
+     *        // Specifies the length of this stream. The units are defined by the
+     *        // "rate" and "scale" members of the stream's header.
+     *     DWORD  suggestedBufferSize;
+     *        // Specifies how large a buffer should be used to read this stream.
+     *        // Typically, this contains a value corresponding to the largest chunk
+     *        // present in the stream. Using the correct buffer size makes playback
+     *        // more efficient. Use zero if you do not know the correct buffer size.
+     *     DWORD  quality;
+     *        // Specifies an indicator of the quality of the data in the stream.
+     *        // Quality is represented as a number between 0 and 10,000. For
+     *        // compressed data, this typically represents the value of the quality
+     *        // parameter passed to the compression software. If set to �1, drivers
+     *        // use the default quality value.
+     *     DWORD  sampleSize;
+     *        // Specifies the size of a single sample of data. This is set to zero if
+     *        // the samples can vary in size. If this number is nonzero, then
+     *        // multiple samples of data can be grouped into a single chunk within
+     *        // the file. If it is zero, each sample of data (such as a video frame)
+     *        // must be in a separate chunk. For video streams, this number is
+     *        // typically zero, although it can be nonzero if all video frames are
+     *        // the same size. For audio streams, this number should be the same as
+     *        // the blockAlign member of the WAVEFORMATEX structure describing the audio.
+     *    aviRectangle frame;
+     *        // Specifies the destination rectangle for a text or video stream within
+     *        // the movie rectangle specified by the "frameWidth" and "frameHeight"
+     *        // members of the AVI main header structure. The "frame" member is
+     *        // typically used in support of multiple video streams. Set this
+     *        // rectangle to the coordinates corresponding to the movie rectangle to
+     *        // update the whole movie rectangle. Units for this member are pixels.
+     *        // The upper-left corner of the destination rectangle is relative to the
+     *        // upper-left corner of the movie rectangle.
+     * } AVISTREAMHEADER; * 
+ */ + protected abstract class Track { + + /** + * The media format. + * + * FIXME - AbstractAVIStream should have no dependencies to Format. + */ + protected Format format; + // Common metadata + /** + * The scale of the media in the track.

Used with rate to specify + * the time scale that this stream will use. Dividing rate by scale + * gives the number of samples per second. For video streams, this is + * the frame rate. For audio streams, this rate corresponds to the time + * needed to play blockAlign bytes of audio, which for PCM audio is just + * the sample rate. + */ + /** + * The rate of the media in scale units.

+ * + * @see scale + */ + /** + * List of samples. + */ + protected ArrayList samples; + /** + * Interval between sync samples (keyframes). 0 = automatic. 1 = write + * all samples as sync samples. n = sync every n-th sample. + */ + protected int syncInterval = 30; + /** + * The twoCC code is used for the ids of the chunks which hold the data + * samples. + */ + protected int twoCC; + // + // AVISTREAMHEADER structure + // ------------------------- + /** + * {@code mediaType.fccType} contains a FOURCC that specifies the type + * of the data contained in the stream. The following standard AVI + * values for video and audio are defined. + * + * FOURCC Description 'auds' Audio stream 'mids' MIDI stream 'txts' Text + * stream 'vids' Video stream + * + */ + protected final AVIMediaType mediaType; + //protected String fccType; + /** + * Optionally, contains a FOURCC that identifies a specific data + * handler. The data handler is the preferred handler for the stream. + * For audio and video streams, this specifies the codec for decoding + * the stream. + */ + protected int fccHandler; + /** + * Contains any flags for the data stream. The bits in the high-order + * word of these flags are specific to the type of data contained in the + * stream. The following standard flags are defined. + * + * Value Description + * + * AVISF_DISABLED 0x00000001 Indicates this stream should not be enabled + * by default. + * + * AVISF_VIDEO_PALCHANGES 0x00010000 Indicates this video stream + * contains palette changes. This flag warns the playback software that + * it will need to animate the palette. + */ + protected int flags; + /** + * Specifies priority of a stream type. For example, in a file with + * multiple audio streams, the one with the highest priority might be + * the default stream. + */ + protected int priority = 0; + /** + * Language tag. + */ + protected int language = 0; + /** + * Specifies how far audio data is skewed ahead of the video frames in + * interleaved files. Typically, this is about 0.75 seconds. If you are + * creating interleaved files, specify the number of frames in the file + * prior to the initial frame of the AVI sequence in this member. For + * more information, see the remarks for the initialFrames member of the + * AVIMAINHEADER structure. + */ + protected long initialFrames = 0; + /** + * Used with rate to specify the time scale that this stream will use. + * Dividing rate by scale gives the number of samples per second. For + * video streams, this is the frame rate. For audio streams, this rate + * corresponds to the time needed to play blockAlign bytes of audio, + * which for PCM audio is the just the sample rate. + */ + protected long scale = 1; + /** + * The rate of the media in scale units. + */ + protected long rate = 30; + /** + * Specifies the starting time for this stream. The units are defined by + * the rate and scale members in the main file header. Usually, this is + * zero, but it can specify a delay time for a stream that does not + * startTime concurrently with the file. + */ + protected long startTime = 0; + /** + * Specifies the length of this stream. The units are defined by the + * rate and scale members of the stream's header. + */ + protected long length; + /** + * Specifies how large a buffer should be used to read this stream. + * Typically, this contains a value corresponding to the largest chunk + * present in the stream. Using the correct buffer size makes playback + * more efficient. Use zero if you do not know the correct buffer size. + */ + //protected long dwSuggestedBufferSize; => this field is computed from tr.samples + /** + * Specifies an indicator of the quality of the data in the stream. + * Quality is represented as a number between 0 and 10,000. For + * compressed data, this typically represents the value of the quality + * parameter passed to the compression software. If set to –1, drivers + * use the default quality value. + */ + protected int quality = -1; + /** + * Specifies the size of a single sample of data. This is set to zero if + * the samples can vary in size. If this number is nonzero, then + * multiple samples of data can be grouped into a single chunk within + * the file. If it is zero, each sample of data (such as a video frame) + * must be in a separate chunk. For video streams, this number is + * typically zero, although it can be nonzero if all video frames are + * the same size. For audio streams, this number should be the same as + * the blockAlign member of the WAVEFORMATEX structure describing the + * audio. + */ + //protected long dwSampleSize; => computed from tr.samples + /** + * Specifies the destination rectangle for a text or video stream within + * the movie rectangle specified by the dwWidth and dwHeight members of + * the AVI main header structure. The rcFrame member is typically used + * in support of multiple video streams. Set this rectangle to the + * coordinates corresponding to the movie rectangle to update the whole + * movie rectangle. Units for this member are pixels. The upper-left + * corner of the destination rectangle is relative to the upper-left + * corner of the movie rectangle. + */ + int frameLeft; + int frameTop; + int frameRight; + int frameBottom; + // -------------------------------- + // End of AVISTREAMHEADER structure + /** + * This chunk holds the AVI Stream Header. + */ + protected FixedSizeDataChunk strhChunk; + /** + * This chunk holds the AVI Stream Format Header. + */ + protected FixedSizeDataChunk strfChunk; + /** + * The optional name of the track. + */ + protected String name; + /** + * The codec. + */ + protected Codec codec; + /** + * The output buffer is used to store the output of the codec. + */ + protected Buffer outputBuffer; + /** + * The input buffer is used when one of the convenience methods without + * a Buffer parameter is used. + */ + protected Buffer inputBuffer; + /** + * The current chunk index of the reader. + */ + protected long readIndex = 0; + /** + * List of additional header chunks. + */ + protected ArrayList extraHeaders; + + public Track(int trackIndex, AVIMediaType mediaType, int fourCC) { + this.mediaType = mediaType; + twoCC = (('0'+trackIndex/10)<<24) | (('0'+trackIndex%10)<<16); + + this.fccHandler = fourCC; + this.samples = new ArrayList(); + this.extraHeaders = new ArrayList(); + } + + public abstract long getSTRFChunkSize(); + + public abstract int getSampleChunkFourCC(boolean isSync); + + public void addSample(Sample s) { + if (!samples.isEmpty()) { + s.timeStamp = samples.get(samples.size() - 1).timeStamp + samples.get(samples.size() - 1).duration; + } + samples.add(s); + length++; + } + } + + /** + * Represents a video track in an AVI file.

The format of a video track + * is defined in a "strf" chunk, which contains a {@code BITMAPINFOHEADER} + * struct. + * + * //---------------------- // AVI Bitmap Info Header // + * ---------------------- typedef struct { BYTE blue; BYTE green; BYTE red; + * BYTE reserved; } RGBQUAD; + * + * // Values for this enum taken from: // + * http://www.fourcc.org/index.php?http%3A//www.fourcc.org/rgb.php enum { + * BI_RGB = 0x00000000, RGB = 0x32424752, // Alias for BI_RGB BI_RLE8 = + * 0x01000000, RLE8 = 0x38454C52, // Alias for BI_RLE8 BI_RLE4 = 0x00000002, + * RLE4 = 0x34454C52, // Alias for BI_RLE4 BI_BITFIELDS = 0x00000003, raw = + * 0x32776173, RGBA = 0x41424752, RGBT = 0x54424752, cvid = "cvid" } + * bitmapCompression; + * + * typedef struct { DWORD structSize; // Specifies the number of bytes + * required by the structure. LONG width; // Specifies the width of the + * bitmap. // - For RGB formats, the width is specified in pixels. // - The + * same is true for YUV formats if the bitdepth is an even power // of 2. // + * - For YUV formats where the bitdepth is not an even power of 2, // + * however, the width is specified in bytes. // Decoders and video sources + * should propose formats where "width" is // the width of the image. If the + * video renderer is using DirectDraw, it // modifies the format so that + * "width" equals the stride of the surface, // and the "target" member of + * the VIDEOINFOHEADER or VIDEOINFOHEADER2 // structure specifies the image + * width. Then it proposes the modified // format using IPin::QueryAccept. + * // For RGB and even-power-of-2 YUV formats, if the video renderer does // + * not specify the stride, then round the width up to the nearst DWORD // + * boundary to find the stride. LONG height; // Specifies the height of the + * bitmap, in pixels. // - For uncompressed RGB bitmaps, if "height" is + * positive, the bitmap // is a bottom-up DIB with the origin at the lower + * left corner. If // "height" is negative, the bitmap is a top-down DIB + * with the origin // at the upper left corner. // - For YUV bitmaps, the + * bitmap is always top-down, regardless of the // sign of "height". + * Decoders should offer YUV formats with postive // "height", but for + * backward compatibility they should accept YUV // formats with either + * positive or negative "height". // - For compressed formats, height must + * be positive, regardless of // image orientation. WORD planes; // + * Specifies the number of planes for the target device. This value must // + * be set to 1. WORD bitCount; // Specifies the number of bits per pixel. + * //DWORD enum bitmapCompression compression; FOURCC enum bitmapCompression + * compression; // If the bitmap is compressed, this member is a FOURCC the + * specifies // the compression. // Value Description // BI_RLE8 A + * run-length encoded (RLE) format for bitmaps with 8 // bpp. The + * compression format is a 2-byte format // consisting of a count byte + * followed by a byte containing a color index. For more information, see + * Bitmap Compression. // BI_RLE4 An RLE format for bitmaps with 4 bpp. The + * compression // format is a 2-byte format consisting of a count byte // + * followed by two word-length color indexes. For more // information, see + * Bitmap Compression. // BI_JPEG Windows 98/Me, Windows 2000/XP: Indicates + * that the // image is a JPEG image. // BI_PNG Windows 98/Me, Windows + * 2000/XP: Indicates that the // image is a PNG image. // For uncompressed + * formats, the following values are possible: // Value Description // + * BI_RGB Uncompressed RGB. // BI_BITFIELDS Specifies that the bitmap is not + * compressed and that // the color table consists of three DWORD color + * masks // that specify the red, green, and blue components, // + * respectively, of each pixel. This is valid when used // with 16- and + * 32-bpp bitmaps. DWORD imageSizeInBytes; // Specifies the size, in bytes, + * of the image. This can be set to 0 for // uncompressed RGB bitmaps. LONG + * xPelsPerMeter; // Specifies the horizontal resolution, in pixels per + * meter, of the // target device for the bitmap. LONG yPelsPerMeter; // + * Specifies the vertical resolution, in pixels per meter, of the target // + * device for the bitmap. DWORD numberOfColorsUsed; // Specifies the number + * of color indices in the color table that are // actually used by the + * bitmap DWORD numberOfColorsImportant; // Specifies the number of color + * indices that are considered important // for displaying the bitmap. If + * this value is zero, all colors are // important. RGBQUAD colors[]; // If + * the bitmap is 8-bpp or less, the bitmap uses a color table, which // + * immediately follows the BITMAPINFOHEADER. The color table consists of // + * an array of RGBQUAD values. The size of the array is given by the // + * "clrUsed" member. If "clrUsed" is zero, the array contains the // maximum + * number of colors for the given bitdepth; that is, // 2^"bitCount" colors. + * } BITMAPINFOHEADER; + * + */ + protected class VideoTrack extends Track { + // Video metadata + + /** + * The video compression quality. + */ + protected float videoQuality = 0.97f; + /** + * Index color model for RAW_RGB4 and RAW_RGB8 formats. + */ + protected IndexColorModel palette; + protected IndexColorModel previousPalette; + /** + * Previous frame for delta compression. + */ + protected Object previousData; + //protected Rectangle rcFrame; + // BITMAPINFOHEADER structure + /** + * Specifies the number of bytes required by the structure. This value + * does not include the size of the color table or the size of the color + * masks, if they are appended to the end of structure. + */ + //protected long biSize; => computed when writing chukn + /** + * Specifies the width of the bitmap, in pixels. For information about + * calculating the stride of the bitmap. + * + */ + int width; + /** + * Specifies the height of the bitmap, in pixels. + * + * For uncompressed RGB bitmaps, if height is positive, the bitmap is a + * bottom-up DIB with the origin at the lower left corner. If height is + * negative, the bitmap is a top-down DIB with the origin at the upper + * left corner. For YUV bitmaps, the bitmap is always top-down, + * regardless of the sign of height. Decoders should offer YUV formats + * with positive height, but for backward compatibility they should + * accept YUV formats with either positive or negative height. For + * compressed formats, height must be positive, regardless of image + * orientation. + */ + int height; + /** + * Specifies the number of planes for the target device. This value must + * be set to 1. + */ + int planes; + /** + * Specifies the number of bits per pixel (bpp). For uncompressed + * formats, this value is the average number of bits per pixel. For + * compressed formats, this value is the implied bit depth of the + * uncompressed image, after the image has been decoded. + */ + int bitCount; + /** + * For compressed video and YUV formats, this member is a FOURCC code, + * specified as a DWORD in little-endian order. For example, YUYV video + * has the FOURCC 'VYUY' or 0x56595559. + * + * For uncompressed RGB formats, the following values are possible: + * + * Value Description + * + * BI_RGB Uncompressed RGB. + * + * BI_BITFIELDS Uncompressed RGB with color masks. Valid for 16-bpp and + * 32-bpp bitmaps. + * + * Note that BI_JPG and BI_PNG are not valid video formats. + * + * For 16-bpp bitmaps, if compression equals BI_RGB, the format is + * always RGB 555. If compression equals BI_BITFIELDS, the format is + * either RGB 555 or RGB 565. Use the subtype GUID in the AM_MEDIA_TYPE + * structure to determine the specific RGB type. + */ + String compression; + /** + * Specifies the size, in bytes, of the image. This can be set to 0 for + * uncompressed RGB bitmaps. + */ + long sizeImage; + /** + * Specifies the horizontal resolution, in pixels per meter, of the + * target device for the bitmap. + */ + long xPelsPerMeter; + /** + * Specifies the vertical resolution, in pixels per meter, of the target + * device for the bitmap. + */ + long yPelsPerMeter; + /** + * Specifies the number of color indices in the color table that are + * actually used by the bitmap. See Remarks for more information. + */ + long clrUsed; + /** + * Specifies the number of color indices that are considered important + * for displaying the bitmap. If this value is zero, all colors are + * important. + */ + long clrImportant; + private int sampleChunkFourCC; + + public VideoTrack(int trackIndex, int fourCC, Format videoFormat) { + super(trackIndex, AVIMediaType.VIDEO, fourCC); + this.format = videoFormat; + sampleChunkFourCC = videoFormat != null && videoFormat.get(EncodingKey).equals(ENCODING_AVI_DIB) ? twoCC | DB_ID : twoCC | DC_ID; + } + + @Override + public long getSTRFChunkSize() { + return palette == null ? 40 : 40 + palette.getMapSize() * 4; + + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + return sampleChunkFourCC; + } + } + + /** + *

The format of a video track is defined in a "strf" chunk, which + * contains a {@code WAVEFORMATEX} struct. + *

+     * ----------------------
+     * AVI Wave Format Header
+     * ----------------------
+     * // values for this enum taken from mmreg.h
+     * enum {
+     *         WAVE_FORMAT_PCM = 0x0001,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_ADPCM = 0x0002,
+     *         //  Microsoft Corporation
+     *          *   IEEE754: range (+1, -1]
+     *          *  32-bit/64-bit format as defined by
+     *          *  MSVC++ float/double type
+     *
+     *         WAVE_FORMAT_IEEE_FLOAT = 0x0003,
+     *         //  IBM Corporation
+     *         WAVE_FORMAT_IBM_CVSD = 0x0005,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_ALAW = 0x0006,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MULAW = 0x0007,
+     *         //  OKI
+     *         WAVE_FORMAT_OKI_ADPCM = 0x0010,
+     *         //  Intel Corporation
+     *         WAVE_FORMAT_DVI_ADPCM = 0x0011,
+     *         //  Intel Corporation
+     *         WAVE_FORMAT_IMA_ADPCM = 0x0011,
+     *         //  Videologic
+     *         WAVE_FORMAT_MEDIASPACE_ADPCM = 0x0012,
+     *         //  Sierra Semiconductor Corp
+     *         WAVE_FORMAT_SIERRA_ADPCM = 0x0013,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G723_ADPCM = 0x0014,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGISTD = 0x0015,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIFIX = 0x0016,
+     *         //  Dialogic Corporation
+     *         WAVE_FORMAT_DIALOGIC_OKI_ADPCM = 0x0017,
+     *         //  Media Vision, Inc.
+     *         WAVE_FORMAT_MEDIAVISION_ADPCM = 0x0018,
+     *         //  Yamaha Corporation of America
+     *         WAVE_FORMAT_YAMAHA_ADPCM = 0x0020,
+     *         //  Speech Compression
+     *         WAVE_FORMAT_SONARC = 0x0021,
+     *         //  DSP Group, Inc
+     *         WAVE_FORMAT_DSPGROUP_TRUESPEECH = 0x0022,
+     *         //  Echo Speech Corporation
+     *         WAVE_FORMAT_ECHOSC1 = 0x0023,
+     *         //
+     *         WAVE_FORMAT_AUDIOFILE_AF36 = 0x0024,
+     *         //  Audio Processing Technology
+     *         WAVE_FORMAT_APTX = 0x0025,
+     *         //
+     *         WAVE_FORMAT_AUDIOFILE_AF10 = 0x0026,
+     *         //  Dolby Laboratories
+     *         WAVE_FORMAT_DOLBY_AC2 = 0x0030,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_GSM610 = 0x0031,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MSNAUDIO = 0x0032,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_ANTEX_ADPCME = 0x0033,
+     *         //  Control Resources Limited
+     *         WAVE_FORMAT_CONTROL_RES_VQLPC = 0x0034,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIREAL = 0x0035,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIADPCM = 0x0036,
+     *         //  Control Resources Limited
+     *         WAVE_FORMAT_CONTROL_RES_CR10 = 0x0037,
+     *         //  Natural MicroSystems
+     *         WAVE_FORMAT_NMS_VBXADPCM = 0x0038,
+     *         // Crystal Semiconductor IMA ADPCM
+     *         WAVE_FORMAT_CS_IMAADPCM = 0x0039,
+     *         // Echo Speech Corporation
+     *         WAVE_FORMAT_ECHOSC3 = 0x003A,
+     *         // Rockwell International
+     *         WAVE_FORMAT_ROCKWELL_ADPCM = 0x003B,
+     *         // Rockwell International
+     *         WAVE_FORMAT_ROCKWELL_DIGITALK = 0x003C,
+     *         // Xebec Multimedia Solutions Limited
+     *         WAVE_FORMAT_XEBEC = 0x003D,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G721_ADPCM = 0x0040,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G728_CELP = 0x0041,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MPEG = 0x0050,
+     *         //  ISO/MPEG Layer3 Format Tag
+     *         WAVE_FORMAT_MPEGLAYER3 = 0x0055,
+     *         //  Cirrus Logic
+     *         WAVE_FORMAT_CIRRUS = 0x0060,
+     *         //  ESS Technology
+     *         WAVE_FORMAT_ESPCM = 0x0061,
+     *         //  Voxware Inc
+     *         WAVE_FORMAT_VOXWARE = 0x0062,
+     *         //  Canopus, co., Ltd.
+     *         WAVE_FORMAT_CANOPUS_ATRAC = 0x0063,
+     *         //  APICOM
+     *         WAVE_FORMAT_G726_ADPCM = 0x0064,
+     *         //  APICOM
+     *         WAVE_FORMAT_G722_ADPCM = 0x0065,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_DSAT = 0x0066,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_DSAT_DISPLAY = 0x0067,
+     *         //  Softsound, Ltd.
+     *         WAVE_FORMAT_SOFTSOUND = 0x0080,
+     *         //  Rhetorex Inc
+     *         WAVE_FORMAT_RHETOREX_ADPCM = 0x0100,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_ADPCM = 0x0200,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_FASTSPEECH8 = 0x0202,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_FASTSPEECH10 = 0x0203,
+     *         //  Quarterdeck Corporation
+     *         WAVE_FORMAT_QUARTERDECK = 0x0220,
+     *         //  Fujitsu Corp.
+     *         WAVE_FORMAT_FM_TOWNS_SND = 0x0300,
+     *         //  Brooktree Corporation
+     *         WAVE_FORMAT_BTV_DIGITAL = 0x0400,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIGSM = 0x1000,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIADPCM = 0x1001,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLICELP = 0x1002,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLISBC = 0x1003,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIOPR = 0x1004,
+     *         //  Lernout & Hauspie
+     *         WAVE_FORMAT_LH_CODEC = 0x1100,
+     *         //  Norris Communications, Inc.
+     *         WAVE_FORMAT_NORRIS = 0x1400,
+     *         //
+     *          *  the WAVE_FORMAT_DEVELOPMENT format tag can be used during the
+     *          *  development phase of a new wave format.  Before shipping, you MUST
+     *          *  acquire an official format tag from Microsoft.
+     *
+     *         WAVE_FORMAT_DEVELOPMENT = 0xFFFF,
+     * } wFormatTagEnum;
+     *
+     * typedef struct {
+     *   WORD enum wFormatTagEnum formatTag;
+     *     // Waveform-audio format type. Format tags are registered with Microsoft
+     *     // Corporation for many compression algorithms. A complete list of format
+     *     // tags can be found in the Mmreg.h header file. For one- or two-channel
+     *     // Pulse Code Modulation (PCM) data, this value should be WAVE_FORMAT_PCM.
+     *   WORD  numberOfChannels;
+     *     // Number of channels in the waveform-audio data. Monaural data uses one
+     *     // channel and stereo data uses two channels.
+     *   DWORD samplesPerSec;
+     *     // Sample rate, in samples per second (hertz). If "formatTag" is
+     *     // "WAVE_FORMAT_PCM", then common values for "samplesPerSec" are 8.0 kHz,
+     *     // 11.025 kHz, 22.05 kHz, and 44.1 kHz. For non-PCM formats, this member
+     *     // must be computed according to the manufacturer's specification of the
+     *     // format tag.
+     *   DWORD avgBytesPerSec;
+     *     // Required average data-transfer rate, in bytes per second, for the format
+     *     // tag. If "formatTag" is "WAVE_FORMAT_PCM", "avgBytesPerSec" should be
+     *     // equal to the product of "samplesPerSec" and "blockAlignment". For non-PCM
+     *     // formats, this member must be computed according to the manufacturer's
+     *     // specification of the format tag.
+     *   WORD  blockAlignment;
+     *     // Block alignment, in bytes. The block alignment is the minimum atomic unit
+     *     // of data for the "formatTag" format type. If "formatTag" is
+     *     // "WAVE_FORMAT_PCM" or "WAVE_FORMAT_EXTENSIBLE, "blockAlignment" must be equal
+     *     // to the product of "numberOfChannels" and "bitsPerSample" divided by 8 (bits per
+     *     // byte). For non-PCM formats, this member must be computed according to the
+     *     // manufacturer's specification of the format tag.
+     *     // Software must process a multiple of "blockAlignment" bytes of data at a
+     *     // time. Data written to and read from a device must always start at the
+     *     // beginning of a block. For example, it is illegal to start playback of PCM
+     *     // data in the middle of a sample (that is, on a non-block-aligned boundary).
+     *   WORD  bitsPerSample;
+     *     // Bits per sample for the waveFormatTag format type. If "formatTag" is
+     *     // "WAVE_FORMAT_PCM", then "bitsPerSample" should be equal to 8 or 16. For
+     *     // non-PCM formats, this member must be set according to the manufacturer's
+     *     // specification of the format tag. If "formatTag" is
+     *     // "WAVE_FORMAT_EXTENSIBLE", this value can be any integer multiple of 8.
+     *     // Some compression schemes cannot define a value for "bitsPerSample", so
+     *     // this member can be zero.
+     *   WORD  cbSize;
+     *     // Size, in bytes, of extra format information appended to the end of the
+     *     // WAVEFORMATEX structure. This information can be used by non-PCM formats
+     *     // to store extra attributes for the "wFormatTag". If no extra information
+     *     // is required by the "wFormatTag", this member must be set to zero. For
+     *     // WAVE_FORMAT_PCM formats (and only WAVE_FORMAT_PCM formats), this member
+     *     // is ignored.
+     *   byte[cbSize] extra;
+     * } WAVEFORMATEX;
+     * 
+ */ + protected class AudioTrack extends Track { + + // WAVEFORMATEX Structure + /** + * Waveform-audio format type. Format tags are registered with Microsoft + * Corporation for many compression algorithms. A complete list of + * format tags can be found in the Mmreg.h header file. For one- or + * two-channel Pulse Code Modulation (PCM) data, this value should be + * WAVE_FORMAT_PCM=0x0001. + * + * If wFormatTag equals WAVE_FORMAT_EXTENSIBLE=0xFFFE, the structure is + * interpreted as a WAVEFORMATEXTENSIBLE structure. If wFormatTag equals + * WAVE_FORMAT_MPEG, the structure is interpreted as an MPEG1WAVEFORMAT + * structure. If wFormatTag equals MPEGLAYER3WAVEFORMAT, the structure + * is interpreted as an MPEGLAYER3WAVEFORMAT structure. Before + * reinterpreting a WAVEFORMATEX structure as one of these extended + * structures, verify that the actual structure size is sufficiently + * large and that the cbSize member indicates a valid size. + */ + protected int wFormatTag; + /** + * Number of channels in the waveform-audio data. Monaural data uses one + * channel and stereo data uses two channels. + */ + protected int channels; + /** + * Sample rate, in samples per second (hertz). If wFormatTag is + * WAVE_FORMAT_PCM, then common values for samplesPerSec are 8.0 kHz, + * 11.025 kHz, 22.05 kHz, and 44.1 kHz. For non-PCM formats, this member + * must be computed according to the manufacturer's specification of the + * format tag. + */ + protected long samplesPerSec; + /** + * Required average data-transfer rate, in bytes per second, for the + * format tag. If wFormatTag is WAVE_FORMAT_PCM, avgBytesPerSec should + * be equal to the product of samplesPerSec and blockAlign. For non-PCM + * formats, this member must be computed according to the manufacturer's + * specification of the format tag. + */ + protected long avgBytesPerSec; + /** + * Block alignment, in bytes. The block alignment is the minimum atomic + * unit of data for the wFormatTag format type. If wFormatTag is + * WAVE_FORMAT_PCM or WAVE_FORMAT_EXTENSIBLE, blockAlign must be equal + * to the product of channels and bitsPerSample divided by 8 (bits per + * byte). For non-PCM formats, this member must be computed according to + * the manufacturer's specification of the format tag. + * + * Software must process a multiple of blockAlign bytes of data at a + * time. Data written to and read from a device must always startTime at + * the beginning of a block. For example, it is illegal to startTime + * playback of PCM data in the middle of a sample (that is, on a + * non-block-aligned boundary). + */ + protected int blockAlign; + /** + * Bits per sample for the wFormatTag format type. If wFormatTag is + * WAVE_FORMAT_PCM, then bitsPerSample should be equal to 8 or 16. For + * non-PCM formats, this member must be set according to the + * manufacturer's specification of the format tag. If wFormatTag is + * WAVE_FORMAT_EXTENSIBLE, this value can be any integer multiple of 8. + * Some compression schemes cannot define a value for bitsPerSample, so + * this member can be zero. + */ + protected int bitsPerSample; + /** + * Size, in bytes, of extra format information appended to the end of + * the WAVEFORMATEX structure. This information can be used by non-PCM + * formats to store extra attributes for the wFormatTag. If no extra + * information is required by the wFormatTag, this member must be set to + * zero. For WAVE_FORMAT_PCM formats (and only WAVE_FORMAT_PCM formats), + * this member is ignored. + */ + //int cbSize; => this value is computed + // Well known wave format tags + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_PCM = 0x0001; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_ADPCM = 0x0002; + /** + * Microsoft Corporation IEEE754: range (+1, -1] 32-bit/64-bit format as + * defined by MSVC++ float/double type + */ + protected final static int WAVE_FORMAT_IEEE_FLOAT = 0x0003; + /** + * IBM Corporation + */ + protected final static int WAVE_FORMAT_IBM_CVSD = 0x0005; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_ALAW = 0x0006; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MULAW = 0x0007; + /** + * OKI + */ + protected final static int WAVE_FORMAT_OKI_ADPCM = 0x0010; + /** + * Intel Corporation + */ + protected final static int WAVE_FORMAT_DVI_ADPCM = 0x0011; + /** + * Intel Corporation + */ + protected final static int WAVE_FORMAT_IMA_ADPCM = WAVE_FORMAT_DVI_ADPCM; + /** + * Videologic + */ + protected final static int WAVE_FORMAT_MEDIASPACE_ADPCM = 0x0012; + /** + * Sierra Semiconductor Corp + */ + protected final static int WAVE_FORMAT_SIERRA_ADPCM = 0x0013; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G723_ADPCM = 0x0014; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGISTD = 0x0015; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIFIX = 0x0016; + /** + * Dialogic Corporation + */ + protected final static int WAVE_FORMAT_DIALOGIC_OKI_ADPCM = 0x0017; + /** + * Media Vision, Inc. + */ + protected final static int WAVE_FORMAT_MEDIAVISION_ADPCM = 0x0018; + /** + * Yamaha Corporation of America + */ + protected final static int WAVE_FORMAT_YAMAHA_ADPCM = 0x0020; + /** + * Speech Compression + */ + protected final static int WAVE_FORMAT_SONARC = 0x0021; + /** + * DSP Group, Inc + */ + protected final static int WAVE_FORMAT_DSPGROUP_TRUESPEECH = 0x0022; + /** + * Echo Speech Corporation + */ + protected final static int WAVE_FORMAT_ECHOSC1 = 0x0023; + /** + * + */ + protected final static int WAVE_FORMAT_AUDIOFILE_AF36 = 0x0024; + /** + * Audio Processing Technology + */ + protected final static int WAVE_FORMAT_APTX = 0x0025; + /** + * + */ + protected final static int WAVE_FORMAT_AUDIOFILE_AF10 = 0x0026; + /** + * Dolby Laboratories + */ + protected final static int WAVE_FORMAT_DOLBY_AC2 = 0x0030; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_GSM610 = 0x0031; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MSNAUDIO = 0x0032; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_ANTEX_ADPCME = 0x0033; + /** + * Control Resources Limited + */ + protected final static int WAVE_FORMAT_CONTROL_RES_VQLPC = 0x0034; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIREAL = 0x0035; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIADPCM = 0x0036; + /** + * Control Resources Limited + */ + protected final static int WAVE_FORMAT_CONTROL_RES_CR10 = 0x0037; + /** + * Natural MicroSystems + */ + protected final static int WAVE_FORMAT_NMS_VBXADPCM = 0x0038; + /** + * Crystal Semiconductor IMA ADPCM + */ + protected final static int WAVE_FORMAT_CS_IMAADPCM = 0x0039; + /** + * Echo Speech Corporation + */ + protected final static int WAVE_FORMAT_ECHOSC3 = 0x003A; + /** + * Rockwell International + */ + protected final static int WAVE_FORMAT_ROCKWELL_ADPCM = 0x003B; + /** + * Rockwell International + */ + protected final static int WAVE_FORMAT_ROCKWELL_DIGITALK = 0x003C; + /** + * Xebec Multimedia Solutions Limited + */ + protected final static int WAVE_FORMAT_XEBEC = 0x003D; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G721_ADPCM = 0x0040; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G728_CELP = 0x0041; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MPEG = 0x0050; + /** + * ISO/MPEG Layer3 Format Tag + */ + protected final static int WAVE_FORMAT_MPEGLAYER3 = 0x0055; + /** + * Cirrus Logic + */ + protected final static int WAVE_FORMAT_CIRRUS = 0x0060; + /** + * ESS Technology + */ + protected final static int WAVE_FORMAT_ESPCM = 0x0061; + /** + * Voxware Inc + */ + protected final static int WAVE_FORMAT_VOXWARE = 0x0062; + /** + * Canopus, co., Ltd. + */ + protected final static int WAVE_FORMAT_CANOPUS_ATRAC = 0x0063; + /** + * APICOM + */ + protected final static int WAVE_FORMAT_G726_ADPCM = 0x0064; + /** + * APICOM + */ + protected final static int WAVE_FORMAT_G722_ADPCM = 0x0065; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_DSAT = 0x0066; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_DSAT_DISPLAY = 0x0067; + /** + * Softsound, Ltd. + */ + protected final static int WAVE_FORMAT_SOFTSOUND = 0x0080; + /** + * Rhetorex Inc + */ + protected final static int WAVE_FORMAT_RHETOREX_ADPCM = 0x0100; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_ADPCM = 0x0200; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_FASTSPEECH8 = 0x0202; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_FASTSPEECH10 = 0x0203; + /** + * Quarterdeck Corporation + */ + protected final static int WAVE_FORMAT_QUARTERDECK = 0x0220; + /** + * Fujitsu Corp. + */ + protected final static int WAVE_FORMAT_FM_TOWNS_SND = 0x0300; + /** + * Brooktree Corporation + */ + protected final static int WAVE_FORMAT_BTV_DIGITAL = 0x0400; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIGSM = 0x1000; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIADPCM = 0x1001; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLICELP = 0x1002; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLISBC = 0x1003; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIOPR = 0x1004; + /** + * Lernout & Hauspie + */ + protected final static int WAVE_FORMAT_LH_CODEC = 0x1100; + /** + * Norris Communications, Inc. + */ + protected final static int WAVE_FORMAT_NORRIS = 0x1400; + /** + * the WAVE_FORMAT_DEVELOPMENT format tag can be used during the + * development phase of a new wave format. Before shipping, you MUST + * acquire an official format tag from Microsoft. + */ + protected final static int WAVE_FORMAT_DEVELOPMENT = 0xFFFF; + private int sampleChunkFourCC; + + public AudioTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.AUDIO, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + return 18; + + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + return sampleChunkFourCC; + } + } + + /** + * Chunk base class. + */ + protected abstract class Chunk { + + /** + * The chunkType of the chunk. A String with the length of 4 characters. + */ + protected int chunkType; + /** + * The offset of the chunk relative to the startTime of the + * ImageOutputStream. + */ + protected long offset; + + /** + * Creates a new Chunk at the current position of the ImageOutputStream. + * + * @param chunkType The chunkType of the chunk. A string with a length + * of 4 characters. + */ + public Chunk(int chunkType) throws IOException { + this.chunkType = chunkType; + offset = getRelativeStreamPosition(); + } + + /** + * Writes the chunk to the ImageOutputStream and disposes it. + */ + public abstract void finish() throws IOException; + + /** + * Returns the size of the chunk including the size of the chunk header. + * + * @return The size of the chunk. + */ + public abstract long size(); + } + + /** + * A CompositeChunk contains an ordered list of Chunks. + */ + protected class CompositeChunk extends Chunk { + + /** + * The type of the composite. A String with the length of 4 characters. + */ + protected int compositeType; + protected LinkedList children; + protected boolean finished; + + /** + * Creates a new CompositeChunk at the current position of the + * ImageOutputStream. + * + * @param compositeType The type of the composite. + * @param chunkType The type of the chunk. + */ + public CompositeChunk(int compositeType, int chunkType) throws IOException { + super(chunkType); + this.compositeType = compositeType; + //out.write + out.writeLong(0); // make room for the chunk header + out.writeInt(0); // make room for the chunk header + children = new LinkedList(); + } + + public void add(Chunk child) throws IOException { + if (children.size() > 0) { + children.getLast().finish(); + } + children.add(child); + } + + /** + * Writes the chunk and all its children to the ImageOutputStream and + * disposes of all resources held by the chunk. + * + * @throws java.io.IOException + */ + @Override + public void finish() throws IOException { + if (!finished) { + if (size() > 0xffffffffL) { + throw new IOException("CompositeChunk \"" + chunkType + "\" is too large: " + size()); + } + + long pointer = getRelativeStreamPosition(); + seekRelative(offset); + + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(compositeType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) (size() - 8)); + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + for (Chunk child : children) { + child.finish(); + } + seekRelative(pointer); + if (size() % 2 == 1) { + out.writeByte(0); // write pad byte + } + finished = true; + } + } + + @Override + public long size() { + long length = 12; + for (Chunk child : children) { + length += child.size() + child.size() % 2; + } + return length; + } + } + + /** + * Data Chunk. + */ + protected class DataChunk extends Chunk { + + //protected SubImageOutputStream data; + protected boolean finished; + private long finishedSize; + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param name The name of the chunk. + */ + public DataChunk(int name) throws IOException { + this(name, -1); + } + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param name The name of the chunk. + * @param dataSize The size of the chunk data, or -1 if not known. + */ + public DataChunk(int name, long dataSize) throws IOException { + super(name); + /* + data = new SubImageOutputStream(out, ByteOrder.LITTLE_ENDIAN, false); + data.writeInt(typeToInt(chunkType)); + data.writeInt((int)Math.max(0, dataSize)); */ + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) Math.max(0, dataSize)); + finishedSize = dataSize == -1 ? -1 : dataSize + 8; + } + + public ImageOutputStream getOutputStream() { + if (finished) { + throw new IllegalStateException("DataChunk is finished"); + } + //return data; + return out; + } + + /** + * Returns the offset of this chunk to the beginning of the random + * access file + */ + public long getOffset() { + return offset; + } + + @Override + public void finish() throws IOException { + if (!finished) { + if (finishedSize == -1) { + finishedSize = size(); + + if (finishedSize > 0xffffffffL) { + throw new IOException("DataChunk \"" + chunkType + "\" is too large: " + size()); + } + + seekRelative(offset + 4); + out.writeInt((int) (finishedSize - 8)); + seekRelative(offset + finishedSize); + } else { + if (size() != finishedSize) { + throw new IOException("DataChunk \"" + chunkType + "\" actual size differs from given size: actual size:" + size() + " given size:" + finishedSize); + } + } + if (size() % 2 == 1) { + out.writeByte(0); // write pad byte + } + + + //data.dispose(); + //data = null; + finished = true; + } + } + + @Override + public long size() { + if (finished) { + return finishedSize; + } + + try { + // return data.length(); + return out.getStreamPosition() - offset; + } catch (IOException ex) { + InternalError ie = new InternalError("IOException"); + ie.initCause(ex); + throw ie; + } + } + } + + /** + * A DataChunk with a fixed size. + */ + protected class FixedSizeDataChunk extends Chunk { + + protected boolean finished; + protected long fixedSize; + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param chunkType The chunkType of the chunk. + */ + public FixedSizeDataChunk(int chunkType, long fixedSize) throws IOException { + super(chunkType); + this.fixedSize = fixedSize; + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) fixedSize); + + // Fill fixed size with nulls + byte[] buf = new byte[(int) Math.min(512, fixedSize)]; + long written = 0; + while (written < fixedSize) { + out.write(buf, 0, (int) Math.min(buf.length, fixedSize - written)); + written += Math.min(buf.length, fixedSize - written); + } + if (fixedSize % 2 == 1) { + out.writeByte(0); // write pad byte + } + seekToStartOfData(); + } + + public ImageOutputStream getOutputStream() { + /*if (finished) { + throw new IllegalStateException("DataChunk is finished"); + }*/ + return out; + } + + /** + * Returns the offset of this chunk to the beginning of the random + * access file + */ + public long getOffset() { + return offset; + } + + public void seekToStartOfData() throws IOException { + seekRelative(offset + 8); + + } + + public void seekToEndOfChunk() throws IOException { + seekRelative(offset + 8 + fixedSize + fixedSize % 2); + } + + @Override + public void finish() throws IOException { + if (!finished) { + finished = true; + } + } + + @Override + public long size() { + return 8 + fixedSize; + } + } + + protected class MidiTrack extends Track { + + private final int sampleChunkFourCC; + + public MidiTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.MIDI, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + protected class TextTrack extends Track { + + private final int sampleChunkFourCC; + + public TextTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.TEXT, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + /** + *

Holds information about the entire movie.

+ * + *
+     * ---------------
+     * AVI Main Header
+     * ---------------
+     *
+     * Set values taken from
+     * http://graphics.cs.uni-sb.de/NMM/dist-0.4.0/Docs/Doxygen/html/avifmt_8h.html
+     *
+     * typedef struct {
+     *     DWORD  microSecPerFrame;
+     *             // Specifies the number of microseconds between frames.
+     *             // This value indicates the overall timing for the file.
+     *     DWORD  maxBytesPerSec;
+     *             // Specifies the approximate maximum data rate of the file. This
+     *             // value indicates the number of bytes per second the system must
+     *             // handle to present an AVI sequence as specified by the other
+     *             // parameters contained in the main header and stream header chunks.
+     *     DWORD  paddingGranularity;
+     *             // Specifies the alignment for data, in bytes. Pad the data to
+     *             // multiples of this value.
+     *     DWORD set avihFlags  flags;
+     *             // Contains a bitwise combination of zero or more of the following flags:
+     *     DWORD  totalFrames;
+     *             // Specifies the total number of frames of data in the file.
+     *     DWORD  initialFrames;
+     *             // Specifies the initial frame for interleaved files. Noninterleaved
+     *             // files should specify zero. If you are creating interleaved files,
+     *             // specify the number of frames in the file prior to the initial
+     *             // frame of the AVI sequence in this member. For more information
+     *             // about the contents of this member, see "Special Information for
+     *             // Interleaved Files" in the Video for Windows Programmer's Guide.
+     *     DWORD  streams;
+     *             // Specifies the number of streams in the file. For example, a file
+     *             // with audio and video has two streams.
+     *     DWORD  suggestedBufferSize;
+     *             // Specifies the suggested buffer size for reading the file.
+     *             // Generally, this size should be large enough to contain the
+     *             // largest chunk in the file. If set to zero, or if it is too small,
+     *             // the playback software will have to reallocate memory during
+     *             // playback, which will reduce performance. For an interleaved file,
+     *             // the buffer size should be large enough to read an entire record,
+     *             // and not just a chunk.
+     *     DWORD  width;
+     *             // Specifies the width of the AVI file in pixels.
+     *     DWORD  height;
+     *             // Specifies the height of the AVI file in pixels.
+     *     DWORD[]  reserved;
+     *             // Reserved. Set this array to zero.
+     * } AVIMAINHEADER;
+     * 
+ */ + protected static class MainHeader { + + /** + * Specifies the number of microseconds (=10E-6 seconds) between frames. + * This value indicates the overall timing for the file. + */ + protected long microSecPerFrame; + protected long maxBytesPerSec; + protected long paddingGranularity; + protected int flags; + protected long totalFrames; + protected long initialFrames; + protected long streams; + protected long suggestedBufferSize; + /** + * Width and height of the movie. Null if not specified. + */ + protected Dimension size; + } + + protected static int typeToInt(String str) { + int value = ((str.charAt(0) & 0xff) << 24) | ((str.charAt(1) & 0xff) << 16) | ((str.charAt(2) & 0xff) << 8) | (str.charAt(3) & 0xff); + return value; + } + + protected static String intToType(int id) { + char[] b=new char[4]; + + b[0] = (char) ((id >>> 24) & 0xff); + b[1] = (char) ((id >>> 16) & 0xff); + b[2] = (char) ((id >>> 8) & 0xff); + b[3] = (char) (id & 0xff); + return String.valueOf(b); + } + + /** + * Returns true, if the specified mask is set on the flag. + */ + protected static boolean isFlagSet(int flag, int mask) { + return (flag & mask) == mask; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java b/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java new file mode 100644 index 000000000..fee2740b6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java @@ -0,0 +1,343 @@ +/* + * @(#)DIBCodec.java + * + * Copyright © 2011-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.avi; + +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferByte; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.Format; +import org.monte.media.io.SeekableByteArrayOutputStream; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.OutputStream; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code DIBCodec} encodes a BufferedImage as a Microsoft Device Independent + * Bitmap (DIB) into a byte array. + *

+ * The DIB codec only works with the AVI file format. Other file formats, such + * as QuickTime, use a different encoding for uncompressed video. + *

+ * This codec currently only supports encoding from a {@code BufferedImage} into + * the file format. Decoding support may be added in the future. + *

+ * This codec does not encode the color palette of an image. This must be done + * separately. + *

+ * The pixels of a frame are written row by row from bottom to top and from + * the left to the right. 24-bit pixels are encoded as BGR. + *

+ * Supported input formats: + *

    + * {@code Format} with {@code BufferedImage.class}, any width, any height, + * depth=4. + *
+ * Supported output formats: + *
    + * {@code Format} with {@code byte[].class}, same width and height as input + * format, depth=4. + *
+ * + * @author Werner Randelshofer + * @version $Id: DIBCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class DIBCodec extends AbstractVideoCodec { + + public DIBCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 4), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 8), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 24), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 4), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 8), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 24), // + }); + } + + @Override + public int process(Buffer in, Buffer out) { + if (outputFormat.get(EncodingKey) == ENCODING_BUFFERED_IMAGE) { + return decode(in, out); + } else { + return encode(in, out); + } + } + + public int decode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + out.sampleCount = 1; + BufferedImage img = null; + + int imgType; + switch (outputFormat.get(DepthKey)) { + case 4: + imgType = BufferedImage.TYPE_BYTE_INDEXED; + break; + case 8: + imgType = BufferedImage.TYPE_BYTE_INDEXED; + break; + case 24: + imgType = BufferedImage.TYPE_INT_RGB; + break; + default: + imgType = BufferedImage.TYPE_INT_RGB; + break; + } + + if (out.data instanceof BufferedImage) { + img = (BufferedImage) out.data; + // Fixme: Handle sub-image + if (img.getWidth() != outputFormat.get(WidthKey) + || img.getHeight() != outputFormat.get(HeightKey) + || img.getType() != imgType) { + img = null; + } + } + if (img == null) { + img = new BufferedImage(outputFormat.get(WidthKey), outputFormat.get(HeightKey), imgType); + } + out.data = img; + + switch (outputFormat.get(DepthKey)) { + case 4: + readKey4((byte[]) in.data, in.offset, in.length, img); + break; + case 8: + readKey8((byte[]) in.data, in.offset, in.length, img); + break; + case 24: + default: + readKey24((int[]) in.data, in.offset, in.length, img); + break; + } + + + return CODEC_OK; + } + + public int encode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + SeekableByteArrayOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new SeekableByteArrayOutputStream((byte[]) out.data); + } else { + tmp = new SeekableByteArrayOutputStream(); + } + + // Handle sub-image + // FIXME - Scanline stride must be a multiple of four. + Rectangle r; + int scanlineStride; + if (in.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) in.data; + WritableRaster raster = image.getRaster(); + scanlineStride = raster.getSampleModel().getWidth(); + r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + out.header = image.getColorModel(); + } else { + r = new Rectangle(0, 0, outputFormat.get(WidthKey), outputFormat.get(HeightKey)); + scanlineStride = outputFormat.get(WidthKey); + out.header = null; + } + + try { + switch (outputFormat.get(DepthKey)) { + case 4: { + byte[] pixels = getIndexed8(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey4(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + case 8: { + byte[] pixels = getIndexed8(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey8(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + case 24: { + int[] pixels = getRGB24(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey24(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + default: + out.setFlag(DISCARD); + return CODEC_OK; + } + + out.setFlag(KEYFRAME); + out.data = tmp.getBuffer(); + out.sampleCount = 1; + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } + + public void readKey4(byte[] in, int offset, int length, BufferedImage img) { + DataBufferByte buf = (DataBufferByte) img.getRaster().getDataBuffer(); + WritableRaster raster = img.getRaster(); + int scanlineStride = raster.getSampleModel().getWidth(); + Rectangle r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + + throw new UnsupportedOperationException("readKey4 not yet implemented"); + } + + public void readKey8(byte[] in, int offset, int length, BufferedImage img) { + DataBufferByte buf = (DataBufferByte) img.getRaster().getDataBuffer(); + WritableRaster raster = img.getRaster(); + int scanlineStride = raster.getSampleModel().getWidth(); + Rectangle r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + + int h=img.getHeight(); + int w=img.getWidth(); + int i=offset; + int j=r.x+r.y*scanlineStride+(h-1)*scanlineStride; + byte[] out=buf.getData(); + for (int y=0;y= 0; y -= scanlineStride) { // Upside down + for (int x = offset, xx = 0, n = offset + width; x < n; x += 2, ++xx) { + bytes[xx] = (byte) (((pixels[y + x] & 0xf) << 4) | (pixels[y + x + 1] & 0xf)); + } + out.write(bytes); + } + + } + + /** Encodes an 8-bit key frame. + * + * @param out The output stream. + * @param pixels The image data. + * @param offset The offset to the first pixel in the data array. + * @param width The width of the image in data elements. + * @param scanlineStride The number to append to offset to get to the next scanline. + */ + public void writeKey8(OutputStream out, byte[] pixels, int width, int height, int offset, int scanlineStride) + throws IOException { + + for (int y = (height - 1) * scanlineStride; y >= 0; y -= scanlineStride) { // Upside down + out.write(pixels, y + offset, width); + } + } + + /** Encodes a 24-bit key frame. + * + * @param out The output stream. + * @param pixels The image data. + * @param offset The offset to the first pixel in the data array. + * @param width The width of the image in data elements. + * @param scanlineStride The number to append to offset to get to the next scanline. + */ + public void writeKey24(OutputStream out, int[] pixels, int width, int height, int offset, int scanlineStride) + throws IOException { + int w3 = width * 3; + byte[] bytes = new byte[w3]; // holds a scanline of raw image data with 3 channels of 8 bit data + for (int xy = (height - 1) * scanlineStride + offset; xy >= offset; xy -= scanlineStride) { // Upside down + for (int x = 0, xp = 0; x < w3; x += 3, ++xp) { + int p = pixels[xy + xp]; + bytes[x] = (byte) (p); // Blue + bytes[x + 1] = (byte) (p >> 8); // Green + bytes[x + 2] = (byte) (p >> 16); // Red + } + out.write(bytes); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/color/Colors.java b/trunk/libsrc/avi/src/org/monte/media/color/Colors.java new file mode 100644 index 000000000..79562e2f4 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/color/Colors.java @@ -0,0 +1,213 @@ +/* + * @(#)Colors.java 1.0 2011-03-13 + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.color; + +import java.awt.image.IndexColorModel; +import static java.lang.Math.*; + +/** + * {@code Colors}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-13 Created. + */ +public class Colors { + + /** Prevent instance creation. */ + private Colors() { + } + + /** + * The macintosh palette is arranged as follows: there are 256 colours to + * allocate, an even distribution of colors through the color cube might be + * desirable but 256 is not the cube of an integer. 6x6x6 is 216 and so the + * first 216 colors are an equal 6x6x6 sampling of the color cube. + * This leaves 40 colours to allocate, this has been done by choosing a ramp of + * 10 shades each for red, green, blue and grey. + * + *

+ * References:
+ * http://paulbourke.net/texture_colour/colourramp/ + * + * @return The Macintosh color palette. + */ + public static IndexColorModel createMacColors() { + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + + // Generate color cube with 216 colors + int index = 0; + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + for (int k = 0; k < 6; k++) { + r[index] = (byte) (255 - 51 * i); + g[index] = (byte) (255 - 51 * j); + b[index] = (byte) (255 - 51 * k); + index++; + } + } + } + + index--; // overwrite last color (black) with color ramp + + // Generate red ramp + byte[] ramp = {(byte) 238, (byte) 221, (byte) 187, (byte) 170, (byte) 136, (byte) 119, 85, 68, 34, 17}; + for (int i = 0; i < 10; i++) { + r[index] = ramp[i]; + g[index] = (byte) (0); + b[index] = (byte) (0); + index++; + } + // Generate green ramp + for (int j = 0; j < 10; j++) { + r[index] = (byte) (0); + g[index] = ramp[j]; + b[index] = (byte) (0); + index++; + } + // Generate blue ramp + for (int k = 0; k < 10; k++) { + r[index] = (byte) (0); + g[index] = (byte) (0); + b[index] = ramp[k]; + index++; + } + // Generate gray ramp + for (int ijk = 0; ijk < 10; ijk++) { + r[index] = ramp[ijk]; + g[index] = ramp[ijk]; + b[index] = ramp[ijk]; + index++; + } + // last color is black (nothing to do) + + /* + for (int i=0;i<256;i++) { + if (i%6==0) System.out.println(); else System.out.print(" "); + System.out.print(Integer.toHexString(r[i]&0xff)+","+Integer.toHexString(g[i]&0xff)+","+Integer.toHexString(b[i]&0xff)); + }*/ + + IndexColorModel icm = new IndexColorModel(8, 256, r, g, b); + return icm; + } + + private static void RGBtoYCC(float[] rgb, float[] ycc) { + float R = rgb[0]; + float G = rgb[1]; + float B = rgb[2]; + float Y = 0.3f * R + 0.6f * G + 0.1f * B; + float V = R - Y; + float U = B - Y; + float Cb = (U / 2f) + 0.5f; + float Cr = (V / 1.6f) + 0.5f; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + private static void YCCtoRGB(float[] ycc, float[] rgb) { + float Y = ycc[0]; + float Cb = ycc[1]; + float Cr = ycc[2]; + float U = (Cb - 0.5f) * 2f; + float V = (Cr - 0.5f) * 1.6f; + float R = V + Y; + float B = U + Y; + float G = (Y - 0.3f * R - 0.1f * B) / 0.6f; + rgb[0] = R; + rgb[1] = G; + rgb[2] = B; + } + + /** RGB 8-bit per channel to YCC 16-bit per channel. */ + private static void RGB8toYCC16(int[] rgb, int[] ycc) { + int R = rgb[0]; + int G = rgb[1]; + int B = rgb[2]; + int Y = 77 * R + 153 * G + 26 * B; + int V = R * 256 - Y; + int U = B * 256 - Y; + int Cb = (U / 2) + 128 * 256; + int Cr = (V * 5 / 8) + 128 * 256; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + /** RGB 8-bit per channel to YCC 16-bit per channel. */ + private static void RGB8toYCC16(int rgb, int[] ycc) { + int R = (rgb & 0xff0000) >>> 16; + int G = (rgb & 0xff00) >>> 8; + int B = rgb & 0xff; + int Y = 77 * R + 153 * G + 26 * B; + int V = R * 256 - Y; + int U = B * 256 - Y; + int Cb = (U / 2) + 128 * 256; + int Cr = (V * 5 / 8) + 128 * 256; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + /** YCC 16-bit per channel to RGB 8-bit per channel. */ + private static void YCC16toRGB8(int[] ycc, int[] rgb) { + int Y = ycc[0]; + int Cb = ycc[1]; + int Cr = ycc[2]; + int U = (Cb - 128 * 256) * 2; + int V = (Cr - 128 * 256) * 8 / 5; + int R = min(255, max(0, (V + Y) / 256)); + int B = min(255, max(0, (U + Y) / 256)); + int G = min(255, max(0, (Y - 77 * R - 26 * B) / 153)); + rgb[0] = R; + rgb[1] = G; + rgb[2] = B; + } + + /** YCC 8-bit per channel to RGB 8-bit per channel. + */ + private static void YCC8toRGB8(int[] ycc, int[] rgb) { + int Y = ycc[0]; + int Cb = ycc[1]; + int Cr = ycc[2]; + // Source: JPEG File Interchange Format Version 1.02, September 1, 1992 + //RGB can be computed directly from YCbCr (256 levels) as follows: + //R = Y + 1.402 (Cr-128) + //G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) + //B = Y + 1.772 (Cb-128) + int R = (1000 * Y + 1402 * (Cr - 128)) / 1000; + int G = (100000 * Y - 34414 * (Cb - 128) - 71414 * (Cr - 128)) / 100000; + int B = (1000 * Y + 1772 * (Cb - 128)) / 1000; + rgb[0] = min(255, max(0, R)); + rgb[1] = min(255, max(0, G)); + rgb[2] = min(255, max(0, B)); + } + + /** YCC 8-bit per channel to RGB 8-bit per channel. + */ + private static void RGB8toYCC8(int[] rgb, int[] ycc) { + int R = rgb[0]; + int G = rgb[1]; + int B = rgb[2]; + // Source: JPEG File Interchange Format Version 1.02, September 1, 1992 + //YCbCr (256 levels) can be computed directly from 8-bit RGB as follows: + //Y = 0.299R +0.587G +0.114B + //Cb = - 0.1687 R - 0.3313 G + 0.5 B + 128 + //Cr = 0.5 R - 0.4187 G - 0.0813 B + 128 + int Y = (299 * R + 587 * G + 114 * B) / 1000; + int Cb = (-1687 * R - 3313 * G + 5000 * B) / 10000 + 128; + int Cr = (5000 * R - 4187 * G - 813 * B) / 10000 + 128; + ycc[0] = min(255, max(0, Y)); + ycc[1] = min(255, max(0, Cb)); + ycc[2] = min(255, max(0, Cr)); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java new file mode 100644 index 000000000..4baa50420 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java @@ -0,0 +1,216 @@ +/* + * @(#)ByteArrayImageInputStream.java + * + * Copyright (c) 2008-2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.io; + +import java.io.IOException; +import java.nio.ByteOrder; + +/** + * A {@code ByteArrayImageInputStream} contains + * an internal buffer that contains bytes that + * may be read from the stream. An internal + * counter keeps track of the next byte to + * be supplied by the {@code read} method. + *

+ * Closing a {@code ByteArrayImageInputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau + * @version $Id: ByteArrayImageInputStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public class ByteArrayImageInputStream extends ImageInputStreamImpl2 { + /** + * An array of bytes that was provided + * by the creator of the stream. Elements buf[0] + * through buf[count-1] are the + * only bytes that can ever be read from the + * stream; element buf[streamPos] is + * the next byte to be read. + */ + protected byte buf[]; + + /** + * The index one greater than the last valid character in the input + * stream buffer. + * This value should always be nonnegative + * and not larger than the length of buf. + * It is one greater than the position of + * the last byte within buf that + * can ever be read from the input stream buffer. + */ + protected int count; + + /** The offset to the start of the array. */ + private final int arrayOffset; + + public ByteArrayImageInputStream(byte[] buf) { + this(buf, ByteOrder.BIG_ENDIAN); + } + + public ByteArrayImageInputStream(byte[] buf, ByteOrder byteOrder) { + this(buf, 0, buf.length, byteOrder); + } + + public ByteArrayImageInputStream(byte[] buf, int offset, int length, ByteOrder byteOrder) { + this.buf = buf; + this.streamPos = offset; + this.count = Math.min(offset + length, buf.length); + this.arrayOffset = offset; + this.byteOrder = byteOrder; + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. + *

+ * This read method + * cannot block. + * + * @return the next byte of data, or -1 if the end of the + * stream has been reached. + */ + @Override + public synchronized int read() { + flushBits(); + return (streamPos < count) ? (buf[(int)(streamPos++)] & 0xff) : -1; + } + + /** + * Reads up to len bytes of data into an array of bytes + * from this input stream. + * If streamPos equals count, + * then -1 is returned to indicate + * end of file. Otherwise, the number k + * of bytes read is equal to the smaller of + * len and count-streamPos. + * If k is positive, then bytes + * buf[streamPos] through buf[streamPos+k-1] + * are copied into b[off] through + * b[off+k-1] in the manner performed + * by System.arraycopy. The + * value k is added into streamPos + * and k is returned. + *

+ * This read method cannot block. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + */ + @Override + public synchronized int read(byte b[], int off, int len) { + flushBits(); + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + if (streamPos >= count) { + return -1; + } + if (streamPos + len > count) { + len = (int)(count - streamPos); + } + if (len <= 0) { + return 0; + } + System.arraycopy(buf, (int)streamPos, b, off, len); + streamPos += len; + return len; + } + + /** + * Skips n bytes of input from this input stream. Fewer + * bytes might be skipped if the end of the input stream is reached. + * The actual number k + * of bytes to be skipped is equal to the smaller + * of n and count-streamPos. + * The value k is added into streamPos + * and k is returned. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + */ + public synchronized long skip(long n) { + if (streamPos + n > count) { + n = count - streamPos; + } + if (n < 0) { + return 0; + } + streamPos += n; + return n; + } + + /** + * Returns the number of remaining bytes that can be read (or skipped over) + * from this input stream. + *

+ * The value returned is count - streamPos, + * which is the number of bytes remaining to be read from the input buffer. + * + * @return the number of remaining bytes that can be read (or skipped + * over) from this input stream without blocking. + */ + public synchronized int available() { + return (int)(count - streamPos); + } + + + + /** + * Closing a ByteArrayInputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ */ + @Override + public void close() { + // does nothing!! + } + + @Override + public long getStreamPosition() throws IOException { + checkClosed(); + return streamPos-arrayOffset; + } + @Override + public void seek(long pos) throws IOException { + checkClosed(); + flushBits(); + + // This test also covers pos < 0 + if (pos < flushedPos) { + throw new IndexOutOfBoundsException("pos < flushedPos!"); + } + + this.streamPos = pos+arrayOffset; + } + + private void flushBits() { + bitOffset=0; + } + @Override + public long length() { + return count-arrayOffset; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java new file mode 100644 index 000000000..beb456ad9 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java @@ -0,0 +1,336 @@ +/* + * @(#)ByteArrayImageOutputStream.java 1.0.1 2011-01-23 + * + * Copyright (c) 2011 Werner Randelshofer + * Staldenmattweg 2, Goldau, CH-6405, Switzerland. + * All rights reserved. + * + * The copyright of this software is owned by Werner Randelshofer. + * You may not use, copy or modify this software, except in + * accordance with the license agreement you entered into with + * Werner Randelshofer. For details see accompanying license terms. + */ +package org.monte.media.io; + +import java.io.OutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; +import java.io.ByteArrayOutputStream; +import javax.imageio.stream.ImageOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.nio.ByteOrder; +import static java.lang.Math.*; + +/** + * This class implements an image output stream in which the data is + * written into a byte array. The buffer automatically grows as data + * is written to it. + * The data can be retrieved using {@code toByteArray()}, {@code toImageOutputStream()} + * and {@code toOutputStream()}. + *

+ * Closing a {@code ByteArrayImageOutputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + * + * @author Werner Randelshofer + * @version 1.0.1 2011-01-23 Implements length method. + *
1.0 2011-01-18 Created. + */ +public class ByteArrayImageOutputStream extends ImageOutputStreamImpl { + + + /** + * An array of bytes that was provided + * by the creator of the stream. Elements buf[0] + * through buf[count-1] are the + * only bytes that can ever be read from the + * stream; element buf[streamPos] is + * the next byte to be read. + */ + protected byte buf[]; + /** + * The index one greater than the last valid character in the input + * stream buffer. + * This value should always be nonnegative + * and not larger than the length of buf. + * It is one greater than the position of + * the last byte within buf that + * can ever be read from the input stream buffer. + */ + protected int count; + /** The offset to the start of the array. */ + private final int arrayOffset; + + public ByteArrayImageOutputStream() { + this(16); + } + + public ByteArrayImageOutputStream(int initialCapacity) { + this(new byte[initialCapacity]); + } + + public ByteArrayImageOutputStream(byte[] buf) { + this(buf, ByteOrder.BIG_ENDIAN); + } + + public ByteArrayImageOutputStream(byte[] buf, ByteOrder byteOrder) { + this(buf, 0, buf.length, byteOrder); + } + + public ByteArrayImageOutputStream(byte[] buf, int offset, int length, ByteOrder byteOrder) { + this.buf = buf; + this.streamPos = offset; + this.count = Math.min(offset + length, buf.length); + this.arrayOffset = offset; + this.byteOrder = byteOrder; + } + + public ByteArrayImageOutputStream(ByteOrder byteOrder) { + this(new byte[16],byteOrder); + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. + *

+ * This read method + * cannot block. + * + * @return the next byte of data, or -1 if the end of the + * stream has been reached. + */ + @Override + public synchronized int read() throws IOException { + flushBits(); + return (streamPos < count) ? (buf[(int) (streamPos++)] & 0xff) : -1; + } + + /** + * Reads up to len bytes of data into an array of bytes + * from this input stream. + * If streamPos equals count, + * then -1 is returned to indicate + * end of file. Otherwise, the number k + * of bytes read is equal to the smaller of + * len and count-streamPos. + * If k is positive, then bytes + * buf[streamPos] through buf[streamPos+k-1] + * are copied into b[off] through + * b[off+k-1] in the manner performed + * by System.arraycopy. The + * value k is added into streamPos + * and k is returned. + *

+ * This read method cannot block. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + */ + @Override + public synchronized int read(byte b[], int off, int len) throws IOException { + flushBits(); + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + if (streamPos >= count) { + return -1; + } + if (streamPos + len > count) { + len = (int) (count - streamPos); + } + if (len <= 0) { + return 0; + } + System.arraycopy(buf, (int) streamPos, b, off, len); + streamPos += len; + return len; + } + + /** + * Skips n bytes of input from this input stream. Fewer + * bytes might be skipped if the end of the input stream is reached. + * The actual number k + * of bytes to be skipped is equal to the smaller + * of n and count-streamPos. + * The value k is added into streamPos + * and k is returned. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + */ + public synchronized long skip(long n) { + if (streamPos + n > count) { + n = count - streamPos; + } + if (n < 0) { + return 0; + } + streamPos += n; + return n; + } + + /** + * Returns the number of remaining bytes that can be read (or skipped over) + * from this input stream. + *

+ * The value returned is count - streamPos, + * which is the number of bytes remaining to be read from the input buffer. + * + * @return the number of remaining bytes that can be read (or skipped + * over) from this input stream without blocking. + */ + public synchronized int available() { + return (int) (count - streamPos); + } + + /** + * Closing a ByteArrayInputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ */ + @Override + public void close() { + // does nothing!! + } + + @Override + public long getStreamPosition() throws IOException { + checkClosed(); + return streamPos - arrayOffset; + } + + @Override + public void seek(long pos) throws IOException { + checkClosed(); + flushBits(); + + // This test also covers pos < 0 + if (pos < flushedPos) { + throw new IndexOutOfBoundsException("pos < flushedPos!"); + } + + this.streamPos = pos + arrayOffset; + } + + /** + * Writes the specified byte to this output stream. + * + * @param b the byte to be written. + */ + @Override + public synchronized void write(int b) throws IOException { + flushBits(); + long newcount = max(streamPos + 1, count); + if (newcount> Integer.MAX_VALUE) { + throw new IndexOutOfBoundsException(newcount+" > max array size"); + } + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, max(buf.length << 1, (int) newcount)); + } + buf[(int) streamPos++] = (byte) b; + count = (int)newcount; + } + + /** + * Writes the specified byte array to this output stream. + * + * @param b the data. + */ + @Override + public synchronized void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + @Override + public synchronized void write(byte b[], int off, int len) throws IOException { + flushBits(); + if ((off < 0) || (off > b.length) || (len < 0) + || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException("off="+off+", len="+len+", b.length="+b.length); + } else if (len == 0) { + return; + } + int newcount = max((int) streamPos + len, count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + System.arraycopy(b, off, buf, (int) streamPos, len); + streamPos += len; + count = newcount; + } + + /** Writes the contents of the byte array into the specified output + * stream. + * @param out + */ + public void toOutputStream(OutputStream out) throws IOException { + out.write(buf, arrayOffset, count); + } + + /** Writes the contents of the byte array into the specified image output + * stream. + * @param out + */ + public void toImageOutputStream(ImageOutputStream out) throws IOException { + out.write(buf, arrayOffset, count); + } + + /** + * Creates a newly allocated byte array. Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it. + * + * @return the current contents of this output stream, as a byte array. + * @see java.io.ByteArrayOutputStream#size() + */ + public synchronized byte[] toByteArray() { + byte[] copy = new byte[count - arrayOffset]; + System.arraycopy(buf, arrayOffset, copy, 0, count); + return copy; + } + + /** Returns the internally used byte buffer. */ + public byte[] getBuffer() { + return buf; + } + + @Override + public long length() { + return count-arrayOffset; + } + + /** + * Resets the count field of this byte array output + * stream to zero, so that all currently accumulated output in the + * output stream is discarded. The output stream can be used again, + * reusing the already allocated buffer space. + * + * @see java.io.ByteArrayInputStream#count + */ + public synchronized void clear() { + count = arrayOffset; + streamPos=arrayOffset; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java new file mode 100644 index 000000000..30fd47813 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java @@ -0,0 +1,189 @@ +/* + * @(#)ImageInputStreamAdapter.java 1.0 2009-12-17 + * + * Copyright (c) 2009 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ + +package org.monte.media.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import javax.imageio.stream.ImageInputStream; + +/** + * ImageInputStreamAdapter. + * + * @author Werner Randelshofer + * @version 1.0 2009-12-17 Created. + */ +public class ImageInputStreamAdapter extends FilterInputStream { + private ImageInputStream iis; + public ImageInputStreamAdapter(ImageInputStream iis) { + super(null); + this.iis=iis; + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

+ * This method + * simply performs in.read() and returns the result. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + return iis.read(); + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. If len is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and 0 is returned. + *

+ * This method simply performs in.read(b, off, len) + * and returns the result. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + return iis.read(b, off, len); + } + + /** + * {@inheritDoc} + *

+ * This method simply performs in.skip(n). + */ + @Override + public long skip(long n) throws IOException { + return iis.skipBytes(n); + } + + /** + * Returns an estimate of the number of bytes that can be read (or + * skipped over) from this input stream without blocking by the next + * caller of a method for this input stream. The next caller might be + * the same thread or another thread. A single read or skip of this + * many bytes will not block, but may read or skip fewer bytes. + *

+ * This method returns the result of {@link #in in}.available(). + * + * @return an estimate of the number of bytes that can be read (or skipped + * over) from this input stream without blocking. + * @exception IOException if an I/O error occurs. + */ + @Override + public int available() throws IOException { + return (iis.isCached()) ? // + (int)Math.min(Integer.MAX_VALUE, iis.length() - iis.getStreamPosition()) : + 0; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * This + * method simply performs in.close(). + * + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public void close() throws IOException { + iis.close(); + } + + /** + * Marks the current position in this input stream. A subsequent + * call to the reset method repositions this stream at + * the last marked position so that subsequent reads re-read the same bytes. + *

+ * The readlimit argument tells this input stream to + * allow that many bytes to be read before the mark position gets + * invalidated. + *

+ * This method simply performs in.mark(readlimit). + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream#reset() + */ + @Override + public synchronized void mark(int readlimit) { + iis.mark(); + } + + /** + * Repositions this stream to the position at the time the + * mark method was last called on this input stream. + *

+ * This method + * simply performs in.reset(). + *

+ * Stream marks are intended to be used in + * situations where you need to read ahead a little to see what's in + * the stream. Often this is most easily done by invoking some + * general parser. If the stream is of the type handled by the + * parse, it just chugs along happily. If the stream is not of + * that type, the parser should toss an exception when it fails. + * If this happens within readlimit bytes, it allows the outer + * code to reset the stream and try another parser. + * + * @exception IOException if the stream has not been marked or if the + * mark has been invalidated. + * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream#mark(int) + */ + @Override + public synchronized void reset() throws IOException { + iis.reset(); + } + + /** + * Tests if this input stream supports the mark + * and reset methods. + * This method + * simply performs in.markSupported(). + * + * @return true if this stream type supports the + * mark and reset method; + * false otherwise. + * @see java.io.FilterInputStream#in + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java new file mode 100644 index 000000000..501224398 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java @@ -0,0 +1,65 @@ +/* + * @(#)ImageInputStreamImpl2.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.io; + +import java.io.IOException; +import java.nio.ByteOrder; +import javax.imageio.stream.ImageInputStreamImpl; + +/** + * {@code ImageInputStreamImpl2} fixes bugs in ImageInputStreamImpl. + *

+ * ImageInputStreamImpl uses read(byte[]) instead of readFully(byte[]) inside of + * readShort. This results in corrupt data input if the underlying stream can + * not fulfill the read operation in a single step. + * + * @author Werner Randelshofer + * @version $Id: ImageInputStreamImpl2.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class ImageInputStreamImpl2 extends ImageInputStreamImpl { + // Length of the buffer used for readFully(type[], int, int) + private static final int BYTE_BUF_LENGTH = 8192; + /** + * Byte buffer used for readFully(type[], int, int). Note that this + * array is also used for bulk reads in readShort(), readInt(), etc, so + * it should be large enough to hold a primitive value (i.e. >= 8 bytes). + * Also note that this array is package protected, so that it can be + * used by ImageOutputStreamImpl in a similar manner. + */ + byte[] byteBuf = new byte[BYTE_BUF_LENGTH]; + + @Override + public short readShort() throws IOException { + readFully(byteBuf, 0, 2); + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return (short) + (((byteBuf[0] & 0xff) << 8) | ((byteBuf[1] & 0xff) << 0)); + } else { + return (short) + (((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0)); + } + } + public int readInt() throws IOException { + readFully(byteBuf, 0, 4); + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return + (((byteBuf[0] & 0xff) << 24) | ((byteBuf[1] & 0xff) << 16) | + ((byteBuf[2] & 0xff) << 8) | ((byteBuf[3] & 0xff) << 0)); + } else { + return + (((byteBuf[3] & 0xff) << 24) | ((byteBuf[2] & 0xff) << 16) | + ((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0)); + } + } + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java new file mode 100644 index 000000000..9f37a3522 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java @@ -0,0 +1,163 @@ +/* + * @(#)SeekableByteArrayOutputStream.java + * + * Copyright © 2010-2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ + +package org.monte.media.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import static java.lang.Math.*; +/** + * {@code SeekableByteArrayOutputStream}. + * + * @author Werner Randelshofer + * @version $Id: SeekableByteArrayOutputStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public class SeekableByteArrayOutputStream extends ByteArrayOutputStream { + + /** + * The current stream position. + */ + private int pos; + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 32 bytes, though its size increases if necessary. + */ + public SeekableByteArrayOutputStream() { + this(32); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size. + * @exception IllegalArgumentException if size is negative. + */ + public SeekableByteArrayOutputStream(int size) { + if (size < 0) { + throw new IllegalArgumentException("Negative initial size: " + + size); + } + buf = new byte[size]; + } + /** + * Creates a new byte array output stream, which reuses the supplied buffer. + */ + public SeekableByteArrayOutputStream(byte[] buf) { + this.buf = buf; + } + + /** + * Writes the specified byte to this byte array output stream. + * + * @param b the byte to be written. + */ + @Override + public synchronized void write(int b) { + int newcount = max(pos + 1, count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + buf[pos++] = (byte)b; + count = newcount; + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to this byte array output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + @Override + public synchronized void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newcount = max(pos+len,count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + System.arraycopy(b, off, buf, pos, len); + pos+=len; + count = newcount; + } + + /** + * Resets the count field of this byte array output + * stream to zero, so that all currently accumulated output in the + * output stream is discarded. The output stream can be used again, + * reusing the already allocated buffer space. + * + * @see java.io.ByteArrayInputStream#count + */ + @Override + public synchronized void reset() { + count = 0; + pos=0; + } + + /** + * Sets the current stream position to the desired location. The + * next read will occur at this location. The bit offset is set + * to 0. + * + *

An IndexOutOfBoundsException will be thrown if + * pos is smaller than the flushed position (as + * returned by getflushedPosition). + * + *

It is legal to seek past the end of the file; an + * EOFException will be thrown only if a read is + * performed. + * + * @param pos a long containing the desired file + * pointer position. + * + * @exception IndexOutOfBoundsException if pos is smaller + * than the flushed position. + * @exception IOException if any other I/O error occurs. + */ + public void seek(long pos) throws IOException { + this.pos = (int)pos; + } + + /** + * Returns the current byte position of the stream. The next write + * will take place starting at this offset. + * + * @return a long containing the position of the stream. + * + * @exception IOException if an I/O error occurs. + */ + public long getStreamPosition() throws IOException { + return pos; + } + + /** Writes the contents of the byte array into the specified output + * stream. + * @param out + */ + public void toOutputStream(OutputStream out) throws IOException { + out.write(buf, 0, count); + } + + /** Returns the underlying byte buffer. */ + public byte[] getBuffer() { + return buf; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java new file mode 100644 index 000000000..89477a7df --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java @@ -0,0 +1,151 @@ +/* + * @(#)SubImageOutputStream.java 1.0 2011-07-20 + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.io; + +import java.io.IOException; +import java.nio.ByteOrder; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; + +/** + * {@code SubImageOutputStream}. + * + * @author Werner Randelshofer + * @version 1.0 2011-07-20 Created. + */ +public class SubImageOutputStream extends ImageOutputStreamImpl { + + private ImageOutputStream out; + private long offset; + private long length; + + /** Whether flush and close request shall be forwarded to underlying stream.*/ + private boolean forwardFlushAndClose; + + public SubImageOutputStream(ImageOutputStream out, ByteOrder bo,boolean forwardFlushAndClose) throws IOException { + this(out, out.getStreamPosition(),bo,forwardFlushAndClose); + } + + public SubImageOutputStream(ImageOutputStream out, long offset, ByteOrder bo,boolean forwardFlushAndClose) throws IOException { + this.out = out; + this.offset = offset; + this.forwardFlushAndClose=forwardFlushAndClose; + setByteOrder(bo); + out.seek(offset); + } + + private long available() throws IOException { + checkClosed(); + long pos = out.getStreamPosition(); + if (pos < offset) { + out.seek(offset); + pos = offset; + } + return offset + out.length() - pos; + } + + @Override + public int read() throws IOException { + if (available() <= 0) { + return -1; + } else { + return out.read(); + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + long av = available(); + if (av <= 0) { + return -1; + } else { + int result = out.read(b, off, (int) Math.min(len, av)); + return result; + } + } + + @Override + public long getStreamPosition() throws IOException { + return out.getStreamPosition() - offset; + } + + @Override + public void seek(long pos) throws IOException { + out.seek(pos + offset); + length=Math.max(pos-offset+1,length); + } + + @Override + public void flush() throws IOException { + if (forwardFlushAndClose) { + out.flush(); + } + } + + @Override + public void close() throws IOException { + if (forwardFlushAndClose) { + super.close(); + } + } + + @Override + public long getFlushedPosition() { + return out.getFlushedPosition() - offset; + } + + /** + * Default implementation returns false. Subclasses should + * override this if they cache data. + */ + @Override + public boolean isCached() { + return out.isCached(); + } + + /** + * Default implementation returns false. Subclasses should + * override this if they cache data in main memory. + */ + @Override + public boolean isCachedMemory() { + return out.isCachedMemory(); + } + + @Override + public boolean isCachedFile() { + return out.isCachedFile(); + } + + @Override + public long length() { + return length; + } + + @Override + public void write(int b) throws IOException { + out.write(b); + length = Math.max(out.getStreamPosition()-offset,length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b,off,len); + length = Math.max(out.getStreamPosition()-offset,length); + } + + public void dispose() throws IOException { + if (forwardFlushAndClose) { + checkClosed(); + } + out=null; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java new file mode 100644 index 000000000..67ed7c1ba --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java @@ -0,0 +1,155 @@ +/* + * @(#)JPGCodec.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.jpeg; + +import org.monte.media.io.ByteArrayImageInputStream; +import javax.imageio.ImageReader; +import org.monte.media.Format; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.io.ByteArrayImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code JPEGCodec} encodes a BufferedImage as a byte[] array. + *

+ * Supported input formats: + *

    + * {@code VideoFormat} with {@code BufferedImage.class}, any width, any height, + * any depth. + *
+ * Supported output formats: + *
    + * {@code VideoFormat} with {@code byte[].class}, same width and height as input + * format, depth=24. + *
+ * + * @author Werner Randelshofer + * @version $Id: JPEGCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class JPEGCodec extends AbstractVideoCodec { + + public JPEGCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_QUICKTIME_JPEG,// + CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_JPEG, // + DataClassKey, byte[].class, DepthKey, 24), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, DataClassKey, byte[].class, DepthKey, 24), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME,// + EncodingKey, ENCODING_QUICKTIME_JPEG,// + CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_JPEG, // + DataClassKey, byte[].class, DepthKey, 24), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, DataClassKey, byte[].class, DepthKey, 24), // + }// + ); + name = "JPEG Codec"; + } + + @Override + public int process(Buffer in, Buffer out) { + if (outputFormat.get(EncodingKey).equals(ENCODING_BUFFERED_IMAGE)) { + return decode(in, out); + } else { + return encode(in, out); + } + } + + public int encode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + BufferedImage image = getBufferedImage(in); + if (image == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + ByteArrayImageOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new ByteArrayImageOutputStream((byte[]) out.data); + } else { + tmp = new ByteArrayImageOutputStream(); + } + + try { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + float quality = outputFormat.get(QualityKey, 1f); + iwParam.setCompressionQuality(quality); + iw.setOutput(tmp); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + + out.sampleCount = 1; + out.setFlag(KEYFRAME); + out.data = tmp.getBuffer(); + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } + + public int decode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + byte[] data = (byte[]) in.data; + if (data == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + ByteArrayImageInputStream tmp = new ByteArrayImageInputStream(data); + + try { + // ImageReader ir = (ImageReader) ImageIO.getImageReadersByMIMEType("image/jpeg").next(); + ImageReader ir = new MJPGImageReader(new MJPGImageReaderSpi()); + ir.setInput(tmp); + out.data = ir.read(0); + ir.dispose(); + + out.sampleCount = 1; + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java new file mode 100644 index 000000000..f70ecc3f6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java @@ -0,0 +1,119 @@ +/* + * @(#)MJPGImageReader.java + * + * Copyright (c) 2010-2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.jpeg; + +import org.monte.media.avi.AVIBMPDIB; +import com.sun.imageio.plugins.jpeg.JPEGImageReader; +import java.awt.image.BufferedImage; +import java.awt.image.DirectColorModel; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedList; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +/** + * Reads an image in the Motion JPEG (MJPG) format. + *

. + * This class can read Motion JPEG files with omitted Huffmann table. + *

+ * For more information see: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + * + * @author Werner Randelshofer + * @version $Id: MJPGImageReader.java 299 2013-01-03 07:40:18Z werner $ + */ +public class MJPGImageReader extends ImageReader { + + private static DirectColorModel RGB = new DirectColorModel(24, 0xff0000, 0xff00, 0xff, 0x0); + /** When we read the header, we read the whole image. */ + private BufferedImage image; + + public MJPGImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return 1; + } + + @Override + public int getWidth(int imageIndex) throws IOException { + readHeader(); + return image.getWidth(); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + readHeader(); + return image.getHeight(); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + readHeader(); + LinkedList l = new LinkedList(); + l.add(new ImageTypeSpecifier(RGB, RGB.createCompatibleSampleModel(image.getWidth(), image.getHeight()))); + return l.iterator(); + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + if (imageIndex > 0) { + throw new IndexOutOfBoundsException(); + } + readHeader(); + + return image; + } + + /** Reads the image header. + * Does nothing if the header has already been loaded. + */ + private void readHeader() throws IOException { + if (image == null) { + ImageReader r = new JPEGImageReader(getOriginatingProvider()); + Object in = getInput(); + /*if (in instanceof Buffer) { + Buffer buffer = (Buffer) in; + in=buffer.getData(); + }*/ + if (in instanceof byte[]) { + r.setInput(new MemoryCacheImageInputStream(AVIBMPDIB.prependDHTSeg((byte[]) in))); + } else if (in instanceof ImageInputStream) { + r.setInput(AVIBMPDIB.prependDHTSeg((ImageInputStream) in)); + } else { + r.setInput(AVIBMPDIB.prependDHTSeg((InputStream) in)); + } + image = r.read(0); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java new file mode 100644 index 000000000..df9428f99 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java @@ -0,0 +1,88 @@ +/* + * @(#)MJPGImageReaderSpi.java + * + * Copyright (c) 2010-2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.jpeg; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + + +/** + * ImageIO service provider interface for images in the Motion JPEG (MJPG) + * format. + *

+ * The reader described by this class can read Motion JPEG files with omitted + * Huffmann table. + *

+ * For more information see: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + + * + * @author Werner Randelshofer + * @version $Id: MJPGImageReaderSpi.java 299 2013-01-03 07:40:18Z werner $ + */ +public class MJPGImageReaderSpi extends ImageReaderSpi { + + public MJPGImageReaderSpi() { + super("Werner Randelshofer",//vendor name + "1.0",//version + new String[]{"MJPG"},//names + new String[]{"mjpg"},//suffixes, + new String[]{"image/mjpg"},// MIMETypes, + "org.monte.media.jmf.renderer.video.MJPGImageReader",// readerClassName, + new Class[]{ImageInputStream.class,InputStream.class,byte[].class/*,javax.media.Buffer.class*/},// inputTypes, + null,// writerSpiNames, + false,// supportsStandardStreamMetadataFormat, + null,// nativeStreamMetadataFormatName, + null,// nativeStreamMetadataFormatClassName, + null,// extraStreamMetadataFormatNames, + null,// extraStreamMetadataFormatClassNames, + false,// supportsStandardImageMetadataFormat, + null,// nativeImageMetadataFormatName, + null,// nativeImageMetadataFormatClassName, + null,// extraImageMetadataFormatNames, + null// extraImageMetadataFormatClassNames + ); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + if (source instanceof ImageInputStream) { + ImageInputStream in = (ImageInputStream) source; + in.mark(); + + // Check if file starts with a JFIF SOI magic (0xffd8=-40) + if (in.readShort() != -40) { + in.reset(); + return false; + } + in.reset(); + return true; + } + return false; + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new MJPGImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "MJPG Image Reader"; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java b/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java new file mode 100644 index 000000000..c9cfb0d0d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java @@ -0,0 +1,250 @@ +/* + * @(#)IntMath.java + * + * Copyright (c) 2002-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ + +package org.monte.media.math; + +import java.math.BigInteger; + +/** + * Utility class for integer arithmetic. + * + * @author Werner Randelshofer + * @version $Id: IntMath.java 299 2013-01-03 07:40:18Z werner $ + */ +public class IntMath { + + /** Creates a new instance of IntMath */ + public IntMath() { + } + + /** + * Returns an int whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static int gcd(int a, int b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = Math.abs(a); + b = Math.abs(b); + + while (a > 0 && b > 0) { + a = a % b; + if (a > 0) b = b % a; + } + return a + b; + } + /** + * Returns a long whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static long gcd(long a, long b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = Math.abs(a); + b = Math.abs(b); + + while (a > 0 && b > 0) { + a = a % b; + if (a > 0) b = b % a; + } + return a + b; + } + /** + * Returns a long whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static BigInteger gcd(BigInteger a, BigInteger b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = a.abs(); + b = b.abs(); + + while (a.compareTo(BigInteger.ZERO) > 0 && b.compareTo(BigInteger.ZERO) > 0) { + a = a.mod(b); + if (a.compareTo(BigInteger.ZERO) > 0) b = b.mod(a); + } + return a.add(b); + } + + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static int scm(int a, int b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a == 0 || b == 0) return 0; + + a = Math.abs(a); + b = Math.abs(b); + + int u = a; + int v = b; + + while (a != b) { + if (a < b) { + b -= a; + v += u; + } else { + a -= b; + u += v; + } + } + + + //return a; // gcd + return (u + v) / 2; // scm + } + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static long scm(long a, long b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a == 0 || b == 0) return 0; + + a = Math.abs(a); + b = Math.abs(b); + if (b==1)return a; + if (a==1)return b; + + long u = a; + long v = b; + + // FIXME - Handle overflow + while (a != b) { + if (a < b) { + b -= a; + v += u; + } else { + a -= b; + u += v; + } + } + + + //return a; // gcd + return (u + v) / 2; // scm + } + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static BigInteger scm(BigInteger a, BigInteger b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a.compareTo(BigInteger.ZERO) == 0 || b.compareTo(BigInteger.ZERO) == 0) { + return BigInteger.ZERO; + } + + a = a.abs(); + b = b.abs(); + if (b.compareTo(BigInteger.ONE)==0)return a; + if (a.compareTo(BigInteger.ONE)==0)return b; + + BigInteger u = a; + BigInteger v = b; + + // FIXME - Handle overflow + while (a.compareTo(b) != 0) { + if (a .compareTo( b)<0) { + b = b.subtract(a); + v = v.add(u); + } else { + a = a.subtract(b); + u = u.add(v); + } + } + + + //return a; // gcd + return (u.add(v)).divide(BigInteger.valueOf(2)); // scm + } + + /** + * Reverses all 32 bits of the provided integer value. + */ + public static int reverseBits(int a) { + return reverseBits(a, 32); + } + /** + * Reverses specified number of bits of the provided integer value. + * @param a The number. + * @param numBits The number of bits (must be between 1 and 32). + */ + public static int reverseBits(int a, int numBits) { + int b = 0; + for (int i=0; i < numBits; i++) { + b <<= 1; + b |= (a & 1); + a >>>= 1; + } + return b; + + } + + public static void main(String[] args) { + for (int i=0; i < 8; i++) { + int a = 1< Two LONGs 32-bit (4-byte) unsigned + * integer: the first represents the numerator of a fraction; the second, the + * denominator.

Invariants:

  • denominator>=0, the + * denominator is always a positive integer
  • 0/1 is the unique + * representation of 0.
  • 1/0,-1/0 are the unique representations of + * infinity.
+ * + * @author Werner Randelshofer + * @version $Id: Rational.java 299 2013-01-03 07:40:18Z werner $ + */ +public class Rational extends Number { + + public static final Rational ONE = new Rational(1, 1,false); + public static final Rational ZERO = new Rational(0, 1,false); + public static final long serialVersionUID = 1L; + private final long num; + private final long den; + + public Rational(long numerator) { + this(numerator, 1); + } + + public Rational(long numerator, long denominator) { + this(numerator, denominator, true); + } + + private Rational(long numerator, long denominator, boolean reduceFraction) { + if (numerator == 0) { + // Invariant: 0/1 is unique representation of 0 + denominator = 1; + } + + if (denominator == 0) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numerator = (numerator > 0) ? 1 : -1; + } else if (denominator < 0) { + // Invariant: denominator is always positive + denominator = -denominator; + numerator = -numerator; + } + + if (reduceFraction) { + long g = gcd(numerator, denominator); + num = numerator / g; + den = denominator / g; + } else { + num = numerator; + den = denominator; + } + } + + private Rational(BigInteger numerator, BigInteger denominator, boolean reduceFraction) { + if (numerator.equals(BigInteger.ZERO)) { + // Invariant: 0/1 is unique representation of 0 + denominator = BigInteger.ONE; + } + + if (denominator.equals(BigInteger.ZERO)) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numerator = (numerator.compareTo(BigInteger.ZERO) > 0) ? BigInteger.ONE : BigInteger.ONE.negate(); + } else if (denominator.compareTo(BigInteger.ZERO) < 0) { + // Invariant: denominator is always positive + denominator = denominator.negate(); + numerator = numerator.negate(); + } + + BigInteger numB, denB; + if (reduceFraction) { + BigInteger g = gcd(numerator, denominator); + numB = numerator.divide(g); + denB = denominator.divide(g); + } else { + numB = numerator; + denB = denominator; + } + int bitLength = Math.max(numB.bitLength(), denB.bitLength()); + if (bitLength > 63) { + numB = numB.shiftRight(bitLength - 63); + denB = denB.shiftRight(bitLength - 63); + if (numB.equals(BigInteger.ZERO)) { + // Invariant: 0/1 is unique representation of 0 + denB = BigInteger.ONE; + } + + if (denB.equals(BigInteger.ZERO)) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numB = (numB.compareTo(BigInteger.ZERO) > 0) ? BigInteger.ONE : BigInteger.ONE.negate(); + + } + } + num = numB.longValue(); + den = denB.longValue(); + } + + public Rational(Rational r) { + this(r.num, r.den); + } + + public long getNumerator() { + return num; + } + + public long getDenominator() { + return den; + } + + public Rational add(Rational that) { + return add(that, true); + } + + private Rational add(Rational that, boolean reduceFraction) { + if (this.den == that.den) { + // => same denominator: add numerators + return new Rational(this.num + that.num, this.den, reduceFraction); + } + + // FIXME - handle overflow + long s = scm(this.den, that.den); + Rational result = new Rational( + this.num * (s / this.den) + that.num * (s / that.den), + s, reduceFraction); + + return result; + } + + /** + * Warning. Rational is supposed to be immutable. * + * + * private Rational addAssign(Rational that) { if (this.den == that.den) { + * // => same denominator: add numerators this.num += that.num; return this; + * } + * + * // FIXME - handle overflow long s = scm(this.den, that.den); this.num = + * this.num * (s / this.den) + that.num * (s / that.den); this.den = s; + * + * + * return reduceAssign(); } + */ + public Rational subtract(Rational that) { + return add(that.negate()); + } + + public Rational negate() { + return valueOf(-num, den); + } + + public Rational inverse() { + return valueOf(den, num, false); + } + + /** + * Returns the closest rational with the specified denominator which is + * smaller or equal than this number. + */ + public Rational floor(long d) { + if (d == den) { + return valueOf(num, den); + } + long s = scm(this.den, d); + + if (s == d) { + return valueOf(num * s / den, d); + } else if (s == den) { + return valueOf(num * d / den, d); + } else { + return valueOf(num * d / den, d); + } + } + + /** + * Returns the closest rational with the specified denominator which is + * greater or equal than this number. + */ + public Rational ceil(long d) { + if (d == den) { + return valueOf(num, den); + } + long s = scm(this.den, d); + + if (s == d) { + return valueOf((num * s + den - 1) / den, d); + } else if (s == den) { + return valueOf((num * d + den - 1) / den, d); + } else { + return valueOf((num * d + den - 1) / den, d); + } + } + + public Rational multiply(Rational that) { + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + return valueOf(this.num * that.num, + this.den * that.den); + } else { + return new Rational( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.num)), + BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.den)), + true); + } + } + + public Rational multiply(long integer) { + if (integer==0) { + return ZERO; + } else if (this.den % integer == 0) { + return valueOf( + this.num, + this.den / integer); + } else if (abs(this.num) < Integer.MAX_VALUE + && abs(integer) < Integer.MAX_VALUE) { + return valueOf( + this.num * integer, + this.den); + } else { + return new Rational( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(integer)), + BigInteger.valueOf(this.den), true); + } + } + + public Rational divide(Rational that) { + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + return valueOf(this.num * that.den, + this.den * that.num); + } else { + return valueOf( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.den)), + BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.num)), + true); + } + } + + @Override + public String toString() { + //long gcd = IntMath.gcd(num, den); + if (num == 0) { + return "0"; + } else if (den == 1) { + return Long.toString(num); + } else { + return num + "/" + den; + /* + } else { + return Float.toString((float) num / den); + */ + } + } + + public String toDescriptiveString() { + long gcd = IntMath.gcd(num, den); + if (gcd == 0 || num == 0) { + return num + "/" + den + " = " + 0; + } else if (gcd == den) { + return num + "/" + den + " = " + Long.toString(num / den); + } else { + return num + "/" + den + " ≈ " + ((float) num / den); + } + } + + @Override + public int intValue() { + return (int) (num / den); + } + + @Override + public long longValue() { + return num / den; + } + + @Override + public float floatValue() { + return (float) num / (float) den; + } + + @Override + public double doubleValue() { + return (double) num / (double) den; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Rational that = (Rational) obj; + + return compareTo(that) == 0; + } + + /** + * return { -1, 0, +1 } if a < b, a = b, or a > b. + */ + public int compareTo(Rational that) { + // The following code avoids BigInteger allocation if the denominators + // are equal + if (this.den == that.den) { + if (this.num < that.num) { + return -1; + } else if (this.num > that.num) { + return 1; + } else { + return 0; + } + } + + // Work with longs if overflow can not occur + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + long lhs = this.num * that.den; + long rhs = this.den * that.num; + if (lhs < rhs) { + return -1; + } else if (lhs > rhs) { + return 1; + } else { + return 0; + } + } + + // Use big integers to avoid overflows + BigInteger lhs; + BigInteger rhs; + lhs = BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.den)); + rhs = BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.num)); + + return lhs.compareTo(rhs); + } + + @Override + public int hashCode() { + return (int) ((num ^ (num >>> 32)) + ^ (den ^ (den >>> 32))); + + } + + public static Rational max(Rational a, Rational b) { + return (a.compareTo(b) >= 0) ? a : b; + } + + public static Rational min(Rational a, Rational b) { + return (a.compareTo(b) <= 0) ? a : b; + } + + public boolean isZero() { + return num == 0; + } + + public boolean isLessOrEqualZero() { + return num <= 0; + } + + public static Rational valueOf(double d) { + if (d == 0) { + return valueOf(0, 1); + } + if (abs(d) > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Value " + d + " is too big."); + } + if (Double.isInfinite(d)) { + return valueOf((long) signum(d), 0); + } + if (Double.isNaN(d)) { + return valueOf(0, 1); // no way to express a NaN :-( + } + return toRational(d, Integer.MAX_VALUE, 100); + } + + public static Rational valueOf(long num, long den) { + return valueOf(num, den, true); + } + + private static Rational valueOf(long num, long den, boolean reduceFraction) { + if (num == den) { + return ONE; + } + if (num == 0) { + return ZERO; + } + return new Rational(num, den, reduceFraction); + } + + public static Rational valueOf(BigInteger num, BigInteger den) { + return valueOf(num, den, true); + } + + private static Rational valueOf(BigInteger num, BigInteger den, boolean reduceFraction) { + if (num.equals(den)) { + return ONE; + } + if (num.equals(BigInteger.ZERO)) { + return ZERO; + } + return new Rational(num, den, reduceFraction); + } + + /** + * Iteratively computes rational from double.

Reference:
+ * http://www2.fz-juelich.de/video/cpp/html/exercises/exercise/Rational_cpp.html + *

+ */ + private static Rational toRational(double x, double limit, int iterations) { + double intpart = Math.floor(x); + double fractpart = x - intpart; + double d = 1.0 / fractpart; + long left = (long) intpart; + if (d > limit || iterations == 0) { + return valueOf(left, 1, false); + } else { + return valueOf(left, 1, false).add(toRational(d, limit * 0.1, iterations - 1).inverse(), false); + } + } + + public Rational round(long d) { + if (d == den) { + return valueOf(num, den); + } + + Rational fl = floor(d); + Rational diffFl = subtract(fl); + + if (diffFl.isZero()) { + return fl; + } + + Rational cl = ceil(d); + Rational diffCl = subtract(cl); + if (diffCl.isZero()) { + return cl; + } + + if (diffFl.isNegative()) { + diffFl = diffFl.negate(); + } + if (diffCl.isNegative()) { + diffCl = diffCl.negate(); + } + return diffFl.compareTo(diffCl) <= 0 ? fl : cl; + } + + private boolean isNegative() { + return num < 0; + } + + /** + * Parses a string. + * + * A rational can be represented in the following ways:
  • As a long + * number
  • As a double number
  • As an integer/integer + * rational number
  • + * + * @throws NumberFormatException if str can not be parsed. + */ + public static Rational valueOf(String str) { + int p = str.indexOf('/'); + if (p != -1) { + return valueOf(Long.valueOf(str.substring(0, p)), Long.valueOf(str.substring(p + 1))); + } + try { + return valueOf(Long.valueOf(str)); + } catch (NumberFormatException e) { + return valueOf(Double.valueOf(str)); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java b/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java new file mode 100644 index 000000000..4da926542 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java @@ -0,0 +1,118 @@ +/* + * @(#)PNGCodec.java + * + * Copyright (c) 2011-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.png; + +import org.monte.media.Format; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.io.ByteArrayImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code PNGCodec} encodes a BufferedImage as a byte[] array.. + *

    + * Supported input formats: + *

      + * {@code VideoFormat} with {@code BufferedImage.class}, any width, any height, + * any depth. + *
    + * Supported output formats: + *
      + * {@code VideoFormat} with {@code byte[].class}, same width and height as input + * format, depth=24. + *
    + * + * @author Werner Randelshofer + * @version $Id: PNGCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class PNGCodec extends AbstractVideoCodec { + + public PNGCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + DepthKey, 24, + EncodingKey, ENCODING_QUICKTIME_PNG, DataClassKey, byte[].class), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + DepthKey, 24, + EncodingKey, ENCODING_AVI_PNG, DataClassKey, byte[].class), // + }); + } + + @Override + public Format setOutputFormat(Format f) { + String mimeType = f.get(MimeTypeKey, MIME_QUICKTIME); + if (mimeType != null && !mimeType.equals(MIME_AVI)) { + return super.setOutputFormat( + f.prepend(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_QUICKTIME_PNG, DataClassKey, + byte[].class, DepthKey, 24)); + } else { + return super.setOutputFormat( + f.prepend(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_PNG, DataClassKey, + byte[].class, DepthKey, 24)); + } + } + + @Override + public int process(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + BufferedImage image = getBufferedImage(in); + if (image == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + + ByteArrayImageOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new ByteArrayImageOutputStream((byte[]) out.data); + } else { + tmp = new ByteArrayImageOutputStream(); + } + + try { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + iw.setOutput(tmp); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + + out.setFlag(KEYFRAME); + out.header = null; + out.data = tmp.getBuffer(); + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java new file mode 100644 index 000000000..67843205b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java @@ -0,0 +1,188 @@ +/* + * @(#)RIFFChunk.java + * + * Copyright (c) 2005-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.riff; + +import java.util.*; +/** + * RIFF Chunks form the building blocks of a RIFF file. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version $Id: RIFFChunk.java 299 2013-01-03 07:40:18Z werner $ + */ +public class RIFFChunk { + private int id; + private int type; + private long size; + private long scan; + private byte[] data; + private Hashtable propertyChunks; + private ArrayList collectionChunks; + /** + * This is used to display parser messages, when the parser encounters and + * error while parsing the chunk. + */ + private String parserMessage; + + public RIFFChunk(int type, int id) { + this.id = id; + this.type = type; + size = -1; + scan = -1; + } + public RIFFChunk(int type, int id, long size, long scan) { + this.id = id; + this.type = type; + this.size = size; + this.scan = scan; + } + public RIFFChunk(int type, int id, long size, long scan, RIFFChunk propGroup) { + this.id = id; + this.type = type; + this.size = size; + this.scan = scan; + if (propGroup != null) { + if (propGroup.propertyChunks != null) { + propertyChunks = new Hashtable(propGroup.propertyChunks); + } + if (propGroup.collectionChunks != null) { + collectionChunks = new ArrayList(propGroup.collectionChunks); + } + } + } + + /** + * @return ID of chunk. + */ + public int getID() { + return id; + } + + /** + * @return Type of chunk. + */ + public int getType() { + return type; + } + + /** + * @return Size of chunk. + */ + public long getSize() { + return size; + } + + /** + * @return Scan position of chunk within the file. + */ + public long getScan() { + return scan; + } + + public void putPropertyChunk(RIFFChunk chunk) { + if (propertyChunks == null) { + propertyChunks = new Hashtable (); + } + propertyChunks.put(chunk,chunk); + } + + public RIFFChunk getPropertyChunk(int id) { + if (propertyChunks == null) { + return null; + } + RIFFChunk chunk = new RIFFChunk(type, id); + return propertyChunks.get(chunk); + } + + public Enumeration propertyChunks() { + if (propertyChunks == null) { + propertyChunks = new Hashtable (); + } + return propertyChunks.keys(); + } + + public void addCollectionChunk(RIFFChunk chunk) { + if (collectionChunks == null) { + collectionChunks = new ArrayList(); + } + collectionChunks.add(chunk); + } + + public RIFFChunk[] getCollectionChunks(int id) { + if (collectionChunks == null) { + return new RIFFChunk[0]; + } + Iterator enm = collectionChunks.iterator(); + int i = 0; + while ( enm.hasNext() ) { + RIFFChunk chunk = enm.next(); + if (chunk.id==id) { + i++; + } + } + RIFFChunk[] array = new RIFFChunk[i]; + i = 0; + enm = collectionChunks.iterator(); + while ( enm.hasNext() ) { + RIFFChunk chunk = enm.next(); + if (chunk.id==id) { + array[i++] = chunk; + } + } + return array; + } + public Iterator collectionChunks() { + if (collectionChunks == null) { + collectionChunks = new ArrayList(); + } + return collectionChunks.iterator(); + } + + /** + * Sets the data. + * Note: The array will not be cloned. + */ + public void setData(byte[] data) { + this.data = data; + } + /** + * Gets the data. + * Note: The array will not be cloned. + */ + public byte[] getData() { + return data; + } + + @Override + public boolean equals(Object another) { + if (another instanceof RIFFChunk) { + RIFFChunk that = (RIFFChunk) another; + return (that.id==this.id) && (that.type==this.type); + } + return false; + } + + @Override + public int hashCode() { + return id; + } + + public void setParserMessage(String newValue) { + this.parserMessage = newValue; + } + public String getParserMessage() { + return this.parserMessage; + } + + @Override + public String toString() { + return super.toString()+"{"+RIFFParser.idToString(getType())+","+RIFFParser.idToString(getID())+"}"; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java new file mode 100644 index 000000000..dd7529186 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java @@ -0,0 +1,841 @@ +/* + * @(#)RIFFParser.java 1.5 2012-08-03 + * + * Copyright (c) 2005-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.riff; + +import org.monte.media.AbortException; +import org.monte.media.ParseException; +import org.monte.media.io.ImageInputStreamAdapter; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.WeakHashMap; +import javax.imageio.stream.ImageInputStream; + +/** + * Interprets Resource Interchange File Format (RIFF) streams. + * + *

    Abstract + *

    + * RIFF File Format + * A RIFF file consists of a RIFF header followed by zero or more lists and chunks. + *

    + * The RIFF header has the following form: + *

    + * 'RIFF' fileSize fileType (data)
    + * 
    + * where 'RIFF' is the literal FOURCC code 'RIFF', fileSize is a 4-byte value + * giving the size of the data in the file, and fileType is a FOURCC that + * identifies the specific file type. The value of fileSize includes the size + * of the fileType FOURCC plus the size of the data that follows, but does not + * include the size of the 'RIFF' FOURCC or the size of fileSize. The file data + * consists of chunks and lists, in any order. + *

    + * FOURCCs
    + * A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The RIFF file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and other + * information. + *

    + * A chunk has the following form: + *

    + * ckID ckSize ckData
    + * 
    + * where ckID is a FOURCC that identifies the data contained in the chunk, + * ckData is a 4-byte value giving the size of the data in ckData, and ckData is + * zero or more bytes of data. The data is always padded to nearest WORD boundary. + * ckSize gives the size of the valid data in the chunk; it does not include + * the padding, the size of ckID, or the size of ckSize. + *

    + * A list has the following form: + *

    + * 'LIST' listSize listType listData
    + * 
    + * where 'LIST' is the literal FOURCC code 'LIST', listSize is a 4-byte value + * giving the size of the list, listType is a FOURCC code, and listData consists + * of chunks or lists, in any order. The value of listSize includes the size of + * listType plus the size of listData; it does not include the 'LIST' FOURCC or + * the size of listSize. + *

    + * For more information see: + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/avirifffilereference.asp + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/aboutriff.asp + *

    + *

    Grammar for RIFF streams used by this parser + *

    + * RIFFFile    ::= 'RIFF' FormGroup
    + * 
    + * GroupChunk ::= FormGroup | ListGroup + * FormGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] } + * ListGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] } + *
    + * LocalChunk ::= DataChunk | CollectionChunk | PropertyChunk + * DataChunk ::= size [ struct ] + * CollectionChunk ::= size [ struct ] + * PropertyChunk ::= size [ struct ] + *
    + * size ::= ULONG + * GroupType ::= FourCC + * ChunkID ::= FourCC + * pad ::= (BYTE)0 + * struct ::= any C language struct built with primitive data types. + *
    + * + *

    Examples + * + *

    Traversing the raw structure of a RIFF file + *

    To traverse the file structure you must first set up a RIFFVisitor object + * that does something useful at each call to the visit method. Then create an + * instance of a RIFFParser and invoke the #interpret method. + * + *

    + * class RIFFRawTraversal
    + * .	{
    + * .	static class Visitor
    + * .	implements RIFFVisitor
    + * .		{
    + * .		...implement the visitor interface here...
    + * .		}
    + * .
    + * .	public static void main(String[] args)
    + * .		{
    + * .		try	{
    + * .			Visitor visitor = new Visitor();
    + * .			FileInputStream stream = new FileInputStream(args[0]);
    + * .			RIFFParser p = new RIFFParser();
    + * .			p.interpret(stream,visitor);
    + * .			stream.close();
    + * .			}
    + * .		catch (IOException e) { System.out.println(e); }
    + * .		catch (InterpreterException e)  { System.out.println(e); }
    + * .		catch (AbortedException e)  { System.out.println(e); }
    + * .		}
    + * .	}
    + * 
    + * + *

    Traversing the RIFF file and interpreting its content. + *

    Since RIFF files are not completely self describing (there is no information + * that helps differentiate between data chunks, property chunks and collection + * chunks) a reader must set up the interpreter with some contextual information + * before starting the interpreter. + *

    + * Once at least one chunk has been declared, the interpreter will only call the + * visitor for occurences of the declared group chunks and data chunks. The property + * chunks and the collection chunks can be obtained from the current group chunk + * by calling #getProperty or #getCollection. + *
    Note: All information the visitor can obtain during interpretation is only + * valid during the actual #visit... call. Dont try to get information about properties + * or collections for chunks that the visitor is not visiting right now. + * + *

    + * class InterpretingAnILBMFile
    + * .	{
    + * .	static class Visitor
    + * .	implements RIFFVisitor
    + * .		{
    + * .		...
    + * .		}
    + * .
    + * .	public static void main(String[] args)
    + * .		{
    + * .		try	{
    + * .			Visitor visitor = new Visitor();
    + * .			FileInputStream stream = new FileInputStream(args[0]);
    + * .			RIFFParser p = new RIFFParser();
    + * .			p.declareGroupChunk('FORM','ILBM');
    + * .			p.declarePropertyChunk('ILBM','BMHD');
    + * .			p.declarePropertyChunk('ILBM','CMAP');
    + * .			p.declareCollectionChunk('ILBM','CRNG');
    + * .			p.declareDataChunk('ILBM','BODY');
    + * .			p.interpret(stream,visitor);
    + * .			stream.close();
    + * .			}
    + * .		catch (IOException e) { System.out.println(e); }
    + * .		catch (InterpreterException e)  { System.out.println(e); }
    + * .		catch (AbortedException e)  { System.out.println(e); }
    + * .		}
    + * .	}
    + * 
    + * + * @see RIFFVisitor + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version 1.5 2012-08-03 Adds offset property. + *
    1.4 2011-08-26 Adds HashMap for stop chunks. + *
    1.3 2011-01-23 Use HashMap instead of Hashtable. + *
    1.2 2010-07-06 Use integer IDs for efficiency. Added support for + * stop chunks. + *
    1.0 2005-01-16 Created. + */ +public class RIFFParser extends Object { +private final static boolean DEBUG =false; + /** ID for FormGroupExpression. */ + public final static int RIFF_ID = stringToID("RIFF"); + /** ID for ListGroupExpression. */ + public final static int LIST_ID = stringToID("LIST"); + /** ID for NULL chunks. */ + public final static int NULL_ID = stringToID(" "); + /** ID for NULL chunks. */ + public final static int NULL_NUL_ID = stringToID("\0\0\0\0"); + /** ID for JUNK chunks. */ + public final static int JUNK_ID = stringToID("JUNK"); + /** The visitor traverses the parse tree. */ + private RIFFVisitor visitor; + /** List of data chunks the visitor is interested in. */ + private HashSet dataChunks; + /** List of property chunks the visitor is interested in. */ + private HashSet propertyChunks; + /** List of collection chunks the visitor is interested in. */ + private HashSet collectionChunks; + /** List of stop chunks the visitor is interested in. */ + private HashSet stopChunkTypes; + /** List of group chunks the visitor is interested in. */ + private HashSet groupChunks; + /** Reference to the input stream. */ + private RIFFPrimitivesInputStream in; + /** Reference to the image input stream. */ + private ImageInputStream iin; + + /** Whether we stop at all chunks. */ + private boolean isStopChunks; + + /** Stream offset. */ + private long streamOffset; + + /* ---- constructors ---- */ + /** + * Constructs a new RIFF parser. + */ + public RIFFParser() { + } + + public long getStreamOffset() { + return streamOffset; + } + + public void setStreamOffset(long offset) { + this.streamOffset = offset; + } + + + + /* ---- accessor methods ---- */ + /* ---- action methods ---- */ + /** + * Interprets the RIFFFile expression located at the + * current position of the indicated InputStream. + * Lets the visitor traverse the RIFF parse tree during + * interpretation. + * + *

    Pre condition + *

  • Data-, property- and collection chunks must have been + * declared prior to this call. + *
  • When the client never declared chunks, then all local + * chunks will be interpreted as data chunks. + *
  • The stream must be positioned at the beginning of the + * RIFFFileExpression. + * + *

    Post condition + *

  • When no exception was thrown then the stream is positioned + * after the RIFFFile expression. + * + *

    Obligation + * The visitor may throw an ParseException or an + * AbortException during tree traversal. + * + * @exception ParseException + * Is thrown when an interpretation error occured. + * The stream is positioned where the error occured. + * @exception AbortException + * Is thrown when the visitor decided to abort the + * interpretation. + */ + public long parse(InputStream in, RIFFVisitor v) + throws ParseException, AbortException, IOException { + this.in = new RIFFPrimitivesInputStream(in); + visitor = v; + parseFile(); + return getScan(this.in); + } + + public long parse(ImageInputStream in, RIFFVisitor v) + throws ParseException, AbortException, IOException { + return parse(new ImageInputStreamAdapter(in), v); + } + + /** + * Parses a RIFF file. + * + *

    +     * RIFF = 'RIFF' FormGroup
    +     * 
    + */ + private void parseFile() + throws ParseException, AbortException, IOException { + int id = in.readFourCC(); + + if (id == RIFF_ID) { + parseFORM(null); + } else if (id == JUNK_ID) { + parseLocalChunk(null,id); + } else { + if (iin!=null) { + throw new ParseException("Invalid RIFF File ID: \"" + idToString(id) +" 0x"+Integer.toHexString(id)+" near "+iin.getStreamPosition()+" 0x"+Long.toHexString(iin.getStreamPosition())); + } else { + throw new ParseException("Invalid RIFF File ID: \"" + idToString(id) +" 0x"+Integer.toHexString(id)); + } + } + } + + private long getScan(RIFFPrimitivesInputStream in) { + return in.getScan()+streamOffset; + } + + /** + * Parses a FORM group. + *
    +     * FormGroup ::= size GroupType { ChunkID LocalChunk [pad]
    +     * | 'FORM' FormGroup  [pad] }
    +     * | 'LIST' ListGroup  [pad] }
    +     * 
    + */ + private void parseFORM(HashMap props) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long offset = getScan(in); + int type = in.readFourCC(); +if (DEBUG)System.out.println("RIFFParser.parseForm "+idToString(type)); + if (!isGroupType(type)) { + throw new ParseException("Invalid FORM Type: \"" + idToString(type) + "\""); + } + + RIFFChunk propGroup = (props == null) ? null : (RIFFChunk) props.get(type); + RIFFChunk chunk = new RIFFChunk(type, RIFF_ID, size, offset, propGroup); + + boolean visitorWantsToEnterGroup = false; + if (isGroupChunk(chunk) && (visitorWantsToEnterGroup = visitor.enteringGroup(chunk))) { + visitor.enterGroup(chunk); + } + + try { + long finish = offset + size; + while (getScan(in) < finish) { + long idscan = getScan(in); + int id = in.readFourCC(); + + if (id == RIFF_ID) { + parseFORM(props); + } else if (id == LIST_ID) { + parseLIST(props); + } else if (isLocalChunkID(id)) { + parseLocalChunk(chunk, id); + } else { + ParseException pex = new ParseException("Invalid Chunk: \"" + id + "\" at offset:" + idscan); + chunk.setParserMessage(pex.getMessage()); + throw pex; + } + + in.align(); + } + } catch (EOFException e) { + e.printStackTrace(); + chunk.setParserMessage( + "Unexpected EOF after " + + NumberFormat.getInstance().format(getScan(in) - offset) + + " bytes"); + } finally { + if (visitorWantsToEnterGroup) { + visitor.leaveGroup(chunk); + } + } + } + + /** + * Parses a LIST group. + *
    +     * ListGroup ::= size GroupType { ChunkID LocalChunk [pad] | 'LIST ' ListGroup  [pad] }
    +     * 
    + */ + private void parseLIST(HashMap props) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long scan = getScan(in); + int type = in.readFourCC(); +if (DEBUG)System.out.println("RIFFParser.parseLIST "+idToString(type)); + + if (!isGroupType(type)) { + throw new ParseException("Invalid LIST Type: \"" + type + "\""); + } + + RIFFChunk propGroup = (props == null) ? null : (RIFFChunk) props.get(type); + RIFFChunk chunk = new RIFFChunk(type, LIST_ID, size, scan, propGroup); + + boolean visitorWantsToEnterGroup = false; + if (isGroupChunk(chunk) && (visitorWantsToEnterGroup = visitor.enteringGroup(chunk))) { + visitor.enterGroup(chunk); + } + try { + if (visitorWantsToEnterGroup) { + long finish = scan + size; + while (getScan(in) < finish) { + long idscan = getScan(in); + int id = in.readFourCC(); + if (id == LIST_ID) { + parseLIST(props); + } else if (isLocalChunkID(id)) { + parseLocalChunk(chunk, id); + } else { + parseGarbage(chunk, id, finish-getScan(in), getScan(in)); + ParseException pex = new ParseException("Invalid Chunk: \"" + id + "\" at offset:" + idscan); + chunk.setParserMessage(pex.getMessage()); + //throw pex; + } + + in.align(); + } + } else { + in.skipFully(size-4); + in.align(); + } + } finally { + if (visitorWantsToEnterGroup) { + visitor.leaveGroup(chunk); + } + } + } + + /** + * Parses a local chunk. + *
    +     * LocalChunk  ::= size { DataChunk | PropertyChunk | CollectionChunk }
    +     * DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
    +     * 
    + */ + private void parseLocalChunk(RIFFChunk parent, int id) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long scan = getScan(in); +if (DEBUG)System.out.println("RIFFParser.parseLocalChunk "+idToString(id)); + RIFFChunk chunk = new RIFFChunk(parent==null?0:parent.getType(), id, size, scan); + + if (isDataChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + visitor.visitChunk(parent, chunk); + } else if (isPropertyChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.putPropertyChunk(chunk); + } else if (isCollectionChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.addCollectionChunk(chunk); + } else { + in.skipFully((int) size); + if (isStopChunks) { + visitor.visitChunk(parent, chunk); + } + } + } + /** + * This method is invoked when we encounter a parsing problem. + *
    +     * LocalChunk  ::= size { DataChunk | PropertyChunk | CollectionChunk }
    +     * DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
    +     * 
    + */ + private void parseGarbage(RIFFChunk parent, int id, long size, long scan) + throws ParseException, AbortException, IOException { + //long size = in.readULONG(); + //long scan = getScan(in); + + RIFFChunk chunk = new RIFFChunk(parent.getType(), id, size, scan); + + if (isDataChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + visitor.visitChunk(parent, chunk); + } else if (isPropertyChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.putPropertyChunk(chunk); + } else if (isCollectionChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.addCollectionChunk(chunk); + } else { + in.skipFully((int) size); + if (isStopChunk(chunk)) { + visitor.visitChunk(parent, chunk); + } + } + } + + /** + * Checks whether the ID of the chunk has been declared as a + * data chunk. + * + *

    Pre condition + *

  • Data chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return true when neither + * data chunks, property chunks nor collection chunks + * have been declared, + * + * @param chunk Chunk to be verified. + * @return True when the parameter is a data chunk. + */ + protected boolean isDataChunk(RIFFChunk chunk) { + if (dataChunks == null) { + if (collectionChunks == null && propertyChunks == null && (stopChunkTypes==null||!stopChunkTypes.contains(chunk.getType()))) { + return true; + } else { + return false; + } + } else { + return dataChunks.contains(chunk); + } + } + + /** + * Checks whether the ID of the chunk has been declared as + * a group chunk. + * + *

    Pre condition + *

  • Group chunks must have been declared before the + * interpretation has been started. + * (Otherwise the response is always true). + * + * @param chunk Chunk to be verified. + * @return True when the visitor is interested in this is a group chunk. + */ + protected boolean isGroupChunk(RIFFChunk chunk) { + if (groupChunks == null) { + return true; + } else { + return groupChunks.contains(chunk); + } + } + + /** + * Checks wether the ID of the chunk has been declared as a + * property chunk. + * + *

    Pre condition + *

  • Property chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return false when neither + * data chunks, property chunks nor collection chunks + * have been declared, + */ + protected boolean isPropertyChunk(RIFFChunk chunk) { + if (propertyChunks == null) { + return false; + } else { + return propertyChunks.contains(chunk); + } + } + + /** + * Checks wether the ID of the chunk has been declared as a + * collection chunk. + * + *

    Pre condition + *

  • Collection chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return true when neither + * data chunks, property chunks nor collection chunks + * have been declared, + * + * @param chunk Chunk to be verified. + * @return True when the parameter is a collection chunk. + */ + protected boolean isCollectionChunk(RIFFChunk chunk) { + if (collectionChunks == null) { + return false; + } else { + return collectionChunks.contains(chunk); + } + } + + /** + * Declares a data chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Data chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declareDataChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (dataChunks == null) { + dataChunks = new HashSet(); + } + dataChunks.add(chunk); + } + + /** + * Declares a FORM group chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Group chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isContentsType. + */ + public void declareGroupChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (groupChunks == null) { + groupChunks = new HashSet(); + } + groupChunks.add(chunk); + } + + /** + * Declares a property chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Group chunk declared + * + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declarePropertyChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (propertyChunks == null) { + propertyChunks = new HashSet(); + } + propertyChunks.add(chunk); + } + + /** + * Declares a collection chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Collection chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declareCollectionChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (collectionChunks == null) { + collectionChunks = new HashSet(); + } + collectionChunks.add(chunk); + } + + /** + * Declares a stop chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Stop chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + */ + public void declareStopChunkType(int type) { + if (stopChunkTypes == null) { + stopChunkTypes = new HashSet(); + } + stopChunkTypes.add(type); + } + + /** Whether the parse should stop at all chunks. + *

    + * The parser does not read the data body of stop chunks. + *

    + * By declaring stop chunks, and not declaring any data, group or + * property chunks, the file structure of a RIFF file can be quickly + * scanned through. + */ + public void declareStopChunks() { + isStopChunks = true; + } + + private boolean isStopChunk(RIFFChunk chunk) { + return isStopChunks||stopChunkTypes!=null&&stopChunkTypes.contains(chunk.getType()); + } + + /* ---- Class methods ---- */ + /** + * Checks wether the argument represents a valid RIFF GroupID. + * + *

    Validation + *

      + *
    • Group ID must be one of RIFF_ID, LIST_ID.
    • + *
    + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a valid Group ID. + */ + public static boolean isGroupID(int id) { + return id == LIST_ID || id == RIFF_ID; + } + + /** + * Checks wether the argument represents a valid RIFF Group Type. + * + *

    Validation + *

      + *
    • Must be a valid ID.
    • + *
    • Must not be a group ID.
    • + *
    • Must not be a NULL_ID.
    • + *
    + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a valid Group ID. + */ + public static boolean isGroupType(int id) { + return isID(id) && !isGroupID(id) && id != NULL_ID; + } + + /** + * Checks if the argument represents a valid RIFF ID. + * + *

    Validation + *

  • Every byte of an ID must be in the range of 0x20..0x7e + *
  • The id may not have leading spaces (unless the id is a NULL_ID). + * + * @param id Chunk ID to be checked. + * @return True when the ID is a valid IFF chunk ID. + */ + public static boolean isID(int id) { + int c0 = id >> 24; + int c1 = (id >> 16) & 0xff; + int c2 = (id >> 8) & 0xff; + int c3 = id & 0xff; + + return id == NULL_NUL_ID + || c0 >= 0x20 && c0 <= 0x7e + && c1 >= 0x20 && c1 <= 0x7e + && c2 >= 0x20 && c2 <= 0x7e + && c3 >= 0x20 && c3 <= 0x7e; + } + + /** + * Returns whether the argument is a valid Local Chunk ID. + * + *

    Validation + * + *

  • Must be valid ID.
  • + *
  • Local Chunk IDs may not collide with GroupIDs. + *
  • Must not be a NULL_ID.
  • + * + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a Local Chunk ID. + */ + public static boolean isLocalChunkID(int id) { + if (isGroupID(id)) { + return false; + } + return id != NULL_ID && isID(id); + } + private WeakHashMap ids; + + /** + * Convert an integer IFF identifier to String. + * + * @param anInt ID to be converted. + * @return String representation of the ID. + */ + public static String idToString(int anInt) { + byte[] bytes = new byte[4]; + + bytes[0] = (byte) (anInt >>> 24); + bytes[1] = (byte) (anInt >>> 16); + bytes[2] = (byte) (anInt >>> 8); + bytes[3] = (byte) (anInt >>> 0); + + try { + return new String(bytes, "ASCII"); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.getMessage()); + } + } + + /** + * Converts the first four letters of the + * String into an IFF Identifier. + * + * @param aString String to be converted. + * @return ID representation of the String. + */ + public static int stringToID(String aString) { + byte[] bytes = aString.getBytes(); + + return ((int) bytes[0]) << 24 + | ((int) bytes[1]) << 16 + | ((int) bytes[2]) << 8 + | ((int) bytes[3]) << 0; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java new file mode 100644 index 000000000..7a13dec80 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java @@ -0,0 +1,265 @@ +/* + * @(#)RIFFPrimitivesInputStream.java 1.0 2005-01-15 + * + * Copyright (c) 2005 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.riff; + +import java.io.*; + +/** + * A RIFF primitives input stream lets an application read primitive data + * types in the Microsoft Resource Interfache File Format (RIFF) format from an + * underlying input stream. + * + * Reference: + * AVI RIFF File Reference + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/avirifffilereference.asp + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version 1.0 2005-01-15 Created. + */ +public class RIFFPrimitivesInputStream extends FilterInputStream { + private long scan, mark; + + /** + * Creates a new instance. + * + * @param in the input stream. + */ + public RIFFPrimitivesInputStream(InputStream in) { + super(in); + } + + /** + * Read 1 byte from the input stream and interpret + * them as an 8 Bit unsigned UBYTE value. + */ + public int readUBYTE() + throws IOException { + int b0 = in.read(); + + if (b0 == -1) { + throw new EOFException(); + } + + scan += 1; + return b0 & 0xff; + } + /** + * Read 2 bytes from the input stream and interpret + * them as a 16 Bit signed WORD value. + */ + public short readWORD() + throws IOException { + int b0 = in.read(); + int b1 = in.read(); + + if (b1 == -1) { + throw new EOFException(); + } + scan += 2; + + return (short) (((b0 & 0xff) << 0) | ((b1 & 0xff) << 8)); + } + + /** + * Read 2 bytes from the input stream and interpret + * them as a 16 Bit unsigned UWORD value. + */ + public int readUWORD() + throws IOException { + return readWORD() & 0xffff; + } + + /** + * Read 4 bytes from the input stream and interpret + * them as a 32 Bit signed LONG value. + */ + public int readLONG() + throws IOException { + int b0 = in.read(); + int b1 = in.read(); + int b2 = in.read(); + int b3 = in.read(); + + if (b3 == -1) { + throw new EOFException(); + } + scan += 4; + + return ((b0&0xff) << 0) + + ((b1&0xff) << 8) + + ((b2&0xff) << 16) + + ((b3&0xff) << 24); + } + + /** + * Read 4 bytes from the input stream and interpret + * them as a four byte character code. + * + * Cited from Referenced "AVI RIFF File Reference": + * "A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The AVI file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and + * other information." + */ + public int readFourCC() + throws IOException { + int b3 = in.read(); + int b2 = in.read(); + int b1 = in.read(); + int b0 = in.read(); + + if (b0 == -1) { + throw new EOFException(); + } + scan += 4; + + return ((b0&0xff) << 0) + + ((b1&0xff) << 8) + + ((b2&0xff) << 16) + + ((b3&0xff) << 24); + } + /** + * Read 4 bytes from the input stream and interpret + * them as a four byte character code. + * + * Cited from Referenced "AVI RIFF File Reference": + * "A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The AVI file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and + * other information." + */ + public String readFourCCString() + throws IOException { + byte[] buf = new byte[4]; + readFully(buf, 0, 4); + //scan += 4; <- scan is updated by method readFully + return new String(buf, "ASCII"); + } + + /** + * Read 4 Bytes from the input Stream and interpret + * them as an unsigned Integer value of type ULONG. + */ + public long readULONG() + throws IOException { + return (long)(readLONG()) & 0x00ffffffff; + } + + /** + * Align to an even byte position in the input stream. + * This will skip one byte in the stream if the current + * read position is not even. + */ + public void align() + throws IOException { + if (scan % 2 == 1) { + in.skip(1); + scan++; + } + } + + /** + * Get the current read position within the file (as seen + * by this input stream filter). + */ + public long getScan() + { return scan; } + + /** + * Reads one byte. + */ + public int read() + throws IOException { + int data = in.read(); + if (data != -1) scan++; + return data; + } + /** + * Reads a sequence of bytes. + */ + public int readFully(byte[] b,int offset, int length) + throws IOException { + int count = read(b, offset, length); + if (count != length) { + throw new EOFException("readFully for "+length+" bytes, unexpected EOF after "+count+" bytes."); + } + //scan += count; <- scan is already counted by read method + return count; + } + /** + * Reads a sequence of bytes. + */ + public int read(byte[] b,int offset, int length) + throws IOException { + int count = 0; + while (count < length) { + int result = in.read(b,offset+count,length-count); + if (result == -1) break; + count += result; + } + scan += count; + return count; + } + /** + * Marks the input stream. + * @param readlimit The maximum limit of bytes that can be read before + * the mark position becomes invalid. + */ + public void mark(int readlimit) { + in.mark(readlimit); + mark = scan; + } + /** + * Repositions the stream at the previously marked position. + * + * @exception IOException If the stream has not been marked or if the + * mark has been invalidated. + */ + public void reset() + throws IOException { + in.reset(); + scan = mark; + } + /** + * Skips over and discards n bytes of data from this input stream. This skip + * method tries to skip the provided number of bytes. + */ + public long skip(long n) + throws IOException { + long skipped = in.skip(n); + scan += skipped; + return skipped; + } + /** + * Skips over and discards n bytes of data from this input stream. Throws + * + * @param n the number of bytes to be skipped. + * @exception EOFException if this input stream reaches the end before + * skipping all the bytes. + */ + public void skipFully(long n) + throws IOException { + if (n==0) return; + + int total = 0; + int cur = 0; + + while ((total 0)) { + total += cur; + } + if (cur == 0) throw new EOFException(); + scan += total; + } +} \ No newline at end of file diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java new file mode 100644 index 000000000..e4eaeabd4 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java @@ -0,0 +1,43 @@ +/* + * @(#)RIFFVIsitor.java 1.0 2005-01-09 + * + * Copyright (c) 2005 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media.riff; + +import org.monte.media.AbortException; +import org.monte.media.ParseException; + +/** + * RIFFVIsitor is notified each time the RIFFParser visits + * a data chunk and when a group is entered or leaved. + * + * @version 1.0 2005-01-09 Created. + */ +public interface RIFFVisitor { + /** This method is invoked when the parser attempts to enter a group. + * The visitor can return false, if the parse shall skip the group contents. + * + * @param group + * @return True to enter the group, false to skip over the group. + */ + public boolean enteringGroup(RIFFChunk group); + + /** This method is invoked when the parser enters a group chunk.*/ + public void enterGroup(RIFFChunk group) + throws ParseException, AbortException; + + /** This method is invoked when the parser leaves a group chunk.*/ + public void leaveGroup(RIFFChunk group) + throws ParseException, AbortException; + + /** This method is invoked when the parser has read a data chunk or + * has skipped a stop chunk.*/ + public void visitChunk(RIFFChunk group, RIFFChunk chunk) + throws ParseException, AbortException; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/util/Methods.java b/trunk/libsrc/avi/src/org/monte/media/util/Methods.java new file mode 100644 index 000000000..f5cb2bd57 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/util/Methods.java @@ -0,0 +1,463 @@ +/* + * @(#)Methods.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance with the + * license agreement you entered into with Werner Randelshofer. + * For details see accompanying license terms. + */ + +package org.monte.media.util; + +import java.lang.reflect.*; + +/** + * Methods contains convenience methods for method invocations using + * java.lang.reflect. + * + * @author Werner Randelshofer + * @version $Id: Methods.java 299 2013-01-03 07:40:18Z werner $ + */ + +@SuppressWarnings("unchecked") +public class Methods { + /** + * Prevent instance creation. + */ + private Methods() { + } + + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not + * accessible. + */ + public static Object invoke(Object obj, String methodName) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified accessible method with a string parameter if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param stringParameter The String parameter + * @return The return value of the method or METHOD_NOT_FOUND. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invoke(Object obj, String methodName, String stringParameter) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { String.class }); + Object result = method.invoke(obj, new Object[] { stringParameter }); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method or METHOD_NOT_FOUND. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName) + throws NoSuchMethodException { + try { + Method method = clazz.getMethod(methodName, new Class[0]); + Object result = method.invoke(null, new Object[0]); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName) + throws NoSuchMethodException { + try { + return invokeStatic(Class.forName(clazz), methodName); + } catch (ClassNotFoundException e) { + throw new NoSuchMethodException("class "+clazz+" not found"); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param type The parameter type. + * @param value The parameter value. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName, Class type, Object value) + throws NoSuchMethodException { + return invokeStatic(clazz,methodName,new Class[]{type},new Object[]{value}); + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName, Class[] types, Object[] values) + throws NoSuchMethodException { + try { + Method method = clazz.getMethod(methodName, types); + Object result = method.invoke(null, values); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName, + Class[] types, Object[] values) + throws NoSuchMethodException { + try { + return invokeStatic(Class.forName(clazz), methodName, types, values); + } catch (ClassNotFoundException e) { + throw new NoSuchMethodException("class "+clazz+" not found"); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @param defaultValue The default value. + * @return The return value of the method or the default value if the method + * does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName, + Class[] types, Object[] values, Object defaultValue) { + try { + return invokeStatic(Class.forName(clazz), methodName, types, values); + } catch (ClassNotFoundException e) { + return defaultValue; + } catch (NoSuchMethodException e) { + return defaultValue; + } + } + + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static int invokeGetter(Object obj, String methodName, int defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Integer) result).intValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static long invokeGetter(Object obj, String methodName, long defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Long) result).longValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static boolean invokeGetter(Object obj, String methodName, boolean defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Boolean) result).booleanValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static Object invokeGetter(Object obj, String methodName, Object defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return result; + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param clazz The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static boolean invokeStaticGetter(Class clazz, String methodName, boolean defaultValue) { + try { + Method method = clazz.getMethod(methodName, new Class[0]); + Object result = method.invoke(null, new Object[0]); + return ((Boolean) result).booleanValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, boolean newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Boolean.TYPE} ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, int newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Integer.TYPE} ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, float newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Float.TYPE} ); + return method.invoke(obj, new Object[] { new Float(newValue)}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, Class clazz, Object newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { clazz } ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, Class[] clazz, Object... newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, clazz ); + return method.invoke(obj, newValue); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + InternalError error = new InternalError(e.getMessage()); + error.initCause((e.getCause() != null) ? e.getCause() : e); + throw error; + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName) { + try { + invoke(obj, methodName); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, float newValue) { + try { + invoke(obj, methodName, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, boolean newValue) { + try { + invoke(obj, methodName, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, Class clazz, Object newValue) { + try { + invoke(obj, methodName, clazz, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExistsWithEnum(Object obj, String methodName, String enumClassName, String enumValueName) { + try { + Class enumClass = Class.forName(enumClassName); + Object enumValue = invokeStatic("java.lang.Enum", "valueOf", new Class[] {Class.class, String.class}, + new Object[] {enumClass, enumValueName} + ); + invoke(obj, methodName, enumClass, enumValue); + } catch (ClassNotFoundException e) { + // ignore + e.printStackTrace(); + } catch (NoSuchMethodException e) { + // ignore + e.printStackTrace(); + } + } +} diff --git a/trunk/libsrc/gif/build.xml b/trunk/libsrc/gif/build.xml new file mode 100644 index 000000000..ece3c2dfc --- /dev/null +++ b/trunk/libsrc/gif/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project gif. + + + diff --git a/trunk/libsrc/gif/nbproject/build-impl.xml b/trunk/libsrc/gif/nbproject/build-impl.xml new file mode 100644 index 000000000..9a4d2773d --- /dev/null +++ b/trunk/libsrc/gif/nbproject/build-impl.xml @@ -0,0 +1,1407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/trunk/libsrc/gif/nbproject/genfiles.properties b/trunk/libsrc/gif/nbproject/genfiles.properties new file mode 100644 index 000000000..d8a93fa60 --- /dev/null +++ b/trunk/libsrc/gif/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=c07cf2d0 +build.xml.script.CRC32=d45952bc +build.xml.stylesheet.CRC32=8064a381@1.68.1.46 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=c07cf2d0 +nbproject/build-impl.xml.script.CRC32=cf654385 +nbproject/build-impl.xml.stylesheet.CRC32=5a01deb7@1.68.1.46 diff --git a/trunk/libsrc/gif/nbproject/project.properties b/trunk/libsrc/gif/nbproject/project.properties new file mode 100644 index 000000000..bb6838723 --- /dev/null +++ b/trunk/libsrc/gif/nbproject/project.properties @@ -0,0 +1,71 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/gif.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.7 +javac.target=1.7 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=true +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/trunk/libsrc/gif/nbproject/project.xml b/trunk/libsrc/gif/nbproject/project.xml new file mode 100644 index 000000000..fb4075c8c --- /dev/null +++ b/trunk/libsrc/gif/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + gif + + + + + + + + + diff --git a/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java b/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java new file mode 100644 index 000000000..dda40623d --- /dev/null +++ b/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java @@ -0,0 +1,191 @@ +package net.kroo.elliot; + +/* + + Created by Elliot Kroo on 2009-04-25. + + This work is licensed under the Creative Commons Attribution 3.0 Unported + License. To view a copy of this license, visit + http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative + Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. +*/ + +import javax.imageio.*; +import javax.imageio.metadata.*; +import javax.imageio.stream.*; +import java.awt.image.*; +import java.io.*; +import java.util.Iterator; + +public class GifSequenceWriter { + protected ImageWriter gifWriter; + protected ImageWriteParam imageWriteParam; + protected IIOMetadata imageMetaData; + + /** + * Creates a new GifSequenceWriter + * + * @param outputStream the ImageOutputStream to be written to + * @param imageType one of the imageTypes specified in BufferedImage + * @param timeBetweenFramesMS the time between frames in miliseconds + * @param loopContinuously wether the gif should loop repeatedly + * @throws IIOException if no gif ImageWriters are found + * + * @author Elliot Kroo (elliot[at]kroo[dot]net) + */ + public GifSequenceWriter( + ImageOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) throws IIOException, IOException { + // my method to create a writer + gifWriter = getWriter(); + imageWriteParam = gifWriter.getDefaultWriteParam(); + ImageTypeSpecifier imageTypeSpecifier = + ImageTypeSpecifier.createFromBufferedImageType(imageType); + + imageMetaData = + gifWriter.getDefaultImageMetadata(imageTypeSpecifier, + imageWriteParam); + + String metaFormatName = imageMetaData.getNativeMetadataFormatName(); + + IIOMetadataNode root = (IIOMetadataNode) + imageMetaData.getAsTree(metaFormatName); + + IIOMetadataNode graphicsControlExtensionNode = getNode( + root, + "GraphicControlExtension"); + + graphicsControlExtensionNode.setAttribute("disposalMethod", "restoreToBackgroundColor"); //JPEXS: changed none to restoreToBackgroundColor + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute( + "transparentColorFlag", + "FALSE"); + graphicsControlExtensionNode.setAttribute( + "delayTime", + Integer.toString(timeBetweenFramesMS / 10)); + graphicsControlExtensionNode.setAttribute( + "transparentColorIndex", + "0"); + + IIOMetadataNode commentsNode = getNode(root, "CommentExtensions"); + commentsNode.setAttribute("CommentExtension", "Created by MAH"); + + IIOMetadataNode appEntensionsNode = getNode( + root, + "ApplicationExtensions"); + + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loop = loopContinuously ? 0 : 1; + + child.setUserObject(new byte[]{ 0x1, (byte) (loop & 0xFF), (byte) + ((loop >> 8) & 0xFF)}); + appEntensionsNode.appendChild(child); + + imageMetaData.setFromTree(metaFormatName, root); + + gifWriter.setOutput(outputStream); + + gifWriter.prepareWriteSequence(null); + } + + public void writeToSequence(RenderedImage img) throws IOException { + gifWriter.writeToSequence( + new IIOImage( + img, + null, + imageMetaData), + imageWriteParam); + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + public void close() throws IOException { + gifWriter.endWriteSequence(); + } + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private static ImageWriter getWriter() throws IIOException { + Iterator iter = ImageIO.getImageWritersBySuffix("gif"); + if(!iter.hasNext()) { + throw new IIOException("No GIF Image Writers Exist"); + } else { + return iter.next(); + } + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * + * @return the child node, if found or a new node created with the given name. + */ + private static IIOMetadataNode getNode( + IIOMetadataNode rootNode, + String nodeName) { + int nNodes = rootNode.getLength(); + for (int i = 0; i < nNodes; i++) { + if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) + == 0) { + return((IIOMetadataNode) rootNode.item(i)); + } + } + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + return(node); + } + + /** + public GifSequenceWriter( + BufferedOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) { + + */ + + public static void main(String[] args) throws Exception { + if (args.length > 1) { + // grab the output image type from the first image in the sequence + BufferedImage firstImage = ImageIO.read(new File(args[0])); + + // create a new BufferedOutputStream with the last argument + ImageOutputStream output = + new FileImageOutputStream(new File(args[args.length - 1])); + + // create a gif sequence with the type of the first image, 1 second + // between frames, which loops continuously + GifSequenceWriter writer = + new GifSequenceWriter(output, firstImage.getType(), 1, false); + + // write out the first image to our sequence... + writer.writeToSequence(firstImage); + for(int i=1; i src - lib/LZMA.jar;lib/jna-3.5.1.jar;lib/jpproxy.jar;lib/jsyntaxpane-0.9.5.jar;lib/trident-6.2.jar;lib/substance-flamingo-6.2.jar;lib/flamingo-6.2.jar;lib/substance-6.2.jar;lib/jl1.0.1.jar;lib/nellymoser.jar + lib/LZMA.jar;lib/jna-3.5.1.jar;lib/jpproxy.jar;lib/jsyntaxpane-0.9.5.jar;lib/trident-6.2.jar;lib/substance-flamingo-6.2.jar;lib/flamingo-6.2.jar;lib/substance-6.2.jar;lib/jl1.0.1.jar;lib/nellymoser.jar;lib/gif.jar;lib/avi.jar build javadoc reports diff --git a/trunk/src/com/jpexs/decompiler/flash/SWF.java b/trunk/src/com/jpexs/decompiler/flash/SWF.java index 9f5ff2bb8..9285d05ff 100644 --- a/trunk/src/com/jpexs/decompiler/flash/SWF.java +++ b/trunk/src/com/jpexs/decompiler/flash/SWF.java @@ -55,7 +55,9 @@ import com.jpexs.decompiler.flash.ecma.Null; import com.jpexs.decompiler.flash.exporters.ExportRectangle; import com.jpexs.decompiler.flash.exporters.Matrix; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; +import com.jpexs.decompiler.flash.exporters.modes.MorphshapeExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; import com.jpexs.decompiler.flash.exporters.modes.ShapeExportMode; @@ -78,6 +80,7 @@ import com.jpexs.decompiler.flash.tags.DoInitActionTag; import com.jpexs.decompiler.flash.tags.ExportAssetsTag; import com.jpexs.decompiler.flash.tags.FileAttributesTag; import com.jpexs.decompiler.flash.tags.JPEGTablesTag; +import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.ShowFrameTag; import com.jpexs.decompiler.flash.tags.SoundStreamBlockTag; import com.jpexs.decompiler.flash.tags.SymbolClassTag; @@ -112,6 +115,7 @@ import com.jpexs.decompiler.flash.treenodes.ContainerNode; import com.jpexs.decompiler.flash.treenodes.FrameNode; import com.jpexs.decompiler.flash.treenodes.TagNode; import com.jpexs.decompiler.flash.treenodes.TreeNode; +import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RECT; @@ -171,6 +175,12 @@ import java.util.logging.Logger; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import net.kroo.elliot.GifSequenceWriter; +import org.monte.media.VideoFormatKeys; +import org.monte.media.avi.AVIWriter; /** * Class representing SWF file @@ -1006,7 +1016,7 @@ public final class SWF implements TreeItem, Timelined { TreeNode addNode = null; if (t instanceof ShowFrameTag) { // do not add PlaceObjects (+etc) to script nodes - FrameNode tti = new FrameNode(new FrameNodeItem(t.getSwf(), frame, parent, false), null); + FrameNode tti = new FrameNode(new FrameNodeItem(t.getSwf(), frame, parent, false), null, true); for (int r = ret.size() - 1; r >= 0; r--) { if (!(ret.get(r).getItem() instanceof DefineSpriteTag)) { @@ -1291,13 +1301,134 @@ public final class SWF implements TreeItem, Timelined { fos.write(chunkBytes); } - /*private static void createWavFromAdpcm(OutputStream fos, int soundRateHz, boolean soundSize, boolean soundType, byte[] data) throws IOException { - createWavFromPcmData(fos, soundRateHz, soundSize, soundType, AdpcmDecoder.decode(data, soundType)); - } - - private static void createWavFromNelly(OutputStream fos, int soundRateHz,byte[] data) throws IOException { - createWavFromPcmData(fos, soundRateHz, true, false, NellyMoserDecoder.decode(data)); - }*/ + //TODO: implement morphshape export. How to handle 65536 frames? + public List exportMorphShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, final MorphshapeExportMode mode) throws IOException { + List ret = new ArrayList<>(); + if (tags.isEmpty()) { + return ret; + } + File foutdir = new File(outdir); + if (!foutdir.exists()) { + if (!foutdir.mkdirs()) { + if (!foutdir.exists()) { + throw new IOException("Cannot create directory " + outdir); + } + } + } + throw new UnsupportedOperationException("Not implemented"); + //return ret; + } + + private static void makeAVI(List images, int frameRate, File file) throws IOException { + if (images.isEmpty()) { + return; + } + AVIWriter out = new AVIWriter(file); + out.addVideoTrack(VideoFormatKeys.ENCODING_AVI_PNG, 1, frameRate, images.get(0).getWidth(), images.get(0).getHeight(), 0, 0); + try { + for (BufferedImage img : images) { + out.write(0, img, 1); + } + } finally { + out.close(); + } + + } + + private static void makeGIF(List images, int frameRate, File file) throws IOException { + if (images.isEmpty()) { + return; + } + try (ImageOutputStream output = new FileImageOutputStream(file)) { + GifSequenceWriter writer = new GifSequenceWriter(output, images.get(0).getType(), 1000 / frameRate, true); + + for (BufferedImage img : images) { + writer.writeToSequence(img); + } + + writer.close(); + } + } + + public List exportFrames(AbortRetryIgnoreHandler handler, String outdir, int containerId, List frames, final FramesExportMode mode) throws IOException { + List ret = new ArrayList<>(); + if (tags.isEmpty()) { + return ret; + } + Timeline tim = null; + String path = ""; + if (containerId == 0) { + tim = getTimeline(); + } else { + tim = ((Timelined) characters.get(containerId)).getTimeline(); + path = File.separator + characters.get(containerId).getExportFileName(); + } + if (frames == null) { + int frameCnt = tim.frames.size(); + frames = new ArrayList<>(); + for (int i = 0; i < frameCnt; i++) { + frames.add(i); + } + } + + final File foutdir = new File(outdir + path); + if (!foutdir.exists()) { + if (!foutdir.mkdirs()) { + if (!foutdir.exists()) { + throw new IOException("Cannot create directory " + outdir); + } + } + } + + final List fframes = frames; + + Color backgroundColor = null; + if (mode == FramesExportMode.AVI) { + for (Tag t : tags) { + if (t instanceof SetBackgroundColorTag) { + SetBackgroundColorTag sb = (SetBackgroundColorTag) t; + backgroundColor = sb.backgroundColor.toColor(); + } + } + } + + final List frameImages = new ArrayList<>(); + for (int frame : frames) { + frameImages.add(frameToImageGet(tim, frame, 0, null, 0, tim.displayRect, new Matrix(), new ColorTransform(), backgroundColor).getBufferedImage()); + } + switch (mode) { + case GIF: + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + makeGIF(frameImages, frameRate, new File(foutdir + File.separator + "frames.gif")); + } + }, handler).run(); + break; + case PNG: + for (int i = 0; i < frameImages.size(); i++) { + final int fi = i; + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + ImageIO.write(frameImages.get(fi), "PNG", new File(foutdir + File.separator + fframes.get(fi) + ".png")); + } + }, handler).run(); + } + break; + case AVI: + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + makeAVI(frameImages, frameRate, new File(foutdir + File.separator + "frames.avi")); + } + }, handler).run(); + break; + } + + return ret; + } + public List exportSounds(AbortRetryIgnoreHandler handler, String outdir, List tags, final SoundExportMode mode) throws IOException { List ret = new ArrayList<>(); if (tags.isEmpty()) { @@ -1518,7 +1649,7 @@ public final class SWF implements TreeItem, Timelined { exportTexts(handler, outdir, tags, mode); } - public static List exportShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, ShapeExportMode mode) throws IOException { + public static List exportShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, final ShapeExportMode mode) throws IOException { List ret = new ArrayList<>(); if (tags.isEmpty()) { return ret; @@ -1538,13 +1669,35 @@ public final class SWF implements TreeItem, Timelined { if (t instanceof CharacterTag) { characterID = ((CharacterTag) t).getCharacterId(); } - final File file = new File(outdir + File.separator + characterID + ".svg"); + String ext = "svg"; + if (mode == ShapeExportMode.PNG) { + ext = "png"; + } + + final File file = new File(outdir + File.separator + characterID + "." + ext); new RetryTask(new RunnableIOEx() { @Override public void run() throws IOException { - try (FileOutputStream fos = new FileOutputStream(file)) { - fos.write(Utf8Helper.getBytes(((ShapeTag) t).toSVG())); + ShapeTag st = (ShapeTag) t; + switch (mode) { + case SVG: + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(Utf8Helper.getBytes(st.toSVG())); + } + break; + case PNG: + RECT rect = st.getRect(); + int newWidth = (int) (rect.getWidth() / SWF.unitDivisor); + int newHeight = (int) (rect.getHeight() / SWF.unitDivisor); + SerializableImage img = new SerializableImage(newWidth, newHeight, SerializableImage.TYPE_INT_ARGB); + img.fillTransparent(); + Matrix m = new Matrix(); + m.translate(-rect.Xmin, -rect.Ymin); + st.toImage(0, 0, 0, null, 0, img, m, new CXFORMWITHALPHA()); + ImageIO.write(img.getBufferedImage(), "PNG", new FileOutputStream(file)); + break; } + } }, handler).run(); ret.add(file); @@ -2194,7 +2347,7 @@ public final class SWF implements TreeItem, Timelined { return ret; } - public static SerializableImage frameToImageGet(Timeline timeline, int frame, int time, DepthState stateUnderCursor, int mouseButton, RECT displayRect, Matrix transformation, ColorTransform colorTransform) { + public static SerializableImage frameToImageGet(Timeline timeline, int frame, int time, DepthState stateUnderCursor, int mouseButton, RECT displayRect, Matrix transformation, ColorTransform colorTransform, Color backGroundColor) { String key = "frame_" + frame + "_" + timeline.id + "_" + timeline.swf.hashCode(); SerializableImage image = getFromCache(key); if (image != null) { @@ -2208,7 +2361,14 @@ public final class SWF implements TreeItem, Timelined { RECT rect = displayRect; image = new SerializableImage((int) (rect.getWidth() / SWF.unitDivisor) + 1, (int) (rect.getHeight() / SWF.unitDivisor) + 1, SerializableImage.TYPE_INT_ARGB); - image.fillTransparent(); + if (backGroundColor == null) { + image.fillTransparent(); + } else { + Graphics2D g = (Graphics2D) image.getBufferedImage().getGraphics(); + g.setComposite(AlphaComposite.Src); + g.setColor(backGroundColor); + g.fill(new Rectangle(image.getWidth(), image.getHeight())); + } Matrix m = new Matrix(); m.translate(-rect.Xmin, -rect.Ymin); frameToImage(timeline, frame, time, stateUnderCursor, mouseButton, image, m, colorTransform); diff --git a/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 2a67c1ec4..b163075ac 100644 --- a/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -27,6 +27,7 @@ import com.jpexs.decompiler.flash.abc.RenameType; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.configuration.ConfigurationItem; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -97,6 +98,7 @@ public class CommandLineArgumentParser { System.out.println(" image - Images (Default format: PNG/JPEG)"); System.out.println(" shape - Shapes (Default format: SVG)"); System.out.println(" movie - Movies (Default format: FLV without sound)"); + System.out.println(" frame - Frames (Default format: PNG)"); System.out.println(" sound - Sounds (Default format: MP3/WAV/FLV only sound)"); System.out.println(" binaryData - Binary data (Default format: Raw data)"); System.out.println(" text - Texts (Default format: Formatted text)"); @@ -116,6 +118,11 @@ public class CommandLineArgumentParser { System.out.println(" script:pcode - ActionScript P-code"); System.out.println(" script:pcodehex - ActionScript P-code with hex"); System.out.println(" script:hex - ActionScript Hex only"); + System.out.println(" shape:svg - SVG format for Shapes"); + System.out.println(" shape:png - PNG format for Shapes"); + System.out.println(" frame:png - PNG format for Frames"); + System.out.println(" frame:gif - GIF format for Frames"); + System.out.println(" frame:avi - AVI format for Frames"); System.out.println(" image:png_jpeg - PNG/JPEG format for Images"); System.out.println(" image:png - PNG format for Images"); System.out.println(" image:jpeg - JPEG format for Images"); @@ -532,6 +539,7 @@ public class CommandLineArgumentParser { "binarydata", "text", "all", + "frame", "fla", "xfl" }; @@ -629,25 +637,11 @@ public class CommandLineArgumentParser { break; case "image": System.out.println("Exporting images..."); - String imageFormat = formats.get("image"); - if (imageFormat == null) { - imageFormat = "png_jpeg"; - } - ImageExportMode iem = ImageExportMode.PNG_JPEG; - switch(imageFormat){ - case "png": - iem = ImageExportMode.PNG; - break; - case "jpeg": - iem = ImageExportMode.JPEG; - break; - } - - exfile.exportImages(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "images" : ""), iem); + exfile.exportImages(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "images" : ""), enumFromStr(formats.get("image"), ImageExportMode.class)); break; case "shape": System.out.println("Exporting shapes..."); - exfile.exportShapes(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "shapes" : ""), ShapeExportMode.SVG); + exfile.exportShapes(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "shapes" : ""), enumFromStr(formats.get("shape"), ShapeExportMode.class)); break; case "script": case "as": @@ -655,60 +649,37 @@ public class CommandLineArgumentParser { case "pcodehex": case "hex": System.out.println("Exporting scripts..."); - ScriptExportMode exportMode = ScriptExportMode.AS; - if (!exportFormat.equals("script")) { - exportMode = strToExportFormat(exportFormat); - } else if (formats.containsKey("script")) { - exportMode = strToExportFormat(formats.get("script")); - } boolean parallel = Configuration.parallelSpeedUp.get(); if (as3classes.isEmpty()) { as3classes = parseSelectClasses(args); } if (!as3classes.isEmpty()) { for (String as3class : as3classes) { - exportOK = exportOK && exfile.exportAS3Class(as3class, outDir.getAbsolutePath(), exportMode, parallel); + exportOK = exportOK && exfile.exportAS3Class(as3class, outDir.getAbsolutePath(), enumFromStr(formats.get("script"), ScriptExportMode.class), parallel); } } else { - exportOK = exportOK && exfile.exportActionScript(handler, outDir.getAbsolutePath(), exportMode, parallel) != null; + exportOK = exportOK && exfile.exportActionScript(handler, outDir.getAbsolutePath(), enumFromStr(formats.get("script"), ScriptExportMode.class), parallel) != null; } break; case "movie": System.out.println("Exporting movies..."); - exfile.exportMovies(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "movies" : ""), MovieExportMode.FLV); + exfile.exportMovies(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "movies" : ""), enumFromStr(formats.get("movie"), MovieExportMode.class)); + break; + case "frame": + System.out.println("Exporting frames..."); + exfile.exportFrames(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "frames" : ""), 0, null, enumFromStr(formats.get("frame"), FramesExportMode.class)); break; case "sound": System.out.println("Exporting sounds..."); - String soundFormat = formats.get("sound"); - if (soundFormat == null) { - soundFormat = "mp3_wav_flv"; - } - SoundExportMode sem=SoundExportMode.MP3_WAV_FLV; - switch(soundFormat){ - case "mp3_wav": - sem = SoundExportMode.MP3_WAV; - break; - case "wav": - sem = SoundExportMode.WAV; - break; - case "flv": - sem = SoundExportMode.FLV; - break; - - } - exfile.exportSounds(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "sounds" : ""), sem); + exfile.exportSounds(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "sounds" : ""), enumFromStr(formats.get("sound"), SoundExportMode.class)); break; case "binarydata": System.out.println("Exporting binaryData..."); - exfile.exportBinaryData(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "binaryData" : ""), BinaryDataExportMode.RAW); + exfile.exportBinaryData(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "binaryData" : ""), enumFromStr(formats.get("binarydata"), BinaryDataExportMode.class)); break; case "text": System.out.println("Exporting texts..."); - String textFormat = formats.get("text"); - if (textFormat == null) { - textFormat = "formatted"; - } - exfile.exportTexts(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "texts" : ""), textFormat.equals("formatted") ? TextExportMode.FORMATTED : TextExportMode.PLAIN); + exfile.exportTexts(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "texts" : ""), enumFromStr(formats.get("text"), TextExportMode.class)); break; case "textplain": System.out.println("Exporting texts..."); @@ -961,4 +932,14 @@ public class CommandLineArgumentParser { } System.exit(0); } + + private static E enumFromStr(String str, Class cls) { + E[] vals = cls.getEnumConstants(); + for (E e : vals) { + if (e.toString().toLowerCase().equals(str.toLowerCase())) { + return e; + } + } + return vals[0]; + } } diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java new file mode 100644 index 000000000..66b2dd76e --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.exporters.modes; + +/** + * + * @author JPEXS + */ +public enum FramesExportMode { + + PNG, + GIF, + AVI +} diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java new file mode 100644 index 000000000..b83bdd378 --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.exporters.modes; + +/** + * + * @author JPEXS + */ +public enum MorphshapeExportMode { + + //TODO: implement morphshape export + PNG, + GIF, + AVI +} diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java index aeb16b6d4..50002fb90 100644 --- a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java @@ -22,5 +22,6 @@ package com.jpexs.decompiler.flash.exporters.modes; */ public enum ShapeExportMode { - SVG + SVG, + PNG } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java b/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java index 2a7757f28..c6f64e359 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java @@ -19,6 +19,7 @@ package com.jpexs.decompiler.flash.gui; import com.jpexs.decompiler.flash.abc.ScriptPack; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -31,6 +32,7 @@ import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundTag; import com.jpexs.decompiler.flash.tags.base.TextTag; +import com.jpexs.decompiler.flash.treeitems.FrameNodeItem; import com.jpexs.decompiler.flash.treenodes.TreeNode; import java.awt.BorderLayout; import java.awt.Container; @@ -62,7 +64,9 @@ public class ExportDialog extends AppDialog { "movies", "sounds", "scripts", - "binaryData" + "binaryData", + "frames" + //,"morphshapes" }; //Display options only when these classes found @@ -73,7 +77,9 @@ public class ExportDialog extends AppDialog { {DefineVideoStreamTag.class}, {SoundTag.class}, {TreeNode.class, ScriptPack.class}, - {DefineBinaryDataTag.class} + {DefineBinaryDataTag.class}, + {FrameNodeItem.class} + //,{MorphShapeTag.class} }; //Enum classes for values @@ -84,7 +90,9 @@ public class ExportDialog extends AppDialog { MovieExportMode.class, SoundExportMode.class, ScriptExportMode.class, - BinaryDataExportMode.class + BinaryDataExportMode.class, + FramesExportMode.class + //MorphshapeExportMode.class }; private final JComboBox[] combos; diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index 9b8736045..f15c27ee7 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -308,7 +308,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis if (stateUnderCursor != null) { ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar2 != 0) { //OverUpToOverDown + if (sounds != null && sounds.buttonSoundChar2 != 0) { //OverUpToOverDown playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar2)); } } @@ -322,7 +322,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis if (stateUnderCursor != null) { ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar3 != 0) { //OverDownToOverUp + if (sounds != null && sounds.buttonSoundChar3 != 0) { //OverDownToOverUp playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar3)); } } @@ -339,7 +339,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis //New mouse entered ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar1 != 0) { //IddleToOverUp + if (sounds != null && sounds.buttonSoundChar1 != 0) { //IddleToOverUp playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar1)); } } @@ -349,7 +349,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis //Old mouse leave ButtonTag b = (ButtonTag) swf.characters.get(lastUnderCur.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar0 != 0) { //OverUpToIddle + if (sounds != null && sounds.buttonSoundChar0 != 0) { //OverUpToIddle playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar0)); } } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 0e9165986..19172234d 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -30,6 +30,7 @@ import com.jpexs.decompiler.flash.action.Action; import com.jpexs.decompiler.flash.action.parser.pcode.ASMParser; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -169,6 +170,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.CancellationException; @@ -1443,29 +1446,7 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec private SearchDialog searchDialog; public boolean hasExportableNodes() { - List sel = getAllSelected(tagTree); - - for (TreeNode d : sel) { - if (d instanceof ContainerNode) { - ContainerNode n = (ContainerNode) d; - TreeNodeType nodeType = TagTree.getTreeNodeType(n.getItem()); - if (nodeType == TreeNodeType.IMAGE - || nodeType == TreeNodeType.SHAPE - || nodeType == TreeNodeType.AS - || nodeType == TreeNodeType.MOVIE - || nodeType == TreeNodeType.SOUND - || nodeType == TreeNodeType.BINARY_DATA - || nodeType == TreeNodeType.TEXT) { - return true; - } - } - if (d instanceof TreeElement) { - if (((TreeElement) d).isLeaf()) { - return true; - } - } - } - return false; + return !getSelection(getCurrentSwf()).isEmpty(); } private List getSelection(SWF swf) { @@ -1500,6 +1481,12 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec ret.add((Tag) n.getItem()); } } + if (d instanceof FrameNode) { + FrameNode fn = (FrameNode) d; + if (!fn.scriptsNode) { + ret.add(d.getItem()); + } + } if (d instanceof TreeElement) { if (((TreeElement) d).isLeaf()) { TreeElement treeElement = (TreeElement) d; @@ -1525,6 +1512,7 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec List texts = new ArrayList<>(); List as12scripts = new ArrayList<>(); List binaryData = new ArrayList<>(); + Map> frames = new HashMap<>(); for (TreeNode d : sel) { if (d.getItem().getSwf() != swf) { @@ -1555,6 +1543,23 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec texts.add((Tag) n.getItem()); } } + if (d instanceof FrameNode) { + FrameNode fn = (FrameNode) d; + if (!fn.scriptsNode) { + + FrameNodeItem fni = (FrameNodeItem) d.getItem(); + Tag par = fni.getParent(); + int frame = fni.getFrame(); + int parentId = 0; + if (par != null) { + parentId = ((CharacterTag) par).getCharacterId(); + } + if (!frames.containsKey(parentId)) { + frames.put(parentId, new ArrayList()); + } + frames.get(parentId).add(frame); + } + } if (d instanceof TreeElement) { if (((TreeElement) d).isLeaf()) { TreeElement treeElement = (TreeElement) d; @@ -1563,16 +1568,6 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec } } - List allnodes = new ArrayList<>(); - allnodes.addAll(as3scripts); - allnodes.addAll(as12scripts); - allnodes.addAll(images); - allnodes.addAll(shapes); - allnodes.addAll(movies); - allnodes.addAll(sounds); - allnodes.addAll(texts); - allnodes.addAll(binaryData); - if (selFile == null) { selFile = selectExportDir(); if (selFile == null) { @@ -1587,6 +1582,9 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec ret.addAll(swf.exportMovies(handler, selFile + File.separator + "movies", movies, export.getValue(MovieExportMode.class))); ret.addAll(swf.exportSounds(handler, selFile + File.separator + "sounds", sounds, export.getValue(SoundExportMode.class))); ret.addAll(SWF.exportBinaryData(handler, selFile + File.separator + "binaryData", binaryData, export.getValue(BinaryDataExportMode.class))); + for (Entry> entry : frames.entrySet()) { + ret.addAll(swf.exportFrames(handler, selFile + File.separator + "frames", entry.getKey(), entry.getValue(), export.getValue(FramesExportMode.class))); + } List abcList = swf.abcList; if (abcPanel != null) { for (int i = 0; i < as3scripts.size(); i++) { @@ -1991,6 +1989,12 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec swf.exportMovies(errorHandler, selFile + File.separator + "movies", export.getValue(MovieExportMode.class)); swf.exportSounds(errorHandler, selFile + File.separator + "sounds", export.getValue(SoundExportMode.class)); swf.exportBinaryData(errorHandler, selFile + File.separator + "binaryData", export.getValue(BinaryDataExportMode.class)); + swf.exportFrames(errorHandler, selFile + File.separator + "frames", 0, null, export.getValue(FramesExportMode.class)); + for (CharacterTag c : swf.characters.values()) { + if (c instanceof DefineSpriteTag) { + swf.exportFrames(errorHandler, selFile + File.separator + "frames", c.getCharacterId(), null, export.getValue(FramesExportMode.class)); + } + } swf.exportActionScript(errorHandler, selFile, exportMode, Configuration.parallelSpeedUp.get()); } } catch (Exception ex) { diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java b/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java index be0112652..e3cdc1b49 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java @@ -165,7 +165,7 @@ public class PreviewImage extends JPanel { } else if (treeItem instanceof FrameNodeItem) { FrameNodeItem fn = (FrameNodeItem) treeItem; RECT rect = swf.displayRect; - imgSrc = SWF.frameToImageGet(swf.getTimeline(), fn.getFrame() - 1, 0, null, 0, rect, Matrix.getScaleInstance(1 / SWF.unitDivisor), new ColorTransform()); + imgSrc = SWF.frameToImageGet(swf.getTimeline(), fn.getFrame() - 1, 0, null, 0, rect, Matrix.getScaleInstance(1 / SWF.unitDivisor), new ColorTransform(), null); width = (imgSrc.getWidth()); height = (imgSrc.getHeight()); } else if (treeItem instanceof ImageTag) { diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java b/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java index 6815f8cd2..213e463de 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java @@ -122,7 +122,7 @@ public class TagTreeModel implements TreeModel { switch (ttype) { case SHOW_FRAME: ShowFrameTag showFrameTag = (ShowFrameTag) t; - frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags)); + frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags, false)); break; case SHAPE: shapes.add(new TagNode(t)); @@ -277,11 +277,13 @@ public class TagTreeModel implements TreeModel { switch (ttype) { case SHOW_FRAME: ShowFrameTag showFrameTag = (ShowFrameTag) t; - frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags)); + frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags, false)); break; default: if (!actionScriptTags.contains(t) && !ShowFrameTag.isNestedTagType(t.getId())) { - others.add(new TagNode(t)); + if (!(t instanceof SoundStreamHeadTypeTag)) { + others.add(new TagNode(t)); + } } break; } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties index a20f64366..a65ba6bbe 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties @@ -14,6 +14,7 @@ # along with this program. If not, see . shapes = Shapes shapes.svg = SVG +shapes.png = PNG texts = Texts texts.plain = Plain text @@ -46,3 +47,11 @@ dialog.title = Export... button.ok = OK button.cancel = Cancel + +morphshapes = Morphshapes +morphshapes.gif = GIF + +frames = Frames +frames.png = PNG +frames.gif = GIF +frames.avi = AVI diff --git a/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java b/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java index 838d832ae..cd46bdcbe 100644 --- a/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java +++ b/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java @@ -42,8 +42,8 @@ public class ShowFrameTag extends Tag { add(StartSound2Tag.ID); add(VideoFrameTag.ID); add(SoundStreamBlockTag.ID); - add(SoundStreamHeadTag.ID); - add(SoundStreamHead2Tag.ID); + /*add(SoundStreamHeadTag.ID); + add(SoundStreamHead2Tag.ID);*/ } }; public List innerTags; diff --git a/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java b/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java index 94ebe6241..d6ac8a90c 100644 --- a/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java +++ b/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java @@ -26,8 +26,11 @@ import java.util.List; */ public class FrameNode extends TreeNode { - public FrameNode(FrameNodeItem item, List innerTags) { + public boolean scriptsNode = false; + + public FrameNode(FrameNodeItem item, List innerTags, boolean scriptsNode) { super(item); + this.scriptsNode = scriptsNode; if (innerTags != null) { for (Tag tag : innerTags) { subNodes.add(new TagNode(tag)); diff --git a/trunk/src/com/jpexs/helpers/SerializableImage.java b/trunk/src/com/jpexs/helpers/SerializableImage.java index 56de36420..e01c713eb 100644 --- a/trunk/src/com/jpexs/helpers/SerializableImage.java +++ b/trunk/src/com/jpexs/helpers/SerializableImage.java @@ -143,9 +143,9 @@ public class SerializableImage implements Serializable { } private void writeObject(ObjectOutputStream out) throws IOException { - try{ + try { ImageIO.write(image, "png", out); - }catch(Exception ex){ + } catch (Exception ex) { //ignore } }