From 0f2a4c1ae5d8492280af126dc7fda9eeef9b9d50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Feb 2023 19:19:17 +1100 Subject: [PATCH 01/25] Added "corners" argument to rounded_rectangle() --- ...agedraw_rounded_rectangle_corners_nnnn.png | Bin 0 -> 544 bytes ...agedraw_rounded_rectangle_corners_nnny.png | Bin 0 -> 685 bytes ...agedraw_rounded_rectangle_corners_nnyn.png | Bin 0 -> 649 bytes ...agedraw_rounded_rectangle_corners_nnyy.png | Bin 0 -> 755 bytes ...agedraw_rounded_rectangle_corners_nynn.png | Bin 0 -> 643 bytes ...agedraw_rounded_rectangle_corners_nyny.png | Bin 0 -> 775 bytes ...agedraw_rounded_rectangle_corners_nyyn.png | Bin 0 -> 741 bytes ...agedraw_rounded_rectangle_corners_nyyy.png | Bin 0 -> 844 bytes ...agedraw_rounded_rectangle_corners_ynnn.png | Bin 0 -> 656 bytes ...agedraw_rounded_rectangle_corners_ynny.png | Bin 0 -> 785 bytes ...agedraw_rounded_rectangle_corners_ynyn.png | Bin 0 -> 752 bytes ...agedraw_rounded_rectangle_corners_ynyy.png | Bin 0 -> 856 bytes ...agedraw_rounded_rectangle_corners_yynn.png | Bin 0 -> 737 bytes ...agedraw_rounded_rectangle_corners_yyny.png | Bin 0 -> 870 bytes ...agedraw_rounded_rectangle_corners_yyyn.png | Bin 0 -> 835 bytes ...agedraw_rounded_rectangle_corners_yyyy.png | Bin 0 -> 934 bytes Tests/test_imagedraw.py | 30 +++++ docs/reference/ImageDraw.rst | 2 + src/PIL/ImageDraw.py | 106 ++++++++++++------ 19 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 0000000000000000000000000000000000000000..3e79e21aedb620a6d057f927610e1f1a791fd5da GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV0`ZB;uumf=j|0kzC#W?3d&6b&3+Jg_Su>JzjK?ym2`v- zDJkj*<%_Sao1Z3b)A4uC?n|AnP7x|CqXb1^;MZ!(KL6Y1T^!TqfCBT;qYZc3<~;7% zpmv|-{P9qaM|(CrdocO-gvmhJbv&mR9xY67H$BamTX;lO{hAK@mZM_tBUJQwvZpcg z>4wGDC7msDh^{HW8ftO6@$ZRl=Ii^C&z_pG?Z=nq>E9zIFN?|7eOp}+b}Xj*P1)|h zy!(>-eD}%EeY#Pnzb7*K%f?S;{CDaOsJGfS?rtxAWoWbf=A4}`z-oFP+I6(Ttoqx>dPD}f)_xZ95B<$(x=d#Wz Gp$P!^IRgOz literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 0000000000000000000000000000000000000000..d825ad2631717dc05270f8c211bae1793ce1c256 GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&YbaSW-L^Y-S&pv49Z4i~R{ zJpB3juLnDJYq741xqiwrUsdwNVOgdU9-tN&_%TyY`tfILv4eYOg}ck`%NLx}xxs^@ zyJdri%{`OfnP#gW_{BWCH}hSDN{g%0C_!NuyeXZTUw?k7=XywB9@TsQ{h;olN5RjI z?Er;(uK8a6eF9>fi5nkYJN`}lP|{haQ2BlxL9y`KU8@vzlny1`49^R9j?np)6q?*~ z-1*wU^&9{2?Wic2YAseT8+uhhtX?MK>IPAxFuTS$>wV1|&SgKI{Oi{XvBbIO*6q*u zb|!MaS1seaGd9mmAKNWxb|y(>j|^&kMy5PUPrr z+2Fy^edK;dSP5 z4k>N4((yl>994Z^yk%>VQoBK(%y$iyjXG;SKApV$;l(w-sy|GY?LKmBLr&E3jc?=h z`CcdO+SZrwOfSC9IQ?v2sbOIB{@c%1slAVI32QUmSD$$K*V=bC*DcevzIL=E_pe=f zn5pmO%*UsSPuHx8nJsRz-}g*i<<)EDYj#e}iG829e$}$ward5IyL|2Ut&OWPe=q)U z?T}n|{*&WxA1r@A{mh+zms3w`ze&AYu_wm9s`y9j{nX4Xyte+pcH?+&cbiHAvXg)z4*}Q$iB}g)$Z3 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e95d487498b4dec519e1b2e3745d5d1bda62f2 GIT binary patch literal 643 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVDj>GaSW-L^Y-RJze5fp4uJ-B zKmJ(e`LN_>a_{Y^IJe1oW@6!IjfVw59Wc=Fyd)+}(xUT@FYN7BUz>5O#4|#r<*4tmL%TLaq`ubP1CnXk z5RrX3-#KxjfEec}K~WfVe)rwozxb!f9vNX^u!6%DmLh<3MVS20FPfX2{qNa=L_J;o KT-G@yGywp+4$-0j literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 0000000000000000000000000000000000000000..274d27984dcb7633223d2557d9d3cb44f109f99f GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU^?gN;uumf=k3jdMT;Cn90KR+ zzqc##~$aFue*C+9jjj@ z{aLwZm9B6%|FOh!qm4fjWXhJ;3coJ8bFgOW=NknVr)B5O-}IUxaqpz5hw|R9yH}I? z|JS=iRi53gx@+?v$aY^&)#>TEc{I1>sQvM6syhFyH|Qo7rvBUbqcXL3!;Fs=(T9@m zHRp16w`_>`a^ixJ*!DYjr)`KRyuUxX#nmZd&1}B4hUFJc6DJCYl}l>>K9n?h@A?mi zloWM@4kfLQ-Ot+!RHOn@IZD97;MyUn@cHMGzbVZy+UUX2edOH+8{V5q&JoK0h4#qx zI|` v(89Ixy0atZ_db_BI=N^4Kcr-NAp1ABZ;XS5VFODSD5ZM3`njxgN@xNAqD46+ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f40bfdbdd968e3392c9ec9e4103595519afe01 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qqp;uumf=j~0$pxXuv4uO|n z9xN^1)gZ>y_f+Lyz@+bG6Ky7m*QcmQiUIY&zz=&{-)G0e@36#Y)f`njRF$(h>PPri z1N%cOZWSecJZF)ksxJ1v=Wy7rH%F@M@{;$RzI>zb;=Jr_9iOS3Tv{NX`0>?^Z7oMNa8$bSDxl1NnTV^80#9uEDEwmQN7u@%rJLU)^cvO>ghMBK_fTPx7>n zpUzo7-Z0gM%X6MLI6Xx~^rzQ+=Y^#>ApPbGyT~5PQvq{CEI^{3u6{1-oD!M5dtaSW-L^Y*59(JcoNhd@j1 z_j9eARYVP#YS-Hroa6D_v#cvxH_=}hs09XU{PVe=A5X90u)F%B$F=3_EjQC2;ifa> z6GL)uZrowuH#_C{qeB+Cm$u2i{=293r&fOQQP=Esm3vMtV{Q5BBgR!-Q&#xn(9hQ| z4^?@UbMG#%VLcz=^ZxOg|Trg8C01Z?}J@_v?6{t5bxES^LwxAG5aAz1Kc; z%VOem1F>_vMJGjwU)xay6wfmIEBEJcpKDTjgxT8b8{hAalf8DfBzJGq%>19Qyf<9R7W$>ejEFervLB)oaH@-N^mOzH_(i9qWB*&;D&+ z7af;)|JtFqPe0x~`mp`p-lxtQZ)Qh)-LdJ}tmCU?WD7FqSoW+pJSSQFD*oQ_nTLNc zJ>KHE{9VL!H`5uPdq2CUCH^(M^-;!j)xpAu>BpYy9#ww4+8&m$f%E~d{fw-szSh0M QadSYzp00i_>zopr09Uk5F8}}l literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 0000000000000000000000000000000000000000..efd27be4f9df7c8c7733920dbe1330f938a4c2f5 GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&tiaSW-L^Y+HUzFQ6=42~V& z4&R@z-mWq6+Q%l|Jf@thXZt6NNuH)Se~<8C=xUyF(VRD4)!-kr*A>ui+Pbn{=n{Jo?+Vzb?~ z%jV_r5u1%Bi!9%5cP~A4nO5Cy(>0MZS9a~sv*Jp(oFKyeyL?R~U&zt@Wo@RX8&^e? zzOaA);rP4_X_gyeKTbWHc(!PPmfnt!8X=<7e;!lXsJrH8N$T!LTGnEnyotX5g!at8 zYdx)ag04)fN{j2GsEE%mPg$ODE7K7=q@*}XP!t9qH)emWoVESI+Z!7_IJ#RlcyMsT f5(fiAL;6?lyA?{unZGam3ljBo^>bP0l+XkKcMjwy literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 0000000000000000000000000000000000000000..d3acd01abe04c4577f0b930dda7611f10912112c GIT binary patch literal 785 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV7lSy;uumf=k3jddAA%y90FJT zdiehQZPzO+EZBH{Olq04`SHz98+ObpJn;~y2M*#df2{OA`+3?U&u4F@-k-nKV7)GP z_A2(zx^HB!z4>FaZ)JS7UCy@}iTk(wtgprFe|o;H`p2Wb&0o*i9@@2I#=N>^?T2T5 z-8ipq$MHk6rp{bqVe{paowjl4$7lC8q@^y?x_7r!Tg)=9z5wk!>5EJ(>KrYxSE56|)x8pVJ?F zy|X9TlKtkPKo6$~m36JA>htF`=cF80Qq&PTG)f?a!SzG4Ci@?c+{-x)7&ZVun{U$k#q-Iko=zcS`+7x6cm|Cbe>IvE#L^%Ke literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 0000000000000000000000000000000000000000..55ddbc033fbeea4f13492b24372bd501975c994e GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVA}5K;uumf=j~0$pv4Xh4uNw% zJ*fRWvvsN81P0F^7iWAgGYL8TDBN7}G7C@-4A@^j{Ws6_UAgwVlsUGaf5ngo}jmc@sq*v}tt{#E=W#o^opr*NOcHF(=hi0s*D9(*YPV@<_*^#9!^O@1pu=r<}=`-e4k5+RZmio*X zy79#Y{r4ZexoEl0O?*YWQY6yr?X`0Z{~-R&N_w0yHDfj-n?#ImXMfW;>I`Ao&p)&d26y} zExYbKuQ@*Xr}=|N545z!=J%}v%EtIlAR*N!ef5 z&Ch3j-F?OU!NH#7WgkDCt3Q@#e3Q%bn`)x%_CQ~qHP4Or{bqzEJ|JE8i~Wb8(fz*) Q5l29xp00i_>zopr07Mr|_y7O^ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 0000000000000000000000000000000000000000..c000b26e9671cd748319fbab7e8921d6a98ef3ae GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV3zlEaSW-L^Y+Hwyu}70Zh=O# z*zNQ0?Ob7%)xpg6=V|}ecLghCJ9THL%>wFyfd{6sa@#{}^EBm_ysP{4J(BzL&7{Q8 ztfYH~UTnzvP_t*Hy|Vi1iIYEGV|yF1weEEIaoasRyuaSFR9bWN=$8_|_QR{bZJ6ix zE+XQp#AK1n3#Wh2>RH*9UwwGhu^Fp=~sI?t3b; zqg%ov^T3lN;bO?wE9vT&d1;~-8h|<#1mCtAq6q#oO#|zgwmDKB6UuXZ15?v$g)EDZ6KVdjnSY zS!PYvtYxRqO^rTXlN&Qz{K`GGSEQ&`{hr~cvU_Ft|??`t^}=W_kE*LmC1t7Yas*e+!$ zc|I_CndGn2)xC3i)K?@L^WNQ9xBAMFp76)Zds@G)-E_G)WT_fo``2h-dY*P=^L|(& c2htA9>tEb*3t(Q@KN}?K>FVdQ&MBb@08oo#djJ3c literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 0000000000000000000000000000000000000000..7056b4fd9347bcce6ce3ddc9d0735efd12b5d0c4 GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qkn;uumf=j{#ezDEus42}|K z559h8ygKfJ*@Ji`9zkoLk6b5L8r_}F4b%bydzQa`y-jNU?JoCY+Gd@HrQTM2le+pq zXH6jQvx>(*Cc3wq^36N$-FsN=uVv09*U;zYe6vd~behi7|G7PZSHD1(TTD0dbH((% z%=2nFZTGHKZ?&!a{_;cY{C)fP&hvYcm-v01{>+_!7nhq&yW)BFxoLG_qpbbC=i5TA zR-V1T`(`(H4sUjzedye~jk&p1B7S9)uC&jr)bU$yv*q8BSvd<OMTnC*-R6jXT$lxi4I`@A$Urx8`Ddczh7^+(T08hSCOU69s?-BP_WwFnn0|kNK9j%}TLfU4=a(s-@L${y(L<-PHR0aqqKv#(y{8_F)g*yZ>hHvbkkzcGjO>FZ%rh)A7i{%d@KU z)_;1#@_k>+yt*~kt#|iTm48}Z_v_cEsw(Ah?%ihl(#{>P|8nZkq^jW4mAZWEJ{&py z`_iOURq?0K=M@&Tx*v{;@(+EVcYK?i6_{80-+gHmAnw+#}zDbA7H(5h`ZwPfb#KMH7Ahss6C(a}s(D3O+yQ zcbk$YRv75a7Tdm-FZ&$(mZQAUWosjI< z#yxIM#TVIZv140~#{2KtbN*LI%ynd{gTe~DWM4fqXlD9 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1f003440063facdb2a8ae9bb6455d62b5f1b0b GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVCM34aSW-L^Y-TLqFV+G4vxz& z^IxC8oqy&QUxR%O{Ja(4d2ddf@llfVgb5>16BHcy{^i?_rNGpZ6=G|?~y;~v@w?<{9xN)&gT%XRZ{mHR93szk>{_Lag zF7}q^by8XA-c{DOt-eez|FJ=R&BxLMWnXW-ySc@zz0`2=s&fyXZOgOa3N4k}Zks)S zqel*Rc1>04ZmZer{cFzL{W4Rw`$*dcw|{CCcF9kFy^}p{S>abbfA``EEgL-6w3^!O zE#jMZbKU#4G995qN{XWdN*HXblAa&mzjL-7M>jCIIJ$lJ&1=qXIqG}tRB7Tw0kPL* zwemIUz%WZXdawWO`JO{=uCm%MWtMrNFZA{Hn$kOmx$mrcylh_ev~WwS zgty)=UgoY9+4kq%s}Iie>-O!L=l3Kt@%*~)XYTyJsLt*EYRRoiQ~ffF50Ce5U9#%j z>c4MhiCe$nUA=YQs&jb>(YyN&-SnFD%b+*8urBrDH`(lM4Xf<@AJ06uUgws?wVYk6 z?5-Z%*7onYY+VlLYB~KI>+Eh%^j&Se=B7(%?7?^2%H&#C?dq#Ne=A={>DQrEyTiAw zUR_!9=<2SQ^JKd_`Zj3&Q?-~MRayVJ@2Sk5Woysh4fJT)&=JAC`dyqQ`}&;A_uI;J zlv-SYVgh1pg*rZu#DecshQo!%8SzkmVTE(;2wjg~sgADZUY zzLQPNopVC<%BkOdPLLSzJG}blm+ueEt;Gbb4{yqhJ-#pft}HMnx2;?7amw@XsBLc| zHh13om9u;GtUt$;Qj#OSRvcXRb!A^p_~m)B*=DiP^PeY#UN*T~w(d^Uv+T#W_ALBn zc5%;+jQ9C#u6b=+w^=&$YRQY^)!DYWbFUv*wd}9^!&`^?jtW=qlC{}=&$#%%`|oMt zH*QB+#!b5)D_i&Ca`e8IL-$;+pPvdzun)FNS@!G?Og_f*_k4A)%rW^DiIdyjMeGmF zGI)IT$Ha{r_Jx+&xQAvHSgg4Z&K>`Lal^6= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +346,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,25 +371,50 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): """Draw text.""" From 60208a325083579361eaecc6d388e265aa52bea6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Feb 2023 10:32:55 +1100 Subject: [PATCH 02/25] Only allow "corners" to be used as a keyword argument --- src/PIL/ImageDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dc77c06ea..a55ebbe8e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -296,7 +296,7 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, corners=None + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): From a55c2b42b9fdeb395cdc387ea768eb12e9a547bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Feb 2023 20:34:52 +1100 Subject: [PATCH 03/25] If following colon, replace Python code-blocks with double colons --- docs/deprecations.rst | 16 +++-------- docs/handbook/image-file-formats.rst | 4 +-- .../writing-your-own-image-plugin.rst | 12 ++------ docs/reference/Image.rst | 28 +++++-------------- docs/reference/ImagePath.rst | 4 +-- docs/reference/ImageWin.rst | 4 +-- docs/reference/open_files.rst | 4 +-- docs/releasenotes/6.1.0.rst | 12 ++------ docs/releasenotes/7.0.0.rst | 12 ++------ docs/releasenotes/9.0.0.rst | 4 +-- docs/releasenotes/9.2.0.rst | 8 ++---- winbuild/build.rst | 4 +-- 12 files changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..0db19a64e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -177,9 +177,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +192,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -336,16 +332,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..d6b42589a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1393,9 +1393,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..976b148fc 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -127,9 +127,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +138,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +158,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +170,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +179,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +207,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +219,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..616cf4aa3 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..3dfb25840 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,9 +59,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..d4275a274 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -96,9 +96,7 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild From 43682de4bdb6c5f93340107386f86becbfbad25a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Feb 2023 08:12:57 +1100 Subject: [PATCH 04/25] Updated harfbuzz to 7.0.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d..35980f19c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", - "filename": "harfbuzz-7.0.0.zip", - "dir": "harfbuzz-7.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", + "filename": "harfbuzz-7.0.1.zip", + "dir": "harfbuzz-7.0.1", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 36bcc0a89866bf9ca3f79c61470d9ab4a58d74ec Mon Sep 17 00:00:00 2001 From: Jasper van der Neut Date: Tue, 21 Feb 2023 10:34:41 +0100 Subject: [PATCH 05/25] Support saving PDF with different X and Y resolution. Add a `dpi` parameter to the PDF save function, which accepts a tuple with X and Y dpi. This is useful for converting tiffg3 (fax) images to pdf, which have split dpi like (204,391), (204,196) or (204,98). --- Tests/test_file_pdf.py | 42 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++++ src/PIL/PdfImagePlugin.py | 19 ++++++++----- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 216b93ca9..a45b22bf6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,6 +80,48 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +def test_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +def test_resolution_and_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=200, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..081b84963 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1497,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..d4f1ef93a 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] info = { "title": None @@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From be489287d2d8923ad57695f1bdf0d28db8be5757 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Feb 2023 18:59:51 +1100 Subject: [PATCH 06/25] Parametrized test --- Tests/test_file_pdf.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a45b22bf6..2afec9960 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,32 +80,18 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) -def test_dpi(tmp_path): +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): im = hopper() outfile = str(tmp_path / "temp.pdf") - im.save(outfile, dpi=(75, 150)) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -def test_resolution_and_dpi(tmp_path): - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=200, dpi=(75, 150)) + im.save(outfile, **params) with open(outfile, "rb") as fp: contents = fp.read() From 0d667f5e0b756844e22dbb78b71c84279992ca2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 21:12:11 +1100 Subject: [PATCH 07/25] Do not read "resolution" parameter if it will not be used --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d4f1ef93a..4fa1998ba 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,12 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - dpi = im.encoderinfo.get("dpi") if dpi: x_resolution = dpi[0] y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None From 21d13e1dea032d734572a9bb954c827aba2b29d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 23:22:18 +1100 Subject: [PATCH 08/25] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..f36ad5d46 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 0.5) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From acc7e0b469ac973bf1ab79bf8369a8b13769d169 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:00:24 +1100 Subject: [PATCH 09/25] Highlight code example --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..9a3ddcab7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. From 742aff3718cebdad390ad808e3cae181ea749e27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:17:10 +1100 Subject: [PATCH 10/25] Replace Python code-blocks with double colons --- docs/handbook/image-file-formats.rst | 4 +-- docs/handbook/text-anchors.rst | 2 +- docs/reference/Image.rst | 12 ++----- docs/reference/ImageDraw.rst | 18 ++++------ docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageMath.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/PixelAccess.rst | 8 ++--- docs/reference/PyAccess.rst | 8 ++--- docs/releasenotes/6.2.0.rst | 8 ++--- docs/releasenotes/7.1.0.rst | 4 +-- docs/releasenotes/8.2.0.rst | 4 +-- docs/releasenotes/8.4.0.rst | 4 +-- docs/releasenotes/9.1.0.rst | 8 ++--- src/PIL/ImageChops.py | 52 +++++++--------------------- src/PIL/ImageFont.py | 12 ++----- 19 files changed, 44 insertions(+), 112 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d6b42589a..4c2af3db8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1402,9 +1402,7 @@ at 72 dpi. To load it at another resolution:: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 976b148fc..0eba1141a 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -242,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..e325a0280 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -597,18 +597,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +613,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..b27228ec9 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..0fb33de75 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..cb46f127c 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..f11953168 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bd13c391e..173b2926f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -297,27 +297,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) From 19299acbb6688d2aaf33e53f64c9cbb04545e274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:39:48 +1100 Subject: [PATCH 11/25] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index f36ad5d46..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.5) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 3c3d88845072b0b8b8a9a00787fb03d733def3b7 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 23 Feb 2023 23:19:13 +0100 Subject: [PATCH 12/25] Update docs/handbook/image-file-formats.rst Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 081b84963..597d1b644 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1498,7 +1498,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum saved in the PDF. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. If both the ``resolution`` parameter and the ``dpi`` parameter are present, ``resolution`` will be ignored. From 57acab55cbf0f00f3064d11ccfd3843c55cdceb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 12:55:13 +1100 Subject: [PATCH 13/25] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc40..d37ba4ab3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + - Fixed writing int as UNDEFINED tag #6950 [radarhere] From f52bbf895036f25932a479a46b20b07f2eb0c1de Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:58:51 +0200 Subject: [PATCH 14/25] Clarify variable names in BdfFontFile Co-authored-by: Yay295 --- src/PIL/BdfFontFile.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..3f7b760d6 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,25 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBox, BBoy) of the lower left corner + # from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH followed by the width in x and y of the character in device units. + dx, dy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dx, dy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im From b6b72170a8d81255e6f491e657e0614102a0e347 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:59:54 +0200 Subject: [PATCH 15/25] Clarify variable names in Image Co-authored-by: Yay295 --- src/PIL/Image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 63bad83a1..670907c67 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,12 +767,12 @@ class Image: data = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + length, error_code, chunk = e.encode(bufsize) + data.append(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if error_code < 0: + msg = f"encoder error {error_code} in tobytes" raise RuntimeError(msg) return b"".join(data) From 04be46d484eccb633b1ad8058e1c4f39208589b0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:04:38 +0200 Subject: [PATCH 16/25] Clarify variable names in ImageFile Co-authored-by: Yay295 --- src/PIL/ImageFile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 132490a8e..dfa715686 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + length, error_code = encoder.encode_to_pyfd() else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + length, error_code, chunk = encoder.encode(bufsize) + fp.write(chunk) + if error_code: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + error_code = encoder.encode_to_file(fh, bufsize) + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() From 6f79e653d68c7bae9c0303f8d8ddd68a3a1cb3f0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:07:29 +0200 Subject: [PATCH 17/25] Clarify variable names in PcfFontFile Co-authored-by: Yay295 --- src/PIL/PcfFontFile.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d5f510f03..1a4b8f6d9 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,23 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ix_metrics = metrics[ix] + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = ix_metrics + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): format, size, offset = self.toc[tag] @@ -206,7 +220,7 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] + x, y = metrics[i][0], metrics[i][1] b, e = offsets[i], offsets[i + 1] bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) From 8e18415cc54c1c87183d36a184b5360d77fb8571 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:09:14 +0200 Subject: [PATCH 18/25] Clarify variable names in TiffImagePlugin Co-authored-by: Yay295 --- src/PIL/TiffImagePlugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index aaaf8fcb9..0491a736d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + length, error_code, chunk = e.encode(16 * 1024) if not _fp: - fp.write(d) - if s: + fp.write(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) else: From 8a1b9aa0adbd84cd944c15a667af4a65052ac522 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 16:44:07 +1100 Subject: [PATCH 19/25] Allow comments in FITS images --- Tests/test_file_fits.py | 6 ++++++ src/PIL/FitsImagePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index d2f5a6d17..3048827e0 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -44,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1185ef2d3..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): From dbcd7372e554ee420a156b968eb9a609ec66ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 18:46:07 +1100 Subject: [PATCH 20/25] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab3..15decc7f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow comments in FITS images #6973 + [radarhere] + - Support saving PDF with different X and Y resolutions #6961 [jvanderneutstulen, radarhere, hugovk] From 132fb9360b291eafedd3ea8238ceebd93a094e87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 19:10:47 +1100 Subject: [PATCH 21/25] Added memoryview support to frombytes() --- Tests/test_image_frombytes.py | 11 +++++++++-- src/decode.c | 7 +++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 7fb05cda7..c299e4544 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,17 @@ +import pytest + from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type): im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) assert_image_equal(im1, im2) diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..82a3af832 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); From 36489c2c396d8801bfa632b6e8d00a094dab5347 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 20:54:35 +1100 Subject: [PATCH 22/25] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f0..fe0230c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added memoryview support to frombytes() #6974 + [radarhere] + - Allow comments in FITS images #6973 [radarhere] From fcc59a4001bb1b1ad47d1a8c0b0a602336b2dcdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 13:40:44 +1100 Subject: [PATCH 23/25] Use existing variable names from ImageFile --- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageFile.py | 14 +++++++------- src/PIL/PcfFontFile.py | 11 ++++++----- src/PIL/TiffImagePlugin.py | 10 +++++----- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 670907c67..cf9ab2df6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -765,17 +765,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - length, error_code, chunk = e.encode(bufsize) - data.append(chunk) - if error_code: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dfa715686..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - length, error_code = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - length, error_code, chunk = encoder.encode(bufsize) - fp.write(chunk) - if error_code: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - error_code = encoder.encode_to_file(fh, bufsize) - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 1a4b8f6d9..2300efe40 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,7 +86,6 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - ix_metrics = metrics[ix] ( xsize, ysize, @@ -96,7 +95,7 @@ class PcfFontFile(FontFile.FontFile): ascent, descent, attributes, - ) = ix_metrics + ) = metrics[ix] self.glyph[ch] = ( (width, 0), (left, descent - ysize, xsize + left, descent), @@ -220,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y = metrics[i][0], metrics[i][1] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + left, right = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + ) return bitmaps diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0491a736d..04d246dd4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - length, error_code, chunk = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(chunk) - if error_code: + fp.write(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: From c799bd8a03f522dc6cc26f6c974140df60558484 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 14:04:10 +1100 Subject: [PATCH 24/25] Adjusted variable names and comments to better match specification --- src/PIL/BdfFontFile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 3f7b760d6..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,18 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - # The word BBX followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBox, BBoy) of the lower left corner - # from the origin of the character. + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - # The word DWIDTH followed by the width in x and y of the character in device units. - dx, dy = [int(p) for p in props["DWIDTH"].split()] + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] bbox = ( - (dx, dy), + (dwx, dwy), (x_disp, -y_disp - height, width + x_disp, -y_disp), (0, 0, width, height), ) From bbbaf3c615e7a60e526e73f3dc6449780dce2271 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 26 Feb 2023 13:03:29 +0200 Subject: [PATCH 25/25] Update src/PIL/PcfFontFile.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PcfFontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 2300efe40..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -219,10 +219,10 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - left, right = metrics[i][:2] + xsize, ysize = metrics[i][:2] b, e = offsets[i : i + 2] bitmaps.append( - Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) ) return bitmaps