From 09da6fa73babd4dd48c038dd8a06196be44702d3 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 May 2022 16:15:01 +0100 Subject: [PATCH 001/205] inline fname2char to fix #6319 --- src/Tk/tkImaging.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 5b3f18ace..16b9a2edd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -364,17 +364,6 @@ load_tkinter_funcs(void) { * tkinter dynamic library (module). */ -/* From module __file__ attribute to char *string for dlopen. */ -char * -fname2char(PyObject *fname) { - PyObject *bytes; - bytes = PyUnicode_EncodeFSDefault(fname); - if (bytes == NULL) { - return NULL; - } - return PyBytes_AsString(bytes); -} - #include void * @@ -442,7 +431,7 @@ load_tkinter_funcs(void) { int ret = -1; void *main_program, *tkinter_lib; char *tkinter_libname; - PyObject *pModule = NULL, *pString = NULL; + PyObject *pModule = NULL, *pString = NULL, *pBytes = NULL; /* Try loading from the main program namespace first */ main_program = dlopen(NULL, RTLD_LAZY); @@ -462,7 +451,12 @@ load_tkinter_funcs(void) { if (pString == NULL) { goto exit; } - tkinter_libname = fname2char(pString); + /* From module __file__ attribute to char *string for dlopen. */ + pBytes = PyUnicode_EncodeFSDefault(pString); + if (pBytes == NULL) { + goto exit; + } + tkinter_libname = PyBytes_AsString(pBytes); if (tkinter_libname == NULL) { goto exit; } @@ -478,6 +472,7 @@ exit: dlclose(main_program); Py_XDECREF(pModule); Py_XDECREF(pString); + Py_XDECREF(pBytes); return ret; } #endif /* end not Windows */ From dacd5d6eb9ee5486fac6de4b7a575a33fe3a5e2d Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 24 May 2022 16:36:30 +0100 Subject: [PATCH 002/205] add decref calls to imagingft to fix #6321 --- src/_imagingft.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 8f19b763c..7dc50b474 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -303,7 +303,7 @@ text_layout_raqm( goto failed; } - len = PySequence_Size(seq); + len = PySequence_Fast_GET_SIZE(seq); for (j = 0; j < len; j++) { PyObject *item = PySequence_Fast_GET_ITEM(seq, j); char *feature = NULL; @@ -311,23 +311,26 @@ text_layout_raqm( PyObject *bytes; if (!PyUnicode_Check(item)) { + Py_DECREF(seq); PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } - - if (PyUnicode_Check(item)) { - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { - goto failed; - } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) { + Py_DECREF(seq); + goto failed; } + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); if (!raqm_add_font_feature(rq, feature, size)) { + Py_DECREF(seq); + Py_DECREF(bytes); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; } + Py_DECREF(bytes); } + Py_DECREF(seq); } if (!raqm_set_freetype_face(rq, self->face)) { From 02e90e21f45b59f212d5f9289773b6eb5653e30d Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Tue, 5 Jul 2022 15:13:39 -0400 Subject: [PATCH 003/205] Release the GIL when applying matrix conversion to images --- src/libImaging/Matrix.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 137ed242a..182eb62a7 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -21,6 +21,7 @@ Imaging ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { Imaging imOut; int x, y; + ImagingSectionCookie cookie; /* Assume there's enough data in the buffer */ if (!im) { @@ -33,6 +34,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { return NULL; } + ImagingSectionEnter(&cookie); for (y = 0; y < im->ysize; y++) { UINT8 *in = (UINT8 *)im->image[y]; UINT8 *out = (UINT8 *)imOut->image[y]; @@ -43,6 +45,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { in += 4; } } + ImagingSectionLeave(&cookie); } else if (strlen(mode) == 3 && im->bands == 3) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); @@ -54,6 +57,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { UINT8 *in = (UINT8 *)im->image[y]; UINT8 *out = (UINT8 *)imOut->image[y]; + ImagingSectionEnter(&cookie); for (x = 0; x < im->xsize; x++) { float v0 = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; float v1 = m[4] * in[0] + m[5] * in[1] + m[6] * in[2] + m[7] + 0.5; @@ -64,6 +68,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { in += 4; out += 4; } + ImagingSectionLeave(&cookie); } } else { return (Imaging)ImagingError_ModeError(); From dea30e4c807f76931d5b37736c6be2dd2f8546b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Jul 2022 08:39:23 +1000 Subject: [PATCH 004/205] Fixed set_variation_by_name offset --- Tests/images/variation_adobe_name.png | Bin 1431 -> 1475 bytes .../variation_adobe_older_harfbuzz_name.png | Bin 1432 -> 1492 bytes Tests/test_imagefont.py | 4 +++- src/PIL/ImageFont.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index 11ceaf6e65b60a9a9fafacf1d72f8bc8227d8e1c..5168e04b99bc16ea9ba6c57f3a7d4ec3cf3a47ad 100644 GIT binary patch literal 1475 zcmb7^c`zFY6voq*MN-ukS@fW2CsLxNHB+QkBuEe>wvN@r5yq%V*R4@Ul!myf?s3$T zNKrkcCXS|5-PB0y3K3Tmwjq>pq;;&$?ChVrvwytzX5M`BX5P&A{nA{pcFI64AOHYR zM%yEChm&+z4vI$)yAq{w766c^qmkBbF{LZT4v_n<8ttp+{Hxz+*#`AK>gn*;dvOJo zm#1rD0O~@ld-P_wBkVE!j1*iR1k_Mz3G}xH2l_6+@x9j&4}$^+_l(KY(gVRA!D^P> zm)%DvSYV(bap0x2&^JXHYrwCR>mM; zzGz`#kC##7 z>W@$Am&*UteEg;oj$R zbuhS~wAAq=($@CjT4h>VTEs064-Jz5&QzxDf=I-iCab8ZWM^i67#PUK_|BM?{{aaO z30Ve#_T9bY1mDn;Oe8*O zGQz8Es?u<@_q?&Og0eF1>(`&dnn@;}cka}^^MOF2x{C8+adll?9fR>%BuYVRYHJ%C z8Hwu6%*`=v8d_TYgM*}Q1qFqbsMY}yolc(*lzsnRHyYgNRqq1nTt$}plgqN78?rum z5(qEHI&q}D&Bl?3u*BXwI z@80FJ(P*?lH!?EPK4*B?h4PZk{#5d}@XVHSk$8DIqdcs2dVG95HMOEbhn&;eY9ZD( zFe@%D#rv6c}$YierjCj^4hRMB8yxV^n%v}+2&x6up5Q4qyCuQfL}_ok*UdY_=# z)Qt*fX3iXcn4B!PHL|c!g=KBjqE61A4VSFFjkK*grKE>GeFJ`FSt|X=a!MA91Cw-s zAdmswi%49Aak<<~!qL(*#rzO32X8hLpJaUbvW;#&Ml?C8WzkLMa2gwP^!Y+z7Q@)k z@TA<_;$n1YsCD+p$cTCdKQYcGBhv%|?*a}uCnF@egTE+;kD6byVV|}_|Zr&f;0R#eT z>+88KE#F=k-L!PPa8(WEgvIJA{G5iw;qe>0yN2DWDA!6A=6nUDpJY<(c&Tm_^O%H0 zA|oOqu-Ghb*g=-4yu94Z%na-7jDGA^<0g~I-ek8VZ&Wqxo9`^f{%?x?7xUIs5@`?8 Ti!MliAR2&1VUf=fK1qK8@g2&Z literal 1431 zcmb7^`BTz)7{~Ei@1s2P*2)wK^9T>rT*3nnP{v48!;741cU2TbJS!bjmSI|@@<1ds zC2}gX;l)#A}7t#He4&+?_aPx`@`q?%zU0_o@YMK>v`Ul(dbZp9ZMY$ z2&9h+Lt=qD9XJtMs({s|3S}UW#(5O-$5=|kMs0#~Zj4E(@`Q2P$ZeOgBoA#jN2S+l zc_N;YgF1-CM%FCZQnKjHiI#}*YnuKsCN9l2&0`kIbfW78t*JL9KBT>$^>XG{cD`ON z!DM_nSHKdA#OuBaz2~V9>{UIj_eg5{#z-@eqb}%%M(L<(P`Zk}0Av&ox()&T+jD7A z{9xwv%*>PT2a62@o#8Op&A?X+3#k^nMRS+_nw-=_gh6j96pH=0BwE0`$%%=Gdbt*7 zZZ6h87LUi!y}i8`+n;NYNFU_2F(UGi->H5<@Ai&1V>;j1#$Yxhp z=a9)?;tBQA1wCHuJ((<-6FoYd>XO`Ruuh>+oFR|~Hak=c)iP68SJ&CuDHIB?Tv6e^ z@bUKU9T*ts>$5O6KK%xRM%&ui0UwkHZ*6UPczCp>F~fx-(Xvz;RTPQAJfE2Gw4{5~ zMmkj&Wn;FJ;x3QS3bP511_zHgIbkoRKA_~~-7=sv7_+8@MMayNo1ZEd7Z(i-N_u0< z5h<|xwKg2zls4>dn@J|$OiF{u5^*HbN!I-IbZN!pg`A)ukj=*WdUsvInKR|=7LkZ; z>1|2R&d%22jEscL(;t*3#Gh-&*&9t?Y;A4DX`@i6TrWSr1@P|9&O5pM^s(#6J6!Hz z+Aty@n2?bnmsqyBle1iPm-B#Dwcqh$p`EHfKTVdEsk6`(ilyac z#pF!C%i-haE9DH^+5-%vc*84V%~frlnxO0WOX= zWMc?UXH!!XkP86R-Ca{f&Ci!gr7OmHWO4(G73}z8WyK-zJQjQPEU*V%erZUgx(z8e zw;38&J?7=^o^rw>u0x{AY{Bt0fA|3yRWKMlU2u=j2gWEgH1w4C93H>F%gjwqw%@a{ zwz6_{b8F=SEdVpCbfTi7T$thfH7u{2&%aEi){U>df8RB*J2N}0rlxiPk!t`?7ElWc zL~)+wQ+J=R6k25&j7RBFx_3c}jmzTP7+n`Zu zP;7K8Kg)4KY7Fd|a}rQTvfE*vlJT jR{ob3{XgXbqGg8g-roW;Vn=-e!3Cj$(a7Hs$yfgYM*NXQ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png index 2adb517a759aab4a06e83cc7ab604f97f88c1d27..fa0e307b4f650e007f30bd2ede877c8fd6843116 100644 GIT binary patch literal 1492 zcmb7E`7_%I6pwQWafhNUDvoNjsWh zI~W5%bCQz;y*#~+4}r)OI$~|F(29ilP7b=h8n;#vrzOZnq4$jLv&Wm(*vHSprzA3D zRMYOa>o=H=GVWzDf-B&saZakzs2c`yP|{4hY*jdE7xmlvUQUD`&9b*e5xNEkUGR8iF&dy!>n**a zR=sdITt(T#n#i`~GzNoFyEkt_0303D_gGvm_Y%F)Qd3j&gS&lIMKg7^@6;~7cSDpg z+nS7bD%1EInb&*8&o5JjzQ(`3$0{i)82}MP!9}-+yp{cZF$(7<5{amPO~>u+uV3vUc#?Qq$mKG&%sK}1Vl2^UA9r^aP;O-Q zTV`fTY+~Zw55ZQGJRZTobCo5-!DmsZiL{R&fizb#ne6TTXtJ&& zq)o}Mh0b+0cFfiNG(OI^N!lN zIWmey>q%nHU#;^dvpt)!064$$OsAd(81IaFQrkl z!Sm1;RWaJy+Uv9;G;25*3WesvPa%;+A0Ktt`M8aR6<+mpj`;7@Wbw9h$tAk1WL<$| zU?w95agqQ4ny2E(%K zm9|}TQAUP^g$;z{*;A<~%!{h3%;(D>ALMmCy}VS-O*u>%NeNuxn=19aSJ#GUqPVy? zJ)5M3C0C6x9?xC{o1APD*)%1zlM4(AqC`fXF^q_a;H1FeaMrbZ9L`UE&!>bz7_|e= ze6wue0Saciva{p4weIiX;Xxog>71*X5)NpQm@a}%QA2e7>(@`;kB(}f4=Kt}!P9POFKSV`E9ag&gE@))=W>TwOO_-=mAP^WD8ZyDln45P*8hrCo0Nzf}5Q9EU8)3w!M+w!Xf)4SOk(2x+L{IR6ZmWa{W%EjtnO;; z$Mtpg7NCtdss<7oWc{!@5F5L_DHx(ssRz8GLuv+oO8o-^imN*;_p-^!$-%+Fg~(?> zf@zDQm9uz1sdZLQQC2>sAyFLb=H}+?{J^EOxe15I<86|^?|tr8PSGC-ijIh={8D@} pf7VPRoiti5U|6dEG=!?AQd6qw#UJIlP35F2aJGs47Py*ijKEFA)}_R`G! zs>dWBJ~%%!L5J$-=#1o23e)nImM#~0?RoB|X8hLJh)`KtT(m#Fy0Q{_;X>`}6--PF z#Ii2v35x}}k6I9mLE73TRNoXbxm0vzKqh;har95l)Ev?P&qP(n^0-`e*J%Umi?L}MLAhy}u+?-ia zA(0fA@(YkD=r~>bs;Vj&3?{YC?%96gkH;TV+zqe}st@`^9MHZpu(7p;yr-(J{`m=i zd)qm`zo#fWD+~O})T`_-_}cn<9-QX;>%^hJ24R#?RZ9y`AoQQEi|RJUeKF@3EQ!S^ z~2h2GSZGM5io*u$ji6g${;?$}&)t7z->u6mveEirk96?LD z``qt*0)a4o7FMjh8ihiU%jMJ4(+Wj$a(IIJk)P${S8+5Fn7O&Rqy%s8yTJ`# z%CPnZiwWDiyPZ6<@mu`7JguTZ*~W%02$UrP8yU6Ke&vWZ7I!ZK;|4rlu-^Vs8?OL?zl^U^8Q5I`GKI$h?CiBO_3TfsxVK z)5`JsYZKA-XZ-zF6J5e+8biUh)QpS_2m~S&3g4Tzw6v6$m&e4!M2${QPCnbkxIm%f z5?3cUJo5Z`q}8}cbW~5x7V72(DH$HMRmtQk1wp5OZ(G~dH7i*i9vV{9)I_R`Ih#bY z6$(XTBaJ&GV=x#>?sxntl%Ba_8cq5C4}Xv+rC7QdXzZt->BBTh3rb2vQmK8M5ZXvM zC%K;JGTqnLH#b)q+_03;hG`11rOxMP=PCUHmz4DN2?Wczym^@{aqZ(imQ5q8o8BO^Ic=frZ diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 16da87d46..4c48d4ef3 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -769,12 +769,14 @@ class TestImageFont: self._check_text(font, "Tests/images/variation_adobe.png", 11) for name in ["Bold", b"Bold"]: font.set_variation_by_name(name) - self._check_text(font, "Tests/images/variation_adobe_name.png", 11) + assert font.getname()[1] == "Bold" + self._check_text(font, "Tests/images/variation_adobe_name.png", 16) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) self._check_text(font, "Tests/images/variation_tiny.png", 40) for name in ["200", b"200"]: font.set_variation_by_name(name) + assert font.getname()[1] == "200" self._check_text(font, "Tests/images/variation_tiny_name.png", 40) def test_variation_set_by_axes(self): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a3b711c60..9a12ba48d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -795,7 +795,7 @@ class FreeTypeFont: names = self.get_variation_names() if not isinstance(name, bytes): name = name.encode() - index = names.index(name) + index = names.index(name) + 1 if index == getattr(self, "_last_variation_index", None): # When the same name is set twice in a row, From b330ff910d4925a5a420dd4b71c7db3792327319 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 24 Aug 2022 22:04:43 +0200 Subject: [PATCH 005/205] upload fribidi.dll to GHA --- .github/workflows/test-windows.yml | 12 +++++++++++- winbuild/build_prepare.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b9accfdf9..d8a1f23fe 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -181,16 +181,26 @@ jobs: id: wheel if: "github.event_name != 'pull_request'" run: | + mkdir fribidi\${{ matrix.architecture }} + copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v3 + - name: Upload wheel + uses: actions/upload-artifact@v3 if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} path: dist\*.whl + - name: Upload fribidi.dll + if: "github.event_name != 'pull_request' && matrix.python-version == 3.10" + uses: actions/upload-artifact@v3 + with: + name: fribidi + path: fribidi\* + success: permissions: contents: none diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94e5dd871..567ca4f7e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -297,6 +297,7 @@ deps = { "filename": "fribidi-1.0.12.zip", "dir": "fribidi-1.0.12", "build": [ + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), cmd_cmake(), cmd_nmake(target="clean"), From ced381edaa4f4217b3e8f575f3ed1dfe6f2784f2 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 25 Aug 2022 00:21:55 +0200 Subject: [PATCH 006/205] Document ImageDraw attributes --- docs/reference/ImageDraw.rst | 43 +++++++++++++++++++++++++++++++----- src/PIL/ImageDraw.py | 10 ++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1ef9079fb..ec21898e1 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -139,17 +139,50 @@ Functions must be the same as the image mode. If omitted, the mode defaults to the mode of the image. +Attributes +---------- + +.. py:attribute:: ImageDraw.fill + :type: bool + :value: False + + Selects whether :py:attr:`ImageDraw.ink` should be used as a fill or outline color. + +.. py:attribute:: ImageDraw.font + + The current default font. + + Can be set per instance:: + + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(image) + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + Or globally for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + +.. py:attribute:: ImageDraw.fontmode + + The current font drawing mode. + + Set to ``"1"`` to disable antialiasing or ``"L"`` to enable it. + +.. py:attribute:: ImageDraw.ink + :type: int + + The internal representation of the current default color. + Methods ------- .. py:method:: ImageDraw.getfont() - Get the current default font. + Get the current default font, :py:attr:`ImageDraw.font`. - To set the default font for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + If the current default font is ``None``, + it is initialized with :py:func:`.ImageFont.load_default`. :returns: An image font. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e84dafb12..7ca03e875 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -87,17 +87,25 @@ class ImageDraw: self.fontmode = "1" else: self.fontmode = "L" # aliasing is okay for other modes - self.fill = 0 + self.fill = False def getfont(self): """ Get the current default font. + To set the default font for this ImageDraw instance:: + + from PIL import ImageDraw, ImageFont + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + To set the default font for all future ImageDraw instances:: from PIL import ImageDraw, ImageFont ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + If the current default font is ``None``, + it is initialized with ``ImageFont.load_default()``. + :returns: An image font.""" if not self.font: # FIXME: should add a font repository From 41a7bfe1c188bda64c5fcc84d6518084d742de9f Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 06:49:48 +0200 Subject: [PATCH 007/205] append dependency licenses to windows wheels on GHA --- .github/workflows/test-windows.yml | 16 +++++++++++++ winbuild/build_prepare.py | 37 +++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b9accfdf9..ba72a7018 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -181,6 +181,22 @@ jobs: id: wheel if: "github.event_name != 'pull_request'" run: | + setlocal EnableDelayedExpansion + for %%f in (winbuild\build\license\*) do ( + set x=%%~nf + rem Skip FriBiDi license, it is not included in the wheel. + set fribidi=!x:~0,7! + if NOT !fribidi!==fribidi ( + rem Skip imagequant license, it is not included in the wheel. + set libimagequant=!x:~0,13! + if NOT !libimagequant!==libimagequant ( + echo. >> LICENSE + echo ===== %%~nf ===== >> LICENSE + echo. >> LICENSE + type %%f >> LICENSE + ) + ) + ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94e5dd871..e878f4c8e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,5 +1,6 @@ import os import platform +import re import shutil import struct import subprocess @@ -111,6 +112,11 @@ deps = { + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", "filename": "libjpeg-turbo-2.1.4.tar.gz", "dir": "libjpeg-turbo-2.1.4", + "license": ["README.ijg", "LICENSE.md"], + "license_pattern": ( + "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" + ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" + ), "build": [ cmd_cmake( [ @@ -135,6 +141,8 @@ deps = { "url": "https://zlib.net/zlib1212.zip", "filename": "zlib1212.zip", "dir": "zlib-1.2.12", + "license": "README", + "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ cmd_nmake(r"win32\Makefile.msc", "clean"), cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), @@ -147,6 +155,7 @@ deps = { "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", "filename": "tiff-4.4.0.tar.gz", "dir": "tiff-4.4.0", + "license": "COPYRIGHT", "build": [ cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_nmake(target="clean"), @@ -160,6 +169,7 @@ deps = { "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", "filename": "libwebp-1.2.4.tar.gz", "dir": "libwebp-1.2.4", + "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean cmd_nmake( @@ -176,6 +186,7 @@ deps = { "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.37/lpng1637.zip/download", "filename": "lpng1637.zip", "dir": "lpng1637", + "license": "LICENSE", "build": [ # lint: do not inline cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), @@ -190,6 +201,7 @@ deps = { "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 "filename": "freetype-2.12.1.tar.gz", "dir": "freetype-2.12.1", + "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { # freetype setting is /MD for .dll and /MT for .lib, we need /MD @@ -225,6 +237,7 @@ deps = { "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download", "filename": "lcms2-2.13.1.tar.gz", "dir": "lcms2-2.13.1", + "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always @@ -250,6 +263,7 @@ deps = { "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", "filename": "openjpeg-2.5.0.tar.gz", "dir": "openjpeg-2.5.0", + "license": "LICENSE", "build": [ cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), @@ -264,6 +278,7 @@ deps = { "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501 "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", + "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { "if(OPENMP_FOUND)": "if(false)", @@ -284,6 +299,7 @@ deps = { "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", "filename": "harfbuzz-5.1.0.zip", "dir": "harfbuzz-5.1.0", + "license": "COPYING", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), @@ -296,6 +312,7 @@ deps = { "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", "filename": "fribidi-1.0.12.zip", "dir": "fribidi-1.0.12", + "license": "COPYING", "build": [ cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), cmd_cmake(), @@ -431,6 +448,21 @@ def build_dep(name): extract_dep(dep["url"], dep["filename"]) + licenses = dep["license"] + if isinstance(licenses, str): + licenses = [licenses] + license_text = "" + for license_file in licenses: + with open(os.path.join(sources_dir, dir, license_file)) as f: + license_text += f.read() + if "license_pattern" in dep: + match = re.search(dep["license_pattern"], license_text, re.DOTALL) + license_text = "\n".join(match.groups()) + assert len(license_text) > 50 + with open(os.path.join(license_dir, f"{dir}.txt"), "w") as f: + print(f"Writing license {dir}.txt") + f.write(license_text) + for patch_file, patch_list in dep.get("patch", {}).items(): patch_file = os.path.join(sources_dir, dir, patch_file.format(**prefs)) with open(patch_file) as f: @@ -551,10 +583,12 @@ if __name__ == "__main__": bin_dir = os.path.join(build_dir, "bin") # directory for storing project files sources_dir = build_dir + sources_dir + # copy dependency licenses to this directory + license_dir = os.path.join(build_dir, "license") shutil.rmtree(build_dir, ignore_errors=True) os.makedirs(build_dir, exist_ok=False) - for path in [inc_dir, lib_dir, bin_dir, sources_dir]: + for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) prefs = { @@ -572,6 +606,7 @@ if __name__ == "__main__": "lib_dir": lib_dir, "bin_dir": bin_dir, "src_dir": sources_dir, + "license_dir": license_dir, # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically From 9699a0e1d6ee4859f1b9da0e9e7a45082f9f4c69 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 10:13:52 +0200 Subject: [PATCH 008/205] test libtiff with lzma --- Tests/images/hopper_lzma.tif | Bin 0 -> 30789 bytes Tests/test_file_libtiff.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 Tests/images/hopper_lzma.tif diff --git a/Tests/images/hopper_lzma.tif b/Tests/images/hopper_lzma.tif new file mode 100644 index 0000000000000000000000000000000000000000..d7ca089fc40949c731fdc7f354ff1072f026039a GIT binary patch literal 30789 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

yOGTie&81$CleX~bmQxxgb>64rCPjpf6|)jF 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 Date: Mon, 5 Sep 2022 11:05:18 +0200 Subject: [PATCH 009/205] compile libtiff with liblzma on windows --- .github/workflows/test-windows.yml | 4 +++ winbuild/build_prepare.py | 41 +++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ba72a7018..23d907806 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,6 +86,10 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_zlib.cmd" + - name: Build dependencies / xz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_xz.cmd" + - name: Build dependencies / LibTiff if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libtiff.cmd" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e878f4c8e..0ac9d3c81 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -151,11 +151,46 @@ deps = { "headers": [r"z*.h"], "libs": [r"*.lib"], }, + "xz": { + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.6.tar.gz/download", + "filename": "xz-5.2.6.tar.gz", + "dir": "xz-5.2.6", + "license": "COPYING", + "patch": { + r"src\liblzma\api\lzma.h": { + "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 + }, + r"windows\vs2019\liblzma.vcxproj": { + # retarget to default toolset (selected by vcvarsall.bat) + "v142": "$(DefaultPlatformToolset)", # noqa: E501 + # retarget to latest (selected by vcvarsall.bat) + "10.0": "$(WindowsSDKVersion)", # noqa: E501 + }, + }, + "build": [ + cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), + cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + cmd_mkdir(r"{inc_dir}\lzma"), + cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), + ], + "headers": [r"src\liblzma\api\lzma.h"], + "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", "filename": "tiff-4.4.0.tar.gz", "dir": "tiff-4.4.0", "license": "COPYRIGHT", + "patch": { + r"cmake\LZMACodec.cmake": { + # fix typo + "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}", + }, + r"libtiff\tif_lzma.c": { + # link against liblzma.lib + "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 + }, + }, "build": [ cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_nmake(target="clean"), @@ -216,7 +251,7 @@ deps = { "": "zlib.lib;libpng16.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { - # link against harfbuzz.lib once it becomes available + # link against harfbuzz.lib "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 }, }, @@ -423,8 +458,8 @@ def write_script(name, lines): name = os.path.join(build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) - with open(name, "w") as f: - f.write("\n\r".join(lines)) + with open(name, "w", newline="") as f: + f.write(os.linesep.join(lines)) if verbose: for line in lines: print(" " + line) From a7df33551ccffc6aea7ba4c65ccc4f769e1eaede Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 11:58:12 +0200 Subject: [PATCH 010/205] libopenjpeg has no dependencies, skip searching for dependencies of openjpeg binaries on windows --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0ac9d3c81..9772883d3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -300,7 +300,7 @@ deps = { "dir": "openjpeg-2.5.0", "license": "LICENSE", "build": [ - cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), + cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), cmd_nmake(target="openjp2"), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), From ffedfe034a50318e579a5d389537b65f91f3b209 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 11:58:42 +0200 Subject: [PATCH 011/205] test libtiff with webp compression --- Tests/images/hopper_webp.png | Bin 0 -> 27598 bytes Tests/images/hopper_webp.tif | Bin 0 -> 3651 bytes Tests/test_file_libtiff.py | 13 +++++++++++++ 3 files changed, 13 insertions(+) create mode 100644 Tests/images/hopper_webp.png create mode 100644 Tests/images/hopper_webp.tif diff --git a/Tests/images/hopper_webp.png b/Tests/images/hopper_webp.png new file mode 100644 index 0000000000000000000000000000000000000000..94b927ac24a337fd22d74cededf764dfeed026ef GIT binary patch literal 27598 zcmV*bKvchpP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 Date: Mon, 5 Sep 2022 12:48:42 +0200 Subject: [PATCH 012/205] compile libtiff with webp on windows --- .github/workflows/test-windows.yml | 8 +++--- winbuild/build_prepare.py | 43 ++++++++++++++++++------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 23d907806..e6452ceb4 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -90,14 +90,14 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_xz.cmd" - - name: Build dependencies / LibTiff - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libtiff.cmd" - - name: Build dependencies / WebP if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libwebp.cmd" + - name: Build dependencies / LibTiff + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libtiff.cmd" + # for FreeType CBDT/SBIX font support - name: Build dependencies / libpng if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9772883d3..dfcbbb979 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,28 @@ deps = { "headers": [r"src\liblzma\api\lzma.h"], "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], }, + "libwebp": { + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", + "filename": "libwebp-1.2.4.tar.gz", + "dir": "libwebp-1.2.4", + "license": "COPYING", + "build": [ + cmd_rmdir(r"output\release-static"), # clean + cmd_nmake( + "Makefile.vc", + "all", + [ + "CFG=release-static", + "OBJDIR=output", + "ARCH={architecture}", + "LIBWEBP_BASENAME=webp", + ], + ), + cmd_mkdir(r"{inc_dir}\webp"), + cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), + ], + "libs": [r"output\release-static\{architecture}\lib\*.lib"], + }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", "filename": "tiff-4.4.0.tar.gz", @@ -190,6 +212,10 @@ deps = { # link against liblzma.lib "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, + r"libtiff\tif_webp.c": { + # link against webp.lib + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + }, }, "build": [ cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -200,23 +226,6 @@ deps = { "libs": [r"libtiff\*.lib"], # "bins": [r"libtiff\*.dll"], }, - "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", - "filename": "libwebp-1.2.4.tar.gz", - "dir": "libwebp-1.2.4", - "license": "COPYING", - "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - ["CFG=release-static", "OBJDIR=output", "ARCH={architecture}"], - ), - cmd_mkdir(r"{inc_dir}\webp"), - cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), - ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], - }, "libpng": { "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.37/lpng1637.zip/download", "filename": "lpng1637.zip", From b22c66eeb80309bd14c5f3b2d802ab7a8b66e874 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 13:44:12 +0200 Subject: [PATCH 013/205] skip libtiif webp test when libtiff is too old --- Tests/test_file_libtiff.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e32ce87af..f1d290a32 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -3,6 +3,7 @@ import io import itertools import os import re +import sys from collections import namedtuple import pytest @@ -844,6 +845,8 @@ class TestFileLibTiff(LibTiffTestCase): captured = capfd.readouterr() if "LZMA compression support is not configured" in captured.err: pytest.skip("LZMA compression support is not configured") + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) raise def test_webp(self, capfd): @@ -857,6 +860,15 @@ class TestFileLibTiff(LibTiffTestCase): captured = capfd.readouterr() if "WEBP compression support is not configured" in captured.err: pytest.skip("WEBP compression support is not configured") + if ( + "Compression scheme 50001 strip decoding is not implemented" + in captured.err + ): + pytest.skip( + "Compression scheme 50001 strip decoding is not implemented" + ) + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) raise def test_lzw(self): From 8b1f92a7567a9c8a55b0c6809e072c277b869f5f Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 5 Sep 2022 14:58:41 +0200 Subject: [PATCH 014/205] restore py_vcruntime_redist --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index dfcbbb979..2dafb3d18 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -553,6 +553,7 @@ def build_pillow(): cmd_cd("{pillow_dir}"), *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 ] From 17e5f1eb3b6b68a1775064194a85f3a562c5faca Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 7 Sep 2022 19:31:31 +0200 Subject: [PATCH 015/205] add recommended build flag for webp set to the default value --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2dafb3d18..104d52ac5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -188,6 +188,7 @@ deps = { "all", [ "CFG=release-static", + "RTLIBCFG=dynamic", "OBJDIR=output", "ARCH={architecture}", "LIBWEBP_BASENAME=webp", From 2f95e49b3659203c4e8d166280f5807f51a64306 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 7 Sep 2022 19:34:34 +0200 Subject: [PATCH 016/205] add test using woff2 font with freetype --- Tests/fonts/LICENSE.txt | 1 + Tests/fonts/OpenSans.woff2 | Bin 0 -> 16740 bytes Tests/images/test_woff2.png | Bin 0 -> 6724 bytes Tests/test_imagefont.py | 18 ++++++++++++++++++ 4 files changed, 19 insertions(+) create mode 100644 Tests/fonts/OpenSans.woff2 create mode 100644 Tests/images/test_woff2.png diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 104ff677c..da559b3d3 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -8,6 +8,7 @@ TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ter-x20b.pcf, from http://terminus-font.sourceforge.net/ BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee +OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. diff --git a/Tests/fonts/OpenSans.woff2 b/Tests/fonts/OpenSans.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..15339ea9ccdd3bc54b25dfe54800f1c3bb730d26 GIT binary patch literal 16740 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 Qc*TNHRn$@_xohhGKUSIoPyhe` literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 16da87d46..b9a0f6db5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1064,6 +1064,24 @@ class TestImageFont: assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) + def test_woff2(self): + try: + font = ImageFont.truetype( + "Tests/fonts/OpenSans.woff2", + size=64, + layout_engine=self.LAYOUT_ENGINE, + ) + except OSError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("FreeType compiled without brotli or WOFF2 support") + + im = Image.new("RGB", (350, 100), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "OpenSans", "black", font=font) + + assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) + def test_fill_deprecation(self): font = self.get_font() with pytest.warns(DeprecationWarning): From e9af622a2bb67548172dea903f9708e4ffd7f0fb Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 7 Sep 2022 19:59:55 +0200 Subject: [PATCH 017/205] build brotli on Windows --- .github/workflows/test-windows.yml | 5 +++++ winbuild/build_prepare.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e6452ceb4..a231bb78c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -103,6 +103,11 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + # for FreeType WOFF2 font support + - name: Build dependencies / brotli + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_brotli.cmd" + - name: Build dependencies / FreeType if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_freetype.cmd" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 104d52ac5..e289027fe 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -242,6 +242,20 @@ deps = { "headers": [r"png*.h"], "libs": [r"libpng16.lib"], }, + "brotli": { + "url": "https://github.com/google/brotli/archive/refs/tags/v1.0.9.tar.gz", + "filename": "brotli-1.0.9.tar.gz", + "dir": "brotli-1.0.9", + "license": "LICENSE", + "build": [ + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(target="brotlicommon-static"), + cmd_nmake(target="brotlidec-static"), + cmd_xcopy(r"c\include", "{inc_dir}"), + ], + "libs": ["*.lib"], + }, "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 "filename": "freetype-2.12.1.tar.gz", @@ -255,10 +269,10 @@ deps = { '': '\n $(WindowsSDKVersion)', # noqa: E501 }, r"builds\windows\vc2010\freetype.user.props": { - "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ", # noqa: E501 + "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 "": "{lib_dir}", # noqa: E501 - "": "zlib.lib;libpng16.lib", # noqa: E501 + "": "zlib.lib;libpng16.lib;brotlicommon-static.lib;brotlidec-static.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { # link against harfbuzz.lib From d03f35b5bb75cd991187945d1a4a0050b680f04c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Oct 2022 17:24:35 +1000 Subject: [PATCH 018/205] Added enums --- docs/reference/ExifTags.rst | 25 +- src/PIL/ExifTags.py | 619 ++++++++++++++++++------------------ 2 files changed, 336 insertions(+), 308 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 794fa238f..ff5788524 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -11,7 +11,7 @@ provide constants and clear-text names for various well-known EXIF tags. :type: dict The TAGS dictionary maps 16-bit integer EXIF tag enumerations to - descriptive string names. For instance: + descriptive string names. For instance: >>> from PIL.ExifTags import TAGS >>> TAGS[0x010e] @@ -20,9 +20,28 @@ provide constants and clear-text names for various well-known EXIF tags. .. py:data:: GPSTAGS :type: dict - The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to - descriptive string names. For instance: + The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to + descriptive string names. For instance: >>> from PIL.ExifTags import GPSTAGS >>> GPSTAGS[20] 'GPSDestLatitude' + + +These values are also exposed as ``enum.IntEnum`` classes. + +.. py:data:: Base + + >>> from PIL.ExifTags import Base + >>> Base.ImageDescription.value + 270 + >>> Base(270).name + 'ImageDescription' + +.. py:data:: GPS + + >>> from PIL.ExifTags import GPS + >>> GPS.GPSDestLatitude.value + 20 + >>> GPS(20).name + 'GPSDestLatitude' diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 7da2ddae5..f3a73bf1a 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -14,318 +14,327 @@ This module provides constants and clear-text names for various well-known EXIF tags. """ +from enum import IntEnum -TAGS = { + +class Base(IntEnum): # possibly incomplete - 0x0001: "InteropIndex", - 0x000B: "ProcessingSoftware", - 0x00FE: "NewSubfileType", - 0x00FF: "SubfileType", - 0x0100: "ImageWidth", - 0x0101: "ImageLength", - 0x0102: "BitsPerSample", - 0x0103: "Compression", - 0x0106: "PhotometricInterpretation", - 0x0107: "Thresholding", - 0x0108: "CellWidth", - 0x0109: "CellLength", - 0x010A: "FillOrder", - 0x010D: "DocumentName", - 0x010E: "ImageDescription", - 0x010F: "Make", - 0x0110: "Model", - 0x0111: "StripOffsets", - 0x0112: "Orientation", - 0x0115: "SamplesPerPixel", - 0x0116: "RowsPerStrip", - 0x0117: "StripByteCounts", - 0x0118: "MinSampleValue", - 0x0119: "MaxSampleValue", - 0x011A: "XResolution", - 0x011B: "YResolution", - 0x011C: "PlanarConfiguration", - 0x011D: "PageName", - 0x0120: "FreeOffsets", - 0x0121: "FreeByteCounts", - 0x0122: "GrayResponseUnit", - 0x0123: "GrayResponseCurve", - 0x0124: "T4Options", - 0x0125: "T6Options", - 0x0128: "ResolutionUnit", - 0x0129: "PageNumber", - 0x012D: "TransferFunction", - 0x0131: "Software", - 0x0132: "DateTime", - 0x013B: "Artist", - 0x013C: "HostComputer", - 0x013D: "Predictor", - 0x013E: "WhitePoint", - 0x013F: "PrimaryChromaticities", - 0x0140: "ColorMap", - 0x0141: "HalftoneHints", - 0x0142: "TileWidth", - 0x0143: "TileLength", - 0x0144: "TileOffsets", - 0x0145: "TileByteCounts", - 0x014A: "SubIFDs", - 0x014C: "InkSet", - 0x014D: "InkNames", - 0x014E: "NumberOfInks", - 0x0150: "DotRange", - 0x0151: "TargetPrinter", - 0x0152: "ExtraSamples", - 0x0153: "SampleFormat", - 0x0154: "SMinSampleValue", - 0x0155: "SMaxSampleValue", - 0x0156: "TransferRange", - 0x0157: "ClipPath", - 0x0158: "XClipPathUnits", - 0x0159: "YClipPathUnits", - 0x015A: "Indexed", - 0x015B: "JPEGTables", - 0x015F: "OPIProxy", - 0x0200: "JPEGProc", - 0x0201: "JpegIFOffset", - 0x0202: "JpegIFByteCount", - 0x0203: "JpegRestartInterval", - 0x0205: "JpegLosslessPredictors", - 0x0206: "JpegPointTransforms", - 0x0207: "JpegQTables", - 0x0208: "JpegDCTables", - 0x0209: "JpegACTables", - 0x0211: "YCbCrCoefficients", - 0x0212: "YCbCrSubSampling", - 0x0213: "YCbCrPositioning", - 0x0214: "ReferenceBlackWhite", - 0x02BC: "XMLPacket", - 0x1000: "RelatedImageFileFormat", - 0x1001: "RelatedImageWidth", - 0x1002: "RelatedImageLength", - 0x4746: "Rating", - 0x4749: "RatingPercent", - 0x800D: "ImageID", - 0x828D: "CFARepeatPatternDim", - 0x828E: "CFAPattern", - 0x828F: "BatteryLevel", - 0x8298: "Copyright", - 0x829A: "ExposureTime", - 0x829D: "FNumber", - 0x83BB: "IPTCNAA", - 0x8649: "ImageResources", - 0x8769: "ExifOffset", - 0x8773: "InterColorProfile", - 0x8822: "ExposureProgram", - 0x8824: "SpectralSensitivity", - 0x8825: "GPSInfo", - 0x8827: "ISOSpeedRatings", - 0x8828: "OECF", - 0x8829: "Interlace", - 0x882A: "TimeZoneOffset", - 0x882B: "SelfTimerMode", - 0x8830: "SensitivityType", - 0x8831: "StandardOutputSensitivity", - 0x8832: "RecommendedExposureIndex", - 0x8833: "ISOSpeed", - 0x8834: "ISOSpeedLatitudeyyy", - 0x8835: "ISOSpeedLatitudezzz", - 0x9000: "ExifVersion", - 0x9003: "DateTimeOriginal", - 0x9004: "DateTimeDigitized", - 0x9010: "OffsetTime", - 0x9011: "OffsetTimeOriginal", - 0x9012: "OffsetTimeDigitized", - 0x9101: "ComponentsConfiguration", - 0x9102: "CompressedBitsPerPixel", - 0x9201: "ShutterSpeedValue", - 0x9202: "ApertureValue", - 0x9203: "BrightnessValue", - 0x9204: "ExposureBiasValue", - 0x9205: "MaxApertureValue", - 0x9206: "SubjectDistance", - 0x9207: "MeteringMode", - 0x9208: "LightSource", - 0x9209: "Flash", - 0x920A: "FocalLength", - 0x920B: "FlashEnergy", + InteropIndex = 0x0001 + ProcessingSoftware = 0x000B + NewSubfileType = 0x00FE + SubfileType = 0x00FF + ImageWidth = 0x0100 + ImageLength = 0x0101 + BitsPerSample = 0x0102 + Compression = 0x0103 + PhotometricInterpretation = 0x0106 + Thresholding = 0x0107 + CellWidth = 0x0108 + CellLength = 0x0109 + FillOrder = 0x010A + DocumentName = 0x010D + ImageDescription = 0x010E + Make = 0x010F + Model = 0x0110 + StripOffsets = 0x0111 + Orientation = 0x0112 + SamplesPerPixel = 0x0115 + RowsPerStrip = 0x0116 + StripByteCounts = 0x0117 + MinSampleValue = 0x0118 + MaxSampleValue = 0x0119 + XResolution = 0x011A + YResolution = 0x011B + PlanarConfiguration = 0x011C + PageName = 0x011D + FreeOffsets = 0x0120 + FreeByteCounts = 0x0121 + GrayResponseUnit = 0x0122 + GrayResponseCurve = 0x0123 + T4Options = 0x0124 + T6Options = 0x0125 + ResolutionUnit = 0x0128 + PageNumber = 0x0129 + TransferFunction = 0x012D + Software = 0x0131 + DateTime = 0x0132 + Artist = 0x013B + HostComputer = 0x013C + Predictor = 0x013D + WhitePoint = 0x013E + PrimaryChromaticities = 0x013F + ColorMap = 0x0140 + HalftoneHints = 0x0141 + TileWidth = 0x0142 + TileLength = 0x0143 + TileOffsets = 0x0144 + TileByteCounts = 0x0145 + SubIFDs = 0x014A + InkSet = 0x014C + InkNames = 0x014D + NumberOfInks = 0x014E + DotRange = 0x0150 + TargetPrinter = 0x0151 + ExtraSamples = 0x0152 + SampleFormat = 0x0153 + SMinSampleValue = 0x0154 + SMaxSampleValue = 0x0155 + TransferRange = 0x0156 + ClipPath = 0x0157 + XClipPathUnits = 0x0158 + YClipPathUnits = 0x0159 + Indexed = 0x015A + JPEGTables = 0x015B + OPIProxy = 0x015F + JPEGProc = 0x0200 + JpegIFOffset = 0x0201 + JpegIFByteCount = 0x0202 + JpegRestartInterval = 0x0203 + JpegLosslessPredictors = 0x0205 + JpegPointTransforms = 0x0206 + JpegQTables = 0x0207 + JpegDCTables = 0x0208 + JpegACTables = 0x0209 + YCbCrCoefficients = 0x0211 + YCbCrSubSampling = 0x0212 + YCbCrPositioning = 0x0213 + ReferenceBlackWhite = 0x0214 + XMLPacket = 0x02BC + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageLength = 0x1002 + Rating = 0x4746 + RatingPercent = 0x4749 + ImageID = 0x800D + CFARepeatPatternDim = 0x828D + BatteryLevel = 0x828F + Copyright = 0x8298 + ExposureTime = 0x829A + FNumber = 0x829D + IPTCNAA = 0x83BB + ImageResources = 0x8649 + ExifOffset = 0x8769 + InterColorProfile = 0x8773 + ExposureProgram = 0x8822 + SpectralSensitivity = 0x8824 + GPSInfo = 0x8825 + ISOSpeedRatings = 0x8827 + OECF = 0x8828 + Interlace = 0x8829 + TimeZoneOffset = 0x882A + SelfTimerMode = 0x882B + SensitivityType = 0x8830 + StandardOutputSensitivity = 0x8831 + RecommendedExposureIndex = 0x8832 + ISOSpeed = 0x8833 + ISOSpeedLatitudeyyy = 0x8834 + ISOSpeedLatitudezzz = 0x8835 + ExifVersion = 0x9000 + DateTimeOriginal = 0x9003 + DateTimeDigitized = 0x9004 + OffsetTime = 0x9010 + OffsetTimeOriginal = 0x9011 + OffsetTimeDigitized = 0x9012 + ComponentsConfiguration = 0x9101 + CompressedBitsPerPixel = 0x9102 + ShutterSpeedValue = 0x9201 + ApertureValue = 0x9202 + BrightnessValue = 0x9203 + ExposureBiasValue = 0x9204 + MaxApertureValue = 0x9205 + SubjectDistance = 0x9206 + MeteringMode = 0x9207 + LightSource = 0x9208 + Flash = 0x9209 + FocalLength = 0x920A + Noise = 0x920D + ImageNumber = 0x9211 + SecurityClassification = 0x9212 + ImageHistory = 0x9213 + TIFFEPStandardID = 0x9216 + MakerNote = 0x927C + UserComment = 0x9286 + SubsecTime = 0x9290 + SubsecTimeOriginal = 0x9291 + SubsecTimeDigitized = 0x9292 + AmbientTemperature = 0x9400 + Humidity = 0x9401 + Pressure = 0x9402 + WaterDepth = 0x9403 + Acceleration = 0x9404 + CameraElevationAngle = 0x9405 + XPTitle = 0x9C9B + XPComment = 0x9C9C + XPAuthor = 0x9C9D + XPKeywords = 0x9C9E + XPSubject = 0x9C9F + FlashPixVersion = 0xA000 + ColorSpace = 0xA001 + ExifImageWidth = 0xA002 + ExifImageHeight = 0xA003 + RelatedSoundFile = 0xA004 + ExifInteroperabilityOffset = 0xA005 + FlashEnergy = 0xA20B + SpatialFrequencyResponse = 0xA20C + FocalPlaneXResolution = 0xA20E + FocalPlaneYResolution = 0xA20F + FocalPlaneResolutionUnit = 0xA210 + SubjectLocation = 0xA214 + ExposureIndex = 0xA215 + SensingMethod = 0xA217 + FileSource = 0xA300 + SceneType = 0xA301 + CFAPattern = 0xA302 + CustomRendered = 0xA401 + ExposureMode = 0xA402 + WhiteBalance = 0xA403 + DigitalZoomRatio = 0xA404 + FocalLengthIn35mmFilm = 0xA405 + SceneCaptureType = 0xA406 + GainControl = 0xA407 + Contrast = 0xA408 + Saturation = 0xA409 + Sharpness = 0xA40A + DeviceSettingDescription = 0xA40B + SubjectDistanceRange = 0xA40C + ImageUniqueID = 0xA420 + CameraOwnerName = 0xA430 + BodySerialNumber = 0xA431 + LensSpecification = 0xA432 + LensMake = 0xA433 + LensModel = 0xA434 + LensSerialNumber = 0xA435 + CompositeImage = 0xA460 + CompositeImageCount = 0xA461 + CompositeImageExposureTimes = 0xA462 + Gamma = 0xA500 + PrintImageMatching = 0xC4A5 + DNGVersion = 0xC612 + DNGBackwardVersion = 0xC613 + UniqueCameraModel = 0xC614 + LocalizedCameraModel = 0xC615 + CFAPlaneColor = 0xC616 + CFALayout = 0xC617 + LinearizationTable = 0xC618 + BlackLevelRepeatDim = 0xC619 + BlackLevel = 0xC61A + BlackLevelDeltaH = 0xC61B + BlackLevelDeltaV = 0xC61C + WhiteLevel = 0xC61D + DefaultScale = 0xC61E + DefaultCropOrigin = 0xC61F + DefaultCropSize = 0xC620 + ColorMatrix1 = 0xC621 + ColorMatrix2 = 0xC622 + CameraCalibration1 = 0xC623 + CameraCalibration2 = 0xC624 + ReductionMatrix1 = 0xC625 + ReductionMatrix2 = 0xC626 + AnalogBalance = 0xC627 + AsShotNeutral = 0xC628 + AsShotWhiteXY = 0xC629 + BaselineExposure = 0xC62A + BaselineNoise = 0xC62B + BaselineSharpness = 0xC62C + BayerGreenSplit = 0xC62D + LinearResponseLimit = 0xC62E + CameraSerialNumber = 0xC62F + LensInfo = 0xC630 + ChromaBlurRadius = 0xC631 + AntiAliasStrength = 0xC632 + ShadowScale = 0xC633 + DNGPrivateData = 0xC634 + MakerNoteSafety = 0xC635 + CalibrationIlluminant1 = 0xC65A + CalibrationIlluminant2 = 0xC65B + BestQualityScale = 0xC65C + RawDataUniqueID = 0xC65D + OriginalRawFileName = 0xC68B + OriginalRawFileData = 0xC68C + ActiveArea = 0xC68D + MaskedAreas = 0xC68E + AsShotICCProfile = 0xC68F + AsShotPreProfileMatrix = 0xC690 + CurrentICCProfile = 0xC691 + CurrentPreProfileMatrix = 0xC692 + ColorimetricReference = 0xC6BF + CameraCalibrationSignature = 0xC6F3 + ProfileCalibrationSignature = 0xC6F4 + AsShotProfileName = 0xC6F6 + NoiseReductionApplied = 0xC6F7 + ProfileName = 0xC6F8 + ProfileHueSatMapDims = 0xC6F9 + ProfileHueSatMapData1 = 0xC6FA + ProfileHueSatMapData2 = 0xC6FB + ProfileToneCurve = 0xC6FC + ProfileEmbedPolicy = 0xC6FD + ProfileCopyright = 0xC6FE + ForwardMatrix1 = 0xC714 + ForwardMatrix2 = 0xC715 + PreviewApplicationName = 0xC716 + PreviewApplicationVersion = 0xC717 + PreviewSettingsName = 0xC718 + PreviewSettingsDigest = 0xC719 + PreviewColorSpace = 0xC71A + PreviewDateTime = 0xC71B + RawImageDigest = 0xC71C + OriginalRawFileDigest = 0xC71D + SubTileBlockSize = 0xC71E + RowInterleaveFactor = 0xC71F + ProfileLookTableDims = 0xC725 + ProfileLookTableData = 0xC726 + OpcodeList1 = 0xC740 + OpcodeList2 = 0xC741 + OpcodeList3 = 0xC74E + NoiseProfile = 0xC761 + + +"""Maps EXIF tags to tag names.""" +TAGS = { + **{i.value: i.name for i in Base}, 0x920C: "SpatialFrequencyResponse", - 0x920D: "Noise", - 0x9211: "ImageNumber", - 0x9212: "SecurityClassification", - 0x9213: "ImageHistory", 0x9214: "SubjectLocation", 0x9215: "ExposureIndex", + 0x828E: "CFAPattern", + 0x920B: "FlashEnergy", 0x9216: "TIFF/EPStandardID", - 0x927C: "MakerNote", - 0x9286: "UserComment", - 0x9290: "SubsecTime", - 0x9291: "SubsecTimeOriginal", - 0x9292: "SubsecTimeDigitized", - 0x9400: "AmbientTemperature", - 0x9401: "Humidity", - 0x9402: "Pressure", - 0x9403: "WaterDepth", - 0x9404: "Acceleration", - 0x9405: "CameraElevationAngle", - 0x9C9B: "XPTitle", - 0x9C9C: "XPComment", - 0x9C9D: "XPAuthor", - 0x9C9E: "XPKeywords", - 0x9C9F: "XPSubject", - 0xA000: "FlashPixVersion", - 0xA001: "ColorSpace", - 0xA002: "ExifImageWidth", - 0xA003: "ExifImageHeight", - 0xA004: "RelatedSoundFile", - 0xA005: "ExifInteroperabilityOffset", - 0xA20B: "FlashEnergy", - 0xA20C: "SpatialFrequencyResponse", - 0xA20E: "FocalPlaneXResolution", - 0xA20F: "FocalPlaneYResolution", - 0xA210: "FocalPlaneResolutionUnit", - 0xA214: "SubjectLocation", - 0xA215: "ExposureIndex", - 0xA217: "SensingMethod", - 0xA300: "FileSource", - 0xA301: "SceneType", - 0xA302: "CFAPattern", - 0xA401: "CustomRendered", - 0xA402: "ExposureMode", - 0xA403: "WhiteBalance", - 0xA404: "DigitalZoomRatio", - 0xA405: "FocalLengthIn35mmFilm", - 0xA406: "SceneCaptureType", - 0xA407: "GainControl", - 0xA408: "Contrast", - 0xA409: "Saturation", - 0xA40A: "Sharpness", - 0xA40B: "DeviceSettingDescription", - 0xA40C: "SubjectDistanceRange", - 0xA420: "ImageUniqueID", - 0xA430: "CameraOwnerName", - 0xA431: "BodySerialNumber", - 0xA432: "LensSpecification", - 0xA433: "LensMake", - 0xA434: "LensModel", - 0xA435: "LensSerialNumber", - 0xA460: "CompositeImage", - 0xA461: "CompositeImageCount", - 0xA462: "CompositeImageExposureTimes", - 0xA500: "Gamma", - 0xC4A5: "PrintImageMatching", - 0xC612: "DNGVersion", - 0xC613: "DNGBackwardVersion", - 0xC614: "UniqueCameraModel", - 0xC615: "LocalizedCameraModel", - 0xC616: "CFAPlaneColor", - 0xC617: "CFALayout", - 0xC618: "LinearizationTable", - 0xC619: "BlackLevelRepeatDim", - 0xC61A: "BlackLevel", - 0xC61B: "BlackLevelDeltaH", - 0xC61C: "BlackLevelDeltaV", - 0xC61D: "WhiteLevel", - 0xC61E: "DefaultScale", - 0xC61F: "DefaultCropOrigin", - 0xC620: "DefaultCropSize", - 0xC621: "ColorMatrix1", - 0xC622: "ColorMatrix2", - 0xC623: "CameraCalibration1", - 0xC624: "CameraCalibration2", - 0xC625: "ReductionMatrix1", - 0xC626: "ReductionMatrix2", - 0xC627: "AnalogBalance", - 0xC628: "AsShotNeutral", - 0xC629: "AsShotWhiteXY", - 0xC62A: "BaselineExposure", - 0xC62B: "BaselineNoise", - 0xC62C: "BaselineSharpness", - 0xC62D: "BayerGreenSplit", - 0xC62E: "LinearResponseLimit", - 0xC62F: "CameraSerialNumber", - 0xC630: "LensInfo", - 0xC631: "ChromaBlurRadius", - 0xC632: "AntiAliasStrength", - 0xC633: "ShadowScale", - 0xC634: "DNGPrivateData", - 0xC635: "MakerNoteSafety", - 0xC65A: "CalibrationIlluminant1", - 0xC65B: "CalibrationIlluminant2", - 0xC65C: "BestQualityScale", - 0xC65D: "RawDataUniqueID", - 0xC68B: "OriginalRawFileName", - 0xC68C: "OriginalRawFileData", - 0xC68D: "ActiveArea", - 0xC68E: "MaskedAreas", - 0xC68F: "AsShotICCProfile", - 0xC690: "AsShotPreProfileMatrix", - 0xC691: "CurrentICCProfile", - 0xC692: "CurrentPreProfileMatrix", - 0xC6BF: "ColorimetricReference", - 0xC6F3: "CameraCalibrationSignature", - 0xC6F4: "ProfileCalibrationSignature", - 0xC6F6: "AsShotProfileName", - 0xC6F7: "NoiseReductionApplied", - 0xC6F8: "ProfileName", - 0xC6F9: "ProfileHueSatMapDims", - 0xC6FA: "ProfileHueSatMapData1", - 0xC6FB: "ProfileHueSatMapData2", - 0xC6FC: "ProfileToneCurve", - 0xC6FD: "ProfileEmbedPolicy", - 0xC6FE: "ProfileCopyright", - 0xC714: "ForwardMatrix1", - 0xC715: "ForwardMatrix2", - 0xC716: "PreviewApplicationName", - 0xC717: "PreviewApplicationVersion", - 0xC718: "PreviewSettingsName", - 0xC719: "PreviewSettingsDigest", - 0xC71A: "PreviewColorSpace", - 0xC71B: "PreviewDateTime", - 0xC71C: "RawImageDigest", - 0xC71D: "OriginalRawFileDigest", - 0xC71E: "SubTileBlockSize", - 0xC71F: "RowInterleaveFactor", - 0xC725: "ProfileLookTableDims", - 0xC726: "ProfileLookTableData", - 0xC740: "OpcodeList1", - 0xC741: "OpcodeList2", - 0xC74E: "OpcodeList3", - 0xC761: "NoiseProfile", } -"""Maps EXIF tags to tag names.""" -GPSTAGS = { - 0: "GPSVersionID", - 1: "GPSLatitudeRef", - 2: "GPSLatitude", - 3: "GPSLongitudeRef", - 4: "GPSLongitude", - 5: "GPSAltitudeRef", - 6: "GPSAltitude", - 7: "GPSTimeStamp", - 8: "GPSSatellites", - 9: "GPSStatus", - 10: "GPSMeasureMode", - 11: "GPSDOP", - 12: "GPSSpeedRef", - 13: "GPSSpeed", - 14: "GPSTrackRef", - 15: "GPSTrack", - 16: "GPSImgDirectionRef", - 17: "GPSImgDirection", - 18: "GPSMapDatum", - 19: "GPSDestLatitudeRef", - 20: "GPSDestLatitude", - 21: "GPSDestLongitudeRef", - 22: "GPSDestLongitude", - 23: "GPSDestBearingRef", - 24: "GPSDestBearing", - 25: "GPSDestDistanceRef", - 26: "GPSDestDistance", - 27: "GPSProcessingMethod", - 28: "GPSAreaInformation", - 29: "GPSDateStamp", - 30: "GPSDifferential", - 31: "GPSHPositioningError", -} +class GPS(IntEnum): + GPSVersionID = 0 + GPSLatitudeRef = 1 + GPSLatitude = 2 + GPSLongitudeRef = 3 + GPSLongitude = 4 + GPSAltitudeRef = 5 + GPSAltitude = 6 + GPSTimeStamp = 7 + GPSSatellites = 8 + GPSStatus = 9 + GPSMeasureMode = 10 + GPSDOP = 11 + GPSSpeedRef = 12 + GPSSpeed = 13 + GPSTrackRef = 14 + GPSTrack = 15 + GPSImgDirectionRef = 16 + GPSImgDirection = 17 + GPSMapDatum = 18 + GPSDestLatitudeRef = 19 + GPSDestLatitude = 20 + GPSDestLongitudeRef = 21 + GPSDestLongitude = 22 + GPSDestBearingRef = 23 + GPSDestBearing = 24 + GPSDestDistanceRef = 25 + GPSDestDistance = 26 + GPSProcessingMethod = 27 + GPSAreaInformation = 28 + GPSDateStamp = 29 + GPSDifferential = 30 + GPSHPositioningError = 31 + + """Maps EXIF GPS tags to tag names.""" +GPSTAGS = {i.value: i.name for i in GPS} From ed990abed4fe1c73d13fb4c17a8ca07af00718cf Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 2 Oct 2022 07:05:18 +0100 Subject: [PATCH 019/205] windows: update xz to 5.2.7, update libpng to 1.6.38 --- winbuild/build_prepare.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 414857fc2..573020ca3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.6.tar.gz/download", - "filename": "xz-5.2.6.tar.gz", - "dir": "xz-5.2.6", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download", + "filename": "xz-5.2.7.tar.gz", + "dir": "xz-5.2.7", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { @@ -228,9 +228,9 @@ deps = { # "bins": [r"libtiff\*.dll"], }, "libpng": { - "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.37/lpng1637.zip/download", - "filename": "lpng1637.zip", - "dir": "lpng1637", + "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download", + "filename": "lpng1638.zip", + "dir": "lpng1638", "license": "LICENSE", "build": [ # lint: do not inline From 59b04644218d7c21b82e1751b704f175c69d1f92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Oct 2022 20:04:02 +1100 Subject: [PATCH 020/205] Note when the security issue was introduced --- docs/releasenotes/9.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index 5a4086748..1753901fd 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -43,7 +43,7 @@ Within the BLP image format, BLP1 data may use JPEG compression. Instead of telling the JPEG library that this data is in BGRX mode, Pillow will now decode the data in its natural CMYK mode, then convert it to RGB and rearrange the channels afterwards. Trying to load the data in an incorrect mode could -result in a segmentation fault. +result in a segmentation fault. This issue was introduced in Pillow 9.1.0. Other Changes ============= From 46b0644c4f780e8f292492b007801bf6e81664c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Oct 2022 22:19:22 +1100 Subject: [PATCH 021/205] Do not modify previous frame when calculating delta --- Tests/test_file_apng.py | 6 ++++-- src/PIL/PngImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 1f5567163..51637c786 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -553,18 +553,20 @@ def test_apng_save_disposal(tmp_path): def test_apng_save_disposal_previous(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + blue = Image.new("RGBA", size, (0, 0, 255, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) # test OP_NONE - transparent.save( + blue.save( test_file, save_all=True, append_images=[red, green], disposal=PngImagePlugin.Disposal.OP_PREVIOUS, ) with Image.open(test_file) as im: + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7fb468877..2c53be109 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1128,7 +1128,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) prev_disposal = Disposal.OP_BACKGROUND if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous["im"] + base_im = previous["im"].copy() dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) bbox = previous["bbox"] if bbox: From d72779ac03b73f9f9538c8e7fc9f57c887452e88 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Oct 2022 09:03:15 +1100 Subject: [PATCH 022/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 060569ab5..6221fe551 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added ExifTags enums #6630 + [radarhere] + +- Do not modify previous frame when calculating delta in PNG #6683 + [radarhere] + - Added support for reading BMP images with RLE4 compression #6674 [npjg, radarhere] From 1324c55ddc7e8d7000395e5119633d1bd4aa68ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Oct 2022 10:16:47 +1100 Subject: [PATCH 023/205] Added release notes for #6630 --- docs/releasenotes/9.3.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index 1753901fd..a20ee4da6 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -32,6 +32,13 @@ Additional images can also be appended when saving, by combining the im.save(out, save_all=True, append_images=[im1, im2, ...]) +Added ExifTags enums +^^^^^^^^^^^^^^^^^^^^ + +The data from :py:data:`~PIL.ExifTags.TAGS` and +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` +classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. + Security ======== From 68b435ed86a9cd579cc8d0bb76831877828d990b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 25 Oct 2022 15:34:31 +0300 Subject: [PATCH 024/205] Test Python 3.11.0 final --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 36bd03e7e..f2c35f6a3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c8a1b85f..645384c02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: python-version: [ "pypy-3.8", "pypy-3.7", - "3.11-dev", + "3.11", "3.10", "3.9", "3.8", From 6788e8f95753be2b13b1b80458d026e6cb3eb9f0 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Wed, 26 Oct 2022 11:11:30 -0700 Subject: [PATCH 025/205] Fix malloc in _imagingft.c:font_setvaraxes --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 8f19b763c..4c3a37fb2 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1179,7 +1179,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { } num_coords = PyObject_Length(axes); - coords = malloc(2 * sizeof(coords)); + coords = (FT_Fixed*)malloc(num_coords * sizeof(FT_Fixed)); if (coords == NULL) { return PyErr_NoMemory(); } From d97db54be0b874199bbf666167c9ac15cebfb58e Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Wed, 26 Oct 2022 11:17:28 -0700 Subject: [PATCH 026/205] Only use ASCII characters in C source file --- src/libImaging/ColorLUT.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index fd6e268b5..aee7cda06 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -7,8 +7,8 @@ #define PRECISION_BITS (16 - 8 - 2) #define PRECISION_ROUNDING (1 << (PRECISION_BITS - 1)) -/* 8 — scales are multiplied on byte. - 6 — max index in the table +/* 8 - scales are multiplied on byte. + 6 - max index in the table (max size is 65, but index 64 is not reachable) */ #define SCALE_BITS (32 - 8 - 6) #define SCALE_MASK ((1 << SCALE_BITS) - 1) @@ -44,14 +44,14 @@ table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) Transforms colors of imIn using provided 3D lookup table and puts the result in imOut. Returns imOut on success or 0 on error. - imOut, imIn — images, should be the same size and may be the same image. + imOut, imIn - images, should be the same size and may be the same image. Should have 3 or 4 channels. - table_channels — number of channels in the lookup table, 3 or 4. + table_channels - number of channels in the lookup table, 3 or 4. Should be less or equal than number of channels in imOut image; - size1D, size_2D and size3D — dimensions of provided table; - table — flat table, - array with table_channels × size1D × size2D × size3D elements, - where channels are changed first, then 1D, then​ 2D, then 3D. + size1D, size_2D and size3D - dimensions of provided table; + table - flat table, + array with table_channels * size1D * size2D * size3D elements, + where channels are changed first, then 1D, then 2D, then 3D. Each element is signed 16-bit int where 0 is lowest output value and 255 << PRECISION_BITS (16320) is highest value. */ From 454b586f15397dc667699c50759c8cea0417b0ab Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 26 Oct 2022 22:02:13 +0300 Subject: [PATCH 027/205] Update release notes for 9.3.0 --- docs/releasenotes/9.3.0.rst | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index a20ee4da6..c05df4a4a 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -1,9 +1,6 @@ 9.3.0 ----- -Backwards Incompatible Changes -============================== - API Additions ============= @@ -55,11 +52,28 @@ result in a segmentation fault. This issue was introduced in Pillow 9.1.0. Other Changes ============= +Windows wheels +^^^^^^^^^^^^^^ + +This release contains wheels for Windows built using GitHub Actions. + +Previously they were built by `Christoph Gohlke `_. + +A huge thanks to Christoph for building Windows binaries for us for around a decade, +plus testing, and fixing over a hundred bug fixes along the way, in addition to building +and hosting unofficial Windows binaries for hundreds of Python projects! + Added DDS ATI1, ATI2 and BC6H reading ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. +Release GIL when converting images using matrix operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when converting images using matrix +operations. + Show all frames with ImageShow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From d3b471b2ae76372d73da64364e06e22807f57369 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Oct 2022 07:42:18 +1100 Subject: [PATCH 028/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6221fe551..b1d802891 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Release Python GIL when converting images using matrix operations #6418 + [hmaarrfk] + - Added ExifTags enums #6630 [radarhere] From fb0e7cdd91ca271c48ed50c66c4d93d678d325e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Oct 2022 22:33:20 +1100 Subject: [PATCH 029/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b1d802891..0724ced59 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Fix malloc in _imagingft.c:font_setvaraxes #6690 + [cgohlke] + - Release Python GIL when converting images using matrix operations #6418 [hmaarrfk] From 4ab80f663e879bb2b83ae2891c47125ff3c7ad6f Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Thu, 27 Oct 2022 08:15:36 -0700 Subject: [PATCH 030/205] Remove backup implementation of Round for Windows platforms --- src/libImaging/Convert.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index bdc680be4..2b45d0cc4 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -43,13 +43,6 @@ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) #define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) -#ifndef round -double -round(double x) { - return floor(x + 0.5); -} -#endif - /* ------------------- */ /* 1 (bit) conversions */ /* ------------------- */ From 3ffd2b2b8f2975e3da3a76c799fc4550c2a1531f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Oct 2022 13:02:19 +0300 Subject: [PATCH 031/205] Double quotes for old CPython on Windows --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 14c404752..7a1fabe23 100755 --- a/setup.py +++ b/setup.py @@ -15,9 +15,7 @@ import subprocess import sys import warnings -from setuptools import Extension -from setuptools import __version__ as setuptools_version -from setuptools import setup +from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -852,7 +850,6 @@ class pil_build_ext(build_ext): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) - and int(setuptools_version.split(".")[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: From b4bf2885f365b23e16772380173e971f89b208bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 28 Oct 2022 21:23:25 +1100 Subject: [PATCH 032/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0724ced59..74ff24f5c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Fixed set_variation_by_name offset #6445 + [radarhere] + - Fix malloc in _imagingft.c:font_setvaraxes #6690 [cgohlke] From 88ba3a0cb0f0ea3487135c157788a0fb5dd963db Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Oct 2022 22:54:08 +0300 Subject: [PATCH 033/205] Document 3.11 wheels in 9.3.0 release notes --- docs/releasenotes/9.3.0.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index c05df4a4a..e5a68ed9e 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -52,6 +52,15 @@ result in a segmentation fault. This issue was introduced in Pillow 9.1.0. Other Changes ============= +Python 3.11 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow 9.2.0 had wheels built against Python 3.11 beta, available as a preview to help +others prepare for 3.11, and ensure Pillow can be used immediately on release day of +3.11.0 final (2022-10-24, :pep:`664`). + +Pillow 9.3.0 now officially includes binary wheels for Python 3.11 final. + Windows wheels ^^^^^^^^^^^^^^ From 7ad021efb0a45ac78c3b021f799e3658ea9ece26 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 28 Oct 2022 22:51:09 +0100 Subject: [PATCH 034/205] GHA: use GITHUB_OUTPUT instead of deprecated set-output --- .github/workflows/test-windows.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 394b67de2..6b7f62c23 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,7 +65,9 @@ jobs: xcopy /S /Y winbuild\depends\test_images\* Tests\images\ # make cache key depend on VS version - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" | find """catalog_buildVersion""" | ForEach-Object { $a = $_.split(" ")[1]; echo "::set-output name=vs::$a" } + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` + | find """catalog_buildVersion""" ` + | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } shell: pwsh - name: Cache build @@ -212,7 +214,7 @@ jobs: ) ) ) - for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a + for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd From c3326da8a34104a78764dc2f74bf3686b858cf02 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 29 Oct 2022 11:31:20 +1100 Subject: [PATCH 035/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 74ff24f5c..86893f4db 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Remove backup implementation of Round for Windows platforms #6693 + [cgohlke] + - Fixed set_variation_by_name offset #6445 [radarhere] From e055ef0356414ce02541faab1009f5b4befbf78f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 29 Oct 2022 19:22:28 +1100 Subject: [PATCH 036/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 86893f4db..796ad23d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Inline fname2char to fix memory leak #6329 + [nulano] + +- Fix memory leaks related to text features #6330 + [nulano] + +- Use double quotes for version check on old CPython on Windows #6695 + [hugovk] + - Remove backup implementation of Round for Windows platforms #6693 [cgohlke] From 93e5fd4b4035096d5e089b3bc7f474e12da84dda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Aug 2022 19:12:09 +1000 Subject: [PATCH 037/205] Initialize libtiff buffer --- src/libImaging/TiffDecode.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 7663f96a9..428cd93d2 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -771,11 +771,11 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { TRACE(("Opening using fd: %d for writing \n", clientstate->fp)); clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); } else { - // malloc a buffer to write the tif, we're going to need to realloc or something + // calloc a buffer to write the tif, we're going to need to realloc or something // if we need bigger. TRACE(("Opening a buffer for writing \n")); - /* malloc check ok, small constant allocation */ - clientstate->data = malloc(bufsize); + /* calloc check ok, small constant allocation */ + clientstate->data = calloc(bufsize, 1); clientstate->size = bufsize; clientstate->flrealloc = 1; From fa71b1107343143c7afecdac578ec5150dd8c715 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Oct 2022 20:06:41 +1100 Subject: [PATCH 038/205] Revert "Temporarily skip valgrind failure" This reverts commit a3e61c1f89ea726d011683486ce81d6c448a2374. --- Tests/test_file_pdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 4129e8783..9667b6a4a 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -42,7 +42,6 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) -@pytest.mark.valgrind_known_error(reason="Temporary skip") def test_monochrome(tmp_path): # Arrange mode = "1" From 13f2c5ae14901c89c38f898496102afd9daeaf6d Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 28 Oct 2022 14:11:25 +0200 Subject: [PATCH 039/205] Prevent DOS with large SAMPLESPERPIXEL in Tiff IFD A large value in the SAMPLESPERPIXEL tag could lead to a memory and runtime DOS in TiffImagePlugin.py when setting up the context for image decoding. --- ...-225817ca0f8c663be7ab4b9e717b02c661e66834.tif | Bin 0 -> 88 bytes Tests/test_file_tiff.py | 15 ++++++++++++++- src/PIL/TiffImagePlugin.py | 8 ++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif new file mode 100644 index 0000000000000000000000000000000000000000..01dca594f53e22fda9b11ed5b704326680af1b8c GIT binary patch literal 88 zcmebD)MDUZU|`^8U|?isU<9(bfS3`=2FWl%*#bZ|Gn5Td$A-ifWn=;Cox~s{WCDf& DV5 MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error("More samples per pixel than can be decoded: %s", samples_per_pixel) + raise SyntaxError("Invalid value for samples per pixel") + if samples_per_pixel < bps_actual_count: # If a file has more values in bps_tuple than expected, # remove the excess. From 05b175ef88c22f5c416bc9b8d5b897dea1abbf2c Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 28 Oct 2022 14:46:20 +0200 Subject: [PATCH 040/205] Tighter test case --- Tests/test_file_tiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 47c4e1b13..752c1e61e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -868,7 +868,7 @@ class TestFileTiff: def test_oom(self, test_file): with pytest.raises(UnidentifiedImageError): with Image.open(test_file) as im: - im.load() + pass From 00b25fd3ac3648bc28eff5d4c4d816e605e3f05f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Oct 2022 18:02:24 +0300 Subject: [PATCH 041/205] Hide UserWarning in logs Tests/test_file_tiff.py::TestFileTiff::test_oom[Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif] PIL/TiffImagePlugin.py:850: UserWarning: Corrupt EXIF data. Expecting to read 12 bytes but only got 6. warnings.warn(str(msg)) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tiff.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 752c1e61e..5953dfa18 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -867,8 +867,9 @@ class TestFileTiff: @pytest.mark.timeout(2) def test_oom(self, test_file): with pytest.raises(UnidentifiedImageError): - with Image.open(test_file) as im: - pass + with pytest.warns(UserWarning): + with Image.open(test_file) as im: + pass From 799a6a01052cea3f417a571d7c64cd14acc18c64 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Oct 2022 18:03:38 +0300 Subject: [PATCH 042/205] Fix linting --- Tests/test_file_tiff.py | 3 +-- src/PIL/TiffImagePlugin.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 5953dfa18..4f3c8e390 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -868,11 +868,10 @@ class TestFileTiff: def test_oom(self, test_file): with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): - with Image.open(test_file) as im: + with Image.open(test_file): pass - @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: def test_fd_leak(self, tmp_path): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 46166fc63..1dfd5275f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1401,7 +1401,9 @@ class TiffImageFile(ImageFile.ImageFile): if samples_per_pixel > MAX_SAMPLESPERPIXEL: # DOS check, samples_per_pixel can be a Long, and we extend the tuple below - logger.error("More samples per pixel than can be decoded: %s", samples_per_pixel) + logger.error( + "More samples per pixel than can be decoded: %s", samples_per_pixel + ) raise SyntaxError("Invalid value for samples per pixel") if samples_per_pixel < bps_actual_count: From 0846bfae48513f2f51ca8547ed3b8954fa501fda Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Oct 2022 18:03:50 +0300 Subject: [PATCH 043/205] Add to release notes --- docs/releasenotes/9.3.0.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5a68ed9e..410666fc0 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -49,6 +49,15 @@ decode the data in its natural CMYK mode, then convert it to RGB and rearrange the channels afterwards. Trying to load the data in an incorrect mode could result in a segmentation fault. This issue was introduced in Pillow 9.1.0. +Limit SAMPLESPERPIXEL to avoid runtime DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A large value in the ``SAMPLESPERPIXEL`` tag could lead to a memory and runtime DOS in +``TiffImagePlugin.py`` when setting up the context for image decoding. +This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limiting +``SAMPLESPERPIXEL`` to the number of planes that we can decode. + + Other Changes ============= @@ -88,3 +97,5 @@ Show all frames with ImageShow When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, all frames will now be shown. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz From 744f455830871d61a8de0a5e629d4c2e33817cbb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Oct 2022 20:28:24 +1100 Subject: [PATCH 044/205] Added release notes --- docs/releasenotes/9.3.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5a68ed9e..a9acad6f8 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -40,6 +40,12 @@ classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. Security ======== +Initialize libtiff buffer when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a TIFF image to a file object using libtiff, the buffer was not +initialized. This behaviour introduced in Pillow 2.0.0, and has now been fixed. + Decode JPEG compressed BLP1 data in original mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 909dc64ed5f676169aa3d9b0c26f132a06321b83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 29 Oct 2022 15:21:20 +0300 Subject: [PATCH 045/205] 9.3.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 8e736a432..43896fabd 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.3.0.dev0" +__version__ = "9.3.0" From d594f4cb8dc47fb0c69ae58d9fff86faae4515bd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 29 Oct 2022 15:25:53 +0300 Subject: [PATCH 046/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 796ad23d6..6f2ba569e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,15 @@ Changelog (Pillow) ================== -9.3.0 (unreleased) +9.3.0 (2022-10-29) ------------------ +- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700 + [wiredfool] + +- Initialize libtiff buffer when saving #6699 + [radarhere] + - Inline fname2char to fix memory leak #6329 [nulano] From 4fc0a4ceb292dcb5856c0ef079c8271a324f8e70 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 29 Oct 2022 17:22:07 +0300 Subject: [PATCH 047/205] 9.4.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 43896fabd..1cc1d0f1c 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.3.0" +__version__ = "9.4.0.dev0" From 76b99756e425099b650bd383e204c259b5bdd3f1 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 29 Oct 2022 18:24:44 +0100 Subject: [PATCH 048/205] disable __CxxFrameHandler4 when compiling harfbuzz --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..bc19c5fa1 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -360,6 +360,7 @@ deps = { "dir": "harfbuzz-5.3.1", "license": "COPYING", "build": [ + cmd_set("CXXFLAGS", "-d2FH4-"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), cmd_nmake(target="harfbuzz"), From eaee7fda973d6915f7071f9b7f7edb7ad481ef7c Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 02:56:05 +0000 Subject: [PATCH 049/205] add xfail mark to test_image_access:test_embeddable --- Tests/test_image_access.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 955740b95..04bdfc5aa 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -406,11 +406,8 @@ class TestImagePutPixelError(AccessTest): class TestEmbeddable: - @pytest.mark.skipif( - not is_win32() or on_ci(), - reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " - "not from shell", - ) + @pytest.mark.xfail(reason="failing test") + @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes From 181fad2918f6ca24f4f149ebd29af73dda34d2cb Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 03:17:13 +0000 Subject: [PATCH 050/205] move import used only on Windows and remove unused import --- Tests/test_image_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 04bdfc5aa..f19db440d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,11 +4,10 @@ import sys import sysconfig import pytest -from setuptools.command.build_ext import new_compiler from PIL import Image -from .helper import assert_image_equal, hopper, is_win32, on_ci +from .helper import assert_image_equal, hopper, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -410,6 +409,7 @@ class TestEmbeddable: @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes + from setuptools.command.build_ext import new_compiler with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( From 23df3bda7f4e052c84dc71d3dbf417f0d8d03741 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 03:17:54 +0000 Subject: [PATCH 051/205] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_access.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index f19db440d..6c4f1ceec 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -409,6 +409,7 @@ class TestEmbeddable: @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes + from setuptools.command.build_ext import new_compiler with open("embed_pil.c", "w", encoding="utf-8") as fh: From e50a3a213ed5f5296deeaefe9ec0699010545157 Mon Sep 17 00:00:00 2001 From: TrellixVulnTeam Date: Sun, 30 Oct 2022 23:44:48 +0000 Subject: [PATCH 052/205] Adding tarfile member sanitization to extractall() --- winbuild/build_prepare.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..0e585f796 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,26 @@ def extract_dep(url, filename): zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - tgz.extractall(sources_dir) + def is_within_directory(directory, target): + + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + + return prefix == abs_directory + + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): + + for member in tar.getmembers(): + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted Path Traversal in Tar File") + + tar.extractall(path, members, numeric_owner=numeric_owner) + + + safe_extract(tgz, sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) From 74c60b47a89502adec658316b06cd0043046d36b Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 23:45:49 +0000 Subject: [PATCH 053/205] simplify patch, also check zipfile --- winbuild/build_prepare.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0e585f796..3cd841484 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -469,31 +469,23 @@ def extract_dep(url, filename): raise RuntimeError(ex) print("Extracting " + filename) + sources_dir_abs = os.path.abspath(sources_dir) if filename.endswith(".zip"): with zipfile.ZipFile(file) as zf: + for member in zf.namelist(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + raise RuntimeError("Attempted Path Traversal in Zip File") zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - def is_within_directory(directory, target): - - abs_directory = os.path.abspath(directory) - abs_target = os.path.abspath(target) - - prefix = os.path.commonprefix([abs_directory, abs_target]) - - return prefix == abs_directory - - def safe_extract(tar, path=".", members=None, *, numeric_owner=False): - - for member in tar.getmembers(): - member_path = os.path.join(path, member.name) - if not is_within_directory(path, member_path): - raise Exception("Attempted Path Traversal in Tar File") - - tar.extractall(path, members, numeric_owner=numeric_owner) - - - safe_extract(tgz, sources_dir) + for member in tgz.getmembers(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + raise RuntimeError("Attempted Path Traversal in Tar File") + tgz.extractall(sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) From 7528b673fa4517604f4af91a6d061802843f3246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Oct 2022 19:36:14 +1100 Subject: [PATCH 054/205] Removed Fedora 35 --- .github/workflows/test-docker.yml | 3 +-- docs/installation.rst | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c68d43935..1e36b3382 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -30,7 +30,6 @@ jobs: centos-stream-9-amd64, debian-10-buster-x86, debian-11-bullseye-x86, - fedora-35-amd64, fedora-36-amd64, gentoo, ubuntu-18.04-bionic-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index eb69d5805..4812b27cf 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -440,8 +440,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 35 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | From 5b4703d6153689bf008ffe37f43c489c4f9211a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Nov 2022 08:39:02 +1100 Subject: [PATCH 055/205] Added conversion from RGBa to RGB --- Tests/test_image_convert.py | 7 +++++++ src/libImaging/Convert.c | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 902d8bf8f..0a7202a33 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -104,6 +104,13 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) +def test_rgba(): + with Image.open("Tests/images/transparent.png") as im: + assert im.mode == "RGBA" + + assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + + def test_trns_p(tmp_path): im = hopper("P") im.info["transparency"] = 0 diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 2b45d0cc4..b03bd02af 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -479,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { } } +static void +rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + *out++ = in[0]; + *out++ = in[1]; + *out++ = in[2]; + } else { + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); + } + *out++ = 255; + } +} + /* * Conversion of RGB + single transparent color to RGBA, * where any pixel that matches the color will have the @@ -934,6 +953,7 @@ static struct { {"RGBA", "HSV", rgb2hsv}, {"RGBa", "RGBA", rgba2rgbA}, + {"RGBa", "RGB", rgba2rgb_}, {"RGBX", "1", rgb2bit}, {"RGBX", "L", rgb2l}, From 6fd772e6694c53aeb1c9d5ff48d931a4db63fb2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 00:08:29 +1100 Subject: [PATCH 056/205] Updated lcms2 to 2.14 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4812b27cf..c65095640 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -147,7 +147,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**. + above uses liblcms2. Tested with **1.19** and **2.7-2.14**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..455481e3d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -293,9 +293,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download", - "filename": "lcms2-2.13.1.tar.gz", - "dir": "lcms2-2.13.1", + "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.14.tar.gz/download", + "filename": "lcms2-2.14.tar.gz", + "dir": "lcms2-2.14", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { From 6b286ed62f6cb2be447a5bb3a7f09f5edad5f3d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 07:47:05 +1100 Subject: [PATCH 057/205] XCB will not be used on Linux if gnome-screenshot is present --- Tests/test_imagegrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa2291582..5e0eca28b 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess import sys @@ -33,7 +34,9 @@ class TestImageGrab: @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self): - if sys.platform not in ("win32", "darwin"): + if sys.platform not in ("win32", "darwin") and not shutil.which( + "gnome-screenshot" + ): with pytest.raises(OSError) as e: ImageGrab.grab() assert str(e.value).startswith("Pillow was built without XCB support") From 6ddbe4cbf029a1d1c33cbd68683801864092cb47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Nov 2022 18:26:31 +1100 Subject: [PATCH 058/205] Added signed option when saving JPEG2000 images --- Tests/test_file_jpeg2k.py | 14 ++++++++++++++ docs/handbook/image-file-formats.rst | 5 +++++ src/PIL/Jpeg2KImagePlugin.py | 2 ++ src/encode.c | 5 ++++- src/libImaging/Jpeg2K.h | 3 +++ src/libImaging/Jpeg2KEncode.c | 2 +- 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index cd142e67f..0229b2243 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -252,6 +252,20 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) +def test_sgnd(tmp_path): + outfile = str(tmp_path / "temp.jp2") + + im = Image.new("L", (1, 1)) + im.save(outfile) + with Image.open(outfile) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + im = Image.new("L", (1, 1)) + im.save(outfile, signed=True) + with Image.open(outfile) as reloaded_signed: + assert reloaded_signed.getpixel((0, 0)) == 128 + + def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e79db68b..93ae1b054 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -563,6 +563,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: encoded using RLCP mode will have increasing resolutions decoded as they arrive, and so on. +**signed** + If true, then tell the encoder to save the image as signed. + + .. versionadded:: 9.4.0 + **cinema_mode** Set the encoder to produce output compliant with the digital cinema specifications. The options here are ``"no"`` (the default), diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index c67d8d6bf..11d1d488a 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -321,6 +321,7 @@ def _save(im, fp, filename): progression = info.get("progression", "LRCP") cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) + signed = info.get("signed", False) fd = -1 if hasattr(fp, "fileno"): @@ -342,6 +343,7 @@ def _save(im, fp, filename): progression, cinema_mode, mct, + signed, fd, ) diff --git a/src/encode.c b/src/encode.c index 72c7f64d0..aa47fe671 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1188,11 +1188,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; char mct = 0; + int sgnd = 0; Py_ssize_t fd = -1; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbn", + "ss|OOOsOnOOOssbbn", &mode, &format, &offset, @@ -1207,6 +1208,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &progression, &cinema_mode, &mct, + &sgnd, &fd)) { return NULL; } @@ -1305,6 +1307,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; + context->sgnd = sgnd; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index d030b0c43..b28a0440a 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -85,6 +85,9 @@ typedef struct { /* Set multiple component transformation */ char mct; + /* Signed */ + int sgnd; + /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ OPJ_PROG_ORDER progression; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fe5511ba5..db1c5c0c9 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -343,7 +343,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; image_params[n].bpp = bpp; - image_params[n].sgnd = 0; + image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } image = opj_image_create(components, image_params, color_space); From c10c6bf8940c2a1bbbc1d01575489cb85e612a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 3 Nov 2022 20:23:59 +0100 Subject: [PATCH 059/205] use os.path.commonpath instead of os.path.commonprefix Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3cd841484..14f8d7ba0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,7 @@ def extract_dep(url, filename): with zipfile.ZipFile(file) as zf: for member in zf.namelist(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) - member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: raise RuntimeError("Attempted Path Traversal in Zip File") zf.extractall(sources_dir) @@ -482,7 +482,7 @@ def extract_dep(url, filename): with tarfile.open(file, "r:gz") as tgz: for member in tgz.getmembers(): member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) - member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: raise RuntimeError("Attempted Path Traversal in Tar File") tgz.extractall(sources_dir) From d93b9919e338f8fe253e76fe0bdc7f0994267385 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 21:40:08 +0100 Subject: [PATCH 060/205] Use verbose flag for pip install * Ensures when developing that compilation warnings are visible * Provides feedback that compilation has occured. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8f2862948..7783bd96b 100644 --- a/Makefile +++ b/Makefile @@ -53,12 +53,12 @@ inplace: clean .PHONY: install install: - python3 -m pip install . + python3 -m pip -v install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . python3 selftest.py .PHONY: debug From 41987cffade673b46b96054f8462f3c45d410de0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 22:57:39 +0100 Subject: [PATCH 061/205] Fix compiler error: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/_imaging.c:1842:17: warning: ‘ImagingTransform’ accessing 64 bytes in a region of size 48 [-Wstringop-overflow=] 1842 | imOut = ImagingTransform( | ^~~~~~~~~~~~~~~~~ 1843 | imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/_imaging.c:1842:17: note: referencing argument 8 of type ‘double *’ --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 0888188fb..940b5fbb3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1829,7 +1829,7 @@ _resize(ImagingObject *self, PyObject *args) { box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) { imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); } else if (filter == IMAGING_TRANSFORM_NEAREST) { - double a[6]; + double a[8]; memset(a, 0, sizeof a); a[0] = (double)(box[2] - box[0]) / xsize; From f9a2f991db4fb91a78ba1bb30bb45b9d2b235348 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 11:48:18 +1100 Subject: [PATCH 062/205] Replaced IOError with OSError --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7cd6dfb1d..bd4099176 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -956,7 +956,7 @@ font_render(FontObject *self, PyObject *args) { /* we didn't ask for color, fall through to default */ #endif default: - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } @@ -1023,7 +1023,7 @@ font_render(FontObject *self, PyObject *args) { } } } else { - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } } From 8947cbf4d113b112660b15db23fe84f94e128788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Fri, 4 Nov 2022 07:31:00 +0100 Subject: [PATCH 063/205] simplify code Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 14f8d7ba0..872e74a20 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -480,8 +480,8 @@ def extract_dep(url, filename): zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - for member in tgz.getmembers(): - member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + for member in tgz.getnames(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: raise RuntimeError("Attempted Path Traversal in Tar File") From bbe9cc6ae5f19207b0c3bf80f591016ec03019b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 20:26:39 +1100 Subject: [PATCH 064/205] Use verbose flag for pip install for debug target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7783bd96b..a2545b54e 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null .PHONY: release-test release-test: From ef1eb2f3d6e7e19c251beb6eda5e9ac06e902ca8 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 4 Nov 2022 11:16:22 +0100 Subject: [PATCH 065/205] Add oss-fuzz badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e7c0ebc5a..7a81e0c40 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ As of 2019, Pillow development is Tidelift Align + Fuzzing Status From 13a4feafb75b1c0cdf4821dd6db88f0c44d9ce4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 16:38:50 +1100 Subject: [PATCH 066/205] Patch OpenJPEG to include uclouvain/openjpeg#1423 --- winbuild/build_prepare.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5277b84f8..9f1e74e53 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -323,6 +323,11 @@ deps = { "filename": "openjpeg-2.5.0.tar.gz", "dir": "openjpeg-2.5.0", "license": "LICENSE", + "patch": { + r"src\lib\openjp2\ht_dec.c": { + "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501 + } + }, "build": [ cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), From b8be24850bc07c04a21635dbd8835c9e8408cf93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 21:54:23 +1100 Subject: [PATCH 067/205] Added file to questionable list --- Tests/test_bmp_reference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index b17aad2ea..ed9aff9cc 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -35,6 +35,7 @@ def test_questionable(): "pal8os2v2.bmp", "rgb24prof.bmp", "pal1p1.bmp", + "pal4rletrns.bmp", "pal8offs.bmp", "rgb24lprof.bmp", "rgb32fakealpha.bmp", From 4001a9fab471dbeaaa6f530d98208e25ef6bf912 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Nov 2022 22:41:06 +1100 Subject: [PATCH 068/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6f2ba569e..fc8d8362a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,15 @@ Changelog (Pillow) ================== +9.4.0 (unreleased) +------------------ + +- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 + [wiredfool] + +- Use verbose flag for pip install #6713 + [wiredfool, radarhere] + 9.3.0 (2022-10-29) ------------------ From 9448532f913665006b0373ae3cee61c550b34339 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:03:51 +0000 Subject: [PATCH 069/205] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/sphinx-contrib/sphinx-lint: v0.6.1 → v0.6.7](https://github.com/sphinx-contrib/sphinx-lint/compare/v0.6.1...v0.6.7) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f81bcb956..2c13fb3b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: ["--target-version", "py37"] @@ -44,7 +44,7 @@ repos: - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.6.1 + rev: v0.6.7 hooks: - id: sphinx-lint From e31ca06b7cde667aa973cfcb35166d17a5408925 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 10:58:33 +1100 Subject: [PATCH 070/205] Updated AppVeyor to Python 3.11 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 20908052b..b817cd9d8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,7 +10,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python310 + - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python37-x64 From 5471dc2b265d2486a9fe0f37d2d7cbfe6f4f4cd6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 11:49:39 +1100 Subject: [PATCH 071/205] Use fractional coordinates when drawing text --- .../images/test_anchor_multiline_mm_right.png | Bin 9514 -> 9525 bytes .../test_combine_multiline_lm_center.png | Bin 4160 -> 4147 bytes .../images/test_combine_multiline_lm_left.png | Bin 4223 -> 4197 bytes .../test_combine_multiline_lm_right.png | Bin 4170 -> 4154 bytes .../test_combine_multiline_mm_center.png | Bin 4243 -> 4204 bytes .../images/test_combine_multiline_mm_left.png | Bin 4216 -> 4189 bytes .../test_combine_multiline_mm_right.png | Bin 4247 -> 4215 bytes .../test_combine_multiline_rm_center.png | Bin 4160 -> 4149 bytes .../images/test_combine_multiline_rm_left.png | Bin 4149 -> 4144 bytes .../test_combine_multiline_rm_right.png | Bin 4215 -> 4186 bytes Tests/images/text_float_coord.png | Bin 2877 -> 2875 bytes Tests/images/text_float_coord_1_alt.png | Bin 807 -> 809 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 10 +++++++-- src/PIL/ImageFont.py | 19 ++++++++++++++-- src/_imagingft.c | 12 ++++++---- 16 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png index cf002b12cd0a89eec1f97cd1c6530b7d1007fc9e..7e98b8eac8d780876d377315b2158f203c30757c 100644 GIT binary patch literal 9525 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 TfD8N|D1`D|b%jrICcgg%hStVx diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png index 7b1e9c4e42f69874b4c508d16bc85527d5d902b7..6a15130248ad8bed5e13a937d3667eb98624b5bf 100644 GIT binary patch literal 4147 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 literal 4160 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 CJA=Fc diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png index a26996c2dbe70748f834607fc49ae8dd079c4e09..8eb254fdf26323e778c1ff5e73dab8c8a0b4b638 100644 GIT binary patch literal 4197 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 diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png index 7caf5cb742a27f001106cc3ade926f2f177e71fb..cb640a7409fccce7b08ec23bdf728f11a78a1533 100644 GIT binary patch literal 4154 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 literal 4170 zcmeHLXH=8f7G*|hDq;r($ske$5d|qyWE?=60TC$@Fe*YsgiuTn2%rcu>Ieub1i>g$ zq=p_!NJOd-Iw1xKO*hP_yX&62&))mo`#rsEWh%Dg z$PNJk0Wotk<0}FJzr_CdiTny8;*1Y(1qAl&H8;L+^+ED1Blw;zivM{5z5nq0Kb^Nd zv-7clF3Jmzx{q~h4*o5{pyX#7rnFHlgrP-lDpseuNy#lT#iq#g4B^C`(lwR z1OC=_c6OFA;kRtNy0!`EIEM>H$U6Qm`E&RQ!9Q8>^4^_0cbI1`{}uQ3jm(Af44he7 zQWcxL?`fAp-mofNrzZ7u+}B${-;bhtY?haoTVaI!{QTTpmQTW|{7Kb0%<=7C_MH4} z7ew^LE{IoB%XIa58)FteWjj@{MNVH`1B~-jqN){j0zr}{(ld0^p1{> zF4y;x^6{c-dG7PX@iCDfE4{aANl;Teal%|7K0Q5sX|i3d&ImJKBka{z0z&fg@;J<* zxvWfTqR64Mcbj5mT^s%~KfW*P>RrxMCnSVp6H4II5x5seH17_PLY^Am+E`scq@e6W z32;nuZR+V~3Z1ulP6Ck;27W0x~C`< z3e5lf@E6NBCz(B2ww?M&!BLT&e$3vyeq!K~E}E5ScAU#*=mjp0$r?SOU^Nt9TWBI5 zXI;&%tXxm_Z#%5*?ObVZ5X?Q1wN+CN_O>8r*Tq^G~_j)5% z=SRf#=Rac+?<)95P1cFymVSLD9;yaH&n{C-JXQ|OS$gugvnuwvj#cQ{=%ac`CAYwC zm6er+c1wS&4qkbD>heH)|4aV}lbahz=@a(`%6%6`>pgpOuaQGH1BOCYTGYrafWiu! z0a!cRPnhfYV3csmWu3$9#>2@M9t(*{Y2^}$8+%|ILk|X%pan|1ki>}vK$#+;|Kaa{ zv-uxSTcV|>FBnGM)L|~MQ z7^21nO67nSgrU%?bkmpGCS`hsB8LPCx9Xn(a7#H#iy2L5#X~iz1zW4%V-9P$m;5L6 zPDm-j_|{}wN{s$ulc-{{j}E>zDJe->3P$i|RIdyM*9b~*rl~nVp;I4Y*qt96=SOPi zh)Zp+RUEX+06fJ-kkP_M8NIPjU_#R7;|w{|Xv)CrM#k5An7U&7Zovh+W1VR@b#K;D z5=f=2r&`IkMr@w1W%E&b^R3+mDA4POTKEkyb}3i;SuOAPpY8YKiI) zNKY}^pPQ_&cc3n8H@}s!s)qrkXH=r}lBs%V*6J2-4pJTo58Kq_B~G!5+w<{whKen_rT)5V)i*mdN`Zx~b7x`KuF-QG@Bm)00ad3GB_82>OLOxfwd?w~n_@T1HU@*| zUPzufwxk<2_R=)t64q~6pv;@$WMi|?k0hrf8e<^1ko>z~H4Q|YejMYRbEu{w;m~e{ zdgYmPO(0{+j29*GOjI!%3WegYSF3w2vl&&Dl|f{;D_K1`cv4@XYa``W$T|n;L(j)E zUeUVkq^-R@cbNunD6iYl{ zS4<8DPNs0~dw4K`0x*;_jbpw6O%gS!)U}Z)$h#0Oox)PV6T3c|#xxv~(Y-$fa>8K^ z?i4gPH@mkcp*WqUnTP~c7`MYn+>OmBa%;&W3?^YybWzhlKry#B4n*JRoF>kH*@=~V zzgO;JGLU*jLi_ICh8I!}c}}Qdb)c+Dz(bAV?hGibItUdM^d&-6FMxf?T3klIJUe^Z zOvw6z=%G9-;Dzo%J)dl%oI{5{KcXXw)Dd+n{=8X&gS~x5CLi$L6JL!6w9;^=I2PQv zu-<^4?Q2ca%z?huYK7|J^_s1d!SswV2A1-1$SIPnPLrsYsIuVEV@Qa|4_kABW~oJ z2bD^-KEE~w)n>eE6i(fPIi7}M0?XsFfF2SXU(jeYc*rspYn|d!g>D@{^?MQAa21)T zeJ7m~wl;Z)c92+R;K1tU$`JYX*Er7o{xUDajrm#%e4~d-eQ{W00%(%*z4m25xXFy) zfNx_8y3Yekr`V;Kdao^x(w2*_CG1pIdc>rS;u#2wZ+9zUF<8NU5o*Dxk72zpCt0Peos!p$h zZJ~^#i)O$iS;+I>3U1cQCXlz*XDltYg4}v(w8?Sj)mJBMNt@h$4(ErP`A<+NMt^~n zpQ+@VS!Z6!^31PLSjux0ruHyoW41pJpXnzc-PeI6oU71Y>~U*@{3o5g@Qi;1HPtngVmYVIMPtH;U>qH zOX$AM&o?fk!NUxABkt|*7~j4n73x8URk87Svy9VQTXy8yC~-LPfv-C^(KWh)Rn!7{ zmn{s&TWUANUn`!d5r*gkV=vO4Uz^0?g4q>V^0KAg_$ymw6_sFcaQR>q1?oQgto69toVaH*udycMT$4(;S+{wYXbLXP!4L`uip@6y2JZlq^`sUXvIoa8|0c=cD zq@{&L8#s8dz9)g!oW?(K+6!cajh_)lZ7eh*!n&EG_0NmRiDzL;=qR~KIoU|xf*38( z;6s?{3E4S0Dv^3Kg8ATk%&&_Q-%{<;uy11ve*WST_%wt!(6?yz z0M;VCkANHkP7O*7SjdFh04i1N3yN0c}#HrhF?Ob}7I;MUECV>AM5&`9j38zdF<>B3ah49F^e%%)lh6`EM?pnF0f7)9lmP{# z7(|2^T0j&;nuZb}1eB5h2}DXlO|qBSJ-d6(eAy5CZRbPIJGpuH|NiAy?#umO%?%`W z%kCBv6O%A9ymUoOY+K6KXXiIyMT(J65EI+0X>{p=RcQ7cBjTPFM%1*B$>?+pI}XLQ zhbKNbX0tE<2rS~m?*@r)lzj@1l>feGP$KfpVD(QO&+bhhkxBJlHZUx7%(_HIS9N4~ zLzWb)-Pm^@&-;+!6eP3lXCixcw!DM29R5j()tRlsipXAIWG{4PqE*t2F64LR>ean` z`LfL50(C!zCnlDZes!DMPMN)T|GNCug8xLpQ_W0M+vHp3Ddt$djG{@QLlVDIWKx*0 zM<+x7>#(_ev+*-@ZI!4tH{i{#uC6YFmBFH-B1OIM{UN%~F~U5twmkidKYri*&6~X^ zlBhz#hGx8twRL}YLr%1=;l*8l0-UsgbSc}MApi5#^ZNSwA>lLB!ZcNj`BXRUz%!5P zTeiadGur?3vin9`l0v3wQE91KT045E2a)z`?2g-q?a-Zg*Jyhyt90|?#Kc5A)OGX_ zVfAZ^R2_z$0hSUH5Q632?!j*_?2=K?M2);oNJy|!>!(tK1Uy>I=0e*YXOXRIlon>T zPq494e5GWjx7aoG%bh#U$b7JazuapfC3p4&&UkKwy!5i;WCVZ6)6u~pDXKct^m=pb zj;X0B2?>c3E1%`*PrTR%>psdtbj*uOVR}3npmY_`b(`fJ-atImwJ)| z^&qa13lqu6XyFlsMJgve0M^cRAFTS+IODQ2D*#pmUIGnSdOlqsQ z#QB4NVCTQMh;~1G_z*nLw79po$c#N(x&Qo|kocpjcB0+tw#1P-tiNq?!PR{=aCx2P zbz8fd8}_zR`6>VSZro_9vNT`E4dMN?Tg9zTKYpjpV*kLvKz~2x-z#qff|ao)Ru6af z@_thgI%!||+hI_XrG=i>Z*T8`f~QbU^`U{a8t@Q~m#4KH3>7)~$UwzBTl?Dk6pnvL zPROvgw2J9tE#HCRYF{bdGaGvNJeeg4XV%u%0y?u4gZiy)F|G4^?d~eCAI8mo>`|f7 zXe4qj_+1Y5I{(?T22rJR*c6UlX6K*NMZ=JAK%X+( zif(f}m|DbKwK#OF4$HM6MJ~3h+ByaP;_mJ~pYiCZL6QK%TiHoaLF` zrDSmvW8RkbfN+4eZ+Nc{ZjWxuk+>0fy&9miVEb!1I?gh*>h;Gt*3c7I5>=k zPCP_Jtxw9xo%-#PrKKf*lylf^;K3oypbj7j6KzT{x}k|%38OMWwnEUJcv?l`KIEMHIdb(Yl(M-<5efXuJg76v7@9LFa&2*Ze4L{}fcbAxNy>$utrKi) zVp7!`u`=HRaThS0gHBvngfHnNSxuDuLM!=iGyLNXq z3fI|j;!=pdHxQ?QWuW_J#OLI$y5FnBPtbI-}w zpY2*LVt2_r*RXr>27{)ArArAVAum5#l;qR zjlMJ@XL+#FUEp$NZtn?)4pGr)Ux}quU8KFey-?MjZ1L7H?DH-04;>vj!9*gd;f|TgGxO!Y(^!o9Y zlo0dBYR8YaBgdjf>%+)#%^gsRt|(9RyBXLJ4W!Q9siiS$W0<_sD(A1e+F4?0uHyj| ze+IpN5N0;MOGbPU(DW4Xa9aB-N|On+p%1`y(=itSbTiIad%tIG@J$;}Lu(tG+<=R1 zF3KJr9&^yI1CIVvdFS(t3%9JGLxY6&Qs0DwSYEus47xuBc$Ee8^E%9bG&^QI!Uluk z?Sa+@v?*Zs2k0xO>R*&h*=P(L;8H{`O*j|~WR+GLt*6t90oMNLhu$TTxG2j%TeUTBr0 zQ*2xfu<^=2luyd(1YJXgcV%g@X%+r4YgOIn)I|_nSy>quFq#iNmj!(OimPkP*Ew$E z#yl8NIw6Xg=?Nq)gVK};ZPX6<+{9T1#G?w4kPtN1FrC==BP8c2H?ymux?NWDx~a3X z^TrnM{V``-Zts#tfR>$wpin56hHze-6ha$4{ZS{FdCkAmxv8SD5h0pA1I|LI;A18k zC)&qG#@{7MY1JHMC{)ZT8+%G(T}~e^>@BJHON4k90|~FY-j$V_ntB$m zTfkWBiMirJZ}07Y_cU03wG%b?;!B^Ug&)h~MbOlTI9er&QG5Bx-!RiteX|NSWn+6I zfq>HwE#}1tGetXc78e(pOy(bfw)`N^O`!fjR;v_P%M;#l4GjUXKQ(oA3!SO-mXl6a zR_K{RJd8#v!VjXOSb8SF3&0H4#@Prv??=(V{LCw2BnR77kK7|F@aMgPm*@{H}D3WYHBfTaK6iXr~t3&Iwn;G z-~OGpvZ-RT@lSXkqrz|1!$4M6D!tXBE@J8I;wdyQ`23eYACUigKl^`vKWi0jOAmF0 VDpa!5u!3HV{D~PO$U_p=~NDI3*4D|vE2!?pp_pXV)u=0=bP^yU z0!j%eHM9t#2+|QSfe;`-XbBKXNaeol&b_mr?zj8RnPkr7WOB}Vpa1jxo_F%(U$z$8 zw5)M0 zrHuH-3oThj2M^dg+6sPolC?w0cbn0T`R9raAyha0|TQqaXBjzjOZcV+aUX@-k!!rpLile4k z8xU+>eoxfA;sAruyEA|Bvy6z4l2WuP`u*fjTV>AvIQ)>{pDxg)BFl0be>MDay;8U{ z-7>b!3dS>mI=HsHOU3ntPRTe~-q59s+m~|lNVoj_YDz-fTwNo09N55oNuL^7` zl(J!H<5|&|Hg>Wn&sjb0=+UF$?9#qU<^dVYq6vIcoQCt|%ZH&|y}bsJ%QMb!bZJFD zmKkNpshlS1A==cScNl0MJz~1x6CIMVSpl}pR9y{Qoaltcrk=h%avaWYw!?7a%F4>V zGLQxYSWQI8jT<){9UWuUzh|lAg))eJ&Bcw5)%{Epd^Y!0G?fT-|3>qAJsRKrY6_za0n0Q=M~l( z2aUHpkd{-2I>6yrRvIdLpAmN&zA9Lp?AEZs2kR*NAdv&LXyNg~YA()PG~AJKmfx>o zojliNr5&YoDwnXy?rI8B9wsf<|vF3z6_el1bEa$0cd*H*( zPL{AbEQw$k;9eazU*^}Z6=YIQ5AB!3%sn6k)RK>6?#jQ^{O#Mf#l@eG)K1~?hn<&G z%tB{e&}cD-US*ehx-)B!wCp|?cXxMtd!68kj)T#x=GE8k(D%2e79Vh$t7I z(U&IY4ZK^Db-X_BGYu=MtgOswJVQHOuf8W!9!Xd0@BW-=8OwTD{>I{#g3{^PR7esEJ*5 zmK1?N@aWN_f1Dx5m`lQXaxa^iJ$(hGayl;GPjFSeJ69KjSmeS06*UoK?CTC=1geaspJmItJ33B+=~}CIH&yC+w}b(I(F$A~9phTPXKRHV z(KWPinO=;rV||oNIaBa(%tS{%63XV`mr)cK-aQ zt=iSk*^6Ag>Au2M5zBlPy!ZoEiP~_;wT8z7!9hU_)!_@qyJe4gWL_n6udZO(c9Zw=9XPh!2YBNp8;XVyaNL(D=8V}w4&8ou2cki zVp0ZcB3BoPtERl2vlYx}kuLLdlyK;QT$^FppY*>3eDPe?RIHY8v@}QgS|w?n(AI9ZAsUC=8mta4C@6Ti)s#07gas1` z>dwl_qDYns3(4d7gB;nn2V6=}D|5pLBHF-QNMMM(fuJn6l|(oH@S%Is++vkYd8pYz z-`;%ww;V_UfettVl;QHg?5ko-xfd~^^CO?i2kv|wdq&(XuSs(Q#FMCW01f?X`QvR# z+S_{XZICuL?erXJc)}14^0>udj#Z)TW*Y`}Rjgr`-eO~i*Kb;SHTj1}r@Nn@A4Wi` z;#3ZC0dyW(yysdp^oP&9Ys|Jzi6sCHVrFZ2g8;<&uBB56&hAb0?*N{se0vwCdZxQ@ zwzSO(2&0gxZ^{AHFNigh*#xZ$O8|M(GZe52S;gak_7Xm8!6&b<5Ia{Vt%_dk6s`Z7 zc!F@!v;J^2!ERICh-M^@bHel>#HZ`Etli-N79goJCc)M=S;;J7u^qEqEShOfIKH5| zDaVH3w8Fr4900$;$*I=1oJfVBE8`+rZE2C56vsXoiVpm1Vouz@tL{~roVjRDw;b5M zWJL|?l<(469>Az~OUDQ%>*W#xA#K4dO?KP!ToS2vDwkAq@z}9r#_*x6B==*c_aM;N zkm=r5qB+GXrd}h?<91>w{MHo!=%qe5yn#L2UtT!Zz)PjkXmHVpB16zy{&eNn*Re5w zq#j;HGgC7yViayOfenBM>?Dq?p}GePC^G=ahZdZSv-dw;@vj(uR8Q@LV{0BOW2fHc zl=;)~1VH(`RY4zRoHF0t)#Y&c@)BcJQvO)ZLLj4n*n86BLzLlQGO?A|6V4Bv`EDQf zUdW@Yin(0wuHx|K-4? zC|>aO^|gP~(7iYr#lE{^v@dMzzev zy>0>Q!J4XtF>@ZdeUH06xvXq?7^lh^O**pxlvGkyZm$uKCBfa_T|19iPC=yaHH!!y zUjH>oTNU)6zW}r-Lo{w^5*|+iG3x3z|8kypB+Ppu!0(Q_ZR+=qjt)L8Dk_TI=ACRD zG6ibS_-aLIX-I#WA4AMRSw!z62TyhlMDbPUB7vp1VAH%owpIK`Q4}F;_+tUAYWc2L z)pzbF<( z#|Z%Vt=K+o1SmJhZ0o%SNEZr53=z$Jl-IDG0nhVSKh1f&gv9< zw+h6wSYj1wZm13f)tFJ}+nS<>VAD#;lj}BYsunNz6U8$KldIfUp)4!7Wp!D1PR^fD z-f3Y9XoB{azdbMx-;1P`Ob+#0}}^) zlufFBv$oDdjYW{1pTX9{&8@#j5TBGp0e9-@=~)ZXMn$coNBP_&%GDJEKSnM3VP8VB z9>P{K6oCU5*@B%cA(2RPIOtXw4KxH~SN1om>gFI@B19}&Xz%OotzGJM12Vfqp-n}e zA1{Dy&yD7I-2R9+aXih|)>g1h0A)y&ESFPMOvC2DMht)mG#i^iS)J(23;+QEx9Ja{ z)XWt*hlDI;+Yq=pPcY+pL@GIm_35hHoJ2U=j>vN^bayNFPt*5tz*)I_pf<@Zzc`8h z>3Ejtl2t75@XEzx2P(2%a>c>1o9Q(%$@+anUh|i2b?fVQ{5brOV9f>3RyQ!O!4LIs T-aH8YY>?KX<8lAr0RHTE diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png index aadb5191f0e7df6ae1838ca6b04745268de17728..f539a8e62e66d1769e1bd692594d5938db7d1d46 100644 GIT binary patch literal 4189 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 AyZ`_I literal 4216 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 Ha8LLb^xtTU literal 4247 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 sq)Epb{z}gH3sL0nTmDag9FofH^0IWV!Z diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png index 7568dd63a33181ae5ec5c8c99ac72c18859109b6..4cce8f6a00ead59f2f061bde6a6b374e9fecbfe3 100644 GIT binary patch literal 4149 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

Gc&a5H1zmVybWfk{$63_yW#%@t&MX| diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png index b8c3b5b143d2b7b67116daaef4f7b3f119777494..93d8162b3bf9196b44bc38721cbc254de2854cb4 100644 GIT binary patch literal 4144 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 literal 4149 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 diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png index 49468698cd4e749ad6a666bb253a45695a3e6839..d2270826a5bd84efa4a8dbde8a987d5db0a2dc94 100644 GIT binary patch delta 2732 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 diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png index 50bdac3d8f39aa492bf41ba3e49c787031d07628..2287071ffab678232370853706c56c3c843d4fbb 100644 GIT binary patch delta 684 zcmV;d0#p5`2B`*+Bs=g)L_t(|obB6Na_b-vMNuV{DP{~FQ^w#yDj!NwB!W&O9tY&T z>VEm!h_-ZqAo*?I_YHu~ySf9wtCQ~nB9VO!f9yluffk;R#EYL_vUV`iu(K8)sI>j( z!}tg@A<)^8-sE7hZClw)_@|X*vOVua~gKmjstaQpP3L4 zE13yDzkY@RV~mVPi&ciwTk@ZX^Gw2t8;h9`5F>8du4`%9CAz1ya4BB4 z@Ux?<#4qL?U*=SFXaDkhT79Y^{}Q^jOCby^gxo(*3-?nxoDW!UCl3*N=(iE2&skcZ(Ml1*+mF8f{l?|oue1q93h7MfAAVLjug*tCR6k-c9tUy$ zTzi|fv>MJ?q2@UU^BNX~Fg~NkQ?s3)+3UhCEq-yKq7FAM?U1_~Yp%oO!?C};?fYKb z)*;!U(w~8NMi--n(!nX$RI7IUK@Cd&6#Tb*Pp|4rbAH>c+G*9`dBVK#b)`O{{ym|w z5?FLJJ~(lOnCqK z8wN6CWH?%^GL&AD{X`sR5>DK3%!Gg#Aq2$MzeRykes_VD=N!}_yzt$*5N1yK>RUTAmsYvS-n+D8`@{B%Ql<6pfGC9d=MVh-%@uQ)Db4@a zB&C&hqg84C4Pk@PMe?vKA+tn(K6($GijA4D;Amm~_2urE=SvG;6qc6y+Vi@Wmdb!@ zR+zxli*|;Om(PSv)-#sE+?~luCVveJhY1$>ar8Ivjp0@C_qpQR( zWE@}SRCH(m^m|%;sv-Llx`Xl%`V~UzpQVNCDILxSthb|w@IAD5pm%BVZ7O32y7g1B zF;jZwEwmy9%FVqTMML?qZ@E3N3sKC3Ol$Zk?=E=hEeZ^FyaBLrI`mkMHh)rjK@Z-S zuisvuwzPW2(z=aS!$Wxa(bvA;xSZ>iHpfVzn+fg1&noBD_{fOrM+}+AL5x4w-exVW zhH+M?dCuY8HLM6>ct$f%&Gvt0uf2R};fo6u> 24) & 0xFF) - x, y = (int(c) for c in coord) + x, y = coord self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 457e906c8..c8de65be2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,6 +26,7 @@ # import base64 +import math import os import sys import warnings @@ -588,6 +589,7 @@ class FreeTypeFont: stroke_width=0, anchor=None, ink=0, + start=None, ): """ Create a bitmap for the text. @@ -659,6 +661,7 @@ class FreeTypeFont: stroke_width=stroke_width, anchor=anchor, ink=ink, + start=start, )[0] def getmask2( @@ -672,6 +675,7 @@ class FreeTypeFont: stroke_width=0, anchor=None, ink=0, + start=None, *args, **kwargs, ): @@ -750,12 +754,23 @@ class FreeTypeFont: size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) - size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 + if start is None: + start = (0, 0) + size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( - text, im.id, mode, direction, features, language, stroke_width, ink + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], ) return im, offset diff --git a/src/_imagingft.c b/src/_imagingft.c index bd4099176..b52d6353e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -777,13 +777,15 @@ font_render(FontObject *self, PyObject *args) { const char *lang = NULL; PyObject *features = Py_None; PyObject *string; + float x_start = 0; + float y_start = 0; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziL:render", + "On|zzOziLff:render", &string, &id, &mode, @@ -791,7 +793,9 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, - &foreground_ink_long)) { + &foreground_ink_long, + &x_start, + &y_start)) { return NULL; } @@ -876,8 +880,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width) << 6; - y = (-y_max + (-stroke_width)) << 6; + x = (-x_min + stroke_width + x_start) * 64; + y = (-y_max + (-stroke_width) - y_start) * 64; if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; From 97a6f651d4ddf07cbd13b2cd38b19dde381050c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 12:01:15 +1100 Subject: [PATCH 072/205] Added Interop tags --- docs/reference/ExifTags.rst | 50 +++++++++++++++++++++---------------- src/PIL/ExifTags.py | 8 ++++++ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index ff5788524..d362334a5 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,35 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which -provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes +which provide constants and clear-text names for various well-known EXIF tags. + +.. py:data:: Base + + >>> from PIL.ExifTags import Base + >>> Base.ImageDescription.value + 270 + >>> Base(270).name + 'ImageDescription' + +.. py:data:: GPS + + >>> from PIL.ExifTags import GPS + >>> GPS.GPSDestLatitude.value + 20 + >>> GPS(20).name + 'GPSDestLatitude' + +.. py:data:: Interop + + >>> from PIL.ExifTags import Interop + >>> Interop.RelatedImageFileFormat.value + 4096 + >>> Interop(4096).name + 'RelatedImageFileFormat' + + +Two of these values are also exposed as dictionaries. .. py:data:: TAGS :type: dict @@ -26,22 +53,3 @@ provide constants and clear-text names for various well-known EXIF tags. >>> from PIL.ExifTags import GPSTAGS >>> GPSTAGS[20] 'GPSDestLatitude' - - -These values are also exposed as ``enum.IntEnum`` classes. - -.. py:data:: Base - - >>> from PIL.ExifTags import Base - >>> Base.ImageDescription.value - 270 - >>> Base(270).name - 'ImageDescription' - -.. py:data:: GPS - - >>> from PIL.ExifTags import GPS - >>> GPS.GPSDestLatitude.value - 20 - >>> GPS(20).name - 'GPSDestLatitude' diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index f3a73bf1a..c00730ba9 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -338,3 +338,11 @@ class GPS(IntEnum): """Maps EXIF GPS tags to tag names.""" GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 1 + InteropVersion = 2 + RelatedImageFileFormat = 4096 + RelatedImageWidth = 4097 + RleatedImageHeight = 4098 From ebde03eae829ca8396dbb446157daf5a5b04e67c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 13:10:08 +1100 Subject: [PATCH 073/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fc8d8362a..574fbdbd3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- CVE-2007-4559 patch when building on Windows #6704 + [TrellixVulnTeam, nulano, radarhere] + - Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 [wiredfool] From 73bec9622413cf52b71f23723aad6ce8b01c4445 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 21:50:06 +1100 Subject: [PATCH 074/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 574fbdbd3..34c00c3d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added Interop to ExifTags #6724 + [radarhere] + - CVE-2007-4559 patch when building on Windows #6704 [TrellixVulnTeam, nulano, radarhere] From 62fd8336b93e97c15ed1e9afee31ccc1d9b15362 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 18:42:46 +1100 Subject: [PATCH 075/205] Update to Python 3.11 in GitHub Actions --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6195f973b..8a14dad92 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip cache-dependency-path: "setup.py" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b7f62c23..5cabb6622 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -226,7 +226,7 @@ jobs: path: dist\*.whl - name: Upload fribidi.dll - if: "github.event_name != 'pull_request' && matrix.python-version == 3.10" + if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" uses: actions/upload-artifact@v3 with: name: fribidi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 645384c02..831e33c13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -96,7 +96,7 @@ jobs: path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 run: | make doccheck From b0ab324f829f8016f89141b332998ba67e3867c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 9 Nov 2022 20:03:16 +1100 Subject: [PATCH 076/205] Use the latest Python version Co-authored-by: Hugo van Kemenade --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a14dad92..49611e287 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" cache: pip cache-dependency-path: "setup.py" From 1c032ff5db5854895314045ee25950a06bea2ae8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Nov 2022 10:37:22 +1100 Subject: [PATCH 077/205] Revert "Install NumPy with OpenBLAS" This reverts commit c82483e35a37919df9700485aa752e8c5a38f28c. --- .github/workflows/macos-install.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 65f2b81d5..dfd7d0553 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage @@ -13,7 +13,6 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg python3 -m pip install numpy # extra test images From 99a11297b108c1427cef683d9dcd196f32794043 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Nov 2022 16:08:42 +1100 Subject: [PATCH 078/205] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c65095640..f4e959fbe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -480,11 +480,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ From 9fbfd3f00efbb6719bb35043315e457614617072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 21:32:40 +1100 Subject: [PATCH 079/205] Added oss-fuzz badge --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 45af4c571..1efbe74c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,6 +61,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Sun, 13 Nov 2022 08:00:20 +1100 Subject: [PATCH 080/205] Added MP Format Version when saving --- Tests/test_file_mpo.py | 1 + src/PIL/MpoImagePlugin.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d94bdaa96..dba1ec1b1 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -268,6 +268,7 @@ def test_save_all(): im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) assert_image_equal(im, im_reloaded) + assert im_reloaded.mpinfo[45056] == b"0100" im_reloaded.seek(1) assert_image_similar(im2, im_reloaded, 1) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 5bfd8efc1..92d288f2f 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -51,7 +51,7 @@ def _save_all(im, fp, filename): if not offsets: # APP2 marker im.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) @@ -60,6 +60,7 @@ def _save_all(im, fp, filename): offsets.append(fp.tell() - offsets[-1]) ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB000] = b"0100" ifd[0xB001] = len(offsets) mpentries = b"" From 20f17cc6a79881ba441adfb734e8dbe6901a9749 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:14:37 -0600 Subject: [PATCH 081/205] remove unused ImagingAccess->line() method defs --- src/libImaging/Access.c | 62 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 514fb2929..83860c38a 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -43,23 +43,6 @@ add_item(const char *mode) { return &access_table[i]; } -/* fetch pointer to pixel line */ - -static void * -line_8(Imaging im, int x, int y) { - return &im->image8[y][x]; -} - -static void * -line_16(Imaging im, int x, int y) { - return &im->image8[y][x + x]; -} - -static void * -line_32(Imaging im, int x, int y) { - return &im->image32[y][x]; -} - /* fetch individual pixel */ static void @@ -187,36 +170,35 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { void ImagingAccessInit() { -#define ADD(mode_, line_, get_pixel_, put_pixel_) \ +#define ADD(mode_, get_pixel_, put_pixel_) \ { \ ImagingAccess access = add_item(mode_); \ - access->line = line_; \ access->get_pixel = get_pixel_; \ access->put_pixel = put_pixel_; \ } /* populate access table */ - ADD("1", line_8, get_pixel_8, put_pixel_8); - ADD("L", line_8, get_pixel_8, put_pixel_8); - ADD("LA", line_32, get_pixel, put_pixel); - ADD("La", line_32, get_pixel, put_pixel); - ADD("I", line_32, get_pixel_32, put_pixel_32); - ADD("I;16", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B); - ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L); - ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B); - ADD("F", line_32, get_pixel_32, put_pixel_32); - ADD("P", line_8, get_pixel_8, put_pixel_8); - ADD("PA", line_32, get_pixel, put_pixel); - ADD("RGB", line_32, get_pixel_32, put_pixel_32); - ADD("RGBA", line_32, get_pixel_32, put_pixel_32); - ADD("RGBa", line_32, get_pixel_32, put_pixel_32); - ADD("RGBX", line_32, get_pixel_32, put_pixel_32); - ADD("CMYK", line_32, get_pixel_32, put_pixel_32); - ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); - ADD("LAB", line_32, get_pixel_32, put_pixel_32); - ADD("HSV", line_32, get_pixel_32, put_pixel_32); + ADD("1", get_pixel_8, put_pixel_8); + ADD("L", get_pixel_8, put_pixel_8); + ADD("LA", get_pixel, put_pixel); + ADD("La", get_pixel, put_pixel); + ADD("I", get_pixel_32, put_pixel_32); + ADD("I;16", get_pixel_16L, put_pixel_16L); + ADD("I;16L", get_pixel_16L, put_pixel_16L); + ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;32L", get_pixel_32L, put_pixel_32L); + ADD("I;32B", get_pixel_32B, put_pixel_32B); + ADD("F", get_pixel_32, put_pixel_32); + ADD("P", get_pixel_8, put_pixel_8); + ADD("PA", get_pixel, put_pixel); + ADD("RGB", get_pixel_32, put_pixel_32); + ADD("RGBA", get_pixel_32, put_pixel_32); + ADD("RGBa", get_pixel_32, put_pixel_32); + ADD("RGBX", get_pixel_32, put_pixel_32); + ADD("CMYK", get_pixel_32, put_pixel_32); + ADD("YCbCr", get_pixel_32, put_pixel_32); + ADD("LAB", get_pixel_32, put_pixel_32); + ADD("HSV", get_pixel_32, put_pixel_32); } ImagingAccess From 16994ccc9b40f97113cb1c8f56abe9a37a2744fa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:15:50 -0600 Subject: [PATCH 082/205] remove unused ImagingAccess->line() method def --- src/libImaging/Imaging.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index b65f8eadd..d9ded1852 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -124,7 +124,6 @@ struct ImagingMemoryInstance { struct ImagingAccessInstance { const char *mode; - void *(*line)(Imaging im, int x, int y); void (*get_pixel)(Imaging im, int x, int y, void *pixel); void (*put_pixel)(Imaging im, int x, int y, const void *pixel); }; From 55abf18f1020b456cd782ebfe94b5847148559f6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:16:50 -0600 Subject: [PATCH 083/205] remove comment about Access.c line methods --- src/PIL/PyAccess.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 9a2ec48fc..039f5ceea 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -13,8 +13,7 @@ # Notes: # -# * Implements the pixel access object following Access. -# * Does not implement the line functions, as they don't appear to be used +# * Implements the pixel access object following Access.c # * Taking only the tuple form, which is used from python. # * Fill.c uses the integer form, but it's still going to use the old # Access.c implementation. From 21f202a22a712e0392c4ee148946967bd05eb936 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 06:06:08 +1100 Subject: [PATCH 084/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34c00c3d4..bf9a236cc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added MP Format Version when saving MPO #6735 + [radarhere] + - Added Interop to ExifTags #6724 [radarhere] From 84458c3988ad22d82afaa33a4d66080a2a089908 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 08:18:31 +1100 Subject: [PATCH 085/205] Updated xz to 5.2.8 --- 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 1fcec66b3..10e2000ae 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download", - "filename": "xz-5.2.7.tar.gz", - "dir": "xz-5.2.7", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.8.tar.gz/download", + "filename": "xz-5.2.8.tar.gz", + "dir": "xz-5.2.8", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 8a3ba659450f2b46da7c0e4df92fa10dfc0a21d4 Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Mon, 14 Nov 2022 10:57:30 -0500 Subject: [PATCH 086/205] Remove Tidelift alignment action and badge Not sure if we still care about this? cf. #5762 #5763 --- .github/workflows/tidelift.yml | 36 ---------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/tidelift.yml diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml deleted file mode 100644 index 69f9e5476..000000000 --- a/.github/workflows/tidelift.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Tidelift Align - -on: - schedule: - - cron: "30 2 * * *" # daily at 02:30 UTC - push: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - pull_request: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.repository_owner == 'python-pillow' - name: Run Tidelift to ensure approved open source packages are in use - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Scan - uses: tidelift/alignment-action@main - env: - TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} - TIDELIFT_ORGANIZATION: team/aclark4life - TIDELIFT_PROJECT: pillow From 70cc8a57415f6a9744cf4d4f0407a4415319dda2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:06:41 +1100 Subject: [PATCH 087/205] Fixed writing int as BYTE tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index d38c1c523..b90dde3d9 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -201,6 +201,22 @@ def test_writing_bytes_to_ascii(tmp_path): assert reloaded.tag_v2[271] == "test" +def test_writing_int_to_bytes(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[700] + assert tag.type == TiffTags.BYTE + + info[700] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[700] == b"\x01" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1dfd5275f..ab9ac5ea2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -719,6 +719,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, int): + data = bytes((data,)) return data @_register_loader(2, 1) From ddc215ce3c6772a9b3aa6995f38c97ea922ecf48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:51:20 +1100 Subject: [PATCH 088/205] Revert "Added Tidelift Align badge to docs" This reverts commit 06ab0324a3bb66965c7c1505dbdd0aa640ba308b. --- docs/index.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1efbe74c4..5bcd5afa5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,10 +57,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 15 Nov 2022 11:35:14 +1100 Subject: [PATCH 089/205] Revert "Add tidelift alignment badge" This reverts commit c8822a6cac65bbe0a7d831ef2b8431435f1feb1d. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7a81e0c40..8ee68f9b8 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,6 @@ As of 2019, Pillow development is Code coverage - Tidelift Align Fuzzing Status From d4c7bd7e19e926d5d47c70d04eaba86cf20a67a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:04:02 +1100 Subject: [PATCH 090/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf9a236cc..87ff33f1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed writing int as BYTE tag #6740 + [radarhere] + - Added MP Format Version when saving MPO #6735 [radarhere] From 70c8e342a514815ae698a3fb22e2ab52f3c09d7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:15:56 +1100 Subject: [PATCH 091/205] Added "start" argument to docstring --- src/PIL/ImageFont.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8de65be2..3b1a2a23a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -649,6 +649,11 @@ class FreeTypeFont: .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ @@ -743,6 +748,11 @@ class FreeTypeFont: .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking From 62db04478733e9baddd41d365f96f9a8dbeb388d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:27:33 +1100 Subject: [PATCH 092/205] Added release notes --- docs/releasenotes/9.4.0.rst | 54 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/9.4.0.rst diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst new file mode 100644 index 000000000..46c7e2f22 --- /dev/null +++ b/docs/releasenotes/9.4.0.rst @@ -0,0 +1,54 @@ +9.4.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added start position for getmask and getmask2 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Text may render differently when starting at fractional coordinates, so +:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now +support a ``start`` argument. This tuple of horizontal and vertical offset +will be used internally by :py:meth:`.ImageDraw.text` to more accurately place +text at the ``xy`` coordinates. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 8c436be3b..a2b588696 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.4.0 9.3.0 9.2.0 9.1.1 From cb40f46ec13f2413163baa00e6e44958dd3f67f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 14:58:21 +1100 Subject: [PATCH 093/205] Added Fedora 37 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 1e36b3382..7331cf8ee 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -31,6 +31,7 @@ jobs: debian-10-buster-x86, debian-11-bullseye-x86, fedora-36-amd64, + fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index f4e959fbe..cf6b9ca8f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,6 +442,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 37 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | From df8e87291254dc22415a0c6e371950e79311dbf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Nov 2022 08:26:47 +1100 Subject: [PATCH 094/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 87ff33f1f..cd1b07be4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Use fractional coordinates when drawing text #6722 + [radarhere] + - Fixed writing int as BYTE tag #6740 [radarhere] From 1f6df76c42dab44e00b128b40fd6657185925aca Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Thu, 17 Nov 2022 13:58:07 -0800 Subject: [PATCH 095/205] updated webp with exact parameter. --- Tests/test_file_webp_alpha.py | 34 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 4 ++++ src/PIL/WebPImagePlugin.py | 2 ++ src/_webp.c | 5 +++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index dc82fb742..07df7a068 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,40 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) +def test_write_rgba_keep_transparent(tmp_path): + """ + Can we write a RGBA mode file to WebP while preserving + the transparent RGB without error. + Does it have the bits we expect? + """ + + temp_output_file = str(tmp_path / "temp.webp") + + input_image = hopper("RGB") + # make a copy of the image + output_image = input_image.copy() + # make a single channel image with the same size as input_image + new_alpha = Image.new("L", input_image.size, 255) + # make the left half transparent + new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + # putalpha on output_image + output_image.putalpha(new_alpha) + + # now save with transparent area preserved. + output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) + # even though it is lossless, if we don't put exact=True, the transparent + # area will be filled with black (or something more conducive to compression) + + with Image.open(temp_output_file) as image: + image.load() + + assert image.mode == "RGBA" + assert image.format == "WEBP" + image.load() + image = image.convert("RGB") + assert_image_similar(image, input_image, 1.0) + + def test_write_unsupported_mode_PA(tmp_path): """ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e79db68b..ffc949148 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1124,6 +1124,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **method** Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. +**exact** + If true, preserve the transparent RGB values. Otherwise, discard + invisible RGB values for better compression. Defaults to false. + **icc_profile** The ICC Profile to include in the saved file. Only supported if the system WebP library was built with webpmux support. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 5eaeb10cc..c88f730a2 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,6 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) + exact = im.encoderinfo.get("exact", False) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -336,6 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, + 1 if exact else 0, exif, xmp, ) diff --git a/src/_webp.c b/src/_webp.c index fd99116cb..ec9425d36 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -576,6 +576,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { int lossless; float quality_factor; int method; + int exact; uint8_t *rgb; uint8_t *icc_bytes; uint8_t *exif_bytes; @@ -597,7 +598,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiifss#is#s#", + "y#iiifss#iis#s#", (char **)&rgb, &size, &width, @@ -608,6 +609,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &icc_bytes, &icc_size, &method, + &exact, &exif_bytes, &exif_size, &xmp_bytes, @@ -633,6 +635,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; + config.exact = exact; // Validate the config if (!WebPValidateConfig(&config)) { From 770560d8e4972d69193a816713be2bda5ff3ed94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:06:14 +0000 Subject: [PATCH 096/205] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp_alpha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 07df7a068..5a57d591a 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,7 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) + def test_write_rgba_keep_transparent(tmp_path): """ Can we write a RGBA mode file to WebP while preserving @@ -111,7 +112,7 @@ def test_write_rgba_keep_transparent(tmp_path): # make a single channel image with the same size as input_image new_alpha = Image.new("L", input_image.size, 255) # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) # putalpha on output_image output_image.putalpha(new_alpha) @@ -130,7 +131,6 @@ def test_write_rgba_keep_transparent(tmp_path): assert_image_similar(image, input_image, 1.0) - def test_write_unsupported_mode_PA(tmp_path): """ Saving a palette-based file with transparency to WebP format From 3587f27780a5be7d02d0c781b39e13919c88d8d6 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:15:24 -0800 Subject: [PATCH 097/205] Added version check for WebP --- src/_webp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index ec9425d36..9231150aa 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -635,7 +635,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; +#if WEBP_ENCODER_ABI_VERSION >= 0x0209 + // the exact flag is only available in libwebp 0.5.0 and later config.exact = exact; +#endif // Validate the config if (!WebPValidateConfig(&config)) { From fdf074b050f272e60e37bbd7f6ff52f3fc95a299 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:22:33 -0800 Subject: [PATCH 098/205] added a note to the docs for webp --- docs/handbook/image-file-formats.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ffc949148..9c2319b44 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,6 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. + Requires LibWebP 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. Only supported if From 509dcbf073b3cf8c4fc3d051ebc884a6672e070b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 15:35:06 +1100 Subject: [PATCH 099/205] Added LightSource tag values --- src/PIL/ExifTags.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba9..3df1dbf72 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,27 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class LightSource(IntEnum): + Unknown = 0 + Daylight = 1 + Fluorescent = 2 + Tungsten = 3 + Flash = 4 + Fine = 9 + Cloudy = 10 + Shade = 11 + DaylightFluorescent = 12 + DayWhiteFluorescent = 13 + CoolWhiteFluorescent = 14 + WhiteFluorescent = 15 + StandardLightA = 17 + StandardLightB = 18 + StandardLightC = 19 + D55 = 20 + D65 = 21 + D75 = 22 + D50 = 23 + ISO = 24 + Other = 255 From 96a4d98abc265dabc1442a3e7b0cfdd5043f90b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:07:43 +1100 Subject: [PATCH 100/205] Simplified code --- src/PIL/WebPImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c88f730a2..e3c19db3d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,7 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) - exact = im.encoderinfo.get("exact", False) + exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -337,7 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, - 1 if exact else 0, + exact, exif, xmp, ) From 7e5e843d5cd9f4a17361853138816773da28d8c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:12:51 +1100 Subject: [PATCH 101/205] Note that the fill behaviour only affects libwebp >= 0.5 --- Tests/test_file_webp_alpha.py | 45 ++++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 5a57d591a..df6cffb17 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -97,38 +97,33 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_write_rgba_keep_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path): """ - Can we write a RGBA mode file to WebP while preserving - the transparent RGB without error. - Does it have the bits we expect? + Saving transparent pixels should retain their original RGB values + when using the "exact" parameter. """ - temp_output_file = str(tmp_path / "temp.webp") + image = hopper("RGB") - input_image = hopper("RGB") - # make a copy of the image - output_image = input_image.copy() - # make a single channel image with the same size as input_image - new_alpha = Image.new("L", input_image.size, 255) - # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) - # putalpha on output_image - output_image.putalpha(new_alpha) + # create a copy of the image + # with the left half transparent + half_transparent_image = image.copy() + new_alpha = Image.new("L", (128, 128), 255) + new_alpha.paste(0, (0, 0, 64, 128)) + half_transparent_image.putalpha(new_alpha) - # now save with transparent area preserved. - output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) - # even though it is lossless, if we don't put exact=True, the transparent - # area will be filled with black (or something more conducive to compression) + # save with transparent area preserved + temp_file = str(tmp_path / "temp.webp") + half_transparent_image.save(temp_file, exact=True, lossless=True) - with Image.open(temp_output_file) as image: - image.load() + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGBA" + assert reloaded.format == "WEBP" - assert image.mode == "RGBA" - assert image.format == "WEBP" - image.load() - image = image.convert("RGB") - assert_image_similar(image, input_image, 1.0) + # even though it is lossless, if we don't use exact=True + # in libwebp >= 0.5, the transparent area will be filled with black + # (or something more conducive to compression) + assert_image_similar(reloaded.convert("RGB"), image, 1) def test_write_unsupported_mode_PA(tmp_path): From 3c7aa133eb62d75c0f96360ba54c7c4ed19d5c6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:18:27 +1100 Subject: [PATCH 102/205] Assert that image is equal --- Tests/test_file_webp_alpha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index df6cffb17..5970fd2a3 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -123,7 +123,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): # even though it is lossless, if we don't use exact=True # in libwebp >= 0.5, the transparent area will be filled with black # (or something more conducive to compression) - assert_image_similar(reloaded.convert("RGB"), image, 1) + assert_image_equal(reloaded.convert("RGB"), image) def test_write_unsupported_mode_PA(tmp_path): From 690446050a1963599dc926b06e0360aea0da3f67 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:26:08 -0800 Subject: [PATCH 103/205] minor fix in the comments --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 9231150aa..c2532a496 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -636,7 +636,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.method = method; #if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the exact flag is only available in libwebp 0.5.0 and later + // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; #endif From d6f10d4876e4f3e1126d3a1d6eac1f75b9d675c2 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:51:06 -0800 Subject: [PATCH 104/205] doc update for libwebp --- 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 9c2319b44..ac39625a2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,7 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. - Requires LibWebP 0.5.0 or later. + Requires libwebp 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. Only supported if From 55a75b9a696e968ab56a19a207959211cd33adf9 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:14:59 -0800 Subject: [PATCH 105/205] added RN for the new exact option. --- docs/releasenotes/9.4.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 46c7e2f22..3a9c3977f 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -37,6 +37,13 @@ support a ``start`` argument. This tuple of horizontal and vertical offset will be used internally by :py:meth:`.ImageDraw.text` to more accurately place text at the ``xy`` coordinates. +Added the ``exact`` encoding option for WebP +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``exact`` encoding option for WebP is now supported. The WebP encoder +removes the hidden RGB values for better compression by default. By setting +this option to ``True``, the encoder will keep the hidden RGB values. + Security ======== From 9c5b00ef7e03b921fd55cd6c432bac3a00562d46 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:19:08 -0800 Subject: [PATCH 106/205] RN trailing space fix --- docs/releasenotes/9.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 3a9c3977f..ad79022fe 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,7 +41,7 @@ Added the ``exact`` encoding option for WebP ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``exact`` encoding option for WebP is now supported. The WebP encoder -removes the hidden RGB values for better compression by default. By setting +removes the hidden RGB values for better compression by default. By setting this option to ``True``, the encoder will keep the hidden RGB values. Security From 8f73a895ec29a8f824466812ef6dae9c90ad0397 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sun, 20 Nov 2022 16:00:24 -0800 Subject: [PATCH 107/205] Update docs/releasenotes/9.4.0.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/9.4.0.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ad79022fe..0f47f5ad6 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,8 +41,9 @@ Added the ``exact`` encoding option for WebP ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``exact`` encoding option for WebP is now supported. The WebP encoder -removes the hidden RGB values for better compression by default. By setting -this option to ``True``, the encoder will keep the hidden RGB values. +removes the hidden RGB values for better compression by default in libwebp 0.5 +or later. By setting this option to ``True``, the encoder will keep the hidden +RGB values. Security ======== From be7d350e3f5425fe9eaa2bd3fe8e0751d48b1013 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 11:56:30 +1100 Subject: [PATCH 108/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd1b07be4..461f34e54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added "exact" option when saving WebP #6747 + [ashafaei, radarhere] + - Use fractional coordinates when drawing text #6722 [radarhere] From 100ed363ce1407481331d864ae444b22391bb397 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 15:42:44 +1100 Subject: [PATCH 109/205] Updated libpng to 1.6.39 --- 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 10e2000ae..e4bf275a1 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -228,9 +228,9 @@ deps = { # "bins": [r"libtiff\*.dll"], }, "libpng": { - "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download", - "filename": "lpng1638.zip", - "dir": "lpng1638", + "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download", + "filename": "lpng1639.zip", + "dir": "lpng1639", "license": "LICENSE", "build": [ # lint: do not inline From 2c513c6448d24e393de63be85ec30e980b99d8fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 22:05:57 +1100 Subject: [PATCH 110/205] Use stdlib for setuptools on Cygwin --- .github/workflows/test-cygwin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b9ab0eda..bbf0ee736 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -76,7 +76,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - .ci/build.sh + SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh - name: Test run: | From 851e7b03ec2a1ccbc98c2d4fcb765e932187c985 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Nov 2022 22:57:10 +1100 Subject: [PATCH 111/205] Document how to install Pillow from a directory --- docs/installation.rst | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index cf6b9ca8f..c50a6cc3c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -103,10 +103,6 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: Building From Source -------------------- -Download and extract the `compressed archive from PyPI`_. - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - .. _external-libraries: External Libraries @@ -191,7 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, run:: +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow --no-binary :all: @@ -211,6 +208,16 @@ prerequisites, it may be necessary to manually clear the pip cache or build without cache using the ``--no-cache-dir`` option to force a build with newly installed external libraries. +If you would like to install from a local copy of the source code instead, you +can download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ Build Options ^^^^^^^^^^^^^ From 58cbcbf10826039376b563521b24b84e3fddbb8f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 08:47:40 +1100 Subject: [PATCH 112/205] Added getxmp() to WebPImagePlugin --- Tests/test_file_webp_metadata.py | 21 +++++++++++++++++++++ src/PIL/WebPImagePlugin.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index f77a245c0..4f513d82b 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -11,6 +11,11 @@ pytestmark = [ skip_unless_feature("webp_mux"), ] +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + def test_read_exif_metadata(): @@ -110,6 +115,22 @@ def test_read_no_exif(): assert not webp_image._getexif() +def test_getxmp(): + with Image.open("Tests/images/flower.webp") as im: + assert "xmp" not in im.info + assert im.getxmp() == {} + + with Image.open("Tests/images/flower2.webp") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + assert ( + im.getxmp()["xmpmeta"]["xmptk"] + == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " + ) + + @skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path): iccp_data = b"" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e3c19db3d..e9a7aac77 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -98,6 +98,9 @@ class WebPImageFile(ImageFile.ImageFile): return None return self.getexif()._get_merged_dict() + def getxmp(self): + return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} + def seek(self, frame): if not self._seek_check(frame): return From 3473eb8e7f225e6d2fc3709b0565289d64ff9cec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 15:44:03 +1100 Subject: [PATCH 113/205] Added Exif hide_offsets() --- Tests/test_image.py | 25 +++++++++++++++++++++++++ src/PIL/Image.py | 19 ++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e57903490..45fedbe4d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -838,6 +838,31 @@ class TestImage: 34665: 196, } + def test_exif_hide_offsets(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + + # Check offsets are present initially + assert 0x8769 in exif + for tag in (0xA005, 0x927C): + assert tag in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + loaded_exif = exif + + with Image.open("Tests/images/flower.jpg") as im: + new_exif = im.getexif() + + for exif in (loaded_exif, new_exif): + exif.hide_offsets() + + # Assert they are hidden afterwards, + # but that the IFDs are still available + assert 0x8769 not in exif + assert exif.get_ifd(0x8769) + for tag in (0xA005, 0x927C): + assert tag not in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size): im = Image.new("RGB", size) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..10ca3a65e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3503,6 +3503,7 @@ class Exif(MutableMapping): def __init__(self): self._data = {} + self._hidden_data = {} self._ifds = {} self._info = None self._loaded_exif = None @@ -3556,6 +3557,7 @@ class Exif(MutableMapping): return self._loaded_exif = data self._data.clear() + self._hidden_data.clear() self._ifds.clear() if data and data.startswith(b"Exif\x00\x00"): data = data[6:] @@ -3576,6 +3578,7 @@ class Exif(MutableMapping): def load_from_fp(self, fp, offset=None): self._loaded_exif = None self._data.clear() + self._hidden_data.clear() self._ifds.clear() # process dictionary @@ -3631,8 +3634,9 @@ class Exif(MutableMapping): if tag not in self._ifds: if tag in [0x8769, 0x8825]: # exif, gpsinfo - if tag in self: - self._ifds[tag] = self._get_ifd_dict(self[tag]) + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + self._ifds[tag] = self._get_ifd_dict(offset) elif tag in [0xA005, 0x927C]: # interop, makernote if 0x8769 not in self._ifds: @@ -3717,7 +3721,16 @@ class Exif(MutableMapping): else: # interop self._ifds[tag] = self._get_ifd_dict(tag_data) - return self._ifds.get(tag, {}) + ifd = self._ifds.get(tag, {}) + if tag == 0x8769 and self._hidden_data: + ifd = {k: v for (k, v) in ifd.items() if k not in (0xA005, 0x927C)} + return ifd + + def hide_offsets(self): + for tag in (0x8769, 0x8825): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] def __str__(self): if self._info is not None: From 710927a311c5699b69d78bcd13ea6ddabc0b0563 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:08:49 +1100 Subject: [PATCH 114/205] Added docstring --- src/PIL/WebPImagePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e9a7aac77..81ed550d9 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -99,6 +99,12 @@ class WebPImageFile(ImageFile.ImageFile): return self.getexif()._get_merged_dict() def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} def seek(self, frame): From 3f9410334cd9efe4d9ebca5eb59d42b7c1c8778c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:11:51 +1100 Subject: [PATCH 115/205] Added getxmp() to release notes --- docs/releasenotes/9.4.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 0f47f5ad6..f2b50fa5b 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,12 @@ removes the hidden RGB values for better compression by default in libwebp 0.5 or later. By setting this option to ``True``, the encoder will keep the hidden RGB values. +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +decoded for WEBP images through ``getxmp()``. + Security ======== From 72372ad23f612a320c470c442afb4adab39d988b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 20:42:04 +1100 Subject: [PATCH 116/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 461f34e54..7fac5201c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added getxmp() to WebPImagePlugin #6758 + [radarhere] + - Added "exact" option when saving WebP #6747 [ashafaei, radarhere] From 24a5405a9f7ea22f28f9c98b3e407292ea5ee1d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 08:39:56 +1100 Subject: [PATCH 117/205] Added IFD enum --- docs/reference/ExifTags.rst | 7 ++++++ src/PIL/ExifTags.py | 7 ++++++ src/PIL/Image.py | 45 ++++++++++++++++++++++++------------- src/PIL/MpoImagePlugin.py | 11 +++++++-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index d362334a5..650bb4f95 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -31,6 +31,13 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> Interop(4096).name 'RelatedImageFileFormat' +.. py:data:: IFD + + >>> from PIL.ExifTags import IFD + >>> IFD.Exif.value + 34665 + >>> IFD(34665).name + 'Exif' Two of these values are also exposed as dictionaries. diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba9..97a21335f 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,10 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class IFD(IntEnum): + Exif = 34665 + GPSInfo = 34853 + Makernote = 37500 + Interop = 40965 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..3fcc86931 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -47,7 +47,14 @@ except ImportError: # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) from ._binary import i32le, o32be, o32le from ._deprecate import deprecate from ._util import DeferredError, is_path @@ -3598,14 +3605,16 @@ class Exif(MutableMapping): merged_dict = dict(self) # get EXIF extension - if 0x8769 in self: - ifd = self._get_ifd_dict(self[0x8769]) + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif]) if ifd: merged_dict.update(ifd) # GPS - if 0x8825 in self: - merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo] + ) return merged_dict @@ -3615,30 +3624,34 @@ class Exif(MutableMapping): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): - if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): + if tag in [ + ExifTags.IFD.Exif, + 0x8225, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): value = self.get_ifd(tag) if ( - tag == 0x8769 - and 0xA005 in value - and not isinstance(value[0xA005], dict) + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) ): value = value.copy() - value[0xA005] = self.get_ifd(0xA005) + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): if tag not in self._ifds: - if tag in [0x8769, 0x8825]: + if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: # exif, gpsinfo if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) - elif tag in [0xA005, 0x927C]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: # interop, makernote - if 0x8769 not in self._ifds: - self.get_ifd(0x8769) - tag_data = self._ifds[0x8769][tag] - if tag == 0x927C: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.Makernote: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 92d288f2f..3ae4d4abf 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -22,7 +22,14 @@ import itertools import os import struct -from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin +from . import ( + ExifTags, + Image, + ImageFile, + ImageSequence, + JpegImagePlugin, + TiffImagePlugin, +) from ._binary import i16be as i16 from ._binary import o32le @@ -137,7 +144,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] if mptype.startswith("Large Thumbnail"): - exif = self.getexif().get_ifd(0x8769) + exif = self.getexif().get_ifd(ExifTags.IFD.Exif) if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: From a0326245a288801b7ea4753eef32d94398c5b9af Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 21:19:13 +1100 Subject: [PATCH 118/205] Removed typo --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3fcc86931..d07fc716c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3626,7 +3626,6 @@ class Exif(MutableMapping): for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, - 0x8225, ExifTags.IFD.GPSInfo, ] and not isinstance(value, dict): value = self.get_ifd(tag) From 50cdf39f505158e37af2cbe39458f3ab27e7377e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 09:18:47 +1100 Subject: [PATCH 119/205] List dependency instructions first --- docs/installation.rst | 186 ++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 99 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c50a6cc3c..af1d3399c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -187,85 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, to install Pillow from the source -code on PyPI, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -If the prerequisites are installed in the standard library locations -for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no -additional configuration should be required. If they are installed in -a non-standard location, you may need to configure setuptools to use -those locations by editing :file:`setup.py` or -:file:`setup.cfg`, or by adding environment variables on the command -line:: - - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: - -If Pillow has been previously built without the required -prerequisites, it may be necessary to manually clear the pip cache or -build without cache using the ``--no-cache-dir`` option to force a -build with newly installed external libraries. - -If you would like to install from a local copy of the source code instead, you -can download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. - -After navigating to the Pillow directory, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install . - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - -Build Options -^^^^^^^^^^^^^ - -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. - -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, - ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``, ``--disable-xcb``. - Disable building the corresponding feature even if the development - libraries are present on the building machine. - -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, - ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``, ``--enable-xcb``. - Require that the corresponding feature is built. The build will raise - an exception if the libraries are not found. Webpmux (WebP metadata) - relies on WebP support. Tcl and Tk also must be used together. - -* Build flags: ``--vendor-raqm --vendor-fribidi`` - These flags are used to compile a modified version of libraqm and - a shim that dynamically loads libfribidi at runtime. These are - used to compile the standard Pillow wheels. Compiling libraqm requires - a C99-compliant compiler. - -* Build flag: ``--disable-platform-guessing``. Skips all of the - platform dependent guessing of include and library directories for - automated build systems that configure the proper paths in the - environment variables (e.g. Buildroot). - -* Build flag: ``--debug``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. - - -Sample usage:: - - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" - - Building on macOS -^^^^^^^^^^^^^^^^^ +""""""""""""""""" The Xcode command line tools are required to compile portions of Pillow. The tools are installed by running ``xcode-select --install`` @@ -285,25 +208,19 @@ To install libraqm on macOS use Homebrew to install its dependencies:: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -or from within the uncompressed source directory:: - - python3 -m pip install . - Building on Windows -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" We recommend you use prebuilt wheels from PyPI. If you wish to compile Pillow manually, you can use the build scripts in the ``winbuild`` directory used for CI testing and development. These scripts require Visual Studio 2017 or newer and NASM. +The scripts also install Pillow from the local copy of the source code, so the +`Installing`_ instructions will not be necessary afterwards. + Building on Windows using MSYS2/MinGW -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""""""""""""""""""""" To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. @@ -332,14 +249,8 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - - Building on FreeBSD -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" .. Note:: Only FreeBSD 10 and 11 tested @@ -353,9 +264,8 @@ Prerequisites are installed on **FreeBSD 10 or 11** with:: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - Building on Linux -^^^^^^^^^^^^^^^^^ +""""""""""""""""" If you didn't build Python from source, make sure you have Python's development libraries installed. @@ -403,7 +313,7 @@ See also the ``Dockerfile``\s in the Test Infrastructure repo install process for other tested distros. Building on Android -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" Basic Android support has been added for compilation within the Termux environment. The dependencies can be installed by:: @@ -413,6 +323,84 @@ environment. The dependencies can be installed by:: This has been tested within the Termux app on ChromeOS, on x86. +Installing +^^^^^^^^^^ + +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: + +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`setup.cfg`, or by adding environment variables on the command +line:: + + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: + +If Pillow has been previously built without the required +prerequisites, it may be necessary to manually clear the pip cache or +build without cache using the ``--no-cache-dir`` option to force a +build with newly installed external libraries. + +If you would like to install from a local copy of the source code instead, you +can download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ + +Build Options +""""""""""""" + +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. + +* Build flags: ``--disable-zlib``, ``--disable-jpeg``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, + ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-imagequant``, ``--disable-xcb``. + Disable building the corresponding feature even if the development + libraries are present on the building machine. + +* Build flags: ``--enable-zlib``, ``--enable-jpeg``, + ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, + ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-imagequant``, ``--enable-xcb``. + Require that the corresponding feature is built. The build will raise + an exception if the libraries are not found. Webpmux (WebP metadata) + relies on WebP support. Tcl and Tk also must be used together. + +* Build flags: ``--vendor-raqm --vendor-fribidi`` + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + +* Build flag: ``--disable-platform-guessing``. Skips all of the + platform dependent guessing of include and library directories for + automated build systems that configure the proper paths in the + environment variables (e.g. Buildroot). + +* Build flag: ``--debug``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to + stdout. + + +Sample usage:: + + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Platform Support ---------------- From 556b672eb2f982a90e271a580d2a1f00b78ca131 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 27 Nov 2022 17:48:12 -0600 Subject: [PATCH 120/205] Fix webp dealloc method definitions --- src/_webp.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index c2532a496..493e0709c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); - Py_RETURN_NONE; } PyObject * @@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); - Py_RETURN_NONE; } PyObject * From 91fe817911cd8f4a4fa30632aeff621e495cfa7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Nov 2022 18:03:08 +1100 Subject: [PATCH 121/205] Updated instructions to download source code Co-authored-by: Hugo van Kemenade --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index af1d3399c..6d67a2536 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -348,15 +348,15 @@ build without cache using the ``--no-cache-dir`` option to force a build with newly installed external libraries. If you would like to install from a local copy of the source code instead, you -can download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. +can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` +or download and extract the `compressed archive from PyPI`_. After navigating to the Pillow directory, run:: python3 -m pip install --upgrade pip python3 -m pip install . -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files Build Options """"""""""""" From e3a46fcfd0111d7f080da0efe5846430771afeeb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 11:08:39 +0200 Subject: [PATCH 122/205] Use sphinx-inline-tabs to organise installation per OS --- .editorconfig | 4 + docs/Makefile | 2 +- docs/conf.py | 7 +- docs/installation.rst | 293 +++++++++++++++++++++--------------------- setup.cfg | 1 + 5 files changed, 157 insertions(+), 150 deletions(-) diff --git a/.editorconfig b/.editorconfig index d74549fe2..7f5eab056 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,10 @@ indent_style = space trim_trailing_whitespace = true +[*.rst] +# Three-space indentation +indent_size = 3 + [*.yml] # Two-space indentation indent_size = 2 diff --git a/docs/Makefile b/docs/Makefile index 458299aac..0a663ce2b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,7 +43,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph html: $(MAKE) install-sphinx diff --git a/docs/conf.py b/docs/conf.py index bc67d9368..04823e2d7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,12 +27,13 @@ needs_sphinx = "2.4" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx_copybutton", - "sphinx_issues", - "sphinx_removed_in", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinx_issues", + "sphinx_removed_in", "sphinxext.opengraph", ] diff --git a/docs/installation.rst b/docs/installation.rst index 6d67a2536..3c86f09cc 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,6 +23,11 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 +.. _Windows Installation: +.. _macOS Installation: +.. _Linux Installation: +.. _FreeBSD Installation: + Basic Installation ------------------ @@ -38,67 +43,69 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -Windows Installation -^^^^^^^^^^^^^^^^^^^^ +.. tab:: Windows -We provide Pillow binaries for Windows compiled for the matrix of -supported Pythons in both 32 and 64-bit versions in the wheel format. -These binaries include support for all optional libraries except -libimagequant and libxcb. Raqm support requires -FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow -To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + +.. tab:: macOS + + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + +.. tab:: Linux + + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. + +.. tab:: FreeBSD + + Pillow can be installed on FreeBSD via the official Ports or Packages systems: + + **Ports**:: + + cd /usr/ports/graphics/py-pillow && make install clean + + **Packages**:: + + pkg install py38-pillow + + .. note:: + + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. -macOS Installation -^^^^^^^^^^^^^^^^^^ - -We provide binaries for macOS for each of the supported Python -versions in the wheel format. These include support for all optional -libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Linux Installation -^^^^^^^^^^^^^^^^^^ - -We provide binaries for Linux for each of the supported Python -versions in the manylinux wheel format. These include support for all -optional libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Most major Linux distributions, including Fedora, Ubuntu and ArchLinux -also include Pillow in packages that previously contained PIL e.g. -``python-imaging``. Debian splits it into two packages, ``python3-pil`` -and ``python3-pil.imagetk``. - -FreeBSD Installation -^^^^^^^^^^^^^^^^^^^^ - -Pillow can be installed on FreeBSD via the official Ports or Packages systems: - -**Ports**:: - - cd /usr/ports/graphics/py-pillow && make install clean - -**Packages**:: - - pkg install py38-pillow - -.. note:: - - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. - +.. _Building on macOS: +.. _Building on Windows: +.. _Building on Windows using MSYS2/MinGW: +.. _Building on FreeBSD: +.. _Building on Linux: +.. _Building on Android: Building From Source -------------------- @@ -187,141 +194,135 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Building on macOS -""""""""""""""""" +.. tab:: macOS -The Xcode command line tools are required to compile portions of -Pillow. The tools are installed by running ``xcode-select --install`` -from the command line. The command line tools are required even if you -have the full Xcode package installed. It may be necessary to run -``sudo xcodebuild -license`` to accept the license prior to using the -tools. + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. -The easiest way to install external libraries is via `Homebrew -`_. After you install Homebrew, run:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp -To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Windows -""""""""""""""""""" +.. tab:: Windows -We recommend you use prebuilt wheels from PyPI. -If you wish to compile Pillow manually, you can use the build scripts -in the ``winbuild`` directory used for CI testing and development. -These scripts require Visual Studio 2017 or newer and NASM. + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. -The scripts also install Pillow from the local copy of the source code, so the -`Installing`_ instructions will not be necessary afterwards. + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. -Building on Windows using MSYS2/MinGW -""""""""""""""""""""""""""""""""""""" +.. tab:: Windows using MSYS2/MinGW -To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or -**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. -The following instructions target the 64-bit build, for 32-bit -replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. -Make sure you have Python and GCC installed:: + Make sure you have Python and GCC installed:: - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools -Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm -Building on FreeBSD -""""""""""""""""""" +.. tab:: FreeBSD -.. Note:: Only FreeBSD 10 and 11 tested + .. Note:: Only FreeBSD 10 and 11 tested -Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 -Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Linux -""""""""""""""""" +.. tab:: Linux -If you didn't build Python from source, make sure you have Python's -development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. -In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools -In Fedora, the command is:: + In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config -In Alpine, the command is:: + In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools -.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev -To install libraqm, ``sudo apt-get install meson`` and then see -``depends/install_raqm.sh``. + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. -Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel -Note that the package manager may be yum or DNF, depending on the -exact distribution. + Note that the package manager may be yum or DNF, depending on the + exact distribution. -Prerequisites are installed for **Alpine** with:: + Prerequisites are installed for **Alpine** with:: - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev -See also the ``Dockerfile``\s in the Test Infrastructure repo -(https://github.com/python-pillow/docker-images) for a known working -install process for other tested distros. + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. -Building on Android -""""""""""""""""""" +.. tab:: Android -Basic Android support has been added for compilation within the Termux -environment. The dependencies can be installed by:: + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo -This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ diff --git a/setup.cfg b/setup.cfg index 44feb25ff..b562e2934 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ docs = olefile sphinx>=2.4 sphinx-copybutton + sphinx-inline-tabs sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph From e6e5a0018e27779827678552ea2b17a7c1034e7e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:34:43 +0200 Subject: [PATCH 123/205] Add missing 'make help' for serve and livehtml --- docs/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 0a663ce2b..a65e2d3f5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,6 +20,8 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -38,6 +40,8 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" clean: -rm -rf $(BUILDDIR)/* From d12c119ec41e62642157a2add640fc0211d46066 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:36:04 +0200 Subject: [PATCH 124/205] Inline PHONY targets to help avoid omissions (texinfo, info, livehtml, serve were missing) --- docs/Makefile | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index a65e2d3f5..d5242f935 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,8 +15,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - +.PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @@ -40,45 +39,50 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" +.PHONY: clean clean: -rm -rf $(BUILDDIR)/* install-sphinx: $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph +.PHONY: html html: $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: dirhtml dirhtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +.PHONY: singlehtml singlehtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +.PHONY: pickle pickle: $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." +.PHONY: json json: $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." +.PHONY: htmlhelp htmlhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @@ -86,6 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @@ -96,6 +101,7 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" +.PHONY: devhelp devhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @@ -106,12 +112,14 @@ devhelp: @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" @echo "# devhelp" +.PHONY: epub epub: $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." +.PHONY: latex latex: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -120,6 +128,7 @@ latex: @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." +.PHONY: latexpdf latexpdf: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -127,18 +136,21 @@ latexpdf: $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." +.PHONY: text text: $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." +.PHONY: man man: $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." +.PHONY: texinfo texinfo: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -147,6 +159,7 @@ texinfo: @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." +.PHONY: info info: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -154,18 +167,21 @@ info: make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." +.PHONY: gettext gettext: $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." +.PHONY: changes changes: $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." +.PHONY: linkcheck linkcheck: $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @@ -173,14 +189,17 @@ linkcheck: @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: doctest doctest: $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 +.PHONY: serve serve: cd $(BUILDDIR)/html; $(PYTHON) -m http.server From 5e42b1779e29ecbd34adcdd911b44cbe87ad439b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:50:55 +0200 Subject: [PATCH 125/205] Reorder tabs: big three OS first --- docs/installation.rst | 146 +++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 3c86f09cc..00924eab9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,9 +23,9 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 -.. _Windows Installation: -.. _macOS Installation: .. _Linux Installation: +.. _macOS Installation: +.. _Windows Installation: .. _FreeBSD Installation: Basic Installation @@ -43,29 +43,6 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -.. tab:: Windows - - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. - -.. tab:: macOS - - We provide binaries for macOS for each of the supported Python - versions in the wheel format. These include support for all optional - libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - .. tab:: Linux We provide binaries for Linux for each of the supported Python @@ -81,6 +58,29 @@ Install Pillow with :command:`pip`:: ``python-imaging``. Debian splits it into two packages, ``python3-pil`` and ``python3-pil.imagetk``. +.. tab:: macOS + + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + +.. tab:: Windows + + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + .. tab:: FreeBSD Pillow can be installed on FreeBSD via the official Ports or Packages systems: @@ -100,11 +100,11 @@ Install Pillow with :command:`pip`:: are tested by the ports team with all supported FreeBSD versions. +.. _Building on Linux: .. _Building on macOS: .. _Building on Windows: .. _Building on Windows using MSYS2/MinGW: .. _Building on FreeBSD: -.. _Building on Linux: .. _Building on Android: Building From Source @@ -194,6 +194,53 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +.. tab:: Linux + + If you didn't build Python from source, make sure you have Python's + development libraries installed. + + In Debian or Ubuntu:: + + sudo apt-get install python3-dev python3-setuptools + + In Fedora, the command is:: + + sudo dnf install python3-devel redhat-rpm-config + + In Alpine, the command is:: + + sudo apk add python3-dev py3-setuptools + + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev + + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. + + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + + Note that the package manager may be yum or DNF, depending on the + exact distribution. + + Prerequisites are installed for **Alpine** with:: + + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev + + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. + .. tab:: macOS The Xcode command line tools are required to compile portions of @@ -267,53 +314,6 @@ Many of Pillow's features require external libraries: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -.. tab:: Linux - - If you didn't build Python from source, make sure you have Python's - development libraries installed. - - In Debian or Ubuntu:: - - sudo apt-get install python3-dev python3-setuptools - - In Fedora, the command is:: - - sudo dnf install python3-devel redhat-rpm-config - - In Alpine, the command is:: - - sudo apk add python3-dev py3-setuptools - - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev - - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. - - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - - Note that the package manager may be yum or DNF, depending on the - exact distribution. - - Prerequisites are installed for **Alpine** with:: - - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev - - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. - .. tab:: Android Basic Android support has been added for compilation within the Termux From 50ccb27a4d57500b9b5e474e276df03158b6c870 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 23:23:46 +0200 Subject: [PATCH 126/205] Remove extra space Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index d5242f935..d32d25a3c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -90,7 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." -.PHONY: qthelp +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp From 3ec8fa614705ae273426d60f994e3b01bb57a69a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Nov 2022 13:49:07 +1100 Subject: [PATCH 127/205] Do not trust JPEG decoder to determine image is CMYK --- src/PIL/BlpImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 533997737..45987ec03 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -373,6 +373,9 @@ class BLP1Decoder(_BLPBaseDecoder): data = BytesIO(data) image = JpegImageFile(data) Image._decompression_bomb_check(image.size) + if image.mode == "CMYK": + decoder_name, extents, offset, args = image.tile[0] + image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))] r, g, b = image.convert("RGB").split() image = Image.merge("RGB", (b, g, r)) self.set_as_raw(image.tobytes()) From aab7983146729c81a5105b6511858f41d76b53f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Dec 2022 07:57:26 +1100 Subject: [PATCH 128/205] Updated xz to 5.2.9 --- 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 e4bf275a1..66e352c73 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.8.tar.gz/download", - "filename": "xz-5.2.8.tar.gz", - "dir": "xz-5.2.8", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.9.tar.gz/download", + "filename": "xz-5.2.9.tar.gz", + "dir": "xz-5.2.9", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 96b316880e284f4221b2d8400ef1bec5c81bcb83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 2 Dec 2022 11:40:06 +0200 Subject: [PATCH 129/205] Use 4-space indents for RST --- .editorconfig | 2 +- docs/installation.rst | 218 +++++++++++++++++++++--------------------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7f5eab056..07f02c236 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ trim_trailing_whitespace = true [*.rst] # Three-space indentation -indent_size = 3 +indent_size = 4 [*.yml] # Two-space indentation diff --git a/docs/installation.rst b/docs/installation.rst index 00924eab9..89b2e558f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -45,59 +45,59 @@ Install Pillow with :command:`pip`:: .. tab:: Linux - We provide binaries for Linux for each of the supported Python - versions in the manylinux wheel format. These include support for all - optional libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - Most major Linux distributions, including Fedora, Ubuntu and ArchLinux - also include Pillow in packages that previously contained PIL e.g. - ``python-imaging``. Debian splits it into two packages, ``python3-pil`` - and ``python3-pil.imagetk``. + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. .. tab:: macOS - We provide binaries for macOS for each of the supported Python - versions in the wheel format. These include support for all optional - libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow .. tab:: Windows - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. .. tab:: FreeBSD - Pillow can be installed on FreeBSD via the official Ports or Packages systems: + Pillow can be installed on FreeBSD via the official Ports or Packages systems: - **Ports**:: + **Ports**:: - cd /usr/ports/graphics/py-pillow && make install clean + cd /usr/ports/graphics/py-pillow && make install clean - **Packages**:: + **Packages**:: - pkg install py38-pillow + pkg install py38-pillow - .. note:: + .. note:: - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. .. _Building on Linux: @@ -196,133 +196,133 @@ Many of Pillow's features require external libraries: .. tab:: Linux - If you didn't build Python from source, make sure you have Python's - development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. - In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools - In Fedora, the command is:: + In Fedora, the command is:: sudo dnf install python3-devel redhat-rpm-config - In Alpine, the command is:: + In Alpine, the command is:: sudo apk add python3-dev py3-setuptools - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - Note that the package manager may be yum or DNF, depending on the - exact distribution. + Note that the package manager may be yum or DNF, depending on the + exact distribution. - Prerequisites are installed for **Alpine** with:: + Prerequisites are installed for **Alpine** with:: - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. .. tab:: macOS - The Xcode command line tools are required to compile portions of - Pillow. The tools are installed by running ``xcode-select --install`` - from the command line. The command line tools are required even if you - have the full Xcode package installed. It may be necessary to run - ``sudo xcodebuild -license`` to accept the license prior to using the - tools. + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. - The easiest way to install external libraries is via `Homebrew - `_. After you install Homebrew, run:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp - To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Windows - We recommend you use prebuilt wheels from PyPI. - If you wish to compile Pillow manually, you can use the build scripts - in the ``winbuild`` directory used for CI testing and development. - These scripts require Visual Studio 2017 or newer and NASM. + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. - The scripts also install Pillow from the local copy of the source code, so the - `Installing`_ instructions will not be necessary afterwards. + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. - Make sure you have Python and GCC installed:: + Make sure you have Python and GCC installed:: - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools - Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm .. tab:: FreeBSD - .. Note:: Only FreeBSD 10 and 11 tested + .. Note:: Only FreeBSD 10 and 11 tested - Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 - Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Android - Basic Android support has been added for compilation within the Termux - environment. The dependencies can be installed by:: + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo - This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ From c120649632391c1f287cae6b7ff7ce7c28ddb20b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Dec 2022 21:29:27 +1100 Subject: [PATCH 130/205] Remove specific number of jobs from comment --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5cabb6622..e2a9de65c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -141,7 +141,7 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_fribidi.cmd" - # trim ~150MB x 9 + # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' run: rmdir /S /Q winbuild\build\src From d822d85af6d9b1c108650832d56b75cd86b2f5a9 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Fri, 2 Dec 2022 17:57:19 +0000 Subject: [PATCH 131/205] support round-tripping JPEG comments --- Tests/test_file_jpeg.py | 12 ++++++++++++ src/PIL/JpegImagePlugin.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b..94ef59565 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -87,6 +87,18 @@ class TestFileJpeg: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + def test_com_write(self): + dummy_text = "this is a test comment" + with Image.open(TEST_FILE) as im: + with BytesIO() as buf: + im.save(buf, format="JPEG") + with Image.open(buf) as im2: + assert im.app['COM'] == im2.app['COM'] + with BytesIO() as buf: + im.save(buf, format="JPEG", comment=dummy_text) + with Image.open(buf) as im2: + assert im2.app['COM'].decode() == dummy_text + def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc..a6abe8b9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,6 +44,7 @@ import warnings from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._binary import o16be as o16 from ._binary import o8 from ._deprecate import deprecate from .JpegPresets import presets @@ -713,6 +714,15 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + comment = info.get("comment") + if comment is None and isinstance(im, JpegImageFile): + comment = im.app.get('COM') + if comment: + if isinstance(comment, str): + comment = comment.encode() + size = o16(2 + len(comment)) + extra += b'\xFF\xFE%s%s' % (size, comment) + icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 From e9f485849157100ddf75f289db6fcb509927706c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:07:07 +0000 Subject: [PATCH 132/205] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 4 ++-- src/PIL/JpegImagePlugin.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 94ef59565..ffaf2caba 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -93,11 +93,11 @@ class TestFileJpeg: with BytesIO() as buf: im.save(buf, format="JPEG") with Image.open(buf) as im2: - assert im.app['COM'] == im2.app['COM'] + assert im.app["COM"] == im2.app["COM"] with BytesIO() as buf: im.save(buf, format="JPEG", comment=dummy_text) with Image.open(buf) as im2: - assert im2.app['COM'].decode() == dummy_text + assert im2.app["COM"].decode() == dummy_text def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6abe8b9f..cb8a4e57f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,8 +44,8 @@ import warnings from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._binary import o16be as o16 from ._binary import o8 +from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets @@ -716,12 +716,12 @@ def _save(im, fp, filename): comment = info.get("comment") if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get('COM') + comment = im.app.get("COM") if comment: if isinstance(comment, str): comment = comment.encode() size = o16(2 + len(comment)) - extra += b'\xFF\xFE%s%s' % (size, comment) + extra += b"\xFF\xFE%s%s" % (size, comment) icc_profile = info.get("icc_profile") if icc_profile: From 976ad5746a0155135337efa75648c550705215c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:29:02 +1100 Subject: [PATCH 133/205] Save comments from any image format by default --- src/PIL/JpegImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index cb8a4e57f..c9de714d8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,9 +714,7 @@ def _save(im, fp, filename): extra = info.get("extra", b"") - comment = info.get("comment") - if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get("COM") + comment = info.get("comment", im.info.get("comment")) if comment: if isinstance(comment, str): comment = comment.encode() From c1d0a00943ee6fcc993f47047b798e2b6f9bac6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:31:05 +1100 Subject: [PATCH 134/205] Use _binary instead of struct --- src/PIL/JpegImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index c9de714d8..92dbb3193 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -732,7 +732,7 @@ def _save(im, fp, filename): icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] i = 1 for marker in markers: - size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) extra += ( b"\xFF\xE2" + size From 525c01143a8a4e0133908826577ccb54ed829a1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:59:22 +1100 Subject: [PATCH 135/205] Test that comment is reread --- Tests/test_file_jpeg.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ffaf2caba..bb4ebb686 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -86,18 +86,26 @@ class TestFileJpeg: assert len(im.applist) == 2 assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + assert im.app["COM"] == im.info["comment"] - def test_com_write(self): - dummy_text = "this is a test comment" + def test_comment_write(self): with Image.open(TEST_FILE) as im: - with BytesIO() as buf: - im.save(buf, format="JPEG") - with Image.open(buf) as im2: - assert im.app["COM"] == im2.app["COM"] - with BytesIO() as buf: - im.save(buf, format="JPEG", comment=dummy_text) - with Image.open(buf) as im2: - assert im2.app["COM"].decode() == dummy_text + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + + # Test that existing comment is saved by default + out = BytesIO() + im.save(out, format="JPEG") + with Image.open(out) as reloaded: + assert im.info["comment"] == reloaded.info["comment"] + + # Test that a comment argument overrides the default comment + for comment in ("Test comment text", b"Text comment text"): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + if not isinstance(comment, bytes): + comment = comment.encode() + assert reloaded.info["comment"] == comment def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, From 61cbcaee64a852bc9902d60ab0732f676d6d0e72 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:35:01 +1100 Subject: [PATCH 136/205] Changed indentation to be consistent --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 89b2e558f..b559c824d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -205,11 +205,11 @@ Many of Pillow's features require external libraries: In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. From eafff0e1396a1b55522c561b7e355c4e7ebaa5b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:54:04 +1100 Subject: [PATCH 137/205] Use compile_python_fuzzer --- Tests/oss-fuzz/build.sh | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 09cc7bc16..b459ee47a 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -19,9 +19,7 @@ python3 setup.py build --build-base=/tmp/build install # Build fuzzers in $OUT. for fuzzer in $(find $SRC -name 'fuzz_*.py'); do - fuzzer_basename=$(basename -s .py $fuzzer) - fuzzer_package=${fuzzer_basename}.pkg - pyinstaller \ + compile_python_fuzzer $fuzzer \ --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ @@ -31,17 +29,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --add-binary /usr/local/lib/libwebp.so.7:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \ - --add-binary /usr/local/lib/libxcb.so.1:. \ - --distpath $OUT --onefile --name $fuzzer_package $fuzzer - - # Create execution wrapper. - echo "#!/bin/sh -# LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") -LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \ -ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \ -\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename - chmod u+x $OUT/$fuzzer_basename + --add-binary /usr/local/lib/libxcb.so.1:. done find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ From 8e70787cf20399f2a88976ad8ad4d3983f81c741 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 01:44:21 +0000 Subject: [PATCH 138/205] Update cygwin/cygwin-install-action action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index bbf0ee736..37dc694c6 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v2 + uses: cygwin/cygwin-install-action@v3 with: platform: x86_64 packages: > From 61f27211c2d50a20ac544ff8ab16c58439c4cbe8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Dec 2022 07:43:58 +0200 Subject: [PATCH 139/205] Fix comment Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 07f02c236..449530717 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_style = space trim_trailing_whitespace = true [*.rst] -# Three-space indentation +# Four-space indentation indent_size = 4 [*.yml] From 1ed1a3a971e127b55353bde39cb7ea6bedb45c04 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Sat, 3 Dec 2022 15:07:37 +0000 Subject: [PATCH 140/205] make sure passing a blank comment removes existing comment --- Tests/test_file_jpeg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bb4ebb686..7a958c7da 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -98,6 +98,13 @@ class TestFileJpeg: with Image.open(out) as reloaded: assert im.info["comment"] == reloaded.info["comment"] + # Ensure that a blank comment causes any existing comment to be removed + for comment in ("", b"", None): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + assert "comment" not in reloaded.info + # Test that a comment argument overrides the default comment for comment in ("Test comment text", b"Text comment text"): out = BytesIO() From 8ada23ed04ee18730d44d14dd82b0aabc12a0917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 09:09:00 +1100 Subject: [PATCH 141/205] Added IFD1 reading --- Tests/test_image.py | 21 ++++++++++++++++++++- src/PIL/ExifTags.py | 1 + src/PIL/Image.py | 10 +++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e57903490..b4e81e466 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,7 +7,14 @@ import warnings import pytest -from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features +from PIL import ( + ExifTags, + Image, + ImageDraw, + ImagePalette, + UnidentifiedImageError, + features, +) from .helper import ( assert_image_equal, @@ -808,6 +815,18 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + def test_exif_ifd1(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(ExifTags.IFD.IFD1) == { + 513: 2036, + 514: 5448, + 259: 6, + 296: 2, + 282: 180.0, + 283: 180.0, + } + def test_exif_ifd(self): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 97a21335f..ffab7e554 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -353,3 +353,4 @@ class IFD(IntEnum): GPSInfo = 34853 Makernote = 37500 Interop = 40965 + IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d07fc716c..1f3d4b74f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3641,17 +3641,17 @@ class Exif(MutableMapping): def get_ifd(self, tag): if tag not in self._ifds: - if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - # exif, gpsinfo + if tag == ExifTags.IFD.IFD1: + if self._info is not None: + self._ifds[tag] = self._get_ifd_dict(self._info.next) + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: - # interop, makernote if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] if tag == ExifTags.IFD.Makernote: - # makernote from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -3727,7 +3727,7 @@ class Exif(MutableMapping): makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self._ifds[tag] = makernote else: - # interop + # Interop self._ifds[tag] = self._get_ifd_dict(tag_data) return self._ifds.get(tag, {}) From e50ae85ea406d86073ca88ffdec469e1e18d7527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 13:57:26 +1100 Subject: [PATCH 142/205] Use jpeg_write_marker to write comment --- src/PIL/JpegImagePlugin.py | 12 +++++------- src/encode.c | 26 ++++++++++++++++++++++---- src/libImaging/Jpeg.h | 4 ++++ src/libImaging/JpegEncode.c | 13 ++++++++++++- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 92dbb3193..7b5b32be0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,13 +714,6 @@ def _save(im, fp, filename): extra = info.get("extra", b"") - comment = info.get("comment", im.info.get("comment")) - if comment: - if isinstance(comment, str): - comment = comment.encode() - size = o16(2 + len(comment)) - extra += b"\xFF\xFE%s%s" % (size, comment) - icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 @@ -743,6 +736,10 @@ def _save(im, fp, filename): ) i += 1 + comment = info.get("comment", im.info.get("comment")) or b"" + if isinstance(comment, str): + comment = comment.encode() + # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) @@ -765,6 +762,7 @@ def _save(im, fp, filename): dpi[1], subsampling, qtables, + comment, extra, exif, ) diff --git a/src/encode.c b/src/encode.c index 72c7f64d0..a2eae81fd 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { PyObject *qtables = NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; + char *comment = NULL; + Py_ssize_t comment_size; char *extra = NULL; Py_ssize_t extra_size; char *rawExif = NULL; @@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#", + "ss|nnnnnnnnOy#y#y#", &mode, &rawmode, &quality, @@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &ydpi, &subsampling, &qtables, + &comment, + &comment_size, &extra, &extra_size, &rawExif, @@ -1090,12 +1094,24 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } - // Freed in JpegEncode, Case 5 + // Freed in JpegEncode, Case 6 qarrays = get_qtables_arrays(qtables, &qtablesLen); + if (comment && comment_size > 0) { + /* malloc check ok, length is from python parsearg */ + char *p = malloc(comment_size); // Freed in JpegEncode, Case 6 + if (!p) { + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + comment = p; + } else { + comment = NULL; + } + if (extra && extra_size > 0) { /* malloc check ok, length is from python parsearg */ - char *p = malloc(extra_size); // Freed in JpegEncode, Case 5 + char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { return ImagingError_MemoryError(); } @@ -1107,7 +1123,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (rawExif && rawExifLen > 0) { /* malloc check ok, length is from python parsearg */ - char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 + char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { if (extra) { free(extra); @@ -1134,6 +1150,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; + ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index a876d3bb6..1d7550818 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -92,6 +92,10 @@ typedef struct { /* in factors of DCTSIZE2 */ int qtablesLen; + /* Comment */ + char *comment; + size_t comment_size; + /* Extra data (to be injected after header) */ char *extra; int extra_size; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index a44debcaf..b6e3acc95 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } case 4: + + if (context->comment_size > 0) { + jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); + } + state->state++; + + case 5: if (1024 > context->destination.pub.free_in_buffer) { break; } @@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->state++; /* fall through */ - case 5: + case 6: /* Finish compression */ if (context->destination.pub.free_in_buffer < 100) { @@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { jpeg_finish_compress(&context->cinfo); /* Clean up */ + if (context->comment) { + free(context->comment); + context->comment = NULL; + } if (context->extra) { free(context->extra); context->extra = NULL; From 72ac7d1ce9e15803e4adb759dbd318b75d652724 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 18:53:28 +1100 Subject: [PATCH 143/205] Corrected default combined frame duration --- Tests/images/duplicate_frame.gif | Bin 0 -> 138 bytes Tests/test_file_gif.py | 16 ++++++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Tests/images/duplicate_frame.gif diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef0c894a540b0ca3074938666fd22a7d93d1fd0d GIT binary patch literal 138 zcmZ?wbhEHb Date: Mon, 5 Dec 2022 17:46:54 +0000 Subject: [PATCH 144/205] switch to #z for comment parameter * means `comment=None` can be passed directly * no need to conditionally run `str.encode()` * clean up checking of whether a comment is passed --- src/PIL/JpegImagePlugin.py | 4 +--- src/encode.c | 2 +- src/libImaging/JpegEncode.c | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7b5b32be0..ef0be6699 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -736,9 +736,7 @@ def _save(im, fp, filename): ) i += 1 - comment = info.get("comment", im.info.get("comment")) or b"" - if isinstance(comment, str): - comment = comment.encode() + comment = info.get("comment", im.info.get("comment")) # "progressive" is the official name, but older documentation # says "progression" diff --git a/src/encode.c b/src/encode.c index a2eae81fd..d37cbfbcf 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1057,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#y#", + "ss|nnnnnnnnOz#y#y#", &mode, &rawmode, &quality, diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index b6e3acc95..2a24eff39 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -278,7 +278,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { case 4: - if (context->comment_size > 0) { + if (context->comment) { jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); } state->state++; From b786ff819a9799974e55db3d639b0de0b49d9b89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:25:01 +0000 Subject: [PATCH 145/205] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c13fb3b1..d44874bf7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -37,7 +37,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-json From c2a42655e10c7b3888f3c50b49717886296e1720 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 19:30:53 +1100 Subject: [PATCH 146/205] Allow get_child_images to access JPEG thumbnails --- Tests/images/flower_thumbnail.png | Bin 0 -> 35617 bytes Tests/test_file_jpeg.py | 7 +++++ src/PIL/Image.py | 43 ++++++++++++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 1 + src/PIL/TiffImagePlugin.py | 33 ----------------------- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 Tests/images/flower_thumbnail.png diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..4a362535f25fcdf10c49c368f4252f48f8d45bd0 GIT binary patch literal 35617 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 RR_6c!002ovPDHLkV1fuV02lxO literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b..987187556 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -415,6 +415,13 @@ class TestFileJpeg: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" + def test_get_child_images(self): + with Image.open("Tests/images/flower.jpg") as im: + ims = im.get_child_images() + + assert len(ims) == 1 + assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1f3d4b74f..e568e6afa 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1454,6 +1454,49 @@ class Image: self._exif._loaded = False self.getexif() + def get_child_images(self): + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + thumbnailOffset = ifd.get(513) + if thumbnailOffset is not None: + try: + thumbnailOffset += self._exif_offset + except AttributeError: + pass + self.fp.seek(thumbnailOffset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) + + with open(fp) as im: + if thumbnailOffset is None: + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def getim(self): """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc..f2d8c4846 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -89,6 +89,7 @@ def APP(self, marker): if "exif" not in self.info: # extract EXIF information (incomplete) self.info["exif"] = s # FIXME: value will change + self._exif_offset = self.fp.tell() - n + 6 elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab9ac5ea2..aa2a782c2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1153,39 +1153,6 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - def get_child_images(self): - if SUBIFD not in self.tag_v2: - return [] - child_images = [] - exif = self.getexif() - offset = None - for im_offset in self.tag_v2[SUBIFD]: - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - current_offset = self._fp.tell() - if offset is None: - offset = current_offset - - fp = self._fp - ifd = exif._get_ifd_dict(im_offset) - jpegInterchangeFormat = ifd.get(513) - if jpegInterchangeFormat is not None: - fp.seek(jpegInterchangeFormat) - jpeg_data = fp.read(ifd.get(514)) - - fp = io.BytesIO(jpeg_data) - - with Image.open(fp) as im: - if jpegInterchangeFormat is None: - im._frame_pos = [im_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self._fp.seek(offset) - return child_images - def getxmp(self): """ Returns a dictionary containing the XMP tags. From 1d780081a620b00007a8fe93db469e5759c86868 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 20:22:12 +1100 Subject: [PATCH 147/205] Free comment when returning early --- src/encode.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encode.c b/src/encode.c index d37cbfbcf..e6352cbfe 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1113,6 +1113,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { + if (comment) { + free(comment); + } return ImagingError_MemoryError(); } memcpy(p, extra, extra_size); @@ -1125,6 +1128,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { + if (comment) { + free(comment); + } if (extra) { free(extra); } From 674ec6ec4dd1083b4666e283459beeba0e422fb4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 6 Dec 2022 20:55:34 +0200 Subject: [PATCH 148/205] Add support for PyPy3.9, drop PyPy3.7 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e2a9de65c..487c3586f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -19,9 +19,9 @@ jobs: architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy-3.7" + - python-version: "pypy3.8" architecture: "x64" - - python-version: "pypy-3.8" + - python-version: "pypy3.9" architecture: "x64" timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 831e33c13..11c7b77be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,8 @@ jobs: "ubuntu-latest", ] python-version: [ - "pypy-3.8", - "pypy-3.7", + "pypy3.9", + "pypy3.8", "3.11", "3.10", "3.9", From ccac8540771120bdeb570ec5b7bbfc4e3e9a38dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 21:33:09 +1100 Subject: [PATCH 149/205] If available, use wl-paste for grabclipboard() on Linux --- Tests/test_imagegrab.py | 10 +++++++--- src/PIL/ImageGrab.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5e0eca28b..1ad4de63f 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,9 +64,13 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - with pytest.raises(NotImplementedError) as e: - ImageGrab.grabclipboard() - assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" + if not shutil.which("wl-paste"): + with pytest.raises(NotImplementedError) as e: + ImageGrab.grabclipboard() + assert ( + str(e.value) + == "wl-paste is required for ImageGrab.grabclipboard() on Linux" + ) return ImageGrab.grabclipboard() diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 38074cb1b..12ad9ad71 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -132,4 +132,14 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") + if not shutil.which("wl-paste"): + raise NotImplementedError( + "wl-paste is required for ImageGrab.grabclipboard() on Linux" + ) + fh, filepath = tempfile.mkstemp() + subprocess.call(["wl-paste"], stdout=fh) + os.close(fh) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + return im From 2ecf88eaa621266f63405ca7e1fdbdb7ed4d5c8d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:01:37 +1100 Subject: [PATCH 150/205] If available, use xclip for grabclipboard() on Linux --- Tests/test_imagegrab.py | 4 ++-- src/PIL/ImageGrab.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 1ad4de63f..01442dc69 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -68,8 +68,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200 with pytest.raises(NotImplementedError) as e: ImageGrab.grabclipboard() assert ( - str(e.value) - == "wl-paste is required for ImageGrab.grabclipboard() on Linux" + str(e.value) == "wl-paste or xclip is required" + " for ImageGrab.grabclipboard() on Linux" ) return diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 12ad9ad71..8cf956809 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -132,12 +132,16 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - if not shutil.which("wl-paste"): + if shutil.which("wl-paste"): + args = ["wl-paste"] + elif shutil.which("xclip"): + args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] + else: raise NotImplementedError( - "wl-paste is required for ImageGrab.grabclipboard() on Linux" + "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" ) fh, filepath = tempfile.mkstemp() - subprocess.call(["wl-paste"], stdout=fh) + subprocess.call(args, stdout=fh) os.close(fh) im = Image.open(filepath) im.load() From 4704cab1a1b4dfc34b0bc0c06bdcdc56b365b69f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:16:14 +1100 Subject: [PATCH 151/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7fac5201c..f3ad8c797 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Support saving JPEG comments #6774 + [smason, radarhere] + - Added getxmp() to WebPImagePlugin #6758 [radarhere] From bef128b04bcc220aa6b57afa58b796f7b289ddf7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:30:15 +1100 Subject: [PATCH 152/205] Added support for saving JPEG comments --- docs/handbook/image-file-formats.rst | 5 +++++ docs/releasenotes/9.4.0.rst | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ac39625a2..c9e32835a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -474,6 +474,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 2.5.0 +**comment** + A comment about the image. + + .. versionadded:: 9.4.0 + .. note:: diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index f2b50fa5b..ccbe62a6b 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -51,6 +51,14 @@ getxmp() `XMP data `_ can now be decoded for WEBP images through ``getxmp()``. +Writing JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When saving a JPEG image, a comment can now be written from +:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving:: + + im.save(out, comment="Test comment") + Security ======== From 4ab837ae23103b841d2e5fa7ac91a8ff92279627 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Dec 2022 11:35:48 +1100 Subject: [PATCH 153/205] Only compare to previous when checking for duplicate frames while saving --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 32 +++++++++++++++++--------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1ee..2cbaf2805 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) +def test_dispose2_background_frame(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [Image.new("RGBA", (1, 20))] + + different_frame = Image.new("RGBA", (1, 20)) + different_frame.putpixel((0, 10), (255, 0, 0, 255)) + im_list.append(different_frame) + + # Frame that matches the background + im_list.append(Image.new("RGBA", (1, 20))) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as im: + assert im.n_frames == 3 + + def test_transparency_in_second_frame(tmp_path): out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e..367958048 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -565,6 +565,16 @@ def _write_single_frame(im, fp, palette): fp.write(b"\0") # end of image data +def _getbbox(base_im, im_frame): + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) + else: + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) + return delta.getbbox() + + def _write_multiple_frames(im, fp, palette): duration = im.encoderinfo.get("duration") @@ -598,6 +608,12 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] + bbox = _getbbox(previous["im"], im_frame) + if not bbox: + # This frame is identical to the previous frame + if duration: + previous["encoderinfo"]["duration"] += encoderinfo["duration"] + continue if encoderinfo.get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( @@ -606,21 +622,7 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - base_im = background_im - else: - base_im = previous["im"] - if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): - delta = ImageChops.subtract_modulo(im_frame, base_im) - else: - delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") - ) - bbox = delta.getbbox() - if not bbox: - # This frame is identical to the previous frame - if duration: - previous["encoderinfo"]["duration"] += encoderinfo["duration"] - continue + bbox = _getbbox(background_im, im_frame) else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) From 7436ae0933ea4897111d2aebfb16b59a5c960a35 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Dec 2022 16:58:04 +0200 Subject: [PATCH 154/205] Remove unnecessary Pipfile --- MANIFEST.in | 2 - Pipfile | 22 ---- Pipfile.lock | 324 --------------------------------------------------- 3 files changed, 348 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/MANIFEST.in b/MANIFEST.in index 08f6dfc08..f51551303 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include *.c include *.h include *.in -include *.lock include *.md include *.py include *.rst @@ -10,7 +9,6 @@ include *.txt include *.yaml include LICENSE include Makefile -include Pipfile include tox.ini graft Tests graft src diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 1e611a63c..000000000 --- a/Pipfile +++ /dev/null @@ -1,22 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -black = "*" -check-manifest = "*" -coverage = "*" -defusedxml = "*" -packaging = "*" -markdown2 = "*" -olefile = "*" -pyroma = "*" -pytest = "*" -pytest-cov = "*" -pytest-timeout = "*" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 600b19050..000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,324 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "black": { - "hashes": [ - "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", - "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" - ], - "index": "pypi", - "version": "==21.12b0" - }, - "build": { - "hashes": [ - "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", - "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "charset-normalizer": { - "hashes": [ - "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", - "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" - ], - "markers": "python_version >= '3'", - "version": "==2.0.9" - }, - "check-manifest": { - "hashes": [ - "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", - "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" - ], - "index": "pypi", - "version": "==0.47" - }, - "click": { - "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" - ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" - }, - "coverage": { - "hashes": [ - "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", - "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", - "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", - "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", - "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", - "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", - "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", - "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", - "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", - "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", - "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", - "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", - "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", - "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", - "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", - "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", - "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", - "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", - "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", - "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", - "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", - "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", - "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", - "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", - "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", - "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", - "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", - "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", - "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", - "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", - "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", - "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", - "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", - "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", - "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", - "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", - "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", - "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", - "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", - "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", - "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", - "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", - "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", - "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", - "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", - "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", - "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" - ], - "index": "pypi", - "version": "==6.2" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "index": "pypi", - "version": "==0.7.1" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "markdown2": { - "hashes": [ - "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", - "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" - ], - "index": "pypi", - "version": "==2.4.2" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "olefile": { - "hashes": [ - "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" - ], - "index": "pypi", - "version": "==0.46" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "index": "pypi", - "version": "==21.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep517": { - "hashes": [ - "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", - "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" - ], - "version": "==0.12.0" - }, - "platformdirs": { - "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" - ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "pyparsing": { - "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" - }, - "pyroma": { - "hashes": [ - "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", - "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" - ], - "index": "pypi", - "version": "==3.2" - }, - "pytest": { - "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" - ], - "index": "pypi", - "version": "==6.2.5" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-timeout": { - "hashes": [ - "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", - "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" - ], - "index": "pypi", - "version": "==2.0.2" - }, - "requests": { - "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.26.0" - }, - "setuptools": { - "hashes": [ - "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", - "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" - ], - "markers": "python_version >= '3.7'", - "version": "==60.0.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" - } - }, - "develop": {} -} From 66f5ad0eae90b6f4b07df1a3154f996c6fe00069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 10:45:09 +1100 Subject: [PATCH 155/205] Ignore non-opaque WebP background when saving as GIF --- Tests/test_file_gif.py | 13 +++++++++++-- src/PIL/GifImagePlugin.py | 15 +++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1ee..a196c1612 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -859,14 +859,23 @@ def test_background(tmp_path): im.info["background"] = 1 im.save(out) with Image.open(out) as reread: - assert reread.info["background"] == im.info["background"] + +def test_webp_background(tmp_path): + out = str(tmp_path / "temp.gif") + + # Test opaque WebP background if features.check("webp") and features.check("webp_anim"): with Image.open("Tests/images/hopper.webp") as im: - assert isinstance(im.info["background"], tuple) + assert im.info["background"] == (255, 255, 255, 255) im.save(out) + # Test non-opaque WebP background + im = Image.new("L", (100, 100), "#000") + im.info["background"] = (0, 0, 0, 0) + im.save(out) + def test_comment(tmp_path): with Image.open(TEST_GIF) as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e..01518b378 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -886,20 +886,23 @@ def _get_palette_bytes(im): def _get_background(im, info_background): background = 0 if info_background: - background = info_background - if isinstance(background, tuple): + if isinstance(info_background, tuple): # WebPImagePlugin stores an RGBA value in info["background"] # So it must be converted to the same format as GifImagePlugin's # info["background"] - a global color table index try: - background = im.palette.getcolor(background, im) + background = im.palette.getcolor(info_background, im) except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": + if str(e) not in ( # If all 256 colors are in use, # then there is no need for the background color - return 0 - else: + "cannot allocate more than 256 colors", + # Ignore non-opaque WebP background + "cannot add non-opaque RGBA color to RGB palette", + ): raise + else: + background = info_background return background From 4f0b83cc54230728bbd3593a3116cac046c5ee4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 12:29:27 +1100 Subject: [PATCH 156/205] Only set tile in ImageFile __setstate__ --- src/PIL/Image.py | 1 - src/PIL/ImageFile.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..bf93917ed 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -704,7 +704,6 @@ class Image: def __setstate__(self, state): Image.__init__(self) - self.tile = [] info, mode, size, palette, data = state self.info = info self.mode = mode diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f281b9e14..dbdc0cb38 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -137,6 +137,10 @@ class ImageFile(Image.Image): if self.format is not None: return Image.MIME.get(self.format.upper()) + def __setstate__(self, state): + self.tile = [] + super().__setstate__(state) + def verify(self): """Check file integrity""" From ae3f43de64afbd59fdc424f37f18964dd25765e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Dec 2022 19:48:07 +1100 Subject: [PATCH 157/205] Document Hue range --- docs/handbook/concepts.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index a9b33e437..083351eec 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -41,6 +41,9 @@ supports the following standard modes: * ``LAB`` (3x8-bit pixels, the L*a*b color space) * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) + + * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees + * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) From f6f622dceee19fef36e6746a7943f2e806d8cabd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:36:27 +1100 Subject: [PATCH 158/205] Clarify apply_transparency() docstring --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..155a546c2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1482,7 +1482,8 @@ class Image: def apply_transparency(self): """ If a P mode image has a "transparency" key in the info dictionary, - remove the key and apply the transparency to the palette instead. + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. """ if self.mode != "P" or "transparency" not in self.info: return From 164311a7568c7fed3c7a1dd60570cc182d3d5a0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:55:10 +1100 Subject: [PATCH 159/205] Specify "I" and "F" ranges --- docs/handbook/concepts.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 083351eec..f3fa1f2b1 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -24,9 +24,10 @@ To get the number and names of bands in an image, use the Modes ----- -The ``mode`` of an image is a string which defines the type and depth of a pixel in the image. -Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range -of 0-1, an 8-bit pixel has a range of 0-255 and so on. The current release +The ``mode`` of an image is a string which defines the type and depth of a pixel in the +image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of +0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of +INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) From 6da4169f3724ffe20c72d8ef4a2e0dc21815b343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Dec 2022 22:40:55 +1100 Subject: [PATCH 160/205] Fixed writing int as ASCII tag --- Tests/test_file_tiff_metadata.py | 13 +++++++------ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index b90dde3d9..48c0273fe 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,20 +185,21 @@ def test_iptc(tmp_path): im.save(out) -def test_writing_bytes_to_ascii(tmp_path): +def test_writing_other_types_to_ascii(tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] assert tag.type == TiffTags.ASCII - info[271] = b"test" - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info) + for (value, expected) in {b"test": "test", 1: "1"}.items(): + info[271] = value - with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == "test" + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected def test_writing_int_to_bytes(tmp_path): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab9ac5ea2..791e692c1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -732,6 +732,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if isinstance(value, int): + value = str(value) if not isinstance(value, bytes): value = value.encode("ascii", "replace") return value + b"\0" From 1f9754cdc0c03405bbe1e3aa73b5dbb6750aa608 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:04:30 +0200 Subject: [PATCH 161/205] Format tox.ini with tox-ini-fmt --- .pre-commit-config.yaml | 9 +++++++-- tox.ini | 24 ++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d44874bf7..8d133b18d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: ["--target-version", "py37"] @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.11.1 hooks: - id: isort @@ -48,5 +48,10 @@ repos: hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 0.5.2 + hooks: + - id: tox-ini-fmt + ci: autoupdate_schedule: monthly diff --git a/tox.ini b/tox.ini index 21b5d4b50..195522ffa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,13 @@ -# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, -# "python3 -m pip install tox" and then run "tox" from this directory. - [tox] envlist = lint - py{37,38,39,310,311,py3} + py{py3, 311, 310, 39, 38, 37} minversion = 1.9 [testenv] +deps = + cffi + numpy extras = tests commands = @@ -17,16 +15,14 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -deps = - cffi - numpy [testenv:lint] +passenv = + PRE_COMMIT_COLOR +skip_install = true +deps = + check-manifest + pre-commit commands = pre-commit run --all-files --show-diff-on-failure check-manifest -deps = - pre-commit - check-manifest -skip_install = true -passenv = PRE_COMMIT_COLOR From bfa1f3290c8ae830e0240dbfad2626fa6b49bb1b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:06:58 +0200 Subject: [PATCH 162/205] Add allowlist_externals=make to fix tox 4 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 195522ffa..9a41ca96b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} +allowlist_externals = make [testenv:lint] passenv = From 56964da7487c7fff897cd3b41f11d62922f84046 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:45:57 +1100 Subject: [PATCH 163/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f3ad8c797..1bcb9d2e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed bug combining GIF frame durations #6779 + [radarhere] + - Support saving JPEG comments #6774 [smason, radarhere] From 5301b86f1cd255fc55a464b38af176f37f91c396 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:48:36 +1100 Subject: [PATCH 164/205] Use snake case --- 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 e568e6afa..c2216e27a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1476,18 +1476,18 @@ class Image: offset = current_offset fp = self.fp - thumbnailOffset = ifd.get(513) - if thumbnailOffset is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: try: - thumbnailOffset += self._exif_offset + thumbnail_offset += self._exif_offset except AttributeError: pass - self.fp.seek(thumbnailOffset) + self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) with open(fp) as im: - if thumbnailOffset is None: + if thumbnail_offset is None: im._frame_pos = [ifd_offset] im._seek(0) im.load() From b564f3e6bf82bb705ae410b44422aefbc56198e2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 07:41:39 +1100 Subject: [PATCH 165/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1bcb9d2e9..0372b5b37 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added IFD enum to ExifTags #6748 + [radarhere] + - Fixed bug combining GIF frame durations #6779 [radarhere] From e25d6031891cd53917379bb489d7b22614fe06fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 09:48:46 +1100 Subject: [PATCH 166/205] Updated xz to 5.4.0 --- 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 66e352c73..0c3152b06 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.9.tar.gz/download", - "filename": "xz-5.2.9.tar.gz", - "dir": "xz-5.2.9", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download", + "filename": "xz-5.4.0.tar.gz", + "dir": "xz-5.4.0", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From d1cb81976cba7fbd3b13525a26b163cc42f029a7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:32:55 +0200 Subject: [PATCH 167/205] Run Bandit on CI via pre-commit --- .pre-commit-config.yaml | 9 ++++++++- src/PIL/ImageShow.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d133b18d..609352f22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 22.12.0 hooks: - id: black - args: ["--target-version", "py37"] + args: [--target-version=py37] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] @@ -13,6 +13,13 @@ repos: hooks: - id: isort + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + args: [--severity-level=high] + files: ^src/ + - repo: https://github.com/asottile/yesqa rev: v1.4.0 hooks: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 76f42a307..9d5224588 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -125,7 +125,7 @@ class Viewer: path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") - os.system(self.get_command(path, **options)) + os.system(self.get_command(path, **options)) # nosec return 1 From 1a051f2e079253c74918ac89cbe899f4c6136bc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Dec 2022 07:50:40 +0000 Subject: [PATCH 168/205] Update egor-tensin/cleanup-path action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 37dc694c6..f297eb1b5 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -48,7 +48,7 @@ jobs: qt5-devel-tools subversion xorg-server-extra zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v2 + uses: egor-tensin/cleanup-path@v3 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' From 7f6fe3c28728f0e68dba58b6ea9843de0b00ca3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Dec 2022 08:15:32 +1100 Subject: [PATCH 169/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0372b5b37..1e5f71b86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 + [radarhere] + - Added IFD enum to ExifTags #6748 [radarhere] From 5eaca52efd86e41dc068802fd2683d433a45003e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 07:04:05 +1100 Subject: [PATCH 170/205] Updated harfbuzz to 6.0.0 --- 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 0c3152b06..a1908e35e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -355,9 +355,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip", - "filename": "harfbuzz-5.3.1.zip", - "dir": "harfbuzz-5.3.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", + "filename": "harfbuzz-6.0.0.zip", + "dir": "harfbuzz-6.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 2a86d7353f1a1435d564d0ac882268c80fc9486d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 08:19:15 +1100 Subject: [PATCH 171/205] Always initialize all plugins in registered_extensions() --- Tests/test_image.py | 6 ------ src/PIL/Image.py | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b4e81e466..69a66b85a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -401,8 +401,6 @@ class TestImage: def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 - extension = Image.EXTENSION - Image.EXTENSION = {} # Act Image.registered_extensions() @@ -410,10 +408,6 @@ class TestImage: # Assert assert Image._initialized == 2 - # Restore the original state and assert - Image.EXTENSION = extension - assert Image.EXTENSION - def test_registered_extensions(self): # Arrange # Open an image to trigger plugin registration diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c2216e27a..6288f46ef 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3418,8 +3418,7 @@ def registered_extensions(): Returns a dictionary containing all file extensions belonging to registered plugins """ - if not EXTENSION: - init() + init() return EXTENSION From 88e127d1b27f6dfbc6ddfe4ffba13748e89f4d11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 22:16:07 +0000 Subject: [PATCH 172/205] Update actions/stale action to v7 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ffac91cec..8c210bc90 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v6 + uses: actions/stale@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" From a065e0252b563b4d7a490ddd1a1eb7c8089662c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 09:29:25 +1100 Subject: [PATCH 173/205] Updated deprecated NumPy alias --- Tests/test_numpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 185e477ec..3de7ec30f 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -34,7 +34,7 @@ def test_numpy_to_image(): # Check supported 1-bit integer formats assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE) # Check supported 8-bit integer formats assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) @@ -193,7 +193,7 @@ def test_putdata(): "dtype", ( bool, - numpy.bool8, + numpy.bool_, numpy.int8, numpy.int16, numpy.int32, From d6e79045280be42cf2273716b18d82661cf7f779 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 12:47:46 +1100 Subject: [PATCH 174/205] Removed Python 3.7 on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index f297eb1b5..7b8070d34 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-minor-version: [7, 8, 9] + python-minor-version: [8, 9] timeout-minutes: 40 From 967034356a72d02e4cddad5ac4b6c75299d08394 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 14:20:47 +1100 Subject: [PATCH 175/205] Fixed BytesWarning --- src/PIL/PpmImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 392771d3e..1670d9d64 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -208,7 +208,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError(f"Invalid token for this mode: {bytes([token])}") + raise ValueError( + b"Invalid token for this mode: %s" % bytes([token]) + ) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -242,13 +244,13 @@ class PpmPlainDecoder(ImageFile.PyDecoder): half_token = tokens.pop() # save half token for later if len(half_token) > max_len: # prevent buildup of half_token raise ValueError( - f"Token too long found in data: {half_token[:max_len + 1]}" + b"Token too long found in data: %s" % half_token[: max_len + 1] ) for token in tokens: if len(token) > max_len: raise ValueError( - f"Token too long found in data: {token[:max_len + 1]}" + b"Token too long found in data: %s" % token[: max_len + 1] ) value = int(token) if value > maxval: From 1df7e75205247ae3ef021a623659d532bd5a4f15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 06:52:06 +1100 Subject: [PATCH 176/205] Python 3.7 on Cygwin is no longer part of CI --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index b559c824d..b188020b9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -460,7 +460,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ -| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 | +| | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ From a4ac40354916401063028fc9af402e830eaf8606 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 07:14:02 +1100 Subject: [PATCH 177/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1e5f71b86..04b3fc4c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Ignore non-opaque WebP background when saving as GIF #6792 + [radarhere] + +- Only set tile in ImageFile __setstate__ #6793 + [radarhere] + - When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 [radarhere] From 9898613c4d276f8065d5e3bbb5fda7f4715e90d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 15:31:36 +1100 Subject: [PATCH 178/205] Fixed saving EXIF data to MPO --- Tests/test_file_mpo.py | 5 ++++- src/PIL/MpoImagePlugin.py | 14 +++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index dba1ec1b1..3e5476222 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -80,7 +80,10 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) def test_exif(test_file): - with Image.open(test_file) as im: + with Image.open(test_file) as im_original: + im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) + + for im in (im_original, im_reloaded): info = im._getexif() assert info[272] == "Nintendo 3DS" assert info[296] == 2 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 3ae4d4abf..095cfe7ee 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -52,14 +52,22 @@ def _save_all(im, fp, filename): _save(im, fp, filename) return + mpf_offset = 28 offsets = [] for imSequence in itertools.chain([im], append_images): for im_frame in ImageSequence.Iterator(imSequence): if not offsets: # APP2 marker - im.encoderinfo["extra"] = ( + im_frame.encoderinfo["extra"] = ( b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) + exif = im_frame.encoderinfo.get("exif") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif + if exif: + mpf_offset += 4 + len(exif) + JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: @@ -79,11 +87,11 @@ def _save_all(im, fp, filename): mptype = 0x000000 # Undefined mpentries += struct.pack(" Date: Thu, 22 Dec 2022 17:16:52 +1100 Subject: [PATCH 179/205] Initialize unsigned char variables --- src/libImaging/Quant.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index dfa6d842d..783852c24 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1717,7 +1717,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { withAlpha = !strcmp(im->mode, "RGBA"); int transparency = 0; - unsigned char r, g, b; + unsigned char r = 0, g = 0, b = 0; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; From 88f15eb9f07b0434a6b2831b02d402dd4efdee6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 12:10:36 +1100 Subject: [PATCH 180/205] Do not save EXIF from info --- Tests/test_file_png.py | 12 ++++++++++-- src/PIL/PngImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 37235fe6f..9481cd5dd 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -706,10 +706,18 @@ class TestFilePng: assert exif[274] == 3 def test_exif_save(self, tmp_path): + # Test exif is not saved from info + test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: - test_file = str(tmp_path / "temp.png") im.save(test_file) + with Image.open(test_file) as reloaded: + assert reloaded._getexif() is None + + # Test passing in exif + with Image.open("Tests/images/exif.png") as im: + im.save(test_file, exif=im.getexif()) + with Image.open(test_file) as reloaded: exif = reloaded._getexif() assert exif[274] == 1 @@ -720,7 +728,7 @@ class TestFilePng: def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") - im.save(test_file) + im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: exif = reloaded._getexif() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2c53be109..b6a3c4cb6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1383,7 +1383,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif", im.info.get("exif")) + exif = im.encoderinfo.get("exif") if exif: if isinstance(exif, Image.Exif): exif = exif.tobytes(8) From 9e6a7d974084a4d7b6be9d68b732558194d20e51 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 17:43:18 +1100 Subject: [PATCH 181/205] Added support for uncompressed L images --- Tests/images/uncompressed_l.dds | Bin 0 -> 16512 bytes Tests/images/uncompressed_l.png | Bin 0 -> 861 bytes Tests/test_file_dds.py | 12 +++++++++-- src/PIL/DdsImagePlugin.py | 35 ++++++++++++++++++++++++-------- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 Tests/images/uncompressed_l.dds create mode 100644 Tests/images/uncompressed_l.png diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds new file mode 100644 index 0000000000000000000000000000000000000000..b82282587ec30665fceafced278f1c59b3402ed1 GIT binary patch literal 16512 zcmeH}-EHGA5QLRH-P>I{2-3o};0}Uxa}TK}#ia!we>Mbmxid@IFa*O(Adcw~@eMx; zf=;LR*MHl#{rikdie4wCtD@J5$rZg$Odd=pyeTl@O@Rr&tAImS3LLsp;L!Id0QjK*;D-W$ zUsiB1AL2y-+`b5a+g}qv@T~yBw*myeRN!Df#TNl|`$YiV{(^u9=Lg$A2l~GQ{ow&5 zpBqU3+`zrxe;YskeE#v{zyA5p51_m@(gG!?cVO_^Sz~$wl>F9wR-n}<1zJu7v^@NP z24p2HAUP}$lKTZm^U(>6`arZRRKVo%R5fre zR}Gw8HE{BUpFaA4SDyNS`QJWZP6L6%3Ic}}1b$%!MXwXnRnhCjXRluPu1rA*)aOis!0Q^t@@IwK>FDp2h5Ah-ZZeIkz?XL+S_*Q`6TLFS!DsV8L z;)?*f{UU&Fe?h>5^Mmbw1o}^b{_p^j&kdx1Zs6YUzl|S%KL7afU;q5)2T)!cX@Qc{ zJ1}_dtTDX-N`7lTE70nb0xc&3S{{Bt1F{kpkQ^2W$^C+(`RD{jeIQ`uK)}f3FOCAW z$Z4Q*r-91D$yC6smsv0<#s|HT48aVmFPj7v|D_{D6`BNV-r-8s>1%bl~ z0>7|=qSuM(s_1oMaz(EblLylYZwgF!Q((gHD&WwS0*9^?IP^UV0DdR{_@MycmlYh$ zhjsv0<#s|HT4 t8aVmFPf`J1DHUKY6=0r08b||aAPuB}G>`_;KpIE`X&?=xfi!T4f&Vw%wVD6` literal 0 HcmV?d00001 diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png new file mode 100644 index 0000000000000000000000000000000000000000..9d22a26a446d3dbdfd8f9c931ea466f6c6424e90 GIT binary patch literal 861 zcmeAS@N?(olHy`uVBq!ia0vp^4Is<`Bp9BB+KDqTFspdFIEGZrc^h?ls*<5Vur)JB zqlg-7P(pKyh(}`Z41qR*8_gT%*K1u(y=E67zCX-RIr5$RbYFRmk9~(3o90{0iTo?q zZokIPVc}P`n#F#$cTRI}Z`b*cHdxb-Qkj7%xN9XC&Z5~v}sUc zQF_LyDDi8i`mdSklfSkId~vT6XS8VuNn~8YHGhS2Q^L#RQrG)UyL@3X&}x&GB>;Tib{;+mb; z9E?oS*}aKX$Uxa5;eK2CjmcMXGaH^hIH47DKd5laQEeT@eua-ZO-D9&R()gjOuir% zvW=}bGBx``)z5EfnVi1_vX=2LOsg_|ey?EeT|36sB|Uh!dQU<13AVi4r5g)!U-!1I-}Y|HguFw3`xCAHw!QqqenxjbTuMSUDiOXag;?62vFzD;OU-N3|GNKs$H|s>F&hgWVD=Cdb6BTa*mH8 zSvtelvqpRLzjllcxGA-Tb?VxK$@jOLdwX91dKc*H#c{mLBht%bs(#FW&Mb`zE`Q|r X-moxli&n>DP`>eW^>bP0l+XkK*RXjv literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 4b9f8949e..f579cd1c2 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -22,6 +22,7 @@ TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -194,8 +195,14 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed_rgb(): - """Check uncompressed RGB images can be opened""" +def test_uncompressed(): + """Check uncompressed images can be opened""" + with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: @@ -305,6 +312,7 @@ def test_save_unsupported_mode(tmp_path): @pytest.mark.parametrize( ("mode", "test_file"), [ + ("L", "Tests/images/linear_gradient.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eea6e3153..b78cc649f 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -135,7 +135,12 @@ class DdsImageFile(ImageFile.ImageFile): fourcc = header.read(4) (bitcount,) = struct.unpack(" Date: Mon, 8 Aug 2022 02:24:55 +0300 Subject: [PATCH 182/205] Add missing LA test textures --- Tests/images/la.dds | Bin 0 -> 32896 bytes Tests/images/la.png | Bin 0 -> 1060 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/images/la.dds create mode 100644 Tests/images/la.png diff --git a/Tests/images/la.dds b/Tests/images/la.dds new file mode 100644 index 0000000000000000000000000000000000000000..30bf93576fd17f80397a1a016c3ee2306e4bf28a GIT binary patch literal 32896 zcmeI4;f>T#420j&0MG%V1zNZc5Z&;DdMJ(-j@^xtFtTUtJV%OdaU|H-`0+4X(6pEBpi3mQLUN zhAVA5p%B0!$UPjuA;|dw9Djv6iQ(a(_u$>I1Ojy6clnLvvWhAUCVm}G%is+ZHu896>c61SC68$csOrpOQNl+^8 zJtZg=cfSOs;_k2AQNU1G`clA9So{<)6l&b7JH(^~&%jUNQoik;evw#x!ROi zq3`pbTr({T)*H&H70aUD|CJNy-#_6Imjze<{;7TgzW@DKCa}DDa=}tu{POEfinTX? zb<;2RP!+ZuVEt!g0Fef~lfiTo-0z~AB=^@93cU%shYJiz?KiL{q5Ws5 z8>+dGpL4Ya?ey1*vMd$_D29ehEs&-Cw(-fT6JTrGTNZ_$gp0 z)VNo7h)D~cfuF*qgfYS;%Yre&CCh?!hx4B)Oj^|Za=oE=_5SH%lR^j>6v)G!66a6; z-I>IsWdU9&rx0aP^H(N77n>Bq1qTIkwJEVe-{(KMW?B}kH^rdg<&vqm`|Ee% zs!aj*l*il^(N7_%fc|P^@WKRkbP;6|{S=c5=&wg1FcjE+64r>;PeEP5`p?J!A`N&a zgXt!?-$gY^?yoBpdJ}XH7Z{S-Z(vPA`_E7}RCAMiI^aT(^Bd$Xk@KHZ^%bt;&-@J6 zasLZk$NlpT0EZy=OaO-<=Lc{Ia{fvc;JSf+I^ep2_5-dPXn#$0flZS4aDh#d`(0p@ z<>kY-L_fHp_6hgqDpmg{j=J}Ih@nI|F6h(W=Tw17T|?)^H(RZZgTdMt~Mp~@1O99%Yv(a|5Segy#M`ICa}D7a=}tu z{PW)cv+q3mQZKq&^c?)NCt#nVTrw4R|J(`K{q;L{y)ubCDI^upe|!SyuSOEp6|p15 zqyqY{=0Exl;J^MJQBYT~{$mrc{xdS}qD_)_a#2l^`*Snk{<>mdO+xoDuqL7X@dUL0 z4AmfSiQLl-@|MW?`3!RYbE-Dz$IbC)ZqSdL<9?jNaeq5{0zHAAKu@42&=cqh^aOeW tJ%OG;PoO8z6X*%_1bPBJfu2B5peN81=n3=$dICLxow)~&Z0 zH5EtM_}-^3Hu{@?P>lS=P97Ps&2Y`uaavk;f+X z|NQ$jeEDRv|F65_YwNN;r`k*j^g*GU zZrQreW}1D!B^c{>PdvO!Q}DcywYc$d-YXYC%pI{Ec)v#0Q1dpzO) z>}bXbHv4f(YB%FVzfuC*mB;y3?5j z1Y_6l`*Zr}^!K%%U%v1;Fm4i`z{@pZZ$nN1!{0UQ-Rxhl{Qn3%@>4BHFG zvO)N_WU1bT6BB&dj~$)VdmKdUP~ty6OIP9JK^?b8&5=DWQEzT4__ZJBudVs8`g7#( zEAQX`@v{F_RsA^p{{34ulKk8O-p@~*f)PiiFVsE|QIi7_fuGYeL8Um$eIo#hy0rW-j^kyJGGiySUreHRK;G{qW88_J1>XAU=NS zyafLm7qg!>=auF^k6h|9g(3RDz4}GE*KdJ{%kKNxA2hsSP1#&w#xU{!KZg8$-&HSO zwq{@j1*3+;H>ORFj9)hIlX{<mc)r>dSOl$mp?&q7^EYrKsANhAsLdGHG zJ20rK7CedkEnz6J*YW@;s5BYRZ}jM#$(_tSC4Y-t#^bFZ{I|zSJfrVe%H_twjVX^e z0tw}HCk}5+m~ShZQl<%b`g*RTKc>bd*xEBgAN@LH9B?8Cq8^B=a` z{7TfFtE}w|A`btsoASe`WPg^sa;d+23pTn#+UJ zi>uD)Er=RDdV4Fvzpy&-gjnM+dDD|ps Date: Fri, 23 Dec 2022 19:07:45 +1100 Subject: [PATCH 183/205] Added support for uncompressed LA images --- Tests/images/{la.dds => uncompressed_la.dds} | Bin Tests/images/{la.png => uncompressed_la.png} | Bin Tests/test_file_dds.py | 40 ++++++++----------- src/PIL/DdsImagePlugin.py | 31 +++++++------- 4 files changed, 34 insertions(+), 37 deletions(-) rename Tests/images/{la.dds => uncompressed_la.dds} (100%) rename Tests/images/{la.png => uncompressed_la.png} (100%) diff --git a/Tests/images/la.dds b/Tests/images/uncompressed_la.dds similarity index 100% rename from Tests/images/la.dds rename to Tests/images/uncompressed_la.dds diff --git a/Tests/images/la.png b/Tests/images/uncompressed_la.png similarity index 100% rename from Tests/images/la.png rename to Tests/images/uncompressed_la.png diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index f579cd1c2..cac4108a8 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -23,6 +23,7 @@ TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" +TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -195,32 +196,24 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed(): +@pytest.mark.parametrize( + ("mode", "size", "test_file"), + [ + ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), + ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), + ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), + ], +) +def test_uncompressed(mode, size, test_file): """Check uncompressed images can be opened""" - with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: + + with Image.open(test_file) as im: assert im.format == "DDS" - assert im.mode == "L" - assert im.size == (128, 128) + assert im.mode == mode + assert im.size == size - assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") - - # convert -format dds -define dds:compression=none hopper.jpg hopper.dds - with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (128, 128) - - assert_image_equal_tofile(im, "Tests/images/hopper.png") - - # Test image with alpha - with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (800, 600) - - assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") - ) + assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) def test__accept_true(): @@ -313,6 +306,7 @@ def test_save_unsupported_mode(tmp_path): ("mode", "test_file"), [ ("L", "Tests/images/linear_gradient.png"), + ("LA", "Tests/images/uncompressed_la.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b78cc649f..f78c8b17c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -136,15 +136,18 @@ class DdsImageFile(ImageFile.ImageFile): (bitcount,) = struct.unpack(" Date: Fri, 23 Dec 2022 23:20:06 +1100 Subject: [PATCH 184/205] Clear pyaccess after re-assigning im --- Tests/test_file_ico.py | 13 +++++++++++++ src/PIL/IcoImagePlugin.py | 1 + 2 files changed, 14 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 3fcd5c61f..afb17b1af 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -71,6 +71,19 @@ def test_save_to_bytes(): ) +def test_getpixel(tmp_path): + temp_file = str(tmp_path / "temp.ico") + + im = hopper() + im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) + + with Image.open(temp_file) as reloaded: + reloaded.load() + reloaded.size = (32, 32) + + assert reloaded.getpixel((0, 0)) == (18, 20, 62) + + def test_no_duplicates(tmp_path): temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 17b9855a0..93b9dfdea 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -327,6 +327,7 @@ class IcoImageFile(ImageFile.ImageFile): # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im + self.pyaccess = None self.mode = im.mode if im.size != self.size: warnings.warn("Image was not the expected size") From 8bd5fbf450f9f5a03a32c7efcfb488bfc2a40d1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 07:32:09 +1100 Subject: [PATCH 185/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 04b3fc4c6..3e409fe64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed PyAccess after changing ICO size #6821 + [radarhere] + +- Do not use EXIF from info when saving PNG images #6819 + [radarhere] + +- Fixed saving EXIF data to MPO #6817 + [radarhere] + +- Added Exif hide_offsets() #6762 + [radarhere] + +- Only compare to previous frame when checking for duplicate GIF frames while saving #6787 + [radarhere] + +- Always initialize all plugins in registered_extensions() #6811 + [radarhere] + - Ignore non-opaque WebP background when saving as GIF #6792 [radarhere] From 5c482e20af8efe0210cd7b0cfe2dec7367d03042 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:32:58 +1100 Subject: [PATCH 186/205] Document new ExifTags enums --- docs/reference/ExifTags.rst | 10 +++++++++- docs/releasenotes/9.4.0.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 650bb4f95..464ab77ea 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -37,7 +37,15 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> IFD.Exif.value 34665 >>> IFD(34665).name - 'Exif' + 'Exif + +.. py:data:: LightSource + + >>> from PIL.ExifTags import LightSource + >>> LightSource.Unknown.value + 0 + >>> LightSource(0).name + 'Unknown' Two of these values are also exposed as dictionaries. diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6b..7da0e61f3 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,34 @@ removes the hidden RGB values for better compression by default in libwebp 0.5 or later. By setting this option to ``True``, the encoder will keep the hidden RGB values. +Added IFD, Interop and LightSource ExifTags enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with +:py:meth:`~PIL.Image.Exif.get_ifd`:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + print(im.getexif().get_ifd(ExifTags.IFD.Exif)) + +``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should +not be used in other contexts, as the enum value is only internally meaningful. + +:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop) + print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98 + +:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource +tag:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/iptc.jpg") + exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif) + print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown + getxmp() ^^^^^^^^ From 941a2d60b28c32f1193ef2f9627fc80f9279802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:41:57 +1100 Subject: [PATCH 187/205] Added release notes --- docs/releasenotes/9.4.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6b..0068f2816 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -70,7 +70,8 @@ TODO Other Changes ============= -TODO -^^^^ +Added support for DDS L and LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Support has been added to read and write L and LA DDS images in the uncompressed +format, known as "luminance" textures. From 426ac9c1fe085d78d501c7143039b83a23eeac3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 14:19:32 +1100 Subject: [PATCH 188/205] Updated libtiff to 4.5.0 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index b188020b9..42fe8c254 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -143,7 +143,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** * **libfreetype** provides type related services diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a1908e35e..0b0c782a0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -200,15 +200,11 @@ deps = { "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", - "filename": "tiff-4.4.0.tar.gz", - "dir": "tiff-4.4.0", - "license": "COPYRIGHT", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", + "filename": "tiff-4.5.0.tar.gz", + "dir": "tiff-4.5.0", + "license": "LICENSE.md", "patch": { - r"cmake\LZMACodec.cmake": { - # fix typo - "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}", - }, r"libtiff\tif_lzma.c": { # link against liblzma.lib "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 From d2590437c4f90b1f6837f951223e18923c4d3467 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 16:21:45 +1100 Subject: [PATCH 189/205] Updated libtiff shared library name --- Tests/oss-fuzz/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index b459ee47a..7e9098f53 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -25,7 +25,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ --add-binary /usr/local/lib/libpng16.so.16:. \ - --add-binary /usr/local/lib/libtiff.so.5:. \ + --add-binary /usr/local/lib/libtiff.so.6:. \ --add-binary /usr/local/lib/libwebp.so.7:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \ From 08816f43ae621830cd4cf9dc1fecfbae63e5cc60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 15:46:14 +1100 Subject: [PATCH 190/205] Added support for I;16 modes in putdata() --- Tests/test_image_putdata.py | 5 +++-- src/_imaging.c | 30 +++++++++++++----------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 3d60e52a2..0e6293349 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -55,10 +55,11 @@ def test_mode_with_L_with_float(): assert im.getpixel((0, 0)) == 2 -def test_mode_i(): +@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) +def test_mode_i(mode): src = hopper("L") data = list(src.getdata()) - im = Image.new("I", src.size, 0) + im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) target = [2 * elt + 256 for elt in data] diff --git a/src/_imaging.c b/src/_imaging.c index 940b5fbb3..05e1370f6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1531,25 +1531,21 @@ if (PySequence_Check(op)) { \ PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } + int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0; double value; - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = (UINT8)CLIP8(value); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - - } else { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = CLIP8(value * scale + offset); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (endian == 0) { + image->image8[y][x] = (UINT8)CLIP8(value); + } else { + image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256); + image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ From 2755e0ffaadc8b29c3e67e223c333c50e197a733 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 19:24:41 +1100 Subject: [PATCH 191/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e409fe64..76fc230a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added DDS support for uncompressed L and LA images #6820 + [radarhere, REDxEYE] + +- Added LightSource tag values to ExifTags #6749 + [radarhere] + - Fixed PyAccess after changing ICO size #6821 [radarhere] From a9c46bc288d23c95fd08ee66493cb07be074f02e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 10:22:10 +1100 Subject: [PATCH 192/205] Document "transparency" info key --- docs/handbook/concepts.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index f3fa1f2b1..f7bc9396b 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -111,6 +111,18 @@ the file format handler (see the chapter on :ref:`image-file-formats`). Most handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when loading an image, but ignore it when saving images. +Transparency +------------ + +If an image does not have an alpha band, transparency may be specified in the +:py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. + +Most of the time, the "transparency" value is a single integer, describing +which pixel value is transparent in an "1", "L", "I" or "P" mode image. +However, PNG images may have three values, one for each channel in an "RGB" +mode image, or can have a byte string for a "P" mode image, to specify the +alpha value for each palette entry. + Orientation ----------- From 0da8e43977f11837d9175419884d6a3295a7651e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 08:58:38 +1100 Subject: [PATCH 193/205] Parametrized test --- Tests/test_file_tiff_metadata.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 48c0273fe..48797ea08 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,21 +185,21 @@ def test_iptc(tmp_path): im.save(out) -def test_writing_other_types_to_ascii(tmp_path): - im = hopper() +@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) +def test_writing_other_types_to_ascii(value, expected, tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] assert tag.type == TiffTags.ASCII + info[271] = value + + im = hopper() out = str(tmp_path / "temp.tiff") - for (value, expected) in {b"test": "test", 1: "1"}.items(): - info[271] = value + im.save(out, tiffinfo=info) - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == expected + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected def test_writing_int_to_bytes(tmp_path): From cd351c4f854b6fffde086ec43c1149f2dbcba472 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 09:41:14 +1100 Subject: [PATCH 194/205] Added release notes --- docs/releasenotes/9.4.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index aae3e2b64..e4e1e40fe 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,12 @@ removes the hidden RGB values for better compression by default in libwebp 0.5 or later. By setting this option to ``True``, the encoder will keep the hidden RGB values. +Added ``signed`` option when saving JPEG2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the ``signed`` keyword argument is present and true when saving JPEG2000 +images, then tell the encoder to save the image as signed. + Added IFD, Interop and LightSource ExifTags enums ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 73a2c3049f905bba20748c82ce12e6ca971360f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 10:27:03 +1100 Subject: [PATCH 195/205] Use pytest.raises match argument --- Tests/test_imagegrab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 01442dc69..317db4c01 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -65,12 +65,12 @@ $bmp = New-Object Drawing.Bitmap 200, 200 p.communicate() else: if not shutil.which("wl-paste"): - with pytest.raises(NotImplementedError) as e: + with pytest.raises( + NotImplementedError, + match="wl-paste or xclip is required for" + r" ImageGrab.grabclipboard\(\) on Linux", + ): ImageGrab.grabclipboard() - assert ( - str(e.value) == "wl-paste or xclip is required" - " for ImageGrab.grabclipboard() on Linux" - ) return ImageGrab.grabclipboard() From a4baeda9f69a7ada9b78437be10adb66c3520b75 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 29 Dec 2022 11:07:16 +1100 Subject: [PATCH 196/205] Fixed typo Co-authored-by: Hugo van Kemenade --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index f7bc9396b..ed25e1865 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -118,7 +118,7 @@ If an image does not have an alpha band, transparency may be specified in the :py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. Most of the time, the "transparency" value is a single integer, describing -which pixel value is transparent in an "1", "L", "I" or "P" mode image. +which pixel value is transparent in a "1", "L", "I" or "P" mode image. However, PNG images may have three values, one for each channel in an "RGB" mode image, or can have a byte string for a "P" mode image, to specify the alpha value for each palette entry. From dc30ccc6b20d7234e0e3a1e5ba29bf80fa61b56e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 12:05:04 +1100 Subject: [PATCH 197/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 76fc230a8..cc6bb2e3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Patch OpenJPEG to include ARM64 fix #6718 + [radarhere] + +- Added support for I;16 modes in putdata() #6825 + [radarhere] + +- Added conversion from RGBa to RGB #6708 + [radarhere] + - Added DDS support for uncompressed L and LA images #6820 [radarhere, REDxEYE] From efa27a70d634e0c9f65f71f3f8fcd9d748ded5c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 13:18:45 +1100 Subject: [PATCH 198/205] Document the meaning of "premultiplied alpha" --- docs/handbook/concepts.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index ed25e1865..01f75e9a3 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -64,6 +64,12 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;24`` (24-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour) +Premultiplied alpha is where the values for each other channel have been +multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` +would convert to an RGBa pixel of ``(5, 10, 15, 127)``. The values of the R, +G and B channels are halved as a result of the half transparency in the alpha +channel. + Apart from these additional modes, Pillow doesn't yet support multichannel images with a depth of more than 8 bits per channel. From a7f8e862cb1310fb093247ff69085efdef51967e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 21:08:58 +1100 Subject: [PATCH 199/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cc6bb2e3e..aa0fa2a74 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- If available, use wl-paste or xclip for grabclipboard() on Linux #6783 + [radarhere] + +- Added signed option when saving JPEG2000 images #6709 + [radarhere] + - Patch OpenJPEG to include ARM64 fix #6718 [radarhere] From 1e3f3ab5963aca613e27c8d2d46f68c89fc78a09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 21:52:09 +1100 Subject: [PATCH 200/205] Do not attempt to read IFD1 if absent --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0a79b1237..f7b1ebd9f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3687,7 +3687,7 @@ class Exif(MutableMapping): def get_ifd(self, tag): if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: - if self._info is not None: + if self._info is not None and self._info.next != 0: self._ifds[tag] = self._get_ifd_dict(self._info.next) elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) From 3a1f4b4919726c1c8a0ec4fbea1a908c41a0491f Mon Sep 17 00:00:00 2001 From: smb123w64gb Date: Thu, 29 Dec 2022 06:16:49 -0800 Subject: [PATCH 201/205] Fix version mismatch --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6ded944da..a061aaf17 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,7 +289,7 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.14.tar.gz/download", + "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", "filename": "lcms2-2.14.tar.gz", "dir": "lcms2-2.14", "license": "COPYING", From 77f6f54ac46f9caa5d5063cbbeda0cddb6235bfc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 08:57:36 +1100 Subject: [PATCH 202/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa0fa2a74..4eebbda6a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Do not attempt to read IFD1 if absent #6840 + [radarhere] + +- Fixed writing int as ASCII tag #6800 + [radarhere] + - If available, use wl-paste or xclip for grabclipboard() on Linux #6783 [radarhere] From 2ae55ccbdad9c842929fb238ea1eb81d1f999024 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 21 Dec 2022 23:51:35 +0200 Subject: [PATCH 203/205] Improve exception traceback readability --- .pre-commit-config.yaml | 3 +- docs/example/DdsImagePlugin.py | 18 +- .../writing-your-own-image-plugin.rst | 3 +- setup.py | 18 +- src/PIL/BdfFontFile.py | 3 +- src/PIL/BlpImagePlugin.py | 35 ++-- src/PIL/BmpImagePlugin.py | 27 ++- src/PIL/BufrStubImagePlugin.py | 6 +- src/PIL/CurImagePlugin.py | 6 +- src/PIL/DcxImagePlugin.py | 3 +- src/PIL/DdsImagePlugin.py | 20 ++- src/PIL/EpsImagePlugin.py | 24 ++- src/PIL/FitsImagePlugin.py | 9 +- src/PIL/FitsStubImagePlugin.py | 3 +- src/PIL/FliImagePlugin.py | 6 +- src/PIL/FpxImagePlugin.py | 15 +- src/PIL/FtexImagePlugin.py | 9 +- src/PIL/GbrImagePlugin.py | 15 +- src/PIL/GdImageFile.py | 9 +- src/PIL/GifImagePlugin.py | 9 +- src/PIL/GimpGradientFile.py | 6 +- src/PIL/GimpPaletteFile.py | 9 +- src/PIL/GribStubImagePlugin.py | 6 +- src/PIL/Hdf5StubImagePlugin.py | 6 +- src/PIL/IcnsImagePlugin.py | 24 ++- src/PIL/IcoImagePlugin.py | 6 +- src/PIL/ImImagePlugin.py | 18 +- src/PIL/Image.py | 154 ++++++++++++------ src/PIL/ImageCms.py | 29 ++-- src/PIL/ImageColor.py | 6 +- src/PIL/ImageDraw.py | 65 +++++--- src/PIL/ImageFile.py | 51 ++++-- src/PIL/ImageFilter.py | 34 ++-- src/PIL/ImageFont.py | 32 ++-- src/PIL/ImageGrab.py | 8 +- src/PIL/ImageMath.py | 12 +- src/PIL/ImageMorph.py | 24 ++- src/PIL/ImageOps.py | 9 +- src/PIL/ImagePalette.py | 26 +-- src/PIL/ImageQt.py | 3 +- src/PIL/ImageSequence.py | 3 +- src/PIL/ImageShow.py | 21 ++- src/PIL/ImageStat.py | 3 +- src/PIL/ImageTk.py | 3 +- src/PIL/ImtImagePlugin.py | 3 +- src/PIL/IptcImagePlugin.py | 9 +- src/PIL/Jpeg2KImagePlugin.py | 23 ++- src/PIL/JpegImagePlugin.py | 54 ++++-- src/PIL/McIdasImagePlugin.py | 6 +- src/PIL/MicImagePlugin.py | 9 +- src/PIL/MpegImagePlugin.py | 3 +- src/PIL/MpoImagePlugin.py | 3 +- src/PIL/MspImagePlugin.py | 20 ++- src/PIL/PaletteFile.py | 3 +- src/PIL/PalmImagePlugin.py | 6 +- src/PIL/PcdImagePlugin.py | 3 +- src/PIL/PcfFontFile.py | 6 +- src/PIL/PcxImagePlugin.py | 12 +- src/PIL/PdfImagePlugin.py | 6 +- src/PIL/PdfParser.py | 14 +- src/PIL/PixarImagePlugin.py | 3 +- src/PIL/PngImagePlugin.py | 85 ++++++---- src/PIL/PpmImagePlugin.py | 33 ++-- src/PIL/PsdImagePlugin.py | 12 +- src/PIL/PyAccess.py | 6 +- src/PIL/SgiImagePlugin.py | 17 +- src/PIL/SpiderImagePlugin.py | 18 +- src/PIL/SunImagePlugin.py | 15 +- src/PIL/TarIO.py | 6 +- src/PIL/TgaImagePlugin.py | 12 +- src/PIL/TiffImagePlugin.py | 83 ++++++---- src/PIL/WebPImagePlugin.py | 14 +- src/PIL/WmfImagePlugin.py | 9 +- src/PIL/XVThumbImagePlugin.py | 6 +- src/PIL/XbmImagePlugin.py | 6 +- src/PIL/XpmImagePlugin.py | 15 +- src/PIL/_deprecate.py | 9 +- src/PIL/features.py | 9 +- winbuild/build_prepare.py | 11 +- 79 files changed, 861 insertions(+), 487 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 609352f22..d019d3e7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,8 @@ repos: rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + additional_dependencies: + [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index ec3938b36..26451533e 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack("= 16 @@ -164,7 +165,8 @@ class BmpImageFile(ImageFile.ImageFile): # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: - raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") + msg = f"Unsupported BMP pixel depth ({file_info['bits']})" + raise OSError(msg) # ---------------- Process BMP with Bitfields compression (not palette) decoder_name = "raw" @@ -205,23 +207,27 @@ class BmpImageFile(ImageFile.ImageFile): ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) elif file_info["compression"] == self.RAW: if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" elif file_info["compression"] in (self.RLE8, self.RLE4): decoder_name = "bmp_rle" else: - raise OSError(f"Unsupported BMP compression ({file_info['compression']})") + msg = f"Unsupported BMP compression ({file_info['compression']})" + raise OSError(msg) # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): - raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})") + msg = f"Unsupported BMP Palette size ({file_info['colors']})" + raise OSError(msg) else: padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) @@ -271,7 +277,8 @@ class BmpImageFile(ImageFile.ImageFile): head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): - raise SyntaxError("Not a BMP file") + msg = "Not a BMP file" + raise SyntaxError(msg) # read the start position of the BMP image data (u32) offset = i32(head_data, 10) # load bitmap information (offset=raster info) @@ -383,7 +390,8 @@ def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as BMP") from e + msg = f"cannot write mode {im.mode} as BMP" + raise OSError(msg) from e info = im.encoderinfo @@ -411,7 +419,8 @@ def _save(im, fp, filename, bitmap_header=True): offset = 14 + header + colors * 4 file_size = offset + image if file_size > 2**32 - 1: - raise ValueError("File size is too large for the BMP format") + msg = "File size is too large for the BMP format" + raise ValueError(msg) fp.write( b"BM" # file type (magic) + o32(file_size) # file size diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 9510f733e..a0da1b786 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -42,7 +42,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(4)): - raise SyntaxError("Not a BUFR file") + msg = "Not a BUFR file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("BUFR save handler not installed") + msg = "BUFR save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 42af5cafc..aedc6ce7f 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -43,7 +43,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # check magic s = self.fp.read(6) if not _accept(s): - raise SyntaxError("not a CUR file") + msg = "not a CUR file" + raise SyntaxError(msg) # pick the largest cursor in the file m = b"" @@ -54,7 +55,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): elif s[0] > m[0] and s[1] > m[1]: m = s if not m: - raise TypeError("No cursors were found") + msg = "No cursors were found" + raise TypeError(msg) # load as bitmap self._bitmap(i32(m, 12) + offset) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index aeed1e7c7..81c0314f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -47,7 +47,8 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a DCX file") + msg = "not a DCX file" + raise SyntaxError(msg) # Component directory self._offset = [] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index f78c8b17c..a946daeaa 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -114,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack(" 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) try: m = split.match(s) except re.error as e: - raise SyntaxError("not an EPS file") from e + msg = "not an EPS file" + raise SyntaxError(msg) from e if m: k, v = m.group(1, 2) @@ -268,7 +271,8 @@ class EpsImageFile(ImageFile.ImageFile): # tools mistakenly put in the Comments section pass else: - raise OSError("bad EPS header") + msg = "bad EPS header" + raise OSError(msg) s_raw = fp.readline() s = s_raw.strip("\r\n") @@ -282,7 +286,8 @@ class EpsImageFile(ImageFile.ImageFile): while s[:1] == "%": if len(s) > 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) if s[:11] == "%ImageData:": # Encoded bitmapped image. @@ -306,7 +311,8 @@ class EpsImageFile(ImageFile.ImageFile): break if not box: - raise OSError("cannot determine EPS bounding box") + msg = "cannot determine EPS bounding box" + raise OSError(msg) def _find_offset(self, fp): @@ -326,7 +332,8 @@ class EpsImageFile(ImageFile.ImageFile): offset = i32(s, 4) length = i32(s, 8) else: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) return length, offset @@ -365,7 +372,8 @@ def _save(im, fp, filename, eps=1): elif im.mode == "CMYK": operator = (8, 4, b"false 4 colorimage") else: - raise ValueError("image mode is not supported") + msg = "image mode is not supported" + raise ValueError(msg) if eps: # diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index c16300efa..536bc1fe6 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -28,7 +28,8 @@ class FitsImageFile(ImageFile.ImageFile): while True: header = self.fp.read(80) if not header: - raise OSError("Truncated FITS file") + msg = "Truncated FITS file" + raise OSError(msg) keyword = header[:8].strip() if keyword == b"END": break @@ -36,12 +37,14 @@ class FitsImageFile(ImageFile.ImageFile): if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): - raise SyntaxError("Not a FITS file") + msg = "Not a FITS file" + raise SyntaxError(msg) headers[keyword] = value naxis = int(headers[b"NAXIS"]) if naxis == 0: - raise ValueError("No image data") + msg = "No image data" + raise ValueError(msg) elif naxis == 1: self._size = 1, int(headers[b"NAXIS1"]) else: diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 440240a99..86eb2d5a2 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -67,7 +67,8 @@ class FITSStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): - raise OSError("FITS save handler not installed") + msg = "FITS save handler not installed" + raise OSError(msg) # -------------------------------------------------------------------- diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 908bed9f4..66681939d 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -50,7 +50,8 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): - raise SyntaxError("not an FLI/FLC file") + msg = "not an FLI/FLC file" + raise SyntaxError(msg) # frames self.n_frames = i16(s, 6) @@ -141,7 +142,8 @@ class FliImageFile(ImageFile.ImageFile): self.load() if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) self.__frame = frame # move to next frame diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index a55376d0e..8ddc6b40b 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -60,10 +60,12 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an FPX file; invalid OLE file") from e + msg = "not an FPX file; invalid OLE file" + raise SyntaxError(msg) from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - raise SyntaxError("not an FPX file; bad root CLSID") + msg = "not an FPX file; bad root CLSID" + raise SyntaxError(msg) self._open_index(1) @@ -99,7 +101,8 @@ class FpxImageFile(ImageFile.ImageFile): colors = [] bands = i32(s, 4) if bands > 4: - raise OSError("Invalid number of bands") + msg = "Invalid number of bands" + raise OSError(msg) for i in range(bands): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) @@ -141,7 +144,8 @@ class FpxImageFile(ImageFile.ImageFile): length = i32(s, 32) if size != self.size: - raise OSError("subimage mismatch") + msg = "subimage mismatch" + raise OSError(msg) # get tile descriptors fp.seek(28 + offset) @@ -217,7 +221,8 @@ class FpxImageFile(ImageFile.ImageFile): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise OSError("unknown/invalid compression") + msg = "unknown/invalid compression" + raise OSError(msg) x = x + xtile if x >= xsize: diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 1b714eb4f..c7c32252b 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -73,7 +73,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class FtexImageFile(ImageFile.ImageFile): @@ -82,7 +83,8 @@ class FtexImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not an FTEX file") + msg = "not an FTEX file" + raise SyntaxError(msg) struct.unpack(" 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = tuple(map(int, s.split()[:3])) if len(v) != 3: - raise ValueError("bad palette entry") + msg = "bad palette entry" + raise ValueError(msg) self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 4575f8237..2088eb7b0 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -42,7 +42,8 @@ class GribStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not a GRIB file") + msg = "Not a GRIB file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class GribStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("GRIB save handler not installed") + msg = "GRIB save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index df11cf2a6..d6f283739 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -42,7 +42,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not an HDF file") + msg = "Not an HDF file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("HDF5 save handler not installed") + msg = "HDF5 save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index fa192f053..e76d0c35a 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -42,7 +42,8 @@ def read_32t(fobj, start_length, size): fobj.seek(start) sig = fobj.read(4) if sig != b"\x00\x00\x00\x00": - raise SyntaxError("Unknown signature, expecting 0x00000000") + msg = "Unknown signature, expecting 0x00000000" + raise SyntaxError(msg) return read_32(fobj, (start + 4, length - 4), size) @@ -82,7 +83,8 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]") + msg = f"Error reading channel [{repr(bytesleft)} left]" + raise SyntaxError(msg) band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -113,10 +115,11 @@ def read_png_or_jpeg2000(fobj, start_length, size): or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: - raise ValueError( + msg = ( "Unsupported icon subimage format (rebuild PIL " "with JPEG 2000 support to fix this)" ) + raise ValueError(msg) # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) @@ -127,7 +130,8 @@ def read_png_or_jpeg2000(fobj, start_length, size): im = im.convert("RGBA") return {"RGBA": im} else: - raise ValueError("Unsupported icon subimage format") + msg = "Unsupported icon subimage format" + raise ValueError(msg) class IcnsFile: @@ -168,12 +172,14 @@ class IcnsFile: self.fobj = fobj sig, filesize = nextheader(fobj) if not _accept(sig): - raise SyntaxError("not an icns file") + msg = "not an icns file" + raise SyntaxError(msg) i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) if blocksize <= 0: - raise SyntaxError("invalid block header") + msg = "invalid block header" + raise SyntaxError(msg) i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) @@ -192,7 +198,8 @@ class IcnsFile: def bestsize(self): sizes = self.itersizes() if not sizes: - raise SyntaxError("No 32bit icon resources found") + msg = "No 32bit icon resources found" + raise SyntaxError(msg) return max(sizes) def dataforsize(self, size): @@ -275,7 +282,8 @@ class IcnsImageFile(ImageFile.ImageFile): if value in simple_sizes: info_size = self.info["sizes"][simple_sizes.index(value)] if info_size not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 93b9dfdea..568e6d38d 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -127,7 +127,8 @@ class IcoFile: # check magic s = buf.read(6) if not _accept(s): - raise SyntaxError("not an ICO file") + msg = "not an ICO file" + raise SyntaxError(msg) self.buf = buf self.entry = [] @@ -316,7 +317,8 @@ class IcoImageFile(ImageFile.ImageFile): @size.setter def size(self, value): if value not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 31b0ff469..d0e9508fe 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -126,7 +126,8 @@ class ImImageFile(ImageFile.ImageFile): # 100 bytes, this is (probably) not a text header. if b"\n" not in self.fp.read(100): - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) self.fp.seek(0) n = 0 @@ -153,7 +154,8 @@ class ImImageFile(ImageFile.ImageFile): s = s + self.fp.readline() if len(s) > 100: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) if s[-2:] == b"\r\n": s = s[:-2] @@ -163,7 +165,8 @@ class ImImageFile(ImageFile.ImageFile): try: m = split.match(s) except re.error as e: - raise SyntaxError("not an IM file") from e + msg = "not an IM file" + raise SyntaxError(msg) from e if m: @@ -203,7 +206,8 @@ class ImImageFile(ImageFile.ImageFile): ) if not n: - raise SyntaxError("Not an IM file") + msg = "Not an IM file" + raise SyntaxError(msg) # Basic attributes self._size = self.info[SIZE] @@ -213,7 +217,8 @@ class ImImageFile(ImageFile.ImageFile): while s and s[:1] != b"\x1A": s = self.fp.read(1) if not s: - raise SyntaxError("File truncated") + msg = "File truncated" + raise SyntaxError(msg) if LUT in self.info: # convert lookup table to palette or lut attribute @@ -332,7 +337,8 @@ def _save(im, fp, filename): try: image_type, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as IM") from e + msg = f"Cannot save {im.mode} images as IM" + raise ValueError(msg) from e frames = im.encoderinfo.get("frames", 1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f7b1ebd9f..386fb7c26 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -80,7 +80,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(name, 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) logger = logging.getLogger(__name__) @@ -107,11 +108,12 @@ try: from . import _imaging as core if __version__ != getattr(core, "PILLOW_VERSION", None): - raise ImportError( + msg = ( "The _imaging extension was built for another version of Pillow or PIL:\n" f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" f"Pillow version: {__version__}" ) + raise ImportError(msg) except ImportError as v: core = DeferredError(ImportError("The _imaging C module is not installed.")) @@ -406,7 +408,8 @@ def _getdecoder(mode, decoder_name, args, extra=()): # get decoder decoder = getattr(core, decoder_name + "_decoder") except AttributeError as e: - raise OSError(f"decoder {decoder_name} not available") from e + msg = f"decoder {decoder_name} not available" + raise OSError(msg) from e return decoder(mode, *args + extra) @@ -429,7 +432,8 @@ def _getencoder(mode, encoder_name, args, extra=()): # get encoder encoder = getattr(core, encoder_name + "_encoder") except AttributeError as e: - raise OSError(f"encoder {encoder_name} not available") from e + msg = f"encoder {encoder_name} not available" + raise OSError(msg) from e return encoder(mode, *args + extra) @@ -675,7 +679,8 @@ class Image: try: self.save(b, "PNG") except Exception as e: - raise ValueError("Could not save to PNG for display") from e + msg = "Could not save to PNG for display" + raise ValueError(msg) from e return b.getvalue() @property @@ -767,7 +772,8 @@ class Image: if s: break if s < 0: - raise RuntimeError(f"encoder error {s} in tobytes") + msg = f"encoder error {s} in tobytes" + raise RuntimeError(msg) return b"".join(data) @@ -784,7 +790,8 @@ class Image: self.load() if self.mode != "1": - raise ValueError("not a bitmap") + msg = "not a bitmap" + raise ValueError(msg) data = self.tobytes("xbm") return b"".join( [ @@ -818,9 +825,11 @@ class Image: s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) def load(self): """ @@ -941,7 +950,8 @@ class Image: if matrix: # matrix conversion if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") + msg = "illegal conversion" + raise ValueError(msg) im = self.im.convert_matrix(mode, matrix) new = self._new(im) if has_transparency and self.im.bands == 3: @@ -1026,7 +1036,8 @@ class Image: elif isinstance(t, int): self.im.putpalettealpha(t, 0) else: - raise ValueError("Transparency for P mode should be bytes or int") + msg = "Transparency for P mode should be bytes or int" + raise ValueError(msg) if mode == "P" and palette == Palette.ADAPTIVE: im = self.im.quantize(colors) @@ -1076,7 +1087,8 @@ class Image: im = self.im.convert(modebase) im = im.convert(mode, dither) except KeyError as e: - raise ValueError("illegal conversion") from e + msg = "illegal conversion" + raise ValueError(msg) from e new_im = self._new(im) if mode == "P" and palette != Palette.ADAPTIVE: @@ -1151,20 +1163,21 @@ class Image: Quantize.LIBIMAGEQUANT, ): # Caller specified an invalid mode. - raise ValueError( + msg = ( "Fast Octree (method == 2) and libimagequant (method == 3) " "are the only valid methods for quantizing RGBA images" ) + raise ValueError(msg) if palette: # use palette from reference image palette.load() if palette.mode != "P": - raise ValueError("bad mode for palette image") + msg = "bad mode for palette image" + raise ValueError(msg) if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) + msg = "only RGB or L mode images can be quantized to a palette" + raise ValueError(msg) im = self.im.convert("P", dither, palette.im) new_im = self._new(im) new_im.palette = palette.palette.copy() @@ -1210,9 +1223,11 @@ class Image: return self.copy() if box[2] < box[0]: - raise ValueError("Coordinate 'right' is less than 'left'") + msg = "Coordinate 'right' is less than 'left'" + raise ValueError(msg) elif box[3] < box[1]: - raise ValueError("Coordinate 'lower' is less than 'upper'") + msg = "Coordinate 'lower' is less than 'upper'" + raise ValueError(msg) self.load() return self._new(self._crop(self.im, box)) @@ -1280,9 +1295,8 @@ class Image: if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): - raise TypeError( - "filter argument should be ImageFilter.Filter instance or class" - ) + msg = "filter argument should be ImageFilter.Filter instance or class" + raise TypeError(msg) multiband = isinstance(filter, ImageFilter.MultibandFilter) if self.im.bands == 1 or multiband: @@ -1691,7 +1705,8 @@ class Image: size = mask.size else: # FIXME: use self.size here? - raise ValueError("cannot determine region size; use 4-item box") + msg = "cannot determine region size; use 4-item box" + raise ValueError(msg) box += (box[0] + size[0], box[1] + size[1]) if isinstance(im, str): @@ -1730,15 +1745,20 @@ class Image: """ if not isinstance(source, (list, tuple)): - raise ValueError("Source must be a tuple") + msg = "Source must be a tuple" + raise ValueError(msg) if not isinstance(dest, (list, tuple)): - raise ValueError("Destination must be a tuple") + msg = "Destination must be a tuple" + raise ValueError(msg) if not len(source) in (2, 4): - raise ValueError("Source must be a 2 or 4-tuple") + msg = "Source must be a 2 or 4-tuple" + raise ValueError(msg) if not len(dest) == 2: - raise ValueError("Destination must be a 2-tuple") + msg = "Destination must be a 2-tuple" + raise ValueError(msg) if min(source) < 0: - raise ValueError("Source must be non-negative") + msg = "Source must be non-negative" + raise ValueError(msg) if len(source) == 2: source = source + im.size @@ -1803,7 +1823,8 @@ class Image: if self.mode == "F": # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") + msg = "point operation not supported for this mode" + raise ValueError(msg) if mode != "F": lut = [round(i) for i in lut] @@ -1837,7 +1858,8 @@ class Image: self.pyaccess = None self.mode = self.im.mode except KeyError as e: - raise ValueError("illegal image mode") from e + msg = "illegal image mode" + raise ValueError(msg) from e if self.mode in ("LA", "PA"): band = 1 @@ -1847,7 +1869,8 @@ class Image: if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) alpha.load() if alpha.mode == "1": alpha = alpha.convert("L") @@ -1903,7 +1926,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "LA", "P", "PA"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: @@ -1972,7 +1996,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) bands = 3 palette_mode = "RGB" @@ -2122,7 +2147,8 @@ class Image: ) if reducing_gap is not None and reducing_gap < 1.0: - raise ValueError("reducing_gap must be 1.0 or greater") + msg = "reducing_gap must be 1.0 or greater" + raise ValueError(msg) size = tuple(size) @@ -2380,7 +2406,8 @@ class Image: try: format = EXTENSION[ext] except KeyError as e: - raise ValueError(f"unknown file extension: {ext}") from e + msg = f"unknown file extension: {ext}" + raise ValueError(msg) from e if format.upper() not in SAVE: init() @@ -2494,7 +2521,8 @@ class Image: try: channel = self.getbands().index(channel) except ValueError as e: - raise ValueError(f'The image has no channel "{channel}"') from e + msg = f'The image has no channel "{channel}"' + raise ValueError(msg) from e return self._new(self.im.getband(channel)) @@ -2665,7 +2693,8 @@ class Image: method, data = method.getdata() if data is None: - raise ValueError("missing method data") + msg = "missing method data" + raise ValueError(msg) im = new(self.mode, size, fillcolor) if self.mode == "P" and self.palette: @@ -2726,7 +2755,8 @@ class Image: ) else: - raise ValueError("unknown transformation method") + msg = "unknown transformation method" + raise ValueError(msg) if resample not in ( Resampling.NEAREST, @@ -2791,7 +2821,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqimage(self) def toqpixmap(self): @@ -2799,7 +2830,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqpixmap(self) @@ -2847,11 +2879,14 @@ def _check_size(size): """ if not isinstance(size, (list, tuple)): - raise ValueError("Size must be a tuple") + msg = "Size must be a tuple" + raise ValueError(msg) if len(size) != 2: - raise ValueError("Size must be a tuple of length 2") + msg = "Size must be a tuple of length 2" + raise ValueError(msg) if size[0] < 0 or size[1] < 0: - raise ValueError("Width and height must be >= 0") + msg = "Width and height must be >= 0" + raise ValueError(msg) return True @@ -3037,7 +3072,8 @@ def fromarray(obj, mode=None): try: typekey = (1, 1) + shape[2:], arr["typestr"] except KeyError as e: - raise TypeError("Cannot handle this data type") from e + msg = "Cannot handle this data type" + raise TypeError(msg) from e try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: @@ -3051,7 +3087,8 @@ def fromarray(obj, mode=None): else: ndmax = 4 if ndim > ndmax: - raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.") + msg = f"Too many dimensions: {ndim} > {ndmax}." + raise ValueError(msg) size = 1 if ndim == 1 else shape[1], shape[0] if strides is not None: @@ -3068,7 +3105,8 @@ def fromqimage(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqimage(im) @@ -3077,7 +3115,8 @@ def fromqpixmap(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqpixmap(im) @@ -3115,10 +3154,11 @@ def _decompression_bomb_check(size): pixels = size[0] * size[1] if pixels > 2 * MAX_IMAGE_PIXELS: - raise DecompressionBombError( + msg = ( f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " "pixels, could be decompression bomb DOS attack." ) + raise DecompressionBombError(msg) if pixels > MAX_IMAGE_PIXELS: warnings.warn( @@ -3158,17 +3198,20 @@ def open(fp, mode="r", formats=None): """ if mode != "r": - raise ValueError(f"bad mode {repr(mode)}") + msg = f"bad mode {repr(mode)}" + raise ValueError(msg) elif isinstance(fp, io.StringIO): - raise ValueError( + msg = ( "StringIO cannot be used to open an image. " "Binary data must be used instead." ) + raise ValueError(msg) if formats is None: formats = ID elif not isinstance(formats, (list, tuple)): - raise TypeError("formats must be a list or tuple") + msg = "formats must be a list or tuple" + raise TypeError(msg) exclusive_fp = False filename = "" @@ -3326,12 +3369,15 @@ def merge(mode, bands): """ if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") + msg = "wrong number of bands" + raise ValueError(msg) for band in bands[1:]: if band.mode != getmodetype(mode): - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if band.size != bands[0].size: - raise ValueError("size mismatch") + msg = "size mismatch" + raise ValueError(msg) for band in bands: band.load() return bands[0]._new(core.merge(mode, *[b.im for b in bands])) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 605252d5d..2a2d372e5 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -124,7 +124,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # @@ -191,7 +192,8 @@ class ImageCmsProfile: elif isinstance(profile, _imagingcms.CmsProfile): self._set(profile) else: - raise TypeError("Invalid type for Profile") + msg = "Invalid type for Profile" + raise TypeError(msg) def _set(self, profile, filename=None): self.profile = profile @@ -269,7 +271,8 @@ class ImageCmsTransform(Image.ImagePointHandler): def apply_in_place(self, im): im.load() if im.mode != self.output_mode: - raise ValueError("mode mismatch") # wrong output mode + msg = "mode mismatch" + raise ValueError(msg) # wrong output mode self.transform.apply(im.im.id, im.im.id) im.info["icc_profile"] = self.output_profile.tobytes() return im @@ -374,10 +377,12 @@ def profileToProfile( outputMode = im.mode if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}") + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -489,7 +494,8 @@ def buildTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) @@ -591,7 +597,8 @@ def buildProofTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) @@ -705,17 +712,17 @@ def createProfile(colorSpace, colorTemp=-1): """ if colorSpace not in ["LAB", "XYZ", "sRGB"]: - raise PyCMSError( + msg = ( f"Color space not supported for on-the-fly profile creation ({colorSpace})" ) + raise PyCMSError(msg) if colorSpace == "LAB": try: colorTemp = float(colorTemp) except (TypeError, ValueError) as e: - raise PyCMSError( - f'Color temperature must be numeric, "{colorTemp}" not valid' - ) from e + msg = f'Color temperature must be numeric, "{colorTemp}" not valid' + raise PyCMSError(msg) from e try: return core.createProfile(colorSpace, colorTemp) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 9cbce4143..e184ed68d 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -33,7 +33,8 @@ def getrgb(color): :return: ``(red, green, blue[, alpha])`` """ if len(color) > 100: - raise ValueError("color specifier is too long") + msg = "color specifier is too long" + raise ValueError(msg) color = color.lower() rgb = colormap.get(color, None) @@ -115,7 +116,8 @@ def getrgb(color): m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def getcolor(color, mode): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 407544234..ce29a163b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -69,7 +69,8 @@ class ImageDraw: if mode == "RGBA" and im.mode == "RGB": blend = 1 else: - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if mode == "P": self.palette = im.palette else: @@ -437,7 +438,8 @@ class ImageDraw: ) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -534,14 +536,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -578,7 +583,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) self.text( (left, top), @@ -672,9 +678,11 @@ class ImageDraw: ): """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): - raise ValueError("can't measure length of multiline text") + msg = "can't measure length of multiline text" + raise ValueError(msg) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -712,7 +720,8 @@ class ImageDraw: ): """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if self._multiline_check(text): return self.multiline_textbbox( @@ -752,14 +761,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -803,7 +815,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) bbox_line = self.textbbox( (left, top), @@ -979,38 +992,44 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1. Error Handling # 1.1 Check `n_sides` has an appropriate value if not isinstance(n_sides, int): - raise TypeError("n_sides should be an int") + msg = "n_sides should be an int" + raise TypeError(msg) if n_sides < 3: - raise ValueError("n_sides should be an int > 2") + msg = "n_sides should be an int > 2" + raise ValueError(msg) # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - raise TypeError("bounding_circle should be a tuple") + msg = "bounding_circle should be a tuple" + raise TypeError(msg) if len(bounding_circle) == 3: *centroid, polygon_radius = bounding_circle elif len(bounding_circle) == 2: centroid, polygon_radius = bounding_circle else: - raise ValueError( + msg = ( "bounding_circle should contain 2D coordinates " "and a radius (e.g. (x, y, r) or ((x, y), r) )" ) + raise ValueError(msg) if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): - raise ValueError("bounding_circle should only contain numeric data") + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) if not len(centroid) == 2: - raise ValueError( - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - ) + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) if polygon_radius <= 0: - raise ValueError("bounding_circle radius should be > 0") + msg = "bounding_circle radius should be > 0" + raise ValueError(msg) # 1.3 Check `rotation` has an appropriate value if not isinstance(rotation, (int, float)): - raise ValueError("rotation should be an int or float") + msg = "rotation should be an int or float" + raise ValueError(msg) # 2. Define Helper Functions def _apply_rotation(point, degrees, centroid): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dbdc0cb38..0d3facf57 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -124,7 +124,8 @@ class ImageFile(Image.Image): raise SyntaxError(v) from v if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - raise SyntaxError("not identified by this driver") + msg = "not identified by this driver" + raise SyntaxError(msg) except BaseException: # close the file only if we have opened it this constructor if self._exclusive_fp: @@ -154,7 +155,8 @@ class ImageFile(Image.Image): """Load image data based on tile list""" if self.tile is None: - raise OSError("cannot load this image") + msg = "cannot load this image" + raise OSError(msg) pixel = Image.Image.load(self) if not self.tile: @@ -249,16 +251,18 @@ class ImageFile(Image.Image): if LOAD_TRUNCATED_IMAGES: break else: - raise OSError("image file is truncated") from e + msg = "image file is truncated" + raise OSError(msg) from e if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: break else: - raise OSError( + msg = ( "image file is truncated " f"({len(b)} bytes not processed)" ) + raise OSError(msg) b = b + s n, err_code = decoder.decode(b) @@ -314,7 +318,8 @@ class ImageFile(Image.Image): and frame >= self.n_frames + self._min_frame ) ): - raise EOFError("attempt to seek outside sequence") + msg = "attempt to seek outside sequence" + raise EOFError(msg) return self.tell() != frame @@ -328,12 +333,14 @@ class StubImageFile(ImageFile): """ def _open(self): - raise NotImplementedError("StubImageFile subclass must implement _open") + msg = "StubImageFile subclass must implement _open" + raise NotImplementedError(msg) def load(self): loader = self._load() if loader is None: - raise OSError(f"cannot find loader for this {self.format} file") + msg = f"cannot find loader for this {self.format} file" + raise OSError(msg) image = loader.load(self) assert image is not None # become the other object (!) @@ -343,7 +350,8 @@ class StubImageFile(ImageFile): def _load(self): """(Hook) Find actual image loader.""" - raise NotImplementedError("StubImageFile subclass must implement _load") + msg = "StubImageFile subclass must implement _load" + raise NotImplementedError(msg) class Parser: @@ -468,9 +476,11 @@ class Parser: self.feed(b"") self.data = self.decoder = None if not self.finished: - raise OSError("image was incomplete") + msg = "image was incomplete" + raise OSError(msg) if not self.image: - raise OSError("cannot parse this image") + msg = "cannot parse this image" + raise OSError(msg) if self.data: # incremental parsing not possible; reopen the file # not that we have all data @@ -535,7 +545,8 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): # slight speedup: compress to real file object s = encoder.encode_to_file(fh, bufsize) if s < 0: - raise OSError(f"encoder error {s} when writing image file") from exc + msg = f"encoder error {s} when writing image file" + raise OSError(msg) from exc finally: encoder.cleanup() @@ -558,7 +569,8 @@ def _safe_read(fp, size): if size <= SAFEBLOCK: data = fp.read(size) if len(data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return data data = [] remaining_size = size @@ -569,7 +581,8 @@ def _safe_read(fp, size): data.append(block) remaining_size -= len(block) if sum(len(d) for d in data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return b"".join(data) @@ -645,13 +658,15 @@ class PyCodec: self.state.ysize = y1 - y0 if self.state.xsize <= 0 or self.state.ysize <= 0: - raise ValueError("Size cannot be negative") + msg = "Size cannot be negative" + raise ValueError(msg) if ( self.state.xsize + self.state.xoff > self.im.size[0] or self.state.ysize + self.state.yoff > self.im.size[1] ): - raise ValueError("Tile cannot extend outside image") + msg = "Tile cannot extend outside image" + raise ValueError(msg) class PyDecoder(PyCodec): @@ -696,9 +711,11 @@ class PyDecoder(PyCodec): s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) class PyEncoder(PyCodec): diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e10c6fdf1..59e2c18b9 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -28,7 +28,8 @@ class MultibandFilter(Filter): class BuiltinFilter(MultibandFilter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) return image.filter(*self.filterargs) @@ -57,7 +58,8 @@ class Kernel(BuiltinFilter): # default scale is sum of kernel scale = functools.reduce(lambda a, b: a + b, kernel) if size[0] * size[1] != len(kernel): - raise ValueError("not enough coefficients in kernel") + msg = "not enough coefficients in kernel" + raise ValueError(msg) self.filterargs = size, scale, offset, kernel @@ -80,7 +82,8 @@ class RankFilter(Filter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) image = image.expand(self.size // 2, self.size // 2) return image.rankfilter(self.size, self.rank) @@ -355,7 +358,8 @@ class Color3DLUT(MultibandFilter): def __init__(self, size, table, channels=3, target_mode=None, **kwargs): if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) self.size = size = self._check_size(size) self.channels = channels self.mode = target_mode @@ -395,19 +399,21 @@ class Color3DLUT(MultibandFilter): table, raw_table = [], table for pixel in raw_table: if len(pixel) != channels: - raise ValueError( + msg = ( "The elements of the table should " - "have a length of {}.".format(channels) + f"have a length of {channels}." ) + raise ValueError(msg) table.extend(pixel) if wrong_size or len(table) != items * channels: - raise ValueError( + msg = ( "The table should have either channels * size**3 float items " "or size**3 items of channels-sized tuples with floats. " f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " f"Actual length: {len(table)}" ) + raise ValueError(msg) self.table = table @staticmethod @@ -415,15 +421,15 @@ class Color3DLUT(MultibandFilter): try: _, _, _ = size except ValueError as e: - raise ValueError( - "Size should be either an integer or a tuple of three integers." - ) from e + msg = "Size should be either an integer or a tuple of three integers." + raise ValueError(msg) from e except TypeError: size = (size, size, size) size = [int(x) for x in size] for size_1d in size: if not 2 <= size_1d <= 65: - raise ValueError("Size should be in [2, 65] range.") + msg = "Size should be in [2, 65] range." + raise ValueError(msg) return size @classmethod @@ -441,7 +447,8 @@ class Color3DLUT(MultibandFilter): """ size_1d, size_2d, size_3d = cls._check_size(size) if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) table = [0] * (size_1d * size_2d * size_3d * channels) idx_out = 0 @@ -481,7 +488,8 @@ class Color3DLUT(MultibandFilter): lookup table. """ if channels not in (None, 3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) ch_in = self.channels ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 3b1a2a23a..b144c3dd2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -50,13 +50,15 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class _ImagingFtNotInstalled: # module placeholder def __getattr__(self, id): - raise ImportError("The _imagingft C module is not installed") + msg = "The _imagingft C module is not installed" + raise ImportError(msg) try: @@ -105,7 +107,8 @@ class ImageFont: else: if image: image.close() - raise OSError("cannot find glyph data file") + msg = "cannot find glyph data file" + raise OSError(msg) self.file = fullname @@ -116,7 +119,8 @@ class ImageFont: # read PILfont header if file.readline() != b"PILfont\n": - raise SyntaxError("Not a PILfont file") + msg = "Not a PILfont file" + raise SyntaxError(msg) file.readline().split(b";") self.info = [] # FIXME: should be a dictionary while True: @@ -130,7 +134,8 @@ class ImageFont: # check image if image.mode not in ("1", "L"): - raise TypeError("invalid font image mode") + msg = "invalid font image mode" + raise TypeError(msg) image.load() @@ -817,7 +822,8 @@ class FreeTypeFont: try: names = self.font.getvarnames() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name): @@ -847,7 +853,8 @@ class FreeTypeFont: try: axes = self.font.getvaraxes() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e for axis in axes: axis["name"] = axis["name"].replace(b"\x00", b"") return axes @@ -860,7 +867,8 @@ class FreeTypeFont: try: self.font.setvaraxes(axes) except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e class TransposedFont: @@ -914,9 +922,8 @@ class TransposedFont: def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - raise ValueError( - "text length is undefined for text rotated by 90 or 270 degrees" - ) + msg = "text length is undefined for text rotated by 90 or 270 degrees" + raise ValueError(msg) return self.font.getlength(text, *args, **kwargs) @@ -1061,7 +1068,8 @@ def load_path(filename): return load(os.path.join(directory, filename)) except OSError: pass - raise OSError("cannot find font file") + msg = "cannot find font file" + raise OSError(msg) def load_default(): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 8cf956809..982f77f20 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -75,7 +75,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im # use xdisplay=None for default display on non-win32/macOS systems if not Image.core.HAVE_XCB: - raise OSError("Pillow was built without XCB support") + msg = "Pillow was built without XCB support" + raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) if bbox: @@ -137,9 +138,8 @@ def grabclipboard(): elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: - raise NotImplementedError( - "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" - ) + msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" + raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() subprocess.call(args, stdout=fh) os.close(fh) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 09d9898d7..ac7d36b69 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -39,7 +39,8 @@ class _Operand: elif im1.im.mode in ("I", "F"): return im1.im else: - raise ValueError(f"unsupported mode: {im1.im.mode}") + msg = f"unsupported mode: {im1.im.mode}" + raise ValueError(msg) else: # argument was a constant if _isconstant(im1) and self.im.mode in ("1", "L", "I"): @@ -56,7 +57,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -80,7 +82,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) @@ -249,7 +252,8 @@ def eval(expression, _dict={}, **kw): for name in code.co_names: if name not in args and name != "abs": - raise ValueError(f"'{name}' not allowed") + msg = f"'{name}' not allowed" + raise ValueError(msg) scan(compiled_code) out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 1e22c36a8..60cbbedc3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -81,7 +81,8 @@ class LutBuilder: ], } if op_name not in known_patterns: - raise Exception("Unknown pattern " + op_name + "!") + msg = "Unknown pattern " + op_name + "!" + raise Exception(msg) self.patterns = known_patterns[op_name] @@ -193,10 +194,12 @@ class MorphOp: Returns a tuple of the number of changed pixels and the morphed image""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -208,10 +211,12 @@ class MorphOp: Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -221,7 +226,8 @@ class MorphOp: of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): @@ -231,12 +237,14 @@ class MorphOp: if len(self.lut) != LUT_SIZE: self.lut = None - raise Exception("Wrong size operator file!") + msg = "Wrong size operator file!" + raise Exception(msg) def save_lut(self, filename): """Save an operator to an mrl file""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) with open(filename, "wb") as f: f.write(self.lut) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 443c540b6..e2168ce62 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -49,13 +49,15 @@ def _color(color, mode): def _lut(image, lut): if image.mode == "P": # FIXME: apply to lookup table, not image data - raise NotImplementedError("mode P support coming soon") + msg = "mode P support coming soon" + raise NotImplementedError(msg) elif image.mode in ("L", "RGB"): if image.mode == "RGB" and len(lut) == 256: lut = lut + lut + lut return image.point(lut) else: - raise OSError("not supported for this image mode") + msg = "not supported for this image mode" + raise OSError(msg) # @@ -332,7 +334,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): if factor == 1: return image.copy() elif factor <= 0: - raise ValueError("the factor must be greater than 0") + msg = "the factor must be greater than 0" + raise ValueError(msg) else: size = (round(factor * image.width), round(factor * image.height)) return image.resize(size, resample) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe76c86f4..fe0d32155 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -42,7 +42,8 @@ class ImagePalette: if size != 0: deprecate("The size parameter", 10, None) if size != len(self.palette): - raise ValueError("wrong palette size") + msg = "wrong palette size" + raise ValueError(msg) @property def palette(self): @@ -97,7 +98,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) @@ -112,14 +114,14 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(color, tuple): if self.mode == "RGB": if len(color) == 4: if color[3] != 255: - raise ValueError( - "cannot add non-opaque RGBA color to RGB palette" - ) + msg = "cannot add non-opaque RGBA color to RGB palette" + raise ValueError(msg) color = color[:3] elif self.mode == "RGBA": if len(color) == 3: @@ -147,7 +149,8 @@ class ImagePalette: index = i break if index >= 256: - raise ValueError("cannot allocate more than 256 colors") from e + msg = "cannot allocate more than 256 colors" + raise ValueError(msg) from e self.colors[color] = index if index * 3 < len(self.palette): self._palette = ( @@ -160,7 +163,8 @@ class ImagePalette: self.dirty = 1 return index else: - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def save(self, fp): """Save palette to text file. @@ -168,7 +172,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(fp, str): fp = open(fp, "w") fp.write("# Palette\n") @@ -263,6 +268,7 @@ def load(filename): # traceback.print_exc() pass else: - raise OSError("cannot load palette") + msg = "cannot load palette" + raise OSError(msg) return lut # data, rawmode diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a34678c78..ad607a97b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -179,7 +179,8 @@ def _toqclass_helper(im): else: if exclusive_fp: im.close() - raise ValueError(f"unsupported image mode {repr(im.mode)}") + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) size = im.size __data = data or align8to32(im.tobytes(), size[0], im.mode) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 9df910a43..c4bb6334a 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -30,7 +30,8 @@ class Iterator: def __init__(self, im): if not hasattr(im, "seek"): - raise AttributeError("im must have seek method") + msg = "im must have seek method" + raise AttributeError(msg) self.im = im self.position = getattr(self.im, "_min_frame", 0) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 9d5224588..29d900bef 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -124,7 +124,8 @@ class Viewer: deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -176,7 +177,8 @@ class MacViewer(Viewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -226,7 +228,8 @@ class XDGViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -255,7 +258,8 @@ class DisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -286,7 +290,8 @@ class GmDisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -311,7 +316,8 @@ class EogViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -342,7 +348,8 @@ class XVViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["xv"] title = options.get("title") if title: diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 1baef7db4..b7ebddf06 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -36,7 +36,8 @@ class Stat: except AttributeError: self.h = image_or_list # assume it to be a histogram list if not isinstance(self.h, list): - raise TypeError("first argument must be image or list") + msg = "first argument must be image or list" + raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) def __getattr__(self, id): diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 949cf1fbf..09a6356fa 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -284,7 +284,8 @@ def _show(image, title): super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise OSError("tkinter not initialized") + msg = "tkinter not initialized" + raise OSError(msg) top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index dc7078012..cfeadd53c 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -41,7 +41,8 @@ class ImtImageFile(ImageFile.ImageFile): buffer = self.fp.read(100) if b"\n" not in buffer: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) xsize = ysize = 0 diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 0bbe50668..774817569 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -66,12 +66,14 @@ class IptcImageFile(ImageFile.ImageFile): # syntax if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9: - raise SyntaxError("invalid IPTC/NAA file") + msg = "invalid IPTC/NAA file" + raise SyntaxError(msg) # field size size = s[3] if size > 132: - raise OSError("illegal field length in IPTC/NAA file") + msg = "illegal field length in IPTC/NAA file" + raise OSError(msg) elif size == 128: size = 0 elif size > 128: @@ -122,7 +124,8 @@ class IptcImageFile(ImageFile.ImageFile): try: compression = COMPRESSION[self.getint((3, 120))] except KeyError as e: - raise OSError("Unknown IPTC image compression") from e + msg = "Unknown IPTC image compression" + raise OSError(msg) from e # tile if tag == (8, 10): diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 11d1d488a..7457874c1 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -44,13 +44,13 @@ class BoxReader: def _read_bytes(self, num_bytes): if not self._can_read(num_bytes): - raise SyntaxError("Not enough data in header") + msg = "Not enough data in header" + raise SyntaxError(msg) data = self.fp.read(num_bytes) if len(data) < num_bytes: - raise OSError( - f"Expected to read {num_bytes} bytes but only got {len(data)}." - ) + msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." + raise OSError(msg) if self.remaining_in_box > 0: self.remaining_in_box -= num_bytes @@ -87,7 +87,8 @@ class BoxReader: hlen = 8 if lbox < hlen or not self._can_read(lbox - hlen): - raise SyntaxError("Invalid header length") + msg = "Invalid header length" + raise SyntaxError(msg) self.remaining_in_box = lbox - hlen return tbox @@ -189,7 +190,8 @@ def _parse_jp2_header(fp): break if size is None or mode is None: - raise SyntaxError("Malformed JP2 header") + msg = "Malformed JP2 header" + raise SyntaxError(msg) return size, mode, mimetype, dpi @@ -217,10 +219,12 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if dpi is not None: self.info["dpi"] = dpi else: - raise SyntaxError("not a JPEG 2000 file") + msg = "not a JPEG 2000 file" + raise SyntaxError(msg) if self.size is None or self.mode is None: - raise SyntaxError("unable to determine size/mode") + msg = "unable to determine size/mode" + raise SyntaxError(msg) self._reduce = 0 self.layers = 0 @@ -312,7 +316,8 @@ def _save(im, fp, filename): ] ) ): - raise ValueError("quality_layers must be a sequence of numbers") + msg = "quality_layers must be a sequence of numbers" + raise ValueError(msg) num_resolutions = info.get("num_resolutions", 0) cblk_size = info.get("codeblock_size", None) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index eb0db5bb3..9657ae9d0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -204,7 +204,8 @@ def SOF(self, marker): self.bits = s[0] if self.bits != 8: - raise SyntaxError(f"cannot handle {self.bits}-bit layers") + msg = f"cannot handle {self.bits}-bit layers" + raise SyntaxError(msg) self.layers = s[5] if self.layers == 1: @@ -214,7 +215,8 @@ def SOF(self, marker): elif self.layers == 4: self.mode = "CMYK" else: - raise SyntaxError(f"cannot handle {self.layers}-layer images") + msg = f"cannot handle {self.layers}-layer images" + raise SyntaxError(msg) if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: self.info["progressive"] = self.info["progression"] = 1 @@ -253,7 +255,8 @@ def DQT(self, marker): precision = 1 if (v // 16 == 0) else 2 # in bytes qt_length = 1 + precision * 64 if len(s) < qt_length: - raise SyntaxError("bad quantization table marker") + msg = "bad quantization table marker" + raise SyntaxError(msg) data = array.array("B" if precision == 1 else "H", s[1:qt_length]) if sys.byteorder == "little" and precision > 1: data.byteswap() # the values are always big-endian @@ -350,7 +353,8 @@ class JpegImageFile(ImageFile.ImageFile): s = self.fp.read(3) if not _accept(s): - raise SyntaxError("not a JPEG file") + msg = "not a JPEG file" + raise SyntaxError(msg) s = b"\xFF" # Create attributes @@ -394,7 +398,8 @@ class JpegImageFile(ImageFile.ImageFile): elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) s = self.fp.read(1) else: - raise SyntaxError("no marker found") + msg = "no marker found" + raise SyntaxError(msg) def load_read(self, read_bytes): """ @@ -458,7 +463,8 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: - raise ValueError("Invalid Filename") + msg = "Invalid Filename" + raise ValueError(msg) try: with Image.open(path) as _im: @@ -524,12 +530,14 @@ def _getmp(self): info.load(file_contents) mp = dict(info) except Exception as e: - raise SyntaxError("malformed MP Index (unreadable directory)") from e + msg = "malformed MP Index (unreadable directory)" + raise SyntaxError(msg) from e # it's an error not to have a number of images try: quant = mp[0xB001] except KeyError as e: - raise SyntaxError("malformed MP Index (no number of images)") from e + msg = "malformed MP Index (no number of images)" + raise SyntaxError(msg) from e # get MP entries mpentries = [] try: @@ -551,7 +559,8 @@ def _getmp(self): if mpentryattr["ImageDataFormat"] == 0: mpentryattr["ImageDataFormat"] = "JPEG" else: - raise SyntaxError("unsupported picture format in MPO") + msg = "unsupported picture format in MPO" + raise SyntaxError(msg) mptypemap = { 0x000000: "Undefined", 0x010001: "Large Thumbnail (VGA Equivalent)", @@ -566,7 +575,8 @@ def _getmp(self): mpentries.append(mpentry) mp[0xB002] = mpentries except KeyError as e: - raise SyntaxError("malformed MP Index (bad MP Entry)") from e + msg = "malformed MP Index (bad MP Entry)" + raise SyntaxError(msg) from e # Next we should try and parse the individual image unique ID list; # we don't because I've never seen this actually used in a real MPO # file and so can't test it. @@ -626,12 +636,14 @@ def get_sampling(im): def _save(im, fp, filename): if im.width == 0 or im.height == 0: - raise ValueError("cannot write empty image as JPEG") + msg = "cannot write empty image as JPEG" + raise ValueError(msg) try: rawmode = RAWMODE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as JPEG") from e + msg = f"cannot write mode {im.mode} as JPEG" + raise OSError(msg) from e info = im.encoderinfo @@ -651,7 +663,8 @@ def _save(im, fp, filename): subsampling = preset.get("subsampling", -1) qtables = preset.get("quantization") elif not isinstance(quality, int): - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) else: if subsampling in presets: subsampling = presets[subsampling].get("subsampling", -1) @@ -670,7 +683,8 @@ def _save(im, fp, filename): subsampling = 2 elif subsampling == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) subsampling = get_sampling(im) def validate_qtables(qtables): @@ -684,7 +698,8 @@ def _save(im, fp, filename): for num in line.split("#", 1)[0].split() ] except ValueError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): @@ -695,21 +710,24 @@ def _save(im, fp, filename): elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): - raise ValueError("None or too many quantization tables") + msg = "None or too many quantization tables" + raise ValueError(msg) for idx, table in enumerate(qtables): try: if len(table) != 64: raise TypeError table = array.array("H", table) except TypeError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables[idx] = list(table) return qtables if qtables == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index cd047fe9d..8d4d826aa 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -39,7 +39,8 @@ class McIdasImageFile(ImageFile.ImageFile): # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: - raise SyntaxError("not an McIdas area file") + msg = "not an McIdas area file" + raise SyntaxError(msg) self.area_descriptor_raw = s self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) @@ -56,7 +57,8 @@ class McIdasImageFile(ImageFile.ImageFile): mode = "I" rawmode = "I;32B" else: - raise SyntaxError("unsupported McIdas format") + msg = "unsupported McIdas format" + raise SyntaxError(msg) self.mode = mode self._size = w[10], w[9] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index d4f6c90f7..e7e1054a3 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -47,7 +47,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an MIC file; invalid OLE file") from e + msg = "not an MIC file; invalid OLE file" + raise SyntaxError(msg) from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -60,7 +61,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # if we didn't find any images, this is probably not # an MIC file. if not self.images: - raise SyntaxError("not an MIC file; no image entries") + msg = "not an MIC file; no image entries" + raise SyntaxError(msg) self.frame = None self._n_frames = len(self.images) @@ -77,7 +79,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: filename = self.images[frame] except IndexError as e: - raise EOFError("no such frame") from e + msg = "no such frame" + raise EOFError(msg) from e self.fp = self.ole.openstream(filename) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index a358dfdce..2d799d6d8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -67,7 +67,8 @@ class MpegImageFile(ImageFile.ImageFile): s = BitStream(self.fp) if s.read(32) != 0x1B3: - raise SyntaxError("not an MPEG file") + msg = "not an MPEG file" + raise SyntaxError(msg) self.mode = "RGB" self._size = s.read(12), s.read(12) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 095cfe7ee..b1ec2c7bc 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -143,7 +143,8 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.fp.seek(self.offset + 2) # skip SOI marker segment = self.fp.read(2) if not segment: - raise ValueError("No data found for frame") + msg = "No data found for frame" + raise ValueError(msg) self._size = self._initial_size if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index c4d7ddbb4..5420894dc 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -53,14 +53,16 @@ class MspImageFile(ImageFile.ImageFile): # Header s = self.fp.read(32) if not _accept(s): - raise SyntaxError("not an MSP file") + msg = "not an MSP file" + raise SyntaxError(msg) # Header checksum checksum = 0 for i in range(0, 32, 2): checksum = checksum ^ i16(s, i) if checksum != 0: - raise SyntaxError("bad MSP checksum") + msg = "bad MSP checksum" + raise SyntaxError(msg) self.mode = "1" self._size = i16(s, 4), i16(s, 6) @@ -118,7 +120,8 @@ class MspDecoder(ImageFile.PyDecoder): f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) ) except struct.error as e: - raise OSError("Truncated MSP file in row map") from e + msg = "Truncated MSP file in row map" + raise OSError(msg) from e for x, rowlen in enumerate(rowmap): try: @@ -127,9 +130,8 @@ class MspDecoder(ImageFile.PyDecoder): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise OSError( - "Truncated MSP file, expected %d bytes on row %s", (rowlen, x) - ) + msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" + raise OSError(msg) idx = 0 while idx < rowlen: runtype = row[idx] @@ -144,7 +146,8 @@ class MspDecoder(ImageFile.PyDecoder): idx += runcount except struct.error as e: - raise OSError(f"Corrupted MSP file in row {x}") from e + msg = f"Corrupted MSP file in row {x}" + raise OSError(msg) from e self.set_as_raw(img.getvalue(), ("1", 0, 1)) @@ -161,7 +164,8 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): if im.mode != "1": - raise OSError(f"cannot write mode {im.mode} as MSP") + msg = f"cannot write mode {im.mode} as MSP" + raise OSError(msg) # create MSP header header = [0] * 16 diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index ee9dca860..07acd5580 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -34,7 +34,8 @@ class PaletteFile: if s[:1] == b"#": continue if len(s) > 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = [int(x) for x in s.split()] try: diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 700f10e3f..109aad9ab 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -138,7 +138,8 @@ def _save(im, fp, filename): bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # we ignore the palette here im.mode = "P" @@ -154,7 +155,8 @@ def _save(im, fp, filename): else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # # make sure image data is available diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 38caf5c63..5802d386a 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -35,7 +35,8 @@ class PcdImageFile(ImageFile.ImageFile): s = self.fp.read(2048) if s[:4] != b"PCD_": - raise SyntaxError("not a PCD file") + msg = "not a PCD file" + raise SyntaxError(msg) orientation = s[1538] & 3 self.tile_post_rotate = None diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 442ac70c4..ecce1b097 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -63,7 +63,8 @@ class PcfFontFile(FontFile.FontFile): magic = l32(fp.read(4)) if magic != PCF_MAGIC: - raise SyntaxError("not a PCF file") + msg = "not a PCF file" + raise SyntaxError(msg) super().__init__() @@ -186,7 +187,8 @@ class PcfFontFile(FontFile.FontFile): nbitmaps = i32(fp.read(4)) if nbitmaps != len(metrics): - raise OSError("Wrong number of bitmaps") + msg = "Wrong number of bitmaps" + raise OSError(msg) offsets = [] for i in range(nbitmaps): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 841c18a22..3202475dc 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,12 +54,14 @@ class PcxImageFile(ImageFile.ImageFile): # header s = self.fp.read(128) if not _accept(s): - raise SyntaxError("not a PCX file") + msg = "not a PCX file" + raise SyntaxError(msg) # image bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - raise SyntaxError("bad PCX image size") + msg = "bad PCX image size" + raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) # format @@ -105,7 +107,8 @@ class PcxImageFile(ImageFile.ImageFile): rawmode = "RGB;L" else: - raise OSError("unknown PCX mode") + msg = "unknown PCX mode" + raise OSError(msg) self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] @@ -144,7 +147,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as PCX") from e + msg = f"Cannot save {im.mode} images as PCX" + raise ValueError(msg) from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 404759a7f..baad4939f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -174,7 +174,8 @@ def _save(im, fp, filename, save_all=False): procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] else: - raise ValueError(f"cannot save mode {im.mode}") + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) # # image @@ -198,7 +199,8 @@ def _save(im, fp, filename, save_all=False): elif filter == "RunLengthDecode": ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: - raise ValueError(f"unsupported PDF filter ({filter})") + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) stream = op.getvalue() if filter == "CCITTFaxDecode": diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index fd5cc5a61..e4a0f25a9 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -138,9 +138,10 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - raise IndexError( + msg = ( "object ID " + str(key) + " cannot be deleted because it doesn't exist" ) + raise IndexError(msg) def __contains__(self, key): return key in self.existing_entries or key in self.new_entries @@ -314,9 +315,8 @@ class PdfStream: expected_length = self.dictionary.Length return zlib.decompress(self.buf, bufsize=int(expected_length)) else: - raise NotImplementedError( - f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" - ) + msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" + raise NotImplementedError(msg) def pdf_repr(x): @@ -358,7 +358,8 @@ class PdfParser: def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): if buf and f: - raise RuntimeError("specify buf or f or filename, but not both buf and f") + msg = "specify buf or f or filename, but not both buf and f" + raise RuntimeError(msg) self.filename = filename self.buf = buf self.f = f @@ -920,7 +921,8 @@ class PdfParser: result.extend(b")") nesting_depth -= 1 offset = m.end() - raise PdfFormatError("unfinished literal string") + msg = "unfinished literal string" + raise PdfFormatError(msg) re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) re_xref_subsection_start = re.compile( diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index c4860b6c4..8d0a34dba 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -44,7 +44,8 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a PIXAR file") + msg = "not a PIXAR file" + raise SyntaxError(msg) # read rest of header s = s + self.fp.read(508) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6a3c4cb6..b6626bbc5 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -138,14 +138,16 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - raise ValueError("Decompressed Data Too Large") + msg = "Decompressed Data Too Large" + raise ValueError(msg) return plaintext @@ -178,7 +180,8 @@ class ChunkStream: if not is_cid(cid): if not ImageFile.LOAD_TRUNCATED_IMAGES: - raise SyntaxError(f"broken PNG file (chunk {repr(cid)})") + msg = f"broken PNG file (chunk {repr(cid)})" + raise SyntaxError(msg) return cid, pos, length @@ -215,13 +218,11 @@ class ChunkStream: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError( - f"broken PNG file (bad header checksum in {repr(cid)})" - ) + msg = f"broken PNG file (bad header checksum in {repr(cid)})" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError( - f"broken PNG file (incomplete checksum in {repr(cid)})" - ) from e + msg = f"broken PNG file (incomplete checksum in {repr(cid)})" + raise SyntaxError(msg) from e def crc_skip(self, cid, data): """Read checksum""" @@ -239,7 +240,8 @@ class ChunkStream: try: cid, pos, length = self.read() except struct.error as e: - raise OSError("truncated PNG file") from e + msg = "truncated PNG file" + raise OSError(msg) from e if cid == endchunk: break @@ -376,10 +378,11 @@ class PngStream(ChunkStream): def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError( + msg = ( "Too much memory used in text chunks: " f"{self.text_memory}>MAX_TEXT_MEMORY" ) + raise ValueError(msg) def save_rewind(self): self.rewind_state = { @@ -407,7 +410,8 @@ class PngStream(ChunkStream): logger.debug("Compression method %s", s[i]) comp_method = s[i] if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk") + msg = f"Unknown compression method {comp_method} in iCCP chunk" + raise SyntaxError(msg) try: icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: @@ -427,7 +431,8 @@ class PngStream(ChunkStream): if length < 13: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated IHDR chunk") + msg = "Truncated IHDR chunk" + raise ValueError(msg) self.im_size = i32(s, 0), i32(s, 4) try: self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] @@ -436,7 +441,8 @@ class PngStream(ChunkStream): if s[12]: self.im_info["interlace"] = 1 if s[11]: - raise SyntaxError("unknown filter category") + msg = "unknown filter category" + raise SyntaxError(msg) return s def chunk_IDAT(self, pos, length): @@ -512,7 +518,8 @@ class PngStream(ChunkStream): if length < 1: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated sRGB chunk") + msg = "Truncated sRGB chunk" + raise ValueError(msg) self.im_info["srgb"] = s[0] return s @@ -523,7 +530,8 @@ class PngStream(ChunkStream): if length < 9: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated pHYs chunk") + msg = "Truncated pHYs chunk" + raise ValueError(msg) px, py = i32(s, 0), i32(s, 4) unit = s[8] if unit == 1: # meter @@ -567,7 +575,8 @@ class PngStream(ChunkStream): else: comp_method = 0 if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk") + msg = f"Unknown compression method {comp_method} in zTXt chunk" + raise SyntaxError(msg) try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -639,7 +648,8 @@ class PngStream(ChunkStream): if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated acTL chunk") + msg = "APNG contains truncated acTL chunk" + raise ValueError(msg) if self.im_n_frames is not None: self.im_n_frames = None warnings.warn("Invalid APNG, will use default PNG image if possible") @@ -658,18 +668,21 @@ class PngStream(ChunkStream): if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated fcTL chunk") + msg = "APNG contains truncated fcTL chunk" + raise ValueError(msg) seq = i32(s) if (self._seq_num is None and seq != 0) or ( self._seq_num is not None and self._seq_num != seq - 1 ): - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq width, height = i32(s, 4), i32(s, 8) px, py = i32(s, 12), i32(s, 16) im_w, im_h = self.im_size if px + width > im_w or py + height > im_h: - raise SyntaxError("APNG contains invalid frames") + msg = "APNG contains invalid frames" + raise SyntaxError(msg) self.im_info["bbox"] = (px, py, px + width, py + height) delay_num, delay_den = i16(s, 20), i16(s, 22) if delay_den == 0: @@ -684,11 +697,13 @@ class PngStream(ChunkStream): if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) return s - raise ValueError("APNG contains truncated fDAT chunk") + msg = "APNG contains truncated fDAT chunk" + raise ValueError(msg) s = ImageFile._safe_read(self.fp, 4) seq = i32(s) if self._seq_num != seq - 1: - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq return self.chunk_IDAT(pos + 4, length - 4) @@ -713,7 +728,8 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(8)): - raise SyntaxError("not a PNG file") + msg = "not a PNG file" + raise SyntaxError(msg) self._fp = self.fp self.__frame = 0 @@ -797,7 +813,8 @@ class PngImageFile(ImageFile.ImageFile): """Verify PNG file""" if self.fp is None: - raise RuntimeError("verify must be called directly after open") + msg = "verify must be called directly after open" + raise RuntimeError(msg) # back up to beginning of IDAT block self.fp.seek(self.tile[0][2] - 8) @@ -821,7 +838,8 @@ class PngImageFile(ImageFile.ImageFile): self._seek(f) except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in APNG file") from e + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame, rewind=False): if frame == 0: @@ -844,7 +862,8 @@ class PngImageFile(ImageFile.ImageFile): self.__frame = 0 else: if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) # ensure previous frame was loaded self.load() @@ -869,11 +888,13 @@ class PngImageFile(ImageFile.ImageFile): break if cid == b"IEND": - raise EOFError("No more images in APNG file") + msg = "No more images in APNG file" + raise EOFError(msg) if cid == b"fcTL": if frame_start: # there must be at least one fdAT chunk between fcTL chunks - raise SyntaxError("APNG missing frame data") + msg = "APNG missing frame data" + raise SyntaxError(msg) frame_start = True try: @@ -1277,7 +1298,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): try: rawmode, mode = _OUTMODES[mode] except KeyError as e: - raise OSError(f"cannot write mode {mode} as PNG") from e + msg = f"cannot write mode {mode} as PNG" + raise OSError(msg) from e # # write minimal PNG file @@ -1358,7 +1380,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if "transparency" in im.encoderinfo: # don't bother with transparency if it's an RGBA # and it's in the info dict. It's probably just stale. - raise OSError("cannot use transparency for this mode") + msg = "cannot use transparency for this mode" + raise OSError(msg) else: if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 1670d9d64..dee2f1e15 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -84,9 +84,11 @@ class PpmImageFile(ImageFile.ImageFile): token += c if not token: # Token was not even 1 byte - raise ValueError("Reached EOF while reading header") + msg = "Reached EOF while reading header" + raise ValueError(msg) elif len(token) > 10: - raise ValueError(f"Token too long in file header: {token.decode()}") + msg = f"Token too long in file header: {token.decode()}" + raise ValueError(msg) return token def _open(self): @@ -94,7 +96,8 @@ class PpmImageFile(ImageFile.ImageFile): try: mode = MODES[magic_number] except KeyError: - raise SyntaxError("not a PPM file") + msg = "not a PPM file" + raise SyntaxError(msg) if magic_number in (b"P1", b"P4"): self.custom_mimetype = "image/x-portable-bitmap" @@ -122,9 +125,8 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 2: # token is maxval maxval = token if not 0 < maxval < 65536: - raise ValueError( - "maxval must be greater than 0 and less than 65536" - ) + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) if maxval > 255 and mode == "L": self.mode = "I" @@ -208,9 +210,8 @@ class PpmPlainDecoder(ImageFile.PyDecoder): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError( - b"Invalid token for this mode: %s" % bytes([token]) - ) + msg = b"Invalid token for this mode: %s" % bytes([token]) + raise ValueError(msg) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -243,18 +244,19 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if block and not block[-1:].isspace(): # block might split token half_token = tokens.pop() # save half token for later if len(half_token) > max_len: # prevent buildup of half_token - raise ValueError( + msg = ( b"Token too long found in data: %s" % half_token[: max_len + 1] ) + raise ValueError(msg) for token in tokens: if len(token) > max_len: - raise ValueError( - b"Token too long found in data: %s" % token[: max_len + 1] - ) + msg = b"Token too long found in data: %s" % token[: max_len + 1] + raise ValueError(msg) value = int(token) if value > maxval: - raise ValueError(f"Channel value too large for this mode: {value}") + msg = f"Channel value too large for this mode: {value}" + raise ValueError(msg) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! @@ -314,7 +316,8 @@ def _save(im, fp, filename): elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" else: - raise OSError(f"cannot write mode {im.mode} as PPM") + msg = f"cannot write mode {im.mode} as PPM" + raise OSError(msg) fp.write(head + b"\n%d %d\n" % im.size) if head == b"P6": fp.write(b"255\n") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index bd10e3b95..c1ca30a03 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -65,7 +65,8 @@ class PsdImageFile(ImageFile.ImageFile): s = read(26) if not _accept(s) or i16(s, 4) != 1: - raise SyntaxError("not a PSD file") + msg = "not a PSD file" + raise SyntaxError(msg) psd_bits = i16(s, 22) psd_channels = i16(s, 12) @@ -74,7 +75,8 @@ class PsdImageFile(ImageFile.ImageFile): mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise OSError("not enough channels") + msg = "not enough channels" + raise OSError(msg) if mode == "RGB" and psd_channels == 4: mode = "RGBA" channels = 4 @@ -152,7 +154,8 @@ class PsdImageFile(ImageFile.ImageFile): self.fp = self._fp return name, bbox except IndexError as e: - raise EOFError("no such layer") from e + msg = "no such layer" + raise EOFError(msg) from e def tell(self): # return layer number (0=image, 1..max=layers) @@ -170,7 +173,8 @@ def _layerinfo(fp, ct_bytes): # sanity check if ct_bytes < (abs(ct) * 20): - raise SyntaxError("Layer block too short for number of layers requested") + msg = "Layer block too short for number of layers requested" + raise SyntaxError(msg) for _ in range(abs(ct)): diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 039f5ceea..e9cb34ced 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -79,7 +79,8 @@ class PyAccess: :param color: The pixel value. """ if self.readonly: - raise ValueError("Attempt to putpixel a read only image") + msg = "Attempt to putpixel a read only image" + raise ValueError(msg) (x, y) = xy if x < 0: x = self.xsize + x @@ -127,7 +128,8 @@ class PyAccess: def check_xy(self, xy): (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): - raise ValueError("pixel location out of range") + msg = "pixel location out of range" + raise ValueError(msg) return xy diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f0207bb77..d533c55e5 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -60,7 +60,8 @@ class SgiImageFile(ImageFile.ImageFile): s = self.fp.read(headlen) if not _accept(s): - raise ValueError("Not an SGI image file") + msg = "Not an SGI image file" + raise ValueError(msg) # compression : verbatim or RLE compression = s[2] @@ -91,7 +92,8 @@ class SgiImageFile(ImageFile.ImageFile): pass if rawmode == "": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) self._size = xsize, ysize self.mode = rawmode.split(";")[0] @@ -124,7 +126,8 @@ class SgiImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) # Get the keyword arguments info = im.encoderinfo @@ -133,7 +136,8 @@ def _save(im, fp, filename): bpc = info.get("bpc", 1) if bpc not in (1, 2): - raise ValueError("Unsupported number of bytes per pixel") + msg = "Unsupported number of bytes per pixel" + raise ValueError(msg) # Flip the image, since the origin of SGI file is the bottom-left corner orientation = -1 @@ -158,9 +162,8 @@ def _save(im, fp, filename): # assert we've got the right number of bands. if len(im.getbands()) != z: - raise ValueError( - f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - ) + msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" + raise ValueError(msg) # Minimum Byte value pinmin = 0 diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index acafc320e..1192c2d73 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -110,14 +110,17 @@ class SpiderImageFile(ImageFile.ImageFile): t = struct.unpack("<27f", f) # little-endian hdrlen = isSpiderHeader(t) if hdrlen == 0: - raise SyntaxError("not a valid Spider file") + msg = "not a valid Spider file" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError("not a valid Spider file") from e + msg = "not a valid Spider file" + raise SyntaxError(msg) from e h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) if iform != 1: - raise SyntaxError("not a Spider 2D image") + msg = "not a Spider 2D image" + raise SyntaxError(msg) self._size = int(h[12]), int(h[2]) # size in pixels (width, height) self.istack = int(h[24]) @@ -140,7 +143,8 @@ class SpiderImageFile(ImageFile.ImageFile): offset = hdrlen + self.stkoffset self.istack = 2 # So Image knows it's still a stack else: - raise SyntaxError("inconsistent stack header values") + msg = "inconsistent stack header values" + raise SyntaxError(msg) if self.bigendian: self.rawmode = "F;32BF" @@ -168,7 +172,8 @@ class SpiderImageFile(ImageFile.ImageFile): def seek(self, frame): if self.istack == 0: - raise EOFError("attempt to seek in a non-stack file") + msg = "attempt to seek in a non-stack file" + raise EOFError(msg) if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) @@ -260,7 +265,8 @@ def _save(im, fp, filename): hdr = makeSpiderHeader(im) if len(hdr) < 256: - raise OSError("Error creating Spider header") + msg = "Error creating Spider header" + raise OSError(msg) # write the SPIDER header fp.writelines(hdr) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c03759a01..c64de4444 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -54,7 +54,8 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) if not _accept(s): - raise SyntaxError("not an SUN raster file") + msg = "not an SUN raster file" + raise SyntaxError(msg) offset = 32 @@ -83,14 +84,17 @@ class SunImageFile(ImageFile.ImageFile): else: self.mode, rawmode = "RGB", "BGRX" else: - raise SyntaxError("Unsupported Mode/Bit Depth") + msg = "Unsupported Mode/Bit Depth" + raise SyntaxError(msg) if palette_length: if palette_length > 1024: - raise SyntaxError("Unsupported Color Palette Length") + msg = "Unsupported Color Palette Length" + raise SyntaxError(msg) if palette_type != 1: - raise SyntaxError("Unsupported Palette Type") + msg = "Unsupported Palette Type" + raise SyntaxError(msg) offset = offset + palette_length self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) @@ -124,7 +128,8 @@ class SunImageFile(ImageFile.ImageFile): elif file_type == 2: self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] else: - raise SyntaxError("Unsupported Sun Raster file type") + msg = "Unsupported Sun Raster file type" + raise SyntaxError(msg) # diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index d108362fc..20e8a083f 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,14 @@ class TarIO(ContainerIO.ContainerIO): s = self.fh.read(512) if len(s) != 512: - raise OSError("unexpected end of tar file") + msg = "unexpected end of tar file" + raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: - raise OSError("cannot find subfile") + msg = "cannot find subfile" + raise OSError(msg) if i > 0: name = name[:i] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index cd454b755..53fe6ef5c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -73,7 +73,8 @@ class TgaImageFile(ImageFile.ImageFile): or self.size[1] <= 0 or depth not in (1, 8, 16, 24, 32) ): - raise SyntaxError("not a TGA file") + msg = "not a TGA file" + raise SyntaxError(msg) # image mode if imagetype in (3, 11): @@ -89,7 +90,8 @@ class TgaImageFile(ImageFile.ImageFile): if depth == 32: self.mode = "RGBA" else: - raise SyntaxError("unknown TGA mode") + msg = "unknown TGA mode" + raise SyntaxError(msg) # orientation orientation = flags & 0x30 @@ -99,7 +101,8 @@ class TgaImageFile(ImageFile.ImageFile): elif orientation in [0, 0x10]: orientation = -1 else: - raise SyntaxError("unknown TGA orientation") + msg = "unknown TGA orientation" + raise SyntaxError(msg) self.info["orientation"] = orientation @@ -175,7 +178,8 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TGA") from e + msg = f"cannot write mode {im.mode} as TGA" + raise OSError(msg) from e if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fa3479b35..431edfd9b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -500,14 +500,16 @@ class ImageFileDirectory_v2(MutableMapping): :param prefix: Override the endianness of the file. """ if not _accept(ifh): - raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") + msg = f"not a TIFF file (header {repr(ifh)} not valid)" + raise SyntaxError(msg) self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" elif self._prefix == II: self._endian = "<" else: - raise SyntaxError("not a TIFF IFD") + msg = "not a TIFF IFD" + raise SyntaxError(msg) self._bigtiff = ifh[2] == 43 self.group = group self.tagtype = {} @@ -524,7 +526,8 @@ class ImageFileDirectory_v2(MutableMapping): @legacy_api.setter def legacy_api(self, value): - raise Exception("Not allowing setting of legacy api") + msg = "Not allowing setting of legacy api" + raise Exception(msg) def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false @@ -780,10 +783,11 @@ class ImageFileDirectory_v2(MutableMapping): def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise OSError( + msg = ( "Corrupt EXIF data. " f"Expecting to read {size} bytes but only got {len(ret)}. " ) + raise OSError(msg) return ret def load(self, fp): @@ -910,7 +914,8 @@ class ImageFileDirectory_v2(MutableMapping): if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError("multistrip support not yet implemented") + msg = "multistrip support not yet implemented" + raise NotImplementedError(msg) value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data @@ -1123,7 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: - raise EOFError("no more images in TIFF file") + msg = "no more images in TIFF file" + raise EOFError(msg) logger.debug( f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" @@ -1230,7 +1236,8 @@ class TiffImageFile(ImageFile.ImageFile): self.load_prepare() if not len(self.tile) == 1: - raise OSError("Not exactly one tile") + msg = "Not exactly one tile" + raise OSError(msg) # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) @@ -1262,7 +1269,8 @@ class TiffImageFile(ImageFile.ImageFile): try: decoder.setimage(self.im, extents) except ValueError as e: - raise OSError("Couldn't set the image") from e + msg = "Couldn't set the image" + raise OSError(msg) from e close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): @@ -1316,7 +1324,8 @@ class TiffImageFile(ImageFile.ImageFile): """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: - raise OSError("Windows Media Photo files not yet supported") + msg = "Windows Media Photo files not yet supported" + raise OSError(msg) # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1375,7 +1384,8 @@ class TiffImageFile(ImageFile.ImageFile): logger.error( "More samples per pixel than can be decoded: %s", samples_per_pixel ) - raise SyntaxError("Invalid value for samples per pixel") + msg = "Invalid value for samples per pixel" + raise SyntaxError(msg) if samples_per_pixel < bps_actual_count: # If a file has more values in bps_tuple than expected, @@ -1387,7 +1397,8 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple = bps_tuple * samples_per_pixel if len(bps_tuple) != samples_per_pixel: - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # mode: check photometric interpretation and bits per pixel key = ( @@ -1403,7 +1414,8 @@ class TiffImageFile(ImageFile.ImageFile): self.mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") - raise SyntaxError("unknown pixel mode") from e + msg = "unknown pixel mode" + raise SyntaxError(msg) from e logger.debug(f"- raw mode: {rawmode}") logger.debug(f"- pil mode: {self.mode}") @@ -1519,7 +1531,8 @@ class TiffImageFile(ImageFile.ImageFile): layer += 1 else: logger.debug("- unsupported data organization") - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # Fix up info. if ICCPROFILE in self.tag_v2: @@ -1571,7 +1584,8 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TIFF") from e + msg = f"cannot write mode {im.mode} as TIFF" + raise OSError(msg) from e ifd = ImageFileDirectory_v2(prefix=prefix) @@ -1736,11 +1750,11 @@ def _save(im, fp, filename): if "quality" in encoderinfo: quality = encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) if compression != "jpeg": - raise ValueError( - "quality setting only supported for 'jpeg' compression" - ) + msg = "quality setting only supported for 'jpeg' compression" + raise ValueError(msg) ifd[JPEGQUALITY] = quality logger.debug("Saving using libtiff encoder") @@ -1837,7 +1851,8 @@ def _save(im, fp, filename): if s: break if s < 0: - raise OSError(f"encoder error {s} when writing image file") + msg = f"encoder error {s} when writing image file" + raise OSError(msg) else: for tag in blocklist: @@ -1912,7 +1927,8 @@ class AppendingTiffWriter: elif iimm == b"MM\x00\x2a": self.setEndian(">") else: - raise RuntimeError("Invalid TIFF file header") + msg = "Invalid TIFF file header" + raise RuntimeError(msg) self.skipIFDs() self.goToEnd() @@ -1926,12 +1942,14 @@ class AppendingTiffWriter: iimm = self.f.read(4) if not iimm: - # raise RuntimeError("nothing written into new page") + # msg = "nothing written into new page" + # raise RuntimeError(msg) # Make it easy to finish a frame without committing to a new one. return if iimm != self.IIMM: - raise RuntimeError("IIMM of new page doesn't match IIMM of first page") + msg = "IIMM of new page doesn't match IIMM of first page" + raise RuntimeError(msg) ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage @@ -2005,29 +2023,34 @@ class AppendingTiffWriter: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def writeShort(self, value): bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def writeLong(self, value): bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def close(self): self.finalize() @@ -2070,7 +2093,8 @@ class AppendingTiffWriter: def fixOffsets(self, count, isShort=False, isLong=False): if not isShort and not isLong: - raise RuntimeError("offset is neither short nor long") + msg = "offset is neither short nor long" + raise RuntimeError(msg) for i in range(count): offset = self.readShort() if isShort else self.readLong() @@ -2078,7 +2102,8 @@ class AppendingTiffWriter: if isShort and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: - raise RuntimeError("not implemented") # XXX TODO + msg = "not implemented" + raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 81ed550d9..1d074f78c 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -130,7 +130,8 @@ class WebPImageFile(ImageFile.ImageFile): if ret is None: self._reset() # Reset just to be safe self.seek(0) - raise EOFError("failed to decode next frame in WebP file") + msg = "failed to decode next frame in WebP file" + raise EOFError(msg) # Compute duration data, timestamp = ret @@ -233,9 +234,8 @@ def _save_all(im, fp, filename): or len(background) != 4 or not all(0 <= v < 256 for v in background) ): - raise OSError( - f"Background color is not an RGBA tuple clamped to (0-255): {background}" - ) + msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" + raise OSError(msg) # Convert to packed uint bg_r, bg_g, bg_b, bg_a = background @@ -311,7 +311,8 @@ def _save_all(im, fp, filename): # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) @@ -351,7 +352,8 @@ def _save(im, fp, filename): xmp, ) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 2f54cdebb..639730b8e 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -109,7 +109,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": - raise SyntaxError("Unsupported WMF file format") + msg = "Unsupported WMF file format" + raise SyntaxError(msg) elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile @@ -137,7 +138,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi, ydpi else: - raise SyntaxError("Unsupported file format") + msg = "Unsupported file format" + raise SyntaxError(msg) self.mode = "RGB" self._size = size @@ -162,7 +164,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("WMF save handler not installed") + msg = "WMF save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 4efedb77e..f0e05e867 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -49,7 +49,8 @@ class XVThumbImageFile(ImageFile.ImageFile): # check magic if not _accept(self.fp.read(6)): - raise SyntaxError("not an XV thumbnail file") + msg = "not an XV thumbnail file" + raise SyntaxError(msg) # Skip to beginning of next line self.fp.readline() @@ -58,7 +59,8 @@ class XVThumbImageFile(ImageFile.ImageFile): while True: s = self.fp.readline() if not s: - raise SyntaxError("Unexpected EOF reading XV thumbnail file") + msg = "Unexpected EOF reading XV thumbnail file" + raise SyntaxError(msg) if s[0] != 35: # ie. when not a comment: '#' break diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 59acabeba..ad18e0031 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -53,7 +53,8 @@ class XbmImageFile(ImageFile.ImageFile): m = xbm_head.match(self.fp.read(512)) if not m: - raise SyntaxError("not a XBM file") + msg = "not a XBM file" + raise SyntaxError(msg) xsize = int(m.group("width")) ysize = int(m.group("height")) @@ -70,7 +71,8 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "1": - raise OSError(f"cannot write mode {im.mode} as XBM") + msg = f"cannot write mode {im.mode} as XBM" + raise OSError(msg) fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index aaed2039d..5fae4cd68 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -40,13 +40,15 @@ class XpmImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(9)): - raise SyntaxError("not an XPM file") + msg = "not an XPM file" + raise SyntaxError(msg) # skip forward to next string while True: s = self.fp.readline() if not s: - raise SyntaxError("broken XPM file") + msg = "broken XPM file" + raise SyntaxError(msg) m = xpm_head.match(s) if m: break @@ -57,7 +59,8 @@ class XpmImageFile(ImageFile.ImageFile): bpp = int(m.group(4)) if pal > 256 or bpp != 1: - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) # # load palette description @@ -91,13 +94,15 @@ class XpmImageFile(ImageFile.ImageFile): ) else: # unknown colour - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) break else: # missing colour key - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) self.mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 30a8a8971..7c4b1623d 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -43,14 +43,17 @@ def deprecate( if when is None: removed = "a future version" elif when <= int(__version__.split(".")[0]): - raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.") + msg = f"{deprecated} {is_} deprecated and should be removed." + raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" else: - raise ValueError(f"Unknown removal version, update {__name__}?") + msg = f"Unknown removal version, update {__name__}?" + raise ValueError(msg) if replacement and action: - raise ValueError("Use only one of 'replacement' and 'action'") + msg = "Use only one of 'replacement' and 'action'" + raise ValueError(msg) if replacement: action = f". Use {replacement} instead." diff --git a/src/PIL/features.py b/src/PIL/features.py index 3838568f3..6f9d99e76 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -25,7 +25,8 @@ def check_module(feature): :raises ValueError: If the module is not defined in this version of Pillow. """ if not (feature in modules): - raise ValueError(f"Unknown module {feature}") + msg = f"Unknown module {feature}" + raise ValueError(msg) module, ver = modules[feature] @@ -78,7 +79,8 @@ def check_codec(feature): :raises ValueError: If the codec is not defined in this version of Pillow. """ if feature not in codecs: - raise ValueError(f"Unknown codec {feature}") + msg = f"Unknown codec {feature}" + raise ValueError(msg) codec, lib = codecs[feature] @@ -135,7 +137,8 @@ def check_feature(feature): :raises ValueError: If the feature is not defined in this version of Pillow. """ if feature not in features: - raise ValueError(f"Unknown feature {feature}") + msg = f"Unknown feature {feature}" + raise ValueError(msg) module, flag, ver = features[feature] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a061aaf17..68c2acd67 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -478,7 +478,8 @@ def extract_dep(url, filename): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: - raise RuntimeError("Attempted Path Traversal in Zip File") + msg = "Attempted Path Traversal in Zip File" + raise RuntimeError(msg) zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: @@ -486,7 +487,8 @@ def extract_dep(url, filename): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: - raise RuntimeError("Attempted Path Traversal in Tar File") + msg = "Attempted Path Traversal in Tar File" + raise RuntimeError(msg) tgz.extractall(sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) @@ -642,9 +644,8 @@ if __name__ == "__main__": msvs = find_msvs() if msvs is None: - raise RuntimeError( - "Visual Studio not found. Please install Visual Studio 2017 or newer." - ) + msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." + raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) print("Using output directory:", build_dir) From 68fdd2a9e76319f0021256a86d388df1a5f9875a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 14:24:28 +1100 Subject: [PATCH 204/205] Further improve exception traceback readability --- src/PIL/ImImagePlugin.py | 5 ++--- src/PIL/Image.py | 24 +++++++++++------------- src/PIL/ImageCms.py | 6 ++++-- src/PIL/ImageFile.py | 11 ++++++----- src/PIL/ImageMorph.py | 3 ++- src/PIL/PdfParser.py | 11 ++++++----- winbuild/build_prepare.py | 6 ++++-- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index d0e9508fe..875a20326 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -201,9 +201,8 @@ class ImImageFile(ImageFile.ImageFile): else: - raise SyntaxError( - "Syntax error in IM header: " + s.decode("ascii", "replace") - ) + msg = "Syntax error in IM header: " + s.decode("ascii", "replace") + raise SyntaxError(msg) if not n: msg = "Not an IM file" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 386fb7c26..b22060965 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2129,7 +2129,7 @@ class Image: Resampling.BOX, Resampling.HAMMING, ): - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2142,9 +2142,8 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: msg = "reducing_gap must be 1.0 or greater" @@ -2764,13 +2763,13 @@ class Image: Resampling.BICUBIC, ): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - message = { + msg = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", }[resample] + f" ({resample}) cannot be used." else: - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2780,9 +2779,8 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) image.load() @@ -3077,7 +3075,8 @@ def fromarray(obj, mode=None): try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: - raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e + msg = "Cannot handle this data type: %s, %s" % typekey + raise TypeError(msg) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -3276,9 +3275,8 @@ def open(fp, mode="r", formats=None): fp.close() for message in accept_warnings: warnings.warn(message) - raise UnidentifiedImageError( - "cannot identify image file %r" % (filename if filename else fp) - ) + msg = "cannot identify image file %r" % (filename if filename else fp) + raise UnidentifiedImageError(msg) # diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 2a2d372e5..f87849680 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -498,7 +498,8 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -601,7 +602,8 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0d3facf57..12391955f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -63,12 +63,13 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`, def raise_oserror(error): try: - message = Image.core.getcodecstatus(error) + msg = Image.core.getcodecstatus(error) except AttributeError: - message = ERRORS.get(error) - if not message: - message = f"decoder error {error}" - raise OSError(message + " when reading image file") + msg = ERRORS.get(error) + if not msg: + msg = f"decoder error {error}" + msg += " when reading image file" + raise OSError(msg) def _tilesort(t): diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 60cbbedc3..6fccc315b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -146,7 +146,8 @@ class LutBuilder: for p in self.patterns: m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: - raise Exception('Syntax error in pattern "' + p + '"') + msg = 'Syntax error in pattern "' + p + '"' + raise Exception(msg) options = m.group(1) pattern = m.group(2) result = int(m.group(3)) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index e4a0f25a9..aa5ea2fbb 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -817,10 +817,10 @@ class PdfParser: try: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError) as e: - raise PdfFormatError( - "bad or missing Length in stream dict (%r)" - % result.get(b"Length", None) - ) from e + msg = "bad or missing Length in stream dict (%r)" % result.get( + b"Length", None + ) + raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") @@ -874,7 +874,8 @@ class PdfParser: if m: return cls.get_literal_string(data, m.end()) # return None, offset # fallback (only for debugging) - raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) + msg = "unrecognized object: " + repr(data[offset : offset + 32]) + raise PdfFormatError(msg) re_lit_str_token = re.compile( rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 68c2acd67..f5050946c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -491,7 +491,8 @@ def extract_dep(url, filename): raise RuntimeError(msg) tgz.extractall(sources_dir) else: - raise RuntimeError("Unknown archive type: " + filename) + msg = "Unknown archive type: " + filename + raise RuntimeError(msg) def write_script(name, lines): @@ -628,7 +629,8 @@ if __name__ == "__main__": elif arg == "--srcdir": sources_dir = os.path.sep + "src" else: - raise ValueError("Unknown parameter: " + arg) + msg = "Unknown parameter: " + arg + raise ValueError(msg) # dependency cache directory os.makedirs(depends_dir, exist_ok=True) From 907d59753bdd66460f0bc73e6022352f5ff14591 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 09:33:12 +1100 Subject: [PATCH 205/205] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4eebbda6a..904c73629 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Improve exception traceback readability #6836 + [hugovk, radarhere] + - Do not attempt to read IFD1 if absent #6840 [radarhere]