From 0c6af8a76c1146d59c0aeb47450314fbcb19c9e4 Mon Sep 17 00:00:00 2001 From: Paul Friederichsen Date: Mon, 3 Jul 2023 11:27:14 -0500 Subject: [PATCH 1/5] Start on SVG support --- hydrus/core/HydrusConstants.py | 15 ++++++++++----- hydrus/core/HydrusFileHandling.py | 3 +++ hydrus/core/HydrusText.py | 26 ++++++++++++++++++++++++-- static/svg.png | Bin 0 -> 12855 bytes 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 static/svg.png diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 865d757b..eb64f763 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -713,13 +713,14 @@ UNDETERMINED_MP4 = 50 APPLICATION_CBOR = 51 APPLICATION_WINDOWS_EXE = 52 AUDIO_WAVPACK = 53 -APPLICATION_SAI2 = 54 +APPLICATION_SAI2 = 54, +IMAGE_SVG = 55, APPLICATION_OCTET_STREAM = 100 APPLICATION_UNKNOWN = 101 GENERAL_FILETYPES = { GENERAL_APPLICATION, GENERAL_AUDIO, GENERAL_IMAGE, GENERAL_VIDEO, GENERAL_ANIMATION } -SEARCHABLE_MIMES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, APPLICATION_FLASH, VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_SAI2, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z, AUDIO_M4A, AUDIO_MP3, AUDIO_REALMEDIA, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_TRUEAUDIO, AUDIO_WMA, VIDEO_WMV, AUDIO_MKV, AUDIO_MP4, AUDIO_WAVPACK } +SEARCHABLE_MIMES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_SVG, APPLICATION_FLASH, VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_SAI2, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z, AUDIO_M4A, AUDIO_MP3, AUDIO_REALMEDIA, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_TRUEAUDIO, AUDIO_WMA, VIDEO_WMV, AUDIO_MKV, AUDIO_MP4, AUDIO_WAVPACK } STORABLE_MIMES = set( SEARCHABLE_MIMES ).union( { APPLICATION_HYDRUS_UPDATE_CONTENT, APPLICATION_HYDRUS_UPDATE_DEFINITIONS } ) @@ -727,7 +728,7 @@ ALLOWED_MIMES = set( STORABLE_MIMES ).union( { IMAGE_BMP } ) DECOMPRESSION_BOMB_IMAGES = { IMAGE_JPEG, IMAGE_PNG } -IMAGES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON } +IMAGES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_SVG } ANIMATIONS = { IMAGE_GIF, IMAGE_APNG } @@ -768,8 +769,8 @@ FILES_THAT_CAN_HAVE_EXIF = { IMAGE_JPEG, IMAGE_TIFF } # images and animations that PIL can handle FILES_THAT_CAN_HAVE_HUMAN_READABLE_EMBEDDED_METADATA = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_GIF, IMAGE_APNG } -FILES_THAT_CAN_HAVE_PIXEL_HASH = set( IMAGES ).union( { IMAGE_GIF } ) -FILES_THAT_HAVE_PERCEPTUAL_HASH = set( IMAGES ) +FILES_THAT_CAN_HAVE_PIXEL_HASH = set( IMAGES ).union( { IMAGE_GIF } ).difference( { IMAGE_SVG } ) +FILES_THAT_HAVE_PERCEPTUAL_HASH = set( IMAGES ).difference( { IMAGE_SVG } ) HYDRUS_UPDATE_FILES = ( APPLICATION_HYDRUS_UPDATE_DEFINITIONS, APPLICATION_HYDRUS_UPDATE_CONTENT ) @@ -786,6 +787,7 @@ mime_enum_lookup = { 'image/webp' : IMAGE_WEBP, 'image/tiff' : IMAGE_TIFF, 'image/x-icon' : IMAGE_ICON, + 'image/svg+xml': IMAGE_SVG, 'image/vnd.microsoft.icon' : IMAGE_ICON, 'image' : IMAGES, 'application/x-shockwave-flash' : APPLICATION_FLASH, @@ -844,6 +846,7 @@ mime_string_lookup = { IMAGE_WEBP : 'webp', IMAGE_TIFF : 'tiff', IMAGE_ICON : 'icon', + IMAGE_SVG : 'svg', APPLICATION_FLASH : 'flash', APPLICATION_OCTET_STREAM : 'application/octet-stream', APPLICATION_YAML : 'yaml', @@ -904,6 +907,7 @@ mime_mimetype_string_lookup = { IMAGE_WEBP : 'image/webp', IMAGE_TIFF : 'image/tiff', IMAGE_ICON : 'image/x-icon', + IMAGE_SVG : 'image/svg+xml', APPLICATION_FLASH : 'application/x-shockwave-flash', APPLICATION_OCTET_STREAM : 'application/octet-stream', APPLICATION_YAML : 'application/x-yaml', @@ -965,6 +969,7 @@ mime_ext_lookup = { IMAGE_WEBP : '.webp', IMAGE_TIFF : '.tiff', IMAGE_ICON : '.ico', + IMAGE_SVG : '.svg', APPLICATION_FLASH : '.swf', APPLICATION_OCTET_STREAM : '.bin', APPLICATION_YAML : '.yaml', diff --git a/hydrus/core/HydrusFileHandling.py b/hydrus/core/HydrusFileHandling.py index efebc342..45d7f621 100644 --- a/hydrus/core/HydrusFileHandling.py +++ b/hydrus/core/HydrusFileHandling.py @@ -460,6 +460,9 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ): return HC.TEXT_HTML + if HydrusText.LooksLikeSVG( bit_to_check ): + + return HC.IMAGE_SVG # it is important this goes at the end, because ffmpeg has a billion false positives! # for instance, it once thought some hydrus update files were mpegs diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py index 4504e525..77ec56c4 100644 --- a/hydrus/core/HydrusText.py +++ b/hydrus/core/HydrusText.py @@ -96,11 +96,33 @@ def LooksLikeHTML( file_data ): if isinstance( file_data, bytes ): - search_elements = ( b'PyR@kvBMRCwC#+<$CU z7KTgQWLr@DHO#h_%mjxf1s&KH3Ygg{XH=+t(y{rlqC_)eu5zrk+XEY}nWmt}fuouU14t@iF=cmZ#d$VNUhM>GwmoMPwsj_)hrrcAVA~~rpyE;iHR;Yc@ z(Qzmt9I-knce@P9pX8RWRVK-v?@y8Uo66*)y%q9F>rA=y(k$tFxl*8h{^Z4(>hJg+ zem7VBo4gD+WkfX}If9^}qvJq)*kEsC&Zzipc_RA<02xAEtxpOwg_ zdnQS?^-l6vwJ$n4Hs?Mo%Sc>!iaEyF=CYwqd`sItpe)K%WgF&Q!dgQ|$Hw^as+DIR zKe%I}nnw(9lH1imC`*+|*;b#jzp9s^jE%V9g&#?{?g~jrgbyCLU^Vk_#_WW=cJcM{ z&ej>ye{iU|MReOJQINFByWG7fEK zZtbS{40(EWmVEs48%+$IENxSDY@x2?1k<;C+mM25#f4NLHl*Bw-~Eq7xB^)+wFF^Oy%O2B!w6Aj7q;CQbgsH+zga zh^mV^&F*^#YA`rnRAUEDI<(#_f`8%_tkACtn~M*#(Sptxt(NV@Nz@@U=<>e?A3=sA zEs@*0wOO3NX6F{xw?Wi>l6WY-Wao7Ml(TrL80p_*i!bE%6s!`x5@piM;h+OhmaB0jRu4a zozEo?jp&J!<>7)0*obCR6LcoaA2gN6yo0>oSY~CS-i*g_K2F+P(2Hi|89FL?*cB&N zj={RjezPtv^2_;_ikNqh&Yk75sL*eA|2{-loV4|oy0LStcM=z-qA^Z8hb7eoJ~^|a zEar2Ncej^Fb-phy+Q25=Xhv|X&Pg(`HO_`rQLC8Zl{cT99`g?J#*+n7p6iW^Hn33- z8jxX(vH0-7Xu5JhnUe03-QUZN`4Fq+ySYN$anS}gyM%RP?H2HCoPyXP;4!2u$0Icr ziE`K6WVv(Bb#ilwUqWAU$NYdMc@3#wnZb(N*maNr)@0RDCw0eqe^ndP)~d~Edvy#- zITn78t@U)E(ikI2#4dy(7O4ZUv@B7!ed`7}{!}3=TZ^URxe__O9XR>y40*euSS`b- zyE{WFL*C2pPdM+JH(jsZ*@#gGQFX1MPU?1?3zeY+^#T}etJ;{hR&5S-3^*3*m~6W* zTW&22h%fri62{K4@WkDPmFR?6F*V&St2r5dTbCync2~%!Z8K%y>dVR1>#85~e)rj( zDsp6qdQpVU+OTR7PnKB~6-FKya)pz@l_ z@i{i?82#qQd1~qZlyrC0|LMeNpRi}+Fb1rAbJdh5O7jCbY6WZffXNdJy>yoMo-dUj zuE>zDOf&aT&Fs4%4=$S^|7a|uUI#k}s6*97osR9#oG&NHk7zUMQf+8<84brutru(h zPL3?)7`m)4OF4`ij8Pg7*5Crf46jQKTjUMiedzf`Wwb*Rb+Fnt1l4$%@*R<$XsB;n zzC|4yVo|oLgSs5skK1UYmmUtPFQq6yvpRN1Iffid@mlN9=TJLZm&#{iHzFpl9BO?1 z)=vwimj<@$Wc##rru=f_G+9#YckJZ4iL#H>=c~_ca|cmnzDC)LiyZs>s_a*2BihQY zPOUo0AGZ|A+9l(yTuCC>g-l}1`%tp7iI=N?qW zb;oh;<%)_z0Idij$V0q{Am!BxK2sAP3|7tfh)I3mGg4DA^@*YpqmCwOR762x6i~uL zNjgoV*2Kg|OdV^dCPrhdhG?6r8K>2Ny!`c>-`wdCXOiLU-QBx)@9xZJV9p=AXV35V zyL-OpoZmU1Nl=ip=0I>LHo^Ao!36s4C|@N7E_YFyYLexuGv5pMRJ6_uM*DHwC^Wgr zHiagUBHSB6mfq5L13(oZ5inYXzOOPyYCQ_{DTXRzdnz{yRJN!WM$=>q5WW}gsaSpE zLfq-IoiW@t3bs9K!5PCO4Nq5q?ru7|`htH5&U5fkE=E9%-AP-k@l zWc%zO)*TAZ{Zg{2BVGDEIR*~7{tDp}aeLOPe+j@A6@Rz%keM<*dN02X?rG`WthXe@VQJmc`^(5#K-3JKX zk$(^#+D5J%5g>ox-j71X(}nZ&BGNs;3P1wzDp)vzz^7XN+4Xz`36`Mfwu^;#<9Ecn zI;x%dT~6diN~!-cz2vF8gDHRn;56C$<&HBP;Kh1-p-B>YS#%d5$Q!4^%wW1#CXnYL ze;IUNBCVe6FXbI3=_N131X2Xgl;9k4q;@*YaIIJS+PYi12@t#szmqLE92p>NycqqE` zVa+7}Cl8}>I^nKR7t84c`ICqKtcGo2V=1SDP1nmp#nx9cI1fkw-hBicEU;m14efNo zU7-MRIzetL<5k+Qsf6Pim`^~Wn+`8uumdy$63q?y!^#Lk!)n67t%SSg14K)L91v{C zwoFgxDD}v^?ZC%PhnESYgR2sE*x+AoIHy1=sB&faqVeo=q`z6S8U%Gi9kn6IVKWX= ze93T3gXCT4uYfc?jb1%@d4U4ZAaz$oY_PyN1)ph+YMk3FMieel?pC{$9BvBa}GJuWH}QMPM~Swu22_C(}MhAMH0a_ zY@=-%JviUP&hsNL^L&~MT<#!Oj&nhx0^Did2p`S_M4}Jpb6~kPMo3f@qug~U$P8x3 zbZ~%h9SYPJu`3*$1|%0xwdZaHYnA+YD*t){ zXd^f#FO_##fLI+P$hoce%TR zX57`*AgyP}HL1hw;1(FImvvX7c_J?|#(qP}1 zr*wYwbrB`M70$wZ~QSUd^1TWvvZ(_Q%jb3qcg16Jy7Ny$&m zfp~#fkOpRgWndjBR`L-0=s-BoJnomPKI2}rfP+M-0BJQtuB|}{nfMwMfn1xfWPmgf z3%v9)7t$W2f*eo*&VVn#UC;TM2S)4Qnev1GodO zg43V^tO5g^ac?XS5Deykcfob=KxE!SZ^HfR?pSJ;arVEt23zIqiqWGW7nv7%0Iu77 zY&Phr^$f#yAQUVHmp~n8(a2H4juh^S>=222>P?VKr3KX4eD3!kOpEg+-M}Pp3^Zw^ z;WQ=Oown=;f?RXK@g&XQeK3)IQUjM6Siu|MHh8R&hSH>PcgC_-f?N}}Yh(B87T5x! z9XWqu1}WfA;J+Ga9L)=NXDYJ^a%T#T);s`5K??h-%9jNU1s6cGMjFIvguCX;v?a*x zp&48R!`0L!9l%)dxzdQW z~OlcEt+cg#nHd^$J6zK{&Z@il@2Tnp>(738oj+j!P+A6oZ9cPhyWq0<|q2)@8R0pDOBsaX-uJ_Fz3py}Ms z?q`1k2Ftl!Pc>+k*a-YmTQdUbgV*}BDIK;2e`|~4=*aSLTKwy-)TN^#-1R2NyL!6O zlCfRs2%NvRI1V;%Q(7~3C+t=P?tO;`Lsaq;Vn47?=3ElX# z+U6R&jLCiElk2VYn<3tYaCdS+K5eKspZ#LzCV;#;`f^?jWy8lz(gk}7`?wv~7;p`c z2tht3?lXRcK4h~@D_I*3C-YHV6-z?tKvsyV9DJo0ea3;VO&uY)RG!-r@1%M=!gl;@ z*iyDL*5PC9Yw$TKL2?a@vFCK!&7xnA?p`L!ozk5(M2o!VZS&{2u_hS_L+aC1qa7!= zw+unzkT!(vU_*J|&8An`=ls1)QobUuOWlkv4tyjk$X^`nP5712L|4KnbDgrwyJFyb zUQ&enhoHZd9P+W?Tha3;4lfHw#kJRfMw!mb@P9RYPr`8ZJ)5taL?;H9h_1I9pX^C@ zO5?TJfJT|_%iw!74n8O$e`K-f8ZCcN3P^;KEZ)6CGktBN%(cqkdpy}FL9CaEW*~-v zPedLP(#1?)6~$?$v5Yd;EW@Z41^WqivpyEx+B6#cQ{*8p4)LP9rSY2i+!|%BSq2}Z z7Y2EW2FPvpd4VfIblZ+49 z)mTQE>y_12B~j&)P$@cOU|UDB%wRMi5#C!JNe?QLsJ=Scz`#-PnfC9l>MbQeMgenM z(`vH;iICE}9gRu!q_If`1`dOLa6oTKr{}H!9&Oz^bSWSa{5qN_#LtAJ7qfwZqhKHO zm6jf{R3tza0TO_dSb}|x05NbZ@Ik2wRf|LdBnyxT?aXe})5i!91IGd%gfB`8kSrww zgz51V0z{>5R%lmuiVX0iexcpy=hk3K>=#Z+{URu#Zz%PT45FxDUt|Tglka&TGSG{k zAxdI&IE8ofkgYCZT`d$F5h$rHeM7qNyU14;ck0ulGsT7nzz!jJSGEKH9~%)seS*9x z>}M9+GZhBD2OoqlYBWHQSg&Y+^a<=lPp2l(%&Ajo<%*@Wc|$H0?0S<9l@!t8%7au> zb%-j*=4X%SQl*a_;mW)U(X9ax3RH-iVAkh)-H3VKTnV9>qYCq z?U%i533rP`ei#NOSZV#*9QxqIajLy}gPNP0B-@~=sS&~QHGO>M6y5-a6~ShleX(&blC%TYs#79L~OH@BQ0*{nq-eb!@H7I&5CF z`U-nci2>3~)<hT!3REt{Cm0Nn(!Au z{sqC*~BrL=w5um8t=#Z#b9cn-5~&pI2TGMvDDvi^>RGS zoAZ*q_oM)b{p^<@?9>y!r&M;q7U53$wq)npPzGKAuxHu7tqxj)4S@ZU&I%c9J6o$_I%a+J-eN;fI^43QshjpZo+zjvRqm zHa0T6YEbW%B>kNYY3q)Kvr!YV^u5lyZUF!IyB|pLr zU$2&oGrp7NBOuY%)&^f+UnnRj5P_p>KvGHszAcd8Z>K%gn>a@|2S`)56Y9;Y{TQIE zK({LS#x;uc;N*0<)zs9$fddCXM@OeMILIcAABiE(I2Pwg`#u#HT~iT&P~NA$u1@m! znHe|1?TtmPHyXWTpWeM-?b@|aR8*uCYd~S)lKb$95Rx9*S0p3R?oTdNQ}+@T0m!33 z>8(!u1cvm(NR^`<630t;{VNAJnys?(A^7=tf&Q>QkIfv1oH_|VH#SIq?w#x`aC&7X z8yxyWd*Xa0un2nM)ytU2Fp?Gb;ig->&>8ab@`R146!ZUoko=B;nmpLHbqkEp8i0F- z_Rb@~P(gq^YLh0J!P&EC1>aaH7NnI?tbX09

