From 7dd0f3dabbf597c293286d67252f0684a7d9a649 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:14:01 -0700 Subject: [PATCH] feat: port thumbnail (#390) and related features to v9.5 (#522) * feat: port v9.4 thumbnail + related feats to v9.5 Ports the following thumbnail and related PRs from the `Alpha-v9.4` branch to `main` (v9.5+): - (#273) Blender thumbnail support - (#307) Add font thumbnail preview support - (#331) refactor: move type constants to new media classes - (#390) feat(ui): expanded thumbnail and preview features - (#370) ui: "open in explorer" action follows os name - (#373) feat(ui): preview support for source engine files - (#274) Refactor video_player.py (Fix #270) - (#430) feat(ui): show file creation/modified dates + restyle path label - (#471) fix(ui): use default audio icon if ffmpeg is absent - (#472) fix(ui): use birthtime for creation time on mac & win Co-Authored-By: Ethnogeny <111099761+050011-code@users.noreply.github.com> Co-Authored-By: Theasacraft <91694323+Thesacraft@users.noreply.github.com> Co-Authored-By: SupKittyMeow <77246128+supkittymeow@users.noreply.github.com> Co-Authored-By: EJ Stinson <93455158+favroitegamers@users.noreply.github.com> Co-Authored-By: Sean Krueger <71362472+seakrueger@users.noreply.github.com> * remove vscode exceptions from `.gitignore` * delete .vscode directory * style: format for `ruff check` * fix(tests): update `test_update_widgets_not_selected` test * remove Send2Trash dependency * refactor: use dataclass for MediaCateogry * refactor: use enums for UI colors * docs: add file docstring for silent_Popen * refactor: replace logger with structlog * use early return inside `ResourceManager.get()` * add `is_ext_in_category()` method to `MediaCategory` Add method to check if an extension is a member of a given MediaCategory. * style: fix docstring style, missing type hints, rename `afm` * fix: use structlog vars in logging * refactor: move platform-dependent strings to PlatformStrings * refactor: move `parents[2]` path to variable * fix: undo logger regressions --------- Co-authored-by: Ethnogeny <111099761+050011-code@users.noreply.github.com> Co-authored-by: Theasacraft <91694323+Thesacraft@users.noreply.github.com> Co-authored-by: SupKittyMeow <77246128+supkittymeow@users.noreply.github.com> Co-authored-by: EJ Stinson <93455158+favroitegamers@users.noreply.github.com> Co-authored-by: Sean Krueger <71362472+seakrueger@users.noreply.github.com> --- .gitignore | 10 +- .vscode/launch.json | 17 - pyproject.toml | 1 + requirements.txt | 5 + .../resources/qt/images/broken_link_icon.png | Bin 0 -> 18571 bytes .../images/file_icons/adobe_illustrator.png | Bin 0 -> 10553 bytes .../qt/images/file_icons/adobe_photoshop.png | Bin 0 -> 12552 bytes .../qt/images/file_icons/affinity_photo.png | Bin 0 -> 11147 bytes .../resources/qt/images/file_icons/audio.png | Bin 0 -> 8860 bytes .../qt/images/file_icons/document.png | Bin 0 -> 9200 bytes .../qt/images/file_icons/file_generic.png | Bin 0 -> 5768 bytes .../resources/qt/images/file_icons/font.png | Bin 0 -> 9027 bytes .../resources/qt/images/file_icons/image.png | Bin 0 -> 8998 bytes .../qt/images/file_icons/image_vector.png | Bin 0 -> 10640 bytes .../qt/images/file_icons/material.png | Bin 0 -> 16977 bytes .../resources/qt/images/file_icons/model.png | Bin 0 -> 13247 bytes .../qt/images/file_icons/presentation.png | Bin 0 -> 11326 bytes .../qt/images/file_icons/program.png | Bin 0 -> 6748 bytes .../qt/images/file_icons/spreadsheet.png | Bin 0 -> 5721 bytes .../resources/qt/images/file_icons/text.png | Bin 0 -> 7126 bytes .../resources/qt/images/file_icons/video.png | Bin 0 -> 8448 bytes .../resources/qt/images/thumb_border_512.png | Bin 5649 -> 0 bytes .../resources/qt/images/thumb_broken_512.png | Bin 24707 -> 0 bytes .../qt/images/thumb_file_default_512.png | Bin 12661 -> 0 bytes .../resources/qt/images/thumb_loading.png | Bin 0 -> 18128 bytes .../resources/qt/images/thumb_loading_512.png | Bin 12467 -> 0 bytes .../qt/images/thumb_loading_dark_512.png | Bin 11270 -> 0 bytes .../resources/qt/images/thumb_mask_128.png | Bin 2245 -> 0 bytes .../resources/qt/images/thumb_mask_512.png | Bin 4902 -> 0 bytes .../resources/qt/images/thumb_mask_hl_512.png | Bin 5392 -> 0 bytes tagstudio/src/core/constants.py | 114 +- tagstudio/src/core/enums.py | 4 + tagstudio/src/core/media_types.py | 507 ++++++ tagstudio/src/core/palette.py | 77 +- .../src/qt/helpers/blender_thumbnailer.py | 111 ++ tagstudio/src/qt/helpers/color_overlay.py | 17 +- tagstudio/src/qt/helpers/file_tester.py | 32 + tagstudio/src/qt/helpers/gradient.py | 43 +- .../src/qt/helpers/rounded_pixmap_style.py | 29 + tagstudio/src/qt/helpers/silent_popen.py | 70 + tagstudio/src/qt/helpers/text_wrapper.py | 49 + tagstudio/src/qt/helpers/vendored/ffmpeg.py | 33 + .../helpers/vendored/pydub/audio_segment.py | 1444 +++++++++++++++++ .../src/qt/helpers/vendored/pydub/utils.py | 89 + tagstudio/src/qt/main_window.py | 25 +- tagstudio/src/qt/platform_strings.py | 16 + tagstudio/src/qt/resource_manager.py | 51 +- tagstudio/src/qt/resources.json | 80 + tagstudio/src/qt/resources_rc.py | 4 +- tagstudio/src/qt/ts_qt.py | 49 +- tagstudio/src/qt/widgets/collage_icon.py | 47 +- tagstudio/src/qt/widgets/item_thumb.py | 29 +- tagstudio/src/qt/widgets/preview_panel.py | 280 +++- tagstudio/src/qt/widgets/thumb_button.py | 59 +- tagstudio/src/qt/widgets/thumb_renderer.py | 1104 +++++++++++-- tagstudio/src/qt/widgets/video_player.py | 9 +- tagstudio/tests/qt/test_preview_panel.py | 2 +- 57 files changed, 3929 insertions(+), 478 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 tagstudio/resources/qt/images/broken_link_icon.png create mode 100644 tagstudio/resources/qt/images/file_icons/adobe_illustrator.png create mode 100644 tagstudio/resources/qt/images/file_icons/adobe_photoshop.png create mode 100644 tagstudio/resources/qt/images/file_icons/affinity_photo.png create mode 100644 tagstudio/resources/qt/images/file_icons/audio.png create mode 100644 tagstudio/resources/qt/images/file_icons/document.png create mode 100644 tagstudio/resources/qt/images/file_icons/file_generic.png create mode 100644 tagstudio/resources/qt/images/file_icons/font.png create mode 100644 tagstudio/resources/qt/images/file_icons/image.png create mode 100644 tagstudio/resources/qt/images/file_icons/image_vector.png create mode 100644 tagstudio/resources/qt/images/file_icons/material.png create mode 100644 tagstudio/resources/qt/images/file_icons/model.png create mode 100644 tagstudio/resources/qt/images/file_icons/presentation.png create mode 100644 tagstudio/resources/qt/images/file_icons/program.png create mode 100644 tagstudio/resources/qt/images/file_icons/spreadsheet.png create mode 100644 tagstudio/resources/qt/images/file_icons/text.png create mode 100644 tagstudio/resources/qt/images/file_icons/video.png delete mode 100644 tagstudio/resources/qt/images/thumb_border_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_broken_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_file_default_512.png create mode 100644 tagstudio/resources/qt/images/thumb_loading.png delete mode 100644 tagstudio/resources/qt/images/thumb_loading_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_loading_dark_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_128.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_hl_512.png create mode 100644 tagstudio/src/core/media_types.py create mode 100644 tagstudio/src/qt/helpers/blender_thumbnailer.py create mode 100644 tagstudio/src/qt/helpers/file_tester.py create mode 100644 tagstudio/src/qt/helpers/rounded_pixmap_style.py create mode 100644 tagstudio/src/qt/helpers/silent_popen.py create mode 100644 tagstudio/src/qt/helpers/text_wrapper.py create mode 100644 tagstudio/src/qt/helpers/vendored/ffmpeg.py create mode 100644 tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py create mode 100644 tagstudio/src/qt/helpers/vendored/pydub/utils.py create mode 100644 tagstudio/src/qt/platform_strings.py diff --git a/.gitignore b/.gitignore index 7b493926..c04f546f 100644 --- a/.gitignore +++ b/.gitignore @@ -232,11 +232,11 @@ compile_commands.json ### VisualStudioCode ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets +# !.vscode/settings.json +# !.vscode/tasks.json +# !.vscode/launch.json +# !.vscode/extensions.json +# !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8838fbb3..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "TagStudio", - "type": "python", - "request": "launch", - "program": "${workspaceRoot}/tagstudio/tag_studio.py", - "console": "integratedTerminal", - "justMyCode": true, - "args": [] - } - ] -} diff --git a/pyproject.toml b/pyproject.toml index 023ee6f1..ca949c6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ line-length = 100 [tool.ruff.lint.per-file-ignores] "tagstudio/tests/**" = ["D", "E402"] +"tagstudio/src/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/requirements.txt b/requirements.txt index 18be43fa..f2b7a90c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,8 @@ pillow-heif==0.16.0 chardet==5.2.0 structlog==24.4.0 SQLAlchemy==2.0.34 +pydub==0.25.1 +mutagen==1.47.0 +numpy==1.26.4 +ffmpeg-python==0.2.0 +vtf2img==0.1.0 diff --git a/tagstudio/resources/qt/images/broken_link_icon.png b/tagstudio/resources/qt/images/broken_link_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d431097084777d038020ea832bdbe5d5e16641f8 GIT binary patch literal 18571 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/adobe_illustrator.png b/tagstudio/resources/qt/images/file_icons/adobe_illustrator.png new file mode 100644 index 0000000000000000000000000000000000000000..141ae6203951666eecbc4b120b03a5fea50ba389 GIT binary patch literal 10553 zcmdsddpy)@*Y|H`j5Dbu-6M2vJJP6d@9dNSJqx_OtJMzxVUJ@Bh!|lTY*et#z$yUF+~&Yh5$PoSkgMgk^;h z1QD~dwRAxcG<-xOg8cB`bX@m5f?#*k+&r0{jt-=Ns0c0pz^FY`t+)s}&=JJgG>+~c z5JqL<_E3XqktWI$^$p56TA+!t$7V+zN4fg(XvoOOT?atM=2CuwWP#>Q&J>S;wq2W#sZ8X9Wr5VQ#dP2kXE?2lyn$7x0~RCpqO z$*`m{0-|YjCM_xw$CK&5Cn|<%qO1&b++V^21OApxkBJUnmK+$MO%10;P$QWPZCx#0 z?SJQ|#?k(zO(f$lbAjQsc@$vR`3F0b7W6-`^CAa@EBe=xJT09Jik;}UgY^Nnu+ojg7&{{?|ytqoaao;Xq@!*qGz&tSt0(4fXXk30l8)0LPJFEiGujr9Tt62s#9! zrp^{kf}WeMK8a{Z($R%Sl8(-@_vR7O+~GuA(vY8DLPWuj~r6hs3@?#DUv@&*UFs-va<&lRyDRFL*RTUGntkMdut z8EgNC^1n|0Z-4^pe!T;r0>sn)8}i`cZ_uPh0#Zf;Br1N+e1{;Y6dDA?e*^-8Y|M(u zs^5k&lSpl_T>po_$`XqIUfWyts@4(__*F#V(3L84q1i3lY(MPH#r?6fYK^4jI=>@J z*k0k62a7iz^z`1;8)7iLN=Z2==k<^8wMLJ=;9r$`Z4M^RE9GjBWYL}T{5;*{v<>J3 zmsYPYlkJ`U9CzmK_r0guj@NGeZRa9`yK9>=?J9*Ke5xXUw?v^t;jT>~QWJu)SMxm0 z=Nw-B)`C4Ufj8Oxacz2+`_fI%Q`Y8`sI@zvHyA9Xzc^92IwyDFDK&kxJ+U(P)`oA4 z>Q(WN>vhv4Hl#gN4LB^lhqW>0{!aFAS#Vdv7`9RWOs&JWZ%xFO#*Rt8+#kD^MwLVx(jT~Y z84GZFS>Jxkm+C)~?l^Lwd(VMN<;`htT-D=Dj@-u5`^n#2YJyFFo;M~leIJaCq&D`S7kH39=+V@3X*_7I4 zN%J%_0$NulFi=lgHt&xo4q8tIGi44}N#qy%tX%VAM`xmYvW4HYRsXc3jg8H$RqCcz z3OVIJd>=*!Hy0DJxF*h=lwrkvqzy!>Z%0=;B>IYvhY9EY649jv9)vLUly$ z5J;A(&-ikzQ!_8gdm(OzKC)MWT>Zg=D&?toq{8gz${+4=6JmC2ZXlo>C4!lT&iT-j zsp|e1T^tg|N-VXI%=Uk-RIY|cj_VJ;wa&#jX;&t{{GJyW0eC8(w!VOw=7sR{wT z=2$}VAuWt;h57CD>!aCIZIk94_J~AwAjv`NiW6=9VG_Ab;`pE$k3 z?ksHl(4fkYWLeym5&)KMZbKIf4x>{oQw4`Q+1ch^h~ytD#kVc!-s~rYaO2D5068_xItsi2j zlpBb!?9XAsPl)fh(M2aIm+Qdu9i*pj_VINadyQsv@hZ8!6R(33Ozbt9L^AH)Mm*)^ zO9V3b9!P$la#8 zW)I3a_u2{Z4hzsy;~~2ETHN%+^!XL|3~?pe93B~M{=^vX1!hSd+VKUsKwmt^*)G~NjSl34UiBc_JErryKlQ|_L z5*WHcl`)+7{Y~U*#277-`&%9(Te(~h7}{#azE@(0z;#3ic>E;I`K?olKX7Z+2H^Jx zen*AHH(PU2sxhGH?be4=$!m9_Qz7Pv#=!m&PqAaBq4Kfe$3dMqJQA8-I{Ia<&pz}5 zmK*NO;uB)!o#n*kRc3mI%$Xz%1`xiQ0NL!$JvFp6eSB7- zO_kF&g3F$7+#a2OoH{q;clv%x4aD)xX%Mp4orxqKZ$Yxl{#=io43(Rcr`s|m5}Q}$ z2js1%1v(+b3dU4h!%hV;Jgpgz$et}7Ir&Q!|DG==P}|XsHUgct@2>o)7#iy|Q+?4c#M(FPmhf%jbRGL6Zxv)i{Xl2K z;s9^a9lm1UJdE-7>N zoAW^UCUYikrZl-!)v+0L+DD}C<3$lV0qK*JzhqH_dax4waUYNLdL5M?>`9(Bjk^5f zma!Y;Xtp(he<6QvDyFA&%Ft+`Alcch?3%(vmjhb$lk0gT#KsT1RZ+eqV9U3s-a|=h z8n&o-lm}$Ko0 z^Y@m<=XOhN+1YUXaV(r4~j_e$}rHL&$;x6Z05xeJFZ_O9fUR+Zg{_>$e327sc znGdchv~$XfvM<@mHC6Q^YOLII2@4Hx@2?5`{B`e8q=}Y2ig$dt=_3!11qB zsH%|1!{$(H{Zy75%f}i;PUtVCmPy=qHXG7Xr%fAIR6eCo{1BgXbT-|{~toO3$_==P_MoliKH%--C-@Z%qJk*Z>Db|I!l}$9=Dh-k)%BX)n z)~)tde#zEttm+brs~X=*Fp^cN!5UUaSaNt2(8vQ%U$v;ZS{p-jq1qM#L-hwy=Q#ptQ6U z%O?j=D|I91Xin0zj$G6en*L#X`zCbewxd(r7WRPVVr6_v?XG#Rn&!zZO0-o{EaM?e zt_>`RcSxS)K6{6W4Eq%PVwA|4gPPu4jSSv6TE-{(Hc^v%@1iz_<%+1U7a=Ri>`T~= z%-&kiYY@2i`fGb-ThKiz+fQ>P?eBFcROl{}(D%B(wkH(5PhQ8d<%B`HpM@j&uQpur z?(gkvzh7Qj2s`BqBlJg5I+V8 zEh@sU`dzgVyEx45lILdBhI~hhz7D4Z08UUnk)xU|C~e>5`F^cO1aeM;jJk;kn?5e! z18&cB!E(eokIWl3H9ut^1NCFt(Km6JR)(Vo3NA`B%9?g!*S+-gzR1_O8V#~kl^-a zU1ScGgIFTe!SAw-+&77WeF|V4T?zx@WV{NQ_RhiVy6w`_!`u5iT-&cK^+1#uNs-|IgtN`8$|bIvQ~y)R5X0U2_n-F zQ2Xph<$Fiwytx^pqa(?rT%gbai zMeX;tWTSjsdk`DHdLyk0gz2Rl*K>cyHSY$SZDub)pgI;|i<6)6XB-#b(R7NHTVmnR zd79l|4UrQCeZQbxS1@X%$f2}n>q%~H0TKyqu5DR^6^#zU2rE3H!!GWPTr{e{V=0F8 zKpsEXF89TtSL~whOyOdKnklC6nD~w~8_YHnDp2qR+~OiACpR@Qam1=_3oyN>XeDg! z+#hwL;4&g7a9&p4{CPK6D*&ToM6y)kI&fEX|4?7#ZaLB1#&TH!6qPQkeS$@WYOHmT zE;mz*XD?^+KSFn`P(!W&lwEBf;~L%7Cy|u|P-~%>5;udpnr{7=7-nDRjcP)iPvF?V zd}q5u6x7xJ@aPMRriTG*^S+2x$$Cn_?jv|)##>wttXr(K25E9dhR^_cnM9HZd695( zXEMrojeH5Up~OqL3D?zM(H$?vy4$AKBj#}}UA!X;@5*AZ)`MwJK0!OzwuIP|r7ScN zi^W*S4P8lV%6srgOv|mG+$*BNfS*Ys#&Yr{640X@c!+ks{)lxJ5JPtU${@Z4@k1W0 z0&NPtpbzL0}b15bvG@C+^|wjje<2WpKMVvC*f^ton*1w(!41OQEmT-2pz*hNnq`qUjA z7VHTa@V=7h+aX=F_@Sf7ycH(Jny=)O0^r}+GM8MB@8|zDU|#aAM!MCyMwsg!8U0M& z{)O)yNPq~TmdC<5Un_d91|tfjtObGJ_37~-@Ymc)G!!BGzC=T`N}h>WXz7+d~4X21~0eA~kr&~kN6$PQL z+LT>iGAp#$yKj9LfmMLIEDQ(IvA&i*_>-9WA@DnHzdm_4s`aF}(x=m)nSq#r7Rk~q z783;%i4ZI+IFM~;n{J6_(;lv$If%u*lY=slcJLkg3k2&i8&*H; zKKN^r-5P{St6c`jV(*mLQVX{|>-Lu+4Au-7JFyilq5UR=*!5+0N1!^AIT7Vsk&nf_ zH0NXkI^DPbwwRpwS%}jEJ~fm#;7v|Pfl7)_G#Nbr{j?7qaKP86)==KFO;3jc@(;D0V4N8#wV5Hok1u2*!sUDd;;_>VTG1j7G&!#p5 zZ`W9n{gL@h)ybL>2^VWAR`gBz#duR5VU|RX0^jwCZp2n0XLQpO2DlCZpS1!HENKaR zhnh$YKy4h5<6H%gBqDvX@+HHO0`E=&ASSU#L6*K0i*Pe>z9BW}#P6Se>d`$wmJffX za5ch=QfPbIPMDf3+84Ns-$2n*PTsuvF}maa3m&!#9}u{sM|w=|bJP=RqUcxYk`A9a zhAobNZCy}{iLyzRPK$k7ldJqD%t{!b@#P-QLBJ)dtaonar;Q&^r>q8`_wq#U)?@|U z%KLNO;QZ17q1mi33+ipLY8Pn{a_vpu)=2($r^I!26f?&x|7;u7Xyd()bwZo~a?cHL zBY8^6c`ATXY;?zk@S-I@ltP-guCpnw=SFiR{~6&bgc+d!eVXwJ;k>>j2pau z&Gbi~p*v2lA(pl}$Z=ygbrIMJ6i22Y4WRLDlm1e&!rT>@#%H&)6~WJg1Xj1M2>Itw z*9jfHI`LOh@_RV9v%-X{RvF+)hlznwtna?o4`#MCa;bp_3WHGvfKL`ds1U!~ogW)e zFi6^d(B+ha^{L+Wr;p!6ZjI4-RsM*th=XOS?H=@;DHxL^!iAFu0CuDzu6%;leFFEOyD1Joy}4XaglzPL_GM4bQ&SAwxn*^yvR}aFcST=q zfs+siI#K)|j46AWX%C(Le9P4cP~;R0?wZde4f7St%QSLb>|pBCtVUY z=$2qLTRfsyE5*BfBGLl}-vq0`eTxnwW5Z>_wGwr7_AUzQTwW;)rrb-aPRIjEIHs@& zyVW53Omp;y4Kye)?%ds3awNg&x6D<{@9w#g*1qpBw5Ev(pPio!e@=Z-?a5XGce zovoK;HrrRUhf>UdJqp%zFmnX49|NPv|qIKTqRmbu}r-Ae84i#BQIbQ zv`B}$)p)N~oPfG~%cihP$uR{>Y47vL&m4kQ!^CvnRBj-jdkuLvtE!GqgIRE$Ynuq`H-m{xfApTU28;F? zbKtfxyX@nhd^;9zKy&!wLTTW=p=#qNpDTDF8-@jUjBfFJ^jyIc*&3ny&d+DGUN7Y` zcr@P&UZ_ATbNG@BLl`%H6wB!{wzZVyhnm0d5qi>24XwtzN;zIi%R#F;K7=Hm|FuRF)^@yF>&feP z)!-rkZq7mjP$542tpKg%s~IrN*!HGii{Ga?%j)|LJa@t)ZBH7Y-%yPiWpd5|&^h9f zDxvKnrwMe>6Bmz?epH%#8Gr3@SmE^A*M6ccAsO7W!~`fWV4|9)-N?eU_Qwwd)^t`Y zmU2F5(a%L;j0F?VCD}kNWEhnWTpFVYuh6-OJt@WTHIsxBP;n4z6?=8)36Mh9(9qH- zsXtj_*Rw4jHlEcqPL|DB|CsBTlayfVnlOC>KF5BVpGx*VaE)JOY!!PKWnx4E1v6*5 z_{E9^{x~AZ(tm(`6gIe3iN%e5;l4?!g)y7dV=W+<86)I8pMC<%*$8(}X}BIs0q3)k z#WGnz(Xv0jZ?!GTyn#TsBQYq(DIAfgc>axT0awumSm)5!!sMaSO*y1}z3wjf)CO70} z#(W`G0ZzC(PHZlcnTB{>7qi#ehVm4w%n)BW82UA_(Yq^m>J2iCf$Q*0`xY03m#|Gu zQw?Pw^uH$hGM7t@+pZjRX3Q+N0ckh^vi7gO1Kl)|i#p?U6kR20unGFB-%2r8a|uc7 zIDwqUU_(0@IQg6YAfNUZt#7gSR?92H_YSpyxrToema_+NAPtwneR1?eKmWNEFjz0Y$Bk{A@XdeZ zb1Cm`(NG*G{N`?>Gp)`dY-QdBwHcJ^-5!*I)Euv^+>$+Etu^{`XMV$P z;lFO5?-C!QGwbS%mOR#isgP*DZD-gW!fgE42O)k)lC-?}q3Hn54+u;B)3MrFtE6|K z3hC$kFVfwnuh9*O-bmN0@Z(9eB}sq z|Ift<$?I@qsS;wAiX!Y>)@rcs)4NmlGk0ZP$;vCU_p)i=)VvY6mAi);4voH6SF!s# z(huiV{Z?a(>8M5PI8`qdogGMiv2m;xrH^L7=3Osp(R76>12~FWN)h1rhP|66A?lHI zmODuMGzwUfFg@{t&1)N0UB}Gw5Qo4z@Y{AQB{@I9p!(~}h6=BjQJ6}8DX>gg>2@q9 z8s6*8>+&waXvo9w%E$c51Qs9SlS4nKxSFV3M}p$f59>u+q%*f+v*S*>Vuu- zsiE3Pe5YJ(9vEe&O@OW0sdO;BxGu=av6DubT;84^Qe>Z<#bBw_LRf|h208ZX5 z#Htv!dw6h7$c$X}2_JsvsBRxb3Y`nn+w=wS6*1zXdXsFg$Dz5=it3DT03S1 z)W9Iz*hMlU85xny3NunG#>}nG4nnJnBirAxHPTIVNCB}|}^G=+em8xh~8k3})@B5kW zb|YVyJv4%(K-j=lSj@(;t;nP^&b=ZBts`Qvo@9xeSh#XAUN#Pz>de#S!@D*uT4^Sq zsb;~L%eQ~c9X04l$S*Cfy(=vY8^5-Iic^6v-jd+!X8oFl9g(Od7?qGWEJtEIa_QpF zB4QNr^+HsBm`~eQ1WI`4jf>o$WtC;s6~UVt~P>U4~y`Cg_BMA&atEudM%-KT7!fP~BU&ij79CtM~QnImP?M Oqn(wLFQ5wOridWX<^QG^5wAkq~O5E~YX6sZz~s8K|! zO0Th^bU~^>?nK{n&U@}T-@X68wRHK+%$~jX?Ad){>WO2ftW5k&5CpMe%#5rc2o65s z5Ca|f=V$D|HU!bSU~TLp?JX@dJVS!e?p`4t-ssq%P_Pa`TDq~J?w$eOkth#uUu>|p z*b<>#42AX57CWV4sbCpu=(k;z}1!AM)o zFETPzLtZ{6CI%gI7#$MsE3c@ot}d^jB(J0-2O#7k;({aHW95P)#HlF$;4t!z@C?U> zMq)#PQB<7n9wAYY+G1j09rYJ-FVBDC4vh*A{EgYmQ{Fq!JIFgYGD2PvttkI@c<)&3 zzqkpG_)A!&>`|A?^ji-MQh_HzZ^_I8tjtGeg_w+W72GSG%EjX%N zG)`b+y#wuyut2C0;5M|yR1}r|uhrB4)2d-eU`V(%P(yERF~#4TsIt?*1iAZqpTc@Y z`uz*rzZNXKgMI&Fh00xE6jdE;gR$T|e^~gv$^Kt!+G0nPo0=PI0uovqSXXG9Upp+Dp zl;sqT$SEDRQB>7XQPWUT0YeQ1h2NNeLj(Kqa*uTX|G`tO0Hx*SsS$~d4D|jz`ePOP zfsubke};irswrp$x(EAei^a-$dHc9W1xAYLM7sM%L`HdGLtLr+yJ9^$^RQIEx@a#rG^R_hPx;AdbGtt!$Z8H zJiWbs9~&Ix&uT=7Ph^aHxVOG9P%mvUeIFkzxX3uv398+ofUPQ^RsVfQjGs5a_CHHi z{_jWm2WwjL|H1s9lmE@3fPH`FfTIE)PyU}S4-Ee4G`)j?rwj)!F~#5$1%hCBEKrF5 zPy`4%m>rc(Fh}TfB)1z$KBMQnzV^}`wnOzOf zJ|h9wi+i+VOhZYx4kp<<%8dD`&2Wi|`Q(jmtv6}DSd<;Pds@X;d0RAJ{#$nFiR-TR zHiGhMp`S{*CGYc({hW`z+Oi&&*>kx`r^0SGg5qE1QpT+h#cE${qz#?cTeWKN)%{hhr5EYkHp{=h ze*1VYA+H^FYTkER)9+9qh;59aW~U<{h?$%E7X}p;^Fa^_!Wik>#1=11#uYn9oum91 zrs3jZcr9HKdj22-?>_x~htkYAN3VXoWISiZE0@CSh}>LzV1_i}tVp_}d(ZYE?LJ!0 zgFIf`G+vx1M3pi6(ODPdM7wtOHZ2MshaiYN|X}%ZhE>prmLlm(n`=gZ83b_hO9Wq7J-+- zb-e0dq%Y|@WSvuZ_cavCSmTQC$TeA|ciTQe2)Vv7BQYe2X*g*E*)zuq7TEXyC=J#= zdI3KT9bs=Eq?ohjl!_YD;@A}K-txp47tv1~yMTARG&RL4`&#yBj?#W;z%BysiqJ(Q z7+m2~+ket3fDbBF$Yp@*!m3`G)8+8@LMXWqRmHM3DH6l8`H66evRFC@`;r$r1-Zh| zFS}@R_&p4wVOB36F|}Uj<80>ML@B5@h+(90Fg<=k5IzJ}hvUYtMRaACF2*tH%}%Wn zM~PUXc4vdxyJj^wDk_92blvEJr z?Hh6@ap1Fq1lEQQDvPav-8_J16*Ng~oXjgN0X*-G?%ULN zfJg@@ak)WRZ`} zUQA*z8K&o-(FYfKgEp|z277$exz$hv2FxH3ccC{ASr<3#4Nfx#D6Lw+&F&2Hy{>5qm%7?q12xWv=Zl*4}iWL2H%gFLea~20Q+B*!00fm8ls6k z&!GoGHM}R_U(}^#UF`th!&p+zvmA%l)Xx?oX4Kg1jXRVzws6qni5o`F6MP0io5*60 zgAExly?0eaS1+V!nG+p^fn1>>>apYOdeFCCO|fbfI~!TDKoy))1=kuH@cHmuE9szW zR(w1C-T`pVF9&B^T-*8ST-8&3L z_CY7wSW{3n+BMv@9Zz2jz1)^}6LC`jSl0SQT@=O!G-!VoojJT;jZcj}_R`Lbk@eW^ zPjMU)4UWk>$VZPIAK((K!|bGvInBWOaw$6xBM1T^%vY67gjQVZ-vr-)Z(HsvUMncw zVqUo^6%}+goiHppc)1Byo?0T>*gDBQBGP=gq43Q1+&o-jEgz<5smkC4Z)J+rUDgX$ zZ&A4x4riGywUBCQmc#Ma%j22jAa zVBg~>vD07eWRz0vP4%tL1I_x<1* zxANTVl1=ZK!F4@OGiwyB4Zl-L2h`ECE~|OpvhL_biB%=s05C=Dddg{R)gQq_-Q5 zN(I)e4lJjr3x6G;24q!$xt^(G*ffdeWNf4Z3TMg;J6Ri(_jHiPx_TL(Q?%IYM_AI zHJ~(6z~Xpj-ps>~s_y?r6|E-Ns21am%yJ_DW&5){H4oY4;rsALN^2VbBJGdf6gRz~$_R$~~b1x_Q_@&k^T-B zC|L42G2JkTuGi2WI2h@n!=toUy9v;nhW$0{=EpXRe0i-;=Hi?$jd^6&O0Z1mRgR4| z*0ccyw5jy1V?KCY9NKMS3_ZAfY)qxS)Jy=PXHiLci<^e$BarZ67<2g1F5ts14>oc_ z2))EWh8^{=cNA-nX4jYdQL1%gylNTE7dOPn9YzQJjI)jaZk3>ij@I}X#FE&h4d;iC zwC3PT0{#(rii8OrnoiiVvZ5L&1~47|YB^jx-QMMbf%{Imctr9TDl875K^2tPpYo1M z$?&(H+t(lBUy`3onUEz|N|(-zdE@xTbVcmV_B{Or90|G2UN!40?ATp3k_1mhx|E?Q z>{YM|`6;o7I7o_0a7~``)FdmD*@;K?pF5Z2u$(Hz6Yv74MrDb!9;*dShS;L1uE2<5 zli-?P)W@8jAfgJXHGS-2B2_X-Ut|{f$vj*-5K_Cqr=f#rIv#vy5*L9#O`O45Ms)4h zq(Z4?K=nZ%KiMysSfB{F0)aa|;|yxq4DcDwm~flzbxSHtV>%>@%5xdysz&)-aYeN1 zJmI|-!sGGbz6@Ry*JSk;1)?EUx;WpI(YHw|IC<_DyYy1Scl=`|m&M5rWY*3GS*p@k z1c^gzlw|Fc_H~`h)fEm?RYnZWS$v}9G&dhw)sCsb`+cnO)$mg=kgeR|S1aHX-9Av; zVJHoFkTr$IJmo)D?*V^z$Kkr(gn^pSrdFz^KR+-J7BYjLfCX(E+m2vCo>SUv0Mqh* zvJTmvYlxGouA@ozrmdD^ypW5@ykufzw5b1;HT-)#0spjX=M~lVe)#}Nr2>PjKN2cl zV+jCC^WmqU>06~;aO!#x3luaewD5xHP1G2@`0z&t>HyRF?ivXk$x}>{ecIOOJ%VWT z@eE}+h1R81sgX@_b zJ|c+nsZoboe5m_fqhXlBeb9Xf3DfjNilx7019~SqDNKY=Yd+#+A>bFWZ)k`ZVjL+W z!RkgOtrHZV+Uy8b-?pwHe+FuFE;PqUd4zrsvhFG2s5JK>yTP zk_gS7CLskb$S9cSe8C3ijCaJJd$mnVUGIQZ_#(4E`-7OCBK(v~O^(1&rZ>lJZbuP$ z6`BBqGkKa`?EoRg=#($A|L!5;llqO7^uhpWQ+D~V-Q$>xz!mq_l7c*gY&W1#>8ZsB zKEDo=r%(N4G@c|(zyXb? z9!((_h%e&x8=u)jS=Y3;LQE>}9B6oE;nwR%!Gh4^E4}<o%tH`xM_Oq%?V>q(mJQwXqdA>pk;}95Vx?TNG;FksE~8RFrLQ^CZVJmhdC(}C zkman0v_l;0MH@(Wz9sT_ZW&SI?oGPrJ`i_>=@UwvjgUGP6XmihlV!xyp1WRP*@FO2 zyyYk|T{)oyFOV=Ez2knpZVV0pumSlfsfd zJAH>{adc3_pg*Z$q1~eD?i)0Gu?HYNz#$Itw{H=b@PtbZ^r_rCb+YQCectmg`Vge0 zv5$H+sTfrGso5Bo{Wjxw=LyN|OQjgQKXgK1!DxQI$uL@!XXD;HyanX9F43GS z%Y30c<~QdbaEsnDJ5TBiK!+XrosUq&Xgo=Sbfk8(;y)x z?++ciHEmO+l9D@SuyRw^Hl*o9-|g|t-02wGpyLQ%?k3ZrFQqLSHBz;r!^iQuIn$Zm zO~=>9Qg??$+jIYQcaE13{!eUU?fJ9WH;wt zFHI*jEm@sMO^LSO{VR7?fm%5?kFVU6kMd_f(bvFnfGhEf&xR|{7@X|ocBAA>{kE!< zYJAJ}9Ub`=XJg9*cFoT*cB7B1_1epH4?y&YBUu!!GS6S%HVLg<(Nwbi35eGF9c`6& zAL05qbQ9M-`vVBc{5{^^v!>m>n~uSD?|TF>#9wGWrTS~@pm!HpSKMN5pmr(_2$~=4Ocrt zT7hEqy&&U>Ma|5e4nNriTDQ!$pGG24ua}yC*AMSEA2H6e7Be{MJI!?`pq20K=I~pn z@xO!=igt9m{obd(l}``$^mtaS{#n;W#(!z<6#Y-@C5P=D!t&xj5)i8eWc(f}%3BC?j&?I@29D&l(xu+x^{O7<@^< zC)cGtybjBgMYd=tc#r%FfCM=E?y)Hnh6ApD+2RZ}i{{iGVQmH;GDq8x1xe8$Wbqdo z?7076^x%0ONYO90=JiItOK02q-sw{pHOvb+T8Au2wTpBGu7-7f602kqq?^#6@{B`p znjM`9#^to>SHfI<8wypNaqm;tD7}K=TtAA&W3pq#^(2A?@rQHT3Iw>2syBdcS)v-U7ti*1Kz`3!j|DI<t7!U4qxKLJt2M>HTFBy4!Ib!eAgu;^_|U7+tNK@a`F?ks{YhO zoOujlx+)D$=JXGYiTYQ>1Nb;cz)9JFEWY(>uWzcLQm;wj+rF9aY$j6wfs6Qa9$s(2 z&f$n%GOv^c6{sr_ZL|5#l6w)BS0C;Q0Z)48_?QBm{K|7JR9&UT<`V;MAbuDw<%4`{ z8pHk9~ls1J*(IDgh>R>dlxaSrvdhRQLrv*oKlH#cAD5~nbE08vL zhG1`-3q*N8E%of)I5m$v+Mo9&!&VHyX&uIGaOFc|w^(6rb@T86Er9*>S6DKfS}+;~ zF+fGW$Xj1Q9o7@h!s8rOnK@QtFf|2=l{nzRBjN)dHdqRnK+zp>kTfWd{4a^XO}PUy zn=A$3L?{evDt(38X`x7{3S#G6egJfk>Bp4?=`1kYG>30`+ijWfYW4?U=&_vQ8*!fJ zpvoE+BEsrDxh5`mzee9Ox`|K)5w8c_@+?+k*|&Z!y8W#dLu;0^5oU-EL~X{bGoVJB zoq>@pNhZ3NX)}GiktgrGz-M|RZ*PNYWo?D8hJ8WfL%MMh8UYnEwW<&p28#gs%u!<7 z?@IaBb)X9%ucnVZ7cE*Y4-!=N4Nm#>CLg5L$shq3Q~-A}c4Av@aQgH$*QC776(T_G zJ)o)|10CQ4FjqJlrh2r>SOruiM)e=+|7Ir$;jDuv3&fTcq&>{B~trsjDtu1ZMrb#?`y;;!v)^4rxzmUc&;&M*2i#V z*jx?38T$2&ts9TuC{QS?y>5Dis~a_Rtw8-}bXdSIuJ`GH;>uH~n5H;s_}5uQ+Jf`q zMaFSS3~+YDarFBVz7L+qIU`QlaPQqQds2niV+#2p^L&+9jUVOI#kV8N7cWS1S>g>+%Nd2#w%LjnTl_vXr0bP!Z)j6{ z`SHAg&7oC%a!UU>`p{n|2)Vq+qm@Coa#JT%uyk$&{$mScuDLe9SxM8zCnxz2sppO zPjS1Gl{~VmOq`xa9C9mDJ5SsqW{@sNmh7zVpa_u4D=!bjD(lqjd%y6Bbd8;|2x)Q^8-UgX=(`LO0CSdeRfob zq&;E9<77M!X=pzI`JRJ!mOXv^CgU_~+}MYBavx|oUeaPOW{AH|dX&Wq`IqZ^T-wu? zRsS%p$GS9VE2>JHz+8FVIYqhKl1PtnXc>cUX`Ic8ostWtXwUDSCbqV|v(fK>Kk!A0 z%6Y<;*-4Mi%U|6}XNBIZz?TJUd1fNIT6M=dN<@#%ryXC}Q>p%8Put8H3rf0o3{_%i zYwgoRN0gz+5Nh+00rP2vpL}Y-uR*opO(Yren9Ocl&DXOknt&IBVnzA#qm(B{&*uN= zVS#eSzFL#ktE_k)Sq5sxd$qS`!}P4iyXmJu$P~4GD^0vkL>^qU+6||Ie&Zx_v|1Yt zMr+l&=(+Mi5)Ffv#FLc&Yp$jgIn-YYMpablRaAAqq!Ir~J(Pf2!oe@1vVtXd?%FHMco^4Cn)6 zx*TbCv}^7A4}K8C#b*`f5q6pf(9lsr(z zcbgM5t#q5Ff~+T^;!_Lw)O6W?j72x?OU*>c z3(QFgKoVs~R#UXcU73n8-8@-J7Bo64L&rg^v{DqN%b;sSpv|g`7oybq_jiE`N}|lZ zUCPI+UAS4}32#GRIjDVBQmL3T1#sFp=M+gtYW3tR z+?P9_t`V+-n8F7#7c&X?JyF@;)Y7;vRxPm}oe$0BQ@!sYxgO>_5EJj>pD5A|y! zaHR=+P$TWS=uqr5x4n2BYH&;dH)p!dj?e?m$DB6`sRc622i!tDwm#Te%UIor7uf7K zqqsxgKqu1bQW=qr#6_wWK0^??1W%ve@Am0-llc!uam?BZOX%bY$3P`cIBznZ?T6_b zWw)N3IqL=sN#6V0i;pQE81yzkl4cWyAZn22Y<<5i+$WlJ)i>nMTB|1q=BKuUuzDhiF`HGl>9cQQ9%7-K{ecXxt-A6J14)Mtf@C_{; zFPnXCeZmuHpMP=@y-z&9HI|*J{2Pvm>=Ij9X6> zH{Kt+(roy-_oWDE^5o3Aam2$L1Wp3m4D(PX%aTv8^a+eE+@5NQ@VddY;|65# z&QBGAMo}iRCsj{Rh;p|qnPYPNKf%a-cm)1*XM@*R*bmUc#aI+S|h`-WoeoIaj;Q}7XFjfRscn5+V zWDs}q!Gm8;sXDP1G>t(I8S^enge!)j9#5SWJx@FYra|us^G;zH0oq|FLGxJx^Fo#; z;i|I{BTyO%xlc(TJE}QEl`;Qf3-y~0d}AfJ@j@vJUW4d91huPFcL-!irQSji=+@Hh zaE+)p2};JQfku_%)7R&tUQd5&o~Krr61H6@WhsO`+2y?MJ2ovcow^-A39KL^$9%X; z+%Fy|I=J@xz3L7(nu zR9fRj?*HKlMEf?qkZRa4Z0?$K^LDWHN}s%UeOWIoYd{r2{7w>HemcU_%sX;t=CY0C z`^SJW)N&;-nIa3QmnP%Tx7cy03HpVM)cP{#g*=h;0Iv@yH`;6EQc{vK3Bt9RJ4if+ zR>bkkS=(rsWP$bPXBwYf`wspt>@Z*Kc{X)+6P-7CYP>PJxtEX7#uo3anzxTnlC6k2 zg&|_0=umsc=>T7{9a%^+UY=)3VN9`h6$|v#zi|+^iZ?v&v;F}5KA?&ETLAN&k^q=4 z>MMV2h1)q6sKW<&GDjWU%z%@1%C9wc(1JIGAAwh!ZSV2l)W$7sjKdlyTY^^D1NUaUL}_LJHR| z8&HzMFUFvqxj1_9irZN-KiL(Zo)hqvs|Z5&SFL_V)%8nC55pfUAEqo1hS>Xyt*;1%qEDhwnC6o|*6Z^0!m`80C#k*_ozPM$0cBa&xE_ z8s!Rr-=uLtQEDp;2n|h(f~OzAb?tw*9x(ib1-xk|WwQDxp~T)J_JxZ^!);aA9%Jm~ zosP1j3#Bd^QOZ;|^GDLyz-XEh6lJ6Ba*$}K`f`GYBuRCgcQbJlN%0HHUnE3F{tfm^ z^t1NQug1Q@bS1trSH8FO9qxx}3*H%j-blPQepfbP<|e@!Q~r1|CH>qGsLKuXSu|NfUBecB_5P(*Hev z2@-)C@3=G}gE8H4%~$xozh3NWX)w%R=)WVp^N;BGQO67O5;R~th@a;T_!r$h5cj^5 zj$TNc?uCDq8dQdjz++$5G|u%qM)|^OgcW=@>7UY|%KX_-jlMy1M}xe_7}P>9WXekz z5YNd9u?S15=Eg;jjL-1>>R&PP*i7hg4>{ubJ$F}?c(p2lKmDt8p8D2#V9yP$teddl z+@c^JkJsnS-EJzz??o8#@d>l=*6wsBG$ib+xVHVuj2eo{FUP>AI|dhL{nlPL$@ z^jeUgXYak}!AaOY`#{QTF4?)~apPDw%svB{*JB~NV#}vKrRGrz$@Y9$BM{=>)JrbQ zX>FPBhVqJA>^XKrs6+1^LtixwuruQmPrsD{9@e`>mjIRBMx%ni2wnr+-A}{IfRva2 z+rYJRN!0O0q9`#D^zl_*H>L6jp90SP(6}`*f#ju!c^U`bB-F;=0bo4c^cikfWTD#0_bl`RYW0S6(k$Fj<}{BA&sipnbx0m}T*&^jdCi8dm+@OuLr?>an!&s)oV-m`Xv7g$@X%mGf1ae$YlxcxzwRjixX(jip$gy}?7T=^BpL#o zoe6aDE%^xQTu%>?jc5$!FpDrY@w|77g@AVo(CYIN~2zoSD&a7}> zs2Uc0yvkVm1v60-KaC6nak#XFP-L(Ct$yC1ROgJa8b@>eLj*|YRGw*(Kar}E(4v+1 zDNlaSz%&dfi@9KV3S>4OyG`HTTc(%qW|AhfFV+$Ej(?#eBWuyWRBnrECC$UG0y zN0qmzWfeCmaxch8gSDWYL(4@8=MaG60Rs2P{>`Hxdsdq;EZ$lAK63;lyZ$Xth^F`> zAN_FUOHLl2{eAvq{f(GR7Y z^XK>@6!F8)FcnpmsX@}h0}+Ie)bQhdAZ17o z)>+neqjyUrBcFVEXd*0eU~>Uuj3@=W?1DL(FT^S)3N0H-D_yC06UVZt>CsGaa#+^- z&Y|m7T(Xp)VbyderZa^ zeZmc5%hELpP0d$^%mUP`9Oi*qL&Dx!$j8jo{c;u(X+d135o?;C1Is)qR;ChU4R+ zC@e?2rNJ{DCF^jjxa#+9=E6DU-qOow@~&{q8>CgaX1b|{3f1mCS0VP0lzRA$LAjc9 z)mJI3;)8kW7Q&G=ph#lKw*CQr8Xm*`)Olym!XZ|u9SJfw|27qz+-Z${vcdAV9cGgB zJxre_r?N&bdVc5L{dg~L@UFD>x17=y%LC{S!TPeXhp#cPSa)pg8GUT|)q1V<2D4UI u4)@NBu2zbgV!QtT#-AHva+HrzMEWq_$=V;eji~>D0b_j3sLH@C>Hh#1-Hj&z literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/affinity_photo.png b/tagstudio/resources/qt/images/file_icons/affinity_photo.png new file mode 100644 index 0000000000000000000000000000000000000000..f4305fb8c67c14e876d11bdd57ccf1eb28481cda GIT binary patch literal 11147 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/document.png b/tagstudio/resources/qt/images/file_icons/document.png new file mode 100644 index 0000000000000000000000000000000000000000..a3dacb01efdab57f803bbe8ec4c9f911ca5f04e9 GIT binary patch literal 9200 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 CuDVJ9 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/file_generic.png b/tagstudio/resources/qt/images/file_icons/file_generic.png new file mode 100644 index 0000000000000000000000000000000000000000..13685e3a60e24547d254059427ee09ec56b95cb2 GIT binary patch literal 5768 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 D6%`Z0 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/image.png b/tagstudio/resources/qt/images/file_icons/image.png new file mode 100644 index 0000000000000000000000000000000000000000..94264aec02eec677d074ad7d623eb54b15429c98 GIT binary patch literal 8998 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/image_vector.png b/tagstudio/resources/qt/images/file_icons/image_vector.png new file mode 100644 index 0000000000000000000000000000000000000000..f0e38a3d9c5c4ed1fdde54c3e3571cd8378e9661 GIT binary patch literal 10640 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/material.png b/tagstudio/resources/qt/images/file_icons/material.png new file mode 100644 index 0000000000000000000000000000000000000000..0c0c10afd37a0f970cddb20a02cc4b9530788d94 GIT binary patch literal 16977 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 NOe~EbpY^)_{{X)gX8ZsE literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/model.png b/tagstudio/resources/qt/images/file_icons/model.png new file mode 100644 index 0000000000000000000000000000000000000000..631db6e9a9ab19037734fb22d14ec9fb469483f0 GIT binary patch literal 13247 zcmeHt=U@BgIMK?nb zgvDE#J3obt1Z4fk=1wRa}Q1`zjp%a|B%Hl!E; z0zp0zo>G*cz+kcg#YpBiT?6np=Vvt;soy9O0Y)+|c88?QLc)EdbX0Xz)n$w~OBsfH z`x-c!Tl`}&_%f0?6%i3?pr#fb9jzLzr5X}WQq$PGcdwedrkbXv3cyey#{@@sQdEM; zvK$hB(J=QR6T|&NBm6>wr8qP_y+R@*jAUd0TG^awxS{{fu?`49BaQy~!{Mh^=5+cwA%2cv-E{$Yf_$@E_g$j&jLK5C9Wp9hCW6z8ow8D zbZ3AM@+A2j^Ye~4^)G7w0@(Tlll}w3kuETj;|@oH{lI?y65;nEmw&;GWb`!E{*BS` z{}%L5TNwP7&o0Cph^O^iynli4W@d-NLwx-L0gUWuc|Zz(&`eijudc3&rs`iDfZ+_l zT0h?ybI%BXrKzr|t)i}{qN(Mqu5X~BX`rJ8J`L2>e^dUA4dTb!Gs5%#hR=xtDMN3f zL4;pKpwI8GzoKG)V8mZve|-k}aYDf$&@-51Btud0_VM+M42+O5j_@RrBO<;1LOeL~ zd-xGUg2{$zoG=RX`vH&&-Zt3CUr=(0Z$z|bxX*qPa4#d7{l30_z>qOghd6N~1!7fQ zRrl{JqEGn%YX7rV)&6}e|0SBC+J6ZD*UtYfp@4ON{RR>h$arf1OnKnrpGngv7-Y(D zkP;DFWYQrBLGuF+@gI%=L5dlX84Wh5{aY_Ko6G%y*>;2A8D{m^8fURJXpeyQX8&!n zEB^FO8>`8%9H~E!R_@qtF6ME5gZu5~fiu?>&$ygaetSxPMnGD|H+y*Xd+omGU-k^$ zJg!61{vn;C_Gd=up7jO*F28CXrm9u(cV^(@g^W~-^eTEp;Tk6uiyqy#0FWdbY`^GEwzKl-Nh?5e<=zI-# z=!EH#V>QWSt-#PU!mVjm^!xYg4m}Oo&4^=jq=|i}lmfxo#uI9FoD4y{f}DQ{D6c>m zf}|k4`F>|g!RIm38{Cop+J#R=A{V4nsRHK7bh5?%Gn?qn>I&Nra-H6SdvOjyvy|9j zt0+b1#wjUoy1|uE@H~krjqiC5Y1zh_%oXLZ&h3jBEH{Y!Iq*ZxRrOYMak&9$!mU5@ z#}((h=V?bgGJI&Ov{&#agI`?27{ig(87!%!P9p?<_d+_ij5v$N!gbBboPwDU*3&( z{G`Gq&Sxtf@-8u`6uKDcpNYe}X=5reVVUmV&zMvhV&}%oj?i(~SGsU>y9aA6KM{3u z*&Bz$`_pz%$!#7lG5J-r4-Lk<>9`&K2`mCZVD<9KAuXO2o`q2ap0160PIF7f%zLYi zj`a}X8H4%5bX;WM=25GS17n{?p}^O-G%|5z53`Z>a*3Yh2Dgzme5Y3c_3RkxPnuA> zM@rSA4Et=4ixu7Zu`x>!PHgj_exm=1XekNV^d86`^Z?^(zj5Z|pzFAZES`ReZNU;= z6dEd>V)QWZ^fy*FsUc8rM)MtAm0pAgW?Gzm$A+F&!aszl%U?hL_+x#8R`$506#i~? zKb5!5L*J%zf_4PM-V4|z52BiHxQ~3z&Q^l27e36!DfNrPIkekt9#SLp+XlohHP7iK z+vkxR2RF_P4XpDHh1V3*f%YDWvlSMFOa=Gyx z%Ub8|aL%;ZS6q)PyS0-$4OvQWR?PL++OeWixrtsfzz|#!RZm+#-!|nd9=50Mb0u!a zeSQjmsKP35dfBlrxHRlM|CN$w_mKq-8wZH9h0RE1S-k&KZkh*_+3w+LtEHK%@4M5S z{zhMxs)lr+H(z7;PUBgLNkpK1Xr8O(N;8$A8O|GKi>Ld~s=$)%9_qHatEkE?^pdv> ze*H}hs|^{?jvjN?MW%)V-dc0ZiZ)w)mG`XQF$r|kv!IJ|W#MAT!7I&M8BsI-tRO3T z?3ZphdCFnE<*8<~Y?>Y3F{hh59z!WwZy#rfe25`Hi>wGt}Q4_YowCei|bN9`WDHh zea^6=Z!{Yd_OYTC75s^@xg)fo4BSJ3P}NnzDvcx4u7rcQvhzEkgw3`xiJH3dvYAO_ zLISRg`vR&MQzVs$)Gg(U5g$s`*Ta=r%|pR5iO+OO)9QJKw%~WSy@o}m94cG1rEd$) zq`YxT_)y}WC$u85Ft^c956H;#@|n~eR`ja6o)g}gQL-V1ju)ULtv8pZ_=QVwMKOV5 zbYL`mb;_ZjB`ZrgnQTM%Cuy)Kiwf36>z*)Xy*xH|89H^x0wtr-^(7vcjT8gRr7D}<)Fra zb(qEs`SJ6fhkwn7wwV;kjmb8vYLd0c*AcYcw^`8fhl)ej*M&L@0LNW$HH0 zcL6?Os!}z(Br@HSo;;6(I@qzSLz6rnsvGp!cZE!GvZBV!op(<$*M@-&Hm%brdBqcd zTFu@vo-Df2i%^5~ST)a6Kb(2xdQ!k+xuU==sbt{E)w(_G+PMSTq3+02MLx7D>Jkt; zE5OG~)!;HC|BV#*Iqem^0rS+cZSLmmpsGPj0=?p4Bk$0ah&#=mFe!;|&21$YfA{t~ z*aGf>o$5Y^evaWcZEP&0#LvGSK5z9Ja5$1Y`{R!F5>OMm;LW0D>}Hh@I2RqHP6&7;*RR<4XVwZfY}v?W@Lq-Epu2hs zl|rEg?4#>DEgbiK+ord7-eOb;sALCuq0C;A9n9Jq@0E>f94KT;TF$V3BCdiRwsgP_ z$(U6uy6=~Gf`eBv;@g#F=|Oix}PKOR$@{y)}7&Ig-hKS;N(5qS8SZOE*7uJ$3jFHjdctG8uk`$N0=0sU;c z^Z9qov*(OahKSKKCSu~-=MlmMwcDcal;^l5;i^Y-c>G^U3<0mC6ANKy{@nx%_*rn4u}8t$Y5dVlP9wlnMv*}Gf_iyoseYjXSKp~ z;F|OwGsdiYum~j)wjGALM)R1F8+MO)GjP}r6ObXqs(8y}=MgI*yeP}9Yt9WTHjd;V z>#h%$5U<>zS6WC_Htl~BoE%l)1Y7OI;1Ge2Fy_4nB^ zd(mq4GDw0&k)@7qZ_Km)= zflk9~)U$qn6z0)OPCUU3n)_Y)h+-UkWc7q zjsP0NH9m7;nK6UUroJr6hfFrRrg8WtWJ?d+N%s|c?P2$Xhn+yMC!EeZQj05#&rB-5 zF3D|cTYk$UFNz29!rVSfTlN-`!3(fSlfLl2DqfK!d0nWlk#|LDq0ItMzZ<1osSuhF zC4Kf)NgU+btev7AD-~HqR$5G z4i#o6cy+QY=COy~dew`vwFpa`eH^UcI)ti|Yq%2ZweVl1mkJ|6-(Pi!(;|z;QCw7 zGT&B8s~lgyK4$}Ik$FS8)VYTs;f#}MIHh(ja6?&ItFS4*6%IT~UDgI+A`=x#$5prb z!Nzc7dt`}U97=smo+-~auBeFjY1lTjs}9$2B{1j-hHz9BF~A+gU1Y^^o73rn^nNXw;+a^|^laz0*Yg2AUfBVG?aLT8DIwh`# z*wSMMPs13$1v4B@=~W|B{OMlRjkA+-)a$EyK(xzD;`y;C7kccc7#4CD(s*%Bs!(q{+;qP@<%d)X^vcedb<5rK{ncACo#g++*37Lwixmwic7lI z`ln?F;ACc8qjYV7zv1(HCajCJLd7Lik&ynCp>WMLbIle;sW(z1y2mEw!nT0%=TCna znPeV!-m#M~ExGcwf@1x6ODK9(;i^W<8za)8ak zZDJ2pOk;i|Rey3v>w>`3YBQng2+lruW(~(o6{i54BRw}qUj*BEb&Gb@Wf{oBEHSFz z)2GAF4_rMq7Izwsf_F_7Gg^fE?v3v6ij+&Z*P#B;!p?{No`_P?1B!Lw=uAa($p@$77X; zJ0k<020~~?^Pw*e3Ij-0>0dbA76xrvz>81$)g|WJzRwy}2x0Tm3A+=MX4`*I`BBRm zi3h}pU31}*8l&$pfW;k{(day zjgUAgMtqwekW_!i+(t0K>gszx)XNXH5Z|>?w_CBN*&jqYAyFMFj|Ud$6$j9U7Ls6(DD zs0l8MZ;N&cB8d2H6#9CYZi}{>g8RQi%K5q0ItLwHST>5m<}$Yw}B z)1;?`@q?tPLE2*lT%tZ<&+2=_n?n$8`T*fuQRABHJt$F@!|z-7D%17wjsE1l#&Id6 z9b(ieTSNl(>2*uul`?*cTBy+K`@obkhc7bI7X+HQPoqMGHEK!eOLJbRLz&=#&)4-T zGowvMfvrOj=U}#X>U*g&G8en{cfE4}Li|j{nszUS5bn8evB2)xEPj53Ppq%TR<7sg zL8ZO{=4hc&4+@~zEs+STU-m6(i|vrSy-QDV1lmrgC;C@$-u!^P z;LK!rR#}kA;TnXZq7&_=(z2)6NklCjSn{z(t>bsIjx_nilzVAdQ(6q)(8X}c+SMI- z-|Fm$c?Gv=YEEB>9nJP#xx?=Zwuw>CkcBFZe8wf&Xx1@p;&Uu9kD@NQ319_R5f-a+xepdX>#|%`s|;1m^7|@2s37VCtp^Fy&If zFUyGKQau?0DmGm~!$j9*aFDtqH~58(>O9M7yULgkJ#M5X?NyFT$rl!bwE7CT_87Achg`6uiM0ca*j%w>Vx#$GL9+9A zVZFV&kZ*PjVntPb8ES3fxqxawrgLI0LMvUcH3~##4>mrXk^iW*{^LW0`p=G^u%+Ts z%k@>785PeF%B&s7B1|$=?IblYV-6eoIRb#?wRJ(4I4Q{kXYX$+FauGb#VA1CH_aW8 zpDvoyP*nje;oQyQJbPaMO22WqhC5gmxZFpFVA-@`0hI$?I!^79GXg{u0bNL8 z)@a&oRs;u@2*)ZSA4CZJ9iyeuIE~sAu+SzAOBD(K0*%|{E@Sp^iKivf7(cu>huNx{ zbMnO$AFv!2i0;-IlND$BfXCH#LpVQb#*2XMc&y1jw%A%{rxe+Q4umu{MOH*AO-+o8 zOFyEFH;vkC=;ud$kYJ&*R#irjaBo=tc*+pOD4OD7@n+S3X1l_*v>9MMr>G)@wCZ}r z-c4RJ z-+MT|+4Ue0Ni%#$nC;b5B3`O(5=22D@Kqlfb~7Ab_2Z?ezc5>gd7wjL-X(sG94xmm$l^n&);s zZ+b{|n|$70(8pA@4o84cC=J_-aiM8Gz^-SmYv`gn<=_)qD~2|eGHf9bf)OSFBO{kf z9NFa%^ZU33?&~{HU$J!j_hCCJo^K8;InG4G+6L*sy`L-fj>6HPbO^LJ`}E~%;B?W) z+k>vpX=6!b<+uX75TK(=ZK-f+mR}eP2RNA9&&A8jy-m^2dDJ@OEC_7pY>&#mVJn6h zq?@z^bypYmUNg7ZHe>%?INzVMxRZ;JNRa5z+Oo2Ry=nFRXtv>NktKoXiUJ{hsy^va z>#(yVAQHHNia(==pzK3nm@mP+n2K@M=2r5tmei(A;1@%40?!~}ckAB$8 z%7+y@w2&(T?Cn67$A;YT8Je{pcO4X8de8CR3J#X<9=37EQxn!VSPeYgH@w41_uWnI zWR%w$PYO$N0wJ~q4L>#zg64EM-_ci)2rboRmnK3+w417iuubNyOup zvf#20SDX~LJgczX5E+JWhJs?(mPm{`H*4MQwpM9Sc~e4d0MEc!c%Zy zLzrK;dH(4|fuJp}4j^zJd7%hhQj2i}o{l$S*|qs0lYucUB~Tjow=@3D&ip4o1~mzoO_7jbt_CM-nTmPOjmwj&t+ zluTbJ{MCEsTx?tr*TQiV{6-k5D}ckx5ge6=N`WsjehQ0^tWIQ~;+@p$G44spJr3-P z@5zNvD)xcm?fP>=oOpbY-Hi5MbkfB^lli$7hj*D`nXPh2lTgqi^M= zuWV7ECmM?M6+6F$oKKT`t(hqEl2XdKI*A0s1pR^Z+1Tk5v~+un-MamPL$AG zaw(O^~+n_Ub>I zRrm_b<1KD|`6U&36-(7tm&62PnowO=T`zlmnwb|UZf^zkY0(#GQ_;G5nj}<OpTDc$dCBmvNO!rkMy5mC8B;p_1{pqIJKIwr~}@ zN=V~qEp$?DP-aIrAlJAJ{=(Fm>qR{WMXy`iV4)<9x4T-cX7>Vm1{D}SR5@lm!_}rR z&_1QTB3Xm(49a_DvG-6yND^eDFi7_k@!Fya9R+nDe-KPcy=%z|!@Jr#_bpZb@st_Y zbX6FI=K&J9l93b0FOc!U;h8D*Nu%-oPiOX$J2T!sm}em@D{nL2?csWY9XFqOF9w4%_mi3pY3c0*8NoBF0fyZ2u&Sz^!3BC zWNr-H3b)D7-UmSBKnp|l8Fj+-QAc+lX;$cs8au%4jROExZIP8wh*zk@s9Soe$kL4V zRnJ{mN`9kQROzVI$hV^O@YTx!o{DC>nQH!zZ)D0mfe^kYGlq1dA8hs@t9a$V-?2O1 z4(|s1;p+8{v8B;6huu^x6^B>xMoHlvd4bL8Zw}}}po((k|Ns2|b@Ko05YCY(^`^-| z&^lHsy0`e^ZZIT)ls~ebN5}P#;k;7s+xBDO?W~PU9!~UMpP{up%x^+MKxC7Eq;bWv6{%(Gjj>bKN{^d)> z6dc|Wn<_iN?C{Zn)S%mPjWsxYWSA)1tA&>ojE;z(46va)WBdX|3W8j@#Eaa{;qXRL z!l7YN+YS{2!k_B!S8!#nV3%$xZ^{tDn0VEOO?Y~&_c8yk!s`Y)5R}N>l*3u&_T9}Z z^&etI(=5^dRvH%8W@*7XoT8<+`5?4UFyQ2aHxCiiArZg0NzmfP z>AWgrp2&VQGBNk28Uay+hq#16j|| zx|O@w&Y-x6vs>O?sZyz?LupA&)G0$SzNo<`;9^cMqk6nJN$2C}+yV66WGuv7%Bodp z@~_va--&lz8?|=L8!vLn6un)Bg(GHA0Z0!-kv%BmIyZqQJ1PK5v?=$o${U9uTVw*y z9?be?Mjogamzg?I&62C94~a%RNc6Qn1(n7LtOzWm!rYveLPr?lVNHB4di`ldv$a1| z+Q2;|FoY1!cfE`&t8WnqXwTlh;`|mIZAo@QJ;xZKi%`iyJ?TkDKN4}z_i{HQJ#q=I)gI5}|Nda$rOvmntUT;*dR zuL9Jl~e-iWnw0^Sa6We*UFhG;44L51obAhr1}#-2`Kf@ zbd(1KDw;hi2k9q1%Xq17NV1ZrPKdUm3G86BF2)!d%|CewSKZOb99^^bQXP;Qg0^&P zv-n|axYln$@D{EN*^8QxnI1KH`K)z0DidoOZ3}JW3`MQ1fJR1{d8;6Kjz5e)r@RvJ zXZb~HA+;8Hj3c~U-6e)ow9K?iWHQz?#FaacQw-M!X7RsqkqQcgWr>HrQL(1;w%qno zO^*z=a=PI-ooD7=J+6qAT^mzRNP$pnh+(B#SYL+-a017|!u?%W*96G7W82Wz43;5D zT!O)$y+?5$ee_WCQqyg5;5vpCqt3c<-N(jDP&5Inj;~xALglIaNADG2P1BU%ZOog> zi5EurZ2WWDK|gdad(&vC-g&HP`E8m}a<}qE9`Mp5?+gh#1T`;|q})Tqor{TrBgdM= zYAdpwOZQPfhsJ$-3%aT4e(+v%(o;QkRnV@Py%fguSi6Tv1ntcV0_qTGGhW;}*pkL1 zx!L%a`oSSd^>eqGqotyNTtzF@jTT9@0oTkDTHU$2szg06RVVB)^kxDJ`@$D#Yh;^@ zlH*VKSGH=9#uz6r`L?33iIgI4Bb1R72zQR>x_{vI3O>Q3ze~+c_YBsw>ggtNQ$yJ` zCNKt%m+A=`Bj4S&{2&Urh3zjy6qhct9Q5oR_qmzPy{VPeP~co-@;-+XP^>;!w*Hot zf3juL-kAsRj`~}t6agq?cc?h%e(Yay62mtx2xzqKap>9`+&2=c3w@yQt;knuhWY|; z?^v{)L^p>BkW)8)yjg?HoBD~#Cxfl?s+C~1q`mJ4>yE^{3WVOiQvxqaQFsC<%GqH?@MPwE z+JtBg*lzREyD z@gbi;&2Z41)yk}Hy2+l(dFv$_)}w}5KgZzZT+m{-4{^Kj*X;rB(io{!sT$T}0^EQ; z44r!NiJD4Fe@AY-_1X}M-#)}uXI#It{~VU;?kCEQ^CDHuzwdE(>}#|t=u}4DX5BJw z1aAP}DQ<#^*XxzjKtA-0_l?|%bp6}6jy*)Yyg%yTDljD6tecfL89Q0{t z)HRx@rJ?Dm9h0;J^u_gLeW7LP9=oy$FoNO|Es(x_e6ytF?jxU%vfSYB%z8yE153Ry z8xLAH6;LloI#<8=u{!G><~)JXreo^KL&22etYZ|c<7|x*v%Xq9R57^ko@yoHke(4w zeDaC~>lt`*Il7t_IC;qQXp|`jLT`A%g9DmJwSWt{PQMEQL$D#(hO6ubcimVY(NOuY zi;H@+h58RxsTvY1Xy7zEjhl`+K5yKne}U0uyId9g%jA;u$=ZwaH$U&q(?3>Hv?K-{ z`QQnPqk`NOpp|$xUpa|PBu%D;l_&|bXnlR#^oGp{amH(v?P0Edw@ zD3j?7dV|{NLZhWXYYncf_$bsP|LaN6*n1=BTgNc-p>lur1a#+1FlHPZII`O7SU83b zG!w6qsJN>m2I;)0hyB5{P!-5$72oz$Vk!+nK@O)o-#<@%o)mC**yG!PoIA3uefy+~ z>nSVQ-FVR}I@HT;9$d3qL1rUwTM%k~W95!KlsU!~$QOTwl1+6!A4lWg+B)71VwrA% zn2@P)?B3{n83J{5QM>{^9X69ZfOo8I&p_%QnYeEQCu#SaR6hTH88sp6U!SOqun&3f$8%zg2MErSl!z!f=hfl5KR!56b|Crv% zD%{bi0PdHdt!e^i4nbe_90Z8EH}2|zi*?1GP3uTT3Ks6t%oV=LeJ@U#jypo!!HNgh zj{GIsRpoeo@I(3UE|@M9jhz3Zb=jJJ!rPt|0PDXUgi>V3*9vwFsNfwRfX>#7mo>YB ztx@Wn;Yh3V-@uJO;1iVN7>o*4T9A;#zYgFGSxQ_hK(i89vXnv(ddYB}*orY*raK67 zhng&(3W2#!$OcmAdx~aF*?*?v{5I*L%WrRB;YfHy`TjwA$s`s72G^{Aic&3Uh5K}X zUL}OFbJ$V41XO7B?d(@OnwoI<2fAB^cIsnX8lDZhiRp_Cl{%d*LH|g6aZV^k#Swo1 Qj3z_)gZAd-2MA~W7s;GBIRF3v literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/presentation.png b/tagstudio/resources/qt/images/file_icons/presentation.png new file mode 100644 index 0000000000000000000000000000000000000000..86a3b37c9eb6cee6ae219c1e09028d526b081d9a GIT binary patch literal 11326 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/program.png b/tagstudio/resources/qt/images/file_icons/program.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d64c1a18fa8a470ea01be63627f60c90ffa238 GIT binary patch literal 6748 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/spreadsheet.png b/tagstudio/resources/qt/images/file_icons/spreadsheet.png new file mode 100644 index 0000000000000000000000000000000000000000..fb1dbeac285d357ab0ba845488832201b204d5a1 GIT binary patch literal 5721 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 literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/file_icons/text.png b/tagstudio/resources/qt/images/file_icons/text.png new file mode 100644 index 0000000000000000000000000000000000000000..79d7d91b03bfd02a16b927ef9ae3f8d72d117c17 GIT binary patch literal 7126 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 B5_$jt literal 0 HcmV?d00001 diff --git a/tagstudio/resources/qt/images/thumb_border_512.png b/tagstudio/resources/qt/images/thumb_border_512.png deleted file mode 100644 index 605717e3de726590cfadfeac82e232272c9c4638..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5649 zcmeHLXXd;UWs3VJ`MiB@oDoB8!C`Ji^c>&k4o%t|d{g7YYegCuHbIv{Y{kl4E zg`xf&eE@)=pRe~?0CbQ?9iTTA`Fkn2GYr6#FltB`H!Q#(M`Y5i2_$AT*;+tnAvgf8 z?gAEp7)R!sN0TX31|B^itwNhqNqBUqeSl2>%ZnUK^%b(o!NR~0qA-r=OhUW6>AMPW zhyXg7OE4GEX$%fdfJcwZ#Uany*BG?I2J7tXjIpuB*xFhl6jq!B2A3ePVsI8|MNG)> zCUb~vDvL{HGR(Cy3DHa*7mr3GxcOV*B;tG7EFPOSE}2BckZEK(nZe~?u+~`2$Mj?Y z^@BAG&f9Pyfx~DKgxuy6axOLIFUYmXXXLC{CYQ;HWwPEI^@)o2@Mnxjxc{6*Kw$kD zaX`R-C#Tataf8G4iAMtWb`w6a>74>jNCJzDSxe?Hd2AxtCmxC3qVd3K=~y<>W4(&68;yxwhGK$Nkkl%%B7LVR})p?LE}!WCYCg+wiIwQ0)v7_ z3#>@w7y^&RMZ0lhdGu%ofl7;{5}6E+D@I#EH0oPHU#%?}bF8(Kwf(0I{8%y~?62dA`Ph?pmbqd+vwgzxk6H=BH}MW>QAn@Dyl-~M z;(fa$Gmsw1Mw%gW_vv~7P^nZT6+TM=0KVVD+jC=;r^h!ts?L;IO!YYvyKqhMvZ5b1 z-}da8BJ(sXYk4{}vtW}F@@$z(|7mMMK#7lL%2~tpx*lsL|LfdcANop0_vYWd8*RFi zUSxBo*-@%z`2RqyxNF$szwSklaK6D2pB??%mg7#C#C>Hc!OhLmTgyvo4<-(8$P6zO zwpAT^2Ifzsf82KpTbgvV-17F(94gp;mbIqr zpJg?Vp2h1IFO029{Ud*EQ*x7Yg7|?XNoL>I?^RXPnwf%}Wt8%Ko__*zs(r=s4d(!0 zV66S2z`;YN08G2(=j{<9IMgrqNZa%z;p#)x+=3{=w@YWI?%L|v9Qsdm@z#HoUS8b( zGii2SO;OanmBqhtWdWyu(Vy9~7djSlWDWXj-~45BUA#TFF2%;p!d`Z=T`SGvxU=28Bxru?s|DbJ zUQ02oDpcG$T*8MHZKgziwuxD5*cqu=0c?>O8F>Php4&1XyiSv>gSrb98SwBE(?^4U zU`kJcr%Gv|;yyea18Eu--&`>`-oCy!NwYca2 zx9hd=a5p@>8(TNTo3-8&NYwt2=685hn8O3&jEx0w${gZ|%EQEIHQ=heprNDdN0QCJ zd3^3otABx}d1W~)9Q1Jwoq{uiZ@h-I&BqF9guUlf#U&Lnd-4PppxMBvwzUN#H+?OKq9`}QLnxEaUg#zFSNW>?%u0BE>X9ib<_bUiTX7= zew60CDs*UYP*-)jelrh>>ep{3<+Wd8AVLCa9-h$+0s@&X*3{?-c2PM6L=i*C^`4MGR#gWu570 zZ`F`I<%~hCfGK zw1{e)JE`i0pFx{kZbDi5Kt2jsKfmm}jS5cgkn`n=$5pvo27?$^L?~b^QRmO*j0`Y% zo!Q0~o8pY#pu~$#m{jR!38Np(O$@@_m*2Ap6h3s28v`3J%!0WWb9?2Q>IMr}mo1kW ziRawGJZi=3R#eKNte&P2NdDTldY^IJ=;pNNm*UF}K@ripFvWgLb95J9bt(dBhVDUh z(LGSzB2jNJw$p4zl3SU~?yMf`RVFf3hA41bq89K|mSJ0~&ILoe{fmZLOdq8f5pRQP zVV&t%FjNvS@71+HDCeNl)1h5`dcbLE@M-m(LqNdv%0_`{SMT&4o(VD?&ddf6SpE&l zaMZKW?GBT`y!3Dsi{0P@#7D`tU^!n^3X0!Pw5r`9k-a4 zh|Gdv{K$R{o~M^M72Lfe?>Ly%gN$v>ji$tMxG|_mHFTwU87Mt>;(&r(MwZ6F<&b{sr)ZJV;P zd-{M8Xg(?(=uwta>if$jiImN!?CfSKl*v3c7DQR@cRy2wgk73MSdQvD6kGE&G6rw=snhL?Srd5`Wk5BhTynHhg^O}HJ;PaHJ z3##Wx)RoFn)#2pJ9T83R&Q2G(W^kv8GGYX~HCNTyG-E+W_1Gkk)WuUhf6_OS5Hv=} z-H>pBlC+Xp-&P+8Y2qNwBh*0mQFxd>ekFip`LHnM%|HgW?yBxa6X3=}QVN!oz=4qL z`jaZ7buuC}B!J`z)%m$4@W3wY$)Nd8mVkk{W*kujZ*X5Z9h{$k5|Y^w)6AOignUo{ zD{5WzQ`SJL5}`FH-I7uOGt;o`S+0YNE#0p(6H+md>S6e?)Yri~OU5Ou3|eSI6o zn({2Qcw~V%=$4!xDzk%z8sOp9fRl#ck_2uqdV4vGLoR2QUJ~sUuLP3qgND4{+yS7d zb3K$(2#3Oosyfq5M^S3=*l+FftFXcn8uGPr>prjQZ9b<0V8u;_)!`kdRlPYd&b_W# d-c>s`Ir7%2y!)Dna_yH~Kc7JFlb%sq{|yg52o(ST diff --git a/tagstudio/resources/qt/images/thumb_broken_512.png b/tagstudio/resources/qt/images/thumb_broken_512.png deleted file mode 100644 index 5022f2eb32c9dcb353cca933f443f6a3337859aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24707 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 Ym$pBgQy!fa1rI=4YWjCdZaX~w541AvfdBvi diff --git a/tagstudio/resources/qt/images/thumb_file_default_512.png b/tagstudio/resources/qt/images/thumb_file_default_512.png deleted file mode 100644 index 28dfbd433568600ee6397a0baec5ad25791308ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12661 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 G-})bN^c95w diff --git a/tagstudio/resources/qt/images/thumb_loading.png b/tagstudio/resources/qt/images/thumb_loading.png new file mode 100644 index 0000000000000000000000000000000000000000..174a1879d211a7a98963b903087326172ee79056 GIT binary patch literal 18128 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 diff --git a/tagstudio/resources/qt/images/thumb_loading_dark_512.png b/tagstudio/resources/qt/images/thumb_loading_dark_512.png deleted file mode 100644 index 7dcd99db78373733cc38310a1ebc5f4a166bdd09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11270 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 diff --git a/tagstudio/resources/qt/images/thumb_mask_128.png b/tagstudio/resources/qt/images/thumb_mask_128.png deleted file mode 100644 index 52a0a1353c955e7a7215bb1378fb3f9ad98e3a09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2245 zcmeHJ`%@EF7+pbL0mdrUA_KBr>fnoP5+;GHNkCqbYA7HKi4Vpl*(3`|c1<=Az)?h{ zLw$@$r7aIpr&>l3wU$;bQzBGl%2>pf+A30U5NBE~l{#WUv3C;+QndXC`onJa-gCaQ z-|K#x(&VI-zMKFK1VO&?#JE)OTf|`g!#f~o<{2_g$tV@8F*Rl6;uxk-;xp5SY|Rkqn`Nr=VjAJ(*ZwCe{=rrKt;Y)M5=1CF4kJ z7!WWL3=Z3j29pJ|NfC!G2An)Vlm1SpaMY{V1!xnO$=@eGg(4d38zLJVNsh&njt9@ z%xdCUl$DVp2*BZSal%GUs+%n1;e$k=EQ0d6JoH66Luy|^XOU@iT2C>QMNiQa0Ze(t z1U$_Ur0?Y>Hk^JLS)urMy3sf#3=0#V3lcG&)G0pw{Q^r`K24yhgoU!2)kJ(QVPZlY znPBsRC6hM7po}ApgvkQ3D@FK%$bX=!SD~?#fikZFZAAbxhY_1QOm4(=L>ZQ;`Eii|F3Q7aq3wKyeLh{(y zWHY5D4FI#Gu8e`@@v$PlSR@J)aGjxm!ePLc)aJ+G3}6X(g77e&IE)vO#us722rOI# zo-v-&9|s$p2Mx~P|HWs!8kPVHg#t??EevJOcWgOZeGRe6xpFQIq@%}S*imT=SF=fy zBD9&(Sk;8a5f+ehq83Wad??} z$58gr{q30qo#wm5L3rVm0z2m(IQ?LZpc8WhEG8xqVFKgF4CYL7d#FDI&DchQs!Xc} z1T8AFmbI*kNtt^&io~OK7w-rNcljni7;eso!#yRbdG>)y%e2M-UGaoc!+oApG6^ zdoS(m+_9l@ee#oUzgI8ZA{3|Jp~{=+tFtO{aozFv)aeZ$H#MAUuI@Ex(-;4Id2^|> ztjb${fA(NPXd)Dxbj<&9r|0(F-$;t9-gMW*@7df*jV)WczrMZU)4i7ji?8)>Dm}lX z+iy~rNQ_>`B>}Fyp~4^zuqhT zB&lfF9$@nhjQV4B=H4*c%}~^7zf!xPy2!H8qbK6|-TOsno~fb)Gd_53FTR2gWNjS? zJRRvFdu>;Seb()pUk_Dn^=&9=d_Q*7tNEacyE*8qPEKTv$~3xZ?K}3ok$PQIjncHU zta2r~?=&a65$gZ{_F@jS8KiNjkV9LKHVIUfd2Tj=A5f+==SfmX?AmHq}vap q;w=LoH3zP%*fr+<#HB~_bm$HHd^ewzL*icc--kRtDXuYQWAR@VJTlw> diff --git a/tagstudio/resources/qt/images/thumb_mask_512.png b/tagstudio/resources/qt/images/thumb_mask_512.png deleted file mode 100644 index ce641abc48961a476b30cd87ea346c215ad834db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4902 zcmeHLX;f3!7Cr$P1T0IjMHC^%K%7WI5RjMnKF}WmxBsa|f2~dnu<@qXD zZxkp)X|)cupcF+#sen`?l|gN(78R#d>Qfo5G$r7>2@dtyzW3+-WUXZ7ob!Eq-#wk3 zz3%pysIW9N6~FNCojlORwiq;v`;Jw2VA?n6doe2RBKKmdhGqtIwx7{g1hmMAzXFNu7%NyV6s zU|7zT38e}lBEg$2^LpLFB z(WL@Jfyf1j^pyi|JmMAnmLL|szZOw(q<=+bv;R9?EPlfcxgsPD3&e|{zG2hL2gu{p zQkW78%MqoF3x}k^62)wDBusIk#|TxhC?QxVh9z<=x=ezvH|;;r#J@v>5D_9*Vl|+d7GiHKWDB(56B>2;?!ZIb@ z3?=?0^ePv0vwduYhneSNmU<0AK|wJxBv~lJFnMg)GCUL#5RWq{+-4z`mAA41z?@ymUG)*mR!r5ggdEn6Egwyv zPCN`X*_|t}-oN0GweQ80E8RzzJn8>z@|Eew?W|Tr)NlL4K4nsI&gXNs2r6q2?bj>U z+b!4EmSzSHW%dT#%p@nAbMg54S|+~aQfi~7J-|It+PPkEE|j$1bwx-#f0+{}rL&u< zOs-Rw)TERTUwi-F#C4lbhu-xL=@>YtJ@-SZxP<5Z`Zuu|bvweZmOvNdO9_d|uFn~%&pC1@->F>g-F1z6;qa%{nb<;w;w&gUJI22XM3 zdq$E6Gvpgk!-s)VXL&EPJMc{R;_lM!()}%6GE@#lc_zAK&bvM?pY#G0kMQsI(iuHL!XKj}F`(bO% z>LG2`ZvD4Jgk)%6m`2ZoGIr~0h%9KpIQ5wg5cP(l?j%D9nz^%jU)UtgAe3FK|8aJY z4Y*#P3Ux&er38c-HRLKUnHtN(6000~_T!iR?k-wOhoa#M+Ec z@(~4%Fz=9N+Ct07>K{9sy-u5th20pzb|P%GK{6 zn#F%KW6e(B${r9ht~!Hv_RWCSU#z0v`C>LezSBE`+>Y%I@(IjK^cV9L8^hxo?a&gYjlC-VFW^ z%|Ni5C%%#cz=IU=ZvC^pVK~szQMm$L^Rb&1pvGgbE$k*@Lp>3I&o5)gkH(I#6`~7B z214^LD@SWk^zCoNc1f2_lCm)e2OivC?$=}a3DKP_BZ7vTSw+N%8Jk?A2Mh)EI89S` zHoAwH?za=%j2h^_cfk^yeZrs~q9e=U*IVazSb-v~jeO*%pwTdGRu3XtYRuz` z9X?uYxU&fw-J}1-%E1m4Rl0i`h99`!cOJ<@jkBA-Qs>)(qOVe+C{pc7!v(=(`ysS0 zpnWn9_zdk6eQMN}6^^WfWN8nYa3ERi+g#i4pKhmdaw)KKEBWOgK=bKoY~CXJ>sZrR zR#gu}QEcq&b+EAlEo~WSe08h7oKa_&bh9p=?&TF?cS$%CAd$B+MLDEbYKvh11&KAxBJ-z%K$nE)XoM<%)~W+iyv(ONeg zU?rgGR)IV`>HZrRd z*;m^=)N`A+I^>dGPVVh}NoO5VBe}Uu_`HEJ8nL0RkJqS$R-aINwR0Rm->Jk1S!&6( i-yEEWDq9D1&mHN|OK7-%wu6+%TvGuSW= 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 ON%Hdb%w4^E|Gxp|9T9&3 diff --git a/tagstudio/src/core/constants.py b/tagstudio/src/core/constants.py index aceac14a..48c087fd 100644 --- a/tagstudio/src/core/constants.py +++ b/tagstudio/src/core/constants.py @@ -6,118 +6,10 @@ TS_FOLDER_NAME: str = ".TagStudio" BACKUP_FOLDER_NAME: str = "backups" COLLAGE_FOLDER_NAME: str = "collages" -# TODO: Turn this whitelist into a user-configurable blacklist. -IMAGE_TYPES: list[str] = [ - ".png", - ".jpg", - ".jpeg", - ".jpg_large", - ".jpeg_large", - ".jfif", - ".gif", - ".tif", - ".tiff", - ".heic", - ".heif", - ".webp", - ".bmp", - ".svg", - ".avif", - ".apng", - ".jp2", - ".j2k", - ".jpg2", -] -RAW_IMAGE_TYPES: list[str] = [ - ".raw", - ".dng", - ".rw2", - ".nef", - ".arw", - ".crw", - ".cr2", - ".cr3", -] -VIDEO_TYPES: list[str] = [ - ".mp4", - ".webm", - ".mov", - ".hevc", - ".mkv", - ".avi", - ".wmv", - ".flv", - ".gifv", - ".m4p", - ".m4v", - ".3gp", -] -AUDIO_TYPES: list[str] = [ - ".mp3", - ".mp4", - ".mpeg4", - ".m4a", - ".aac", - ".wav", - ".flac", - ".alac", - ".wma", - ".ogg", - ".aiff", -] -DOC_TYPES: list[str] = [ - ".txt", - ".rtf", - ".md", - ".doc", - ".docx", - ".pdf", - ".tex", - ".odt", - ".pages", -] -PLAINTEXT_TYPES: list[str] = [ - ".txt", - ".md", - ".css", - ".html", - ".xml", - ".json", - ".js", - ".ts", - ".ini", - ".htm", - ".csv", - ".php", - ".sh", - ".bat", -] -SPREADSHEET_TYPES: list[str] = [".csv", ".xls", ".xlsx", ".numbers", ".ods"] -PRESENTATION_TYPES: list[str] = [".ppt", ".pptx", ".key", ".odp"] -ARCHIVE_TYPES: list[str] = [ - ".zip", - ".rar", - ".tar", - ".tar", - ".gz", - ".tgz", - ".7z", - ".s7z", -] -PROGRAM_TYPES: list[str] = [".exe", ".app"] -SHORTCUT_TYPES: list[str] = [".lnk", ".desktop", ".url"] - -ALL_FILE_TYPES: list[str] = ( - IMAGE_TYPES - + VIDEO_TYPES - + AUDIO_TYPES - + DOC_TYPES - + SPREADSHEET_TYPES - + PRESENTATION_TYPES - + ARCHIVE_TYPES - + PROGRAM_TYPES - + SHORTCUT_TYPES +FONT_SAMPLE_TEXT: str = ( + """ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]""" ) +FONT_SAMPLE_SIZES: list[int] = [10, 15, 20] TAG_FAVORITE = 1 TAG_ARCHIVED = 0 diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py index d457f716..d4a9aa3d 100644 --- a/tagstudio/src/core/enums.py +++ b/tagstudio/src/core/enums.py @@ -14,7 +14,11 @@ class SettingItems(str, enum.Enum): class Theme(str, enum.Enum): + COLOR_BG_DARK = "#65000000" + COLOR_BG_LIGHT = "#22000000" + COLOR_DARK_LABEL = "#DD000000" COLOR_BG = "#65000000" + COLOR_HOVER = "#65AAAAAA" COLOR_PRESSED = "#65EEEEEE" COLOR_DISABLED = "#65F39CAA" diff --git a/tagstudio/src/core/media_types.py b/tagstudio/src/core/media_types.py new file mode 100644 index 00000000..bb190700 --- /dev/null +++ b/tagstudio/src/core/media_types.py @@ -0,0 +1,507 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +import logging +import mimetypes +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + +logging.basicConfig(format="%(message)s", level=logging.INFO) + + +class MediaType(str, Enum): + """Names of media types.""" + + ADOBE_PHOTOSHOP: str = "adobe_photoshop" + AFFINITY_PHOTO: str = "affinity_photo" + ARCHIVE: str = "archive" + AUDIO_MIDI: str = "audio_midi" + AUDIO: str = "audio" + BLENDER: str = "blender" + DATABASE: str = "database" + DISK_IMAGE: str = "disk_image" + DOCUMENT: str = "document" + FONT: str = "font" + IMAGE_ANIMATED: str = "image_animated" + IMAGE_RAW: str = "image_raw" + IMAGE_VECTOR: str = "image_vector" + IMAGE: str = "image" + INSTALLER: str = "installer" + MATERIAL: str = "material" + MODEL: str = "model" + PACKAGE: str = "package" + PDF: str = "pdf" + PLAINTEXT: str = "plaintext" + PRESENTATION: str = "presentation" + PROGRAM: str = "program" + SHORTCUT: str = "shortcut" + SOURCE_ENGINE: str = "source_engine" + SPREADSHEET: str = "spreadsheet" + TEXT: str = "text" + VIDEO: str = "video" + + +@dataclass(frozen=True) +class MediaCategory: + """An object representing a category of media. + + Includes a MediaType identifier, extensions set, and IANA status flag. + + Args: + media_type (MediaType): The MediaType Enum representing this category. + + extensions (set[str]): The set of file extensions associated with this category. + Includes leading ".", all lowercase, and does not need to be unique to this category. + + is_iana (bool): Represents whether or not this is an IANA registered category. + """ + + media_type: MediaType + extensions: set[str] + is_iana: bool = False + + +class MediaCategories: + """Contain pre-made MediaCategory objects as well as methods to interact with them.""" + + # These sets are used either individually or together to form the final sets + # for the MediaCategory(s). + # These sets may be combined and are NOT 1:1 with the final categories. + _ADOBE_PHOTOSHOP_SET: set[str] = { + ".pdd", + ".psb", + ".psd", + } + _AFFINITY_PHOTO_SET: set[str] = {".afphoto"} + _ARCHIVE_SET: set[str] = { + ".7z", + ".gz", + ".rar", + ".s7z", + ".tar", + ".tgz", + ".zip", + } + _AUDIO_MIDI_SET: set[str] = { + ".mid", + ".midi", + } + _AUDIO_SET: set[str] = { + ".aac", + ".aif", + ".aiff", + ".alac", + ".flac", + ".m4a", + ".m4p", + ".mp3", + ".mpeg4", + ".ogg", + ".wav", + ".wma", + } + _BLENDER_SET: set[str] = { + ".blen_tc", + ".blend", + ".blend1", + ".blend2", + ".blend3", + ".blend4", + ".blend5", + ".blend6", + ".blend7", + ".blend8", + ".blend9", + ".blend10", + ".blend11", + ".blend12", + ".blend13", + ".blend14", + ".blend15", + ".blend16", + ".blend17", + ".blend18", + ".blend19", + ".blend20", + ".blend21", + ".blend22", + ".blend23", + ".blend24", + ".blend25", + ".blend26", + ".blend27", + ".blend28", + ".blend29", + ".blend30", + ".blend31", + ".blend32", + } + _DATABASE_SET: set[str] = { + ".accdb", + ".mdb", + ".sqlite", + ".sqlite3", + } + _DISK_IMAGE_SET: set[str] = {".bios", ".dmg", ".iso"} + _DOCUMENT_SET: set[str] = { + ".doc", + ".docm", + ".docx", + ".dot", + ".dotm", + ".dotx", + ".odt", + ".pages", + ".pdf", + ".rtf", + ".tex", + ".wpd", + ".wps", + } + _FONT_SET: set[str] = { + ".fon", + ".otf", + ".ttc", + ".ttf", + ".woff", + ".woff2", + } + _IMAGE_ANIMATED_SET: set[str] = { + ".apng", + ".gif", + ".webp", + ".jxl", + } + _IMAGE_RAW_SET: set[str] = { + ".arw", + ".cr2", + ".cr3", + ".crw", + ".dng", + ".nef", + ".orf", + ".raf", + ".raw", + ".rw2", + } + _IMAGE_VECTOR_SET: set[str] = {".svg"} + _IMAGE_SET: set[str] = { + ".apng", + ".avif", + ".bmp", + ".exr", + ".gif", + ".heic", + ".heif", + ".j2k", + ".jfif", + ".jp2", + ".jpeg_large", + ".jpeg", + ".jpg_large", + ".jpg", + ".jpg2", + ".jxl", + ".png", + ".psb", + ".psd", + ".tif", + ".tiff", + ".webp", + } + _INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"} + _MATERIAL_SET: set[str] = {".mtl"} + _MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"} + _PACKAGE_SET: set[str] = { + ".aab", + ".akp", + ".apk", + ".apkm", + ".apks", + ".pkg", + ".xapk", + } + _PDF_SET: set[str] = { + ".pdf", + } + _PLAINTEXT_SET: set[str] = { + ".bat", + ".cfg", + ".conf", + ".cpp", + ".cs", + ".css", + ".csv", + ".fgd", + ".gi", + ".h", + ".hpp", + ".htm", + ".html", + ".inf", + ".ini", + ".js", + ".json", + ".jsonc", + ".kv3", + ".lua", + ".md", + ".nut", + ".php", + ".plist", + ".prefs", + ".py", + ".pyc", + ".qss", + ".sh", + ".toml", + ".ts", + ".txt", + ".vcfg", + ".vdf", + ".vmt", + ".vqlayout", + ".vsc", + ".vsnd_template", + ".xml", + ".yaml", + ".yml", + } + _PRESENTATION_SET: set[str] = { + ".key", + ".odp", + ".ppt", + ".pptx", + } + _PROGRAM_SET: set[str] = {".app", ".exe"} + _SOURCE_ENGINE_SET: set[str] = { + ".vtf", + } + _SHORTCUT_SET: set[str] = {".desktop", ".lnk", ".url"} + _SPREADSHEET_SET: set[str] = { + ".csv", + ".numbers", + ".ods", + ".xls", + ".xlsx", + } + _VIDEO_SET: set[str] = { + ".3gp", + ".avi", + ".flv", + ".gifv", + ".hevc", + ".m4p", + ".m4v", + ".mkv", + ".mov", + ".mp4", + ".webm", + ".wmv", + } + + ADOBE_PHOTOSHOP_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.ADOBE_PHOTOSHOP, + extensions=_ADOBE_PHOTOSHOP_SET, + is_iana=False, + ) + AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.AFFINITY_PHOTO, + extensions=_AFFINITY_PHOTO_SET, + is_iana=False, + ) + ARCHIVE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.ARCHIVE, + extensions=_ARCHIVE_SET, + is_iana=False, + ) + AUDIO_MIDI_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.AUDIO_MIDI, + extensions=_AUDIO_MIDI_SET, + is_iana=False, + ) + AUDIO_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.AUDIO, + extensions=_AUDIO_SET | _AUDIO_MIDI_SET, + is_iana=True, + ) + BLENDER_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.BLENDER, + extensions=_BLENDER_SET, + is_iana=False, + ) + DATABASE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.DATABASE, + extensions=_DATABASE_SET, + is_iana=False, + ) + DISK_IMAGE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.DISK_IMAGE, + extensions=_DISK_IMAGE_SET, + is_iana=False, + ) + DOCUMENT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.DOCUMENT, + extensions=_DOCUMENT_SET, + is_iana=False, + ) + FONT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.FONT, + extensions=_FONT_SET, + is_iana=True, + ) + IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.IMAGE_ANIMATED, + extensions=_IMAGE_ANIMATED_SET, + is_iana=False, + ) + IMAGE_RAW_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.IMAGE_RAW, + extensions=_IMAGE_RAW_SET, + is_iana=False, + ) + IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.IMAGE_VECTOR, + extensions=_IMAGE_VECTOR_SET, + is_iana=False, + ) + IMAGE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.IMAGE, + extensions=_IMAGE_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET, + is_iana=True, + ) + INSTALLER_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.INSTALLER, + extensions=_INSTALLER_SET, + is_iana=False, + ) + MATERIAL_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.MATERIAL, + extensions=_MATERIAL_SET, + is_iana=False, + ) + MODEL_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.MODEL, + extensions=_MODEL_SET, + is_iana=True, + ) + PACKAGE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.PACKAGE, + extensions=_PACKAGE_SET, + is_iana=False, + ) + PDF_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.PDF, + extensions=_PDF_SET, + is_iana=False, + ) + PLAINTEXT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.PLAINTEXT, + extensions=_PLAINTEXT_SET, + is_iana=False, + ) + PRESENTATION_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.PRESENTATION, + extensions=_PRESENTATION_SET, + is_iana=False, + ) + PROGRAM_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.PROGRAM, + extensions=_PROGRAM_SET, + is_iana=False, + ) + SHORTCUT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.SHORTCUT, + extensions=_SHORTCUT_SET, + is_iana=False, + ) + SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.SOURCE_ENGINE, + extensions=_SOURCE_ENGINE_SET, + is_iana=False, + ) + SPREADSHEET_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.SPREADSHEET, + extensions=_SPREADSHEET_SET, + is_iana=False, + ) + TEXT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.TEXT, + extensions=_DOCUMENT_SET | _PLAINTEXT_SET, + is_iana=True, + ) + VIDEO_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.VIDEO, + extensions=_VIDEO_SET, + is_iana=True, + ) + + ALL_CATEGORIES: list[MediaCategory] = [ + ADOBE_PHOTOSHOP_TYPES, + AFFINITY_PHOTO_TYPES, + ARCHIVE_TYPES, + AUDIO_MIDI_TYPES, + AUDIO_TYPES, + BLENDER_TYPES, + DATABASE_TYPES, + DISK_IMAGE_TYPES, + DOCUMENT_TYPES, + FONT_TYPES, + IMAGE_ANIMATED_TYPES, + IMAGE_RAW_TYPES, + IMAGE_TYPES, + IMAGE_VECTOR_TYPES, + INSTALLER_TYPES, + MATERIAL_TYPES, + MODEL_TYPES, + PACKAGE_TYPES, + PDF_TYPES, + PLAINTEXT_TYPES, + PRESENTATION_TYPES, + PROGRAM_TYPES, + SHORTCUT_TYPES, + SOURCE_ENGINE_TYPES, + SPREADSHEET_TYPES, + TEXT_TYPES, + VIDEO_TYPES, + ] + + @staticmethod + def get_types(ext: str, mime_fallback: bool = False) -> set[MediaType]: + """Return a set of MediaTypes given a file extension. + + Args: + ext (str): File extension with a leading "." and in all lowercase. + mime_fallback (bool): Flag to guess MIME type if no set matches are made. + """ + media_types: set[MediaType] = set() + # mime_guess: bool = False + + for cat in MediaCategories.ALL_CATEGORIES: + if ext in cat.extensions: + media_types.add(cat.media_type) + elif mime_fallback and cat.is_iana: + mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0] + if mime_type and mime_type.startswith(cat.media_type.value): + media_types.add(cat.media_type) + # mime_guess = True + return media_types + + @staticmethod + def is_ext_in_category(ext: str, media_cat: MediaCategory, mime_fallback: bool = False) -> bool: + """Check if an extension is a member of a MediaCategory. + + Args: + ext (str): File extension with a leading "." and in all lowercase. + media_cat (MediaCategory): The MediaCategory to to check for extension membership. + mime_fallback (bool): Flag to guess MIME type if no set matches are made. + """ + if ext in media_cat.extensions: + return True + elif mime_fallback and media_cat.is_iana: + mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0] + if mime_type and mime_type.startswith(media_cat.media_type.value): + return True + return False diff --git a/tagstudio/src/core/palette.py b/tagstudio/src/core/palette.py index f104b692..422b35f7 100644 --- a/tagstudio/src/core/palette.py +++ b/tagstudio/src/core/palette.py @@ -19,6 +19,15 @@ class ColorType(IntEnum): DARK_ACCENT = 4 +class UiColor(IntEnum): + DEFAULT = 0 + THEME_DARK = 1 + THEME_LIGHT = 2 + RED = 3 + GREEN = 4 + PURPLE = 5 + + TAG_COLORS: dict[TagColor, dict[ColorType, Any]] = { TagColor.DEFAULT: { ColorType.PRIMARY: "#1e1e1e", @@ -283,8 +292,56 @@ TAG_COLORS: dict[TagColor, dict[ColorType, Any]] = { }, } +UI_COLORS: dict[UiColor, dict[ColorType, Any]] = { + UiColor.DEFAULT: { + ColorType.PRIMARY: "#333333", + ColorType.BORDER: "#555555", + ColorType.LIGHT_ACCENT: "#FFFFFF", + ColorType.DARK_ACCENT: "#1e1e1e", + }, + UiColor.RED: { + ColorType.PRIMARY: "#e22c3c", + ColorType.BORDER: "#e54252", + ColorType.LIGHT_ACCENT: "#f39caa", + ColorType.DARK_ACCENT: "#440d12", + }, + UiColor.GREEN: { + ColorType.PRIMARY: "#28bb48", + ColorType.BORDER: "#43c568", + ColorType.LIGHT_ACCENT: "#DDFFCC", + ColorType.DARK_ACCENT: "#0d3828", + }, + UiColor.PURPLE: { + ColorType.PRIMARY: "#C76FF3", + ColorType.BORDER: "#c364f2", + ColorType.LIGHT_ACCENT: "#EFD4FB", + ColorType.DARK_ACCENT: "#3E1555", + }, + UiColor.THEME_DARK: { + ColorType.PRIMARY: "#333333", + ColorType.BORDER: "#555555", + ColorType.LIGHT_ACCENT: "#FFFFFF", + ColorType.DARK_ACCENT: "#1e1e1e", + }, + UiColor.THEME_LIGHT: { + ColorType.PRIMARY: "#FFFFFF", + ColorType.BORDER: "#333333", + ColorType.LIGHT_ACCENT: "#999999", + ColorType.DARK_ACCENT: "#888888", + }, +} + def get_tag_color(color_type: ColorType, color_id: TagColor) -> str: + """Return a hex value given a tag color name and ColorType. + + Args: + color_type (ColorType): The ColorType category to retrieve from. + color_id (ColorType): The color name enum to retrieve from. + + Return: + A hex value string representing a color with a leading "#". + """ try: if color_type == ColorType.TEXT: text_account: ColorType = TAG_COLORS[color_id][color_type] @@ -293,5 +350,23 @@ def get_tag_color(color_type: ColorType, color_id: TagColor) -> str: return TAG_COLORS[color_id][color_type] except KeyError: traceback.print_stack() - logger.error("Color not found", color_id=color_id) + logger.error("[PALETTE] Tag color not found.", color_id=color_id) + return "#FF00FF" + + +def get_ui_color(color_type: ColorType, color_id: UiColor) -> str: + """Return a hex value given a UI color name and ColorType. + + Args: + color_type (ColorType): The ColorType category to retrieve from. + color_id (UiColor): The color name enum to retrieve from. + + Return: + A hex value string representing a color with a leading "#". + """ + try: + return UI_COLORS[color_id][color_type] + except KeyError: + traceback.print_stack() + logger.error("[PALETTE] UI color not found", color_id=color_id) return "#FF00FF" diff --git a/tagstudio/src/qt/helpers/blender_thumbnailer.py b/tagstudio/src/qt/helpers/blender_thumbnailer.py new file mode 100644 index 00000000..2df0a350 --- /dev/null +++ b/tagstudio/src/qt/helpers/blender_thumbnailer.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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 2 +# 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, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + + +## This file is a modified script that gets the thumbnail data stored in a blend file + + +import gzip +import os +import struct +from io import BufferedReader + +from PIL import ( + Image, + ImageOps, +) + + +def blend_extract_thumb(path): + rend = b"REND" + test = b"TEST" + + blendfile: BufferedReader | gzip.GzipFile = open(path, "rb") # noqa: SIM115 + + head = blendfile.read(12) + + if head[0:2] == b"\x1f\x8b": # gzip magic + blendfile.close() + blendfile = gzip.GzipFile("", "rb", 0, open(path, "rb")) # noqa: SIM115 + head = blendfile.read(12) + + if not head.startswith(b"BLENDER"): + blendfile.close() + return None, 0, 0 + + is_64_bit = head[7] == b"-"[0] + + # true for PPC, false for X86 + is_big_endian = head[8] == b"V"[0] + + # blender pre 2.5 had no thumbs + if head[9:11] <= b"24": + return None, 0, 0 + + sizeof_bhead = 24 if is_64_bit else 20 + int_endian = ">i" if is_big_endian else " Image.Image: +def theme_fg_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image: """Overlay the foreground theme color onto an image. Args: image (Image): The PIL Image object to apply an overlay to. + use_alpha (bool): Option to retain the base image's alpha value when applying the overlay. """ + dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG + light_fg: str = _THEME_LIGHT_FG[:-2] if not use_alpha else _THEME_LIGHT_FG + overlay_color = ( - _THEME_DARK_FG - if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark - else _THEME_LIGHT_FG + dark_fg if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark else light_fg ) + im = Image.new(mode="RGBA", size=image.size, color=overlay_color) return _apply_overlay(image, im) @@ -42,7 +47,7 @@ def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image: def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image: - """Apply an overlay on top of an image, using the image's alpha channel as a mask. + """Apply an overlay on top of an image using the image's alpha channel as a mask. Args: image (Image): The PIL Image object to apply an overlay to. diff --git a/tagstudio/src/qt/helpers/file_tester.py b/tagstudio/src/qt/helpers/file_tester.py new file mode 100644 index 00000000..022ac191 --- /dev/null +++ b/tagstudio/src/qt/helpers/file_tester.py @@ -0,0 +1,32 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from pathlib import Path + +import ffmpeg +from src.qt.helpers.vendored.ffmpeg import _probe + + +def is_readable_video(filepath: Path | str): + """Test if a video is in a readable format. + + Examples of unreadable videos include files with undetermined codecs and DRM-protected content. + + Args: + filepath (Path | str): The filepath of the video to check. + """ + try: + probe = _probe(Path(filepath)) + for stream in probe["streams"]: + # DRM check + if stream.get("codec_tag_string") in [ + "drma", + "drms", + "drmi", + ]: + return False + except ffmpeg.Error: + return False + return True diff --git a/tagstudio/src/qt/helpers/gradient.py b/tagstudio/src/qt/helpers/gradient.py index a5f3b9fa..fe3f7c7d 100644 --- a/tagstudio/src/qt/helpers/gradient.py +++ b/tagstudio/src/qt/helpers/gradient.py @@ -2,26 +2,14 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PIL import Image, ImageChops, ImageEnhance +from PIL import Image -def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl) -> Image.Image: - if image.size != (adj_size, adj_size): - # Old 1 color method. - # bg_col = image.copy().resize((1, 1)).getpixel((0,0)) - # bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col) - # bg.thumbnail((1, 1)) - # bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST) - - # Small gradient background. Looks decent, and is only a one-liner. - # bg = ( - # image.copy() - # .resize((2, 2), resample=Image.Resampling.BILINEAR) - # .resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR) - # ) - +def four_corner_gradient( + image: Image.Image, size: tuple[int, int], mask: Image.Image +) -> Image.Image: + if image.size != size: # Four-Corner Gradient Background. - # Not exactly a one-liner, but it's (subjectively) really cool. tl = image.getpixel((0, 0)) tr = image.getpixel(((image.size[0] - 1), 0)) bl = image.getpixel((0, (image.size[1] - 1))) @@ -31,26 +19,25 @@ def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl) -> I bg.paste(tr, (1, 0, 2, 2)) bg.paste(bl, (0, 1, 2, 2)) bg.paste(br, (1, 1, 2, 2)) - bg = bg.resize((adj_size, adj_size), resample=Image.Resampling.BICUBIC) - + bg = bg.resize(size, resample=Image.Resampling.BICUBIC) bg.paste( image, box=( - (adj_size - image.size[0]) // 2, - (adj_size - image.size[1]) // 2, + (size[0] - image.size[0]) // 2, + (size[1] - image.size[1]) // 2, ), ) - bg.putalpha(mask) - final = bg + final = Image.new("RGBA", bg.size, (0, 0, 0, 0)) + final.paste(bg, mask=mask.getchannel(0)) else: - image.putalpha(mask) - final = image + final = Image.new("RGBA", size, (0, 0, 0, 0)) + final.paste(image, mask=mask.getchannel(0)) + + if final.mode != "RGBA": + final = final.convert("RGBA") - hl_soft = hl.copy() - hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5)) - final.paste(ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3)) return final diff --git a/tagstudio/src/qt/helpers/rounded_pixmap_style.py b/tagstudio/src/qt/helpers/rounded_pixmap_style.py new file mode 100644 index 00000000..36c33c79 --- /dev/null +++ b/tagstudio/src/qt/helpers/rounded_pixmap_style.py @@ -0,0 +1,29 @@ +# Based on the implementation by eyllanesc: +# https://stackoverflow.com/questions/54230005/qmovie-with-border-radius +# Licensed under the Creative Commons CC BY-SA 4.0 License: +# https://creativecommons.org/licenses/by-sa/4.0/ +# Modified for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PySide6.QtGui import QBrush, QColor, QPainter, QPixmap +from PySide6.QtWidgets import ( + QProxyStyle, +) + + +class RoundedPixmapStyle(QProxyStyle): + def __init__(self, radius=8): + super().__init__() + self._radius = radius + + def drawItemPixmap(self, painter, rectangle, alignment, pixmap): # noqa: N802 + painter.save() + pix = QPixmap(pixmap.size()) + pix.fill(QColor("transparent")) + p = QPainter(pix) + p.setBrush(QBrush(pixmap)) + p.setPen(QColor("transparent")) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.drawRoundedRect(pixmap.rect(), self._radius, self._radius) + p.end() + super().drawItemPixmap(painter, rectangle, alignment, pix) + painter.restore() diff --git a/tagstudio/src/qt/helpers/silent_popen.py b/tagstudio/src/qt/helpers/silent_popen.py new file mode 100644 index 00000000..df426fe5 --- /dev/null +++ b/tagstudio/src/qt/helpers/silent_popen.py @@ -0,0 +1,70 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import subprocess +import sys + +"""Implementation of subprocess.Popen that does not spawn console windows or log output.""" + + +def promptless_Popen( # noqa: N802 + args, + bufsize=-1, + executable=None, + stdin=None, + stdout=None, + stderr=None, + preexec_fn=None, + close_fds=True, + shell=False, + cwd=None, + env=None, + universal_newlines=None, + startupinfo=None, + restore_signals=True, + start_new_session=False, + pass_fds=(), + *, + group=None, + extra_groups=None, + user=None, + umask=-1, + encoding=None, + errors=None, + text=None, + pipesize=-1, + process_group=None, +): + """Call subprocess.Popen without creating a console window.""" + creation_flags = 0 + if sys.platform == "win32": + creation_flags = subprocess.CREATE_NO_WINDOW + + return subprocess.Popen( + args=args, + bufsize=bufsize, + executable=executable, + stdin=stdin, + stdout=stdout, + stderr=stderr, + preexec_fn=preexec_fn, + close_fds=close_fds, + shell=shell, + cwd=cwd, + env=env, + universal_newlines=universal_newlines, + startupinfo=startupinfo, + creationflags=creation_flags, + restore_signals=restore_signals, + start_new_session=start_new_session, + pass_fds=pass_fds, + group=group, + extra_groups=extra_groups, + user=user, + umask=umask, + encoding=encoding, + errors=errors, + text=text, + pipesize=pipesize, + process_group=process_group, + ) diff --git a/tagstudio/src/qt/helpers/text_wrapper.py b/tagstudio/src/qt/helpers/text_wrapper.py new file mode 100644 index 00000000..073f6e15 --- /dev/null +++ b/tagstudio/src/qt/helpers/text_wrapper.py @@ -0,0 +1,49 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PIL import Image, ImageDraw, ImageFont + + +def wrap_line( # type: ignore + text: str, + font: ImageFont.ImageFont, + width: int = 256, + draw: ImageDraw.ImageDraw = None, +) -> int: + """Take in a single text line and return the index it should be broken up at. + + Only splits once. + """ + if draw is None: + bg = Image.new("RGB", (width, width), color="#1e1e1e") + draw = ImageDraw.Draw(bg) + if draw.textlength(text, font=font) > width: + for i in range( + int(len(text) / int(draw.textlength(text, font=font)) * width) - 2, + 0, + -1, + ): + if draw.textlength(text[:i], font=font) < width: + return i + else: + return -1 + + +def wrap_full_text( + text: str, + font: ImageFont.ImageFont, + width: int = 256, + draw: ImageDraw.ImageDraw = None, +) -> str: + """Break up a string to fit the canvas given a kerning value, font size, etc.""" + lines = [] + i = 0 + last_i = 0 + while wrap_line(text[i:], font=font, width=width, draw=draw) > 0: + i = wrap_line(text[i:], font=font, width=width, draw=draw) + last_i + lines.append(text[last_i:i]) + last_i = i + lines.append(text[last_i:]) + text_wrapped = "\n".join(lines) + return text_wrapped diff --git a/tagstudio/src/qt/helpers/vendored/ffmpeg.py b/tagstudio/src/qt/helpers/vendored/ffmpeg.py new file mode 100644 index 00000000..097e78a2 --- /dev/null +++ b/tagstudio/src/qt/helpers/vendored/ffmpeg.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 Karl Kroening (kkroening). +# Licensed under the GPL-3.0 License. +# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803 + +import json +import subprocess + +import ffmpeg +from src.qt.helpers.silent_popen import promptless_Popen + + +def _probe(filename, cmd="ffprobe", timeout=None, **kwargs): + """Run ffprobe on the specified file and return a JSON representation of the output. + + Raises: + :class:`ffmpeg.Error`: if ffprobe returns a non-zero exit code, + an :class:`Error` is returned with a generic error message. + The stderr output can be retrieved by accessing the + ``stderr`` property of the exception. + """ + args = [cmd, "-show_format", "-show_streams", "-of", "json"] + args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) + args += [filename] + + # PATCHED + p = promptless_Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + communicate_kwargs = {} + if timeout is not None: + communicate_kwargs["timeout"] = timeout + out, err = p.communicate(**communicate_kwargs) + if p.returncode != 0: + raise ffmpeg.Error("ffprobe", out, err) + return json.loads(out.decode("utf-8")) diff --git a/tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py b/tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py new file mode 100644 index 00000000..a43ea5b3 --- /dev/null +++ b/tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py @@ -0,0 +1,1444 @@ +# type: ignore +# Copyright (C) 2022 James Robert (jiaaro). +# Licensed under the MIT License. +# Vendored from pydub + + +import array +import base64 +import os +import struct +import subprocess +import sys +import wave +from collections import namedtuple +from io import BytesIO, StringIO +from tempfile import NamedTemporaryFile + +from pydub.logging_utils import log_conversion, log_subprocess_output +from pydub.utils import fsdecode + +try: + from itertools import izip +except Exception: + izip = zip + +from pydub.exceptions import ( + CouldntDecodeError, + CouldntEncodeError, + InvalidDuration, + InvalidID3TagVersion, + InvalidTag, + MissingAudioParameter, + TooManyMissingFrames, +) +from pydub.utils import ( + _fd_or_path_or_tempfile, + audioop, + db_to_float, + get_array_type, + get_encoder_name, + ratio_to_db, +) +from src.qt.helpers.silent_popen import promptless_Popen +from src.qt.helpers.vendored.pydub.utils import _mediainfo_json + +basestring = str +xrange = range +StringIO = BytesIO # noqa: F811 + + +class ClassPropertyDescriptor: + def __init__(self, fget, fset=None): + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + def __set__(self, obj, value): + if not self.fset: + raise AttributeError("can't set attribute") + type_ = type(obj) + return self.fset.__get__(obj, type_)(value) + + def setter(self, func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + self.fset = func + return self + + +def classproperty(func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return ClassPropertyDescriptor(func) + + +AUDIO_FILE_EXT_ALIASES = { + "m4a": "mp4", + "wave": "wav", +} + +WavSubChunk = namedtuple("WavSubChunk", ["id", "position", "size"]) +WavData = namedtuple( + "WavData", ["audio_format", "channels", "sample_rate", "bits_per_sample", "raw_data"] +) + + +def extract_wav_headers(data): + # def search_subchunk(data, subchunk_id): + pos = 12 # The size of the RIFF chunk descriptor + subchunks = [] + while pos + 8 <= len(data) and len(subchunks) < 10: + subchunk_id = data[pos : pos + 4] + subchunk_size = struct.unpack_from(" 2**32: + raise CouldntDecodeError("Unable to process >4GB files") + + # Set the file size in the RIFF chunk descriptor + data[4:8] = struct.pack(" b"\x7f"[0]]) + old_bytes = struct.pack(pack_fmt, b0, b1, b2) + byte_buffer.write(old_bytes) + + self._data = byte_buffer.getvalue() + self.sample_width = 4 + self.frame_width = self.channels * self.sample_width + + super(_AudioSegment, self).__init__(*args, **kwargs) + + @property + def raw_data(self): + """Public access to the raw audio data as a bytestring.""" + return self._data + + def get_array_of_samples(self, array_type_override=None): + """Return the raw_data as an array of samples.""" + if array_type_override is None: + array_type_override = self.array_type + return array.array(array_type_override, self._data) + + @property + def array_type(self): + return get_array_type(self.sample_width * 8) + + def __len__(self): + """Return the length of this audio segment in milliseconds.""" + return round(1000 * (self.frame_count() / self.frame_rate)) + + def __eq__(self, other): + try: + return self._data == other._data + except Exception: + return False + + def __hash__(self): + return hash(_AudioSegment) ^ hash( + (self.channels, self.frame_rate, self.sample_width, self._data) + ) + + def __ne__(self, other): + return not (self == other) + + def __iter__(self): + return (self[i] for i in xrange(len(self))) + + def __getitem__(self, millisecond): + if isinstance(millisecond, slice): + if millisecond.step: + return ( + self[i : i + millisecond.step] for i in xrange(*millisecond.indices(len(self))) + ) + + start = millisecond.start if millisecond.start is not None else 0 + end = millisecond.stop if millisecond.stop is not None else len(self) + + start = min(start, len(self)) + end = min(end, len(self)) + else: + start = millisecond + end = millisecond + 1 + + start = self._parse_position(start) * self.frame_width + end = self._parse_position(end) * self.frame_width + data = self._data[start:end] + + # ensure the output is as long as the requester is expecting + expected_length = end - start + missing_frames = (expected_length - len(data)) // self.frame_width + if missing_frames: + if missing_frames > self.frame_count(ms=2): + raise TooManyMissingFrames( + "You should never be filling in " # noqa: UP031 + " more than 2 ms with silence here, " + "missing frames: %s" % missing_frames + ) + silence = audioop.mul(data[: self.frame_width], self.sample_width, 0) + data += silence * missing_frames + + return self._spawn(data) + + def get_sample_slice(self, start_sample=None, end_sample=None): + """Get a section of the audio segment by sample index. + + NOTE: Negative indices do *not* address samples backword + from the end of the audio segment like a python list. + This is intentional. + """ + max_val = int(self.frame_count()) + + def bounded(val, default): + if val is None: + return default + if val < 0: + return 0 + if val > max_val: + return max_val + return val + + start_i = bounded(start_sample, 0) * self.frame_width + end_i = bounded(end_sample, max_val) * self.frame_width + + data = self._data[start_i:end_i] + return self._spawn(data) + + def __add__(self, arg): + if isinstance(arg, _AudioSegment): + return self.append(arg, crossfade=0) + else: + return self.apply_gain(arg) + + def __radd__(self, rarg): + """Permit use of sum() builtin with an iterable of AudioSegments.""" + if rarg == 0: + return self + raise TypeError("Gains must be the second addend after the " "AudioSegment") + + def __sub__(self, arg): + if isinstance(arg, _AudioSegment): + raise TypeError("AudioSegment objects can't be subtracted from " "each other") + else: + return self.apply_gain(-arg) + + def __mul__(self, arg): + """If the argument is an AudioSegment, overlay the multiplied audio segment. + + If it's a number, just use the string multiply operation to repeat the + audio. + + The following would return an AudioSegment that contains the + audio of audio_seg eight times + + `audio_seg * 8` + """ + if isinstance(arg, _AudioSegment): + return self.overlay(arg, position=0, loop=True) + else: + return self._spawn(data=self._data * arg) + + def _spawn(self, data, overrides={}): # noqa: B006 + """Create a new audio segment using the metadata from the current one & the data passed in. + + Should be used whenever an AudioSegment is being returned by an operation that would alters + the current one, since AudioSegment objects are immutable. + """ + # accept lists of data chunks + if isinstance(data, list): + data = b"".join(data) + + if isinstance(data, array.array): + try: + data = data.tobytes() + except Exception: + data = data.tostring() + + # accept file-like objects + if hasattr(data, "read"): + if hasattr(data, "seek"): + data.seek(0) + data = data.read() + + metadata = { + "sample_width": self.sample_width, + "frame_rate": self.frame_rate, + "frame_width": self.frame_width, + "channels": self.channels, + } + metadata.update(overrides) + return self.__class__(data=data, metadata=metadata) + + @classmethod + def _sync(cls, *segs): + channels = max(seg.channels for seg in segs) + frame_rate = max(seg.frame_rate for seg in segs) + sample_width = max(seg.sample_width for seg in segs) + + return tuple( + seg.set_channels(channels).set_frame_rate(frame_rate).set_sample_width(sample_width) + for seg in segs + ) + + def _parse_position(self, val): + if val < 0: + val = len(self) - abs(val) + val = self.frame_count(ms=len(self)) if val == float("inf") else self.frame_count(ms=val) + return int(val) + + @classmethod + def empty(cls): + return cls( + b"", metadata={"channels": 1, "sample_width": 1, "frame_rate": 1, "frame_width": 1} + ) + + @classmethod + def silent(cls, duration=1000, frame_rate=11025): + """Generate a silent audio segment. + + Duration specified in milliseconds (default duration: 1000ms, default frame_rate: 11025). + """ + frames = int(frame_rate * (duration / 1000.0)) + data = b"\0\0" * frames + return cls( + data, + metadata={"channels": 1, "sample_width": 2, "frame_rate": frame_rate, "frame_width": 2}, + ) + + @classmethod + def from_mono_audiosegments(cls, *mono_segments): + if not len(mono_segments): + raise ValueError("At least one AudioSegment instance is required") + + segs = cls._sync(*mono_segments) + + if segs[0].channels != 1: + raise ValueError( + "AudioSegment.from_mono_audiosegments requires all " + "arguments are mono AudioSegment instances" + ) + + channels = len(segs) + sample_width = segs[0].sample_width + frame_rate = segs[0].frame_rate + + frame_count = max(int(seg.frame_count()) for seg in segs) + data = array.array(segs[0].array_type, b"\0" * (frame_count * sample_width * channels)) + + for i, seg in enumerate(segs): + data[i::channels] = seg.get_array_of_samples() + + return cls( + data, + channels=channels, + sample_width=sample_width, + frame_rate=frame_rate, + ) + + @classmethod + def from_file_using_temporary_files( + cls, + file, + format=None, + codec=None, + parameters=None, + start_second=None, + duration=None, + **kwargs, + ): + orig_file = file + file, close_file = _fd_or_path_or_tempfile(file, "rb", tempfile=False) + + if format: + format = format.lower() + format = AUDIO_FILE_EXT_ALIASES.get(format, format) + + def is_format(f): + f = f.lower() + if format == f: + return True + if isinstance(orig_file, basestring): + return orig_file.lower().endswith(f".{f}") + if isinstance(orig_file, bytes): + return orig_file.lower().endswith((f".{f}").encode()) + return False + + if is_format("wav"): + try: + obj = cls._from_safe_wav(file) + if close_file: + file.close() + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[start_second * 1000 :] + elif start_second is None and duration is not None: + return obj[: duration * 1000] + else: + return obj[start_second * 1000 : (start_second + duration) * 1000] + except Exception: + file.seek(0) + elif is_format("raw") or is_format("pcm"): + sample_width = kwargs["sample_width"] + frame_rate = kwargs["frame_rate"] + channels = kwargs["channels"] + metadata = { + "sample_width": sample_width, + "frame_rate": frame_rate, + "channels": channels, + "frame_width": channels * sample_width, + } + obj = cls(data=file.read(), metadata=metadata) + if close_file: + file.close() + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[start_second * 1000 :] + elif start_second is None and duration is not None: + return obj[: duration * 1000] + else: + return obj[start_second * 1000 : (start_second + duration) * 1000] + + input_file = NamedTemporaryFile(mode="wb", delete=False) + try: + input_file.write(file.read()) + except OSError: + input_file.flush() + input_file.close() + input_file = NamedTemporaryFile(mode="wb", delete=False, buffering=2**31 - 1) + if close_file: + file.close() + close_file = True + file = open(orig_file, buffering=2**13 - 1, mode="rb") + reader = file.read(2**31 - 1) + while reader: + input_file.write(reader) + reader = file.read(2**31 - 1) + input_file.flush() + if close_file: + file.close() + + output = NamedTemporaryFile(mode="rb", delete=False) + + conversion_command = [ + cls.converter, + "-y", # always overwrite existing files + ] + + # If format is not defined + # ffmpeg/avconv will detect it automatically + if format: + conversion_command += ["-f", format] + + if codec: + # force audio decoder + conversion_command += ["-acodec", codec] + + conversion_command += [ + "-i", + input_file.name, # input_file options (filename last) + "-vn", # Drop any video streams if there are any + "-f", + "wav", # output options (filename last) + ] + + if start_second is not None: + conversion_command += ["-ss", str(start_second)] + + if duration is not None: + conversion_command += ["-t", str(duration)] + + conversion_command += [output.name] + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + log_conversion(conversion_command) + + with open(os.devnull, "rb") as devnull: + # PATCHED + p = promptless_Popen( + conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + p_out, p_err = p.communicate() + + log_subprocess_output(p_out) + log_subprocess_output(p_err) + + try: + if p.returncode != 0: + raise CouldntDecodeError( + "Decoding failed. ffmpeg returned error code: " # noqa: UP030 + "{0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format( + p.returncode, p_err.decode(errors="ignore") + ) + ) + obj = cls._from_safe_wav(output) + finally: + input_file.close() + output.close() + os.unlink(input_file.name) + os.unlink(output.name) + + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[0:] + elif start_second is None and duration is not None: + return obj[: duration * 1000] + else: + return obj[0 : duration * 1000] + + @classmethod + def from_file( + cls, + file, + format=None, + codec=None, + parameters=None, + start_second=None, + duration=None, + **kwargs, + ): + orig_file = file + try: + filename = fsdecode(file) + except TypeError: + filename = None + file, close_file = _fd_or_path_or_tempfile(file, "rb", tempfile=False) + + if format: + format = format.lower() + format = AUDIO_FILE_EXT_ALIASES.get(format, format) + + def is_format(f): + f = f.lower() + if format == f: + return True + + if filename: + return filename.lower().endswith(f".{f}") + + return False + + if is_format("wav"): + try: + if start_second is None and duration is None: + return cls._from_safe_wav(file) + elif start_second is not None and duration is None: + return cls._from_safe_wav(file)[start_second * 1000 :] + elif start_second is None and duration is not None: + return cls._from_safe_wav(file)[: duration * 1000] + else: + return cls._from_safe_wav(file)[ + start_second * 1000 : (start_second + duration) * 1000 + ] + except Exception: + file.seek(0) + elif is_format("raw") or is_format("pcm"): + sample_width = kwargs["sample_width"] + frame_rate = kwargs["frame_rate"] + channels = kwargs["channels"] + metadata = { + "sample_width": sample_width, + "frame_rate": frame_rate, + "channels": channels, + "frame_width": channels * sample_width, + } + if start_second is None and duration is None: + return cls(data=file.read(), metadata=metadata) + elif start_second is not None and duration is None: + return cls(data=file.read(), metadata=metadata)[start_second * 1000 :] + elif start_second is None and duration is not None: + return cls(data=file.read(), metadata=metadata)[: duration * 1000] + else: + return cls(data=file.read(), metadata=metadata)[ + start_second * 1000 : (start_second + duration) * 1000 + ] + + conversion_command = [ + cls.converter, + "-y", # always overwrite existing files + ] + + # If format is not defined + # ffmpeg/avconv will detect it automatically + if format: + conversion_command += ["-f", format] + + if codec: + # force audio decoder + conversion_command += ["-acodec", codec] + + read_ahead_limit = kwargs.get("read_ahead_limit", -1) + if filename: + conversion_command += ["-i", filename] + stdin_parameter = None + stdin_data = None + else: + if cls.converter == "ffmpeg": + conversion_command += [ + "-read_ahead_limit", + str(read_ahead_limit), + "-i", + "cache:pipe:0", + ] + else: + conversion_command += ["-i", "-"] + stdin_parameter = subprocess.PIPE + stdin_data = file.read() + + if codec: + info = None + else: + # PATCHED + try: + info = _mediainfo_json(orig_file, read_ahead_limit=read_ahead_limit) + except FileNotFoundError: + raise ChildProcessError + if info: + audio_streams = [x for x in info["streams"] if x["codec_type"] == "audio"] + # This is a workaround for some ffprobe versions that always say + # that mp3/mp4/aac/webm/ogg files contain fltp samples + audio_codec = audio_streams[0].get("codec_name") + if audio_streams[0].get("sample_fmt") == "fltp" and audio_codec in [ + "mp3", + "mp4", + "aac", + "webm", + "ogg", + ]: + bits_per_sample = 16 + else: + bits_per_sample = audio_streams[0]["bits_per_sample"] + acodec = "pcm_u8" if bits_per_sample == 8 else "pcm_s%dle" % bits_per_sample + + conversion_command += ["-acodec", acodec] + + conversion_command += [ + "-vn", # Drop any video streams if there are any + "-f", + "wav", # output options (filename last) + ] + + if start_second is not None: + conversion_command += ["-ss", str(start_second)] + + if duration is not None: + conversion_command += ["-t", str(duration)] + + conversion_command += ["-"] + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + log_conversion(conversion_command) + + # PATCHED + p = promptless_Popen( + conversion_command, + stdin=stdin_parameter, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + p_out, p_err = p.communicate(input=stdin_data) + + if p.returncode != 0 or len(p_out) == 0: + if close_file: + file.close() + raise CouldntDecodeError( + "Decoding failed. ffmpeg returned error code: " # noqa: UP030 + "{0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format( + p.returncode, p_err.decode(errors="ignore") + ) + ) + + p_out = bytearray(p_out) + fix_wav_headers(p_out) + p_out = bytes(p_out) + obj = cls(p_out) + + if close_file: + file.close() + + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[0:] + elif start_second is None and duration is not None: + return obj[: duration * 1000] + else: + return obj[0 : duration * 1000] + + @classmethod + def from_mp3(cls, file, parameters=None): + return cls.from_file(file, "mp3", parameters=parameters) + + @classmethod + def from_flv(cls, file, parameters=None): + return cls.from_file(file, "flv", parameters=parameters) + + @classmethod + def from_ogg(cls, file, parameters=None): + return cls.from_file(file, "ogg", parameters=parameters) + + @classmethod + def from_wav(cls, file, parameters=None): + return cls.from_file(file, "wav", parameters=parameters) + + @classmethod + def from_raw(cls, file, **kwargs): + return cls.from_file( + file, + "raw", + sample_width=kwargs["sample_width"], + frame_rate=kwargs["frame_rate"], + channels=kwargs["channels"], + ) + + @classmethod + def _from_safe_wav(cls, file): + file, close_file = _fd_or_path_or_tempfile(file, "rb", tempfile=False) + file.seek(0) + obj = cls(data=file) + if close_file: + file.close() + return obj + + def export( + self, + out_f=None, + format="mp3", + codec=None, + bitrate=None, + parameters=None, + tags=None, + id3v2_version="4", + cover=None, + ): + """Export an AudioSegment to a file with the given options. + + out_f (string): + Path to destination audio file. Also accepts os.PathLike objects on + python >= 3.6 + + format (string) + Format for destination audio file. + ('mp3', 'wav', 'raw', 'ogg' or other ffmpeg/avconv supported files) + + codec (string) + Codec used to encode the destination file. + + bitrate (string) + Bitrate used when encoding destination file. (64, 92, 128, 256, 312k...) + Each codec accepts different bitrate arguments so take a look at the + ffmpeg documentation for details (bitrate usually shown as -b, -ba or + -a:b). + + parameters (list of strings) + Aditional ffmpeg/avconv parameters + + tags (dict) + Set metadata information to destination files + usually used as tags. ({title='Song Title', artist='Song Artist'}) + + id3v2_version (string) + Set ID3v2 version for tags. (default: '4') + + cover (file) + Set cover for audio file from image file. (png or jpg) + """ + id3v2_allowed_versions = ["3", "4"] + + if format == "raw" and (codec is not None or parameters is not None): + raise AttributeError( + 'Can not invoke ffmpeg when export format is "raw"; ' + 'specify an ffmpeg raw format like format="s16le" instead ' + 'or call export(format="raw") with no codec or parameters' + ) + + out_f, _ = _fd_or_path_or_tempfile(out_f, "wb+") + out_f.seek(0) + + if format == "raw": + out_f.write(self._data) + out_f.seek(0) + return out_f + + # wav with no ffmpeg parameters can just be written directly to out_f + easy_wav = format == "wav" and codec is None and parameters is None + + data = out_f if easy_wav else NamedTemporaryFile(mode="wb", delete=False) + + pcm_for_wav = self._data + if self.sample_width == 1: + # convert to unsigned integers for wav + pcm_for_wav = audioop.bias(self._data, 1, 128) + + wave_data = wave.open(data, "wb") + wave_data.setnchannels(self.channels) + wave_data.setsampwidth(self.sample_width) + wave_data.setframerate(self.frame_rate) + # For some reason packing the wave header struct with + # a float in python 2 doesn't throw an exception + wave_data.setnframes(int(self.frame_count())) + wave_data.writeframesraw(pcm_for_wav) + wave_data.close() + + # for easy wav files, we're done (wav data is written directly to out_f) + if easy_wav: + out_f.seek(0) + return out_f + + output = NamedTemporaryFile(mode="w+b", delete=False) + + # build converter command to export + conversion_command = [ + self.converter, + "-y", # always overwrite existing files + "-f", + "wav", + "-i", + data.name, # input options (filename last) + ] + + if codec is None: + codec = self.DEFAULT_CODECS.get(format, None) + + if cover is not None: + if ( + cover.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff")) + and format == "mp3" + ): + conversion_command.extend(["-i", cover, "-map", "0", "-map", "1", "-c:v", "mjpeg"]) + else: + raise AttributeError( + "Currently cover images are only supported by MP3 files. The allowed image " + "formats are: .tif, .jpg, .bmp, .jpeg and .png." + ) + + if codec is not None: + # force audio encoder + conversion_command.extend(["-acodec", codec]) + + if bitrate is not None: + conversion_command.extend(["-b:a", bitrate]) + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + if tags is not None: + if not isinstance(tags, dict): + raise InvalidTag("Tags must be a dictionary.") + else: + # Extend converter command with tags + # print(tags) + for key, value in tags.items(): + conversion_command.extend(["-metadata", f"{key}={value}"]) + + if format == "mp3": + # set id3v2 tag version + if id3v2_version not in id3v2_allowed_versions: + raise InvalidID3TagVersion( + "id3v2_version not allowed, allowed versions: %s" # noqa: UP031 + % id3v2_allowed_versions + ) + conversion_command.extend(["-id3v2_version", id3v2_version]) + + if sys.platform == "darwin" and codec == "mp3": + conversion_command.extend(["-write_xing", "0"]) + + conversion_command.extend( + [ + "-f", + format, + output.name, # output options (filename last) + ] + ) + + log_conversion(conversion_command) + + # read stdin / write stdout + with open(os.devnull, "rb") as devnull: + # PATCHED + p = promptless_Popen( + conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + p_out, p_err = p.communicate() + + log_subprocess_output(p_out) + log_subprocess_output(p_err) + + try: + if p.returncode != 0: + raise CouldntEncodeError( + "Encoding failed. ffmpeg/avlib returned error code: " # noqa: UP030 + "{0}\n\nCommand:{1}\n\nOutput from ffmpeg/avlib:\n\n{2}".format( + p.returncode, conversion_command, p_err.decode(errors="ignore") + ) + ) + + output.seek(0) + out_f.write(output.read()) + + finally: + data.close() + output.close() + os.unlink(data.name) + os.unlink(output.name) + + out_f.seek(0) + return out_f + + def get_frame(self, index): + frame_start = index * self.frame_width + frame_end = frame_start + self.frame_width + return self._data[frame_start:frame_end] + + def frame_count(self, ms=None): + """Return the number of frames for the given number of milliseconds. + + If not specified, return the number of frames in the whole AudioSegment + """ + if ms is not None: + return ms * (self.frame_rate / 1000.0) + else: + return float(len(self._data) // self.frame_width) + + def set_sample_width(self, sample_width): + if sample_width == self.sample_width: + return self + + frame_width = self.channels * sample_width + + return self._spawn( + audioop.lin2lin(self._data, self.sample_width, sample_width), + overrides={"sample_width": sample_width, "frame_width": frame_width}, + ) + + def set_frame_rate(self, frame_rate): + if frame_rate == self.frame_rate: + return self + + if self._data: + converted, _ = audioop.ratecv( + self._data, self.sample_width, self.channels, self.frame_rate, frame_rate, None + ) + else: + converted = self._data + + return self._spawn(data=converted, overrides={"frame_rate": frame_rate}) + + def set_channels(self, channels): + if channels == self.channels: + return self + + if channels == 2 and self.channels == 1: + fn = audioop.tostereo + frame_width = self.frame_width * 2 + fac = 1 + converted = fn(self._data, self.sample_width, fac, fac) + elif channels == 1 and self.channels == 2: + fn = audioop.tomono + frame_width = self.frame_width // 2 + fac = 0.5 + converted = fn(self._data, self.sample_width, fac, fac) + elif channels == 1: + channels_data = [seg.get_array_of_samples() for seg in self.split_to_mono()] + frame_count = int(self.frame_count()) + converted = array.array( + channels_data[0].typecode, b"\0" * (frame_count * self.sample_width) + ) + for raw_channel_data in channels_data: + for i in range(frame_count): + converted[i] += raw_channel_data[i] // self.channels + frame_width = self.frame_width // self.channels + elif self.channels == 1: + dup_channels = [self for iChannel in range(channels)] + return _AudioSegment.from_mono_audiosegments(*dup_channels) + else: + raise ValueError( + "AudioSegment.set_channels only supports mono-to-multi channel " + "and multi-to-mono channel conversion" + ) + + return self._spawn( + data=converted, overrides={"channels": channels, "frame_width": frame_width} + ) + + def split_to_mono(self): + if self.channels == 1: + return [self] + + samples = self.get_array_of_samples() + + mono_channels = [] + for i in range(self.channels): + samples_for_current_channel = samples[i :: self.channels] + + try: + mono_data = samples_for_current_channel.tobytes() + except AttributeError: + mono_data = samples_for_current_channel.tostring() + + mono_channels.append( + self._spawn(mono_data, overrides={"channels": 1, "frame_width": self.sample_width}) + ) + + return mono_channels + + @property + def rms(self): + return audioop.rms(self._data, self.sample_width) + + @property + def dBFS(self): # noqa: N802 + rms = self.rms + if not rms: + return -float("infinity") + return ratio_to_db(self.rms / self.max_possible_amplitude) + + @property + def max(self): + return audioop.max(self._data, self.sample_width) + + @property + def max_possible_amplitude(self): + bits = self.sample_width * 8 + max_possible_val = 2**bits + + # since half is above 0 and half is below the max amplitude is divided + return max_possible_val / 2 + + @property + def max_dBFS(self): + return ratio_to_db(self.max, self.max_possible_amplitude) + + @property + def duration_seconds(self): + return self.frame_rate and self.frame_count() / self.frame_rate or 0.0 + + def get_dc_offset(self, channel=1): + """Return a value between -1.0 and 1.0 representing the DC offset of a channel. + + 1 for left, 2 for right. + """ + if not 1 <= channel <= 2: + raise ValueError("channel value must be 1 (left) or 2 (right)") + + if self.channels == 1: + data = self._data + elif channel == 1: + data = audioop.tomono(self._data, self.sample_width, 1, 0) + else: + data = audioop.tomono(self._data, self.sample_width, 0, 1) + + return float(audioop.avg(data, self.sample_width)) / self.max_possible_amplitude + + def remove_dc_offset(self, channel=None, offset=None): + """Remove DC offset of given channel. Calculates offset if it's not given. + + Offset values must be in range -1.0 to 1.0. If channel is None, removes + DC offset from all available channels. + """ + if channel and not 1 <= channel <= 2: + raise ValueError("channel value must be None, 1 (left) or 2 (right)") + + if offset and not -1.0 <= offset <= 1.0: + raise ValueError("offset value must be in range -1.0 to 1.0") + + if offset: + offset = int(round(offset * self.max_possible_amplitude)) + + def remove_data_dc(data, off): + if not off: + off = audioop.avg(data, self.sample_width) + return audioop.bias(data, self.sample_width, -off) + + if self.channels == 1: + return self._spawn(data=remove_data_dc(self._data, offset)) + + left_channel = audioop.tomono(self._data, self.sample_width, 1, 0) + right_channel = audioop.tomono(self._data, self.sample_width, 0, 1) + + if not channel or channel == 1: + left_channel = remove_data_dc(left_channel, offset) + + if not channel or channel == 2: + right_channel = remove_data_dc(right_channel, offset) + + left_channel = audioop.tostereo(left_channel, self.sample_width, 1, 0) + right_channel = audioop.tostereo(right_channel, self.sample_width, 0, 1) + + return self._spawn(data=audioop.add(left_channel, right_channel, self.sample_width)) + + def apply_gain(self, volume_change): + return self._spawn( + data=audioop.mul(self._data, self.sample_width, db_to_float(float(volume_change))) + ) + + def overlay(self, seg, position=0, loop=False, times=None, gain_during_overlay=None): + """Overlay the provided segment on to this segment. + + Starts at the specified position and uses the specified looping behavior. + + seg (AudioSegment): + The audio segment to overlay on to this one. + + position (optional int): + The position to start overlaying the provided segment in to this + one. + + loop (optional bool): + Loop seg as many times as necessary to match this segment's length. + Overrides loops param. + + times (optional int): + Loop seg the specified number of times or until it matches this + segment's length. 1 means once, 2 means twice, ... 0 would make the + call a no-op + gain_during_overlay (optional int): + Changes this segment's volume by the specified amount during the + duration of time that seg is overlaid on top of it. When negative, + this has the effect of 'ducking' the audio under the overlay. + """ + if loop: + # match loop=True's behavior with new times (count) mechanism. + times = -1 + elif times is None: + # no times specified, just once through + times = 1 + elif times == 0: + # it's a no-op, make a copy since we never mutate + return self._spawn(self._data) + + output = StringIO() + + seg1, seg2 = _AudioSegment._sync(self, seg) + sample_width = seg1.sample_width + spawn = seg1._spawn + + output.write(seg1[:position]._data) + + # drop down to the raw data + seg1 = seg1[position:]._data + seg2 = seg2._data + pos = 0 + seg1_len = len(seg1) + seg2_len = len(seg2) + while times: + remaining = max(0, seg1_len - pos) + if seg2_len >= remaining: + seg2 = seg2[:remaining] + seg2_len = remaining + # we've hit the end, we're done looping (if we were) and this + # is our last go-around + times = 1 + + if gain_during_overlay: + seg1_overlaid = seg1[pos : pos + seg2_len] + seg1_adjusted_gain = audioop.mul( + seg1_overlaid, self.sample_width, db_to_float(float(gain_during_overlay)) + ) + output.write(audioop.add(seg1_adjusted_gain, seg2, sample_width)) + else: + output.write(audioop.add(seg1[pos : pos + seg2_len], seg2, sample_width)) + pos += seg2_len + + # dec times to break our while loop (eventually) + times -= 1 + + output.write(seg1[pos:]) + + return spawn(data=output) + + def append(self, seg, crossfade=100): + seg1, seg2 = _AudioSegment._sync(self, seg) + + if not crossfade: + return seg1._spawn(seg1._data + seg2._data) + elif crossfade > len(self): + raise ValueError( + f"Crossfade is longer than the original AudioSegment ({crossfade}ms > {len(self)}ms)" + ) + elif crossfade > len(seg): + raise ValueError( + f"Crossfade is longer than the appended AudioSegment ({crossfade}ms > {len(seg)}ms)" + ) + + xf = seg1[-crossfade:].fade(to_gain=-120, start=0, end=float("inf")) + xf *= seg2[:crossfade].fade(from_gain=-120, start=0, end=float("inf")) + + output = BytesIO() + + output.write(seg1[:-crossfade]._data) + output.write(xf._data) + output.write(seg2[crossfade:]._data) + + output.seek(0) + obj = seg1._spawn(data=output) + output.close() + return obj + + def fade(self, to_gain=0, from_gain=0, start=None, end=None, duration=None): + """Fade the volume of this audio segment. + + to_gain (float): + resulting volume_change in db + + start (int): + default = beginning of the segment + when in this segment to start fading in milliseconds + + end (int): + default = end of the segment + when in this segment to start fading in milliseconds + + duration (int): + default = until the end of the audio segment + the duration of the fade + """ + if None not in [duration, end, start]: + raise TypeError( + 'Only two of the three arguments, "start", ' + '"end", and "duration" may be specified' + ) + + # no fade == the same audio + if to_gain == 0 and from_gain == 0: + return self + + start = min(len(self), start) if start is not None else None + end = min(len(self), end) if end is not None else None + + if start is not None and start < 0: + start += len(self) + if end is not None and end < 0: + end += len(self) + + if duration is not None and duration < 0: + raise InvalidDuration("duration must be a positive integer") + + if duration: + if start is not None: + end = start + duration + elif end is not None: + start = end - duration + else: + duration = end - start + + from_power = db_to_float(from_gain) + + output = [] + + # original data - up until the crossfade portion, as is + before_fade = self[:start]._data + if from_gain != 0: + before_fade = audioop.mul(before_fade, self.sample_width, from_power) + output.append(before_fade) + + gain_delta = db_to_float(to_gain) - from_power + + # fades longer than 100ms can use coarse fading (one gain step per ms), + # shorter fades will have audible clicks so they use precise fading + # (one gain step per sample) + if duration > 100: + scale_step = gain_delta / duration + + for i in range(duration): + volume_change = from_power + (scale_step * i) + chunk = self[start + i] + chunk = audioop.mul(chunk._data, self.sample_width, volume_change) + + output.append(chunk) + else: + start_frame = self.frame_count(ms=start) + end_frame = self.frame_count(ms=end) + fade_frames = end_frame - start_frame + scale_step = gain_delta / fade_frames + + for i in range(int(fade_frames)): + volume_change = from_power + (scale_step * i) + sample = self.get_frame(int(start_frame + i)) + sample = audioop.mul(sample, self.sample_width, volume_change) + + output.append(sample) + + # original data after the crossfade portion, at the new volume + after_fade = self[end:]._data + if to_gain != 0: + after_fade = audioop.mul(after_fade, self.sample_width, db_to_float(to_gain)) + output.append(after_fade) + + return self._spawn(data=output) + + def fade_out(self, duration): + return self.fade(to_gain=-120, duration=duration, end=float("inf")) + + def fade_in(self, duration): + return self.fade(from_gain=-120, duration=duration, start=0) + + def reverse(self): + return self._spawn(data=audioop.reverse(self._data, self.sample_width)) + + def _repr_html_(self): + src = """ + + """ + fh = self.export() + data = base64.b64encode(fh.read()).decode("ascii") + return src.format(base64=data) diff --git a/tagstudio/src/qt/helpers/vendored/pydub/utils.py b/tagstudio/src/qt/helpers/vendored/pydub/utils.py new file mode 100644 index 00000000..5c9e3c4b --- /dev/null +++ b/tagstudio/src/qt/helpers/vendored/pydub/utils.py @@ -0,0 +1,89 @@ +import json +import re +import subprocess + +from pydub.utils import ( + _fd_or_path_or_tempfile, + fsdecode, + get_extra_info, + get_prober_name, +) +from src.qt.helpers.silent_popen import promptless_Popen + + +def _mediainfo_json(filepath, read_ahead_limit=-1): + """Return json dictionary with media info(codec, duration, size, bitrate...) from filepath.""" + prober = get_prober_name() + command_args = [ + "-v", + "info", + "-show_format", + "-show_streams", + ] + try: + command_args += [fsdecode(filepath)] + stdin_parameter = None + stdin_data = None + except TypeError: + if prober == "ffprobe": + command_args += ["-read_ahead_limit", str(read_ahead_limit), "cache:pipe:0"] + else: + command_args += ["-"] + stdin_parameter = subprocess.PIPE + file, close_file = _fd_or_path_or_tempfile(filepath, "rb", tempfile=False) + file.seek(0) + stdin_data = file.read() + if close_file: + file.close() + + command = [prober, "-of", "json"] + command_args + # PATCHED + res = promptless_Popen( + command, stdin=stdin_parameter, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + output, stderr = res.communicate(input=stdin_data) + output = output.decode("utf-8", "ignore") + stderr = stderr.decode("utf-8", "ignore") + + try: + info = json.loads(output) + except json.decoder.JSONDecodeError: + # If ffprobe didn't give any information, just return it + # (for example, because the file doesn't exist) + return None + if not info: + return info + + extra_info = get_extra_info(stderr) + + audio_streams = [x for x in info["streams"] if x["codec_type"] == "audio"] + if len(audio_streams) == 0: + return info + + # We just operate on the first audio stream in case there are more + stream = audio_streams[0] + + def set_property(stream, prop, value): + if prop not in stream or stream[prop] == 0: + stream[prop] = value + + for token in extra_info[stream["index"]]: + m = re.match(r"([su]([0-9]{1,2})p?) \(([0-9]{1,2}) bit\)$", token) + m2 = re.match(r"([su]([0-9]{1,2})p?)( \(default\))?$", token) + if m: + set_property(stream, "sample_fmt", m.group(1)) + set_property(stream, "bits_per_sample", int(m.group(2))) + set_property(stream, "bits_per_raw_sample", int(m.group(3))) + elif m2: + set_property(stream, "sample_fmt", m2.group(1)) + set_property(stream, "bits_per_sample", int(m2.group(2))) + set_property(stream, "bits_per_raw_sample", int(m2.group(2))) + elif re.match(r"(flt)p?( \(default\))?$", token): + set_property(stream, "sample_fmt", token) + set_property(stream, "bits_per_sample", 32) + set_property(stream, "bits_per_raw_sample", 32) + elif re.match(r"(dbl)p?( \(default\))?$", token): + set_property(stream, "sample_fmt", token) + set_property(stream, "bits_per_sample", 64) + set_property(stream, "bits_per_raw_sample", 64) + return info diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index ce6b1e33..d3274c7e 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -36,7 +36,7 @@ class Ui_MainWindow(QMainWindow): def __init__(self, driver: "QtDriver", parent=None) -> None: super().__init__(parent) - self.driver = driver + self.driver: "QtDriver" = driver self.setupUi(self) # NOTE: These are old attempts to allow for a translucent/acrylic @@ -66,7 +66,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setObjectName(u"horizontalLayout") - # ComboBox goup for search type and thumbnail size + # ComboBox group for search type and thumbnail size self.horizontalLayout_3 = QHBoxLayout() self.horizontalLayout_3.setObjectName("horizontalLayout_3") @@ -74,7 +74,7 @@ class Ui_MainWindow(QMainWindow): spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem) - # Search type selector + # Search type selector self.comboBox_2 = QComboBox(self.centralwidget) self.comboBox_2.setMinimumSize(QSize(165, 0)) self.comboBox_2.setObjectName("comboBox_2") @@ -83,17 +83,17 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_3.addWidget(self.comboBox_2) # Thumbnail Size placeholder - self.comboBox = QComboBox(self.centralwidget) - self.comboBox.setObjectName(u"comboBox") + self.thumb_size_combobox = QComboBox(self.centralwidget) + self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox") sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( - self.comboBox.sizePolicy().hasHeightForWidth()) - self.comboBox.setSizePolicy(sizePolicy) - self.comboBox.setMinimumWidth(128) - self.comboBox.setMaximumWidth(128) - self.horizontalLayout_3.addWidget(self.comboBox) + self.thumb_size_combobox.sizePolicy().hasHeightForWidth()) + self.thumb_size_combobox.setSizePolicy(sizePolicy) + self.thumb_size_combobox.setMinimumWidth(128) + self.thumb_size_combobox.setMaximumWidth(352) + self.horizontalLayout_3.addWidget(self.thumb_size_combobox) self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) self.splitter = QSplitter() @@ -212,10 +212,10 @@ class Ui_MainWindow(QMainWindow): # Search type selector self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) - self.comboBox.setCurrentText("") + self.thumb_size_combobox.setCurrentText("") # Thumbnail size selector - self.comboBox.setPlaceholderText( + self.thumb_size_combobox.setPlaceholderText( QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) # retranslateUi @@ -236,3 +236,4 @@ class Ui_MainWindow(QMainWindow): self.landing_widget.setHidden(True) self.landing_widget.set_status_label("") self.scrollArea.setHidden(False) + \ No newline at end of file diff --git a/tagstudio/src/qt/platform_strings.py b/tagstudio/src/qt/platform_strings.py new file mode 100644 index 00000000..9eda3ef8 --- /dev/null +++ b/tagstudio/src/qt/platform_strings.py @@ -0,0 +1,16 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +"""A collection of platform-dependant strings.""" + +import platform + + +class PlatformStrings: + open_file_str: str = "Open in file explorer" + + if platform.system() == "Windows": + open_file_str = "Open in Explorer" + elif platform.system() == "Darwin": + open_file_str = "Reveal in Finder" diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index a9826e72..d9929e7b 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -7,6 +7,7 @@ from typing import Any import structlog import ujson +from PIL import Image logger = structlog.get_logger(__name__) @@ -17,6 +18,7 @@ class ResourceManager: _map: dict = {} _cache: dict[str, Any] = {} _initialized: bool = False + _res_folder: Path = Path(__file__).parents[2] def __init__(self) -> None: # Load JSON resource map @@ -26,6 +28,21 @@ class ResourceManager: logger.info("resources registered", count=len(ResourceManager._map.items())) ResourceManager._initialized = True + @staticmethod + def get_path(id: str) -> Path | None: + """Get a resource's path from the ResourceManager. + + Args: + id (str): The name of the resource. + + Returns: + Path: The resource path if found, else None. + """ + res: dict = ResourceManager._map.get(id) + if res: + return ResourceManager._res_folder / "resources" / res.get("path") + return None + def get(self, id: str) -> Any: """Get a resource from the ResourceManager. @@ -43,19 +60,29 @@ class ResourceManager: return cached_res else: res: dict = ResourceManager._map.get(id) - if res.get("mode") in ["r", "rb"]: - with open( - (Path(__file__).parents[2] / "resources" / res.get("path")), - res.get("mode"), - ) as f: - data = f.read() - if res.get("mode") == "rb": - data = bytes(data) - ResourceManager._cache[id] = data + if not res: + return None + try: + if res.get("mode") in ["r", "rb"]: + with open( + (ResourceManager._res_folder / "resources" / res.get("path")), + res.get("mode"), + ) as f: + data = f.read() + if res.get("mode") == "rb": + data = bytes(data) + ResourceManager._cache[id] = data + return data + elif res and res.get("mode") == "pil": + data = Image.open(ResourceManager._res_folder / "resources" / res.get("path")) return data - elif res.get("mode") in ["qt"]: - # TODO: Qt resource loading logic - pass + elif res.get("mode") in ["qt"]: + # TODO: Qt resource loading logic + pass + except FileNotFoundError: + path: Path = ResourceManager._res_folder / "resources" / res.get("path") + logger.error("[ResourceManager][ERROR]: Could not find resource: ", path) + return None def __getattr__(self, __name: str) -> Any: attr = self.get(__name) diff --git a/tagstudio/src/qt/resources.json b/tagstudio/src/qt/resources.json index 1f8663d3..e5857909 100644 --- a/tagstudio/src/qt/resources.json +++ b/tagstudio/src/qt/resources.json @@ -14,5 +14,85 @@ "volume_mute_icon": { "path": "qt/images/volume_mute.svg", "mode": "rb" + }, + "broken_link_icon": { + "path": "qt/images/broken_link_icon.png", + "mode": "pil" + }, + "adobe_illustrator": { + "path": "qt/images/file_icons/adobe_illustrator.png", + "mode": "pil" + }, + "adobe_photoshop": { + "path": "qt/images/file_icons/adobe_photoshop.png", + "mode": "pil" + }, + "affinity_photo": { + "path": "qt/images/file_icons/affinity_photo.png", + "mode": "pil" + }, + "audio": { + "path": "qt/images/file_icons/audio.png", + "mode": "pil" + }, + "blender": { + "path": "qt/images/file_icons/blender.png", + "mode": "pil" + }, + "document": { + "path": "qt/images/file_icons/document.png", + "mode": "pil" + }, + "file_generic": { + "path": "qt/images/file_icons/file_generic.png", + "mode": "pil" + }, + "font": { + "path": "qt/images/file_icons/font.png", + "mode": "pil" + }, + "image": { + "path": "qt/images/file_icons/image.png", + "mode": "pil" + }, + "image_vector": { + "path": "qt/images/file_icons/image_vector.png", + "mode": "pil" + }, + "material": { + "path": "qt/images/file_icons/material.png", + "mode": "pil" + }, + "model": { + "path": "qt/images/file_icons/model.png", + "mode": "pil" + }, + "presentation": { + "path": "qt/images/file_icons/presentation.png", + "mode": "pil" + }, + "program": { + "path": "qt/images/file_icons/program.png", + "mode": "pil" + }, + "spreadsheet": { + "path": "qt/images/file_icons/spreadsheet.png", + "mode": "pil" + }, + "text": { + "path": "qt/images/file_icons/text.png", + "mode": "pil" + }, + "video": { + "path": "qt/images/file_icons/video.png", + "mode": "pil" + }, + "thumb_loading": { + "path": "qt/images/thumb_loading.png", + "mode": "pil" + }, + "placeholder_mp4": { + "path": "qt/videos/placeholder.mp4", + "mode": "rb" } } diff --git a/tagstudio/src/qt/resources_rc.py b/tagstudio/src/qt/resources_rc.py index f11bf7fb..c2d3874d 100644 --- a/tagstudio/src/qt/resources_rc.py +++ b/tagstudio/src/qt/resources_rc.py @@ -1,6 +1,6 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.6.3 +# Created by: The Resource Compiler for Qt version 6.7.1 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore @@ -16240,7 +16240,7 @@ qt_resource_struct = b"\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x8a\xfb\xc6\x86\xda\ \x00\x00\x00H\x00\x00\x00\x00\x00\x01\x00\x00\x22\x83\ -\x00\x00\x01\x8e\xfd%\xc3\xc7\ +\x00\x00\x01\x92\x0cdgU\ " def qInitResources(): diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 9490e783..e80aa84e 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -453,7 +453,13 @@ class QtDriver(DriverMixin, QObject): str(Path(__file__).parents[2] / "resources/qt/fonts/Oxanium-Bold.ttf") ) - self.thumb_size = 128 + self.thumb_sizes: list[tuple[str, int]] = [ + ("Extra Large Thumbnails", 256), + ("Large Thumbnails", 192), + ("Medium Thumbnails", 128), + ("Small Thumbnails", 96), + ("Mini Thumbnails", 76), + ] self.item_thumbs: list[ItemThumb] = [] self.thumb_renderers: list[ThumbRenderer] = [] self.filter = FilterState() @@ -488,26 +494,37 @@ class QtDriver(DriverMixin, QObject): def init_library_window(self): # self._init_landing_page() # Taken care of inside the widget now - self._init_thumb_grid() # TODO: Put this into its own method that copies the font file(s) into memory # so the resource isn't being used, then store the specific size variations # in a global dict for methods to access for different DPIs. # adj_font_size = math.floor(12 * self.main_window.devicePixelRatio()) + # Search Button search_button: QPushButton = self.main_window.searchButton search_button.clicked.connect( lambda: self.filter_items(FilterState(query=self.main_window.searchField.text())) ) + # Search Field search_field: QLineEdit = self.main_window.searchField search_field.returnPressed.connect( # TODO - parse search field for filters lambda: self.filter_items(FilterState(query=self.main_window.searchField.text())) ) + # Search Type Selector search_type_selector: QComboBox = self.main_window.comboBox_2 search_type_selector.currentIndexChanged.connect( lambda: self.set_search_type(SearchMode(search_type_selector.currentIndex())) ) + # Thumbnail Size ComboBox + thumb_size_combobox: QComboBox = self.main_window.thumb_size_combobox + for size in self.thumb_sizes: + thumb_size_combobox.addItem(size[0]) + thumb_size_combobox.setCurrentIndex(2) # Default: Medium + thumb_size_combobox.currentIndexChanged.connect( + lambda: self.thumb_size_callback(thumb_size_combobox.currentIndex()) + ) + self._init_thumb_grid() back_button: QPushButton = self.main_window.backButton back_button.clicked.connect(lambda: self.page_move(-1)) @@ -802,6 +819,34 @@ class QtDriver(DriverMixin, QObject): content=strip_web_protocol(field.value), ) + def thumb_size_callback(self, index: int): + """Perform actions needed when the thumbnail size selection is changed. + + Args: + index (int): The index of the item_thumbs/ComboBox list to use. + """ + spacing_divisor: int = 10 + min_spacing: int = 12 + # Index 2 is the default (Medium) + if index < len(self.thumb_sizes) and index >= 0: + self.thumb_size = self.thumb_sizes[index][1] + else: + logger.error(f"ERROR: Invalid thumbnail size index ({index}). Defaulting to 128px.") + self.thumb_size = 128 + + self.update_thumbs() + blank_icon: QIcon = QIcon() + for it in self.item_thumbs: + it.thumb_button.setIcon(blank_icon) + it.resize(self.thumb_size, self.thumb_size) + it.thumb_size = (self.thumb_size, self.thumb_size) + it.setMinimumSize(self.thumb_size, self.thumb_size) + it.setMaximumSize(self.thumb_size, self.thumb_size) + it.thumb_button.thumb_size = (self.thumb_size, self.thumb_size) + self.flow_container.layout().setSpacing( + min(self.thumb_size // spacing_divisor, min_spacing) + ) + def mouse_navigation(self, event: QMouseEvent): # print(event.button()) if event.button() == Qt.MouseButton.ForwardButton: diff --git a/tagstudio/src/qt/widgets/collage_icon.py b/tagstudio/src/qt/widgets/collage_icon.py index f3e1b364..e15cdd52 100644 --- a/tagstudio/src/qt/widgets/collage_icon.py +++ b/tagstudio/src/qt/widgets/collage_icon.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import math from pathlib import Path import cv2 @@ -12,9 +13,10 @@ from PySide6.QtCore import ( QObject, Signal, ) -from src.core.constants import DOC_TYPES, IMAGE_TYPES, VIDEO_TYPES from src.core.library import Library from src.core.library.alchemy.fields import _FieldID +from src.core.media_types import MediaCategories +from src.qt.helpers.file_tester import is_readable_video logger = structlog.get_logger(__name__) @@ -74,7 +76,8 @@ class CollageIconRenderer(QObject): color=self.get_file_color(filepath.suffix.lower()), ) - if filepath.suffix.lower() in IMAGE_TYPES: + ext: str = filepath.suffix.lower() + if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES): try: with Image.open(str(self.lib.library_dir / entry.path)) as pic: if keep_aspect: @@ -84,23 +87,34 @@ class CollageIconRenderer(QObject): if data_tint_mode and color: pic = pic.convert(mode="RGB") pic = ImageChops.hard_light(pic, Image.new("RGB", size, color)) - # collage.paste(pic, (y*thumb_size, x*thumb_size)) self.rendered.emit(pic) - except DecompressionBombError: - logger.exception("One of the images was too big", entry=entry.path) - elif filepath.suffix.lower() in VIDEO_TYPES: - video = cv2.VideoCapture(str(filepath)) + except DecompressionBombError as e: + logger.info(f"[ERROR] One of the images was too big ({e})") + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.VIDEO_TYPES + ) and is_readable_video(filepath): + video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG) video.set( cv2.CAP_PROP_POS_FRAMES, (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2), ) success, frame = video.read() - if not success: - # Depending on the video format, compression, and frame - # count, seeking halfway does not work and the thumb - # must be pulled from the earliest available frame. - video.set(cv2.CAP_PROP_POS_FRAMES, 0) + # NOTE: Depending on the video format, compression, and + # frame count, seeking halfway does not work and the thumb + # must be pulled from the earliest available frame. + max_frame_seek: int = 10 + for i in range( + 0, + min( + max_frame_seek, + math.floor(video.get(cv2.CAP_PROP_FRAME_COUNT)), + ), + ): success, frame = video.read() + if not success: + video.set(cv2.CAP_PROP_POS_FRAMES, i) + else: + break frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) with Image.fromarray(frame, mode="RGB") as pic: if keep_aspect: @@ -109,7 +123,6 @@ class CollageIconRenderer(QObject): pic = pic.resize(size) if data_tint_mode and color: pic = ImageChops.hard_light(pic, Image.new("RGB", size, color)) - # collage.paste(pic, (y*thumb_size, x*thumb_size)) self.rendered.emit(pic) except (UnidentifiedImageError, FileNotFoundError): logger.error("Couldn't read entry", entry=entry.path) @@ -130,13 +143,13 @@ class CollageIconRenderer(QObject): self.done.emit() def get_file_color(self, ext: str): - if ext.lower().replace(".", "", 1) == "gif": + if ext.lower() == "gif": return "\033[93m" - if ext.lower().replace(".", "", 1) in IMAGE_TYPES: + if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES): return "\033[37m" - elif ext.lower().replace(".", "", 1) in VIDEO_TYPES: + elif MediaCategories.is_ext_in_category(ext, MediaCategories.VIDEO_TYPES): return "\033[96m" - elif ext.lower().replace(".", "", 1) in DOC_TYPES: + elif MediaCategories.is_ext_in_category(ext, MediaCategories.PLAINTEXT_TYPES): return "\033[92m" else: return "\033[97m" diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index 005cf1b0..da82a19d 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -21,17 +21,16 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.constants import ( - AUDIO_TYPES, - IMAGE_TYPES, TAG_ARCHIVED, TAG_FAVORITE, - VIDEO_TYPES, ) from src.core.library import Entry, ItemType, Library from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import _FieldID +from src.core.media_types import MediaCategories, MediaType from src.qt.flowlayout import FlowWidget from src.qt.helpers.file_opener import FileOpenerHelper +from src.qt.platform_strings import PlatformStrings from src.qt.widgets.thumb_button import ThumbButton from src.qt.widgets.thumb_renderer import ThumbRenderer @@ -88,6 +87,7 @@ class ItemThumb(FlowWidget): small_text_style = ( "background-color:rgba(0, 0, 0, 192);" + "color:#FFFFFF;" "font-family:Oxanium;" "font-weight:bold;" "font-size:12px;" @@ -100,6 +100,7 @@ class ItemThumb(FlowWidget): med_text_style = ( "background-color:rgba(0, 0, 0, 192);" + "color:#FFFFFF;" "font-family:Oxanium;" "font-weight:bold;" "font-size:18px;" @@ -198,7 +199,7 @@ class ItemThumb(FlowWidget): self.opener = FileOpenerHelper("") open_file_action = QAction("Open file", self) open_file_action.triggered.connect(self.opener.open_file) - open_explorer_action = QAction("Open file in explorer", self) + open_explorer_action = QAction(PlatformStrings.open_file_str, self) open_explorer_action.triggered.connect(self.opener.open_explorer) self.thumb_button.addAction(open_file_action) self.thumb_button.addAction(open_explorer_action) @@ -330,10 +331,26 @@ class ItemThumb(FlowWidget): def set_extension(self, ext: str) -> None: if ext and ext.startswith(".") is False: ext = "." + ext - if ext and ext not in IMAGE_TYPES or ext in [".gif", ".apng"]: + media_types: set[MediaType] = MediaCategories.get_types(ext) + if ( + ext + and not MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES) + or MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES) + or MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_VECTOR_TYPES) + or MediaCategories.is_ext_in_category(ext, MediaCategories.ADOBE_PHOTOSHOP_TYPES) + or ext + in [ + ".apng", + ".avif", + ".exr", + ".gif", + ".jxl", + ".webp", + ] + ): self.ext_badge.setHidden(False) self.ext_badge.setText(ext.upper()[1:]) - if ext in VIDEO_TYPES + AUDIO_TYPES: + if MediaType.VIDEO in media_types or MediaType.AUDIO in media_types: self.count_badge.setHidden(False) else: if self.mode == ItemType.ENTRY: diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index f595644f..b38fc6c1 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -1,6 +1,8 @@ # Copyright (C) 2024 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import os +import platform import sys import time import typing @@ -12,10 +14,10 @@ import cv2 import rawpy import structlog from humanfriendly import format_size -from PIL import Image, UnidentifiedImageError +from PIL import Image, ImageFont, UnidentifiedImageError from PIL.Image import DecompressionBombError -from PySide6.QtCore import QSize, Qt, Signal -from PySide6.QtGui import QAction, QResizeEvent +from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt, Signal +from PySide6.QtGui import QAction, QGuiApplication, QMovie, QResizeEvent from PySide6.QtWidgets import ( QFrame, QHBoxLayout, @@ -28,7 +30,9 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME, VIDEO_TYPES +from src.core.constants import ( + TS_FOLDER_NAME, +) from src.core.enums import SettingItems, Theme from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import ( @@ -40,9 +44,13 @@ from src.core.library.alchemy.fields import ( _FieldID, ) from src.core.library.alchemy.library import Library +from src.core.media_types import MediaCategories from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel, open_file +from src.qt.helpers.file_tester import is_readable_video from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from src.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle from src.qt.modals.add_field import AddFieldModal +from src.qt.platform_strings import PlatformStrings from src.qt.widgets.fields import FieldContainer from src.qt.widgets.panel import PanelModal from src.qt.widgets.tag_box import TagBoxWidget @@ -80,8 +88,6 @@ class PreviewPanel(QWidget): self.driver: QtDriver = driver self.initialized = False self.is_open: bool = False - # self.filepath = None - # self.item = None # DEPRECATED, USE self.selected self.common_fields: list = [] self.mixed_fields: list = [] self.selected: list[int] = [] # New way of tracking items @@ -91,20 +97,55 @@ class PreviewPanel(QWidget): self.img_button_size: tuple[int, int] = (266, 266) self.image_ratio: float = 1.0 + self.label_bg_color = ( + Theme.COLOR_BG_DARK.value + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else Theme.COLOR_DARK_LABEL.value + ) + self.panel_bg_color = ( + Theme.COLOR_BG_DARK.value + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else Theme.COLOR_BG_LIGHT.value + ) + self.image_container = QWidget() image_layout = QHBoxLayout(self.image_container) image_layout.setContentsMargins(0, 0, 0, 0) + file_label_style = "font-size: 12px" + properties_style = ( + f"background-color:{self.label_bg_color};" + "color:#FFFFFF;" + "font-family:Oxanium;" + "font-weight:bold;" + "font-size:12px;" + "border-radius:3px;" + "padding-top: 4px;" + "padding-right: 1px;" + "padding-bottom: 1px;" + "padding-left: 1px;" + ) + date_style = "font-size:12px;" + self.open_file_action = QAction("Open file", self) - self.open_explorer_action = QAction("Open file in explorer", self) + self.open_explorer_action = QAction(PlatformStrings.open_file_str, self) self.preview_img = QPushButtonWrapper() self.preview_img.setMinimumSize(*self.img_button_size) self.preview_img.setFlat(True) self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) - self.preview_img.addAction(self.open_file_action) self.preview_img.addAction(self.open_explorer_action) + + self.preview_gif = QLabel() + self.preview_gif.setMinimumSize(*self.img_button_size) + self.preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.preview_gif.setCursor(Qt.CursorShape.ArrowCursor) + self.preview_gif.addAction(self.open_file_action) + self.preview_gif.addAction(self.open_explorer_action) + self.preview_gif.hide() + self.gif_buffer: QBuffer = QBuffer() + self.preview_vid = VideoPlayer(driver) self.preview_vid.hide() self.thumb_renderer = ThumbRenderer() @@ -124,31 +165,29 @@ class PreviewPanel(QWidget): image_layout.addWidget(self.preview_img) image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter) + image_layout.addWidget(self.preview_gif) + image_layout.setAlignment(self.preview_gif, Qt.AlignmentFlag.AlignCenter) image_layout.addWidget(self.preview_vid) image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter) self.image_container.setMinimumSize(*self.img_button_size) - self.file_label = FileOpenerLabel("Filename") + self.file_label = FileOpenerLabel("filename") + self.file_label.setTextFormat(Qt.TextFormat.RichText) self.file_label.setWordWrap(True) self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) - self.file_label.setStyleSheet("font-weight: bold; font-size: 12px") + self.file_label.setStyleSheet(file_label_style) - self.dimensions_label = QLabel("Dimensions") + self.date_created_label = QLabel("dateCreatedLabel") + self.date_created_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.date_created_label.setTextFormat(Qt.TextFormat.RichText) + self.date_created_label.setStyleSheet(date_style) + + self.date_modified_label = QLabel("dateModifiedLabel") + self.date_modified_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.date_modified_label.setTextFormat(Qt.TextFormat.RichText) + self.date_modified_label.setStyleSheet(date_style) + + self.dimensions_label = QLabel("dimensionsLabel") self.dimensions_label.setWordWrap(True) - # self.dim_label.setTextInteractionFlags( - # Qt.TextInteractionFlag.TextSelectableByMouse) - - properties_style = ( - f"background-color:{Theme.COLOR_BG.value};" - f"font-family:Oxanium;" - f"font-weight:bold;" - f"font-size:12px;" - f"border-radius:6px;" - f"padding-top: 4px;" - f"padding-right: 1px;" - f"padding-bottom: 1px;" - f"padding-left: 1px;" - ) - self.dimensions_label.setStyleSheet(properties_style) self.scroll_layout = QVBoxLayout() @@ -175,15 +214,24 @@ class PreviewPanel(QWidget): # background and NOT the scroll container background, so that the # rounded corners are maintained when scrolling. I was unable to # find the right trick to only select that particular element. + scroll_area.setStyleSheet( "QWidget#entryScrollContainer{" - f"background: {Theme.COLOR_BG.value};" + f"background:{self.panel_bg_color};" "border-radius:6px;" "}" ) scroll_area.setWidget(scroll_container) + date_container = QWidget() + date_layout = QVBoxLayout(date_container) + date_layout.setContentsMargins(0, 2, 0, 0) + date_layout.setSpacing(0) + date_layout.addWidget(self.date_created_label) + date_layout.addWidget(self.date_modified_label) + info_layout.addWidget(self.file_label) + info_layout.addWidget(date_container) info_layout.addWidget(self.dimensions_label) info_layout.addWidget(scroll_area) @@ -326,11 +374,11 @@ class PreviewPanel(QWidget): return lambda: self.driver.open_library(Path(path)) button.clicked.connect(open_library_button_clicked(full_val)) - set_button_style(button) - button_remove = QPushButton("➖") + set_button_style(button, ["padding-left: 6px;", "text-align: left;"]) + button_remove = QPushButton("—") button_remove.setCursor(Qt.CursorShape.PointingHandCursor) - button_remove.setFixedWidth(30) - set_button_style(button_remove) + button_remove.setFixedWidth(24) + set_button_style(button_remove, ["font-weight:bold;", "text-align:center;"]) def remove_recent_library_clicked(key: str): return lambda: ( @@ -364,19 +412,14 @@ class PreviewPanel(QWidget): def update_image_size(self, size: tuple[int, int], ratio: float = None): if ratio: self.set_image_ratio(ratio) - # self.img_button_size = size - # logging.info(f'') - # self.preview_img.setMinimumSize(64,64) adj_width: float = size[0] adj_height: float = size[1] # Landscape if self.image_ratio > 1: - # logging.info('Landscape') adj_height = size[0] * (1 / self.image_ratio) # Portrait elif self.image_ratio <= 1: - # logging.info('Portrait') adj_width = size[1] * self.image_ratio if adj_width > size[0]: @@ -386,11 +429,6 @@ class PreviewPanel(QWidget): adj_width = adj_width * (size[1] / adj_height) adj_height = size[1] - # adj_width = min(adj_width, self.image_container.size().width()) - # adj_height = min(adj_width, self.image_container.size().height()) - - # self.preview_img.setMinimumSize(s) - # self.preview_img.setMaximumSize(s_max) adj_size = QSize(int(adj_width), int(adj_height)) self.img_button_size = (int(adj_width), int(adj_height)) self.preview_img.setMaximumSize(adj_size) @@ -398,7 +436,14 @@ class PreviewPanel(QWidget): self.preview_vid.resize_video(adj_size) self.preview_vid.setMaximumSize(adj_size) self.preview_vid.setMinimumSize(adj_size) - # self.preview_img.setMinimumSize(adj_size) + self.preview_gif.setMaximumSize(adj_size) + self.preview_gif.setMinimumSize(adj_size) + proxy_style = RoundedPixmapStyle(radius=8) + self.preview_gif.setStyle(proxy_style) + self.preview_vid.setStyle(proxy_style) + m = self.preview_gif.movie() + if m: + m.setScaledSize(adj_size) def place_add_field_button(self): self.scroll_layout.addWidget(self.afb_container) @@ -410,11 +455,7 @@ class PreviewPanel(QWidget): self.add_field_button.clicked.disconnect() self.add_field_modal.done.connect( - lambda items: ( - self.add_field_to_selected(items), - update_selected_entry(self.driver), - self.update_widgets(), - ) + lambda f: (self.add_field_to_selected(f), self.update_widgets()) ) self.add_field_modal.is_connected = True self.add_field_button.clicked.connect(self.add_field_modal.show) @@ -430,6 +471,32 @@ class PreviewPanel(QWidget): field_id=field_item.data(Qt.ItemDataRole.UserRole), ) + def update_date_label(self, filepath: Path | None = None) -> None: + """Update the "Date Created" and "Date Modified" file property labels.""" + if filepath and filepath.is_file(): + created: dt = None + if platform.system() == "Windows" or platform.system() == "Darwin": + created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined] + else: + created = dt.fromtimestamp(filepath.stat().st_ctime) + modified: dt = dt.fromtimestamp(filepath.stat().st_mtime) + self.date_created_label.setText( + f"Date Created: {dt.strftime(created, "%a, %x, %X")}" + ) + self.date_modified_label.setText( + f"Date Modified: {dt.strftime(modified, "%a, %x, %X")}" + ) + self.date_created_label.setHidden(False) + self.date_modified_label.setHidden(False) + elif filepath: + self.date_created_label.setText("Date Created: N/A") + self.date_modified_label.setText("Date Modified: N/A") + self.date_created_label.setHidden(False) + self.date_modified_label.setHidden(False) + else: + self.date_created_label.setHidden(True) + self.date_modified_label.setHidden(True) + def update_widgets(self) -> bool: """Render the panel widgets with the newest data from the Library.""" logger.info("update_widgets", selected=self.driver.selected) @@ -442,11 +509,12 @@ class PreviewPanel(QWidget): if not self.driver.selected: if self.selected or not self.initialized: - self.file_label.setText("No Items Selected") + self.file_label.setText("No Items Selected") self.file_label.set_file_path("") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.dimensions_label.setText("") + self.update_date_label() self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self.preview_img.setCursor(Qt.CursorShape.ArrowCursor) @@ -466,6 +534,7 @@ class PreviewPanel(QWidget): self.preview_img.show() self.preview_vid.stop() self.preview_vid.hide() + self.preview_gif.hide() self.selected = list(self.driver.selected) self.add_field_button.setHidden(True) @@ -497,6 +566,7 @@ class PreviewPanel(QWidget): self.preview_img.show() self.preview_vid.stop() self.preview_vid.hide() + self.preview_gif.hide() # If a new selection is made, update the thumbnail and filepath. if not self.selected or self.selected != self.driver.selected: @@ -510,7 +580,15 @@ class PreviewPanel(QWidget): ratio, update_on_ratio_change=True, ) - self.file_label.setText("\u200b".join(str(filepath))) + file_str: str = "" + separator: str = f"{os.path.sep}" # Gray + for i, part in enumerate(filepath.parts): + part_ = part.strip(os.path.sep) + if i != len(filepath.parts) - 1: + file_str += f"{"\u200b".join(part_)}{separator}" + else: + file_str += f"
    {"\u200b".join(part_)}" + self.file_label.setText(file_str) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) @@ -520,12 +598,42 @@ class PreviewPanel(QWidget): self.open_file_action.triggered.connect(self.opener.open_file) self.open_explorer_action.triggered.connect(self.opener.open_explorer) - # TODO: Do this somewhere else, this is just here temporarily. + # TODO: Do this all somewhere else, this is just here temporarily. + ext: str = filepath.suffix.lower() try: - image = None - if filepath.suffix.lower() in IMAGE_TYPES: + if filepath.suffix.lower() in [".gif"]: + with open(filepath, mode="rb") as file: + if self.preview_gif.movie(): + self.preview_gif.movie().stop() + self.gif_buffer.close() + + ba = file.read() + self.gif_buffer.setData(ba) + movie = QMovie(self.gif_buffer, QByteArray()) + self.preview_gif.setMovie(movie) + movie.start() + image = Image.open(str(filepath)) - elif filepath.suffix.lower() in RAW_IMAGE_TYPES: + self.resizeEvent( + QResizeEvent( + QSize(image.width, image.height), + QSize(image.width, image.height), + ) + ) + self.preview_img.hide() + self.preview_vid.hide() + self.preview_gif.show() + + image = None + if ( + MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES) + and MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES) + and MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_VECTOR_TYPES + ) + ): + image = Image.open(str(filepath)) + elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES): try: with rawpy.imread(str(filepath)) as raw: rgb = raw.postprocess() @@ -535,11 +643,14 @@ class PreviewPanel(QWidget): rawpy._rawpy.LibRawFileUnsupportedError, ): pass - elif filepath.suffix.lower() in VIDEO_TYPES: - video = cv2.VideoCapture(str(filepath)) - if video.get(cv2.CAP_PROP_FRAME_COUNT) <= 0: - raise cv2.error("File is invalid or has 0 frames") - video.set(cv2.CAP_PROP_POS_FRAMES, 0) + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.VIDEO_TYPES + ) and is_readable_video(filepath): + video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG) + video.set( + cv2.CAP_PROP_POS_FRAMES, + (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2), + ) success, frame = video.read() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image = Image.fromarray(frame) @@ -555,35 +666,60 @@ class PreviewPanel(QWidget): self.preview_vid.show() # Stats for specific file types are displayed here. - if image and filepath.suffix.lower() in ( - IMAGE_TYPES + VIDEO_TYPES + RAW_IMAGE_TYPES + if image and ( + MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_TYPES, mime_fallback=True + ) + or MediaCategories.is_ext_in_category( + ext, MediaCategories.VIDEO_TYPES, mime_fallback=True + ) + or MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True + ) ): self.dimensions_label.setText( - f"{filepath.suffix.upper()[1:]}" - f" • {format_size(filepath.stat().st_size)}\n{image.width} " - f"x {image.height} px" + f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n" + f"{image.width} x {image.height} px" ) + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.FONT_TYPES, mime_fallback=True + ): + try: + font = ImageFont.truetype(filepath) + self.dimensions_label.setText( + f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n" + f"{font.getname()[0]} ({font.getname()[1]}) " + ) + except OSError: + self.dimensions_label.setText( + f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}" + ) + logger.info( + f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}" + ) else: + self.dimensions_label.setText(f"{ext.upper()[1:]}") self.dimensions_label.setText( - f"{filepath.suffix.upper()[1:]}" - f" • {format_size(filepath.stat().st_size)}" + f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}" ) + self.update_date_label(filepath) if not filepath.is_file(): raise FileNotFoundError except (FileNotFoundError, cv2.error) as e: - self.dimensions_label.setText(f"{filepath.suffix.upper()}") - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) - + self.dimensions_label.setText(f"{ext.upper()[1:]}") + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + self.update_date_label() except ( UnidentifiedImageError, DecompressionBombError, ) as e: self.dimensions_label.setText( - f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}" + f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}" ) - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + self.update_date_label(filepath) if self.preview_img.is_connected: self.preview_img.clicked.disconnect() @@ -610,10 +746,12 @@ class PreviewPanel(QWidget): # Multiple Selected Items elif len(self.driver.selected) > 1: self.preview_img.show() + self.preview_gif.hide() self.preview_vid.stop() self.preview_vid.hide() + self.update_date_label() if self.selected != self.driver.selected: - self.file_label.setText(f"{len(self.driver.selected)} Items Selected") + self.file_label.setText(f"{len(self.driver.selected)} Items Selected") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.file_label.set_file_path("") self.dimensions_label.setText("") diff --git a/tagstudio/src/qt/widgets/thumb_button.py b/tagstudio/src/qt/widgets/thumb_button.py index e56408b7..cf8dae37 100644 --- a/tagstudio/src/qt/widgets/thumb_button.py +++ b/tagstudio/src/qt/widgets/thumb_button.py @@ -5,19 +5,51 @@ from PySide6 import QtCore from PySide6.QtCore import QEvent -from PySide6.QtGui import QColor, QEnterEvent, QPainter, QPainterPath, QPaintEvent, QPen +from PySide6.QtGui import ( + QColor, + QEnterEvent, + QPainter, + QPainterPath, + QPaintEvent, + QPalette, + QPen, +) from PySide6.QtWidgets import QWidget from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper class ThumbButton(QPushButtonWrapper): - def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None: + def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None: # noqa: N802 super().__init__(parent) self.thumb_size: tuple[int, int] = thumb_size self.hovered = False self.selected = False - # self.clicked.connect(lambda checked: self.set_selected(True)) + self.select_color: QColor = QPalette.color( + self.palette(), + QPalette.ColorGroup.Active, + QPalette.ColorRole.Accent, + ) + + self.select_color_faded: QColor = QColor(self.select_color) + self.select_color_faded.setHsl( + self.select_color_faded.hslHue(), + self.select_color_faded.hslSaturation(), + max(self.select_color_faded.lightness(), 127), + 127, + ) + + self.hover_color: QColor = QPalette.color( + self.palette(), + QPalette.ColorGroup.Active, + QPalette.ColorRole.Accent, + ) + self.hover_color.setHsl( + self.hover_color.hslHue(), + self.hover_color.hslSaturation(), + min(self.hover_color.lightness() + 80, 255), + self.hover_color.alpha(), + ) def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 super().paintEvent(event) @@ -25,7 +57,6 @@ class ThumbButton(QPushButtonWrapper): painter = QPainter() painter.begin(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) - # painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) path = QPainterPath() width = 3 radius = 6 @@ -40,32 +71,24 @@ class ThumbButton(QPushButtonWrapper): radius, ) - # color = QColor('#bb4ff0') if self.selected else QColor('#55bbf6') - # pen = QPen(color, width) - # painter.setPen(pen) - # # brush.setColor(fill) - # painter.drawPath(path) - if self.selected: painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_HardLight) - color = QColor("#bb4ff0") - color.setAlphaF(0.5) - pen = QPen(color, width) + pen = QPen(self.select_color_faded, width) painter.setPen(pen) - painter.fillPath(path, color) + painter.fillPath(path, self.select_color_faded) painter.drawPath(path) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) - color = QColor("#bb4ff0") if not self.hovered else QColor("#55bbf6") + color: QColor = self.select_color if not self.hovered else self.hover_color pen = QPen(color, width) painter.setPen(pen) painter.drawPath(path) elif self.hovered: painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) - color = QColor("#55bbf6") - pen = QPen(color, width) + pen = QPen(self.hover_color, width) painter.setPen(pen) painter.drawPath(path) + painter.end() def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 @@ -78,6 +101,6 @@ class ThumbButton(QPushButtonWrapper): self.repaint() return super().leaveEvent(event) - def set_selected(self, value: bool) -> None: + def set_selected(self, value: bool) -> None: # noqa: N802 self.selected = value self.repaint() diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index ff9a99b0..83eb1c57 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -4,14 +4,21 @@ import math +import struct +from copy import deepcopy +from io import BytesIO from pathlib import Path import cv2 +import numpy as np import rawpy import structlog +from mutagen import MutagenError, flac, id3, mp4 from PIL import ( Image, + ImageChops, ImageDraw, + ImageEnhance, ImageFile, ImageFont, ImageOps, @@ -20,68 +27,842 @@ from PIL import ( ) from PIL.Image import DecompressionBombError from pillow_heif import register_avif_opener, register_heif_opener -from PySide6.QtCore import QObject, QSize, Signal -from PySide6.QtGui import QPixmap -from src.core.constants import ( - IMAGE_TYPES, - PLAINTEXT_TYPES, - RAW_IMAGE_TYPES, - VIDEO_TYPES, -) +from pydub import exceptions +from PySide6.QtCore import QObject, QSize, Qt, Signal +from PySide6.QtGui import QGuiApplication, QPixmap +from src.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT +from src.core.media_types import MediaCategories, MediaType +from src.core.palette import ColorType, UiColor, get_ui_color from src.core.utils.encoding import detect_char_encoding -from src.qt.helpers.gradient import four_corner_gradient_background +from src.qt.helpers.blender_thumbnailer import blend_thumb +from src.qt.helpers.color_overlay import theme_fg_overlay +from src.qt.helpers.file_tester import is_readable_video +from src.qt.helpers.gradient import four_corner_gradient +from src.qt.helpers.text_wrapper import wrap_full_text +from src.qt.helpers.vendored.pydub.audio_segment import ( # type: ignore + _AudioSegment as AudioSegment, +) +from src.qt.resource_manager import ResourceManager +from vtf2img import Parser ImageFile.LOAD_TRUNCATED_IMAGES = True - logger = structlog.get_logger(__name__) - register_heif_opener() register_avif_opener() class ThumbRenderer(QObject): - # finished = Signal() + """A class for rendering image and file thumbnails.""" + + rm: ResourceManager = ResourceManager() updated = Signal(float, QPixmap, QSize, str) updated_ratio = Signal(float) - # updatedImage = Signal(QPixmap) - # updatedSize = Signal(QSize) - thumb_mask_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_mask_512.png" - ) - thumb_mask_512.load() + def __init__(self) -> None: + """Initialize the class.""" + super().__init__() - thumb_mask_hl_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_mask_hl_512.png" - ) - thumb_mask_hl_512.load() + # Cached thumbnail elements. + # Key: Size + Pixel Ratio Tuple + Radius Scale + # (Ex. (512, 512, 1.25, 4)) + self.thumb_masks: dict = {} + self.raised_edges: dict = {} - thumb_loading_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_loading_512.png" - ) - thumb_loading_512.load() + # Key: ("name", UiColor, 512, 512, 1.25) + self.icons: dict = {} - thumb_broken_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_broken_512.png" - ) - thumb_broken_512.load() + def _get_resource_id(self, url: Path) -> str: + """Return the name of the icon resource to use for a file type. - thumb_file_default_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_file_default_512.png" - ) - thumb_file_default_512.load() + Special terms will return special resources. - # thumb_debug: Image.Image = Image.open(Path( - # f'{Path(__file__).parents[2]}/resources/qt/images/temp.jpg')) - # thumb_debug.load() + Args: + url (Path): The file url to assess. "$LOADING" will return the loading graphic. + """ + ext = url.suffix.lower() + types: set[MediaType] = MediaCategories.get_types(ext, mime_fallback=True) - # TODO: Make dynamic font sized given different pixel ratios - font_pixel_ratio: float = 1 - ext_font = ImageFont.truetype( - Path(__file__).parents[3] / "resources/qt/fonts/Oxanium-Bold.ttf", - math.floor(12 * font_pixel_ratio), - ) + # Loop though the specific (non-IANA) categories and return the string + # name of the first matching category found. + for cat in MediaCategories.ALL_CATEGORIES: + if not cat.is_iana and cat.media_type in types: + return cat.media_type.value + + # If the type is broader (IANA registered) then search those types. + for cat in MediaCategories.ALL_CATEGORIES: + if cat.is_iana and cat.media_type in types: + return cat.media_type.value + + return "file_generic" + + def _get_mask( + self, size: tuple[int, int], pixel_ratio: float, scale_radius: bool = False + ) -> Image.Image: + """Return a thumbnail mask given a size, pixel ratio, and radius scaling option. + + If one is not already cached, a new one will be rendered. + + Args: + size (tuple[int, int]): The size of the graphic. + pixel_ratio (float): The screen pixel ratio. + scale_radius (bool): Option to scale the radius up (Used for Preview Panel). + """ + thumb_scale: int = 512 + radius_scale: float = 1 + if scale_radius: + radius_scale = max(size[0], size[1]) / thumb_scale + + item: Image.Image = self.thumb_masks.get((*size, pixel_ratio, radius_scale)) + if not item: + item = self._render_mask(size, pixel_ratio, radius_scale) + self.thumb_masks[(*size, pixel_ratio, radius_scale)] = item + return item + + def _get_edge( + self, size: tuple[int, int], pixel_ratio: float + ) -> tuple[Image.Image, Image.Image]: + """Return a thumbnail edge given a size, pixel ratio, and radius scaling option. + + If one is not already cached, a new one will be rendered. + + Args: + size (tuple[int, int]): The size of the graphic. + pixel_ratio (float): The screen pixel ratio. + """ + item: tuple[Image.Image, Image.Image] = self.raised_edges.get((*size, pixel_ratio)) + if not item: + item = self._render_edge(size, pixel_ratio) + self.raised_edges[(*size, pixel_ratio)] = item + return item + + def _get_icon( + self, name: str, color: UiColor, size: tuple[int, int], pixel_ratio: float = 1.0 + ) -> Image.Image: + """Return an icon given a size, pixel ratio, and radius scaling option. + + Args: + name (str): The name of the icon resource. "thumb_loading" will not draw a border. + color (str): The color to use for the icon. + size (tuple[int,int]): The size of the icon. + pixel_ratio (float): The screen pixel ratio. + """ + draw_border: bool = True + if name == "thumb_loading": + draw_border = False + + item: Image.Image = self.icons.get((name, color, *size, pixel_ratio)) + if not item: + item_flat: Image.Image = self._render_icon(name, color, size, pixel_ratio, draw_border) + edge: tuple[Image.Image, Image.Image] = self._get_edge(size, pixel_ratio) + item = self._apply_edge(item_flat, edge, faded=True) + self.icons[(name, color, *size, pixel_ratio)] = item + return item + + def _render_mask( + self, size: tuple[int, int], pixel_ratio: float, radius_scale: float = 1 + ) -> Image.Image: + """Render a thumbnail mask graphic. + + Args: + size (tuple[int,int]): The size of the graphic. + pixel_ratio (float): The screen pixel ratio. + radius_scale (float): The scale factor of the border radius (Used by Preview Panel). + """ + smooth_factor: int = 2 + radius_factor: int = 8 + + im: Image.Image = Image.new( + mode="L", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="black", + ) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle( + (0, 0) + tuple([d - 1 for d in im.size]), + radius=math.ceil(radius_factor * smooth_factor * pixel_ratio * radius_scale), + fill="white", + ) + im = im.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + return im + + def _render_edge( + self, size: tuple[int, int], pixel_ratio: float + ) -> tuple[Image.Image, Image.Image]: + """Render a thumbnail edge graphic. + + Args: + size (tuple[int,int]): The size of the graphic. + pixel_ratio (float): The screen pixel ratio. + """ + smooth_factor: int = 2 + radius_factor: int = 8 + width: int = math.floor(pixel_ratio * 2) + + # Highlight + im_hl: Image.Image = Image.new( + mode="RGBA", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="#00000000", + ) + draw = ImageDraw.Draw(im_hl) + draw.rounded_rectangle( + (width, width) + tuple([d - (width + 1) for d in im_hl.size]), + radius=math.ceil((radius_factor * smooth_factor * pixel_ratio) - (pixel_ratio * 3)), + fill=None, + outline="white", + width=width, + ) + im_hl = im_hl.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + + # Shadow + im_sh: Image.Image = Image.new( + mode="RGBA", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="#00000000", + ) + draw = ImageDraw.Draw(im_sh) + draw.rounded_rectangle( + (0, 0) + tuple([d - 1 for d in im_sh.size]), + radius=math.ceil(radius_factor * smooth_factor * pixel_ratio), + fill=None, + outline="black", + width=width, + ) + im_sh = im_sh.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + + return (im_hl, im_sh) + + def _render_icon( + self, + name: str, + color: UiColor, + size: tuple[int, int], + pixel_ratio: float, + draw_border: bool = True, + ) -> Image.Image: + """Render a thumbnail icon. + + Args: + name (str): The name of the icon resource. + color (UiColor): The color to use for the icon. + size (tuple[int,int]): The size of the icon. + pixel_ratio (float): The screen pixel ratio. + draw_border (bool): Option to draw a border. + """ + border_factor: int = 5 + smooth_factor: int = math.ceil(2 * pixel_ratio) + radius_factor: int = 8 + icon_ratio: float = 1.75 + + # Create larger blank image based on smooth_factor + im: Image.Image = Image.new( + "RGBA", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="#00000000", + ) + + # Create solid background color + bg: Image.Image = Image.new( + "RGB", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="#000000", + ) + + # Paste background color with rounded rectangle mask onto blank image + im.paste( + bg, + (0, 0), + mask=self._get_mask( + tuple([d * smooth_factor for d in size]), # type: ignore + (pixel_ratio * smooth_factor), + ), + ) + + # Draw rounded rectangle border + if draw_border: + draw = ImageDraw.Draw(im) + draw.rounded_rectangle( + (0, 0) + tuple([d - 1 for d in im.size]), + radius=math.ceil( + (radius_factor * smooth_factor * pixel_ratio) + (pixel_ratio * 1.5) + ), + fill="black", + outline="#FF0000", + width=math.floor( + (border_factor * smooth_factor * pixel_ratio) - (pixel_ratio * 1.5) + ), + ) + + # Resize image to final size + im = im.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + fg: Image.Image = Image.new( + "RGB", + size=size, + color="#00FF00", + ) + + # Get icon by name + icon: Image.Image = self.rm.get(name) + if not icon: + icon = self.rm.get("file_generic") + if not icon: + icon = Image.new(mode="RGBA", size=(32, 32), color="magenta") + + # Resize icon to fit icon_ratio + icon = icon.resize((math.ceil(size[0] // icon_ratio), math.ceil(size[1] // icon_ratio))) + + # Paste icon centered + im.paste( + im=fg.resize((math.ceil(size[0] // icon_ratio), math.ceil(size[1] // icon_ratio))), + box=( + math.ceil((size[0] - (size[0] // icon_ratio)) // 2), + math.ceil((size[1] - (size[1] // icon_ratio)) // 2), + ), + mask=icon.getchannel(3), + ) + + # Apply color overlay + im = self._apply_overlay_color( + im, + color, + ) + + return im + + def _apply_overlay_color(self, image: Image.Image, color: UiColor) -> Image.Image: + """Apply a color overlay effect to an image based on its color channel data. + + Red channel for foreground, green channel for outline, none for background. + + Args: + image (Image.Image): The image to apply an overlay to. + color (UiColor): The name of the ColorType color to use. + """ + bg_color: str = ( + get_ui_color(ColorType.DARK_ACCENT, color) + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else get_ui_color(ColorType.PRIMARY, color) + ) + fg_color: str = ( + get_ui_color(ColorType.PRIMARY, color) + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else get_ui_color(ColorType.LIGHT_ACCENT, color) + ) + ol_color: str = ( + get_ui_color(ColorType.BORDER, color) + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else get_ui_color(ColorType.LIGHT_ACCENT, color) + ) + + bg: Image.Image = Image.new(image.mode, image.size, color=bg_color) + fg: Image.Image = Image.new(image.mode, image.size, color=fg_color) + ol: Image.Image = Image.new(image.mode, image.size, color=ol_color) + + bg.paste(fg, (0, 0), mask=image.getchannel(0)) + bg.paste(ol, (0, 0), mask=image.getchannel(1)) + + if image.mode == "RGBA": + alpha_bg: Image.Image = bg.copy() + alpha_bg.convert("RGBA") + alpha_bg.putalpha(0) + alpha_bg.paste(bg, (0, 0), mask=image.getchannel(3)) + bg = alpha_bg + + return bg + + def _apply_edge( + self, + image: Image.Image, + edge: tuple[Image.Image, Image.Image], + faded: bool = False, + ): + """Apply a given edge effect to an image. + + Args: + image (Image.Image): The image to apply the edge to. + edge (tuple[Image.Image, Image.Image]): The edge images to apply. + Item 0 is the inner highlight, and item 1 is the outer shadow. + faded (bool): Whether or not to apply a faded version of the edge. + Used for light themes. + """ + opacity: float = 1.0 if not faded else 0.8 + shade_reduction: float = ( + 0 if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark else 0.3 + ) + im: Image.Image = image + im_hl, im_sh = deepcopy(edge) + + # Configure and apply a soft light overlay. + # This makes up the bulk of the effect. + im_hl.putalpha(ImageEnhance.Brightness(im_hl.getchannel(3)).enhance(opacity)) + im.paste(ImageChops.soft_light(im, im_hl), mask=im_hl.getchannel(3)) + + # Configure and apply a normal shading overlay. + # This helps with contrast. + im_sh.putalpha( + ImageEnhance.Brightness(im_sh.getchannel(3)).enhance(max(0, opacity - shade_reduction)) + ) + im.paste(im_sh, mask=im_sh.getchannel(3)) + + return im + + def _audio_album_thumb(self, filepath: Path, ext: str) -> Image.Image | None: + """Return an album cover thumb from an audio file if a cover is present. + + Args: + filepath (Path): The path of the file. + ext (str): The file extension (with leading "."). + """ + image: Image.Image = None + try: + if not filepath.is_file(): + raise FileNotFoundError + + artwork = None + if ext in [".mp3"]: + id3_tags: id3.ID3 = id3.ID3(filepath) + id3_covers: list = id3_tags.getall("APIC") + if id3_covers: + artwork = Image.open(BytesIO(id3_covers[0].data)) + elif ext in [".flac"]: + flac_tags: flac.FLAC = flac.FLAC(filepath) + flac_covers: list = flac_tags.pictures + if flac_covers: + artwork = Image.open(BytesIO(flac_covers[0].data)) + elif ext in [".mp4", ".m4a", ".aac"]: + mp4_tags: mp4.MP4 = mp4.MP4(filepath) + mp4_covers: list = mp4_tags.get("covr") + if mp4_covers: + artwork = Image.open(BytesIO(mp4_covers[0])) + if artwork: + image = artwork + except ( + mp4.MP4MetadataError, + mp4.MP4StreamInfoError, + id3.ID3NoHeaderError, + MutagenError, + ) as e: + logger.error("Couldn't read album artwork", path=filepath, error=e) + return image + + def _audio_waveform_thumb( + self, filepath: Path, ext: str, size: int, pixel_ratio: float + ) -> Image.Image | None: + """Render a waveform image from an audio file. + + Args: + filepath (Path): The path of the file. + ext (str): The file extension (with leading "."). + size (tuple[int,int]): The size of the thumbnail. + pixel_ratio (float): The screen pixel ratio. + """ + # BASE_SCALE used for drawing on a larger image and resampling down + # to provide an antialiased effect. + base_scale: int = 2 + samples_per_bar: int = 3 + size_scaled: int = size * base_scale + allow_small_min: bool = False + im: Image.Image = None + + try: + bar_count: int = min(math.floor((size // pixel_ratio) / 5), 64) + audio: AudioSegment = AudioSegment.from_file(filepath, ext[1:]) + data = np.fromstring(audio._data, np.int16) # type: ignore + data_indices = np.linspace(1, len(data), num=bar_count * samples_per_bar) + bar_margin: float = ((size_scaled / (bar_count * 3)) * base_scale) / 2 + line_width: float = ((size_scaled - bar_margin) / (bar_count * 3)) * base_scale + bar_height: float = (size_scaled) - (size_scaled // bar_margin) + + count: int = 0 + maximum_item: int = 0 + max_array: list = [] + highest_line: int = 0 + + for i in range(-1, len(data_indices)): + d = data[math.ceil(data_indices[i]) - 1] + if count < samples_per_bar: + count = count + 1 + if abs(d) > maximum_item: + maximum_item = abs(d) + else: + max_array.append(maximum_item) + + if maximum_item > highest_line: + highest_line = maximum_item + + maximum_item = 0 + count = 1 + + line_ratio = max(highest_line / bar_height, 1) + + im = Image.new("RGB", (size_scaled, size_scaled), color="#000000") + draw = ImageDraw.Draw(im) + + current_x = bar_margin + for item in max_array: + item_height = item / line_ratio + + # If small minimums are not allowed, raise all values + # smaller than the line width to the same value. + if not allow_small_min: + item_height = max(item_height, line_width) + + current_y = (bar_height - item_height + (size_scaled // bar_margin)) // 2 + + draw.rounded_rectangle( + ( + current_x, + current_y, + (current_x + line_width), + (current_y + item_height), + ), + radius=100 * base_scale, + fill=("#FF0000"), + outline=("#FFFF00"), + width=max(math.ceil(line_width / 6), base_scale), + ) + + current_x = current_x + line_width + bar_margin + + im.resize((size, size), Image.Resampling.BILINEAR) + + except exceptions.CouldntDecodeError as e: + logger.error("Couldn't render waveform", path=filepath.name, error=e) + + return im + + def _blender(self, filepath: Path) -> Image.Image: + """Get an emended thumbnail from a Blender file, if a thumbnail is present. + + Args: + filepath (Path): The path of the file. + """ + bg_color: str = ( + "#1e1e1e" + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else "#FFFFFF" + ) + im: Image.Image = None + try: + blend_image = blend_thumb(str(filepath)) + + bg = Image.new("RGB", blend_image.size, color=bg_color) + bg.paste(blend_image, mask=blend_image.getchannel(3)) + im = bg + + except ( + AttributeError, + UnidentifiedImageError, + FileNotFoundError, + TypeError, + ) as e: + if str(e) == "expected string or buffer": + logger.info( + f"[ThumbRenderer][BLENDER][INFO] {filepath.name} " + f"Doesn't have an embedded thumbnail. ({type(e).__name__})" + ) + + else: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _source_engine(self, filepath: Path) -> Image.Image: + """This is a function to convert the VTF (Valve Texture Format) files to thumbnails. + + It works using the VTF2IMG library for PILLOW. + """ + parser = Parser(filepath) + im: Image.Image = None + try: + im = parser.get_image() + + except ( + AttributeError, + UnidentifiedImageError, + FileNotFoundError, + TypeError, + struct.error, + ) as e: + if str(e) == "expected string or buffer": + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + + else: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _font_short_thumb(self, filepath: Path, size: int) -> Image.Image: + """Render a small font preview ("Aa") thumbnail from a font file. + + Args: + filepath (Path): The path of the file. + size (tuple[int,int]): The size of the thumbnail. + """ + im: Image.Image = None + try: + bg = Image.new("RGB", (size, size), color="#000000") + raw = Image.new("RGB", (size * 3, size * 3), color="#000000") + draw = ImageDraw.Draw(raw) + font = ImageFont.truetype(filepath, size=size) + # NOTE: While a stroke effect is desired, the text + # method only allows for outer strokes, which looks + # a bit weird when rendering fonts. + draw.text( + (size // 8, size // 8), + "Aa", + font=font, + fill="#FF0000", + # stroke_width=math.ceil(size / 96), + # stroke_fill="#FFFF00", + ) + # NOTE: Change to getchannel(1) if using an outline. + data = np.asarray(raw.getchannel(0)) + + m, n = data.shape[:2] + col: np.ndarray = data.any(0) + row: np.ndarray = data.any(1) + cropped_data = np.asarray(raw)[ + row.argmax() : m - row[::-1].argmax(), + col.argmax() : n - col[::-1].argmax(), + ] + cropped_im: Image.Image = Image.fromarray(cropped_data, "RGB") + + margin: int = math.ceil(size // 16) + + orig_x, orig_y = cropped_im.size + new_x, new_y = (size, size) + if orig_x > orig_y: + new_x = size + new_y = math.ceil(size * (orig_y / orig_x)) + elif orig_y > orig_x: + new_y = size + new_x = math.ceil(size * (orig_x / orig_y)) + + cropped_im = cropped_im.resize( + size=(new_x - (margin * 2), new_y - (margin * 2)), + resample=Image.Resampling.BILINEAR, + ) + bg.paste( + cropped_im, + box=(margin, margin + ((size - new_y) // 2)), + ) + im = self._apply_overlay_color(bg, UiColor.PURPLE) + except OSError as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _font_long_thumb(self, filepath: Path, size: int) -> Image.Image: + """Render a large font preview ("Alphabet") thumbnail from a font file. + + Args: + filepath (Path): The path of the file. + size (tuple[int,int]): The size of the thumbnail. + """ + # Scale the sample font sizes to the preview image + # resolution,assuming the sizes are tuned for 256px. + im: Image.Image = None + try: + scaled_sizes: list[int] = [math.floor(x * (size / 256)) for x in FONT_SAMPLE_SIZES] + bg = Image.new("RGBA", (size, size), color="#00000000") + draw = ImageDraw.Draw(bg) + lines_of_padding = 2 + y_offset = 0 + + for font_size in scaled_sizes: + font = ImageFont.truetype(filepath, size=font_size) + text_wrapped: str = wrap_full_text( + FONT_SAMPLE_TEXT, font=font, width=size, draw=draw + ) + draw.multiline_text((0, y_offset), text_wrapped, font=font) + y_offset += (len(text_wrapped.split("\n")) + lines_of_padding) * draw.textbbox( + (0, 0), "A", font=font + )[-1] + im = theme_fg_overlay(bg, use_alpha=False) + except OSError as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _image_raw_thumb(self, filepath: Path) -> Image.Image: + """Render a thumbnail for a RAW image type. + + Args: + filepath (Path): The path of the file. + """ + im: Image.Image = None + try: + with rawpy.imread(str(filepath)) as raw: + rgb = raw.postprocess() + im = Image.frombytes( + "RGB", + (rgb.shape[1], rgb.shape[0]), + rgb, + decoder_name="raw", + ) + except DecompressionBombError as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + except ( + rawpy._rawpy.LibRawIOError, + rawpy._rawpy.LibRawFileUnsupportedError, + ) as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _image_thumb(self, filepath: Path) -> Image.Image: + """Render a thumbnail for a standard image type. + + Args: + filepath (Path): The path of the file. + """ + im: Image.Image = None + try: + im = Image.open(filepath) + if im.mode != "RGB" and im.mode != "RGBA": + im = im.convert(mode="RGBA") + if im.mode == "RGBA": + new_bg = Image.new("RGB", im.size, color="#1e1e1e") + new_bg.paste(im, mask=im.getchannel(3)) + im = new_bg + + im = ImageOps.exif_transpose(im) + except ( + UnidentifiedImageError, + DecompressionBombError, + ) as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _image_vector_thumb(self, filepath: Path, size: int) -> Image.Image: + """Render a thumbnail for a vector image, such as SVG. + + Args: + filepath (Path): The path of the file. + size (tuple[int,int]): The size of the thumbnail. + """ + # TODO: Implement. + im: Image.Image = None + return im + + def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image: + """Render a thumbnail for an STL file. + + Args: + filepath (Path): The path of the file. + size (tuple[int,int]): The size of the icon. + """ + # TODO: Implement. + # The following commented code describes a method for rendering via + # matplotlib. + # This implementation did not play nice with multithreading. + im: Image.Image = None + # # Create a new plot + # matplotlib.use('agg') + # figure = plt.figure() + # axes = figure.add_subplot(projection='3d') + + # # Load the STL files and add the vectors to the plot + # your_mesh = mesh.Mesh.from_file(_filepath) + + # poly_collection = mplot3d.art3d.Poly3DCollection(your_mesh.vectors) + # poly_collection.set_color((0,0,1)) # play with color + # scale = your_mesh.points.flatten() + # axes.auto_scale_xyz(scale, scale, scale) + # axes.add_collection3d(poly_collection) + # # plt.show() + # img_buf = io.BytesIO() + # plt.savefig(img_buf, format='png') + # im = Image.open(img_buf) + + return im + + def _text_thumb(self, filepath: Path) -> Image.Image: + """Render a thumbnail for a plaintext file. + + Args: + filepath (Path): The path of the file. + """ + im: Image.Image = None + + bg_color: str = ( + "#1e1e1e" + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else "#FFFFFF" + ) + fg_color: str = ( + "#FFFFFF" + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else "#111111" + ) + + try: + encoding = detect_char_encoding(filepath) + with open(filepath, encoding=encoding) as text_file: + text = text_file.read(256) + bg = Image.new("RGB", (256, 256), color=bg_color) + draw = ImageDraw.Draw(bg) + draw.text((16, 16), text, fill=fg_color) + im = bg + except ( + UnidentifiedImageError, + cv2.error, + DecompressionBombError, + UnicodeDecodeError, + OSError, + ) as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im + + def _video_thumb(self, filepath: Path) -> Image.Image: + """Render a thumbnail for a video file. + + Args: + filepath (Path): The path of the file. + """ + im: Image.Image = None + try: + if is_readable_video(filepath): + video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG) + # TODO: Move this check to is_readable_video() + if video.get(cv2.CAP_PROP_FRAME_COUNT) <= 0: + raise cv2.error("File is invalid or has 0 frames") + video.set( + cv2.CAP_PROP_POS_FRAMES, + (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2), + ) + # NOTE: Depending on the video format, compression, and + # frame count, seeking halfway does not work and the thumb + # must be pulled from the earliest available frame. + max_frame_seek: int = 10 + for i in range( + 0, + min(max_frame_seek, math.floor(video.get(cv2.CAP_PROP_FRAME_COUNT))), + ): + success, frame = video.read() + if not success: + video.set(cv2.CAP_PROP_POS_FRAMES, i) + else: + break + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + im = Image.fromarray(frame) + except ( + UnidentifiedImageError, + cv2.error, + DecompressionBombError, + OSError, + ) as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + return im def render( self, @@ -90,29 +871,42 @@ class ThumbRenderer(QObject): base_size: tuple[int, int], pixel_ratio: float, is_loading: bool = False, - gradient: bool = False, + is_grid_thumb: bool = False, update_on_ratio_change: bool = False, ): - """Internal renderer. Render an entry/element thumbnail for the GUI.""" - logger.debug("rendering thumbnail", path=filepath) + """Render a thumbnail or preview image. + Args: + timestamp (float): The timestamp for which this this job was dispatched. + filepath (str | Path): The path of the file to render a thumbnail for. + base_size (tuple[int,int]): The unmodified base size of the thumbnail. + pixel_ratio (float): The screen pixel ratio. + is_loading (bool): Is this a loading graphic? + is_grid_thumb (bool): Is this a thumbnail for the thumbnail grid? + Or else the Preview Pane? + update_on_ratio_change (bool): Should an updated ratio signal be sent? + + """ + adj_size = math.ceil(max(base_size[0], base_size[1]) * pixel_ratio) image: Image.Image = None pixmap: QPixmap = None final: Image.Image = None _filepath: Path = Path(filepath) resampling_method = Image.Resampling.BILINEAR - if ThumbRenderer.font_pixel_ratio != pixel_ratio: - ThumbRenderer.font_pixel_ratio = pixel_ratio - ThumbRenderer.ext_font = ImageFont.truetype( - Path(__file__).parents[3] / "resources/qt/fonts/Oxanium-Bold.ttf", - math.floor(12 * ThumbRenderer.font_pixel_ratio), - ) - adj_size = math.ceil(max(base_size[0], base_size[1]) * pixel_ratio) + theme_color: UiColor = ( + UiColor.THEME_LIGHT + if QGuiApplication.styleHints().colorScheme() == Qt.ColorScheme.Light + else UiColor.THEME_DARK + ) + + # Initialize "Loading" thumbnail + loading_thumb: Image.Image = self._get_icon( + "thumb_loading", theme_color, (adj_size, adj_size), pixel_ratio + ) + if is_loading: - final = ThumbRenderer.thumb_loading_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR - ) + final = loading_thumb.resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR) qim = ImageQt.ImageQt(final) pixmap = QPixmap.fromImage(qim) pixmap.setDevicePixelRatio(pixel_ratio) @@ -120,92 +914,69 @@ class ThumbRenderer(QObject): self.updated_ratio.emit(1) elif _filepath: try: + ext: str = _filepath.suffix.lower() # Images ======================================================= - if _filepath.suffix.lower() in IMAGE_TYPES: - try: - image = Image.open(_filepath) - if image.mode != "RGB" and image.mode != "RGBA": - image = image.convert(mode="RGBA") - if image.mode == "RGBA": - new_bg = Image.new("RGB", image.size, color="#1e1e1e") - new_bg.paste(image, mask=image.getchannel(3)) - image = new_bg - - image = ImageOps.exif_transpose(image) - except DecompressionBombError as e: - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) - - elif _filepath.suffix.lower() in RAW_IMAGE_TYPES: - try: - with rawpy.imread(str(_filepath)) as raw: - rgb = raw.postprocess() - image = Image.frombytes( - "RGB", - (rgb.shape[1], rgb.shape[0]), - rgb, - decoder_name="raw", - ) - except DecompressionBombError as e: - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) - - except ( - rawpy._rawpy.LibRawIOError, - rawpy._rawpy.LibRawFileUnsupportedError, - ) as e: - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) - + if MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_TYPES, mime_fallback=True + ): + # Raw Images ----------------------------------------------- + if MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True + ): + image = self._image_raw_thumb(_filepath) + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True + ): + image = self._image_vector_thumb(_filepath, adj_size) + # Normal Images -------------------------------------------- + else: + image = self._image_thumb(_filepath) # Videos ======================================================= - elif _filepath.suffix.lower() in VIDEO_TYPES: - video = cv2.VideoCapture(str(_filepath)) - frame_count = video.get(cv2.CAP_PROP_FRAME_COUNT) - if frame_count <= 0: - raise cv2.error("File is invalid or has 0 frames") - video.set(cv2.CAP_PROP_POS_FRAMES, frame_count // 2) - success, frame = video.read() - if not success: - # Depending on the video format, compression, and frame - # count, seeking halfway does not work and the thumb - # must be pulled from the earliest available frame. - video.set(cv2.CAP_PROP_POS_FRAMES, 0) - success, frame = video.read() - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - image = Image.fromarray(frame) - + if MediaCategories.is_ext_in_category( + ext, MediaCategories.VIDEO_TYPES, mime_fallback=True + ): + image = self._video_thumb(_filepath) # Plain Text =================================================== - elif _filepath.suffix.lower() in PLAINTEXT_TYPES: - encoding = detect_char_encoding(_filepath) - with open(_filepath, encoding=encoding) as text_file: - text = text_file.read(256) - bg = Image.new("RGB", (256, 256), color="#1e1e1e") - draw = ImageDraw.Draw(bg) - draw.text((16, 16), text, file=(255, 255, 255)) - image = bg - # 3D =========================================================== - # elif extension == 'stl': - # # Create a new plot - # matplotlib.use('agg') - # figure = plt.figure() - # axes = figure.add_subplot(projection='3d') + if MediaCategories.is_ext_in_category( + ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True + ): + image = self._text_thumb(_filepath) + # Fonts ======================================================== + if MediaCategories.is_ext_in_category( + ext, MediaCategories.FONT_TYPES, mime_fallback=True + ): + if is_grid_thumb: + # Short (Aa) Preview + image = self._font_short_thumb(_filepath, adj_size) + else: + # Large (Full Alphabet) Preview + image = self._font_long_thumb(_filepath, adj_size) + # Audio ======================================================== + if MediaCategories.is_ext_in_category( + ext, MediaCategories.AUDIO_TYPES, mime_fallback=True + ): + image = self._audio_album_thumb(_filepath, ext) + if image is None: + image = self._audio_waveform_thumb(_filepath, ext, adj_size, pixel_ratio) + if image is not None: + image = self._apply_overlay_color(image, UiColor.GREEN) - # # Load the STL files and add the vectors to the plot - # your_mesh = mesh.Mesh.from_file(_filepath) + # Blender =========================================================== + if MediaCategories.is_ext_in_category( + ext, MediaCategories.BLENDER_TYPES, mime_fallback=True + ): + image = self._blender(_filepath) + + # VTF ========================================================== + if MediaCategories.is_ext_in_category( + ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True + ): + image = self._source_engine(_filepath) - # poly_collection = mplot3d.art3d.Poly3DCollection(your_mesh.vectors) - # poly_collection.set_color((0,0,1)) # play with color - # scale = your_mesh.points.flatten() - # axes.auto_scale_xyz(scale, scale, scale) - # axes.add_collection3d(poly_collection) - # # plt.show() - # img_buf = io.BytesIO() - # plt.savefig(img_buf, format='png') - # image = Image.open(img_buf) # No Rendered Thumbnail ======================================== - else: - image = ThumbRenderer.thumb_file_default_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR - ) - - if not image: + if not _filepath.exists(): + raise FileNotFoundError + elif not image: raise UnidentifiedImageError orig_x, orig_y = image.size @@ -227,47 +998,46 @@ class ThumbRenderer(QObject): else Image.Resampling.BILINEAR ) image = image.resize((new_x, new_y), resample=resampling_method) - if gradient: - mask: Image.Image = ThumbRenderer.thumb_mask_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR - ).getchannel(3) - hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR + mask: Image.Image = None + if is_grid_thumb: + mask = self._get_mask((adj_size, adj_size), pixel_ratio) + edge: tuple[Image.Image, Image.Image] = self._get_edge( + (adj_size, adj_size), pixel_ratio + ) + final = self._apply_edge( + four_corner_gradient(image, (adj_size, adj_size), mask), + edge, ) - final = four_corner_gradient_background(image, adj_size, mask, hl) else: - scalar = 4 - rec: Image.Image = Image.new( - "RGB", - tuple([d * scalar for d in image.size]), # type: ignore - "black", - ) - draw = ImageDraw.Draw(rec) - draw.rounded_rectangle( - (0, 0) + rec.size, - (base_size[0] // 32) * scalar * pixel_ratio, - fill="red", - ) - rec = rec.resize( - tuple([d // scalar for d in rec.size]), - resample=Image.Resampling.BILINEAR, - ) + mask = self._get_mask(image.size, pixel_ratio, scale_radius=True) final = Image.new("RGBA", image.size, (0, 0, 0, 0)) - final.paste(image, mask=rec.getchannel(0)) + final.paste(image, mask=mask.getchannel(0)) + + except FileNotFoundError as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + if update_on_ratio_change: + self.updated_ratio.emit(1) + final = self._get_icon( + name="broken_link_icon", + color=UiColor.RED, + size=(adj_size, adj_size), + pixel_ratio=pixel_ratio, + ) except ( UnidentifiedImageError, - FileNotFoundError, - cv2.error, DecompressionBombError, - UnicodeDecodeError, + ValueError, + ChildProcessError, ) as e: - if e is not UnicodeDecodeError: - logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) if update_on_ratio_change: self.updated_ratio.emit(1) - final = ThumbRenderer.thumb_broken_512.resize( - (adj_size, adj_size), resample=resampling_method + final = self._get_icon( + name=self._get_resource_id(_filepath), + color=theme_color, + size=(adj_size, adj_size), + pixel_ratio=pixel_ratio, ) qim = ImageQt.ImageQt(final) if image: diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index e1a4c141..2b4434b1 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -30,6 +30,7 @@ from PySide6.QtSvgWidgets import QSvgWidget from PySide6.QtWidgets import QGraphicsScene, QGraphicsView from src.core.enums import SettingItems from src.qt.helpers.file_opener import FileOpenerHelper +from src.qt.platform_strings import PlatformStrings if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -75,6 +76,8 @@ class VideoPlayer(QGraphicsView): self.scene().addItem(self.video_preview) self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) + self.setStyleSheet("border-style:solid;border-width:0px;") + # Set up the video tint. self.video_tint = self.scene().addRect( 0, @@ -116,14 +119,16 @@ class VideoPlayer(QGraphicsView): autoplay_action.setCheckable(True) self.addAction(autoplay_action) autoplay_action.setChecked( - bool(self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool)) + self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool) # type: ignore ) autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action open_file_action = QAction("Open file", self) open_file_action.triggered.connect(self.opener.open_file) - open_explorer_action = QAction("Open file in explorer", self) + + open_explorer_action = QAction(PlatformStrings.open_file_str, self) + open_explorer_action.triggered.connect(self.opener.open_explorer) self.addAction(open_file_action) self.addAction(open_explorer_action) diff --git a/tagstudio/tests/qt/test_preview_panel.py b/tagstudio/tests/qt/test_preview_panel.py index 3b612e32..f8550b86 100644 --- a/tagstudio/tests/qt/test_preview_panel.py +++ b/tagstudio/tests/qt/test_preview_panel.py @@ -16,7 +16,7 @@ def test_update_widgets_not_selected(qt_driver, library): panel.update_widgets() assert panel.preview_img.isVisible() - assert panel.file_label.text() == "No Items Selected" + assert panel.file_label.text() == "No Items Selected" @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)