From 69f967301d11a5b37495542ba64633b085c9b357 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:48:46 +0300 Subject: [PATCH 001/136] Call PyUnstable_Module_SetGIL() to indicate support of running with GIL disabled --- src/_imaging.c | 4 ++++ src/_imagingcms.c | 4 ++++ src/_imagingft.c | 4 ++++ src/_imagingmath.c | 4 ++++ src/_imagingmorph.c | 4 ++++ src/_imagingtk.c | 5 +++++ src/_webp.c | 4 ++++ 7 files changed, 29 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index ddc8d2885..03e10e547 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4448,5 +4448,9 @@ PyInit__imaging(void) { return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 590e1b983..628662b30 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1538,5 +1538,9 @@ PyInit__imagingcms(void) { PyDateTime_IMPORT; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_imagingft.c b/src/_imagingft.c index ba36cc72c..1bef876e1 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1576,5 +1576,9 @@ PyInit__imagingft(void) { return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 067c165b2..a2ddc91b9 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -290,5 +290,9 @@ PyInit__imagingmath(void) { return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 8815c2b7e..a95ce75bf 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -269,5 +269,9 @@ PyInit__imagingmorph(void) { m = PyModule_Create(&module_def); +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index efa7fc1b6..c70d044bb 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -62,5 +62,10 @@ PyInit__imagingtk(void) { Py_DECREF(m); return NULL; } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/_webp.c b/src/_webp.c index 0a70e3357..dfa24da41 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1005,5 +1005,9 @@ PyInit__webp(void) { return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } From 377bdc0db1415dcbd664b1b9c467d995500809d2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:27:09 +0300 Subject: [PATCH 002/136] encode: Replace PyList_GetItem with PyList_GetItemRef --- src/encode.c | 5 +- src/thirdparty/pythoncapi_compat.h | 1360 ++++++++++++++++++++++++++++ 2 files changed, 1363 insertions(+), 2 deletions(-) create mode 100644 src/thirdparty/pythoncapi_compat.h diff --git a/src/encode.c b/src/encode.c index 442b5d04f..72ad3fa07 100644 --- a/src/encode.c +++ b/src/encode.c @@ -25,6 +25,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include "libImaging/Gif.h" @@ -671,7 +672,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { tags_size = PyList_Size(tags); TRACE(("tags size: %d\n", (int)tags_size)); for (pos = 0; pos < tags_size; pos++) { - item = PyList_GetItem(tags, pos); + item = PyList_GetItemRef(tags, pos); if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { PyErr_SetString(PyExc_ValueError, "Invalid tags list"); return NULL; @@ -703,7 +704,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { - item = PyList_GetItem(tags, pos); + item = PyList_GetItemRef(tags, pos); // We already checked that tags is a 2-tuple list. key = PyTuple_GetItem(item, 0); key_int = (int)PyLong_AsLong(key); diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h new file mode 100644 index 000000000..51e8c0de7 --- /dev/null +++ b/src/thirdparty/pythoncapi_compat.h @@ -0,0 +1,1360 @@ +// Header file providing new C API functions to old Python versions. +// +// File distributed under the Zero Clause BSD (0BSD) license. +// Copyright Contributors to the pythoncapi_compat project. +// +// Homepage: +// https://github.com/python/pythoncapi_compat +// +// Latest version: +// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// +// SPDX-License-Identifier: 0BSD + +#ifndef PYTHONCAPI_COMPAT +#define PYTHONCAPI_COMPAT + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// Python 3.11.0b4 added PyFrame_Back() to Python.h +#if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) +# include "frameobject.h" // PyFrameObject, PyFrame_GetBack() +#endif + + +#ifndef _Py_CAST +# define _Py_CAST(type, expr) ((type)(expr)) +#endif + +// Static inline functions should use _Py_NULL rather than using directly NULL +// to prevent C++ compiler warnings. On C23 and newer and on C++11 and newer, +// _Py_NULL is defined as nullptr. +#if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ + || (defined(__cplusplus) && __cplusplus >= 201103) +# define _Py_NULL nullptr +#else +# define _Py_NULL NULL +#endif + +// Cast argument to PyObject* type. +#ifndef _PyObject_CAST +# define _PyObject_CAST(op) _Py_CAST(PyObject*, op) +#endif + + +// bpo-42262 added Py_NewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) +static inline PyObject* _Py_NewRef(PyObject *obj) +{ + Py_INCREF(obj); + return obj; +} +#define Py_NewRef(obj) _Py_NewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-42262 added Py_XNewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_XNewRef) +static inline PyObject* _Py_XNewRef(PyObject *obj) +{ + Py_XINCREF(obj); + return obj; +} +#define Py_XNewRef(obj) _Py_XNewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-39573 added Py_SET_REFCNT() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_REFCNT) +static inline void _Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) +{ + ob->ob_refcnt = refcnt; +} +#define Py_SET_REFCNT(ob, refcnt) _Py_SET_REFCNT(_PyObject_CAST(ob), refcnt) +#endif + + +// Py_SETREF() and Py_XSETREF() were added to Python 3.5.2. +// It is excluded from the limited C API. +#if (PY_VERSION_HEX < 0x03050200 && !defined(Py_SETREF)) && !defined(Py_LIMITED_API) +#define Py_SETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_DECREF(_tmp_dst); \ + } while (0) + +#define Py_XSETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_XDECREF(_tmp_dst); \ + } while (0) +#endif + + +// bpo-43753 added Py_Is(), Py_IsNone(), Py_IsTrue() and Py_IsFalse() +// to Python 3.10.0b1. +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_Is) +# define Py_Is(x, y) ((x) == (y)) +#endif +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_IsNone) +# define Py_IsNone(x) Py_Is(x, Py_None) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsTrue) +# define Py_IsTrue(x) Py_Is(x, Py_True) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsFalse) +# define Py_IsFalse(x) Py_Is(x, Py_False) +#endif + + +// bpo-39573 added Py_SET_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_TYPE) +static inline void _Py_SET_TYPE(PyObject *ob, PyTypeObject *type) +{ + ob->ob_type = type; +} +#define Py_SET_TYPE(ob, type) _Py_SET_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-39573 added Py_SET_SIZE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_SIZE) +static inline void _Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) +{ + ob->ob_size = size; +} +#define Py_SET_SIZE(ob, size) _Py_SET_SIZE((PyVarObject*)(ob), size) +#endif + + +// bpo-40421 added PyFrame_GetCode() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 || defined(PYPY_VERSION) +static inline PyCodeObject* PyFrame_GetCode(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + assert(frame->f_code != _Py_NULL); + return _Py_CAST(PyCodeObject*, Py_NewRef(frame->f_code)); +} +#endif + +static inline PyCodeObject* _PyFrame_GetCodeBorrow(PyFrameObject *frame) +{ + PyCodeObject *code = PyFrame_GetCode(frame); + Py_DECREF(code); + return code; +} + + +// bpo-40421 added PyFrame_GetBack() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyFrame_GetBack(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + return _Py_CAST(PyFrameObject*, Py_XNewRef(frame->f_back)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* _PyFrame_GetBackBorrow(PyFrameObject *frame) +{ + PyFrameObject *back = PyFrame_GetBack(frame); + Py_XDECREF(back); + return back; +} +#endif + + +// bpo-40421 added PyFrame_GetLocals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetLocals(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030400B1 + if (PyFrame_FastToLocalsWithError(frame) < 0) { + return NULL; + } +#else + PyFrame_FastToLocals(frame); +#endif + return Py_NewRef(frame->f_locals); +} +#endif + + +// bpo-40421 added PyFrame_GetGlobals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetGlobals(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_globals); +} +#endif + + +// bpo-40421 added PyFrame_GetBuiltins() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetBuiltins(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_builtins); +} +#endif + + +// bpo-40421 added PyFrame_GetLasti() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline int PyFrame_GetLasti(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030A00A7 + // bpo-27129: Since Python 3.10.0a7, f_lasti is an instruction offset, + // not a bytes offset anymore. Python uses 16-bit "wordcode" (2 bytes) + // instructions. + if (frame->f_lasti < 0) { + return -1; + } + return frame->f_lasti * 2; +#else + return frame->f_lasti; +#endif +} +#endif + + +// gh-91248 added PyFrame_GetVar() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetVar(PyFrameObject *frame, PyObject *name) +{ + PyObject *locals, *value; + + locals = PyFrame_GetLocals(frame); + if (locals == NULL) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + value = PyDict_GetItemWithError(locals, name); +#else + value = _PyDict_GetItemWithError(locals, name); +#endif + Py_DECREF(locals); + + if (value == NULL) { + if (PyErr_Occurred()) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + PyErr_Format(PyExc_NameError, "variable %R does not exist", name); +#else + PyErr_SetString(PyExc_NameError, "variable does not exist"); +#endif + return NULL; + } + return Py_NewRef(value); +} +#endif + + +// gh-91248 added PyFrame_GetVarString() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* +PyFrame_GetVarString(PyFrameObject *frame, const char *name) +{ + PyObject *name_obj, *value; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(name); +#else + name_obj = PyString_FromString(name); +#endif + if (name_obj == NULL) { + return NULL; + } + value = PyFrame_GetVar(frame, name_obj); + Py_DECREF(name_obj); + return value; +} +#endif + + +// bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +static inline PyInterpreterState * +PyThreadState_GetInterpreter(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->interp; +} +#endif + + +// bpo-40429 added PyThreadState_GetFrame() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return _Py_CAST(PyFrameObject *, Py_XNewRef(tstate->frame)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* +_PyThreadState_GetFrameBorrow(PyThreadState *tstate) +{ + PyFrameObject *frame = PyThreadState_GetFrame(tstate); + Py_XDECREF(frame); + return frame; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +static inline PyInterpreterState* PyInterpreterState_Get(void) +{ + PyThreadState *tstate; + PyInterpreterState *interp; + + tstate = PyThreadState_GET(); + if (tstate == _Py_NULL) { + Py_FatalError("GIL released (tstate is NULL)"); + } + interp = tstate->interp; + if (interp == _Py_NULL) { + Py_FatalError("no current interpreter"); + } + return interp; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a6 +#if 0x030700A1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline uint64_t PyThreadState_GetID(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->id; +} +#endif + +// bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_EnterTracing(PyThreadState *tstate) +{ + tstate->tracing++; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = 0; +#else + tstate->use_tracing = 0; +#endif +} +#endif + +// bpo-43760 added PyThreadState_LeaveTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) +{ + int use_tracing = (tstate->c_tracefunc != _Py_NULL + || tstate->c_profilefunc != _Py_NULL); + tstate->tracing--; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = use_tracing; +#else + tstate->use_tracing = use_tracing; +#endif +} +#endif + + +// bpo-37194 added PyObject_CallNoArgs() to Python 3.9.0a1 +// PyObject_CallNoArgs() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallNoArgs) && PY_VERSION_HEX < 0x030900A1 +static inline PyObject* PyObject_CallNoArgs(PyObject *func) +{ + return PyObject_CallFunctionObjArgs(func, NULL); +} +#endif + + +// bpo-39245 made PyObject_CallOneArg() public (previously called +// _PyObject_CallOneArg) in Python 3.9.0a4 +// PyObject_CallOneArg() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallOneArg) && PY_VERSION_HEX < 0x030900A4 +static inline PyObject* PyObject_CallOneArg(PyObject *func, PyObject *arg) +{ + return PyObject_CallFunctionObjArgs(func, arg, NULL); +} +#endif + + +// bpo-1635741 added PyModule_AddObjectRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 +static inline int +PyModule_AddObjectRef(PyObject *module, const char *name, PyObject *value) +{ + int res; + + if (!value && !PyErr_Occurred()) { + // PyModule_AddObject() raises TypeError in this case + PyErr_SetString(PyExc_SystemError, + "PyModule_AddObjectRef() must be called " + "with an exception raised if value is NULL"); + return -1; + } + + Py_XINCREF(value); + res = PyModule_AddObject(module, name, value); + if (res < 0) { + Py_XDECREF(value); + } + return res; +} +#endif + + +// bpo-40024 added PyModule_AddType() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 +static inline int PyModule_AddType(PyObject *module, PyTypeObject *type) +{ + const char *name, *dot; + + if (PyType_Ready(type) < 0) { + return -1; + } + + // inline _PyType_Name() + name = type->tp_name; + assert(name != _Py_NULL); + dot = strrchr(name, '.'); + if (dot != _Py_NULL) { + name = dot + 1; + } + + return PyModule_AddObjectRef(module, name, _PyObject_CAST(type)); +} +#endif + + +// bpo-40241 added PyObject_GC_IsTracked() to Python 3.9.0a6. +// bpo-4688 added _PyObject_GC_IS_TRACKED() to Python 2.7.0a2. +#if PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsTracked(PyObject* obj) +{ + return (PyObject_IS_GC(obj) && _PyObject_GC_IS_TRACKED(obj)); +} +#endif + +// bpo-40241 added PyObject_GC_IsFinalized() to Python 3.9.0a6. +// bpo-18112 added _PyGCHead_FINALIZED() to Python 3.4.0 final. +#if PY_VERSION_HEX < 0x030900A6 && PY_VERSION_HEX >= 0x030400F0 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsFinalized(PyObject *obj) +{ + PyGC_Head *gc = _Py_CAST(PyGC_Head*, obj) - 1; + return (PyObject_IS_GC(obj) && _PyGCHead_FINALIZED(gc)); +} +#endif + + +// bpo-39573 added Py_IS_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_IS_TYPE) +static inline int _Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { + return Py_TYPE(ob) == type; +} +#define Py_IS_TYPE(ob, type) _Py_IS_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-46906 added PyFloat_Pack2() and PyFloat_Unpack2() to Python 3.11a7. +// bpo-11734 added _PyFloat_Pack2() and _PyFloat_Unpack2() to Python 3.6.0b1. +// Python 3.11a2 moved _PyFloat_Pack2() and _PyFloat_Unpack2() to the internal +// C API: Python 3.11a2-3.11a6 versions are not supported. +#if 0x030600B1 <= PY_VERSION_HEX && PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack2(double x, char *p, int le) +{ return _PyFloat_Pack2(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack2(const char *p, int le) +{ return _PyFloat_Unpack2((const unsigned char *)p, le); } +#endif + + +// bpo-46906 added PyFloat_Pack4(), PyFloat_Pack8(), PyFloat_Unpack4() and +// PyFloat_Unpack8() to Python 3.11a7. +// Python 3.11a2 moved _PyFloat_Pack4(), _PyFloat_Pack8(), _PyFloat_Unpack4() +// and _PyFloat_Unpack8() to the internal C API: Python 3.11a2-3.11a6 versions +// are not supported. +#if PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack4(double x, char *p, int le) +{ return _PyFloat_Pack4(x, (unsigned char*)p, le); } + +static inline int PyFloat_Pack8(double x, char *p, int le) +{ return _PyFloat_Pack8(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack4(const char *p, int le) +{ return _PyFloat_Unpack4((const unsigned char *)p, le); } + +static inline double PyFloat_Unpack8(const char *p, int le) +{ return _PyFloat_Unpack8((const unsigned char *)p, le); } +#endif + + +// gh-92154 added PyCode_GetCode() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCode(PyCodeObject *code) +{ + return Py_NewRef(code->co_code); +} +#endif + + +// gh-95008 added PyCode_GetVarnames() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetVarnames(PyCodeObject *code) +{ + return Py_NewRef(code->co_varnames); +} +#endif + +// gh-95008 added PyCode_GetFreevars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetFreevars(PyCodeObject *code) +{ + return Py_NewRef(code->co_freevars); +} +#endif + +// gh-95008 added PyCode_GetCellvars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCellvars(PyCodeObject *code) +{ + return Py_NewRef(code->co_cellvars); +} +#endif + + +// Py_UNUSED() was added to Python 3.4.0b2. +#if PY_VERSION_HEX < 0x030400B2 && !defined(Py_UNUSED) +# if defined(__GNUC__) || defined(__clang__) +# define Py_UNUSED(name) _unused_ ## name __attribute__((unused)) +# else +# define Py_UNUSED(name) _unused_ ## name +# endif +#endif + + +// gh-105922 added PyImport_AddModuleRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A0 +static inline PyObject* PyImport_AddModuleRef(const char *name) +{ + return Py_XNewRef(PyImport_AddModule(name)); +} +#endif + + +// gh-105927 added PyWeakref_GetRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D0000 +static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) +{ + PyObject *obj; + if (ref != NULL && !PyWeakref_Check(ref)) { + *pobj = NULL; + PyErr_SetString(PyExc_TypeError, "expected a weakref"); + return -1; + } + obj = PyWeakref_GetObject(ref); + if (obj == NULL) { + // SystemError if ref is NULL + *pobj = NULL; + return -1; + } + if (obj == Py_None) { + *pobj = NULL; + return 0; + } + *pobj = Py_NewRef(obj); + return (*pobj != NULL); +} +#endif + + +// bpo-36974 added PY_VECTORCALL_ARGUMENTS_OFFSET to Python 3.8b1 +#ifndef PY_VECTORCALL_ARGUMENTS_OFFSET +# define PY_VECTORCALL_ARGUMENTS_OFFSET (_Py_CAST(size_t, 1) << (8 * sizeof(size_t) - 1)) +#endif + +// bpo-36974 added PyVectorcall_NARGS() to Python 3.8b1 +#if PY_VERSION_HEX < 0x030800B1 +static inline Py_ssize_t PyVectorcall_NARGS(size_t n) +{ + return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; +} +#endif + + +// gh-105922 added PyObject_Vectorcall() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 +static inline PyObject* +PyObject_Vectorcall(PyObject *callable, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ +#if PY_VERSION_HEX >= 0x030800B1 && !defined(PYPY_VERSION) + // bpo-36974 added _PyObject_Vectorcall() to Python 3.8.0b1 + return _PyObject_Vectorcall(callable, args, nargsf, kwnames); +#else + PyObject *posargs = NULL, *kwargs = NULL; + PyObject *res; + Py_ssize_t nposargs, nkwargs, i; + + if (nargsf != 0 && args == NULL) { + PyErr_BadInternalCall(); + goto error; + } + if (kwnames != NULL && !PyTuple_Check(kwnames)) { + PyErr_BadInternalCall(); + goto error; + } + + nposargs = (Py_ssize_t)PyVectorcall_NARGS(nargsf); + if (kwnames) { + nkwargs = PyTuple_GET_SIZE(kwnames); + } + else { + nkwargs = 0; + } + + posargs = PyTuple_New(nposargs); + if (posargs == NULL) { + goto error; + } + if (nposargs) { + for (i=0; i < nposargs; i++) { + PyTuple_SET_ITEM(posargs, i, Py_NewRef(*args)); + args++; + } + } + + if (nkwargs) { + kwargs = PyDict_New(); + if (kwargs == NULL) { + goto error; + } + + for (i = 0; i < nkwargs; i++) { + PyObject *key = PyTuple_GET_ITEM(kwnames, i); + PyObject *value = *args; + args++; + if (PyDict_SetItem(kwargs, key, value) < 0) { + goto error; + } + } + } + else { + kwargs = NULL; + } + + res = PyObject_Call(callable, posargs, kwargs); + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return res; + +error: + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return NULL; +#endif +} +#endif + + +// gh-106521 added PyObject_GetOptionalAttr() and +// PyObject_GetOptionalAttrString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_GetOptionalAttr(PyObject *obj, PyObject *attr_name, PyObject **result) +{ + // bpo-32571 added _PyObject_LookupAttr() to Python 3.7.0b1 +#if PY_VERSION_HEX >= 0x030700B1 && !defined(PYPY_VERSION) + return _PyObject_LookupAttr(obj, attr_name, result); +#else + *result = PyObject_GetAttr(obj, attr_name); + if (*result != NULL) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; +#endif +} + +static inline int +PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result) +{ + PyObject *name_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(attr_name); +#else + name_obj = PyString_FromString(attr_name); +#endif + if (name_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyObject_GetOptionalAttr(obj, name_obj, result); + Py_DECREF(name_obj); + return rc; +} +#endif + + +// gh-106307 added PyObject_GetOptionalAttr() and +// PyMapping_GetOptionalItemString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_GetOptionalItem(PyObject *obj, PyObject *key, PyObject **result) +{ + *result = PyObject_GetItem(obj, key); + if (*result) { + return 1; + } + if (!PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; +} + +static inline int +PyMapping_GetOptionalItemString(PyObject *obj, const char *key, PyObject **result) +{ + PyObject *key_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + key_obj = PyUnicode_FromString(key); +#else + key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyMapping_GetOptionalItem(obj, key_obj, result); + Py_DECREF(key_obj); + return rc; +} +#endif + +// gh-108511 added PyMapping_HasKeyWithError() and +// PyMapping_HasKeyStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_HasKeyWithError(PyObject *obj, PyObject *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItem(obj, key, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyMapping_HasKeyStringWithError(PyObject *obj, const char *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItemString(obj, key, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-108511 added PyObject_HasAttrWithError() and +// PyObject_HasAttrStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_HasAttrWithError(PyObject *obj, PyObject *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttr(obj, attr, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyObject_HasAttrStringWithError(PyObject *obj, const char *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttrString(obj, attr, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-106004 added PyDict_GetItemRef() and PyDict_GetItemStringRef() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result) +{ +#if PY_VERSION_HEX >= 0x03000000 + PyObject *item = PyDict_GetItemWithError(mp, key); +#else + PyObject *item = _PyDict_GetItemWithError(mp, key); +#endif + if (item != NULL) { + *result = Py_NewRef(item); + return 1; // found + } + if (!PyErr_Occurred()) { + *result = NULL; + return 0; // not found + } + *result = NULL; + return -1; +} + +static inline int +PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) +{ + int res; +#if PY_VERSION_HEX >= 0x03000000 + PyObject *key_obj = PyUnicode_FromString(key); +#else + PyObject *key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + res = PyDict_GetItemRef(mp, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-106307 added PyModule_Add() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) +{ + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + + +// gh-108014 added Py_IsFinalizing() to Python 3.13.0a1 +// bpo-1856 added _Py_Finalizing to Python 3.2.1b1. +// _Py_IsFinalizing() was added to PyPy 7.3.0. +#if (0x030201B1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030D00A1) \ + && (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x7030000) +static inline int Py_IsFinalizing(void) +{ +#if PY_VERSION_HEX >= 0x030700A1 + // _Py_IsFinalizing() was added to Python 3.7.0a1. + return _Py_IsFinalizing(); +#else + return (_Py_Finalizing != NULL); +#endif +} +#endif + + +// gh-108323 added PyDict_ContainsString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyDict_ContainsString(PyObject *op, const char *key) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + return -1; + } + int res = PyDict_Contains(op, key_obj); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-108445 added PyLong_AsInt() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyLong_AsInt(PyObject *obj) +{ +#ifdef PYPY_VERSION + long value = PyLong_AsLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + if (value < (long)INT_MIN || (long)INT_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C int"); + return -1; + } + return (int)value; +#else + return _PyLong_AsInt(obj); +#endif +} +#endif + + +// gh-107073 added PyObject_VisitManagedDict() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (*dict == NULL) { + return -1; + } + Py_VISIT(*dict); + return 0; +} + +static inline void +PyObject_ClearManagedDict(PyObject *obj) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (*dict == NULL) { + return; + } + Py_CLEAR(*dict); +} +#endif + +// gh-108867 added PyThreadState_GetUnchecked() to Python 3.13.0a1 +// Python 3.5.2 added _PyThreadState_UncheckedGet(). +#if PY_VERSION_HEX >= 0x03050200 && PY_VERSION_HEX < 0x030D00A1 +static inline PyThreadState* +PyThreadState_GetUnchecked(void) +{ + return _PyThreadState_UncheckedGet(); +} +#endif + +// gh-110289 added PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_len) +{ + Py_ssize_t len; + const void *utf8; + PyObject *exc_type, *exc_value, *exc_tb; + int res; + + // API cannot report errors so save/restore the exception + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + + // Python 3.3.0a1 added PyUnicode_AsUTF8AndSize() +#if PY_VERSION_HEX >= 0x030300A1 + if (PyUnicode_IS_ASCII(unicode)) { + utf8 = PyUnicode_DATA(unicode); + len = PyUnicode_GET_LENGTH(unicode); + } + else { + utf8 = PyUnicode_AsUTF8AndSize(unicode, &len); + if (utf8 == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + } + + if (len != str_len) { + res = 0; + goto done; + } + res = (memcmp(utf8, str, (size_t)len) == 0); +#else + PyObject *bytes = PyUnicode_AsUTF8String(unicode); + if (bytes == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + +#if PY_VERSION_HEX >= 0x03000000 + len = PyBytes_GET_SIZE(bytes); + utf8 = PyBytes_AS_STRING(bytes); +#else + len = PyString_GET_SIZE(bytes); + utf8 = PyString_AS_STRING(bytes); +#endif + if (len != str_len) { + Py_DECREF(bytes); + res = 0; + goto done; + } + + res = (memcmp(utf8, str, (size_t)len) == 0); + Py_DECREF(bytes); +#endif + +done: + PyErr_Restore(exc_type, exc_value, exc_tb); + return res; +} + +static inline int +PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) +{ + return PyUnicode_EqualToUTF8AndSize(unicode, str, (Py_ssize_t)strlen(str)); +} +#endif + + +// gh-111138 added PyList_Extend() and PyList_Clear() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyList_Extend(PyObject *list, PyObject *iterable) +{ + return PyList_SetSlice(list, PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, iterable); +} + +static inline int +PyList_Clear(PyObject *list) +{ + return PyList_SetSlice(list, 0, PY_SSIZE_T_MAX, NULL); +} +#endif + +// gh-111262 added PyDict_Pop() and PyDict_PopString() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result) +{ + PyObject *value; + + if (!PyDict_Check(dict)) { + PyErr_BadInternalCall(); + if (result) { + *result = NULL; + } + return -1; + } + + // bpo-16991 added _PyDict_Pop() to Python 3.5.0b2. + // Python 3.6.0b3 changed _PyDict_Pop() first argument type to PyObject*. + // Python 3.13.0a1 removed _PyDict_Pop(). +#if defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030500b2 || PY_VERSION_HEX >= 0x030D0000 + value = PyObject_CallMethod(dict, "pop", "O", key); +#elif PY_VERSION_HEX < 0x030600b3 + value = _PyDict_Pop(_Py_CAST(PyDictObject*, dict), key, NULL); +#else + value = _PyDict_Pop(dict, key, NULL); +#endif + if (value == NULL) { + if (result) { + *result = NULL; + } + if (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; + } + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; +} + +static inline int +PyDict_PopString(PyObject *dict, const char *key, PyObject **result) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + if (result != NULL) { + *result = NULL; + } + return -1; + } + + int res = PyDict_Pop(dict, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +#if PY_VERSION_HEX < 0x030200A4 +// Python 3.2.0a4 added Py_hash_t type +typedef Py_ssize_t Py_hash_t; +#endif + + +// gh-111545 added Py_HashPointer() to Python 3.13.0a3 +#if PY_VERSION_HEX < 0x030D00A3 +static inline Py_hash_t Py_HashPointer(const void *ptr) +{ +#if PY_VERSION_HEX >= 0x030900A4 && !defined(PYPY_VERSION) + return _Py_HashPointer(ptr); +#else + return _Py_HashPointer(_Py_CAST(void*, ptr)); +#endif +} +#endif + + +// Python 3.13a4 added a PyTime API. +// Use the private API added to Python 3.5. +#if PY_VERSION_HEX < 0x030D00A4 && PY_VERSION_HEX >= 0x03050000 +typedef _PyTime_t PyTime_t; +#define PyTime_MIN _PyTime_MIN +#define PyTime_MAX _PyTime_MAX + +static inline double PyTime_AsSecondsDouble(PyTime_t t) +{ return _PyTime_AsSecondsDouble(t); } + +static inline int PyTime_Monotonic(PyTime_t *result) +{ return _PyTime_GetMonotonicClockWithInfo(result, NULL); } + +static inline int PyTime_Time(PyTime_t *result) +{ return _PyTime_GetSystemClockWithInfo(result, NULL); } + +static inline int PyTime_PerfCounter(PyTime_t *result) +{ +#if PY_VERSION_HEX >= 0x03070000 && !defined(PYPY_VERSION) + return _PyTime_GetPerfCounterWithInfo(result, NULL); +#elif PY_VERSION_HEX >= 0x03070000 + // Call time.perf_counter_ns() and convert Python int object to PyTime_t. + // Cache time.perf_counter_ns() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter_ns"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + long long value = PyLong_AsLongLong(res); + Py_DECREF(res); + + if (value == -1 && PyErr_Occurred()) { + return -1; + } + + Py_BUILD_ASSERT(sizeof(value) >= sizeof(PyTime_t)); + *result = (PyTime_t)value; + return 0; +#else + // Call time.perf_counter() and convert C double to PyTime_t. + // Cache time.perf_counter() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + double d = PyFloat_AsDouble(res); + Py_DECREF(res); + + if (d == -1.0 && PyErr_Occurred()) { + return -1; + } + + // Avoid floor() to avoid having to link to libm + *result = (PyTime_t)(d * 1e9); + return 0; +#endif +} + +#endif + +// gh-111389 added hash constants to Python 3.13.0a5. These constants were +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9. +#if (!defined(PyHASH_BITS) \ + && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ + || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ + && PYPY_VERSION_NUM >= 0x07090000))) +# define PyHASH_BITS _PyHASH_BITS +# define PyHASH_MODULUS _PyHASH_MODULUS +# define PyHASH_INF _PyHASH_INF +# define PyHASH_IMAG _PyHASH_IMAG +#endif + + +// gh-111545 added Py_GetConstant() and Py_GetConstantBorrowed() +// to Python 3.13.0a6 +#if PY_VERSION_HEX < 0x030D00A6 && !defined(Py_CONSTANT_NONE) + +#define Py_CONSTANT_NONE 0 +#define Py_CONSTANT_FALSE 1 +#define Py_CONSTANT_TRUE 2 +#define Py_CONSTANT_ELLIPSIS 3 +#define Py_CONSTANT_NOT_IMPLEMENTED 4 +#define Py_CONSTANT_ZERO 5 +#define Py_CONSTANT_ONE 6 +#define Py_CONSTANT_EMPTY_STR 7 +#define Py_CONSTANT_EMPTY_BYTES 8 +#define Py_CONSTANT_EMPTY_TUPLE 9 + +static inline PyObject* Py_GetConstant(unsigned int constant_id) +{ + static PyObject* constants[Py_CONSTANT_EMPTY_TUPLE + 1] = {NULL}; + + if (constants[Py_CONSTANT_NONE] == NULL) { + constants[Py_CONSTANT_NONE] = Py_None; + constants[Py_CONSTANT_FALSE] = Py_False; + constants[Py_CONSTANT_TRUE] = Py_True; + constants[Py_CONSTANT_ELLIPSIS] = Py_Ellipsis; + constants[Py_CONSTANT_NOT_IMPLEMENTED] = Py_NotImplemented; + + constants[Py_CONSTANT_ZERO] = PyLong_FromLong(0); + if (constants[Py_CONSTANT_ZERO] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_ONE] = PyLong_FromLong(1); + if (constants[Py_CONSTANT_ONE] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_STR] = PyUnicode_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_STR] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_BYTES] = PyBytes_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_BYTES] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_TUPLE] = PyTuple_New(0); + if (constants[Py_CONSTANT_EMPTY_TUPLE] == NULL) { + goto fatal_error; + } + // goto dance to avoid compiler warnings about Py_FatalError() + goto init_done; + +fatal_error: + // This case should never happen + Py_FatalError("Py_GetConstant() failed to get constants"); + } + +init_done: + if (constant_id <= Py_CONSTANT_EMPTY_TUPLE) { + return Py_NewRef(constants[constant_id]); + } + else { + PyErr_BadInternalCall(); + return NULL; + } +} + +static inline PyObject* Py_GetConstantBorrowed(unsigned int constant_id) +{ + PyObject *obj = Py_GetConstant(constant_id); + Py_XDECREF(obj); + return obj; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline PyObject * +PyList_GetItemRef(PyObject *op, Py_ssize_t index) +{ + PyObject *item = PyList_GetItem(op, index); + Py_XINCREF(item); + return item; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline int +PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, + PyObject **result) +{ + PyObject *value; + if (PyDict_GetItemRef(d, key, &value) < 0) { + // get error + if (result) { + *result = NULL; + } + return -1; + } + if (value != NULL) { + // present + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; + } + + // missing: set the item + if (PyDict_SetItem(d, key, default_value) < 0) { + // set error + if (result) { + *result = NULL; + } + return -1; + } + if (result) { + *result = Py_NewRef(default_value); + } + return 0; +} +#endif + + +// gh-116560 added PyLong_GetSign() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyLong_GetSign(PyObject *obj, int *sign) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + + *sign = _PyLong_Sign(obj); + return 0; +} +#endif + + +#ifdef __cplusplus +} +#endif +#endif // PYTHONCAPI_COMPAT From 7c64ae0c73666924fa1e298f1be55abbf6973586 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:46:52 +0300 Subject: [PATCH 003/136] encode: Replace PyDict_GetItem with PyDict_GetItemRef --- src/encode.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/encode.c b/src/encode.c index 72ad3fa07..620774f67 100644 --- a/src/encode.c +++ b/src/encode.c @@ -722,7 +722,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (!is_core_tag) { - PyObject *tag_type = PyDict_GetItem(types, key); + PyObject *tag_type; + if (PyDict_GetItemRef(types, key, &tag_type) == 0) { + PyErr_SetString(PyExc_KeyError, "unknown tag type"); + } if (tag_type) { int type_int = PyLong_AsLong(tag_type); if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { From 87596bd747f1cfadd2f7ec6beb5bca67e6aeba0a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:51:15 +0300 Subject: [PATCH 004/136] imagingft: Replace PyDict_GetItem with PyDict_GetItemRef --- src/_imagingft.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 1bef876e1..468f16884 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -20,6 +20,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include @@ -1219,7 +1220,7 @@ font_getvarnames(FontObject *self) { } for (j = 0; j < num_namedstyles; j++) { - if (PyList_GetItem(list_names, j) != NULL) { + if (PyList_GetItemRef(list_names, j) != NULL) { continue; } From 40e7f511b33204a23faa0f26140dd7b74c13be97 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 5 Jul 2024 16:30:48 +0200 Subject: [PATCH 005/136] Don't use PyList_GetItemRef immediately after PyList_New --- src/_imagingft.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 468f16884..ddcf28f97 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1205,6 +1205,16 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + + int *list_names_filled = PyMem_Malloc(num_namedstyles * sizeof(int)); + if (list_names_filled == NULL) { + return PyErr_NoMemory(); + } + + for (int i = 0; i < num_namedstyles; i++) { + list_names_filled[i] = 0; + } + if (list_names == NULL) { FT_Done_MM_Var(library, master); return NULL; @@ -1220,13 +1230,14 @@ font_getvarnames(FontObject *self) { } for (j = 0; j < num_namedstyles; j++) { - if (PyList_GetItemRef(list_names, j) != NULL) { + if (list_names_filled[j]) { continue; } if (master->namedstyle[j].strid == name.name_id) { list_name = Py_BuildValue("y#", name.string, name.string_len); PyList_SetItem(list_names, j, list_name); + list_names_filled[j] = 1; break; } } From c416f0ea1db1dfe5b3a96a0d24af0f0318883317 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jul 2024 06:11:11 +1000 Subject: [PATCH 006/136] Build wheels with free threading --- .github/workflows/wheels-test.sh | 6 ++++++ .github/workflows/wheels.yml | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 3fbf3be69..023a33824 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -12,8 +12,14 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then else yum install -y fribidi fi + if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then + # TODO Update condition when NumPy supports free-threading + if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then + python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + else python3 -m pip install numpy + fi fi if [ ! -d "test-images-main" ]; then diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f8dff3a3e..e32f14529 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -129,6 +129,7 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} + CIBW_FREE_THREADED_SUPPORT: True CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_PRERELEASE_PYTHONS: True @@ -201,6 +202,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_FREE_THREADED_SUPPORT: True CIBW_PRERELEASE_PYTHONS: True CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From 5bae9343171d653dcf5dff0669c3665fd97dea3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 12 Jul 2024 21:16:56 +1000 Subject: [PATCH 007/136] Added type hints --- Tests/test_file_png.py | 2 +- Tests/test_imagefile.py | 6 +++--- src/PIL/ImageFile.py | 34 +++++++++++++++--------------- src/PIL/ImageSequence.py | 2 +- src/PIL/JpegImagePlugin.py | 4 ++-- src/PIL/PdfImagePlugin.py | 20 +++++++++--------- src/PIL/PngImagePlugin.py | 36 ++++++++++++++++--------------- src/PIL/TiffImagePlugin.py | 43 +++++++++++++++++++------------------- src/PIL/WalImageFile.py | 5 ++++- 9 files changed, 79 insertions(+), 73 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index dfe8f9e99..e2913e944 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() - PngImagePlugin.putchunk(*(test_file, cid) + data) + PngImagePlugin.putchunk(test_file, cid, *data) return test_file.getvalue() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b996860ce..44a6e6a42 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -127,7 +127,7 @@ class TestImageFile: def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() - parser.feed(1) + parser.feed(1) # type: ignore[arg-type] def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: @@ -305,7 +305,7 @@ class TestPyDecoder(CodecsTest): im.load() def test_decode(self) -> None: - decoder = ImageFile.PyDecoder(None) + decoder = ImageFile.PyDecoder("") with pytest.raises(NotImplementedError): decoder.decode(b"") @@ -383,7 +383,7 @@ class TestPyEncoder(CodecsTest): ) def test_encode(self) -> None: - encoder = ImageFile.PyEncoder(None) + encoder = ImageFile.PyEncoder("") with pytest.raises(NotImplementedError): encoder.encode(0) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 99d7e73f1..6b2953451 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError: raise _get_oserror(error, encoder=False) -def _tilesort(t): +def _tilesort(t) -> int: # sort on offset return t[2] @@ -161,7 +161,7 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None - def __setstate__(self, state): + def __setstate__(self, state) -> None: self.tile = [] super().__setstate__(state) @@ -333,14 +333,14 @@ class ImageFile(Image.Image): # def load_read(self, read_bytes: int) -> bytes: # pass - def _seek_check(self, frame): + def _seek_check(self, frame: int) -> bool: if ( frame < self._min_frame # Only check upper limit on frames if additional seek operations # are not required to do so or ( not (hasattr(self, "_n_frames") and self._n_frames is None) - and frame >= self.n_frames + self._min_frame + and frame >= getattr(self, "n_frames") + self._min_frame ) ): msg = "attempt to seek outside sequence" @@ -370,7 +370,7 @@ class StubImageFile(ImageFile): msg = "StubImageFile subclass must implement _open" raise NotImplementedError(msg) - def load(self): + def load(self) -> Image.core.PixelAccess | None: loader = self._load() if loader is None: msg = f"cannot find loader for this {self.format} file" @@ -378,7 +378,7 @@ class StubImageFile(ImageFile): image = loader.load(self) assert image is not None # become the other object (!) - self.__class__ = image.__class__ + self.__class__ = image.__class__ # type: ignore[assignment] self.__dict__ = image.__dict__ return image.load() @@ -396,8 +396,8 @@ class Parser: incremental = None image: Image.Image | None = None - data = None - decoder = None + data: bytes | None = None + decoder: Image.core.ImagingDecoder | PyDecoder | None = None offset = 0 finished = 0 @@ -409,7 +409,7 @@ class Parser: """ assert self.data is None, "cannot reuse parsers" - def feed(self, data): + def feed(self, data: bytes) -> None: """ (Consumer) Feed data to the parser. @@ -491,7 +491,7 @@ class Parser: def __exit__(self, *args: object) -> None: self.close() - def close(self): + def close(self) -> Image.Image: """ (Consumer) Close the stream. @@ -525,7 +525,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0) -> None: +def _save(im, fp, tile, bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -553,7 +553,7 @@ def _save(im, fp, tile, bufsize=0) -> None: fp.flush() -def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): +def _encode_tile(im, fp, tile: list[_Tile], bufsize: int, fh, exc=None) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: fp.seek(offset) @@ -629,18 +629,18 @@ class PyCodecState: class PyCodec: fd: IO[bytes] | None - def __init__(self, mode, *args): - self.im = None + def __init__(self, mode: str, *args: Any) -> None: + self.im: Image.core.ImagingCore | None = None self.state = PyCodecState() self.fd = None self.mode = mode self.init(args) - def init(self, args) -> None: + def init(self, args: tuple[Any, ...]) -> None: """ Override to perform codec specific initialization - :param args: Array of args items from the tile entry + :param args: Tuple of arg items from the tile entry :returns: None """ self.args = args @@ -662,7 +662,7 @@ class PyCodec: """ self.fd = fd - def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: + def setimage(self, im, extents=None): """ Called from ImageFile to set the core output image for the codec diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 2c1850276..a6fc340d5 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -33,7 +33,7 @@ class Iterator: :param im: An image object. """ - def __init__(self, im: Image.Image): + def __init__(self, im: Image.Image) -> None: if not hasattr(im, "seek"): msg = "im must have seek method" raise AttributeError(msg) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 4916727be..54f756014 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -827,11 +827,11 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ## # Factory for making JPEG and MPO instances -def jpeg_factory(fp=None, filename=None): +def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): im = JpegImageFile(fp, filename) try: mpheader = im._getmp() - if mpheader[45057] > 1: + if mpheader is not None and mpheader[45057] > 1: for segment, content in im.applist: if segment == "APP1" and b' hdrgm:Version="' in content: # Ultra HDR images are not yet supported diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index f0da1e047..e0f732199 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -174,12 +174,15 @@ def _write_image(im, filename, existing_pdf, image_refs): return image_ref, procset -def _save(im, fp, filename, save_all=False): +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: is_appending = im.encoderinfo.get("append", False) + filename_str = filename.decode() if isinstance(filename, bytes) else filename if is_appending: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") else: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") dpi = im.encoderinfo.get("dpi") if dpi: @@ -228,12 +231,7 @@ def _save(im, fp, filename, save_all=False): for im in ims: im_number_of_pages = 1 if save_all: - try: - im_number_of_pages = im.n_frames - except AttributeError: - # Image format does not have n_frames. - # It is a single frame image - pass + im_number_of_pages = getattr(im, "n_frames", 1) number_of_pages += im_number_of_pages for i in range(im_number_of_pages): image_refs.append(existing_pdf.next_object_id(0)) @@ -250,7 +248,9 @@ def _save(im, fp, filename, save_all=False): page_number = 0 for im_sequence in ims: - im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + im_pages: ImageSequence.Iterator | list[Image.Image] = ( + ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + ) for im in im_pages: image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index fa117d19a..6990b6d05 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -144,7 +144,7 @@ def _safe_zlib_decompress(s): return plaintext -def _crc32(data, seed=0): +def _crc32(data: bytes, seed: int = 0) -> int: return zlib.crc32(data, seed) & 0xFFFFFFFF @@ -191,7 +191,7 @@ class ChunkStream: assert self.queue is not None self.queue.append((cid, pos, length)) - def call(self, cid, pos, length): + def call(self, cid: bytes, pos: int, length: int) -> bytes: """Call the appropriate chunk handler""" logger.debug("STREAM %r %s %s", cid, pos, length) @@ -1091,21 +1091,21 @@ _OUTMODES = { } -def putchunk(fp, cid, *data): +def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: """Write a PNG chunk (including CRC field)""" - data = b"".join(data) + byte_data = b"".join(data) - fp.write(o32(len(data)) + cid) - fp.write(data) - crc = _crc32(data, _crc32(cid)) + fp.write(o32(len(byte_data)) + cid) + fp.write(byte_data) + crc = _crc32(byte_data, _crc32(cid)) fp.write(o32(crc)) class _idat: # wrap output from the encoder in IDAT chunks - def __init__(self, fp, chunk): + def __init__(self, fp, chunk) -> None: self.fp = fp self.chunk = chunk @@ -1116,7 +1116,7 @@ class _idat: class _fdat: # wrap encoder output in fdAT chunks - def __init__(self, fp, chunk, seq_num): + def __init__(self, fp: IO[bytes], chunk, seq_num: int) -> None: self.fp = fp self.chunk = chunk self.seq_num = seq_num @@ -1259,7 +1259,9 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) -def _save(im, fp, filename, chunk=putchunk, save_all=False): +def _save( + im: Image.Image, fp, filename: str | bytes, chunk=putchunk, save_all: bool = False +) -> None: # save an image to disk (called by the save method) if save_all: @@ -1461,7 +1463,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # PNG chunk converter -def getchunks(im, **params): +def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: """Return a list of PNG chunks representing this image.""" class collector: @@ -1470,19 +1472,19 @@ def getchunks(im, **params): def write(self, data: bytes) -> None: pass - def append(self, chunk: bytes) -> None: + def append(self, chunk: tuple[bytes, bytes, bytes]) -> None: self.data.append(chunk) - def append(fp, cid, *data): - data = b"".join(data) - crc = o32(_crc32(data, _crc32(cid))) - fp.append((cid, data, crc)) + def append(fp: collector, cid: bytes, *data: bytes) -> None: + byte_data = b"".join(data) + crc = o32(_crc32(byte_data, _crc32(cid))) + fp.append((cid, byte_data, crc)) fp = collector() try: im.encoderinfo = params - _save(im, fp, None, append) + _save(im, fp, "", append) finally: del im.encoderinfo diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b89144803..253f64852 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -334,12 +334,13 @@ class IFDRational(Rational): __slots__ = ("_numerator", "_denominator", "_val") - def __init__(self, value, denominator=1): + def __init__(self, value, denominator: int = 1) -> None: """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ + self._val: Fraction | float if isinstance(value, IFDRational): self._numerator = value.numerator self._denominator = value.denominator @@ -636,13 +637,13 @@ class ImageFileDirectory_v2(_IFDv2Base): val = (val,) return val - def __contains__(self, tag): + def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag, value): + def __setitem__(self, tag, value) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag, value, legacy_api): + def _setitem(self, tag, value, legacy_api) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -758,7 +759,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return data @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data): + def write_byte(self, data) -> bytes: if isinstance(data, IFDRational): data = int(data) if isinstance(data, int): @@ -772,7 +773,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return data.decode("latin-1", "replace") @_register_writer(2) - def write_string(self, value): + def write_string(self, value) -> bytes: # remerge of https://github.com/python-pillow/Pillow/pull/1416 if isinstance(value, int): value = str(value) @@ -790,7 +791,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) - def write_rational(self, *values): + def write_rational(self, *values) -> bytes: return b"".join( self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @@ -800,7 +801,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return data @_register_writer(7) - def write_undefined(self, value): + def write_undefined(self, value) -> bytes: if isinstance(value, IFDRational): value = int(value) if isinstance(value, int): @@ -817,13 +818,13 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) - def write_signed_rational(self, *values): + def write_signed_rational(self, *values) -> bytes: return b"".join( self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values ) - def _ensure_read(self, fp, size): + def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: ret = fp.read(size) if len(ret) != size: msg = ( @@ -977,7 +978,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return result - def save(self, fp): + def save(self, fp: IO[bytes]) -> int: if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1017,7 +1018,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._legacy_api = True @@ -1029,7 +1030,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): """Dictionary of tag types""" @classmethod - def from_v2(cls, original): + def from_v2(cls, original) -> ImageFileDirectory_v1: """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original @@ -1063,7 +1064,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd._tags_v2 = dict(self._tags_v2) return ifd - def __contains__(self, tag): + def __contains__(self, tag: object) -> bool: return tag in self._tags_v1 or tag in self._tagdata def __len__(self) -> int: @@ -1072,7 +1073,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag, value): + def __setitem__(self, tag, value) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) @@ -1122,7 +1123,7 @@ class TiffImageFile(ImageFile.ImageFile): self.tag_v2 = ImageFileDirectory_v2(ifh) # legacy IFD entries will be filled in later - self.ifd = None + self.ifd: ImageFileDirectory_v1 | None = None # setup frame pointers self.__first = self.__next = self.tag_v2.next @@ -1343,7 +1344,7 @@ class TiffImageFile(ImageFile.ImageFile): return Image.Image.load(self) - def _setup(self): + def _setup(self) -> None: """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: @@ -1537,13 +1538,13 @@ class TiffImageFile(ImageFile.ImageFile): # adjust stride width accordingly stride /= bps_count - a = (tile_rawmode, int(stride), 1) + args = (tile_rawmode, int(stride), 1) self.tile.append( ( self._compression, (x, y, min(x + w, xsize), min(y + h, ysize)), offset, - a, + args, ) ) x = x + w @@ -1938,7 +1939,7 @@ class AppendingTiffWriter: 521, # JPEGACTables } - def __init__(self, fn, new=False): + def __init__(self, fn, new: bool = False) -> None: if hasattr(fn, "read"): self.f = fn self.close_fp = False @@ -2015,7 +2016,7 @@ class AppendingTiffWriter: def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage - def seek(self, offset, whence=io.SEEK_SET): + def seek(self, offset: int, whence=io.SEEK_SET) -> int: if whence == os.SEEK_SET: offset += self.offsetOfNewPage diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 895d5616a..ec5c74900 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -24,8 +24,11 @@ and has been tested with a few sample files found using google. """ from __future__ import annotations +from typing import IO + from . import Image, ImageFile from ._binary import i32le as i32 +from ._typing import StrOrBytesPath class WalImageFile(ImageFile.ImageFile): @@ -58,7 +61,7 @@ class WalImageFile(ImageFile.ImageFile): return Image.Image.load(self) -def open(filename): +def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: """ Load texture from a Quake2 WAL texture file. From f5313db9ce5f800de29cb40af12c0722ecbe7173 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Sat, 13 Jul 2024 11:00:57 +0200 Subject: [PATCH 008/136] Add necessary PyMem_Free and fix PyDict_GetItemRef call --- src/_imagingft.c | 2 +- src/encode.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index ddcf28f97..eb04945f0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1243,8 +1243,8 @@ font_getvarnames(FontObject *self) { } } + PyMem_Free(list_names_filled); FT_Done_MM_Var(library, master); - return list_names; } diff --git a/src/encode.c b/src/encode.c index 620774f67..82aed7783 100644 --- a/src/encode.c +++ b/src/encode.c @@ -723,8 +723,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (!is_core_tag) { PyObject *tag_type; - if (PyDict_GetItemRef(types, key, &tag_type) == 0) { - PyErr_SetString(PyExc_KeyError, "unknown tag type"); + if (PyDict_GetItemRef(types, key, &tag_type) < 0) { + return NULL; // Exception has been already set } if (tag_type) { int type_int = PyLong_AsLong(tag_type); From 9c576d63c3e79964d12ebe07f43144db735cdd35 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Sat, 13 Jul 2024 12:24:02 +0200 Subject: [PATCH 009/136] Fix refcounts after porting to GetItemRef & better error checking --- src/_imaging.c | 5 +++++ src/_imagingft.c | 35 ++++++++++++++++++++++++++++------- src/encode.c | 16 ++++++++++++++-- src/path.c | 12 +++++++++--- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 03e10e547..ac6310a44 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2251,6 +2251,11 @@ _getcolors(ImagingObject *self, PyObject *args) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); + if (item == NULL) { + Py_DECREF(out); + free(items); + return NULL; + } PyList_SetItem(out, i, item); } } diff --git a/src/_imagingft.c b/src/_imagingft.c index eb04945f0..6c2885c61 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1205,9 +1205,15 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } int *list_names_filled = PyMem_Malloc(num_namedstyles * sizeof(int)); if (list_names_filled == NULL) { + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); return PyErr_NoMemory(); } @@ -1215,15 +1221,11 @@ font_getvarnames(FontObject *self) { list_names_filled[i] = 0; } - if (list_names == NULL) { - FT_Done_MM_Var(library, master); - return NULL; - } - name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { + PyMem_Free(list_names_filled); Py_DECREF(list_names); FT_Done_MM_Var(library, master); return geterror(error); @@ -1236,6 +1238,12 @@ font_getvarnames(FontObject *self) { if (master->namedstyle[j].strid == name.name_id) { list_name = Py_BuildValue("y#", name.string, name.string_len); + if (list_name == NULL) { + PyMem_Free(list_names_filled); + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); + return NULL; + } PyList_SetItem(list_names, j, list_name); list_names_filled[j] = 1; break; @@ -1301,9 +1309,15 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); + if (axis_name == NULL) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return NULL; + } PyDict_SetItemString( list_axis, "name", axis_name ? axis_name : Py_None); - Py_XDECREF(axis_name); + Py_DECREF(axis_name); break; } } @@ -1357,7 +1371,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { return PyErr_NoMemory(); } for (i = 0; i < num_coords; i++) { - item = PyList_GET_ITEM(axes, i); + item = PyList_GetItemRef(axes, i); + if (item == NULL) { + free(coords); + return NULL; + } + if (PyFloat_Check(item)) { coord = PyFloat_AS_DOUBLE(item); } else if (PyLong_Check(item)) { @@ -1365,10 +1384,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { } else if (PyNumber_Check(item)) { coord = PyFloat_AsDouble(item); } else { + Py_DECREF(item); free(coords); PyErr_SetString(PyExc_TypeError, "list must contain numbers"); return NULL; } + Py_DECREF(item); coords[i] = coord * 65536; } diff --git a/src/encode.c b/src/encode.c index 82aed7783..2c95b7ebc 100644 --- a/src/encode.c +++ b/src/encode.c @@ -673,10 +673,16 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { TRACE(("tags size: %d\n", (int)tags_size)); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); + if (item == NULL) { + return NULL; + } + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + Py_DECREF(item); PyErr_SetString(PyExc_ValueError, "Invalid tags list"); return NULL; } + Py_DECREF(item); } pos = 0; } @@ -705,10 +711,16 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); + if (item == NULL) { + return NULL; + } + // We already checked that tags is a 2-tuple list. - key = PyTuple_GetItem(item, 0); + key = PyTuple_GET_ITEM(item, 0); key_int = (int)PyLong_AsLong(key); - value = PyTuple_GetItem(item, 1); + value = PyTuple_GET_ITEM(item, 1); + Py_DECREF(item); + status = 0; is_core_tag = 0; is_var_length = 0; diff --git a/src/path.c b/src/path.c index 6bc90abed..f8a99eb5b 100644 --- a/src/path.c +++ b/src/path.c @@ -179,14 +179,21 @@ PyPath_Flatten(PyObject *data, double **pxy) { } \ free(xy); \ return -1; \ + } \ + if (decref) { \ + Py_DECREF(op); \ } /* Copy table to path array */ if (PyList_Check(data)) { for (i = 0; i < n; i++) { double x, y; - PyObject *op = PyList_GET_ITEM(data, i); - assign_item_to_array(op, 0); + PyObject *op = PyList_GetItemRef(data, i); + if (op == NULL) { + free(xy); + return -1; + } + assign_item_to_array(op, 1); } } else if (PyTuple_Check(data)) { for (i = 0; i < n; i++) { @@ -209,7 +216,6 @@ PyPath_Flatten(PyObject *data, double **pxy) { } } assign_item_to_array(op, 1); - Py_DECREF(op); } } From 8854e4677e0d5d07bfa3d8725e85b681f81a704a Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Sat, 13 Jul 2024 12:34:17 +0200 Subject: [PATCH 010/136] Add include --- src/path.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/path.c b/src/path.c index f8a99eb5b..bd6ad2259 100644 --- a/src/path.c +++ b/src/path.c @@ -26,6 +26,7 @@ */ #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include From 01529d8b0979597902d26fcf4c0dc4f3da2a667e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jul 2024 19:23:36 +1000 Subject: [PATCH 011/136] Added type hints --- Tests/test_imagewin.py | 3 ++ src/PIL/EpsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 84 ++++++++++++++++++++------------------ src/PIL/Image.py | 14 +++---- src/PIL/ImageFile.py | 6 ++- src/PIL/ImageQt.py | 11 +++-- src/PIL/ImageWin.py | 28 ++++++++----- src/PIL/IptcImagePlugin.py | 16 ++++---- src/PIL/JpegImagePlugin.py | 10 +++-- src/PIL/PngImagePlugin.py | 56 ++++++++++++++++--------- 10 files changed, 138 insertions(+), 92 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index b43c31b52..a836bb90b 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -57,6 +57,9 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) + with pytest.raises(ValueError): + ImageWin.Dib(mode) + def test_dib_paste(self) -> None: # Arrange im = hopper() diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 59bb8594d..7a73d1f69 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -65,7 +65,7 @@ def has_ghostscript() -> bool: return gs_binary is not False -def Ghostscript(tile, size, fp, scale=1, transparency=False): +def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 650f5e4f1..8be1bd316 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -25,7 +25,7 @@ from __future__ import annotations import warnings from io import BytesIO from math import ceil, log -from typing import IO +from typing import IO, NamedTuple from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 @@ -119,8 +119,22 @@ def _accept(prefix: bytes) -> bool: return prefix[:4] == _MAGIC +class IconHeader(NamedTuple): + width: int + height: int + nb_color: int + reserved: int + planes: int + bpp: int + size: int + offset: int + dim: tuple[int, int] + square: int + color_depth: int + + class IcoFile: - def __init__(self, buf) -> None: + def __init__(self, buf: IO[bytes]) -> None: """ Parse image from file-like object containing ico file data """ @@ -141,51 +155,44 @@ class IcoFile: for i in range(self.nb_items): s = buf.read(16) - icon_header = { - "width": s[0], - "height": s[1], - "nb_color": s[2], # No. of colors in image (0 if >=8bpp) - "reserved": s[3], - "planes": i16(s, 4), - "bpp": i16(s, 6), - "size": i32(s, 8), - "offset": i32(s, 12), - } - # See Wikipedia - for j in ("width", "height"): - if not icon_header[j]: - icon_header[j] = 256 + width = s[0] or 256 + height = s[1] or 256 - # See Wikipedia notes about color depth. - # We need this just to differ images with equal sizes - icon_header["color_depth"] = ( - icon_header["bpp"] - or ( - icon_header["nb_color"] != 0 - and ceil(log(icon_header["nb_color"], 2)) - ) - or 256 + # No. of colors in image (0 if >=8bpp) + nb_color = s[2] + bpp = i16(s, 6) + icon_header = IconHeader( + width=width, + height=height, + nb_color=nb_color, + reserved=s[3], + planes=i16(s, 4), + bpp=i16(s, 6), + size=i32(s, 8), + offset=i32(s, 12), + dim=(width, height), + square=width * height, + # See Wikipedia notes about color depth. + # We need this just to differ images with equal sizes + color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, ) - icon_header["dim"] = (icon_header["width"], icon_header["height"]) - icon_header["square"] = icon_header["width"] * icon_header["height"] - self.entry.append(icon_header) - self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) + self.entry = sorted(self.entry, key=lambda x: x.color_depth) # ICO images are usually squares - self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) + self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) def sizes(self) -> set[tuple[int, int]]: """ - Get a list of all available icon sizes and color depths. + Get a set of all available icon sizes and color depths. """ - return {(h["width"], h["height"]) for h in self.entry} + return {(h.width, h.height) for h in self.entry} def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: for i, h in enumerate(self.entry): - if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): + if size == h.dim and (bpp is False or bpp == h.color_depth): return i return 0 @@ -202,9 +209,9 @@ class IcoFile: header = self.entry[idx] - self.buf.seek(header["offset"]) + self.buf.seek(header.offset) data = self.buf.read(8) - self.buf.seek(header["offset"]) + self.buf.seek(header.offset) im: Image.Image if data[:8] == PngImagePlugin._MAGIC: @@ -222,8 +229,7 @@ class IcoFile: im.tile[0] = d, (0, 0) + im.size, o, a # figure out where AND mask image starts - bpp = header["bpp"] - if 32 == bpp: + if 32 == header.bpp: # 32-bit color depth icon image allows semitransparent areas # PIL's DIB format ignores transparency bits, recover them. # The DIB is packed in BGRX byte order where X is the alpha @@ -253,7 +259,7 @@ class IcoFile: # padded row size * height / bits per char total_bytes = int((w * im.size[1]) / 8) - and_mask_offset = header["offset"] + header["size"] - total_bytes + and_mask_offset = header.offset + header.size - total_bytes self.buf.seek(and_mask_offset) mask_data = self.buf.read(total_bytes) @@ -307,7 +313,7 @@ class IcoImageFile(ImageFile.ImageFile): def _open(self) -> None: self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() - self.size = self.ico.entry[0]["dim"] + self.size = self.ico.entry[0].dim self.load() @property diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 565abe71d..9d901e028 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3286,7 +3286,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromqimage(im): +def fromqimage(im) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt @@ -3296,7 +3296,7 @@ def fromqimage(im): return ImageQt.fromqimage(im) -def fromqpixmap(im): +def fromqpixmap(im) -> ImageFile.ImageFile: """Creates an image instance from a QPixmap image""" from . import ImageQt @@ -3867,7 +3867,7 @@ class Exif(_ExifBase): # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset, group=None): + def _get_ifd_dict(self, offset: int, group=None): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. @@ -3881,7 +3881,7 @@ class Exif(_ExifBase): info.load(self.fp) return self._fixup_dict(info) - def _get_head(self): + def _get_head(self) -> bytes: version = b"\x2B" if self.bigtiff else b"\x2A" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) @@ -4102,16 +4102,16 @@ class Exif(_ExifBase): keys.update(self._info) return len(keys) - def __getitem__(self, tag): + def __getitem__(self, tag: int): if self._info is not None and tag not in self._data and tag in self._info: self._data[tag] = self._fixup(self._info[tag]) del self._info[tag] return self._data[tag] - def __contains__(self, tag) -> bool: + def __contains__(self, tag: object) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag, value) -> None: + def __setitem__(self, tag: int, value) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 6b2953451..e4a7dba44 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -553,7 +553,9 @@ def _save(im, fp, tile, bufsize: int = 0) -> None: fp.flush() -def _encode_tile(im, fp, tile: list[_Tile], bufsize: int, fh, exc=None) -> None: +def _encode_tile( + im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None +) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: fp.seek(offset) @@ -653,7 +655,7 @@ class PyCodec: """ pass - def setfd(self, fd) -> None: + def setfd(self, fd: IO[bytes]) -> None: """ Called from ImageFile to set the Python file-like object diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 35a37760c..346fe49d3 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,14 @@ from __future__ import annotations import sys from io import BytesIO -from typing import Callable +from typing import TYPE_CHECKING, Callable from . import Image from ._util import is_path +if TYPE_CHECKING: + from . import ImageFile + qt_version: str | None qt_versions = [ ["6", "PyQt6"], @@ -90,11 +93,11 @@ def fromqimage(im): return Image.open(b) -def fromqpixmap(im): +def fromqpixmap(im) -> ImageFile.ImageFile: return fromqimage(im) -def align8to32(bytes, width, mode): +def align8to32(bytes: bytes, width: int, mode: str) -> bytes: """ converts each scanline of data from 8 bit to 32 bit aligned """ @@ -172,7 +175,7 @@ def _toqclass_helper(im): if qt_is_installed: class ImageQt(QImage): - def __init__(self, im): + def __init__(self, im) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage class. diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 978c5a9d1..4f9956087 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -70,11 +70,14 @@ class Dib: """ def __init__( - self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None + self, image: Image.Image | str, size: tuple[int, int] | None = None ) -> None: if isinstance(image, str): mode = image image = "" + if size is None: + msg = "If first argument is mode, size is required" + raise ValueError(msg) else: mode = image.mode size = image.size @@ -105,7 +108,12 @@ class Dib: result = self.image.expose(handle) return result - def draw(self, handle, dst, src=None): + def draw( + self, + handle, + dst: tuple[int, int, int, int], + src: tuple[int, int, int, int] | None = None, + ): """ Same as expose, but allows you to specify where to draw the image, and what part of it to draw. @@ -115,7 +123,7 @@ class Dib: the destination have different sizes, the image is resized as necessary. """ - if not src: + if src is None: src = (0, 0) + self.size if isinstance(handle, HWND): dc = self.image.getdc(handle) @@ -202,22 +210,22 @@ class Window: title, self.__dispatcher, width or 0, height or 0 ) - def __dispatcher(self, action, *args): + def __dispatcher(self, action: str, *args): return getattr(self, f"ui_handle_{action}")(*args) - def ui_handle_clear(self, dc, x0, y0, x1, y1): + def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None: pass - def ui_handle_damage(self, x0, y0, x1, y1): + def ui_handle_damage(self, x0, y0, x1, y1) -> None: pass def ui_handle_destroy(self) -> None: pass - def ui_handle_repair(self, dc, x0, y0, x1, y1): + def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: pass - def ui_handle_resize(self, width, height): + def ui_handle_resize(self, width, height) -> None: pass def mainloop(self) -> None: @@ -227,12 +235,12 @@ class Window: class ImageWindow(Window): """Create an image window which displays the given image.""" - def __init__(self, image, title="PIL"): + def __init__(self, image, title: str = "PIL") -> None: if not isinstance(image, Dib): image = Dib(image) self.image = image width, height = image.size super().__init__(title, width=width, height=height) - def ui_handle_repair(self, dc, x0, y0, x1, y1): + def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index a04616fbd..16a18ddfa 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations from collections.abc import Sequence from io import BytesIO +from typing import cast from . import Image, ImageFile from ._binary import i16be as i16 @@ -184,7 +185,7 @@ Image.register_open(IptcImageFile.format, IptcImageFile) Image.register_extension(IptcImageFile.format, ".iim") -def getiptcinfo(im): +def getiptcinfo(im: ImageFile.ImageFile): """ Get IPTC information from TIFF, JPEG, or IPTC file. @@ -221,16 +222,17 @@ def getiptcinfo(im): class FakeImage: pass - im = FakeImage() - im.__class__ = IptcImageFile + fake_im = FakeImage() + fake_im.__class__ = IptcImageFile # type: ignore[assignment] + iptc_im = cast(IptcImageFile, fake_im) # parse the IPTC information chunk - im.info = {} - im.fp = BytesIO(data) + iptc_im.info = {} + iptc_im.fp = BytesIO(data) try: - im._open() + iptc_im._open() except (IndexError, KeyError): pass # expected failure - return im.info + return iptc_im.info diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 54f756014..af24faa5d 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -685,7 +685,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: raise ValueError(msg) subsampling = get_sampling(im) - def validate_qtables(qtables): + def validate_qtables( + qtables: ( + str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None + ) + ) -> list[list[int]] | None: if qtables is None: return qtables if isinstance(qtables, str): @@ -715,12 +719,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if len(table) != 64: msg = "Invalid quantization table" raise TypeError(msg) - table = array.array("H", table) + table_array = array.array("H", table) except TypeError as e: msg = "Invalid quantization table" raise ValueError(msg) from e else: - qtables[idx] = list(table) + qtables[idx] = list(table_array) return qtables if qtables == "keep": diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 6990b6d05..247f908ed 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,7 +39,7 @@ import struct import warnings import zlib from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NoReturn +from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -1126,7 +1126,21 @@ class _fdat: self.seq_num += 1 -def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images): +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, + fp: IO[bytes], + chunk, + mode: str, + rawmode: str, + default_image: Image.Image | None, + append_images: list[Image.Image], +) -> Image.Image | None: duration = im.encoderinfo.get("duration") loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) @@ -1137,7 +1151,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i else: chain = itertools.chain([im], append_images) - im_frames = [] + im_frames: list[_Frame] = [] frame_count = 0 for im_seq in chain: for im_frame in ImageSequence.Iterator(im_seq): @@ -1158,24 +1172,24 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i if im_frames: previous = im_frames[-1] - prev_disposal = previous["encoderinfo"].get("disposal") - prev_blend = previous["encoderinfo"].get("blend") + prev_disposal = previous.encoderinfo.get("disposal") + prev_blend = previous.encoderinfo.get("blend") if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: prev_disposal = Disposal.OP_BACKGROUND if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous["im"].copy() + base_im = previous.im.copy() dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) - bbox = previous["bbox"] + bbox = previous.bbox if bbox: dispose = dispose.crop(bbox) else: bbox = (0, 0) + im.size base_im.paste(dispose, bbox) elif prev_disposal == Disposal.OP_PREVIOUS: - base_im = im_frames[-2]["im"] + base_im = im_frames[-2].im else: - base_im = previous["im"] + base_im = previous.im delta = ImageChops.subtract_modulo( im_frame.convert("RGBA"), base_im.convert("RGBA") ) @@ -1186,14 +1200,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i and prev_blend == encoderinfo.get("blend") and "duration" in encoderinfo ): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous.encoderinfo["duration"] += encoderinfo["duration"] continue else: bbox = None - im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) + im_frames.append(_Frame(im_frame, bbox, encoderinfo)) if len(im_frames) == 1 and not default_image: - return im_frames[0]["im"] + return im_frames[0].im # animation control chunk( @@ -1211,14 +1225,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i seq_num = 0 for frame, frame_data in enumerate(im_frames): - im_frame = frame_data["im"] - if not frame_data["bbox"]: + im_frame = frame_data.im + if not frame_data.bbox: bbox = (0, 0) + im_frame.size else: - bbox = frame_data["bbox"] + bbox = frame_data.bbox im_frame = im_frame.crop(bbox) size = im_frame.size - encoderinfo = frame_data["encoderinfo"] + encoderinfo = frame_data.encoderinfo frame_duration = int(round(encoderinfo.get("duration", 0))) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) @@ -1253,6 +1267,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num + return None def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -1437,12 +1452,15 @@ def _save( exif = exif[6:] chunk(fp, b"eXIf", exif) + single_im: Image.Image | None = im if save_all: - im = _write_multiple_frames( + single_im = _write_multiple_frames( im, fp, chunk, mode, rawmode, default_image, append_images ) - if im: - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + if single_im: + ImageFile._save( + single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] + ) if info: for info_chunk in info.chunks: From 76e5e12f9855e59eaf393c40e68f4e737f1175fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jul 2024 20:48:39 +1000 Subject: [PATCH 012/136] Simplified code --- src/_imagingft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 6c2885c61..c6d20fe45 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1315,8 +1315,7 @@ font_getvaraxes(FontObject *self) { FT_Done_MM_Var(library, master); return NULL; } - PyDict_SetItemString( - list_axis, "name", axis_name ? axis_name : Py_None); + PyDict_SetItemString(list_axis, "name", axis_name); Py_DECREF(axis_name); break; } From 3eeef83517b12d4e7af826b3aa0f6ebc82faf1fa Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:40:17 +1000 Subject: [PATCH 013/136] Updated condition Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/IcoImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 8be1bd316..c891024f5 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -229,7 +229,7 @@ class IcoFile: im.tile[0] = d, (0, 0) + im.size, o, a # figure out where AND mask image starts - if 32 == header.bpp: + if header.bpp == 32: # 32-bit color depth icon image allows semitransparent areas # PIL's DIB format ignores transparency bits, recover them. # The DIB is packed in BGRX byte order where X is the alpha From e89be771452731e1387f2a9c2fcb47a6d8e3350d Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 10:21:24 +0200 Subject: [PATCH 014/136] Upload wheels to scientific-python-nightly-wheels index --- .github/workflows/wheels.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e32f14529..3013f0c37 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,6 +1,14 @@ name: Wheels on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + - cron: "42 1 * * 0,3" push: paths: - ".ci/requirements-cibw.txt" @@ -140,6 +148,13 @@ jobs: name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl + - name: Upload wheels to scientific-python-nightly-wheels + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + with: + artifacts_path: ./wheelhouse + anaconda_nightly_upload_token: ${{ secrets.PILLOW_NIGHTLY_UPLOAD_TOKEN }} + windows: name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest @@ -226,6 +241,13 @@ jobs: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* + - name: Upload wheels to scientific-python-nightly-wheels + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + with: + artifacts_path: ./wheelhouse + anaconda_nightly_upload_token: ${{ secrets.PILLOW_NIGHTLY_UPLOAD_TOKEN }} + sdist: runs-on: ubuntu-latest steps: From 784a87449063cb2ee2c65988574e83399b018f60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jul 2024 18:38:50 +1000 Subject: [PATCH 015/136] Trim whitespace --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3013f0c37..191eaffeb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -149,7 +149,7 @@ jobs: path: ./wheelhouse/*.whl - name: Upload wheels to scientific-python-nightly-wheels - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 with: artifacts_path: ./wheelhouse @@ -242,7 +242,7 @@ jobs: path: winbuild\build\bin\fribidi* - name: Upload wheels to scientific-python-nightly-wheels - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 with: artifacts_path: ./wheelhouse From 68c3542d096e314921d25539bb65e24e3760a1f3 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 10:42:18 +0200 Subject: [PATCH 016/136] Rename secret --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 191eaffeb..4501802cb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -153,7 +153,7 @@ jobs: uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 with: artifacts_path: ./wheelhouse - anaconda_nightly_upload_token: ${{ secrets.PILLOW_NIGHTLY_UPLOAD_TOKEN }} + anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} windows: name: Windows ${{ matrix.cibw_arch }} @@ -246,7 +246,7 @@ jobs: uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 with: artifacts_path: ./wheelhouse - anaconda_nightly_upload_token: ${{ secrets.PILLOW_NIGHTLY_UPLOAD_TOKEN }} + anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} sdist: runs-on: ubuntu-latest From d83c7b38c42952dbd3fcab1807732363d6e64607 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 16 Jul 2024 20:18:25 +1000 Subject: [PATCH 017/136] Skip other jobs on schedule (#1) Co-authored-by: Andrew Murray --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4501802cb..fa1825e45 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,6 +41,7 @@ env: jobs: build-1-QEMU-emulated-wheels: + if: github.event_name != 'schedule' name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: @@ -249,6 +250,7 @@ jobs: anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} sdist: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 87b23d0207fb5f703e6c143e3ca8ed7d174930ff Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 19 May 2024 01:54:21 -0500 Subject: [PATCH 018/136] change AlignAfterOpenBracket in .clang-format to BlockIndent to match the Python code --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 3199e330b..143dde82c 100644 --- a/.clang-format +++ b/.clang-format @@ -3,7 +3,7 @@ BasedOnStyle: Google AlwaysBreakAfterReturnType: All AllowShortIfStatementsOnASingleLine: false -AlignAfterOpenBracket: AlwaysBreak +AlignAfterOpenBracket: BlockIndent BinPackArguments: false BinPackParameters: false BreakBeforeBraces: Attach From 2973b041c76482da213fd93e151c542577178fe3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:58:00 +0000 Subject: [PATCH 019/136] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Tk/_tkmini.h | 9 +- src/Tk/tkImaging.c | 24 +++-- src/_imaging.c | 175 ++++++++++++++++++++------------- src/_imagingcms.c | 79 ++++++++++----- src/_imagingft.c | 91 +++++++++++------ src/_imagingmath.c | 3 +- src/_imagingmorph.c | 3 +- src/_webp.c | 45 ++++++--- src/decode.c | 6 +- src/display.c | 50 ++++++---- src/encode.c | 108 ++++++++++++-------- src/libImaging/Access.c | 3 +- src/libImaging/BcnDecode.c | 39 +++++--- src/libImaging/BoxBlur.c | 18 ++-- src/libImaging/Chops.c | 9 +- src/libImaging/ColorLUT.c | 30 ++++-- src/libImaging/Convert.c | 34 +++---- src/libImaging/ConvertYCbCr.c | 36 ++++--- src/libImaging/Dib.c | 12 ++- src/libImaging/Draw.c | 134 ++++++++++++++----------- src/libImaging/Fill.c | 4 +- src/libImaging/Filter.c | 6 +- src/libImaging/Geometry.c | 34 ++++--- src/libImaging/GetBBox.c | 24 ++--- src/libImaging/GifEncode.c | 9 +- src/libImaging/HexDecode.c | 3 +- src/libImaging/Imaging.h | 60 +++++++---- src/libImaging/Jpeg2KDecode.c | 57 +++++------ src/libImaging/Jpeg2KEncode.c | 14 +-- src/libImaging/JpegDecode.c | 8 +- src/libImaging/JpegEncode.c | 18 ++-- src/libImaging/PackDecode.c | 6 +- src/libImaging/Paste.c | 54 +++++----- src/libImaging/PcxDecode.c | 6 +- src/libImaging/PcxEncode.c | 3 +- src/libImaging/Point.c | 4 +- src/libImaging/Quant.c | 105 +++++++++++++------- src/libImaging/QuantHash.c | 6 +- src/libImaging/QuantHash.h | 12 +-- src/libImaging/QuantOctree.c | 40 +++++--- src/libImaging/QuantPngQuant.c | 3 +- src/libImaging/QuantPngQuant.h | 3 +- src/libImaging/RankFilter.c | 3 +- src/libImaging/RawDecode.c | 3 +- src/libImaging/RawEncode.c | 3 +- src/libImaging/Reduce.c | 105 +++++++++++++------- src/libImaging/Resample.c | 36 ++++--- src/libImaging/SgiRleDecode.c | 9 +- src/libImaging/Storage.c | 8 +- src/libImaging/SunRleDecode.c | 3 +- src/libImaging/TgaRleDecode.c | 3 +- src/libImaging/TgaRleEncode.c | 6 +- src/libImaging/TiffDecode.c | 112 ++++++++++++++------- src/libImaging/TiffDecode.h | 3 +- src/libImaging/Unpack.c | 24 +++-- src/libImaging/UnpackYCC.c | 15 ++- src/libImaging/UnsharpMask.c | 3 +- src/libImaging/XbmEncode.c | 3 +- src/libImaging/ZipDecode.c | 9 +- src/libImaging/ZipEncode.c | 9 +- src/map.c | 3 +- src/path.c | 9 +- 62 files changed, 1088 insertions(+), 668 deletions(-) diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 68247bc47..a260fa0d1 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -80,7 +80,8 @@ typedef struct Tcl_Command_ *Tcl_Command; typedef void *ClientData; typedef int(Tcl_CmdProc)( - ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]); + ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[] +); typedef void(Tcl_CmdDeleteProc)(ClientData clientData); /* Typedefs derived from function signatures in Tcl header */ @@ -90,7 +91,8 @@ typedef Tcl_Command (*Tcl_CreateCommand_t)( const char *cmdName, Tcl_CmdProc *proc, ClientData clientData, - Tcl_CmdDeleteProc *deleteProc); + Tcl_CmdDeleteProc *deleteProc +); /* Tcl_AppendResult */ typedef void (*Tcl_AppendResult_t)(Tcl_Interp *interp, ...); @@ -127,7 +129,8 @@ typedef int (*Tk_PhotoPutBlock_t)( int y, int width, int height, - int compRule); + int compRule +); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ef1c00a94..727ee6bed 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -73,14 +73,16 @@ ImagingFind(const char *name) { static int PyImagingPhotoPut( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv +) { Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; if (argc != 3) { TCL_APPEND_RESULT( - interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); + interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL + ); return TCL_ERROR; } @@ -128,14 +130,16 @@ PyImagingPhotoPut( block.pixelPtr = (unsigned char *)im->block; TK_PHOTO_PUT_BLOCK( - interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); + interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET + ); return TCL_OK; } static int PyImagingPhotoGet( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv +) { Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; @@ -143,7 +147,8 @@ PyImagingPhotoGet( if (argc != 3) { TCL_APPEND_RESULT( - interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL); + interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL + ); return TCL_ERROR; } @@ -183,13 +188,15 @@ TkImaging_Init(Tcl_Interp *interp) { "PyImagingPhoto", PyImagingPhotoPut, (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); + (Tcl_CmdDeleteProc *)NULL + ); TCL_CREATE_COMMAND( interp, "PyImagingPhotoGet", PyImagingPhotoGet, (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); + (Tcl_CmdDeleteProc *)NULL + ); } /* @@ -394,7 +401,8 @@ _func_loader(void *lib) { } return ( (TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == - NULL); + NULL + ); } int diff --git a/src/_imaging.c b/src/_imaging.c index ac6310a44..5c2f7b4b6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -290,7 +290,8 @@ ImagingError_ModeError(void) { void * ImagingError_ValueError(const char *message) { PyErr_SetString( - PyExc_ValueError, (message) ? (char *)message : "unrecognized argument value"); + PyExc_ValueError, (message) ? (char *)message : "unrecognized argument value" + ); return NULL; } @@ -467,7 +468,8 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); case 4: return Py_BuildValue( - "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3]); + "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3] + ); } break; case IMAGING_TYPE_INT32: @@ -518,7 +520,8 @@ getink(PyObject *color, Imaging im, char *ink) { rIsInt = 1; } else if (im->bands == 1) { PyErr_SetString( - PyExc_TypeError, "color must be int or single-element tuple"); + PyExc_TypeError, "color must be int or single-element tuple" + ); return NULL; } else if (tupleSize == -1) { PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); @@ -534,8 +537,8 @@ getink(PyObject *color, Imaging im, char *ink) { if (rIsInt != 1) { if (tupleSize != 1) { PyErr_SetString( - PyExc_TypeError, - "color must be int or single-element tuple"); + PyExc_TypeError, "color must be int or single-element tuple" + ); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { return NULL; @@ -556,7 +559,8 @@ getink(PyObject *color, Imaging im, char *ink) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString( PyExc_TypeError, - "color must be int, or tuple of one or two elements"); + "color must be int, or tuple of one or two elements" + ); return NULL; } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; @@ -567,7 +571,8 @@ getink(PyObject *color, Imaging im, char *ink) { PyErr_SetString( PyExc_TypeError, "color must be int, or tuple of one, three or four " - "elements"); + "elements" + ); return NULL; } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; @@ -608,7 +613,8 @@ getink(PyObject *color, Imaging im, char *ink) { } else if (tupleSize != 3) { PyErr_SetString( PyExc_TypeError, - "color must be int, or tuple of one or three elements"); + "color must be int, or tuple of one or three elements" + ); return NULL; } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; @@ -733,7 +739,8 @@ _alpha_composite(ImagingObject *self, PyObject *args) { ImagingObject *imagep2; if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2 + )) { return NULL; } @@ -748,7 +755,8 @@ _blend(ImagingObject *self, PyObject *args) { alpha = 0.5; if (!PyArg_ParseTuple( - args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha)) { + args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha + )) { return NULL; } @@ -827,7 +835,8 @@ _prepare_lut_table(PyObject *table, Py_ssize_t table_size) { break; case TYPE_FLOAT32: memcpy( - &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); + &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32) + ); break; case TYPE_DOUBLE: memcpy(&dtmp, ((char *)table_data) + i * sizeof(dtmp), sizeof(dtmp)); @@ -878,7 +887,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { &size1D, &size2D, &size3D, - &table)) { + &table + )) { return NULL; } @@ -896,7 +906,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (2 > size1D || size1D > 65 || 2 > size2D || size2D > 65 || 2 > size3D || size3D > 65) { PyErr_SetString( - PyExc_ValueError, "Table size in any dimension should be from 2 to 65"); + PyExc_ValueError, "Table size in any dimension should be from 2 to 65" + ); return NULL; } @@ -913,13 +924,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { } if (!ImagingColorLUT3D_linear( - imOut, - self->image, - table_channels, - size1D, - size2D, - size3D, - prepared_table)) { + imOut, self->image, table_channels, size1D, size2D, size3D, prepared_table + )) { free(prepared_table); ImagingDelete(imOut); return NULL; @@ -943,7 +949,8 @@ _convert(ImagingObject *self, PyObject *args) { if (!PyImaging_Check(paletteimage)) { PyObject_Print((PyObject *)paletteimage, stderr, 0); PyErr_SetString( - PyExc_ValueError, "palette argument must be image with mode 'P'"); + PyExc_ValueError, "palette argument must be image with mode 'P'" + ); return NULL; } if (paletteimage->image->palette == NULL) { @@ -953,7 +960,8 @@ _convert(ImagingObject *self, PyObject *args) { } return PyImagingNew(ImagingConvert( - self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); + self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither + )); } static PyObject * @@ -961,7 +969,8 @@ _convert2(ImagingObject *self, PyObject *args) { ImagingObject *imagep1; ImagingObject *imagep2; if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2 + )) { return NULL; } @@ -994,7 +1003,8 @@ _convert_matrix(ImagingObject *self, PyObject *args) { m + 8, m + 9, m + 10, - m + 11)) { + m + 11 + )) { return NULL; } } @@ -1055,7 +1065,8 @@ _filter(ImagingObject *self, PyObject *args) { float divisor, offset; PyObject *kernel = NULL; if (!PyArg_ParseTuple( - args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel)) { + args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel + )) { return NULL; } @@ -1138,7 +1149,8 @@ _getpalette(ImagingObject *self, PyObject *args) { } pack( - (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize); + (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize + ); return palette; } @@ -1232,7 +1244,8 @@ union hist_extrema { static union hist_extrema * parse_histogram_extremap( - ImagingObject *self, PyObject *extremap, union hist_extrema *ep) { + ImagingObject *self, PyObject *extremap, union hist_extrema *ep +) { int i0, i1; double f0, f1; @@ -1392,7 +1405,8 @@ _paste(ImagingObject *self, PyObject *args) { int x0, y0, x1, y1; ImagingObject *maskp = NULL; if (!PyArg_ParseTuple( - args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp)) { + args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp + )) { return NULL; } @@ -1404,14 +1418,16 @@ _paste(ImagingObject *self, PyObject *args) { x0, y0, x1, - y1); + y1 + ); } else { if (!getink(source, self->image, ink)) { return NULL; } status = ImagingFill2( - self->image, ink, (maskp) ? maskp->image : NULL, x0, y0, x1, y1); + self->image, ink, (maskp) ? maskp->image : NULL, x0, y0, x1, y1 + ); } if (status < 0) { @@ -1729,7 +1745,8 @@ _putpalette(ImagingObject *self, PyObject *args) { UINT8 *palette; Py_ssize_t palettesize; if (!PyArg_ParseTuple( - args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize)) { + args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize + )) { return NULL; } @@ -1887,7 +1904,8 @@ _resize(ImagingObject *self, PyObject *args) { &box[0], &box[1], &box[2], - &box[3])) { + &box[3] + )) { return NULL; } @@ -1923,7 +1941,8 @@ _resize(ImagingObject *self, PyObject *args) { imOut = ImagingNewDirty(imIn->mode, xsize, ysize); imOut = ImagingTransform( - imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); + imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1 + ); } else { imOut = ImagingResample(imIn, xsize, ysize, filter, box); } @@ -1944,14 +1963,8 @@ _reduce(ImagingObject *self, PyObject *args) { box[3] = imIn->ysize; if (!PyArg_ParseTuple( - args, - "(ii)|(iiii)", - &xscale, - &yscale, - &box[0], - &box[1], - &box[2], - &box[3])) { + args, "(ii)|(iiii)", &xscale, &yscale, &box[0], &box[1], &box[2], &box[3] + )) { return NULL; } @@ -2053,7 +2066,8 @@ _transform(ImagingObject *self, PyObject *args) { &method, &data, &filter, - &fill)) { + &fill + )) { return NULL; } @@ -2077,7 +2091,8 @@ _transform(ImagingObject *self, PyObject *args) { } imOut = ImagingTransform( - self->image, imagep->image, method, x0, y0, x1, y1, a, filter, fill); + self->image, imagep->image, method, x0, y0, x1, y1, a, filter, fill + ); free(a); @@ -2250,7 +2265,8 @@ _getcolors(ImagingObject *self, PyObject *args) { for (i = 0; i < colors; i++) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( - "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); + "iN", v->count, getpixel(self->image, self->access, v->x, v->y) + ); if (item == NULL) { Py_DECREF(out); free(items); @@ -2316,14 +2332,16 @@ _getprojection(ImagingObject *self) { } ImagingGetProjection( - self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); + self->image, (unsigned char *)xprofile, (unsigned char *)yprofile + ); result = Py_BuildValue( "y#y#", xprofile, (Py_ssize_t)self->image->xsize, yprofile, - (Py_ssize_t)self->image->ysize); + (Py_ssize_t)self->image->ysize + ); free(xprofile); free(yprofile); @@ -2397,7 +2415,8 @@ _merge(PyObject *self, PyObject *args) { &Imaging_Type, &band2, &Imaging_Type, - &band3)) { + &band3 + )) { return NULL; } @@ -2643,7 +2662,8 @@ _font_new(PyObject *self_, PyObject *args) { unsigned char *glyphdata; Py_ssize_t glyphdata_length; if (!PyArg_ParseTuple( - args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) { + args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length + )) { return NULL; } @@ -2801,7 +2821,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { if (i == 0 || text[i] != text[i - 1]) { ImagingDelete(bitmap); bitmap = ImagingCrop( - self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1 + ); if (!bitmap) { goto failed; } @@ -2813,7 +2834,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dx0 + x, glyph->dy0 + b, glyph->dx1 + x, - glyph->dy1 + b); + glyph->dy1 + b + ); if (status < 0) { goto failed; } @@ -2952,7 +2974,8 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { end, &ink, width, - self->blend); + self->blend + ); free(xy); @@ -2982,13 +3005,15 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { } if (n != 1) { PyErr_SetString( - PyExc_TypeError, "coordinate list must contain exactly 1 coordinate"); + PyExc_TypeError, "coordinate list must contain exactly 1 coordinate" + ); free(xy); return NULL; } n = ImagingDrawBitmap( - self->image->image, (int)xy[0], (int)xy[1], bitmap->image, &ink, self->blend); + self->image->image, (int)xy[0], (int)xy[1], bitmap->image, &ink, self->blend + ); free(xy); @@ -3044,7 +3069,8 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3098,7 +3124,8 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3138,14 +3165,16 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[2], (int)p[3], &ink, - self->blend) < 0) { + self->blend + ) < 0) { free(xy); return NULL; } } if (p) { /* draw last point */ ImagingDrawPoint( - self->image->image, (int)p[2], (int)p[3], &ink, self->blend); + self->image->image, (int)p[2], (int)p[3], &ink, self->blend + ); } } else { for (i = 0; i < n - 1; i++) { @@ -3158,7 +3187,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend) < 0) { + self->blend + ) < 0) { free(xy); return NULL; } @@ -3190,7 +3220,8 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { for (i = 0; i < n; i++) { double *p = &xy[i + i]; if (ImagingDrawPoint( - self->image->image, (int)p[0], (int)p[1], &ink, self->blend) < 0) { + self->image->image, (int)p[0], (int)p[1], &ink, self->blend + ) < 0) { free(xy); return NULL; } @@ -3279,7 +3310,8 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3311,7 +3343,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { } if (n < 2) { PyErr_SetString( - PyExc_TypeError, "coordinate list must contain at least 2 coordinates"); + PyExc_TypeError, "coordinate list must contain at least 2 coordinates" + ); free(xy); return NULL; } @@ -3384,7 +3417,8 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3524,7 +3558,8 @@ _effect_mandelbrot(ImagingObject *self, PyObject *args) { &extent[1], &extent[2], &extent[3], - &quality)) { + &quality + )) { return NULL; } @@ -3752,7 +3787,8 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { "image32", self->image->image32, "image", - self->image->image); + self->image->image + ); } static struct PyGetSetDef getsetters[] = { @@ -3762,7 +3798,8 @@ static struct PyGetSetDef getsetters[] = { {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, - {NULL}}; + {NULL} +}; /* basic sequence semantics */ @@ -4071,9 +4108,8 @@ _set_blocks_max(PyObject *self, PyObject *args) { if (blocks_max < 0) { PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); return NULL; - } else if ( - (unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + } else if ((unsigned long)blocks_max > + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } @@ -4428,7 +4464,8 @@ setup_module(PyObject *m) { PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString( - d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None + ); Py_XDECREF(pillow_version); return 0; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 628662b30..bafe787a7 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -331,7 +331,8 @@ pyCMScopyAux(cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) { memcpy( pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, - channelSize); + channelSize + ); } } } @@ -373,7 +374,8 @@ _buildTransform( char *sInMode, char *sOutMode, int iRenderingIntent, - cmsUInt32Number cmsFLAGS) { + cmsUInt32Number cmsFLAGS +) { cmsHTRANSFORM hTransform; Py_BEGIN_ALLOW_THREADS @@ -385,7 +387,8 @@ _buildTransform( hOutputProfile, findLCMStype(sOutMode), iRenderingIntent, - cmsFLAGS); + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -405,7 +408,8 @@ _buildProofTransform( char *sOutMode, int iRenderingIntent, int iProofIntent, - cmsUInt32Number cmsFLAGS) { + cmsUInt32Number cmsFLAGS +) { cmsHTRANSFORM hTransform; Py_BEGIN_ALLOW_THREADS @@ -419,7 +423,8 @@ _buildProofTransform( hProofProfile, iRenderingIntent, iProofIntent, - cmsFLAGS); + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -454,7 +459,8 @@ buildTransform(PyObject *self, PyObject *args) { &sInMode, &sOutMode, &iRenderingIntent, - &cmsFLAGS)) { + &cmsFLAGS + )) { return NULL; } @@ -464,7 +470,8 @@ buildTransform(PyObject *self, PyObject *args) { sInMode, sOutMode, iRenderingIntent, - cmsFLAGS); + cmsFLAGS + ); if (!transform) { return NULL; @@ -499,7 +506,8 @@ buildProofTransform(PyObject *self, PyObject *args) { &sOutMode, &iRenderingIntent, &iProofIntent, - &cmsFLAGS)) { + &cmsFLAGS + )) { return NULL; } @@ -511,7 +519,8 @@ buildProofTransform(PyObject *self, PyObject *args) { sOutMode, iRenderingIntent, iProofIntent, - cmsFLAGS); + cmsFLAGS + ); if (!transform) { return NULL; @@ -563,7 +572,8 @@ createProfile(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_ValueError, "ERROR: Could not calculate white point from color temperature " - "provided, must be float in degrees Kelvin"); + "provided, must be float in degrees Kelvin" + ); return NULL; } hProfile = cmsCreateLab2Profile(&whitePoint); @@ -624,7 +634,8 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { HANDLE handle = 0; int is_dc = 0; if (!PyArg_ParseTuple( - args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { + args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc + )) { return NULL; } @@ -729,7 +740,8 @@ _xyz_py(cmsCIEXYZ *XYZ) { cmsCIExyY xyY; cmsXYZ2xyY(&xyY, XYZ); return Py_BuildValue( - "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); + "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y + ); } static PyObject * @@ -758,7 +770,8 @@ _xyz3_py(cmsCIEXYZ *XYZ) { xyY[1].Y, xyY[2].x, xyY[2].y, - xyY[2].Y); + xyY[2].Y + ); } static PyObject * @@ -809,7 +822,8 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { triple->Green.Y, triple->Blue.x, triple->Blue.y, - triple->Blue.Y); + triple->Blue.Y + ); } static PyObject * @@ -873,7 +887,8 @@ _calculate_rgb_primaries(CmsProfileObject *self, cmsCIEXYZTRIPLE *result) { hXYZ, TYPE_XYZ_DBL, INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE + ); cmsCloseProfile(hXYZ); if (hTransform == NULL) { return 0; @@ -889,7 +904,8 @@ _check_intent( int clut, cmsHPROFILE hProfile, cmsUInt32Number Intent, - cmsUInt32Number UsedDirection) { + cmsUInt32Number UsedDirection +) { if (clut) { return cmsIsCLUT(hProfile, Intent, UsedDirection); } else { @@ -934,7 +950,8 @@ _is_intent_supported(CmsProfileObject *self, int clut) { _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True - : Py_False); + : Py_False + ); if (id == NULL || entry == NULL) { Py_XDECREF(id); Py_XDECREF(entry); @@ -968,7 +985,8 @@ static PyMethodDef pyCMSdll_methods[] = { {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, #endif - {NULL, NULL}}; + {NULL, NULL} +}; static struct PyMethodDef cms_profile_methods[] = { {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, @@ -1028,7 +1046,8 @@ cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { } return PyDateTime_FromDateAndTime( - 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0); + 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0 + ); } static PyObject * @@ -1106,13 +1125,15 @@ cms_profile_getattr_colorimetric_intent(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_perceptual_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); } static PyObject * cms_profile_getattr_saturation_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); } @@ -1145,7 +1166,8 @@ cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_media_white_point_temperature( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { cmsCIEXYZ *XYZ; cmsCIExyY xyY; cmsFloat64Number tempK; @@ -1329,7 +1351,8 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos "flare", mc->Flare, "illuminant_type", - _illu_map(mc->IlluminantType)); + _illu_map(mc->IlluminantType) + ); } static PyObject * @@ -1359,7 +1382,8 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, "illuminant_type", - _illu_map(vc->IlluminantType)); + _illu_map(vc->IlluminantType) + ); } static struct PyGetSetDef cms_profile_getsetters[] = { @@ -1407,11 +1431,12 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, {"clut", (getter)cms_profile_getattr_is_clut}, - {"icc_measurement_condition", - (getter)cms_profile_getattr_icc_measurement_condition}, + {"icc_measurement_condition", (getter)cms_profile_getattr_icc_measurement_condition + }, {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, - {NULL}}; + {NULL} +}; static PyTypeObject CmsProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsProfile", /*tp_name*/ diff --git a/src/_imagingft.c b/src/_imagingft.c index c6d20fe45..da03e3ba9 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -126,7 +126,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { unsigned char *font_bytes; Py_ssize_t font_bytes_size = 0; static char *kwlist[] = { - "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL}; + "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL + }; if (!library) { PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); @@ -148,7 +149,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &encoding, &font_bytes, &font_bytes_size, - &layout_engine)) { + &layout_engine + )) { PyConfig_Clear(&config); return NULL; } @@ -166,7 +168,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &encoding, &font_bytes, &font_bytes_size, - &layout_engine)) { + &layout_engine + )) { return NULL; } #endif @@ -199,7 +202,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { (FT_Byte *)self->font_bytes, font_bytes_size, index, - &self->face); + &self->face + ); } } @@ -243,7 +247,8 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info) { + GlyphInfo **glyph_info +) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -297,13 +302,14 @@ text_layout_raqm( #if !defined(RAQM_VERSION_ATLEAST) /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ PyErr_SetString( - PyExc_ValueError, - "libraqm 0.7 or greater required for 'ttb' direction"); + PyExc_ValueError, "libraqm 0.7 or greater required for 'ttb' direction" + ); goto failed; #endif } else { PyErr_SetString( - PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); + PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'" + ); goto failed; } } @@ -399,7 +405,8 @@ text_layout_fallback( const char *lang, GlyphInfo **glyph_info, int mask, - int color) { + int color +) { int error, load_flags, i; char *buffer = NULL; FT_ULong ch; @@ -412,7 +419,8 @@ text_layout_fallback( PyErr_SetString( PyExc_KeyError, "setting text direction, language or font features is not supported " - "without libraqm"); + "without libraqm" + ); } if (PyUnicode_Check(string)) { @@ -459,7 +467,8 @@ text_layout_fallback( last_index, (*glyph_info)[i].index, ft_kerning_default, - &delta) == 0) { + &delta + ) == 0) { (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); } @@ -483,7 +492,8 @@ text_layout( const char *lang, GlyphInfo **glyph_info, int mask, - int color) { + int color +) { size_t count; #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { @@ -492,7 +502,8 @@ text_layout( #endif { count = text_layout_fallback( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info, mask, color + ); } return count; } @@ -514,7 +525,8 @@ font_getlength(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { + args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang + )) { return NULL; } @@ -556,7 +568,8 @@ bounding_box_and_anchors( int *width, int *height, int *x_offset, - int *y_offset) { + int *y_offset +) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ @@ -661,7 +674,8 @@ bounding_box_and_anchors( case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( (face->size->metrics.ascender + face->size->metrics.descender) / - 2); + 2 + ); break; case 's': // horizontal baseline y_anchor = 0; @@ -741,7 +755,8 @@ font_getsize(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor + )) { return NULL; } @@ -773,7 +788,8 @@ font_getsize(FontObject *self, PyObject *args) { &width, &height, &x_offset, - &y_offset); + &y_offset + ); if (glyph_info) { PyMem_Free(glyph_info); glyph_info = NULL; @@ -842,7 +858,8 @@ font_render(FontObject *self, PyObject *args) { &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start + )) { return NULL; } @@ -889,7 +906,8 @@ font_render(FontObject *self, PyObject *args) { &width, &height, &x_offset, - &y_offset); + &y_offset + ); if (error) { PyMem_Del(glyph_info); return NULL; @@ -929,7 +947,8 @@ font_render(FontObject *self, PyObject *args) { (FT_Fixed)stroke_width * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, - 0); + 0 + ); } /* @@ -1104,8 +1123,8 @@ font_render(FontObject *self, PyObject *args) { BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); target[k * 4 + 3] = CLIP8( src_alpha + - MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp) + ); } else { /* paste unpremultiplied RGBA values */ target[k * 4 + 0] = src_red; @@ -1123,15 +1142,20 @@ font_render(FontObject *self, PyObject *args) { if (src_alpha > 0) { if (target[k * 4 + 3] > 0) { target[k * 4 + 0] = BLEND( - src_alpha, target[k * 4 + 0], ink[0], tmp); + src_alpha, target[k * 4 + 0], ink[0], tmp + ); target[k * 4 + 1] = BLEND( - src_alpha, target[k * 4 + 1], ink[1], tmp); + src_alpha, target[k * 4 + 1], ink[1], tmp + ); target[k * 4 + 2] = BLEND( - src_alpha, target[k * 4 + 2], ink[2], tmp); + src_alpha, target[k * 4 + 2], ink[2], tmp + ); target[k * 4 + 3] = CLIP8( src_alpha + MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); + target[k * 4 + 3], (255 - src_alpha), tmp + ) + ); } else { target[k * 4 + 0] = ink[0]; target[k * 4 + 1] = ink[1]; @@ -1149,7 +1173,9 @@ font_render(FontObject *self, PyObject *args) { ? CLIP8( src_alpha + MULDIV255( - target[k], (255 - src_alpha), tmp)) + target[k], (255 - src_alpha), tmp + ) + ) : src_alpha; } } @@ -1425,7 +1451,8 @@ static PyMethodDef font_methods[] = { {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, #endif - {NULL, NULL}}; + {NULL, NULL} +}; static PyObject * font_getattr_family(FontObject *self, void *closure) { @@ -1482,7 +1509,8 @@ static struct PyGetSetDef font_getsetters[] = { {"x_ppem", (getter)font_getattr_x_ppem}, {"y_ppem", (getter)font_getattr_y_ppem}, {"glyphs", (getter)font_getattr_glyphs}, - {NULL}}; + {NULL} +}; static PyTypeObject Font_Type = { PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ @@ -1518,7 +1546,8 @@ static PyTypeObject Font_Type = { }; static PyMethodDef _functions[] = { - {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL}}; + {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL} +}; static int setup_module(PyObject *m) { diff --git a/src/_imagingmath.c b/src/_imagingmath.c index a2ddc91b9..550a10903 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -209,7 +209,8 @@ _binop(PyObject *self, PyObject *args) { } static PyMethodDef _functions[] = { - {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL}}; + {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL} +}; static void install(PyObject *d, char *name, void *value) { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index a95ce75bf..614dfbe7f 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -253,7 +253,8 @@ static PyMethodDef functions[] = { {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, {"get_on_pixels", (PyCFunction)get_on_pixels, METH_VARARGS, NULL}, {"match", (PyCFunction)match, METH_VARARGS, NULL}, - {NULL, NULL, 0, NULL}}; + {NULL, NULL, 0, NULL} +}; PyMODINIT_FUNC PyInit__imagingmorph(void) { diff --git a/src/_webp.c b/src/_webp.c index dfa24da41..e686ec820 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -42,7 +42,8 @@ static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA", "WEBP_MUX_MEMORY_ERROR", - "WEBP_MUX_NOT_ENOUGH_DATA"}; + "WEBP_MUX_NOT_ENOUGH_DATA" +}; PyObject * HandleMuxError(WebPMuxError err, char *chunk) { @@ -61,7 +62,8 @@ HandleMuxError(WebPMuxError err, char *chunk) { sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); } else { message_len = sprintf( - message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); + message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err] + ); } if (message_len < 0) { PyErr_SetString(PyExc_RuntimeError, "failed to construct error message"); @@ -138,7 +140,8 @@ _anim_encoder_new(PyObject *self, PyObject *args) { &kmin, &kmax, &allow_mixed, - &verbose)) { + &verbose + )) { return NULL; } @@ -214,7 +217,8 @@ _anim_encoder_add(PyObject *self, PyObject *args) { &lossless, &quality_factor, &alpha_quality_factor, - &method)) { + &method + )) { return NULL; } @@ -283,7 +287,8 @@ _anim_encoder_assemble(PyObject *self, PyObject *args) { &exif_bytes, &exif_size, &xmp_bytes, - &xmp_size)) { + &xmp_size + )) { return NULL; } @@ -421,7 +426,8 @@ _anim_decoder_get_info(PyObject *self) { info->loop_count, info->bgcolor, info->frame_count, - decp->mode); + decp->mode + ); } PyObject * @@ -466,7 +472,8 @@ _anim_decoder_get_next(PyObject *self) { } bytes = PyBytes_FromStringAndSize( - (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); + (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height + ); ret = Py_BuildValue("Si", bytes, timestamp); @@ -621,7 +628,8 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &exif_bytes, &exif_size, &xmp_bytes, - &xmp_size)) { + &xmp_size + )) { return NULL; } @@ -828,12 +836,14 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { icc_profile = PyBytes_FromStringAndSize( - (const char *)icc_profile_data.bytes, icc_profile_data.size); + (const char *)icc_profile_data.bytes, icc_profile_data.size + ); } if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size); + (const char *)exif_data.bytes, exif_data.size + ); } WebPDataClear(&image.bitstream); @@ -848,12 +858,14 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { if (config.output.colorspace < MODE_YUV) { bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size); + (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size + ); } else { // Skipping YUV for now. Need Test Images. // UNDONE -- unclear if we'll ever get here if we set mode_rgb* bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size); + (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size + ); } pymode = PyUnicode_FromString(mode); @@ -864,7 +876,8 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { config.output.height, pymode, NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif); + NULL == exif ? Py_None : exif + ); end: WebPFreeDecBuffer(&config.output); @@ -898,7 +911,8 @@ WebPDecoderVersion_str(void) { "%d.%d.%d", version_number >> 16, (version_number >> 8) % 0x100, - version_number % 0x100); + version_number % 0x100 + ); return version; } @@ -932,7 +946,8 @@ static PyMethodDef webpMethods[] = { WebPDecoderBuggyAlpha_wrapper, METH_NOARGS, "WebPDecoderBuggyAlpha"}, - {NULL, NULL}}; + {NULL, NULL} +}; void addMuxFlagToModule(PyObject *m) { diff --git a/src/decode.c b/src/decode.c index ea2f3af80..51d0aced2 100644 --- a/src/decode.c +++ b/src/decode.c @@ -46,7 +46,8 @@ typedef struct { PyObject_HEAD int (*decode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes + ); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; @@ -889,7 +890,8 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { PY_LONG_LONG length = -1; if (!PyArg_ParseTuple( - args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length)) { + args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length + )) { return NULL; } diff --git a/src/display.c b/src/display.c index 990f4b0a5..b4e2e3899 100644 --- a/src/display.c +++ b/src/display.c @@ -105,7 +105,8 @@ _draw(ImagingDisplayObject *display, PyObject *args) { src + 0, src + 1, src + 2, - src + 3)) { + src + 3 + )) { return NULL; } @@ -221,7 +222,8 @@ _tobytes(ImagingDisplayObject *display, PyObject *args) { } return PyBytes_FromStringAndSize( - display->dib->bits, display->dib->ysize * display->dib->linesize); + display->dib->bits, display->dib->ysize * display->dib->linesize + ); } static struct PyMethodDef methods[] = { @@ -247,7 +249,8 @@ _getattr_size(ImagingDisplayObject *self, void *closure) { } static struct PyGetSetDef getsetters[] = { - {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL}}; + {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL} +}; static PyTypeObject ImagingDisplayType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ @@ -341,9 +344,8 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = - (Func_SetThreadDpiAwarenessContext)GetProcAddress( - user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext + )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); @@ -403,7 +405,8 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { height, PyBytes_AS_STRING(buffer), (BITMAPINFO *)&core, - DIB_RGB_COLORS)) { + DIB_RGB_COLORS + )) { goto error; } @@ -547,7 +550,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right, - ps.rcPaint.bottom); + ps.rcPaint.bottom + ); if (result) { Py_DECREF(result); } else { @@ -562,7 +566,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { 0, 0, rect.right - rect.left, - rect.bottom - rect.top); + rect.bottom - rect.top + ); if (result) { Py_DECREF(result); } else { @@ -577,7 +582,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { 0, 0, rect.right - rect.left, - rect.bottom - rect.top); + rect.bottom - rect.top + ); if (result) { Py_DECREF(result); } else { @@ -591,7 +597,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { case WM_SIZE: /* resize window */ result = PyObject_CallFunction( - callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)); + callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam) + ); if (result) { InvalidateRect(wnd, NULL, 1); Py_DECREF(result); @@ -670,7 +677,8 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { HWND_DESKTOP, NULL, NULL, - NULL); + NULL + ); if (!wnd) { PyErr_SetString(PyExc_OSError, "failed to create window"); @@ -732,7 +740,8 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { &x0, &x1, &y0, - &y1)) { + &y1 + )) { return NULL; } @@ -844,7 +853,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { PyErr_Format( PyExc_OSError, "X connection failed: error %i", - xcb_connection_has_error(connection)); + xcb_connection_has_error(connection) + ); xcb_disconnect(connection); return NULL; } @@ -878,8 +888,10 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { 0, width, height, - 0x00ffffff), - &error); + 0x00ffffff + ), + &error + ); if (reply == NULL) { PyErr_Format( PyExc_OSError, @@ -887,7 +899,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { error->error_code, error->major_code, error->minor_code, - error->resource_id); + error->resource_id + ); free(error); xcb_disconnect(connection); return NULL; @@ -897,7 +910,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { if (reply->depth == 24) { buffer = PyBytes_FromStringAndSize( - (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)); + (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply) + ); } else { PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); } diff --git a/src/encode.c b/src/encode.c index 2c95b7ebc..f711865d5 100644 --- a/src/encode.c +++ b/src/encode.c @@ -39,7 +39,8 @@ typedef struct { PyObject_HEAD int (*encode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes + ); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; @@ -135,7 +136,8 @@ _encode(ImagingEncoderObject *encoder, PyObject *args) { } status = encoder->encode( - encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize); + encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize + ); /* adjust string length to avoid slicing in encoder */ if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) { @@ -572,7 +574,8 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { &compress_level, &compress_type, &dictionary, - &dictionary_size)) { + &dictionary_size + )) { return NULL; } @@ -653,15 +656,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { PyObject *item; if (!PyArg_ParseTuple( - args, - "sssnsOO", - &mode, - &rawmode, - &compname, - &fp, - &filename, - &tags, - &types)) { + args, "sssnsOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types + )) { return NULL; } @@ -785,7 +781,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { is_var_length = 1; } if (ImagingLibTiffMergeFieldInfo( - &encoder->state, type, key_int, is_var_length)) { + &encoder->state, type, key_int, is_var_length + )) { continue; } } @@ -795,7 +792,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, PyBytes_Size(value), - PyBytes_AsString(value)); + PyBytes_AsString(value) + ); } else if (is_var_length) { Py_ssize_t len, i; TRACE(("Setting from Tuple: %d \n", key_int)); @@ -805,7 +803,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int stride = 256; if (len != 768) { PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for Colormap"); + PyExc_ValueError, "Requiring 768 items for Colormap" + ); return NULL; } UINT16 *av; @@ -820,7 +819,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { (ttag_t)key_int, av, av + stride, - av + stride * 2); + av + stride * 2 + ); free(av); } } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { @@ -828,7 +828,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), - (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)) + ); } else if (type == TIFF_SHORT) { UINT16 *av; /* malloc check ok, calloc checks for overflow */ @@ -838,7 +839,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_LONG) { @@ -850,7 +852,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SBYTE) { @@ -862,7 +865,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SSHORT) { @@ -874,7 +878,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SLONG) { @@ -886,7 +891,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_FLOAT) { @@ -898,7 +904,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_DOUBLE) { @@ -910,43 +917,54 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } } else { if (type == TIFF_SHORT) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value) + ); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + ); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value) + ); } else if (type == TIFF_SLONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT32)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, (INT32)PyLong_AsLong(value) + ); } else if (type == TIFF_FLOAT) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value)); + &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value) + ); } else if (type == TIFF_DOUBLE) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) + ); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value) + ); } else if (type == TIFF_ASCII) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyBytes_AsString(value)); + &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) + ); } else if (type == TIFF_RATIONAL) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", key_int, - PyBytes_AsString(PyObject_Str(value)))); + PyBytes_AsString(PyObject_Str(value))) + ); } } if (!status) { @@ -1007,7 +1025,8 @@ get_qtables_arrays(PyObject *qtables, int *qtablesLen) { if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { PyErr_SetString( PyExc_ValueError, - "Not a valid number of quantization tables. Should be between 1 and 4."); + "Not a valid number of quantization tables. Should be between 1 and 4." + ); Py_DECREF(tables); return NULL; } @@ -1096,7 +1115,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &extra, &extra_size, &rawExif, - &rawExifLen)) { + &rawExifLen + )) { return NULL; } @@ -1266,7 +1286,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &fd, &comment, &comment_size, - &plt)) { + &plt + )) { return NULL; } @@ -1323,7 +1344,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { j2k_decode_coord_tuple(offset, &context->offset_x, &context->offset_y); j2k_decode_coord_tuple( - tile_offset, &context->tile_offset_x, &context->tile_offset_y); + tile_offset, &context->tile_offset_x, &context->tile_offset_y + ); j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); /* Error on illegal tile offsets */ @@ -1333,7 +1355,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 tile offset too small; top left tile must " - "intersect image area"); + "intersect image area" + ); Py_DECREF(encoder); return NULL; } @@ -1341,8 +1364,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (context->tile_offset_x > context->offset_x || context->tile_offset_y > context->offset_y) { PyErr_SetString( - PyExc_ValueError, - "JPEG 2000 tile offset too large to cover image area"); + PyExc_ValueError, "JPEG 2000 tile offset too large to cover image area" + ); Py_DECREF(encoder); return NULL; } @@ -1376,7 +1399,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { j2k_decode_coord_tuple(cblk_size, &context->cblk_width, &context->cblk_height); j2k_decode_coord_tuple( - precinct_size, &context->precinct_width, &context->precinct_height); + precinct_size, &context->precinct_width, &context->precinct_height + ); context->irreversible = PyObject_IsTrue(irreversible); context->progression = prog_order; diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3a5e918e8..bf7db281e 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -36,7 +36,8 @@ add_item(const char *mode) { "AccessInit: hash collision: %d for both %s and %s\n", i, mode, - access_table[i].mode); + access_table[i].mode + ); exit(1); } access_table[i].mode = mode; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 72f478d8d..9a41febc7 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -243,7 +243,8 @@ static const bc7_mode_info bc7_modes[] = { {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, - {2, 6, 0, 0, 5, 5, 1, 0, 2, 0}}; + {2, 6, 0, 0, 5, 5, 1, 0, 2, 0} +}; /* Subset indices: Table.P2, 1 bit per index */ @@ -254,7 +255,8 @@ static const UINT16 bc7_si2[] = { 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, 0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718, - 0xccf0, 0x0fcc, 0x7744, 0xee22}; + 0xccf0, 0x0fcc, 0x7744, 0xee22 +}; /* Table.P3, 2 bits per index */ static const UINT32 bc7_si3[] = { @@ -267,20 +269,23 @@ static const UINT32 bc7_si3[] = { 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, - 0x2a4a5254}; + 0x2a4a5254 +}; /* Anchor indices: Table.A2 */ -static const char bc7_ai0[] = { - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, - 8, 15, 2, 8, 2, 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, - 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15}; +static const char bc7_ai0[] = {15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, + 2, 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, + 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, + 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15}; /* Table.A3a */ -static const char bc7_ai1[] = { - 3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, - 6, 10, 5, 8, 8, 6, 8, 5, 15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, - 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3}; +static const char bc7_ai1[] = {3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, + 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, + 8, 6, 8, 5, 15, 15, 8, 15, 3, 5, 6, 10, 8, + 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, + 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3}; /* Table.A3b */ static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, @@ -293,7 +298,8 @@ static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 1 static const char bc7_weights2[] = {0, 21, 43, 64}; static const char bc7_weights3[] = {0, 9, 18, 27, 37, 46, 55, 64}; static const char bc7_weights4[] = { - 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 +}; static const char * bc7_get_weights(int n) { @@ -526,7 +532,8 @@ static const bc6_mode_info bc6_modes[] = { {1, 0, 0, 10, 10, 10, 10}, {1, 1, 0, 11, 9, 9, 9}, {1, 1, 0, 12, 8, 8, 8}, - {1, 1, 0, 16, 4, 4, 4}}; + {1, 1, 0, 16, 4, 4, 4} +}; /* Table.F, encoded as a sequence of bit indices */ static const UINT8 bc6_bit_packings[][75] = { @@ -591,7 +598,8 @@ static const UINT8 bc6_bit_packings[][75] = { 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, 42}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, - 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, 42}}; + 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, 42} +}; static void bc6_sign_extend(UINT16 *v, int prec) { @@ -830,7 +838,8 @@ decode_bcn( int bytes, int N, int C, - char *pixel_format) { + char *pixel_format +) { int ymax = state->ysize + state->yoff; const UINT8 *ptr = src; switch (N) { diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 4ea9c7717..51cb7e102 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -13,7 +13,8 @@ void static inline ImagingLineBoxBlur32( int edgeA, int edgeB, UINT32 ww, - UINT32 fw) { + UINT32 fw +) { int x; UINT32 acc[4]; UINT32 bulk[4]; @@ -109,7 +110,8 @@ void static inline ImagingLineBoxBlur8( int edgeA, int edgeB, UINT32 ww, - UINT32 fw) { + UINT32 fw +) { int x; UINT32 acc; UINT32 bulk; @@ -198,7 +200,8 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { edgeA, edgeB, ww, - fw); + fw + ); if (imIn == imOut) { // Commit. memcpy(imOut->image8[y], lineOut, imIn->xsize); @@ -214,7 +217,8 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { edgeA, edgeB, ww, - fw); + fw + ); if (imIn == imOut) { // Commit. memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); @@ -314,11 +318,13 @@ _gaussian_blur_radius(float radius, int passes) { Imaging ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes +) { return ImagingBoxBlur( imOut, imIn, _gaussian_blur_radius(xradius, passes), _gaussian_blur_radius(yradius, passes), - passes); + passes + ); } diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index f9c005efe..f326d402f 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -142,7 +142,8 @@ ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) { CHOP2( (((255 - in1[x]) * (in1[x] * in2[x])) / 65536) + (in1[x] * (255 - ((255 - in1[x]) * (255 - in2[x]) / 255))) / 255, - NULL); + NULL + ); } Imaging @@ -150,7 +151,8 @@ ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { CHOP2( (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), - NULL); + NULL + ); } Imaging @@ -158,5 +160,6 @@ ImagingOverlay(Imaging imIn1, Imaging imIn2) { CHOP2( (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), - NULL); + NULL + ); } diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index aee7cda06..5559de689 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -63,7 +63,8 @@ ImagingColorLUT3D_linear( int size1D, int size2D, int size3D, - INT16 *table) { + INT16 *table +) { /* This float to int conversion doesn't have rounding error compensation (+0.5) for two reasons: 1. As we don't hit the highest value, @@ -112,7 +113,8 @@ ImagingColorLUT3D_linear( index2D >> SCALE_BITS, index3D >> SCALE_BITS, size1D, - size1D_2D); + size1D_2D + ); INT16 result[4], left[4], right[4]; INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; @@ -123,19 +125,22 @@ ImagingColorLUT3D_linear( leftright, &table[idx + size1D * 3], &table[idx + size1D * 3 + 3], - shift1D); + shift1D + ); interpolate3(left, leftleft, leftright, shift2D); interpolate3( rightleft, &table[idx + size1D_2D * 3], &table[idx + size1D_2D * 3 + 3], - shift1D); + shift1D + ); interpolate3( rightright, &table[idx + size1D_2D * 3 + size1D * 3], &table[idx + size1D_2D * 3 + size1D * 3 + 3], - shift1D); + shift1D + ); interpolate3(right, rightleft, rightright, shift2D); interpolate3(result, left, right, shift3D); @@ -144,7 +149,8 @@ ImagingColorLUT3D_linear( clip8(result[0]), clip8(result[1]), clip8(result[2]), - rowIn[x * 4 + 3]); + rowIn[x * 4 + 3] + ); memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } @@ -155,19 +161,22 @@ ImagingColorLUT3D_linear( leftright, &table[idx + size1D * 4], &table[idx + size1D * 4 + 4], - shift1D); + shift1D + ); interpolate4(left, leftleft, leftright, shift2D); interpolate4( rightleft, &table[idx + size1D_2D * 4], &table[idx + size1D_2D * 4 + 4], - shift1D); + shift1D + ); interpolate4( rightright, &table[idx + size1D_2D * 4 + size1D * 4], &table[idx + size1D_2D * 4 + size1D * 4 + 4], - shift1D); + shift1D + ); interpolate4(right, rightleft, rightright, shift2D); interpolate4(result, left, right, shift3D); @@ -176,7 +185,8 @@ ImagingColorLUT3D_linear( clip8(result[0]), clip8(result[1]), clip8(result[2]), - clip8(result[3])); + clip8(result[3]) + ); memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index fcb5f7ad9..c8f234261 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1044,7 +1044,8 @@ static struct { {"I;16L", "F", I16L_F}, {"I;16B", "F", I16B_F}, - {NULL}}; + {NULL} +}; /* FIXME: translate indexed versions to pointer versions below this line */ @@ -1316,7 +1317,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { (UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize, - imIn->palette); + imIn->palette + ); } ImagingSectionLeave(&cookie); @@ -1328,11 +1330,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { #endif static Imaging topalette( - Imaging imOut, - Imaging imIn, - const char *mode, - ImagingPalette inpalette, - int dither) { + Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalette, int dither +) { ImagingSectionCookie cookie; int alpha; int x, y; @@ -1623,7 +1622,8 @@ tobilevel(Imaging imOut, Imaging imIn) { static Imaging convert( - Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither) { + Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither +) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; @@ -1677,7 +1677,8 @@ convert( #else static char buf[100]; snprintf( - buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); + buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode + ); return (Imaging)ImagingError_ValueError(buf); #endif } @@ -1727,18 +1728,16 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { if (strcmp(mode, "RGBa") == 0) { premultiplied = 1; } - } else if ( - strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { + } else if (strcmp(imIn->mode, "RGB") == 0 && + (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { convert = rgb2la; source_transparency = 1; if (strcmp(mode, "La") == 0) { premultiplied = 1; } - } else if ( - (strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || - strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { + } else if ((strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && + (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { if (strcmp(imIn->mode, "1") == 0) { convert = bit2rgb; } else if (strcmp(imIn->mode, "I") == 0) { @@ -1756,7 +1755,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { 100, "conversion from %.10s to %.10s not supported in convert_transparent", imIn->mode, - mode); + mode + ); return (Imaging)ImagingError_ValueError(buf); } diff --git a/src/libImaging/ConvertYCbCr.c b/src/libImaging/ConvertYCbCr.c index 142f065e5..285b43327 100644 --- a/src/libImaging/ConvertYCbCr.c +++ b/src/libImaging/ConvertYCbCr.c @@ -47,7 +47,8 @@ static INT16 Y_R[] = { 4019, 4038, 4057, 4076, 4095, 4114, 4133, 4153, 4172, 4191, 4210, 4229, 4248, 4267, 4286, 4306, 4325, 4344, 4363, 4382, 4401, 4420, 4440, 4459, 4478, 4497, 4516, 4535, 4554, 4574, 4593, 4612, 4631, 4650, 4669, 4688, 4707, 4727, 4746, 4765, 4784, 4803, - 4822, 4841, 4861, 4880}; + 4822, 4841, 4861, 4880 +}; static INT16 Y_G[] = { 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, 376, 413, 451, 488, @@ -68,7 +69,8 @@ static INT16 Y_G[] = { 7889, 7927, 7964, 8002, 8040, 8077, 8115, 8152, 8190, 8227, 8265, 8303, 8340, 8378, 8415, 8453, 8490, 8528, 8566, 8603, 8641, 8678, 8716, 8753, 8791, 8828, 8866, 8904, 8941, 8979, 9016, 9054, 9091, 9129, 9167, 9204, 9242, 9279, 9317, 9354, 9392, 9430, - 9467, 9505, 9542, 9580}; + 9467, 9505, 9542, 9580 +}; static INT16 Y_B[] = { 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, 88, 95, @@ -89,7 +91,8 @@ static INT16 Y_B[] = { 1532, 1539, 1547, 1554, 1561, 1569, 1576, 1583, 1591, 1598, 1605, 1612, 1620, 1627, 1634, 1642, 1649, 1656, 1663, 1671, 1678, 1685, 1693, 1700, 1707, 1715, 1722, 1729, 1736, 1744, 1751, 1758, 1766, 1773, 1780, 1788, 1795, 1802, 1809, 1817, 1824, 1831, - 1839, 1846, 1853, 1860}; + 1839, 1846, 1853, 1860 +}; static INT16 Cb_R[] = { 0, -10, -21, -31, -42, -53, -64, -75, -85, -96, -107, -118, @@ -113,7 +116,8 @@ static INT16 Cb_R[] = { -2332, -2342, -2353, -2364, -2375, -2386, -2396, -2407, -2418, -2429, -2440, -2450, -2461, -2472, -2483, -2494, -2504, -2515, -2526, -2537, -2548, -2558, -2569, -2580, -2591, -2602, -2612, -2623, -2634, -2645, -2656, -2666, -2677, -2688, -2699, -2710, - -2720, -2731, -2742, -2753}; + -2720, -2731, -2742, -2753 +}; static INT16 Cb_G[] = { 0, -20, -41, -63, -84, -105, -126, -147, -169, -190, -211, -232, @@ -137,7 +141,8 @@ static INT16 Cb_G[] = { -4578, -4600, -4621, -4642, -4663, -4684, -4706, -4727, -4748, -4769, -4790, -4812, -4833, -4854, -4875, -4896, -4918, -4939, -4960, -4981, -5002, -5024, -5045, -5066, -5087, -5108, -5130, -5151, -5172, -5193, -5214, -5236, -5257, -5278, -5299, -5320, - -5342, -5363, -5384, -5405}; + -5342, -5363, -5384, -5405 +}; static INT16 Cb_B[] = { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, @@ -158,7 +163,8 @@ static INT16 Cb_B[] = { 6720, 6752, 6784, 6816, 6848, 6880, 6912, 6944, 6976, 7008, 7040, 7072, 7104, 7136, 7168, 7200, 7232, 7264, 7296, 7328, 7360, 7392, 7424, 7456, 7488, 7520, 7552, 7584, 7616, 7648, 7680, 7712, 7744, 7776, 7808, 7840, 7872, 7904, 7936, 7968, 8000, 8032, - 8064, 8096, 8128, 8160}; + 8064, 8096, 8128, 8160 +}; #define Cr_R Cb_B @@ -184,7 +190,8 @@ static INT16 Cr_G[] = { -5787, -5814, -5841, -5867, -5894, -5921, -5948, -5975, -6001, -6028, -6055, -6082, -6109, -6135, -6162, -6189, -6216, -6243, -6269, -6296, -6323, -6350, -6376, -6403, -6430, -6457, -6484, -6510, -6537, -6564, -6591, -6618, -6644, -6671, -6698, -6725, - -6752, -6778, -6805, -6832}; + -6752, -6778, -6805, -6832 +}; static INT16 Cr_B[] = { 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, -51, -56, @@ -208,7 +215,8 @@ static INT16 Cr_B[] = { -1123, -1128, -1133, -1139, -1144, -1149, -1154, -1159, -1165, -1170, -1175, -1180, -1185, -1191, -1196, -1201, -1206, -1211, -1217, -1222, -1227, -1232, -1238, -1243, -1248, -1253, -1258, -1264, -1269, -1274, -1279, -1284, -1290, -1295, -1300, -1305, - -1310, -1316, -1321, -1326}; + -1310, -1316, -1321, -1326 +}; static INT16 R_Cr[] = { -11484, -11394, -11305, -11215, -11125, -11036, -10946, -10856, -10766, -10677, @@ -236,7 +244,8 @@ static INT16 R_Cr[] = { 8255, 8345, 8434, 8524, 8614, 8704, 8793, 8883, 8973, 9063, 9152, 9242, 9332, 9421, 9511, 9601, 9691, 9780, 9870, 9960, 10050, 10139, 10229, 10319, 10408, 10498, 10588, 10678, 10767, 10857, - 10947, 11037, 11126, 11216, 11306, 11395}; + 10947, 11037, 11126, 11216, 11306, 11395 +}; static INT16 G_Cb[] = { 2819, 2797, 2775, 2753, 2731, 2709, 2687, 2665, 2643, 2621, 2599, 2577, @@ -260,7 +269,8 @@ static INT16 G_Cb[] = { -1937, -1959, -1981, -2003, -2025, -2047, -2069, -2091, -2113, -2135, -2157, -2179, -2201, -2224, -2246, -2268, -2290, -2312, -2334, -2356, -2378, -2400, -2422, -2444, -2466, -2488, -2510, -2532, -2554, -2576, -2598, -2620, -2642, -2664, -2686, -2708, - -2730, -2752, -2774, -2796}; + -2730, -2752, -2774, -2796 +}; static INT16 G_Cr[] = { 5850, 5805, 5759, 5713, 5667, 5622, 5576, 5530, 5485, 5439, 5393, 5347, @@ -284,7 +294,8 @@ static INT16 G_Cr[] = { -4021, -4067, -4112, -4158, -4204, -4250, -4295, -4341, -4387, -4432, -4478, -4524, -4569, -4615, -4661, -4707, -4752, -4798, -4844, -4889, -4935, -4981, -5027, -5072, -5118, -5164, -5209, -5255, -5301, -5346, -5392, -5438, -5484, -5529, -5575, -5621, - -5666, -5712, -5758, -5804}; + -5666, -5712, -5758, -5804 +}; static INT16 B_Cb[] = { -14515, -14402, -14288, -14175, -14062, -13948, -13835, -13721, -13608, -13495, @@ -312,7 +323,8 @@ static INT16 B_Cb[] = { 10434, 10547, 10660, 10774, 10887, 11001, 11114, 11227, 11341, 11454, 11568, 11681, 11794, 11908, 12021, 12135, 12248, 12361, 12475, 12588, 12702, 12815, 12929, 13042, 13155, 13269, 13382, 13496, 13609, 13722, - 13836, 13949, 14063, 14176, 14289, 14403}; + 13836, 13949, 14063, 14176, 14289, 14403 +}; void ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels) { diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 269be1058..c69e9e552 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -95,7 +95,8 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } dib->bitmap = CreateDIBSection( - dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); + dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0 + ); if (!dib->bitmap) { free(dib->info); free(dib); @@ -218,7 +219,8 @@ ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) { dib->bits + dib->linesize * (dib->ysize - (xy[1] + y) - 1) + xy[0] * dib->pixelsize, im->image[y], - im->xsize); + im->xsize + ); } } @@ -251,7 +253,8 @@ ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { dib->bits, dib->info, DIB_RGB_COLORS, - SRCCOPY); + SRCCOPY + ); } else { /* stretchblt (displays) */ if (dib->palette != 0) { @@ -268,7 +271,8 @@ ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { src[1], src[2] - src[0], src[3] - src[1], - SRCCOPY); + SRCCOPY + ); } } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 133696dd8..f1c8ffcff 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -120,9 +120,8 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { if (x0 <= x1) { pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; memset( - im->image8[y0] + x0 * pixelwidth, - (UINT8)ink, - (x1 - x0 + 1) * pixelwidth); + im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth + ); } } } @@ -408,7 +407,8 @@ x_cmp(const void *x0, const void *x1) { static void draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) { + Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline +) { int i; for (i = 0; i < n; i++) { if (e[i].ymin == y && e[i].ymin == e[i].ymax) { @@ -440,13 +440,8 @@ draw_horizontal_lines( */ static inline int polygon_generic( - Imaging im, - int n, - Edge *e, - int ink, - int eofill, - hline_handler hline, - int hasAlpha) { + Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha +) { Edge **edge_table; float *xx; int edge_count = 0; @@ -530,25 +525,29 @@ polygon_generic( other_edge->x0; if (ymin == current->ymax) { if (current->dx > 0) { - xx[k] = fmax( - adjacent_line_x, - adjacent_line_x_other_edge) + - 1; + xx[k] = + fmax( + adjacent_line_x, adjacent_line_x_other_edge + ) + + 1; } else { - xx[k] = fmin( - adjacent_line_x, - adjacent_line_x_other_edge) - - 1; + xx[k] = + fmin( + adjacent_line_x, adjacent_line_x_other_edge + ) - + 1; } } else { if (current->dx > 0) { xx[k] = fmin( - adjacent_line_x, adjacent_line_x_other_edge); + adjacent_line_x, adjacent_line_x_other_edge + ); } else { - xx[k] = fmax( - adjacent_line_x, - adjacent_line_x_other_edge) + - 1; + xx[k] = + fmax( + adjacent_line_x, adjacent_line_x_other_edge + ) + + 1; } } break; @@ -699,7 +698,8 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op) { + Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op +) { DRAW *draw; INT32 ink; int dx, dy; @@ -730,7 +730,8 @@ ImagingDrawWideLine( {x0 - dxmin, y0 + dymax}, {x1 - dxmin, y1 + dymax}, {x1 + dxmax, y1 - dymin}, - {x0 + dxmax, y0 - dymin}}; + {x0 + dxmax, y0 - dymin} + }; add_edge(e + 0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); add_edge(e + 1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); @@ -752,7 +753,8 @@ ImagingDrawRectangle( const void *ink_, int fill, int width, - int op) { + int op +) { int i; int y; int tmp; @@ -800,7 +802,8 @@ ImagingDrawRectangle( int ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { + Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op +) { int i, n, x0, y0, x1, y1; DRAW *draw; INT32 ink; @@ -851,7 +854,8 @@ ImagingDrawPolygon( if (width == 1) { for (i = 0; i < count - 1; i++) { draw->line( - im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); + im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink + ); } draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); } else { @@ -864,10 +868,12 @@ ImagingDrawPolygon( xy[i * 2 + 3], ink_, width, - op); + op + ); } ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op + ); } } @@ -877,7 +883,8 @@ ImagingDrawPolygon( int ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op) { return ImagingFill2( - im, ink, bitmap, x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize); + im, ink, bitmap, x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize + ); } /* -------------------------------------------------------------------- */ @@ -1086,7 +1093,8 @@ clip_tree_transpose(clip_node *root) { // segments, i.e. something like correct bracket sequences. int clip_tree_do_clip( - clip_node *root, int32_t x0, int32_t y, int32_t x1, event_list **ret) { + clip_node *root, int32_t x0, int32_t y, int32_t x1, event_list **ret +) { if (root == NULL) { event_list *start = malloc(sizeof(event_list)); if (!start) { @@ -1223,7 +1231,8 @@ typedef struct { } clip_ellipse_state; typedef void (*clip_ellipse_init)( - clip_ellipse_state *, int32_t, int32_t, int32_t, float, float); + clip_ellipse_state *, int32_t, int32_t, int32_t, float, float +); void debug_clip_tree(clip_node *root, int space) { @@ -1335,7 +1344,8 @@ arc_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float // A chord line. void chord_line_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar +) { ellipse_init(&s->st, a, b, a + b + 1); s->head = NULL; @@ -1362,7 +1372,8 @@ chord_line_init( // Pie side. void pie_side_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _) { + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _ +) { ellipse_init(&s->st, a, b, a + b + 1); s->head = NULL; @@ -1478,7 +1489,8 @@ clip_ellipse_free(clip_ellipse_state *s) { int8_t clip_ellipse_next( - clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1 +) { int32_t x0, y, x1; while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { @@ -1512,7 +1524,8 @@ ellipseNew( const void *ink_, int fill, int width, - int op) { + int op +) { DRAW *draw; INT32 ink; DRAWINIT(); @@ -1547,7 +1560,8 @@ clipEllipseNew( const void *ink_, int width, int op, - clip_ellipse_init init) { + clip_ellipse_init init +) { DRAW *draw; INT32 ink; DRAWINIT(); @@ -1580,7 +1594,8 @@ arcNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); } @@ -1595,7 +1610,8 @@ chordNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); } @@ -1610,9 +1626,11 @@ chordLineNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew( - im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); + im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init + ); } static int @@ -1626,7 +1644,8 @@ pieNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); } @@ -1640,7 +1659,8 @@ pieSideNew( float start, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); } @@ -1654,7 +1674,8 @@ ImagingDrawEllipse( const void *ink, int fill, int width, - int op) { + int op +) { return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); } @@ -1669,7 +1690,8 @@ ImagingDrawArc( float end, const void *ink, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); @@ -1692,7 +1714,8 @@ ImagingDrawChord( const void *ink, int fill, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); @@ -1722,7 +1745,8 @@ ImagingDrawPieslice( const void *ink, int fill, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); @@ -1850,13 +1874,8 @@ ImagingOutlineLine(ImagingOutline outline, float x1, float y1) { int ImagingOutlineCurve( - ImagingOutline outline, - float x1, - float y1, - float x2, - float y2, - float x3, - float y3) { + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3 +) { Edge *e; int i; float xo, yo; @@ -1970,7 +1989,8 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) { int ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink_, int fill, int op) { + Imaging im, ImagingOutline outline, const void *ink_, int fill, int op +) { DRAW *draw; INT32 ink; diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 5b6bfb89c..8fb481e7e 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,8 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int)sqrt( - (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int + )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 85de77fcb..fbd6b425f 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -59,7 +59,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { } imOut = ImagingNewDirty( - imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); + imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin + ); if (!imOut) { return NULL; } @@ -369,7 +370,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } } memcpy( - out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); + out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2 + ); } } memcpy(imOut->image[y], im->image[y], im->linesize); diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index cf3bc9979..2bfeed7b6 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -781,7 +781,8 @@ ImagingGenericTransform( ImagingTransformMap transform, void *transform_data, int filterid, - int fill) { + int fill +) { /* slow generic transformation. use ImagingTransformAffine or ImagingScaleAffine where possible. */ @@ -836,14 +837,8 @@ ImagingGenericTransform( static Imaging ImagingScaleAffine( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - double a[6], - int fill) { + Imaging imOut, Imaging imIn, int x0, int y0, int x1, int y1, double a[6], int fill +) { /* scale, nearest neighbour resampling */ ImagingSectionCookie cookie; @@ -936,7 +931,8 @@ static inline int check_fixed(double a[6], int x, int y) { return ( fabs(x * a[0] + y * a[1] + a[2]) < 32768.0 && - fabs(x * a[3] + y * a[4] + a[5]) < 32768.0); + fabs(x * a[3] + y * a[4] + a[5]) < 32768.0 + ); } static inline Imaging @@ -949,7 +945,8 @@ affine_fixed( int y1, double a[6], int filterid, - int fill) { + int fill +) { /* affine transform, nearest neighbour resampling, fixed point arithmetics */ @@ -1026,7 +1023,8 @@ ImagingTransformAffine( int y1, double a[6], int filterid, - int fill) { + int fill +) { /* affine transform, nearest neighbour resampling, floating point arithmetics*/ @@ -1039,7 +1037,8 @@ ImagingTransformAffine( if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( - imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill); + imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill + ); } if (a[1] == 0 && a[3] == 0) { @@ -1134,13 +1133,15 @@ ImagingTransform( int y1, double a[8], int filterid, - int fill) { + int fill +) { ImagingTransformMap transform; switch (method) { case IMAGING_TRANSFORM_AFFINE: return ImagingTransformAffine( - imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + imOut, imIn, x0, y0, x1, y1, a, filterid, fill + ); break; case IMAGING_TRANSFORM_PERSPECTIVE: transform = perspective_transform; @@ -1153,5 +1154,6 @@ ImagingTransform( } return ImagingGenericTransform( - imOut, imIn, x0, y0, x1, y1, transform, a, filterid, fill); + imOut, imIn, x0, y0, x1, y1, transform, a, filterid, fill + ); } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index bd2a2778c..c61a27d3b 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -58,11 +58,10 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if ( - alpha_only && - (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || - strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0)) { + } else if (alpha_only && + (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0)) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else @@ -246,13 +245,14 @@ getcolors32(Imaging im, int maxcolors, int *size) { code in Python 2.1.3; the exact implementation is borrowed from Python's Unicode property database (written by yours truly) /F */ - static int SIZES[] = { - 4, 3, 8, 3, 16, 3, 32, 5, 64, 3, - 128, 3, 256, 29, 512, 17, 1024, 9, 2048, 5, - 4096, 83, 8192, 27, 16384, 43, 32768, 3, 65536, 45, - 131072, 9, 262144, 39, 524288, 39, 1048576, 9, 2097152, 5, - 4194304, 3, 8388608, 33, 16777216, 27, 33554432, 9, 67108864, 71, - 134217728, 39, 268435456, 9, 536870912, 5, 1073741824, 83, 0}; + static int SIZES[] = {4, 3, 8, 3, 16, 3, 32, 5, + 64, 3, 128, 3, 256, 29, 512, 17, + 1024, 9, 2048, 5, 4096, 83, 8192, 27, + 16384, 43, 32768, 3, 65536, 45, 131072, 9, + 262144, 39, 524288, 39, 1048576, 9, 2097152, 5, + 4194304, 3, 8388608, 33, 16777216, 27, 33554432, 9, + 67108864, 71, 134217728, 39, 268435456, 9, 536870912, 5, + 1073741824, 83, 0}; code_size = code_poly = code_mask = 0; diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index 45b67616d..203fb9d0a 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -79,7 +79,8 @@ glzwe( UINT8 *out_ptr, UINT32 *in_avail, UINT32 *out_avail, - UINT32 end_of_data) { + UINT32 end_of_data +) { switch (st->entry_state) { case LZW_TRY_IN1: get_first_byte: @@ -312,7 +313,8 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->x = 0; /* step forward, according to the interlace settings */ @@ -348,7 +350,8 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { ptr, &in_avail, &out_avail, - state->state == FINISH); + state->state == FINISH + ); out_used = sub_block_limit - ptr - out_avail; *sub_block_ptr += out_used; ptr += out_used; diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index bd16cdbe1..e26c0e9b3 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -49,7 +49,8 @@ ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt if (++state->x >= state->bytes) { /* Got a full line, unpack it */ state->shuffle( - (UINT8 *)im->image[state->y], state->buffer, state->xsize); + (UINT8 *)im->image[state->y], state->buffer, state->xsize + ); state->x = 0; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e93..b1c3aed41 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -295,7 +295,8 @@ extern Imaging ImagingFill(Imaging im, const void *ink); extern int ImagingFill2( - Imaging into, const void *ink, Imaging mask, int x0, int y0, int x1, int y1); + Imaging into, const void *ink, Imaging mask, int x0, int y0, int x1, int y1 +); extern Imaging ImagingFillBand(Imaging im, int band, int color); extern Imaging @@ -310,7 +311,8 @@ extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes +); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging @@ -373,7 +375,8 @@ ImagingTransform( int y1, double a[8], int filter, - int fill); + int fill +); extern Imaging ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging @@ -386,7 +389,8 @@ ImagingColorLUT3D_linear( int size1D, int size2D, int size3D, - INT16 *table); + INT16 *table +); extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn); @@ -440,7 +444,8 @@ ImagingDrawArc( float end, const void *ink, int width, - int op); + int op +); extern int ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op); extern int @@ -455,7 +460,8 @@ ImagingDrawChord( const void *ink, int fill, int width, - int op); + int op +); extern int ImagingDrawEllipse( Imaging im, @@ -466,12 +472,14 @@ ImagingDrawEllipse( const void *ink, int fill, int width, - int op); + int op +); extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); extern int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op); + Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op +); extern int ImagingDrawPieslice( Imaging im, @@ -484,12 +492,14 @@ ImagingDrawPieslice( const void *ink, int fill, int width, - int op); + int op +); extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int ImagingDrawPolygon( - Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); + Imaging im, int points, int *xy, const void *ink, int fill, int width, int op +); extern int ImagingDrawRectangle( Imaging im, @@ -500,7 +510,8 @@ ImagingDrawRectangle( const void *ink, int fill, int width, - int op); + int op +); /* Level 2 graphics (WORK IN PROGRESS) */ extern ImagingOutline @@ -510,7 +521,8 @@ ImagingOutlineDelete(ImagingOutline outline); extern int ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink, int fill, int op); + Imaging im, ImagingOutline outline, const void *ink, int fill, int op +); extern int ImagingOutlineMove(ImagingOutline outline, float x, float y); @@ -518,7 +530,8 @@ extern int ImagingOutlineLine(ImagingOutline outline, float x, float y); extern int ImagingOutlineCurve( - ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3); + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3 +); extern int ImagingOutlineTransform(ImagingOutline outline, double a[6]); @@ -545,7 +558,8 @@ ImagingSavePPM(Imaging im, const char *filename); /* Codecs */ typedef struct ImagingCodecStateInstance *ImagingCodecState; typedef int (*ImagingCodec)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes +); extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); @@ -575,7 +589,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) #ifdef HAVE_OPENJPEG extern int ImagingJpeg2KDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingJpeg2KDecodeCleanup(ImagingCodecState state); extern int @@ -586,7 +601,8 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state); #ifdef HAVE_LIBTIFF extern int ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); #endif @@ -598,7 +614,8 @@ extern int ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int ImagingPackbitsDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int @@ -611,13 +628,16 @@ extern int ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); extern int ImagingSgiRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingSunRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingTgaRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); extern int diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index dd066c10b..5b3d7ffc4 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -67,7 +67,8 @@ j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { /* -------------------------------------------------------------------- */ typedef void (*j2k_unpacker_t)( - opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im); + opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im +); struct j2k_decode_unpacker { const char *mode; @@ -89,10 +90,8 @@ j2ku_shift(unsigned x, int n) { static void j2ku_gray_l( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -145,10 +144,8 @@ j2ku_gray_l( static void j2ku_gray_i( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -204,10 +201,8 @@ j2ku_gray_i( static void j2ku_gray_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -268,10 +263,8 @@ j2ku_gray_rgb( static void j2ku_graya_la( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -347,10 +340,8 @@ j2ku_graya_la( static void j2ku_srgb_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -413,10 +404,8 @@ j2ku_srgb_rgb( static void j2ku_sycc_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -482,10 +471,8 @@ j2ku_sycc_rgb( static void j2ku_srgba_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -547,10 +534,8 @@ j2ku_srgba_rgba( static void j2ku_sycca_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im +) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -815,7 +800,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { &tile_info.x1, &tile_info.y1, &tile_info.nb_comps, - &should_continue)) { + &should_continue + )) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -906,7 +892,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.tile_index, (OPJ_BYTE *)state->buffer, tile_info.data_size, - stream)) { + stream + )) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 7f1aeaddb..cb21a186c 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -89,7 +89,8 @@ j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { /* -------------------------------------------------------------------- */ typedef void (*j2k_pack_tile_t)( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h); + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h +); static void j2k_pack_l(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { @@ -157,7 +158,8 @@ j2k_pack_rgb(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsig static void j2k_pack_rgba( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h +) { UINT8 *pr = buf; UINT8 *pg = pr + w * h; UINT8 *pb = pg + w * h; @@ -205,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / - (CINEMA_24_CS_LENGTH * 8)); + ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) + ); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -241,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / - (CINEMA_48_CS_LENGTH * 8)); + ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) + ); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 6f75d8670..30c64f235 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -206,9 +206,8 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif - else if ( - strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) { + else if (strcmp(context->rawmode, "CMYK") == 0 || + strcmp(context->rawmode, "CMYK;I") == 0) { context->cinfo.out_color_space = JCS_CMYK; } else if (strcmp(context->rawmode, "YCbCr") == 0) { context->cinfo.out_color_space = JCS_YCbCr; @@ -256,7 +255,8 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->y++; } if (ok != 1) { diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index ba8353c2d..4372d51d5 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -175,7 +175,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { i, &context->qtables[i * DCTSIZE2], quality, - FALSE); + FALSE + ); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; } @@ -183,7 +184,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE); + &context->cinfo, 1, &context->qtables[0], quality, FALSE + ); } for (i = last_q; i < context->cinfo.num_components; i++) { context->cinfo.comp_info[i].quant_tbl_no = last_q; @@ -273,7 +275,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { &context->cinfo, JPEG_APP0 + 1, (unsigned char *)context->rawExif, - context->rawExifLen); + context->rawExifLen + ); } state->state++; @@ -289,7 +292,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { memcpy( context->destination.pub.next_output_byte, context->extra + context->extra_offset, - n); + n + ); context->destination.pub.next_output_byte += n; context->destination.pub.free_in_buffer -= n; context->extra_offset += n; @@ -309,7 +313,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { &context->cinfo, JPEG_COM, (unsigned char *)context->comment, - context->comment_size); + context->comment_size + ); } state->state++; @@ -324,7 +329,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); if (ok != 1) { break; diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index 7dd432b91..52f1ac502 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -17,7 +17,8 @@ int ImagingPackbitsDecode( - Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes +) { UINT8 n; UINT8 *ptr; int i; @@ -79,7 +80,8 @@ ImagingPackbitsDecode( (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index dc67cb41d..86085942a 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -33,7 +33,8 @@ paste( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste opaque region */ int y; @@ -59,7 +60,8 @@ paste_mask_1( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "1" mask */ int x, y; @@ -120,7 +122,8 @@ paste_mask_L( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "L" matte */ int x, y; @@ -167,7 +170,8 @@ paste_mask_RGBA( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "RGBA" matte */ int x, y; @@ -214,7 +218,8 @@ paste_mask_RGBa( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "RGBa" matte */ int x, y; @@ -252,7 +257,8 @@ paste_mask_RGBa( int ImagingPaste( - Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1) { + Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1 +) { int xsize, ysize; int pixelsize; int sx0, sy0; @@ -315,13 +321,15 @@ ImagingPaste( } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { ImagingSectionEnter(&cookie); paste_mask_RGBA( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize + ); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "RGBa") == 0) { ImagingSectionEnter(&cookie); paste_mask_RGBa( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize + ); ImagingSectionLeave(&cookie); } else { @@ -334,13 +342,8 @@ ImagingPaste( static inline void fill( - Imaging imOut, - const void *ink_, - int dx, - int dy, - int xsize, - int ysize, - int pixelsize) { + Imaging imOut, const void *ink_, int dx, int dy, int xsize, int ysize, int pixelsize +) { /* fill opaque region */ int x, y; @@ -378,7 +381,8 @@ fill_mask_1( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "1" mask */ int x, y; @@ -425,7 +429,8 @@ fill_mask_L( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "L" matte */ int x, y, i; @@ -484,7 +489,8 @@ fill_mask_RGBA( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "RGBA" matte */ int x, y, i; @@ -529,7 +535,8 @@ fill_mask_RGBa( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "RGBa" matte */ int x, y, i; @@ -565,13 +572,8 @@ fill_mask_RGBa( int ImagingFill2( - Imaging imOut, - const void *ink, - Imaging imMask, - int dx0, - int dy0, - int dx1, - int dy1) { + Imaging imOut, const void *ink, Imaging imMask, int dx0, int dy0, int dx1, int dy1 +) { ImagingSectionCookie cookie; int xsize, ysize; int pixelsize; diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index c95ffc869..942c8dc22 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -68,7 +68,8 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt memmove( &state->buffer[i * state->xsize], &state->buffer[i * stride], - state->xsize); + state->xsize + ); } } /* Got a full line, unpack it */ @@ -76,7 +77,8 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/PcxEncode.c b/src/libImaging/PcxEncode.c index 549614bfd..625cf7ffa 100644 --- a/src/libImaging/PcxEncode.c +++ b/src/libImaging/PcxEncode.c @@ -71,7 +71,8 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->y += 1; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index dd06f3940..6a4060b4b 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,8 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging)ImagingError_ValueError( - "point operation not supported for this mode"); + return (Imaging + )ImagingError_ValueError("point operation not supported for this mode"); } Imaging diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index cdc614536..197f9f3ee 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -103,7 +103,8 @@ static uint32_t pixel_hash(const HashTable *h, const Pixel pixel) { PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); return PIXEL_HASH( - pixel.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale); + pixel.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale + ); } static int @@ -111,9 +112,11 @@ pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); uint32_t A, B; A = PIXEL_HASH( - pixel1.c.r >> d->scale, pixel1.c.g >> d->scale, pixel1.c.b >> d->scale); + pixel1.c.r >> d->scale, pixel1.c.g >> d->scale, pixel1.c.b >> d->scale + ); B = PIXEL_HASH( - pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale); + pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale + ); return (A == B) ? 0 : ((A < B) ? -1 : 1); } @@ -129,7 +132,8 @@ new_count_func(const HashTable *h, const Pixel key, uint32_t *val) { static void rehash_collide( - const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval) { + const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval +) { *valp += newval; } @@ -157,7 +161,8 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { #endif for (i = 0; i < nPixels; i++) { if (!hashtable_insert_or_update_computed( - hash, pixelData[i], new_count_func, exists_count_func)) { + hash, pixelData[i], new_count_func, exists_count_func + )) { ; } while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { @@ -335,7 +340,8 @@ splitlists( PixelList *nt[2][3], uint32_t nCount[2], int axis, - uint32_t pixelCount) { + uint32_t pixelCount +) { uint32_t left; PixelList *l, *r, *c, *n; @@ -387,7 +393,8 @@ splitlists( _prevCount[2], _nextCount[0], _nextCount[1], - _nextCount[2]); + _nextCount[2] + ); exit(1); } } @@ -531,12 +538,14 @@ split(BoxNode *node) { if (node->tail[_i]->next[_i]) { printf("tail is not tail\n"); printf( - "node->tail[%d]->next[%d]=%p\n", _i, _i, node->tail[_i]->next[_i]); + "node->tail[%d]->next[%d]=%p\n", _i, _i, node->tail[_i]->next[_i] + ); } if (node->head[_i]->prev[_i]) { printf("head is not head\n"); printf( - "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i]); + "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i] + ); } } @@ -573,14 +582,16 @@ split(BoxNode *node) { _prevCount[2], _nextCount[0], _nextCount[1], - _nextCount[2]); + _nextCount[2] + ); } } } #endif node->axis = axis; if (!splitlists( - node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount)) { + node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount + )) { #ifndef NO_OUTPUT printf("list split failed.\n"); #endif @@ -772,7 +783,8 @@ _distance_index_cmp(const void *a, const void *b) { static int resort_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries +) { uint32_t i, j, k; uint32_t **skRow; uint32_t *skElt; @@ -801,7 +813,8 @@ resort_distance_tables( static int build_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries +) { uint32_t i, j; DistanceWithIndex *dwi; @@ -841,7 +854,8 @@ map_image_pixels( uint32_t nPaletteEntries, uint32_t *avgDist, uint32_t **avgDistSortKey, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -888,7 +902,8 @@ map_image_pixels_from_quantized_pixels( uint32_t **avgDistSortKey, uint32_t *pixelArray, uint32_t *avg[3], - uint32_t *count) { + uint32_t *count +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -946,7 +961,8 @@ map_image_pixels_from_median_box( HashTable *medianBoxHash, uint32_t *avgDist, uint32_t **avgDistSortKey, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -998,7 +1014,8 @@ compute_palette_from_median_cut( uint32_t nPixels, HashTable *medianBoxHash, Pixel **palette, - uint32_t nPaletteEntries) { + uint32_t nPaletteEntries +) { uint32_t i; uint32_t paletteEntry; Pixel *p; @@ -1055,7 +1072,8 @@ compute_palette_from_median_cut( printf( "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", (int)paletteEntry, - (int)nPaletteEntries); + (int)nPaletteEntries + ); #endif for (i = 0; i < 3; i++) { free(avg[i]); @@ -1092,7 +1110,8 @@ compute_palette_from_median_cut( static int recompute_palette_from_averages( - Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count) { + Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count +) { uint32_t i; for (i = 0; i < nPaletteEntries; i++) { @@ -1111,7 +1130,8 @@ compute_palette_from_quantized_pixels( uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count, - uint32_t *qp) { + uint32_t *qp +) { uint32_t i; memset(count, 0, sizeof(uint32_t) * nPaletteEntries); @@ -1145,7 +1165,8 @@ k_means( Pixel *paletteData, uint32_t nPaletteEntries, uint32_t *qp, - int threshold) { + int threshold +) { uint32_t *avg[3]; uint32_t *count; uint32_t i; @@ -1194,16 +1215,19 @@ k_means( while (1) { if (!built) { compute_palette_from_quantized_pixels( - pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); + pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp + ); if (!build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + avgDist, avgDistSortKey, paletteData, nPaletteEntries + )) { goto error_3; } built = 1; } else { recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); resort_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries); + avgDist, avgDistSortKey, paletteData, nPaletteEntries + ); } changes = map_image_pixels_from_quantized_pixels( pixelData, @@ -1214,7 +1238,8 @@ k_means( avgDistSortKey, qp, avg, - count); + count + ); if (changes < 0) { goto error_3; } @@ -1273,7 +1298,8 @@ quantize( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int kmeans) { + int kmeans +) { PixelList *hl[3]; HashTable *h; BoxNode *root; @@ -1399,7 +1425,8 @@ quantize( } if (!map_image_pixels_from_median_box( - pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { + pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp + )) { goto error_7; } @@ -1445,7 +1472,8 @@ quantize( _SQR(pixelData[i].c.b - p[qp[i]].c.b))), sqrt((double)(_SQR(pixelData[i].c.r - p[bestmatch].c.r) + _SQR(pixelData[i].c.g - p[bestmatch].c.g) + - _SQR(pixelData[i].c.b - p[bestmatch].c.b)))); + _SQR(pixelData[i].c.b - p[bestmatch].c.b))) + ); } } hashtable_free(h2); @@ -1545,7 +1573,8 @@ quantize2( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int kmeans) { + int kmeans +) { HashTable *h; uint32_t i; uint32_t mean[3]; @@ -1609,7 +1638,8 @@ quantize2( } if (!map_image_pixels( - pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { + pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp + )) { goto error_4; } if (kmeans > 0) { @@ -1752,7 +1782,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - kmeans); + kmeans + ); break; case 1: /* maximum coverage */ @@ -1763,7 +1794,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - kmeans); + kmeans + ); break; case 2: result = quantize_octree( @@ -1773,7 +1805,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - withAlpha); + withAlpha + ); break; case 3: #ifdef HAVE_LIBIMAGEQUANT @@ -1785,7 +1818,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - withAlpha); + withAlpha + ); #else result = -1; #endif @@ -1836,7 +1870,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { if (result == -1) { return (Imaging)ImagingError_ValueError( "dependency required by this method was not " - "enabled at compile time"); + "enabled at compile time" + ); } return (Imaging)ImagingError_ValueError("quantization error"); diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index ea75d6037..bf2f29fde 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -132,7 +132,8 @@ _hashtable_resize(HashTable *h) { static int _hashtable_insert_node( - HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf) { + HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf +) { uint32_t hash = h->hashFunc(h, node->key) % h->length; HashNode **n, *nv; int i; @@ -207,7 +208,8 @@ _hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val, int resize, int up int hashtable_insert_or_update_computed( - HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc) { + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc +) { HashNode **n, *nv; HashNode *t; int i; diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index fc1a99003..0462cfd49 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -20,13 +20,12 @@ typedef uint32_t HashVal_t; typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); -typedef void (*IteratorFunc)( - const HashTable *, const HashKey_t, const HashVal_t, void *); -typedef void (*IteratorUpdateFunc)( - const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*IteratorFunc)(const HashTable *, const HashKey_t, const HashVal_t, void *); +typedef void (*IteratorUpdateFunc)(const HashTable *, const HashKey_t, HashVal_t *, void *); typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); typedef void (*CollisionFunc)( - const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t); + const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t +); HashTable * hashtable_new(HashFunc hf, HashCmpFunc cf); @@ -42,7 +41,8 @@ int hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp); int hashtable_insert_or_update_computed( - HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc); + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc +); void * hashtable_set_user_data(HashTable *h, void *data); void * diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 1331a30ad..7e02ebf65 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -107,11 +107,8 @@ free_color_cube(ColorCube cube) { static long color_bucket_offset_pos( - const ColorCube cube, - unsigned int r, - unsigned int g, - unsigned int b, - unsigned int a) { + const ColorCube cube, unsigned int r, unsigned int g, unsigned int b, unsigned int a +) { return r << cube->rOffset | g << cube->gOffset | b << cube->bOffset | a << cube->aOffset; } @@ -191,7 +188,8 @@ create_sorted_color_palette(const ColorCube cube) { buckets, cube->size, sizeof(struct _ColorBucket), - (int (*)(void const *, void const *)) & compare_bucket_count); + (int (*)(void const *, void const *)) & compare_bucket_count + ); return buckets; } @@ -212,7 +210,8 @@ copy_color_cube( unsigned int rBits, unsigned int gBits, unsigned int bBits, - unsigned int aBits) { + unsigned int aBits +) { unsigned int r, g, b, a; long src_pos, dst_pos; unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; @@ -262,15 +261,18 @@ copy_color_cube( r >> src_reduce[0], g >> src_reduce[1], b >> src_reduce[2], - a >> src_reduce[3]); + a >> src_reduce[3] + ); dst_pos = color_bucket_offset_pos( result, r >> dst_reduce[0], g >> dst_reduce[1], b >> dst_reduce[2], - a >> dst_reduce[3]); + a >> dst_reduce[3] + ); add_bucket_values( - &cube->buckets[src_pos], &result->buckets[dst_pos]); + &cube->buckets[src_pos], &result->buckets[dst_pos] + ); } } } @@ -328,7 +330,8 @@ combined_palette( ColorBucket bucketsA, unsigned long nBucketsA, ColorBucket bucketsB, - unsigned long nBucketsB) { + unsigned long nBucketsB +) { ColorBucket result; if (nBucketsA > LONG_MAX - nBucketsB || (nBucketsA + nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { @@ -366,7 +369,8 @@ map_image_pixels( const Pixel *pixelData, uint32_t nPixels, const ColorCube lookupCube, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { long i; for (i = 0; i < nPixels; i++) { pixelArray[i] = lookup_color(lookupCube, &pixelData[i]); @@ -384,7 +388,8 @@ quantize_octree( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int withAlpha) { + int withAlpha +) { ColorCube fineCube = NULL; ColorCube coarseCube = NULL; ColorCube lookupCube = NULL; @@ -461,7 +466,8 @@ quantize_octree( subtract_color_buckets( coarseCube, &paletteBucketsFine[nAlreadySubtracted], - nFineColors - nAlreadySubtracted); + nFineColors - nAlreadySubtracted + ); } /* create our palette buckets with fine and coarse combined */ @@ -470,7 +476,8 @@ quantize_octree( goto error; } paletteBuckets = combined_palette( - paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); + paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors + ); free(paletteBucketsFine); paletteBucketsFine = NULL; @@ -491,7 +498,8 @@ quantize_octree( /* expand coarse cube (64) to larger fine cube (4k). the value of each coarse bucket is then present in the according 64 fine buckets. */ lookupCube = copy_color_cube( - coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); + coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3] + ); if (!lookupCube) { goto error; } diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index 7a36300e4..a2258c3a2 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -26,7 +26,8 @@ quantize_pngquant( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int withAlpha) { + int withAlpha +) { int result = 0; liq_image *image = NULL; liq_attr *attr = NULL; diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d65e42590..ae96a52f3 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -12,6 +12,7 @@ quantize_pngquant( Pixel **, uint32_t *, uint32_t **, - int); + int +); #endif diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 73a6baecb..899b1fd3a 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -102,7 +102,8 @@ MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) memcpy( \ buf + i * size, \ &IMAGING_PIXEL_##type(im, x, y + i), \ - size * sizeof(type)); \ + size * sizeof(type) \ + ); \ } \ IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ } \ diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 24abe4804..80ed0d688 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -74,7 +74,8 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt state->shuffle( (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, ptr, - state->xsize); + state->xsize + ); ptr += state->bytes; bytes -= state->bytes; diff --git a/src/libImaging/RawEncode.c b/src/libImaging/RawEncode.c index 50de8d982..5e60e1106 100644 --- a/src/libImaging/RawEncode.c +++ b/src/libImaging/RawEncode.c @@ -65,7 +65,8 @@ ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->shuffle( ptr, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (state->bytes > state->count) { /* zero-pad the buffer, if necessary */ diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 61566f0c5..022daa000 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -82,7 +82,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale } } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -124,7 +125,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -171,7 +173,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -226,7 +229,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { ss3 += line[xx * 4 + 3]; } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -251,7 +255,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -278,7 +283,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -329,7 +335,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { ss3 += line[xx * 4 + 3]; } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -351,7 +358,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -375,7 +383,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -425,7 +434,8 @@ ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; v = MAKE_UINT32( - (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -440,7 +450,8 @@ ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, - (ss3 + amend) >> 1); + (ss3 + amend) >> 1 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -488,7 +499,8 @@ ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; v = MAKE_UINT32( - (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -503,7 +515,8 @@ ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, - (ss3 + amend) >> 1); + (ss3 + amend) >> 1 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -558,7 +571,8 @@ ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + line1[xx * 4 + 6]; v = MAKE_UINT32( - (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0); + (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -577,7 +591,8 @@ ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, - (ss3 + amend) >> 2); + (ss3 + amend) >> 2 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -623,7 +638,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -637,7 +653,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -652,7 +669,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -694,7 +712,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -708,7 +727,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -723,7 +743,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -775,7 +796,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -795,7 +817,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -818,7 +841,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -900,7 +924,8 @@ ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + line3[xx * 4 + 14]; v = MAKE_UINT32( - (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0); + (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -935,7 +960,8 @@ ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, - (ss3 + amend) >> 4); + (ss3 + amend) >> 4 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1007,7 +1033,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -1045,7 +1072,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -1092,7 +1120,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1181,7 +1210,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1207,7 +1237,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1232,7 +1263,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1240,7 +1272,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys void ImagingReduceNxN_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale +) { /* The most general implementation for any xscale and yscale */ int x, y, xx, yy; @@ -1313,7 +1346,8 @@ ImagingReduceNxN_32bpc( void ImagingReduceCorners_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale +) { /* Fill the last row and the last column for any xscale and yscale. */ int x, y, xx, yy; @@ -1427,7 +1461,8 @@ ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { } imOut = ImagingNewDirty( - imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); + imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale + ); if (!imOut) { return NULL; } diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 59c27b3f4..222d6bca4 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -186,7 +186,8 @@ precompute_coeffs( int outSize, struct filter *filterp, int **boundsp, - double **kkp) { + double **kkp +) { double support, scale, filterscale; double center, ww, ss; int xx, x, ksize, xmin, xmax; @@ -284,7 +285,8 @@ normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) { void ImagingResampleHorizontal_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk +) { ImagingSectionCookie cookie; int ss0, ss1, ss2, ss3; int xx, yy, x, xmin, xmax; @@ -376,7 +378,8 @@ ImagingResampleHorizontal_8bpc( void ImagingResampleVertical_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk +) { ImagingSectionCookie cookie; int ss0, ss1, ss2, ss3; int xx, yy, y, ymin, ymax; @@ -459,7 +462,8 @@ ImagingResampleVertical_8bpc( void ImagingResampleHorizontal_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { ImagingSectionCookie cookie; double ss; int xx, yy, x, xmin, xmax; @@ -502,7 +506,8 @@ ImagingResampleHorizontal_32bpc( void ImagingResampleVertical_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { ImagingSectionCookie cookie; double ss; int xx, yy, y, ymin, ymax; @@ -544,7 +549,8 @@ ImagingResampleVertical_32bpc( } typedef void (*ResampleFunction)( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk); + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +); Imaging ImagingResampleInner( @@ -554,7 +560,8 @@ ImagingResampleInner( struct filter *filterp, float box[4], ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical); + ResampleFunction ResampleVertical +); Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { @@ -609,7 +616,8 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { } return ImagingResampleInner( - imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical); + imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical + ); } Imaging @@ -620,7 +628,8 @@ ImagingResampleInner( struct filter *filterp, float box[4], ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical) { + ResampleFunction ResampleVertical +) { Imaging imTemp = NULL; Imaging imOut = NULL; @@ -634,13 +643,15 @@ ImagingResampleInner( need_vertical = ysize != imIn->ysize || box[1] || box[3] != ysize; ksize_horiz = precompute_coeffs( - imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz); + imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz + ); if (!ksize_horiz) { return NULL; } ksize_vert = precompute_coeffs( - imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert); + imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert + ); if (!ksize_vert) { free(bounds_horiz); free(kk_horiz); @@ -662,7 +673,8 @@ ImagingResampleInner( imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); if (imTemp) { ResampleHorizontal( - imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); + imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz + ); } free(bounds_horiz); free(kk_horiz); diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 89dedb525..a8db11740 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -114,7 +114,8 @@ expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer static int expandrow2( - UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer +) { UINT8 pixel, count; int x = 0; @@ -252,7 +253,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize - 1]); + &ptr[c->bufsize - 1] + ); } else { status = expandrow2( &state->buffer[c->channo * 2], @@ -260,7 +262,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize - 1]); + &ptr[c->bufsize - 1] + ); } if (status == -1) { state->errcode = IMAGING_CODEC_OVERRUN; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a35..9dc133b0f 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -110,9 +110,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; - } else if ( - strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || - strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { + } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || + strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { /* EXPERIMENTAL */ /* 16-bit raw integer images */ im->bands = 1; @@ -227,7 +226,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize) { return ImagingNewPrologueSubtype( - mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); + mode, xsize, ysize, sizeof(struct ImagingMemoryInstance) + ); } void diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index 9d8e1292a..d3231ad90 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -107,7 +107,8 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index 95ae9b622..fbf29452c 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -93,7 +93,8 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c index aa7e7b96d..dde476614 100644 --- a/src/libImaging/TgaRleEncode.c +++ b/src/libImaging/TgaRleEncode.c @@ -63,7 +63,8 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); } row = state->buffer; @@ -146,7 +147,8 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) } memcpy( - dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount); + dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount + ); dst += flushCount; bytes -= flushCount; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index abffdeabc..18a54f633 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -44,7 +44,8 @@ dump_state(const TIFFSTATE *state) { (int)state->size, (uint)state->eof, state->data, - state->ifd)); + state->ifd) + ); } /* @@ -64,7 +65,8 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { "_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, - state->eof); + state->eof + ); return 0; } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); @@ -200,13 +202,15 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE(("State: context %p \n", state->context)); @@ -226,7 +230,8 @@ _pickUnpackers( ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, - ImagingShuffler *unpackers) { + ImagingShuffler *unpackers +) { // if number of bands is 1, there is no difference with contig case if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { uint16_t bits_per_sample = 8; @@ -356,7 +361,8 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { (UINT8 *)im->image[state->y + state->yoff + current_row] + state->xoff * im->pixelsize, state->buffer + current_row * row_byte_size, - state->xsize); + state->xsize + ); } } @@ -374,7 +380,8 @@ _decodeTile( ImagingCodecState state, TIFF *tiff, int planes, - ImagingShuffler *unpackers) { + ImagingShuffler *unpackers +) { INT32 x, y, tile_y, current_tile_length, current_tile_width; UINT32 tile_width, tile_length; tsize_t tile_bytes_size, row_byte_size; @@ -453,7 +460,8 @@ _decodeTile( ("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, - current_tile_width)); + current_tile_width) + ); // UINT8 * bbb = state->buffer + tile_y * row_byte_size; // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], @@ -462,7 +470,8 @@ _decodeTile( shuffler( (UINT8 *)im->image[tile_y + y] + x * im->pixelsize, state->buffer + tile_y * row_byte_size, - current_tile_width); + current_tile_width + ); } } } @@ -477,7 +486,8 @@ _decodeStrip( ImagingCodecState state, TIFF *tiff, int planes, - ImagingShuffler *unpackers) { + ImagingShuffler *unpackers +) { INT32 strip_row = 0; UINT8 *new_data; UINT32 rows_per_strip; @@ -544,9 +554,10 @@ _decodeStrip( tiff, TIFFComputeStrip(tiff, state->y, plane), (tdata_t)state->buffer, - strip_size) == -1) { - TRACE( - ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); + strip_size + ) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + ); state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -567,7 +578,8 @@ _decodeStrip( (UINT8 *)im->image[state->y + state->yoff + strip_row] + state->xoff * im->pixelsize, state->buffer + strip_row * row_byte_size, - state->xsize); + state->xsize + ); } } } @@ -577,7 +589,8 @@ _decodeStrip( int ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char *filename = "tempfile.tif"; char *mode = "rC"; @@ -602,13 +615,15 @@ ImagingLibTiffDecode( state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE( ("Buffer: %p: %c%c%c%c\n", @@ -616,26 +631,30 @@ ImagingLibTiffDecode( (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (char)buffer[3]) + ); TRACE( ("State->Buffer: %c%c%c%c\n", (char)state->buffer[0], (char)state->buffer[1], (char)state->buffer[2], - (char)state->buffer[3])); + (char)state->buffer[3]) + ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", im->mode, im->type, im->bands, im->xsize, - im->ysize)); + im->ysize) + ); TRACE( ("Image: image8 %p, image32 %p, image %p, block %p \n", im->image8, im->image32, im->image, - im->block)); + im->block) + ); TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); dump_state(clientstate); @@ -665,7 +684,8 @@ ImagingLibTiffDecode( _tiffCloseProc, _tiffSizeProc, _tiffMapProc, - _tiffUnmapProc); + _tiffUnmapProc + ); } if (!tiff) { @@ -694,7 +714,8 @@ ImagingLibTiffDecode( state->xsize, img_width, state->ysize, - img_height)); + img_height) + ); state->errcode = IMAGING_CODEC_BROKEN; goto decode_err; } @@ -739,7 +760,8 @@ ImagingLibTiffDecode( INT32 y; TIFFGetFieldDefaulted( - tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); + tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo + ); if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { @@ -793,13 +815,15 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE(("State: context %p \n", state->context)); @@ -839,7 +863,8 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { _tiffCloseProc, _tiffSizeProc, _tiffNullMapProc, - _tiffUnmapProc); /*force no mmap*/ + _tiffUnmapProc + ); /*force no mmap*/ } if (!clientstate->tiff) { @@ -852,7 +877,8 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { int ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length +) { // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; uint32_t n; @@ -874,7 +900,8 @@ ImagingLibTiffMergeFieldInfo( FIELD_CUSTOM, 1, passcount, - "CustomField"}}; + "CustomField"} + }; n = sizeof(info) / sizeof(info[0]); @@ -922,13 +949,15 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE( ("Buffer: %p: %c%c%c%c\n", @@ -936,26 +965,30 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (char)buffer[3]) + ); TRACE( ("State->Buffer: %c%c%c%c\n", (char)state->buffer[0], (char)state->buffer[1], (char)state->buffer[2], - (char)state->buffer[3])); + (char)state->buffer[3]) + ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", im->mode, im->type, im->bands, im->xsize, - im->ysize)); + im->ysize) + ); TRACE( ("Image: image8 %p, image32 %p, image %p, block %p \n", im->image8, im->image32, im->image, - im->block)); + im->block) + ); TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); dump_state(clientstate); @@ -967,10 +1000,12 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (TIFFWriteScanline( - tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0 + ) == -1) { TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); @@ -1013,7 +1048,8 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (char)buffer[3]) + ); if (clientstate->loc == clientstate->eof) { TRACE(("Hit EOF, calling an end, freeing data")); state->errcode = IMAGING_CODEC_END; diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 212b7dee6..22361210d 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -41,7 +41,8 @@ extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length +); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index eaa4374e3..c23d5d889 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -104,7 +104,8 @@ static UINT8 BITFLIP[] = { 3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, 11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, 7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, - 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255}; + 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255 +}; /* Unpack to "1" image */ @@ -882,7 +883,8 @@ unpackRGBa16L(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[1] * 255 / a), CLIP8(in[3] * 255 / a), CLIP8(in[5] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 8; @@ -906,7 +908,8 @@ unpackRGBa16B(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[2] * 255 / a), CLIP8(in[4] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 8; @@ -930,7 +933,8 @@ unpackRGBa(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 4; @@ -954,7 +958,8 @@ unpackRGBaskip1(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } in += 5; } @@ -976,7 +981,8 @@ unpackRGBaskip2(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } in += 6; } @@ -998,7 +1004,8 @@ unpackBGRa(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[2] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[0] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 4; @@ -1029,7 +1036,8 @@ unpackRGBAL(UINT8 *_out, const UINT8 *in, int pixels) { in[i], in[i + pixels], in[i + pixels + pixels], - in[i + pixels + pixels + pixels]); + in[i + pixels + pixels + pixels] + ); memcpy(_out, &iv, sizeof(iv)); } } diff --git a/src/libImaging/UnpackYCC.c b/src/libImaging/UnpackYCC.c index 0b177bdd4..35b0c3b69 100644 --- a/src/libImaging/UnpackYCC.c +++ b/src/libImaging/UnpackYCC.c @@ -34,7 +34,8 @@ static INT16 L[] = { 261, 262, 264, 265, 266, 268, 269, 270, 272, 273, 274, 276, 277, 278, 280, 281, 283, 284, 285, 287, 288, 289, 291, 292, 293, 295, 296, 297, 299, 300, 302, 303, 304, 306, 307, 308, 310, 311, 312, 314, 315, 317, 318, 319, 321, 322, 323, 325, - 326, 327, 329, 330, 331, 333, 334, 336, 337, 338, 340, 341, 342, 344, 345, 346}; + 326, 327, 329, 330, 331, 333, 334, 336, 337, 338, 340, 341, 342, 344, 345, 346 +}; static INT16 CB[] = { -345, -343, -341, -338, -336, -334, -332, -329, -327, -325, -323, -321, -318, -316, @@ -55,7 +56,8 @@ static INT16 CB[] = { 120, 122, 124, 126, 129, 131, 133, 135, 138, 140, 142, 144, 146, 149, 151, 153, 155, 157, 160, 162, 164, 166, 169, 171, 173, 175, 177, 180, 182, 184, 186, 189, 191, 193, 195, 197, 200, 202, 204, 206, 208, 211, - 213, 215, 217, 220}; + 213, 215, 217, 220 +}; static INT16 GB[] = { 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, 62, 62, 61, 61, @@ -73,7 +75,8 @@ static INT16 GB[] = { -14, -15, -15, -16, -16, -17, -17, -18, -18, -18, -19, -19, -20, -20, -21, -21, -21, -22, -22, -23, -23, -24, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, -28, -29, -29, -30, -30, -30, -31, -31, -32, -32, -33, -33, -33, -34, -34, -35, - -35, -36, -36, -36, -37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42}; + -35, -36, -36, -36, -37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42 +}; static INT16 CR[] = { -249, -247, -245, -243, -241, -239, -238, -236, -234, -232, -230, -229, -227, -225, @@ -94,7 +97,8 @@ static INT16 CR[] = { 133, 135, 137, 138, 140, 142, 144, 146, 148, 149, 151, 153, 155, 157, 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 180, 182, 184, 186, 188, 189, 191, 193, 195, 197, 199, 200, 202, 204, 206, 208, - 209, 211, 213, 215}; + 209, 211, 213, 215 +}; static INT16 GR[] = { 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 114, @@ -114,7 +118,8 @@ static INT16 GR[] = { -67, -68, -69, -69, -70, -71, -72, -73, -74, -75, -76, -77, -78, -79, -80, -81, -82, -82, -83, -84, -85, -86, -87, -88, -89, -90, -91, -92, -93, -94, -94, -95, -96, -97, -98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, - -108}; + -108 +}; #define R 0 #define G 1 diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 2853ce903..e714749ef 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -23,7 +23,8 @@ clip8(int in) { Imaging ImagingUnsharpMask( - Imaging imOut, Imaging imIn, float radius, int percent, int threshold) { + Imaging imOut, Imaging imIn, float radius, int percent, int threshold +) { ImagingSectionCookie cookie; Imaging result; diff --git a/src/libImaging/XbmEncode.c b/src/libImaging/XbmEncode.c index eec4c0d84..65cc3c633 100644 --- a/src/libImaging/XbmEncode.c +++ b/src/libImaging/XbmEncode.c @@ -40,7 +40,8 @@ ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->shuffle( state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (state->y < state->ysize - 1) { /* any line but the last */ diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index 874967834..d964ff2ca 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -217,7 +217,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt state->shuffle( (UINT8 *)im->image[state->y] + col * im->pixelsize, state->buffer + context->prefix + i, - 1); + 1 + ); col += COL_INCREMENT[context->pass]; } } else { @@ -229,7 +230,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt UINT8 byte = *(state->buffer + context->prefix + (i / 8)); byte <<= (i % 8); state->shuffle( - (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1); + (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1 + ); col += COL_INCREMENT[context->pass]; } } @@ -253,7 +255,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer + context->prefix, - state->xsize); + state->xsize + ); state->y++; } diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index edbce3682..44f2629cc 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -98,7 +98,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { 15, 9, /* compression strategy (image data are filtered)*/ - compress_type); + compress_type + ); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; @@ -108,7 +109,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { err = deflateSetDictionary( &context->z_stream, (unsigned char *)context->dictionary, - context->dictionary_size); + context->dictionary_size + ); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; @@ -163,7 +165,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer + 1, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->y++; diff --git a/src/map.c b/src/map.c index c298bd148..c66702981 100644 --- a/src/map.c +++ b/src/map.c @@ -72,7 +72,8 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { &offset, &mode, &stride, - &ystep)) { + &ystep + )) { return NULL; } diff --git a/src/path.c b/src/path.c index bd6ad2259..b96e8b78a 100644 --- a/src/path.c +++ b/src/path.c @@ -489,7 +489,8 @@ path_transform(PyPathObject *self, PyObject *args) { double wrap = 0.0; if (!PyArg_ParseTuple( - args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap)) { + args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap + )) { return NULL; } @@ -570,7 +571,8 @@ path_subscript(PyPathObject *self, PyObject *item) { PyErr_Format( PyExc_TypeError, "Path indices must be integers, not %.200s", - Py_TYPE(item)->tp_name); + Py_TYPE(item)->tp_name + ); return NULL; } } @@ -586,7 +588,8 @@ static PySequenceMethods path_as_sequence = { }; static PyMappingMethods path_as_mapping = { - (lenfunc)path_len, (binaryfunc)path_subscript, NULL}; + (lenfunc)path_len, (binaryfunc)path_subscript, NULL +}; static PyTypeObject PyPathType = { PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ From ea7b5c5b667b0bc80a70f9ca4ee123e82450c123 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 10:13:26 +0200 Subject: [PATCH 020/136] Lock around usages of imaging memory arenas --- src/_imaging.c | 48 +++++++++++++++++++++++++++++++--------- src/libImaging/Imaging.h | 12 ++++++++++ src/libImaging/Storage.c | 20 ++++++++++++----- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index ac6310a44..66709d9bc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -92,6 +92,7 @@ #define _USE_MATH_DEFINES #include +#include /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ @@ -3934,7 +3935,6 @@ static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; PyObject *v; - ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { return NULL; @@ -3944,6 +3944,10 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } + + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; + v = PyLong_FromLong(arena->stats_new_count); PyDict_SetItemString(d, "new_count", v ? v : Py_None); Py_XDECREF(v); @@ -3967,22 +3971,25 @@ _get_stats(PyObject *self, PyObject *args) { v = PyLong_FromLong(arena->blocks_cached); PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); Py_XDECREF(v); + + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return d; } static PyObject * _reset_stats(PyObject *self, PyObject *args) { - ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":reset_stats")) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; arena->stats_new_count = 0; arena->stats_allocated_blocks = 0; arena->stats_reused_blocks = 0; arena->stats_reallocated_blocks = 0; arena->stats_freed_blocks = 0; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -3994,7 +4001,10 @@ _get_alignment(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.alignment); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int alignment = ImagingDefaultArena.alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(alignment); } static PyObject * @@ -4003,7 +4013,10 @@ _get_block_size(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.block_size); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int block_size = ImagingDefaultArena.block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(block_size); } static PyObject * @@ -4012,7 +4025,10 @@ _get_blocks_max(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.blocks_max); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int blocks_max = ImagingDefaultArena.blocks_max; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(blocks_max); } static PyObject * @@ -4032,7 +4048,9 @@ _set_alignment(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.alignment = alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -4055,7 +4073,9 @@ _set_block_size(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.block_size = block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; @@ -4071,14 +4091,20 @@ _set_blocks_max(PyObject *self, PyObject *args) { if (blocks_max < 0) { PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); return NULL; - } else if ( - (unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + } + + MUTEX_LOCK(&ImagingDefaultArena.mutex); + size_t blocksize = sizeof(ImagingDefaultArena.blocks_pool[0]); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if ((unsigned long)blocks_max > SIZE_MAX / blocksize) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } - if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int status = ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (!status) { return ImagingError_MemoryError(); } @@ -4094,7 +4120,9 @@ _clear_cache(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingMemoryClearCache(&ImagingDefaultArena, i); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); Py_INCREF(Py_None); return Py_None; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e93..283111216 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -161,6 +161,9 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif } *ImagingMemoryArena; /* Objects */ @@ -690,6 +693,15 @@ _imaging_tell_pyFd(PyObject *fd); #include "ImagingUtils.h" extern UINT8 *clip8_lookups; +/* Mutex lock/unlock helpers */ +#ifdef Py_GIL_DISABLED +#define MUTEX_LOCK(m) PyMutex_Lock(m) +#define MUTEX_UNLOCK(m) PyMutex_Unlock(m) +#else +#define MUTEX_LOCK(m) +#define MUTEX_UNLOCK(m) +#endif + #if defined(__cplusplus) } #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a35..9331ed5d5 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -219,7 +219,9 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return im; } @@ -267,7 +269,8 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, - 0 // Stats + 0, // Stats + {0}, }; int @@ -365,7 +368,9 @@ ImagingDestroyArray(Imaging im) { if (im->blocks) { while (im->blocks[y].ptr) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); memory_return_block(&ImagingDefaultArena, im->blocks[y]); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); y += 1; } free(im->blocks); @@ -373,9 +378,8 @@ ImagingDestroyArray(Imaging im) { } Imaging -ImagingAllocateArray(Imaging im, int dirty, int block_size) { +ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_size) { int y, line_in_block, current_block; - ImagingMemoryArena arena = &ImagingDefaultArena; ImagingMemoryBlock block = {NULL, 0}; int aligned_linesize, lines_per_block, blocks_count; char *aligned_ptr = NULL; @@ -498,14 +502,20 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return NULL; } - if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + Imaging tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } ImagingError_Clear(); // Try to allocate the image once more with smallest possible block size - if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, IMAGING_PAGE_SIZE); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } From 9f110aa702b0faef7f1ecc218fce3984d89b389a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:47:12 +0000 Subject: [PATCH 021/136] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/Storage.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 9331ed5d5..8b6d46da6 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -503,7 +503,8 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { } MUTEX_LOCK(&ImagingDefaultArena.mutex); - Imaging tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + Imaging tmp = ImagingAllocateArray( + im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); if (tmp) { return im; From 5999b9b0ccefe916c364490d7de86a71ea383b96 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 16:56:15 +0200 Subject: [PATCH 022/136] Initialize PyMutex only under the free-threaded build --- src/libImaging/Storage.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 8b6d46da6..3a49113b6 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -270,7 +270,9 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats +#ifdef Py_GIL_DISABLED {0}, +#endif }; int From 06767fc3257fdd7932c2c04d7bc83cc26a0899a5 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 17:00:14 +0200 Subject: [PATCH 023/136] Address feedback; do not lock in a loop --- src/libImaging/Storage.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 3a49113b6..a680fe63c 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -369,12 +369,12 @@ ImagingDestroyArray(Imaging im) { int y = 0; if (im->blocks) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); while (im->blocks[y].ptr) { - MUTEX_LOCK(&ImagingDefaultArena.mutex); memory_return_block(&ImagingDefaultArena, im->blocks[y]); - MUTEX_UNLOCK(&ImagingDefaultArena.mutex); y += 1; } + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); free(im->blocks); } } From 98b173928ad434d53c0e9ced859fcf1f14cf8e8f Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 16 Jul 2024 21:31:29 +0200 Subject: [PATCH 024/136] Address more feedback; don't unlock around sizeof --- src/_imaging.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 66709d9bc..15520040f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4093,10 +4093,8 @@ _set_blocks_max(PyObject *self, PyObject *args) { return NULL; } - MUTEX_LOCK(&ImagingDefaultArena.mutex); - size_t blocksize = sizeof(ImagingDefaultArena.blocks_pool[0]); - MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - if ((unsigned long)blocks_max > SIZE_MAX / blocksize) { + if ((unsigned long)blocks_max > SIZE_MAX / + sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } From e144707520f2db6256927028cbaee02f8337b73d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:31:59 +0000 Subject: [PATCH 025/136] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imaging.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 15520040f..f1071b1ce 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4093,8 +4093,8 @@ _set_blocks_max(PyObject *self, PyObject *args) { return NULL; } - if ((unsigned long)blocks_max > SIZE_MAX / - sizeof(ImagingDefaultArena.blocks_pool[0])) { + if ((unsigned long)blocks_max > + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } From 31469407166026ec6d74d2df07196ef2d4e32ab4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jul 2024 08:26:05 +1000 Subject: [PATCH 026/136] Temporarily disable cifuzz --- .github/workflows/cifuzz.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index eb73fc6a7..033ff98ce 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,6 +24,8 @@ concurrency: jobs: Fuzzing: + # Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+ + if: false runs-on: ubuntu-latest steps: - name: Build Fuzzers From a3f93b3f68804d7c459faee165edf021e54f850a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jul 2024 16:23:29 +1000 Subject: [PATCH 027/136] Changed ContainerIO to subclass IO --- Tests/test_file_container.py | 91 +++++++++++++++++++++++++++++++----- pyproject.toml | 1 - src/PIL/ContainerIO.py | 72 ++++++++++++++++++++++++---- src/PIL/TarIO.py | 10 ---- 4 files changed, 142 insertions(+), 32 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 7f76fb47a..237045acc 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Literal - import pytest from PIL import ContainerIO, Image @@ -23,6 +21,13 @@ def test_isatty() -> None: assert container.isatty() is False +def test_seekable() -> None: + with hopper() as im: + container = ContainerIO.ContainerIO(im, 0, 0) + + assert container.seekable() is True + + @pytest.mark.parametrize( "mode, expected_position", ( @@ -31,7 +36,7 @@ def test_isatty() -> None: (2, 100), ), ) -def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: +def test_seek_mode(mode: int, expected_position: int) -> None: # Arrange with open(TEST_FILE, "rb") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -44,6 +49,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: assert container.tell() == expected_position +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readable(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert container.readable() is True + + @pytest.mark.parametrize("bytesmode", (True, False)) def test_read_n0(bytesmode: bool) -> None: # Arrange @@ -51,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(81) + assert container.seek(81) == 81 data = container.read() # Assert @@ -67,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(81) + assert container.seek(81) == 81 data = container.read(3) # Assert @@ -83,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(100) + assert container.seek(100) == 100 data = container.read() # Assert @@ -94,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) def test_readline(bytesmode: bool) -> None: - # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) - # Act data = container.readline() - - # Assert if bytesmode: data = data.decode() assert data == "This is line 1\n" + data = container.readline(4) + if bytesmode: + data = data.decode() + assert data == "This" + @pytest.mark.parametrize("bytesmode", (True, False)) def test_readlines(bytesmode: bool) -> None: + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + data = container.readlines() + if bytesmode: + data = [line.decode() for line in data] + assert data == expected + + assert container.seek(0) == 0 + + data = container.readlines(2) + if bytesmode: + data = [line.decode() for line in data] + assert data == expected[:2] + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_write(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert container.writable() is False + + with pytest.raises(NotImplementedError): + container.write(b"" if bytesmode else "") + with pytest.raises(NotImplementedError): + container.writelines([]) + with pytest.raises(NotImplementedError): + container.truncate() + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_iter(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", @@ -124,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 0, 120) # Act - data = container.readlines() + data = [] + for line in container: + data.append(line) # Assert if bytesmode: data = [line.decode() for line in data] assert data == expected + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_file(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert isinstance(container.fileno(), int) + container.flush() + container.close() diff --git a/pyproject.toml b/pyproject.toml index cd7248669..b76f3c24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,5 +161,4 @@ exclude = [ '^Tests/test_qt_image_qapplication.py$', '^Tests/test_font_pcf_charsets.py$', '^Tests/test_font_pcf.py$', - '^Tests/test_file_tar.py$', ] diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 0035296a4..ec9e66c71 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -16,10 +16,11 @@ from __future__ import annotations import io -from typing import IO, AnyStr, Generic, Literal +from collections.abc import Iterable +from typing import IO, AnyStr, NoReturn -class ContainerIO(Generic[AnyStr]): +class ContainerIO(IO[AnyStr]): """ A file object that provides read access to a part of an existing file (for example a TAR file). @@ -45,7 +46,10 @@ class ContainerIO(Generic[AnyStr]): def isatty(self) -> bool: return False - def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: + def seekable(self) -> bool: + return True + + def seek(self, offset: int, mode: int = io.SEEK_SET) -> int: """ Move file pointer. @@ -53,6 +57,7 @@ class ContainerIO(Generic[AnyStr]): :param mode: Starting position. Use 0 for beginning of region, 1 for current offset, and 2 for end of region. You cannot move the pointer outside the defined region. + :returns: Offset from start of region, in bytes. """ if mode == 1: self.pos = self.pos + offset @@ -63,6 +68,7 @@ class ContainerIO(Generic[AnyStr]): # clamp self.pos = max(0, min(self.pos, self.length)) self.fh.seek(self.offset + self.pos) + return self.pos def tell(self) -> int: """ @@ -72,27 +78,32 @@ class ContainerIO(Generic[AnyStr]): """ return self.pos - def read(self, n: int = 0) -> AnyStr: + def readable(self) -> bool: + return True + + def read(self, n: int = -1) -> AnyStr: """ Read data. - :param n: Number of bytes to read. If omitted or zero, + :param n: Number of bytes to read. If omitted, zero or negative, read until end of region. :returns: An 8-bit string. """ - if n: + if n > 0: n = min(n, self.length - self.pos) else: n = self.length - self.pos - if not n: # EOF + if n <= 0: # EOF return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] self.pos = self.pos + n return self.fh.read(n) - def readline(self) -> AnyStr: + def readline(self, n: int = -1) -> AnyStr: """ Read a line of text. + :param n: Number of bytes to read. If omitted, zero or negative, + read until end of line. :returns: An 8-bit string. """ s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] @@ -102,14 +113,16 @@ class ContainerIO(Generic[AnyStr]): if not c: break s = s + c - if c == newline_character: + if c == newline_character or len(s) == n: break return s - def readlines(self) -> list[AnyStr]: + def readlines(self, n: int | None = -1) -> list[AnyStr]: """ Read multiple lines of text. + :param n: Number of lines to read. If omitted, zero, negative or None, + read until end of region. :returns: A list of 8-bit strings. """ lines = [] @@ -118,4 +131,43 @@ class ContainerIO(Generic[AnyStr]): if not s: break lines.append(s) + if len(lines) == n: + break return lines + + def writable(self) -> bool: + return False + + def write(self, b: AnyStr) -> NoReturn: + raise NotImplementedError() + + def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: + raise NotImplementedError() + + def truncate(self, size: int | None = None) -> int: + raise NotImplementedError() + + def __enter__(self) -> ContainerIO[AnyStr]: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def __iter__(self) -> ContainerIO[AnyStr]: + return self + + def __next__(self) -> AnyStr: + line = self.readline() + if not line: + msg = "end of region" + raise StopIteration(msg) + return line + + def fileno(self) -> int: + return self.fh.fileno() + + def flush(self) -> None: + self.fh.flush() + + def close(self) -> None: + self.fh.close() diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index cba26d4b0..779288b1c 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -55,13 +55,3 @@ class TarIO(ContainerIO.ContainerIO[bytes]): # Open region super().__init__(self.fh, self.fh.tell(), size) - - # Context manager support - def __enter__(self) -> TarIO: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.fh.close() From 10faa5df390c160570110b48bd4cd03fa4390dc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jul 2024 22:08:53 +1000 Subject: [PATCH 028/136] Deprecate lambda_eval and unsafe_eval options argument --- Tests/test_imagemath_lambda_eval.py | 65 ++++++++++++++++------------ Tests/test_imagemath_unsafe_eval.py | 67 ++++++++++++++++------------- docs/deprecations.rst | 9 ++++ docs/reference/ImageMath.rst | 16 +++---- docs/releasenotes/11.0.0.rst | 8 ++-- src/PIL/ImageMath.py | 24 ++++++++--- 6 files changed, 115 insertions(+), 74 deletions(-) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 5769c903e..360325780 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -1,5 +1,9 @@ from __future__ import annotations +from typing import Any + +import pytest + from PIL import Image, ImageMath @@ -19,7 +23,7 @@ I = Image.new("I", (1, 1), 4) # noqa: E741 A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) -images = {"A": A, "B": B, "F": F, "I": I} +images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I} def test_sanity() -> None: @@ -30,13 +34,13 @@ def test_sanity() -> None: == "I 3" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images)) == "I 3" ) assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images + lambda args: args["float"](args["A"]) + args["B"], **images ) ) == "F 3.0" @@ -44,42 +48,47 @@ def test_sanity() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["int"](args["float"](args["A"]) + args["B"]), images + lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images ) ) == "I 3" ) +def test_options_deprecated() -> None: + with pytest.warns(DeprecationWarning): + assert ImageMath.lambda_eval(lambda args: 1, images) == 1 + + def test_ops() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1" assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images)) == "I 3" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images)) == "I -1" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images)) == "I 2" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images)) == "I 0" ) - assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" + assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4" assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) + pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images)) == "I 2147483647" ) assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images + lambda args: args["float"](args["A"]) + args["B"], **images ) ) == "F 3.0" @@ -87,7 +96,7 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) - args["B"], images + lambda args: args["float"](args["A"]) - args["B"], **images ) ) == "F -1.0" @@ -95,7 +104,7 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) * args["B"], images + lambda args: args["float"](args["A"]) * args["B"], **images ) ) == "F 2.0" @@ -103,31 +112,33 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) / args["B"], images + lambda args: args["float"](args["A"]) / args["B"], **images ) ) == "F 0.5" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) + pixel( + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images) + ) == "F 4.0" ) assert ( pixel( - ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images) ) == "F 8589934592.0" ) def test_logical() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 + assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0 assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images)) == "L 2" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images)) == "L 1" ) @@ -136,7 +147,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "L"), images + lambda args: args["convert"](args["A"] + args["B"], "L"), **images ) ) == "L 3" @@ -144,7 +155,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "1"), images + lambda args: args["convert"](args["A"] + args["B"], "1"), **images ) ) == "1 0" @@ -152,7 +163,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "RGB"), images + lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images ) ) == "RGB (3, 3, 3)" @@ -163,7 +174,7 @@ def test_compare() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["min"](args["A"], args["B"]), images + lambda args: args["min"](args["A"], args["B"]), **images ) ) == "I 1" @@ -171,13 +182,13 @@ def test_compare() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["max"](args["A"], args["B"]), images + lambda args: args["max"](args["A"], args["B"]), **images ) ) == "I 2" ) - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0" def test_one_image_larger() -> None: diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index 7b8a562d7..b7ac84691 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageMath @@ -21,16 +23,16 @@ I = Image.new("I", (1, 1), 4) # noqa: E741 A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) -images = {"A": A, "B": B, "F": F, "I": I} +images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I} def test_sanity() -> None: assert ImageMath.unsafe_eval("1") == 1 assert ImageMath.unsafe_eval("1+A", A=2) == 3 assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3" def test_eval_deprecated() -> None: @@ -38,23 +40,28 @@ def test_eval_deprecated() -> None: assert ImageMath.eval("1") == 1 +def test_options_deprecated() -> None: + with pytest.warns(DeprecationWarning): + assert ImageMath.unsafe_eval("1", images) == 1 + + def test_ops() -> None: - assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2" - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" - assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" + assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4" + assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647" - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" - assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" - assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" - assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" - assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" + assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0" + assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0" + assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5" + assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0" + assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0" @pytest.mark.parametrize( @@ -72,33 +79,33 @@ def test_prevent_exec(expression: str) -> None: def test_prevent_double_underscores() -> None: with pytest.raises(ValueError): - ImageMath.unsafe_eval("1", {"__": None}) + ImageMath.unsafe_eval("1", __=None) def test_prevent_builtins() -> None: with pytest.raises(ValueError): - ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) + ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None) def test_logical() -> None: - assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 - assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" - assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" + assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0 + assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1" def test_convert() -> None: - assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" - assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" + assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3" + assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0" assert ( - pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)" ) def test_compare() -> None: - assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0" def test_one_image_larger() -> None: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 792fd1c70..2f5800e07 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -109,6 +109,15 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword +arguments can be used instead. + Removed features ---------------- diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 2535db711..a72ca92d0 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -37,10 +37,10 @@ Example: Using the :py:mod:`~PIL.ImageMath` module :param expression: A function that receives a dictionary. :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. + names to Image instances. Deprecated. + You can instead use one or more keyword arguments, as + shown in the above example. Note that the names must be + valid Python identifiers. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. @@ -62,10 +62,10 @@ Example: Using the :py:mod:`~PIL.ImageMath` module syntax. In addition to the standard operators, you can also use the functions described below. :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. + names to Image instances. Deprecated. + You can instead use one or more keyword arguments, as + shown in the above example. Note that the names must be + valid Python identifiers. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 964423ae0..bb6179de8 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -43,10 +43,12 @@ similarly removed. Deprecations ============ -TODO -^^^^ +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more +keyword arguments can be used instead. API Changes =========== diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 6664434ea..e79eb3ec9 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -249,14 +249,20 @@ def lambda_eval( :py:func:`~PIL.Image.merge` function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. You - can either use a dictionary, or one or more keyword - arguments. + :param options: Values to add to the function's dictionary. Deprecated. + You can instead use one or more keyword arguments. :return: The expression result. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ + if options: + deprecate( + "ImageMath.lambda_eval options", + 12, + "ImageMath.lambda_eval keyword arguments", + ) + args: dict[str, Any] = ops.copy() args.update(options) args.update(kw) @@ -287,14 +293,20 @@ def unsafe_eval( :py:func:`~PIL.Image.merge` function. :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. + :param options: Values to add to the evaluation context. Deprecated. + You can instead use one or more keyword arguments. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ + if options: + deprecate( + "ImageMath.unsafe_eval options", + 12, + "ImageMath.unsafe_eval keyword arguments", + ) + # build execution namespace args: dict[str, Any] = ops.copy() for k in list(options.keys()) + list(kw.keys()): From 47fc36a323892040b1945afd22d036a15fb99abd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jul 2024 22:28:56 +1000 Subject: [PATCH 029/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 856458aa9..a2e02fe62 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Changed ContainerIO to subclass IO #8240 + [radarhere] + +- Move away from APIs that use borrowed references under the free-threaded build #8216 + [hugovk, lysnikolaou] + +- Allow size argument to resize() to be a NumPy array #8201 + [radarhere] + - Drop support for Python 3.8 #8183 [hugovk, radarhere] From f39ca5db5a39c2c82e62fbee1ebf95d5b61bdfba Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 17 Jul 2024 16:21:16 +0200 Subject: [PATCH 030/136] Skip QEMU-emulated wheels on workflow dispatch event --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fa1825e45..10e37f343 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,7 +41,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - if: github.event_name != 'schedule' + if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: From 09c817a2ec7c0dc4988235aa50b8073192b3e9e2 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 17 Jul 2024 23:05:48 +0200 Subject: [PATCH 031/136] Move uploading nightly wheels to different job This is needed cause the job does not support running on OS's other than Ubuntu. --- .github/workflows/wheels.yml | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 10e37f343..087d2bd56 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -149,13 +149,6 @@ jobs: name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl - - name: Upload wheels to scientific-python-nightly-wheels - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 - with: - artifacts_path: ./wheelhouse - anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} - windows: name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest @@ -242,13 +235,6 @@ jobs: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* - - name: Upload wheels to scientific-python-nightly-wheels - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 - with: - artifacts_path: ./wheelhouse - anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} - sdist: if: github.event_name != 'schedule' runs-on: ubuntu-latest @@ -269,6 +255,23 @@ jobs: name: dist-sdist path: dist/*.tar.gz + scientific-python-nightly-wheels-publish: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + needs: [build-2-native-wheels, windows] + runs-on: ubuntu-latest + name: Upload release to PyPI + steps: + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: Upload wheels to scientific-python-nightly-wheels + uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + with: + artifacts_path: dist + anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} + pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] From 7248cde50b985aba3198b70201fed462a15997d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jul 2024 11:00:27 +1000 Subject: [PATCH 032/136] Documented keyword arguments --- docs/reference/ImageMath.rst | 22 ++++++++++++---------- src/PIL/ImageMath.py | 2 ++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index a72ca92d0..f4e1081e6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -31,20 +31,21 @@ Example: Using the :py:mod:`~PIL.ImageMath` module b=im2 ) -.. py:function:: lambda_eval(expression, options) +.. py:function:: lambda_eval(expression, options, **kw) Returns the result of an image function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. Deprecated. + :param options: Values to add to the function's dictionary. Note that the names + must be valid Python identifiers. Deprecated. You can instead use one or more keyword arguments, as - shown in the above example. Note that the names must be - valid Python identifiers. + shown in the above example. + :param \**kw: Values to add to the function's dictionary, mapping image names to + Image instances. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. -.. py:function:: unsafe_eval(expression, options) +.. py:function:: unsafe_eval(expression, options, **kw) Evaluates an image expression. @@ -61,11 +62,12 @@ Example: Using the :py:mod:`~PIL.ImageMath` module :param expression: A string which uses the standard Python expression syntax. In addition to the standard operators, you can also use the functions described below. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. Deprecated. + :param options: Values to add to the evaluation context. Note that the names must + be valid Python identifiers. Deprecated. You can instead use one or more keyword arguments, as - shown in the above example. Note that the names must be - valid Python identifiers. + shown in the above example. + :param \**kw: Values to add to the evaluation context, mapping image names to Image + instances. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index e79eb3ec9..191cc2a5f 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -251,6 +251,7 @@ def lambda_eval( :param expression: A function that receives a dictionary. :param options: Values to add to the function's dictionary. Deprecated. You can instead use one or more keyword arguments. + :param **kw: Values to add to the function's dictionary. :return: The expression result. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. @@ -295,6 +296,7 @@ def unsafe_eval( :param expression: A string containing a Python-style expression. :param options: Values to add to the evaluation context. Deprecated. You can instead use one or more keyword arguments. + :param **kw: Values to add to the evaluation context. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. From a6e93939566619aa1eb10fd5531aa59bb6e39bdc Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 18 Jul 2024 10:35:09 +0200 Subject: [PATCH 033/136] Update .github/workflows/wheels.yml Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 087d2bd56..ffd1abd95 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -259,7 +259,7 @@ jobs: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' needs: [build-2-native-wheels, windows] runs-on: ubuntu-latest - name: Upload release to PyPI + name: Upload wheels to scientific-python-nightly-wheels steps: - uses: actions/download-artifact@v4 with: From 73dfe67736565805f4c769cdfc92447f2b098187 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jul 2024 22:46:02 +1000 Subject: [PATCH 034/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a2e02fe62..bd8d3af03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 + [radarhere] + - Changed ContainerIO to subclass IO #8240 [radarhere] From 18d8020cab65a4aaca528f7848adfa06a9de5b85 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Thu, 18 Jul 2024 15:17:32 -0400 Subject: [PATCH 035/136] Add tutorial images --- docs/handbook/contrasted_hopper.jpg | Bin 0 -> 5572 bytes docs/handbook/cropped_hopper.jpg | Bin 0 -> 2017 bytes docs/handbook/enhanced_hopper.jpg | Bin 0 -> 5721 bytes docs/handbook/flip_left_right_hopper.jpg | Bin 0 -> 4637 bytes docs/handbook/flip_top_bottom_hopper.jpg | Bin 0 -> 4652 bytes docs/handbook/masked_hopper.jpg | Bin 0 -> 4904 bytes docs/handbook/merged_hopper.png | Bin 0 -> 35390 bytes docs/handbook/pasted_hopper.jpg | Bin 0 -> 4645 bytes docs/handbook/rebanded_hopper.jpg | Bin 0 -> 4743 bytes docs/handbook/rolled_hopper.jpg | Bin 0 -> 4651 bytes docs/handbook/rotated_hopper.jpg | Bin 0 -> 4895 bytes docs/handbook/rotated_hopper_180.jpg | Bin 0 -> 4636 bytes docs/handbook/rotated_hopper_270.jpg | Bin 0 -> 4895 bytes docs/handbook/rotated_hopper_90.jpg | Bin 0 -> 4903 bytes docs/handbook/show_hopper.png | Bin 0 -> 56122 bytes docs/handbook/thumbnail_hopper.jpg | Bin 0 -> 1984 bytes docs/handbook/transformed_hopper.jpg | Bin 0 -> 3608 bytes docs/handbook/tutorial.rst | 67 +++++++++++++++++++++++ 18 files changed, 67 insertions(+) create mode 100644 docs/handbook/contrasted_hopper.jpg create mode 100644 docs/handbook/cropped_hopper.jpg create mode 100644 docs/handbook/enhanced_hopper.jpg create mode 100644 docs/handbook/flip_left_right_hopper.jpg create mode 100644 docs/handbook/flip_top_bottom_hopper.jpg create mode 100644 docs/handbook/masked_hopper.jpg create mode 100644 docs/handbook/merged_hopper.png create mode 100644 docs/handbook/pasted_hopper.jpg create mode 100644 docs/handbook/rebanded_hopper.jpg create mode 100644 docs/handbook/rolled_hopper.jpg create mode 100644 docs/handbook/rotated_hopper.jpg create mode 100644 docs/handbook/rotated_hopper_180.jpg create mode 100644 docs/handbook/rotated_hopper_270.jpg create mode 100644 docs/handbook/rotated_hopper_90.jpg create mode 100644 docs/handbook/show_hopper.png create mode 100644 docs/handbook/thumbnail_hopper.jpg create mode 100644 docs/handbook/transformed_hopper.jpg diff --git a/docs/handbook/contrasted_hopper.jpg b/docs/handbook/contrasted_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2d7cfacbb6b62fbcbbbc14aba4e955027bd201d GIT binary patch literal 5572 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/cropped_hopper.jpg b/docs/handbook/cropped_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..912d0a29cb0d154e7b7062123863630e8a1ef3a8 GIT binary patch literal 2017 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/flip_left_right_hopper.jpg b/docs/handbook/flip_left_right_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1ef519e5cd8a34c65d681071b46318e3dd7f6a4 GIT binary patch literal 4637 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/flip_top_bottom_hopper.jpg b/docs/handbook/flip_top_bottom_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03a5020f3904067cb13d2ea918dd382dc4e29cc5 GIT binary patch literal 4652 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 D@-E{; literal 0 HcmV?d00001 diff --git a/docs/handbook/masked_hopper.jpg b/docs/handbook/masked_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f70a5ec4769e9c861fcfd4e1e90f812cd4369782 GIT binary patch literal 4904 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/merged_hopper.png b/docs/handbook/merged_hopper.png new file mode 100644 index 0000000000000000000000000000000000000000..662816cbdad42a04dd522acbbd195c2d789fb87b GIT binary patch literal 35390 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/rolled_hopper.jpg b/docs/handbook/rolled_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2220f39cbe11fa43e25b698a0dae49a4fadc39f GIT binary patch literal 4651 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 AApigX literal 0 HcmV?d00001 diff --git a/docs/handbook/rotated_hopper.jpg b/docs/handbook/rotated_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e4d22be70ce2bc4887a41799e1343fa0cc513cd9 GIT binary patch literal 4895 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/show_hopper.png b/docs/handbook/show_hopper.png new file mode 100644 index 0000000000000000000000000000000000000000..1f2e6f243858d8a9944c4db0b1c0758111cc45ce GIT binary patch literal 56122 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/transformed_hopper.jpg b/docs/handbook/transformed_hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33afe67e62be8f3e2922c5cfa5c0018774ac0b5c GIT binary patch literal 3608 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 im.show() +.. image:: show_hopper.png + :align: center + .. note:: The standard version of :py:meth:`~PIL.Image.Image.show` is not very @@ -79,6 +82,9 @@ Convert files to JPEG except OSError: print("cannot convert", infile) +.. image:: ../../Tests/images/hopper.jpg + :align: center + A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` method which explicitly specifies a file format. If you use a non-standard extension, you must always specify the format this way: @@ -103,6 +109,9 @@ Create JPEG thumbnails except OSError: print("cannot create thumbnail for", infile) +.. image:: thumbnail_hopper.jpg + :align: center + It is important to note that the library doesn’t decode or load the raster data unless it really has to. When you open a file, the file header is read to determine the file format and extract things like mode, size, and other @@ -150,6 +159,9 @@ pixels, so the region in the above example is exactly 300x300 pixels. The region could now be processed in a certain manner and pasted back. +.. image:: cropped_hopper.jpg + :align: center + Processing a subrectangle, and pasting it back ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -164,6 +176,9 @@ modes of the original image and the region do not need to match. If they don’t the region is automatically converted before being pasted (see the section on :ref:`color-transforms` below for details). +.. image:: pasted_hopper.jpg + :align: center + Here’s an additional example: Rolling an image @@ -186,6 +201,9 @@ Rolling an image return im +.. image:: rolled_hopper.jpg + :align: center + Or if you would like to merge two images into a wider image: Merging images @@ -203,6 +221,9 @@ Merging images return im +.. image:: merged_hopper.png + :align: center + For more advanced tricks, the paste method can also take a transparency mask as an optional argument. In this mask, the value 255 indicates that the pasted image is opaque in that position (that is, the pasted image should be used as @@ -229,6 +250,9 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns the image itself. To work with individual color bands, you may want to convert the image to “RGB” first. +.. image:: rebanded_hopper.jpg + :align: center + Geometrical transforms ---------------------- @@ -245,6 +269,9 @@ Simple geometry transforms out = im.resize((128, 128)) out = im.rotate(45) # degrees counter-clockwise +.. image:: rotated_hopper_90.jpg + :align: center + To rotate the image in 90 degree steps, you can either use the :py:meth:`~PIL.Image.Image.rotate` method or the :py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to @@ -256,11 +283,38 @@ Transposing an image :: out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + +.. image:: flip_left_right_hopper.jpg + :align: center + +:: + out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + +.. image:: flip_top_bottom_hopper.jpg + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_90) + +.. image:: rotated_hopper_90.jpg + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_180) + +.. image:: rotated_hopper_180.jpg + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_270) +.. image:: rotated_hopper_270.jpg + :align: center + ``transpose(ROTATE)`` operations can also be performed identically with :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. @@ -342,6 +396,9 @@ Applying filters from PIL import ImageFilter out = im.filter(ImageFilter.DETAIL) +.. image:: enhanced_hopper.jpg + :align: center + Point Operations ^^^^^^^^^^^^^^^^ @@ -358,6 +415,9 @@ Applying point transforms # multiply each pixel by 1.2 out = im.point(lambda i: i * 1.2) +.. image:: transformed_hopper.jpg + :align: center + Using the above technique, you can quickly apply any simple expression to an image. You can also combine the :py:meth:`~PIL.Image.Image.point` and :py:meth:`~PIL.Image.Image.paste` methods to selectively modify an image: @@ -388,6 +448,9 @@ Note the syntax used to create the mask:: imout = im.point(lambda i: expression and 255) +.. image:: masked_hopper.jpg + :align: center + Python only evaluates the portion of a logical expression as is necessary to determine the outcome, and returns the last value examined as the result of the expression. So if the expression above is false (0), Python does not look at @@ -412,6 +475,10 @@ Enhancing images enh = ImageEnhance.Contrast(im) enh.enhance(1.3).show("30% more contrast") + +.. image:: contrasted_hopper.jpg + :align: center + Image sequences --------------- From 96a1af9fa859ad36ab9041923dd6dcd420a86f16 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Thu, 18 Jul 2024 18:33:14 -0400 Subject: [PATCH 036/136] Add tutorial images Animated gif example --- docs/handbook/animated_hopper.gif | Bin 0 -> 58616 bytes docs/handbook/tutorial.rst | 37 ++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 docs/handbook/animated_hopper.gif diff --git a/docs/handbook/animated_hopper.gif b/docs/handbook/animated_hopper.gif new file mode 100644 index 0000000000000000000000000000000000000000..e6eeaffcc4e519810e323704e68d81953fad7e12 GIT binary patch literal 58616 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ƒ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 Date: Fri, 19 Jul 2024 14:32:59 +1000 Subject: [PATCH 037/136] Correct reference --- docs/handbook/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 2840377e3..6c4e3ad59 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -546,8 +546,8 @@ You can create animated GIFs with Pillow, e.g. The following class lets you use the for-statement to loop over the sequence: -Using the :py:meth:`ImageSequence.Iterator` class -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the :py:class:`~PIL.ImageSequence.Iterator` class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: From 1daaef02cd2a6faeb0dfbc0a23360fdfb49a8621 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jul 2024 18:27:00 +1000 Subject: [PATCH 038/136] Updated code to match image --- docs/handbook/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 6c4e3ad59..e19da47a5 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -149,13 +149,13 @@ Copying a subrectangle from an image :: - box = (100, 100, 400, 400) + box = (0, 0, 64, 64) region = im.crop(box) The region is defined by a 4-tuple, where coordinates are (left, upper, right, lower). The Python Imaging Library uses a coordinate system with (0, 0) in the upper left corner. Also note that coordinates refer to positions between the -pixels, so the region in the above example is exactly 300x300 pixels. +pixels, so the region in the above example is exactly 64x64 pixels. The region could now be processed in a certain manner and pasted back. From 54055c76c4fe3ffe6293f5ed1ab8ded2c0f7d669 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jul 2024 18:45:00 +1000 Subject: [PATCH 039/136] Converted images to WebP --- docs/handbook/cropped_hopper.jpg | Bin 2017 -> 0 bytes docs/handbook/cropped_hopper.webp | Bin 0 -> 1656 bytes docs/handbook/enhanced_hopper.jpg | Bin 5721 -> 0 bytes docs/handbook/enhanced_hopper.webp | Bin 0 -> 4684 bytes docs/handbook/flip_left_right_hopper.jpg | Bin 4637 -> 0 bytes docs/handbook/flip_left_right_hopper.webp | Bin 0 -> 3976 bytes docs/handbook/flip_top_bottom_hopper.jpg | Bin 4652 -> 0 bytes docs/handbook/flip_top_bottom_hopper.webp | Bin 0 -> 3966 bytes docs/handbook/masked_hopper.jpg | Bin 4904 -> 0 bytes docs/handbook/masked_hopper.webp | Bin 0 -> 3954 bytes docs/handbook/merged_hopper.png | Bin 35390 -> 0 bytes docs/handbook/merged_hopper.webp | Bin 0 -> 7536 bytes docs/handbook/pasted_hopper.jpg | Bin 4645 -> 0 bytes docs/handbook/pasted_hopper.webp | Bin 0 -> 3922 bytes docs/handbook/rebanded_hopper.jpg | Bin 4743 -> 0 bytes docs/handbook/rebanded_hopper.webp | Bin 0 -> 3866 bytes docs/handbook/rolled_hopper.jpg | Bin 4651 -> 0 bytes docs/handbook/rolled_hopper.webp | Bin 0 -> 3912 bytes docs/handbook/rotated_hopper_180.jpg | Bin 4636 -> 0 bytes docs/handbook/rotated_hopper_180.webp | Bin 0 -> 3946 bytes docs/handbook/rotated_hopper_270.jpg | Bin 4895 -> 0 bytes docs/handbook/rotated_hopper_270.webp | Bin 0 -> 3922 bytes docs/handbook/rotated_hopper_90.jpg | Bin 4903 -> 0 bytes docs/handbook/rotated_hopper_90.webp | Bin 0 -> 3888 bytes docs/handbook/show_hopper.png | Bin 56122 -> 0 bytes docs/handbook/show_hopper.webp | Bin 0 -> 6548 bytes docs/handbook/tutorial.rst | 28 +++++++++++----------- 27 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 docs/handbook/cropped_hopper.jpg create mode 100644 docs/handbook/cropped_hopper.webp delete mode 100644 docs/handbook/enhanced_hopper.jpg create mode 100644 docs/handbook/enhanced_hopper.webp delete mode 100644 docs/handbook/flip_left_right_hopper.jpg create mode 100644 docs/handbook/flip_left_right_hopper.webp delete mode 100644 docs/handbook/flip_top_bottom_hopper.jpg create mode 100644 docs/handbook/flip_top_bottom_hopper.webp delete mode 100644 docs/handbook/masked_hopper.jpg create mode 100644 docs/handbook/masked_hopper.webp delete mode 100644 docs/handbook/merged_hopper.png create mode 100644 docs/handbook/merged_hopper.webp delete mode 100644 docs/handbook/pasted_hopper.jpg create mode 100644 docs/handbook/pasted_hopper.webp delete mode 100644 docs/handbook/rebanded_hopper.jpg create mode 100644 docs/handbook/rebanded_hopper.webp delete mode 100644 docs/handbook/rolled_hopper.jpg create mode 100644 docs/handbook/rolled_hopper.webp delete mode 100644 docs/handbook/rotated_hopper_180.jpg create mode 100644 docs/handbook/rotated_hopper_180.webp delete mode 100644 docs/handbook/rotated_hopper_270.jpg create mode 100644 docs/handbook/rotated_hopper_270.webp delete mode 100644 docs/handbook/rotated_hopper_90.jpg create mode 100644 docs/handbook/rotated_hopper_90.webp delete mode 100644 docs/handbook/show_hopper.png create mode 100644 docs/handbook/show_hopper.webp diff --git a/docs/handbook/cropped_hopper.jpg b/docs/handbook/cropped_hopper.jpg deleted file mode 100644 index 912d0a29cb0d154e7b7062123863630e8a1ef3a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2017 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 C4MrUR literal 0 HcmV?d00001 diff --git a/docs/handbook/enhanced_hopper.jpg b/docs/handbook/enhanced_hopper.jpg deleted file mode 100644 index a87903eba41acc0c4b2873f45b0aec0290c0adc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5721 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 diff --git a/docs/handbook/enhanced_hopper.webp b/docs/handbook/enhanced_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..a582ac0c216f10b4371d363683f2605be432b7fb GIT binary patch literal 4684 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 OQUa_$C79z|Jb(aTVNbaL literal 0 HcmV?d00001 diff --git a/docs/handbook/flip_left_right_hopper.jpg b/docs/handbook/flip_left_right_hopper.jpg deleted file mode 100644 index e1ef519e5cd8a34c65d681071b46318e3dd7f6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4637 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 diff --git a/docs/handbook/flip_left_right_hopper.webp b/docs/handbook/flip_left_right_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..59452268dfe48ec4c1646c3202b3b43a0fe02a35 GIT binary patch literal 3976 zcmV;34|niVNk&G14*&pHMM6+kP&goT4*&qrI{=*lDu4ih06uLjl0~EKeqGI z>2ESWnSJ~HSNDHyAJ6~5@c-iupkBiNL%xy#KSchg|1;CK=RDT&0{u$=iR=G<|C@UO ze=Ppt|9kFd>MQ&I{yt{@q5swXf8am)&-ZS&zqfwQ-?$2NOpqnAhZr9435z)xTD?aD zh^8@W=v$FBfgu3r3VP+G3KXrsq1spx48b5d9nzXa1LN)7JT z;!eVriL2t7Hq4I31^W(seE9FyZmJFiO^_p%W- zj3=Y+RCbfpybx0~g7n&mKrKp1lf_QUvgdfn@2kAypiX@P2rM+p^@I@7`D> zw`rpYrG!1WaisWS*0c}vLwj(7&-9g-XLe-1M&jkRtt;StF7t)$9y6Qoaex5+>-vUT z(@9Hf?Y%Ggl3#N_3~X=n{NFH*1aiQjCC?Dxi78}0k+F451xWFYewhwz); z&1H_(o?tnn1;i?IN3`{VhjMlZ_v`G9QWUR@k!8FB_+-3gZ~u`>V8_ClrQ=($PpdW~ zyA``U_EE5@a`9OJO)8l9=awpINmfHFaPIe+9AmAw*=Yn#9bZ6-MOyJ(gG zMB6@(+c1x>7yO*!oUl&$BD%N69m0`T5*jLzBI!k1=IPutC5Y-db}hsX7KQWD+%6~R zX>aWLiyA1v70eYsmGs8T^K~*G)}Tb3fPUxybn*x`(7)U4 zMmIh2+H`|}ueZMxl3v)EVW+rR!p{)gm4h}*4?K7B-d@T~Nj zA^P^;?N=ib=Lh|QrSHh0$S3Rarp&&{1JPd{km;5`gV*==RF|(wA$35)KY~vnh2iMn zb&GdTfu-6u9Ie-fjPo>MTHCG*Cr-x11k*_q| z0ikCYCa^#GuZ6jk8?&objFXa2|8wV-7(Jb|2SV$|q{}AzO^ZTDK+{9(aAV|Fei{N9 zg0!CX2@eK9CawH@?TPvxTvHGZJw9Wd1y`z)2)iy(N!%9nqaEVc8qMNd>@YP42ltCw zrOa{1IPlkJVpYs`cjk+U9nGgaGHl?I?c6BwB8B$oQ;hu94%jzt**h4-bAR#T2bosT zI_m#cTe~qd7zk~`R20sfTva6i!G?~oK1A2m{a2Bp##POf-n&d6{^0}c5B8_U!+ZZv zc#nG~e_p+tgx}Bm)N@wv6bCvD<%o9sJ6?et5G<@PJ-KJ*FUZTNE_}xE+9^!{gmZ2u zd=Y=FD9rqa^!5Q_G{H=nGx4lE8_O2F^WSxCsX#Cj9SB`0FkmmS`~ns@cKQ`0%^+s; zL|J(mB{DVO71Mc8l*Bps?+dgqL2*SX`{$Ovv5sx)!LBkF>*6e6J%-*COKzR_spu|A z5_n8PCQJbYq4^r3T_5bF`Nj@6xjr{FOTlfrCT$Lr=ATtS4WPe|9!u1E$L@AIuQ`jA zNZ8x$;Sj7(wWC%s54N7zZ&v9EV%Z^{Zh_ua(iol15_~lL{v$>`5o6tLsRaJBa(;v& z7ZsQu>zM=ho$K#k9^P0ONqCf7(g8pA7uDZ5fsXR$bj`*!%kOVEM$@PN5EKd2WWa|* zDXqM#qrEEXU-wJ?BtvQSzowf^vh;!uF_U(49A+jb!+nnfcRM_2G-Ka%^F(6VPGH5r zphe;c?U^$Pp-G_)q!Ali9ZclnI7E-V-4B_sU7+e!Nz6le2U2{36|T80XtQ!$%KI(; zB`1HLwy-Ji1n)22YAjbDbGQa(L-NsjzZI`zA&=^cjPcd~Jw#;Ne2wt$c+q1G9if@f z+pE8?f8LIVaDra?uCJ#f1kj|~oMb1*@X#4W5*9vWUh4{+Wmd)OL!!X8#>4>>w*kK} z3cbwC{>YZ``l=o ztnVcF-|XK53%vz*Jxe!8gui|X(m ztVPiVc;+gp9Yw>Z zAg378bcWjG^(#h|FDBq}Z+^A$enOmnfKholB|&IoU?28o6risosq$9ufNm6peLhXz z>DMT$?Hf$C6ed!C=*C53566O7)gG zLo!z}LV*D@)54bnKMGY7+1W%;^Cx=phlGN^Hx)*;sk+FPomv7*|D~}w`6nRK%VWsC zV#RM-=_qV>?%(}Eh;b(yPIrj3hW9Lf(Tk_Ny@xmHfqCj9crlPw{g9J#6ePq-wbA16 z6TTsEh>SAilhroQ!acIzVOhU9_Au zL0imgk4}v$$V8KhluA(`ah{ksBi#`*+8V*zIFLc8LG$D+&3>tDa>rlMur{(#!_i(r zIU@>S{}V#hX-X!uH=AGDcM`%mo@7otD>~Sqa3~ZG`UHAnvO?ofW%=sr>GPhtc>cze z_1jG|KhBu#1B6!B#dD_;Nw=o{9OKBxoCWh>kWak3F`2~q538TpHr0PBh(`Z&Rg984 zUK5ygKskS!T;^0gz`L9sayDP<;eHtsjP|px=%$3bG)tS(tGW@Md@aT`o0ZH1fvjce zJIKyuf2Ujk`-X$mPz*;#Y*7|h7=xzRDeC zO(G2uW};FFmh?*84m8Z1KlUibxZA@p!3;cwCMNqg8-2oxqI zILs2f;YjiScAMc9!%Q2|ziz<{O`9#%)*E|1y2|8eix^kXdlKb%hi9}$-S+uHqE*x7(7N51cuz$Fbn&gP&!7Izt< z)_x1YMKe7b$?b{`*F@okl^5A$r&-QmFN6OhI~UM&lUJ?sO`2(WNEQtb!5U^?K#0@f z!uJn@`8;HPQCPLw-{BCM2caQBxv~sWo{DoZHK+pMzm#rioUS$Z1fmoC=|#y?WsjC4 z$s&H;7>MkLQrL@0>LR3nch&<=r0KB#NGZ1D(bD{2m}h3dvjj7So($3n(Q8H#?NCU) zgu|Hgr5dTDF+B2w+x_(!Vm3PX(94X`ym7fvnKqU}RNmZZloJsMbQs=cX<+Ye4tXed z^!$;M%;y;T}F)}i;FthNnvhpcj5xk=Ke-6q=fP(>e1U#al5&@_= zsAxE-C|>}f^L^4${VRa~3@U0GS~_|V10xgjc>?YNKutwMLrqIVM@LJ0o*jKY572VZ zab8i-rRTD82Zn%_J?caZPcK7xV z4v+qEQ2{jnW}T1!hW!s0$GMA|mX?MV^pA^*I`q8LaM03SQK0A4wF0>#xkMD>7`Sg? z3TrO#h^O~!o%#=IIQIv}fvXrCFdeU)23-YGDu5xO1 zwFfI{{ct4oRLRW}ro+Muy^Pkpoicd2Il15K(3Rnn8{&ol9|=*?S85&@VKY+UZF$w( zX4vwEZKbnu(3NMFTM^D0abx%G9_MI&A=$NZTw&HXbZpK!VO}d4fn*?6 zuN~T!oq<25&9?ZVk9kaG_F{MQ)j~-^kkdad4awE>Ki~5-HfO=Tym&8^5p6(MV~1qi z&bZ_QE3UIq2}RZp#paa8Bkd3?K{hhZ51a{BC?Q&0F`i+wI9&|>Ru5sIlMmx{9Ko8@ zH#EzMRt)tkfk!`HrT{|tDi~O9T&~J(oxs`UKjZM_CPZh1zk^fBk#~BisC4E|Kxi<& zi}7?iO&+IJx6-!5lodGld8#z>wBL5*K!igrxk6FhdNZcb9OAS>CGr81I1=7+@<%Xc zd6x9ckOHg`Z0W*_2Fi*Z@lj6CF010rR|5@x=u!Zt-w`|5q=c>rGgZdbG$Sr{^R(GK z>KJN$X-NhpDK{^nS8lb7h)Y8yO$zr}G%a^8qe&#x)>EA3hR(4;M`QB`={~ioS#@c7 zUG;tO);!J#LI8PfVPY#)1&*(%@s(FQy3;LhbVj5AxFf2*4VU3(%Hb!FYUrvOWDV}6 zup$%|0(ti7Z=zDNYJ>oPXv&UA3ci*$Hr1M1DgAS~Ra0v22iOp42u8$J68;-f!19msguqcHuRxYB= zJ5=Si)ZbuB%34F^vy)5uZ_S_9D6GgVSx2haZm>I_*`Q7LRtr8HV29j7R1oii-T4 zZpUjLYupo~W@ck2InT4e@oPlYQpr2i$Z-8mK(uh^Ra0Be1D--3 z^EqOvTqgF4vk9-}?SrbiZqB#O!6mMN z;ol`I^W>_7JB@|Q4cW^GNtuince^6=Uy^_KQ*~J9SBaf1`gp`XKb0w|AKm$`>0AFk zUR3l5H+AA{&&?7W07z)vGfa%Z<*l zV$h=iQL(#gxi1Xs$mef{G6lHcr?vP`^{IT$fjH z48^i(>c$+*eu#*cY49{*5Y6?a21$gh$HcBoreCwR(T_A)ks1<59)cn*v@_3I}iR>#N-fX|0*D%p-c1L(X>_{Mp{P;v> zFhPa){#cN|Gc2+0LlWxFeSTKlQ~I`<&UT3!!n((?5^YmzY&fDPlLByTK^fI&(0_?k z_~xb#Wt8ehKR6aoh;!3hk1iJ&&#ZEhRw>c@sck*K5pjF+Q$vn8GxnpM%d~11&uQvJ z{pOf+V)EDTnBl09^O8T2Y;j=<8b0Zdj1(*#1kWjY%*sbgGhXk0Jv#eZG6}1?*}UbZ z@h%B!@WGrH{=D?7to?{<^I-b2254VD43dAtF-ob#a7(4F+(c+DRelh=Z*UM=DZ?y@ zgNX}GLv=IZ&dBE>dm+oZiRt!kElOz+ z!I;dtnO=-?kQQ8#O)F~}BAY9wA~u?H5etI{2q(JD4LI5BNB!AYwY_6gz#=lub4WWQKj1u; za_TqS1>y76kr>-lKGrPDcGV#*uI$^3@`*)09@DN{a~Z1LX11QV)=@VWu;=g|O<2dj zUHDykc={?-l97u_%?cv|83Uz9SLwYjID^vVwv3df8zr$h(784u&W^lNSRNq`&`N|>Z z6VGGOZwZbX$}LLL!?px}Zvn5#G|xK_4P`TP;xel3L1+c?FKlo0@zK4E zp2Cw~FHeLUEP}+6rbn)a*;*=405rkb%cDWQN6?X}=+Yf8Wo#!(KVU;JyNoCW_sB-! zFqwv?A47IPGt@Qp6oB_@+4rr+(OG2N(iI78NyM581z;U6YswYA{c-VzDj)lON2!Hy zVxttY^cTZ|F;@Cs5vX+7$yi}&KW;pl$%yE1T`qoxJ?FkZKd)cLdZZ1xdym}3jYj`^ zq;R(z=@Ob;G!`u5oj5`Piu?6%_5jg0>H1?Ui{JaCJ!Z`QUL~2N&w}FTOfuDZIrZ&1vNvy@f9Cp8ZCh5Q55h}Q3{Gr$W137)L$1R# z%_jnOUSYIT2hw^K`U@D2>>86vf3&R1A}VGKmXdnms=L=k8X}GGz72=SdQV=^TTm2% zPTeU}Zt9Xvz_d}-igUoWJv8p745H@)y!*jRV)Zl6pWDKqn%%dn9n4Wir6x@FAl8INB}Lw3LhB6!u3WgWfbxP1@^;gp`NCQr_B z8;Qud2XHp#hlU3}G5wnNtjHxjkUb{6%dAu!0&^^P`4cXYP4v$81sn0D5A=u;Dps*A zW3r^pn%AdvfiJ-6Yt&a2Ka|5H}Tcj#{&>ud6LX zj$exTg;Ry+-l{=P1~eb7*nJj(570QW(XC4gYFshBvo`;H1oHXPH8oqGruK38k`eUV z6ZuD7mYA&<_*T;A0+A;2;&kR-*Ca7gzcF>3bY5bYPvR=L20SAK&Z~U|;&amPGJH~t zw!Ju%fcvfo&7CtdcGue07ya{B)OuSZm;yLG`)(0i=G^w>tpT-q9OSztH+!$Gk-nFk z3)A>WaCxe|&F>BSsI=<*8a79JS`xWD&&K21-&-OUTzLnt>>xWk6kw<=X;O|gY?DDt zrOaqgJHQ+bsWoSt#4yQ3l{AC%!-gxvC6fKC;8D7zU$x`}AG#QhDpjea?1JZZee7yF zCbaBXocuWlAF5QgCe^ZFcTI*V0JL?9&dNGN00VZzYcEG=?Xq5?1*RV)Q|HBOt|&qURof#@Q*Y$&}8dyH&lR7 zsq8BEg%DBdxU(jS*+8V3o0c$rEeK+748V*lEyxNmG5z!v*j3J|LBOac0%(Q{8&kqC$LqB)l2iS`n3m!v31j56gSRBcZLyJ9{UdMKALfOlL4Iq6*?rZqx83b8W@A)O+^f4Pe;Hom^~!2lPe7*Z^z|U-PyLpg-^}M7 zjRny~+N{1qT0LJeH#c9m!cL|`aGr&J&%kH*2a**miW$Hfw D@-E{; diff --git a/docs/handbook/flip_top_bottom_hopper.webp b/docs/handbook/flip_top_bottom_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..28af4bf36591c0fc44ad2bc03bdabddc920ee3b6 GIT binary patch literal 3966 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 YSj>lQ@V5l_3oj|F$BFYaXz>q#0Q=j;w*UYD literal 0 HcmV?d00001 diff --git a/docs/handbook/masked_hopper.jpg b/docs/handbook/masked_hopper.jpg deleted file mode 100644 index f70a5ec4769e9c861fcfd4e1e90f812cd4369782..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4904 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 diff --git a/docs/handbook/masked_hopper.webp b/docs/handbook/masked_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..ef892cb94f7985b7ef38584d78d479e2c46c0e98 GIT binary patch literal 3954 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 Me`MlElL5m302v_I#{d8T literal 0 HcmV?d00001 diff --git a/docs/handbook/merged_hopper.png b/docs/handbook/merged_hopper.png deleted file mode 100644 index 662816cbdad42a04dd522acbbd195c2d789fb87b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35390 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 E0m`+u%m4rY literal 0 HcmV?d00001 diff --git a/docs/handbook/pasted_hopper.jpg b/docs/handbook/pasted_hopper.jpg deleted file mode 100644 index 36e3492a40f92e1c37d91722e1a993793fffbdec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4645 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 diff --git a/docs/handbook/rebanded_hopper.webp b/docs/handbook/rebanded_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..7a9069c9f3d18b5ec4b705144cf9460cdb870093 GIT binary patch literal 3866 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 AApigX diff --git a/docs/handbook/rolled_hopper.webp b/docs/handbook/rolled_hopper.webp new file mode 100644 index 0000000000000000000000000000000000000000..7fe8024636331b408b5ba490068f5ef0e6d47b7c GIT binary patch literal 3912 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 E0O35#SpWb4 literal 0 HcmV?d00001 diff --git a/docs/handbook/rotated_hopper_270.jpg b/docs/handbook/rotated_hopper_270.jpg deleted file mode 100644 index e4d22be70ce2bc4887a41799e1343fa0cc513cd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4895 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 oaT?m_M8uk0LZ?++fwj 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/rotated_hopper_90.jpg b/docs/handbook/rotated_hopper_90.jpg deleted file mode 100644 index 763785c7a1d5b2d45df93884dc63f9a8217ba686..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4903 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 diff --git a/docs/handbook/rotated_hopper_90.webp b/docs/handbook/rotated_hopper_90.webp new file mode 100644 index 0000000000000000000000000000000000000000..9a5f70b2030835a5feeaf54665ddae2ad49cbb53 GIT binary patch literal 3888 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/show_hopper.png b/docs/handbook/show_hopper.png deleted file mode 100644 index 1f2e6f243858d8a9944c4db0b1c0758111cc45ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56122 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

    ViV@m zMy|x4r;}FhOSn>e$X6W};h$iqZ&67r=TNKRV{_Y%-XiLv!>t{ z-LQDV=<&LB;<)r+*N;7;_+N$c%EfeYkzEX#+T&=Go4bWhD)XuD9<6dSYjh|c$XjA7 zVeP7Ef;2Zv7FQ;tHH%R7NaVayVi$NPfvbUG0}pg}<2Y$WxMI>TO^hXI!10Ru=o(7B zG(lXviP}v1e#1!7gQ4L?>Nuwcu;UPkzn++`0G6ksq?1QU{AXyRcbZ2tA^Wiq5(Q?|t*_@(o{nV-`8hV4HE#LM!WH%WH4cFrCor@8V z%k=CQGbO9By}jow`zs5yY}j^?6182&e35fE6Q6L6uk${Q7Jsx4v=EV+D?8F04ohGc=*&h>g8ku7;Sp@OnJOAk2cIY{%4dS zeupl9>9K8})JuJ)7n6I%!PF4ui=s1u^HZGVjX{fJ>H{nOeH`&8%z8!d%61;%4oq^S zQ#bj7lZyw{oaDIUXdKk{<%TZd_s&R*nxd)lDELw5Z2^rVczD9St$drJ7V3BS58&tqHTS`s%yE z*fTJHbNDsSSy;mrbgs6@$|7n`G01xzo*kR;ORQj}1eoV<@-a4Yq)E*{}5_GzB0HM=# z%^RUO`@pMs&61?AY4adANTbxD?=>dNGn$gJM9+}>SX^l|WQJ9!>CYY0Sk4>l&e2}`lau-u^4Pm66>3rR zE7nP$_!xepi^8A@Cly$COXVYHLd=rz@|v=~hVs814~#S3O_o7;&Ype9uie2b zVq=};q3~>Ndw+%q?vVBadrP;5$EUFT={!FG#Sy2W5RxE#zAs=Yf~S?I`vNq!<-uY3 z8s_wNV3B;Fja}vYHFFH{V={9GzF76==CZees%7`yi+ zI-0AXtUhl%rejodQfHHERrIx{z(fH62LWxAf3*gIh)q4*s+ZtgQuSWF=s5+FA80qd&kCY(jslxOKRq*5V*J2n9!;v?pJ3mKva{*;{QEFlA5EPqR#m-Pg9J0i zXhzDl?n!i~l1WMt!awVwqqxyqVv*%V);KZ5Mmr$SdfgHJsq5^Kl&abZYB7+C8!{Gp zr|+!Uo>Ce5iA&angQlNK!dQD~q?bTwyRYk+PtZ$Ql9-K!OIzjeN@e9;zZt_?;fA}{ z?NL=F$_a*r<aJfc_|Pyoc~#4EpSe9+goZTW)aXBN#60Z5Vo<_5wJwjJPMRub$LFC4yn~~SM(d% z)St4Ja*>O4lbU!*nT_7Rv9v4oGerXoEmu=Lp%+EPYpugK|Hr{5ZhI-Htc? zv=B7AvZ^_8J|<3Mj<5;1ee$J`3kxK?qjA3&fbC9>A zgOj_kw-}wdrLzT07>RGy5IWFr7r4C`-7QJR*$oEb=iubvq!Y&hfk2{emR7=Avhx3; zBPB698#vrW7y|L~^5XE~;c#}dhHwc92|+lyA>7>TNDp>*A1AoEH@lNN{huKJjw6eF zPj|C*f!jJefo|iPTR3~b#pvj66aDY_Qzy*Z_HQO9caGmhb3mOPA-5I?7Y8TgKhfZ} zR{w=|YxzU_y{|v%L~nx$YuI|j91LY`9br!H$kN2Qx%owZr}=-DzlHu`>bk?+WSkw5 zM7X#%)D4CVVB-w`7x|yY|4aOx(%^4O0ZyTRru>KHAI#e=2&=o>> im.show() -.. image:: show_hopper.png +.. image:: show_hopper.webp :align: center .. note:: @@ -159,7 +159,7 @@ pixels, so the region in the above example is exactly 64x64 pixels. The region could now be processed in a certain manner and pasted back. -.. image:: cropped_hopper.jpg +.. image:: cropped_hopper.webp :align: center Processing a subrectangle, and pasting it back @@ -176,7 +176,7 @@ modes of the original image and the region do not need to match. If they don’t the region is automatically converted before being pasted (see the section on :ref:`color-transforms` below for details). -.. image:: pasted_hopper.jpg +.. image:: pasted_hopper.webp :align: center Here’s an additional example: @@ -201,7 +201,7 @@ Rolling an image return im -.. image:: rolled_hopper.jpg +.. image:: rolled_hopper.webp :align: center Or if you would like to merge two images into a wider image: @@ -221,7 +221,7 @@ Merging images return im -.. image:: merged_hopper.png +.. image:: merged_hopper.webp :align: center For more advanced tricks, the paste method can also take a transparency mask as @@ -250,7 +250,7 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns the image itself. To work with individual color bands, you may want to convert the image to “RGB” first. -.. image:: rebanded_hopper.jpg +.. image:: rebanded_hopper.webp :align: center Geometrical transforms @@ -269,7 +269,7 @@ Simple geometry transforms out = im.resize((128, 128)) out = im.rotate(45) # degrees counter-clockwise -.. image:: rotated_hopper_90.jpg +.. image:: rotated_hopper_90.webp :align: center To rotate the image in 90 degree steps, you can either use the @@ -284,35 +284,35 @@ Transposing an image out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -.. image:: flip_left_right_hopper.jpg +.. image:: flip_left_right_hopper.webp :align: center :: out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -.. image:: flip_top_bottom_hopper.jpg +.. image:: flip_top_bottom_hopper.webp :align: center :: out = im.transpose(Image.Transpose.ROTATE_90) -.. image:: rotated_hopper_90.jpg +.. image:: rotated_hopper_90.webp :align: center :: out = im.transpose(Image.Transpose.ROTATE_180) -.. image:: rotated_hopper_180.jpg +.. image:: rotated_hopper_180.webp :align: center :: out = im.transpose(Image.Transpose.ROTATE_270) -.. image:: rotated_hopper_270.jpg +.. image:: rotated_hopper_270.webp :align: center ``transpose(ROTATE)`` operations can also be performed identically with @@ -396,7 +396,7 @@ Applying filters from PIL import ImageFilter out = im.filter(ImageFilter.DETAIL) -.. image:: enhanced_hopper.jpg +.. image:: enhanced_hopper.webp :align: center Point Operations @@ -448,7 +448,7 @@ Note the syntax used to create the mask:: imout = im.point(lambda i: expression and 255) -.. image:: masked_hopper.jpg +.. image:: masked_hopper.webp :align: center Python only evaluates the portion of a logical expression as is necessary to From 4db814042615a45aec0847ee55ef35f0f32521bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jul 2024 18:49:04 +1000 Subject: [PATCH 040/136] Removed unused image --- docs/handbook/rotated_hopper.jpg | Bin 4895 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/handbook/rotated_hopper.jpg diff --git a/docs/handbook/rotated_hopper.jpg b/docs/handbook/rotated_hopper.jpg deleted file mode 100644 index e4d22be70ce2bc4887a41799e1343fa0cc513cd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4895 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 oaT?m_M8uk0LZ?+ Date: Fri, 19 Jul 2024 12:14:51 +0000 Subject: [PATCH 041/136] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imaging.c | 2 +- src/libImaging/Storage.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 21c6b0a02..a2606df15 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4131,7 +4131,7 @@ _set_blocks_max(PyObject *self, PyObject *args) { } if ((unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index ae4bf72ff..522e9f375 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -506,7 +506,8 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { MUTEX_LOCK(&ImagingDefaultArena.mutex); Imaging tmp = ImagingAllocateArray( - im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size); + im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size + ); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); if (tmp) { return im; From eff22bc34ef5b509bfc996363204bb8723dd7d89 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 19 Jul 2024 09:00:26 -0400 Subject: [PATCH 042/136] Update docs/handbook/tutorial.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 402f57f69..ffa8fa019 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -528,7 +528,7 @@ You can create animated GIFs with Pillow, e.g. "rotated_hopper_90.jpg", ] - # Open images and append them to a list + # Open images and create a list images = [Image.open(filename) for filename in image_filenames] # Save the images as an animated GIF From 204ec11f64e2ec6a322a39dc525bd97e31113d12 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 19 Jul 2024 08:28:59 -0500 Subject: [PATCH 043/136] use local variable instead of casting every line --- src/encode.c | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/encode.c b/src/encode.c index f711865d5..529982dad 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1186,29 +1186,27 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { encoder->encode = ImagingJpegEncode; - strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); - - ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; - ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; - ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; - ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; - ((JPEGENCODERSTATE *)encoder->state.context)->subsampling = subsampling; - ((JPEGENCODERSTATE *)encoder->state.context)->progressive = progressive; - ((JPEGENCODERSTATE *)encoder->state.context)->smooth = smooth; - ((JPEGENCODERSTATE *)encoder->state.context)->optimize = optimize; - ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; - ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; - ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = - restart_marker_blocks; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = - restart_marker_rows; - ((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; - ((JPEGENCODERSTATE *)encoder->state.context)->rawExifLen = rawExifLen; + JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context; + strncpy(jpeg_encoder_state->rawmode, rawmode, 8); + jpeg_encoder_state->keep_rgb = keep_rgb; + jpeg_encoder_state->quality = quality; + jpeg_encoder_state->qtables = qarrays; + jpeg_encoder_state->qtablesLen = qtablesLen; + jpeg_encoder_state->subsampling = subsampling; + jpeg_encoder_state->progressive = progressive; + jpeg_encoder_state->smooth = smooth; + jpeg_encoder_state->optimize = optimize; + jpeg_encoder_state->streamtype = streamtype; + jpeg_encoder_state->xdpi = xdpi; + jpeg_encoder_state->ydpi = ydpi; + jpeg_encoder_state->restart_marker_blocks = restart_marker_blocks; + jpeg_encoder_state->restart_marker_rows = restart_marker_rows; + jpeg_encoder_state->comment = comment; + jpeg_encoder_state->comment_size = comment_size; + jpeg_encoder_state->extra = extra; + jpeg_encoder_state->extra_size = extra_size; + jpeg_encoder_state->rawExif = rawExif; + jpeg_encoder_state->rawExifLen = rawExifLen; return (PyObject *)encoder; } From f62446032166827cd0e20c711bca07650019d43d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jul 2024 13:14:18 +1000 Subject: [PATCH 044/136] Added type hints --- Tests/test_file_libtiff.py | 7 +++--- docs/reference/ImageFile.rst | 5 ++++ src/PIL/ImageWin.py | 38 ++++++++++++++-------------- src/PIL/PsdImagePlugin.py | 48 +++++++++++++++++++++--------------- src/PIL/TiffImagePlugin.py | 28 ++++++++++----------- src/PIL/TiffTags.py | 27 ++++++++++++-------- 6 files changed, 86 insertions(+), 67 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index d5dbeeb6f..58ac705c9 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -240,10 +240,11 @@ class TestFileLibTiff(LibTiffTestCase): new_ifd = TiffImagePlugin.ImageFileDirectory_v2() for tag, info in core_items.items(): - if info.length == 1: - new_ifd[tag] = values[info.type] - if info.length == 0: + assert info.type is not None + if not info.length: new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + elif info.length == 1: + new_ifd[tag] = values[info.type] else: new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e59c7311a..fdfeb60f9 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -37,6 +37,11 @@ Example: Parse an image Classes ------- +.. autoclass:: PIL.ImageFile._Tile() + :member-order: bysource + :members: + :show-inheritance: + .. autoclass:: PIL.ImageFile.Parser() :members: diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 4f9956087..6fc7cfaf5 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -90,7 +90,7 @@ class Dib: assert not isinstance(image, str) self.paste(image) - def expose(self, handle): + def expose(self, handle: int | HDC | HWND) -> None: """ Copy the bitmap contents to a device context. @@ -101,19 +101,18 @@ class Dib: if isinstance(handle, HWND): dc = self.image.getdc(handle) try: - result = self.image.expose(dc) + self.image.expose(dc) finally: self.image.releasedc(handle, dc) else: - result = self.image.expose(handle) - return result + self.image.expose(handle) def draw( self, - handle, + handle: int | HDC | HWND, dst: tuple[int, int, int, int], src: tuple[int, int, int, int] | None = None, - ): + ) -> None: """ Same as expose, but allows you to specify where to draw the image, and what part of it to draw. @@ -128,14 +127,13 @@ class Dib: if isinstance(handle, HWND): dc = self.image.getdc(handle) try: - result = self.image.draw(dc, dst, src) + self.image.draw(dc, dst, src) finally: self.image.releasedc(handle, dc) else: - result = self.image.draw(handle, dst, src) - return result + self.image.draw(handle, dst, src) - def query_palette(self, handle): + def query_palette(self, handle: int | HDC | HWND) -> int: """ Installs the palette associated with the image in the given device context. @@ -147,8 +145,8 @@ class Dib: :param handle: Device context (HDC), cast to a Python integer, or an HDC or HWND instance. - :return: A true value if one or more entries were changed (this - indicates that the image should be redrawn). + :return: The number of entries that were changed (if one or more entries, + this indicates that the image should be redrawn). """ if isinstance(handle, HWND): handle = self.image.getdc(handle) @@ -210,22 +208,22 @@ class Window: title, self.__dispatcher, width or 0, height or 0 ) - def __dispatcher(self, action: str, *args): - return getattr(self, f"ui_handle_{action}")(*args) + def __dispatcher(self, action: str, *args: int) -> None: + getattr(self, f"ui_handle_{action}")(*args) - def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None: + def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: pass - def ui_handle_damage(self, x0, y0, x1, y1) -> None: + def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None: pass def ui_handle_destroy(self) -> None: pass - def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: pass - def ui_handle_resize(self, width, height) -> None: + def ui_handle_resize(self, width: int, height: int) -> None: pass def mainloop(self) -> None: @@ -235,12 +233,12 @@ class Window: class ImageWindow(Window): """Create an image window which displays the given image.""" - def __init__(self, image, title: str = "PIL") -> None: + def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None: if not isinstance(image, Dib): image = Dib(image) self.image = image width, height = image.size super().__init__(title, width=width, height=height) - def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 31dfd4d12..cc99cd6d8 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -19,6 +19,7 @@ from __future__ import annotations import io from functools import cached_property +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i8 @@ -142,7 +143,9 @@ class PsdImageFile(ImageFile.ImageFile): self._min_frame = 1 @cached_property - def layers(self): + def layers( + self, + ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: layers = [] if self._layers_position is not None: self._fp.seek(self._layers_position) @@ -181,7 +184,9 @@ class PsdImageFile(ImageFile.ImageFile): return self.frame -def _layerinfo(fp, ct_bytes): +def _layerinfo( + fp: IO[bytes], ct_bytes: int +) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: # read layerinfo block layers = [] @@ -203,7 +208,7 @@ def _layerinfo(fp, ct_bytes): x1 = si32(read(4)) # image info - mode = [] + bands = [] ct_types = i16(read(2)) if ct_types > 4: fp.seek(ct_types * 6 + 12, io.SEEK_CUR) @@ -215,23 +220,23 @@ def _layerinfo(fp, ct_bytes): type = i16(read(2)) if type == 65535: - m = "A" + b = "A" else: - m = "RGBA"[type] + b = "RGBA"[type] - mode.append(m) + bands.append(b) read(4) # size # figure out the image mode - mode.sort() - if mode == ["R"]: + bands.sort() + if bands == ["R"]: mode = "L" - elif mode == ["B", "G", "R"]: + elif bands == ["B", "G", "R"]: mode = "RGB" - elif mode == ["A", "B", "G", "R"]: + elif bands == ["A", "B", "G", "R"]: mode = "RGBA" else: - mode = None # unknown + mode = "" # unknown # skip over blend flags and extra information read(12) # filler @@ -258,19 +263,22 @@ def _layerinfo(fp, ct_bytes): layers.append((name, mode, (x0, y0, x1, y1))) # get tiles + layerinfo = [] for i, (name, mode, bbox) in enumerate(layers): tile = [] for m in mode: t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) - layers[i] = name, mode, bbox, tile + layerinfo.append((name, mode, bbox, tile)) - return layers + return layerinfo -def _maketile(file, mode, bbox, channels): - tile = None +def _maketile( + file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int +) -> list[ImageFile._Tile] | None: + tiles = None read = file.read compression = i16(read(2)) @@ -283,26 +291,26 @@ def _maketile(file, mode, bbox, channels): if compression == 0: # # raw compression - tile = [] + tiles = [] for channel in range(channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append(("raw", bbox, offset, layer)) + tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) offset = offset + xsize * ysize elif compression == 1: # # packbits compression i = 0 - tile = [] + tiles = [] bytecount = read(channels * ysize * 2) offset = file.tell() for channel in range(channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append(("packbits", bbox, offset, layer)) + tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) for y in range(ysize): offset = offset + i16(bytecount, i) i += 2 @@ -312,7 +320,7 @@ def _maketile(file, mode, bbox, channels): if offset & 1: read(1) # padding - return tile + return tiles # -------------------------------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 253f64852..1dab0d50b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -445,7 +445,7 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -def _register_loader(idx, size): +def _register_loader(idx: int, size: int): def decorator(func): from .TiffTags import TYPES @@ -457,7 +457,7 @@ def _register_loader(idx, size): return decorator -def _register_writer(idx): +def _register_writer(idx: int): def decorator(func): _write_dispatch[idx] = func # noqa: F821 return func @@ -465,7 +465,7 @@ def _register_writer(idx): return decorator -def _register_basic(idx_fmt_name): +def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: from .TiffTags import TYPES idx, fmt, name = idx_fmt_name @@ -640,7 +640,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag, value) -> None: + def __setitem__(self, tag: int, value) -> None: self._setitem(tag, value, self.legacy_api) def _setitem(self, tag, value, legacy_api) -> None: @@ -731,10 +731,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt, data): + def _unpack(self, fmt: str, data): return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt, *values): + def _pack(self, fmt: str, *values): return struct.pack(self._endian + fmt, *values) list( @@ -755,7 +755,7 @@ class ImageFileDirectory_v2(_IFDv2Base): ) @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data, legacy_api=True): + def load_byte(self, data, legacy_api: bool = True): return data @_register_writer(1) # Basic type, except for the legacy API. @@ -767,7 +767,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return data @_register_loader(2, 1) - def load_string(self, data, legacy_api=True): + def load_string(self, data: bytes, legacy_api: bool = True) -> str: if data.endswith(b"\0"): data = data[:-1] return data.decode("latin-1", "replace") @@ -797,7 +797,7 @@ class ImageFileDirectory_v2(_IFDv2Base): ) @_register_loader(7, 1) - def load_undefined(self, data, legacy_api=True): + def load_undefined(self, data, legacy_api: bool = True): return data @_register_writer(7) @@ -809,7 +809,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data, legacy_api=True): + def load_signed_rational(self, data, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}l", data) def combine(a, b): @@ -1030,7 +1030,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): """Dictionary of tag types""" @classmethod - def from_v2(cls, original) -> ImageFileDirectory_v1: + def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original @@ -1073,7 +1073,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag, value) -> None: + def __setitem__(self, tag: int, value) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) @@ -1212,7 +1212,7 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - def get_photoshop_blocks(self): + def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: """ Returns a dictionary of Photoshop "Image Resource Blocks". The keys are the image resource ID. For more information, see @@ -1259,7 +1259,7 @@ class TiffImageFile(ImageFile.ImageFile): if ExifTags.Base.Orientation in self.tag_v2: del self.tag_v2[ExifTags.Base.Orientation] - def _load_libtiff(self): + def _load_libtiff(self) -> Image.core.PixelAccess | None: """Overload method triggered when we detect a compressed tiff Calls out to libtiff""" diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index e318c8739..86adaa458 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -32,17 +32,24 @@ class _TagInfo(NamedTuple): class TagInfo(_TagInfo): __slots__: list[str] = [] - def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): + def __new__( + cls, + value: int | None = None, + name: str = "unknown", + type: int | None = None, + length: int | None = None, + enum: dict[str, int] | None = None, + ) -> TagInfo: return super().__new__(cls, value, name, type, length, enum or {}) - def cvt_enum(self, value): + def cvt_enum(self, value: str) -> int | str: # Using get will call hash(value), which can be expensive # for some types (e.g. Fraction). Since self.enum is rarely # used, it's usually better to test it first. return self.enum.get(value, value) if self.enum else value -def lookup(tag, group=None): +def lookup(tag: int, group: int | None = None) -> TagInfo: """ :param tag: Integer tag number :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in @@ -89,7 +96,7 @@ DOUBLE = 12 IFD = 13 LONG8 = 16 -_tags_v2 = { +_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = { 254: ("NewSubfileType", LONG, 1), 255: ("SubfileType", SHORT, 1), 256: ("ImageWidth", LONG, 1), @@ -233,7 +240,7 @@ _tags_v2 = { 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 } -TAGS_V2_GROUPS = { +_tags_v2_groups = { # ExifIFD 34665: { 36864: ("ExifVersion", UNDEFINED, 1), @@ -281,7 +288,7 @@ TAGS_V2_GROUPS = { # Legacy Tags structure # these tags aren't included above, but were in the previous versions -TAGS = { +TAGS: dict[int | tuple[int, int], str] = { 347: "JPEGTables", 700: "XMP", # Additional Exif Info @@ -426,9 +433,10 @@ TAGS = { } TAGS_V2: dict[int, TagInfo] = {} +TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {} -def _populate(): +def _populate() -> None: for k, v in _tags_v2.items(): # Populate legacy structure. TAGS[k] = v[0] @@ -438,9 +446,8 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) - for tags in TAGS_V2_GROUPS.values(): - for k, v in tags.items(): - tags[k] = TagInfo(k, *v) + for group, tags in _tags_v2_groups.items(): + TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()} _populate() From b976a8496df8ad741595decac321f5469a100d77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 08:07:12 +0000 Subject: [PATCH 045/136] Update dependency mypy to v1.11.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6dd432488..776bb0dbb 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.10.1 +mypy==1.11.0 From 882a196a8f04f446c9ea6eaaac900fdecf220429 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jul 2024 18:59:27 +1000 Subject: [PATCH 046/136] Removed unused ignores --- Tests/test_file_png.py | 2 +- Tests/test_file_ppm.py | 2 +- Tests/test_psdraw.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index e2913e944..4958b2222 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -776,7 +776,7 @@ class TestFilePng: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout # type: ignore[assignment] + sys.stdout = mystdout with Image.open(TEST_PNG_FILE) as im: im.save(sys.stdout, "PNG") diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d6451ec18..0a61830a4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -373,7 +373,7 @@ def test_save_stdout(buffer: bool) -> None: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout # type: ignore[assignment] + sys.stdout = mystdout with Image.open(TEST_FILE) as im: im.save(sys.stdout, "PPM") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 130ffa863..c3afa9089 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -59,7 +59,7 @@ def test_stdout(buffer: bool) -> None: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout # type: ignore[assignment] + sys.stdout = mystdout ps = PSDraw.PSDraw() _create_document(ps) From 38458a204cbbe1a83b458dd8d6be8762bc73ca99 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jul 2024 19:07:42 +1000 Subject: [PATCH 047/136] Corrected type hints --- Tests/test_file_mpo.py | 2 ++ src/PIL/JpegImagePlugin.py | 4 ++-- src/PIL/PngImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 5402fcb44..e0f42a266 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -85,7 +85,9 @@ def test_exif(test_file: str) -> None: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) for im in (im_original, im_reloaded): + assert isinstance(im, MpoImagePlugin.MpoImageFile) info = im._getexif() + assert info is not None assert info[272] == "Nintendo 3DS" assert info[296] == 2 assert info[34665] == 188 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index af24faa5d..d83d60b7b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -468,7 +468,7 @@ class JpegImageFile(ImageFile.ImageFile): self.tile = [] - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: return _getexif(self) def _read_dpi_from_exif(self) -> None: @@ -504,7 +504,7 @@ class JpegImageFile(ImageFile.ImageFile): return _getmp(self) -def _getexif(self: JpegImageFile) -> dict[str, Any] | None: +def _getexif(self: JpegImageFile) -> dict[int, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 247f908ed..58db7777c 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1054,7 +1054,7 @@ class PngImageFile(ImageFile.ImageFile): self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: if "exif" not in self.info: self.load() if "exif" not in self.info and "Raw profile type exif" not in self.info: diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 011de9c6a..cec796340 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -96,7 +96,7 @@ class WebPImageFile(ImageFile.ImageFile): # Initialize seek state self._reset(reset=False) - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() From ddc02bb78c327a1025a772619dd3114752cbb68b Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Sat, 20 Jul 2024 14:41:47 -0400 Subject: [PATCH 048/136] Update and add image to PostScript printing example --- docs/handbook/hopper_ps.png | Bin 0 -> 76966 bytes docs/handbook/tutorial.rst | 65 ++++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 docs/handbook/hopper_ps.png diff --git a/docs/handbook/hopper_ps.png b/docs/handbook/hopper_ps.png new file mode 100644 index 0000000000000000000000000000000000000000..fe44e79843f1f46eb2db109d96248670bb645eed GIT binary patch literal 76966 zcmdqJWn5HS^fyjQg9ri&k}BN-Lk^7y5+XG;NJ$Lc-5n|=4H8OsmvooJ(A`}_^FMm; z{axZB#ASZ=``4kfY0Rcz)wfH*(1f)&`1ms@y z2f&juvFD2j2#C6-Vqyx?Vq%mEHc%r|b3+6Kx@h|-@M|P%!pB|!v=daEjvgU|m>&jan@^lQ^b~OgsJzfcN&M&8%$UCac=D#D>Wmyt^l2y& zV|ZJCyXKJ1-ixyVRxf~Cb-M;nYVlF#l%A&}{0zgTY!4d43B<5`C}sXM#7VNN>Ql^in!k1Q ziqx5g{Lejd@9&BtN@CK|z_*ftjiI5XtqIglD>*I`=xW^by_%icTUkB>s0EAO2dKUw zi<5=b?o|MU0PI1Qal z|D|MU`}eef39|k!VP$7wWBs4Lfu;h#pYkb~IvJX)ii>;Y&9@$#?>{Hgzcl>AHa zUQ4xqTk^2~r{%qp|J_pA*3d=_Y5|b66Z%&&f0Mmm_;*7A*55O~$BDn#{O2jaXCX`h z*8h>25av^X)#nHZ!U)phBJZ6LcN#F9G*_L^?)tK|Dw+1L8uxc}(Q&ErG1M{ok-nLJ zdYNweHE(w;Zxej;DO+b`TShw3Z8xsK6v@vozz+fADgu#|loa`adA*)S<*M>{;joF( zE&PT&c{o~tCOrH@Wd2o!l(Fkz-Sq$y+!43B0`hztzCk z+q3XC?V@GxJY#r}d_x=kYI1aeid;zOG`Tg|*IZlf_N~Sj{+l;$+4*bNXE|#_-Vd4VDh}tle6B56H}ZSAJ?}yR3T;0=rQVxH#6vnsMy+K zU}RvZ92t4Hg2$WQx0R4oz4tM&inI4NW&Prb@0IVicII`&WsQ33);IX>WBGFzQU~JHrKB z?lzAn!1LgOf`ak6IZLTX@(N!xJXnfgT|wbqPz+%lL|R&1^7+;N>Z>0(;iUXE$HrV1 zhfbuJK^hW@ZM7>pNLJ0kbovL_YgcE-o$&F)?3)wibeN79?uQ ze=px?gjq(qsJuL>$+KPl-2!H)J#kkt8Gj(wx(2qMfbBRoDcB?F8L!Q+uZf9h2B~94 z_zw~Py}I6bX+N|p3F=(tcGw|Ve{ndj0af=C>W+>-&drSD;3$VkNF=;X7wA5BF z3DSfuO-#y<2b-Er^&m0yQLUO!*!5Aane_)c&t(JB8iVigDacFSYk7NQlh7?HFZ_W) zwiS|fSSR&+(y_9f_?sO_$o;C#2{K+&aoPj~w6fwKDjg<<*=Wy6cx}G0<0gIjm-B!W zuwNjNCGptⅆ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

    5#|r#- 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index ffa8fa019..bc4339928 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -567,25 +567,62 @@ Drawing PostScript :: - from PIL import Image - from PIL import PSDraw + from PIL import Image, PSDraw + import os - with Image.open("hopper.ppm") as im: - title = "hopper" - box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points + # Define the PostScript file + ps_file = open("hopper.ps", "wb") - ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer - ps.begin_document(title) + # Create a PSDraw object + ps = PSDraw.PSDraw(ps_file) - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) + # Start the document + ps.begin_document() - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3 * 72, 4 * 72), title) + # Set the text to be drawn + text = "Hopper" - ps.end_document() + # Define the PostScript font + font_name = "Helvetica-Narrow-Bold" + font_size = 36 + + # Calculate text size (approximation as PSDraw doesn't provide direct method) + # Assuming average character width as 0.6 of the font size + text_width = len(text) * font_size * 0.6 + text_height = font_size + + # Set the position (top-center) + page_width, page_height = 595, 842 # A4 size in points + text_x = (page_width - text_width) // 2 + text_y = page_height - text_height - 50 # Distance from the top of the page + + # Load the image + image_path = os.path.join("img", "hopper.ppm") # Update this with your image path + with Image.open(image_path) as im: + # Resize the image if it's too large + im.thumbnail((page_width - 100, page_height // 2)) + + # Define the box where the image will be placed + img_width, img_height = im.size + img_x = (page_width - img_width) // 2 + img_y = text_y + text_height - 200 # 200 points below the text + + # Draw the image (75 dpi) + ps.image((img_x, img_y, img_x + img_width, img_y + img_height), im, 75) + + # Draw the text + ps.setfont(font_name, font_size) + ps.text((text_x, text_y), text) + + # End the document + ps.end_document() + ps_file.close() + +.. image:: hopper_ps.png + +.. note:: + + PostScript converted to PDF for display purposes More on reading images ---------------------- From 1cf887dbec2387d1e3228497cc12329bf84d18e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jul 2024 05:22:13 +1000 Subject: [PATCH 049/136] Rearranged code --- Tests/test_file_libtiff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 58ac705c9..62f8719af 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -241,10 +241,10 @@ class TestFileLibTiff(LibTiffTestCase): new_ifd = TiffImagePlugin.ImageFileDirectory_v2() for tag, info in core_items.items(): assert info.type is not None - if not info.length: - new_ifd[tag] = tuple(values[info.type] for _ in range(3)) - elif info.length == 1: + if info.length == 1: new_ifd[tag] = values[info.type] + elif not info.length: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) else: new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) From 22ef8df59abf461824e4672bba8c47137730ef57 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Jul 2024 00:29:55 -0500 Subject: [PATCH 050/136] Do not run scheduled wheel jobs on forks --- .github/workflows/wheels.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ffd1abd95..3ed1b5926 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -97,6 +97,7 @@ jobs: path: ./wheelhouse/*.whl build-2-native-wheels: + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -150,6 +151,7 @@ jobs: path: ./wheelhouse/*.whl windows: + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: @@ -256,7 +258,7 @@ jobs: path: dist/*.tar.gz scientific-python-nightly-wheels-publish: - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') needs: [build-2-native-wheels, windows] runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels @@ -273,7 +275,7 @@ jobs: anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} pypi-publish: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI From 96fa1f5dbf43fe26a9b7abd98697ae46e8165435 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Tue, 23 Jul 2024 14:55:04 -0400 Subject: [PATCH 051/136] multiply each pixel by 20 --- docs/handbook/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index bc4339928..4e7477143 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -412,8 +412,8 @@ Applying point transforms :: - # multiply each pixel by 1.2 - out = im.point(lambda i: i * 1.2) + # multiply each pixel by 20 + out = im.point(lambda i: i * 20) .. image:: transformed_hopper.jpg :align: center From cf6ec5e065e910d8ce5951de0d19a84879ea77c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Jul 2024 06:44:06 +1000 Subject: [PATCH 052/136] Converted images to WebP --- docs/handbook/hopper_ps.png | Bin 76966 -> 0 bytes docs/handbook/hopper_ps.webp | Bin 0 -> 8322 bytes docs/handbook/transformed_hopper.jpg | Bin 3608 -> 0 bytes docs/handbook/transformed_hopper.webp | Bin 0 -> 3054 bytes docs/handbook/tutorial.rst | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/handbook/hopper_ps.png create mode 100644 docs/handbook/hopper_ps.webp delete mode 100644 docs/handbook/transformed_hopper.jpg create mode 100644 docs/handbook/transformed_hopper.webp diff --git a/docs/handbook/hopper_ps.png b/docs/handbook/hopper_ps.png deleted file mode 100644 index fe44e79843f1f46eb2db109d96248670bb645eed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76966 zcmdqJWn5HS^fyjQg9ri&k}BN-Lk^7y5+XG;NJ$Lc-5n|=4H8OsmvooJ(A`}_^FMm; z{axZB#ASZ=``4kfY0Rcz)wfH*(1f)&`1ms@y z2f&juvFD2j2#C6-Vqyx?Vq%mEHc%r|b3+6Kx@h|-@M|P%!pB|!v=daEjvgU|m>&jan@^lQ^b~OgsJzfcN&M&8%$UCac=D#D>Wmyt^l2y& zV|ZJCyXKJ1-ixyVRxf~Cb-M;nYVlF#l%A&}{0zgTY!4d43B<5`C}sXM#7VNN>Ql^in!k1Q ziqx5g{Lejd@9&BtN@CK|z_*ftjiI5XtqIglD>*I`=xW^by_%icTUkB>s0EAO2dKUw zi<5=b?o|MU0PI1Qal z|D|MU`}eef39|k!VP$7wWBs4Lfu;h#pYkb~IvJX)ii>;Y&9@$#?>{Hgzcl>AHa zUQ4xqTk^2~r{%qp|J_pA*3d=_Y5|b66Z%&&f0Mmm_;*7A*55O~$BDn#{O2jaXCX`h z*8h>25av^X)#nHZ!U)phBJZ6LcN#F9G*_L^?)tK|Dw+1L8uxc}(Q&ErG1M{ok-nLJ zdYNweHE(w;Zxej;DO+b`TShw3Z8xsK6v@vozz+fADgu#|loa`adA*)S<*M>{;joF( zE&PT&c{o~tCOrH@Wd2o!l(Fkz-Sq$y+!43B0`hztzCk z+q3XC?V@GxJY#r}d_x=kYI1aeid;zOG`Tg|*IZlf_N~Sj{+l;$+4*bNXE|#_-Vd4VDh}tle6B56H}ZSAJ?}yR3T;0=rQVxH#6vnsMy+K zU}RvZ92t4Hg2$WQx0R4oz4tM&inI4NW&Prb@0IVicII`&WsQ33);IX>WBGFzQU~JHrKB z?lzAn!1LgOf`ak6IZLTX@(N!xJXnfgT|wbqPz+%lL|R&1^7+;N>Z>0(;iUXE$HrV1 zhfbuJK^hW@ZM7>pNLJ0kbovL_YgcE-o$&F)?3)wibeN79?uQ ze=px?gjq(qsJuL>$+KPl-2!H)J#kkt8Gj(wx(2qMfbBRoDcB?F8L!Q+uZf9h2B~94 z_zw~Py}I6bX+N|p3F=(tcGw|Ve{ndj0af=C>W+>-&drSD;3$VkNF=;X7wA5BF z3DSfuO-#y<2b-Er^&m0yQLUO!*!5Aane_)c&t(JB8iVigDacFSYk7NQlh7?HFZ_W) zwiS|fSSR&+(y_9f_?sO_$o;C#2{K+&aoPj~w6fwKDjg<<*=Wy6cx}G0<0gIjm-B!W zuwNjNCGptⅆ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

    5#|r#- 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 diff --git a/docs/handbook/hopper_ps.webp b/docs/handbook/hopper_ps.webp new file mode 100644 index 0000000000000000000000000000000000000000..3dd2943d68262e42c96df88647fb958413560f20 GIT binary patch literal 8322 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 T%)hKKorTfBZ2#<;FxvkBGmtjo literal 0 HcmV?d00001 diff --git a/docs/handbook/transformed_hopper.jpg b/docs/handbook/transformed_hopper.jpg deleted file mode 100644 index 33afe67e62be8f3e2922c5cfa5c0018774ac0b5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3608 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 literal 0 HcmV?d00001 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 4e7477143..d8a1ae9c9 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -415,7 +415,7 @@ Applying point transforms # multiply each pixel by 20 out = im.point(lambda i: i * 20) -.. image:: transformed_hopper.jpg +.. image:: transformed_hopper.webp :align: center Using the above technique, you can quickly apply any simple expression to an @@ -618,7 +618,7 @@ Drawing PostScript ps.end_document() ps_file.close() -.. image:: hopper_ps.png +.. image:: hopper_ps.webp .. note:: From 888b2f716e0873bd7692fd1055c0b588de648352 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Jul 2024 06:56:42 +1000 Subject: [PATCH 053/136] Improved consistency of example paths --- docs/handbook/tutorial.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index d8a1ae9c9..fead6fb21 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -332,7 +332,7 @@ choose to resize relative to a given size. from PIL import Image, ImageOps size = (100, 150) - with Image.open("Tests/images/hopper.webp") as im: + with Image.open("hopper.webp") as im: ImageOps.contain(im, size).save("imageops_contain.webp") ImageOps.cover(im, size).save("imageops_cover.webp") ImageOps.fit(im, size).save("imageops_fit.webp") @@ -597,7 +597,7 @@ Drawing PostScript text_y = page_height - text_height - 50 # Distance from the top of the page # Load the image - image_path = os.path.join("img", "hopper.ppm") # Update this with your image path + image_path = "hopper.ppm" # Update this with your image path with Image.open(image_path) as im: # Resize the image if it's too large im.thumbnail((page_width - 100, page_height // 2)) @@ -690,7 +690,7 @@ Reading from a tar archive from PIL import Image, TarIO - fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") + fp = TarIO.TarIO("hopper.tar", "hopper.jpg") im = Image.open(fp) @@ -705,7 +705,6 @@ in the current directory can be saved as JPEGs at reduced quality. import glob from PIL import Image - def compress_image(source_path, dest_path): with Image.open(source_path) as img: if img.mode != "RGB": From 8f4dbfe6a64240f613372a6a66f2e7cc563b4251 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Jul 2024 06:56:59 +1000 Subject: [PATCH 054/136] Simplified code --- docs/handbook/tutorial.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index fead6fb21..c36011362 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -603,12 +603,11 @@ Drawing PostScript im.thumbnail((page_width - 100, page_height // 2)) # Define the box where the image will be placed - img_width, img_height = im.size - img_x = (page_width - img_width) // 2 + img_x = (page_width - im.width) // 2 img_y = text_y + text_height - 200 # 200 points below the text # Draw the image (75 dpi) - ps.image((img_x, img_y, img_x + img_width, img_y + img_height), im, 75) + ps.image((img_x, img_y, img_x + im.width, img_y + im.height), im, 75) # Draw the text ps.setfont(font_name, font_size) From ab635be11bfbf1ac67fdf13f50c66781a7849ca9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Jul 2024 22:08:15 +1000 Subject: [PATCH 055/136] Removed unused argument --- src/PIL/ImageDraw.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 2b3620e71..ce8e66571 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -623,7 +623,7 @@ class ImageDraw: return fill_ink return ink - def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: + def draw_text(ink, stroke_width=0) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -664,8 +664,6 @@ class ImageDraw: ) except TypeError: mask = font.getmask(text) - if stroke_offset: - coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha From 7a570d67bf518684a63721ae40d151d40cf2874d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:32:20 +0300 Subject: [PATCH 056/136] Remove unused _util.is_directory --- Tests/test_util.py | 22 ---------------------- src/PIL/_util.py | 5 ----- 2 files changed, 27 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 197ef79ee..9eaabf18a 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -30,28 +30,6 @@ def test_is_not_path(tmp_path: Path) -> None: assert not it_is_not -def test_is_directory() -> None: - # Arrange - directory = "Tests" - - # Act - it_is = _util.is_directory(directory) - - # Assert - assert it_is - - -def test_is_not_directory() -> None: - # Arrange - text = "abc" - - # Act - it_is_not = _util.is_directory(text) - - # Assert - assert not it_is_not - - def test_deferred_error() -> None: # Arrange diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 6bc762816..8ef0d36f7 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -10,11 +10,6 @@ def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: - """Checks if an object is a string, and that it points to a directory.""" - return is_path(f) and os.path.isdir(f) - - class DeferredError: def __init__(self, ex: BaseException): self.ex = ex From 72a243c49815b95df5a64a950db6dc815c91cb88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:44:44 -0600 Subject: [PATCH 057/136] Revert "Corrected exiv2.org links" --- Tests/test_file_jpeg.py | 2 +- src/PIL/JpegPresets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 68705094b..9660e2828 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -829,7 +829,7 @@ class TestFileJpeg: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." - # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html + # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) def test_invalid_exif(self) -> None: diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 3aefa073c..d0e64a35e 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -37,7 +37,7 @@ You can get the subsampling of a JPEG with the :func:`.JpegImagePlugin.get_sampling` function. In JPEG compressed data a JPEG marker is used instead of an EXIF tag. -(ref.: https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html) +(ref.: https://exiv2.org/tags.html) Quantization tables From 726cdf5eed05b2494f2948878c3f2a8c1bc9bfab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jul 2024 22:55:49 +1000 Subject: [PATCH 058/136] Added type hints --- Tests/test_font_pcf.py | 3 +- Tests/test_imagefont.py | 4 +- docs/reference/ImageFont.rst | 8 ++ docs/reference/internal_modules.rst | 4 + pyproject.toml | 2 - src/PIL/Image.py | 68 ++++++++------ src/PIL/ImageDraw.py | 140 +++++++++++++++------------- src/PIL/ImageFont.py | 93 +++++++++++------- src/PIL/_imaging.pyi | 1 + src/PIL/_imagingft.pyi | 58 ++++++------ 10 files changed, 223 insertions(+), 158 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 997809e46..567ddaf13 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -2,6 +2,7 @@ from __future__ import annotations import os from pathlib import Path +from typing import AnyStr import pytest @@ -92,7 +93,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: def _test_high_characters( - request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes + request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr ) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9cb420371..340cc4742 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -717,14 +717,14 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) _check_text(font, "Tests/images/variation_adobe.png", 11) - for name in ["Bold", b"Bold"]: + for name in ("Bold", b"Bold"): font.set_variation_by_name(name) assert font.getname()[1] == "Bold" _check_text(font, "Tests/images/variation_adobe_name.png", 16) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) _check_text(font, "Tests/images/variation_tiny.png", 40) - for name in ["200", b"200"]: + for name in ("200", b"200"): font.set_variation_by_name(name) assert font.getname()[1] == "200" _check_text(font, "Tests/images/variation_tiny_name.png", 40) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index edbdd9a32..d9d9cac6e 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -91,3 +91,11 @@ Constants Set to 1,000,000, to protect against potential DOS attacks. Pillow will raise a :py:exc:`ValueError` if the number of characters is over this limit. The check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. + +Dictionaries +------------ + +.. autoclass:: Axis + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index e4cb17c4d..2fb4ff8c0 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -78,3 +78,7 @@ on some Python versions. An internal interface module previously known as :mod:`~PIL._imaging`, implemented in :file:`_imaging.c`. + +.. py:class:: ImagingCore + + A representation of the image data. diff --git a/pyproject.toml b/pyproject.toml index b76f3c24d..0940664f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,6 +159,4 @@ exclude = [ '^Tests/oss-fuzz/fuzz_font.py$', '^Tests/oss-fuzz/fuzz_pillow.py$', '^Tests/test_qt_image_qapplication.py$', - '^Tests/test_font_pcf_charsets.py$', - '^Tests/test_font_pcf.py$', ] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9d901e028..aa5eeabb2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -218,6 +218,8 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: + from xml.etree.ElementTree import Element + from . import ImageFile, ImagePalette from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] @@ -241,9 +243,9 @@ ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} _ENDIAN = "<" if sys.byteorder == "little" else ">" -def _conv_type_shape(im): +def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: m = ImageMode.getmode(im.mode) - shape = (im.height, im.width) + shape: tuple[int, ...] = (im.height, im.width) extra = len(m.bands) if extra != 1: shape += (extra,) @@ -470,10 +472,10 @@ class _E: self.scale = scale self.offset = offset - def __neg__(self): + def __neg__(self) -> _E: return _E(-self.scale, -self.offset) - def __add__(self, other): + def __add__(self, other) -> _E: if isinstance(other, _E): return _E(self.scale + other.scale, self.offset + other.offset) return _E(self.scale, self.offset + other) @@ -486,14 +488,14 @@ class _E: def __rsub__(self, other): return other + -self - def __mul__(self, other): + def __mul__(self, other) -> _E: if isinstance(other, _E): return NotImplemented return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other): + def __truediv__(self, other) -> _E: if isinstance(other, _E): return NotImplemented return _E(self.scale / other, self.offset / other) @@ -718,9 +720,9 @@ class Image: return self._repr_image("JPEG") @property - def __array_interface__(self): + def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: # numpy array interface support - new = {"version": 3} + new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} try: if self.mode == "1": # Binary images need to be extended from bits to bytes @@ -1418,7 +1420,7 @@ class Image: return out return self.im.getcolors(maxcolors) - def getdata(self, band: int | None = None): + def getdata(self, band: int | None = None) -> core.ImagingCore: """ Returns the contents of this image as a sequence object containing pixel values. The sequence object is flattened, so @@ -1467,8 +1469,8 @@ class Image: def get_name(tag: str) -> str: return re.sub("^{[^}]+}", "", tag) - def get_value(element): - value = {get_name(k): v for k, v in element.attrib.items()} + def get_value(element: Element) -> str | dict[str, Any] | None: + value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} children = list(element) if children: for child in children: @@ -1712,7 +1714,7 @@ class Image: return self.im.histogram(extrema) return self.im.histogram() - def entropy(self, mask=None, extrema=None): + def entropy(self, mask: Image | None = None, extrema=None): """ Calculates and returns the entropy for the image. @@ -1996,7 +1998,7 @@ class Image: def putdata( self, - data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, + data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, scale: float = 1.0, offset: float = 0.0, ) -> None: @@ -2184,7 +2186,12 @@ class Image: return m_im - def _get_safe_box(self, size, resample, box): + def _get_safe_box( + self, + size: tuple[int, int], + resample: Resampling, + box: tuple[float, float, float, float], + ) -> tuple[int, int, int, int]: """Expands the box so it includes adjacent pixels that may be used by resampling with the given resampling filter. """ @@ -2294,7 +2301,7 @@ class Image: factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 if factor_x > 1 or factor_y > 1: - reduce_box = self._get_safe_box(size, resample, box) + reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) factor = (factor_x, factor_y) self = ( self.reduce(factor, box=reduce_box) @@ -2430,7 +2437,7 @@ class Image: 0.0, ] - def transform(x, y, matrix): + def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: (a, b, c, d, e, f) = matrix return a * x + b * y + c, d * x + e * y + f @@ -2445,9 +2452,9 @@ class Image: xx = [] yy = [] for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y, matrix) - xx.append(x) - yy.append(y) + transformed_x, transformed_y = transform(x, y, matrix) + xx.append(transformed_x) + yy.append(transformed_y) nw = math.ceil(max(xx)) - math.floor(min(xx)) nh = math.ceil(max(yy)) - math.floor(min(yy)) @@ -2705,7 +2712,7 @@ class Image: provided_size = tuple(map(math.floor, size)) def preserve_aspect_ratio() -> tuple[int, int] | None: - def round_aspect(number, key): + def round_aspect(number: float, key: Callable[[int], float]) -> int: return max(min(math.floor(number), math.ceil(number), key=key), 1) x, y = provided_size @@ -2849,7 +2856,13 @@ class Image: return im def __transformer( - self, box, image, method, data, resample=Resampling.NEAREST, fill=1 + self, + box: tuple[int, int, int, int], + image: Image, + method, + data, + resample: int = Resampling.NEAREST, + fill: bool = True, ): w = box[2] - box[0] h = box[3] - box[1] @@ -2899,11 +2912,12 @@ class Image: Resampling.BICUBIC, ): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - msg = { + unusable: dict[int, str] = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", - }[resample] + f" ({resample}) cannot be used." + } + msg = unusable[resample] + f" ({resample}) cannot be used." else: msg = f"Unknown resampling filter ({resample})." @@ -3843,7 +3857,7 @@ class Exif(_ExifBase): print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ - endian = None + endian: str | None = None bigtiff = False _loaded = False @@ -3892,7 +3906,7 @@ class Exif(_ExifBase): head += b"\x00\x00\x00\x00" return head - def load(self, data): + def load(self, data: bytes) -> None: # Extract EXIF information. This is highly experimental, # and is likely to be replaced with something better in a future # version. @@ -3911,7 +3925,7 @@ class Exif(_ExifBase): self._info = None return - self.fp = io.BytesIO(data) + self.fp: IO[bytes] = io.BytesIO(data) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin @@ -3921,7 +3935,7 @@ class Exif(_ExifBase): self.fp.seek(self._info.next) self._info.load(self.fp) - def load_from_fp(self, fp, offset=None): + def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: self._loaded_exif = None self._data.clear() self._hidden_data.clear() diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce8e66571..6f56d0236 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -36,7 +36,7 @@ import numbers import struct from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast +from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast from . import Image, ImageColor from ._deprecate import deprecate @@ -561,7 +561,12 @@ class ImageDraw: def _multiline_split(self, text: AnyStr) -> list[AnyStr]: return text.split("\n" if isinstance(text, str) else b"\n") - def _multiline_spacing(self, font, spacing, stroke_width): + def _multiline_spacing( + self, + font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, + spacing: float, + stroke_width: float, + ) -> float: return ( self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + stroke_width @@ -571,25 +576,25 @@ class ImageDraw: def text( self, xy: tuple[float, float], - text: str, - fill=None, + text: AnyStr, + fill: _Ink | None = None, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, - *args, - **kwargs, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, + *args: Any, + **kwargs: Any, ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): @@ -623,15 +628,14 @@ class ImageDraw: return fill_ink return ink - def draw_text(ink, stroke_width=0) -> None: + def draw_text(ink: int, stroke_width: float = 0) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" coord = [] - start = [] for i in range(2): coord.append(int(xy[i])) - start.append(math.modf(xy[i])[0]) + start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) try: mask, offset = font.getmask2( # type: ignore[union-attr,misc] text, @@ -697,25 +701,25 @@ class ImageDraw: def multiline_text( self, xy: tuple[float, float], - text: str, - fill=None, + text: AnyStr, + fill: _Ink | None = None, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, *, - font_size=None, + font_size: float | None = None, ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" @@ -788,19 +792,19 @@ class ImageDraw: def textlength( self, - text: str, + text: AnyStr, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - direction=None, - features=None, - language=None, - embedded_color=False, + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + embedded_color: bool = False, *, - font_size=None, + font_size: float | None = None, ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): @@ -817,20 +821,25 @@ class ImageDraw: def textbbox( self, - xy, - text, - font=None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - embedded_color=False, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, *, - font_size=None, - ) -> tuple[int, int, int, int]: + font_size: float | None = None, + ) -> tuple[float, float, float, float]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -862,20 +871,25 @@ class ImageDraw: def multiline_textbbox( self, - xy, - text, - font=None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - embedded_color=False, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, *, - font_size=None, - ) -> tuple[int, int, int, int]: + font_size: float | None = None, + ) -> tuple[float, float, float, float]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -914,7 +928,7 @@ class ImageDraw: elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox: tuple[int, int, int, int] | None = None + bbox: tuple[float, float, float, float] | None = None for idx, line in enumerate(lines): left = xy[0] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d260eef69..24490348c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO +from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict from . import Image from ._typing import StrOrBytesPath @@ -46,6 +46,13 @@ if TYPE_CHECKING: from ._imagingft import Font +class Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + + class Layout(IntEnum): BASIC = 0 RAQM = 1 @@ -138,7 +145,9 @@ class ImageFont: self.font = Image.core.font(image.im, data) - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: """ Create a bitmap for the text. @@ -236,7 +245,7 @@ class FreeTypeFont: self.layout_engine = layout_engine - def load_from_bytes(f): + def load_from_bytes(f) -> None: self.font_bytes = f.read() self.font = core.getfont( "", size, index, encoding, self.font_bytes, layout_engine @@ -283,7 +292,12 @@ class FreeTypeFont: return self.font.ascent, self.font.descent def getlength( - self, text: str | bytes, mode="", direction=None, features=None, language=None + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered @@ -424,16 +438,16 @@ class FreeTypeFont: def getmask( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - ): + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + ) -> Image.core.ImagingCore: """ Create a bitmap for the text. @@ -516,17 +530,17 @@ class FreeTypeFont: def getmask2( self, text: str | bytes, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - *args, - **kwargs, - ): + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + *args: Any, + **kwargs: Any, + ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: """ Create a bitmap for the text. @@ -599,7 +613,7 @@ class FreeTypeFont: if start is None: start = (0, 0) - def fill(width, height): + def fill(width: int, height: int) -> Image.core.ImagingCore: size = (width, height) Image._decompression_bomb_check(size) return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) @@ -619,8 +633,13 @@ class FreeTypeFont: ) def font_variant( - self, font=None, size=None, index=None, encoding=None, layout_engine=None - ): + self, + font: StrOrBytesPath | BinaryIO | None = None, + size: float | None = None, + index: int | None = None, + encoding: str | None = None, + layout_engine: Layout | None = None, + ) -> FreeTypeFont: """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -655,7 +674,7 @@ class FreeTypeFont: raise NotImplementedError(msg) from e return [name.replace(b"\x00", b"") for name in names] - def set_variation_by_name(self, name): + def set_variation_by_name(self, name: str | bytes) -> None: """ :param name: The name of the style. :exception OSError: If the font is not a variation font. @@ -674,7 +693,7 @@ class FreeTypeFont: self.font.setvarname(index) - def get_variation_axes(self): + def get_variation_axes(self) -> list[Axis]: """ :returns: A list of the axes in a variation font. :exception OSError: If the font is not a variation font. @@ -704,7 +723,9 @@ class FreeTypeFont: class TransposedFont: """Wrapper for writing rotated or mirrored text""" - def __init__(self, font, orientation=None): + def __init__( + self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None + ): """ Wrapper that creates a transposed font from any existing font object. @@ -718,13 +739,17 @@ class TransposedFont: self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: return im.transpose(self.orientation) return im - def getbbox(self, text, *args, **kwargs): + def getbbox( + self, text: str | bytes, *args: Any, **kwargs: Any + ) -> tuple[int, int, float, float]: # TransposedFont doesn't support getmask2, move top-left point to (0, 0) # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) @@ -734,7 +759,7 @@ class TransposedFont: return 0, 0, height, width return 0, 0, width, height - def getlength(self, text: str | bytes, *args, **kwargs) -> float: + def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index 8cccd3ac7..998bc52eb 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,6 +1,7 @@ from typing import Any class ImagingCore: + def __getitem__(self, index: int) -> float: ... def __getattr__(self, name: str) -> Any: ... class ImagingFont: diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 5e97b40b2..9cc9822f5 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,12 +1,6 @@ -from typing import Any, TypedDict +from typing import Any, Callable -from . import _imaging - -class _Axis(TypedDict): - minimum: int | None - default: int | None - maximum: int | None - name: bytes | None +from . import ImageFont, _imaging class Font: @property @@ -28,42 +22,48 @@ class Font: def render( self, string: str | bytes, - fill, - mode=..., - dir=..., - features=..., - lang=..., - stroke_width=..., - anchor=..., - foreground_ink_long=..., - x_start=..., - y_start=..., + fill: Callable[[int, int], _imaging.ImagingCore], + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + stroke_width: float, + anchor: str | None, + foreground_ink_long: int, + x_start: float, + y_start: float, /, ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( self, string: str | bytes | bytearray, - mode=..., - dir=..., - features=..., - lang=..., - anchor=..., + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + anchor: str | None, /, ) -> tuple[tuple[int, int], tuple[int, int]]: ... def getlength( - self, string: str | bytes, mode=..., dir=..., features=..., lang=..., / + self, + string: str | bytes, + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + /, ) -> float: ... def getvarnames(self) -> list[bytes]: ... - def getvaraxes(self) -> list[_Axis] | None: ... + def getvaraxes(self) -> list[ImageFont.Axis]: ... def setvarname(self, instance_index: int, /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ... def getfont( filename: str | bytes, size: float, - index=..., - encoding=..., - font_bytes=..., - layout_engine=..., + index: int, + encoding: str, + font_bytes: bytes, + layout_engine: int, ) -> Font: ... def __getattr__(name: str) -> Any: ... From 046285ac5d0998ec8c80e4cf70192dcdf4753ec4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Jul 2024 16:42:28 +1000 Subject: [PATCH 059/136] Added type hints --- Tests/test_file_iptc.py | 1 + src/PIL/Image.py | 14 +++++------ src/PIL/ImageDraw2.py | 49 ++++++++++++++++++++++---------------- src/PIL/ImageQt.py | 2 +- src/PIL/IptcImagePlugin.py | 4 +++- src/PIL/TiffImagePlugin.py | 24 ++++++++++--------- 6 files changed, 53 insertions(+), 41 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 88c30d468..b0ea2bf42 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None: iptc = IptcImagePlugin.getiptcinfo(im) # Assert + assert iptc is not None for tag in iptc.keys(): if tag[0] == 240: return diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aa5eeabb2..72d167a8a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -468,40 +468,40 @@ def _getencoder( class _E: - def __init__(self, scale, offset) -> None: + def __init__(self, scale: float, offset: float) -> None: self.scale = scale self.offset = offset def __neg__(self) -> _E: return _E(-self.scale, -self.offset) - def __add__(self, other) -> _E: + def __add__(self, other: _E | float) -> _E: if isinstance(other, _E): return _E(self.scale + other.scale, self.offset + other.offset) return _E(self.scale, self.offset + other) __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other: _E | float) -> _E: return self + -other - def __rsub__(self, other): + def __rsub__(self, other: _E | float) -> _E: return other + -self - def __mul__(self, other) -> _E: + def __mul__(self, other: _E | float) -> _E: if isinstance(other, _E): return NotImplemented return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other) -> _E: + def __truediv__(self, other: _E | float) -> _E: if isinstance(other, _E): return NotImplemented return _E(self.scale / other, self.offset / other) -def _getscaleoffset(expr): +def _getscaleoffset(expr) -> tuple[float, float]: a = expr(_E(1, 0)) return (a.scale, a.offset) if isinstance(a, _E) else (0, a) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index e89a78be4..c0d89e4aa 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,10 +24,10 @@ """ from __future__ import annotations -from typing import BinaryIO +from typing import AnyStr, BinaryIO from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._typing import StrOrBytesPath +from ._typing import Coords, StrOrBytesPath class Pen: @@ -74,12 +74,14 @@ class Draw: image = Image.new(image, size, color) self.draw = ImageDraw.Draw(image) self.image = image - self.transform = None + self.transform: tuple[float, float, float, float, float, float] | None = None def flush(self) -> Image.Image: return self.image - def render(self, op, xy, pen, brush=None): + def render( + self, op: str, xy: Coords, pen: Pen | Brush, brush: Brush | Pen | None = None + ) -> None: # handle color arguments outline = fill = None width = 1 @@ -95,20 +97,21 @@ class Draw: fill = pen.color # handle transformation if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path # render the item if op == "line": self.draw.line(xy, fill=outline, width=width) else: getattr(self.draw, op)(xy, fill=fill, outline=outline) - def settransform(self, offset): + def settransform(self, offset: tuple[float, float]) -> None: """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy, start, end, *options): + def arc(self, xy: Coords, start, end, *options) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -117,7 +120,7 @@ class Draw: """ self.render("arc", xy, start, end, *options) - def chord(self, xy, start, end, *options): + def chord(self, xy: Coords, start, end, *options) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. @@ -126,7 +129,7 @@ class Draw: """ self.render("chord", xy, start, end, *options) - def ellipse(self, xy, *options): + def ellipse(self, xy: Coords, *options) -> None: """ Draws an ellipse inside the given bounding box. @@ -134,7 +137,7 @@ class Draw: """ self.render("ellipse", xy, *options) - def line(self, xy, *options): + def line(self, xy: Coords, *options) -> None: """ Draws a line between the coordinates in the ``xy`` list. @@ -142,7 +145,7 @@ class Draw: """ self.render("line", xy, *options) - def pieslice(self, xy, start, end, *options): + def pieslice(self, xy: Coords, start, end, *options) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -151,7 +154,7 @@ class Draw: """ self.render("pieslice", xy, start, end, *options) - def polygon(self, xy, *options): + def polygon(self, xy: Coords, *options) -> None: """ Draws a polygon. @@ -164,7 +167,7 @@ class Draw: """ self.render("polygon", xy, *options) - def rectangle(self, xy, *options): + def rectangle(self, xy: Coords, *options) -> None: """ Draws a rectangle. @@ -172,18 +175,21 @@ class Draw: """ self.render("rectangle", xy, *options) - def text(self, xy, text, font): + def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: """ Draws the string at the given position. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` """ if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path self.draw.text(xy, text, font=font.font, fill=font.color) - def textbbox(self, xy, text, font): + def textbbox( + self, xy: tuple[float, float], text: AnyStr, font: Font + ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text. @@ -192,11 +198,12 @@ class Draw: .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` """ if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path return self.draw.textbbox(xy, text, font=font.font) - def textlength(self, text, font): + def textlength(self, text: AnyStr, font: Font) -> float: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 346fe49d3..a2d946714 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -58,7 +58,7 @@ else: qt_version = None -def rgb(r, g, b, a=255): +def rgb(r: int, g: int, b: int, a: int = 255) -> int: """(Internal) Turns an RGB color into a Qt compatible color integer.""" # use qRgb to pack the colors, and then turn the resulting long # into a negative integer with the same bitpattern. diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 16a18ddfa..17243e705 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -185,7 +185,9 @@ Image.register_open(IptcImageFile.format, IptcImageFile) Image.register_extension(IptcImageFile.format, ".iim") -def getiptcinfo(im: ImageFile.ImageFile): +def getiptcinfo( + im: ImageFile.ImageFile, +) -> dict[tuple[int, int], bytes | list[bytes]] | None: """ Get IPTC information from TIFF, JPEG, or IPTC file. diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1dab0d50b..ad050ffe4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -584,8 +584,10 @@ class ImageFileDirectory_v2(_IFDv2Base): self.tagtype: dict[int, int] = {} """ Dictionary of tag types """ self.reset() - (self.next,) = ( - self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) + self.next = ( + self._unpack("Q", ifh[8:])[0] + if self._bigtiff + else self._unpack("L", ifh[4:])[0] ) self._legacy_api = False @@ -643,7 +645,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __setitem__(self, tag: int, value) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag, value, legacy_api) -> None: + def _setitem(self, tag: int, value, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -731,7 +733,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt: str, data): + def _unpack(self, fmt: str, data: bytes): return struct.unpack(self._endian + fmt, data) def _pack(self, fmt: str, *values): @@ -755,11 +757,11 @@ class ImageFileDirectory_v2(_IFDv2Base): ) @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data, legacy_api: bool = True): + def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: return data @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data) -> bytes: + def write_byte(self, data: bytes | int | IFDRational) -> bytes: if isinstance(data, IFDRational): data = int(data) if isinstance(data, int): @@ -773,7 +775,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return data.decode("latin-1", "replace") @_register_writer(2) - def write_string(self, value) -> bytes: + def write_string(self, value: str | bytes | int) -> bytes: # remerge of https://github.com/python-pillow/Pillow/pull/1416 if isinstance(value, int): value = str(value) @@ -782,7 +784,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return value + b"\0" @_register_loader(5, 8) - def load_rational(self, data, legacy_api=True): + def load_rational(self, data, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}L", data) def combine(a, b): @@ -797,11 +799,11 @@ class ImageFileDirectory_v2(_IFDv2Base): ) @_register_loader(7, 1) - def load_undefined(self, data, legacy_api: bool = True): + def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: return data @_register_writer(7) - def write_undefined(self, value) -> bytes: + def write_undefined(self, value: bytes | int | IFDRational) -> bytes: if isinstance(value, IFDRational): value = int(value) if isinstance(value, int): @@ -809,7 +811,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data, legacy_api: bool = True): + def load_signed_rational(self, data: bytes, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}l", data) def combine(a, b): From c85eb0cae5568ba9286ac15c3e8f1709f7bbe9e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jul 2024 12:53:02 +1000 Subject: [PATCH 060/136] Added type hints --- Tests/test_file_png.py | 2 ++ src/PIL/Image.py | 30 +++++++++++++++++------------- src/PIL/ImageDraw2.py | 14 +++++++------- src/PIL/PngImagePlugin.py | 38 +++++++++++++++++++------------------- src/PIL/TiffImagePlugin.py | 8 +++++--- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 4958b2222..0abf9866f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -424,8 +424,10 @@ class TestFilePng: im = roundtrip(im, pnginfo=info) assert im.info == {"spam": "Eggs", "eggs": "Spam"} assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert isinstance(im.text["spam"], PngImagePlugin.iTXt) assert im.text["spam"].lang == "en" assert im.text["spam"].tkey == "Spam" + assert isinstance(im.text["eggs"], PngImagePlugin.iTXt) assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 72d167a8a..eb3dd5a36 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,7 +38,7 @@ import struct import sys import tempfile import warnings -from collections.abc import Callable, MutableMapping, Sequence +from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType from typing import ( @@ -744,11 +744,11 @@ class Image: new["shape"], new["typestr"] = _conv_type_shape(self) return new - def __getstate__(self): + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) info, mode, size, palette, data = state self.info = info @@ -1683,7 +1683,9 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: + def histogram( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1709,12 +1711,14 @@ class Image: mask.load() return self.im.histogram((0, 0), mask.im) if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) + return self.im.histogram( + extrema if extrema is not None else self.getextrema() + ) return self.im.histogram() - def entropy(self, mask: Image | None = None, extrema=None): + def entropy( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> float: """ Calculates and returns the entropy for the image. @@ -1735,9 +1739,9 @@ class Image: mask.load() return self.im.entropy((0, 0), mask.im) if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.entropy(extrema) + return self.im.entropy( + extrema if extrema is not None else self.getextrema() + ) return self.im.entropy() def paste( @@ -3881,7 +3885,7 @@ class Exif(_ExifBase): # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset: int, group=None): + def _get_ifd_dict(self, offset: int, group: int | None = None): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. @@ -4136,7 +4140,7 @@ class Exif(_ExifBase): else: del self._data[tag] - def __iter__(self): + def __iter__(self) -> Iterator[int]: keys = set(self._data) if self._info is not None: keys.update(self._info) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index c0d89e4aa..1ad239382 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,7 +24,7 @@ """ from __future__ import annotations -from typing import AnyStr, BinaryIO +from typing import Any, AnyStr, BinaryIO from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from ._typing import Coords, StrOrBytesPath @@ -111,7 +111,7 @@ class Draw: (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, start, end, *options) -> None: + def arc(self, xy: Coords, start, end, *options: Any) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -120,7 +120,7 @@ class Draw: """ self.render("arc", xy, start, end, *options) - def chord(self, xy: Coords, start, end, *options) -> None: + def chord(self, xy: Coords, start, end, *options: Any) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. @@ -129,7 +129,7 @@ class Draw: """ self.render("chord", xy, start, end, *options) - def ellipse(self, xy: Coords, *options) -> None: + def ellipse(self, xy: Coords, *options: Any) -> None: """ Draws an ellipse inside the given bounding box. @@ -137,7 +137,7 @@ class Draw: """ self.render("ellipse", xy, *options) - def line(self, xy: Coords, *options) -> None: + def line(self, xy: Coords, *options: Any) -> None: """ Draws a line between the coordinates in the ``xy`` list. @@ -145,7 +145,7 @@ class Draw: """ self.render("line", xy, *options) - def pieslice(self, xy: Coords, start, end, *options) -> None: + def pieslice(self, xy: Coords, start, end, *options: Any) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -154,7 +154,7 @@ class Draw: """ self.render("pieslice", xy, start, end, *options) - def polygon(self, xy: Coords, *options) -> None: + def polygon(self, xy: Coords, *options: Any) -> None: """ Draws a polygon. diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 58db7777c..52cfcd13a 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,6 +38,7 @@ import re import struct import warnings import zlib +from collections.abc import Callable from enum import IntEnum from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn @@ -135,7 +136,7 @@ class Blend(IntEnum): """ -def _safe_zlib_decompress(s): +def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: @@ -783,7 +784,7 @@ class PngImageFile(ImageFile.ImageFile): self._mode = self.png.im_mode self._size = self.png.im_size self.info = self.png.im_info - self._text = None + self._text: dict[str, str | iTXt] | None = None self.tile = self.png.im_tile self.custom_mimetype = self.png.im_custom_mimetype self.n_frames = self.png.im_n_frames or 1 @@ -810,7 +811,7 @@ class PngImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 @property - def text(self): + def text(self) -> dict[str, str | iTXt]: # experimental if self._text is None: # iTxt, tEXt and zTXt chunks may appear at the end of the file @@ -822,6 +823,7 @@ class PngImageFile(ImageFile.ImageFile): self.load() if self.is_animated: self.seek(frame) + assert self._text is not None return self._text def verify(self) -> None: @@ -1105,7 +1107,7 @@ def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: class _idat: # wrap output from the encoder in IDAT chunks - def __init__(self, fp, chunk) -> None: + def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: self.fp = fp self.chunk = chunk @@ -1116,7 +1118,7 @@ class _idat: class _fdat: # wrap encoder output in fdAT chunks - def __init__(self, fp: IO[bytes], chunk, seq_num: int) -> None: + def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: self.fp = fp self.chunk = chunk self.seq_num = seq_num @@ -1135,7 +1137,7 @@ class _Frame(NamedTuple): def _write_multiple_frames( im: Image.Image, fp: IO[bytes], - chunk, + chunk: Callable[..., None], mode: str, rawmode: str, default_image: Image.Image | None, @@ -1275,7 +1277,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save( - im: Image.Image, fp, filename: str | bytes, chunk=putchunk, save_all: bool = False + im: Image.Image, + fp: IO[bytes], + filename: str | bytes, + chunk: Callable[..., None] = putchunk, + save_all: bool = False, ) -> None: # save an image to disk (called by the save method) @@ -1483,22 +1489,16 @@ def _save( def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: """Return a list of PNG chunks representing this image.""" + from io import BytesIO - class collector: - data = [] + chunks = [] - def write(self, data: bytes) -> None: - pass - - def append(self, chunk: tuple[bytes, bytes, bytes]) -> None: - self.data.append(chunk) - - def append(fp: collector, cid: bytes, *data: bytes) -> None: + def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: byte_data = b"".join(data) crc = o32(_crc32(byte_data, _crc32(cid))) - fp.append((cid, byte_data, crc)) + chunks.append((cid, byte_data, crc)) - fp = collector() + fp = BytesIO() try: im.encoderinfo = params @@ -1506,7 +1506,7 @@ def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes] finally: del im.encoderinfo - return fp.data + return chunks # -------------------------------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ad050ffe4..318e9825c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1020,7 +1020,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._legacy_api = True @@ -1142,13 +1142,15 @@ class TiffImageFile(ImageFile.ImageFile): self._seek(0) @property - def n_frames(self): - if self._n_frames is None: + def n_frames(self) -> int: + current_n_frames = self._n_frames + if current_n_frames is None: current = self.tell() self._seek(len(self._frame_pos)) while self._n_frames is None: self._seek(self.tell() + 1) self.seek(current) + assert self._n_frames is not None return self._n_frames def seek(self, frame: int) -> None: From a03033e7f3cdc1bdf9a6aabc09d6dc2aca97cff7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 15:52:46 +0400 Subject: [PATCH 061/136] Remove all WITH_* flags from _imaging.c --- src/_imaging.c | 89 +++----------------------------------------------- 1 file changed, 4 insertions(+), 85 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 5c2f7b4b6..d675cf22e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -93,19 +93,6 @@ #define _USE_MATH_DEFINES #include -/* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ -#define WITH_MAPPING /* use memory mapping to read some file formats */ -#define WITH_IMAGEPATH /* ImagePath stuff */ -#define WITH_ARROW /* arrow graphics stuff (experimental) */ -#define WITH_EFFECTS /* special effects */ -#define WITH_QUANTIZE /* quantization support */ -#define WITH_RANKFILTER /* rank filter */ -#define WITH_MODEFILTER /* mode filter */ -#define WITH_THREADING /* "friendly" threading support */ -#define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ - #undef VERBOSE #define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) @@ -123,8 +110,6 @@ typedef struct { static PyTypeObject Imaging_Type; -#ifdef WITH_IMAGEDRAW - typedef struct { /* to write a character, cut out sxy from glyph data, place at current position plus dxy, and advance by (dx, dy) */ @@ -151,8 +136,6 @@ typedef struct { static PyTypeObject ImagingDraw_Type; -#endif - typedef struct { PyObject_HEAD ImagingObject *image; int readonly; @@ -215,16 +198,12 @@ PyImaging_AsImaging(PyObject *op) { void ImagingSectionEnter(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING *cookie = (PyThreadState *)PyEval_SaveThread(); -#endif } void ImagingSectionLeave(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING PyEval_RestoreThread((PyThreadState *)*cookie); -#endif } /* -------------------------------------------------------------------- */ @@ -1091,7 +1070,6 @@ _filter(ImagingObject *self, PyObject *args) { return imOut; } -#ifdef WITH_UNSHARPMASK static PyObject * _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -1116,7 +1094,6 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#endif static PyObject * _getpalette(ImagingObject *self, PyObject *args) { @@ -1374,7 +1351,6 @@ _entropy(ImagingObject *self, PyObject *args) { return PyFloat_FromDouble(-entropy); } -#ifdef WITH_MODEFILTER static PyObject * _modefilter(ImagingObject *self, PyObject *args) { int size; @@ -1384,7 +1360,6 @@ _modefilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingModeFilter(self->image, size)); } -#endif static PyObject * _offset(ImagingObject *self, PyObject *args) { @@ -1716,8 +1691,6 @@ _putdata(ImagingObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_QUANTIZE - static PyObject * _quantize(ImagingObject *self, PyObject *args) { int colours = 256; @@ -1734,7 +1707,6 @@ _quantize(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); } -#endif static PyObject * _putpalette(ImagingObject *self, PyObject *args) { @@ -1870,7 +1842,6 @@ _putpixel(ImagingObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_RANKFILTER static PyObject * _rankfilter(ImagingObject *self, PyObject *args) { int size, rank; @@ -1880,7 +1851,6 @@ _rankfilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingRankFilter(self->image, size, rank)); } -#endif static PyObject * _resize(ImagingObject *self, PyObject *args) { @@ -2162,7 +2132,6 @@ _transpose(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#ifdef WITH_UNSHARPMASK static PyObject * _unsharp_mask(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -2186,7 +2155,6 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#endif static PyObject * _box_blur(ImagingObject *self, PyObject *args) { @@ -2463,9 +2431,7 @@ _split(ImagingObject *self) { return list; } -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGECHOPS +/* Channel operations (ImageChops) ------------------------------------ */ static PyObject * _chop_invert(ImagingObject *self) { @@ -2646,11 +2612,8 @@ _chop_overlay(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } -#endif -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGEDRAW +/* Graphics (ImageDraw) ----------------------------------------------- */ static PyObject * _font_new(PyObject *self_, PyObject *args) { @@ -3233,8 +3196,6 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { return Py_None; } -#ifdef WITH_ARROW - /* from outline.c */ extern ImagingOutline PyOutline_AsOutline(PyObject *outline); @@ -3264,8 +3225,6 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return Py_None; } -#endif - static PyObject * _draw_pieslice(ImagingDrawObject *self, PyObject *args) { double *xy; @@ -3431,12 +3390,9 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { } static struct PyMethodDef _draw_methods[] = { -#ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, -#ifdef WITH_ARROW {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, -#endif {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, @@ -3446,11 +3402,9 @@ static struct PyMethodDef _draw_methods[] = { {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, -#endif {NULL, NULL} /* sentinel */ }; -#endif static PyObject * pixel_access_new(ImagingObject *imagep, PyObject *args) { @@ -3532,11 +3486,9 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { } /* -------------------------------------------------------------------- */ -/* EFFECTS (experimental) */ +/* EFFECTS (experimental) */ /* -------------------------------------------------------------------- */ -#ifdef WITH_EFFECTS - static PyObject * _effect_mandelbrot(ImagingObject *self, PyObject *args) { int xsize = 512; @@ -3588,8 +3540,6 @@ _effect_spread(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingEffectSpread(self->image, dist)); } -#endif - /* -------------------------------------------------------------------- */ /* UTILITIES */ /* -------------------------------------------------------------------- */ @@ -3670,20 +3620,14 @@ static struct PyMethodDef methods[] = { {"filter", (PyCFunction)_filter, METH_VARARGS}, {"histogram", (PyCFunction)_histogram, METH_VARARGS}, {"entropy", (PyCFunction)_entropy, METH_VARARGS}, -#ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, -#endif {"offset", (PyCFunction)_offset, METH_VARARGS}, {"paste", (PyCFunction)_paste, METH_VARARGS}, {"point", (PyCFunction)_point, METH_VARARGS}, {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, {"putdata", (PyCFunction)_putdata, METH_VARARGS}, -#ifdef WITH_QUANTIZE {"quantize", (PyCFunction)_quantize, METH_VARARGS}, -#endif -#ifdef WITH_RANKFILTER {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, -#endif {"resize", (PyCFunction)_resize, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS}, @@ -3709,7 +3653,6 @@ static struct PyMethodDef methods[] = { {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, -#ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, @@ -3728,20 +3671,14 @@ static struct PyMethodDef methods[] = { {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, -#endif - -#ifdef WITH_UNSHARPMASK - /* Kevin Cazabon's unsharpmask extension */ + /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, -#endif {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, -#ifdef WITH_EFFECTS /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, -#endif /* Misc. */ {"new_block", (PyCFunction)_new_block, METH_VARARGS}, @@ -3870,8 +3807,6 @@ static PyTypeObject Imaging_Type = { getsetters, /*tp_getset*/ }; -#ifdef WITH_IMAGEDRAW - static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ sizeof(ImagingFontObject), /*tp_basicsize*/ @@ -3938,8 +3873,6 @@ static PyTypeObject ImagingDraw_Type = { 0, /*tp_getset*/ }; -#endif - static PyMappingMethods pixel_access_as_mapping = { (lenfunc)NULL, /*mp_length*/ (binaryfunc)pixel_access_getitem, /*mp_subscript*/ @@ -4283,9 +4216,7 @@ static PyMethodDef functions[] = { #endif /* Memory mapping */ -#ifdef WITH_MAPPING {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -#endif /* Display support */ #ifdef _WIN32 @@ -4305,29 +4236,21 @@ static PyMethodDef functions[] = { {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, /* Special effects (experimental) */ -#ifdef WITH_EFFECTS {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -#endif /* Drawing support stuff */ -#ifdef WITH_IMAGEDRAW {"font", (PyCFunction)_font_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -#endif /* Experimental path stuff */ -#ifdef WITH_IMAGEPATH {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -#endif /* Experimental arrow graphics stuff */ -#ifdef WITH_ARROW {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, -#endif /* Resource management */ {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, @@ -4352,16 +4275,12 @@ setup_module(PyObject *m) { if (PyType_Ready(&Imaging_Type) < 0) { return -1; } - -#ifdef WITH_IMAGEDRAW if (PyType_Ready(&ImagingFont_Type) < 0) { return -1; } - if (PyType_Ready(&ImagingDraw_Type) < 0) { return -1; } -#endif if (PyType_Ready(&PixelAccess_Type) < 0) { return -1; } From cfce566d17999698e6f8fa2ccd815c8146b5de20 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 16:08:52 +0400 Subject: [PATCH 062/136] codestyle --- src/_imaging.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d675cf22e..91cae2e5d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3405,7 +3405,6 @@ static struct PyMethodDef _draw_methods[] = { {NULL, NULL} /* sentinel */ }; - static PyObject * pixel_access_new(ImagingObject *imagep, PyObject *args) { PixelAccessObject *self; @@ -4215,11 +4214,11 @@ static PyMethodDef functions[] = { {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif -/* Memory mapping */ + /* Memory mapping */ {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -/* Display support */ #ifdef _WIN32 + /* Display support */ {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, @@ -4235,21 +4234,21 @@ static PyMethodDef functions[] = { /* Utilities */ {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, -/* Special effects (experimental) */ + /* Special effects (experimental) */ {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -/* Drawing support stuff */ + /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -/* Experimental path stuff */ + /* Experimental path stuff */ {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -/* Experimental arrow graphics stuff */ + /* Experimental arrow graphics stuff */ {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, /* Resource management */ From dc53356c1a9dd222e504d390861fdac7368bb300 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 23:47:21 +0400 Subject: [PATCH 063/136] Not needed since memcpy is used here --- src/libImaging/Pack.c | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index f3b714215..c29473d90 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -258,16 +258,6 @@ void ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { int i = 0; /* RGB triplets */ -#ifdef __sparc - /* SPARC CPUs cannot read integers from nonaligned addresses. */ - for (; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[G]; - out[2] = in[B]; - out += 3; - in += 4; - } -#else for (; i < pixels - 1; i++) { memcpy(out, in + i * 4, 4); out += 3; @@ -278,7 +268,6 @@ ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { out[2] = in[i * 4 + B]; out += 3; } -#endif } void From d00fb87fa3b06776c14b882d0b39f737c39ec8da Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 11:08:32 +0400 Subject: [PATCH 064/136] Rename Not NO_OUTPUT to DEBUG, remove TEST_MERGESORT and TEST_SPLIT* flags --- src/libImaging/Quant.c | 106 +++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 197f9f3ee..595ee42a0 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -36,7 +36,8 @@ #define UINT32_MAX 0xffffffff #endif -#define NO_OUTPUT +// #define DEBUG +// #define TEST_NEAREST_NEIGHBOUR typedef struct { uint32_t scale; @@ -144,7 +145,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { PixelHashData *d; HashTable *hash; uint32_t i; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2, timer3; #endif @@ -156,7 +157,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { hash = hashtable_new(pixel_hash, pixel_cmp); hashtable_set_user_data(hash, d); d->scale = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG timer = timer3 = clock(); #endif for (i = 0; i < nPixels; i++) { @@ -167,22 +168,22 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { } while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { d->scale++; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("rehashing - new scale: %d\n", (int)d->scale); timer2 = clock(); #endif hashtable_rehash_compute(hash, rehash_collide); -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock() - timer2; printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); timer += timer2; #endif } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); #endif return hash; @@ -304,7 +305,7 @@ mergesort_pixels(PixelList *head, int i) { return head; } -#if defined(TEST_MERGESORT) || defined(TEST_SORTED) +#ifdef DEBUG static int test_sorted(PixelList *pl[3]) { int i, n, l; @@ -347,12 +348,12 @@ splitlists( PixelList *l, *r, *c, *n; int i; int nRight; -#ifndef NO_OUTPUT +#ifdef DEBUG int nLeft; #endif int splitColourVal; -#ifdef TEST_SPLIT +#ifdef DEBUG { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -402,14 +403,14 @@ splitlists( #endif nCount[0] = nCount[1] = 0; nRight = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft = 0; #endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif c = c->next[axis]; @@ -424,7 +425,7 @@ splitlists( break; } c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif nCount[0] += c->count; @@ -442,14 +443,14 @@ splitlists( } c->flag = 1; nRight++; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft--; #endif nCount[0] -= c->count; nCount[1] += c->count; } } -#ifndef NO_OUTPUT +#ifdef DEBUG if (!nLeft) { for (c = h[axis]; c; c = c->next[axis]) { printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); @@ -511,7 +512,7 @@ split(BoxNode *node) { gl = node->tail[1]->p.c.g; bh = node->head[2]->p.c.b; bl = node->tail[2]->p.c.b; -#ifdef TEST_SPLIT +#ifdef DEBUG printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); #endif f[0] = (rh - rl) * 77; @@ -526,11 +527,8 @@ split(BoxNode *node) { axis = i; } } -#ifdef TEST_SPLIT +#ifdef DEBUG printf("along axis %d\n", axis + 1); -#endif - -#ifdef TEST_SPLIT { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -592,12 +590,12 @@ split(BoxNode *node) { if (!splitlists( node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount )) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("list split failed.\n"); #endif return 0; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (!test_sorted(heads[0])) { printf("bug in split"); exit(1); @@ -623,7 +621,7 @@ split(BoxNode *node) { node->head[i] = NULL; node->tail[i] = NULL; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (left->head[0]) { rh = left->head[0]->p.c.r; rl = left->tail[0]->p.c.r; @@ -687,7 +685,7 @@ median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { } } while (compute_box_volume(thisNode) == 1); if (!split(thisNode)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("Oops, split failed...\n"); #endif exit(1); @@ -716,16 +714,14 @@ free_box_tree(BoxNode *n) { free(n); } -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG static int checkContained(BoxNode *n, Pixel *pp) { if (n->l && n->r) { return checkContained(n->l, pp) + checkContained(n->r, pp); } if (n->l || n->r) { -#ifndef NO_OUTPUT printf("box tree is dead\n"); -#endif return 0; } if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && @@ -746,7 +742,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); } if (n->l || n->r) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("box tree is dead\n"); #endif return 0; @@ -754,7 +750,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { for (p = n->head[0]; p; p = p->next[0]) { PIXEL_UNSCALE(&(p->p), &q, d->scale); if (!hashtable_insert(h, q, *box)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("hashtable insert failed\n"); #endif return 0; @@ -978,7 +974,7 @@ map_image_pixels_from_median_box( continue; } if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif return 0; @@ -1043,7 +1039,7 @@ compute_palette_from_median_cut( } } for (i = 0; i < nPixels; i++) { -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG if (!(i % 100)) { printf("%05d\r", i); fflush(stdout); @@ -1058,7 +1054,7 @@ compute_palette_from_median_cut( } #endif if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif for (i = 0; i < 3; i++) { @@ -1068,7 +1064,7 @@ compute_palette_from_median_cut( return 0; } if (paletteEntry >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf( "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", (int)paletteEntry, @@ -1140,7 +1136,7 @@ compute_palette_from_quantized_pixels( } for (i = 0; i < nPixels; i++) { if (qp[i] >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("scream\n"); #endif return 0; @@ -1208,7 +1204,7 @@ k_means( goto error_2; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("["); fflush(stdout); #endif @@ -1243,7 +1239,7 @@ k_means( if (changes < 0) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf(".(%d)", changes); fflush(stdout); #endif @@ -1251,7 +1247,7 @@ k_means( break; } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("]\n"); #endif if (avgDistSortKey) { @@ -1311,32 +1307,32 @@ quantize( uint32_t **avgDistSortKey; Pixel *p; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2; #endif -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock(); printf("create hash table..."); fflush(stdout); timer = clock(); #endif h = create_pixel_hash(pixelData, nPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!h) { goto error_0; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("create lists from hash table..."); fflush(stdout); timer = clock(); #endif hl[0] = hl[1] = hl[2] = NULL; hashtable_foreach(h, hash_to_list, hl); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1344,7 +1340,7 @@ quantize( goto error_1; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("mergesort lists..."); fflush(stdout); timer = clock(); @@ -1352,39 +1348,37 @@ quantize( for (i = 0; i < 3; i++) { hl[i] = mergesort_pixels(hl[i], i); } -#ifdef TEST_MERGESORT +#ifdef DEBUG if (!test_sorted(hl)) { printf("bug in mergesort\n"); goto error_1; } -#endif -#ifndef NO_OUTPUT printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut..."); fflush(stdout); timer = clock(); #endif root = median_cut(hl, nPixels, nQuantPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!root) { goto error_1; } nPaletteEntries = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut tree to hash table..."); fflush(stdout); timer = clock(); #endif annotate_hash_table(root, h, &nPaletteEntries); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("compute palette...\n"); fflush(stdout); timer = clock(); @@ -1392,7 +1386,7 @@ quantize( if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1479,7 +1473,7 @@ quantize( hashtable_free(h2); } #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("k means...\n"); fflush(stdout); timer = clock(); @@ -1487,7 +1481,7 @@ quantize( if (kmeans > 0) { k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1495,7 +1489,7 @@ quantize( *palette = p; *paletteLength = nPaletteEntries; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("cleanup..."); fflush(stdout); timer = clock(); @@ -1507,7 +1501,7 @@ quantize( free(avgDistSortKey); } destroy_pixel_hash(h); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); #endif From a6d83ec9ccc23284bf5274325591499500e269d7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 28 Jul 2024 17:09:31 +0400 Subject: [PATCH 065/136] Add relesenotes --- docs/releasenotes/11.0.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb6179de8..936a441d9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -77,3 +77,10 @@ others prepare for 3.13, and to ensure Pillow could be used immediately at the r of 3.13.0 final (2024-10-01, :pep:`719`). Pillow 11.0.0 now officially supports Python 3.13. + +C-level Flags +^^^^^^^^^^^^^ + +Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other +``WITH_*`` were removed. These flags were not available in the build system, +but they could be edited in the C source. From 302962dae10b7dcae7e373ba8193e846211f3922 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 28 Jul 2024 17:19:50 +0400 Subject: [PATCH 066/136] Updated comments by @radarhere, lost during rebase --- docs/releasenotes/11.0.0.rst | 2 +- src/_imaging.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 936a441d9..f30442aef 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -82,5 +82,5 @@ C-level Flags ^^^^^^^^^^^^^ Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other -``WITH_*`` were removed. These flags were not available in the build system, +``WITH_*`` were removed. These flags were not available through the build system, but they could be edited in the C source. diff --git a/src/_imaging.c b/src/_imaging.c index 91cae2e5d..b24776ead 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2613,7 +2613,7 @@ _chop_overlay(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } -/* Graphics (ImageDraw) ----------------------------------------------- */ +/* Fonts (ImageDraw and ImageFont) ------------------------------------ */ static PyObject * _font_new(PyObject *self_, PyObject *args) { @@ -2842,7 +2842,7 @@ static struct PyMethodDef _font_methods[] = { {NULL, NULL} /* sentinel */ }; -/* -------------------------------------------------------------------- */ +/* Graphics (ImageDraw) ----------------------------------------------- */ static PyObject * _draw_new(PyObject *self_, PyObject *args) { From 6420f73613dce5533e2d8a417f8d7fede98de07e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Jul 2024 23:46:07 +1000 Subject: [PATCH 067/136] Added type hints --- src/PIL/Image.py | 6 +++- src/PIL/ImageDraw2.py | 2 +- src/PIL/ImageFont.py | 6 ++-- src/PIL/ImagePalette.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/PdfImagePlugin.py | 45 +++++++++++++++----------- src/PIL/TiffImagePlugin.py | 61 ++++++++++++++++++++---------------- 7 files changed, 71 insertions(+), 53 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index eb3dd5a36..ae8634539 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2052,7 +2052,11 @@ class Image: msg = "illegal image mode" raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) + if data.rawmode is not None: + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + palette = ImagePalette.ImagePalette(palette=data.palette) + palette.dirty = 1 else: if not isinstance(data, bytes): data = bytes(data) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 1ad239382..58302f950 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -167,7 +167,7 @@ class Draw: """ self.render("polygon", xy, *options) - def rectangle(self, xy: Coords, *options) -> None: + def rectangle(self, xy: Coords, *options: Any) -> None: """ Draws a rectangle. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 24490348c..2ab65bfef 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -269,12 +269,12 @@ class FreeTypeFont: else: load_from_bytes(font) - def __getstate__(self): + def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] - def __setstate__(self, state): + def __setstate__(self, state: list[Any]) -> None: path, size, index, encoding, layout_engine = state - self.__init__(path, size, index, encoding, layout_engine) + FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) def getname(self) -> tuple[str | None, str | None]: """ diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 8ccecbd07..183f85526 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -208,7 +208,7 @@ class ImagePalette: # Internal -def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: +def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index eeec41686..ef9107f00 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -324,7 +324,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): return self._reduce or super().reduce @reduce.setter - def reduce(self, value): + def reduce(self, value: int) -> None: self._reduce = value def load(self) -> Image.core.PixelAccess | None: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e0f732199..7fc1108bb 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import io import math import os import time -from typing import IO +from typing import IO, Any from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features @@ -48,7 +48,12 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # (Internal) Image save plugin for the PDF format. -def _write_image(im, filename, existing_pdf, image_refs): +def _write_image( + im: Image.Image, + filename: str | bytes, + existing_pdf: PdfParser.PdfParser, + image_refs: list[PdfParser.IndirectReference], +) -> tuple[PdfParser.IndirectReference, str]: # FIXME: Should replace ASCIIHexDecode with RunLengthDecode # (packbits) or LZWDecode (tiff/lzw compression). Note that # PDF 1.2 also supports Flatedecode (zip compression). @@ -61,10 +66,10 @@ def _write_image(im, filename, existing_pdf, image_refs): width, height = im.size - dict_obj = {"BitsPerComponent": 8} + dict_obj: dict[str, Any] = {"BitsPerComponent": 8} if im.mode == "1": if features.check("libtiff"): - filter = "CCITTFaxDecode" + decode_filter = "CCITTFaxDecode" dict_obj["BitsPerComponent"] = 1 params = PdfParser.PdfArray( [ @@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs): ] ) else: - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": - filter = "DCTDecode" + decode_filter = "DCTDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "LA": - filter = "JPXDecode" + decode_filter = "JPXDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" procset = "ImageB" # grayscale dict_obj["SMaskInData"] = 1 elif im.mode == "P": - filter = "ASCIIHexDecode" + decode_filter = "ASCIIHexDecode" palette = im.getpalette() + assert palette is not None dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), @@ -110,15 +116,15 @@ def _write_image(im, filename, existing_pdf, image_refs): image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] dict_obj["SMask"] = image_ref elif im.mode == "RGB": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images elif im.mode == "RGBA": - filter = "JPXDecode" + decode_filter = "JPXDecode" procset = "ImageC" # color images dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] @@ -131,9 +137,9 @@ def _write_image(im, filename, existing_pdf, image_refs): op = io.BytesIO() - if filter == "ASCIIHexDecode": + if decode_filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": + elif decode_filter == "CCITTFaxDecode": im.save( op, "TIFF", @@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs): # use a single strip strip_size=math.ceil(width / 8) * height, ) - elif filter == "DCTDecode": + elif decode_filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": + elif decode_filter == "JPXDecode": del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) else: - msg = f"unsupported PDF filter ({filter})" + msg = f"unsupported PDF filter ({decode_filter})" raise ValueError(msg) stream = op.getvalue() - if filter == "CCITTFaxDecode": + filter: PdfParser.PdfArray | PdfParser.PdfName + if decode_filter == "CCITTFaxDecode": stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) else: - filter = PdfParser.PdfName(filter) + filter = PdfParser.PdfName(decode_filter) image_ref = image_refs.pop(0) existing_pdf.write_obj( diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 318e9825c..dd013a0cf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,16 +47,18 @@ import math import os import struct import warnings -from collections.abc import MutableMapping +from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn +from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate +from ._typing import StrOrBytesPath +from ._util import is_path from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -313,7 +315,7 @@ _load_dispatch = {} _write_dispatch = {} -def _delegate(op): +def _delegate(op: str): def delegate(self, *args): return getattr(self._val, op)(*args) @@ -334,7 +336,9 @@ class IFDRational(Rational): __slots__ = ("_numerator", "_denominator", "_val") - def __init__(self, value, denominator: int = 1) -> None: + def __init__( + self, value: float | Fraction | IFDRational, denominator: int = 1 + ) -> None: """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational @@ -358,18 +362,20 @@ class IFDRational(Rational): self._val = float("nan") elif denominator == 1: self._val = Fraction(value) + elif int(value) == value: + self._val = Fraction(int(value), denominator) else: - self._val = Fraction(value, denominator) + self._val = Fraction(value / denominator) @property def numerator(self): return self._numerator @property - def denominator(self): + def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator): + def limit_rational(self, max_denominator: int) -> tuple[float, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -379,6 +385,7 @@ class IFDRational(Rational): if self.denominator == 0: return self.numerator, self.denominator + assert isinstance(self._val, Fraction) f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator @@ -396,14 +403,15 @@ class IFDRational(Rational): val = float(val) return val == other - def __getstate__(self): + def __getstate__(self) -> list[float | Fraction]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state): + def __setstate__(self, state: list[float | Fraction]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state self._val = _val self._numerator = _numerator + assert isinstance(_denominator, int) self._denominator = _denominator """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', @@ -730,13 +738,13 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) def _unpack(self, fmt: str, data: bytes): return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values): + def _pack(self, fmt: str, *values) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -787,7 +795,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def load_rational(self, data, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}L", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -814,7 +822,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def load_signed_rational(self, data: bytes, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}l", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -903,11 +911,11 @@ class ImageFileDirectory_v2(_IFDv2Base): warnings.warn(str(msg)) return - def tobytes(self, offset=0): + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? result = self._pack("H", len(self._tags_v2)) - entries = [] + entries: list[tuple[int, int, int, bytes, bytes]] = [] offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None @@ -916,7 +924,7 @@ class ImageFileDirectory_v2(_IFDv2Base): for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) - typ = self.tagtype.get(tag) + typ = self.tagtype[tag] logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: @@ -1072,7 +1080,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v1)) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) def __setitem__(self, tag: int, value) -> None: @@ -1943,17 +1951,18 @@ class AppendingTiffWriter: 521, # JPEGACTables } - def __init__(self, fn, new: bool = False) -> None: - if hasattr(fn, "read"): - self.f = fn - self.close_fp = False - else: + def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: + self.f: IO[bytes] + if is_path(fn): self.name = fn self.close_fp = True try: self.f = open(fn, "w+b" if new else "r+b") except OSError: self.f = open(fn, "w+b") + else: + self.f = cast(IO[bytes], fn) + self.close_fp = False self.beginning = self.f.tell() self.setup() @@ -1961,7 +1970,7 @@ class AppendingTiffWriter: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) - self.whereToWriteNewIFDOffset = None + self.whereToWriteNewIFDOffset: int | None = None self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) @@ -2000,6 +2009,7 @@ class AppendingTiffWriter: ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage + assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) self.writeLong(ifd_offset) self.f.seek(ifd_offset) @@ -2020,7 +2030,7 @@ class AppendingTiffWriter: def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage - def seek(self, offset: int, whence=io.SEEK_SET) -> int: + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2111,7 +2121,6 @@ class AppendingTiffWriter: field_size = self.fieldSizes[field_type] total_size = field_size * count is_local = total_size <= 4 - offset: int | None if not is_local: offset = self.readLong() + self.offsetOfNewPage self.rewriteLastLong(offset) @@ -2131,8 +2140,6 @@ class AppendingTiffWriter: ) self.f.seek(cur_pos) - offset = cur_pos = None - elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) From db5c4fbb2cb0978f58d503aadc6901c01c023a47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:41:37 +1000 Subject: [PATCH 068/136] Include required arguments --- src/PIL/ImageDraw2.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 58302f950..6e848663b 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -80,7 +80,7 @@ class Draw: return self.image def render( - self, op: str, xy: Coords, pen: Pen | Brush, brush: Brush | Pen | None = None + self, op: str, xy: Coords, pen: Pen | Brush | None, brush: Brush | Pen | None = None ) -> None: # handle color arguments outline = fill = None @@ -111,50 +111,50 @@ class Draw: (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, start, end, *options: Any) -> None: + def arc(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` """ - self.render("arc", xy, start, end, *options) + self.render("arc", xy, pen, start, end, *options) - def chord(self, xy: Coords, start, end, *options: Any) -> None: + def chord(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` """ - self.render("chord", xy, start, end, *options) + self.render("chord", xy, pen, start, end, *options) - def ellipse(self, xy: Coords, *options: Any) -> None: + def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws an ellipse inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` """ - self.render("ellipse", xy, *options) + self.render("ellipse", xy, pen, *options) - def line(self, xy: Coords, *options: Any) -> None: + def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a line between the coordinates in the ``xy`` list. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` """ - self.render("line", xy, *options) + self.render("line", xy, pen, *options) - def pieslice(self, xy: Coords, start, end, *options: Any) -> None: + def pieslice(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` """ - self.render("pieslice", xy, start, end, *options) + self.render("pieslice", xy, pen, start, end, *options) - def polygon(self, xy: Coords, *options: Any) -> None: + def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a polygon. @@ -165,15 +165,15 @@ class Draw: .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` """ - self.render("polygon", xy, *options) + self.render("polygon", xy, pen, *options) - def rectangle(self, xy: Coords, *options: Any) -> None: + def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a rectangle. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` """ - self.render("rectangle", xy, *options) + self.render("rectangle", xy, pen, *options) def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: """ From b84e2a9935a49197012002122ad84a856940133a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:55:17 +1000 Subject: [PATCH 069/136] Do not pass outline to arc --- src/PIL/ImageDraw2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 6e848663b..97898ffd9 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -101,7 +101,9 @@ class Draw: path.transform(self.transform) xy = path # render the item - if op == "line": + if op == "arc": + self.draw.arc(xy, fill=outline) + elif op == "line": self.draw.line(xy, fill=outline, width=width) else: getattr(self.draw, op)(xy, fill=fill, outline=outline) From 955854728acf3456866bbfa152920b5d81eb449a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jul 2024 18:44:13 +1000 Subject: [PATCH 070/136] Pass start and end to arc, chord and pieslice --- Tests/test_imagedraw2.py | 46 +++++++++++++++++++++++++++++++++++ src/PIL/ImageDraw2.py | 52 ++++++++++++++++++++++++++++++---------- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index c80aa739c..e0d368228 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -65,6 +65,36 @@ def test_mode() -> None: ImageDraw2.Draw("L") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("white", width=1) + + # Act + draw.arc(bbox, pen, start, end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow") + brush = ImageDraw2.Brush("red") + + # Act + draw.chord(bbox, pen, 0, 180, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1) + + @pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox: Coords) -> None: # Arrange @@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue") + brush = ImageDraw2.Brush("white") + + # Act + draw.pieslice(bbox, pen, start, end, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + @pytest.mark.parametrize("points", POINTS) def test_polygon(points: Coords) -> None: # Arrange diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 97898ffd9..3d68658ed 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -80,7 +80,12 @@ class Draw: return self.image def render( - self, op: str, xy: Coords, pen: Pen | Brush | None, brush: Brush | Pen | None = None + self, + op: str, + xy: Coords, + pen: Pen | Brush | None, + brush: Brush | Pen | None = None, + **kwargs: Any, ) -> None: # handle color arguments outline = fill = None @@ -101,35 +106,51 @@ class Draw: path.transform(self.transform) xy = path # render the item - if op == "arc": - self.draw.arc(xy, fill=outline) - elif op == "line": - self.draw.line(xy, fill=outline, width=width) + if op in ("arc", "line"): + kwargs.setdefault("fill", outline) else: - getattr(self.draw, op)(xy, fill=fill, outline=outline) + kwargs.setdefault("fill", fill) + kwargs.setdefault("outline", outline) + if op == "line": + kwargs.setdefault("width", width) + getattr(self.draw, op)(xy, **kwargs) def settransform(self, offset: tuple[float, float]) -> None: """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def arc( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` """ - self.render("arc", xy, pen, start, end, *options) + self.render("arc", xy, pen, *options, start=start, end=end) - def chord(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def chord( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` """ - self.render("chord", xy, pen, start, end, *options) + self.render("chord", xy, pen, *options, start=start, end=end) def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ @@ -147,14 +168,21 @@ class Draw: """ self.render("line", xy, pen, *options) - def pieslice(self, xy: Coords, pen: Pen | Brush | None, start, end, *options: Any) -> None: + def pieslice( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ Same as arc, but also draws straight lines between the end points and the center of the bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` """ - self.render("pieslice", xy, pen, start, end, *options) + self.render("pieslice", xy, pen, *options, start=start, end=end) def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ From accfaf1c092517f7f3918c33b8f9fc38f9ae6a62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jul 2024 20:20:09 +1000 Subject: [PATCH 071/136] Added type hints --- Tests/test_file_jpeg.py | 2 +- Tests/test_image.py | 2 +- docs/reference/plugins.rst | 8 +++++++ src/PIL/EpsImagePlugin.py | 20 ++++++++++++---- src/PIL/Image.py | 48 +++++++++++++++++++++----------------- src/PIL/ImageFile.py | 19 +++++++++++---- src/PIL/JpegImagePlugin.py | 25 ++++++++++++-------- src/PIL/PngImagePlugin.py | 16 +++++++++---- src/PIL/TiffImagePlugin.py | 14 +++++++---- 9 files changed, 101 insertions(+), 53 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 9660e2828..b4b3f71cb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -154,7 +154,7 @@ class TestFileJpeg: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: return tuple(v[0] for v in im.layer) im = hopper() diff --git a/Tests/test_image.py b/Tests/test_image.py index 4fdc41791..d8372789b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -99,7 +99,7 @@ class TestImage: im = Image.new("L", (100, 100)) p = Pretty() - im._repr_pretty_(p, None) + im._repr_pretty_(p, False) assert p.pretty_output == "" def test_open_formats(self) -> None: diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 18cd99cf3..454b94d8c 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -185,6 +185,14 @@ Plugin reference :undoc-members: :show-inheritance: +:mod:`~PIL.MpoImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.MpoImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.MspImagePlugin` Module --------------------------------- diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 7a73d1f69..35915e652 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -65,16 +65,24 @@ def has_ghostscript() -> bool: return gs_binary is not False -def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: +def Ghostscript( + tile: list[ImageFile._Tile], + size: tuple[int, int], + fp: IO[bytes], + scale: int = 1, + transparency: bool = False, +) -> Image.Image: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): msg = "Unable to locate Ghostscript on paths" raise OSError(msg) + assert isinstance(gs_binary, str) # Unpack decoder tile - decoder, tile, offset, data = tile[0] - length, bbox = data + args = tile[0].args + assert isinstance(args, tuple) + length, bbox = args # Hack to support hi-res rendering scale = int(scale) or 1 @@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile): # put floating point values there anyway. box = [int(float(i)) for i in v.split()] self._size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] + self.tile = [ + ImageFile._Tile( + "eps", (0, 0) + self.size, offset, (length, box) + ) + ] except Exception: pass return True diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ae8634539..ebf4f46c4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -220,7 +220,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): if TYPE_CHECKING: from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette + from . import ImageFile, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -676,7 +676,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle) -> None: + def _repr_pretty_(self, p, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), @@ -1551,6 +1551,7 @@ class Image: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(513): + assert exif._info is not None ifds.append((ifd1, exif._info.next)) offset = None @@ -1560,12 +1561,13 @@ class Image: offset = current_offset fp = self.fp - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) + if ifd is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) with open(fp) as im: from . import TiffImagePlugin @@ -3869,14 +3871,14 @@ class Exif(_ExifBase): bigtiff = False _loaded = False - def __init__(self): - self._data = {} - self._hidden_data = {} - self._ifds = {} - self._info = None - self._loaded_exif = None + def __init__(self) -> None: + self._data: dict[int, Any] = {} + self._hidden_data: dict[int, Any] = {} + self._ifds: dict[int, dict[int, Any]] = {} + self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None + self._loaded_exif: bytes | None = None - def _fixup(self, value): + def _fixup(self, value: Any) -> Any: try: if len(value) == 1 and isinstance(value, tuple): return value[0] @@ -3884,24 +3886,26 @@ class Exif(_ExifBase): pass return value - def _fixup_dict(self, src_dict): + def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: # Helper function # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset: int, group: int | None = None): + def _get_ifd_dict( + self, offset: int, group: int | None = None + ) -> dict[int, Any] | None: try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. self.fp.seek(offset) except (KeyError, TypeError): - pass + return None else: from . import TiffImagePlugin info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info.load(self.fp) - return self._fixup_dict(info) + return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: version = b"\x2B" if self.bigtiff else b"\x2A" @@ -3966,7 +3970,7 @@ class Exif(_ExifBase): self.fp.seek(offset) self._info.load(self.fp) - def _get_merged_dict(self): + def _get_merged_dict(self) -> dict[int, Any]: merged_dict = dict(self) # get EXIF extension @@ -4124,7 +4128,7 @@ class Exif(_ExifBase): keys.update(self._info) return len(keys) - def __getitem__(self, tag: int): + def __getitem__(self, tag: int) -> Any: if self._info is not None and tag not in self._data and tag in self._info: self._data[tag] = self._fixup(self._info[tag]) del self._info[tag] @@ -4133,7 +4137,7 @@ class Exif(_ExifBase): def __contains__(self, tag: object) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e4a7dba44..2a8846c1a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError: raise _get_oserror(error, encoder=False) -def _tilesort(t) -> int: +def _tilesort(t: _Tile) -> int: # sort on offset return t[2] @@ -161,7 +161,7 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: self.tile = [] super().__setstate__(state) @@ -525,7 +525,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -554,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None: def _encode_tile( - im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None + im: Image.Image, + fp: IO[bytes], + tile: list[_Tile], + bufsize: int, + fh, + exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: @@ -664,7 +669,11 @@ class PyCodec: """ self.fd = fd - def setimage(self, im, extents=None): + def setimage( + self, + im: Image.core.ImagingCore, + extents: tuple[int, int, int, int] | None = None, + ) -> None: """ Called from ImageFile to set the core output image for the codec diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d83d60b7b..3a83f9ca6 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, Any +from typing import IO, TYPE_CHECKING, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -51,6 +51,9 @@ from ._binary import o8 from ._binary import o16be as o16 from .JpegPresets import presets +if TYPE_CHECKING: + from .MpoImagePlugin import MpoImageFile + # # Parser @@ -329,7 +332,7 @@ class JpegImageFile(ImageFile.ImageFile): format = "JPEG" format_description = "JPEG (ISO 10918)" - def _open(self): + def _open(self) -> None: s = self.fp.read(3) if not _accept(s): @@ -342,13 +345,13 @@ class JpegImageFile(ImageFile.ImageFile): self._exif_offset = 0 # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] + self.layer: list[tuple[int, int, int, int]] = [] + self.huffman_dc: dict[Any, Any] = {} + self.huffman_ac: dict[Any, Any] = {} + self.quantization: dict[int, list[int]] = {} + self.app: dict[str, bytes] = {} # compatibility + self.applist: list[tuple[str, bytes]] = [] + self.icclist: list[bytes] = [] while True: i = s[0] @@ -831,7 +834,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ## # Factory for making JPEG and MPO instances -def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): +def jpeg_factory( + fp: IO[bytes] | None = None, filename: str | bytes | None = None +) -> JpegImageFile | MpoImageFile: im = JpegImageFile(fp, filename) try: mpheader = im._getmp() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 52cfcd13a..910fa9755 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn +from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -1223,7 +1223,11 @@ def _write_multiple_frames( if default_image: if im.mode != mode: im = im.convert(mode) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + ImageFile._save( + im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + im.size, 0, rawmode)], + ) seq_num = 0 for frame, frame_data in enumerate(im_frames): @@ -1258,14 +1262,14 @@ def _write_multiple_frames( # first frame must be in IDAT chunks for backwards compatibility ImageFile._save( im_frame, - _idat(fp, chunk), + cast(IO[bytes], _idat(fp, chunk)), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, - fdat_chunks, + cast(IO[bytes], fdat_chunks), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num @@ -1465,7 +1469,9 @@ def _save( ) if single_im: ImageFile._save( - single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] + single_im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index dd013a0cf..d9d1bab5a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -288,8 +288,10 @@ def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES -def _limit_rational(val, max_val): - inv = abs(val) > 1 +def _limit_rational( + val: float | Fraction | IFDRational, max_val: int +) -> tuple[float, float]: + inv = abs(float(val)) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d @@ -792,7 +794,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value + b"\0" @_register_loader(5, 8) - def load_rational(self, data, legacy_api: bool = True): + def load_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}L", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -801,7 +805,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) - def write_rational(self, *values) -> bytes: + def write_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @@ -828,7 +832,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) - def write_signed_rational(self, *values) -> bytes: + def write_signed_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values From e6fd8359d3184e09b8194c8ae8ec87c5a8c27c7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Aug 2024 12:27:45 +1000 Subject: [PATCH 072/136] Deprecate huffman_ac and huffman_dc --- Tests/test_file_jpeg.py | 7 +++++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/JpegImagePlugin.py | 11 +++++++++-- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index b4b3f71cb..df589e24a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1045,6 +1045,13 @@ class TestFileJpeg: assert im._repr_jpeg_() is None + def test_deprecation(self) -> None: + with Image.open(TEST_FILE) as im: + with pytest.warns(DeprecationWarning): + assert im.huffman_ac == {} + with pytest.warns(DeprecationWarning): + assert im.huffman_dc == {} + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2f5800e07..a03f274a9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -118,6 +118,14 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have +been deprecated. + Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb6179de8..579821ba9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -50,6 +50,14 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have +been deprecated. + API Changes =========== diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3a83f9ca6..fc897e2b9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -49,6 +49,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 +from ._deprecate import deprecate from .JpegPresets import presets if TYPE_CHECKING: @@ -346,8 +347,8 @@ class JpegImageFile(ImageFile.ImageFile): # JPEG specifics (internal) self.layer: list[tuple[int, int, int, int]] = [] - self.huffman_dc: dict[Any, Any] = {} - self.huffman_ac: dict[Any, Any] = {} + self._huffman_dc: dict[Any, Any] = {} + self._huffman_ac: dict[Any, Any] = {} self.quantization: dict[int, list[int]] = {} self.app: dict[str, bytes] = {} # compatibility self.applist: list[tuple[str, bytes]] = [] @@ -386,6 +387,12 @@ class JpegImageFile(ImageFile.ImageFile): self._read_dpi_from_exif() + def __getattr__(self, name: str) -> Any: + if name in ("huffman_ac", "huffman_dc"): + deprecate(name, 12) + return getattr(self, "_" + name) + raise AttributeError(name) + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data From 488e1982bd8d1152c202652260ce8f239178c8f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Aug 2024 16:29:46 +1000 Subject: [PATCH 073/136] Added removal version and date --- docs/deprecations.rst | 4 ++-- docs/releasenotes/11.0.0.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a03f274a9..058468cfe 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -123,8 +123,8 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc .. deprecated:: 11.0.0 -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have -been deprecated. +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 579821ba9..0175ae52d 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -55,8 +55,8 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc .. deprecated:: 11.0.0 -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused, and have -been deprecated. +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). API Changes =========== From 3e93b806cc68aedb887fa9071cfe35a2144cd960 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 02:29:19 +0000 Subject: [PATCH 074/136] Update dependency mypy to v1.11.1 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 776bb0dbb..ebb80e03b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.11.0 +mypy==1.11.1 From 69076fa3d9c04e00ab20cc7ba13ab046547b5138 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 06:21:13 +0000 Subject: [PATCH 075/136] Update dependency cibuildwheel to v2.20.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index a2bf2a7b0..b8e6c3947 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.19.2 +cibuildwheel==2.20.0 From 554f8bb23a9a35fbfd638b7677f1af12043ce9d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:37:42 +0000 Subject: [PATCH 076/136] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.6) - [github.com/python-jsonschema/check-jsonschema: 0.28.6 → 0.29.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.6...0.29.1) - [github.com/tox-dev/pyproject-fmt: 2.1.3 → 2.2.1](https://github.com/tox-dev/pyproject-fmt/compare/2.1.3...2.2.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 659409de4..74764752e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.5.6 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.6 + rev: 0.29.1 hooks: - id: check-github-workflows - id: check-readthedocs @@ -62,7 +62,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.1.3 + rev: 2.2.1 hooks: - id: pyproject-fmt From 1fd4d1478226e0b745b42ed33a9ade993cd393df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 06:31:47 +1000 Subject: [PATCH 077/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bd8d3af03..ae995e0a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Remove all WITH_* flags from _imaging.c and other flags #8211 + [homm] + +- Improve ImageDraw2 shape methods #8265 + [radarhere] + +- Lock around usages of imaging memory arenas #8238 + [lysnikolaou] + +- Deprecate JpegImageFile huffman_ac and huffman_dc #8274 + [radarhere] + - Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 [radarhere] From 5e8dbbbf9e22b7615157172fa11bc7f7bf86f4c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 13:32:46 +1000 Subject: [PATCH 078/136] Removed unused variable --- src/libImaging/Quant.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 595ee42a0..55b5e7a55 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -308,11 +308,10 @@ mergesort_pixels(PixelList *head, int i) { #ifdef DEBUG static int test_sorted(PixelList *pl[3]) { - int i, n, l; + int i, l; PixelList *t; for (i = 0; i < 3; i++) { - n = 0; l = 256; for (t = pl[i]; t; t = t->next[i]) { if (l < t->p.a.v[i]) From 9d6d16d5cc7bcf6070f5ad002de8642cbb2a17ab Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 6 Aug 2024 15:19:40 +0300 Subject: [PATCH 079/136] Parametrize some color_lut tests for DRYness --- Tests/test_color_lut.py | 133 +++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 0d9c0b419..152a7a1e6 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -105,91 +105,68 @@ class TestColorLut3DCoreAPI: with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self) -> None: + @pytest.mark.parametrize( + ("lut_mode", "table_size"), + [ + ("RGB", (3, 3)), + ("CMYK", (4, 3)), + ("RGB", (3, (2, 3, 3))), + ("RGB", (3, (65, 3, 3))), + ("RGB", (3, (3, 65, 3))), + ("RGB", (3, (3, 3, 65))), + ], + ) + def test_correct_args( + self, lut_mode: str, table_size: tuple[int, int | tuple[int, int, int]] + ) -> None: im = Image.new("RGB", (10, 10), 0) - + assert im.im is not None im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im.im.color_lut_3d( - "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im.im.color_lut_3d( - "RGB", + lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (2, 3, 3)), + *self.generate_identity_table(*table_size), ) + @pytest.mark.parametrize( + ("image_mode", "lut_mode", "table_size"), + [ + ("L", "RGB", (3, 3)), + ("RGB", "L", (3, 3)), + ("L", "L", (3, 3)), + ("RGB", "RGBA", (3, 3)), + ("RGB", "RGB", (4, 3)), + ], + ) + def test_wrong_mode( + self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + ) -> None: + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new(image_mode, (10, 10), 0) + assert im.im is not None + im.im.color_lut_3d( + lut_mode, + Image.Resampling.BILINEAR, + *self.generate_identity_table(*table_size), + ) + + @pytest.mark.parametrize( + ("image_mode", "lut_mode", "table_size"), + [ + ("RGBA", "RGB", (3, 3)), + ("RGBA", "RGBA", (4, 3)), + ("RGB", "HSV", (3, 3)), + ("RGB", "RGBA", (4, 3)), + ], + ) + def test_correct_mode( + self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + ) -> None: + im = Image.new(image_mode, (10, 10), 0) + assert im.im is not None im.im.color_lut_3d( - "RGB", + lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (65, 3, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 65, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 3, 65)), - ) - - def test_wrong_mode(self) -> None: - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - def test_correct_mode(self) -> None: - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + *self.generate_identity_table(*table_size), ) def test_identities(self) -> None: From 70298d3be948e4245e2f24481b33d8d877c45e1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 06:38:46 +1000 Subject: [PATCH 080/136] Fix undefined variable --- src/libImaging/Quant.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 55b5e7a55..a489a882d 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -314,8 +314,9 @@ test_sorted(PixelList *pl[3]) { for (i = 0; i < 3; i++) { l = 256; for (t = pl[i]; t; t = t->next[i]) { - if (l < t->p.a.v[i]) + if (l < t->p.a.v[i]) { return 0; + } l = t->p.a.v[i]; } } @@ -1009,7 +1010,8 @@ compute_palette_from_median_cut( uint32_t nPixels, HashTable *medianBoxHash, Pixel **palette, - uint32_t nPaletteEntries + uint32_t nPaletteEntries, + BoxNode *root ) { uint32_t i; uint32_t paletteEntry; @@ -1382,7 +1384,9 @@ quantize( fflush(stdout); timer = clock(); #endif - if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + if (!compute_palette_from_median_cut( + pixelData, nPixels, h, &p, nPaletteEntries, root + )) { goto error_3; } #ifdef DEBUG From 59c69f8d71d7f3e43cb09bd6bd2271c6e332c7ba Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 5 Aug 2024 19:12:12 +0300 Subject: [PATCH 081/136] Move auxiliary mypy requirements to a requirements file For easier installation outside tox --- .ci/requirements-mypy.txt | 9 +++++++++ tox.ini | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index ebb80e03b..23792281b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1,10 @@ mypy==1.11.1 +IceSpringPySideStubs-PyQt6 +IceSpringPySideStubs-PySide6 +ipython +numpy +packaging +pytest +types-defusedxml +types-olefile +types-setuptools diff --git a/tox.ini b/tox.ini index c1bc3b17d..4b4059455 100644 --- a/tox.ini +++ b/tox.ini @@ -33,15 +33,6 @@ commands = skip_install = true deps = -r .ci/requirements-mypy.txt - IceSpringPySideStubs-PyQt6 - IceSpringPySideStubs-PySide6 - ipython - numpy - packaging - pytest - types-defusedxml - types-olefile - types-setuptools extras = typing commands = From 7581b48706cb3ce8af112305bae22af705134608 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:44:43 +1000 Subject: [PATCH 082/136] Fixed sign comparison warning --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index da03e3ba9..f8143e0cc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1243,7 +1243,7 @@ font_getvarnames(FontObject *self) { return PyErr_NoMemory(); } - for (int i = 0; i < num_namedstyles; i++) { + for (unsigned int i = 0; i < num_namedstyles; i++) { list_names_filled[i] = 0; } From 4fddc625f177d8b7cd2120fa2c7880f664e25e67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:26:24 +1000 Subject: [PATCH 083/136] Corrected lut mode --- Tests/test_color_lut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 152a7a1e6..1543faf01 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -152,7 +152,7 @@ class TestColorLut3DCoreAPI: @pytest.mark.parametrize( ("image_mode", "lut_mode", "table_size"), [ - ("RGBA", "RGB", (3, 3)), + ("RGBA", "RGBA", (3, 3)), ("RGBA", "RGBA", (4, 3)), ("RGB", "HSV", (3, 3)), ("RGB", "RGBA", (4, 3)), From 5c4aeaa3296c748facfafc265e29fbb1c7c8e518 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:24:50 +1000 Subject: [PATCH 084/136] Concatenate parameters into single string --- Tests/test_color_lut.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 1543faf01..346ea749a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -106,7 +106,7 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) @pytest.mark.parametrize( - ("lut_mode", "table_size"), + "lut_mode, table_size", [ ("RGB", (3, 3)), ("CMYK", (4, 3)), @@ -128,7 +128,7 @@ class TestColorLut3DCoreAPI: ) @pytest.mark.parametrize( - ("image_mode", "lut_mode", "table_size"), + "image_mode, lut_mode, table_size", [ ("L", "RGB", (3, 3)), ("RGB", "L", (3, 3)), @@ -150,7 +150,7 @@ class TestColorLut3DCoreAPI: ) @pytest.mark.parametrize( - ("image_mode", "lut_mode", "table_size"), + "image_mode, lut_mode, table_size", [ ("RGBA", "RGBA", (3, 3)), ("RGBA", "RGBA", (4, 3)), From a06529a3a89d3e57e584b85c25fa24cf075f4dcd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 18:28:51 +1000 Subject: [PATCH 085/136] Added channels parameter --- Tests/test_color_lut.py | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 346ea749a..084cb19e1 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -106,39 +106,39 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) @pytest.mark.parametrize( - "lut_mode, table_size", + "lut_mode, table_channels, table_size", [ - ("RGB", (3, 3)), - ("CMYK", (4, 3)), - ("RGB", (3, (2, 3, 3))), - ("RGB", (3, (65, 3, 3))), - ("RGB", (3, (3, 65, 3))), - ("RGB", (3, (3, 3, 65))), + ("RGB", 3, 3), + ("CMYK", 4, 3), + ("RGB", 3, (2, 3, 3)), + ("RGB", 3, (65, 3, 3)), + ("RGB", 3, (3, 65, 3)), + ("RGB", 3, (2, 3, 65)), ], ) def test_correct_args( - self, lut_mode: str, table_size: tuple[int, int | tuple[int, int, int]] + self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int] ) -> None: im = Image.new("RGB", (10, 10), 0) assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) @pytest.mark.parametrize( - "image_mode, lut_mode, table_size", + "image_mode, lut_mode, table_channels, table_size", [ - ("L", "RGB", (3, 3)), - ("RGB", "L", (3, 3)), - ("L", "L", (3, 3)), - ("RGB", "RGBA", (3, 3)), - ("RGB", "RGB", (4, 3)), + ("L", "RGB", 3, 3), + ("RGB", "L", 3, 3), + ("L", "L", 3, 3), + ("RGB", "RGBA", 3, 3), + ("RGB", "RGB", 4, 3), ], ) def test_wrong_mode( - self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int ) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new(image_mode, (10, 10), 0) @@ -146,27 +146,27 @@ class TestColorLut3DCoreAPI: im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) @pytest.mark.parametrize( - "image_mode, lut_mode, table_size", + "image_mode, lut_mode, table_channels, table_size", [ - ("RGBA", "RGBA", (3, 3)), - ("RGBA", "RGBA", (4, 3)), - ("RGB", "HSV", (3, 3)), - ("RGB", "RGBA", (4, 3)), + ("RGBA", "RGBA", 3, 3), + ("RGBA", "RGBA", 4, 3), + ("RGB", "HSV", 3, 3), + ("RGB", "RGBA", 4, 3), ], ) def test_correct_mode( - self, image_mode: str, lut_mode: str, table_size: tuple[int, int] + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int ) -> None: im = Image.new(image_mode, (10, 10), 0) assert im.im is not None im.im.color_lut_3d( lut_mode, Image.Resampling.BILINEAR, - *self.generate_identity_table(*table_size), + *self.generate_identity_table(table_channels, table_size), ) def test_identities(self) -> None: From 8ca53b312d7fbac101a688c6864b4eeed9bae168 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 7 Aug 2024 12:17:49 +0300 Subject: [PATCH 086/136] Enforce CSV parametrize names style --- Tests/test_file_dds.py | 6 +++--- Tests/test_file_eps.py | 4 +--- Tests/test_image_access.py | 2 +- Tests/test_pickle.py | 2 +- pyproject.toml | 3 +++ 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index ebc0e89a1..9a826ebe8 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None: @pytest.mark.parametrize( - ("image_path", "expected_path"), + "image_path, expected_path", ( # hexeditted to be typeless (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), @@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: @pytest.mark.parametrize( - ("mode", "size", "test_file"), + "mode, size, test_file", [ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), @@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: @pytest.mark.parametrize( - ("mode", "test_file"), + "mode, test_file", [ ("L", "Tests/images/linear_gradient.png"), ("LA", "Tests/images/uncompressed_la.png"), diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b54238132..d54deb515 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = ( @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize( - ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) -) +@pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252)))) @pytest.mark.parametrize("scale", (1, 2)) def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 854c79dae..bb30b462d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -230,7 +230,7 @@ class TestImagePutPixelError: im.putpixel((0, 0), v) # type: ignore[arg-type] @pytest.mark.parametrize( - ("mode", "band_numbers", "match"), + "mode, band_numbers, match", ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index ed415953f..be143e9c6 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non @pytest.mark.parametrize( - ("test_file", "test_mode"), + "test_file, test_mode", [ ("Tests/images/hopper.jpg", None), ("Tests/images/hopper.jpg", "L"), diff --git a/pyproject.toml b/pyproject.toml index 0940664f3..d81f79c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks + "PT006", # pytest-parametrize-names-wrong-type "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) "UP", # pyupgrade @@ -129,6 +130,8 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ "I002", ] +lint.flake8-pytest-style.parametrize-names-type = "csv" + lint.isort.known-first-party = [ "PIL", ] From 1c998d7f7c5378ae2ed3a029ef7c4724633050e1 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 8 Aug 2024 14:10:18 +0300 Subject: [PATCH 087/136] Update pyproject.toml Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d81f79c5f..8bb21019c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,6 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ "I002", ] lint.flake8-pytest-style.parametrize-names-type = "csv" - lint.isort.known-first-party = [ "PIL", ] From 0d79a38e77ddcf5e386d2153e791f12e5d67a2f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 11 Aug 2024 15:14:29 +0400 Subject: [PATCH 088/136] Add missing TIFF CMYK;16B reader --- src/PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d9d1bab5a..deae199d5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -259,6 +259,7 @@ OPEN_INFO = { (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), (II, 6, (1,), 1, (8,), ()): ("L", "L"), (MM, 6, (1,), 1, (8,), ()): ("L", "L"), # JPEG compressed images handled by LibTiff and auto-converted to RGBX From c1e8375af834b190386cd2cfa30703c7975675a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 21:51:14 +0400 Subject: [PATCH 089/136] Require webpmux and webpdemux --- Tests/check_wheel.py | 1 - Tests/test_features.py | 5 --- Tests/test_file_webp_metadata.py | 5 +-- docs/handbook/image-file-formats.rst | 9 ++--- docs/installation/building-from-source.rst | 8 ++-- docs/reference/features.rst | 1 - setup.py | 46 ++++++++-------------- src/PIL/features.py | 2 - src/_webp.c | 33 ---------------- winbuild/build_prepare.py | 2 +- 10 files changed, 26 insertions(+), 86 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..2e4498db6 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "webp_mux", "transp_webp", "raqm", "fribidi", diff --git a/Tests/test_features.py b/Tests/test_features.py index b7eefa09a..33509a346 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -57,11 +57,6 @@ def test_webp_transparency() -> None: assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY -@skip_unless_feature("webp") -def test_webp_mux() -> None: - assert features.check("webp_mux") == _webp.HAVE_WEBPMUX - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c3df4ad7b..52a84a83e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,10 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_mux"), -] +pytestmark = [skip_unless_feature("webp")] ElementTree: ModuleType | None try: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ec972149..8561cc4aa 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1252,16 +1252,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Requires libwebp 0.5.0 or later. **icc_profile** - The ICC Profile to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The ICC Profile to include in the saved file. **exif** - The exif data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The exif data to include in the saved file. **xmp** - The XMP data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The XMP data to include in the saved file. Saving sequences ~~~~~~~~~~~~~~~~ diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7f7dfa6ff..f47edbc5b 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -275,18 +275,18 @@ Build Options * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, - ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. 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. + an exception if the libraries are not found. Tcl and Tk must be used + together. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c66193061..55c0b1200 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -55,7 +55,6 @@ Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``transp_webp``: Support for transparency in WebP images. -* ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/setup.py b/setup.py index b26852b0b..8d0dfb6fa 100644 --- a/setup.py +++ b/setup.py @@ -295,7 +295,6 @@ class pil_build_ext(build_ext): "raqm", "lcms", "webp", - "webpmux", "jpeg2000", "imagequant", "xcb", @@ -794,29 +793,22 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") - if _find_include_file(self, "webp/encode.h") and _find_include_file( - self, "webp/decode.h" + if all( + _find_include_file(self, src) + for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] ): # In Google's precompiled zip it is call "libwebp": - if _find_library_file(self, "webp"): + if all( + _find_library_file(self, lib) + for lib in ["webp", "webpmux", "webpdemux"] + ): feature.webp = "webp" - elif _find_library_file(self, "libwebp"): + elif all( + _find_library_file(self, lib) + for lib in ["libwebp", "libwebpmux", "libwebpdemux"] + ): feature.webp = "libwebp" - if feature.want("webpmux"): - _dbg("Looking for webpmux") - if _find_include_file(self, "webp/mux.h") and _find_include_file( - self, "webp/demux.h" - ): - if _find_library_file(self, "webpmux") and _find_library_file( - self, "webpdemux" - ): - feature.webpmux = "webpmux" - if _find_library_file(self, "libwebpmux") and _find_library_file( - self, "libwebpdemux" - ): - feature.webpmux = "libwebpmux" - if feature.want("xcb"): _dbg("Looking for xcb") if _find_include_file(self, "xcb/xcb.h"): @@ -904,15 +896,12 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [feature.webp] - defs = [] - - if feature.webpmux: - defs.append(("HAVE_WEBPMUX", None)) - libs.append(feature.webpmux) - libs.append(feature.webpmux.replace("pmux", "pdemux")) - - self._update_extension("PIL._webp", libs, defs) + libs = [ + feature.webp, + feature.webp + "mux", + feature.webp + "demux", + ] + self._update_extension("PIL._webp", libs, []) else: self._remove_extension("PIL._webp") @@ -953,7 +942,6 @@ class pil_build_ext(build_ext): (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), - (feature.webpmux, "WEBPMUX"), (feature.xcb, "XCB (X protocol)"), ] diff --git a/src/PIL/features.py b/src/PIL/features.py index 13908c4eb..7f6cdb161 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), @@ -272,7 +271,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index e686ec820..ab229058b 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -4,8 +4,6 @@ #include #include #include - -#ifdef HAVE_WEBPMUX #include #include @@ -19,8 +17,6 @@ #define HAVE_WEBPANIM #endif -#endif - void ImagingSectionEnter(ImagingSectionCookie *cookie) { *cookie = (PyThreadState *)PyEval_SaveThread(); @@ -35,8 +31,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie) { /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPMUX - static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", @@ -89,8 +83,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } -#endif - /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ @@ -693,13 +685,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { output = writer.mem; ret_size = writer.size; -#ifndef HAVE_WEBPMUX - if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); - free(output); - return ret; - } -#else { /* I want to truncate the *_size items that get passed into WebP data. Pypy2.1.0 had some issues where the Py_ssize_t items had @@ -775,7 +760,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return ret; } } -#endif Py_RETURN_NONE; } @@ -809,9 +793,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { mode = "RGBA"; } -#ifndef HAVE_WEBPMUX - vp8_status_code = WebPDecode(webp, size, &config); -#else { int copy_data = 0; WebPData data = {webp, size}; @@ -849,7 +830,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { WebPDataClear(&image.bitstream); WebPMuxDelete(mux); } -#endif } if (vp8_status_code != VP8_STATUS_OK) { @@ -949,18 +929,6 @@ static PyMethodDef webpMethods[] = { {NULL, NULL} }; -void -addMuxFlagToModule(PyObject *m) { - PyObject *have_webpmux; -#ifdef HAVE_WEBPMUX - have_webpmux = Py_True; -#else - have_webpmux = Py_False; -#endif - Py_INCREF(have_webpmux); - PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); -} - void addAnimFlagToModule(PyObject *m) { PyObject *have_webpanim; @@ -991,7 +959,6 @@ setup_module(PyObject *m) { } #endif PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2..7129699eb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -201,7 +201,7 @@ DEPS = { }, "build": [ *cmds_cmake( - "webp webpdemux webpmux", + "webp webpmux webpdemux", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWEBP_LINK_STATIC:BOOL=OFF", ), From 9bed5b426437ad4aacd36288fcfb9422cac8c9c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:05:57 +0400 Subject: [PATCH 090/136] Remove _webp.WebPDecoderBuggyAlpha and _webp.HAVE_TRANSPARENCY --- Tests/check_wheel.py | 1 - Tests/test_features.py | 6 ------ Tests/test_file_webp.py | 1 - Tests/test_file_webp_alpha.py | 8 -------- docs/reference/features.rst | 1 - src/PIL/features.py | 2 -- src/_webp.c | 27 --------------------------- 7 files changed, 46 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 2e4498db6..f862e4fff 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 33509a346..577e7a5d5 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -51,12 +51,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_transparency() -> None: - assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() - assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cbc905de4..f132d0a64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -49,7 +49,6 @@ class TestFileWebp: def test_version(self) -> None: _webp.WebPDecoderVersion() - _webp.WebPDecoderBuggyAlpha() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c74452121..e80ef7d4f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -16,11 +16,6 @@ from .helper import ( _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module() -> None: - if _webp.WebPDecoderBuggyAlpha(): - pytest.skip("Buggy early version of WebP installed, not testing transparency") - - def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? @@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None: pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(): - return - with Image.open(temp_file) as image: image.load() diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 55c0b1200..d4fb340bd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``transp_webp``: Support for transparency in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/src/PIL/features.py b/src/PIL/features.py index 7f6cdb161..60590dc09 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -270,7 +269,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index ab229058b..a10eb111c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -896,20 +896,6 @@ WebPDecoderVersion_str(void) { return version; } -/* - * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. - * Files that are valid with 0.3 are reported as being invalid. - */ -int -WebPDecoderBuggyAlpha(void) { - return WebPGetDecoderVersion() == 0x0103; -} - -PyObject * -WebPDecoderBuggyAlpha_wrapper() { - return Py_BuildValue("i", WebPDecoderBuggyAlpha()); -} - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ @@ -922,10 +908,6 @@ static PyMethodDef webpMethods[] = { {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", - WebPDecoderBuggyAlpha_wrapper, - METH_NOARGS, - "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; @@ -941,14 +923,6 @@ addAnimFlagToModule(PyObject *m) { PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); } -void -addTransparencyFlagToModule(PyObject *m) { - PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); - if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { - Py_DECREF(have_transparency); - } -} - static int setup_module(PyObject *m) { #ifdef HAVE_WEBPANIM @@ -960,7 +934,6 @@ setup_module(PyObject *m) { #endif PyObject *d = PyModule_GetDict(m); addAnimFlagToModule(m); - addTransparencyFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From a3468996c0b7b6df2b685ff21c4f515f5105ff8c Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:47:06 +0400 Subject: [PATCH 091/136] Remove webp animations flags and conditions Removed: _webp.WebPDecode _webp.HAVE_WEBPANIM features.webp_anim --- Tests/check_wheel.py | 1 - Tests/test_features.py | 10 -- Tests/test_file_gif.py | 2 +- Tests/test_file_webp.py | 24 ++--- Tests/test_file_webp_alpha.py | 7 +- Tests/test_file_webp_animated.py | 5 +- Tests/test_file_webp_lossless.py | 3 - Tests/test_file_webp_metadata.py | 3 +- Tests/test_image.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 2 +- docs/handbook/image-file-formats.rst | 9 +- docs/reference/features.rst | 1 - setup.py | 7 +- src/PIL/WebPImagePlugin.py | 47 +++------ src/PIL/features.py | 2 - src/_webp.c | 150 +-------------------------- 17 files changed, 34 insertions(+), 241 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index f862e4fff..8fcb75dad 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,7 +27,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { - "webp_anim", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 577e7a5d5..40466082f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -10,11 +10,6 @@ from PIL import features from .helper import skip_unless_feature -try: - from PIL import _webp -except ImportError: - pass - def test_check() -> None: # Check the correctness of the convenience function @@ -51,11 +46,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_anim() -> None: - assert features.check("webp_anim") == _webp.HAVE_WEBPANIM - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 85b017d29..8cefdb628 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): with Image.open("Tests/images/hopper.webp") as im: assert im.info["background"] == (255, 255, 255, 255) im.save(out) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f132d0a64..6e9eea4ae 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -48,7 +48,6 @@ class TestFileWebp: self.rgb_mode = "RGB" def test_version(self) -> None: - _webp.WebPDecoderVersion() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) @@ -116,7 +115,6 @@ class TestFileWebp: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - @skip_unless_feature("webp_anim") def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) @@ -131,10 +129,9 @@ class TestFileWebp: def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) - if _webp.HAVE_WEBPANIM: - self._roundtrip( - tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} - ) + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ @@ -164,10 +161,8 @@ class TestFileWebp: """ Calling encoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() with pytest.raises(TypeError): _webp.WebPEncode() @@ -175,12 +170,8 @@ class TestFileWebp: """ Calling decoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimDecoder() with pytest.raises(TypeError): - _webp.WebPDecode() + _webp.WebPAnimDecoder() def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" @@ -199,7 +190,6 @@ class TestFileWebp: "background", (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) - @skip_unless_feature("webp_anim") def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: @@ -208,7 +198,6 @@ class TestFileWebp: with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) - @skip_unless_feature("webp_anim") def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: @@ -233,7 +222,6 @@ class TestFileWebp: difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 - @skip_unless_feature("webp_anim") def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index e80ef7d4f..53cdfff03 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -85,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None: image.load() image.getdata() - # Early versions of WebP are known to produce higher deviations: - # deal with it - if _webp.WebPDecoderVersion() <= 0x201: - assert_image_similar(image, pil_image, 3.0) - else: - assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index e0d7999e3..967a0aae8 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -15,10 +15,7 @@ from .helper import ( skip_unless_feature, ) -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_anim"), -] +pytestmark = skip_unless_feature("webp") def test_n_frames() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 32e29de56..dc98ad3f0 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,9 +13,6 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - if _webp.WebPDecoderVersion() < 0x0200: - pytest.skip("lossless not included") - temp_file = str(tmp_path / "temp.webp") hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 52a84a83e..4ef3d95f2 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,7 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [skip_unless_feature("webp")] +pytestmark = skip_unless_feature("webp") ElementTree: ModuleType | None try: @@ -133,7 +133,6 @@ def test_getxmp() -> None: ) -@skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" diff --git a/Tests/test_image.py b/Tests/test_image.py index d8372789b..719732d12 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -817,7 +817,6 @@ class TestImage: assert reloaded_exif[305] == "Pillow test" @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 44a6e6a42..fe7d44785 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -94,7 +94,6 @@ class TestImageFile: assert (48, 48) == p.image.size @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e33e6d4c8..2fb2a60b6 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -390,7 +390,7 @@ def test_colorize_3color_offset() -> None: def test_exif_transpose() -> None: exts = [".jpg"] - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8561cc4aa..49a17bb5c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo. WebP ^^^^ -Pillow reads and writes WebP files. The specifics of Pillow's capabilities with -this format are currently undocumented. +Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. .. _webp-saving: @@ -1263,12 +1262,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Saving sequences ~~~~~~~~~~~~~~~~ -.. note:: - - Support for animated WebP files will only be enabled if the system WebP - library is v0.5.0 or later. You can check webp animation support at - runtime by calling ``features.check("webp_anim")``. - When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` argument is present and true, then all frames will be saved, and the following diff --git a/docs/reference/features.rst b/docs/reference/features.rst index d4fb340bd..26c8ab8cd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/setup.py b/setup.py index 8d0dfb6fa..edfa9ee28 100644 --- a/setup.py +++ b/setup.py @@ -795,7 +795,12 @@ class pil_build_ext(build_ext): _dbg("Looking for webp") if all( _find_include_file(self, src) - for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] + for src in [ + "webp/encode.h", + "webp/decode.h", + "webp/mux.h", + "webp/demux.h", + ] ): # In Google's precompiled zip it is call "libwebp": if all( diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cec796340..fc8f4fc2e 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -45,22 +45,6 @@ class WebPImageFile(ImageFile.ImageFile): __logical_frame = 0 def _open(self) -> None: - if not _webp.HAVE_WEBPANIM: - # Legacy mode - data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( - self.fp.read() - ) - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - self._size = width, height - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self.n_frames = 1 - self.is_animated = False - return - # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. self._decoder = _webp.WebPAnimDecoder(self.fp.read()) @@ -145,21 +129,20 @@ class WebPImageFile(ImageFile.ImageFile): self._get_next() # Advance to the requested frame def load(self) -> Image.core.PixelAccess | None: - if _webp.HAVE_WEBPANIM: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame + # We need to load the image data for this frame + data, timestamp, duration = self._get_next() + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__logical_frame - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() @@ -167,9 +150,6 @@ class WebPImageFile(ImageFile.ImageFile): pass def tell(self) -> int: - if not _webp.HAVE_WEBPANIM: - return super().tell() - return self.__logical_frame @@ -357,7 +337,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: Image.register_open(WebPImageFile.format, WebPImageFile, _accept) if SUPPORTED: Image.register_save(WebPImageFile.format, _save) - if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_save_all(WebPImageFile.format, _save_all) Image.register_extension(WebPImageFile.format, ".webp") Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/src/PIL/features.py b/src/PIL/features.py index 60590dc09..bbf7c641b 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,6 @@ def get_supported_codecs() -> list[str]: features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -269,7 +268,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_webp.c b/src/_webp.c index a10eb111c..8302925b0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -13,8 +13,8 @@ * very early versions had some significant differences, so we require later * versions, before enabling animation support. */ -#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 -#define HAVE_WEBPANIM +#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag #endif void @@ -87,8 +87,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { /* WebP Animation Support */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPANIM - // Encoder type typedef struct { PyObject_HEAD WebPAnimEncoder *enc; @@ -568,8 +566,6 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; -#endif - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ @@ -644,10 +640,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.alpha_quality = alpha_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)) { @@ -763,124 +756,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { Py_RETURN_NONE; } -PyObject * -WebPDecode_wrapper(PyObject *self, PyObject *args) { - PyBytesObject *webp_string; - const uint8_t *webp; - Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, - *exif = NULL; - WebPDecoderConfig config; - VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char *mode = "RGB"; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - - if (!WebPInitDecoderConfig(&config)) { - Py_RETURN_NONE; - } - - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - - vp8_status_code = WebPGetFeatures(webp, size, &config.input); - if (vp8_status_code == VP8_STATUS_OK) { - // If we don't set it, we don't get alpha. - // Initialized to MODE_RGB - if (config.input.has_alpha) { - config.output.colorspace = MODE_RGBA; - mode = "RGBA"; - } - - { - int copy_data = 0; - WebPData data = {webp, size}; - WebPMuxFrameInfo image; - WebPData icc_profile_data = {0}; - WebPData exif_data = {0}; - - WebPMux *mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) { - goto end; - } - - if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { - WebPMuxDelete(mux); - goto end; - } - - webp = image.bitstream.bytes; - size = image.bitstream.size; - - vp8_status_code = WebPDecode(webp, size, &config); - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { - icc_profile = PyBytes_FromStringAndSize( - (const char *)icc_profile_data.bytes, icc_profile_data.size - ); - } - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { - exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size - ); - } - - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); - } - } - - if (vp8_status_code != VP8_STATUS_OK) { - goto end; - } - - if (config.output.colorspace < MODE_YUV) { - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size - ); - } else { - // Skipping YUV for now. Need Test Images. - // UNDONE -- unclear if we'll ever get here if we set mode_rgb* - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size - ); - } - - pymode = PyUnicode_FromString(mode); - ret = Py_BuildValue( - "SiiSSS", - bytes, - config.output.width, - config.output.height, - pymode, - NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif - ); - -end: - WebPFreeDecBuffer(&config.output); - - Py_XDECREF(bytes); - Py_XDECREF(pymode); - Py_XDECREF(icc_profile); - Py_XDECREF(exif); - - if (Py_None == ret) { - Py_RETURN_NONE; - } - - return ret; -} - -// Return the decoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject * -WebPDecoderVersion_wrapper() { - return Py_BuildValue("i", WebPGetDecoderVersion()); -} - // Version as string const char * WebPDecoderVersion_str(void) { @@ -901,39 +776,20 @@ WebPDecoderVersion_str(void) { /* -------------------------------------------------------------------- */ static PyMethodDef webpMethods[] = { -#ifdef HAVE_WEBPANIM {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, -#endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, - {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, {NULL, NULL} }; -void -addAnimFlagToModule(PyObject *m) { - PyObject *have_webpanim; -#ifdef HAVE_WEBPANIM - have_webpanim = Py_True; -#else - have_webpanim = Py_False; -#endif - Py_INCREF(have_webpanim); - PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); -} - static int setup_module(PyObject *m) { -#ifdef HAVE_WEBPANIM + PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } -#endif - PyObject *d = PyModule_GetDict(m); - addAnimFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From 56ca359c657ee2f4e81426f780c4b4e2e281e404 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:07:43 +0400 Subject: [PATCH 092/136] Bring back removed features, add deprecations and Release notes --- Tests/check_wheel.py | 3 +++ Tests/test_features.py | 12 ++++++++++++ docs/reference/features.rst | 3 +++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/features.py | 10 +++++++++- 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8fcb75dad..4b91984f5 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,6 +27,9 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 40466082f..cb8fd8688 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -46,6 +46,18 @@ def test_version() -> None: test(feature, features.version_feature) +def test_webp_transparency() -> None: + assert features.check("transp_webp") == features.check_module("webp") + + +def test_webp_mux() -> None: + assert features.check("webp_mux") == features.check_module("webp") + + +def test_webp_anim() -> None: + assert features.check("webp_anim") == features.check_module("webp") + + @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 26c8ab8cd..fcff96735 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -57,6 +57,9 @@ Support for the following features can be checked: * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. +* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. .. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.version_feature diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1d1afcde5..bb707a044 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -58,6 +58,14 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following features ``features.check("transp_webp")``, +``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now +always ``True`` if the WebP module is installed and should not be used. +These checks will be removed in Pillow 12.0.0 (2025-10-15). + API Changes =========== diff --git a/src/PIL/features.py b/src/PIL/features.py index bbf7c641b..f594a2cdc 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,6 +7,7 @@ import warnings from typing import IO import PIL +from PIL import _deprecate from . import Image @@ -119,6 +120,9 @@ def get_supported_codecs() -> list[str]: features = { + "webp_anim": ("PIL._webp", True, None), + "webp_mux": ("PIL._webp", True, None), + "transp_webp": ("PIL._webp", True, None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -144,7 +148,11 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - return getattr(imported_module, flag) + if isinstance(flag, str): + return getattr(imported_module, flag) + else: + _deprecate.deprecate(f'check_feature("{feature}")', 12) + return flag except ModuleNotFoundError: return None except ImportError as ex: From 6180abc75c3dfa950276b2586cd1a8aab1bede70 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:32:35 +0400 Subject: [PATCH 093/136] Remove WebP versions notes from docs --- docs/handbook/image-file-formats.rst | 1 - docs/installation/building-from-source.rst | 4 ---- 2 files changed, 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 49a17bb5c..861e09a43 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1248,7 +1248,6 @@ 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. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index f47edbc5b..71787311f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -55,10 +55,6 @@ Many of Pillow's features require external libraries: * **libwebp** provides the WebP format. - * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and above support - transparency. - * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, From 924df9e60b39c6ced91553895497fa43db2d232d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 06:46:46 +1000 Subject: [PATCH 094/136] Moved line after early return Improve compiler advice Update src/PIL/features.py --- src/PIL/features.py | 5 ++--- src/_webp.c | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index f594a2cdc..a5487042e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -148,11 +148,10 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - if isinstance(flag, str): - return getattr(imported_module, flag) - else: + if isinstance(flag, bool): _deprecate.deprecate(f'check_feature("{feature}")', 12) return flag + return getattr(imported_module, flag) except ModuleNotFoundError: return None except ImportError as ex: diff --git a/src/_webp.c b/src/_webp.c index 8302925b0..306a290bd 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -14,7 +14,7 @@ * versions, before enabling animation support. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 -#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag #endif void @@ -784,13 +784,13 @@ static PyMethodDef webpMethods[] = { static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } + PyObject *d = PyModule_GetDict(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); Py_XDECREF(v); From 93ce9ce0048639706eecd523fc54c7d6cfc15bef Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Tue, 13 Aug 2024 09:52:07 +0400 Subject: [PATCH 095/136] Update features type Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index a5487042e..9421dbd3c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,7 @@ def get_supported_codecs() -> list[str]: return [f for f in codecs if check_codec(f)] -features = { +features: dict[str, tuple[str, str | bool, str | None]] = { "webp_anim": ("PIL._webp", True, None), "webp_mux": ("PIL._webp", True, None), "transp_webp": ("PIL._webp", True, None), From 55469948282fe2dd763dcb8e66a767b5c63adb54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:03:14 +1000 Subject: [PATCH 096/136] Removed unnecessary variable --- Tests/test_file_webp_alpha.py | 2 +- Tests/test_file_webp_lossless.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 53cdfff03..c88fe3589 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -13,7 +13,7 @@ from .helper import ( hopper, ) -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") def test_read_rgba() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index dc98ad3f0..80429715e 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -8,7 +8,7 @@ from PIL import Image from .helper import assert_image_equal, hopper -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" From f3aec6dd383bb27676e7e02a7749a5bff1cc30c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:05:32 +1000 Subject: [PATCH 097/136] Simplified code --- setup.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index edfa9ee28..863d102cf 100644 --- a/setup.py +++ b/setup.py @@ -794,25 +794,17 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") if all( - _find_include_file(self, src) - for src in [ - "webp/encode.h", - "webp/decode.h", - "webp/mux.h", - "webp/demux.h", - ] + _find_include_file(self, "webp/" + include) + for include in ("encode.h", "decode.h", "mux.h", "demux.h") ): - # In Google's precompiled zip it is call "libwebp": - if all( - _find_library_file(self, lib) - for lib in ["webp", "webpmux", "webpdemux"] - ): - feature.webp = "webp" - elif all( - _find_library_file(self, lib) - for lib in ["libwebp", "libwebpmux", "libwebpdemux"] - ): - feature.webp = "libwebp" + # In Google's precompiled zip it is called "libwebp" + for prefix in ("", "lib"): + if all( + _find_library_file(self, prefix + library) + for library in ("webp", "webpmux", "webpdemux") + ): + feature.webp = prefix + "webp" + break if feature.want("xcb"): _dbg("Looking for xcb") @@ -901,12 +893,8 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [ - feature.webp, - feature.webp + "mux", - feature.webp + "demux", - ] - self._update_extension("PIL._webp", libs, []) + libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] + self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") From c7e6289b36d4eb1823b6bfcace27674826a58e43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:07:01 +1000 Subject: [PATCH 098/136] Use relative import --- src/PIL/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 9421dbd3c..e505adae0 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,9 +7,9 @@ import warnings from typing import IO import PIL -from PIL import _deprecate from . import Image +from ._deprecate import deprecate modules = { "pil": ("PIL._imaging", "PILLOW_VERSION"), @@ -149,7 +149,7 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) if isinstance(flag, bool): - _deprecate.deprecate(f'check_feature("{feature}")', 12) + deprecate(f'check_feature("{feature}")', 12) return flag return getattr(imported_module, flag) except ModuleNotFoundError: From ba82dff7bc6c489f299a63d2f2d3722dfe11852e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:10:54 +1000 Subject: [PATCH 099/136] Updated test name --- Tests/test_file_webp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6e9eea4ae..6ccd489bb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -166,7 +166,7 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self) -> None: + def test_WebPAnimDecoder_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ From 66319fcce764f4f09e53900b2896ec15457f691f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:17:36 +1000 Subject: [PATCH 100/136] Animation support is no longer conditionally enabled --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 306a290bd..d1943b3e0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -11,7 +11,7 @@ * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The * very early versions had some significant differences, so we require later - * versions, before enabling animation support. + * versions. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 #error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag From 45552b5b4f6ea470639e5c7ff0d9efb96ad7db68 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:24:34 +1000 Subject: [PATCH 101/136] Updated documentation --- docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/11.0.0.rst | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 058468cfe..a9498d5ed 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -126,6 +126,16 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). + Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb707a044..ac9237acf 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -61,10 +61,10 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15). Specific WebP Feature Checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The following features ``features.check("transp_webp")``, -``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now -always ``True`` if the WebP module is installed and should not be used. -These checks will be removed in Pillow 12.0.0 (2025-10-15). +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). API Changes =========== From 359d7592c7861729092434154d131310c7bf1d71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Aug 2024 18:41:39 +1000 Subject: [PATCH 102/136] Test deprecation warnings --- Tests/test_features.py | 21 ++++++++++++++++----- src/PIL/features.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index cb8fd8688..807782847 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,7 +18,11 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - assert features.check_feature(feature) == features.check(feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + assert features.check_feature(feature) == features.check(feature) + else: + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -43,19 +47,26 @@ def test_version() -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - test(feature, features.version_feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + test(feature, features.version_feature) + else: + test(feature, features.version_feature) def test_webp_transparency() -> None: - assert features.check("transp_webp") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("transp_webp") == features.check_module("webp") def test_webp_mux() -> None: - assert features.check("webp_mux") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_mux") == features.check_module("webp") def test_webp_anim() -> None: - assert features.check("webp_anim") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_anim") == features.check_module("webp") @skip_unless_feature("libjpeg_turbo") diff --git a/src/PIL/features.py b/src/PIL/features.py index e505adae0..24c5ee978 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -180,7 +180,17 @@ def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ - return [f for f in features if check_feature(f)] + supported_features = [] + for f, (module, flag, _) in features.items(): + if flag is True: + for feature, (feature_module, _) in modules.items(): + if feature_module == module: + if check_module(feature): + supported_features.append(f) + break + elif check_feature(f): + supported_features.append(f) + return supported_features def check(feature: str) -> bool | None: From b14142462e1d4f0205b7e77c647a02c760b99a05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 05:11:00 +1000 Subject: [PATCH 103/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae995e0a0..25dcb1887 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Remove WebP support without anim, mux/demux, and with buggy alpha #8213 + [homm, radarhere] + +- Add missing TIFF CMYK;16B reader #8298 + [homm] + - Remove all WITH_* flags from _imaging.c and other flags #8211 [homm] From 873770978197484b284a267d247d1b832afc2a24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 13:32:59 +1000 Subject: [PATCH 104/136] Added return type to ImageFile.load() --- src/PIL/Image.py | 8 +++----- src/PIL/ImageFile.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebf4f46c4..a6eefff56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -218,9 +218,10 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: + import mmap from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -612,7 +613,7 @@ class Image: logger.debug("Error closing: %s", msg) if getattr(self, "map", None): - self.map = None + self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -1336,9 +1337,6 @@ class Image: self.load() return self._new(self.im.expand(xmargin, ymargin)) - if TYPE_CHECKING: - from . import ImageFilter - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2a8846c1a..829082e94 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -174,7 +174,7 @@ class ImageFile(Image.Image): self.fp.close() self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" if self.tile is None: @@ -185,7 +185,7 @@ class ImageFile(Image.Image): if not self.tile: return pixel - self.map = None + self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") @@ -193,17 +193,17 @@ class ImageFile(Image.Image): readonly = 0 # look for read/seek overrides - try: + if hasattr(self, "load_read"): read = self.load_read # don't use mmap if there are custom read/seek functions use_mmap = False - except AttributeError: + else: read = self.fp.read - try: + if hasattr(self, "load_seek"): seek = self.load_seek use_mmap = False - except AttributeError: + else: seek = self.fp.seek if use_mmap: @@ -243,11 +243,8 @@ class ImageFile(Image.Image): # sort tiles in file order self.tile.sort(key=_tilesort) - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") # Remove consecutive duplicates that only differ by their offset self.tile = [ From 497080f63b62c9acbb427c7e554f222de70fe3a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 15:20:34 +1000 Subject: [PATCH 105/136] Added type hint to ImageFile._save tile parameter --- Tests/test_imagefile.py | 30 ++++++++++++++++++++++++------ src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 4 +++- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 8 ++++++-- src/PIL/IcoImagePlugin.py | 4 +++- src/PIL/ImImagePlugin.py | 4 +++- src/PIL/ImageFile.py | 4 ++-- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 4 +++- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PalmImagePlugin.py | 4 +++- src/PIL/PcxImagePlugin.py | 4 +++- src/PIL/PdfImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 8 ++++---- src/PIL/PpmImagePlugin.py | 4 +++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TgaImagePlugin.py | 8 ++++++-- src/PIL/XbmImagePlugin.py | 2 +- 19 files changed, 72 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fe7d44785..95e91db83 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -317,7 +317,13 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], ) assert MockPyEncoder.last @@ -333,7 +339,7 @@ class TestPyEncoder(CodecsTest): im.tile = [("MOCK", None, 32, None)] fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == 0 @@ -350,7 +356,9 @@ class TestPyEncoder(CodecsTest): MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], ) last: MockPyEncoder | None = MockPyEncoder.last assert last @@ -358,7 +366,9 @@ class TestPyEncoder(CodecsTest): with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], ) def test_oversize(self) -> None: @@ -371,14 +381,22 @@ class TestPyEncoder(CodecsTest): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" + ) + ], ) with pytest.raises(ValueError): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" + ) + ], ) def test_encode(self) -> None: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6d71049a9..569f2c9bf 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(" None: if bits != 32: and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], ) else: frame.save(image_io, "png") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52..b94165089 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: palette += im_palette[colors * i : colors * (i + 1)] palette += b"\x00" * (256 - colors) fp.write(palette) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) # diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 829082e94..73554fa53 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -93,7 +93,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str - extents: tuple[int, int, int, int] + extents: tuple[int, int, int, int] | None offset: int args: tuple[Any, ...] | str | None @@ -522,7 +522,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ef9107f00..02c2e48cf 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: plt, ) - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) # ------------------------------------------------------------ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..bd4539be4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -826,7 +826,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Ensure that our buffer is big enough. Same with the icc_profile block. bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b..40e5fa435 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f8..62bf5f542 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -213,7 +213,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..4fb04715b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) if im.mode == "P": # colour palette diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 7fc1108bb..e9c20ddc1 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -138,7 +138,7 @@ def _write_image( op = io.BytesIO() if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) elif decode_filter == "CCITTFaxDecode": im.save( op, diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 910fa9755..fc20b18a8 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1226,7 +1226,7 @@ def _write_multiple_frames( ImageFile._save( im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], ) seq_num = 0 @@ -1263,14 +1263,14 @@ def _write_multiple_frames( ImageFile._save( im_frame, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, cast(IO[bytes], fdat_chunks), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num return None @@ -1471,7 +1471,7 @@ def _save( ImageFile._save( single_im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + single_im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..7bdaa9fe7 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif head == b"Pf": fp.write(b"-1.0\n") row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a07101e54..7045ab566 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -278,7 +278,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] + ) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aece..a43aae1ec 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -238,11 +238,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if rle: ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], ) # write targa version 2 footer diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf..6c2e32804 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") From 8afb7ddb4ed265caf67213f4fd5416cd29ae24a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 08:08:43 +1000 Subject: [PATCH 106/136] Added type hints --- .ci/requirements-mypy.txt | 1 + docs/example/DdsImagePlugin.py | 29 +++++++++++++++------------ src/PIL/TiffImagePlugin.py | 36 ++++++++++++++++++++-------------- tox.ini | 2 +- winbuild/build_prepare.py | 12 ++++++------ 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 23792281b..47fc64399 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pytest +sphinx types-defusedxml types-olefile types-setuptools diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba29..caa852b1f 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,6 +14,7 @@ from __future__ import annotations import struct from io import BytesIO +from typing import IO from PIL import Image, ImageFile @@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844 DXT5_FOURCC = 0x35545844 -def _decode565(bits): +def _decode565(bits: int) -> tuple[int, int, int]: a = ((bits >> 11) & 0x1F) << 3 b = ((bits >> 5) & 0x3F) << 2 c = (bits & 0x1F) << 3 return a, b, c -def _c2a(a, b): +def _c2a(a: int, b: int) -> int: return (2 * a + b) // 3 -def _c2b(a, b): +def _c2b(a: int, b: int) -> int: return (a + b) // 2 -def _c3(a, b): +def _c3(a: int, b: int) -> int: return (2 * b + a) // 3 -def _dxt1(data, width, height): +def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -151,7 +152,7 @@ def _dxt1(data, width, height): return bytes(ret) -def _dxtc_alpha(a0, a1, ac0, ac1, ai): +def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: if ai <= 12: ac = (ac0 >> ai) & 7 elif ai == 15: @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): return alpha -def _dxt5(data, width, height): +def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"DXT5": self.decoder = "DXT5" else: - msg = f"Unimplemented pixel format {fourcc}" + msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT5", DXT5Decoder) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..2ce8a6c2a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -456,8 +456,11 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -def _register_loader(idx: int, size: int): - def decorator(func): +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: from .TiffTags import TYPES if func.__name__.startswith("load_"): @@ -482,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) @@ -560,7 +564,7 @@ class ImageFileDirectory_v2(_IFDv2Base): """ - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} def __init__( @@ -653,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag: int, value, legacy_api: bool) -> None: + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -744,10 +748,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt: str, data: bytes): + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values) -> bytes: + def _pack(self, fmt: str, *values: Any) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -824,7 +828,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data: bytes, legacy_api: bool = True): + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}l", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -1088,7 +1094,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) diff --git a/tox.ini b/tox.ini index 4b4059455..70b8bf145 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy src Tests {posargs} + mypy docs src winbuild Tests {posargs} diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 7129699eb..1021e4f22 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +from typing import Any def cmd_cd(path: str) -> str: @@ -43,21 +44,19 @@ def cmd_nmake( target: str = "", params: list[str] | None = None, ) -> str: - params = "" if params is None else " ".join(params) - return " ".join( [ "{nmake}", "-nologo", f'-f "{makefile}"' if makefile is not None else "", - f"{params}", + f'{" ".join(params)}' if params is not None else "", f'"{target}"', ] ) def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." + target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -129,7 +128,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation -DEPS = { +DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", @@ -538,7 +537,7 @@ def write_script( print(" " + line) -def get_footer(dep: dict) -> list[str]: +def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): lines.append(cmd_copy(out, "{inc_dir}")) @@ -583,6 +582,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) + assert match is not None license_text = "\n".join(match.groups()) assert len(license_text) > 50 with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: From d4c72da6b2ae5aabc45f679f8feeaf2726976a2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:10:31 +1000 Subject: [PATCH 107/136] Added type hints to example code --- docs/handbook/image-file-formats.rst | 12 +++++++----- docs/handbook/tutorial.rst | 6 +++--- docs/handbook/writing-your-own-image-plugin.rst | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 861e09a43..ca0e05eb6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1517,19 +1517,21 @@ To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF handler. :: - from PIL import Image + from typing import IO + + from PIL import Image, ImageFile from PIL import WmfImagePlugin - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: ... - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: ... return image - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: ... diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index c36011362..3df8e0d20 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -186,7 +186,7 @@ Rolling an image :: - def roll(im, delta): + def roll(im: Image.Image, delta: int) -> Image.Image: """Roll an image sideways.""" xsize, ysize = im.size @@ -211,7 +211,7 @@ Merging images :: - def merge(im1, im2): + def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) @@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality. import glob from PIL import Image - def compress_image(source_path, dest_path): + def compress_image(source_path: str, dest_path: str) -> None: with Image.open(source_path) as img: if img.mode != "RGB": img = img.convert("RGB") diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa7..2e853224d 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -53,7 +53,7 @@ true color. from PIL import Image, ImageFile - def _accept(prefix): + def _accept(prefix: bytes) -> bool: return prefix[:4] == b"SPAM" @@ -62,7 +62,7 @@ true color. format = "SPAM" format_description = "Spam raster image" - def _open(self): + def _open(self) -> None: header = self.fp.read(128).split() @@ -82,7 +82,7 @@ true color. raise SyntaxError(msg) # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] Image.register_open(SpamImageFile.format, SpamImageFile, _accept) From d5e3f6b51673564c16a8973a72a2b68e8ba96f0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:46:23 +1000 Subject: [PATCH 108/136] If left and right sides meet, do not draw rectangle to fill gap --- ...d_rectangle_joined_x_different_corners.png | Bin 0 -> 411 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png diff --git a/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png b/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png new file mode 100644 index 0000000000000000000000000000000000000000..b225afc2dc1449375e748d59e2336eedd9ddceaf GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^DIm None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + (20, 10, 80, 90), + 30, + fill="red", + outline="green", + width=5, + corners=(True, False, False, False), + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6f56d0236..5609bf971 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -505,7 +505,7 @@ class ImageDraw: if full_x: self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - else: + elif x1 - r - 1 > x0 + r + 1: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] From 0a03b77daf088e2aac0e32acbbc370704c749591 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 06:36:31 +1000 Subject: [PATCH 109/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 25dcb1887..a7edc340c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 + [radarhere] + - Remove WebP support without anim, mux/demux, and with buggy alpha #8213 [homm, radarhere] From 617699ffc76230d481d903c7a9b000d96b7e89e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 12:03:02 +1000 Subject: [PATCH 110/136] Log value from tag_v2 --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2ce8a6c2a..7bbbc832e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1393,7 +1393,7 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- photometric_interpretation: %s", photo) logger.debug("- planar_configuration: %s", self._planar_configuration) logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) + logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) From 6f506d2ae3879256e8a893233fc28ba6fb2518d8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:59:46 +0300 Subject: [PATCH 111/136] Run all flake8-pytest-style except rules some that fail --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74764752e..8ab478e84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.19 hooks: - id: validate-pyproject diff --git a/pyproject.toml b/pyproject.toml index 8bb21019c..d4b83ef7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks - "PT006", # pytest-parametrize-names-wrong-type + "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) "UP", # pyupgrade @@ -121,6 +121,14 @@ lint.ignore = [ "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' + "PT001", # pytest-fixture-incorrect-parentheses-style + "PT007", # pytest-parametrize-values-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT014", # pytest-duplicate-parametrize-test-cases + "PT016", # pytest-fail-without-message + "PT017", # pytest-assert-in-except + "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From 5c282d02991e6bca1993c974d85dd19bce734ddd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:47:11 +0300 Subject: [PATCH 112/136] Fix PT014: duplicate test cases in pytest.mark.parametrize --- Tests/test_file_jpeg2k.py | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5cfa7c6c..5e11465ca 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -233,7 +233,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"), - ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + (None, {"no_jp2": False}, 4, b"jP"), ), ) def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: diff --git a/pyproject.toml b/pyproject.toml index d4b83ef7f..b42ad7424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,6 @@ lint.ignore = [ "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements - "PT014", # pytest-duplicate-parametrize-test-cases "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except "PT018", # pytest-composite-assertion From 5747267eb39961101d8ed6f18e77cefc1fc5c4b8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:52:56 +0300 Subject: [PATCH 113/136] Fix PT018: Assert only one thing --- Tests/test_file_jpeg.py | 9 ++++++--- Tests/test_imagechops.py | 3 ++- Tests/test_imagemorph.py | 3 ++- Tests/test_tiff_ifdrational.py | 4 ++-- pyproject.toml | 1 - 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index df589e24a..8e6221750 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1019,13 +1019,16 @@ class TestFileJpeg: # SOI, EOI for marker in b"\xff\xd8", b"\xff\xd9": - assert marker in data[1] and marker in data[2] + assert marker in data[1] + assert marker in data[2] # DHT, DQT for marker in b"\xff\xc4", b"\xff\xdb": - assert marker in data[1] and marker not in data[2] + assert marker in data[1] + assert marker not in data[2] # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": - assert marker not in data[1] and marker in data[2] + assert marker not in data[1] + assert marker in data[2] with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4fc28cdb9..4309214f5 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -398,7 +398,8 @@ def test_logical() -> None: for y in (a, b): imy = Image.new("1", (1, 1), y) value = op(imx, imy).getpixel((0, 0)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None out.append(value) return out diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 4363f456e..80d8c3815 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str: line = "" for c in range(im.width): value = im.getpixel((c, r)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None line += chars[value > 0] result.append(line) return "\n".join(result) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 9d06a9332..13f1f9c80 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -54,8 +54,8 @@ def test_nonetype() -> None: assert xres.denominator is not None assert yres._val is not None - assert xres and 1 - assert xres and yres + assert xres + assert yres @pytest.mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index b42ad7424..9e53d4bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,6 @@ lint.ignore = [ "PT012", # pytest-raises-with-multiple-statements "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except - "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From f26b47595b10eaf3d781a8ea3f357f78f0a2774c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 06:56:31 +1000 Subject: [PATCH 114/136] Get IPTC info from tag_v2 --- Tests/test_file_iptc.py | 10 ++++++++++ src/PIL/IptcImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index b0ea2bf42..8a7c59fb1 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -77,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None: assert len(iptc) == 3 +def test_getiptcinfo_tiff() -> None: + # Arrange + with Image.open("Tests/images/hopper.Lab.tif") as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + + def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 17243e705..153243519 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -213,7 +213,7 @@ def getiptcinfo( # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] except (AttributeError, KeyError): pass From cd76b4853319619a0cb9726dcd7907551794f049 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 16 Aug 2024 09:08:18 -0500 Subject: [PATCH 115/136] move repeated code to private helper function --- src/PIL/TiffImagePlugin.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..7dda413e6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2080,38 +2080,34 @@ class AppendingTiffWriter: (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value + @staticmethod + def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: + if bytes_written is not None and bytes_written != expected: + msg = f"wrote only {bytes_written} bytes but wanted {expected}" + raise RuntimeError(msg) + def rewriteLastShortToLong(self, value: int) -> None: 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: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def rewriteLastShort(self, value: int) -> None: 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: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def rewriteLastLong(self, value: int) -> None: 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: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def writeLong(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def close(self) -> None: self.finalize() From 2467369c134a882473e46928eac952d7fab5f7a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 20:50:45 +1000 Subject: [PATCH 116/136] Uninstall gradle and maven on macOS 13 --- .github/workflows/macos-install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index d35cfcd31..25943776f 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,6 +2,9 @@ set -e +if [[ "$ImageOS" == "macos13" ]]; then + brew uninstall gradle maven +fi brew install \ freetype \ ghostscript \ From d1d567bb59886f15e91522d937e5399395158692 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 08:47:35 +1000 Subject: [PATCH 117/136] Do not use sys.stdout in PSDraw --- Tests/test_psdraw.py | 11 +++-------- src/PIL/PSDraw.py | 11 ++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index c3afa9089..a743d831f 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -5,8 +5,6 @@ import sys from io import BytesIO from pathlib import Path -import pytest - from PIL import Image, PSDraw @@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None: assert os.path.getsize(tempfile) > 0 -@pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer: bool) -> None: +def test_stdout() -> None: # Temporarily redirect stdout old_stdout = sys.stdout class MyStdOut: buffer = BytesIO() - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + mystdout = MyStdOut() sys.stdout = mystdout @@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None: # Reset stdout sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - assert mystdout.getvalue() != b"" + assert mystdout.buffer.getvalue() != b"" diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 673eae1d1..02939d26b 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,7 +17,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING +from typing import IO, TYPE_CHECKING from . import EpsImagePlugin @@ -28,15 +28,12 @@ from . import EpsImagePlugin class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. + ``sys.stdout.buffer`` is assumed. """ - def __init__(self, fp=None): + def __init__(self, fp: IO[bytes] | None = None) -> None: if not fp: - try: - fp = sys.stdout.buffer - except AttributeError: - fp = sys.stdout + fp = sys.stdout.buffer self.fp = fp def begin_document(self, id: str | None = None) -> None: From 2ed8502d12a0f0efd7d4cd2c65b4c6a7e59aaa2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 22:55:07 +1000 Subject: [PATCH 118/136] Use ipython PrettyPrinter, rather than custom class --- .appveyor.yml | 2 +- .ci/install.sh | 1 + .github/workflows/macos-install.sh | 1 + .github/workflows/test-cygwin.yml | 1 + Tests/test_image.py | 17 +++++++++++------ src/PIL/Image.py | 4 +++- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e12987a5f..f490561cd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -51,7 +51,7 @@ build_script: test_script: - cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' diff --git a/.ci/install.sh b/.ci/install.sh index 8e65f64c4..b2d615866 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 25943776f..ddb421230 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 8e2827099..0aa79e423 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -74,6 +74,7 @@ jobs: perl python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel + python3${{ matrix.python-minor-version }}-ipython python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..2afcf2efd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -42,6 +42,12 @@ try: except ImportError: ElementTree = None +PrettyPrinter: type | None +try: + from IPython.lib.pretty import PrettyPrinter +except ImportError: + PrettyPrinter = None + # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: @@ -91,16 +97,15 @@ class TestImage: # with pytest.raises(MemoryError): # Image.new("L", (1000000, 1000000)) + @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed") def test_repr_pretty(self) -> None: - class Pretty: - def text(self, text: str) -> None: - self.pretty_output = text - im = Image.new("L", (100, 100)) - p = Pretty() + output = io.StringIO() + assert PrettyPrinter is not None + p = PrettyPrinter(output) im._repr_pretty_(p, False) - assert p.pretty_output == "" + assert output.getvalue() == "" def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..705d0a144 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -221,6 +221,8 @@ if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element + from IPython.lib.pretty import PrettyPrinter + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] @@ -677,7 +679,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle: bool) -> None: + def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), From e6e5ef5c5fbd83ac5dd63301e4d7d6860a7b2d09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Aug 2024 08:05:02 +1000 Subject: [PATCH 119/136] Added type hints --- Tests/test_file_tiff.py | 7 +++++ Tests/test_imagefile.py | 3 +- docs/reference/Image.rst | 1 + src/PIL/IcoImagePlugin.py | 4 +-- src/PIL/Image.py | 42 ++++++++++++++++++--------- src/PIL/ImageFile.py | 8 ++++-- src/PIL/IptcImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 59 ++++++++++++++++++++++---------------- 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8cad25272..190f83f40 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -684,6 +684,13 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert_image_equal_tofile(reloaded, infile) + def test_invalid_tiled_dimensions(self) -> None: + with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp: + data = fp.read() + b = BytesIO(data[:144] + b"\x02" + data[145:]) + with pytest.raises(ValueError): + Image.open(b) + @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 95e91db83..a8bd798c1 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest): with pytest.raises(NotImplementedError): encoder.encode_to_pyfd() - fh = BytesIO() with pytest.raises(NotImplementedError): - encoder.encode_to_file(fh, 0) + encoder.encode_to_file(0, 0) def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 66c5e5422..02e714f20 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -363,6 +363,7 @@ Classes :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImageTransformHandler +.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 3bf5d9c04..926562497 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile): self.load() @property - def size(self): + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value): + def size(self, value: tuple[int, int]) -> None: if value not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 705d0a144..ffc6bec34 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -504,7 +504,7 @@ class _E: return _E(self.scale / other, self.offset / other) -def _getscaleoffset(expr) -> tuple[float, float]: +def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: a = expr(_E(1, 0)) return (a.scale, a.offset) if isinstance(a, _E) else (0, a) @@ -1894,7 +1894,13 @@ class Image: def point( self, - lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, + lut: ( + Sequence[float] + | NumpyArray + | Callable[[int], float] + | Callable[[_E], _E | float] + | ImagePointHandler + ), mode: str | None = None, ) -> Image: """ @@ -1930,10 +1936,10 @@ class Image: # check if the function can be used with point_transform # UNDONE wiredfool -- I think this prevents us from ever doing # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) + scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands + flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] else: flatLut = lut @@ -2869,11 +2875,11 @@ class Image: self, box: tuple[int, int, int, int], image: Image, - method, - data, + method: Transform, + data: Sequence[float], resample: int = Resampling.NEAREST, fill: bool = True, - ): + ) -> None: w = box[2] - box[0] h = box[3] - box[1] @@ -4008,15 +4014,19 @@ class Exif(_ExifBase): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) - def get_ifd(self, tag): + def get_ifd(self, tag: int) -> dict[int, Any]: if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: if self._info is not None and self._info.next != 0: - self._ifds[tag] = self._get_ifd_dict(self._info.next) + ifd = self._get_ifd_dict(self._info.next) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) if offset is not None: - self._ifds[tag] = self._get_ifd_dict(offset, tag) + ifd = self._get_ifd_dict(offset, tag) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) @@ -4073,7 +4083,9 @@ class Exif(_ExifBase): (offset,) = struct.unpack(">L", data) self.fp.seek(offset) - camerainfo = {"ModelID": self.fp.read(4)} + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } self.fp.read(4) # Seconds since 2000 @@ -4089,16 +4101,18 @@ class Exif(_ExifBase): ][1] camerainfo["Parallax"] = handler( ImageFileDirectory_v2(), parallax, False - ) + )[0] self.fp.read(4) camerainfo["Category"] = self.fp.read(2) - makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + makernote = {0x1101: camerainfo} self._ifds[tag] = makernote else: # Interop - self._ifds[tag] = self._get_ifd_dict(tag_data, tag) + ifd = self._get_ifd_dict(tag_data, tag) + if ifd is not None: + self._ifds[tag] = ifd ifd = self._ifds.get(tag, {}) if tag == ExifTags.IFD.Exif and self._hidden_data: ifd = { diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 73554fa53..fdeb81d7a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,6 +31,7 @@ from __future__ import annotations import abc import io import itertools +import os import struct import sys from typing import IO, Any, NamedTuple @@ -555,7 +556,7 @@ def _encode_tile( fp: IO[bytes], tile: list[_Tile], bufsize: int, - fh, + fh: int | None, exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: @@ -577,6 +578,7 @@ def _encode_tile( break else: # slight speedup: compress to real file object + assert fh is not None errcode = encoder.encode_to_file(fh, bufsize) if errcode < 0: raise _get_oserror(errcode, encoder=True) from exc @@ -801,7 +803,7 @@ class PyEncoder(PyCodec): self.fd.write(data) return bytes_consumed, errcode - def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: + def encode_to_file(self, fh: int, bufsize: int) -> int: """ :param fh: File handle. :param bufsize: Buffer size. @@ -814,5 +816,5 @@ class PyEncoder(PyCodec): while errcode == 0: status, errcode, buf = self.encode(bufsize) if status > 0: - fh.write(buf[status:]) + os.write(fh, buf[status:]) return errcode diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 153243519..6ccf28aa1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -214,7 +214,7 @@ def getiptcinfo( # as 4-byte integers, so we cannot use the get method...) try: data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] - except (AttributeError, KeyError): + except KeyError: pass if data is None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7bbbc832e..5c6996a65 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata: dict[int, bytes] = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None - self._offset = None + self._offset: int | None = None def __str__(self) -> str: return str(dict(self)) - def named(self): + def named(self) -> dict[str, Any]: """ :returns: dict of name|key: value @@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base): raise OSError(msg) return ret - def load(self, fp): + def load(self, fp: IO[bytes]) -> None: self.reset() self._offset = fp.tell() @@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): for legacy_api in (False, True): self._setitem(tag, value, legacy_api) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v1: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile): format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False - def __init__(self, fp=None, filename=None): - self.tag_v2 = None + def __init__( + self, + fp: StrOrBytesPath | IO[bytes] | None = None, + filename: str | bytes | None = None, + ) -> None: + self.tag_v2: ImageFileDirectory_v2 """ Image file directory (tag dictionary) """ - self.tag = None + self.tag: ImageFileDirectory_v1 """ Legacy tag entries """ super().__init__(fp, filename) @@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile): self.tag_v2 = ImageFileDirectory_v2(ifh) - # legacy IFD entries will be filled in later - self.ifd: ImageFileDirectory_v1 | None = None - # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 @@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = int(self.tag_v2.get(IMAGEWIDTH)) - ysize = int(self.tag_v2.get(IMAGELENGTH)) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) + if not isinstance(xsize, int) or not isinstance(ysize, int): + msg = "Invalid dimensions" + raise ValueError(msg) self._size = xsize, ysize logger.debug("- size: %s", self.size) @@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile): else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(TILEWIDTH) + tilewidth = self.tag_v2.get(TILEWIDTH) h = self.tag_v2.get(TILELENGTH) + if not isinstance(tilewidth, int) or not isinstance(h, int): + msg = "Invalid tile dimensions" + raise ValueError(msg) + w = tilewidth for offset in offsets: if x + w > xsize: @@ -1624,7 +1632,7 @@ SAVE_INFO = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp, filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1760,10 +1768,11 @@ def _save(im, fp, filename): if im.mode == "1": inverted_im = im.copy() px = inverted_im.load() - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im + if px is not None: + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im else: im = ImageOps.invert(im) @@ -1805,11 +1814,11 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if im.mode == "YCbCr": - for tag, value in { + for tag, default_value in { YCBCRSUBSAMPLING: (1, 1), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), }.items(): - ifd.setdefault(tag, value) + ifd.setdefault(tag, default_value) blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: @@ -1852,7 +1861,7 @@ def _save(im, fp, filename): ] # bits per sample is a single short in the tiff directory, not a list. - atts = {BITSPERSAMPLE: bits[0]} + atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. @@ -1923,13 +1932,15 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], ) # -- helper for multi-page save -- if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) - im._debug_multipage = ifd + setattr(im, "_debug_multipage", ifd) class AppendingTiffWriter: From 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 15:32:06 -0400 Subject: [PATCH 120/136] Add/update project-makefile files --- Makefile | 4757 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 4644 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index 94f7565d8..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -1,125 +1,4656 @@ -.DEFAULT_GOAL := help +# Project Makefile +# +# A makefile to automate setup of a Wagtail CMS project and related tasks. +# +# https://github.com/aclark4life/project-makefile +# +# -------------------------------------------------------------------------------- +# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` +# -------------------------------------------------------------------------------- -.PHONY: clean -clean: - rm src/PIL/*.so || true - rm -r build || true - find . -name __pycache__ | xargs rm -r || true +.DEFAULT_GOAL := git-commit -.PHONY: coverage -coverage: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - rm -r htmlcov || true - python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage - python3 -m coverage report +# -------------------------------------------------------------------------------- +# Single line variables to be used by phony target rules +# -------------------------------------------------------------------------------- -.PHONY: doc -.PHONY: html -doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs html +ADD_DIR := mkdir -pv +ADD_FILE := touch +AWS_OPTS := --no-cli-pager --output table +COPY_DIR := cp -rv +COPY_FILE := cp -v +DEL_DIR := rm -rv +DEL_FILE := rm -v +DJANGO_DB_COL = awk -F\= '{print $$2}' +DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" +DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') +DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') +DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') +DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') +DJANGO_BACKEND_APPS_FILE := backend/apps.py +DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py +DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ + package.json postcss.config.js +DJANGO_SETTINGS_DIR = backend/settings +DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py +DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py +DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py +DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) +DJANGO_URLS_FILE = backend/urls.py +EB_DIR_NAME := .elasticbeanstalk +EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) +EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" +EC2_INSTANCE_MAX ?= 1 +EC2_INSTANCE_MIN ?= 1 +EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role +EC2_INSTANCE_TYPE ?= t4g.small +EC2_LB_TYPE ?= application +EDITOR_REVIEW = subl +GIT_ADD := git add +GIT_BRANCH = $(shell git branch --show-current) +GIT_BRANCHES = $(shell git branch -a) +GIT_CHECKOUT = git checkout +GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" +GIT_COMMIT = git commit +GIT_PUSH = git push +GIT_PUSH_FORCE = git push --force-with-lease +GIT_REV = $(shell git rev-parse --short HEAD) +MAKEFILE_CUSTOM_FILE := project.mk +PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') +PAGER ?= less +PIP_ENSURE = python -m ensurepip +PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt +PROJECT_DIRS = backend contactpage home privacy siteuser +PROJECT_EMAIL := aclark@aclark.net +PROJECT_NAME = project-makefile +RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') +TMPDIR := $(shell mktemp -d) +UNAME := $(shell uname) +WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser +WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt -.PHONY: htmlview -htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs htmlview +# -------------------------------------------------------------------------------- +# Include $(MAKEFILE_CUSTOM_FILE) if it exists +# -------------------------------------------------------------------------------- -.PHONY: doccheck -doccheck: - $(MAKE) doc -# Don't make our tests rely on the links in the docs being up every single build. -# We don't control them. But do check, and update them to the target of their redirects. - $(MAKE) -C docs linkcheck || true +ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) + include $(MAKEFILE_CUSTOM_FILE) +endif -.PHONY: docserve -docserve: - cd docs/_build/html && python3 -m http.server 2> /dev/null& +# -------------------------------------------------------------------------------- +# Multi-line variables to be used in phony target rules +# -------------------------------------------------------------------------------- -.PHONY: help -help: - @echo "Welcome to Pillow development. Please use \`make \` where is one of" - @echo " clean remove build products" - @echo " coverage run coverage test (in progress)" - @echo " doc make HTML docs" - @echo " docserve run an HTTP server on the docs directory" - @echo " html make HTML docs" - @echo " htmlview open the index page built by the html target in your browser" - @echo " install make and install" - @echo " install-coverage make and install with C coverage" - @echo " lint run the lint checks" - @echo " lint-fix run Ruff to (mostly) fix lint issues" - @echo " release-test run code and package tests before release" - @echo " test run tests on installed Pillow" +define DJANGO_ALLAUTH_BASE_TEMPLATE +{% extends 'base.html' %} +endef -.PHONY: install -install: - python3 -m pip -v install . - python3 selftest.py - -.PHONY: install-coverage -install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . - python3 selftest.py - -.PHONY: debug -debug: -# make a debug version if we don't have a -dbg python. Leaves in symbols -# 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 -v install . > /dev/null - -.PHONY: release-test -release-test: - python3 Tests/check_release_notes.py - python3 -m pip install -e .[tests] - python3 selftest.py - python3 -m pytest Tests - python3 -m pip install . - python3 -m pytest -qq - python3 -m check_manifest - python3 -m pyroma . - $(MAKE) readme - -.PHONY: sdist -sdist: - python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build - python3 -m build --sdist - python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine - python3 -m twine check --strict dist/* - -.PHONY: test -test: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - -.PHONY: valgrind -valgrind: - python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ - --log-file=/tmp/valgrind-output \ - python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output - -.PHONY: readme -readme: - python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 - python3 -m markdown2 README.md > .long-description.html && open .long-description.html +define DJANGO_API_SERIALIZERS +from rest_framework import serializers +from siteuser.models import User -.PHONY: lint -lint: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e lint +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ["url", "username", "email", "is_staff"] +endef -.PHONY: lint-fix -lint-fix: - python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black - python3 -m black . - python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . +define DJANGO_API_VIEWS +from ninja import NinjaAPI +from rest_framework import viewsets +from siteuser.models import User +from .serializers import UserSerializer -.PHONY: mypy -mypy: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e mypy +api = NinjaAPI() + + +@api.get("/hello") +def hello(request): + return "Hello world" + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer +endef + +define DJANGO_APP_TESTS +from django.test import TestCase +from django.urls import reverse +from .models import YourModel +from .forms import YourForm + + +class YourModelTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_instance_creation(self): + self.assertIsInstance(self.instance, YourModel) + self.assertEqual(self.instance.field1, "value1") + self.assertEqual(self.instance.field2, "value2") + + def test_str_method(self): + self.assertEqual(str(self.instance), "Expected String Representation") + + +class YourViewTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_view_url_exists_at_desired_location(self): + response = self.client.get("/your-url/") + self.assertEqual(response.status_code, 200) + + def test_view_url_accessible_by_name(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + + def test_view_uses_correct_template(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "your_template.html") + + def test_view_context(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertIn("context_variable", response.context) + + +class YourFormTest(TestCase): + def test_form_valid_data(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + self.assertTrue(form.is_valid()) + + def test_form_invalid_data(self): + form = YourForm(data={"field1": "", "field2": "value2"}) + self.assertFalse(form.is_valid()) + self.assertIn("field1", form.errors) + + def test_form_save(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + if form.is_valid(): + instance = form.save() + self.assertEqual(instance.field1, "value1") + self.assertEqual(instance.field2, "value2") +endef + +define DJANGO_BACKEND_APPS +from django.contrib.admin.apps import AdminConfig + + +class CustomAdminConfig(AdminConfig): + default_site = "backend.admin.CustomAdminSite" +endef + +define DJANGO_BASE_TEMPLATE +{% load static webpack_loader %} + + + + + + {% block title %}{% endblock %} + {% block title_suffix %}{% endblock %} + + + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +

    +
    + {% include 'header.html' %} + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} +
    + {% block content %}{% endblock %} +
    +
    + {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define DJANGO_CUSTOM_ADMIN +from django.contrib.admin import AdminSite + + +class CustomAdminSite(AdminSite): + site_header = "Project Makefile" + site_title = "Project Makefile" + index_title = "Project Makefile" + + +custom_admin_site = CustomAdminSite(name="custom_admin") +endef + +define DJANGO_DOCKERCOMPOSE +version: '3' + +services: + db: + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: project + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + + web: + build: . + command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" + volumes: + - .:/app + ports: + - "8000:8000" + depends_on: + - db + environment: + DATABASE_URL: postgres://admin:admin@db:5432/project + +volumes: + postgres_data: +endef + +define DJANGO_DOCKERFILE +FROM amazonlinux:2023 +RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server +USER postgres +RUN initdb -D /var/lib/pgsql/data +USER root +RUN useradd wagtail +EXPOSE 8000 +ENV PYTHONUNBUFFERED=1 PORT=8000 +COPY requirements.txt / +RUN python3.11 -m pip install -r /requirements.txt +WORKDIR /app +RUN chown wagtail:wagtail /app +COPY --chown=wagtail:wagtail . . +USER wagtail +RUN npm-20 install; npm-20 run build +RUN python3.11 manage.py collectstatic --noinput --clear +CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application +endef + +define DJANGO_FAVICON_TEMPLATE +{% load static %} + +endef + +define DJANGO_FOOTER_TEMPLATE + +endef + +define DJANGO_FRONTEND_APP +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'bootstrap'; +import '@fortawesome/fontawesome-free/js/fontawesome'; +import '@fortawesome/fontawesome-free/js/solid'; +import '@fortawesome/fontawesome-free/js/regular'; +import '@fortawesome/fontawesome-free/js/brands'; +import getDataComponents from '../dataComponents'; +import UserContextProvider from '../context'; +import * as components from '../components'; +import "../styles/index.scss"; +import "../styles/theme-blue.scss"; +import "./config"; + +const { ErrorBoundary } = components; +const dataComponents = getDataComponents(components); +const container = document.getElementById('app'); +const root = createRoot(container); +const App = () => ( + + + {dataComponents} + + +); +root.render(); +endef + +define DJANGO_FRONTEND_APP_CONFIG +import '../utils/themeToggler.js'; +// import '../utils/tinymce.js'; +endef + +define DJANGO_FRONTEND_BABELRC +{ + "presets": [ + [ + "@babel/preset-react", + ], + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": "3.0.0" + } + ] + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-class-properties" + ] +} +endef + +define DJANGO_FRONTEND_COMPONENTS +export { default as ErrorBoundary } from './ErrorBoundary'; +export { default as UserMenu } from './UserMenu'; +endef + +define DJANGO_FRONTEND_COMPONENT_CLOCK +// Via ChatGPT +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import PropTypes from 'prop-types'; + +const Clock = ({ color = '#fff' }) => { + const [date, setDate] = useState(new Date()); + const [blink, setBlink] = useState(true); + const timerID = useRef(); + + const tick = useCallback(() => { + setDate(new Date()); + setBlink(prevBlink => !prevBlink); + }, []); + + useEffect(() => { + timerID.current = setInterval(() => tick(), 1000); + + // Return a cleanup function to be run on component unmount + return () => clearInterval(timerID.current); + }, [tick]); + + const formattedDate = date.toLocaleDateString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + }); + + const formattedTime = date.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: 'numeric', + }); + + return ( + <> +
    {formattedDate} {formattedTime}
    + + ); +}; + +Clock.propTypes = { + color: PropTypes.string, +}; + +export default Clock; +endef + +define DJANGO_FRONTEND_COMPONENT_ERROR +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ErrorBoundary extends Component { + constructor (props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError () { + return { hasError: true }; + } + + componentDidCatch (error, info) { + const { onError } = this.props; + console.error(error); + onError && onError(error, info); + } + + render () { + const { children = null } = this.props; + const { hasError } = this.state; + + return hasError ? null : children; + } +} + +ErrorBoundary.propTypes = { + onError: PropTypes.func, + children: PropTypes.node, +}; + +export default ErrorBoundary; +endef + +define DJANGO_FRONTEND_COMPONENT_USER_MENU +// UserMenu.js +import React from 'react'; +import PropTypes from 'prop-types'; + +function handleLogout() { + window.location.href = '/accounts/logout'; +} + +const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { + return ( +
    + {isAuthenticated ? ( +
  1. + + +
  2. + ) : ( +
  3. + +
  4. + )} +
    + ); +}; + +UserMenu.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + isSuperuser: PropTypes.bool.isRequired, + textColor: PropTypes.string, +}; + +export default UserMenu; +endef + +define DJANGO_FRONTEND_CONTEXT_INDEX +export { UserContextProvider as default } from './UserContextProvider'; +endef + +define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +// UserContextProvider.js +import React, { createContext, useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +const UserContext = createContext(); + +export const UserContextProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const login = () => { + try { + // Add logic to handle login, set isAuthenticated to true + setIsAuthenticated(true); + } catch (error) { + console.error('Login error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + const logout = () => { + try { + // Add logic to handle logout, set isAuthenticated to false + setIsAuthenticated(false); + } catch (error) { + console.error('Logout error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + return ( + + {children} + + ); +}; + +UserContextProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useUserContext = () => { + const context = useContext(UserContext); + + if (!context) { + throw new Error('useUserContext must be used within a UserContextProvider'); + } + + return context; +}; + +// Add PropTypes for the return value of useUserContext +useUserContext.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + login: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, +}; +endef + +define DJANGO_FRONTEND_ESLINTRC +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "no-unused-vars": "off" + }, + settings: { + react: { + version: 'detect', + }, + }, +} +endef + +define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +
    + +
    + +
    +
    +endef + +define DJANGO_FRONTEND_PORTAL +// Via pwellever +import React from 'react'; +import { createPortal } from 'react-dom'; + +const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (value.toLowerCase() === 'null') { + value = null; + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + // Parse numeric value + value = parseFloat(value); + } else if ( + (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') + ) { + // Parse JSON strings + value = JSON.parse(value); + } + + result[key] = value; + return result; +}, {}); + +// This method of using portals instead of calling ReactDOM.render on individual components +// ensures that all components are mounted under a single React tree, and are therefore able +// to share context. + +export default function getPageComponents (components) { + const getPortalComponent = domEl => { + // The element's "data-component" attribute is used to determine which component to render. + // All other "data-*" attributes are passed as props. + const { component: componentName, ...rest } = domEl.dataset; + const Component = components[componentName]; + if (!Component) { + console.error(`Component "$${componentName}" not found.`); + return null; + } + const props = parseProps(rest); + domEl.innerHTML = ''; + + // eslint-disable-next-line no-unused-vars + const { ErrorBoundary } = components; + return createPortal( + + + , + domEl, + ); + }; + + return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); +} +endef + +define DJANGO_FRONTEND_STYLES +// If you comment out code below, bootstrap will use red as primary color +// and btn-primary will become red + +// $primary: red; + +@import "~bootstrap/scss/bootstrap.scss"; + +.jumbotron { + // should be relative path of the entry scss file + background-image: url("../../vendors/images/sample.jpg"); + background-size: cover; +} + +#theme-toggler-authenticated:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} + +#theme-toggler-anonymous:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} +endef + +define DJANGO_FRONTEND_THEME_BLUE +@import "~bootstrap/scss/bootstrap.scss"; + +[data-bs-theme="blue"] { + --bs-body-color: var(--bs-white); + --bs-body-color-rgb: #{to-rgb($$white)}; + --bs-body-bg: var(--bs-blue); + --bs-body-bg-rgb: #{to-rgb($$blue)}; + --bs-tertiary-bg: #{$$blue-600}; + + .dropdown-menu { + --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; + --bs-dropdown-link-active-bg: #{$$blue-700}; + } + + .btn-secondary { + --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; + --bs-btn-border-color: #{rgba($$white, .25)}; + --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; + --bs-btn-hover-border-color: #{rgba($$white, .25)}; + --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; + --bs-btn-active-border-color: #{rgba($$white, .5)}; + --bs-btn-focus-border-color: #{rgba($$white, .5)}; + + // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); + } +} +endef + +define DJANGO_FRONTEND_THEME_TOGGLER +document.addEventListener('DOMContentLoaded', function () { + const rootElement = document.documentElement; + const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); + const authThemeToggle = document.getElementById('theme-toggler-authenticated'); + if (authThemeToggle) { + localStorage.removeItem('data-bs-theme'); + } + const anonSavedTheme = localStorage.getItem('data-bs-theme'); + if (anonSavedTheme) { + rootElement.setAttribute('data-bs-theme', anonSavedTheme); + } + if (anonThemeToggle) { + anonThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + rootElement.setAttribute('data-bs-theme', newTheme); + localStorage.setItem('data-bs-theme', newTheme); + }); + } + if (authThemeToggle) { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + authThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + fetch('/user/update_theme_preference/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers + }, + body: JSON.stringify({ theme: newTheme }), + }) + .then(response => response.json()) + .then(data => { + rootElement.setAttribute('data-bs-theme', newTheme); + }) + .catch(error => { + console.error('Error updating theme preference:', error); + }); + }); + } +}); +endef + +define DJANGO_HEADER_TEMPLATE +
    +
    + +
    +
    +endef + +define DJANGO_HOME_PAGE_ADMIN +from django.contrib import admin # noqa + +# Register your models here. +endef + +define DJANGO_HOME_PAGE_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% block content %} +
    +
    +{% endblock %} +endef + +define DJANGO_HOME_PAGE_URLS +from django.urls import path +from .views import HomeView + +urlpatterns = [path("", HomeView.as_view(), name="home")] +endef + +define DJANGO_HOME_PAGE_VIEWS +from django.views.generic import TemplateView + + +class HomeView(TemplateView): + template_name = "home.html" +endef + +define DJANGO_LOGGING_DEMO_ADMIN +# Register your models here. +endef + +define DJANGO_LOGGING_DEMO_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_LOGGING_DEMO_SETTINGS +INSTALLED_APPS.append("logging_demo") # noqa +endef + +define DJANGO_LOGGING_DEMO_URLS +from django.urls import path +from .views import logging_demo + +urlpatterns = [ + path("", logging_demo, name="logging_demo"), +] +endef + +define DJANGO_LOGGING_DEMO_VIEWS +from django.http import HttpResponse +import logging + +logger = logging.getLogger(__name__) + + +def logging_demo(request): + logger.debug("Hello, world!") + return HttpResponse("Hello, world!") +endef + +define DJANGO_MANAGE_PY +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() +endef + +define DJANGO_MODEL_FORM_DEMO_ADMIN +from django.contrib import admin +from .models import ModelFormDemo + + +@admin.register(ModelFormDemo) +class ModelFormDemoAdmin(admin.ModelAdmin): + pass +endef + +define DJANGO_MODEL_FORM_DEMO_FORMS +from django import forms +from .models import ModelFormDemo + + +class ModelFormDemoForm(forms.ModelForm): + class Meta: + model = ModelFormDemo + fields = ["name", "email", "age", "is_active"] +endef + +define DJANGO_MODEL_FORM_DEMO_MODEL +from django.db import models +from django.shortcuts import reverse + + +class ModelFormDemo(models.Model): + name = models.CharField(max_length=100, blank=True, null=True) + email = models.EmailField(blank=True, null=True) + age = models.IntegerField(blank=True, null=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name or f"test-model-{self.pk}" + + def get_absolute_url(self): + return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +{% extends 'base.html' %} +{% block content %} +

    Test Model Detail: {{ model_form_demo.name }}

    +

    Name: {{ model_form_demo.name }}

    +

    Email: {{ model_form_demo.email }}

    +

    Age: {{ model_form_demo.age }}

    +

    Active: {{ model_form_demo.is_active }}

    +

    Created At: {{ model_form_demo.created_at }}

    + Edit Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +{% extends 'base.html' %} +{% block content %} +

    + {% if form.instance.pk %} + Update Test Model + {% else %} + Create Test Model + {% endif %} +

    +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +{% extends 'base.html' %} +{% block content %} +

    Test Models List

    + + Create New Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_URLS +from django.urls import path +from .views import ( + ModelFormDemoListView, + ModelFormDemoCreateView, + ModelFormDemoUpdateView, + ModelFormDemoDetailView, +) + +urlpatterns = [ + path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), + path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), + path( + "/update/", + ModelFormDemoUpdateView.as_view(), + name="model_form_demo_update", + ), + path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), +] +endef + +define DJANGO_MODEL_FORM_DEMO_VIEWS +from django.views.generic import ListView, CreateView, UpdateView, DetailView +from .models import ModelFormDemo +from .forms import ModelFormDemoForm + + +class ModelFormDemoListView(ListView): + model = ModelFormDemo + template_name = "model_form_demo_list.html" + context_object_name = "model_form_demos" + + +class ModelFormDemoCreateView(CreateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + +class ModelFormDemoUpdateView(UpdateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + +class ModelFormDemoDetailView(DetailView): + model = ModelFormDemo + template_name = "model_form_demo_detail.html" + context_object_name = "model_form_demo" +endef + +define DJANGO_PAYMENTS_ADMIN +from django.contrib import admin +from .models import Product, Order + +admin.site.register(Product) +admin.site.register(Order) +endef + +define DJANGO_PAYMENTS_FORM +from django import forms + + +class PaymentsForm(forms.Form): + stripeToken = forms.CharField(widget=forms.HiddenInput()) + amount = forms.DecimalField( + max_digits=10, decimal_places=2, widget=forms.HiddenInput() + ) +endef + +define DJANGO_PAYMENTS_MIGRATION_0002 +from django.db import migrations +import os +import secrets +import logging + +logger = logging.getLogger(__name__) + + +def generate_default_key(): + return "sk_test_" + secrets.token_hex(24) + + +def set_stripe_api_keys(apps, schema_editor): + # Get the Stripe API Key model + APIKey = apps.get_model("djstripe", "APIKey") + + # Fetch the keys from environment variables or generate default keys + test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) + live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) + + logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) + logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) + + # Check if the keys are not already in the database + if not APIKey.objects.filter(secret=test_secret_key).exists(): + APIKey.objects.create(secret=test_secret_key, livemode=False) + logger.info("Added test secret key to the database.") + else: + logger.info("Test secret key already exists in the database.") + + if not APIKey.objects.filter(secret=live_secret_key).exists(): + APIKey.objects.create(secret=live_secret_key, livemode=True) + logger.info("Added live secret key to the database.") + else: + logger.info("Live secret key already exists in the database.") + + +class Migration(migrations.Migration): + + dependencies = [ + ("payments", "0001_initial"), + ] + + operations = [ + migrations.RunPython(set_stripe_api_keys), + ] +endef + +define DJANGO_PAYMENTS_MIGRATION_0003 +from django.db import migrations + + +def create_initial_products(apps, schema_editor): + Product = apps.get_model("payments", "Product") + Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) + Product.objects.create(name="Mug", description="A nice mug", price=10.00) + Product.objects.create(name="Hat", description="A stylish hat", price=15.00) + + +class Migration(migrations.Migration): + dependencies = [ + ( + "payments", + "0002_set_stripe_api_keys", + ), + ] + + operations = [ + migrations.RunPython(create_initial_products), + ] +endef + +define DJANGO_PAYMENTS_MODELS +from django.db import models + + +class Product(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return self.name + + +class Order(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + stripe_checkout_session_id = models.CharField(max_length=200) + + def __str__(self): + return f"Order {self.id} for {self.product.name}" +endef + +define DJANGO_PAYMENTS_TEMPLATE_CANCEL +{% extends "base.html" %} +{% block title %}Cancel{% endblock %} +{% block content %} +

    Payment Cancelled

    +

    Your payment was cancelled.

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +{% extends "base.html" %} +{% block title %}Checkout{% endblock %} +{% block content %} +

    Checkout

    +
    + {% csrf_token %} + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +{% extends "base.html" %} +{% block title %}{{ product.name }}{% endblock %} +{% block content %} +

    {{ product.name }}

    +

    {{ product.description }}

    +

    Price: ${{ product.price }}

    +
    + {% csrf_token %} + + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +{% extends "base.html" %} +{% block title %}Products{% endblock %} +{% block content %} +

    Products

    + +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_SUCCESS +{% extends "base.html" %} +{% block title %}Success{% endblock %} +{% block content %} +

    Payment Successful

    +

    Thank you for your purchase!

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_URLS +from django.urls import path +from .views import ( + CheckoutView, + SuccessView, + CancelView, + ProductListView, + ProductDetailView, +) + +urlpatterns = [ + path("", ProductListView.as_view(), name="product_list"), + path("product//", ProductDetailView.as_view(), name="product_detail"), + path("checkout/", CheckoutView.as_view(), name="checkout"), + path("success/", SuccessView.as_view(), name="success"), + path("cancel/", CancelView.as_view(), name="cancel"), +] +endef + +define DJANGO_PAYMENTS_VIEW +from django.conf import settings +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.generic import TemplateView, View, ListView, DetailView +import stripe +from .models import Product, Order + +stripe.api_key = settings.STRIPE_TEST_SECRET_KEY + + +class ProductListView(ListView): + model = Product + template_name = "payments/product_list.html" + context_object_name = "products" + + +class ProductDetailView(DetailView): + model = Product + template_name = "payments/product_detail.html" + context_object_name = "product" + + +class CheckoutView(View): + template_name = "payments/checkout.html" + + def get(self, request, *args, **kwargs): + products = Product.objects.all() + return render(request, self.template_name, {"products": products}) + + def post(self, request, *args, **kwargs): + product_id = request.POST.get("product_id") + product = get_object_or_404(Product, id=product_id) + + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "price_data": { + "currency": "usd", + "product_data": { + "name": product.name, + }, + "unit_amount": int(product.price * 100), + }, + "quantity": 1, + } + ], + mode="payment", + success_url="http://localhost:8000/payments/success/", + cancel_url="http://localhost:8000/payments/cancel/", + ) + + Order.objects.create(product=product, stripe_checkout_session_id=session.id) + return redirect(session.url, code=303) + + +class SuccessView(TemplateView): + + template_name = "payments/success.html" + + +class CancelView(TemplateView): + + template_name = "payments/cancel.html" +endef + +define DJANGO_SEARCH_FORMS +from django import forms + + +class SearchForm(forms.Form): + query = forms.CharField(max_length=100, required=True, label="Search") + +endef + +define DJANGO_SEARCH_SETTINGS +SEARCH_MODELS = [ + # Add search models here. +] +endef + +define DJANGO_SEARCH_TEMPLATE +{% extends "base.html" %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define DJANGO_SEARCH_URLS +from django.urls import path +from .views import SearchView + +urlpatterns = [ + path("search/", SearchView.as_view(), name="search"), +] +endef + +define DJANGO_SEARCH_UTILS +from django.apps import apps +from django.conf import settings + +def get_search_models(): + models = [] + for model_path in settings.SEARCH_MODELS: + app_label, model_name = model_path.split(".") + model = apps.get_model(app_label, model_name) + models.append(model) + return models +endef + +define DJANGO_SEARCH_VIEWS +from django.views.generic import ListView +from django.db import models +from django.db.models import Q +from .forms import SearchForm +from .utils import get_search_models + + +class SearchView(ListView): + template_name = "your_app/search_results.html" + context_object_name = "results" + paginate_by = 10 + + def get_queryset(self): + form = SearchForm(self.request.GET) + query = None + results = [] + + if form.is_valid(): + query = form.cleaned_data["query"] + search_models = get_search_models() + + for model in search_models: + fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] + queries = [Q(**{f"{field}__icontains": query}) for field in fields] + model_results = model.objects.filter(queries.pop()) + + for item in queries: + model_results = model_results.filter(item) + + results.extend(model_results) + + return results + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = SearchForm(self.request.GET) + context["query"] = self.request.GET.get("query", "") + return context +endef + +define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] +endef + +define DJANGO_SETTINGS_BASE +# $(PROJECT_NAME) +# +# Uncomment next two lines to enable custom admin +# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] +# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') +import os # noqa +import dj_database_url # noqa + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +EXPLORER_CONNECTIONS = {"Default": "default"} +EXPLORER_DEFAULT_CONNECTION = "default" +LOGIN_REDIRECT_URL = "/" +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +endef + +define DJANGO_SETTINGS_BASE_MINIMAL +# $(PROJECT_NAME) +import os # noqa +import dj_database_url # noqa + +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("webpack_boilerplate") +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +endef + +define DJANGO_SETTINGS_CRISPY_FORMS +CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" +endef + +define DJANGO_SETTINGS_DATABASE +DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") +DATABASES["default"] = dj_database_url.parse(DATABASE_URL) +endef + +define DJANGO_SETTINGS_DEV +from .base import * # noqa + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# SECURITY WARNING: define the correct hosts in production! +ALLOWED_HOSTS = ["*"] + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +try: + from .local import * # noqa +except ImportError: + pass + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, +} + +INTERNAL_IPS = [ + "127.0.0.1", +] + +MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa +MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa +INSTALLED_APPS.append("django.contrib.admindocs") # noqa +SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" +endef + + +define DJANGO_SETTINGS_HOME_PAGE +INSTALLED_APPS.append("home") +endef + +define DJANGO_SETTINGS_INSTALLED_APPS +INSTALLED_APPS.append("allauth") +INSTALLED_APPS.append("allauth.account") +INSTALLED_APPS.append("allauth.socialaccount") +INSTALLED_APPS.append("crispy_bootstrap5") +INSTALLED_APPS.append("crispy_forms") +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("django_extensions") +INSTALLED_APPS.append("django_recaptcha") +INSTALLED_APPS.append("rest_framework") +INSTALLED_APPS.append("rest_framework.authtoken") +INSTALLED_APPS.append("webpack_boilerplate") +INSTALLED_APPS.append("explorer") +endef + +define DJANGO_SETTINGS_MIDDLEWARE +MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") +endef + +define DJANGO_SETTINGS_MODEL_FORM_DEMO +INSTALLED_APPS.append("model_form_demo") # noqa +endef + +define DJANGO_SETTINGS_PAYMENTS +DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" +DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" +STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") +STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") +STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") +INSTALLED_APPS.append("payments") # noqa +INSTALLED_APPS.append("djstripe") # noqa +endef + +define DJANGO_SETTINGS_REST_FRAMEWORK +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ] +} +endef + +define DJANGO_SETTINGS_SITEUSER +INSTALLED_APPS.append("siteuser") # noqa +AUTH_USER_MODEL = "siteuser.User" +endef + +define DJANGO_SETTINGS_PROD +from .base import * # noqa +from backend.utils import get_ec2_metadata + +DEBUG = False + +try: + from .local import * # noqa +except ImportError: + pass + +LOCAL_IPV4 = get_ec2_metadata() +ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa +endef + +define DJANGO_SETTINGS_THEMES +THEMES = [ + ("light", "Light Theme"), + ("dark", "Dark Theme"), +] +endef + +define DJANGO_SITEUSER_ADMIN +from django.contrib.auth.admin import UserAdmin +from django.contrib import admin + +from .models import User + +admin.site.register(User, UserAdmin) +endef + +define DJANGO_SITEUSER_EDIT_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags %} +{% block content %} +

    Edit User

    + {% crispy form %} +{% endblock %} +endef + +define DJANGO_SITEUSER_FORM +from django import forms +from django.contrib.auth.forms import UserChangeForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit +from .models import User + + +class SiteUserForm(UserChangeForm): + bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) + + class Meta(UserChangeForm.Meta): + model = User + fields = ("username", "user_theme_preference", "bio", "rate") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.layout = Layout( + Fieldset( + "Edit Your Profile", + "username", + "user_theme_preference", + "bio", + "rate", + ), + ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), + ) +endef + +define DJANGO_SITEUSER_MODEL +from django.db import models +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.conf import settings + + +class User(AbstractUser): + groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) + user_permissions = models.ManyToManyField( + Permission, related_name="siteuser_set", blank=True + ) + + user_theme_preference = models.CharField( + max_length=10, choices=settings.THEMES, default="light" + ) + + bio = models.TextField(blank=True, null=True) + rate = models.FloatField(blank=True, null=True) +endef + +define DJANGO_SITEUSER_URLS +from django.urls import path +from .views import UserProfileView, UpdateThemePreferenceView, UserEditView + +urlpatterns = [ + path("profile/", UserProfileView.as_view(), name="user-profile"), + path( + "update_theme_preference/", + UpdateThemePreferenceView.as_view(), + name="update_theme_preference", + ), + path("/edit/", UserEditView.as_view(), name="user-edit"), +] +endef + +define DJANGO_SITEUSER_VIEW +import json + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView +from django.views.generic.edit import UpdateView +from django.urls import reverse_lazy + +from .models import User +from .forms import SiteUserForm + + +class UserProfileView(LoginRequiredMixin, DetailView): + model = User + template_name = "profile.html" + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(csrf_exempt, name="dispatch") +class UpdateThemePreferenceView(View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode("utf-8")) + new_theme = data.get("theme") + user = request.user + user.user_theme_preference = new_theme + user.save() + response_data = {"theme": new_theme} + return JsonResponse(response_data) + except json.JSONDecodeError as e: + return JsonResponse({"error": e}, status=400) + + def http_method_not_allowed(self, request, *args, **kwargs): + return JsonResponse({"error": "Invalid request method"}, status=405) + + +class UserEditView(LoginRequiredMixin, UpdateView): + model = User + template_name = "user_edit.html" # Create this template in your templates folder + form_class = SiteUserForm + + def get_success_url(self): + # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) + return reverse_lazy("user-profile") +endef + +define DJANGO_SITEUSER_VIEW_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    User Profile

    +
    + Edit +
    +

    Username: {{ user.username }}

    +

    Theme: {{ user.user_theme_preference }}

    +

    Bio: {{ user.bio|default:""|safe }}

    +

    Rate: {{ user.rate|default:"" }}

    +{% endblock %} +endef + +define DJANGO_URLS +from django.contrib import admin +from django.urls import path, include +from django.conf import settings + +urlpatterns = [ + path("django/", admin.site.urls), +] +endef + +define DJANGO_URLS_ALLAUTH +urlpatterns += [path("accounts/", include("allauth.urls"))] +endef + +define DJANGO_URLS_API +from rest_framework import routers # noqa +from .api import UserViewSet, api # noqa + +router = routers.DefaultRouter() +router.register(r"users", UserViewSet) +# urlpatterns += [path("api/", include(router.urls))] +urlpatterns += [path("api/", api.urls)] +endef + +define DJANGO_URLS_DEBUG_TOOLBAR +if settings.DEBUG: + urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] +endef + +define DJANGO_URLS_HOME_PAGE +urlpatterns += [path("", include("home.urls"))] +endef + +define DJANGO_URLS_LOGGING_DEMO +urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] +endef + +define DJANGO_URLS_MODEL_FORM_DEMO +urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] +endef + +define DJANGO_URLS_PAYMENTS +urlpatterns += [path("payments/", include("payments.urls"))] +endef + +define DJANGO_URLS_SITEUSER +urlpatterns += [path("user/", include("siteuser.urls"))] +endef + +define DJANGO_UTILS +from django.urls import URLResolver +import requests + + +def get_ec2_metadata(): + try: + # Step 1: Get the token + token_url = "http://169.254.169.254/latest/api/token" + headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} + response = requests.put(token_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + token = response.text + + # Step 2: Use the token to get the instance metadata + metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" + headers = {"X-aws-ec2-metadata-token": token} + response = requests.get(metadata_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + metadata = response.text + return metadata + except requests.RequestException as e: + print(f"Error retrieving EC2 metadata: {e}") + return None + + +# Function to remove a specific URL pattern based on its route (including catch-all) +def remove_urlpattern(urlpatterns, route_to_remove): + urlpatterns[:] = [ + urlpattern + for urlpattern in urlpatterns + if not ( + isinstance(urlpattern, URLResolver) + and urlpattern.pattern._route == route_to_remove + ) + ] +endef + +define EB_CUSTOM_ENV_EC2_USER +files: + "/home/ec2-user/.bashrc": + mode: "000644" + owner: ec2-user + group: ec2-user + content: | + # .bashrc + + # Source global definitions + if [ -f /etc/bashrc ]; then + . /etc/bashrc + fi + + # User specific aliases and functions + set -o vi + + source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) +endef + +define EB_CUSTOM_ENV_VAR_FILE +#!/bin/bash + +# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ + +#Create a copy of the environment variable file. +cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var + +#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. +chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var + +# add the virtual env path in. +VENV=/var/app/venv/`ls /var/app/venv` +cat <> /opt/elasticbeanstalk/deployment/custom_env_var +VENV=$$ENV +EOF + +#Remove duplicate files upon deployment. +rm -f /opt/elasticbeanstalk/deployment/*.bak +endef + +define GIT_IGNORE +__pycache__ +*.pyc +dist/ +node_modules/ +_build/ +.elasticbeanstalk/ +db.sqlite3 +static/ +backend/var +endef + +define JENKINS_FILE +pipeline { + agent any + stages { + stage('') { + steps { + echo '' + } + } + } +} +endef + +define MAKEFILE_CUSTOM +# Custom Makefile +# Add your custom makefile commands here +# +# PROJECT_NAME := my-new-project +endef + +define PIP_INSTALL_REQUIREMENTS_TEST +pytest +pytest-runner +coverage +pytest-mock +pytest-cov +hypothesis +selenium +pytest-django +factory-boy +flake8 +tox +endef + +define PROGRAMMING_INTERVIEW +from rich import print as rprint +from rich.console import Console +from rich.panel import Panel + +import argparse +import locale +import math +import time + +import code # noqa +import readline # noqa +import rlcompleter # noqa + + +locale.setlocale(locale.LC_ALL, "en_US.UTF-8") + + +class DataStructure: + # Data Structure: Binary Tree + class TreeNode: + def __init__(self, value=0, left=None, right=None): + self.value = value + self.left = left + self.right = right + + # Data Structure: Stack + class Stack: + def __init__(self): + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + if not self.is_empty(): + return self.items.pop() + return None + + def peek(self): + if not self.is_empty(): + return self.items[-1] + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Queue + class Queue: + def __init__(self): + self.items = [] + + def enqueue(self, item): + self.items.append(item) + + def dequeue(self): + if not self.is_empty(): + return self.items.pop(0) + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Linked List + class ListNode: + def __init__(self, value=0, next=None): + self.value = value + self.next = next + + +class Interview(DataStructure): + + # Protected methods for factorial calculation + def _factorial_recursive(self, n): + if n == 0: + return 1 + return n * self._factorial_recursive(n - 1) + + def _factorial_divide_and_conquer(self, low, high): + if low > high: + return 1 + if low == high: + return low + mid = (low + high) // 2 + return self._factorial_divide_and_conquer( + low, mid + ) * self._factorial_divide_and_conquer(mid + 1, high) + + # Recursive Factorial with Timing + def factorial_recursive(self, n): + start_time = time.time() # Start timing + result = self._factorial_recursive(n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Iterative Factorial with Timing + def factorial_iterative(self, n): + start_time = time.time() # Start timing + result = 1 + for i in range(1, n + 1): + result *= i + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Divide and Conquer Factorial with Timing + def factorial_divide_and_conquer(self, n): + start_time = time.time() # Start timing + result = self._factorial_divide_and_conquer(1, n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Built-in Factorial with Timing + def factorial_builtin(self, n): + start_time = time.time() # Start timing + result = math.factorial(n) # Calculate factorial using built-in + end_time = time.time() # End timing + + # Calculate elapsed time + elapsed_time = end_time - start_time + + # Print complexity and runtime + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Recursion: Fibonacci + def fibonacci_recursive(self, n): + if n <= 1: + return n + return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) + + # Iteration: Fibonacci + def fibonacci_iterative(self, n): + if n <= 1: + return n + a, b = 0, 1 + for _ in range(n - 1): + a, b = b, a + b + return b + + # Searching: Linear Search + def linear_search(self, arr, target): + for i, value in enumerate(arr): + if value == target: + return i + return -1 + + # Searching: Binary Search + def binary_search(self, arr, target): + left, right = 0, len(arr) - 1 + while left <= right: + mid = (left + right) // 2 + if arr[mid] == target: + return mid + elif arr[mid] < target: + left = mid + 1 + else: + right = mid - 1 + return -1 + + # Sorting: Bubble Sort + def bubble_sort(self, arr): + n = len(arr) + for i in range(n): + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr + + # Sorting: Merge Sort + def merge_sort(self, arr): + if len(arr) > 1: + mid = len(arr) // 2 + left_half = arr[:mid] + right_half = arr[mid:] + + self.merge_sort(left_half) + self.merge_sort(right_half) + + i = j = k = 0 + + while i < len(left_half) and j < len(right_half): + if left_half[i] < right_half[j]: + arr[k] = left_half[i] + i += 1 + else: + arr[k] = right_half[j] + j += 1 + k += 1 + + while i < len(left_half): + arr[k] = left_half[i] + i += 1 + k += 1 + + while j < len(right_half): + arr[k] = right_half[j] + j += 1 + k += 1 + return arr + + def insert_linked_list(self, head, value): + new_node = self.ListNode(value) + if not head: + return new_node + current = head + while current.next: + current = current.next + current.next = new_node + return head + + def print_linked_list(self, head): + current = head + while current: + print(current.value, end=" -> ") + current = current.next + print("None") + + def inorder_traversal(self, root): + return ( + self.inorder_traversal(root.left) + + [root.value] + + self.inorder_traversal(root.right) + if root + else [] + ) + + def preorder_traversal(self, root): + return ( + [root.value] + + self.preorder_traversal(root.left) + + self.preorder_traversal(root.right) + if root + else [] + ) + + def postorder_traversal(self, root): + return ( + self.postorder_traversal(root.left) + + self.postorder_traversal(root.right) + + [root.value] + if root + else [] + ) + + # Graph Algorithms: Depth-First Search + def dfs(self, graph, start): + visited, stack = set(), [start] + while stack: + vertex = stack.pop() + if vertex not in visited: + visited.add(vertex) + stack.extend(set(graph[vertex]) - visited) + return visited + + # Graph Algorithms: Breadth-First Search + def bfs(self, graph, start): + visited, queue = set(), [start] + while queue: + vertex = queue.pop(0) + if vertex not in visited: + visited.add(vertex) + queue.extend(set(graph[vertex]) - visited) + return visited + + +def setup_readline(local): + + # Enable tab completion + readline.parse_and_bind("tab: complete") + # Optionally, you can set the completer function manually + readline.set_completer(rlcompleter.Completer(local).complete) + + +def main(): + + console = Console() + interview = Interview() + + parser = argparse.ArgumentParser(description="Programming Interview Questions") + + parser.add_argument( + "-f", "--factorial", type=int, help="Factorial algorithm examples" + ) + parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") + parser.add_argument( + "--search", action="store_true", help="Search algorithm examples" + ) + parser.add_argument("--sort", action="store_true", help="Search algorithm examples") + parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") + parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") + parser.add_argument( + "--list", action="store_true", help="Linked List algorithm examples" + ) + parser.add_argument( + "--tree", action="store_true", help="Tree traversal algorithm examples" + ) + parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") + parser.add_argument( + "-i", "--interactive", action="store_true", help="Interactive mode" + ) + + args = parser.parse_args() + + if args.factorial: + # Factorial examples + console.rule("Factorial Examples") + rprint( + Panel( + "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_recursive(args.factorial)), + title="Factorial Recursive", + ) + ) + rprint( + Panel( + "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_iterative(args.factorial)), + title="Factorial Iterative", + ) + ) + rprint( + Panel( + "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_builtin(args.factorial)), + title="Factorial Built-in", + ) + ) + rprint( + Panel( + "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" + + str(interview.factorial_divide_and_conquer(args.factorial)), + title="Factorial Divide and Conquer", + ) + ) + exit() + + if args.fibonacci: + # Fibonacci examples + console.rule("Fibonacci Examples") + rprint( + Panel( + str(interview.fibonacci_recursive(args.fibonacci)), + title="Fibonacci Recursive", + ) + ) + rprint( + Panel( + str(interview.fibonacci_iterative(args.fibonacci)), + title="Fibonacci Iterative", + ) + ) + exit() + + if args.search: + # Searching examples + console.rule("Searching Examples") + array = [1, 3, 5, 7, 9] + rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) + rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) + exit() + + if args.sort: + # Sorting examples + console.rule("Sorting Examples") + unsorted_array = [64, 34, 25, 12, 22, 11, 90] + rprint( + Panel( + str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" + ) + ) + rprint( + Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") + ) + exit() + + if args.stack: + # Stack example + console.rule("Stack Example") + stack = interview.Stack() + stack.push(1) + stack.push(2) + stack.push(3) + rprint(Panel(str(stack.pop()), title="Stack Pop")) + rprint(Panel(str(stack.peek()), title="Stack Peek")) + rprint(Panel(str(stack.size()), title="Stack Size")) + + if args.queue: + # Queue example + console.rule("Queue Example") + queue = interview.Queue() + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) + rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) + rprint(Panel(str(queue.size()), title="Queue Size")) + + if args.list: + # Linked List example + console.rule("Linked List Example") + head = None + head = interview.insert_linked_list(head, 1) + head = interview.insert_linked_list(head, 2) + head = interview.insert_linked_list(head, 3) + interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None + + if args.tree: + # Tree Traversal example + console.rule("Tree Traversal Example") + root = interview.TreeNode(1) + root.left = interview.TreeNode(2) + root.right = interview.TreeNode(3) + root.left.left = interview.TreeNode(4) + root.left.right = interview.TreeNode(5) + rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) + rprint( + Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") + ) + rprint( + Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") + ) + + if args.graph: + # Graph Algorithms example + console.rule("Graph Algorithms Example") + graph = { + "A": ["B", "C"], + "B": ["A", "D", "E"], + "C": ["A", "F"], + "D": ["B"], + "E": ["B", "F"], + "F": ["C", "E"], + } + rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) + rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) + + if args.interactive: + # Starting interactive session with tab completion + setup_readline(locals()) + banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." + code.interact( + banner=banner, + local=locals(), + exitmsg="Great interview!", + ) + + +if __name__ == "__main__": + main() + +endef + +define PYTHON_CI_YAML +name: Build Wheels +endef + +define PYTHON_LICENSE_TXT +MIT License + +Copyright (c) [YEAR] [OWNER NAME] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +endef + +define PYTHON_PROJECT_TOML +[build-system] +endef + +define SEPARATOR +.==========================================================================================================================================. +| | +| _|_|_| _| _| _| _| _| _|_| _| _| | +| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | +| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | +| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | +| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | +| _| | +| _| | +`==========================================================================================================================================' +endef + +define TINYMCE_JS +import tinymce from 'tinymce'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; +import 'tinymce/skins/ui/oxide/skin.css'; +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/code'; +import 'tinymce/plugins/emoticons'; +import 'tinymce/plugins/emoticons/js/emojis'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/table'; +import 'tinymce/models/dom'; + +tinymce.init({ + selector: 'textarea#editor', + plugins: 'advlist code emoticons link lists table', + toolbar: 'bold italic | bullist numlist | link emoticons', + skin: false, + content_css: false, +}); +endef + +define WAGTAIL_BASE_TEMPLATE +{% load static wagtailcore_tags wagtailuserbar webpack_loader %} + + + + + + {% block title %} + {% if page.seo_title %} + {{ page.seo_title }} + {% else %} + {{ page.title }} + {% endif %} + {% endblock %} + {% block title_suffix %} + {% wagtail_site as current_site %} + {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} + {% endblock %} + + {% if page.search_description %}{% endif %} + + {# Force all links in the live preview panel to be opened in a new tab #} + {% if request.in_preview_panel %}{% endif %} + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +
    + {% wagtailuserbar %} +
    + {% include 'header.html' %} + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} +
    + {% block content %}{% endblock %} +
    +
    + {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define WAGTAIL_BLOCK_CAROUSEL + +endef + +define WAGTAIL_BLOCK_MARKETING +{% load wagtailcore_tags %} +
    + {% if block.value.images.0 %} + {% include 'blocks/carousel_block.html' %} + {% else %} + {{ self.title }} + {{ self.content }} + {% endif %} +
    +endef + +define WAGTAIL_CONTACT_PAGE_LANDING +{% extends 'base.html' %} +{% block content %}

    Thank you!

    {% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_MODEL +from django.db import models +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import ( + FieldPanel, FieldRowPanel, + InlinePanel, MultiFieldPanel +) +from wagtail.fields import RichTextField +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField + + +class FormField(AbstractFormField): + page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') + + +class ContactPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FieldPanel('intro'), + InlinePanel('form_fields', label="Form fields"), + FieldPanel('thank_you_text'), + MultiFieldPanel([ + FieldRowPanel([ + FieldPanel('from_address', classname="col6"), + FieldPanel('to_address', classname="col6"), + ]), + FieldPanel('subject'), + ], "Email"), + ] + + class Meta: + verbose_name = "Contact Page" +endef + +define WAGTAIL_CONTACT_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags static wagtailcore_tags %} +{% block content %} +

    {{ page.title }}

    + {{ page.intro|richtext }} +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_TEST +from django.test import TestCase +from wagtail.test.utils import WagtailPageTestCase +from wagtail.models import Page + +from contactpage.models import ContactPage, FormField + +class ContactPageTest(TestCase, WagtailPageTestCase): + def test_contact_page_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + + # Save the ContactPage instance + self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) + + def test_form_field_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Create a FormField associated with the ContactPage + form_field = FormField( + page=contact_page, + label='Your Name', + field_type='singleline', + required=True + ) + form_field.save() + + # Retrieve the ContactPage from the database + contact_page_from_db = Page.objects.get(id=contact_page.id).specific + + # Check if the FormField is associated with the ContactPage + self.assertEqual(contact_page_from_db.form_fields.first(), form_field) + + def test_contact_page_form_submission(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Simulate a form submission + form_data = { + 'your_name': 'John Doe', + # Add other form fields as needed + } + + response = self.client.post(contact_page.url, form_data) + + # Check if the form submission is successful (assuming a 302 redirect) + self.assertEqual(response.status_code, 302) + + # You may add more assertions based on your specific requirements +endef + +define WAGTAIL_HEADER_PREFIX +{% load wagtailcore_tags %} +{% wagtail_site as current_site %} +endef + +define WAGTAIL_HOME_PAGE_MODEL +from wagtail.models import Page +from wagtail.fields import StreamField +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.images.blocks import ImageChooserBlock + + +class MarketingBlock(blocks.StructBlock): + title = blocks.CharBlock(required=False, help_text="Enter the block title") + content = blocks.RichTextBlock(required=False, help_text="Enter the block content") + images = blocks.ListBlock( + ImageChooserBlock(required=False), + help_text="Select one or two images for column display. Select three or more images for carousel display.", + ) + image = ImageChooserBlock( + required=False, help_text="Select one image for background display." + ) + block_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the marketing block", + classname="full title", + default="vh-100 bg-secondary", + ) + image_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the column display image(s)", + classname="full title", + default="img-thumbnail p-5", + ) + layout_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the layout.", + classname="full title", + default="d-flex flex-row", + ) + + class Meta: + icon = "placeholder" + template = "blocks/marketing_block.html" + + +class HomePage(Page): + template = "home/home_page.html" # Create a template for rendering the home page + + marketing_blocks = StreamField( + [ + ("marketing_block", MarketingBlock()), + ], + blank=True, + null=True, + use_json_field=True, + ) + content_panels = Page.content_panels + [ + FieldPanel("marketing_blocks"), + ] + + class Meta: + verbose_name = "Home Page" +endef + +define WAGTAIL_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% load wagtailcore_tags %} +{% block content %} +
    + {% for block in page.marketing_blocks %} + {% include_block block %} + {% endfor %} +
    +{% endblock %} +endef + +define WAGTAIL_PRIVACY_PAGE_MODEL +from wagtail.models import Page +from wagtail.admin.panels import FieldPanel +from wagtailmarkdown.fields import MarkdownField + + +class PrivacyPage(Page): + """ + A Wagtail Page model for the Privacy Policy page. + """ + + template = "privacy_page.html" + + body = MarkdownField() + + content_panels = Page.content_panels + [ + FieldPanel("body", classname="full"), + ] + + class Meta: + verbose_name = "Privacy Page" +endef + +define WAGTAIL_PRIVACY_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load wagtailmarkdown %} +{% block content %}
    {{ page.body|markdown }}
    {% endblock %} +endef + +define WAGTAIL_SEARCH_TEMPLATE +{% extends "base.html" %} +{% load static wagtailcore_tags %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define WAGTAIL_SEARCH_URLS +from django.urls import path +from .views import search + +urlpatterns = [path("", search, name="search")] +endef + +define WAGTAIL_SETTINGS +INSTALLED_APPS.append("wagtail_color_panel") +INSTALLED_APPS.append("wagtail_modeladmin") +INSTALLED_APPS.append("wagtail.contrib.settings") +INSTALLED_APPS.append("wagtailmarkdown") +INSTALLED_APPS.append("wagtailmenus") +INSTALLED_APPS.append("wagtailseo") +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtail.contrib.settings.context_processors.settings" +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtailmenus.context_processors.wagtailmenus" +) +endef + +define WAGTAIL_SITEPAGE_MODEL +from wagtail.models import Page + + +class SitePage(Page): + template = "sitepage/site_page.html" + + class Meta: + verbose_name = "Site Page" +endef + +define WAGTAIL_SITEPAGE_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    {{ page.title }}

    +{% endblock %} +endef + +define WAGTAIL_URLS +from django.conf import settings +from django.urls import include, path +from django.contrib import admin + +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls + +from search import views as search_views + +urlpatterns = [ + path("django/", admin.site.urls), + path("wagtail/", include(wagtailadmin_urls)), + path("documents/", include(wagtaildocs_urls)), + path("search/", search_views.search, name="search"), +] + +if settings.DEBUG: + from django.conf.urls.static import static + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # Serve static and media files from development server + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +endef + +define WAGTAIL_URLS_HOME +urlpatterns += [ + # For anything not caught by a more specific rule above, hand over to + # Wagtail's page serving mechanism. This should be the last pattern in + # the list: + path("", include("wagtail.urls")), + # Alternatively, if you want Wagtail pages to be served from a subpath + # of your site, rather than the site root: + # path("pages/", include("wagtail.urls"), +] +endef + +define WEBPACK_CONFIG_JS +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, +}; +endef + +define WEBPACK_INDEX_HTML + + + + + + Hello, Webpack! + + + + + +endef + +define WEBPACK_INDEX_JS +const message = "Hello, World!"; +console.log(message); +endef + +define WEBPACK_REVEAL_CONFIG_JS +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'bundle.css', + }), + ], +}; +endef + +define WEBPACK_REVEAL_INDEX_HTML + + + + + + Project Makefile + + +
    +
    +
    + Slide 1: Draw some circles +
    +
    + Slide 2: Draw the rest of the owl +
    +
    +
    + + +endef + +define WEBPACK_REVEAL_INDEX_JS +import 'reveal.js/dist/reveal.css'; +import 'reveal.js/dist/theme/black.css'; +import Reveal from 'reveal.js'; +import RevealNotes from 'reveal.js/plugin/notes/notes.js'; +Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); +endef + +# ------------------------------------------------------------------------------ +# Export variables used by phony target rules +# ------------------------------------------------------------------------------ + +export DJANGO_ALLAUTH_BASE_TEMPLATE +export DJANGO_API_SERIALIZERS +export DJANGO_API_VIEWS +export DJANGO_APP_TESTS +export DJANGO_BACKEND_APPS +export DJANGO_BASE_TEMPLATE +export DJANGO_CUSTOM_ADMIN +export DJANGO_DOCKERCOMPOSE +export DJANGO_DOCKERFILE +export DJANGO_FAVICON_TEMPLATE +export DJANGO_FOOTER_TEMPLATE +export DJANGO_FRONTEND_APP +export DJANGO_FRONTEND_APP_CONFIG +export DJANGO_FRONTEND_BABELRC +export DJANGO_FRONTEND_COMPONENTS +export DJANGO_FRONTEND_COMPONENT_CLOCK +export DJANGO_FRONTEND_COMPONENT_ERROR +export DJANGO_FRONTEND_COMPONENT_USER_MENU +export DJANGO_FRONTEND_CONTEXT_INDEX +export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +export DJANGO_FRONTEND_ESLINTRC +export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +export DJANGO_FRONTEND_PORTAL +export DJANGO_FRONTEND_STYLES +export DJANGO_FRONTEND_THEME_BLUE +export DJANGO_FRONTEND_THEME_TOGGLER +export DJANGO_HEADER_TEMPLATE +export DJANGO_HOME_PAGE_ADMIN +export DJANGO_HOME_PAGE_MODELS +export DJANGO_HOME_PAGE_TEMPLATE +export DJANGO_HOME_PAGE_URLS +export DJANGO_HOME_PAGE_VIEWS +export DJANGO_LOGGING_DEMO_ADMIN +export DJANGO_LOGGING_DEMO_MODELS +export DJANGO_LOGGING_DEMO_SETTINGS +export DJANGO_LOGGING_DEMO_URLS +export DJANGO_LOGGING_DEMO_VIEWS +export DJANGO_MANAGE_PY +export DJANGO_MODEL_FORM_DEMO_ADMIN +export DJANGO_MODEL_FORM_DEMO_FORMS +export DJANGO_MODEL_FORM_DEMO_MODEL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +export DJANGO_MODEL_FORM_DEMO_URLS +export DJANGO_MODEL_FORM_DEMO_VIEWS +export DJANGO_PAYMENTS_ADMIN +export DJANGO_PAYMENTS_FORM +export DJANGO_PAYMENTS_MIGRATION_0002 +export DJANGO_PAYMENTS_MIGRATION_0003 +export DJANGO_PAYMENTS_MODELS +export DJANGO_PAYMENTS_TEMPLATE_CANCEL +export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +export DJANGO_PAYMENTS_TEMPLATE_SUCCESS +export DJANGO_PAYMENTS_URLS +export DJANGO_PAYMENTS_VIEW +export DJANGO_SEARCH_FORMS +export DJANGO_SEARCH_SETTINGS +export DJANGO_SEARCH_TEMPLATE +export DJANGO_SEARCH_URLS +export DJANGO_SEARCH_UTILS +export DJANGO_SEARCH_VIEWS +export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +export DJANGO_SETTINGS_BASE +export DJANGO_SETTINGS_BASE_MINIMAL +export DJANGO_SETTINGS_CRISPY_FORMS +export DJANGO_SETTINGS_DATABASE +export DJANGO_SETTINGS_DEV +export DJANGO_SETTINGS_HOME_PAGE +export DJANGO_SETTINGS_INSTALLED_APPS +export DJANGO_SETTINGS_MIDDLEWARE +export DJANGO_SETTINGS_MODEL_FORM_DEMO +export DJANGO_SETTINGS_PAYMENTS +export DJANGO_SETTINGS_PROD +export DJANGO_SETTINGS_REST_FRAMEWORK +export DJANGO_SETTINGS_SITEUSER +export DJANGO_SETTINGS_THEMES +export DJANGO_SITEUSER_ADMIN +export DJANGO_SITEUSER_EDIT_TEMPLATE +export DJANGO_SITEUSER_FORM +export DJANGO_SITEUSER_MODEL +export DJANGO_SITEUSER_URLS +export DJANGO_SITEUSER_VIEW +export DJANGO_SITEUSER_VIEW_TEMPLATE +export DJANGO_URLS +export DJANGO_URLS_ALLAUTH +export DJANGO_URLS_API +export DJANGO_URLS_DEBUG_TOOLBAR +export DJANGO_URLS_HOME_PAGE +export DJANGO_URLS_LOGGING_DEMO +export DJANGO_URLS_MODEL_FORM_DEMO +export DJANGO_URLS_SITEUSER +export DJANGO_UTILS +export EB_CUSTOM_ENV_EC2_USER +export EB_CUSTOM_ENV_VAR_FILE +export GIT_IGNORE +export JENKINS_FILE +export MAKEFILE_CUSTOM +export PIP_INSTALL_REQUIREMENTS_TEST +export PROGRAMMING_INTERVIEW +export PYTHON_CI_YAML +export PYTHON_LICENSE_TXT +export PYTHON_PROJECT_TOML +export SEPARATOR +export TINYMCE_JS +export WAGTAIL_BASE_TEMPLATE +export WAGTAIL_BLOCK_CAROUSEL +export WAGTAIL_BLOCK_MARKETING +export WAGTAIL_CONTACT_PAGE_LANDING +export WAGTAIL_CONTACT_PAGE_MODEL +export WAGTAIL_CONTACT_PAGE_TEMPLATE +export WAGTAIL_CONTACT_PAGE_TEST +export WAGTAIL_HOME_PAGE_MODEL +export WAGTAIL_HOME_PAGE_TEMPLATE +export WAGTAIL_HOME_PAGE_URLS +export WAGTAIL_HOME_PAGE_VIEWS +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_TEMPLATE +export WAGTAIL_SEARCH_TEMPLATE +export WAGTAIL_SEARCH_URLS +export WAGTAIL_SETTINGS +export WAGTAIL_SITEPAGE_MODEL +export WAGTAIL_SITEPAGE_TEMPLATE +export WAGTAIL_URLS +export WAGTAIL_URLS_HOME +export WEBPACK_CONFIG_JS +export WEBPACK_INDEX_HTML +export WEBPACK_INDEX_JS +export WEBPACK_REVEAL_CONFIG_JS +export WEBPACK_REVEAL_INDEX_HTML +export WEBPACK_REVEAL_INDEX_JS + +# ------------------------------------------------------------------------------ +# Multi-line phony target rules +# ------------------------------------------------------------------------------ + +.PHONY: aws-check-env-profile-default +aws-check-env-profile-default: +ifndef AWS_PROFILE + $(error AWS_PROFILE is undefined) +endif + +.PHONY: aws-check-env-region-default +aws-check-env-region-default: +ifndef AWS_REGION + $(error AWS_REGION is undefined) +endif + +.PHONY: aws-secret-default +aws-secret-default: aws-check-env + @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String + +.PHONY: aws-sg-default +aws-sg-default: aws-check-env + aws ec2 describe-security-groups $(AWS_OPTS) + +.PHONY: aws-ssm-default +aws-ssm-default: aws-check-env + aws ssm describe-parameters $(AWS_OPTS) + @echo "Get parameter values with: aws ssm getparameter --name ." + +.PHONY: aws-subnet-default +aws-subnet-default: aws-check-env + aws ec2 describe-subnets $(AWS_OPTS) + +.PHONY: aws-vol-available-default +aws-vol-available-default: aws-check-env + aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table + +.PHONY: aws-vol-default +aws-vol-default: aws-check-env + aws ec2 describe-volumes --output table + +.PHONY: aws-vpc-default +aws-vpc-default: aws-check-env + aws ec2 describe-vpcs $(AWS_OPTS) + +.PHONY: db-import-default +db-import-default: + @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql + +.PHONY: db-init-default +db-init-default: + -dropdb $(PROJECT_NAME) + -createdb $(PROJECT_NAME) + +.PHONY: db-init-mysql-default +db-init-mysql-default: + -mysqladmin -u root drop $(PROJECT_NAME) + -mysqladmin -u root create $(PROJECT_NAME) + +.PHONY: db-init-test-default +db-init-test-default: + -dropdb test_$(PROJECT_NAME) + -createdb test_$(PROJECT_NAME) + +.PHONY: django-allauth-default +django-allauth-default: + $(ADD_DIR) backend/templates/allauth/layouts + @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html + @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) backend/templates/allauth/layouts/base.html + +.PHONY: django-app-tests-default +django-app-tests-default: + @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py + +.PHONY: django-base-template-default +django-base-template-default: + @$(ADD_DIR) backend/templates + @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html + -$(GIT_ADD) backend/templates/base.html + +.PHONY: django-custom-admin-default +django-custom-admin-default: + @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) + @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) + -$(GIT_ADD) backend/*.py + +.PHONY: django-db-shell-default +django-db-shell-default: + python manage.py dbshell + +.PHONY: django-dockerfile-default +django-dockerfile-default: + @echo "$$DJANGO_DOCKERFILE" > Dockerfile + -$(GIT_ADD) Dockerfile + @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml + -$(GIT_ADD) docker-compose.yml + +.PHONY: django-favicon-default +django-favicon-default: + @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html + -$(GIT_ADD) backend/templates/favicon.html + +.PHONY: django-footer-template-default +django-footer-template-default: + @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html + -$(GIT_ADD) backend/templates/footer.html + +.PHONY: django-frontend-default +django-frontend-default: python-webpack-init + $(ADD_DIR) frontend/src/context + $(ADD_DIR) frontend/src/images + $(ADD_DIR) frontend/src/utils + @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js + @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js + @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc + @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js + @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js + @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js + @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js + @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js + @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js + @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc + @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js + @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss + @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss + @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js + # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js + @$(MAKE) npm-install-django + @$(MAKE) npm-install-django-dev + -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) + +.PHONY: django-graph-default +django-graph-default: + python manage.py graph_models -a -o $(PROJECT_NAME).png + +.PHONY: django-header-template-default +django-header-template-default: + @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html + -$(GIT_ADD) backend/templates/header.html + +.PHONY: django-home-default +django-home-default: + python manage.py startapp home + $(ADD_DIR) home/templates + @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py + @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py + @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html + @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py + @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py + @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="home"; $(MAKE) django-app-tests + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + -$(GIT_ADD) home/migrations/*.py + +.PHONY: django-init-default +django-init-default: separator \ + db-init \ + django-install \ + django-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-settings-directory \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + django-siteuser \ + django-home \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-init-minimal-default +django-init-minimal-default: separator \ + db-init \ + django-install-minimal \ + django-project \ + django-settings-directory \ + django-settings-base-minimal \ + django-settings-dev \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-favicon \ + django-settings-prod \ + django-home \ + django-utils \ + django-frontend \ + django-migrate \ + git-ignore \ + django-su + +.PHONY: django-init-wagtail-default +django-init-wagtail-default: separator \ + db-init \ + django-install \ + wagtail-install \ + wagtail-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + wagtail-header-prefix-template \ + django-header-template \ + wagtail-base-template \ + django-footer-template \ + django-manage-py \ + wagtail-home \ + wagtail-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + wagtail-search \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + wagtail-settings \ + django-siteuser \ + django-model-form-demo \ + django-logging-demo \ + django-payments-demo-default \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + wagtail-urls-home \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-install-default +django-install-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + Faker \ + boto3 \ + crispy-bootstrap5 \ + djangorestframework \ + django-allauth \ + django-after-response \ + django-ckeditor \ + django-colorful \ + django-cors-headers \ + django-countries \ + django-crispy-forms \ + django-debug-toolbar \ + django-extensions \ + django-hijack \ + django-honeypot \ + django-imagekit \ + django-import-export \ + django-ipware \ + django-multiselectfield \ + django-ninja \ + django-phonenumber-field \ + django-recurrence \ + django-recaptcha \ + django-registration \ + django-richtextfield \ + django-sendgrid-v5 \ + django-social-share \ + django-sql-explorer \ + django-storages \ + django-tables2 \ + django-timezone-field \ + django-widget-tweaks \ + dj-database-url \ + dj-rest-auth \ + dj-stripe \ + docutils \ + enmerkar \ + gunicorn \ + html2docx \ + icalendar \ + mailchimp-marketing \ + mailchimp-transactional \ + phonenumbers \ + pipdeptree \ + psycopg2-binary \ + pydotplus \ + python-webpack-boilerplate \ + python-docx \ + reportlab \ + texttable + +.PHONY: django-install-minimal-default +django-install-minimal-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + dj-database-url \ + django-debug-toolbar \ + python-webpack-boilerplate + +.PHONY: django-lint-default +django-lint-default: + -ruff format -v + -djlint --reformat --format-css --format-js . + -ruff check -v --fix + +.PHONY: django-loaddata-default +django-loaddata-default: + python manage.py loaddata + +.PHONY: django-logging-demo-default +django-logging-demo-default: + python manage.py startapp logging_demo + @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py + @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py + @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py + @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py + @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="logging_demo"; $(MAKE) django-app-tests + -$(GIT_ADD) logging_demo/*.py + -$(GIT_ADD) logging_demo/migrations/*.py + +.PHONY: django-manage-py-default +django-manage-py-default: + @echo "$$DJANGO_MANAGE_PY" > manage.py + -$(GIT_ADD) manage.py + +.PHONY: django-migrate-default +django-migrate-default: + python manage.py migrate + +.PHONY: django-migrations-make-default +django-migrations-make-default: + python manage.py makemigrations + +.PHONY: django-migrations-show-default +django-migrations-show-default: + python manage.py showmigrations + +.PHONY: django-model-form-demo-default +django-model-form-demo-default: + python manage.py startapp model_form_demo + @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py + @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py + @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py + @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py + @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py + $(ADD_DIR) model_form_demo/templates + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html + @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="model_form_demo"; $(MAKE) django-app-tests + python manage.py makemigrations + -$(GIT_ADD) model_form_demo/*.py + -$(GIT_ADD) model_form_demo/templates + -$(GIT_ADD) model_form_demo/migrations + +.PHONY: django-offcanvas-template-default +django-offcanvas-template-default: + -$(ADD_DIR) backend/templates + @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html + -$(GIT_ADD) backend/templates/offcanvas.html + +.PHONY: django-open-default +django-open-default: +ifeq ($(UNAME), Linux) + @echo "Opening on Linux." + xdg-open http://0.0.0.0:8000 +else ifeq ($(UNAME), Darwin) + @echo "Opening on macOS (Darwin)." + open http://0.0.0.0:8000 +else + @echo "Unable to open on: $(UNAME)" +endif + +.PHONY: django-payments-demo-default +django-payments-demo-default: + python manage.py startapp payments + @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py + @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py + @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py + @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py + @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py + $(ADD_DIR) payments/templates/payments + $(ADD_DIR) payments/management/commands + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html + @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) + export APP_DIR="payments"; $(MAKE) django-app-tests + python manage.py makemigrations payments + @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py + @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py + -$(GIT_ADD) payments/ + +.PHONY: django-project-default +django-project-default: + django-admin startproject backend . + -$(GIT_ADD) backend + +.PHONY: django-rest-serializers-default +django-rest-serializers-default: + @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py + -$(GIT_ADD) backend/serializers.py + +.PHONY: django-rest-views-default +django-rest-views-default: + @echo "$$DJANGO_API_VIEWS" > backend/api.py + -$(GIT_ADD) backend/api.py + +.PHONY: django-search-default +django-search-default: + python manage.py startapp search + $(ADD_DIR) search/templates + @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html + @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py + @echo "$$DJANGO_SEARCH_URLS" > search/urls.py + @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py + @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py + @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: django-secret-key-default +django-secret-key-default: + @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" + +.PHONY: django-serve-default +django-serve-default: + npm run watch & + python manage.py runserver 0.0.0.0:8000 + +.PHONY: django-settings-base-default +django-settings-base-default: + @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-base-minimal-default +django-settings-base-minimal-default: + @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-dev-default +django-settings-dev-default: + @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) + @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py + -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) + +.PHONY: django-settings-directory-default +django-settings-directory-default: + @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) + @$(COPY_FILE) backend/settings.py backend/settings/base.py + @$(DEL_FILE) backend/settings.py + -$(GIT_ADD) backend/settings/*.py + +.PHONY: django-settings-prod-default +django-settings-prod-default: + @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) + -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) + +.PHONY: django-shell-default +django-shell-default: + python manage.py shell + +.PHONY: django-siteuser-default +django-siteuser-default: + python manage.py startapp siteuser + $(ADD_DIR) siteuser/templates/ + @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py + @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py + @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py + @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py + @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py + @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html + @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html + @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html + @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="siteuser"; $(MAKE) django-app-tests + -$(GIT_ADD) siteuser/templates + -$(GIT_ADD) siteuser/*.py + python manage.py makemigrations siteuser + -$(GIT_ADD) siteuser/migrations/*.py + +.PHONY: django-static-default +django-static-default: + python manage.py collectstatic --noinput + +.PHONY: django-su-default +django-su-default: + DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) + +.PHONY: django-test-default +django-test-default: npm-install django-static + -$(MAKE) pip-install-test + python manage.py test + +.PHONY: django-urls-api-default +django-urls-api-default: + @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-debug-toolbar-default +django-urls-debug-toolbar-default: + @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) + +.PHONY: django-urls-default +django-urls-default: + @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-show-default +django-urls-show-default: + python manage.py show_urls + +.PHONY: django-user-default +django-user-default: + python manage.py shell -c "from django.contrib.auth.models import User; \ + User.objects.create_user('user', '', 'user')" + +.PHONY: django-utils-default +django-utils-default: + @echo "$$DJANGO_UTILS" > backend/utils.py + -$(GIT_ADD) backend/utils.py + +.PHONY: docker-build-default +docker-build-default: + podman build -t $(PROJECT_NAME) . + +.PHONY: docker-compose-default +docker-compose-default: + podman compose up + +.PHONY: docker-list-default +docker-list-default: + podman container list --all + podman images --all + +.PHONY: docker-run-default +docker-run-default: + podman run $(PROJECT_NAME) + +.PHONY: docker-serve-default +docker-serve-default: + podman run -p 8000:8000 $(PROJECT_NAME) + +.PHONY: docker-shell-default +docker-shell-default: + podman run -it $(PROJECT_NAME) /bin/bash + +.PHONY: eb-check-env-default +eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 +ifndef EB_SSH_KEY + $(error EB_SSH_KEY is undefined) +endif +ifndef VPC_ID + $(error VPC_ID is undefined) +endif +ifndef VPC_SG + $(error VPC_SG is undefined) +endif +ifndef VPC_SUBNET_EC2 + $(error VPC_SUBNET_EC2 is undefined) +endif +ifndef VPC_SUBNET_ELB + $(error VPC_SUBNET_ELB is undefined) +endif + +.PHONY: eb-create-default +eb-create-default: aws-check-env eb-check-env + eb create $(EB_ENV_NAME) \ + -im $(EC2_INSTANCE_MIN) \ + -ix $(EC2_INSTANCE_MAX) \ + -ip $(EC2_INSTANCE_PROFILE) \ + -i $(EC2_INSTANCE_TYPE) \ + -k $(EB_SSH_KEY) \ + -p $(EB_PLATFORM) \ + --elb-type $(EC2_LB_TYPE) \ + --vpc \ + --vpc.id $(VPC_ID) \ + --vpc.elbpublic \ + --vpc.publicip \ + --vpc.ec2subnets $(VPC_SUBNET_EC2) \ + --vpc.elbsubnets $(VPC_SUBNET_ELB) \ + --vpc.securitygroups $(VPC_SG) + +.PHONY: eb-custom-env-default +eb-custom-env-default: + $(ADD_DIR) .ebextensions + @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config + -$(GIT_ADD) .ebextensions/bash.config + $(ADD_DIR) .platform/hooks/postdeploy + @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh + -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh + +.PHONY: eb-deploy-default +eb-deploy-default: + eb deploy + +.PHONY: eb-export-default +eb-export-default: + @if [ ! -d $(EB_DIR_NAME) ]; then \ + echo "Directory $(EB_DIR_NAME) does not exist"; \ + else \ + echo "Directory $(EB_DIR_NAME) does exist!"; \ + eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ + echo "Wrote $(DJANGO_DB_NAME).sql"; \ + fi + +.PHONY: eb-restart-default +eb-restart-default: + eb ssh -c "systemctl restart web" + +.PHONY: eb-rebuild-default +eb-rebuild-default: + aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) + +.PHONY: eb-upgrade-default +eb-upgrade-default: + eb upgrade + +.PHONY: eb-init-default +eb-init-default: aws-check-env-profile + eb init --profile=$(AWS_PROFILE) + +.PHONY: eb-list-default +eb-list-platforms-default: + aws elasticbeanstalk list-platform-versions + +.PHONY: eb-list-databases-default +eb-list-databases-default: + @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" + +.PHONY: eb-logs-default +eb-logs-default: + eb logs + +.PHONY: eb-print-env-default +eb-print-env-default: + eb printenv + +.PHONY: favicon-default +favicon-init-default: + dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png + convert favicon.png favicon.ico + -$(GIT_ADD) favicon.ico + $(DEL_FILE) favicon.png + +.PHONY: git-ignore-default +git-ignore-default: + @echo "$$GIT_IGNORE" > .gitignore + -$(GIT_ADD) .gitignore + +.PHONY: git-branches-default +git-branches-default: + -for i in $(GIT_BRANCHES) ; do \ + -@$(GIT_CHECKOUT) -t $$i ; done + +.PHONY: git-commit-message-clean-default +git-commit-message-clean-default: + -@$(GIT_COMMIT) -a -m "Clean" + +.PHONY: git-commit-message-default +git-commit-message-default: + -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) + +.PHONY: git-commit-message-empty-default +git-commit-message-empty-default: + -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" + +.PHONY: git-commit-message-init-default +git-commit-message-init-default: + -@$(GIT_COMMIT) -a -m "Init" + +.PHONY: git-commit-message-last-default +git-commit-message-last-default: + git log -1 --pretty=%B > $(TMPDIR)/commit.txt + -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt + +.PHONY: git-commit-message-lint-default +git-commit-message-lint-default: + -@$(GIT_COMMIT) -a -m "Lint" + +.PHONY: git-commit-message-mk-default +git-commit-message-mk-default: + -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" + +.PHONY: git-commit-message-rename-default +git-commit-message-rename-default: + -@$(GIT_COMMIT) -a -m "Rename" + +.PHONY: git-commit-message-sort-default +git-commit-message-sort-default: + -@$(GIT_COMMIT) -a -m "Sort" + +.PHONY: git-push-default +git-push-default: + -@$(GIT_PUSH) + +.PHONY: git-push-force-default +git-push-force-default: + -@$(GIT_PUSH_FORCE) + +.PHONY: git-commit-edit-default +git-commit-edit-default: + -$(GIT_COMMIT) -a + +.PHONY: git-prune-default +git-prune-default: + git remote update origin --prune + +.PHONY: git-set-upstream-default +git-set-upstream-default: + git push --set-upstream origin main + +.PHONY: git-set-default-default +git-set-default-default: + gh repo set-default + +.PHONY: git-short-default +git-short-default: + @echo $(GIT_REV) + +.PHONY: help-default +help-default: + @echo "Project Makefile 🤷" + @echo "Usage: make [options] [target] ..." + @echo "Examples:" + @echo " make help Print this message" + @echo " make list-defines list all defines in the Makefile" + @echo " make list-commands list all targets in the Makefile" + +.PHONY: jenkins-init-default +jenkins-init-default: + @echo "$$JENKINS_FILE" > Jenkinsfile + +.PHONY: makefile-list-commands-default +makefile-list-commands-default: + @for makefile in $(MAKEFILE_LIST); do \ + echo "Commands from $$makefile:"; \ + $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ + awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ + if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ + egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ + tr ' ' '\n' | \ + sort | \ + awk '{print $$0}' ; \ + echo; \ + done | $(PAGER) + +.PHONY: makefile-list-defines-default +makefile-list-defines-default: + @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile + +.PHONY: makefile-list-exports-default +makefile-list-exports-default: + @grep '^export [A-Z][A-Z_]*' Makefile + +.PHONY: makefile-list-targets-default +makefile-list-targets-default: + @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY + +.PHONY: make-default +make-default: + -$(GIT_ADD) Makefile + -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" + -git push + +.PHONY: npm-init-default +npm-init-default: + npm init -y + -$(GIT_ADD) package.json + -$(GIT_ADD) package-lock.json + +.PHONY: npm-build-default +npm-build-default: + npm run build + +.PHONY: npm-install-default +npm-install-default: + npm install + -$(GIT_ADD) package-lock.json + +.PHONY: npm-install-django-default +npm-install-django-default: + npm install \ + @fortawesome/fontawesome-free \ + @fortawesome/fontawesome-svg-core \ + @fortawesome/free-brands-svg-icons \ + @fortawesome/free-solid-svg-icons \ + @fortawesome/react-fontawesome \ + bootstrap \ + camelize \ + date-fns \ + history \ + mapbox-gl \ + query-string \ + react-animate-height \ + react-chartjs-2 \ + react-copy-to-clipboard \ + react-date-range \ + react-dom \ + react-dropzone \ + react-hook-form \ + react-image-crop \ + react-map-gl \ + react-modal \ + react-resize-detector \ + react-select \ + react-swipeable \ + snakeize \ + striptags \ + url-join \ + viewport-mercator-project + +.PHONY: npm-install-django-dev-default +npm-install-django-dev-default: + npm install \ + eslint-plugin-react \ + eslint-config-standard \ + eslint-config-standard-jsx \ + @babel/core \ + @babel/preset-env \ + @babel/preset-react \ + --save-dev + +.PHONY: npm-serve-default +npm-serve-default: + npm run start + +.PHONY: npm-test-default +npm-test-default: + npm run test + +.PHONY: pip-deps-default +pip-deps-default: + $(PIP_ENSURE) + python -m pip install pipdeptree + python -m pipdeptree + pipdeptree + +.PHONY: pip-freeze-default +pip-freeze-default: + $(PIP_ENSURE) + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-default +pip-init-default: + touch requirements.txt + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-test-default +pip-init-test-default: + @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt + -$(GIT_ADD) requirements-test.txt + +.PHONY: pip-install-default +pip-install-default: + $(PIP_ENSURE) + $(MAKE) pip-upgrade + python -m pip install wheel + python -m pip install -r requirements.txt + +.PHONY: pip-install-dev-default +pip-install-dev-default: + $(PIP_ENSURE) + python -m pip install -r requirements-dev.txt + +.PHONY: pip-install-test-default +pip-install-test-default: + $(PIP_ENSURE) + python -m pip install -r requirements-test.txt + +.PHONY: pip-install-upgrade-default +pip-install-upgrade-default: + cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + $(PIP_ENSURE) + python -m pip install -U -r requirements.txt + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + +.PHONY: pip-upgrade-default +pip-upgrade-default: + $(PIP_ENSURE) + python -m pip install -U pip + +.PHONY: pip-uninstall-default +pip-uninstall-default: + $(PIP_ENSURE) + python -m pip freeze | xargs python -m pip uninstall -y + +.PHONY: plone-clean-default +plone-clean-default: + $(DEL_DIR) $(PROJECT_NAME) + $(DEL_DIR) $(PACKAGE_NAME) + +.PHONY: plone-init-default +plone-init-default: git-ignore plone-install plone-instance plone-serve + +.PHONY: plone-install-default +plone-install-default: + $(PIP_ENSURE) + python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) + +.PHONY: plone-instance-default +plone-instance-default: + mkwsgiinstance -d backend -u admin:admin + cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini + mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini + -$(GIT_ADD) backend/etc/site.zcml + -$(GIT_ADD) backend/etc/zope.conf + -$(GIT_ADD) backend/etc/zope.ini + +.PHONY: plone-serve-default +plone-serve-default: + runwsgi backend/etc/zope.ini + +.PHONY: plone-build-default +plone-build-default: + buildout + +.PHONY: programming-interview-default +programming-interview-default: + @echo "$$PROGRAMMING_INTERVIEW" > interview.py + @echo "Created interview.py!" + -@$(GIT_ADD) interview.py > /dev/null 2>&1 + +# .NOT_PHONY! +$(MAKEFILE_CUSTOM_FILE): + @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) + -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) + +.PHONY: python-license-default +python-license-default: + @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt + -$(GIT_ADD) LICENSE.txt + +.PHONY: python-project-default +python-project-default: + @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml + -$(GIT_ADD) pyproject.toml + +.PHONY: python-serve-default +python-serve-default: + @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" + python3 -m http.server + +.PHONY: python-sdist-default +python-sdist-default: + $(PIP_ENSURE) + python setup.py sdist --format=zip + +.PHONY: python-webpack-init-default +python-webpack-init-default: + python manage.py webpack_init --no-input + +.PHONY: python-ci-default +python-ci-default: + $(ADD_DIR) .github/workflows + @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml + -$(GIT_ADD) .github/workflows/build_wheels.yml + +.PHONY: rand-default +rand-default: + @openssl rand -base64 12 | sed 's/\///g' + +.PHONY: readme-init-default +readme-init-default: + @echo "# $(PROJECT_NAME)" > README.md + -$(GIT_ADD) README.md + +.PHONY: readme-edit-default +readme-edit-default: + $(EDITOR) README.md + +.PHONY: reveal-init-default +reveal-init-default: webpack-init-reveal + npm install \ + css-loader \ + mini-css-extract-plugin \ + reveal.js \ + style-loader + jq '.scripts += {"build": "webpack"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + +.PHONY: reveal-serve-default +reveal-serve-default: + npm run watch & + python -m http.server + +.PHONY: review-default +review-default: +ifeq ($(UNAME), Darwin) + $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` +else + @echo "Unsupported" +endif + +.PHONY: separator-default +separator-default: + @echo "$$SEPARATOR" + +.PHONY: sphinx-init-default +sphinx-init-default: sphinx-install + sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) + $(COPY_DIR) $(RANDIR)/* . + $(DEL_DIR) $(RANDIR) + -$(GIT_ADD) index.rst + -$(GIT_ADD) conf.py + $(DEL_FILE) make.bat + -@$(GIT_CHECKOUT) Makefile + $(MAKE) git-ignore + +.PHONY: sphinx-theme-init-default +sphinx-theme-init-default: + export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static + +.PHONY: sphinx-install-default +sphinx-install-default: + echo "Sphinx\n" > requirements.txt + @$(MAKE) pip-install + @$(MAKE) pip-freeze + -$(GIT_ADD) requirements.txt + +.PHONY: sphinx-build-default +sphinx-build-default: + sphinx-build -b html -d _build/doctrees . _build/html + sphinx-build -b rinoh . _build/rinoh + +.PHONY: sphinx-serve-default +sphinx-serve-default: + cd _build/html;python3 -m http.server + +.PHONY: wagtail-base-template-default +wagtail-base-template-default: + @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html + +.PHONY: wagtail-clean-default +wagtail-clean-default: + -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ + echo "Cleaning $$dir"; \ + $(DEL_DIR) $$dir >/dev/null 2>&1; \ + done + -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ + echo "Cleaning $$file"; \ + $(DEL_FILE) $$file >/dev/null 2>&1; \ + done + +.PHONY: wagtail-contactpage-default +wagtail-contactpage-default: + python manage.py startapp contactpage + @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py + @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py + $(ADD_DIR) contactpage/templates/contactpage/ + @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html + @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html + @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations contactpage + -$(GIT_ADD) contactpage/templates + -$(GIT_ADD) contactpage/*.py + -$(GIT_ADD) contactpage/migrations/*.py + +.PHONY: wagtail-header-prefix-template-default +wagtail-header-prefix-template-default: + @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html + +.PHONY: wagtail-home-default +wagtail-home-default: + @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py + @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html + $(ADD_DIR) home/templates/blocks + @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html + @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + python manage.py makemigrations home + -$(GIT_ADD) home/migrations/*.py + +.PHONY: wagtail-install-default +wagtail-install-default: + $(PIP_ENSURE) + python -m pip install \ + wagtail \ + wagtailmenus \ + wagtail-color-panel \ + wagtail-django-recaptcha \ + wagtail-markdown \ + wagtail-modeladmin \ + wagtail-seo \ + weasyprint \ + whitenoise \ + xhtml2pdf + +.PHONY: wagtail-private-default +wagtail-privacy-default: + python manage.py startapp privacy + @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py + $(ADD_DIR) privacy/templates + @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html + @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations privacy + -$(GIT_ADD) privacy/templates + -$(GIT_ADD) privacy/*.py + -$(GIT_ADD) privacy/migrations/*.py + +.PHONY: wagtail-project-default +wagtail-project-default: + wagtail start backend . + $(DEL_FILE) home/templates/home/welcome_page.html + -$(GIT_ADD) backend/ + -$(GIT_ADD) .dockerignore + -$(GIT_ADD) Dockerfile + -$(GIT_ADD) manage.py + -$(GIT_ADD) requirements.txt + +.PHONY: wagtail-search-default +wagtail-search-default: + @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html + @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: wagtail-settings-default +wagtail-settings-default: + @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: wagtail-sitepage-default +wagtail-sitepage-default: + python manage.py startapp sitepage + @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py + $(ADD_DIR) sitepage/templates/sitepage/ + @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html + @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations sitepage + -$(GIT_ADD) sitepage/templates + -$(GIT_ADD) sitepage/*.py + -$(GIT_ADD) sitepage/migrations/*.py + +.PHONY: wagtail-urls-default +wagtail-urls-default: + @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) + +.PHONY: wagtail-urls-home-default +wagtail-urls-home-default: + @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) + +.PHONY: webpack-init-default +webpack-init-default: npm-init + @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +.PHONY: webpack-init-reveal-default +webpack-init-reveal-default: npm-init + @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +# -------------------------------------------------------------------------------- +# Single-line phony target rules +# -------------------------------------------------------------------------------- + +.PHONY: aws-check-env-default +aws-check-env-default: aws-check-env-profile aws-check-env-region + +.PHONY: ce-default +ce-default: git-commit-edit git-push + +.PHONY: clean-default +clean-default: wagtail-clean + +.PHONY: cp-default +cp-default: git-commit-message git-push + +.PHONY: db-dump-default +db-dump-default: eb-export + +.PHONY: dbshell-default +dbshell-default: django-db-shell + +.PHONY: deploy-default +deploy-default: eb-deploy + +.PHONY: d-default +d-default: eb-deploy + +.PHONY: deps-default +deps-default: pip-deps + +.PHONY: e-default +e-default: edit + +.PHONY: edit-default +edit-default: readme-edit + +.PHONY: empty-default +empty-default: git-commit-message-empty git-push + +.PHONY: fp-default +fp-default: git-push-force + +.PHONY: freeze-default +freeze-default: pip-freeze git-push + +.PHONY: git-commit-default +git-commit-default: git-commit-message git-push + +.PHONY: git-commit-clean-default +git-commit-clean-default: git-commit-message-clean git-push + +.PHONY: git-commit-init-default +git-commit-init-default: git-commit-message-init git-push + +.PHONY: git-commit-lint-default +git-commit-lint-default: git-commit-message-lint git-push + +.PHONY: gitignore-default +gitignore-default: git-ignore + +.PHONY: h-default +h-default: help + +.PHONY: init-default +init-default: django-init-wagtail django-serve + +.PHONY: init-wagtail-default +init-wagtail-default: django-init-wagtail + +.PHONY: install-default +install-default: pip-install + +.PHONY: l-default +l-default: makefile-list-commands + +.PHONY: last-default +last-default: git-commit-message-last git-push + +.PHONY: lint-default +lint-default: django-lint + +.PHONY: list-commands-default +list-commands-default: makefile-list-commands + +.PHONY: list-defines-default +list-defines-default: makefile-list-defines + +.PHONY: list-exports-default +list-exports-default: makefile-list-exports + +.PHONY: list-targets-default +list-targets-default: makefile-list-targets + +.PHONY: migrate-default +migrate-default: django-migrate + +.PHONY: migrations-default +migrations-default: django-migrations-make + +.PHONY: migrations-show-default +migrations-show-default: django-migrations-show + +.PHONY: mk-default +mk-default: project.mk git-commit-message-mk git-push + +.PHONY: open-default +open-default: django-open + +.PHONY: o-default +o-default: django-open + +.PHONY: readme-default +readme-default: readme-init + +.PHONY: rename-default +rename-default: git-commit-message-rename git-push + +.PHONY: s-default +s-default: django-serve + +.PHONY: shell-default +shell-default: django-shell + +.PHONY: serve-default +serve-default: django-serve + +.PHONY: static-default +static-default: django-static + +.PHONY: sort-default +sort-default: git-commit-message-sort git-push + +.PHONY: su-default +su-default: django-su + +.PHONY: test-default +test-default: django-test + +.PHONY: t-default +t-default: django-test + +.PHONY: u-default +u-default: help + +.PHONY: urls-default +urls-default: django-urls-show + +# -------------------------------------------------------------------------------- +# Allow customizing rules defined in this Makefile with rules defined in +# $(MAKEFILE_CUSTOM_FILE) +# -------------------------------------------------------------------------------- + +%: %-default # https://stackoverflow.com/a/49804748 + @ true From 776eb9e9b7b4406767a45523cc833f7fbc20100b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:08:26 -0400 Subject: [PATCH 121/136] Add/update project-makefile files --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell +.PHONY: d-default +d-default: deploy + .PHONY: deploy-default deploy-default: eb-deploy -.PHONY: d-default -d-default: eb-deploy - .PHONY: deps-default deps-default: pip-deps -.PHONY: e-default -e-default: edit - .PHONY: edit-default edit-default: readme-edit +.PHONY: e-default +e-default: edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push -.PHONY: open-default -open-default: django-open - .PHONY: o-default o-default: django-open +.PHONY: open-default +open-default: open + .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell +s-default: serve .PHONY: serve-default serve-default: django-serve +.PHONY: shell-default +shell-default: django-shell + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: django-test +t-default: test .PHONY: u-default u-default: help From 837af6420d40fe772bcd910d9c531c2ca8fe980e Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:43:28 -0400 Subject: [PATCH 122/136] Add/update project-makefile files --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index b5fbff2c9..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -3980,6 +3980,10 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" +.PHONY: git-commit-message-ignore-default +git-commit-message-ignore-default: + -@$(GIT_COMMIT) -a -m "Ignore" + .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4225,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4530,6 +4535,9 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps +.PHONY: dump-default +dump-default: db-dump + .PHONY: edit-default edit-default: readme-edit @@ -4551,6 +4559,9 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push +.PHONY: git-commit-ignore-default +git-commit-ignore-default: git-commit-message-ignore git-push + .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4563,6 +4574,9 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help +.PHONY: ignore-default +ignore-default: git-commit-message-ignore git-push + .PHONY: init-default init-default: django-init-wagtail django-serve From 575fad24726fe25d06ea985ca59eb9126e26a00b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 17:02:53 -0400 Subject: [PATCH 123/136] Add/update project-makefile files --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d8e621c6c..a16a18055 100644 --- a/Makefile +++ b/Makefile @@ -1996,6 +1996,7 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ +backend/inituser backend/var endef @@ -4229,7 +4230,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From 132663a881bfe2bfc5265e7e05de7113f06501a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 07:36:52 +1000 Subject: [PATCH 124/136] Updated error message for invalid width or height --- Tests/test_file_webp.py | 8 ++++++++ src/_webp.c | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6ccd489bb..ad08da364 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -157,6 +157,14 @@ class TestFileWebp: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("L", (16384, 16384)) + with pytest.raises(ValueError) as e: + im.save(temp_file) + assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. diff --git a/src/_webp.c b/src/_webp.c index d1943b3e0..0d2d6f023 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -672,7 +672,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { - PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); + int error_code = (&pic)->error_code; + const char *message = ""; + if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { + message = ": Image size exceeds WebP limit"; + } + PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; } output = writer.mem; From 0f64d08e64e2a1d8bedf280626006a95cbb2b086 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:02 -0400 Subject: [PATCH 125/136] Revert "Add/update project-makefile files" This reverts commit 575fad24726fe25d06ea985ca59eb9126e26a00b. Sorry for the noise (1/4)! --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a16a18055..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -1996,7 +1996,6 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ -backend/inituser backend/var endef @@ -4230,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From e19841afa486c33673d9ff6cb8330761414f91de Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:35 -0400 Subject: [PATCH 126/136] Revert "Add/update project-makefile files" This reverts commit 837af6420d40fe772bcd910d9c531c2ca8fe980e. Sorry for the noise (2/4)! --- Makefile | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Makefile b/Makefile index d8e621c6c..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -3980,10 +3980,6 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" -.PHONY: git-commit-message-ignore-default -git-commit-message-ignore-default: - -@$(GIT_COMMIT) -a -m "Ignore" - .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4229,7 +4225,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4535,9 +4530,6 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps -.PHONY: dump-default -dump-default: db-dump - .PHONY: edit-default edit-default: readme-edit @@ -4559,9 +4551,6 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push -.PHONY: git-commit-ignore-default -git-commit-ignore-default: git-commit-message-ignore git-push - .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4574,9 +4563,6 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help -.PHONY: ignore-default -ignore-default: git-commit-message-ignore git-push - .PHONY: init-default init-default: django-init-wagtail django-serve From 1143bffe1a0a5fe6b7d58dd2c3660f91299cff8d Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:06 -0400 Subject: [PATCH 127/136] Revert "Add/update project-makefile files" This reverts commit 776eb9e9b7b4406767a45523cc833f7fbc20100b. Sorry for the noise (3/4)! --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index b5fbff2c9..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell -.PHONY: d-default -d-default: deploy - .PHONY: deploy-default deploy-default: eb-deploy +.PHONY: d-default +d-default: eb-deploy + .PHONY: deps-default deps-default: pip-deps -.PHONY: edit-default -edit-default: readme-edit - .PHONY: e-default e-default: edit +.PHONY: edit-default +edit-default: readme-edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push +.PHONY: open-default +open-default: django-open + .PHONY: o-default o-default: django-open -.PHONY: open-default -open-default: open - .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: serve - -.PHONY: serve-default -serve-default: django-serve +s-default: django-serve .PHONY: shell-default shell-default: django-shell +.PHONY: serve-default +serve-default: django-serve + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: test +t-default: django-test .PHONY: u-default u-default: help From d6cfebd016db25549d05a9c5caa2ef3b53cff5c5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:29 -0400 Subject: [PATCH 128/136] Revert "Add/update project-makefile files" This reverts commit 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307. Sorry for the noise (4/4)! --- Makefile | 4781 ++---------------------------------------------------- 1 file changed, 125 insertions(+), 4656 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..94f7565d8 100644 --- a/Makefile +++ b/Makefile @@ -1,4656 +1,125 @@ -# Project Makefile -# -# A makefile to automate setup of a Wagtail CMS project and related tasks. -# -# https://github.com/aclark4life/project-makefile -# -# -------------------------------------------------------------------------------- -# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` -# -------------------------------------------------------------------------------- - -.DEFAULT_GOAL := git-commit - -# -------------------------------------------------------------------------------- -# Single line variables to be used by phony target rules -# -------------------------------------------------------------------------------- - -ADD_DIR := mkdir -pv -ADD_FILE := touch -AWS_OPTS := --no-cli-pager --output table -COPY_DIR := cp -rv -COPY_FILE := cp -v -DEL_DIR := rm -rv -DEL_FILE := rm -v -DJANGO_DB_COL = awk -F\= '{print $$2}' -DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" -DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') -DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') -DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') -DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') -DJANGO_BACKEND_APPS_FILE := backend/apps.py -DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py -DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ - package.json postcss.config.js -DJANGO_SETTINGS_DIR = backend/settings -DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py -DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py -DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py -DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) -DJANGO_URLS_FILE = backend/urls.py -EB_DIR_NAME := .elasticbeanstalk -EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) -EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" -EC2_INSTANCE_MAX ?= 1 -EC2_INSTANCE_MIN ?= 1 -EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role -EC2_INSTANCE_TYPE ?= t4g.small -EC2_LB_TYPE ?= application -EDITOR_REVIEW = subl -GIT_ADD := git add -GIT_BRANCH = $(shell git branch --show-current) -GIT_BRANCHES = $(shell git branch -a) -GIT_CHECKOUT = git checkout -GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" -GIT_COMMIT = git commit -GIT_PUSH = git push -GIT_PUSH_FORCE = git push --force-with-lease -GIT_REV = $(shell git rev-parse --short HEAD) -MAKEFILE_CUSTOM_FILE := project.mk -PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') -PAGER ?= less -PIP_ENSURE = python -m ensurepip -PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt -PROJECT_DIRS = backend contactpage home privacy siteuser -PROJECT_EMAIL := aclark@aclark.net -PROJECT_NAME = project-makefile -RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') -TMPDIR := $(shell mktemp -d) -UNAME := $(shell uname) -WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser -WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt - -# -------------------------------------------------------------------------------- -# Include $(MAKEFILE_CUSTOM_FILE) if it exists -# -------------------------------------------------------------------------------- - -ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) - include $(MAKEFILE_CUSTOM_FILE) -endif - -# -------------------------------------------------------------------------------- -# Multi-line variables to be used in phony target rules -# -------------------------------------------------------------------------------- - -define DJANGO_ALLAUTH_BASE_TEMPLATE -{% extends 'base.html' %} -endef - -define DJANGO_API_SERIALIZERS -from rest_framework import serializers -from siteuser.models import User - - -class UserSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = User - fields = ["url", "username", "email", "is_staff"] -endef - -define DJANGO_API_VIEWS -from ninja import NinjaAPI -from rest_framework import viewsets -from siteuser.models import User -from .serializers import UserSerializer - -api = NinjaAPI() - - -@api.get("/hello") -def hello(request): - return "Hello world" - - -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer -endef - -define DJANGO_APP_TESTS -from django.test import TestCase -from django.urls import reverse -from .models import YourModel -from .forms import YourForm - - -class YourModelTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_instance_creation(self): - self.assertIsInstance(self.instance, YourModel) - self.assertEqual(self.instance.field1, "value1") - self.assertEqual(self.instance.field2, "value2") - - def test_str_method(self): - self.assertEqual(str(self.instance), "Expected String Representation") - - -class YourViewTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_view_url_exists_at_desired_location(self): - response = self.client.get("/your-url/") - self.assertEqual(response.status_code, 200) - - def test_view_url_accessible_by_name(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - - def test_view_uses_correct_template(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "your_template.html") - - def test_view_context(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertIn("context_variable", response.context) - - -class YourFormTest(TestCase): - def test_form_valid_data(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - self.assertTrue(form.is_valid()) - - def test_form_invalid_data(self): - form = YourForm(data={"field1": "", "field2": "value2"}) - self.assertFalse(form.is_valid()) - self.assertIn("field1", form.errors) - - def test_form_save(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - if form.is_valid(): - instance = form.save() - self.assertEqual(instance.field1, "value1") - self.assertEqual(instance.field2, "value2") -endef - -define DJANGO_BACKEND_APPS -from django.contrib.admin.apps import AdminConfig - - -class CustomAdminConfig(AdminConfig): - default_site = "backend.admin.CustomAdminSite" -endef - -define DJANGO_BASE_TEMPLATE -{% load static webpack_loader %} - - - - - - {% block title %}{% endblock %} - {% block title_suffix %}{% endblock %} - - - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define DJANGO_CUSTOM_ADMIN -from django.contrib.admin import AdminSite - - -class CustomAdminSite(AdminSite): - site_header = "Project Makefile" - site_title = "Project Makefile" - index_title = "Project Makefile" - - -custom_admin_site = CustomAdminSite(name="custom_admin") -endef - -define DJANGO_DOCKERCOMPOSE -version: '3' - -services: - db: - image: postgres:latest - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: project - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin - - web: - build: . - command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" - volumes: - - .:/app - ports: - - "8000:8000" - depends_on: - - db - environment: - DATABASE_URL: postgres://admin:admin@db:5432/project - -volumes: - postgres_data: -endef - -define DJANGO_DOCKERFILE -FROM amazonlinux:2023 -RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server -USER postgres -RUN initdb -D /var/lib/pgsql/data -USER root -RUN useradd wagtail -EXPOSE 8000 -ENV PYTHONUNBUFFERED=1 PORT=8000 -COPY requirements.txt / -RUN python3.11 -m pip install -r /requirements.txt -WORKDIR /app -RUN chown wagtail:wagtail /app -COPY --chown=wagtail:wagtail . . -USER wagtail -RUN npm-20 install; npm-20 run build -RUN python3.11 manage.py collectstatic --noinput --clear -CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application -endef - -define DJANGO_FAVICON_TEMPLATE -{% load static %} - -endef - -define DJANGO_FOOTER_TEMPLATE -
    -

    © {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

    -
      -
    • - Home -
    • - {% for child in current_site.root_page.get_children %} -
    • - {{ child }} -
    • - {% endfor %} -
    -
    -endef - -define DJANGO_FRONTEND_APP -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import 'bootstrap'; -import '@fortawesome/fontawesome-free/js/fontawesome'; -import '@fortawesome/fontawesome-free/js/solid'; -import '@fortawesome/fontawesome-free/js/regular'; -import '@fortawesome/fontawesome-free/js/brands'; -import getDataComponents from '../dataComponents'; -import UserContextProvider from '../context'; -import * as components from '../components'; -import "../styles/index.scss"; -import "../styles/theme-blue.scss"; -import "./config"; - -const { ErrorBoundary } = components; -const dataComponents = getDataComponents(components); -const container = document.getElementById('app'); -const root = createRoot(container); -const App = () => ( - - - {dataComponents} - - -); -root.render(); -endef - -define DJANGO_FRONTEND_APP_CONFIG -import '../utils/themeToggler.js'; -// import '../utils/tinymce.js'; -endef - -define DJANGO_FRONTEND_BABELRC -{ - "presets": [ - [ - "@babel/preset-react", - ], - [ - "@babel/preset-env", - { - "useBuiltIns": "usage", - "corejs": "3.0.0" - } - ] - ], - "plugins": [ - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-class-properties" - ] -} -endef - -define DJANGO_FRONTEND_COMPONENTS -export { default as ErrorBoundary } from './ErrorBoundary'; -export { default as UserMenu } from './UserMenu'; -endef - -define DJANGO_FRONTEND_COMPONENT_CLOCK -// Via ChatGPT -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import PropTypes from 'prop-types'; - -const Clock = ({ color = '#fff' }) => { - const [date, setDate] = useState(new Date()); - const [blink, setBlink] = useState(true); - const timerID = useRef(); - - const tick = useCallback(() => { - setDate(new Date()); - setBlink(prevBlink => !prevBlink); - }, []); - - useEffect(() => { - timerID.current = setInterval(() => tick(), 1000); - - // Return a cleanup function to be run on component unmount - return () => clearInterval(timerID.current); - }, [tick]); - - const formattedDate = date.toLocaleDateString(undefined, { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - const formattedTime = date.toLocaleTimeString(undefined, { - hour: 'numeric', - minute: 'numeric', - }); - - return ( - <> -
    {formattedDate} {formattedTime}
    - - ); -}; - -Clock.propTypes = { - color: PropTypes.string, -}; - -export default Clock; -endef - -define DJANGO_FRONTEND_COMPONENT_ERROR -import { Component } from 'react'; -import PropTypes from 'prop-types'; - -class ErrorBoundary extends Component { - constructor (props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError () { - return { hasError: true }; - } - - componentDidCatch (error, info) { - const { onError } = this.props; - console.error(error); - onError && onError(error, info); - } - - render () { - const { children = null } = this.props; - const { hasError } = this.state; - - return hasError ? null : children; - } -} - -ErrorBoundary.propTypes = { - onError: PropTypes.func, - children: PropTypes.node, -}; - -export default ErrorBoundary; -endef - -define DJANGO_FRONTEND_COMPONENT_USER_MENU -// UserMenu.js -import React from 'react'; -import PropTypes from 'prop-types'; - -function handleLogout() { - window.location.href = '/accounts/logout'; -} - -const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { - return ( -
    - {isAuthenticated ? ( -
  5. - - -
  6. - ) : ( -
  7. - -
  8. - )} -
    - ); -}; - -UserMenu.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - isSuperuser: PropTypes.bool.isRequired, - textColor: PropTypes.string, -}; - -export default UserMenu; -endef - -define DJANGO_FRONTEND_CONTEXT_INDEX -export { UserContextProvider as default } from './UserContextProvider'; -endef - -define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -// UserContextProvider.js -import React, { createContext, useContext, useState } from 'react'; -import PropTypes from 'prop-types'; - -const UserContext = createContext(); - -export const UserContextProvider = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const login = () => { - try { - // Add logic to handle login, set isAuthenticated to true - setIsAuthenticated(true); - } catch (error) { - console.error('Login error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - const logout = () => { - try { - // Add logic to handle logout, set isAuthenticated to false - setIsAuthenticated(false); - } catch (error) { - console.error('Logout error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - return ( - - {children} - - ); -}; - -UserContextProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export const useUserContext = () => { - const context = useContext(UserContext); - - if (!context) { - throw new Error('useUserContext must be used within a UserContextProvider'); - } - - return context; -}; - -// Add PropTypes for the return value of useUserContext -useUserContext.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - login: PropTypes.func.isRequired, - logout: PropTypes.func.isRequired, -}; -endef - -define DJANGO_FRONTEND_ESLINTRC -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "react" - ], - "rules": { - "no-unused-vars": "off" - }, - settings: { - react: { - version: 'detect', - }, - }, -} -endef - -define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -
    - -
    - -
    -
    -endef - -define DJANGO_FRONTEND_PORTAL -// Via pwellever -import React from 'react'; -import { createPortal } from 'react-dom'; - -const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (value.toLowerCase() === 'null') { - value = null; - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - // Parse numeric value - value = parseFloat(value); - } else if ( - (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') - ) { - // Parse JSON strings - value = JSON.parse(value); - } - - result[key] = value; - return result; -}, {}); - -// This method of using portals instead of calling ReactDOM.render on individual components -// ensures that all components are mounted under a single React tree, and are therefore able -// to share context. - -export default function getPageComponents (components) { - const getPortalComponent = domEl => { - // The element's "data-component" attribute is used to determine which component to render. - // All other "data-*" attributes are passed as props. - const { component: componentName, ...rest } = domEl.dataset; - const Component = components[componentName]; - if (!Component) { - console.error(`Component "$${componentName}" not found.`); - return null; - } - const props = parseProps(rest); - domEl.innerHTML = ''; - - // eslint-disable-next-line no-unused-vars - const { ErrorBoundary } = components; - return createPortal( - - - , - domEl, - ); - }; - - return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); -} -endef - -define DJANGO_FRONTEND_STYLES -// If you comment out code below, bootstrap will use red as primary color -// and btn-primary will become red - -// $primary: red; - -@import "~bootstrap/scss/bootstrap.scss"; - -.jumbotron { - // should be relative path of the entry scss file - background-image: url("../../vendors/images/sample.jpg"); - background-size: cover; -} - -#theme-toggler-authenticated:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} - -#theme-toggler-anonymous:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} -endef - -define DJANGO_FRONTEND_THEME_BLUE -@import "~bootstrap/scss/bootstrap.scss"; - -[data-bs-theme="blue"] { - --bs-body-color: var(--bs-white); - --bs-body-color-rgb: #{to-rgb($$white)}; - --bs-body-bg: var(--bs-blue); - --bs-body-bg-rgb: #{to-rgb($$blue)}; - --bs-tertiary-bg: #{$$blue-600}; - - .dropdown-menu { - --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; - --bs-dropdown-link-active-bg: #{$$blue-700}; - } - - .btn-secondary { - --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; - --bs-btn-border-color: #{rgba($$white, .25)}; - --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; - --bs-btn-hover-border-color: #{rgba($$white, .25)}; - --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; - --bs-btn-active-border-color: #{rgba($$white, .5)}; - --bs-btn-focus-border-color: #{rgba($$white, .5)}; - - // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); - } -} -endef - -define DJANGO_FRONTEND_THEME_TOGGLER -document.addEventListener('DOMContentLoaded', function () { - const rootElement = document.documentElement; - const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); - const authThemeToggle = document.getElementById('theme-toggler-authenticated'); - if (authThemeToggle) { - localStorage.removeItem('data-bs-theme'); - } - const anonSavedTheme = localStorage.getItem('data-bs-theme'); - if (anonSavedTheme) { - rootElement.setAttribute('data-bs-theme', anonSavedTheme); - } - if (anonThemeToggle) { - anonThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - rootElement.setAttribute('data-bs-theme', newTheme); - localStorage.setItem('data-bs-theme', newTheme); - }); - } - if (authThemeToggle) { - const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; - authThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - fetch('/user/update_theme_preference/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers - }, - body: JSON.stringify({ theme: newTheme }), - }) - .then(response => response.json()) - .then(data => { - rootElement.setAttribute('data-bs-theme', newTheme); - }) - .catch(error => { - console.error('Error updating theme preference:', error); - }); - }); - } -}); -endef - -define DJANGO_HEADER_TEMPLATE -
    -
    - -
    -
    -endef - -define DJANGO_HOME_PAGE_ADMIN -from django.contrib import admin # noqa - -# Register your models here. -endef - -define DJANGO_HOME_PAGE_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% block content %} -
    -
    -{% endblock %} -endef - -define DJANGO_HOME_PAGE_URLS -from django.urls import path -from .views import HomeView - -urlpatterns = [path("", HomeView.as_view(), name="home")] -endef - -define DJANGO_HOME_PAGE_VIEWS -from django.views.generic import TemplateView - - -class HomeView(TemplateView): - template_name = "home.html" -endef - -define DJANGO_LOGGING_DEMO_ADMIN -# Register your models here. -endef - -define DJANGO_LOGGING_DEMO_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_LOGGING_DEMO_SETTINGS -INSTALLED_APPS.append("logging_demo") # noqa -endef - -define DJANGO_LOGGING_DEMO_URLS -from django.urls import path -from .views import logging_demo - -urlpatterns = [ - path("", logging_demo, name="logging_demo"), -] -endef - -define DJANGO_LOGGING_DEMO_VIEWS -from django.http import HttpResponse -import logging - -logger = logging.getLogger(__name__) - - -def logging_demo(request): - logger.debug("Hello, world!") - return HttpResponse("Hello, world!") -endef - -define DJANGO_MANAGE_PY -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" - -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() -endef - -define DJANGO_MODEL_FORM_DEMO_ADMIN -from django.contrib import admin -from .models import ModelFormDemo - - -@admin.register(ModelFormDemo) -class ModelFormDemoAdmin(admin.ModelAdmin): - pass -endef - -define DJANGO_MODEL_FORM_DEMO_FORMS -from django import forms -from .models import ModelFormDemo - - -class ModelFormDemoForm(forms.ModelForm): - class Meta: - model = ModelFormDemo - fields = ["name", "email", "age", "is_active"] -endef - -define DJANGO_MODEL_FORM_DEMO_MODEL -from django.db import models -from django.shortcuts import reverse - - -class ModelFormDemo(models.Model): - name = models.CharField(max_length=100, blank=True, null=True) - email = models.EmailField(blank=True, null=True) - age = models.IntegerField(blank=True, null=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.name or f"test-model-{self.pk}" - - def get_absolute_url(self): - return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -{% extends 'base.html' %} -{% block content %} -

    Test Model Detail: {{ model_form_demo.name }}

    -

    Name: {{ model_form_demo.name }}

    -

    Email: {{ model_form_demo.email }}

    -

    Age: {{ model_form_demo.age }}

    -

    Active: {{ model_form_demo.is_active }}

    -

    Created At: {{ model_form_demo.created_at }}

    - Edit Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -{% extends 'base.html' %} -{% block content %} -

    - {% if form.instance.pk %} - Update Test Model - {% else %} - Create Test Model - {% endif %} -

    -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -{% extends 'base.html' %} -{% block content %} -

    Test Models List

    - - Create New Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_URLS -from django.urls import path -from .views import ( - ModelFormDemoListView, - ModelFormDemoCreateView, - ModelFormDemoUpdateView, - ModelFormDemoDetailView, -) - -urlpatterns = [ - path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), - path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), - path( - "/update/", - ModelFormDemoUpdateView.as_view(), - name="model_form_demo_update", - ), - path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), -] -endef - -define DJANGO_MODEL_FORM_DEMO_VIEWS -from django.views.generic import ListView, CreateView, UpdateView, DetailView -from .models import ModelFormDemo -from .forms import ModelFormDemoForm - - -class ModelFormDemoListView(ListView): - model = ModelFormDemo - template_name = "model_form_demo_list.html" - context_object_name = "model_form_demos" - - -class ModelFormDemoCreateView(CreateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - def form_valid(self, form): - form.instance.created_by = self.request.user - return super().form_valid(form) - - -class ModelFormDemoUpdateView(UpdateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - -class ModelFormDemoDetailView(DetailView): - model = ModelFormDemo - template_name = "model_form_demo_detail.html" - context_object_name = "model_form_demo" -endef - -define DJANGO_PAYMENTS_ADMIN -from django.contrib import admin -from .models import Product, Order - -admin.site.register(Product) -admin.site.register(Order) -endef - -define DJANGO_PAYMENTS_FORM -from django import forms - - -class PaymentsForm(forms.Form): - stripeToken = forms.CharField(widget=forms.HiddenInput()) - amount = forms.DecimalField( - max_digits=10, decimal_places=2, widget=forms.HiddenInput() - ) -endef - -define DJANGO_PAYMENTS_MIGRATION_0002 -from django.db import migrations -import os -import secrets -import logging - -logger = logging.getLogger(__name__) - - -def generate_default_key(): - return "sk_test_" + secrets.token_hex(24) - - -def set_stripe_api_keys(apps, schema_editor): - # Get the Stripe API Key model - APIKey = apps.get_model("djstripe", "APIKey") - - # Fetch the keys from environment variables or generate default keys - test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) - live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) - - logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) - logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) - - # Check if the keys are not already in the database - if not APIKey.objects.filter(secret=test_secret_key).exists(): - APIKey.objects.create(secret=test_secret_key, livemode=False) - logger.info("Added test secret key to the database.") - else: - logger.info("Test secret key already exists in the database.") - - if not APIKey.objects.filter(secret=live_secret_key).exists(): - APIKey.objects.create(secret=live_secret_key, livemode=True) - logger.info("Added live secret key to the database.") - else: - logger.info("Live secret key already exists in the database.") - - -class Migration(migrations.Migration): - - dependencies = [ - ("payments", "0001_initial"), - ] - - operations = [ - migrations.RunPython(set_stripe_api_keys), - ] -endef - -define DJANGO_PAYMENTS_MIGRATION_0003 -from django.db import migrations - - -def create_initial_products(apps, schema_editor): - Product = apps.get_model("payments", "Product") - Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) - Product.objects.create(name="Mug", description="A nice mug", price=10.00) - Product.objects.create(name="Hat", description="A stylish hat", price=15.00) - - -class Migration(migrations.Migration): - dependencies = [ - ( - "payments", - "0002_set_stripe_api_keys", - ), - ] - - operations = [ - migrations.RunPython(create_initial_products), - ] -endef - -define DJANGO_PAYMENTS_MODELS -from django.db import models - - -class Product(models.Model): - name = models.CharField(max_length=100) - description = models.TextField() - price = models.DecimalField(max_digits=10, decimal_places=2) - - def __str__(self): - return self.name - - -class Order(models.Model): - product = models.ForeignKey(Product, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - stripe_checkout_session_id = models.CharField(max_length=200) - - def __str__(self): - return f"Order {self.id} for {self.product.name}" -endef - -define DJANGO_PAYMENTS_TEMPLATE_CANCEL -{% extends "base.html" %} -{% block title %}Cancel{% endblock %} -{% block content %} -

    Payment Cancelled

    -

    Your payment was cancelled.

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -{% extends "base.html" %} -{% block title %}Checkout{% endblock %} -{% block content %} -

    Checkout

    -
    - {% csrf_token %} - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -{% extends "base.html" %} -{% block title %}{{ product.name }}{% endblock %} -{% block content %} -

    {{ product.name }}

    -

    {{ product.description }}

    -

    Price: ${{ product.price }}

    -
    - {% csrf_token %} - - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -{% extends "base.html" %} -{% block title %}Products{% endblock %} -{% block content %} -

    Products

    - -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_SUCCESS -{% extends "base.html" %} -{% block title %}Success{% endblock %} -{% block content %} -

    Payment Successful

    -

    Thank you for your purchase!

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_URLS -from django.urls import path -from .views import ( - CheckoutView, - SuccessView, - CancelView, - ProductListView, - ProductDetailView, -) - -urlpatterns = [ - path("", ProductListView.as_view(), name="product_list"), - path("product//", ProductDetailView.as_view(), name="product_detail"), - path("checkout/", CheckoutView.as_view(), name="checkout"), - path("success/", SuccessView.as_view(), name="success"), - path("cancel/", CancelView.as_view(), name="cancel"), -] -endef - -define DJANGO_PAYMENTS_VIEW -from django.conf import settings -from django.shortcuts import render, redirect, get_object_or_404 -from django.views.generic import TemplateView, View, ListView, DetailView -import stripe -from .models import Product, Order - -stripe.api_key = settings.STRIPE_TEST_SECRET_KEY - - -class ProductListView(ListView): - model = Product - template_name = "payments/product_list.html" - context_object_name = "products" - - -class ProductDetailView(DetailView): - model = Product - template_name = "payments/product_detail.html" - context_object_name = "product" - - -class CheckoutView(View): - template_name = "payments/checkout.html" - - def get(self, request, *args, **kwargs): - products = Product.objects.all() - return render(request, self.template_name, {"products": products}) - - def post(self, request, *args, **kwargs): - product_id = request.POST.get("product_id") - product = get_object_or_404(Product, id=product_id) - - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[ - { - "price_data": { - "currency": "usd", - "product_data": { - "name": product.name, - }, - "unit_amount": int(product.price * 100), - }, - "quantity": 1, - } - ], - mode="payment", - success_url="http://localhost:8000/payments/success/", - cancel_url="http://localhost:8000/payments/cancel/", - ) - - Order.objects.create(product=product, stripe_checkout_session_id=session.id) - return redirect(session.url, code=303) - - -class SuccessView(TemplateView): - - template_name = "payments/success.html" - - -class CancelView(TemplateView): - - template_name = "payments/cancel.html" -endef - -define DJANGO_SEARCH_FORMS -from django import forms - - -class SearchForm(forms.Form): - query = forms.CharField(max_length=100, required=True, label="Search") - -endef - -define DJANGO_SEARCH_SETTINGS -SEARCH_MODELS = [ - # Add search models here. -] -endef - -define DJANGO_SEARCH_TEMPLATE -{% extends "base.html" %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define DJANGO_SEARCH_URLS -from django.urls import path -from .views import SearchView - -urlpatterns = [ - path("search/", SearchView.as_view(), name="search"), -] -endef - -define DJANGO_SEARCH_UTILS -from django.apps import apps -from django.conf import settings - -def get_search_models(): - models = [] - for model_path in settings.SEARCH_MODELS: - app_label, model_name = model_path.split(".") - model = apps.get_model(app_label, model_name) - models.append(model) - return models -endef - -define DJANGO_SEARCH_VIEWS -from django.views.generic import ListView -from django.db import models -from django.db.models import Q -from .forms import SearchForm -from .utils import get_search_models - - -class SearchView(ListView): - template_name = "your_app/search_results.html" - context_object_name = "results" - paginate_by = 10 - - def get_queryset(self): - form = SearchForm(self.request.GET) - query = None - results = [] - - if form.is_valid(): - query = form.cleaned_data["query"] - search_models = get_search_models() - - for model in search_models: - fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] - queries = [Q(**{f"{field}__icontains": query}) for field in fields] - model_results = model.objects.filter(queries.pop()) - - for item in queries: - model_results = model_results.filter(item) - - results.extend(model_results) - - return results - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"] = SearchForm(self.request.GET) - context["query"] = self.request.GET.get("query", "") - return context -endef - -define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "allauth.account.auth_backends.AuthenticationBackend", -] -endef - -define DJANGO_SETTINGS_BASE -# $(PROJECT_NAME) -# -# Uncomment next two lines to enable custom admin -# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] -# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') -import os # noqa -import dj_database_url # noqa - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -EXPLORER_CONNECTIONS = {"Default": "default"} -EXPLORER_DEFAULT_CONNECTION = "default" -LOGIN_REDIRECT_URL = "/" -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -endef - -define DJANGO_SETTINGS_BASE_MINIMAL -# $(PROJECT_NAME) -import os # noqa -import dj_database_url # noqa - -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("webpack_boilerplate") -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -endef - -define DJANGO_SETTINGS_CRISPY_FORMS -CRISPY_TEMPLATE_PACK = "bootstrap5" -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -endef - -define DJANGO_SETTINGS_DATABASE -DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") -DATABASES["default"] = dj_database_url.parse(DATABASE_URL) -endef - -define DJANGO_SETTINGS_DEV -from .base import * # noqa - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -# SECURITY WARNING: define the correct hosts in production! -ALLOWED_HOSTS = ["*"] - -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -try: - from .local import * # noqa -except ImportError: - pass - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "loggers": { - "django": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - }, -} - -INTERNAL_IPS = [ - "127.0.0.1", -] - -MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa -MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa -INSTALLED_APPS.append("django.contrib.admindocs") # noqa -SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" -endef - - -define DJANGO_SETTINGS_HOME_PAGE -INSTALLED_APPS.append("home") -endef - -define DJANGO_SETTINGS_INSTALLED_APPS -INSTALLED_APPS.append("allauth") -INSTALLED_APPS.append("allauth.account") -INSTALLED_APPS.append("allauth.socialaccount") -INSTALLED_APPS.append("crispy_bootstrap5") -INSTALLED_APPS.append("crispy_forms") -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("django_extensions") -INSTALLED_APPS.append("django_recaptcha") -INSTALLED_APPS.append("rest_framework") -INSTALLED_APPS.append("rest_framework.authtoken") -INSTALLED_APPS.append("webpack_boilerplate") -INSTALLED_APPS.append("explorer") -endef - -define DJANGO_SETTINGS_MIDDLEWARE -MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") -endef - -define DJANGO_SETTINGS_MODEL_FORM_DEMO -INSTALLED_APPS.append("model_form_demo") # noqa -endef - -define DJANGO_SETTINGS_PAYMENTS -DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" -DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" -STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") -STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") -STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") -INSTALLED_APPS.append("payments") # noqa -INSTALLED_APPS.append("djstripe") # noqa -endef - -define DJANGO_SETTINGS_REST_FRAMEWORK -REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" - ] -} -endef - -define DJANGO_SETTINGS_SITEUSER -INSTALLED_APPS.append("siteuser") # noqa -AUTH_USER_MODEL = "siteuser.User" -endef - -define DJANGO_SETTINGS_PROD -from .base import * # noqa -from backend.utils import get_ec2_metadata - -DEBUG = False - -try: - from .local import * # noqa -except ImportError: - pass - -LOCAL_IPV4 = get_ec2_metadata() -ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa -endef - -define DJANGO_SETTINGS_THEMES -THEMES = [ - ("light", "Light Theme"), - ("dark", "Dark Theme"), -] -endef - -define DJANGO_SITEUSER_ADMIN -from django.contrib.auth.admin import UserAdmin -from django.contrib import admin - -from .models import User - -admin.site.register(User, UserAdmin) -endef - -define DJANGO_SITEUSER_EDIT_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags %} -{% block content %} -

    Edit User

    - {% crispy form %} -{% endblock %} -endef - -define DJANGO_SITEUSER_FORM -from django import forms -from django.contrib.auth.forms import UserChangeForm -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit -from .models import User - - -class SiteUserForm(UserChangeForm): - bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) - - class Meta(UserChangeForm.Meta): - model = User - fields = ("username", "user_theme_preference", "bio", "rate") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.layout = Layout( - Fieldset( - "Edit Your Profile", - "username", - "user_theme_preference", - "bio", - "rate", - ), - ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), - ) -endef - -define DJANGO_SITEUSER_MODEL -from django.db import models -from django.contrib.auth.models import AbstractUser, Group, Permission -from django.conf import settings - - -class User(AbstractUser): - groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) - user_permissions = models.ManyToManyField( - Permission, related_name="siteuser_set", blank=True - ) - - user_theme_preference = models.CharField( - max_length=10, choices=settings.THEMES, default="light" - ) - - bio = models.TextField(blank=True, null=True) - rate = models.FloatField(blank=True, null=True) -endef - -define DJANGO_SITEUSER_URLS -from django.urls import path -from .views import UserProfileView, UpdateThemePreferenceView, UserEditView - -urlpatterns = [ - path("profile/", UserProfileView.as_view(), name="user-profile"), - path( - "update_theme_preference/", - UpdateThemePreferenceView.as_view(), - name="update_theme_preference", - ), - path("/edit/", UserEditView.as_view(), name="user-edit"), -] -endef - -define DJANGO_SITEUSER_VIEW -import json - -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse -from django.utils.decorators import method_decorator -from django.views import View -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import DetailView -from django.views.generic.edit import UpdateView -from django.urls import reverse_lazy - -from .models import User -from .forms import SiteUserForm - - -class UserProfileView(LoginRequiredMixin, DetailView): - model = User - template_name = "profile.html" - - def get_object(self, queryset=None): - return self.request.user - - -@method_decorator(csrf_exempt, name="dispatch") -class UpdateThemePreferenceView(View): - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode("utf-8")) - new_theme = data.get("theme") - user = request.user - user.user_theme_preference = new_theme - user.save() - response_data = {"theme": new_theme} - return JsonResponse(response_data) - except json.JSONDecodeError as e: - return JsonResponse({"error": e}, status=400) - - def http_method_not_allowed(self, request, *args, **kwargs): - return JsonResponse({"error": "Invalid request method"}, status=405) - - -class UserEditView(LoginRequiredMixin, UpdateView): - model = User - template_name = "user_edit.html" # Create this template in your templates folder - form_class = SiteUserForm - - def get_success_url(self): - # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) - return reverse_lazy("user-profile") -endef - -define DJANGO_SITEUSER_VIEW_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    User Profile

    -
    - Edit -
    -

    Username: {{ user.username }}

    -

    Theme: {{ user.user_theme_preference }}

    -

    Bio: {{ user.bio|default:""|safe }}

    -

    Rate: {{ user.rate|default:"" }}

    -{% endblock %} -endef - -define DJANGO_URLS -from django.contrib import admin -from django.urls import path, include -from django.conf import settings - -urlpatterns = [ - path("django/", admin.site.urls), -] -endef - -define DJANGO_URLS_ALLAUTH -urlpatterns += [path("accounts/", include("allauth.urls"))] -endef - -define DJANGO_URLS_API -from rest_framework import routers # noqa -from .api import UserViewSet, api # noqa - -router = routers.DefaultRouter() -router.register(r"users", UserViewSet) -# urlpatterns += [path("api/", include(router.urls))] -urlpatterns += [path("api/", api.urls)] -endef - -define DJANGO_URLS_DEBUG_TOOLBAR -if settings.DEBUG: - urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] -endef - -define DJANGO_URLS_HOME_PAGE -urlpatterns += [path("", include("home.urls"))] -endef - -define DJANGO_URLS_LOGGING_DEMO -urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] -endef - -define DJANGO_URLS_MODEL_FORM_DEMO -urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] -endef - -define DJANGO_URLS_PAYMENTS -urlpatterns += [path("payments/", include("payments.urls"))] -endef - -define DJANGO_URLS_SITEUSER -urlpatterns += [path("user/", include("siteuser.urls"))] -endef - -define DJANGO_UTILS -from django.urls import URLResolver -import requests - - -def get_ec2_metadata(): - try: - # Step 1: Get the token - token_url = "http://169.254.169.254/latest/api/token" - headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} - response = requests.put(token_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - token = response.text - - # Step 2: Use the token to get the instance metadata - metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" - headers = {"X-aws-ec2-metadata-token": token} - response = requests.get(metadata_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - metadata = response.text - return metadata - except requests.RequestException as e: - print(f"Error retrieving EC2 metadata: {e}") - return None - - -# Function to remove a specific URL pattern based on its route (including catch-all) -def remove_urlpattern(urlpatterns, route_to_remove): - urlpatterns[:] = [ - urlpattern - for urlpattern in urlpatterns - if not ( - isinstance(urlpattern, URLResolver) - and urlpattern.pattern._route == route_to_remove - ) - ] -endef - -define EB_CUSTOM_ENV_EC2_USER -files: - "/home/ec2-user/.bashrc": - mode: "000644" - owner: ec2-user - group: ec2-user - content: | - # .bashrc - - # Source global definitions - if [ -f /etc/bashrc ]; then - . /etc/bashrc - fi - - # User specific aliases and functions - set -o vi - - source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) -endef - -define EB_CUSTOM_ENV_VAR_FILE -#!/bin/bash - -# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ - -#Create a copy of the environment variable file. -cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var - -#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. -chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var - -# add the virtual env path in. -VENV=/var/app/venv/`ls /var/app/venv` -cat <> /opt/elasticbeanstalk/deployment/custom_env_var -VENV=$$ENV -EOF - -#Remove duplicate files upon deployment. -rm -f /opt/elasticbeanstalk/deployment/*.bak -endef - -define GIT_IGNORE -__pycache__ -*.pyc -dist/ -node_modules/ -_build/ -.elasticbeanstalk/ -db.sqlite3 -static/ -backend/var -endef - -define JENKINS_FILE -pipeline { - agent any - stages { - stage('') { - steps { - echo '' - } - } - } -} -endef - -define MAKEFILE_CUSTOM -# Custom Makefile -# Add your custom makefile commands here -# -# PROJECT_NAME := my-new-project -endef - -define PIP_INSTALL_REQUIREMENTS_TEST -pytest -pytest-runner -coverage -pytest-mock -pytest-cov -hypothesis -selenium -pytest-django -factory-boy -flake8 -tox -endef - -define PROGRAMMING_INTERVIEW -from rich import print as rprint -from rich.console import Console -from rich.panel import Panel - -import argparse -import locale -import math -import time - -import code # noqa -import readline # noqa -import rlcompleter # noqa - - -locale.setlocale(locale.LC_ALL, "en_US.UTF-8") - - -class DataStructure: - # Data Structure: Binary Tree - class TreeNode: - def __init__(self, value=0, left=None, right=None): - self.value = value - self.left = left - self.right = right - - # Data Structure: Stack - class Stack: - def __init__(self): - self.items = [] - - def push(self, item): - self.items.append(item) - - def pop(self): - if not self.is_empty(): - return self.items.pop() - return None - - def peek(self): - if not self.is_empty(): - return self.items[-1] - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Queue - class Queue: - def __init__(self): - self.items = [] - - def enqueue(self, item): - self.items.append(item) - - def dequeue(self): - if not self.is_empty(): - return self.items.pop(0) - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Linked List - class ListNode: - def __init__(self, value=0, next=None): - self.value = value - self.next = next - - -class Interview(DataStructure): - - # Protected methods for factorial calculation - def _factorial_recursive(self, n): - if n == 0: - return 1 - return n * self._factorial_recursive(n - 1) - - def _factorial_divide_and_conquer(self, low, high): - if low > high: - return 1 - if low == high: - return low - mid = (low + high) // 2 - return self._factorial_divide_and_conquer( - low, mid - ) * self._factorial_divide_and_conquer(mid + 1, high) - - # Recursive Factorial with Timing - def factorial_recursive(self, n): - start_time = time.time() # Start timing - result = self._factorial_recursive(n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Iterative Factorial with Timing - def factorial_iterative(self, n): - start_time = time.time() # Start timing - result = 1 - for i in range(1, n + 1): - result *= i - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Divide and Conquer Factorial with Timing - def factorial_divide_and_conquer(self, n): - start_time = time.time() # Start timing - result = self._factorial_divide_and_conquer(1, n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Built-in Factorial with Timing - def factorial_builtin(self, n): - start_time = time.time() # Start timing - result = math.factorial(n) # Calculate factorial using built-in - end_time = time.time() # End timing - - # Calculate elapsed time - elapsed_time = end_time - start_time - - # Print complexity and runtime - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Recursion: Fibonacci - def fibonacci_recursive(self, n): - if n <= 1: - return n - return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) - - # Iteration: Fibonacci - def fibonacci_iterative(self, n): - if n <= 1: - return n - a, b = 0, 1 - for _ in range(n - 1): - a, b = b, a + b - return b - - # Searching: Linear Search - def linear_search(self, arr, target): - for i, value in enumerate(arr): - if value == target: - return i - return -1 - - # Searching: Binary Search - def binary_search(self, arr, target): - left, right = 0, len(arr) - 1 - while left <= right: - mid = (left + right) // 2 - if arr[mid] == target: - return mid - elif arr[mid] < target: - left = mid + 1 - else: - right = mid - 1 - return -1 - - # Sorting: Bubble Sort - def bubble_sort(self, arr): - n = len(arr) - for i in range(n): - for j in range(0, n - i - 1): - if arr[j] > arr[j + 1]: - arr[j], arr[j + 1] = arr[j + 1], arr[j] - return arr - - # Sorting: Merge Sort - def merge_sort(self, arr): - if len(arr) > 1: - mid = len(arr) // 2 - left_half = arr[:mid] - right_half = arr[mid:] - - self.merge_sort(left_half) - self.merge_sort(right_half) - - i = j = k = 0 - - while i < len(left_half) and j < len(right_half): - if left_half[i] < right_half[j]: - arr[k] = left_half[i] - i += 1 - else: - arr[k] = right_half[j] - j += 1 - k += 1 - - while i < len(left_half): - arr[k] = left_half[i] - i += 1 - k += 1 - - while j < len(right_half): - arr[k] = right_half[j] - j += 1 - k += 1 - return arr - - def insert_linked_list(self, head, value): - new_node = self.ListNode(value) - if not head: - return new_node - current = head - while current.next: - current = current.next - current.next = new_node - return head - - def print_linked_list(self, head): - current = head - while current: - print(current.value, end=" -> ") - current = current.next - print("None") - - def inorder_traversal(self, root): - return ( - self.inorder_traversal(root.left) - + [root.value] - + self.inorder_traversal(root.right) - if root - else [] - ) - - def preorder_traversal(self, root): - return ( - [root.value] - + self.preorder_traversal(root.left) - + self.preorder_traversal(root.right) - if root - else [] - ) - - def postorder_traversal(self, root): - return ( - self.postorder_traversal(root.left) - + self.postorder_traversal(root.right) - + [root.value] - if root - else [] - ) - - # Graph Algorithms: Depth-First Search - def dfs(self, graph, start): - visited, stack = set(), [start] - while stack: - vertex = stack.pop() - if vertex not in visited: - visited.add(vertex) - stack.extend(set(graph[vertex]) - visited) - return visited - - # Graph Algorithms: Breadth-First Search - def bfs(self, graph, start): - visited, queue = set(), [start] - while queue: - vertex = queue.pop(0) - if vertex not in visited: - visited.add(vertex) - queue.extend(set(graph[vertex]) - visited) - return visited - - -def setup_readline(local): - - # Enable tab completion - readline.parse_and_bind("tab: complete") - # Optionally, you can set the completer function manually - readline.set_completer(rlcompleter.Completer(local).complete) - - -def main(): - - console = Console() - interview = Interview() - - parser = argparse.ArgumentParser(description="Programming Interview Questions") - - parser.add_argument( - "-f", "--factorial", type=int, help="Factorial algorithm examples" - ) - parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") - parser.add_argument( - "--search", action="store_true", help="Search algorithm examples" - ) - parser.add_argument("--sort", action="store_true", help="Search algorithm examples") - parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") - parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") - parser.add_argument( - "--list", action="store_true", help="Linked List algorithm examples" - ) - parser.add_argument( - "--tree", action="store_true", help="Tree traversal algorithm examples" - ) - parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") - parser.add_argument( - "-i", "--interactive", action="store_true", help="Interactive mode" - ) - - args = parser.parse_args() - - if args.factorial: - # Factorial examples - console.rule("Factorial Examples") - rprint( - Panel( - "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_recursive(args.factorial)), - title="Factorial Recursive", - ) - ) - rprint( - Panel( - "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_iterative(args.factorial)), - title="Factorial Iterative", - ) - ) - rprint( - Panel( - "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_builtin(args.factorial)), - title="Factorial Built-in", - ) - ) - rprint( - Panel( - "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" - + str(interview.factorial_divide_and_conquer(args.factorial)), - title="Factorial Divide and Conquer", - ) - ) - exit() - - if args.fibonacci: - # Fibonacci examples - console.rule("Fibonacci Examples") - rprint( - Panel( - str(interview.fibonacci_recursive(args.fibonacci)), - title="Fibonacci Recursive", - ) - ) - rprint( - Panel( - str(interview.fibonacci_iterative(args.fibonacci)), - title="Fibonacci Iterative", - ) - ) - exit() - - if args.search: - # Searching examples - console.rule("Searching Examples") - array = [1, 3, 5, 7, 9] - rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) - rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) - exit() - - if args.sort: - # Sorting examples - console.rule("Sorting Examples") - unsorted_array = [64, 34, 25, 12, 22, 11, 90] - rprint( - Panel( - str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" - ) - ) - rprint( - Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") - ) - exit() - - if args.stack: - # Stack example - console.rule("Stack Example") - stack = interview.Stack() - stack.push(1) - stack.push(2) - stack.push(3) - rprint(Panel(str(stack.pop()), title="Stack Pop")) - rprint(Panel(str(stack.peek()), title="Stack Peek")) - rprint(Panel(str(stack.size()), title="Stack Size")) - - if args.queue: - # Queue example - console.rule("Queue Example") - queue = interview.Queue() - queue.enqueue(1) - queue.enqueue(2) - queue.enqueue(3) - rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) - rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) - rprint(Panel(str(queue.size()), title="Queue Size")) - - if args.list: - # Linked List example - console.rule("Linked List Example") - head = None - head = interview.insert_linked_list(head, 1) - head = interview.insert_linked_list(head, 2) - head = interview.insert_linked_list(head, 3) - interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None - - if args.tree: - # Tree Traversal example - console.rule("Tree Traversal Example") - root = interview.TreeNode(1) - root.left = interview.TreeNode(2) - root.right = interview.TreeNode(3) - root.left.left = interview.TreeNode(4) - root.left.right = interview.TreeNode(5) - rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) - rprint( - Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") - ) - rprint( - Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") - ) - - if args.graph: - # Graph Algorithms example - console.rule("Graph Algorithms Example") - graph = { - "A": ["B", "C"], - "B": ["A", "D", "E"], - "C": ["A", "F"], - "D": ["B"], - "E": ["B", "F"], - "F": ["C", "E"], - } - rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) - rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) - - if args.interactive: - # Starting interactive session with tab completion - setup_readline(locals()) - banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." - code.interact( - banner=banner, - local=locals(), - exitmsg="Great interview!", - ) - - -if __name__ == "__main__": - main() - -endef - -define PYTHON_CI_YAML -name: Build Wheels -endef - -define PYTHON_LICENSE_TXT -MIT License - -Copyright (c) [YEAR] [OWNER NAME] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -endef - -define PYTHON_PROJECT_TOML -[build-system] -endef - -define SEPARATOR -.==========================================================================================================================================. -| | -| _|_|_| _| _| _| _| _| _|_| _| _| | -| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | -| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | -| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | -| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | -| _| | -| _| | -`==========================================================================================================================================' -endef - -define TINYMCE_JS -import tinymce from 'tinymce'; -import 'tinymce/icons/default'; -import 'tinymce/themes/silver'; -import 'tinymce/skins/ui/oxide/skin.css'; -import 'tinymce/plugins/advlist'; -import 'tinymce/plugins/code'; -import 'tinymce/plugins/emoticons'; -import 'tinymce/plugins/emoticons/js/emojis'; -import 'tinymce/plugins/link'; -import 'tinymce/plugins/lists'; -import 'tinymce/plugins/table'; -import 'tinymce/models/dom'; - -tinymce.init({ - selector: 'textarea#editor', - plugins: 'advlist code emoticons link lists table', - toolbar: 'bold italic | bullist numlist | link emoticons', - skin: false, - content_css: false, -}); -endef - -define WAGTAIL_BASE_TEMPLATE -{% load static wagtailcore_tags wagtailuserbar webpack_loader %} - - - - - - {% block title %} - {% if page.seo_title %} - {{ page.seo_title }} - {% else %} - {{ page.title }} - {% endif %} - {% endblock %} - {% block title_suffix %} - {% wagtail_site as current_site %} - {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} - {% endblock %} - - {% if page.search_description %}{% endif %} - - {# Force all links in the live preview panel to be opened in a new tab #} - {% if request.in_preview_panel %}{% endif %} - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    - {% wagtailuserbar %} -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define WAGTAIL_BLOCK_CAROUSEL - -endef - -define WAGTAIL_BLOCK_MARKETING -{% load wagtailcore_tags %} -
    - {% if block.value.images.0 %} - {% include 'blocks/carousel_block.html' %} - {% else %} - {{ self.title }} - {{ self.content }} - {% endif %} -
    -endef - -define WAGTAIL_CONTACT_PAGE_LANDING -{% extends 'base.html' %} -{% block content %}

    Thank you!

    {% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_MODEL -from django.db import models -from modelcluster.fields import ParentalKey -from wagtail.admin.panels import ( - FieldPanel, FieldRowPanel, - InlinePanel, MultiFieldPanel -) -from wagtail.fields import RichTextField -from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField - - -class FormField(AbstractFormField): - page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') - - -class ContactPage(AbstractEmailForm): - intro = RichTextField(blank=True) - thank_you_text = RichTextField(blank=True) - - content_panels = AbstractEmailForm.content_panels + [ - FieldPanel('intro'), - InlinePanel('form_fields', label="Form fields"), - FieldPanel('thank_you_text'), - MultiFieldPanel([ - FieldRowPanel([ - FieldPanel('from_address', classname="col6"), - FieldPanel('to_address', classname="col6"), - ]), - FieldPanel('subject'), - ], "Email"), - ] - - class Meta: - verbose_name = "Contact Page" -endef - -define WAGTAIL_CONTACT_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags static wagtailcore_tags %} -{% block content %} -

    {{ page.title }}

    - {{ page.intro|richtext }} -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_TEST -from django.test import TestCase -from wagtail.test.utils import WagtailPageTestCase -from wagtail.models import Page - -from contactpage.models import ContactPage, FormField - -class ContactPageTest(TestCase, WagtailPageTestCase): - def test_contact_page_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - - # Save the ContactPage instance - self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) - - def test_form_field_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Create a FormField associated with the ContactPage - form_field = FormField( - page=contact_page, - label='Your Name', - field_type='singleline', - required=True - ) - form_field.save() - - # Retrieve the ContactPage from the database - contact_page_from_db = Page.objects.get(id=contact_page.id).specific - - # Check if the FormField is associated with the ContactPage - self.assertEqual(contact_page_from_db.form_fields.first(), form_field) - - def test_contact_page_form_submission(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Simulate a form submission - form_data = { - 'your_name': 'John Doe', - # Add other form fields as needed - } - - response = self.client.post(contact_page.url, form_data) - - # Check if the form submission is successful (assuming a 302 redirect) - self.assertEqual(response.status_code, 302) - - # You may add more assertions based on your specific requirements -endef - -define WAGTAIL_HEADER_PREFIX -{% load wagtailcore_tags %} -{% wagtail_site as current_site %} -endef - -define WAGTAIL_HOME_PAGE_MODEL -from wagtail.models import Page -from wagtail.fields import StreamField -from wagtail import blocks -from wagtail.admin.panels import FieldPanel -from wagtail.images.blocks import ImageChooserBlock - - -class MarketingBlock(blocks.StructBlock): - title = blocks.CharBlock(required=False, help_text="Enter the block title") - content = blocks.RichTextBlock(required=False, help_text="Enter the block content") - images = blocks.ListBlock( - ImageChooserBlock(required=False), - help_text="Select one or two images for column display. Select three or more images for carousel display.", - ) - image = ImageChooserBlock( - required=False, help_text="Select one image for background display." - ) - block_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the marketing block", - classname="full title", - default="vh-100 bg-secondary", - ) - image_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the column display image(s)", - classname="full title", - default="img-thumbnail p-5", - ) - layout_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the layout.", - classname="full title", - default="d-flex flex-row", - ) - - class Meta: - icon = "placeholder" - template = "blocks/marketing_block.html" - - -class HomePage(Page): - template = "home/home_page.html" # Create a template for rendering the home page - - marketing_blocks = StreamField( - [ - ("marketing_block", MarketingBlock()), - ], - blank=True, - null=True, - use_json_field=True, - ) - content_panels = Page.content_panels + [ - FieldPanel("marketing_blocks"), - ] - - class Meta: - verbose_name = "Home Page" -endef - -define WAGTAIL_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% load wagtailcore_tags %} -{% block content %} -
    - {% for block in page.marketing_blocks %} - {% include_block block %} - {% endfor %} -
    -{% endblock %} -endef - -define WAGTAIL_PRIVACY_PAGE_MODEL -from wagtail.models import Page -from wagtail.admin.panels import FieldPanel -from wagtailmarkdown.fields import MarkdownField - - -class PrivacyPage(Page): - """ - A Wagtail Page model for the Privacy Policy page. - """ - - template = "privacy_page.html" - - body = MarkdownField() - - content_panels = Page.content_panels + [ - FieldPanel("body", classname="full"), - ] - - class Meta: - verbose_name = "Privacy Page" -endef - -define WAGTAIL_PRIVACY_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load wagtailmarkdown %} -{% block content %}
    {{ page.body|markdown }}
    {% endblock %} -endef - -define WAGTAIL_SEARCH_TEMPLATE -{% extends "base.html" %} -{% load static wagtailcore_tags %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define WAGTAIL_SEARCH_URLS -from django.urls import path -from .views import search - -urlpatterns = [path("", search, name="search")] -endef - -define WAGTAIL_SETTINGS -INSTALLED_APPS.append("wagtail_color_panel") -INSTALLED_APPS.append("wagtail_modeladmin") -INSTALLED_APPS.append("wagtail.contrib.settings") -INSTALLED_APPS.append("wagtailmarkdown") -INSTALLED_APPS.append("wagtailmenus") -INSTALLED_APPS.append("wagtailseo") -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtail.contrib.settings.context_processors.settings" -) -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtailmenus.context_processors.wagtailmenus" -) -endef - -define WAGTAIL_SITEPAGE_MODEL -from wagtail.models import Page - - -class SitePage(Page): - template = "sitepage/site_page.html" - - class Meta: - verbose_name = "Site Page" -endef - -define WAGTAIL_SITEPAGE_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    {{ page.title }}

    -{% endblock %} -endef - -define WAGTAIL_URLS -from django.conf import settings -from django.urls import include, path -from django.contrib import admin - -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls - -from search import views as search_views - -urlpatterns = [ - path("django/", admin.site.urls), - path("wagtail/", include(wagtailadmin_urls)), - path("documents/", include(wagtaildocs_urls)), - path("search/", search_views.search, name="search"), -] - -if settings.DEBUG: - from django.conf.urls.static import static - from django.contrib.staticfiles.urls import staticfiles_urlpatterns - - # Serve static and media files from development server - urlpatterns += staticfiles_urlpatterns() - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -endef - -define WAGTAIL_URLS_HOME -urlpatterns += [ - # For anything not caught by a more specific rule above, hand over to - # Wagtail's page serving mechanism. This should be the last pattern in - # the list: - path("", include("wagtail.urls")), - # Alternatively, if you want Wagtail pages to be served from a subpath - # of your site, rather than the site root: - # path("pages/", include("wagtail.urls"), -] -endef - -define WEBPACK_CONFIG_JS -const path = require('path'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, -}; -endef - -define WEBPACK_INDEX_HTML - - - - - - Hello, Webpack! - - - - - -endef - -define WEBPACK_INDEX_JS -const message = "Hello, World!"; -console.log(message); -endef - -define WEBPACK_REVEAL_CONFIG_JS -const path = require('path'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, - module: { - rules: [ - { - test: /\.css$$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: 'bundle.css', - }), - ], -}; -endef - -define WEBPACK_REVEAL_INDEX_HTML - - - - - - Project Makefile - - -
    -
    -
    - Slide 1: Draw some circles -
    -
    - Slide 2: Draw the rest of the owl -
    -
    -
    - - -endef - -define WEBPACK_REVEAL_INDEX_JS -import 'reveal.js/dist/reveal.css'; -import 'reveal.js/dist/theme/black.css'; -import Reveal from 'reveal.js'; -import RevealNotes from 'reveal.js/plugin/notes/notes.js'; -Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); -endef - -# ------------------------------------------------------------------------------ -# Export variables used by phony target rules -# ------------------------------------------------------------------------------ - -export DJANGO_ALLAUTH_BASE_TEMPLATE -export DJANGO_API_SERIALIZERS -export DJANGO_API_VIEWS -export DJANGO_APP_TESTS -export DJANGO_BACKEND_APPS -export DJANGO_BASE_TEMPLATE -export DJANGO_CUSTOM_ADMIN -export DJANGO_DOCKERCOMPOSE -export DJANGO_DOCKERFILE -export DJANGO_FAVICON_TEMPLATE -export DJANGO_FOOTER_TEMPLATE -export DJANGO_FRONTEND_APP -export DJANGO_FRONTEND_APP_CONFIG -export DJANGO_FRONTEND_BABELRC -export DJANGO_FRONTEND_COMPONENTS -export DJANGO_FRONTEND_COMPONENT_CLOCK -export DJANGO_FRONTEND_COMPONENT_ERROR -export DJANGO_FRONTEND_COMPONENT_USER_MENU -export DJANGO_FRONTEND_CONTEXT_INDEX -export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -export DJANGO_FRONTEND_ESLINTRC -export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -export DJANGO_FRONTEND_PORTAL -export DJANGO_FRONTEND_STYLES -export DJANGO_FRONTEND_THEME_BLUE -export DJANGO_FRONTEND_THEME_TOGGLER -export DJANGO_HEADER_TEMPLATE -export DJANGO_HOME_PAGE_ADMIN -export DJANGO_HOME_PAGE_MODELS -export DJANGO_HOME_PAGE_TEMPLATE -export DJANGO_HOME_PAGE_URLS -export DJANGO_HOME_PAGE_VIEWS -export DJANGO_LOGGING_DEMO_ADMIN -export DJANGO_LOGGING_DEMO_MODELS -export DJANGO_LOGGING_DEMO_SETTINGS -export DJANGO_LOGGING_DEMO_URLS -export DJANGO_LOGGING_DEMO_VIEWS -export DJANGO_MANAGE_PY -export DJANGO_MODEL_FORM_DEMO_ADMIN -export DJANGO_MODEL_FORM_DEMO_FORMS -export DJANGO_MODEL_FORM_DEMO_MODEL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -export DJANGO_MODEL_FORM_DEMO_URLS -export DJANGO_MODEL_FORM_DEMO_VIEWS -export DJANGO_PAYMENTS_ADMIN -export DJANGO_PAYMENTS_FORM -export DJANGO_PAYMENTS_MIGRATION_0002 -export DJANGO_PAYMENTS_MIGRATION_0003 -export DJANGO_PAYMENTS_MODELS -export DJANGO_PAYMENTS_TEMPLATE_CANCEL -export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -export DJANGO_PAYMENTS_TEMPLATE_SUCCESS -export DJANGO_PAYMENTS_URLS -export DJANGO_PAYMENTS_VIEW -export DJANGO_SEARCH_FORMS -export DJANGO_SEARCH_SETTINGS -export DJANGO_SEARCH_TEMPLATE -export DJANGO_SEARCH_URLS -export DJANGO_SEARCH_UTILS -export DJANGO_SEARCH_VIEWS -export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -export DJANGO_SETTINGS_BASE -export DJANGO_SETTINGS_BASE_MINIMAL -export DJANGO_SETTINGS_CRISPY_FORMS -export DJANGO_SETTINGS_DATABASE -export DJANGO_SETTINGS_DEV -export DJANGO_SETTINGS_HOME_PAGE -export DJANGO_SETTINGS_INSTALLED_APPS -export DJANGO_SETTINGS_MIDDLEWARE -export DJANGO_SETTINGS_MODEL_FORM_DEMO -export DJANGO_SETTINGS_PAYMENTS -export DJANGO_SETTINGS_PROD -export DJANGO_SETTINGS_REST_FRAMEWORK -export DJANGO_SETTINGS_SITEUSER -export DJANGO_SETTINGS_THEMES -export DJANGO_SITEUSER_ADMIN -export DJANGO_SITEUSER_EDIT_TEMPLATE -export DJANGO_SITEUSER_FORM -export DJANGO_SITEUSER_MODEL -export DJANGO_SITEUSER_URLS -export DJANGO_SITEUSER_VIEW -export DJANGO_SITEUSER_VIEW_TEMPLATE -export DJANGO_URLS -export DJANGO_URLS_ALLAUTH -export DJANGO_URLS_API -export DJANGO_URLS_DEBUG_TOOLBAR -export DJANGO_URLS_HOME_PAGE -export DJANGO_URLS_LOGGING_DEMO -export DJANGO_URLS_MODEL_FORM_DEMO -export DJANGO_URLS_SITEUSER -export DJANGO_UTILS -export EB_CUSTOM_ENV_EC2_USER -export EB_CUSTOM_ENV_VAR_FILE -export GIT_IGNORE -export JENKINS_FILE -export MAKEFILE_CUSTOM -export PIP_INSTALL_REQUIREMENTS_TEST -export PROGRAMMING_INTERVIEW -export PYTHON_CI_YAML -export PYTHON_LICENSE_TXT -export PYTHON_PROJECT_TOML -export SEPARATOR -export TINYMCE_JS -export WAGTAIL_BASE_TEMPLATE -export WAGTAIL_BLOCK_CAROUSEL -export WAGTAIL_BLOCK_MARKETING -export WAGTAIL_CONTACT_PAGE_LANDING -export WAGTAIL_CONTACT_PAGE_MODEL -export WAGTAIL_CONTACT_PAGE_TEMPLATE -export WAGTAIL_CONTACT_PAGE_TEST -export WAGTAIL_HOME_PAGE_MODEL -export WAGTAIL_HOME_PAGE_TEMPLATE -export WAGTAIL_HOME_PAGE_URLS -export WAGTAIL_HOME_PAGE_VIEWS -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_TEMPLATE -export WAGTAIL_SEARCH_TEMPLATE -export WAGTAIL_SEARCH_URLS -export WAGTAIL_SETTINGS -export WAGTAIL_SITEPAGE_MODEL -export WAGTAIL_SITEPAGE_TEMPLATE -export WAGTAIL_URLS -export WAGTAIL_URLS_HOME -export WEBPACK_CONFIG_JS -export WEBPACK_INDEX_HTML -export WEBPACK_INDEX_JS -export WEBPACK_REVEAL_CONFIG_JS -export WEBPACK_REVEAL_INDEX_HTML -export WEBPACK_REVEAL_INDEX_JS - -# ------------------------------------------------------------------------------ -# Multi-line phony target rules -# ------------------------------------------------------------------------------ - -.PHONY: aws-check-env-profile-default -aws-check-env-profile-default: -ifndef AWS_PROFILE - $(error AWS_PROFILE is undefined) -endif - -.PHONY: aws-check-env-region-default -aws-check-env-region-default: -ifndef AWS_REGION - $(error AWS_REGION is undefined) -endif - -.PHONY: aws-secret-default -aws-secret-default: aws-check-env - @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String - -.PHONY: aws-sg-default -aws-sg-default: aws-check-env - aws ec2 describe-security-groups $(AWS_OPTS) - -.PHONY: aws-ssm-default -aws-ssm-default: aws-check-env - aws ssm describe-parameters $(AWS_OPTS) - @echo "Get parameter values with: aws ssm getparameter --name ." - -.PHONY: aws-subnet-default -aws-subnet-default: aws-check-env - aws ec2 describe-subnets $(AWS_OPTS) - -.PHONY: aws-vol-available-default -aws-vol-available-default: aws-check-env - aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table - -.PHONY: aws-vol-default -aws-vol-default: aws-check-env - aws ec2 describe-volumes --output table - -.PHONY: aws-vpc-default -aws-vpc-default: aws-check-env - aws ec2 describe-vpcs $(AWS_OPTS) - -.PHONY: db-import-default -db-import-default: - @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql - -.PHONY: db-init-default -db-init-default: - -dropdb $(PROJECT_NAME) - -createdb $(PROJECT_NAME) - -.PHONY: db-init-mysql-default -db-init-mysql-default: - -mysqladmin -u root drop $(PROJECT_NAME) - -mysqladmin -u root create $(PROJECT_NAME) - -.PHONY: db-init-test-default -db-init-test-default: - -dropdb test_$(PROJECT_NAME) - -createdb test_$(PROJECT_NAME) - -.PHONY: django-allauth-default -django-allauth-default: - $(ADD_DIR) backend/templates/allauth/layouts - @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html - @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) backend/templates/allauth/layouts/base.html - -.PHONY: django-app-tests-default -django-app-tests-default: - @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py - -.PHONY: django-base-template-default -django-base-template-default: - @$(ADD_DIR) backend/templates - @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html - -$(GIT_ADD) backend/templates/base.html - -.PHONY: django-custom-admin-default -django-custom-admin-default: - @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) - @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) - -$(GIT_ADD) backend/*.py - -.PHONY: django-db-shell-default -django-db-shell-default: - python manage.py dbshell - -.PHONY: django-dockerfile-default -django-dockerfile-default: - @echo "$$DJANGO_DOCKERFILE" > Dockerfile - -$(GIT_ADD) Dockerfile - @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml - -$(GIT_ADD) docker-compose.yml - -.PHONY: django-favicon-default -django-favicon-default: - @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html - -$(GIT_ADD) backend/templates/favicon.html - -.PHONY: django-footer-template-default -django-footer-template-default: - @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html - -$(GIT_ADD) backend/templates/footer.html - -.PHONY: django-frontend-default -django-frontend-default: python-webpack-init - $(ADD_DIR) frontend/src/context - $(ADD_DIR) frontend/src/images - $(ADD_DIR) frontend/src/utils - @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js - @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js - @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc - @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js - @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js - @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js - @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js - @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js - @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js - @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc - @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js - @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss - @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss - @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js - # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js - @$(MAKE) npm-install-django - @$(MAKE) npm-install-django-dev - -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) - -.PHONY: django-graph-default -django-graph-default: - python manage.py graph_models -a -o $(PROJECT_NAME).png - -.PHONY: django-header-template-default -django-header-template-default: - @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html - -$(GIT_ADD) backend/templates/header.html - -.PHONY: django-home-default -django-home-default: - python manage.py startapp home - $(ADD_DIR) home/templates - @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py - @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py - @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html - @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py - @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py - @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="home"; $(MAKE) django-app-tests - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - -$(GIT_ADD) home/migrations/*.py - -.PHONY: django-init-default -django-init-default: separator \ - db-init \ - django-install \ - django-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-settings-directory \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - django-siteuser \ - django-home \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-init-minimal-default -django-init-minimal-default: separator \ - db-init \ - django-install-minimal \ - django-project \ - django-settings-directory \ - django-settings-base-minimal \ - django-settings-dev \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-favicon \ - django-settings-prod \ - django-home \ - django-utils \ - django-frontend \ - django-migrate \ - git-ignore \ - django-su - -.PHONY: django-init-wagtail-default -django-init-wagtail-default: separator \ - db-init \ - django-install \ - wagtail-install \ - wagtail-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - wagtail-header-prefix-template \ - django-header-template \ - wagtail-base-template \ - django-footer-template \ - django-manage-py \ - wagtail-home \ - wagtail-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - wagtail-search \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - wagtail-settings \ - django-siteuser \ - django-model-form-demo \ - django-logging-demo \ - django-payments-demo-default \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - wagtail-urls-home \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-install-default -django-install-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - Faker \ - boto3 \ - crispy-bootstrap5 \ - djangorestframework \ - django-allauth \ - django-after-response \ - django-ckeditor \ - django-colorful \ - django-cors-headers \ - django-countries \ - django-crispy-forms \ - django-debug-toolbar \ - django-extensions \ - django-hijack \ - django-honeypot \ - django-imagekit \ - django-import-export \ - django-ipware \ - django-multiselectfield \ - django-ninja \ - django-phonenumber-field \ - django-recurrence \ - django-recaptcha \ - django-registration \ - django-richtextfield \ - django-sendgrid-v5 \ - django-social-share \ - django-sql-explorer \ - django-storages \ - django-tables2 \ - django-timezone-field \ - django-widget-tweaks \ - dj-database-url \ - dj-rest-auth \ - dj-stripe \ - docutils \ - enmerkar \ - gunicorn \ - html2docx \ - icalendar \ - mailchimp-marketing \ - mailchimp-transactional \ - phonenumbers \ - pipdeptree \ - psycopg2-binary \ - pydotplus \ - python-webpack-boilerplate \ - python-docx \ - reportlab \ - texttable - -.PHONY: django-install-minimal-default -django-install-minimal-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - dj-database-url \ - django-debug-toolbar \ - python-webpack-boilerplate - -.PHONY: django-lint-default -django-lint-default: - -ruff format -v - -djlint --reformat --format-css --format-js . - -ruff check -v --fix - -.PHONY: django-loaddata-default -django-loaddata-default: - python manage.py loaddata - -.PHONY: django-logging-demo-default -django-logging-demo-default: - python manage.py startapp logging_demo - @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py - @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py - @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py - @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py - @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="logging_demo"; $(MAKE) django-app-tests - -$(GIT_ADD) logging_demo/*.py - -$(GIT_ADD) logging_demo/migrations/*.py - -.PHONY: django-manage-py-default -django-manage-py-default: - @echo "$$DJANGO_MANAGE_PY" > manage.py - -$(GIT_ADD) manage.py - -.PHONY: django-migrate-default -django-migrate-default: - python manage.py migrate - -.PHONY: django-migrations-make-default -django-migrations-make-default: - python manage.py makemigrations - -.PHONY: django-migrations-show-default -django-migrations-show-default: - python manage.py showmigrations - -.PHONY: django-model-form-demo-default -django-model-form-demo-default: - python manage.py startapp model_form_demo - @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py - @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py - @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py - @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py - @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py - $(ADD_DIR) model_form_demo/templates - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html - @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="model_form_demo"; $(MAKE) django-app-tests - python manage.py makemigrations - -$(GIT_ADD) model_form_demo/*.py - -$(GIT_ADD) model_form_demo/templates - -$(GIT_ADD) model_form_demo/migrations - -.PHONY: django-offcanvas-template-default -django-offcanvas-template-default: - -$(ADD_DIR) backend/templates - @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html - -$(GIT_ADD) backend/templates/offcanvas.html - -.PHONY: django-open-default -django-open-default: -ifeq ($(UNAME), Linux) - @echo "Opening on Linux." - xdg-open http://0.0.0.0:8000 -else ifeq ($(UNAME), Darwin) - @echo "Opening on macOS (Darwin)." - open http://0.0.0.0:8000 -else - @echo "Unable to open on: $(UNAME)" -endif - -.PHONY: django-payments-demo-default -django-payments-demo-default: - python manage.py startapp payments - @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py - @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py - @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py - @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py - @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py - $(ADD_DIR) payments/templates/payments - $(ADD_DIR) payments/management/commands - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html - @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) - export APP_DIR="payments"; $(MAKE) django-app-tests - python manage.py makemigrations payments - @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py - @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py - -$(GIT_ADD) payments/ - -.PHONY: django-project-default -django-project-default: - django-admin startproject backend . - -$(GIT_ADD) backend - -.PHONY: django-rest-serializers-default -django-rest-serializers-default: - @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py - -$(GIT_ADD) backend/serializers.py - -.PHONY: django-rest-views-default -django-rest-views-default: - @echo "$$DJANGO_API_VIEWS" > backend/api.py - -$(GIT_ADD) backend/api.py - -.PHONY: django-search-default -django-search-default: - python manage.py startapp search - $(ADD_DIR) search/templates - @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html - @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py - @echo "$$DJANGO_SEARCH_URLS" > search/urls.py - @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py - @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py - @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: django-secret-key-default -django-secret-key-default: - @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" - -.PHONY: django-serve-default -django-serve-default: - npm run watch & - python manage.py runserver 0.0.0.0:8000 - -.PHONY: django-settings-base-default -django-settings-base-default: - @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-base-minimal-default -django-settings-base-minimal-default: - @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-dev-default -django-settings-dev-default: - @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) - @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py - -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) - -.PHONY: django-settings-directory-default -django-settings-directory-default: - @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) - @$(COPY_FILE) backend/settings.py backend/settings/base.py - @$(DEL_FILE) backend/settings.py - -$(GIT_ADD) backend/settings/*.py - -.PHONY: django-settings-prod-default -django-settings-prod-default: - @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) - -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) - -.PHONY: django-shell-default -django-shell-default: - python manage.py shell - -.PHONY: django-siteuser-default -django-siteuser-default: - python manage.py startapp siteuser - $(ADD_DIR) siteuser/templates/ - @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py - @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py - @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py - @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py - @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py - @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html - @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html - @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html - @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="siteuser"; $(MAKE) django-app-tests - -$(GIT_ADD) siteuser/templates - -$(GIT_ADD) siteuser/*.py - python manage.py makemigrations siteuser - -$(GIT_ADD) siteuser/migrations/*.py - -.PHONY: django-static-default -django-static-default: - python manage.py collectstatic --noinput - -.PHONY: django-su-default -django-su-default: - DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) - -.PHONY: django-test-default -django-test-default: npm-install django-static - -$(MAKE) pip-install-test - python manage.py test - -.PHONY: django-urls-api-default -django-urls-api-default: - @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-debug-toolbar-default -django-urls-debug-toolbar-default: - @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) - -.PHONY: django-urls-default -django-urls-default: - @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-show-default -django-urls-show-default: - python manage.py show_urls - -.PHONY: django-user-default -django-user-default: - python manage.py shell -c "from django.contrib.auth.models import User; \ - User.objects.create_user('user', '', 'user')" - -.PHONY: django-utils-default -django-utils-default: - @echo "$$DJANGO_UTILS" > backend/utils.py - -$(GIT_ADD) backend/utils.py - -.PHONY: docker-build-default -docker-build-default: - podman build -t $(PROJECT_NAME) . - -.PHONY: docker-compose-default -docker-compose-default: - podman compose up - -.PHONY: docker-list-default -docker-list-default: - podman container list --all - podman images --all - -.PHONY: docker-run-default -docker-run-default: - podman run $(PROJECT_NAME) - -.PHONY: docker-serve-default -docker-serve-default: - podman run -p 8000:8000 $(PROJECT_NAME) - -.PHONY: docker-shell-default -docker-shell-default: - podman run -it $(PROJECT_NAME) /bin/bash - -.PHONY: eb-check-env-default -eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 -ifndef EB_SSH_KEY - $(error EB_SSH_KEY is undefined) -endif -ifndef VPC_ID - $(error VPC_ID is undefined) -endif -ifndef VPC_SG - $(error VPC_SG is undefined) -endif -ifndef VPC_SUBNET_EC2 - $(error VPC_SUBNET_EC2 is undefined) -endif -ifndef VPC_SUBNET_ELB - $(error VPC_SUBNET_ELB is undefined) -endif - -.PHONY: eb-create-default -eb-create-default: aws-check-env eb-check-env - eb create $(EB_ENV_NAME) \ - -im $(EC2_INSTANCE_MIN) \ - -ix $(EC2_INSTANCE_MAX) \ - -ip $(EC2_INSTANCE_PROFILE) \ - -i $(EC2_INSTANCE_TYPE) \ - -k $(EB_SSH_KEY) \ - -p $(EB_PLATFORM) \ - --elb-type $(EC2_LB_TYPE) \ - --vpc \ - --vpc.id $(VPC_ID) \ - --vpc.elbpublic \ - --vpc.publicip \ - --vpc.ec2subnets $(VPC_SUBNET_EC2) \ - --vpc.elbsubnets $(VPC_SUBNET_ELB) \ - --vpc.securitygroups $(VPC_SG) - -.PHONY: eb-custom-env-default -eb-custom-env-default: - $(ADD_DIR) .ebextensions - @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config - -$(GIT_ADD) .ebextensions/bash.config - $(ADD_DIR) .platform/hooks/postdeploy - @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh - -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh - -.PHONY: eb-deploy-default -eb-deploy-default: - eb deploy - -.PHONY: eb-export-default -eb-export-default: - @if [ ! -d $(EB_DIR_NAME) ]; then \ - echo "Directory $(EB_DIR_NAME) does not exist"; \ - else \ - echo "Directory $(EB_DIR_NAME) does exist!"; \ - eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ - echo "Wrote $(DJANGO_DB_NAME).sql"; \ - fi - -.PHONY: eb-restart-default -eb-restart-default: - eb ssh -c "systemctl restart web" - -.PHONY: eb-rebuild-default -eb-rebuild-default: - aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) - -.PHONY: eb-upgrade-default -eb-upgrade-default: - eb upgrade - -.PHONY: eb-init-default -eb-init-default: aws-check-env-profile - eb init --profile=$(AWS_PROFILE) - -.PHONY: eb-list-default -eb-list-platforms-default: - aws elasticbeanstalk list-platform-versions - -.PHONY: eb-list-databases-default -eb-list-databases-default: - @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" - -.PHONY: eb-logs-default -eb-logs-default: - eb logs - -.PHONY: eb-print-env-default -eb-print-env-default: - eb printenv - -.PHONY: favicon-default -favicon-init-default: - dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png - convert favicon.png favicon.ico - -$(GIT_ADD) favicon.ico - $(DEL_FILE) favicon.png - -.PHONY: git-ignore-default -git-ignore-default: - @echo "$$GIT_IGNORE" > .gitignore - -$(GIT_ADD) .gitignore - -.PHONY: git-branches-default -git-branches-default: - -for i in $(GIT_BRANCHES) ; do \ - -@$(GIT_CHECKOUT) -t $$i ; done - -.PHONY: git-commit-message-clean-default -git-commit-message-clean-default: - -@$(GIT_COMMIT) -a -m "Clean" - -.PHONY: git-commit-message-default -git-commit-message-default: - -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) - -.PHONY: git-commit-message-empty-default -git-commit-message-empty-default: - -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" - -.PHONY: git-commit-message-init-default -git-commit-message-init-default: - -@$(GIT_COMMIT) -a -m "Init" - -.PHONY: git-commit-message-last-default -git-commit-message-last-default: - git log -1 --pretty=%B > $(TMPDIR)/commit.txt - -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt - -.PHONY: git-commit-message-lint-default -git-commit-message-lint-default: - -@$(GIT_COMMIT) -a -m "Lint" - -.PHONY: git-commit-message-mk-default -git-commit-message-mk-default: - -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" - -.PHONY: git-commit-message-rename-default -git-commit-message-rename-default: - -@$(GIT_COMMIT) -a -m "Rename" - -.PHONY: git-commit-message-sort-default -git-commit-message-sort-default: - -@$(GIT_COMMIT) -a -m "Sort" - -.PHONY: git-push-default -git-push-default: - -@$(GIT_PUSH) - -.PHONY: git-push-force-default -git-push-force-default: - -@$(GIT_PUSH_FORCE) - -.PHONY: git-commit-edit-default -git-commit-edit-default: - -$(GIT_COMMIT) -a - -.PHONY: git-prune-default -git-prune-default: - git remote update origin --prune - -.PHONY: git-set-upstream-default -git-set-upstream-default: - git push --set-upstream origin main - -.PHONY: git-set-default-default -git-set-default-default: - gh repo set-default - -.PHONY: git-short-default -git-short-default: - @echo $(GIT_REV) - -.PHONY: help-default -help-default: - @echo "Project Makefile 🤷" - @echo "Usage: make [options] [target] ..." - @echo "Examples:" - @echo " make help Print this message" - @echo " make list-defines list all defines in the Makefile" - @echo " make list-commands list all targets in the Makefile" - -.PHONY: jenkins-init-default -jenkins-init-default: - @echo "$$JENKINS_FILE" > Jenkinsfile - -.PHONY: makefile-list-commands-default -makefile-list-commands-default: - @for makefile in $(MAKEFILE_LIST); do \ - echo "Commands from $$makefile:"; \ - $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ - awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ - if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ - egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ - tr ' ' '\n' | \ - sort | \ - awk '{print $$0}' ; \ - echo; \ - done | $(PAGER) - -.PHONY: makefile-list-defines-default -makefile-list-defines-default: - @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile - -.PHONY: makefile-list-exports-default -makefile-list-exports-default: - @grep '^export [A-Z][A-Z_]*' Makefile - -.PHONY: makefile-list-targets-default -makefile-list-targets-default: - @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY - -.PHONY: make-default -make-default: - -$(GIT_ADD) Makefile - -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" - -git push - -.PHONY: npm-init-default -npm-init-default: - npm init -y - -$(GIT_ADD) package.json - -$(GIT_ADD) package-lock.json - -.PHONY: npm-build-default -npm-build-default: - npm run build - -.PHONY: npm-install-default -npm-install-default: - npm install - -$(GIT_ADD) package-lock.json - -.PHONY: npm-install-django-default -npm-install-django-default: - npm install \ - @fortawesome/fontawesome-free \ - @fortawesome/fontawesome-svg-core \ - @fortawesome/free-brands-svg-icons \ - @fortawesome/free-solid-svg-icons \ - @fortawesome/react-fontawesome \ - bootstrap \ - camelize \ - date-fns \ - history \ - mapbox-gl \ - query-string \ - react-animate-height \ - react-chartjs-2 \ - react-copy-to-clipboard \ - react-date-range \ - react-dom \ - react-dropzone \ - react-hook-form \ - react-image-crop \ - react-map-gl \ - react-modal \ - react-resize-detector \ - react-select \ - react-swipeable \ - snakeize \ - striptags \ - url-join \ - viewport-mercator-project - -.PHONY: npm-install-django-dev-default -npm-install-django-dev-default: - npm install \ - eslint-plugin-react \ - eslint-config-standard \ - eslint-config-standard-jsx \ - @babel/core \ - @babel/preset-env \ - @babel/preset-react \ - --save-dev - -.PHONY: npm-serve-default -npm-serve-default: - npm run start - -.PHONY: npm-test-default -npm-test-default: - npm run test - -.PHONY: pip-deps-default -pip-deps-default: - $(PIP_ENSURE) - python -m pip install pipdeptree - python -m pipdeptree - pipdeptree - -.PHONY: pip-freeze-default -pip-freeze-default: - $(PIP_ENSURE) - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-default -pip-init-default: - touch requirements.txt - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-test-default -pip-init-test-default: - @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt - -$(GIT_ADD) requirements-test.txt - -.PHONY: pip-install-default -pip-install-default: - $(PIP_ENSURE) - $(MAKE) pip-upgrade - python -m pip install wheel - python -m pip install -r requirements.txt - -.PHONY: pip-install-dev-default -pip-install-dev-default: - $(PIP_ENSURE) - python -m pip install -r requirements-dev.txt - -.PHONY: pip-install-test-default -pip-install-test-default: - $(PIP_ENSURE) - python -m pip install -r requirements-test.txt - -.PHONY: pip-install-upgrade-default -pip-install-upgrade-default: - cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - $(PIP_ENSURE) - python -m pip install -U -r requirements.txt - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -.PHONY: pip-upgrade-default -pip-upgrade-default: - $(PIP_ENSURE) - python -m pip install -U pip - -.PHONY: pip-uninstall-default -pip-uninstall-default: - $(PIP_ENSURE) - python -m pip freeze | xargs python -m pip uninstall -y - -.PHONY: plone-clean-default -plone-clean-default: - $(DEL_DIR) $(PROJECT_NAME) - $(DEL_DIR) $(PACKAGE_NAME) - -.PHONY: plone-init-default -plone-init-default: git-ignore plone-install plone-instance plone-serve - -.PHONY: plone-install-default -plone-install-default: - $(PIP_ENSURE) - python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) - -.PHONY: plone-instance-default -plone-instance-default: - mkwsgiinstance -d backend -u admin:admin - cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini - mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini - -$(GIT_ADD) backend/etc/site.zcml - -$(GIT_ADD) backend/etc/zope.conf - -$(GIT_ADD) backend/etc/zope.ini - -.PHONY: plone-serve-default -plone-serve-default: - runwsgi backend/etc/zope.ini - -.PHONY: plone-build-default -plone-build-default: - buildout - -.PHONY: programming-interview-default -programming-interview-default: - @echo "$$PROGRAMMING_INTERVIEW" > interview.py - @echo "Created interview.py!" - -@$(GIT_ADD) interview.py > /dev/null 2>&1 - -# .NOT_PHONY! -$(MAKEFILE_CUSTOM_FILE): - @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) - -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) - -.PHONY: python-license-default -python-license-default: - @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt - -$(GIT_ADD) LICENSE.txt - -.PHONY: python-project-default -python-project-default: - @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml - -$(GIT_ADD) pyproject.toml - -.PHONY: python-serve-default -python-serve-default: - @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" - python3 -m http.server - -.PHONY: python-sdist-default -python-sdist-default: - $(PIP_ENSURE) - python setup.py sdist --format=zip - -.PHONY: python-webpack-init-default -python-webpack-init-default: - python manage.py webpack_init --no-input - -.PHONY: python-ci-default -python-ci-default: - $(ADD_DIR) .github/workflows - @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml - -$(GIT_ADD) .github/workflows/build_wheels.yml - -.PHONY: rand-default -rand-default: - @openssl rand -base64 12 | sed 's/\///g' - -.PHONY: readme-init-default -readme-init-default: - @echo "# $(PROJECT_NAME)" > README.md - -$(GIT_ADD) README.md - -.PHONY: readme-edit-default -readme-edit-default: - $(EDITOR) README.md - -.PHONY: reveal-init-default -reveal-init-default: webpack-init-reveal - npm install \ - css-loader \ - mini-css-extract-plugin \ - reveal.js \ - style-loader - jq '.scripts += {"build": "webpack"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - -.PHONY: reveal-serve-default -reveal-serve-default: - npm run watch & - python -m http.server - -.PHONY: review-default -review-default: -ifeq ($(UNAME), Darwin) - $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` -else - @echo "Unsupported" -endif - -.PHONY: separator-default -separator-default: - @echo "$$SEPARATOR" - -.PHONY: sphinx-init-default -sphinx-init-default: sphinx-install - sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) - $(COPY_DIR) $(RANDIR)/* . - $(DEL_DIR) $(RANDIR) - -$(GIT_ADD) index.rst - -$(GIT_ADD) conf.py - $(DEL_FILE) make.bat - -@$(GIT_CHECKOUT) Makefile - $(MAKE) git-ignore - -.PHONY: sphinx-theme-init-default -sphinx-theme-init-default: - export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static - -.PHONY: sphinx-install-default -sphinx-install-default: - echo "Sphinx\n" > requirements.txt - @$(MAKE) pip-install - @$(MAKE) pip-freeze - -$(GIT_ADD) requirements.txt - -.PHONY: sphinx-build-default -sphinx-build-default: - sphinx-build -b html -d _build/doctrees . _build/html - sphinx-build -b rinoh . _build/rinoh - -.PHONY: sphinx-serve-default -sphinx-serve-default: - cd _build/html;python3 -m http.server - -.PHONY: wagtail-base-template-default -wagtail-base-template-default: - @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html - -.PHONY: wagtail-clean-default -wagtail-clean-default: - -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ - echo "Cleaning $$dir"; \ - $(DEL_DIR) $$dir >/dev/null 2>&1; \ - done - -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ - echo "Cleaning $$file"; \ - $(DEL_FILE) $$file >/dev/null 2>&1; \ - done - -.PHONY: wagtail-contactpage-default -wagtail-contactpage-default: - python manage.py startapp contactpage - @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py - @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py - $(ADD_DIR) contactpage/templates/contactpage/ - @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html - @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html - @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations contactpage - -$(GIT_ADD) contactpage/templates - -$(GIT_ADD) contactpage/*.py - -$(GIT_ADD) contactpage/migrations/*.py - -.PHONY: wagtail-header-prefix-template-default -wagtail-header-prefix-template-default: - @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html - -.PHONY: wagtail-home-default -wagtail-home-default: - @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py - @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html - $(ADD_DIR) home/templates/blocks - @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html - @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - python manage.py makemigrations home - -$(GIT_ADD) home/migrations/*.py - -.PHONY: wagtail-install-default -wagtail-install-default: - $(PIP_ENSURE) - python -m pip install \ - wagtail \ - wagtailmenus \ - wagtail-color-panel \ - wagtail-django-recaptcha \ - wagtail-markdown \ - wagtail-modeladmin \ - wagtail-seo \ - weasyprint \ - whitenoise \ - xhtml2pdf - -.PHONY: wagtail-private-default -wagtail-privacy-default: - python manage.py startapp privacy - @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py - $(ADD_DIR) privacy/templates - @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html - @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations privacy - -$(GIT_ADD) privacy/templates - -$(GIT_ADD) privacy/*.py - -$(GIT_ADD) privacy/migrations/*.py - -.PHONY: wagtail-project-default -wagtail-project-default: - wagtail start backend . - $(DEL_FILE) home/templates/home/welcome_page.html - -$(GIT_ADD) backend/ - -$(GIT_ADD) .dockerignore - -$(GIT_ADD) Dockerfile - -$(GIT_ADD) manage.py - -$(GIT_ADD) requirements.txt - -.PHONY: wagtail-search-default -wagtail-search-default: - @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html - @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: wagtail-settings-default -wagtail-settings-default: - @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: wagtail-sitepage-default -wagtail-sitepage-default: - python manage.py startapp sitepage - @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py - $(ADD_DIR) sitepage/templates/sitepage/ - @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html - @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations sitepage - -$(GIT_ADD) sitepage/templates - -$(GIT_ADD) sitepage/*.py - -$(GIT_ADD) sitepage/migrations/*.py - -.PHONY: wagtail-urls-default -wagtail-urls-default: - @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) - -.PHONY: wagtail-urls-home-default -wagtail-urls-home-default: - @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) - -.PHONY: webpack-init-default -webpack-init-default: npm-init - @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -.PHONY: webpack-init-reveal-default -webpack-init-reveal-default: npm-init - @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -# -------------------------------------------------------------------------------- -# Single-line phony target rules -# -------------------------------------------------------------------------------- - -.PHONY: aws-check-env-default -aws-check-env-default: aws-check-env-profile aws-check-env-region - -.PHONY: ce-default -ce-default: git-commit-edit git-push - -.PHONY: clean-default -clean-default: wagtail-clean - -.PHONY: cp-default -cp-default: git-commit-message git-push - -.PHONY: db-dump-default -db-dump-default: eb-export - -.PHONY: dbshell-default -dbshell-default: django-db-shell - -.PHONY: deploy-default -deploy-default: eb-deploy - -.PHONY: d-default -d-default: eb-deploy - -.PHONY: deps-default -deps-default: pip-deps - -.PHONY: e-default -e-default: edit - -.PHONY: edit-default -edit-default: readme-edit - -.PHONY: empty-default -empty-default: git-commit-message-empty git-push - -.PHONY: fp-default -fp-default: git-push-force - -.PHONY: freeze-default -freeze-default: pip-freeze git-push - -.PHONY: git-commit-default -git-commit-default: git-commit-message git-push - -.PHONY: git-commit-clean-default -git-commit-clean-default: git-commit-message-clean git-push - -.PHONY: git-commit-init-default -git-commit-init-default: git-commit-message-init git-push - -.PHONY: git-commit-lint-default -git-commit-lint-default: git-commit-message-lint git-push - -.PHONY: gitignore-default -gitignore-default: git-ignore - -.PHONY: h-default -h-default: help - -.PHONY: init-default -init-default: django-init-wagtail django-serve - -.PHONY: init-wagtail-default -init-wagtail-default: django-init-wagtail - -.PHONY: install-default -install-default: pip-install - -.PHONY: l-default -l-default: makefile-list-commands - -.PHONY: last-default -last-default: git-commit-message-last git-push - -.PHONY: lint-default -lint-default: django-lint - -.PHONY: list-commands-default -list-commands-default: makefile-list-commands - -.PHONY: list-defines-default -list-defines-default: makefile-list-defines - -.PHONY: list-exports-default -list-exports-default: makefile-list-exports - -.PHONY: list-targets-default -list-targets-default: makefile-list-targets - -.PHONY: migrate-default -migrate-default: django-migrate - -.PHONY: migrations-default -migrations-default: django-migrations-make - -.PHONY: migrations-show-default -migrations-show-default: django-migrations-show - -.PHONY: mk-default -mk-default: project.mk git-commit-message-mk git-push - -.PHONY: open-default -open-default: django-open - -.PHONY: o-default -o-default: django-open - -.PHONY: readme-default -readme-default: readme-init - -.PHONY: rename-default -rename-default: git-commit-message-rename git-push - -.PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell - -.PHONY: serve-default -serve-default: django-serve - -.PHONY: static-default -static-default: django-static - -.PHONY: sort-default -sort-default: git-commit-message-sort git-push - -.PHONY: su-default -su-default: django-su - -.PHONY: test-default -test-default: django-test - -.PHONY: t-default -t-default: django-test - -.PHONY: u-default -u-default: help - -.PHONY: urls-default -urls-default: django-urls-show - -# -------------------------------------------------------------------------------- -# Allow customizing rules defined in this Makefile with rules defined in -# $(MAKEFILE_CUSTOM_FILE) -# -------------------------------------------------------------------------------- - -%: %-default # https://stackoverflow.com/a/49804748 - @ true +.DEFAULT_GOAL := help + +.PHONY: clean +clean: + rm src/PIL/*.so || true + rm -r build || true + find . -name __pycache__ | xargs rm -r || true + +.PHONY: coverage +coverage: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + rm -r htmlcov || true + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report + +.PHONY: doc +.PHONY: html +doc html: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs html + +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + +.PHONY: doccheck +doccheck: + $(MAKE) doc +# Don't make our tests rely on the links in the docs being up every single build. +# We don't control them. But do check, and update them to the target of their redirects. + $(MAKE) -C docs linkcheck || true + +.PHONY: docserve +docserve: + cd docs/_build/html && python3 -m http.server 2> /dev/null& + +.PHONY: help +help: + @echo "Welcome to Pillow development. Please use \`make \` where is one of" + @echo " clean remove build products" + @echo " coverage run coverage test (in progress)" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" + @echo " install make and install" + @echo " install-coverage make and install with C coverage" + @echo " lint run the lint checks" + @echo " lint-fix run Ruff to (mostly) fix lint issues" + @echo " release-test run code and package tests before release" + @echo " test run tests on installed Pillow" + +.PHONY: install +install: + python3 -m pip -v install . + python3 selftest.py + +.PHONY: install-coverage +install-coverage: + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . + python3 selftest.py + +.PHONY: debug +debug: +# make a debug version if we don't have a -dbg python. Leaves in symbols +# 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 -v install . > /dev/null + +.PHONY: release-test +release-test: + python3 Tests/check_release_notes.py + python3 -m pip install -e .[tests] + python3 selftest.py + python3 -m pytest Tests + python3 -m pip install . + python3 -m pytest -qq + python3 -m check_manifest + python3 -m pyroma . + $(MAKE) readme + +.PHONY: sdist +sdist: + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine + python3 -m twine check --strict dist/* + +.PHONY: test +test: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + +.PHONY: readme +readme: + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html + + +.PHONY: lint +lint: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint + +.PHONY: lint-fix +lint-fix: + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -m black . + python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff + python3 -m ruff --fix . + +.PHONY: mypy +mypy: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e mypy From d49ea378115f4ae32e12f1f0c3e6146ebdc68fe0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 17:50:42 +1000 Subject: [PATCH 129/136] Include limit in error message --- Tests/test_file_webp.py | 4 +++- src/_webp.c | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ad08da364..a86757e64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -163,7 +163,9 @@ class TestFileWebp: im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: im.save(temp_file) - assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + assert ( + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + ) def test_WebPEncode_with_invalid_args(self) -> None: """ diff --git a/src/_webp.c b/src/_webp.c index 0d2d6f023..c0e1a6f63 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -673,9 +673,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { int error_code = (&pic)->error_code; - const char *message = ""; + char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { - message = ": Image size exceeds WebP limit"; + sprintf( + message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; From fed916825290cb1f45610017993c82ccca2b7843 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 18:48:28 +1000 Subject: [PATCH 130/136] Catch defusedxml warnings --- Tests/test_file_webp_metadata.py | 10 +++++++++- Tests/test_image.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4ef3d95f2..d9a834c75 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -116,7 +116,15 @@ def test_read_no_exif() -> None: def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} with Image.open("Tests/images/flower2.webp") as im: if ElementTree is None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..b1ecc6184 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -938,7 +938,15 @@ class TestImage: def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} def test_getxmp_padded(self) -> None: im = Image.new("RGB", (1, 1)) From e382ebed3ad757567ce8522c41a6c704c68c16a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 19:38:07 +1000 Subject: [PATCH 131/136] Remove warning if NumPy failed to raise an error during conversion --- Tests/test_image_array.py | 2 +- src/PIL/Image.py | 24 ++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb6064882..38425a515 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -47,7 +47,7 @@ def test_toarray() -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(UserWarning): + with pytest.warns(DeprecationWarning): numpy.array(im_truncated) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..2f1a878c1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -724,24 +724,12 @@ class Image: def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: # numpy array interface support new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} - try: - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - except Exception as e: - if not isinstance(e, (MemoryError, RecursionError)): - try: - import numpy - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if parse_version(numpy.__version__) < parse_version("1.23"): - warnings.warn(str(e)) - raise + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() new["shape"], new["typestr"] = _conv_type_shape(self) return new From 6e9518b88de595c56a955a20a7359ff93a7e9668 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 19:18:25 +1000 Subject: [PATCH 132/136] Added type hint to example code --- 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 ffc6bec34..37d8cb335 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1917,7 +1917,7 @@ class Image: object:: class Example(Image.ImagePointHandler): - def point(self, data): + def point(self, im: Image) -> Image: # Return result :param mode: Output mode (default is same as input). This can only be used if the source image has mode "L" or "P", and the output has mode "1" or the From 8aa58e320fd2dde581325c63d1673d6a2e3648f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 21:25:41 +1000 Subject: [PATCH 133/136] Rename _E to ImagePointTransform --- docs/reference/Image.rst | 2 +- src/PIL/Image.py | 48 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 02e714f20..bc3758218 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -362,8 +362,8 @@ Classes :undoc-members: :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImagePointTransform .. autoclass:: PIL.Image.ImageTransformHandler -.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 37d8cb335..5aa49b619 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -470,43 +470,53 @@ def _getencoder( # Simple expression analyzer -class _E: +class ImagePointTransform: + """ + Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than + 8 bits, this represents an affine transformation, where the value is multiplied by + ``scale`` and ``offset`` is added. + """ + def __init__(self, scale: float, offset: float) -> None: self.scale = scale self.offset = offset - def __neg__(self) -> _E: - return _E(-self.scale, -self.offset) + def __neg__(self) -> ImagePointTransform: + return ImagePointTransform(-self.scale, -self.offset) - def __add__(self, other: _E | float) -> _E: - if isinstance(other, _E): - return _E(self.scale + other.scale, self.offset + other.offset) - return _E(self.scale, self.offset + other) + def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return ImagePointTransform( + self.scale + other.scale, self.offset + other.offset + ) + return ImagePointTransform(self.scale, self.offset + other) __radd__ = __add__ - def __sub__(self, other: _E | float) -> _E: + def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return self + -other - def __rsub__(self, other: _E | float) -> _E: + def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return other + -self - def __mul__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale * other, self.offset * other) + return ImagePointTransform(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale / other, self.offset / other) + return ImagePointTransform(self.scale / other, self.offset / other) -def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: - a = expr(_E(1, 0)) - return (a.scale, a.offset) if isinstance(a, _E) else (0, a) +def _getscaleoffset( + expr: Callable[[ImagePointTransform], ImagePointTransform | float] +) -> tuple[float, float]: + a = expr(ImagePointTransform(1, 0)) + return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) # -------------------------------------------------------------------- @@ -1898,7 +1908,7 @@ class Image: Sequence[float] | NumpyArray | Callable[[int], float] - | Callable[[_E], _E | float] + | Callable[[ImagePointTransform], ImagePointTransform | float] | ImagePointHandler ), mode: str | None = None, From 658b60e3a33d6bf4e3ff3a2053cb73faa466772d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:26:56 +1000 Subject: [PATCH 134/136] Include units Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_webp.py | 2 +- src/_webp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a86757e64..4a048f2c2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,7 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index c0e1a6f63..b4cf9c329 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From a3e3639a59ffaf33430b08b089da1eedf04286ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:27:40 +0000 Subject: [PATCH 135/136] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp.py | 3 ++- src/_webp.c | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 4a048f2c2..247fc6021 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,8 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" + str(e.value) + == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index b4cf9c329..f59ad3036 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION + message, + ": Image size exceeds WebP limit of %d pixels", + WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From cfb093af39cf01fc631845d941e0bfda0bb4a52f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 23:15:35 +1000 Subject: [PATCH 136/136] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a7edc340c..9da76d82a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Updated error message when saving WebP with invalid width or height #8322 + [radarhere, hugovk] + +- Remove warning if NumPy failed to raise an error during conversion #8326 + [radarhere] + - If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 [radarhere]