Dbjk?2kE4eIEpLxHy2)1t8zy`=m zQzu|}I+I0!zrGlW1YZ{Lwlk)}wePM7|3)e1z4c9!oSY1=Iy=KN&otMW{{aA^si_J6 z{{DhHqZA9#)}~5EuHi6ehRmi~y1*WC@8)pwFO;Oz<0c4Q7ejttE+^lBKK&k<=Nal~ z!WYL5K|@2mQp|Z<78Vu;1_lOy9DoQU_~y-%=0#FTnmMT`ARX@@*<2YSFZfy03;wF zK#1xn#pKJgjC6tAE*A3;ZFPwMHkK7wxR!VYW>25QP%l3NXU~MP<}D}tHrU&U#gdk$ ze}$-v5lZXgwoWEqzI<75vs51-`uh5ilar$`A;kYr-f&|>iml6^nGc%L^4F9e}!uJmzJOB?557h_Au3ftn<{k(N zT8yBeAXv0$5zLq|1I#8(0%H>sFgG^`TU%RLy?Qmo#Kd4ks#0N~WTvOVve%qJPs9fn zk)oC(nA{z(Q!)=(Jbx~vr6x1xssRwcrsK&VYr9 zfux$41N-(uV}s<}3kd<|&uxjs8DJG=z)nBnPh38LYe=`o;K762ugx1ckdl$v+1czh zsI9GqLx&HmE=+mu8?(lCc6O$lgn4_B{%Vf3m1yIh2|3YRFeJ&Y zqx9X$&bHkEG17Y$lanV{zq_cg0JHa6&CjAzSggQQhzododqdBj(p&2c8iexs&6^=V zKcCHaVq;@r)TmLa2M{6m5gi>(FUdKz5|H@KoHj2Qz5 z4;~}}&-o20DJd{--aOR({RbH8nN%_-bgzhT|bJ{2V=#Q#R|f zWus{0x-NMYB`1y2H!k|{{=VKYww*QdE=v|d*6j?|hTiwj4@J*%o;Mc{i5m88WIy+1 zkqVe&Q>ILzXd~mWBr&C@=O)zy2u7PIImo#^l9G~$a5KggsdDu&yy@rt9;;LX57Kgt18k>uq#H_o?xY06F zd9J%%#}HlC*VhZtOVtC2;NIN5dzUjIbS3k~m6fc!xhz3Gk;ST|4nH3%+H6)+jN!_q zSk`9y3W>V8@i=(^p~%_pfE_$ii@a@tqqsUIWtia8^>~s;=hd+Z8j5w=ivB}VAdjM z-AYHHpoKhuP<7q0!=E$9$iOiyMbGp1_GAI1x~dZ1d3!zRiKOy;kc=KZnxc}N_d0(3 zxDX^zEr4JgoHHSG%AkA30*6K~*>T3Au#m?aX5MR`#vmrNnt@A^}%Z0=5tUl9!Prh=mGKZ@sN>`A=OB# z)Td7$)dGkRH!mqEVF}e-TwGY-(2=cTq;88-jSOJ+OiM{-i)LYFD8mmtH*SEM>M9oL z_gue9E5_k4sB6LEo7;I8c~fS_AS?x}1&2_GbrQ2k(GVIkRcb#+yb zgJ4Emh(ItDwbifeQnO#)v@CHKs$Z zg&M`>F&xd%Nmal?2kW+;Pl?1^8R@KqT3Bc(jMvwt-}ux4A_fbs)o@(1-s(yL1my&X z5Vg2);R3fo1IemaxVggEVSR<3NJ{ac7LBHpam|`Fl;h!4Ebj2x{G=LmYJtO1+a5Y} z2zsci%S7@N2ME9O`e;q1<|)QfUK9Sjiu!SZaszks^(fu!pJVq;^Y zh}@YLHt|XVc3HPq8gMjqBqTXK2lU^krhs7^JY;`RAV*?ClAfmAkqk&W79f0(X!TV( zUee_Nq2x|vWTYZJU#X4(>oDj$7c-|$M19rM0EYa;SZFA-UOWry#b>?vSPfQOYz+Ly za7nT9uB9wM3=*XPNb_9w>!B3DP#%B?=I-uJ*>MG%ijg7}`%ICXJ18&!)_J(XLPvWN z5L#PKq0fNfXV@qY&{2e(I7iz`0dgNXk*s$flQ6V+L6%aOch)MpNr5IQaQ~ncwC5`h3XC zy~Bz4Q7P;b6b(36od*ze!%L_v-25~RHm#DihOhEcx_Bci`a!t|)fEi3sBCRLMcz`uUNkXDuW z>etopP97KM4#OB+g0x417$jkpRpd;f;m@DKdu3%MZ5x90MiiDMIjq(pB~j|^X{`1u z`TMO~x4Hyr&jc|n?aUHhTvW&!br7KaRq>Wuw(#(9s}7msY#k-|<~Oo`goM+diy|*! zAan`R-U(uWnxsi6G*kB04t1sOPwMY~YiQ$QFSJTqu52`Nr;!$7mmbkxwEBOHO54{PFZNJ?^NeC?>j-^!HI+ ztvVn}EKThhkN8UN8&Yk`(6Qmin!?ok~ z?b~Y2nl+YO`t$PgBz^H4KDhLr=x>i4`IW~G;}WDJ5hSGZ^V9bX>J^v1xr7Ib;k{3v`l{7pxoQiy2#s}u719(jr=y~3VD5)+FxC21@Q#(3;2 zcP&`Bq7j_Xh%h-xM#n@UgUH(~ znI^1{NE?TVvGS?2bLUP;9TK5{endq_M?0P%SUbE1c;I0yEW6fQ)66rq$b;t#oQTFcLqe4PLY$eDwB$r4}OZCh3&>-*!<+83Yc~hgDkBsPE;9 zejfX@ksypzoI7_e4=Bu~$$94*ajlG718@n_hBpc=?tIhs=^B8Pq%FXI>lMcIGTsfu zEQogn>O~kPS-{dPf#cx1e*X7Wb`ylP3p2i#hCb~i2n~@Wi1z>p3-TyWoYQ#V;eIN#<-G4vf1N&z{V;Gnp#VmItPv@4i7 zxhY3U2y*@Ub(NNurfD6h=@V2OBN%~-y8G>mASmL=3R5_VE#WRo1T0yyMAtY-HrO;I zHwgdEM1?EX#bD9?vBT9zSL*B{2%R=_S1e;+ebJ|xtm)a=*%E?q9WpaBwXH+%-o1S( z;_25Kg~33UZh|aZwoKL6*9(eWHhD7zYQfBzGnw33BBZ}__^>I0cr&R-M+T{ycdJ<; z*A9Yw4BJG?lnLzXE8WczBqJk3LJ-PQaN17<|aqpXGLBJy2 z1erH)9^<_QCpVuq2&B?+t+Q4lNGIAL+e=DJ5hThW2!JQh&zKU`4uaHv_<G!$!O|(E@w&u>6Z9||vB5?DxFw#lU zuU|h^QBffc!=;z52^oadl)MDV&B-(oAt>|uJ$a(in@bC&&dkUl)Z$4!jp=}im=i5I z(JoxLp#CGcSsNM}824`jL1u98n`i+*qTbF~LxWl%A7aFrM@v2idf5u!y*#=2_htz4 zN*5A@nEG10x~o4q9{tJ+^4EL&L7%1w!f<97{ls&yapOi-3lpvZLzp*j-YkAz)z;Q( z*QOa5&%JN5jRyV==+s*9?TF`%)E9~#wf+0|m-Nq#QB_qXIJF2W7!nf`P4(3^m}*{P zNS`k1CbP6Z)owvgoApg^KkU=Y*ai+9NG*^rp2O+s=@J=$f;AGC^Cl8{R&x!Fz%ZS7 z48#Ja+i>8n-a$}6WSqATG2%SIa2r|bH#s?(4ts!a<7edculEd)Y|25ZBH4XR6*F6NSR zGlqo(A+{Fe>9nZRCiDJ#HD39HeOV}s$;9Ef5q+AEt5>gbPLTAk)&7EmgH={m7S|qQ z_i61HFJ5d1K`wF6TWCFiQb6g(u3fvtLZSgCh(?k$YNU9ofAKlT_y`y#dFzO@HZ6^0 zR+R*N^7QK-U=-s_*O}*w8+2dAF8((9U|$x-#`0{wmY={O$2`QRPoHMeZfF_*Z(Pir zIdkZu148UIt9J&cFiTiSEIS8#j(2tU5Lck~Py^0Rcn2ddteo zRV5a_uC9(U%fp8cb%e9>w=FIVPxJ^Q>BNkw$vQPzR9dWA`9uD8gvHy#IMMl3)sa~r zHzNjPN-WCA5PfTK=T7u&O+?Ma%kN**H6{{2b>{2*ZS)};TWoABH!m8NwhpUSt@7OFj+pB&0n1}RhOvVj%|ICt0f^dC;DO#j@oh>=Dzp-g zSb*o8h7>B{65+cI=#{Q&YvCqsV>|VmN;Pu$*L)rq9R8S9ANCDL%=wq)0~j%4gdGHV z0AvE<76ViPN;e=xQ>TsV(-DjC3|M3|H#b*P$+-ZPyZR0FFHIYA1!5`ulY2)=k&<(p4Obt>rzw zZtYsuFVX2p3Srt2bN@_1AR32ll0BM$wcO)Awn(7b23drLCK#OP`hX{mBNoL&j-yA9 zQi#>~t@L4nO4Qf!F@^%R0Nqu-sMUg2&=G3@a4hBJgSt;_L)kPyLf3~xj=kn#t)#b~VsY7q;6RWJO%v54HIT-FLj>9%U zwew3ay=2=M`}XZi7m|>cmVkDE~l%w-Qz?OI%Slx&xphD(497In%67UZt!G;YMWya z7RjOJPtdnW%Q$At7?q!&&wgPv)x8e%2!O8~pLOx^@pj&f&Bno=!98qq3kDVoNFos& zS&Rm!E;qD~9vV=?p?X^Uk2%R!_(4w8f+kLz_%eE4WD{py3V^%j_E1?DVn)X*lU{f9DB{cQaMBcs@~}#fIc_4aGI-#p>_G zb+cy8;thuQd@JDh;ko083j#fa|9LQ#l_=acJftK!3qed~50pld_jr=z}lTBov*G`N^!z>XeC5LVm za5yjr_yf@7l*Mz=;m`zJ02Tn@+?x(-y@7Q=74QIPcF4rJXuoJS_F5INmab6_Z-GDz zuoNf4$G{z950wDv zKoa*@J7}GO9>5@AI1mdY04YEQkOgFSVB`R~z Date: Mon, 3 Jul 2023 11:45:21 -0500 Subject: [PATCH 2/5] Remove incorrect commas --- hydrus/core/HydrusConstants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index eb64f763..e8bcd64d 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -713,8 +713,8 @@ UNDETERMINED_MP4 = 50 APPLICATION_CBOR = 51 APPLICATION_WINDOWS_EXE = 52 AUDIO_WAVPACK = 53 -APPLICATION_SAI2 = 54, -IMAGE_SVG = 55, +APPLICATION_SAI2 = 54 +IMAGE_SVG = 55 APPLICATION_OCTET_STREAM = 100 APPLICATION_UNKNOWN = 101 From 3b6a89f219d43aba3629c2e1b6bfe4a32791bef4 Mon Sep 17 00:00:00 2001 From: Paul Friederichsen Date: Mon, 3 Jul 2023 12:13:35 -0500 Subject: [PATCH 3/5] Move SVG from IMAGES to APPLICATIONS --- hydrus/client/caches/ClientCaches.py | 3 ++- hydrus/core/HydrusConstants.py | 8 ++++---- hydrus/core/HydrusPaths.py | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/hydrus/client/caches/ClientCaches.py b/hydrus/client/caches/ClientCaches.py index ab59fbb6..586ed694 100644 --- a/hydrus/client/caches/ClientCaches.py +++ b/hydrus/client/caches/ClientCaches.py @@ -744,7 +744,7 @@ class ThumbnailCache( object ): self._special_thumbs = {} - names = [ 'hydrus', 'pdf', 'psd', 'clip', 'sai', 'audio', 'video', 'zip' ] + names = [ 'hydrus', 'pdf', 'psd', 'clip', 'sai', 'svg', 'audio', 'video', 'zip' ] bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ] thumbnail_scale_type = self._controller.new_options.GetInteger( 'thumbnail_scale_type' ) @@ -861,6 +861,7 @@ class ThumbnailCache( object ): elif mime == HC.APPLICATION_PDF: return self._special_thumbs[ 'pdf' ] elif mime == HC.APPLICATION_PSD: return self._special_thumbs[ 'psd' ] elif mime == HC.APPLICATION_SAI2: return self._special_thumbs[ 'sai' ] + elif mime == HC.IMAGE_SVG: return self._special_thumbs[ 'svg' ] elif mime in HC.ARCHIVES: return self._special_thumbs[ 'zip' ] else: return self._special_thumbs[ 'hydrus' ] diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index e8bcd64d..2082b77a 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -728,7 +728,7 @@ ALLOWED_MIMES = set( STORABLE_MIMES ).union( { IMAGE_BMP } ) DECOMPRESSION_BOMB_IMAGES = { IMAGE_JPEG, IMAGE_PNG } -IMAGES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_SVG } +IMAGES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON } ANIMATIONS = { IMAGE_GIF, IMAGE_APNG } @@ -736,7 +736,7 @@ AUDIO = { AUDIO_M4A, AUDIO_MP3, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_WMA, AU VIDEO = { VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_WMV, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG } -APPLICATIONS = { APPLICATION_FLASH, APPLICATION_PSD, APPLICATION_CLIP, APPLICATION_SAI2, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z } +APPLICATIONS = { IMAGE_SVG, APPLICATION_FLASH, APPLICATION_PSD, APPLICATION_CLIP, APPLICATION_SAI2, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z } general_mimetypes_to_mime_groups = { GENERAL_APPLICATION : APPLICATIONS, @@ -769,8 +769,8 @@ FILES_THAT_CAN_HAVE_EXIF = { IMAGE_JPEG, IMAGE_TIFF } # images and animations that PIL can handle FILES_THAT_CAN_HAVE_HUMAN_READABLE_EMBEDDED_METADATA = { IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_GIF, IMAGE_APNG } -FILES_THAT_CAN_HAVE_PIXEL_HASH = set( IMAGES ).union( { IMAGE_GIF } ).difference( { IMAGE_SVG } ) -FILES_THAT_HAVE_PERCEPTUAL_HASH = set( IMAGES ).difference( { IMAGE_SVG } ) +FILES_THAT_CAN_HAVE_PIXEL_HASH = set( IMAGES ).union( { IMAGE_GIF } ) +FILES_THAT_HAVE_PERCEPTUAL_HASH = set( IMAGES ) HYDRUS_UPDATE_FILES = ( APPLICATION_HYDRUS_UPDATE_DEFINITIONS, APPLICATION_HYDRUS_UPDATE_CONTENT ) diff --git a/hydrus/core/HydrusPaths.py b/hydrus/core/HydrusPaths.py index b53b56b5..49c5b740 100644 --- a/hydrus/core/HydrusPaths.py +++ b/hydrus/core/HydrusPaths.py @@ -24,6 +24,7 @@ mimes_to_default_thumbnail_paths[ HC.APPLICATION_PDF ] = os.path.join( HC.STATIC mimes_to_default_thumbnail_paths[ HC.APPLICATION_PSD ] = os.path.join( HC.STATIC_DIR, 'psd.png' ) mimes_to_default_thumbnail_paths[ HC.APPLICATION_CLIP ] = os.path.join( HC.STATIC_DIR, 'clip.png' ) mimes_to_default_thumbnail_paths[ HC.APPLICATION_SAI2 ] = os.path.join( HC.STATIC_DIR, 'sai.png' ) +mimes_to_default_thumbnail_paths[ HC.IMAGE_SVG ] = os.path.join( HC.STATIC_DIR, 'svg.png' ) for mime in HC.AUDIO: From 1e8052d90578f24a040492a57450e0e366574d42 Mon Sep 17 00:00:00 2001 From: Paul Friederichsen Date: Sat, 8 Jul 2023 13:35:49 -0500 Subject: [PATCH 4/5] Add basic SVG thumbnails --- hydrus/core/HydrusConstants.py | 2 +- hydrus/core/HydrusFileHandling.py | 20 ++++++++++ hydrus/core/HydrusImageHandling.py | 1 + hydrus/core/HydrusSVGHandling.py | 61 ++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 hydrus/core/HydrusSVGHandling.py diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index f506db8d..ea0b1521 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -761,7 +761,7 @@ MIMES_THAT_MAY_HAVE_AUDIO = tuple( list( MIMES_THAT_DEFINITELY_HAVE_AUDIO ) + li ARCHIVES = { APPLICATION_ZIP, APPLICATION_HYDRUS_ENCRYPTED_ZIP, APPLICATION_RAR, APPLICATION_7Z } -MIMES_WITH_THUMBNAILS = set( IMAGES ).union( ANIMATIONS ).union( VIDEO ).union( { APPLICATION_FLASH, APPLICATION_CLIP, APPLICATION_PSD } ) +MIMES_WITH_THUMBNAILS = set( IMAGES ).union( ANIMATIONS ).union( VIDEO ).union( { IMAGE_SVG, APPLICATION_FLASH, APPLICATION_CLIP, APPLICATION_PSD } ) FILES_THAT_CAN_HAVE_ICC_PROFILE = { IMAGE_JPEG, IMAGE_PNG, IMAGE_GIF, IMAGE_TIFF } diff --git a/hydrus/core/HydrusFileHandling.py b/hydrus/core/HydrusFileHandling.py index 45d7f621..4defe15e 100644 --- a/hydrus/core/HydrusFileHandling.py +++ b/hydrus/core/HydrusFileHandling.py @@ -4,6 +4,7 @@ import struct from hydrus.core import HydrusAudioHandling from hydrus.core import HydrusClipHandling +from hydrus.core import HydrusSVGHandling from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusDocumentHandling @@ -151,6 +152,21 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, finally: HydrusTemp.CleanUpTempPath( os_file_handle, temp_path ) + + elif mime == HC.IMAGE_SVG: + + try: + + thumbnail_bytes = HydrusSVGHandling.GenerateThumbnailBytesFromSVGPath( path, target_resolution, clip_rect = clip_rect ) + + except Exception as e: + + HydrusData.Print( 'Problem generating thumbnail for "{}":'.format( path ) ) + HydrusData.PrintException( e ) + + thumb_path = os.path.join( HC.STATIC_DIR, 'svg.png' ) + + thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect ) elif mime == HC.APPLICATION_FLASH: @@ -314,6 +330,10 @@ def GetFileInfo( path, mime = None, ok_to_look_for_hydrus_updates = False ): ( ( width, height ), duration, num_frames ) = HydrusClipHandling.GetClipProperties( path ) + elif mime == HC.IMAGE_SVG: + + ( width, height ) = HydrusSVGHandling.GetSVGResolution( path ) + elif mime == HC.APPLICATION_FLASH: ( ( width, height ), duration, num_frames ) = HydrusFlashHandling.GetFlashProperties( path ) diff --git a/hydrus/core/HydrusImageHandling.py b/hydrus/core/HydrusImageHandling.py index ffd18d0f..efa9881b 100644 --- a/hydrus/core/HydrusImageHandling.py +++ b/hydrus/core/HydrusImageHandling.py @@ -879,6 +879,7 @@ def GetThumbnailResolutionAndClipRegion( image_resolution: typing.Tuple[ int, in bounding_height = int( bounding_height * thumbnail_dpr ) bounding_width = int( bounding_width * thumbnail_dpr ) + # TODO SVG thumbs should always scale up to the bounding dimensions if thumbnail_scale_type == THUMBNAIL_SCALE_DOWN_ONLY: diff --git a/hydrus/core/HydrusSVGHandling.py b/hydrus/core/HydrusSVGHandling.py new file mode 100644 index 00000000..4398e3fc --- /dev/null +++ b/hydrus/core/HydrusSVGHandling.py @@ -0,0 +1,61 @@ +from qtpy import QtSvg +from qtpy import QtGui as QG +from qtpy import QtCore as QC + +from hydrus.core import HydrusExceptions +from hydrus.core import HydrusImageHandling + +from hydrus.client.gui import ClientGUIFunctions + +def LoadSVGRenderer(path: str): + + renderer = QtSvg.QSvgRenderer(); + + try: + renderer.load(path) + + except: + + raise HydrusExceptions.DamagedOrUnusualFileException('Could not load SVG file.') + + if not renderer.isValid(): + + raise HydrusExceptions.DamagedOrUnusualFileException('SVG file is invalid!') + + return renderer + +def GenerateThumbnailBytesFromSVGPath(path: str, target_resolution: tuple[int, int], clip_rect = None) -> bytes: + + # TODO handle clipping + + ( target_width, target_height ) = target_resolution + + renderer = LoadSVGRenderer(path) + + try: + qt_image = QG.QImage( target_width, target_height, QG.QImage.Format_RGBA8888 ) + + qt_image.fill( QC.Qt.transparent ) + + painter = QG.QPainter(qt_image) + + renderer.render(painter) + + numpy_image = ClientGUIFunctions.ConvertQtImageToNumPy(qt_image) + + painter.end() + + return HydrusImageHandling.GenerateThumbnailBytesNumPy(numpy_image) + + except: + + raise HydrusExceptions.UnsupportedFileException() + + +def GetSVGResolution( path: str ): + + renderer = LoadSVGRenderer(path) + + resolution = renderer.defaultSize().toTuple() + + return resolution \ No newline at end of file From 284126e8559f242a5a9de9ad374014274dad27e7 Mon Sep 17 00:00:00 2001 From: Paul Friederichsen Date: Sat, 8 Jul 2023 14:41:37 -0500 Subject: [PATCH 5/5] Set svg renderer aspect ratio mode to KeepAspectRatio --- hydrus/core/HydrusSVGHandling.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hydrus/core/HydrusSVGHandling.py b/hydrus/core/HydrusSVGHandling.py index 4398e3fc..e77b9d00 100644 --- a/hydrus/core/HydrusSVGHandling.py +++ b/hydrus/core/HydrusSVGHandling.py @@ -31,8 +31,12 @@ def GenerateThumbnailBytesFromSVGPath(path: str, target_resolution: tuple[int, i ( target_width, target_height ) = target_resolution renderer = LoadSVGRenderer(path) + + # Seems to help for some weird floating point dimension SVGs + renderer.setAspectRatioMode(QC.Qt.AspectRatioMode.KeepAspectRatio) try: + qt_image = QG.QImage( target_width, target_height, QG.QImage.Format_RGBA8888 ) qt_image.fill( QC.Qt.transparent )