From 51f0fc07faa9f608c2d419fe99e93407acbc93d0 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 25 May 2009 11:48:46 +0000 Subject: [PATCH] SPR-5782 - BufferedImageHttpMessageConverter --- .../BufferedImageHttpMessageConverter.java | 230 ++++++++++++++++++ ...ufferedImageHttpMessageConverterTests.java | 75 ++++++ .../springframework/http/converter/logo.jpg | Bin 0 -> 17031 bytes 3 files changed, 305 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java create mode 100644 org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java create mode 100644 org.springframework.web/src/test/resources/org/springframework/http/converter/logo.jpg diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java new file mode 100644 index 0000000000..9880e6ae2c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.FileCacheImageInputStream; +import javax.imageio.stream.FileCacheImageOutputStream; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +/** + * Implementation of {@link HttpMessageConverter} that can read and write {@link BufferedImage BufferedImages}. + * + *

By default, this converter can read all media types that are supported by the {@linkplain + * ImageIO#getReaderMIMETypes() registered image readers}, and writes using the media type of the first available + * {@linkplain javax.imageio.ImageIO#getWriterMIMETypes() registered image writer}. This behavior can be overriden by + * setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} and {@link + * #setContentType(org.springframework.http.MediaType) contentType} properties respectively. + * + *

If the {@link #setCacheDir(java.io.File) cacheDir} property is set to an existing directory, this converter will + * cache image data. + * + *

The {@link #process(ImageReadParam)} and {@link #process(ImageWriteParam)} template methods allow subclasses to + * override Image I/O parameters. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConverter { + + private MediaType contentType; + + private File cacheDir; + + public BufferedImageHttpMessageConverter() { + String[] readerMediaTypes = ImageIO.getReaderMIMETypes(); + List supportedMediaTypes = new ArrayList(readerMediaTypes.length); + for (String mediaType : readerMediaTypes) { + supportedMediaTypes.add(MediaType.parseMediaType(mediaType)); + } + setSupportedMediaTypes(supportedMediaTypes); + String[] writerMediaTypes = ImageIO.getWriterMIMETypes(); + if (writerMediaTypes.length > 0) { + contentType = MediaType.parseMediaType(writerMediaTypes[0]); + } + } + + /** + * Sets the {@link MediaType MediaTypes} supported for reading. + * + * @throws IllegalArgumentException if the given media type is not supported by the Java Image I/O API + */ + @Override + public void setSupportedMediaTypes(List supportedMediaTypes) { + Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' must not be empty"); + for (MediaType supportedMediaType : supportedMediaTypes) { + Iterator imageReaders = ImageIO.getImageReadersByMIMEType(supportedMediaType.toString()); + if (!imageReaders.hasNext()) { + throw new IllegalArgumentException( + "MediaType [" + supportedMediaType + "] is not supported by the Java Image I/O API"); + } + } + super.setSupportedMediaTypes(supportedMediaTypes); + } + + /** + * Sets the {@code Content-Type} to be used for writing. + * + * @throws IllegalArgumentException if the given content type is not supported by the Java Image I/O API + */ + public void setContentType(MediaType contentType) { + Assert.notNull(contentType, "'contentType' must not be null"); + Iterator imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString()); + if (!imageWriters.hasNext()) { + throw new IllegalArgumentException( + "ContentType [" + contentType + "] is not supported by the Java Image I/O API"); + } + + this.contentType = contentType; + } + + /** Sets the cache directory. If this property is set to an existing directory, this converter will cache image data. */ + public void setCacheDir(File cacheDir) { + Assert.notNull(cacheDir, "'cacheDir' must not be null"); + Assert.isTrue(cacheDir.isDirectory(), "'cacheDir' is not a valid directory"); + this.cacheDir = cacheDir; + } + + public boolean supports(Class clazz) { + return BufferedImage.class.equals(clazz); + } + + @Override + public BufferedImage readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + ImageInputStream imageInputStream = null; + ImageReader imageReader = null; + try { + imageInputStream = createImageInputStream(inputMessage.getBody()); + MediaType contentType = inputMessage.getHeaders().getContentType(); + Iterator imageReaders = ImageIO.getImageReadersByMIMEType(contentType.toString()); + if (imageReaders.hasNext()) { + imageReader = imageReaders.next(); + ImageReadParam irp = imageReader.getDefaultReadParam(); + process(irp); + imageReader.setInput(imageInputStream, true); + return imageReader.read(0, irp); + } + else { + throw new HttpMessageNotReadableException( + "Could not find javax.imageio.ImageReader for Content-Type [" + contentType + "]"); + } + } + finally { + if (imageReader != null) { + imageReader.dispose(); + } + if (imageInputStream != null) { + try { + imageInputStream.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + + private ImageInputStream createImageInputStream(InputStream is) throws IOException { + if (cacheDir != null) { + return new FileCacheImageInputStream(is, cacheDir); + } + else { + return new MemoryCacheImageInputStream(is); + } + } + + @Override + protected MediaType getContentType(BufferedImage image) { + return contentType; + } + + @Override + protected void writeInternal(BufferedImage image, HttpOutputMessage outputMessage) throws IOException { + ImageOutputStream imageOutputStream = null; + ImageWriter imageWriter = null; + try { + imageOutputStream = createImageOutputStream(outputMessage.getBody()); + Iterator imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString()); + if (imageWriters.hasNext()) { + imageWriter = imageWriters.next(); + ImageWriteParam iwp = imageWriter.getDefaultWriteParam(); + process(iwp); + imageWriter.setOutput(imageOutputStream); + imageWriter.write(null, new IIOImage(image, null, null), iwp); + } + } + finally { + if (imageWriter != null) { + imageWriter.dispose(); + } + if (imageOutputStream != null) { + try { + imageOutputStream.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + + private ImageOutputStream createImageOutputStream(OutputStream os) throws IOException { + if (cacheDir != null) { + return new FileCacheImageOutputStream(os, cacheDir); + } + else { + return new MemoryCacheImageOutputStream(os); + } + } + + /** + * Template method that allows for manipulating the {@link ImageReadParam} before it is used to read an image. + * + *

Default implementation is empty. + */ + protected void process(ImageReadParam irp) { + } + + /** + * Template method that allows for manipulating the {@link ImageWriteParam} before it is used to write an image. + * + *

Default implementation is empty. + */ + protected void process(ImageWriteParam iwp) { + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java new file mode 100644 index 0000000000..e8df5930e5 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.util.FileCopyUtils; + +public class BufferedImageHttpMessageConverterTests { + + private BufferedImageHttpMessageConverter converter; + + @Before + public void setUp() { + converter = new BufferedImageHttpMessageConverter(); + } + + @Test + public void supports() { + assertTrue("Image not supported", converter.supports(BufferedImage.class)); + } + + @Test + public void read() throws IOException { + Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); + byte[] body = FileCopyUtils.copyToByteArray(logo.getInputStream()); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + inputMessage.getHeaders().setContentType(new MediaType("image", "jpeg")); + BufferedImage result = converter.read(BufferedImage.class, inputMessage); + assertEquals("Invalid height", 500, result.getHeight()); + assertEquals("Invalid width", 750, result.getWidth()); + } + + @Test + public void write() throws IOException { + Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); + MediaType contentType = new MediaType("image", "png"); + converter.setContentType(contentType); + BufferedImage body = ImageIO.read(logo.getFile()); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(body, outputMessage); + assertEquals("Invalid content type", contentType, outputMessage.getHeaders().getContentType()); + assertTrue("Invalid size", outputMessage.getBodyAsBytes().length > 0); + BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes())); + assertEquals("Invalid height", 500, result.getHeight()); + assertEquals("Invalid width", 750, result.getWidth()); + } + +} diff --git a/org.springframework.web/src/test/resources/org/springframework/http/converter/logo.jpg b/org.springframework.web/src/test/resources/org/springframework/http/converter/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a70e6af172588135f7f6d62c01519a638392b4a GIT binary patch literal 17031 zcmeHucT`i^zi*TgL`0M-eMUM68G0u(C?FspT?he{CWH_|hfqcZQHl&*2t}p0Ftmga ziqeaK)DS`qz4s7G;4$~Ud*6NU-u2e{t^59XfBbe3h?vr z@%=0;Blh#X`$Bwt;>r^Df0b2GP!JST)l`wwkdarA`&-BrMn=Y)OgHZ`GvAf_h3^-+ zf4E$H1>CuQ<>2ZT-IaTQt9P!@-MMnn2H*o+0bHf~ySx8huF?O*aGmbz-?gT<0aveH zxk`79j)DFLBi&U70O0cMr)ziUS$|<*ll|TFIpWoI_Iq*(pDQ_(&3voAaSACYDrvv) z_8H_7mVaQLR6R6j;kRRzEb`Fu^_#?&OOZU6ZT(C6_x3JDUi*oT{<2Q;&SeeY>J>V= ztMqjLT7Tuv)nDj-lKuS}tLZ&CdNv{LSA!1{b}l9WH|Z|#yhC>f00PX+{x4boJo+aB z|3u)Q2>cU){}%{QF%v5#{v?%_IvFoy6W>EciAupL2}jG!uoE+jt({_iNM>Ob^W0ytq{DZoW1?*>+{$t2QVAMN6~ztOiU1rny5%* zdd^Ky>1PEO>uhC6C6(YEdz}%P{vTh4cWJSZOVY{NLi<+ zL(;BmuQ*kz%mjU2946MF^u+L+OoWV5epKGd$Ey0ijVyzHoiVedweutLcXy$p^>Q0z zwhaAg(Sy7)d%{W>)5vkqNxbylKD>SP*n9qLIuvns0jTJCk)@p(Q)sV&@77y409k2K zoX|04gs;>uu_3n4EiH?*rJOKYa{DjaVuOydl2##4?1oVX`hnRdShz_%2peuvy$-yW zso^%`xh1;;K1|v(oY428WO)iG;Cpd#%|CJ>%ou^b<&zj|Hie-)8qt7| z&ZE9K*+hMD)U>Diru2j)7j7Vxfl($ec|-UJ=NamT1jM3e+$W17E6wt@;Z0oriHuR* zafQg-0#dKmbIdo|+2wPq`x?&j!}mD3@gkkmXG0FR)l_(xjE@ak8x}_cy-nklZC)&R z4v`L=Qn$n1rB4FjAx!BGO2#f$KjF*~&79urE1B#K0OHqIgMoy%burT9Rh|wR7wz(i zHME1ijt2@#ZW;>wKL$x(vI(t4sR&H?mWyTpN8zOnnVVA`@32)!Ah%|AZy3t+Xp*_m z`OgxZjnn*f9nVFvDji;j`I5=|?L)JL^@}MKy}(*p@>v+@`GZz+*w18Ux?6qTRD8Zb zz>V2Y7`R`(J<`LWPB}97?gNW~^8Kb7OOZKR;c3%w!os1wgXv-u3xoMmeG|x#yc4*L z8knNK>r;MnsA_EmqmJ5Pz>Xwn1ZK5*=QQW@vzSqQyX~N~VJHs20;5tnzpB!>hWGlg z7l806^?{}3rNtxf=6SLdli}E2eobNA8}nVaBTRT2*adxdrU(` z@Ke$>Wd;Az9M9s?CD$3(AWx%VjMl5+bDe-*zvx9^3ij5=1{)Lm*NBAn<7K@Sz#mI%iKG< zm0u^SzKy!a#1a%$Ikwg@TAgbRgy>Ulk~8!JRh=b{`ho@&5ua?1?ZA81+NSAI3pbv< zWJ#)678JVxz_91}u4a7Amx*TZCmkz_+~7I%YUg0KhRQCjnrE>$4LIcs z;JJ)%{(D7Bx`b<34Hli^6BGO8YQSB|Bq!5b%M~Ae14dsb{Zty*XlnnSaaU+2@p)>olpvoy<_D z)b0v}Mr(V|WM{@YV;Q*^VgbhO)l@Lc?NvLY0_!3-?%GAifqs@UhrG1tunzr9iO&em zidMDP1B2C@tzCw$;vOSuxhB2@6Z*K}mT+;LHisj^DtqZjf9ZK%_HAP@G=%X-!~qqTM#GT|jvGO@h7J4UB>S)YBm02F8qTmVi!R2*|&0LG10GU2U! zI^wPRV>^4Wv`B+~Fz7sS!ruN5lRqve?y6k>y+Ns-3eMoB8(o;uhmyx_L5r>{n@y(& zp^QxE3W4+~zXWs%c~P}*<4c+qY45ktH)aTy9;UYnvBxjm#))dBZzckU=5I99szRM9 zuoa@nx6oRzZLT%N)emKH>RNZn^z^#8JhFqNoZn+f&-gL) z_jkI610^0ecYdGg$dru>^@X(YI7wrjuZOJh_M)fMk}mssg)6JAo-^ICLjCF2&Px5? zTPELi7?^y6H7$GsSRRHAQbF2X$4$8vWNZITwb`k}_{QHgo5ZqF6Yy*9M`{R_ct&Eq zfl~L#Onj6Ym>P7&al2{)@?So zlFV{qh4oLAtCKfefKUy_bSN4v(i)q%G`%R9Mp8&UZ<&fu(uLe<=2_b+2)F84$miO6 z$?_!7dFjQhqluXOy|Un64$Cm^vJRXNXAu=lYw3TseI+w1i;0Gtl~g%O7*1l>ep{dOtQEiF=HTnA|A06o0 zBXthJrzgRD-->3PR&7&_bJD>TVmw4kC&A<-V%7#?z~gD{9Hp{i)LQv|IhufbPGf5) zX|$5HNJWk0Y-e}gdLd(?gUzOc;8`NU{Hd27hy zNeq-Wa^wX*<#TE5z7kCC=Nlkvc({87#(;FT3Kmu8H1l1J65+lSh_Uyk>nrM;2xnd0 zfZI-MC;rY=i&+%I3?)CLfs9E2HiJS9ItPn9-as^U|}TbRRy6A{QjrJ3WGkJaVe`o2smJ9(E@$rlq2lZU4iHz&9Hz_)@w z#G~FodeRq6)l$%WYkY&yP%M~A^^87Z^oO@hQb^K(*klY;{jJJGFU#e@%06i@&a%eQ`DZfCgeS z7+(4MwX;Hy^PRZ0CDfVykL9OlD8=P?2}8SKOA`92cwc*k(jsQ{RLKQ5e~XS#CnQ;4 z8)8v{rKReaWZ*T;!kRk#yY)Ar+U*3S(io?n{+y{sk4x&7JX*#fcq-(#AC1YB{30)O zWla8>syY7m`q0VGpuAnuNvA@xXiG#m6@C^Cdd!vo8R->Ox$X zGXSEbO?=4jn1~zyNa)YHffqzPw3ey_GTN-24ed>ihko=I%3oLste;`GbTPvD9i=>1 zn!7Q3#)m;-2#|VMbMfZ`yWncYDG(2J-`wBG@6z9DuuFu7`>1~txm(mOoJbT|pf4i# zf1|pZ55a8dl^6PS?@XsliAXLHl9S-MhxYuM1KGazLS(J0V-?45jmA~|x<^CVKJ3D@ zN=!oqUy(Y0rVwB835@z#b6n$#gsf_-drWeko`Q|ee#7hxxY}Z&8D?b9pUJ&q15#2Y zA6v6Vg(<}Hq3D(D?m&e;hxP`|aC$_TOYGh#;kofGRgOaDqga;=@IknK5Ea$CKrF)7 zA;H6$1?R9NNH?e?B>`H94W7OL(76!5HpqtGUVswSw3Kx7kuh0>&W827MFqLiLn~>o z_voY|_PqB4)gSisdj|zP8+Pu8x-89RV(sxs3}gcd;Y7nPk7)m>eFH$72-LKvbIdG; zW4`!Yp0$x*_%fWZ+n=ZD)SzOZt{49kFL+= zQ^dB_>;M3U_XD9=e{f9XdsmJXm7WYTVW6@k`bjk0b5E+bR6l=vb!0^w1Zk--o7*&` zC)CKvdX$*JD8|yB4A=Q9iDRENt*aXuAFf?tj(1sPO6V}H04)ol1w zdhSF-Omugu(GfGbmrSAhz;>=KTv}{cPS$3?j2`Vq|JO^SgBj?j;J0(nBcH(plN=6^ zb~I1HzdZN#&N40^+KJ_LFWQ^SVDn1*RMBjsdd=3)ofC{2SobGj;ezC$Q(?%tv1&A0 zaimg}H(*xP2R_$(DRR>E)TAdH?$+?YFUcG#XIX$Dr3&hTd`AG!Q6FqkUJcPFAM0NLbQHn~IUDkR8Rm;(H1@#@fPMI1Uxy%>Vcvm07XYt9 zN&<<2je5$fUSRP(vVeVTX9Gy`zuzt(ntD5k+3 zGQKyDxzTc6GJ3HUeJ&2`3gDJ<6M9HKw*Peu_*JpL0aP4T3g3{|_(}lf zo}{f|&Rgl;^!IKg?OV5A07{!l2^~?+6#e7UxeLJP%w`wUXOm6Cht2QQmB+kf%~NPY z^>+*d#O#uft(((c%$l8QZwfx1Wmb}Jl9DpTA8YZs_DTR%^$!>0v7Sv#<9q>;DK7M% zV~)*X zN{!h&YBr6j+h56B!-pk)=Yxl3XHfO;0p2CWKAx8h8#AG@4!f(QcYAlhJrnZgSlM`v z|H0#25k)EY_F4lkXlp~FsUchob;@#!@fhT;YNNQgYd8P?J!k&DK|#KAHoq|8QH9UU z5ok z_8P(@e7Xne-g%4`yS1xRl0JHCTkuIHCiY9(U|e>5FKVEG7X0U=&3GEydPr!wvs=xJ z;ex58=}|w7xP(u>1dIc}LIQ7K$IIO{-y)`S95fUreD3AqkI@gE2-R*Lq$5!VaIu7r zfwaO&LBqgs(t&`?rckx>O)1|E!W%}FPA(9ScX?1Pqc3TeLgC`kGUqkrNN72`oN*fh zs+Viam}QqPJUM%$u9qt!P+rNqQSV7liNyt2ynL{P;|5AUyXu+J23OMQf({39onaL0yr6+)bx*nzYn8SCp?GxwoLLpXn;KJt&e zoq(IpyHV`VSE!ZY9ewI$4qNLEwhq^hHvG4gG@pK~nK!+5qc{Q@Vf{K9EJEQx)6X&s z{c(VEblXkADQ)b@l^bS{HBR4w_&da`+5W*7PI6sFLXIw1uy*xdw?LcK`;F++l?%Y< zr<~16%Xi6cYE2F-ExDaSL;%NCRR!m;BFZb@Y1W^^# zi18P6(cltboZSeK;yR(7I($4bf$})$C@u=J)O^@ju7UtxDfMG!a-ghH_p3zsbIrme zYsvM#u*0xEiqg+?1|gSz(3cF*`T&KyTBXoo#Fx*YY*+;;Z`;&C2#6EmV8*PAo@v;V z#AaVkE6;6>A`O}ag~Z;@zmNQ|P`Mk1u;UBZ^i1k8g%`gVBm8|;CN|Iu+-FU=)qLV5zzT=dMVkEcNJ z*0QS$_%5h>H`&qI)6l=*Bp>^R6jtzYK2~7l^l^IykDUY`v$DWBh27v}Ue&}UDJe4v zDWNzhGI~a*w>r}wm^1%?k}%!*b5k2qTfe!>bagddkG4dZ^OWBN>JP7c8w+FsYO!l` zXrJUyjgu3>y`dx7)3eH|{1yF*J4=PC-MX@EwK)Ba#0A$;+sLt(P}9_$B&Mue+v)l; zhla3Y4gaOR-CacnE~sV^BAZ?}CT|B;Z!e%@7P;S5&qvWYK$s=PnzxEfyxD{IltO=7L)RguofI@mW2MbO7aODJ(vMg z*(Shpa-Jyfc1-0#{h|CN?&;iPQi$DZ0R{Hpc5^$Nnf(Fk2;(;vR|F|>Q*c>uJLU<+ z!24p7utD}iP;?&}58ryZFg>mURsU_1$*lQa@fpFiDd_2u;(<%vQR2*~GGvLZ zb-C5z!c@WwyArFuzWi1E-rCn~zRXEGH3-p0VbE-485Vs>Z-fpf80$R2ph~N7Bj zt;%r3ScYeAOzG;!i4PnllXe;NFmwHK^4NOY%}@Whp}G7A9oL>G!+8wvxkY$a49g*q!5p%384q!KgyVM_n_zT(46vC3iI^K3&6j7 zVQ_%fb33V}g$T#*$N4>&sgq{6ga5L3f=b4U)>2|3cXd<^j5ZSWWarlRHsP2l$+BZm z*9b(FOzxL7T|mnq^|j)&zQLQ5NL@2$i2cH^!AIYllS5jzOjv$|HEP^4*}&Sh%}oy; zgzBjosPuGOs$#2NXY#4$RQh<*{_soNk%|PJIO<#gHY6_qHI()_d;S)DQ`-;U2BbeJ zyG(6Px>5O2YHY5GM2mrG;*`RXk>ICiGhbG1EP68I(JA$>?*XKz8#SkEoZWj4}4)zV@s7`}`hrt4i8Egnm4 z%r|&qrEH}G4bK-DlcNH2DX6odyMkg-`7Q)h;qC`d86%TGu(4chTKnT#vKPVasa&!( z-<6$+H%!KSkg?i( za=y{vxV1y!b$ERIXeA)8R+CNr&f!d5l`_yZ9yl8aZ5E|zon~bS)Orfr)eomQ1b?Zg z>U+>M0*+FUVMb?WO&6e6oY234HKH`dR|4Y%8!7G5(AKTUoCL8V*=Ie%cI*lI(rNsxB?Sapu ziSRW)5q8VCcnG^S&svG(?Fg$&&cG@0V4PNvQCWnJmW;F@aNM=NUeqBGc)2B__4U7k z{-4+W?+DygfZkPAt!gVdluD&3AvGKPT@XxBtskkudDV^{`J;7Kgi$zYiFLc>C4l|k zM9Egm@9sDE|q3lgICbHLty?+D^o z>y!S6QehIxqh~C9p+3gl)%XfYN!UiN0(oZQfmd~*(quzsT-kT;nILIO%z=C$%D7bM zarG}&_%&;}M2P>sjuEc->+d@l-+cuvq{=%>ZgJ|k0yslqi?HzpT>IC-gn<1ltTyVq z2-oH%G!vHJqvEsd$~tOouJ@+YG;~e>Y}Q+lrBsfee7{ZTkzFG)wWxZ=o_Z}0H-hud z@|t&>$GN9>eE{@-{EIY!fc|lF33D?{`y7J|-JPyS?ATj=$x|rh9kGUnJuxN zIKg4!>Eh{?42+GFB-+a6(iXubKtILW@td=lEO-Vv( zOZ1ow3UM)5w2lOwL%mT^V_=2zeW82roaTu-s687lE^JNQ;DOq~d$9QAW(rDrg5<>3 zXb8Ca$KPTB^n8bQJlnp+6ubK-E(Mgc$1T8jvTMz{-TuF;YV!>u{fvGptLO<7JlTLB zS6%>Qk4@+<0Fb!L=oGYM6{r^Nnd($tf~!-TwEL0+9NW1+JnZFRw$%E0Kd@;NPKih* zR;WUGd|f{6yLpH4Rn6?kN592?wv2LC{g{9#ZHUg)um}hf$3~`}{qe8k{FhGs$Diw= zV4JR?qvXF-SK+BjN2R5tKG=`$!TrTzx83eccD}r^?h|>o=;n`Q=V#$}TAIIGUI4-z z-nlNb{RSBB6}s18n}xxN1G(<1!ge0Db`XA*m-`$a53TAJJMHo=m-MEX?Eb5FN3DKE`K1wvG=h-| zemF~Q{bf-6$ZVOEZ~&dPt=e_(Yp)hq*=8(noF3g|l&*|2ZZ24uC@DBLhJVv7Zk}{r zbRLehMuz0y4XS03ZakHVG>LLPR(5E4y4M6Pm>}cID+B!6{k)Uw ze>NOm<^>P$%UC*2Nr|4Nn6GXleYL6&Lo`)8R5`WwAdc4y76$qRjo+dQTawcP35tn_ z33DlV&oy<`QjU7qVw0OP(||g2Bf!AZU<$u~3w}OKJ!dq)f}GPy)-SA|;%E~6{;g+l zk!SB^Rad`UbmaSH6QHATV6b=gn-~eD87XkHj_9BI$PO2m$6h;IT{)2mZxUhr;m+u- za<$3zfNeS9jU*DfHyl_)C4(W`Y=-4DjTpAnX?@T+=#{#qX~9R!Yc#a3?+A6D+_07y6YsE%?J(tGVB1H1N>fNP8vy@PWOP-E(|4!v!|JCjrCLQseb-YEqDm?{l-QE{x*0pz1N%*n7E` zjI(s2p;NSfZt5PjKoRnIJ68ZK35{1z?F|?nC<1vHp@qLr$Y(UwFU~oYgjA6Ahy83g zq$H}8ey17$eRZ7NEf-!8<28Y%3z>HQ$Ia)Dw%FhLOCLGT#E1I!Xms~8i->F=(!NfQ zJua*NwK%{D%ro0ClY_l# zr|YGwe#^@rCazLBzudlI)K#ndV0CxA%m(3(?z-HTd^0U?pnu98pI-dJZ4D?%xCUhC7Ng!%j`R^QPAo~r zX0a};pJ+KO?uf|GbFUuDCK?5ptq}f-^Xy<(i7~9S5nrI)g^jkOVKlB7jpj%-|8-Q; zYdCBXOuT)@A8(0MRqDTCUaptWytdtR0eA)NZ7Wpf@pa%3*AI;!Bci{ol+ zc%{Zg zHv^MCOhZ?;^jCDCp^|K@2U=f$b4>r>>=C5T=EnatDnB(dFAf0p%84QpW0So5#6bAY zJ@NDGff?Yq1hPFz-n4yqp4-_*T-Tl@(I~Ww`fU`UC_&`2!5z6N9os@e&*vrAK8MPe zbQNnu-Ncrn3$KNS%=m(WI7y`*dAVE?3W>Wg>YaFp*5r%=<>}mcNq28Uvd*pxBr#hn z3#<>W$=_VB(uwC1Xn5?tQX-+v2Z!>3nT$=7keKDLjZRH!&(S@33@G?YI*%qmxg7|LWgvrxztD65`&S zB8yb#FnRHSO2s2Z+)Dc*dbl=w;DM{t zLnU>3=D9MA+8wM<&T(rUU4|;rkdf-KOKtQ(VoHV#%U86^m6kzoVcPOJffbH9i0XH-8lGxbA(g*ddS~c%s=dpEF!i91@-+q_9rYFWv(c`BM8DbjfwT5ijv_>P z6M6v%Q0)1R-;?O5XsB<$4##Yi?z-GWEEZ`jZT4>iG9B-EYEzK}TGK=(f zFcD1ED#9cnV{>?$1;qP8+H0_h=LYErNEeF94Stg47GZ1>oUUMZswCGosz6tP*L0bbW!93$0Pp zYb9!-Ay_ndwj|o!u+u3%w$Xu(-jkdA2CIoHKPrs(Sct^jJ^N#kQ5!ND5S!Yk*}%=w zj))l)o-pb_5Nqc~;_P!pX~?UYkL=6*1K!tZOReg|F{eSmpyT}Yx{31Yk|HA0Jsi#@ z&BAe~gIQtKZ&7^PuX|trSo)0XufEnHS+6Qs7n*m=9dp-;1fo10V zVkYWzdi2`-zAGm1$ZR7v(nhyS5hmmyNdB|o5eqrNd|%@N*AUaIf5=06TyMvGRzLRplR-jnzpE&}>8Pstwt z_;h@{vZUnsT@Nfc@RVloR_U5xEBA~3I)fCayw6x83{1MIC3QSIjl&I5onqsZwkodq z-Y(>Coj3CI@9B;;GBN4s8iq#{zn-{d>Mo%$v@*#2Elt_jQo1o$yxVUDJ9G99Hd(5f zs$H_EE}VP4oCJ4zSJ?KEuPT%C;d*>rLh)Y3dye+>4?%%cen_a8BF89(BH}(OS@3R{ zS?Y;pheioVc@;Zhj&vov%)44n&x3jIWe>oC!M|>hrDcliY?8#gecdgM6pwDJSq24W zK9G`V?XF;5tr;nH6K`gwwWYDI#z-+N`*l*+Lj2EbLe2*SYj_b7aV0NYtkq?gRa+P_ zqh@;ttD~oP;$(z}o{Moit8{B-_+3gAumdJB9k{PYPgr0Xfz9C!ugSrV{-MOXhoL+{ z&qD}rl6GiaoRgz?L6D&2dxyt`l4L9#pExs_vA!cKc*o7Xe{(_KBov1%-fk`+aohSs zf-?M+Tb;ReJkxYI8%(+L$)l)}#kHZ=2%m+|wX1`@TW62(VQdC#QhoyD64R33u9${qZZ`VkTIltL(=}hCBD`p8nO+S}JfyUt2;mEoT3@ zvZuzcwV5}Mz0JM1L34fHy%zuwo#a_G0=f;X)aKp9OEy>+wsKC?-V7%%t8U8J=NPst zQaG7;W&Tx7cxqH318WEWGMv^}B^>l_Zmo4272%Y~JCSc`ixz2G9dUIDA+pEG0t$PX-4c zXmxb@_`KUwl860%AK9a96ko!MT7r&lq!Fn9HVKy9GHQXkVxuX!%MA2cAWoG7v2M=ky z^O0>+rE^9~*2Zx_gy@O#Wg@@!mbTLlCMzLi^IQolhL7{s4@F(=d$tP+w0vfL|L0r=Rt12(wy>p;h`g2kH!Yi1*R@TwFHf4H_Bv}|#SMhX-0dEz}- zSGsHrYVU_E8>0#b&-Z5Dzc;?6@$w5_ZD(leX^v{0%1Pv{%;=Hag);)~&#Li*Leb3V zT$oaa*CnMpAX&>765Bm*F6K0g&HRFm4Mm+?020{i9vB%ZYea4MK*Gq5Y%GE|B8@c> z5nP2iwI`7oMefGIAddUp-+FL{4fZ2r1LXQN9&fk5eQ*y~#z1}fd3gpwt?YHtS`})A z1ugcS-7f{jqRW2{h24TyOj`76CZ@PQ#V#3gqSSkZ1l&~0$&IL!bl10{{C6!R5$jK0 z@7cpgvlm`Ur3d-BhZw$pU1prYn^{kD%b#tWSu$~W=q>5jW2FP)VytVfy%Y0Rp{`r; z@i4^P%J8fO@>oFCS5J&}P3+L)O$%YG(C$nqb zWVd;3>wD17H2Y4?yHd8e)b8=&y@S&PW}QFJ`TO>zaf|%41{92vN`hLA%`U1GE;Fk9 z4SSTr%G8XwZP^Se3oLzfIOAor`QAq13&~Bm9%B(GgRowD5S};BnO3QM{<0bp38AjV z+{d-Ta(3fXfzW+fM9ob;L*!&2$?i-!uId-(5oInikym!V6mkzIBC>Ea1!~I~R^;)! zuQCO;n`V1UdqKNdY{?{f8F8v8R-1#jw|`k|sSgj{KnJm77a^#yS?9DDR%i9HILomP z8F;ExU-Jve2BS3R`bHI{r*qLbJIv}lnfSbAaA(J51I?OM?y*s2Hw03Y=4G=$q&{>i zB;ZA)+Yd~*KE|Zt||F9cincowd5>pZeP0`7X(cMniyy63MjpL69q@c^Fp&pa9+Vz zOfsLSovi)mv0Vl0i(8}vB&M0QJ#YCkh5ZoB2p3^5z^-!#rCV7-YSqeLS225xq^>5I z&NUdIvpy0Q9dUsrFk%>>SLUy}R!P~`LQc7JIiDqj!yzMD_aScR~Tp zA0%6&p%D_5AhUOhow5nRWyquR9UO!Uovh9k``T$mtN|rqF*IQFDdqfr-PC!@z*h6u z*r;1>qZ8#3(sA#%3d$3ar$_Q~zq)sKT7cCvS&5sDcGI+m-$kN3>BFUZ&dFzn@456? zW?vAepP);Sum*E7QtZ~Y=h+T{wk0pV%ir9z-b3Lmghf?Sfu^BLX{`K(_kTv$N zUUJx9Ay!xXpTLJOuv*IYV zcXL-t)kh#$*F{6&u?+MDD!Lq~3}nXqe5o}&m8tMuv4c$}CZkHyV^=9b9d&_C;Pd@( z7Pn(1JTMMpbeRln)!p1Co%&s$aNZ-ZVDU23LBOb38kdKi8AyV+Rhz%E(o(2g&+5To z96B)M#`vyQJJmud{34SjYTxImY9<_V^ow)Z*}#IT;9T#H6d%|5u<{hk^wT|sHSfxK zo$7ka{A$R$-DM=;@IF57)7%S_9Dayh!!ayzW`IH!)I0HAnx$lvSyyzJ0o3`3?tY_) z%m|{mr<8jj`&6LkHn^tI;fq{8hj?ALXa=!x>BTd*zzkSO-J!?K7;0T?+vSN@pOMK1MjZ&J!on0Ht>%Oujtt&V6@ckJJee|drr z1R2pfp1C|W^^)7$DeW1wux%o#-Xk1>SH&n?cEP{I#6dX9KigTD&enW)F426Da`$Zbm+ zTdV4N?-RAM$zWyu_7RaL6Qn|e(6l1Q4#=yf30y)Hht6l3g$om#O-e+^-Q@BlxnB<&LfGd&Y{rJ` zj8B|$Gv0Sx3s_iqdaxm`Z=-^yEDu_9QLBL_UvrJx54iHG)Wte3 z%7f2U{W7i3@w#%;Ivhx+am8rvGI3sv^k-FZ(~Wikn*cLt!TEq2yks15O>MEkq1tid zrzDoEh3%?LO63 M`2U*|!3*O508laa3;+NC literal 0 HcmV?d00001