From 70b83101d46a098dac2d86d4eeebe7cc89784b76 Mon Sep 17 00:00:00 2001 From: Bernie Jenny Date: Mon, 2 Apr 2018 21:18:25 +1000 Subject: [PATCH] Added grayscale simulation --- src/ika/colororacle/ColorOracle.java | 84 +++++--------- src/ika/colororacle/Simulator.java | 158 ++++++++++++++++++++++----- src/ika/icons/grayscalepanel.png | Bin 0 -> 11945 bytes 3 files changed, 156 insertions(+), 86 deletions(-) create mode 100644 src/ika/icons/grayscalepanel.png diff --git a/src/ika/colororacle/ColorOracle.java b/src/ika/colororacle/ColorOracle.java index 51b0b56..eaa8ff5 100755 --- a/src/ika/colororacle/ColorOracle.java +++ b/src/ika/colororacle/ColorOracle.java @@ -44,19 +44,12 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList private static final String MENUICON = "menuIcon.gif"; /** - * The information panel image for deuteranopia. + * The information panels for the different types of simulation. */ private final Image deutanPanel = loadImage("deutanpanel.png"); - - /** - * The information panel image for protanopia. - */ private final Image protanPanel = loadImage("protanpanel.png"); - - /** - * The information panel image for tritanopia. - */ private final Image tritanPanel = loadImage("tritanpanel.png"); + private final Image grayscalePanel = loadImage("grayscalepanel.png"); /** * Wait a few milliseconds before taking a screenshot until the menu has @@ -67,9 +60,9 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList /** * Enumerate the four possible states of the current simulation. */ - enum Simulation { + protected enum Simulation { - normal, deutan, protan, tritan + normal, deutan, protan, tritan, grayscale } /** @@ -77,11 +70,6 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList */ private Simulation currentSimulation = Simulation.normal; - /** - * A flag that is true when a save-as dialog is open and false otherwise. - */ - private boolean currentlySavingImage = false; - /** * The about dialog. */ @@ -93,24 +81,14 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList private final Simulator simulator = new Simulator(); /** - * A menu item for normal vision that will be added to the tray menu. + * Menu items for different types of vision that will be added to the tray + * menu. */ private final CheckboxMenuItem normalMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for deuteranopia that will be added to the tray menu. - */ private final CheckboxMenuItem deutanMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for protanopia that will be added to the tray menu. - */ private final CheckboxMenuItem protanMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for tritanopia that will be added to the tray menu. - */ private final CheckboxMenuItem tritanMenuItem = new CheckboxMenuItem(); + private final CheckboxMenuItem grayscaleMenuItem = new CheckboxMenuItem(); /** * The About menu item that will be added to the tray menu. @@ -408,6 +386,21 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList }); menu.add(tritanMenuItem); + // grayscale vision + grayscaleMenuItem.setLabel("Grayscale"); + grayscaleMenuItem.addItemListener(new java.awt.event.ItemListener() { + + @Override + public void itemStateChanged(ItemEvent evt) { + if (evt.getStateChange() == ItemEvent.SELECTED) { + simulate(ColorOracle.Simulation.grayscale); + } else if (currentSimulation == Simulation.grayscale) { + grayscaleMenuItem.setState(true); // this will not trigger another event + } + } + }); + menu.add(grayscaleMenuItem); + menu.addSeparator(); // about @@ -438,22 +431,6 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList } - /** - * Returns the name of the current simulation. - */ - private String currentSimulationName() { - switch (currentSimulation) { - case deutan: - return "Deuteranopia"; - case protan: - return "Protanopia"; - case tritan: - return "Tritanopia"; - default: - return ""; - } - } - /** * Takes a screenshot, simulates color-impaired vision on the screenshot and * shows the simulation. @@ -484,9 +461,7 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList // apply a simulation filter to the screenshot BufferedImage img = simulator.filter(screen.screenshotImage); - //img = computeDifference(img, screen.screenshotImage); - // ImageIO.write(img, "png", new File("screen" + Screen.getScreens().indexOf(screen) + ".png")); // show the result of the simulation in a window screen.showSimulationImage(img, this, panel); } @@ -519,23 +494,15 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList } /** - * Updates the menu: makes sure only one item has a check mark, enables or - * disables the menu items, and changes the title of the Save menu item. + * Updates the menu: makes sure only one item has a check mark. */ private void updateMenuState() { - // make sure only one menu item is checked normalMenuItem.setState(currentSimulation == Simulation.normal); deutanMenuItem.setState(currentSimulation == Simulation.deutan); protanMenuItem.setState(currentSimulation == Simulation.protan); tritanMenuItem.setState(currentSimulation == Simulation.tritan); - - // disable menu items if the Save dialog is in the foreground. - normalMenuItem.setEnabled(!currentlySavingImage); - deutanMenuItem.setEnabled(!currentlySavingImage); - protanMenuItem.setEnabled(!currentlySavingImage); - tritanMenuItem.setEnabled(!currentlySavingImage); - aboutMenuItem.setEnabled(!currentlySavingImage); + grayscaleMenuItem.setState(currentSimulation == Simulation.grayscale); } /** @@ -587,6 +554,9 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList case tritan: simulateAndShow(tritanPanel); break; + case grayscale: + simulateAndShow(grayscalePanel); + break; } } catch (Exception ex) { Logger.getLogger(ColorOracle.class.getName()).log(Level.SEVERE, null, ex); diff --git a/src/ika/colororacle/Simulator.java b/src/ika/colororacle/Simulator.java index 225849b..9b4537d 100755 --- a/src/ika/colororacle/Simulator.java +++ b/src/ika/colororacle/Simulator.java @@ -27,47 +27,48 @@ public class Simulator { /** * Default screen gamma on Windows is 2.2. */ - static final double GAMMA = 2.2; - static final double GAMMA_INV = 1. / GAMMA; + private static final double GAMMA = 2.2; + private static final double GAMMA_INV = 1. / GAMMA; + /** * A lookup table for the conversion from gamma-corrected sRGB values * [0..255] to linear RGB values [0..32767]. */ - static final short[] rgb2lin_red_LUT; + private static final short[] SRGB_TO_LINRGB; static { - // initialize rgb2lin_red_LUT - rgb2lin_red_LUT = new short[256]; + // initialize SRGB_TO_LINRGB + SRGB_TO_LINRGB = new short[256]; for (int i = 0; i < 256; i++) { // compute linear rgb between 0 and 1 final double lin = (0.992052 * Math.pow(i / 255., GAMMA) + 0.003974); // scale linear rgb to 0..32767 - rgb2lin_red_LUT[i] = (short) (lin * 32767.); + SRGB_TO_LINRGB[i] = (short) (lin * 32767.); } } /** * A lookup table for the conversion of linear RGB values [0..255] to * gamma-corrected sRGB values [0..255]. */ - static final byte[] lin2rgb_LUT; + private static final byte[] LINRGB_TO_SRGB; static { - // initialize lin2rgb_LUT - lin2rgb_LUT = new byte[256]; + // initialize LINRGB_TO_SRGB + LINRGB_TO_SRGB = new byte[256]; for (int i = 0; i < 256; i++) { - lin2rgb_LUT[i] = (byte) (255. * Math.pow(i / 255., GAMMA_INV)); + LINRGB_TO_SRGB[i] = (byte) (255. * Math.pow(i / 255., GAMMA_INV)); } } /** * Use this BufferedImageOp for the simulation. */ - BufferedImageOp op; + private BufferedImageOp op; /** * Creates a new instance of Simulator */ - public Simulator() { + protected Simulator() { } /** @@ -76,7 +77,7 @@ public class Simulator { * @normal The image with normal vision. * @return The image with simulated color vision impairment. */ - public BufferedImage filter(BufferedImage normal) { + protected BufferedImage filter(BufferedImage normal) { return op.filter(normal, null); } @@ -85,7 +86,7 @@ public class Simulator { * * @param simulationType The type of impairment to simulate. */ - void simulate(Simulation simulationType) { + protected void simulate(Simulation simulationType) { switch (simulationType) { case deutan: op = new RedGreenFilter(9591, 23173, -730); @@ -96,6 +97,9 @@ public class Simulator { case tritan: op = new TritanFilter(); break; + case grayscale: + op = new GrayscaleFilter(); + break; } } @@ -141,9 +145,9 @@ public class Simulator { final int b = 0xff & in; // get linear rgb values in the range 0..2^15-1 - final int r_lin = rgb2lin_red_LUT[r]; - final int g_lin = rgb2lin_red_LUT[g]; - final int b_lin = rgb2lin_red_LUT[b]; + final int r_lin = SRGB_TO_LINRGB[r]; + final int g_lin = SRGB_TO_LINRGB[g]; + final int b_lin = SRGB_TO_LINRGB[b]; // simulated red and green are identical // scale the matrix values to 0..2^15 for integer computations @@ -168,10 +172,10 @@ public class Simulator { } // convert reduced linear rgb to gamma corrected rgb - int red = lin2rgb_LUT[r_blind]; + int red = LINRGB_TO_SRGB[r_blind]; red = red >= 0 ? red : 256 + red; // from unsigned to signed - int blue = lin2rgb_LUT[b_blind]; - blue = blue >= 0 ? blue : 256 + blue; // from unsigned to signed + int blue = LINRGB_TO_SRGB[b_blind]; + blue = blue >= 0 ? blue : 256 + blue; // from unsigned to signed final int out = 0xff000000 | red << 16 | red << 8 | blue; @@ -223,9 +227,6 @@ public class Simulator { */ private class TritanFilter implements BufferedImageOp { - public TritanFilter() { - } - @Override public BufferedImage filter(BufferedImage src, BufferedImage dst) { @@ -278,9 +279,9 @@ public class Simulator { int b = 0xff & rgb; // get linear rgb values in the range 0..2^15-1 - r = rgb2lin_red_LUT[r]; - g = rgb2lin_red_LUT[g]; - b = rgb2lin_red_LUT[b]; + r = SRGB_TO_LINRGB[r]; + g = SRGB_TO_LINRGB[g]; + b = SRGB_TO_LINRGB[b]; /* Convert to LMS (dot product with transform matrix) */ final float L = (r * 0.05059983f + g * 0.08585369f + b * 0.00952420f) / 32767.f; @@ -310,7 +311,7 @@ public class Simulator { } else if (ired > 255) { ired = 255; } else { - ired = lin2rgb_LUT[ired]; + ired = LINRGB_TO_SRGB[ired]; ired = ired >= 0 ? ired : 256 + ired; // from unsigned to signed } if (igreen < 0) { @@ -318,7 +319,7 @@ public class Simulator { } else if (igreen > 255) { igreen = 255; } else { - igreen = lin2rgb_LUT[igreen]; + igreen = LINRGB_TO_SRGB[igreen]; igreen = igreen >= 0 ? igreen : 256 + igreen; // from unsigned to signed } if (iblue < 0) { @@ -326,7 +327,7 @@ public class Simulator { } else if (iblue > 255) { iblue = 255; } else { - iblue = lin2rgb_LUT[iblue]; + iblue = LINRGB_TO_SRGB[iblue]; iblue = iblue >= 0 ? iblue : 256 + iblue; // from unsigned to signed } @@ -374,4 +375,103 @@ public class Simulator { return null; } } + + /** + * A filter for grayscale conversion: perceptual luminance-preserving + * conversion to grayscale. + * https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale + */ + private class GrayscaleFilter implements BufferedImageOp { + + @Override + public BufferedImage filter(BufferedImage src, BufferedImage dst) { + if (dst == null) { + dst = createCompatibleDestImage(src, null); + } + + // make sure the two images have the same size, color space, etc. + // MISSING FIXME + DataBufferInt inBuffer = (DataBufferInt) src.getRaster().getDataBuffer(); + DataBufferInt outBuffer = (DataBufferInt) dst.getRaster().getDataBuffer(); + int[] inData = inBuffer.getData(); + int[] outData = outBuffer.getData(); + + int prevIn = 0; + int prevOut = 0; + final int length = inData.length; + for (int i = 0; i < length; i++) { + final int in = inData[i]; + if (in == prevIn) { + outData[i] = prevOut; + } else { + final int rgb = inData[i]; + + final int r = (0xff0000 & rgb) >> 16; + final int g = (0xff00 & rgb) >> 8; + final int b = 0xff & rgb; + + // get linear rgb values in the range 0..2^15-1 + final int r_lin = SRGB_TO_LINRGB[r]; + final int g_lin = SRGB_TO_LINRGB[g]; + final int b_lin = SRGB_TO_LINRGB[b]; + + // perceptual luminance-preserving conversion to grayscale + // https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale + double luminance = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin; + int linRGB = ((int) (luminance)) >> 8; // divide by 2^8 to rescale + + // convert linear rgb to gamma corrected sRGB + if (linRGB < 0) { + linRGB = 0; + } else if (linRGB > 255) { + linRGB = 255; + } else { + linRGB = LINRGB_TO_SRGB[linRGB]; + linRGB = linRGB >= 0 ? linRGB : 256 + linRGB; // from unsigned to signed + } + + final int out = (int) (linRGB << 16 | linRGB << 8 | linRGB | 0xff000000); + + outData[i] = out; + prevIn = in; + prevOut = out; + } + } + + return dst; + } + + @Override + public Rectangle2D getBounds2D(BufferedImage src) { + return src.getRaster().getBounds(); + } + + @Override + public BufferedImage createCompatibleDestImage(BufferedImage src, + ColorModel destCM) { + if (destCM == null) { + destCM = src.getColorModel(); + } + int width = src.getWidth(); + int height = src.getHeight(); + BufferedImage image = new BufferedImage(destCM, + destCM.createCompatibleWritableRaster(width, height), + destCM.isAlphaPremultiplied(), null); + return image; + } + + @Override + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Float(); + } + dstPt.setLocation(srcPt.getX(), srcPt.getY()); + return dstPt; + } + + @Override + public RenderingHints getRenderingHints() { + return null; + } + } } diff --git a/src/ika/icons/grayscalepanel.png b/src/ika/icons/grayscalepanel.png new file mode 100644 index 0000000000000000000000000000000000000000..3783452a34ca52d1c9d99ed5498574f0d44fc7f8 GIT binary patch literal 11945 zcmX9^bzBth*QGm`ZdjI(2I+2=_#z;sQi~wnAlCwdbc=L@G`!>Q z{fFT*JM+vFH_koxz;(3M2=Qp~P*6|^)uGCIC@82%;QuHbEa2}|9YHT}q488P_I&B` z&eI#_ZjGW~FAqiC>w(9tbzET+_TD-5C_ZNQ_@tygs`-cTg5 zN)ritO(bY*1=w=oihYjtWyHSh!*sYS~HFmbl|s17#KDQJQ~j~FF=?G z%@C$0z$$bj)eL6_u@eI?+qM@WMZmDLe~)}t;G*FGv6K9}{W=0E2p&cY6@Up+$75M< ze&ouxihxc+!G7|Cz=u^uaUr}>;xDa`@4>jq%AV|aSmACsr2knANGwMnMZvf+@)tY^ ztQ4~F?x7ODlM{w#fi9B65ME^@7{2q2+S8C1m_}`KVrGvoQgmsTIrSSHq7FLMWE|$A zU?;~4pGZ!P%iOR*3au%ms9Pu_QPCz|>N9A8fw%I>;7kQjxK0=(MLq_C3p!O~Zhe8d z;l%{3R+wDxREWy*Ry>+NoRK`UNDJ~TIp%0HPzuG_2GPvGNPc3vp^n^3RVwOu7G~y zMi@WLq%d>++gwijzPKLfpXyckt-QW^>#XCI64<#fUy3A}slOLEiA)HI*rd)(tn+E7 z5qkAv3H^(06htyX<~2JK!aiF`&}jsoU`TY6bdfZ`B?E=K?U2YzQo!>i#=WWgR;aVY z#6BBdFcN}`R0^4Q{ zpBBZYzi_EXJB;l2H8Ti`lR}5EN#_yaxf@=B)C7j?L2~>3KNm|W%2XBc@{LPDqpG1h zX4NCraU;(WKKiWMr25)L`EN+Dmk*%*y8)#jTr%wNZfKNod(>rCmj85mTKP!w-|_!o zb1h%dyxodlB5Xxfm&YmfpuKR%iqu517C|>)k3Cc3cEh1s1;RT;EmibndP9RXx@?Ar z7t-b%YH2=TP@cUqevJ4*jPg~!y)d?QAF!5P2vyS=dlY8=4&`04h5L|=zL%vMyLQSk zp*Esf#r6iAf$6r98;GtlTb?`|9v^r*}Gx}I~xCIzq<3VnV33TdB{HR87Shp&Y# zzJ;`i{)X9KvbG81|8Jb4b-T{KaxS-og|mFsY;cJuorr=uq~3A9E=5yA<1-H0E{oZR z59#U7i_LDEkHwlFj@H$ZfPpc|DJlOD(Tks)taSJ{G0Fz`L>Ckkyz3d0S*(21q(*)w z!22kM15@TrO<0%L&~_H}_f2B~p90%U^c4MlWaLR-M@QhXvT0@EWcuZc%F42`vQ4!_ zU|=sCp=Yf}t^2oswzrQnWdm=wfc0Ie(xK%mhuw7~!%dBAXP#cMftreXN#=zkmEAGm z(r>8;hMQJ{?j}VXejRM9#dTZI+c#gW_}>vSNY+2#^S7UzB=j_=7#y)$3X zhqEF8+u2JmEJ09bGQ+j9#ZSo$SqQK;b)t0G zeQQUm7cwK0>-zr&UM+jucHUQB9o*vk32yW+ocl*oDr zsQ+xuxy+K9-&yL7*q^O_@^x}@I;S%8fw9U-0^d4iwXk^jyXaDVxBDX4F9#hV?i(OH zDy#R)-lDo#LsEeh{$YUqz+t8RW}J%0xF(T_4sc8;hH_PUYAOM(sG~6|3#pU=2u~hS zRAho=11;O2?+e;|6m`N1y50REb&dEpz1uRY!-a;$>ejuo6+TEoTM03T|6!bT_GhWd z-39kl-IkV?QHhCUAY@n6Ixn~GDTHGYHnI>)3Dv%Qmi5PMP1G*pK`80PWX%sY-^~QE z+hfu-waEWH?XucA1enc4L!=IC<V? z2?at|FY0GIOCmZb4kB2_OPqGY|Z zEN;XZX<8ZMEHZRubM4e_prHTc_2#H?B?!nVto}8350Q#2DgIv+$Z!o9v2?l0{JD!t z2=v*t(?VXhUX_C2FZDwX@uIaLguO9C(fSN-8IHzTe;=an66s{|0#@k=5!6I|u(S+Y zhyraVobAA%6+>RY4VN-OH4|DV&p7F`)?5096fVq_WfH~gY%$j?9JCa0KDs=ZKsSqR zW@Ko{NG)PN9epDGO=fI(7@qbk`Re1bSMTj%8K%pjKKu4z1Xv{2D zlHtc}+8H87=?w0{L{(>JXZ>V#Wk+4{#AA}3r9JrfaIuL*)F*I|!TTal%w^^C{8t+nvkt$@PFHaP zEOst}B$fnxx(roqaEUf1;&=Bk>2%C>SJkVp?>@H=>K@fbpBush@YQd?s~zY+9^-U`{;;*+i-f~F;CcTlJw;2 zcE9=sbMC^zLi^H^3B$J5H%##M&$%ICRCEI)BPuK~svc>0cuDi@eVb#3Yq{x;uC1eE#YppDEM~3v+(Yy{d`PBG5>%}75#o)Jd}u! z;Mze)G4!quaM6WmrXZguC1RO+mB7^`uDS&mHQDzp!}CPy4OvFe22%wS2b~8M#gJ#A zJ3(C^m7=FKAX)DYd-R5T?Y=o1v9s0OAYG$wSqtMT=nD!pAE>&nRhU{`2I6Tx+Kgs3 zn7319FQUk(XalUJaj2`mOhs39`Pq zH=&NjTirjIzYb4Y-HfcC?v5XPzh1ku7uPg098%UoM;Z98H76xosc^4*T*5ImN?Wxf z>rLLPOZe)e1?2G8q;%`o>~;mNd>;c&8di{4oyDvnt=j;N4f)g#PX<}bap&E)oHTAi z?qqD`KlB(NM1C$V!gw0`=``djRBO#xn6hcfT~iF1R1RCzRvmxh~E!ne!L?e_^bV%9-R_l3HMy)Zj(v2^ z(Mdf{GcudkF*P;iSg3u~xGdj*GaHf|nD!+X{Z7MVgz^V={zF=YhWwQ^3*GaLT>qHJoxb|8eA#JTR$kT+X^I}S;EK5Z8?Fg`AReDIGfUZ zlIpPby!~MZo0xGa{pcg$`-!%~w3Nq@MIbm1s5BCXr&OcB{mi7WkgNa#n#Pc5%=Je< zBgM!h*Upu`v4tIga!a$avZU>-3QltR`}*m%6^T*M# z94X@O4k+Oafk3YK{tOXwd~|ZywCaqa#7?$fU>h@$0h1*}4n02H?PHW4lvGxZrsYiq zU$w1z(Z~e_@Ry5v9WAw_*=ln8bhvMQ=f`k6>~#Rq9cVL?U3cmhsy1|-4$$4bMAt$M zWE-RLP-(QCD44Ien+ie+qYA{UQag-cO&n?ga7QaVXy;FHHDB97olALHnZ5N#O9__A z6KdJzVZmOZ_Xl%-!|JhLF}MPG(mTXyt_CKBp<9)TffG()8sy@b;NmMsJ>1sVUgc`5 z2Bx8D$8J$9*uD~H5%eLT7GQ>jnZ{FJ^+LGHNMXj77-3bs*R?4VypHRXhuKVPa0s9A%l>ePf!RFeZtSub8#51%-Q<>&~J{xduhTnS+`T*di2{) zuW#qwQCk*M&^3#hjNi`Be6^mt6VnYB>M)E7f-gEwrzOJqT#Ziib!?D@bo@i~LOV4u z*Cz&&39R+6KN+(tK97r)B>Nw=Y-31)bXms8(w*QrWLPL+)RIz&7!`9bvcR8O&ObYA z96uhf(v@gSf%5;Qq0^45l{+%Gq^xfCF0uE7`tw3)9xg4F{2OWVXbiuBdZ#+Gf}9fc`HZlDXge( z9fCU05@HoRx7YVoAnL|)s`GjyF3=_~@UP$QcJy%I^;k1$e#-6hcRx7M^JL>YtuqsA z0v-tj%U0_Bua1VQgD%U7YL07qiAHFEs$m^?@0AD=e4hgVYf1&kaxKF~Tb$hOP9g0z zQL`CFBPu!Cm~Vw?+k106yJsX$)QL_22F5Wl@~IkR3J&P?7?&~PUBJeY#aI;!e!Snq z#HT#SmGs&(%pR$^ZIOR6@!d-6*zKGtH#(+@wQ*8W!E9$5IC~yev&R^4(p{+`kri=1 znl0hcGA9~78HVVbON+x0w3{sY&;RcM{Qm4iS5e@M2FaeR`*C62xMy|L{-2L7vb0a; zT|t4Dm{cY;ie>S}E5k@OO`-r`clZ04KN(#eEx(P`Byl_a=+fEpf;AK*ZjNa{Dm*5m z(ciH3S+>sEA>7sJrnIZS2#M;r(%zDu2!+#em&9a6+%?X8lM7D#e zS)#BGUj*Myy_V=^crHvr$evWLboJ)dt5+g2!3qireDOWS;iFu9Z9esqu==gY&(^)o@Q`h(Q>a zSitp3At!E_6uvsfF+b?(?zqcbUCzG;mqR!@h|P*oRgB?D?`7lHAohvNkOOa33V8ai z>T~J$&GAEuRN=F~6$PX%YTUw~++HS8upTY|I59-9clcHu9s2jusSfsXq6`m%xoFryf;6REPhPO@Hr z=Q2XwyY0d>+^G7+>nZ-8`5m0}mR*&*zlZ1KLKE%HLwP{;muKlqF+@zt zdQa?{8n@mhiiLZQQ!;{;P!O8OrfXhH4Af*&Ozxs@x;{#x|VuJN&OyS#}i4X!rKw%4sB7{yRo! zT>mt($@P8bpYh|1ucUUTSLD6S!=IB|RE``^Jw)>#u-tg=z1hwkCvQhj*LQii(rh;V zpLYQyOn>gCuWJE6ezAJeVXU*roa1!(Pv<&ZsI85td-r=O4K4m zq9o`XepSq0?VCUO?M|1zd`i-8N9XIfm}Ir^-!1;o__$6SY>a;xvP3!uX`wC3anm<4 zQY(!krJz>*smn&@P0D$0r1N*c#(d)O`_8xjT0u(G`D)MC+;Srdo6xRytk^WTKUVRo z(kh&Dp(sTW41XyM7MqPSFntDPPi>(&UBYVH@^wSyd8N_+X#zIwBOlcBdpPBVaxV_e z=D5t&(n4q8UC_@jFY@#R&mMOM=v;VSE-DffsTxdf=LL5<0M_6}6{n+HInjU?`@4Qh zJMQkDMK}|K#e!1QSx`vgYRN;+AE?v}m5S2*a!CH%c4ITqV;^zjFc{wTJO56|JkP*< zaqhk&9b<3B@-gQx#nL7)ZqR>SK@X3tx8?d5v(buzyhgWN%4cGAFnz5$J6bh|;&5eJ zlSmRn$bT5-!4F>6U$k>J9-Ra=TpFx~k+gQkCDltWB`{jT%$^H0uSMnG;M99!iyV-Q zhQqoH&jph&)XvGiamoT9fdq?yti{&~gQ1QYqyavS_ov>%H>kV(&RuDw_tL~&KJgF6yKD*1sTK$e;* zs1_ZDR!BpPvfFfK=sJ>yHvs&>L#2#K>I^F=+pv$7R+B6vEj1|=qUARm;mx3E&(x~t z(JJ~1mcR&MT}+aDY}ON8z;s${>_6BbQ5BGgv@E09cb`sRJr)RB*E0n; zdSO(F>PJCC%vi>lj>`dZ&U@tHYPE|(=st9*dn*RkVYtUZL4krtYR(jVe_A;dC{h~B zx&^ab9}+qv3d6Ry2rm()NhRqo&YG{B`D9|L1&O~k1~!psXlURGCd{KcqZV4*O;13B zXTO_Uw}5?vvIADM-dyjl7p5c$(|ouuE-v=(=0wx&z9}Xx5-HHH)5-`*KLWpOE&8kY zSEN(S?_#HS{q?xq(|t4Ts~U@L#%wE&%tzcCkq9kIuVJ9b3X(60Ll4v8Is(cduloNO zBt313FgP`v`i3aSo@R{94JI}t%Ix3|<@%_Nhw=^8vH;*&HV(poVjP9*8<2|F;}>=y zMHUJk(ozKgclj6>n-JxNzY(L71D7pBys4<}Yh*I(m5os@R37b`WS9Q5#b=AAL}Z6y z9(1#H8B6LQ0$`ZNwZUi;6jv6Q8EVvJk6V$JkBm3y@+n-C`<0P85dJ}R$Dz^!;~})( z60HUvtd7qM5n3z$#~pY}vtMn-1Dc%)Btv#esmuf1_!(c=!`YfMciJQM+2uKF1Bl%o z4~3_jz4m{d3cOXEY}HZQst+_W5A=c^uXIdMQ1skWdWx0JqTd0f#DXHr1$`DXyoG+w z+;Cj6ZXC(Hgm?P{#EB`&?Jk|#d^A)c1x1z{0c^o8O!Yqd$7qD`4`HTpW%rU4T&wtC zAeb-iJ_13!^zdN*Ya2N^`R@@@;XjxW$vOw@vBMFSN412F$3UXvgPr+Jm*_^;Hx~#e z3RI)AscP)h4$^xNL>28(mS6YBPw!WEbtcgIH9H5$*oZdMu;V*7Li*od@t)R&z1ulE7+mSXB4kcWo9 z@Tb6(>=9QgMvn$05X16TIu{_BYY7gji7}5+2EeUV~u08gS`?HZaO-;=rJ|C))AQ9ULZQHIVLCXjviZQ%06R! zT(W6itO@%Q@5{ZJJLl&0sOo+h>JToh)Zxon^Hn0za*y=1G=pwwd?Gw$^U5#kS=+h( z&Mc0GfenBw(Q95 z$n?)-6Vf(+hs^x{$jRb8XD9R^h8fRo#?nbyRe7EkS04?lIyvI4svrn@p~3Kx16mSS z`7{)Z$NQT*=pETFV+Hh9*YzHT4gan}u5Uyyq)+?UIWb=hNokzD+u)|h!)(^1Ufwdi z;WP}{&nI(c6E!W; zSFM>Z-hfwJRRhCvpz6K0~&^k!$#Tk@FG93eCQpe5dAk!*e{C~Pnj}OFTg}>61 zZSsRC&O(WxCa>duA{29qdn!@GQ1S=T79@= z3o!)eu2$}31(rLy(BM$c4^-yXz0t%ji`C{`zdo4x?dtjj^@U;LH@Ym;m#UJCb^dcp zdC8VDnm&3Y2{HZijiWj+b=ws>M|y<1_y-aEY4ur>c_wi146ho9(wXJ7kly`=;NTK} z(OvM3FAn9-Uj#z7Htqg5r(jal2QhRTp?0nLeUY*wBM{mXrQ4xAxG_4-V zExez~u0HW%e(?L(UK@c6Dp4B4Z28xH9jd0!vj|2jyL+OI2|(N{E3UnS%WgNXVUFIW z0BIw&Ci%&9!w2U@LLlY72@7IvTm8<}UApL*Cdu0;b@wvXG@pGd%fhJQC77&66>SDb z`A*c(#+#i-cuPNIcm^h%Ua?An+;99nY(UdHomQ&v^=o)3k&jH2Rj7&emytVar9iGG z2sfdptTz-@wFp7M(tGw?GB>Y`rZuV~ej%1BJym%mVgtQt`TZOI)AlOT@GK6PD@#+! z{TrLwq{pN;4D&Y0qmba4QF!u+x7`94=%p-!EVb^{aPB>GF%5(Y+>K3iH#Aw!;@frF z$)U{Ejjl>q{Ca_2Hrf|+GSCtV&foWdT=#8lYtdj*uwcDPDJQYF)ltV1N9suDBl(E6 zmTg^mw1nbF18nZc#8!OXinV?f=z+d_!(o-)Yd*m*dB~j+|3Qw@C&^In*HB&GDOy^F zj~eAkY-JnYirNg8(?C1DB-!JxSx>c86vk3 zk#;%lk$ezfLWjZegDi+=8;HcZmKap3-qV0({tYu%p&h;!F;0j_7`vi!^RS`!#mz=P zL@0;1(AUd+Z&{}W+eWO5XVPcYsA3OK=#1M<4hic!7DZ^akJek^>bhkSP$ZJ5SXkDo z+kW*_rjD@M-LQ#rG>-@fp0-Tn3StnVO>Di(fa=}L+|^XS+TV5sDd)Ex7@6BdvSexz zB+z0Bhzc2ng49hie^XB0$ZNstPL4S9B5;rbJa24+@~9b4hkECbA_?};V!t`w-c z5wKmT(y}vRR)E4h4&M*C}VJvRvs3HmtNv>CmGweM3&fPY|DWMo&x`mU;*^)t<;)Fj> z9$z%$??lLE5bfUCn|#F#+n}SsmZ)wG(mXgHND^5nR8}wWE8-JjzK(z>3!hxHvbfWX ztdf1mT@;~q*Jji3@O8XpA1cntgg!RW@Hz9m@SrDLZrB?5RH0E< zHeh=uUcKl?%jXo!uVX!nB^e#A8dt^<@C3A%_9OEC@eGVHRbeqKo>KXB1!Om(5Gq!U zhe20XbX+1+Q_rftGMDVJQd-xo>@3sZDN&dOYT$U8-1K?*)o`MgjET~{#3YnNQ7Ew2 z75mjSPq%fM#E1PRd>UtRWlJQYvfO=meB5OqNqt~OT{`Uh7Z=gpxA%?1f3h1_(ew6! z1RDo5oBO#R_+HnS#A4tp?=0+@?a5BGP?dLT{Apc2QmAt}TQuJdO3Vxz6?&yHSp}ioLlRlB^Ymi+1B7fcC0c46N*g>JQ8BdwHHRyobMp z#notFSWse?c;DAHb6sY`*gjr~iHnw8*x{jCQitN+Q+zAw=cS$k;53aaMn(@4tz+U>6;t%j1pK_=9t ziLtwVG7)2wpzYBtk@6H$GcD$L)kvVzOT}wkV^NvD^GnEyJeDDI4iT|<-0dM`siVB{ zJ8tS=$m;yhXmt|3L>;;uU;HN=m2^=xa4D-j7XTf-sf<{5xZx^*jB^`O+F99sYw)eqY?aaw~@2fNcG+nl$&y;+(4!Gn;yf8ADkheBB{q-BoWr8yarLTD5veuM)`9-a%`j*hfb3 zat16xvHY*MvTa|xr=0xlY7S!*wiT_~( z`YO_O#33~6`5HS`D$l1Kk((aUQDPx*#orsOdKWeLOZ~BeE)wH zO9CK<5y=jIyio&GEEk*#%xYD@Qg#6@Cof8CYV2U5aBv5SL@JzMQczGx{p(XWxaqd`VxFJn0;02S zl>HlY!NXn+VnBz#cMsAhndXs}v-U5$mdkA1O9g9`JyH7HA68P0`4 z6-Bw<!Qerv!C@o9m_AjU=|wayWl_+~H3BYwpf3d<846pj> zG%2{kwANCk;hoIWdZaP|epCAT$-;jKV*?`X9H?|_Q}!SA?4>u_)dp8aiP7VeuAB66 zqB;d%?c4lJoiV;!2oMoMoCgbRGIP z4Gx@_BfH-nmJ7Q@$%O>fKTh-RxjX0J0wOwyf`IcSX3jz0o_lO-3E3s}mmOoj_|L^= z!6T&W;A`B*AjA&-qRt1;U8kxS( zrLzWk>F`8h`d)Vs@zRJO%fP1vI$CsWFcs0L0__9mWIHA1&+d%Ns$17Ar0hgk;p_d( zifw}O*Izy2f;z~J{tpz3c+`kn$SybvG$%XKq zSy@sv6ibm{{S&z1O#XUR3i|JBb&PY`CaD(-jTqX5<4~{XoJ(*hp9poYGt+xFm&{nf z_m5fs2#L9t8co}rXK754Gn>T;JRN4rd( z=#k1EY(W^=muo`}zbktqbfJ{GbYX(Hu#$7|h19ck@zFSe(0^*iWVrp+beFR)i!+|; z)V+0ujZ9J>TzQE+ZU$2q4ae?sq&nku;8j{yfUWJNX2UfLF*_wNQvx^fj$^BKLTr_d zBB5+U=tWk;5nR5#>IH?i(x7BZb44Pd9Xw?(Dj~K8Ol;tlA&V1H*Z`P#vegC~x$(K} zNlt1k)!7NCmckgPh6Ak5@}*BxY37Cml2-E0lv>Y#p;b{h>s_AbkTdVUBe8VL@|hL3 zxk}Yw&Kjw8CVds!Qa@P=)mQrODwD{n;9Y}*XhgwSoNdJTCtOS~!yY;pu5Co*<^kM#j&_H_GZe_l%@%r`D?cK?v9aF6H0pMNTR_@9q2X z?TSoNC#q@TG&2y{&6$4dFF5yBxl*OX{8c<+AebI7pjYjUFH3z*&qeKu4N3k2f$!#@EKF628T8U4 zFZo5fypSsR*)vdHS)Y2QF(M>Sz)Y@+oBf~F+kq#9x8W{LOJs7t3w(X}bwKKwP&9Tj z4n;Rs%&v&;h#m_8AqD0jTy`4pX?X-18v>BsVhXvjrdkb!jr2aJjvr7I2!en6&l+N| z-qcN^4zk+#prgkEVy6IfygyUnM((GIM0UKSdAeYAq*gm9HbqZ8jWfdpm>zo!tAPsg zd#xQS0uTUW3Y}_l2I=FG;RCC~>037lEJ~0LV^6LpRgbko;*~;_u(W9ZNu2vEKl>aN zCA^n}LY$OA%=tx{UjH=7Z^6Z>gbkUhZ}SWRQLF;K_=JNn3Turt)M6&OaGm7Q4T7cZp3TG(+cq zs_7K9@c&;`w{7oy!e