From f7060939330a157fd48274bd371e4161d249f28f Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 13 Jun 2011 08:43:42 -0700 Subject: [PATCH] Support thumbnailing non-image files and straighten out setup for 'gd' Summary: Make 'gd' an explicit optional dependency, test for it in setup, and make the software behave correctly if it is not available. When generating file thumnails, provide reasonable defaults and behavior for non-image files. Test Plan: Uploaded text files, pdf files, etc., and got real thumbnails instead of a broken image. Simulated setup and gd failures and walked through setup process and image fallback for thumbnails. Reviewed By: aran Reviewers: toulouse, jungejason, tuomaspelkonen, aran CC: aran, epriestley Differential Revision: 446 --- .../PhabricatorFileTransformController.php | 63 +++++++++++++++--- .../files/storage/file/PhabricatorFile.php | 30 ++++++++- src/infrastructure/setup/PhabricatorSetup.php | 33 +++++++++ .../icon/fatcow/thumbnails/default160x120.png | Bin 0 -> 1006 bytes .../icon/fatcow/thumbnails/default60x45.png | Bin 0 -> 762 bytes .../icon/fatcow/thumbnails/image160x120.png | Bin 0 -> 1459 bytes .../icon/fatcow/thumbnails/image60x45.png | Bin 0 -> 1179 bytes .../icon/fatcow/thumbnails/pdf160x120.png | Bin 0 -> 1654 bytes .../image/icon/fatcow/thumbnails/pdf60x45.png | Bin 0 -> 1378 bytes .../icon/fatcow/thumbnails/zip160x120.png | Bin 0 -> 1383 bytes .../image/icon/fatcow/thumbnails/zip60x45.png | Bin 0 -> 1078 bytes 11 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png create mode 100644 webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png diff --git a/src/applications/files/controller/transform/PhabricatorFileTransformController.php b/src/applications/files/controller/transform/PhabricatorFileTransformController.php index c766bbbaca..7f3a42253d 100644 --- a/src/applications/files/controller/transform/PhabricatorFileTransformController.php +++ b/src/applications/files/controller/transform/PhabricatorFileTransformController.php @@ -43,12 +43,10 @@ class PhabricatorFileTransformController extends PhabricatorFileController { return new Aphront404Response(); } - if (!$file->isViewableInBrowser()) { - return new Aphront400Response(); - } + $type = $file->getMimeType(); - if (!$file->isTransformableImage()) { - return new Aphront400Response(); + if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) { + return $this->buildDefaultTransformation($file); } switch ($this->transform) { @@ -78,6 +76,39 @@ class PhabricatorFileTransformController extends PhabricatorFileController { return $this->buildTransformedFileResponse($xform); } + private function buildDefaultTransformation(PhabricatorFile $file) { + static $regexps = array( + '@application/zip@' => 'zip', + '@image/@' => 'image', + '@application/pdf@' => 'pdf', + '@.*@' => 'default', + ); + + $type = $file->getMimeType(); + $prefix = 'default'; + foreach ($regexps as $regexp => $implied_prefix) { + if (preg_match($regexp, $type)) { + $prefix = $implied_prefix; + break; + } + } + + switch ($this->transform) { + case 'thumb-160x120': + $suffix = '160x120'; + break; + case 'thumb-60x45': + $suffix = '60x45'; + break; + default: + throw new Exception("Unsupported transformation type!"); + } + + $path = "/rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png"; + return id(new AphrontRedirectResponse()) + ->setURI($path); + } + private function buildTransformedFileResponse( PhabricatorTransformedFile $xform) { @@ -123,9 +154,25 @@ class PhabricatorFileTransformController extends PhabricatorFileController { $dx, $dy, $scale * $dx, $scale * $dy); - ob_start(); - imagejpeg($dst); - return ob_get_clean(); + $img = null; + + if (function_exists('imagejpeg')) { + ob_start(); + imagejpeg($dst); + $img = ob_get_clean(); + } else if (function_exists('imagepng')) { + ob_start(); + imagepng($dst); + $img = ob_get_clean(); + } else if (function_exists('imagegif')) { + ob_start(); + imagegif($dst); + $img = ob_get_clean(); + } else { + throw new Exception("No image generation functions exist!"); + } + + return $img; } } diff --git a/src/applications/files/storage/file/PhabricatorFile.php b/src/applications/files/storage/file/PhabricatorFile.php index b2a53948cd..0c70be9320 100644 --- a/src/applications/files/storage/file/PhabricatorFile.php +++ b/src/applications/files/storage/file/PhabricatorFile.php @@ -216,7 +216,35 @@ class PhabricatorFile extends PhabricatorFileDAO { } public function isTransformableImage() { - return preg_match('@^image/(gif|png|jpe?g)@', $this->getViewableMimeType()); + + // NOTE: The way the 'gd' extension works in PHP is that you can install it + // with support for only some file types, so it might be able to handle + // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup + // warns you if you don't have complete support. + + $matches = null; + $ok = preg_match( + '@^image/(gif|png|jpe?g)@', + $this->getViewableMimeType(), + $matches); + if (!$ok) { + return false; + } + + switch ($matches[1]) { + case 'jpg'; + case 'jpeg': + return function_exists('imagejpeg'); + break; + case 'png': + return function_exists('imagepng'); + break; + case 'gif': + return function_exists('imagegif'); + break; + default: + throw new Exception('Unknown type matched as image MIME type.'); + } } public function getViewableMimeType() { diff --git a/src/infrastructure/setup/PhabricatorSetup.php b/src/infrastructure/setup/PhabricatorSetup.php index 023de19800..ff4e559b79 100644 --- a/src/infrastructure/setup/PhabricatorSetup.php +++ b/src/infrastructure/setup/PhabricatorSetup.php @@ -165,6 +165,39 @@ class PhabricatorSetup { self::write("[OKAY] Basic configuration OKAY\n"); + + $issue_gd_warning = false; + self::writeHeader('GD LIBRARY'); + if (extension_loaded('gd')) { + self::write(" okay Extension 'gd' is loaded.\n"); + $image_type_map = array( + 'imagepng' => 'PNG', + 'imagegif' => 'GIF', + 'imagejpeg' => 'JPEG', + ); + foreach ($image_type_map as $function => $image_type) { + if (function_exists($function)) { + self::write(" okay Support for '{$image_type}' is available.\n"); + } else { + self::write(" warn Support for '{$image_type}' is not available!\n"); + $issue_gd_warning = true; + } + } + } else { + self::write(" warn Extension 'gd' is not loaded.\n"); + $issue_gd_warning = true; + } + + if ($issue_gd_warning) { + self::write( + "[WARN] The 'gd' library is missing or lacks full support. ". + "Phabricator will not be able to generate image thumbnails without ". + "gd.\n"); + } else { + self::write("[OKAY] 'gd' loaded and has full image type support.\n"); + } + + self::writeHeader('FACEBOOK INTEGRATION'); $fb_auth = PhabricatorEnv::getEnvConfig('facebook.auth-enabled'); if (!$fb_auth) { diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png new file mode 100644 index 0000000000000000000000000000000000000000..16d6fd4f907913415c3387189dd1129385865575 GIT binary patch literal 1006 zcmeAS@N?(olHy`uVBq!ia0vp^3xK$SgAGWQF8Y}dq$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~;1FfdQ>ba4!+xb^0)ZN7**L&L+Fd--z&1!DvpMYu8)R$f}l zP$l5f_w$AD6sbT*$3_9sDSsNjY)s-2TGY5-AWEfa!li})A%zjxPd`XfkS|?iBw9-K|q0tg%O!*IkwgJptj+$ zZ`X>iKb!yc(ZQv0AJzK4-aoN%>d_0lf3@r9{{NQo>(l(HeIMB-GSy5!skBf1Ip3OB zD(3aSldFC`y>q@>oKN06{+rKghSjqq*3>A*?ER~7{~u{+r3@MDOsE5 zx|#p6>M3~lO?|F+1+ z|17+J=E}^UmDlg)Ij!>biMrS&Jt4yI)wI)> znUw$84Gj19v!-8V*7PQucyC$-NL{c@ltAoUqhtTw}Xee3;w_C zF8UfB$IxE=%l!6ox7*bwyP4K(U`l(sEo%L=AJ?+gl@P}*%tBj{OnxDbs`DNBUmoKSa)%K#mr+#vp%bC)^+&D z{wLXr$)!QTfrW!qs)NC?fkOZoY)I6s@9P2w-fDjS&y06{XpDhTGIYQW*a16W2ke0VGhjs26OUeYw?mxmhdf{L zIX#6?gJ}8d<15$ca&NyeBI58Q!#dfa=7U#eSX){BY{u>d0rQs?s}{9}vStJTp zzx3eJ=_3R^-EQ0sI)75y2H@O$3S1L~@8MrP9;a0P$j0k6c)Ru;W~Yvp1}zTkQ7RYp z%vudiUV}F&bg($nrkWOo76Z=ur$@?h$1UP%K~s7wIMF-+lg$RD_ZD2F^j^Moq20RQ z?a~O5exs!o)}$CWEFAD5Wdh%rJ_UH^Ipja&n^6=uvcB>!(^Z3f(>h>QY*1jfhz5S# z*oJ2d=VIT_!YIz4x}CsS3|P_F_1-S5ZSKbE<^rcRa(=Rvl?z&#(jL_zV$taEx3yVW zOJ4aJ_E1C)BO~a)Nog5isEEQUNlyu2tct>;0c;>&ON*PSKn>e!rPZ*|7OXloJTkzV zia1_02y`thp*+NaYYaRRT{?|!<^894eJo)SWy;*yk@xfEn!8iLAZz+HWb+FB*5}7C^29^-4!M?A5wWJ^xx7rolcUsN_c&8fZxbJP^=ra0Q)R5uRBUw^Zz8@ sqFDP@09VNzumg6$4%h+PY`+8;0Iwg8LNtyxvH$=807*qoM6N<$g3|6-kpKVy literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png new file mode 100644 index 0000000000000000000000000000000000000000..90cc11c02d86c9654463b2148399cc4faa998ed6 GIT binary patch literal 1459 zcmeAS@N?(olHy`uVBq!ia0vp^3xK$SgAGWQF8Y}dq$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~;1FtCPtx;TbZ+>mA-6Y;H2|ZxX-lEqbt-_3=?Tk(P$`0PQZ;g;F`py+1E5xjA?4 z85^J>vs08k%bPk`f7U$Mn4eyD|Ip*f@4wgHf0NQ%?8hJ{GEI|#1qd8~fIP>RRpBqqp_5zT?_brwGI?ewX zztObaqDp4{+>Sk;{=MP;wPtayEaSpuZxrb?9K*`iV0f; zT)p-dR|Y;!6b^6n`yF-k7t_+vgqL5M2Z{hDk(6T9esa6b=#@M zi={U1t9i3%jhcVwmv=4Ce*c?cKi%!;rI022|I8Fwr|f!j;+|?}pZ{(h3%0iC^+g*U zUGhN9wIlqP`+=C>;mh;wW9N__tbEIPG7p;aQ)wpS+y_X7pi`` zd-U|A+&?<%ezkhr)*So%_z}>tx7#LaEO>KbyLX7~#1##%>uc3Le+N!I^G2)ww9(X@ zm$wVvhIfF1jN`@8B4*daaZJC&f0@QA%4&X|HzUmCQn1LZs%k5@<&)OMF{`;abWKp) z@s=aQS#V>1_icmobN+=+nwa@AE!(KH)Y#nU^on&#Y7BuK=PTnGRYgSAB(kj+Pby#i^3Tg(JyY(+-Ppjl@%*ok&r0uzs);kUKVE-d z>i32(n+3lf=j)GLwu|rj{^+$CIpOou-)5}iT)IVGKasJc!FP3nsoSO;ZLS*)djyIf zro2d#uGMv6Tf#g2lTN4sliB)~xl=Cfy?*sU+HBoh-+mi8{YdkbJa*WgB}XS=TKQ#z23#-Zpdc)GSYkrNfDm0w z7P9V->||%Ud%DW^s;W1$(M?vSmT5>AJf^05`b~Z6{is(pL&g|9NloDiYYlJ%+yFPg z4R8ZoqZW^ALwUFt@mx9ky412`S$Dcub<>%X)rcmZj%|qe(~N3Q#h~0b?sZQi z_8R^TMxSoOKRy{csr1%zS&B*Aj;z+V zi!eQsOfxxP1G5(XNrzqfSTefdT6F; z$j2WFaizjrI4E$HofBa!O6S+)qu6ktv#cbvxNou^znu^zT}czFd=giHA)RAYc6wrD^jT+eXp53CeF;52>Jy*Q;w8!{mU)!Ugv) z&s;?5-7zw5SyINuxX9YAkJZdt}Nn)S+XnlSx4JJ zRxuofvv9DaWwtG>Y|`0P{Ic}Y@!t@&X3@(wFmq+giuC20+*3ZeiN4BnYGdJQM`#NL znnaqo;$pWmesSwo(!#4JzDG0(T)i@5ug{%cLV2Ivdf=XoygpThNbO>nwN2f69=}c> zlNw`JVAv!?c7{}Yf|nyVjO*9tpnH8=pjg`~(YMHY*XmhmN&QReY2;ap$EzN16LNKB ze$Pdg>hb$5^)p2_)r-EUwzae!ze1J`Jn&vNyd~(uQQOVBIVY~6NwTG>s_*8(#hFI$ zevgf(_7-%zZ655-OIv**PmF>CXGIrl8+RP=$R{b<>Z=`am7L`K>65do!O!qYUu}i> zWjCmWr44lWQ%kB_3~wLB@T;c4Ge{4rqY&UhfMcchXM_ES?-?s_8lyY+!Tgk*8}G+I zDC~xVVNHMir%?+o8udROM09{(W?7~KNAJJV1Ir4_pFHNxaRPJYj7fV+x4%sW%5^7t tqx`l+&A)gW;0Cw>Zh#x$`m}!p7y#Z%ch+0IL<|4`002ovPDHLkV1o2xKfV9} literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png new file mode 100644 index 0000000000000000000000000000000000000000..20f08f955bf72d402ad77cde599dec462218ba4b GIT binary patch literal 1654 zcmbuAYd8}M7{^CjvYt6DcHB#9>23ujhHrw|>v_zW?8+_rv?;pXTA_1Oe-S0RRBR#TkWB zWQk%ZTQ)0pbm?-qBDAld@mIXUFJ8GyiX;OZ{KNgo1}>qb05XP5@~1`!$j1PH%~~!f z2P|dq+x_5FELrQ#&w#EEG2&s|KZCjL9~w+Iv(tnJs#ABS8ye-kawU0cq%wBWQy1u{ z^T=&xst?RL(x7KS*BqQRXE0&uMn2Bik%OX6p)ad%z~;+*k)@?{N^Wb(+%B3{>F&Dx znKvhqG+8px_tf{|~e#-aBZVWhMkSh73RJTuJGoQdn6+ba4T2F z%0^f(DbK~q*FZQdHZYbj+|liG)-C@f6mvW2W{t|G@-^|fY=pVim%MlQQvriNF=R43 zsIt=3I&36fLls&_RN9(hR*y%aXDP+WnSG)=hjO>6LJuZ}3>BZLUVF)1i6srGRX#mh z1BJtBy%B^^nO5%@3x(v8BBRy4aVLVHFDbxN!i3*Isw=b2g(1jTn26GAF4zRnMXP?geC_VdQKa+ zxbAJT#s!==5t47Wi-E&0`B=1}!QLm>?X8XEH>Ho)Ls_Q)u$i(m_Ptc+QF+eiZhDy* z2=m-nyz^A#2-3ToR94ABQg2P>-t>9PP9s`QX@+F%+$TF> zr1-;rV$6dv?NF7aj@) z9n`hr-9#lq-FK?VE6qNvEvHc!I{c`;NcJLu=s&=mYI^S2mU3129e((!XIL@`%zx)q zci(rFkU%T<6XpT)#^XOun`IO1&}nJC$&FUW#dhVManDQ<~$aoutQtYRpeFC zgogXZeI7V!DT>gD`-Fr0;!ETeD&rj#q*rrubdV@H{2Qg=RlbfZMP5;#$gN(yy<52z z*Y9}1JTIQB-yRb&i)kAT&RSiC!o{>QQRXT+gO6LJsbkj!Qza+Qj>`!s# z(P5WlvgPW(s{HD;`R`xBfyqiN){QBk9g z@1hYbJ*%VLt@s$<*`+&_0ik})6Zc8GhtO>kD>y55ak}nhP1i(YFJrbEce1B+bFNil zRe#kZmGx)ea`do$^5pk>?9{3*P4~0sGs^iCW4fDqPwms2x57{s7JoWrn$CW^-P83q zn0aK(<9m5={PAr6I{jYqM^;*l`j^vt1h%l5^n*oW4ta>%Gq$+A0eidUtD~Wb4fW)2 z2}|W_N6at_trIJf8?ciJoZ&!`^u*dBe;lMp-%Ti%n(GSYlXy|R56dHWce?hmnxcZ~ zf^nRxi_P7kfqR#^@|A@L7Czc-qL)ioM z8s<0_-ywU6O=*V;y83GU0#5I#nLJXnucXo;MpJ-L{yY*Zn1`-cT*yolK2u`0kMB^r z7OG`$4*)8#SMUo0+yhEd0NA47zhZskIYs`C literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png new file mode 100644 index 0000000000000000000000000000000000000000..8a16eaf488e00f922e2ce0d77c0f6ec43ba311da GIT binary patch literal 1378 zcmV-o1)chdP)OP7(~*v7V$+2foPG6FPdZ%n_GnlQoKMf z+L#hrq7+e+wp5a?O`6*#o!y!7KeMxI+-{m(XVab7{o!MVGv}Y3@BG(u5~k}qZY2?L zi%FJnmT;Ev(iVpmWyYAJqpPc%ZZ_$fhSc&spUogJ231v$JYQ8+MFZiaG_6z?uKZs? zl1qejt1>b&qBr~fo%B9&vA@}2(L?0uY7Y*b=N30oqoOrpP)0ZAbJBdFemWZ&i#=!dCoH5=gUu|>N>%}< z&wx!%MhHh66Sf2hGbWmuI!SPyRYgE=KXBWMIDOInF`T+p%*JE=qyJbn3cK!ylZQvYo$d#EaU$64mBlz042EFB*nqJNKKDklOdu3cx`^*!N3=%ep@`JhS zb~$qwAsj~d%m8M0)gjma30zyANqm43>%qt&rh;fx#`_xsF+B*iFClY~)1fG|c-GMcoQu1|J$cjp?%F>@_Ugf&Tm z=h849{^&EDt=omtoqO@ahx?#>)kB{h;)hrBdW~^*9U&XyVV}0pv$EMHD=w!?adl0N zPf?VAy(p}z+Qdvy?K=MJ$MtvR)xWLh>_0ko`QYL5wLd#`-RPQc^lqoq$y%D5Ptffj z@rQVpzS$ySo;=MCR0{4)3Q-3+k$X3?1Xs; z=MxIe%*w5Fg%FI(O*bN}C2HTSd~-U=6907-iT?mLVUF{~nBfhUm8K!%F}xHd`!5e! k!db#u!db$0mcIoU0Lp_RQgVwEW&i*H07*qoM6N<$f~#YcIRF3v literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe19e59f601befc61f4eb320229d6748236e536 GIT binary patch literal 1383 zcmeAS@N?(olHy`uVBq!ia0vp^3xK$SgAGWQF8Y}dq$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~;1FtBoZx;TbZ+GHcZz2HBP z;G<%v?Dmp-VdokV;irx2$w?Ix#pN%|S6KMcNt&bcW&8y8dIlz531(Hd1n=#knnGgb zw$IPKx%BQ_TKP$V+8_I#JV|?hX4}cL(~IBjH1^$_wpo+mzo?xz0|yW^00D&zoCZv= zlAQK{bKdjT`)74^a^-{wC1=t;Z$*S&>VS*JWJ9z1ZoxxM`Tvh1r_Tfe-x@-yDQ zrs{x7&5=z(P3);_rq0c~6EkVH_x4>$FR$enmH(VqQ}m=G#YFgAs8-STU2oP&rayna zG(zW8`uq1YfBgFW*}O#S^d_YT2VP&)>I;k0ZBlr_G57M%Q)han=4h_&SX&+2d2Qpi zU202%JXLGNIR!tg{NC)RB|UkOjqTfatLND5jdbWWm_B(`+R2X}3sqbWEEhMhRMJcj zwRwEu;o-8)v$H3j?)9+axh+(;FaP|J&R~y@2IqvO*S_yvcR}mTu}z)J&tz%xJ<48p zxb*&6kA(uKiWe__TU7I<`McV_nL87tk50I{l>43Uot6sO$hDhIW>@^Ix!1Yl*EZUHnH9>e{xY;vY>*IS%12JZs(5)8L$}FQH<0F!Z97 z?X%j`CHE3;iU<_E5V~`&#ZzCrVlRJ!fB<8=q5i9k()#JwqIGf^1qB&@Kg_Qv*MEEF z>E}2Frf-Ya?LETJ%{{NW)^6?hACJtFUR_&y;MvlLsV9AxehQVncX31U)Ge=<>VA1I zW&d)P{>EDzGgJ~B+rv%V=3iVNaXsRiY5jpLWraWT-|pxzDgXfk6A)0yz-55Lt3TQY a7@{7Uo=|XDd>vSFGI+ZBxvXK@`B>%r5P=fTAX~ z$jJop3t;fZAHC_NhL}Ji6*zg|WK8@D`XL}Ke+nT&y%?_Y5fEs^1Ak(o1zKt-npoQP zz1eM3+O3i}tGi(*c^UT2>~?>_JEz-R2dIpjHz#y zmy`I%1HDuNtIW<&6jcSE&j*}yI4u;QfKoE&bH<=pEWUj_I5>!xr&hlsrDmXq{HghC-0=#SV+psD5^A4Gib3ItJS(Xfd&Er0bV_~tFsf(noz!zo<=F_ z$?!0xvWIvXKsj~}a0QH80vpc6g)1*27=icn+{L*H!GsuFh}lTs+g=q0i%zO8GD$(71A>!U8$xhHR8z9 z^1K$Nd^{72frgQo(i125M?EW9?|C~uKgmx^3Ns*Tgk$tpeVBFvK~ZVVO;6*sD)d(K zg(%GVz}zASbJM!(Yaigt*8@1r6PC3_=9{?Ja zmgKsrR0=M&UWVr{UW%TsN#D2}WiE))+&V(j+6gX`%|H&HuNDO&z79qg)RBnqTyET^VPtwXkBA{%7lJFu!SKvk2_dM6Z~@ zMnr}NU&37M($}uGVWeIYu1rz6qvOUOT-pu5R2|h+ggVoqh=8wNy{?;2dK$M1cHo96 zO$P$Y3K+pk4fLEW$a&RLJK@X*~YczoC4g0T{DW$rUL0F w$_cKu;0RzhHSZtd9OV07*qoM6N<$g2S-(b^rhX literal 0 HcmV?d00001