Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +209 -0
- Lib/site-packages/.DS_Store +0 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/AUTHORS +59 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/INSTALLER +1 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/LICENSE +29 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/METADATA +295 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/RECORD +82 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/WHEEL +5 -0
- Lib/site-packages/GitPython-3.1.44.dist-info/top_level.txt +1 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER +1 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt +28 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA +92 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD +14 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL +5 -0
- Lib/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt +1 -0
- Lib/site-packages/PIL/BdfFontFile.py +133 -0
- Lib/site-packages/PIL/BlpImagePlugin.py +501 -0
- Lib/site-packages/PIL/BmpImagePlugin.py +511 -0
- Lib/site-packages/PIL/BufrStubImagePlugin.py +76 -0
- Lib/site-packages/PIL/ContainerIO.py +173 -0
- Lib/site-packages/PIL/CurImagePlugin.py +75 -0
- Lib/site-packages/PIL/DcxImagePlugin.py +80 -0
- Lib/site-packages/PIL/DdsImagePlugin.py +573 -0
- Lib/site-packages/PIL/EpsImagePlugin.py +474 -0
- Lib/site-packages/PIL/ExifTags.py +382 -0
- Lib/site-packages/PIL/FitsImagePlugin.py +152 -0
- Lib/site-packages/PIL/FliImagePlugin.py +175 -0
- Lib/site-packages/PIL/FontFile.py +134 -0
- Lib/site-packages/PIL/FpxImagePlugin.py +257 -0
- Lib/site-packages/PIL/FtexImagePlugin.py +115 -0
- Lib/site-packages/PIL/GbrImagePlugin.py +103 -0
- Lib/site-packages/PIL/GdImageFile.py +102 -0
- Lib/site-packages/PIL/GifImagePlugin.py +1197 -0
- Lib/site-packages/PIL/GimpGradientFile.py +149 -0
- Lib/site-packages/PIL/GimpPaletteFile.py +58 -0
- Lib/site-packages/PIL/GribStubImagePlugin.py +76 -0
- Lib/site-packages/PIL/Hdf5StubImagePlugin.py +76 -0
- Lib/site-packages/PIL/IcnsImagePlugin.py +412 -0
- Lib/site-packages/PIL/IcoImagePlugin.py +381 -0
- Lib/site-packages/PIL/ImImagePlugin.py +386 -0
- Lib/site-packages/PIL/Image.py +0 -0
- Lib/site-packages/PIL/ImageChops.py +311 -0
- Lib/site-packages/PIL/ImageCms.py +1125 -0
- Lib/site-packages/PIL/ImageColor.py +320 -0
- Lib/site-packages/PIL/ImageDraw.py +1218 -0
- Lib/site-packages/PIL/ImageDraw2.py +243 -0
- Lib/site-packages/PIL/ImageEnhance.py +113 -0
- Lib/site-packages/PIL/ImageFile.py +832 -0
- Lib/site-packages/PIL/ImageFilter.py +605 -0
- Lib/site-packages/PIL/ImageFont.py +1338 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,212 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Lib/site-packages/__pycache__/typing_extensions.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
Lib/site-packages/altair/vegalite/v5/__pycache__/api.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
Lib/site-packages/altair/vegalite/v5/schema/__pycache__/_config.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
Lib/site-packages/altair/vegalite/v5/schema/__pycache__/channels.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
Lib/site-packages/altair/vegalite/v5/schema/__pycache__/core.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
Lib/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
Lib/site-packages/charset_normalizer/md__mypyc.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
Lib/site-packages/click/__pycache__/core.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
Lib/site-packages/google/_upb/_message.pyd filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
Lib/site-packages/google/protobuf/__pycache__/descriptor_pb2.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
Lib/site-packages/idna/__pycache__/uts46data.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
Lib/site-packages/jinja2/__pycache__/compiler.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
Lib/site-packages/jsonschema/tests/__pycache__/test_validators.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
Lib/site-packages/narwhals/__pycache__/dataframe.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
Lib/site-packages/narwhals/__pycache__/expr.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
Lib/site-packages/narwhals/__pycache__/series.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
Lib/site-packages/numpy/_core/__pycache__/_add_newdocs.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
Lib/site-packages/numpy/_core/__pycache__/fromnumeric.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
Lib/site-packages/numpy/_core/_multiarray_umath.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
Lib/site-packages/numpy/_core/_simd.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
Lib/site-packages/numpy/_core/lib/npymath.lib filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_datetime.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_dtype.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_multiarray.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_nditer.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_numeric.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_regression.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_ufunc.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
Lib/site-packages/numpy/_core/tests/__pycache__/test_umath.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
Lib/site-packages/numpy/f2py/__pycache__/crackfortran.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
Lib/site-packages/numpy/fft/_pocketfft_umath.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
Lib/site-packages/numpy/lib/__pycache__/_function_base_impl.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
Lib/site-packages/numpy/lib/tests/__pycache__/test_function_base.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
Lib/site-packages/numpy/lib/tests/__pycache__/test_io.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
Lib/site-packages/numpy/linalg/__pycache__/_linalg.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
Lib/site-packages/numpy/linalg/_umath_linalg.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 72 |
+
Lib/site-packages/numpy/linalg/tests/__pycache__/test_linalg.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
Lib/site-packages/numpy/ma/__pycache__/core.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
Lib/site-packages/numpy/ma/tests/__pycache__/test_core.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
Lib/site-packages/numpy/ma/tests/__pycache__/test_extras.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
Lib/site-packages/numpy/random/_bounded_integers.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
Lib/site-packages/numpy/random/_common.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
Lib/site-packages/numpy/random/_generator.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
Lib/site-packages/numpy/random/bit_generator.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
Lib/site-packages/numpy/random/lib/npyrandom.lib filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
Lib/site-packages/numpy/random/mtrand.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
Lib/site-packages/numpy/random/tests/__pycache__/test_generator_mt19937.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
Lib/site-packages/numpy/random/tests/__pycache__/test_random.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
Lib/site-packages/numpy/random/tests/__pycache__/test_randomstate.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
Lib/site-packages/numpy/testing/_private/__pycache__/utils.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
Lib/site-packages/numpy/testing/tests/__pycache__/test_utils.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 87 |
+
Lib/site-packages/numpy.libs/libscipy_openblas64_-43e11ff0749b8cbe0a615c9cf6737e0e.dll filter=lfs diff=lfs merge=lfs -text
|
| 88 |
+
Lib/site-packages/numpy.libs/msvcp140-d64049c6e3865410a7dda6a7e9f0c575.dll filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
Lib/site-packages/pandas/_libs/algos.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
Lib/site-packages/pandas/_libs/groupby.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
Lib/site-packages/pandas/_libs/hashing.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
Lib/site-packages/pandas/_libs/hashtable.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
Lib/site-packages/pandas/_libs/index.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
Lib/site-packages/pandas/_libs/internals.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
Lib/site-packages/pandas/_libs/interval.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
Lib/site-packages/pandas/_libs/join.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
Lib/site-packages/pandas/_libs/lib.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
Lib/site-packages/pandas/_libs/missing.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
Lib/site-packages/pandas/_libs/ops.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
Lib/site-packages/pandas/_libs/parsers.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
Lib/site-packages/pandas/_libs/reshape.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
Lib/site-packages/pandas/_libs/sas.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
Lib/site-packages/pandas/_libs/sparse.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
Lib/site-packages/pandas/_libs/tslib.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
Lib/site-packages/pandas/_libs/tslibs/conversion.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
Lib/site-packages/pandas/_libs/tslibs/dtypes.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
Lib/site-packages/pandas/_libs/tslibs/fields.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 108 |
+
Lib/site-packages/pandas/_libs/tslibs/nattype.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 109 |
+
Lib/site-packages/pandas/_libs/tslibs/np_datetime.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 110 |
+
Lib/site-packages/pandas/_libs/tslibs/offsets.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 111 |
+
Lib/site-packages/pandas/_libs/tslibs/parsing.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 112 |
+
Lib/site-packages/pandas/_libs/tslibs/period.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 113 |
+
Lib/site-packages/pandas/_libs/tslibs/strptime.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 114 |
+
Lib/site-packages/pandas/_libs/tslibs/timedeltas.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 115 |
+
Lib/site-packages/pandas/_libs/tslibs/timestamps.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 116 |
+
Lib/site-packages/pandas/_libs/tslibs/timezones.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 117 |
+
Lib/site-packages/pandas/_libs/tslibs/tzconversion.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 118 |
+
Lib/site-packages/pandas/_libs/tslibs/vectorized.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 119 |
+
Lib/site-packages/pandas/_libs/window/aggregations.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 120 |
+
Lib/site-packages/pandas/_libs/window/indexers.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 121 |
+
Lib/site-packages/pandas/_libs/writers.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 122 |
+
Lib/site-packages/pandas/core/__pycache__/frame.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 123 |
+
Lib/site-packages/pandas/core/__pycache__/generic.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 124 |
+
Lib/site-packages/pandas/core/__pycache__/indexing.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 125 |
+
Lib/site-packages/pandas/core/__pycache__/resample.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 126 |
+
Lib/site-packages/pandas/core/__pycache__/series.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 127 |
+
Lib/site-packages/pandas/core/arrays/__pycache__/categorical.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 128 |
+
Lib/site-packages/pandas/core/arrays/arrow/__pycache__/array.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 129 |
+
Lib/site-packages/pandas/core/groupby/__pycache__/generic.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 130 |
+
Lib/site-packages/pandas/core/groupby/__pycache__/groupby.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 131 |
+
Lib/site-packages/pandas/core/indexes/__pycache__/base.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 132 |
+
Lib/site-packages/pandas/core/indexes/__pycache__/multi.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 133 |
+
Lib/site-packages/pandas/core/strings/__pycache__/accessor.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 134 |
+
Lib/site-packages/pandas/io/__pycache__/pytables.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 135 |
+
Lib/site-packages/pandas/io/__pycache__/sql.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 136 |
+
Lib/site-packages/pandas/io/__pycache__/stata.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 137 |
+
Lib/site-packages/pandas/io/formats/__pycache__/style.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 138 |
+
Lib/site-packages/pandas/tests/__pycache__/test_algos.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 139 |
+
Lib/site-packages/pandas/tests/arithmetic/__pycache__/test_datetime64.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 140 |
+
Lib/site-packages/pandas/tests/arithmetic/__pycache__/test_timedelta64.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 141 |
+
Lib/site-packages/pandas/tests/computation/__pycache__/test_eval.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 142 |
+
Lib/site-packages/pandas/tests/copy_view/__pycache__/test_methods.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 143 |
+
Lib/site-packages/pandas/tests/dtypes/__pycache__/test_inference.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 144 |
+
Lib/site-packages/pandas/tests/extension/__pycache__/test_arrow.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 145 |
+
Lib/site-packages/pandas/tests/frame/__pycache__/test_arithmetic.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 146 |
+
Lib/site-packages/pandas/tests/frame/__pycache__/test_constructors.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 147 |
+
Lib/site-packages/pandas/tests/frame/__pycache__/test_reductions.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 148 |
+
Lib/site-packages/pandas/tests/frame/__pycache__/test_stack_unstack.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 149 |
+
Lib/site-packages/pandas/tests/frame/indexing/__pycache__/test_indexing.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 150 |
+
Lib/site-packages/pandas/tests/groupby/__pycache__/test_categorical.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 151 |
+
Lib/site-packages/pandas/tests/groupby/__pycache__/test_groupby.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 152 |
+
Lib/site-packages/pandas/tests/indexing/__pycache__/test_loc.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 153 |
+
Lib/site-packages/pandas/tests/io/__pycache__/test_sql.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 154 |
+
Lib/site-packages/pandas/tests/io/__pycache__/test_stata.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 155 |
+
Lib/site-packages/pandas/tests/io/formats/__pycache__/test_format.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 156 |
+
Lib/site-packages/pandas/tests/io/json/__pycache__/test_pandas.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 157 |
+
Lib/site-packages/pandas/tests/plotting/__pycache__/test_datetimelike.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 158 |
+
Lib/site-packages/pandas/tests/plotting/frame/__pycache__/test_frame.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 159 |
+
Lib/site-packages/pandas/tests/resample/__pycache__/test_datetime_index.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 160 |
+
Lib/site-packages/pandas/tests/reshape/__pycache__/test_pivot.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 161 |
+
Lib/site-packages/pandas/tests/reshape/merge/__pycache__/test_merge.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 162 |
+
Lib/site-packages/pandas/tests/series/__pycache__/test_constructors.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 163 |
+
Lib/site-packages/pandas/tests/tools/__pycache__/test_to_datetime.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 164 |
+
Lib/site-packages/pandas.libs/msvcp140-0f2ea95580b32bcfc81c235d5751ce78.dll filter=lfs diff=lfs merge=lfs -text
|
| 165 |
+
Lib/site-packages/PIL/__pycache__/Image.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 166 |
+
Lib/site-packages/PIL/_imaging.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 167 |
+
Lib/site-packages/PIL/_imagingcms.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 168 |
+
Lib/site-packages/PIL/_imagingft.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 169 |
+
Lib/site-packages/PIL/_webp.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 170 |
+
Lib/site-packages/pip/_vendor/__pycache__/typing_extensions.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 171 |
+
Lib/site-packages/pip/_vendor/distlib/t64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 172 |
+
Lib/site-packages/pip/_vendor/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text
|
| 173 |
+
Lib/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 174 |
+
Lib/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
|
| 175 |
+
Lib/site-packages/pip/_vendor/idna/__pycache__/uts46data.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 176 |
+
Lib/site-packages/pip/_vendor/pkg_resources/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 177 |
+
Lib/site-packages/pip/_vendor/rich/__pycache__/_emoji_codes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 178 |
+
Lib/site-packages/pip/_vendor/rich/__pycache__/console.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 179 |
+
Lib/site-packages/pyarrow/_acero.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 180 |
+
Lib/site-packages/pyarrow/_compute.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 181 |
+
Lib/site-packages/pyarrow/_csv.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 182 |
+
Lib/site-packages/pyarrow/_dataset.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 183 |
+
Lib/site-packages/pyarrow/_dataset_parquet.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 184 |
+
Lib/site-packages/pyarrow/_flight.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 185 |
+
Lib/site-packages/pyarrow/_fs.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 186 |
+
Lib/site-packages/pyarrow/_orc.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 187 |
+
Lib/site-packages/pyarrow/_parquet.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 188 |
+
Lib/site-packages/pyarrow/_parquet_encryption.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 189 |
+
Lib/site-packages/pyarrow/_s3fs.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 190 |
+
Lib/site-packages/pyarrow/_substrait.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 191 |
+
Lib/site-packages/pyarrow/arrow.dll filter=lfs diff=lfs merge=lfs -text
|
| 192 |
+
Lib/site-packages/pyarrow/arrow.lib filter=lfs diff=lfs merge=lfs -text
|
| 193 |
+
Lib/site-packages/pyarrow/arrow_acero.dll filter=lfs diff=lfs merge=lfs -text
|
| 194 |
+
Lib/site-packages/pyarrow/arrow_acero.lib filter=lfs diff=lfs merge=lfs -text
|
| 195 |
+
Lib/site-packages/pyarrow/arrow_dataset.dll filter=lfs diff=lfs merge=lfs -text
|
| 196 |
+
Lib/site-packages/pyarrow/arrow_dataset.lib filter=lfs diff=lfs merge=lfs -text
|
| 197 |
+
Lib/site-packages/pyarrow/arrow_flight.dll filter=lfs diff=lfs merge=lfs -text
|
| 198 |
+
Lib/site-packages/pyarrow/arrow_flight.lib filter=lfs diff=lfs merge=lfs -text
|
| 199 |
+
Lib/site-packages/pyarrow/arrow_python.dll filter=lfs diff=lfs merge=lfs -text
|
| 200 |
+
Lib/site-packages/pyarrow/arrow_python.lib filter=lfs diff=lfs merge=lfs -text
|
| 201 |
+
Lib/site-packages/pyarrow/arrow_substrait.dll filter=lfs diff=lfs merge=lfs -text
|
| 202 |
+
Lib/site-packages/pyarrow/arrow_substrait.lib filter=lfs diff=lfs merge=lfs -text
|
| 203 |
+
Lib/site-packages/pyarrow/lib.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 204 |
+
Lib/site-packages/pyarrow/msvcp140-c691275f5538a516494cdd39ad3f5ca6.dll filter=lfs diff=lfs merge=lfs -text
|
| 205 |
+
Lib/site-packages/pyarrow/parquet.dll filter=lfs diff=lfs merge=lfs -text
|
| 206 |
+
Lib/site-packages/pyarrow/parquet.lib filter=lfs diff=lfs merge=lfs -text
|
| 207 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_array.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 208 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_compute.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 209 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_convert_builtin.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 210 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_csv.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 211 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_dataset.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 212 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_extension_type.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 213 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_flight.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 214 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_io.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 215 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_pandas.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 216 |
+
Lib/site-packages/pyarrow/tests/__pycache__/test_table.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 217 |
+
Lib/site-packages/pydantic/__pycache__/json_schema.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 218 |
+
Lib/site-packages/pydantic/_internal/__pycache__/_generate_schema.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 219 |
+
Lib/site-packages/pydantic_core/__pycache__/core_schema.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 220 |
+
Lib/site-packages/pydantic_core/_pydantic_core.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 221 |
+
Lib/site-packages/pydeck/nbextension/static/index.js.map filter=lfs diff=lfs merge=lfs -text
|
| 222 |
+
Lib/site-packages/pygments/lexers/__pycache__/lisp.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 223 |
+
Lib/site-packages/rich/__pycache__/_emoji_codes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 224 |
+
Lib/site-packages/rich/__pycache__/console.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 225 |
+
Lib/site-packages/rpds/rpds.cp312-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
|
| 226 |
+
Lib/site-packages/streamlit/static/static/media/fireworks.B4d-_KUe.gif filter=lfs diff=lfs merge=lfs -text
|
| 227 |
+
Lib/site-packages/streamlit/static/static/media/MaterialSymbols-Rounded.MSqyuJUI.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 228 |
+
Lib/site-packages/tornado/__pycache__/web.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 229 |
+
Lib/site-packages/tornado/test/__pycache__/web_test.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 230 |
+
Scripts/f2py.exe filter=lfs diff=lfs merge=lfs -text
|
| 231 |
+
Scripts/httpx.exe filter=lfs diff=lfs merge=lfs -text
|
| 232 |
+
Scripts/jsonschema.exe filter=lfs diff=lfs merge=lfs -text
|
| 233 |
+
Scripts/markdown-it.exe filter=lfs diff=lfs merge=lfs -text
|
| 234 |
+
Scripts/normalizer.exe filter=lfs diff=lfs merge=lfs -text
|
| 235 |
+
Scripts/numpy-config.exe filter=lfs diff=lfs merge=lfs -text
|
| 236 |
+
Scripts/pip.exe filter=lfs diff=lfs merge=lfs -text
|
| 237 |
+
Scripts/pip3.12.exe filter=lfs diff=lfs merge=lfs -text
|
| 238 |
+
Scripts/pip3.exe filter=lfs diff=lfs merge=lfs -text
|
| 239 |
+
Scripts/pygmentize.exe filter=lfs diff=lfs merge=lfs -text
|
| 240 |
+
Scripts/python.exe filter=lfs diff=lfs merge=lfs -text
|
| 241 |
+
Scripts/pythonw.exe filter=lfs diff=lfs merge=lfs -text
|
| 242 |
+
Scripts/streamlit.exe filter=lfs diff=lfs merge=lfs -text
|
| 243 |
+
Scripts/watchmedo.exe filter=lfs diff=lfs merge=lfs -text
|
| 244 |
+
share/jupyter/nbextensions/pydeck/index.js.map filter=lfs diff=lfs merge=lfs -text
|
Lib/site-packages/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
Lib/site-packages/GitPython-3.1.44.dist-info/AUTHORS
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GitPython was originally written by Michael Trier.
|
| 2 |
+
GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich.
|
| 3 |
+
|
| 4 |
+
Contributors are:
|
| 5 |
+
|
| 6 |
+
-Michael Trier <mtrier _at_ gmail.com>
|
| 7 |
+
-Alan Briolat
|
| 8 |
+
-Florian Apolloner <florian _at_ apolloner.eu>
|
| 9 |
+
-David Aguilar <davvid _at_ gmail.com>
|
| 10 |
+
-Jelmer Vernooij <jelmer _at_ samba.org>
|
| 11 |
+
-Steve Frécinaux <code _at_ istique.net>
|
| 12 |
+
-Kai Lautaportti <kai _at_ lautaportti.fi>
|
| 13 |
+
-Paul Sowden <paul _at_ idontsmoke.co.uk>
|
| 14 |
+
-Sebastian Thiel <byronimo _at_ gmail.com>
|
| 15 |
+
-Jonathan Chu <jonathan.chu _at_ me.com>
|
| 16 |
+
-Vincent Driessen <me _at_ nvie.com>
|
| 17 |
+
-Phil Elson <pelson _dot_ pub _at_ gmail.com>
|
| 18 |
+
-Bernard `Guyzmo` Pratz <guyzmo+gitpython+pub@m0g.net>
|
| 19 |
+
-Timothy B. Hartman <tbhartman _at_ gmail.com>
|
| 20 |
+
-Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
|
| 21 |
+
-Peter Jones <pjones _at_ redhat.com>
|
| 22 |
+
-Anson Mansfield <anson.mansfield _at_ gmail.com>
|
| 23 |
+
-Ken Odegard <ken.odegard _at_ gmail.com>
|
| 24 |
+
-Alexis Horgix Chotard
|
| 25 |
+
-Piotr Babij <piotr.babij _at_ gmail.com>
|
| 26 |
+
-Mikuláš Poul <mikulaspoul _at_ gmail.com>
|
| 27 |
+
-Charles Bouchard-Légaré <cblegare.atl _at_ ntis.ca>
|
| 28 |
+
-Yaroslav Halchenko <debian _at_ onerussian.com>
|
| 29 |
+
-Tim Swast <swast _at_ google.com>
|
| 30 |
+
-William Luc Ritchie
|
| 31 |
+
-David Host <hostdm _at_ outlook.com>
|
| 32 |
+
-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
|
| 33 |
+
-Steven Whitman <ninloot _at_ gmail.com>
|
| 34 |
+
-Stefan Stancu <stefan.stancu _at_ gmail.com>
|
| 35 |
+
-César Izurieta <cesar _at_ caih.org>
|
| 36 |
+
-Arthur Milchior <arthur _at_ milchior.fr>
|
| 37 |
+
-Anil Khatri <anil.soccer.khatri _at_ gmail.com>
|
| 38 |
+
-JJ Graham <thetwoj _at_ gmail.com>
|
| 39 |
+
-Ben Thayer <ben _at_ benthayer.com>
|
| 40 |
+
-Dries Kennes <admin _at_ dries007.net>
|
| 41 |
+
-Pratik Anurag <panurag247365 _at_ gmail.com>
|
| 42 |
+
-Harmon <harmon.public _at_ gmail.com>
|
| 43 |
+
-Liam Beguin <liambeguin _at_ gmail.com>
|
| 44 |
+
-Ram Rachum <ram _at_ rachum.com>
|
| 45 |
+
-Alba Mendez <me _at_ alba.sh>
|
| 46 |
+
-Robert Westman <robert _at_ byteflux.io>
|
| 47 |
+
-Hugo van Kemenade
|
| 48 |
+
-Hiroki Tokunaga <tokusan441 _at_ gmail.com>
|
| 49 |
+
-Julien Mauroy <pro.julien.mauroy _at_ gmail.com>
|
| 50 |
+
-Patrick Gerard
|
| 51 |
+
-Luke Twist <itsluketwist@gmail.com>
|
| 52 |
+
-Joseph Hale <me _at_ jhale.dev>
|
| 53 |
+
-Santos Gallegos <stsewd _at_ proton.me>
|
| 54 |
+
-Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
|
| 55 |
+
-Eliah Kagan <eliah.kagan _at_ gmail.com>
|
| 56 |
+
-Ethan Lin <et.repositories _at_ gmail.com>
|
| 57 |
+
-Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
|
| 58 |
+
|
| 59 |
+
Portions derived from other open source works and are clearly marked.
|
Lib/site-packages/GitPython-3.1.44.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
Lib/site-packages/GitPython-3.1.44.dist-info/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (C) 2008, 2009 Michael Trier and contributors
|
| 2 |
+
All rights reserved.
|
| 3 |
+
|
| 4 |
+
Redistribution and use in source and binary forms, with or without
|
| 5 |
+
modification, are permitted provided that the following conditions
|
| 6 |
+
are met:
|
| 7 |
+
|
| 8 |
+
* Redistributions of source code must retain the above copyright
|
| 9 |
+
notice, this list of conditions and the following disclaimer.
|
| 10 |
+
|
| 11 |
+
* Redistributions in binary form must reproduce the above copyright
|
| 12 |
+
notice, this list of conditions and the following disclaimer in the
|
| 13 |
+
documentation and/or other materials provided with the distribution.
|
| 14 |
+
|
| 15 |
+
* Neither the name of the GitPython project nor the names of
|
| 16 |
+
its contributors may be used to endorse or promote products derived
|
| 17 |
+
from this software without specific prior written permission.
|
| 18 |
+
|
| 19 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| 20 |
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| 21 |
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| 22 |
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| 23 |
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| 24 |
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
| 25 |
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
| 26 |
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
| 27 |
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
| 28 |
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 29 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
Lib/site-packages/GitPython-3.1.44.dist-info/METADATA
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: GitPython
|
| 3 |
+
Version: 3.1.44
|
| 4 |
+
Summary: GitPython is a Python library used to interact with Git repositories
|
| 5 |
+
Home-page: https://github.com/gitpython-developers/GitPython
|
| 6 |
+
Author: Sebastian Thiel, Michael Trier
|
| 7 |
+
Author-email: byronimo@gmail.com, mtrier@gmail.com
|
| 8 |
+
License: BSD-3-Clause
|
| 9 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 10 |
+
Classifier: Environment :: Console
|
| 11 |
+
Classifier: Intended Audience :: Developers
|
| 12 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 13 |
+
Classifier: Operating System :: OS Independent
|
| 14 |
+
Classifier: Operating System :: POSIX
|
| 15 |
+
Classifier: Operating System :: Microsoft :: Windows
|
| 16 |
+
Classifier: Operating System :: MacOS :: MacOS X
|
| 17 |
+
Classifier: Typing :: Typed
|
| 18 |
+
Classifier: Programming Language :: Python
|
| 19 |
+
Classifier: Programming Language :: Python :: 3
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 25 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 26 |
+
Requires-Python: >=3.7
|
| 27 |
+
Description-Content-Type: text/markdown
|
| 28 |
+
License-File: LICENSE
|
| 29 |
+
License-File: AUTHORS
|
| 30 |
+
Requires-Dist: gitdb<5,>=4.0.1
|
| 31 |
+
Requires-Dist: typing-extensions>=3.7.4.3; python_version < "3.8"
|
| 32 |
+
Provides-Extra: test
|
| 33 |
+
Requires-Dist: coverage[toml]; extra == "test"
|
| 34 |
+
Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
|
| 35 |
+
Requires-Dist: mock; python_version < "3.8" and extra == "test"
|
| 36 |
+
Requires-Dist: mypy; extra == "test"
|
| 37 |
+
Requires-Dist: pre-commit; extra == "test"
|
| 38 |
+
Requires-Dist: pytest>=7.3.1; extra == "test"
|
| 39 |
+
Requires-Dist: pytest-cov; extra == "test"
|
| 40 |
+
Requires-Dist: pytest-instafail; extra == "test"
|
| 41 |
+
Requires-Dist: pytest-mock; extra == "test"
|
| 42 |
+
Requires-Dist: pytest-sugar; extra == "test"
|
| 43 |
+
Requires-Dist: typing-extensions; python_version < "3.11" and extra == "test"
|
| 44 |
+
Provides-Extra: doc
|
| 45 |
+
Requires-Dist: sphinx<7.2,>=7.1.2; extra == "doc"
|
| 46 |
+
Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
| 47 |
+
Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
|
| 48 |
+
|
| 49 |
+

|
| 50 |
+
[](https://readthedocs.org/projects/gitpython/?badge=stable)
|
| 51 |
+
[](https://repology.org/metapackage/python:gitpython/versions)
|
| 52 |
+
|
| 53 |
+
## [Gitoxide](https://github.com/Byron/gitoxide): A peek into the future…
|
| 54 |
+
|
| 55 |
+
I started working on GitPython in 2009, back in the days when Python was 'my thing' and I had great plans with it.
|
| 56 |
+
Of course, back in the days, I didn't really know what I was doing and this shows in many places. Somewhat similar to
|
| 57 |
+
Python this happens to be 'good enough', but at the same time is deeply flawed and broken beyond repair.
|
| 58 |
+
|
| 59 |
+
By now, GitPython is widely used and I am sure there is a good reason for that, it's something to be proud of and happy about.
|
| 60 |
+
The community is maintaining the software and is keeping it relevant for which I am absolutely grateful. For the time to come I am happy to continue maintaining GitPython, remaining hopeful that one day it won't be needed anymore.
|
| 61 |
+
|
| 62 |
+
More than 15 years after my first meeting with 'git' I am still in excited about it, and am happy to finally have the tools and
|
| 63 |
+
probably the skills to scratch that itch of mine: implement `git` in a way that makes tool creation a piece of cake for most.
|
| 64 |
+
|
| 65 |
+
If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
|
| 66 |
+
implementation of 'git' in [Rust](https://www.rust-lang.org).
|
| 67 |
+
|
| 68 |
+
*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)*
|
| 69 |
+
|
| 70 |
+
## GitPython
|
| 71 |
+
|
| 72 |
+
GitPython is a python library used to interact with git repositories, high-level like git-porcelain,
|
| 73 |
+
or low-level like git-plumbing.
|
| 74 |
+
|
| 75 |
+
It provides abstractions of git objects for easy access of repository data often backed by calling the `git`
|
| 76 |
+
command-line program.
|
| 77 |
+
|
| 78 |
+
### DEVELOPMENT STATUS
|
| 79 |
+
|
| 80 |
+
This project is in **maintenance mode**, which means that
|
| 81 |
+
|
| 82 |
+
- …there will be no feature development, unless these are contributed
|
| 83 |
+
- …there will be no bug fixes, unless they are relevant to the safety of users, or contributed
|
| 84 |
+
- …issues will be responded to with waiting times of up to a month
|
| 85 |
+
|
| 86 |
+
The project is open to contributions of all kinds, as well as new maintainers.
|
| 87 |
+
|
| 88 |
+
### REQUIREMENTS
|
| 89 |
+
|
| 90 |
+
GitPython needs the `git` executable to be installed on the system and available in your
|
| 91 |
+
`PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it
|
| 92 |
+
by setting the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
|
| 93 |
+
|
| 94 |
+
- Git (1.7.x or newer)
|
| 95 |
+
- Python >= 3.7
|
| 96 |
+
|
| 97 |
+
The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
|
| 98 |
+
The installer takes care of installing them for you.
|
| 99 |
+
|
| 100 |
+
### INSTALL
|
| 101 |
+
|
| 102 |
+
GitPython and its required package dependencies can be installed in any of the following ways, all of which should typically be done in a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
| 103 |
+
|
| 104 |
+
#### From PyPI
|
| 105 |
+
|
| 106 |
+
To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run:
|
| 107 |
+
|
| 108 |
+
```sh
|
| 109 |
+
pip install GitPython
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
(A distribution package can also be downloaded for manual installation at [the PyPI page](https://pypi.org/project/GitPython/).)
|
| 113 |
+
|
| 114 |
+
#### From downloaded source code
|
| 115 |
+
|
| 116 |
+
If you have downloaded the source code, run this from inside the unpacked `GitPython` directory:
|
| 117 |
+
|
| 118 |
+
```sh
|
| 119 |
+
pip install .
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
#### By cloning the source code repository
|
| 123 |
+
|
| 124 |
+
To clone the [the GitHub repository](https://github.com/gitpython-developers/GitPython) from source to work on the code, you can do it like so:
|
| 125 |
+
|
| 126 |
+
```sh
|
| 127 |
+
git clone https://github.com/gitpython-developers/GitPython
|
| 128 |
+
cd GitPython
|
| 129 |
+
./init-tests-after-clone.sh
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
|
| 133 |
+
|
| 134 |
+
If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`):
|
| 135 |
+
|
| 136 |
+
```sh
|
| 137 |
+
gh repo clone GitPython
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
| 141 |
+
|
| 142 |
+
Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs):
|
| 143 |
+
|
| 144 |
+
```sh
|
| 145 |
+
pip install -e ".[test]"
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
In the less common case that you do not want to install test dependencies, `pip install -e .` can be used instead.
|
| 149 |
+
|
| 150 |
+
#### With editable *dependencies* (not preferred, and rarely needed)
|
| 151 |
+
|
| 152 |
+
In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediately reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
|
| 153 |
+
|
| 154 |
+
If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
|
| 155 |
+
|
| 156 |
+
```sh
|
| 157 |
+
pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
The submodules must have been cloned for that to work, but that will already be the case if you have run `./init-tests-after-clone.sh`. You can use `pip list` to check which packages are installed editably and which are installed normally.
|
| 161 |
+
|
| 162 |
+
To reiterate, this approach should only rarely be used. For most development it is preferable to allow the gitdb and smmap dependencices to be retrieved automatically from PyPI in their latest stable packaged versions.
|
| 163 |
+
|
| 164 |
+
### Limitations
|
| 165 |
+
|
| 166 |
+
#### Leakage of System Resources
|
| 167 |
+
|
| 168 |
+
GitPython is not suited for long-running processes (like daemons) as it tends to
|
| 169 |
+
leak system resources. It was written in a time where destructors (as implemented
|
| 170 |
+
in the `__del__` method) still ran deterministically.
|
| 171 |
+
|
| 172 |
+
In case you still want to use it in such a context, you will want to search the
|
| 173 |
+
codebase for `__del__` implementations and call these yourself when you see fit.
|
| 174 |
+
|
| 175 |
+
Another way assure proper cleanup of resources is to factor out GitPython into a
|
| 176 |
+
separate process which can be dropped periodically.
|
| 177 |
+
|
| 178 |
+
#### Windows support
|
| 179 |
+
|
| 180 |
+
See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525).
|
| 181 |
+
|
| 182 |
+
### RUNNING TESTS
|
| 183 |
+
|
| 184 |
+
_Important_: Right after cloning this repository, please be sure to have executed
|
| 185 |
+
the `./init-tests-after-clone.sh` script in the repository root. Otherwise
|
| 186 |
+
you will encounter test failures.
|
| 187 |
+
|
| 188 |
+
#### Install test dependencies
|
| 189 |
+
|
| 190 |
+
Ensure testing libraries are installed. This is taken care of already if you installed with:
|
| 191 |
+
|
| 192 |
+
```sh
|
| 193 |
+
pip install -e ".[test]"
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
If you had installed with a command like `pip install -e .` instead, you can still run
|
| 197 |
+
the above command to add the testing dependencies.
|
| 198 |
+
|
| 199 |
+
#### Test commands
|
| 200 |
+
|
| 201 |
+
To test, run:
|
| 202 |
+
|
| 203 |
+
```sh
|
| 204 |
+
pytest
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
To lint, and apply some linting fixes as well as automatic code formatting, run:
|
| 208 |
+
|
| 209 |
+
```sh
|
| 210 |
+
pre-commit run --all-files
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
This includes the linting and autoformatting done by Ruff, as well as some other checks.
|
| 214 |
+
|
| 215 |
+
To typecheck, run:
|
| 216 |
+
|
| 217 |
+
```sh
|
| 218 |
+
mypy
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
#### CI (and tox)
|
| 222 |
+
|
| 223 |
+
Style and formatting checks, and running tests on all the different supported Python versions, will be performed:
|
| 224 |
+
|
| 225 |
+
- Upon submitting a pull request.
|
| 226 |
+
- On each push, *if* you have a fork with GitHub Actions enabled.
|
| 227 |
+
- Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
|
| 228 |
+
|
| 229 |
+
#### Configuration files
|
| 230 |
+
|
| 231 |
+
Specific tools are all configured in the `./pyproject.toml` file:
|
| 232 |
+
|
| 233 |
+
- `pytest` (test runner)
|
| 234 |
+
- `coverage.py` (code coverage)
|
| 235 |
+
- `ruff` (linter and formatter)
|
| 236 |
+
- `mypy` (type checker)
|
| 237 |
+
|
| 238 |
+
Orchestration tools:
|
| 239 |
+
|
| 240 |
+
- Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file.
|
| 241 |
+
- Configuration for `tox` is in `./tox.ini`.
|
| 242 |
+
- Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`.
|
| 243 |
+
|
| 244 |
+
### Contributions
|
| 245 |
+
|
| 246 |
+
Please have a look at the [contributions file][contributing].
|
| 247 |
+
|
| 248 |
+
### INFRASTRUCTURE
|
| 249 |
+
|
| 250 |
+
- [User Documentation](http://gitpython.readthedocs.org)
|
| 251 |
+
- [Questions and Answers](http://stackexchange.com/filters/167317/gitpython)
|
| 252 |
+
- Please post on Stack Overflow and use the `gitpython` tag
|
| 253 |
+
- [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues)
|
| 254 |
+
- Post reproducible bugs and feature requests as a new issue.
|
| 255 |
+
Please be sure to provide the following information if posting bugs:
|
| 256 |
+
- GitPython version (e.g. `import git; git.__version__`)
|
| 257 |
+
- Python version (e.g. `python --version`)
|
| 258 |
+
- The encountered stack-trace, if applicable
|
| 259 |
+
- Enough information to allow reproducing the issue
|
| 260 |
+
|
| 261 |
+
### How to make a new release
|
| 262 |
+
|
| 263 |
+
1. Update/verify the **version** in the `VERSION` file.
|
| 264 |
+
2. Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
|
| 265 |
+
3. Commit everything.
|
| 266 |
+
4. Run `git tag -s <version>` to tag the version in Git.
|
| 267 |
+
5. _Optionally_ create and activate a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment). (Then the next step can install `build` and `twine`.)
|
| 268 |
+
6. Run `make release`.
|
| 269 |
+
7. Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog.
|
| 270 |
+
|
| 271 |
+
### Projects using GitPython
|
| 272 |
+
|
| 273 |
+
- [PyDriller](https://github.com/ishepard/pydriller)
|
| 274 |
+
- [Kivy Designer](https://github.com/kivy/kivy-designer)
|
| 275 |
+
- [Prowl](https://github.com/nettitude/Prowl)
|
| 276 |
+
- [Python Taint](https://github.com/python-security/pyt)
|
| 277 |
+
- [Buster](https://github.com/axitkhurana/buster)
|
| 278 |
+
- [git-ftp](https://github.com/ezyang/git-ftp)
|
| 279 |
+
- [Git-Pandas](https://github.com/wdm0006/git-pandas)
|
| 280 |
+
- [PyGitUp](https://github.com/msiemens/PyGitUp)
|
| 281 |
+
- [PyJFuzz](https://github.com/mseclab/PyJFuzz)
|
| 282 |
+
- [Loki](https://github.com/Neo23x0/Loki)
|
| 283 |
+
- [Omniwallet](https://github.com/OmniLayer/omniwallet)
|
| 284 |
+
- [GitViper](https://github.com/BeayemX/GitViper)
|
| 285 |
+
- [Git Gud](https://github.com/bthayer2365/git-gud)
|
| 286 |
+
|
| 287 |
+
### LICENSE
|
| 288 |
+
|
| 289 |
+
[3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
|
| 290 |
+
|
| 291 |
+
One file exclusively used for fuzz testing is subject to [a separate license, detailed here](./fuzzing/README.md#license).
|
| 292 |
+
This file is not included in the wheel or sdist packages published by the maintainers of GitPython.
|
| 293 |
+
|
| 294 |
+
[contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
|
| 295 |
+
[license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
|
Lib/site-packages/GitPython-3.1.44.dist-info/RECORD
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GitPython-3.1.44.dist-info/AUTHORS,sha256=tZ9LuyBks2V2HKTPK7kCmtd9Guu_LyU1oZHvU0NiAok,2334
|
| 2 |
+
GitPython-3.1.44.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
GitPython-3.1.44.dist-info/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
|
| 4 |
+
GitPython-3.1.44.dist-info/METADATA,sha256=0O_Fr2Y7A-DlPYhlbSxGjblBC2mWkw3USNUhyL80Ip8,13245
|
| 5 |
+
GitPython-3.1.44.dist-info/RECORD,,
|
| 6 |
+
GitPython-3.1.44.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
| 7 |
+
GitPython-3.1.44.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
|
| 8 |
+
git/__init__.py,sha256=nkQImgv-bWdiZOFDjzN-gbt93FoRHD0nY6_t9LQxy4Y,8899
|
| 9 |
+
git/__pycache__/__init__.cpython-312.pyc,,
|
| 10 |
+
git/__pycache__/cmd.cpython-312.pyc,,
|
| 11 |
+
git/__pycache__/compat.cpython-312.pyc,,
|
| 12 |
+
git/__pycache__/config.cpython-312.pyc,,
|
| 13 |
+
git/__pycache__/db.cpython-312.pyc,,
|
| 14 |
+
git/__pycache__/diff.cpython-312.pyc,,
|
| 15 |
+
git/__pycache__/exc.cpython-312.pyc,,
|
| 16 |
+
git/__pycache__/remote.cpython-312.pyc,,
|
| 17 |
+
git/__pycache__/types.cpython-312.pyc,,
|
| 18 |
+
git/__pycache__/util.cpython-312.pyc,,
|
| 19 |
+
git/cmd.py,sha256=QwiaBy0mFbi9xjRKhRgUVK-_-K6xVdFqh9l0cxPqPSc,67724
|
| 20 |
+
git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
|
| 21 |
+
git/config.py,sha256=vTUlK6d8ORqFqjOv4Vbq_Hm-5mp-jOAt1dkq0IdzJ3U,34933
|
| 22 |
+
git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
|
| 23 |
+
git/diff.py,sha256=wmpMCIdMiVOqreGVPOGYyO4gFboGOAicyrvvI7PPjEg,27095
|
| 24 |
+
git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
|
| 25 |
+
git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
|
| 26 |
+
git/index/__pycache__/__init__.cpython-312.pyc,,
|
| 27 |
+
git/index/__pycache__/base.cpython-312.pyc,,
|
| 28 |
+
git/index/__pycache__/fun.cpython-312.pyc,,
|
| 29 |
+
git/index/__pycache__/typ.cpython-312.pyc,,
|
| 30 |
+
git/index/__pycache__/util.cpython-312.pyc,,
|
| 31 |
+
git/index/base.py,sha256=nDD7XVLNbgBKpJMrrTVyHBy6NVLWgDkk7oUw6ZOegPc,60808
|
| 32 |
+
git/index/fun.py,sha256=37cA3DBC9vpAnSVu5TGA072SnoF5XZOkOukExwlejHs,16736
|
| 33 |
+
git/index/typ.py,sha256=uuKNwitUw83FhVaLSwo4pY7PHDQudtZTLJrLGym4jcI,6570
|
| 34 |
+
git/index/util.py,sha256=fULi7GPG-MvprKrRCD5c15GNdzku_1E38We0d97WB3A,3659
|
| 35 |
+
git/objects/__init__.py,sha256=O6ZL_olX7e5-8iIbKviRPkVSJxN37WA-EC0q9d48U5Y,637
|
| 36 |
+
git/objects/__pycache__/__init__.cpython-312.pyc,,
|
| 37 |
+
git/objects/__pycache__/base.cpython-312.pyc,,
|
| 38 |
+
git/objects/__pycache__/blob.cpython-312.pyc,,
|
| 39 |
+
git/objects/__pycache__/commit.cpython-312.pyc,,
|
| 40 |
+
git/objects/__pycache__/fun.cpython-312.pyc,,
|
| 41 |
+
git/objects/__pycache__/tag.cpython-312.pyc,,
|
| 42 |
+
git/objects/__pycache__/tree.cpython-312.pyc,,
|
| 43 |
+
git/objects/__pycache__/util.cpython-312.pyc,,
|
| 44 |
+
git/objects/base.py,sha256=0dqNkSRVH0mk0-7ZKIkGBK7iNYrzLTVxwQFUd6CagsE,10277
|
| 45 |
+
git/objects/blob.py,sha256=zwwq0KfOMYeP5J2tW5CQatoLyeqFRlfkxP1Vwx1h07s,1215
|
| 46 |
+
git/objects/commit.py,sha256=GH1_83C9t7RGTukwozTHDgvxYQPRjTHhPDkXJyBbJyo,30553
|
| 47 |
+
git/objects/fun.py,sha256=B4jCqhAjm6Hl79GK58FPzW1H9K6Wc7Tx0rssyWmAcEE,8935
|
| 48 |
+
git/objects/submodule/__init__.py,sha256=6xySp767LVz3UylWgUalntS_nGXRuVzXxDuFAv_Wc2c,303
|
| 49 |
+
git/objects/submodule/__pycache__/__init__.cpython-312.pyc,,
|
| 50 |
+
git/objects/submodule/__pycache__/base.cpython-312.pyc,,
|
| 51 |
+
git/objects/submodule/__pycache__/root.cpython-312.pyc,,
|
| 52 |
+
git/objects/submodule/__pycache__/util.cpython-312.pyc,,
|
| 53 |
+
git/objects/submodule/base.py,sha256=MQ-2xV8JznGwy2hLQv1aeQNgAkhBhgc5tdtClFL3DmE,63901
|
| 54 |
+
git/objects/submodule/root.py,sha256=5eTtYNHasqdPq6q0oDCPr7IaO6uAHL3b4DxMoiO2LhE,20246
|
| 55 |
+
git/objects/submodule/util.py,sha256=sQqAYaiSJdFkZa9NlAuK_wTsMNiS-kkQnQjvIoJtc_o,3509
|
| 56 |
+
git/objects/tag.py,sha256=jAGESnpmTEv-dLakPzheT5ILZFFArcItnXYqfxfDrgc,4441
|
| 57 |
+
git/objects/tree.py,sha256=jJH888SHiP4dGzE-ra1yenQOyya_0C_MkHr06c1gHpM,13849
|
| 58 |
+
git/objects/util.py,sha256=Nlza4zLgdPmr_Yasyvvs6c1rKtW_wMxI6wDmQpQ3ufw,23846
|
| 59 |
+
git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 60 |
+
git/refs/__init__.py,sha256=DWlJNnsx-4jM_E-VycbP-FZUdn6iWhjnH_uZ_pZXBro,509
|
| 61 |
+
git/refs/__pycache__/__init__.cpython-312.pyc,,
|
| 62 |
+
git/refs/__pycache__/head.cpython-312.pyc,,
|
| 63 |
+
git/refs/__pycache__/log.cpython-312.pyc,,
|
| 64 |
+
git/refs/__pycache__/reference.cpython-312.pyc,,
|
| 65 |
+
git/refs/__pycache__/remote.cpython-312.pyc,,
|
| 66 |
+
git/refs/__pycache__/symbolic.cpython-312.pyc,,
|
| 67 |
+
git/refs/__pycache__/tag.cpython-312.pyc,,
|
| 68 |
+
git/refs/head.py,sha256=SGa3N301HfAi79X6UR5Mcg7mO9TnCH3Bk549kHlJVaQ,10513
|
| 69 |
+
git/refs/log.py,sha256=kXiuAgTo1DIuM_BfbDUk9gQ0YO-mutIMVdHv1_ES90o,12493
|
| 70 |
+
git/refs/reference.py,sha256=l6mhF4YLSEwtjz6b9PpOQH-fkng7EYWMaJhkjn-2jXA,5630
|
| 71 |
+
git/refs/remote.py,sha256=WwqV9T7BbYf3F_WZNUQivu9xktIIKGklCjDpwQrhD-A,2806
|
| 72 |
+
git/refs/symbolic.py,sha256=c8zOwaqzcg-J-rGrpuWdvh8zwMvSUqAHghd4vJoYG_s,34552
|
| 73 |
+
git/refs/tag.py,sha256=kgzV2vhpL4FD2TqHb0BJuMRAHgAvJF-TcoyWlaB-djQ,5010
|
| 74 |
+
git/remote.py,sha256=pYn9dAlz-QwvNMWXD1M57pMPQitthOM86qTRK_cpTqU,46786
|
| 75 |
+
git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
|
| 76 |
+
git/repo/__pycache__/__init__.cpython-312.pyc,,
|
| 77 |
+
git/repo/__pycache__/base.cpython-312.pyc,,
|
| 78 |
+
git/repo/__pycache__/fun.cpython-312.pyc,,
|
| 79 |
+
git/repo/base.py,sha256=0GU6nKNdT8SYjDI5Y5DeZ1zCEX3tHeq1VW2MSpne05g,59891
|
| 80 |
+
git/repo/fun.py,sha256=HSGC0-rqeKKx9fDg7JyQyMZgIwUWn-FnSZR_gRGpG-E,13573
|
| 81 |
+
git/types.py,sha256=MQzIDEOnoueXGsAJF_0MgUc_osH7Eu0Sw3DQofYzCVE,10272
|
| 82 |
+
git/util.py,sha256=2uAv34zZ_827-zJ3-D5ACrVH-4Q4EO_KLUTH23zi2AI,43770
|
Lib/site-packages/GitPython-3.1.44.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (75.6.0)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
Lib/site-packages/GitPython-3.1.44.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
git
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright 2010 Pallets
|
| 2 |
+
|
| 3 |
+
Redistribution and use in source and binary forms, with or without
|
| 4 |
+
modification, are permitted provided that the following conditions are
|
| 5 |
+
met:
|
| 6 |
+
|
| 7 |
+
1. Redistributions of source code must retain the above copyright
|
| 8 |
+
notice, this list of conditions and the following disclaimer.
|
| 9 |
+
|
| 10 |
+
2. Redistributions in binary form must reproduce the above copyright
|
| 11 |
+
notice, this list of conditions and the following disclaimer in the
|
| 12 |
+
documentation and/or other materials provided with the distribution.
|
| 13 |
+
|
| 14 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 15 |
+
contributors may be used to endorse or promote products derived from
|
| 16 |
+
this software without specific prior written permission.
|
| 17 |
+
|
| 18 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| 19 |
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| 20 |
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
| 21 |
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| 22 |
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| 23 |
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
| 24 |
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
| 25 |
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
| 26 |
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
| 27 |
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 28 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: MarkupSafe
|
| 3 |
+
Version: 3.0.2
|
| 4 |
+
Summary: Safely add untrusted strings to HTML/XML markup.
|
| 5 |
+
Maintainer-email: Pallets <contact@palletsprojects.com>
|
| 6 |
+
License: Copyright 2010 Pallets
|
| 7 |
+
|
| 8 |
+
Redistribution and use in source and binary forms, with or without
|
| 9 |
+
modification, are permitted provided that the following conditions are
|
| 10 |
+
met:
|
| 11 |
+
|
| 12 |
+
1. Redistributions of source code must retain the above copyright
|
| 13 |
+
notice, this list of conditions and the following disclaimer.
|
| 14 |
+
|
| 15 |
+
2. Redistributions in binary form must reproduce the above copyright
|
| 16 |
+
notice, this list of conditions and the following disclaimer in the
|
| 17 |
+
documentation and/or other materials provided with the distribution.
|
| 18 |
+
|
| 19 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 20 |
+
contributors may be used to endorse or promote products derived from
|
| 21 |
+
this software without specific prior written permission.
|
| 22 |
+
|
| 23 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| 24 |
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| 25 |
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
| 26 |
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| 27 |
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| 28 |
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
| 29 |
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
| 30 |
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
| 31 |
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
| 32 |
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 33 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 34 |
+
|
| 35 |
+
Project-URL: Donate, https://palletsprojects.com/donate
|
| 36 |
+
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
| 37 |
+
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
| 38 |
+
Project-URL: Source, https://github.com/pallets/markupsafe/
|
| 39 |
+
Project-URL: Chat, https://discord.gg/pallets
|
| 40 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 41 |
+
Classifier: Environment :: Web Environment
|
| 42 |
+
Classifier: Intended Audience :: Developers
|
| 43 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 44 |
+
Classifier: Operating System :: OS Independent
|
| 45 |
+
Classifier: Programming Language :: Python
|
| 46 |
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
| 47 |
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
| 48 |
+
Classifier: Typing :: Typed
|
| 49 |
+
Requires-Python: >=3.9
|
| 50 |
+
Description-Content-Type: text/markdown
|
| 51 |
+
License-File: LICENSE.txt
|
| 52 |
+
|
| 53 |
+
# MarkupSafe
|
| 54 |
+
|
| 55 |
+
MarkupSafe implements a text object that escapes characters so it is
|
| 56 |
+
safe to use in HTML and XML. Characters that have special meanings are
|
| 57 |
+
replaced so that they display as the actual characters. This mitigates
|
| 58 |
+
injection attacks, meaning untrusted user input can safely be displayed
|
| 59 |
+
on a page.
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
## Examples
|
| 63 |
+
|
| 64 |
+
```pycon
|
| 65 |
+
>>> from markupsafe import Markup, escape
|
| 66 |
+
|
| 67 |
+
>>> # escape replaces special characters and wraps in Markup
|
| 68 |
+
>>> escape("<script>alert(document.cookie);</script>")
|
| 69 |
+
Markup('<script>alert(document.cookie);</script>')
|
| 70 |
+
|
| 71 |
+
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
| 72 |
+
>>> Markup("<strong>Hello</strong>")
|
| 73 |
+
Markup('<strong>hello</strong>')
|
| 74 |
+
|
| 75 |
+
>>> escape(Markup("<strong>Hello</strong>"))
|
| 76 |
+
Markup('<strong>hello</strong>')
|
| 77 |
+
|
| 78 |
+
>>> # Markup is a str subclass
|
| 79 |
+
>>> # methods and operators escape their arguments
|
| 80 |
+
>>> template = Markup("Hello <em>{name}</em>")
|
| 81 |
+
>>> template.format(name='"World"')
|
| 82 |
+
Markup('Hello <em>"World"</em>')
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## Donate
|
| 86 |
+
|
| 87 |
+
The Pallets organization develops and supports MarkupSafe and other
|
| 88 |
+
popular packages. In order to grow the community of contributors and
|
| 89 |
+
users, and allow the maintainers to devote more time to the projects,
|
| 90 |
+
[please donate today][].
|
| 91 |
+
|
| 92 |
+
[please donate today]: https://palletsprojects.com/donate
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
|
| 3 |
+
MarkupSafe-3.0.2.dist-info/METADATA,sha256=nhoabjupBG41j_JxPCJ3ylgrZ6Fx8oMCFbiLF9Kafqc,4067
|
| 4 |
+
MarkupSafe-3.0.2.dist-info/RECORD,,
|
| 5 |
+
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=62QJgqtUFevqILau0n0UncooEMoOyVCKVQitJpcuCig,101
|
| 6 |
+
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
| 7 |
+
markupsafe/__init__.py,sha256=pREerPwvinB62tNCMOwqxBS2YHV6R52Wcq1d-rB4Z5o,13609
|
| 8 |
+
markupsafe/__pycache__/__init__.cpython-312.pyc,,
|
| 9 |
+
markupsafe/__pycache__/_native.cpython-312.pyc,,
|
| 10 |
+
markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
|
| 11 |
+
markupsafe/_speedups.c,sha256=SglUjn40ti9YgQAO--OgkSyv9tXq9vvaHyVhQows4Ok,4353
|
| 12 |
+
markupsafe/_speedups.cp312-win_amd64.pyd,sha256=sC88mCi7HJOQhbSSrdMPZfdCvi_VBfOzwkVuQ7V6T3M,13312
|
| 13 |
+
markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
|
| 14 |
+
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (75.2.0)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp312-cp312-win_amd64
|
| 5 |
+
|
Lib/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
markupsafe
|
Lib/site-packages/PIL/BdfFontFile.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# bitmap distribution font (bdf) file parser
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1996-05-16 fl created (as bdf2pil)
|
| 9 |
+
# 1997-08-25 fl converted to FontFile driver
|
| 10 |
+
# 2001-05-25 fl removed bogus __init__ call
|
| 11 |
+
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
|
| 12 |
+
# 2003-04-22 fl more robustification (from Graham Dumpleton)
|
| 13 |
+
#
|
| 14 |
+
# Copyright (c) 1997-2003 by Secret Labs AB.
|
| 15 |
+
# Copyright (c) 1997-2003 by Fredrik Lundh.
|
| 16 |
+
#
|
| 17 |
+
# See the README file for information on usage and redistribution.
|
| 18 |
+
#
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
Parse X Bitmap Distribution Format (BDF)
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
from typing import BinaryIO
|
| 26 |
+
|
| 27 |
+
from . import FontFile, Image
|
| 28 |
+
|
| 29 |
+
bdf_slant = {
|
| 30 |
+
"R": "Roman",
|
| 31 |
+
"I": "Italic",
|
| 32 |
+
"O": "Oblique",
|
| 33 |
+
"RI": "Reverse Italic",
|
| 34 |
+
"RO": "Reverse Oblique",
|
| 35 |
+
"OT": "Other",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def bdf_char(
|
| 42 |
+
f: BinaryIO,
|
| 43 |
+
) -> (
|
| 44 |
+
tuple[
|
| 45 |
+
str,
|
| 46 |
+
int,
|
| 47 |
+
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
|
| 48 |
+
Image.Image,
|
| 49 |
+
]
|
| 50 |
+
| None
|
| 51 |
+
):
|
| 52 |
+
# skip to STARTCHAR
|
| 53 |
+
while True:
|
| 54 |
+
s = f.readline()
|
| 55 |
+
if not s:
|
| 56 |
+
return None
|
| 57 |
+
if s[:9] == b"STARTCHAR":
|
| 58 |
+
break
|
| 59 |
+
id = s[9:].strip().decode("ascii")
|
| 60 |
+
|
| 61 |
+
# load symbol properties
|
| 62 |
+
props = {}
|
| 63 |
+
while True:
|
| 64 |
+
s = f.readline()
|
| 65 |
+
if not s or s[:6] == b"BITMAP":
|
| 66 |
+
break
|
| 67 |
+
i = s.find(b" ")
|
| 68 |
+
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
| 69 |
+
|
| 70 |
+
# load bitmap
|
| 71 |
+
bitmap = bytearray()
|
| 72 |
+
while True:
|
| 73 |
+
s = f.readline()
|
| 74 |
+
if not s or s[:7] == b"ENDCHAR":
|
| 75 |
+
break
|
| 76 |
+
bitmap += s[:-1]
|
| 77 |
+
|
| 78 |
+
# The word BBX
|
| 79 |
+
# followed by the width in x (BBw), height in y (BBh),
|
| 80 |
+
# and x and y displacement (BBxoff0, BByoff0)
|
| 81 |
+
# of the lower left corner from the origin of the character.
|
| 82 |
+
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
| 83 |
+
|
| 84 |
+
# The word DWIDTH
|
| 85 |
+
# followed by the width in x and y of the character in device pixels.
|
| 86 |
+
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
| 87 |
+
|
| 88 |
+
bbox = (
|
| 89 |
+
(dwx, dwy),
|
| 90 |
+
(x_disp, -y_disp - height, width + x_disp, -y_disp),
|
| 91 |
+
(0, 0, width, height),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
|
| 96 |
+
except ValueError:
|
| 97 |
+
# deal with zero-width characters
|
| 98 |
+
im = Image.new("1", (width, height))
|
| 99 |
+
|
| 100 |
+
return id, int(props["ENCODING"]), bbox, im
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class BdfFontFile(FontFile.FontFile):
|
| 104 |
+
"""Font file plugin for the X11 BDF format."""
|
| 105 |
+
|
| 106 |
+
def __init__(self, fp: BinaryIO) -> None:
|
| 107 |
+
super().__init__()
|
| 108 |
+
|
| 109 |
+
s = fp.readline()
|
| 110 |
+
if s[:13] != b"STARTFONT 2.1":
|
| 111 |
+
msg = "not a valid BDF file"
|
| 112 |
+
raise SyntaxError(msg)
|
| 113 |
+
|
| 114 |
+
props = {}
|
| 115 |
+
comments = []
|
| 116 |
+
|
| 117 |
+
while True:
|
| 118 |
+
s = fp.readline()
|
| 119 |
+
if not s or s[:13] == b"ENDPROPERTIES":
|
| 120 |
+
break
|
| 121 |
+
i = s.find(b" ")
|
| 122 |
+
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
| 123 |
+
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
|
| 124 |
+
if s.find(b"LogicalFontDescription") < 0:
|
| 125 |
+
comments.append(s[i + 1 : -1].decode("ascii"))
|
| 126 |
+
|
| 127 |
+
while True:
|
| 128 |
+
c = bdf_char(fp)
|
| 129 |
+
if not c:
|
| 130 |
+
break
|
| 131 |
+
id, ch, (xy, dst, src), im = c
|
| 132 |
+
if 0 <= ch < len(self.glyph):
|
| 133 |
+
self.glyph[ch] = xy, dst, src, im
|
Lib/site-packages/PIL/BlpImagePlugin.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Blizzard Mipmap Format (.blp)
|
| 3 |
+
Jerome Leclanche <jerome@leclan.ch>
|
| 4 |
+
|
| 5 |
+
The contents of this file are hereby released in the public domain (CC0)
|
| 6 |
+
Full text of the CC0 license:
|
| 7 |
+
https://creativecommons.org/publicdomain/zero/1.0/
|
| 8 |
+
|
| 9 |
+
BLP1 files, used mostly in Warcraft III, are not fully supported.
|
| 10 |
+
All types of BLP2 files used in World of Warcraft are supported.
|
| 11 |
+
|
| 12 |
+
The BLP file structure consists of a header, up to 16 mipmaps of the
|
| 13 |
+
texture
|
| 14 |
+
|
| 15 |
+
Texture sizes must be powers of two, though the two dimensions do
|
| 16 |
+
not have to be equal; 512x256 is valid, but 512x200 is not.
|
| 17 |
+
The first mipmap (mipmap #0) is the full size image; each subsequent
|
| 18 |
+
mipmap halves both dimensions. The final mipmap should be 1x1.
|
| 19 |
+
|
| 20 |
+
BLP files come in many different flavours:
|
| 21 |
+
* JPEG-compressed (type == 0) - only supported for BLP1.
|
| 22 |
+
* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
|
| 23 |
+
array of 8-bit values, one per pixel, left to right, top to bottom.
|
| 24 |
+
Each value is an index to the palette.
|
| 25 |
+
* DXT-compressed (type == 1, encoding == 2):
|
| 26 |
+
- DXT1 compression is used if alpha_encoding == 0.
|
| 27 |
+
- An additional alpha bit is used if alpha_depth == 1.
|
| 28 |
+
- DXT3 compression is used if alpha_encoding == 1.
|
| 29 |
+
- DXT5 compression is used if alpha_encoding == 7.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import abc
|
| 35 |
+
import os
|
| 36 |
+
import struct
|
| 37 |
+
from enum import IntEnum
|
| 38 |
+
from io import BytesIO
|
| 39 |
+
from typing import IO
|
| 40 |
+
|
| 41 |
+
from . import Image, ImageFile
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class Format(IntEnum):
|
| 45 |
+
JPEG = 0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Encoding(IntEnum):
|
| 49 |
+
UNCOMPRESSED = 1
|
| 50 |
+
DXT = 2
|
| 51 |
+
UNCOMPRESSED_RAW_BGRA = 3
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class AlphaEncoding(IntEnum):
|
| 55 |
+
DXT1 = 0
|
| 56 |
+
DXT3 = 1
|
| 57 |
+
DXT5 = 7
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def unpack_565(i: int) -> tuple[int, int, int]:
|
| 61 |
+
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def decode_dxt1(
|
| 65 |
+
data: bytes, alpha: bool = False
|
| 66 |
+
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
| 67 |
+
"""
|
| 68 |
+
input: one "row" of data (i.e. will produce 4*width pixels)
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
blocks = len(data) // 8 # number of blocks in row
|
| 72 |
+
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
| 73 |
+
|
| 74 |
+
for block_index in range(blocks):
|
| 75 |
+
# Decode next 8-byte block.
|
| 76 |
+
idx = block_index * 8
|
| 77 |
+
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
|
| 78 |
+
|
| 79 |
+
r0, g0, b0 = unpack_565(color0)
|
| 80 |
+
r1, g1, b1 = unpack_565(color1)
|
| 81 |
+
|
| 82 |
+
# Decode this block into 4x4 pixels
|
| 83 |
+
# Accumulate the results onto our 4 row accumulators
|
| 84 |
+
for j in range(4):
|
| 85 |
+
for i in range(4):
|
| 86 |
+
# get next control op and generate a pixel
|
| 87 |
+
|
| 88 |
+
control = bits & 3
|
| 89 |
+
bits = bits >> 2
|
| 90 |
+
|
| 91 |
+
a = 0xFF
|
| 92 |
+
if control == 0:
|
| 93 |
+
r, g, b = r0, g0, b0
|
| 94 |
+
elif control == 1:
|
| 95 |
+
r, g, b = r1, g1, b1
|
| 96 |
+
elif control == 2:
|
| 97 |
+
if color0 > color1:
|
| 98 |
+
r = (2 * r0 + r1) // 3
|
| 99 |
+
g = (2 * g0 + g1) // 3
|
| 100 |
+
b = (2 * b0 + b1) // 3
|
| 101 |
+
else:
|
| 102 |
+
r = (r0 + r1) // 2
|
| 103 |
+
g = (g0 + g1) // 2
|
| 104 |
+
b = (b0 + b1) // 2
|
| 105 |
+
elif control == 3:
|
| 106 |
+
if color0 > color1:
|
| 107 |
+
r = (2 * r1 + r0) // 3
|
| 108 |
+
g = (2 * g1 + g0) // 3
|
| 109 |
+
b = (2 * b1 + b0) // 3
|
| 110 |
+
else:
|
| 111 |
+
r, g, b, a = 0, 0, 0, 0
|
| 112 |
+
|
| 113 |
+
if alpha:
|
| 114 |
+
ret[j].extend([r, g, b, a])
|
| 115 |
+
else:
|
| 116 |
+
ret[j].extend([r, g, b])
|
| 117 |
+
|
| 118 |
+
return ret
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
| 122 |
+
"""
|
| 123 |
+
input: one "row" of data (i.e. will produce 4*width pixels)
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
blocks = len(data) // 16 # number of blocks in row
|
| 127 |
+
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
| 128 |
+
|
| 129 |
+
for block_index in range(blocks):
|
| 130 |
+
idx = block_index * 16
|
| 131 |
+
block = data[idx : idx + 16]
|
| 132 |
+
# Decode next 16-byte block.
|
| 133 |
+
bits = struct.unpack_from("<8B", block)
|
| 134 |
+
color0, color1 = struct.unpack_from("<HH", block, 8)
|
| 135 |
+
|
| 136 |
+
(code,) = struct.unpack_from("<I", block, 12)
|
| 137 |
+
|
| 138 |
+
r0, g0, b0 = unpack_565(color0)
|
| 139 |
+
r1, g1, b1 = unpack_565(color1)
|
| 140 |
+
|
| 141 |
+
for j in range(4):
|
| 142 |
+
high = False # Do we want the higher bits?
|
| 143 |
+
for i in range(4):
|
| 144 |
+
alphacode_index = (4 * j + i) // 2
|
| 145 |
+
a = bits[alphacode_index]
|
| 146 |
+
if high:
|
| 147 |
+
high = False
|
| 148 |
+
a >>= 4
|
| 149 |
+
else:
|
| 150 |
+
high = True
|
| 151 |
+
a &= 0xF
|
| 152 |
+
a *= 17 # We get a value between 0 and 15
|
| 153 |
+
|
| 154 |
+
color_code = (code >> 2 * (4 * j + i)) & 0x03
|
| 155 |
+
|
| 156 |
+
if color_code == 0:
|
| 157 |
+
r, g, b = r0, g0, b0
|
| 158 |
+
elif color_code == 1:
|
| 159 |
+
r, g, b = r1, g1, b1
|
| 160 |
+
elif color_code == 2:
|
| 161 |
+
r = (2 * r0 + r1) // 3
|
| 162 |
+
g = (2 * g0 + g1) // 3
|
| 163 |
+
b = (2 * b0 + b1) // 3
|
| 164 |
+
elif color_code == 3:
|
| 165 |
+
r = (2 * r1 + r0) // 3
|
| 166 |
+
g = (2 * g1 + g0) // 3
|
| 167 |
+
b = (2 * b1 + b0) // 3
|
| 168 |
+
|
| 169 |
+
ret[j].extend([r, g, b, a])
|
| 170 |
+
|
| 171 |
+
return ret
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
| 175 |
+
"""
|
| 176 |
+
input: one "row" of data (i.e. will produce 4 * width pixels)
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
blocks = len(data) // 16 # number of blocks in row
|
| 180 |
+
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
| 181 |
+
|
| 182 |
+
for block_index in range(blocks):
|
| 183 |
+
idx = block_index * 16
|
| 184 |
+
block = data[idx : idx + 16]
|
| 185 |
+
# Decode next 16-byte block.
|
| 186 |
+
a0, a1 = struct.unpack_from("<BB", block)
|
| 187 |
+
|
| 188 |
+
bits = struct.unpack_from("<6B", block, 2)
|
| 189 |
+
alphacode1 = bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24)
|
| 190 |
+
alphacode2 = bits[0] | (bits[1] << 8)
|
| 191 |
+
|
| 192 |
+
color0, color1 = struct.unpack_from("<HH", block, 8)
|
| 193 |
+
|
| 194 |
+
(code,) = struct.unpack_from("<I", block, 12)
|
| 195 |
+
|
| 196 |
+
r0, g0, b0 = unpack_565(color0)
|
| 197 |
+
r1, g1, b1 = unpack_565(color1)
|
| 198 |
+
|
| 199 |
+
for j in range(4):
|
| 200 |
+
for i in range(4):
|
| 201 |
+
# get next control op and generate a pixel
|
| 202 |
+
alphacode_index = 3 * (4 * j + i)
|
| 203 |
+
|
| 204 |
+
if alphacode_index <= 12:
|
| 205 |
+
alphacode = (alphacode2 >> alphacode_index) & 0x07
|
| 206 |
+
elif alphacode_index == 15:
|
| 207 |
+
alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
|
| 208 |
+
else: # alphacode_index >= 18 and alphacode_index <= 45
|
| 209 |
+
alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
|
| 210 |
+
|
| 211 |
+
if alphacode == 0:
|
| 212 |
+
a = a0
|
| 213 |
+
elif alphacode == 1:
|
| 214 |
+
a = a1
|
| 215 |
+
elif a0 > a1:
|
| 216 |
+
a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
|
| 217 |
+
elif alphacode == 6:
|
| 218 |
+
a = 0
|
| 219 |
+
elif alphacode == 7:
|
| 220 |
+
a = 255
|
| 221 |
+
else:
|
| 222 |
+
a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
|
| 223 |
+
|
| 224 |
+
color_code = (code >> 2 * (4 * j + i)) & 0x03
|
| 225 |
+
|
| 226 |
+
if color_code == 0:
|
| 227 |
+
r, g, b = r0, g0, b0
|
| 228 |
+
elif color_code == 1:
|
| 229 |
+
r, g, b = r1, g1, b1
|
| 230 |
+
elif color_code == 2:
|
| 231 |
+
r = (2 * r0 + r1) // 3
|
| 232 |
+
g = (2 * g0 + g1) // 3
|
| 233 |
+
b = (2 * b0 + b1) // 3
|
| 234 |
+
elif color_code == 3:
|
| 235 |
+
r = (2 * r1 + r0) // 3
|
| 236 |
+
g = (2 * g1 + g0) // 3
|
| 237 |
+
b = (2 * b1 + b0) // 3
|
| 238 |
+
|
| 239 |
+
ret[j].extend([r, g, b, a])
|
| 240 |
+
|
| 241 |
+
return ret
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
class BLPFormatError(NotImplementedError):
|
| 245 |
+
pass
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _accept(prefix: bytes) -> bool:
|
| 249 |
+
return prefix[:4] in (b"BLP1", b"BLP2")
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
class BlpImageFile(ImageFile.ImageFile):
|
| 253 |
+
"""
|
| 254 |
+
Blizzard Mipmap Format
|
| 255 |
+
"""
|
| 256 |
+
|
| 257 |
+
format = "BLP"
|
| 258 |
+
format_description = "Blizzard Mipmap Format"
|
| 259 |
+
|
| 260 |
+
def _open(self) -> None:
|
| 261 |
+
self.magic = self.fp.read(4)
|
| 262 |
+
if not _accept(self.magic):
|
| 263 |
+
msg = f"Bad BLP magic {repr(self.magic)}"
|
| 264 |
+
raise BLPFormatError(msg)
|
| 265 |
+
|
| 266 |
+
compression = struct.unpack("<i", self.fp.read(4))[0]
|
| 267 |
+
if self.magic == b"BLP1":
|
| 268 |
+
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
|
| 269 |
+
else:
|
| 270 |
+
encoding = struct.unpack("<b", self.fp.read(1))[0]
|
| 271 |
+
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
|
| 272 |
+
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
|
| 273 |
+
self.fp.seek(1, os.SEEK_CUR) # mips
|
| 274 |
+
|
| 275 |
+
self._size = struct.unpack("<II", self.fp.read(8))
|
| 276 |
+
|
| 277 |
+
args: tuple[int, int, bool] | tuple[int, int, bool, int]
|
| 278 |
+
if self.magic == b"BLP1":
|
| 279 |
+
encoding = struct.unpack("<i", self.fp.read(4))[0]
|
| 280 |
+
self.fp.seek(4, os.SEEK_CUR) # subtype
|
| 281 |
+
|
| 282 |
+
args = (compression, encoding, alpha)
|
| 283 |
+
offset = 28
|
| 284 |
+
else:
|
| 285 |
+
args = (compression, encoding, alpha, alpha_encoding)
|
| 286 |
+
offset = 20
|
| 287 |
+
|
| 288 |
+
decoder = self.magic.decode()
|
| 289 |
+
|
| 290 |
+
self._mode = "RGBA" if alpha else "RGB"
|
| 291 |
+
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
| 295 |
+
_pulls_fd = True
|
| 296 |
+
|
| 297 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 298 |
+
try:
|
| 299 |
+
self._read_header()
|
| 300 |
+
self._load()
|
| 301 |
+
except struct.error as e:
|
| 302 |
+
msg = "Truncated BLP file"
|
| 303 |
+
raise OSError(msg) from e
|
| 304 |
+
return -1, 0
|
| 305 |
+
|
| 306 |
+
@abc.abstractmethod
|
| 307 |
+
def _load(self) -> None:
|
| 308 |
+
pass
|
| 309 |
+
|
| 310 |
+
def _read_header(self) -> None:
|
| 311 |
+
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
| 312 |
+
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
| 313 |
+
|
| 314 |
+
def _safe_read(self, length: int) -> bytes:
|
| 315 |
+
assert self.fd is not None
|
| 316 |
+
return ImageFile._safe_read(self.fd, length)
|
| 317 |
+
|
| 318 |
+
def _read_palette(self) -> list[tuple[int, int, int, int]]:
|
| 319 |
+
ret = []
|
| 320 |
+
for i in range(256):
|
| 321 |
+
try:
|
| 322 |
+
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
|
| 323 |
+
except struct.error:
|
| 324 |
+
break
|
| 325 |
+
ret.append((b, g, r, a))
|
| 326 |
+
return ret
|
| 327 |
+
|
| 328 |
+
def _read_bgra(
|
| 329 |
+
self, palette: list[tuple[int, int, int, int]], alpha: bool
|
| 330 |
+
) -> bytearray:
|
| 331 |
+
data = bytearray()
|
| 332 |
+
_data = BytesIO(self._safe_read(self._lengths[0]))
|
| 333 |
+
while True:
|
| 334 |
+
try:
|
| 335 |
+
(offset,) = struct.unpack("<B", _data.read(1))
|
| 336 |
+
except struct.error:
|
| 337 |
+
break
|
| 338 |
+
b, g, r, a = palette[offset]
|
| 339 |
+
d: tuple[int, ...] = (r, g, b)
|
| 340 |
+
if alpha:
|
| 341 |
+
d += (a,)
|
| 342 |
+
data.extend(d)
|
| 343 |
+
return data
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class BLP1Decoder(_BLPBaseDecoder):
|
| 347 |
+
def _load(self) -> None:
|
| 348 |
+
self._compression, self._encoding, alpha = self.args
|
| 349 |
+
|
| 350 |
+
if self._compression == Format.JPEG:
|
| 351 |
+
self._decode_jpeg_stream()
|
| 352 |
+
|
| 353 |
+
elif self._compression == 1:
|
| 354 |
+
if self._encoding in (4, 5):
|
| 355 |
+
palette = self._read_palette()
|
| 356 |
+
data = self._read_bgra(palette, alpha)
|
| 357 |
+
self.set_as_raw(data)
|
| 358 |
+
else:
|
| 359 |
+
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
|
| 360 |
+
raise BLPFormatError(msg)
|
| 361 |
+
else:
|
| 362 |
+
msg = f"Unsupported BLP compression {repr(self._encoding)}"
|
| 363 |
+
raise BLPFormatError(msg)
|
| 364 |
+
|
| 365 |
+
def _decode_jpeg_stream(self) -> None:
|
| 366 |
+
from .JpegImagePlugin import JpegImageFile
|
| 367 |
+
|
| 368 |
+
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
| 369 |
+
jpeg_header = self._safe_read(jpeg_header_size)
|
| 370 |
+
assert self.fd is not None
|
| 371 |
+
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
|
| 372 |
+
data = self._safe_read(self._lengths[0])
|
| 373 |
+
data = jpeg_header + data
|
| 374 |
+
image = JpegImageFile(BytesIO(data))
|
| 375 |
+
Image._decompression_bomb_check(image.size)
|
| 376 |
+
if image.mode == "CMYK":
|
| 377 |
+
decoder_name, extents, offset, args = image.tile[0]
|
| 378 |
+
assert isinstance(args, tuple)
|
| 379 |
+
image.tile = [
|
| 380 |
+
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
|
| 381 |
+
]
|
| 382 |
+
r, g, b = image.convert("RGB").split()
|
| 383 |
+
reversed_image = Image.merge("RGB", (b, g, r))
|
| 384 |
+
self.set_as_raw(reversed_image.tobytes())
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class BLP2Decoder(_BLPBaseDecoder):
|
| 388 |
+
def _load(self) -> None:
|
| 389 |
+
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
|
| 390 |
+
|
| 391 |
+
palette = self._read_palette()
|
| 392 |
+
|
| 393 |
+
assert self.fd is not None
|
| 394 |
+
self.fd.seek(self._offsets[0])
|
| 395 |
+
|
| 396 |
+
if self._compression == 1:
|
| 397 |
+
# Uncompressed or DirectX compression
|
| 398 |
+
|
| 399 |
+
if self._encoding == Encoding.UNCOMPRESSED:
|
| 400 |
+
data = self._read_bgra(palette, alpha)
|
| 401 |
+
|
| 402 |
+
elif self._encoding == Encoding.DXT:
|
| 403 |
+
data = bytearray()
|
| 404 |
+
if self._alpha_encoding == AlphaEncoding.DXT1:
|
| 405 |
+
linesize = (self.state.xsize + 3) // 4 * 8
|
| 406 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 407 |
+
for d in decode_dxt1(self._safe_read(linesize), alpha):
|
| 408 |
+
data += d
|
| 409 |
+
|
| 410 |
+
elif self._alpha_encoding == AlphaEncoding.DXT3:
|
| 411 |
+
linesize = (self.state.xsize + 3) // 4 * 16
|
| 412 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 413 |
+
for d in decode_dxt3(self._safe_read(linesize)):
|
| 414 |
+
data += d
|
| 415 |
+
|
| 416 |
+
elif self._alpha_encoding == AlphaEncoding.DXT5:
|
| 417 |
+
linesize = (self.state.xsize + 3) // 4 * 16
|
| 418 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 419 |
+
for d in decode_dxt5(self._safe_read(linesize)):
|
| 420 |
+
data += d
|
| 421 |
+
else:
|
| 422 |
+
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
|
| 423 |
+
raise BLPFormatError(msg)
|
| 424 |
+
else:
|
| 425 |
+
msg = f"Unknown BLP encoding {repr(self._encoding)}"
|
| 426 |
+
raise BLPFormatError(msg)
|
| 427 |
+
|
| 428 |
+
else:
|
| 429 |
+
msg = f"Unknown BLP compression {repr(self._compression)}"
|
| 430 |
+
raise BLPFormatError(msg)
|
| 431 |
+
|
| 432 |
+
self.set_as_raw(data)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
class BLPEncoder(ImageFile.PyEncoder):
|
| 436 |
+
_pushes_fd = True
|
| 437 |
+
|
| 438 |
+
def _write_palette(self) -> bytes:
|
| 439 |
+
data = b""
|
| 440 |
+
assert self.im is not None
|
| 441 |
+
palette = self.im.getpalette("RGBA", "RGBA")
|
| 442 |
+
for i in range(len(palette) // 4):
|
| 443 |
+
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
| 444 |
+
data += struct.pack("<4B", b, g, r, a)
|
| 445 |
+
while len(data) < 256 * 4:
|
| 446 |
+
data += b"\x00" * 4
|
| 447 |
+
return data
|
| 448 |
+
|
| 449 |
+
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
| 450 |
+
palette_data = self._write_palette()
|
| 451 |
+
|
| 452 |
+
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
| 453 |
+
data = struct.pack("<16I", offset, *((0,) * 15))
|
| 454 |
+
|
| 455 |
+
assert self.im is not None
|
| 456 |
+
w, h = self.im.size
|
| 457 |
+
data += struct.pack("<16I", w * h, *((0,) * 15))
|
| 458 |
+
|
| 459 |
+
data += palette_data
|
| 460 |
+
|
| 461 |
+
for y in range(h):
|
| 462 |
+
for x in range(w):
|
| 463 |
+
data += struct.pack("<B", self.im.getpixel((x, y)))
|
| 464 |
+
|
| 465 |
+
return len(data), 0, data
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 469 |
+
if im.mode != "P":
|
| 470 |
+
msg = "Unsupported BLP image mode"
|
| 471 |
+
raise ValueError(msg)
|
| 472 |
+
|
| 473 |
+
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
|
| 474 |
+
fp.write(magic)
|
| 475 |
+
|
| 476 |
+
assert im.palette is not None
|
| 477 |
+
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
| 478 |
+
|
| 479 |
+
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
|
| 480 |
+
if magic == b"BLP1":
|
| 481 |
+
fp.write(struct.pack("<L", alpha_depth))
|
| 482 |
+
else:
|
| 483 |
+
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
| 484 |
+
fp.write(struct.pack("<b", alpha_depth))
|
| 485 |
+
fp.write(struct.pack("<b", 0)) # alpha encoding
|
| 486 |
+
fp.write(struct.pack("<b", 0)) # mips
|
| 487 |
+
fp.write(struct.pack("<II", *im.size))
|
| 488 |
+
if magic == b"BLP1":
|
| 489 |
+
fp.write(struct.pack("<i", 5))
|
| 490 |
+
fp.write(struct.pack("<i", 0))
|
| 491 |
+
|
| 492 |
+
ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
|
| 496 |
+
Image.register_extension(BlpImageFile.format, ".blp")
|
| 497 |
+
Image.register_decoder("BLP1", BLP1Decoder)
|
| 498 |
+
Image.register_decoder("BLP2", BLP2Decoder)
|
| 499 |
+
|
| 500 |
+
Image.register_save(BlpImageFile.format, _save)
|
| 501 |
+
Image.register_encoder("BLP", BLPEncoder)
|
Lib/site-packages/PIL/BmpImagePlugin.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# BMP file handler
|
| 6 |
+
#
|
| 7 |
+
# Windows (and OS/2) native bitmap storage format.
|
| 8 |
+
#
|
| 9 |
+
# history:
|
| 10 |
+
# 1995-09-01 fl Created
|
| 11 |
+
# 1996-04-30 fl Added save
|
| 12 |
+
# 1997-08-27 fl Fixed save of 1-bit images
|
| 13 |
+
# 1998-03-06 fl Load P images as L where possible
|
| 14 |
+
# 1998-07-03 fl Load P images as 1 where possible
|
| 15 |
+
# 1998-12-29 fl Handle small palettes
|
| 16 |
+
# 2002-12-30 fl Fixed load of 1-bit palette images
|
| 17 |
+
# 2003-04-21 fl Fixed load of 1-bit monochrome images
|
| 18 |
+
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
|
| 19 |
+
#
|
| 20 |
+
# Copyright (c) 1997-2003 by Secret Labs AB
|
| 21 |
+
# Copyright (c) 1995-2003 by Fredrik Lundh
|
| 22 |
+
#
|
| 23 |
+
# See the README file for information on usage and redistribution.
|
| 24 |
+
#
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import os
|
| 28 |
+
from typing import IO, Any
|
| 29 |
+
|
| 30 |
+
from . import Image, ImageFile, ImagePalette
|
| 31 |
+
from ._binary import i16le as i16
|
| 32 |
+
from ._binary import i32le as i32
|
| 33 |
+
from ._binary import o8
|
| 34 |
+
from ._binary import o16le as o16
|
| 35 |
+
from ._binary import o32le as o32
|
| 36 |
+
|
| 37 |
+
#
|
| 38 |
+
# --------------------------------------------------------------------
|
| 39 |
+
# Read BMP file
|
| 40 |
+
|
| 41 |
+
BIT2MODE = {
|
| 42 |
+
# bits => mode, rawmode
|
| 43 |
+
1: ("P", "P;1"),
|
| 44 |
+
4: ("P", "P;4"),
|
| 45 |
+
8: ("P", "P"),
|
| 46 |
+
16: ("RGB", "BGR;15"),
|
| 47 |
+
24: ("RGB", "BGR"),
|
| 48 |
+
32: ("RGB", "BGRX"),
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _accept(prefix: bytes) -> bool:
|
| 53 |
+
return prefix[:2] == b"BM"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _dib_accept(prefix: bytes) -> bool:
|
| 57 |
+
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# =============================================================================
|
| 61 |
+
# Image plugin for the Windows BMP format.
|
| 62 |
+
# =============================================================================
|
| 63 |
+
class BmpImageFile(ImageFile.ImageFile):
|
| 64 |
+
"""Image plugin for the Windows Bitmap format (BMP)"""
|
| 65 |
+
|
| 66 |
+
# ------------------------------------------------------------- Description
|
| 67 |
+
format_description = "Windows Bitmap"
|
| 68 |
+
format = "BMP"
|
| 69 |
+
|
| 70 |
+
# -------------------------------------------------- BMP Compression values
|
| 71 |
+
COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
|
| 72 |
+
for k, v in COMPRESSIONS.items():
|
| 73 |
+
vars()[k] = v
|
| 74 |
+
|
| 75 |
+
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
|
| 76 |
+
"""Read relevant info about the BMP"""
|
| 77 |
+
read, seek = self.fp.read, self.fp.seek
|
| 78 |
+
if header:
|
| 79 |
+
seek(header)
|
| 80 |
+
# read bmp header size @offset 14 (this is part of the header size)
|
| 81 |
+
file_info: dict[str, bool | int | tuple[int, ...]] = {
|
| 82 |
+
"header_size": i32(read(4)),
|
| 83 |
+
"direction": -1,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# -------------------- If requested, read header at a specific position
|
| 87 |
+
# read the rest of the bmp header, without its size
|
| 88 |
+
assert isinstance(file_info["header_size"], int)
|
| 89 |
+
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
|
| 90 |
+
|
| 91 |
+
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
|
| 92 |
+
# ----- This format has different offsets because of width/height types
|
| 93 |
+
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
|
| 94 |
+
if file_info["header_size"] == 12:
|
| 95 |
+
file_info["width"] = i16(header_data, 0)
|
| 96 |
+
file_info["height"] = i16(header_data, 2)
|
| 97 |
+
file_info["planes"] = i16(header_data, 4)
|
| 98 |
+
file_info["bits"] = i16(header_data, 6)
|
| 99 |
+
file_info["compression"] = self.COMPRESSIONS["RAW"]
|
| 100 |
+
file_info["palette_padding"] = 3
|
| 101 |
+
|
| 102 |
+
# --------------------------------------------- Windows Bitmap v3 to v5
|
| 103 |
+
# 40: BITMAPINFOHEADER
|
| 104 |
+
# 52: BITMAPV2HEADER
|
| 105 |
+
# 56: BITMAPV3HEADER
|
| 106 |
+
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
|
| 107 |
+
# 108: BITMAPV4HEADER
|
| 108 |
+
# 124: BITMAPV5HEADER
|
| 109 |
+
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
|
| 110 |
+
file_info["y_flip"] = header_data[7] == 0xFF
|
| 111 |
+
file_info["direction"] = 1 if file_info["y_flip"] else -1
|
| 112 |
+
file_info["width"] = i32(header_data, 0)
|
| 113 |
+
file_info["height"] = (
|
| 114 |
+
i32(header_data, 4)
|
| 115 |
+
if not file_info["y_flip"]
|
| 116 |
+
else 2**32 - i32(header_data, 4)
|
| 117 |
+
)
|
| 118 |
+
file_info["planes"] = i16(header_data, 8)
|
| 119 |
+
file_info["bits"] = i16(header_data, 10)
|
| 120 |
+
file_info["compression"] = i32(header_data, 12)
|
| 121 |
+
# byte size of pixel data
|
| 122 |
+
file_info["data_size"] = i32(header_data, 16)
|
| 123 |
+
file_info["pixels_per_meter"] = (
|
| 124 |
+
i32(header_data, 20),
|
| 125 |
+
i32(header_data, 24),
|
| 126 |
+
)
|
| 127 |
+
file_info["colors"] = i32(header_data, 28)
|
| 128 |
+
file_info["palette_padding"] = 4
|
| 129 |
+
assert isinstance(file_info["pixels_per_meter"], tuple)
|
| 130 |
+
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
|
| 131 |
+
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
| 132 |
+
masks = ["r_mask", "g_mask", "b_mask"]
|
| 133 |
+
if len(header_data) >= 48:
|
| 134 |
+
if len(header_data) >= 52:
|
| 135 |
+
masks.append("a_mask")
|
| 136 |
+
else:
|
| 137 |
+
file_info["a_mask"] = 0x0
|
| 138 |
+
for idx, mask in enumerate(masks):
|
| 139 |
+
file_info[mask] = i32(header_data, 36 + idx * 4)
|
| 140 |
+
else:
|
| 141 |
+
# 40 byte headers only have the three components in the
|
| 142 |
+
# bitfields masks, ref:
|
| 143 |
+
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
| 144 |
+
# See also
|
| 145 |
+
# https://github.com/python-pillow/Pillow/issues/1293
|
| 146 |
+
# There is a 4th component in the RGBQuad, in the alpha
|
| 147 |
+
# location, but it is listed as a reserved component,
|
| 148 |
+
# and it is not generally an alpha channel
|
| 149 |
+
file_info["a_mask"] = 0x0
|
| 150 |
+
for mask in masks:
|
| 151 |
+
file_info[mask] = i32(read(4))
|
| 152 |
+
assert isinstance(file_info["r_mask"], int)
|
| 153 |
+
assert isinstance(file_info["g_mask"], int)
|
| 154 |
+
assert isinstance(file_info["b_mask"], int)
|
| 155 |
+
assert isinstance(file_info["a_mask"], int)
|
| 156 |
+
file_info["rgb_mask"] = (
|
| 157 |
+
file_info["r_mask"],
|
| 158 |
+
file_info["g_mask"],
|
| 159 |
+
file_info["b_mask"],
|
| 160 |
+
)
|
| 161 |
+
file_info["rgba_mask"] = (
|
| 162 |
+
file_info["r_mask"],
|
| 163 |
+
file_info["g_mask"],
|
| 164 |
+
file_info["b_mask"],
|
| 165 |
+
file_info["a_mask"],
|
| 166 |
+
)
|
| 167 |
+
else:
|
| 168 |
+
msg = f"Unsupported BMP header type ({file_info['header_size']})"
|
| 169 |
+
raise OSError(msg)
|
| 170 |
+
|
| 171 |
+
# ------------------ Special case : header is reported 40, which
|
| 172 |
+
# ---------------------- is shorter than real size for bpp >= 16
|
| 173 |
+
assert isinstance(file_info["width"], int)
|
| 174 |
+
assert isinstance(file_info["height"], int)
|
| 175 |
+
self._size = file_info["width"], file_info["height"]
|
| 176 |
+
|
| 177 |
+
# ------- If color count was not found in the header, compute from bits
|
| 178 |
+
assert isinstance(file_info["bits"], int)
|
| 179 |
+
file_info["colors"] = (
|
| 180 |
+
file_info["colors"]
|
| 181 |
+
if file_info.get("colors", 0)
|
| 182 |
+
else (1 << file_info["bits"])
|
| 183 |
+
)
|
| 184 |
+
assert isinstance(file_info["colors"], int)
|
| 185 |
+
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
|
| 186 |
+
offset += 4 * file_info["colors"]
|
| 187 |
+
|
| 188 |
+
# ---------------------- Check bit depth for unusual unsupported values
|
| 189 |
+
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
|
| 190 |
+
if not self.mode:
|
| 191 |
+
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
|
| 192 |
+
raise OSError(msg)
|
| 193 |
+
|
| 194 |
+
# ---------------- Process BMP with Bitfields compression (not palette)
|
| 195 |
+
decoder_name = "raw"
|
| 196 |
+
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
| 197 |
+
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
|
| 198 |
+
32: [
|
| 199 |
+
(0xFF0000, 0xFF00, 0xFF, 0x0),
|
| 200 |
+
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
|
| 201 |
+
(0xFF000000, 0xFF00, 0xFF, 0x0),
|
| 202 |
+
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
|
| 203 |
+
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
|
| 204 |
+
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
|
| 205 |
+
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
|
| 206 |
+
(0x0, 0x0, 0x0, 0x0),
|
| 207 |
+
],
|
| 208 |
+
24: [(0xFF0000, 0xFF00, 0xFF)],
|
| 209 |
+
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
|
| 210 |
+
}
|
| 211 |
+
MASK_MODES = {
|
| 212 |
+
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
|
| 213 |
+
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
|
| 214 |
+
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
|
| 215 |
+
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
|
| 216 |
+
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
|
| 217 |
+
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
|
| 218 |
+
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
|
| 219 |
+
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
| 220 |
+
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
|
| 221 |
+
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
|
| 222 |
+
(16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
|
| 223 |
+
}
|
| 224 |
+
if file_info["bits"] in SUPPORTED:
|
| 225 |
+
if (
|
| 226 |
+
file_info["bits"] == 32
|
| 227 |
+
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
|
| 228 |
+
):
|
| 229 |
+
assert isinstance(file_info["rgba_mask"], tuple)
|
| 230 |
+
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
|
| 231 |
+
self._mode = "RGBA" if "A" in raw_mode else self.mode
|
| 232 |
+
elif (
|
| 233 |
+
file_info["bits"] in (24, 16)
|
| 234 |
+
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
|
| 235 |
+
):
|
| 236 |
+
assert isinstance(file_info["rgb_mask"], tuple)
|
| 237 |
+
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
|
| 238 |
+
else:
|
| 239 |
+
msg = "Unsupported BMP bitfields layout"
|
| 240 |
+
raise OSError(msg)
|
| 241 |
+
else:
|
| 242 |
+
msg = "Unsupported BMP bitfields layout"
|
| 243 |
+
raise OSError(msg)
|
| 244 |
+
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
|
| 245 |
+
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
| 246 |
+
raw_mode, self._mode = "BGRA", "RGBA"
|
| 247 |
+
elif file_info["compression"] in (
|
| 248 |
+
self.COMPRESSIONS["RLE8"],
|
| 249 |
+
self.COMPRESSIONS["RLE4"],
|
| 250 |
+
):
|
| 251 |
+
decoder_name = "bmp_rle"
|
| 252 |
+
else:
|
| 253 |
+
msg = f"Unsupported BMP compression ({file_info['compression']})"
|
| 254 |
+
raise OSError(msg)
|
| 255 |
+
|
| 256 |
+
# --------------- Once the header is processed, process the palette/LUT
|
| 257 |
+
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
|
| 258 |
+
# ---------------------------------------------------- 1-bit images
|
| 259 |
+
if not (0 < file_info["colors"] <= 65536):
|
| 260 |
+
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
|
| 261 |
+
raise OSError(msg)
|
| 262 |
+
else:
|
| 263 |
+
assert isinstance(file_info["palette_padding"], int)
|
| 264 |
+
padding = file_info["palette_padding"]
|
| 265 |
+
palette = read(padding * file_info["colors"])
|
| 266 |
+
grayscale = True
|
| 267 |
+
indices = (
|
| 268 |
+
(0, 255)
|
| 269 |
+
if file_info["colors"] == 2
|
| 270 |
+
else list(range(file_info["colors"]))
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# ----------------- Check if grayscale and ignore palette if so
|
| 274 |
+
for ind, val in enumerate(indices):
|
| 275 |
+
rgb = palette[ind * padding : ind * padding + 3]
|
| 276 |
+
if rgb != o8(val) * 3:
|
| 277 |
+
grayscale = False
|
| 278 |
+
|
| 279 |
+
# ------- If all colors are gray, white or black, ditch palette
|
| 280 |
+
if grayscale:
|
| 281 |
+
self._mode = "1" if file_info["colors"] == 2 else "L"
|
| 282 |
+
raw_mode = self.mode
|
| 283 |
+
else:
|
| 284 |
+
self._mode = "P"
|
| 285 |
+
self.palette = ImagePalette.raw(
|
| 286 |
+
"BGRX" if padding == 4 else "BGR", palette
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# ---------------------------- Finally set the tile data for the plugin
|
| 290 |
+
self.info["compression"] = file_info["compression"]
|
| 291 |
+
args: list[Any] = [raw_mode]
|
| 292 |
+
if decoder_name == "bmp_rle":
|
| 293 |
+
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
|
| 294 |
+
else:
|
| 295 |
+
assert isinstance(file_info["width"], int)
|
| 296 |
+
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
|
| 297 |
+
args.append(file_info["direction"])
|
| 298 |
+
self.tile = [
|
| 299 |
+
ImageFile._Tile(
|
| 300 |
+
decoder_name,
|
| 301 |
+
(0, 0, file_info["width"], file_info["height"]),
|
| 302 |
+
offset or self.fp.tell(),
|
| 303 |
+
tuple(args),
|
| 304 |
+
)
|
| 305 |
+
]
|
| 306 |
+
|
| 307 |
+
def _open(self) -> None:
|
| 308 |
+
"""Open file, check magic number and read header"""
|
| 309 |
+
# read 14 bytes: magic number, filesize, reserved, header final offset
|
| 310 |
+
head_data = self.fp.read(14)
|
| 311 |
+
# choke if the file does not have the required magic bytes
|
| 312 |
+
if not _accept(head_data):
|
| 313 |
+
msg = "Not a BMP file"
|
| 314 |
+
raise SyntaxError(msg)
|
| 315 |
+
# read the start position of the BMP image data (u32)
|
| 316 |
+
offset = i32(head_data, 10)
|
| 317 |
+
# load bitmap information (offset=raster info)
|
| 318 |
+
self._bitmap(offset=offset)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class BmpRleDecoder(ImageFile.PyDecoder):
|
| 322 |
+
_pulls_fd = True
|
| 323 |
+
|
| 324 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 325 |
+
assert self.fd is not None
|
| 326 |
+
rle4 = self.args[1]
|
| 327 |
+
data = bytearray()
|
| 328 |
+
x = 0
|
| 329 |
+
dest_length = self.state.xsize * self.state.ysize
|
| 330 |
+
while len(data) < dest_length:
|
| 331 |
+
pixels = self.fd.read(1)
|
| 332 |
+
byte = self.fd.read(1)
|
| 333 |
+
if not pixels or not byte:
|
| 334 |
+
break
|
| 335 |
+
num_pixels = pixels[0]
|
| 336 |
+
if num_pixels:
|
| 337 |
+
# encoded mode
|
| 338 |
+
if x + num_pixels > self.state.xsize:
|
| 339 |
+
# Too much data for row
|
| 340 |
+
num_pixels = max(0, self.state.xsize - x)
|
| 341 |
+
if rle4:
|
| 342 |
+
first_pixel = o8(byte[0] >> 4)
|
| 343 |
+
second_pixel = o8(byte[0] & 0x0F)
|
| 344 |
+
for index in range(num_pixels):
|
| 345 |
+
if index % 2 == 0:
|
| 346 |
+
data += first_pixel
|
| 347 |
+
else:
|
| 348 |
+
data += second_pixel
|
| 349 |
+
else:
|
| 350 |
+
data += byte * num_pixels
|
| 351 |
+
x += num_pixels
|
| 352 |
+
else:
|
| 353 |
+
if byte[0] == 0:
|
| 354 |
+
# end of line
|
| 355 |
+
while len(data) % self.state.xsize != 0:
|
| 356 |
+
data += b"\x00"
|
| 357 |
+
x = 0
|
| 358 |
+
elif byte[0] == 1:
|
| 359 |
+
# end of bitmap
|
| 360 |
+
break
|
| 361 |
+
elif byte[0] == 2:
|
| 362 |
+
# delta
|
| 363 |
+
bytes_read = self.fd.read(2)
|
| 364 |
+
if len(bytes_read) < 2:
|
| 365 |
+
break
|
| 366 |
+
right, up = self.fd.read(2)
|
| 367 |
+
data += b"\x00" * (right + up * self.state.xsize)
|
| 368 |
+
x = len(data) % self.state.xsize
|
| 369 |
+
else:
|
| 370 |
+
# absolute mode
|
| 371 |
+
if rle4:
|
| 372 |
+
# 2 pixels per byte
|
| 373 |
+
byte_count = byte[0] // 2
|
| 374 |
+
bytes_read = self.fd.read(byte_count)
|
| 375 |
+
for byte_read in bytes_read:
|
| 376 |
+
data += o8(byte_read >> 4)
|
| 377 |
+
data += o8(byte_read & 0x0F)
|
| 378 |
+
else:
|
| 379 |
+
byte_count = byte[0]
|
| 380 |
+
bytes_read = self.fd.read(byte_count)
|
| 381 |
+
data += bytes_read
|
| 382 |
+
if len(bytes_read) < byte_count:
|
| 383 |
+
break
|
| 384 |
+
x += byte[0]
|
| 385 |
+
|
| 386 |
+
# align to 16-bit word boundary
|
| 387 |
+
if self.fd.tell() % 2 != 0:
|
| 388 |
+
self.fd.seek(1, os.SEEK_CUR)
|
| 389 |
+
rawmode = "L" if self.mode == "L" else "P"
|
| 390 |
+
self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
|
| 391 |
+
return -1, 0
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
# =============================================================================
|
| 395 |
+
# Image plugin for the DIB format (BMP alias)
|
| 396 |
+
# =============================================================================
|
| 397 |
+
class DibImageFile(BmpImageFile):
|
| 398 |
+
format = "DIB"
|
| 399 |
+
format_description = "Windows Bitmap"
|
| 400 |
+
|
| 401 |
+
def _open(self) -> None:
|
| 402 |
+
self._bitmap()
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
#
|
| 406 |
+
# --------------------------------------------------------------------
|
| 407 |
+
# Write BMP file
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
SAVE = {
|
| 411 |
+
"1": ("1", 1, 2),
|
| 412 |
+
"L": ("L", 8, 256),
|
| 413 |
+
"P": ("P", 8, 256),
|
| 414 |
+
"RGB": ("BGR", 24, 0),
|
| 415 |
+
"RGBA": ("BGRA", 32, 0),
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 420 |
+
_save(im, fp, filename, False)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _save(
|
| 424 |
+
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
|
| 425 |
+
) -> None:
|
| 426 |
+
try:
|
| 427 |
+
rawmode, bits, colors = SAVE[im.mode]
|
| 428 |
+
except KeyError as e:
|
| 429 |
+
msg = f"cannot write mode {im.mode} as BMP"
|
| 430 |
+
raise OSError(msg) from e
|
| 431 |
+
|
| 432 |
+
info = im.encoderinfo
|
| 433 |
+
|
| 434 |
+
dpi = info.get("dpi", (96, 96))
|
| 435 |
+
|
| 436 |
+
# 1 meter == 39.3701 inches
|
| 437 |
+
ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
|
| 438 |
+
|
| 439 |
+
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
| 440 |
+
header = 40 # or 64 for OS/2 version 2
|
| 441 |
+
image = stride * im.size[1]
|
| 442 |
+
|
| 443 |
+
if im.mode == "1":
|
| 444 |
+
palette = b"".join(o8(i) * 4 for i in (0, 255))
|
| 445 |
+
elif im.mode == "L":
|
| 446 |
+
palette = b"".join(o8(i) * 4 for i in range(256))
|
| 447 |
+
elif im.mode == "P":
|
| 448 |
+
palette = im.im.getpalette("RGB", "BGRX")
|
| 449 |
+
colors = len(palette) // 4
|
| 450 |
+
else:
|
| 451 |
+
palette = None
|
| 452 |
+
|
| 453 |
+
# bitmap header
|
| 454 |
+
if bitmap_header:
|
| 455 |
+
offset = 14 + header + colors * 4
|
| 456 |
+
file_size = offset + image
|
| 457 |
+
if file_size > 2**32 - 1:
|
| 458 |
+
msg = "File size is too large for the BMP format"
|
| 459 |
+
raise ValueError(msg)
|
| 460 |
+
fp.write(
|
| 461 |
+
b"BM" # file type (magic)
|
| 462 |
+
+ o32(file_size) # file size
|
| 463 |
+
+ o32(0) # reserved
|
| 464 |
+
+ o32(offset) # image data offset
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
# bitmap info header
|
| 468 |
+
fp.write(
|
| 469 |
+
o32(header) # info header size
|
| 470 |
+
+ o32(im.size[0]) # width
|
| 471 |
+
+ o32(im.size[1]) # height
|
| 472 |
+
+ o16(1) # planes
|
| 473 |
+
+ o16(bits) # depth
|
| 474 |
+
+ o32(0) # compression (0=uncompressed)
|
| 475 |
+
+ o32(image) # size of bitmap
|
| 476 |
+
+ o32(ppm[0]) # resolution
|
| 477 |
+
+ o32(ppm[1]) # resolution
|
| 478 |
+
+ o32(colors) # colors used
|
| 479 |
+
+ o32(colors) # colors important
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
| 483 |
+
|
| 484 |
+
if palette:
|
| 485 |
+
fp.write(palette)
|
| 486 |
+
|
| 487 |
+
ImageFile._save(
|
| 488 |
+
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
#
|
| 493 |
+
# --------------------------------------------------------------------
|
| 494 |
+
# Registry
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
|
| 498 |
+
Image.register_save(BmpImageFile.format, _save)
|
| 499 |
+
|
| 500 |
+
Image.register_extension(BmpImageFile.format, ".bmp")
|
| 501 |
+
|
| 502 |
+
Image.register_mime(BmpImageFile.format, "image/bmp")
|
| 503 |
+
|
| 504 |
+
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
| 505 |
+
|
| 506 |
+
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
| 507 |
+
Image.register_save(DibImageFile.format, _dib_save)
|
| 508 |
+
|
| 509 |
+
Image.register_extension(DibImageFile.format, ".dib")
|
| 510 |
+
|
| 511 |
+
Image.register_mime(DibImageFile.format, "image/bmp")
|
Lib/site-packages/PIL/BufrStubImagePlugin.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# BUFR stub adapter
|
| 6 |
+
#
|
| 7 |
+
# Copyright (c) 1996-2003 by Fredrik Lundh
|
| 8 |
+
#
|
| 9 |
+
# See the README file for information on usage and redistribution.
|
| 10 |
+
#
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import IO
|
| 14 |
+
|
| 15 |
+
from . import Image, ImageFile
|
| 16 |
+
|
| 17 |
+
_handler = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
| 21 |
+
"""
|
| 22 |
+
Install application-specific BUFR image handler.
|
| 23 |
+
|
| 24 |
+
:param handler: Handler object.
|
| 25 |
+
"""
|
| 26 |
+
global _handler
|
| 27 |
+
_handler = handler
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# --------------------------------------------------------------------
|
| 31 |
+
# Image adapter
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _accept(prefix: bytes) -> bool:
|
| 35 |
+
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class BufrStubImageFile(ImageFile.StubImageFile):
|
| 39 |
+
format = "BUFR"
|
| 40 |
+
format_description = "BUFR"
|
| 41 |
+
|
| 42 |
+
def _open(self) -> None:
|
| 43 |
+
offset = self.fp.tell()
|
| 44 |
+
|
| 45 |
+
if not _accept(self.fp.read(4)):
|
| 46 |
+
msg = "Not a BUFR file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
self.fp.seek(offset)
|
| 50 |
+
|
| 51 |
+
# make something up
|
| 52 |
+
self._mode = "F"
|
| 53 |
+
self._size = 1, 1
|
| 54 |
+
|
| 55 |
+
loader = self._load()
|
| 56 |
+
if loader:
|
| 57 |
+
loader.open(self)
|
| 58 |
+
|
| 59 |
+
def _load(self) -> ImageFile.StubHandler | None:
|
| 60 |
+
return _handler
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 64 |
+
if _handler is None or not hasattr(_handler, "save"):
|
| 65 |
+
msg = "BUFR save handler not installed"
|
| 66 |
+
raise OSError(msg)
|
| 67 |
+
_handler.save(im, fp, filename)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# --------------------------------------------------------------------
|
| 71 |
+
# Registry
|
| 72 |
+
|
| 73 |
+
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
|
| 74 |
+
Image.register_save(BufrStubImageFile.format, _save)
|
| 75 |
+
|
| 76 |
+
Image.register_extension(BufrStubImageFile.format, ".bufr")
|
Lib/site-packages/PIL/ContainerIO.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# a class to read from a container file
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1995-06-18 fl Created
|
| 9 |
+
# 1995-09-07 fl Added readline(), readlines()
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) 1997-2001 by Secret Labs AB
|
| 12 |
+
# Copyright (c) 1995 by Fredrik Lundh
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import io
|
| 19 |
+
from collections.abc import Iterable
|
| 20 |
+
from typing import IO, AnyStr, NoReturn
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ContainerIO(IO[AnyStr]):
|
| 24 |
+
"""
|
| 25 |
+
A file object that provides read access to a part of an existing
|
| 26 |
+
file (for example a TAR file).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
|
| 30 |
+
"""
|
| 31 |
+
Create file object.
|
| 32 |
+
|
| 33 |
+
:param file: Existing file.
|
| 34 |
+
:param offset: Start of region, in bytes.
|
| 35 |
+
:param length: Size of region, in bytes.
|
| 36 |
+
"""
|
| 37 |
+
self.fh: IO[AnyStr] = file
|
| 38 |
+
self.pos = 0
|
| 39 |
+
self.offset = offset
|
| 40 |
+
self.length = length
|
| 41 |
+
self.fh.seek(offset)
|
| 42 |
+
|
| 43 |
+
##
|
| 44 |
+
# Always false.
|
| 45 |
+
|
| 46 |
+
def isatty(self) -> bool:
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
def seekable(self) -> bool:
|
| 50 |
+
return True
|
| 51 |
+
|
| 52 |
+
def seek(self, offset: int, mode: int = io.SEEK_SET) -> int:
|
| 53 |
+
"""
|
| 54 |
+
Move file pointer.
|
| 55 |
+
|
| 56 |
+
:param offset: Offset in bytes.
|
| 57 |
+
:param mode: Starting position. Use 0 for beginning of region, 1
|
| 58 |
+
for current offset, and 2 for end of region. You cannot move
|
| 59 |
+
the pointer outside the defined region.
|
| 60 |
+
:returns: Offset from start of region, in bytes.
|
| 61 |
+
"""
|
| 62 |
+
if mode == 1:
|
| 63 |
+
self.pos = self.pos + offset
|
| 64 |
+
elif mode == 2:
|
| 65 |
+
self.pos = self.length + offset
|
| 66 |
+
else:
|
| 67 |
+
self.pos = offset
|
| 68 |
+
# clamp
|
| 69 |
+
self.pos = max(0, min(self.pos, self.length))
|
| 70 |
+
self.fh.seek(self.offset + self.pos)
|
| 71 |
+
return self.pos
|
| 72 |
+
|
| 73 |
+
def tell(self) -> int:
|
| 74 |
+
"""
|
| 75 |
+
Get current file pointer.
|
| 76 |
+
|
| 77 |
+
:returns: Offset from start of region, in bytes.
|
| 78 |
+
"""
|
| 79 |
+
return self.pos
|
| 80 |
+
|
| 81 |
+
def readable(self) -> bool:
|
| 82 |
+
return True
|
| 83 |
+
|
| 84 |
+
def read(self, n: int = -1) -> AnyStr:
|
| 85 |
+
"""
|
| 86 |
+
Read data.
|
| 87 |
+
|
| 88 |
+
:param n: Number of bytes to read. If omitted, zero or negative,
|
| 89 |
+
read until end of region.
|
| 90 |
+
:returns: An 8-bit string.
|
| 91 |
+
"""
|
| 92 |
+
if n > 0:
|
| 93 |
+
n = min(n, self.length - self.pos)
|
| 94 |
+
else:
|
| 95 |
+
n = self.length - self.pos
|
| 96 |
+
if n <= 0: # EOF
|
| 97 |
+
return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
|
| 98 |
+
self.pos = self.pos + n
|
| 99 |
+
return self.fh.read(n)
|
| 100 |
+
|
| 101 |
+
def readline(self, n: int = -1) -> AnyStr:
|
| 102 |
+
"""
|
| 103 |
+
Read a line of text.
|
| 104 |
+
|
| 105 |
+
:param n: Number of bytes to read. If omitted, zero or negative,
|
| 106 |
+
read until end of line.
|
| 107 |
+
:returns: An 8-bit string.
|
| 108 |
+
"""
|
| 109 |
+
s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
|
| 110 |
+
newline_character = b"\n" if "b" in self.fh.mode else "\n"
|
| 111 |
+
while True:
|
| 112 |
+
c = self.read(1)
|
| 113 |
+
if not c:
|
| 114 |
+
break
|
| 115 |
+
s = s + c
|
| 116 |
+
if c == newline_character or len(s) == n:
|
| 117 |
+
break
|
| 118 |
+
return s
|
| 119 |
+
|
| 120 |
+
def readlines(self, n: int | None = -1) -> list[AnyStr]:
|
| 121 |
+
"""
|
| 122 |
+
Read multiple lines of text.
|
| 123 |
+
|
| 124 |
+
:param n: Number of lines to read. If omitted, zero, negative or None,
|
| 125 |
+
read until end of region.
|
| 126 |
+
:returns: A list of 8-bit strings.
|
| 127 |
+
"""
|
| 128 |
+
lines = []
|
| 129 |
+
while True:
|
| 130 |
+
s = self.readline()
|
| 131 |
+
if not s:
|
| 132 |
+
break
|
| 133 |
+
lines.append(s)
|
| 134 |
+
if len(lines) == n:
|
| 135 |
+
break
|
| 136 |
+
return lines
|
| 137 |
+
|
| 138 |
+
def writable(self) -> bool:
|
| 139 |
+
return False
|
| 140 |
+
|
| 141 |
+
def write(self, b: AnyStr) -> NoReturn:
|
| 142 |
+
raise NotImplementedError()
|
| 143 |
+
|
| 144 |
+
def writelines(self, lines: Iterable[AnyStr]) -> NoReturn:
|
| 145 |
+
raise NotImplementedError()
|
| 146 |
+
|
| 147 |
+
def truncate(self, size: int | None = None) -> int:
|
| 148 |
+
raise NotImplementedError()
|
| 149 |
+
|
| 150 |
+
def __enter__(self) -> ContainerIO[AnyStr]:
|
| 151 |
+
return self
|
| 152 |
+
|
| 153 |
+
def __exit__(self, *args: object) -> None:
|
| 154 |
+
self.close()
|
| 155 |
+
|
| 156 |
+
def __iter__(self) -> ContainerIO[AnyStr]:
|
| 157 |
+
return self
|
| 158 |
+
|
| 159 |
+
def __next__(self) -> AnyStr:
|
| 160 |
+
line = self.readline()
|
| 161 |
+
if not line:
|
| 162 |
+
msg = "end of region"
|
| 163 |
+
raise StopIteration(msg)
|
| 164 |
+
return line
|
| 165 |
+
|
| 166 |
+
def fileno(self) -> int:
|
| 167 |
+
return self.fh.fileno()
|
| 168 |
+
|
| 169 |
+
def flush(self) -> None:
|
| 170 |
+
self.fh.flush()
|
| 171 |
+
|
| 172 |
+
def close(self) -> None:
|
| 173 |
+
self.fh.close()
|
Lib/site-packages/PIL/CurImagePlugin.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# Windows Cursor support for PIL
|
| 6 |
+
#
|
| 7 |
+
# notes:
|
| 8 |
+
# uses BmpImagePlugin.py to read the bitmap data.
|
| 9 |
+
#
|
| 10 |
+
# history:
|
| 11 |
+
# 96-05-27 fl Created
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 14 |
+
# Copyright (c) Fredrik Lundh 1996.
|
| 15 |
+
#
|
| 16 |
+
# See the README file for information on usage and redistribution.
|
| 17 |
+
#
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from . import BmpImagePlugin, Image, ImageFile
|
| 21 |
+
from ._binary import i16le as i16
|
| 22 |
+
from ._binary import i32le as i32
|
| 23 |
+
|
| 24 |
+
#
|
| 25 |
+
# --------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _accept(prefix: bytes) -> bool:
|
| 29 |
+
return prefix[:4] == b"\0\0\2\0"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
##
|
| 33 |
+
# Image plugin for Windows Cursor files.
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class CurImageFile(BmpImagePlugin.BmpImageFile):
|
| 37 |
+
format = "CUR"
|
| 38 |
+
format_description = "Windows Cursor"
|
| 39 |
+
|
| 40 |
+
def _open(self) -> None:
|
| 41 |
+
offset = self.fp.tell()
|
| 42 |
+
|
| 43 |
+
# check magic
|
| 44 |
+
s = self.fp.read(6)
|
| 45 |
+
if not _accept(s):
|
| 46 |
+
msg = "not a CUR file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
# pick the largest cursor in the file
|
| 50 |
+
m = b""
|
| 51 |
+
for i in range(i16(s, 4)):
|
| 52 |
+
s = self.fp.read(16)
|
| 53 |
+
if not m:
|
| 54 |
+
m = s
|
| 55 |
+
elif s[0] > m[0] and s[1] > m[1]:
|
| 56 |
+
m = s
|
| 57 |
+
if not m:
|
| 58 |
+
msg = "No cursors were found"
|
| 59 |
+
raise TypeError(msg)
|
| 60 |
+
|
| 61 |
+
# load as bitmap
|
| 62 |
+
self._bitmap(i32(m, 12) + offset)
|
| 63 |
+
|
| 64 |
+
# patch up the bitmap height
|
| 65 |
+
self._size = self.size[0], self.size[1] // 2
|
| 66 |
+
d, e, o, a = self.tile[0]
|
| 67 |
+
self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
#
|
| 71 |
+
# --------------------------------------------------------------------
|
| 72 |
+
|
| 73 |
+
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
| 74 |
+
|
| 75 |
+
Image.register_extension(CurImageFile.format, ".cur")
|
Lib/site-packages/PIL/DcxImagePlugin.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# DCX file handling
|
| 6 |
+
#
|
| 7 |
+
# DCX is a container file format defined by Intel, commonly used
|
| 8 |
+
# for fax applications. Each DCX file consists of a directory
|
| 9 |
+
# (a list of file offsets) followed by a set of (usually 1-bit)
|
| 10 |
+
# PCX files.
|
| 11 |
+
#
|
| 12 |
+
# History:
|
| 13 |
+
# 1995-09-09 fl Created
|
| 14 |
+
# 1996-03-20 fl Properly derived from PcxImageFile.
|
| 15 |
+
# 1998-07-15 fl Renamed offset attribute to avoid name clash
|
| 16 |
+
# 2002-07-30 fl Fixed file handling
|
| 17 |
+
#
|
| 18 |
+
# Copyright (c) 1997-98 by Secret Labs AB.
|
| 19 |
+
# Copyright (c) 1995-96 by Fredrik Lundh.
|
| 20 |
+
#
|
| 21 |
+
# See the README file for information on usage and redistribution.
|
| 22 |
+
#
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
from . import Image
|
| 26 |
+
from ._binary import i32le as i32
|
| 27 |
+
from .PcxImagePlugin import PcxImageFile
|
| 28 |
+
|
| 29 |
+
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _accept(prefix: bytes) -> bool:
|
| 33 |
+
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
##
|
| 37 |
+
# Image plugin for the Intel DCX format.
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class DcxImageFile(PcxImageFile):
|
| 41 |
+
format = "DCX"
|
| 42 |
+
format_description = "Intel DCX"
|
| 43 |
+
_close_exclusive_fp_after_loading = False
|
| 44 |
+
|
| 45 |
+
def _open(self) -> None:
|
| 46 |
+
# Header
|
| 47 |
+
s = self.fp.read(4)
|
| 48 |
+
if not _accept(s):
|
| 49 |
+
msg = "not a DCX file"
|
| 50 |
+
raise SyntaxError(msg)
|
| 51 |
+
|
| 52 |
+
# Component directory
|
| 53 |
+
self._offset = []
|
| 54 |
+
for i in range(1024):
|
| 55 |
+
offset = i32(self.fp.read(4))
|
| 56 |
+
if not offset:
|
| 57 |
+
break
|
| 58 |
+
self._offset.append(offset)
|
| 59 |
+
|
| 60 |
+
self._fp = self.fp
|
| 61 |
+
self.frame = -1
|
| 62 |
+
self.n_frames = len(self._offset)
|
| 63 |
+
self.is_animated = self.n_frames > 1
|
| 64 |
+
self.seek(0)
|
| 65 |
+
|
| 66 |
+
def seek(self, frame: int) -> None:
|
| 67 |
+
if not self._seek_check(frame):
|
| 68 |
+
return
|
| 69 |
+
self.frame = frame
|
| 70 |
+
self.fp = self._fp
|
| 71 |
+
self.fp.seek(self._offset[frame])
|
| 72 |
+
PcxImageFile._open(self)
|
| 73 |
+
|
| 74 |
+
def tell(self) -> int:
|
| 75 |
+
return self.frame
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
| 79 |
+
|
| 80 |
+
Image.register_extension(DcxImageFile.format, ".dcx")
|
Lib/site-packages/PIL/DdsImagePlugin.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A Pillow loader for .dds files (S3TC-compressed aka DXTC)
|
| 3 |
+
Jerome Leclanche <jerome@leclan.ch>
|
| 4 |
+
|
| 5 |
+
Documentation:
|
| 6 |
+
https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
|
| 7 |
+
|
| 8 |
+
The contents of this file are hereby released in the public domain (CC0)
|
| 9 |
+
Full text of the CC0 license:
|
| 10 |
+
https://creativecommons.org/publicdomain/zero/1.0/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import io
|
| 16 |
+
import struct
|
| 17 |
+
import sys
|
| 18 |
+
from enum import IntEnum, IntFlag
|
| 19 |
+
from typing import IO
|
| 20 |
+
|
| 21 |
+
from . import Image, ImageFile, ImagePalette
|
| 22 |
+
from ._binary import i32le as i32
|
| 23 |
+
from ._binary import o8
|
| 24 |
+
from ._binary import o32le as o32
|
| 25 |
+
|
| 26 |
+
# Magic ("DDS ")
|
| 27 |
+
DDS_MAGIC = 0x20534444
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# DDS flags
|
| 31 |
+
class DDSD(IntFlag):
|
| 32 |
+
CAPS = 0x1
|
| 33 |
+
HEIGHT = 0x2
|
| 34 |
+
WIDTH = 0x4
|
| 35 |
+
PITCH = 0x8
|
| 36 |
+
PIXELFORMAT = 0x1000
|
| 37 |
+
MIPMAPCOUNT = 0x20000
|
| 38 |
+
LINEARSIZE = 0x80000
|
| 39 |
+
DEPTH = 0x800000
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# DDS caps
|
| 43 |
+
class DDSCAPS(IntFlag):
|
| 44 |
+
COMPLEX = 0x8
|
| 45 |
+
TEXTURE = 0x1000
|
| 46 |
+
MIPMAP = 0x400000
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class DDSCAPS2(IntFlag):
|
| 50 |
+
CUBEMAP = 0x200
|
| 51 |
+
CUBEMAP_POSITIVEX = 0x400
|
| 52 |
+
CUBEMAP_NEGATIVEX = 0x800
|
| 53 |
+
CUBEMAP_POSITIVEY = 0x1000
|
| 54 |
+
CUBEMAP_NEGATIVEY = 0x2000
|
| 55 |
+
CUBEMAP_POSITIVEZ = 0x4000
|
| 56 |
+
CUBEMAP_NEGATIVEZ = 0x8000
|
| 57 |
+
VOLUME = 0x200000
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Pixel Format
|
| 61 |
+
class DDPF(IntFlag):
|
| 62 |
+
ALPHAPIXELS = 0x1
|
| 63 |
+
ALPHA = 0x2
|
| 64 |
+
FOURCC = 0x4
|
| 65 |
+
PALETTEINDEXED8 = 0x20
|
| 66 |
+
RGB = 0x40
|
| 67 |
+
LUMINANCE = 0x20000
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# dxgiformat.h
|
| 71 |
+
class DXGI_FORMAT(IntEnum):
|
| 72 |
+
UNKNOWN = 0
|
| 73 |
+
R32G32B32A32_TYPELESS = 1
|
| 74 |
+
R32G32B32A32_FLOAT = 2
|
| 75 |
+
R32G32B32A32_UINT = 3
|
| 76 |
+
R32G32B32A32_SINT = 4
|
| 77 |
+
R32G32B32_TYPELESS = 5
|
| 78 |
+
R32G32B32_FLOAT = 6
|
| 79 |
+
R32G32B32_UINT = 7
|
| 80 |
+
R32G32B32_SINT = 8
|
| 81 |
+
R16G16B16A16_TYPELESS = 9
|
| 82 |
+
R16G16B16A16_FLOAT = 10
|
| 83 |
+
R16G16B16A16_UNORM = 11
|
| 84 |
+
R16G16B16A16_UINT = 12
|
| 85 |
+
R16G16B16A16_SNORM = 13
|
| 86 |
+
R16G16B16A16_SINT = 14
|
| 87 |
+
R32G32_TYPELESS = 15
|
| 88 |
+
R32G32_FLOAT = 16
|
| 89 |
+
R32G32_UINT = 17
|
| 90 |
+
R32G32_SINT = 18
|
| 91 |
+
R32G8X24_TYPELESS = 19
|
| 92 |
+
D32_FLOAT_S8X24_UINT = 20
|
| 93 |
+
R32_FLOAT_X8X24_TYPELESS = 21
|
| 94 |
+
X32_TYPELESS_G8X24_UINT = 22
|
| 95 |
+
R10G10B10A2_TYPELESS = 23
|
| 96 |
+
R10G10B10A2_UNORM = 24
|
| 97 |
+
R10G10B10A2_UINT = 25
|
| 98 |
+
R11G11B10_FLOAT = 26
|
| 99 |
+
R8G8B8A8_TYPELESS = 27
|
| 100 |
+
R8G8B8A8_UNORM = 28
|
| 101 |
+
R8G8B8A8_UNORM_SRGB = 29
|
| 102 |
+
R8G8B8A8_UINT = 30
|
| 103 |
+
R8G8B8A8_SNORM = 31
|
| 104 |
+
R8G8B8A8_SINT = 32
|
| 105 |
+
R16G16_TYPELESS = 33
|
| 106 |
+
R16G16_FLOAT = 34
|
| 107 |
+
R16G16_UNORM = 35
|
| 108 |
+
R16G16_UINT = 36
|
| 109 |
+
R16G16_SNORM = 37
|
| 110 |
+
R16G16_SINT = 38
|
| 111 |
+
R32_TYPELESS = 39
|
| 112 |
+
D32_FLOAT = 40
|
| 113 |
+
R32_FLOAT = 41
|
| 114 |
+
R32_UINT = 42
|
| 115 |
+
R32_SINT = 43
|
| 116 |
+
R24G8_TYPELESS = 44
|
| 117 |
+
D24_UNORM_S8_UINT = 45
|
| 118 |
+
R24_UNORM_X8_TYPELESS = 46
|
| 119 |
+
X24_TYPELESS_G8_UINT = 47
|
| 120 |
+
R8G8_TYPELESS = 48
|
| 121 |
+
R8G8_UNORM = 49
|
| 122 |
+
R8G8_UINT = 50
|
| 123 |
+
R8G8_SNORM = 51
|
| 124 |
+
R8G8_SINT = 52
|
| 125 |
+
R16_TYPELESS = 53
|
| 126 |
+
R16_FLOAT = 54
|
| 127 |
+
D16_UNORM = 55
|
| 128 |
+
R16_UNORM = 56
|
| 129 |
+
R16_UINT = 57
|
| 130 |
+
R16_SNORM = 58
|
| 131 |
+
R16_SINT = 59
|
| 132 |
+
R8_TYPELESS = 60
|
| 133 |
+
R8_UNORM = 61
|
| 134 |
+
R8_UINT = 62
|
| 135 |
+
R8_SNORM = 63
|
| 136 |
+
R8_SINT = 64
|
| 137 |
+
A8_UNORM = 65
|
| 138 |
+
R1_UNORM = 66
|
| 139 |
+
R9G9B9E5_SHAREDEXP = 67
|
| 140 |
+
R8G8_B8G8_UNORM = 68
|
| 141 |
+
G8R8_G8B8_UNORM = 69
|
| 142 |
+
BC1_TYPELESS = 70
|
| 143 |
+
BC1_UNORM = 71
|
| 144 |
+
BC1_UNORM_SRGB = 72
|
| 145 |
+
BC2_TYPELESS = 73
|
| 146 |
+
BC2_UNORM = 74
|
| 147 |
+
BC2_UNORM_SRGB = 75
|
| 148 |
+
BC3_TYPELESS = 76
|
| 149 |
+
BC3_UNORM = 77
|
| 150 |
+
BC3_UNORM_SRGB = 78
|
| 151 |
+
BC4_TYPELESS = 79
|
| 152 |
+
BC4_UNORM = 80
|
| 153 |
+
BC4_SNORM = 81
|
| 154 |
+
BC5_TYPELESS = 82
|
| 155 |
+
BC5_UNORM = 83
|
| 156 |
+
BC5_SNORM = 84
|
| 157 |
+
B5G6R5_UNORM = 85
|
| 158 |
+
B5G5R5A1_UNORM = 86
|
| 159 |
+
B8G8R8A8_UNORM = 87
|
| 160 |
+
B8G8R8X8_UNORM = 88
|
| 161 |
+
R10G10B10_XR_BIAS_A2_UNORM = 89
|
| 162 |
+
B8G8R8A8_TYPELESS = 90
|
| 163 |
+
B8G8R8A8_UNORM_SRGB = 91
|
| 164 |
+
B8G8R8X8_TYPELESS = 92
|
| 165 |
+
B8G8R8X8_UNORM_SRGB = 93
|
| 166 |
+
BC6H_TYPELESS = 94
|
| 167 |
+
BC6H_UF16 = 95
|
| 168 |
+
BC6H_SF16 = 96
|
| 169 |
+
BC7_TYPELESS = 97
|
| 170 |
+
BC7_UNORM = 98
|
| 171 |
+
BC7_UNORM_SRGB = 99
|
| 172 |
+
AYUV = 100
|
| 173 |
+
Y410 = 101
|
| 174 |
+
Y416 = 102
|
| 175 |
+
NV12 = 103
|
| 176 |
+
P010 = 104
|
| 177 |
+
P016 = 105
|
| 178 |
+
OPAQUE_420 = 106
|
| 179 |
+
YUY2 = 107
|
| 180 |
+
Y210 = 108
|
| 181 |
+
Y216 = 109
|
| 182 |
+
NV11 = 110
|
| 183 |
+
AI44 = 111
|
| 184 |
+
IA44 = 112
|
| 185 |
+
P8 = 113
|
| 186 |
+
A8P8 = 114
|
| 187 |
+
B4G4R4A4_UNORM = 115
|
| 188 |
+
P208 = 130
|
| 189 |
+
V208 = 131
|
| 190 |
+
V408 = 132
|
| 191 |
+
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
|
| 192 |
+
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class D3DFMT(IntEnum):
|
| 196 |
+
UNKNOWN = 0
|
| 197 |
+
R8G8B8 = 20
|
| 198 |
+
A8R8G8B8 = 21
|
| 199 |
+
X8R8G8B8 = 22
|
| 200 |
+
R5G6B5 = 23
|
| 201 |
+
X1R5G5B5 = 24
|
| 202 |
+
A1R5G5B5 = 25
|
| 203 |
+
A4R4G4B4 = 26
|
| 204 |
+
R3G3B2 = 27
|
| 205 |
+
A8 = 28
|
| 206 |
+
A8R3G3B2 = 29
|
| 207 |
+
X4R4G4B4 = 30
|
| 208 |
+
A2B10G10R10 = 31
|
| 209 |
+
A8B8G8R8 = 32
|
| 210 |
+
X8B8G8R8 = 33
|
| 211 |
+
G16R16 = 34
|
| 212 |
+
A2R10G10B10 = 35
|
| 213 |
+
A16B16G16R16 = 36
|
| 214 |
+
A8P8 = 40
|
| 215 |
+
P8 = 41
|
| 216 |
+
L8 = 50
|
| 217 |
+
A8L8 = 51
|
| 218 |
+
A4L4 = 52
|
| 219 |
+
V8U8 = 60
|
| 220 |
+
L6V5U5 = 61
|
| 221 |
+
X8L8V8U8 = 62
|
| 222 |
+
Q8W8V8U8 = 63
|
| 223 |
+
V16U16 = 64
|
| 224 |
+
A2W10V10U10 = 67
|
| 225 |
+
D16_LOCKABLE = 70
|
| 226 |
+
D32 = 71
|
| 227 |
+
D15S1 = 73
|
| 228 |
+
D24S8 = 75
|
| 229 |
+
D24X8 = 77
|
| 230 |
+
D24X4S4 = 79
|
| 231 |
+
D16 = 80
|
| 232 |
+
D32F_LOCKABLE = 82
|
| 233 |
+
D24FS8 = 83
|
| 234 |
+
D32_LOCKABLE = 84
|
| 235 |
+
S8_LOCKABLE = 85
|
| 236 |
+
L16 = 81
|
| 237 |
+
VERTEXDATA = 100
|
| 238 |
+
INDEX16 = 101
|
| 239 |
+
INDEX32 = 102
|
| 240 |
+
Q16W16V16U16 = 110
|
| 241 |
+
R16F = 111
|
| 242 |
+
G16R16F = 112
|
| 243 |
+
A16B16G16R16F = 113
|
| 244 |
+
R32F = 114
|
| 245 |
+
G32R32F = 115
|
| 246 |
+
A32B32G32R32F = 116
|
| 247 |
+
CxV8U8 = 117
|
| 248 |
+
A1 = 118
|
| 249 |
+
A2B10G10R10_XR_BIAS = 119
|
| 250 |
+
BINARYBUFFER = 199
|
| 251 |
+
|
| 252 |
+
UYVY = i32(b"UYVY")
|
| 253 |
+
R8G8_B8G8 = i32(b"RGBG")
|
| 254 |
+
YUY2 = i32(b"YUY2")
|
| 255 |
+
G8R8_G8B8 = i32(b"GRGB")
|
| 256 |
+
DXT1 = i32(b"DXT1")
|
| 257 |
+
DXT2 = i32(b"DXT2")
|
| 258 |
+
DXT3 = i32(b"DXT3")
|
| 259 |
+
DXT4 = i32(b"DXT4")
|
| 260 |
+
DXT5 = i32(b"DXT5")
|
| 261 |
+
DX10 = i32(b"DX10")
|
| 262 |
+
BC4S = i32(b"BC4S")
|
| 263 |
+
BC4U = i32(b"BC4U")
|
| 264 |
+
BC5S = i32(b"BC5S")
|
| 265 |
+
BC5U = i32(b"BC5U")
|
| 266 |
+
ATI1 = i32(b"ATI1")
|
| 267 |
+
ATI2 = i32(b"ATI2")
|
| 268 |
+
MULTI2_ARGB8 = i32(b"MET1")
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# Backward compatibility layer
|
| 272 |
+
module = sys.modules[__name__]
|
| 273 |
+
for item in DDSD:
|
| 274 |
+
assert item.name is not None
|
| 275 |
+
setattr(module, f"DDSD_{item.name}", item.value)
|
| 276 |
+
for item1 in DDSCAPS:
|
| 277 |
+
assert item1.name is not None
|
| 278 |
+
setattr(module, f"DDSCAPS_{item1.name}", item1.value)
|
| 279 |
+
for item2 in DDSCAPS2:
|
| 280 |
+
assert item2.name is not None
|
| 281 |
+
setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
|
| 282 |
+
for item3 in DDPF:
|
| 283 |
+
assert item3.name is not None
|
| 284 |
+
setattr(module, f"DDPF_{item3.name}", item3.value)
|
| 285 |
+
|
| 286 |
+
DDS_FOURCC = DDPF.FOURCC
|
| 287 |
+
DDS_RGB = DDPF.RGB
|
| 288 |
+
DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
|
| 289 |
+
DDS_LUMINANCE = DDPF.LUMINANCE
|
| 290 |
+
DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
|
| 291 |
+
DDS_ALPHA = DDPF.ALPHA
|
| 292 |
+
DDS_PAL8 = DDPF.PALETTEINDEXED8
|
| 293 |
+
|
| 294 |
+
DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
|
| 295 |
+
DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
|
| 296 |
+
DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
|
| 297 |
+
DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
|
| 298 |
+
DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
|
| 299 |
+
|
| 300 |
+
DDS_HEIGHT = DDSD.HEIGHT
|
| 301 |
+
DDS_WIDTH = DDSD.WIDTH
|
| 302 |
+
|
| 303 |
+
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
|
| 304 |
+
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
|
| 305 |
+
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
|
| 306 |
+
|
| 307 |
+
DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
|
| 308 |
+
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
|
| 309 |
+
DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
|
| 310 |
+
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
|
| 311 |
+
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
|
| 312 |
+
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
|
| 313 |
+
|
| 314 |
+
DXT1_FOURCC = D3DFMT.DXT1
|
| 315 |
+
DXT3_FOURCC = D3DFMT.DXT3
|
| 316 |
+
DXT5_FOURCC = D3DFMT.DXT5
|
| 317 |
+
|
| 318 |
+
DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
|
| 319 |
+
DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
|
| 320 |
+
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
|
| 321 |
+
DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
|
| 322 |
+
DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
|
| 323 |
+
DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
|
| 324 |
+
DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
|
| 325 |
+
DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
|
| 326 |
+
DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
|
| 327 |
+
DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
|
| 328 |
+
DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class DdsImageFile(ImageFile.ImageFile):
|
| 332 |
+
format = "DDS"
|
| 333 |
+
format_description = "DirectDraw Surface"
|
| 334 |
+
|
| 335 |
+
def _open(self) -> None:
|
| 336 |
+
if not _accept(self.fp.read(4)):
|
| 337 |
+
msg = "not a DDS file"
|
| 338 |
+
raise SyntaxError(msg)
|
| 339 |
+
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
| 340 |
+
if header_size != 124:
|
| 341 |
+
msg = f"Unsupported header size {repr(header_size)}"
|
| 342 |
+
raise OSError(msg)
|
| 343 |
+
header_bytes = self.fp.read(header_size - 4)
|
| 344 |
+
if len(header_bytes) != 120:
|
| 345 |
+
msg = f"Incomplete header: {len(header_bytes)} bytes"
|
| 346 |
+
raise OSError(msg)
|
| 347 |
+
header = io.BytesIO(header_bytes)
|
| 348 |
+
|
| 349 |
+
flags, height, width = struct.unpack("<3I", header.read(12))
|
| 350 |
+
self._size = (width, height)
|
| 351 |
+
extents = (0, 0) + self.size
|
| 352 |
+
|
| 353 |
+
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
| 354 |
+
struct.unpack("<11I", header.read(44)) # reserved
|
| 355 |
+
|
| 356 |
+
# pixel format
|
| 357 |
+
pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16))
|
| 358 |
+
n = 0
|
| 359 |
+
rawmode = None
|
| 360 |
+
if pfflags & DDPF.RGB:
|
| 361 |
+
# Texture contains uncompressed RGB data
|
| 362 |
+
if pfflags & DDPF.ALPHAPIXELS:
|
| 363 |
+
self._mode = "RGBA"
|
| 364 |
+
mask_count = 4
|
| 365 |
+
else:
|
| 366 |
+
self._mode = "RGB"
|
| 367 |
+
mask_count = 3
|
| 368 |
+
|
| 369 |
+
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
|
| 370 |
+
self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
|
| 371 |
+
return
|
| 372 |
+
elif pfflags & DDPF.LUMINANCE:
|
| 373 |
+
if bitcount == 8:
|
| 374 |
+
self._mode = "L"
|
| 375 |
+
elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
|
| 376 |
+
self._mode = "LA"
|
| 377 |
+
else:
|
| 378 |
+
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
|
| 379 |
+
raise OSError(msg)
|
| 380 |
+
elif pfflags & DDPF.PALETTEINDEXED8:
|
| 381 |
+
self._mode = "P"
|
| 382 |
+
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
| 383 |
+
self.palette.mode = "RGBA"
|
| 384 |
+
elif pfflags & DDPF.FOURCC:
|
| 385 |
+
offset = header_size + 4
|
| 386 |
+
if fourcc == D3DFMT.DXT1:
|
| 387 |
+
self._mode = "RGBA"
|
| 388 |
+
self.pixel_format = "DXT1"
|
| 389 |
+
n = 1
|
| 390 |
+
elif fourcc == D3DFMT.DXT3:
|
| 391 |
+
self._mode = "RGBA"
|
| 392 |
+
self.pixel_format = "DXT3"
|
| 393 |
+
n = 2
|
| 394 |
+
elif fourcc == D3DFMT.DXT5:
|
| 395 |
+
self._mode = "RGBA"
|
| 396 |
+
self.pixel_format = "DXT5"
|
| 397 |
+
n = 3
|
| 398 |
+
elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
|
| 399 |
+
self._mode = "L"
|
| 400 |
+
self.pixel_format = "BC4"
|
| 401 |
+
n = 4
|
| 402 |
+
elif fourcc == D3DFMT.BC5S:
|
| 403 |
+
self._mode = "RGB"
|
| 404 |
+
self.pixel_format = "BC5S"
|
| 405 |
+
n = 5
|
| 406 |
+
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
|
| 407 |
+
self._mode = "RGB"
|
| 408 |
+
self.pixel_format = "BC5"
|
| 409 |
+
n = 5
|
| 410 |
+
elif fourcc == D3DFMT.DX10:
|
| 411 |
+
offset += 20
|
| 412 |
+
# ignoring flags which pertain to volume textures and cubemaps
|
| 413 |
+
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
|
| 414 |
+
self.fp.read(16)
|
| 415 |
+
if dxgi_format in (
|
| 416 |
+
DXGI_FORMAT.BC1_UNORM,
|
| 417 |
+
DXGI_FORMAT.BC1_TYPELESS,
|
| 418 |
+
):
|
| 419 |
+
self._mode = "RGBA"
|
| 420 |
+
self.pixel_format = "BC1"
|
| 421 |
+
n = 1
|
| 422 |
+
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
|
| 423 |
+
self._mode = "L"
|
| 424 |
+
self.pixel_format = "BC4"
|
| 425 |
+
n = 4
|
| 426 |
+
elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
|
| 427 |
+
self._mode = "RGB"
|
| 428 |
+
self.pixel_format = "BC5"
|
| 429 |
+
n = 5
|
| 430 |
+
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
|
| 431 |
+
self._mode = "RGB"
|
| 432 |
+
self.pixel_format = "BC5S"
|
| 433 |
+
n = 5
|
| 434 |
+
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
|
| 435 |
+
self._mode = "RGB"
|
| 436 |
+
self.pixel_format = "BC6H"
|
| 437 |
+
n = 6
|
| 438 |
+
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
|
| 439 |
+
self._mode = "RGB"
|
| 440 |
+
self.pixel_format = "BC6HS"
|
| 441 |
+
n = 6
|
| 442 |
+
elif dxgi_format in (
|
| 443 |
+
DXGI_FORMAT.BC7_TYPELESS,
|
| 444 |
+
DXGI_FORMAT.BC7_UNORM,
|
| 445 |
+
DXGI_FORMAT.BC7_UNORM_SRGB,
|
| 446 |
+
):
|
| 447 |
+
self._mode = "RGBA"
|
| 448 |
+
self.pixel_format = "BC7"
|
| 449 |
+
n = 7
|
| 450 |
+
if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
|
| 451 |
+
self.info["gamma"] = 1 / 2.2
|
| 452 |
+
elif dxgi_format in (
|
| 453 |
+
DXGI_FORMAT.R8G8B8A8_TYPELESS,
|
| 454 |
+
DXGI_FORMAT.R8G8B8A8_UNORM,
|
| 455 |
+
DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
|
| 456 |
+
):
|
| 457 |
+
self._mode = "RGBA"
|
| 458 |
+
if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
|
| 459 |
+
self.info["gamma"] = 1 / 2.2
|
| 460 |
+
else:
|
| 461 |
+
msg = f"Unimplemented DXGI format {dxgi_format}"
|
| 462 |
+
raise NotImplementedError(msg)
|
| 463 |
+
else:
|
| 464 |
+
msg = f"Unimplemented pixel format {repr(fourcc)}"
|
| 465 |
+
raise NotImplementedError(msg)
|
| 466 |
+
else:
|
| 467 |
+
msg = f"Unknown pixel format flags {pfflags}"
|
| 468 |
+
raise NotImplementedError(msg)
|
| 469 |
+
|
| 470 |
+
if n:
|
| 471 |
+
self.tile = [
|
| 472 |
+
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
| 473 |
+
]
|
| 474 |
+
else:
|
| 475 |
+
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
|
| 476 |
+
|
| 477 |
+
def load_seek(self, pos: int) -> None:
|
| 478 |
+
pass
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class DdsRgbDecoder(ImageFile.PyDecoder):
|
| 482 |
+
_pulls_fd = True
|
| 483 |
+
|
| 484 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 485 |
+
assert self.fd is not None
|
| 486 |
+
bitcount, masks = self.args
|
| 487 |
+
|
| 488 |
+
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
|
| 489 |
+
# Calculate how many zeros each mask is padded with
|
| 490 |
+
mask_offsets = []
|
| 491 |
+
# And the maximum value of each channel without the padding
|
| 492 |
+
mask_totals = []
|
| 493 |
+
for mask in masks:
|
| 494 |
+
offset = 0
|
| 495 |
+
if mask != 0:
|
| 496 |
+
while mask >> (offset + 1) << (offset + 1) == mask:
|
| 497 |
+
offset += 1
|
| 498 |
+
mask_offsets.append(offset)
|
| 499 |
+
mask_totals.append(mask >> offset)
|
| 500 |
+
|
| 501 |
+
data = bytearray()
|
| 502 |
+
bytecount = bitcount // 8
|
| 503 |
+
dest_length = self.state.xsize * self.state.ysize * len(masks)
|
| 504 |
+
while len(data) < dest_length:
|
| 505 |
+
value = int.from_bytes(self.fd.read(bytecount), "little")
|
| 506 |
+
for i, mask in enumerate(masks):
|
| 507 |
+
masked_value = value & mask
|
| 508 |
+
# Remove the zero padding, and scale it to 8 bits
|
| 509 |
+
data += o8(
|
| 510 |
+
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
|
| 511 |
+
)
|
| 512 |
+
self.set_as_raw(data)
|
| 513 |
+
return -1, 0
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 517 |
+
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
| 518 |
+
msg = f"cannot write mode {im.mode} as DDS"
|
| 519 |
+
raise OSError(msg)
|
| 520 |
+
|
| 521 |
+
alpha = im.mode[-1] == "A"
|
| 522 |
+
if im.mode[0] == "L":
|
| 523 |
+
pixel_flags = DDPF.LUMINANCE
|
| 524 |
+
rawmode = im.mode
|
| 525 |
+
if alpha:
|
| 526 |
+
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
|
| 527 |
+
else:
|
| 528 |
+
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
|
| 529 |
+
else:
|
| 530 |
+
pixel_flags = DDPF.RGB
|
| 531 |
+
rawmode = im.mode[::-1]
|
| 532 |
+
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
|
| 533 |
+
|
| 534 |
+
if alpha:
|
| 535 |
+
r, g, b, a = im.split()
|
| 536 |
+
im = Image.merge("RGBA", (a, r, g, b))
|
| 537 |
+
if alpha:
|
| 538 |
+
pixel_flags |= DDPF.ALPHAPIXELS
|
| 539 |
+
rgba_mask.append(0xFF000000 if alpha else 0)
|
| 540 |
+
|
| 541 |
+
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
|
| 542 |
+
bitcount = len(im.getbands()) * 8
|
| 543 |
+
pitch = (im.width * bitcount + 7) // 8
|
| 544 |
+
|
| 545 |
+
fp.write(
|
| 546 |
+
o32(DDS_MAGIC)
|
| 547 |
+
+ struct.pack(
|
| 548 |
+
"<7I",
|
| 549 |
+
124, # header size
|
| 550 |
+
flags, # flags
|
| 551 |
+
im.height,
|
| 552 |
+
im.width,
|
| 553 |
+
pitch,
|
| 554 |
+
0, # depth
|
| 555 |
+
0, # mipmaps
|
| 556 |
+
)
|
| 557 |
+
+ struct.pack("11I", *((0,) * 11)) # reserved
|
| 558 |
+
# pfsize, pfflags, fourcc, bitcount
|
| 559 |
+
+ struct.pack("<4I", 32, pixel_flags, 0, bitcount)
|
| 560 |
+
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
|
| 561 |
+
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
|
| 562 |
+
)
|
| 563 |
+
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def _accept(prefix: bytes) -> bool:
|
| 567 |
+
return prefix[:4] == b"DDS "
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
| 571 |
+
Image.register_decoder("dds_rgb", DdsRgbDecoder)
|
| 572 |
+
Image.register_save(DdsImageFile.format, _save)
|
| 573 |
+
Image.register_extension(DdsImageFile.format, ".dds")
|
Lib/site-packages/PIL/EpsImagePlugin.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# EPS file handling
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1995-09-01 fl Created (0.1)
|
| 9 |
+
# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
|
| 10 |
+
# 1996-08-22 fl Don't choke on floating point BoundingBox values
|
| 11 |
+
# 1996-08-23 fl Handle files from Macintosh (0.3)
|
| 12 |
+
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
|
| 13 |
+
# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
|
| 14 |
+
# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
|
| 15 |
+
# resizing
|
| 16 |
+
#
|
| 17 |
+
# Copyright (c) 1997-2003 by Secret Labs AB.
|
| 18 |
+
# Copyright (c) 1995-2003 by Fredrik Lundh
|
| 19 |
+
#
|
| 20 |
+
# See the README file for information on usage and redistribution.
|
| 21 |
+
#
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import io
|
| 25 |
+
import os
|
| 26 |
+
import re
|
| 27 |
+
import subprocess
|
| 28 |
+
import sys
|
| 29 |
+
import tempfile
|
| 30 |
+
from typing import IO
|
| 31 |
+
|
| 32 |
+
from . import Image, ImageFile
|
| 33 |
+
from ._binary import i32le as i32
|
| 34 |
+
|
| 35 |
+
# --------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
| 39 |
+
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
| 40 |
+
|
| 41 |
+
gs_binary: str | bool | None = None
|
| 42 |
+
gs_windows_binary = None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def has_ghostscript() -> bool:
|
| 46 |
+
global gs_binary, gs_windows_binary
|
| 47 |
+
if gs_binary is None:
|
| 48 |
+
if sys.platform.startswith("win"):
|
| 49 |
+
if gs_windows_binary is None:
|
| 50 |
+
import shutil
|
| 51 |
+
|
| 52 |
+
for binary in ("gswin32c", "gswin64c", "gs"):
|
| 53 |
+
if shutil.which(binary) is not None:
|
| 54 |
+
gs_windows_binary = binary
|
| 55 |
+
break
|
| 56 |
+
else:
|
| 57 |
+
gs_windows_binary = False
|
| 58 |
+
gs_binary = gs_windows_binary
|
| 59 |
+
else:
|
| 60 |
+
try:
|
| 61 |
+
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
| 62 |
+
gs_binary = "gs"
|
| 63 |
+
except OSError:
|
| 64 |
+
gs_binary = False
|
| 65 |
+
return gs_binary is not False
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def Ghostscript(
|
| 69 |
+
tile: list[ImageFile._Tile],
|
| 70 |
+
size: tuple[int, int],
|
| 71 |
+
fp: IO[bytes],
|
| 72 |
+
scale: int = 1,
|
| 73 |
+
transparency: bool = False,
|
| 74 |
+
) -> Image.core.ImagingCore:
|
| 75 |
+
"""Render an image using Ghostscript"""
|
| 76 |
+
global gs_binary
|
| 77 |
+
if not has_ghostscript():
|
| 78 |
+
msg = "Unable to locate Ghostscript on paths"
|
| 79 |
+
raise OSError(msg)
|
| 80 |
+
assert isinstance(gs_binary, str)
|
| 81 |
+
|
| 82 |
+
# Unpack decoder tile
|
| 83 |
+
args = tile[0].args
|
| 84 |
+
assert isinstance(args, tuple)
|
| 85 |
+
length, bbox = args
|
| 86 |
+
|
| 87 |
+
# Hack to support hi-res rendering
|
| 88 |
+
scale = int(scale) or 1
|
| 89 |
+
width = size[0] * scale
|
| 90 |
+
height = size[1] * scale
|
| 91 |
+
# resolution is dependent on bbox and size
|
| 92 |
+
res_x = 72.0 * width / (bbox[2] - bbox[0])
|
| 93 |
+
res_y = 72.0 * height / (bbox[3] - bbox[1])
|
| 94 |
+
|
| 95 |
+
out_fd, outfile = tempfile.mkstemp()
|
| 96 |
+
os.close(out_fd)
|
| 97 |
+
|
| 98 |
+
infile_temp = None
|
| 99 |
+
if hasattr(fp, "name") and os.path.exists(fp.name):
|
| 100 |
+
infile = fp.name
|
| 101 |
+
else:
|
| 102 |
+
in_fd, infile_temp = tempfile.mkstemp()
|
| 103 |
+
os.close(in_fd)
|
| 104 |
+
infile = infile_temp
|
| 105 |
+
|
| 106 |
+
# Ignore length and offset!
|
| 107 |
+
# Ghostscript can read it
|
| 108 |
+
# Copy whole file to read in Ghostscript
|
| 109 |
+
with open(infile_temp, "wb") as f:
|
| 110 |
+
# fetch length of fp
|
| 111 |
+
fp.seek(0, io.SEEK_END)
|
| 112 |
+
fsize = fp.tell()
|
| 113 |
+
# ensure start position
|
| 114 |
+
# go back
|
| 115 |
+
fp.seek(0)
|
| 116 |
+
lengthfile = fsize
|
| 117 |
+
while lengthfile > 0:
|
| 118 |
+
s = fp.read(min(lengthfile, 100 * 1024))
|
| 119 |
+
if not s:
|
| 120 |
+
break
|
| 121 |
+
lengthfile -= len(s)
|
| 122 |
+
f.write(s)
|
| 123 |
+
|
| 124 |
+
if transparency:
|
| 125 |
+
# "RGBA"
|
| 126 |
+
device = "pngalpha"
|
| 127 |
+
else:
|
| 128 |
+
# "pnmraw" automatically chooses between
|
| 129 |
+
# PBM ("1"), PGM ("L"), and PPM ("RGB").
|
| 130 |
+
device = "pnmraw"
|
| 131 |
+
|
| 132 |
+
# Build Ghostscript command
|
| 133 |
+
command = [
|
| 134 |
+
gs_binary,
|
| 135 |
+
"-q", # quiet mode
|
| 136 |
+
f"-g{width:d}x{height:d}", # set output geometry (pixels)
|
| 137 |
+
f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
|
| 138 |
+
"-dBATCH", # exit after processing
|
| 139 |
+
"-dNOPAUSE", # don't pause between pages
|
| 140 |
+
"-dSAFER", # safe mode
|
| 141 |
+
f"-sDEVICE={device}",
|
| 142 |
+
f"-sOutputFile={outfile}", # output file
|
| 143 |
+
# adjust for image origin
|
| 144 |
+
"-c",
|
| 145 |
+
f"{-bbox[0]} {-bbox[1]} translate",
|
| 146 |
+
"-f",
|
| 147 |
+
infile, # input file
|
| 148 |
+
# showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
|
| 149 |
+
"-c",
|
| 150 |
+
"showpage",
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
# push data through Ghostscript
|
| 154 |
+
try:
|
| 155 |
+
startupinfo = None
|
| 156 |
+
if sys.platform.startswith("win"):
|
| 157 |
+
startupinfo = subprocess.STARTUPINFO()
|
| 158 |
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
| 159 |
+
subprocess.check_call(command, startupinfo=startupinfo)
|
| 160 |
+
with Image.open(outfile) as out_im:
|
| 161 |
+
out_im.load()
|
| 162 |
+
return out_im.im.copy()
|
| 163 |
+
finally:
|
| 164 |
+
try:
|
| 165 |
+
os.unlink(outfile)
|
| 166 |
+
if infile_temp:
|
| 167 |
+
os.unlink(infile_temp)
|
| 168 |
+
except OSError:
|
| 169 |
+
pass
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _accept(prefix: bytes) -> bool:
|
| 173 |
+
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
##
|
| 177 |
+
# Image plugin for Encapsulated PostScript. This plugin supports only
|
| 178 |
+
# a few variants of this format.
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class EpsImageFile(ImageFile.ImageFile):
|
| 182 |
+
"""EPS File Parser for the Python Imaging Library"""
|
| 183 |
+
|
| 184 |
+
format = "EPS"
|
| 185 |
+
format_description = "Encapsulated Postscript"
|
| 186 |
+
|
| 187 |
+
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
|
| 188 |
+
|
| 189 |
+
def _open(self) -> None:
|
| 190 |
+
(length, offset) = self._find_offset(self.fp)
|
| 191 |
+
|
| 192 |
+
# go to offset - start of "%!PS"
|
| 193 |
+
self.fp.seek(offset)
|
| 194 |
+
|
| 195 |
+
self._mode = "RGB"
|
| 196 |
+
|
| 197 |
+
# When reading header comments, the first comment is used.
|
| 198 |
+
# When reading trailer comments, the last comment is used.
|
| 199 |
+
bounding_box: list[int] | None = None
|
| 200 |
+
imagedata_size: tuple[int, int] | None = None
|
| 201 |
+
|
| 202 |
+
byte_arr = bytearray(255)
|
| 203 |
+
bytes_mv = memoryview(byte_arr)
|
| 204 |
+
bytes_read = 0
|
| 205 |
+
reading_header_comments = True
|
| 206 |
+
reading_trailer_comments = False
|
| 207 |
+
trailer_reached = False
|
| 208 |
+
|
| 209 |
+
def check_required_header_comments() -> None:
|
| 210 |
+
"""
|
| 211 |
+
The EPS specification requires that some headers exist.
|
| 212 |
+
This should be checked when the header comments formally end,
|
| 213 |
+
when image data starts, or when the file ends, whichever comes first.
|
| 214 |
+
"""
|
| 215 |
+
if "PS-Adobe" not in self.info:
|
| 216 |
+
msg = 'EPS header missing "%!PS-Adobe" comment'
|
| 217 |
+
raise SyntaxError(msg)
|
| 218 |
+
if "BoundingBox" not in self.info:
|
| 219 |
+
msg = 'EPS header missing "%%BoundingBox" comment'
|
| 220 |
+
raise SyntaxError(msg)
|
| 221 |
+
|
| 222 |
+
def read_comment(s: str) -> bool:
|
| 223 |
+
nonlocal bounding_box, reading_trailer_comments
|
| 224 |
+
try:
|
| 225 |
+
m = split.match(s)
|
| 226 |
+
except re.error as e:
|
| 227 |
+
msg = "not an EPS file"
|
| 228 |
+
raise SyntaxError(msg) from e
|
| 229 |
+
|
| 230 |
+
if not m:
|
| 231 |
+
return False
|
| 232 |
+
|
| 233 |
+
k, v = m.group(1, 2)
|
| 234 |
+
self.info[k] = v
|
| 235 |
+
if k == "BoundingBox":
|
| 236 |
+
if v == "(atend)":
|
| 237 |
+
reading_trailer_comments = True
|
| 238 |
+
elif not bounding_box or (trailer_reached and reading_trailer_comments):
|
| 239 |
+
try:
|
| 240 |
+
# Note: The DSC spec says that BoundingBox
|
| 241 |
+
# fields should be integers, but some drivers
|
| 242 |
+
# put floating point values there anyway.
|
| 243 |
+
bounding_box = [int(float(i)) for i in v.split()]
|
| 244 |
+
except Exception:
|
| 245 |
+
pass
|
| 246 |
+
return True
|
| 247 |
+
|
| 248 |
+
while True:
|
| 249 |
+
byte = self.fp.read(1)
|
| 250 |
+
if byte == b"":
|
| 251 |
+
# if we didn't read a byte we must be at the end of the file
|
| 252 |
+
if bytes_read == 0:
|
| 253 |
+
if reading_header_comments:
|
| 254 |
+
check_required_header_comments()
|
| 255 |
+
break
|
| 256 |
+
elif byte in b"\r\n":
|
| 257 |
+
# if we read a line ending character, ignore it and parse what
|
| 258 |
+
# we have already read. if we haven't read any other characters,
|
| 259 |
+
# continue reading
|
| 260 |
+
if bytes_read == 0:
|
| 261 |
+
continue
|
| 262 |
+
else:
|
| 263 |
+
# ASCII/hexadecimal lines in an EPS file must not exceed
|
| 264 |
+
# 255 characters, not including line ending characters
|
| 265 |
+
if bytes_read >= 255:
|
| 266 |
+
# only enforce this for lines starting with a "%",
|
| 267 |
+
# otherwise assume it's binary data
|
| 268 |
+
if byte_arr[0] == ord("%"):
|
| 269 |
+
msg = "not an EPS file"
|
| 270 |
+
raise SyntaxError(msg)
|
| 271 |
+
else:
|
| 272 |
+
if reading_header_comments:
|
| 273 |
+
check_required_header_comments()
|
| 274 |
+
reading_header_comments = False
|
| 275 |
+
# reset bytes_read so we can keep reading
|
| 276 |
+
# data until the end of the line
|
| 277 |
+
bytes_read = 0
|
| 278 |
+
byte_arr[bytes_read] = byte[0]
|
| 279 |
+
bytes_read += 1
|
| 280 |
+
continue
|
| 281 |
+
|
| 282 |
+
if reading_header_comments:
|
| 283 |
+
# Load EPS header
|
| 284 |
+
|
| 285 |
+
# if this line doesn't start with a "%",
|
| 286 |
+
# or does start with "%%EndComments",
|
| 287 |
+
# then we've reached the end of the header/comments
|
| 288 |
+
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
| 289 |
+
check_required_header_comments()
|
| 290 |
+
reading_header_comments = False
|
| 291 |
+
continue
|
| 292 |
+
|
| 293 |
+
s = str(bytes_mv[:bytes_read], "latin-1")
|
| 294 |
+
if not read_comment(s):
|
| 295 |
+
m = field.match(s)
|
| 296 |
+
if m:
|
| 297 |
+
k = m.group(1)
|
| 298 |
+
if k[:8] == "PS-Adobe":
|
| 299 |
+
self.info["PS-Adobe"] = k[9:]
|
| 300 |
+
else:
|
| 301 |
+
self.info[k] = ""
|
| 302 |
+
elif s[0] == "%":
|
| 303 |
+
# handle non-DSC PostScript comments that some
|
| 304 |
+
# tools mistakenly put in the Comments section
|
| 305 |
+
pass
|
| 306 |
+
else:
|
| 307 |
+
msg = "bad EPS header"
|
| 308 |
+
raise OSError(msg)
|
| 309 |
+
elif bytes_mv[:11] == b"%ImageData:":
|
| 310 |
+
# Check for an "ImageData" descriptor
|
| 311 |
+
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
|
| 312 |
+
|
| 313 |
+
# If we've already read an "ImageData" descriptor,
|
| 314 |
+
# don't read another one.
|
| 315 |
+
if imagedata_size:
|
| 316 |
+
bytes_read = 0
|
| 317 |
+
continue
|
| 318 |
+
|
| 319 |
+
# Values:
|
| 320 |
+
# columns
|
| 321 |
+
# rows
|
| 322 |
+
# bit depth (1 or 8)
|
| 323 |
+
# mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
|
| 324 |
+
# number of padding channels
|
| 325 |
+
# block size (number of bytes per row per channel)
|
| 326 |
+
# binary/ascii (1: binary, 2: ascii)
|
| 327 |
+
# data start identifier (the image data follows after a single line
|
| 328 |
+
# consisting only of this quoted value)
|
| 329 |
+
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
| 330 |
+
columns, rows, bit_depth, mode_id = (
|
| 331 |
+
int(value) for value in image_data_values[:4]
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
if bit_depth == 1:
|
| 335 |
+
self._mode = "1"
|
| 336 |
+
elif bit_depth == 8:
|
| 337 |
+
try:
|
| 338 |
+
self._mode = self.mode_map[mode_id]
|
| 339 |
+
except ValueError:
|
| 340 |
+
break
|
| 341 |
+
else:
|
| 342 |
+
break
|
| 343 |
+
|
| 344 |
+
# Parse the columns and rows after checking the bit depth and mode
|
| 345 |
+
# in case the bit depth and/or mode are invalid.
|
| 346 |
+
imagedata_size = columns, rows
|
| 347 |
+
elif bytes_mv[:5] == b"%%EOF":
|
| 348 |
+
break
|
| 349 |
+
elif trailer_reached and reading_trailer_comments:
|
| 350 |
+
# Load EPS trailer
|
| 351 |
+
s = str(bytes_mv[:bytes_read], "latin-1")
|
| 352 |
+
read_comment(s)
|
| 353 |
+
elif bytes_mv[:9] == b"%%Trailer":
|
| 354 |
+
trailer_reached = True
|
| 355 |
+
bytes_read = 0
|
| 356 |
+
|
| 357 |
+
# A "BoundingBox" is always required,
|
| 358 |
+
# even if an "ImageData" descriptor size exists.
|
| 359 |
+
if not bounding_box:
|
| 360 |
+
msg = "cannot determine EPS bounding box"
|
| 361 |
+
raise OSError(msg)
|
| 362 |
+
|
| 363 |
+
# An "ImageData" size takes precedence over the "BoundingBox".
|
| 364 |
+
self._size = imagedata_size or (
|
| 365 |
+
bounding_box[2] - bounding_box[0],
|
| 366 |
+
bounding_box[3] - bounding_box[1],
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
self.tile = [
|
| 370 |
+
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
|
| 371 |
+
]
|
| 372 |
+
|
| 373 |
+
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
|
| 374 |
+
s = fp.read(4)
|
| 375 |
+
|
| 376 |
+
if s == b"%!PS":
|
| 377 |
+
# for HEAD without binary preview
|
| 378 |
+
fp.seek(0, io.SEEK_END)
|
| 379 |
+
length = fp.tell()
|
| 380 |
+
offset = 0
|
| 381 |
+
elif i32(s) == 0xC6D3D0C5:
|
| 382 |
+
# FIX for: Some EPS file not handled correctly / issue #302
|
| 383 |
+
# EPS can contain binary data
|
| 384 |
+
# or start directly with latin coding
|
| 385 |
+
# more info see:
|
| 386 |
+
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
| 387 |
+
s = fp.read(8)
|
| 388 |
+
offset = i32(s)
|
| 389 |
+
length = i32(s, 4)
|
| 390 |
+
else:
|
| 391 |
+
msg = "not an EPS file"
|
| 392 |
+
raise SyntaxError(msg)
|
| 393 |
+
|
| 394 |
+
return length, offset
|
| 395 |
+
|
| 396 |
+
def load(
|
| 397 |
+
self, scale: int = 1, transparency: bool = False
|
| 398 |
+
) -> Image.core.PixelAccess | None:
|
| 399 |
+
# Load EPS via Ghostscript
|
| 400 |
+
if self.tile:
|
| 401 |
+
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
| 402 |
+
self._mode = self.im.mode
|
| 403 |
+
self._size = self.im.size
|
| 404 |
+
self.tile = []
|
| 405 |
+
return Image.Image.load(self)
|
| 406 |
+
|
| 407 |
+
def load_seek(self, pos: int) -> None:
|
| 408 |
+
# we can't incrementally load, so force ImageFile.parser to
|
| 409 |
+
# use our custom load method by defining this method.
|
| 410 |
+
pass
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
# --------------------------------------------------------------------
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
|
| 417 |
+
"""EPS Writer for the Python Imaging Library."""
|
| 418 |
+
|
| 419 |
+
# make sure image data is available
|
| 420 |
+
im.load()
|
| 421 |
+
|
| 422 |
+
# determine PostScript image mode
|
| 423 |
+
if im.mode == "L":
|
| 424 |
+
operator = (8, 1, b"image")
|
| 425 |
+
elif im.mode == "RGB":
|
| 426 |
+
operator = (8, 3, b"false 3 colorimage")
|
| 427 |
+
elif im.mode == "CMYK":
|
| 428 |
+
operator = (8, 4, b"false 4 colorimage")
|
| 429 |
+
else:
|
| 430 |
+
msg = "image mode is not supported"
|
| 431 |
+
raise ValueError(msg)
|
| 432 |
+
|
| 433 |
+
if eps:
|
| 434 |
+
# write EPS header
|
| 435 |
+
fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
|
| 436 |
+
fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
|
| 437 |
+
# fp.write("%%CreationDate: %s"...)
|
| 438 |
+
fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
|
| 439 |
+
fp.write(b"%%Pages: 1\n")
|
| 440 |
+
fp.write(b"%%EndComments\n")
|
| 441 |
+
fp.write(b"%%Page: 1 1\n")
|
| 442 |
+
fp.write(b"%%ImageData: %d %d " % im.size)
|
| 443 |
+
fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
|
| 444 |
+
|
| 445 |
+
# image header
|
| 446 |
+
fp.write(b"gsave\n")
|
| 447 |
+
fp.write(b"10 dict begin\n")
|
| 448 |
+
fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
|
| 449 |
+
fp.write(b"%d %d scale\n" % im.size)
|
| 450 |
+
fp.write(b"%d %d 8\n" % im.size) # <= bits
|
| 451 |
+
fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
|
| 452 |
+
fp.write(b"{ currentfile buf readhexstring pop } bind\n")
|
| 453 |
+
fp.write(operator[2] + b"\n")
|
| 454 |
+
if hasattr(fp, "flush"):
|
| 455 |
+
fp.flush()
|
| 456 |
+
|
| 457 |
+
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
|
| 458 |
+
|
| 459 |
+
fp.write(b"\n%%%%EndBinary\n")
|
| 460 |
+
fp.write(b"grestore end\n")
|
| 461 |
+
if hasattr(fp, "flush"):
|
| 462 |
+
fp.flush()
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# --------------------------------------------------------------------
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
|
| 469 |
+
|
| 470 |
+
Image.register_save(EpsImageFile.format, _save)
|
| 471 |
+
|
| 472 |
+
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
|
| 473 |
+
|
| 474 |
+
Image.register_mime(EpsImageFile.format, "application/postscript")
|
Lib/site-packages/PIL/ExifTags.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# EXIF tags
|
| 6 |
+
#
|
| 7 |
+
# Copyright (c) 2003 by Secret Labs AB
|
| 8 |
+
#
|
| 9 |
+
# See the README file for information on usage and redistribution.
|
| 10 |
+
#
|
| 11 |
+
|
| 12 |
+
"""
|
| 13 |
+
This module provides constants and clear-text names for various
|
| 14 |
+
well-known EXIF tags.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from enum import IntEnum
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Base(IntEnum):
|
| 22 |
+
# possibly incomplete
|
| 23 |
+
InteropIndex = 0x0001
|
| 24 |
+
ProcessingSoftware = 0x000B
|
| 25 |
+
NewSubfileType = 0x00FE
|
| 26 |
+
SubfileType = 0x00FF
|
| 27 |
+
ImageWidth = 0x0100
|
| 28 |
+
ImageLength = 0x0101
|
| 29 |
+
BitsPerSample = 0x0102
|
| 30 |
+
Compression = 0x0103
|
| 31 |
+
PhotometricInterpretation = 0x0106
|
| 32 |
+
Thresholding = 0x0107
|
| 33 |
+
CellWidth = 0x0108
|
| 34 |
+
CellLength = 0x0109
|
| 35 |
+
FillOrder = 0x010A
|
| 36 |
+
DocumentName = 0x010D
|
| 37 |
+
ImageDescription = 0x010E
|
| 38 |
+
Make = 0x010F
|
| 39 |
+
Model = 0x0110
|
| 40 |
+
StripOffsets = 0x0111
|
| 41 |
+
Orientation = 0x0112
|
| 42 |
+
SamplesPerPixel = 0x0115
|
| 43 |
+
RowsPerStrip = 0x0116
|
| 44 |
+
StripByteCounts = 0x0117
|
| 45 |
+
MinSampleValue = 0x0118
|
| 46 |
+
MaxSampleValue = 0x0119
|
| 47 |
+
XResolution = 0x011A
|
| 48 |
+
YResolution = 0x011B
|
| 49 |
+
PlanarConfiguration = 0x011C
|
| 50 |
+
PageName = 0x011D
|
| 51 |
+
FreeOffsets = 0x0120
|
| 52 |
+
FreeByteCounts = 0x0121
|
| 53 |
+
GrayResponseUnit = 0x0122
|
| 54 |
+
GrayResponseCurve = 0x0123
|
| 55 |
+
T4Options = 0x0124
|
| 56 |
+
T6Options = 0x0125
|
| 57 |
+
ResolutionUnit = 0x0128
|
| 58 |
+
PageNumber = 0x0129
|
| 59 |
+
TransferFunction = 0x012D
|
| 60 |
+
Software = 0x0131
|
| 61 |
+
DateTime = 0x0132
|
| 62 |
+
Artist = 0x013B
|
| 63 |
+
HostComputer = 0x013C
|
| 64 |
+
Predictor = 0x013D
|
| 65 |
+
WhitePoint = 0x013E
|
| 66 |
+
PrimaryChromaticities = 0x013F
|
| 67 |
+
ColorMap = 0x0140
|
| 68 |
+
HalftoneHints = 0x0141
|
| 69 |
+
TileWidth = 0x0142
|
| 70 |
+
TileLength = 0x0143
|
| 71 |
+
TileOffsets = 0x0144
|
| 72 |
+
TileByteCounts = 0x0145
|
| 73 |
+
SubIFDs = 0x014A
|
| 74 |
+
InkSet = 0x014C
|
| 75 |
+
InkNames = 0x014D
|
| 76 |
+
NumberOfInks = 0x014E
|
| 77 |
+
DotRange = 0x0150
|
| 78 |
+
TargetPrinter = 0x0151
|
| 79 |
+
ExtraSamples = 0x0152
|
| 80 |
+
SampleFormat = 0x0153
|
| 81 |
+
SMinSampleValue = 0x0154
|
| 82 |
+
SMaxSampleValue = 0x0155
|
| 83 |
+
TransferRange = 0x0156
|
| 84 |
+
ClipPath = 0x0157
|
| 85 |
+
XClipPathUnits = 0x0158
|
| 86 |
+
YClipPathUnits = 0x0159
|
| 87 |
+
Indexed = 0x015A
|
| 88 |
+
JPEGTables = 0x015B
|
| 89 |
+
OPIProxy = 0x015F
|
| 90 |
+
JPEGProc = 0x0200
|
| 91 |
+
JpegIFOffset = 0x0201
|
| 92 |
+
JpegIFByteCount = 0x0202
|
| 93 |
+
JpegRestartInterval = 0x0203
|
| 94 |
+
JpegLosslessPredictors = 0x0205
|
| 95 |
+
JpegPointTransforms = 0x0206
|
| 96 |
+
JpegQTables = 0x0207
|
| 97 |
+
JpegDCTables = 0x0208
|
| 98 |
+
JpegACTables = 0x0209
|
| 99 |
+
YCbCrCoefficients = 0x0211
|
| 100 |
+
YCbCrSubSampling = 0x0212
|
| 101 |
+
YCbCrPositioning = 0x0213
|
| 102 |
+
ReferenceBlackWhite = 0x0214
|
| 103 |
+
XMLPacket = 0x02BC
|
| 104 |
+
RelatedImageFileFormat = 0x1000
|
| 105 |
+
RelatedImageWidth = 0x1001
|
| 106 |
+
RelatedImageLength = 0x1002
|
| 107 |
+
Rating = 0x4746
|
| 108 |
+
RatingPercent = 0x4749
|
| 109 |
+
ImageID = 0x800D
|
| 110 |
+
CFARepeatPatternDim = 0x828D
|
| 111 |
+
BatteryLevel = 0x828F
|
| 112 |
+
Copyright = 0x8298
|
| 113 |
+
ExposureTime = 0x829A
|
| 114 |
+
FNumber = 0x829D
|
| 115 |
+
IPTCNAA = 0x83BB
|
| 116 |
+
ImageResources = 0x8649
|
| 117 |
+
ExifOffset = 0x8769
|
| 118 |
+
InterColorProfile = 0x8773
|
| 119 |
+
ExposureProgram = 0x8822
|
| 120 |
+
SpectralSensitivity = 0x8824
|
| 121 |
+
GPSInfo = 0x8825
|
| 122 |
+
ISOSpeedRatings = 0x8827
|
| 123 |
+
OECF = 0x8828
|
| 124 |
+
Interlace = 0x8829
|
| 125 |
+
TimeZoneOffset = 0x882A
|
| 126 |
+
SelfTimerMode = 0x882B
|
| 127 |
+
SensitivityType = 0x8830
|
| 128 |
+
StandardOutputSensitivity = 0x8831
|
| 129 |
+
RecommendedExposureIndex = 0x8832
|
| 130 |
+
ISOSpeed = 0x8833
|
| 131 |
+
ISOSpeedLatitudeyyy = 0x8834
|
| 132 |
+
ISOSpeedLatitudezzz = 0x8835
|
| 133 |
+
ExifVersion = 0x9000
|
| 134 |
+
DateTimeOriginal = 0x9003
|
| 135 |
+
DateTimeDigitized = 0x9004
|
| 136 |
+
OffsetTime = 0x9010
|
| 137 |
+
OffsetTimeOriginal = 0x9011
|
| 138 |
+
OffsetTimeDigitized = 0x9012
|
| 139 |
+
ComponentsConfiguration = 0x9101
|
| 140 |
+
CompressedBitsPerPixel = 0x9102
|
| 141 |
+
ShutterSpeedValue = 0x9201
|
| 142 |
+
ApertureValue = 0x9202
|
| 143 |
+
BrightnessValue = 0x9203
|
| 144 |
+
ExposureBiasValue = 0x9204
|
| 145 |
+
MaxApertureValue = 0x9205
|
| 146 |
+
SubjectDistance = 0x9206
|
| 147 |
+
MeteringMode = 0x9207
|
| 148 |
+
LightSource = 0x9208
|
| 149 |
+
Flash = 0x9209
|
| 150 |
+
FocalLength = 0x920A
|
| 151 |
+
Noise = 0x920D
|
| 152 |
+
ImageNumber = 0x9211
|
| 153 |
+
SecurityClassification = 0x9212
|
| 154 |
+
ImageHistory = 0x9213
|
| 155 |
+
TIFFEPStandardID = 0x9216
|
| 156 |
+
MakerNote = 0x927C
|
| 157 |
+
UserComment = 0x9286
|
| 158 |
+
SubsecTime = 0x9290
|
| 159 |
+
SubsecTimeOriginal = 0x9291
|
| 160 |
+
SubsecTimeDigitized = 0x9292
|
| 161 |
+
AmbientTemperature = 0x9400
|
| 162 |
+
Humidity = 0x9401
|
| 163 |
+
Pressure = 0x9402
|
| 164 |
+
WaterDepth = 0x9403
|
| 165 |
+
Acceleration = 0x9404
|
| 166 |
+
CameraElevationAngle = 0x9405
|
| 167 |
+
XPTitle = 0x9C9B
|
| 168 |
+
XPComment = 0x9C9C
|
| 169 |
+
XPAuthor = 0x9C9D
|
| 170 |
+
XPKeywords = 0x9C9E
|
| 171 |
+
XPSubject = 0x9C9F
|
| 172 |
+
FlashPixVersion = 0xA000
|
| 173 |
+
ColorSpace = 0xA001
|
| 174 |
+
ExifImageWidth = 0xA002
|
| 175 |
+
ExifImageHeight = 0xA003
|
| 176 |
+
RelatedSoundFile = 0xA004
|
| 177 |
+
ExifInteroperabilityOffset = 0xA005
|
| 178 |
+
FlashEnergy = 0xA20B
|
| 179 |
+
SpatialFrequencyResponse = 0xA20C
|
| 180 |
+
FocalPlaneXResolution = 0xA20E
|
| 181 |
+
FocalPlaneYResolution = 0xA20F
|
| 182 |
+
FocalPlaneResolutionUnit = 0xA210
|
| 183 |
+
SubjectLocation = 0xA214
|
| 184 |
+
ExposureIndex = 0xA215
|
| 185 |
+
SensingMethod = 0xA217
|
| 186 |
+
FileSource = 0xA300
|
| 187 |
+
SceneType = 0xA301
|
| 188 |
+
CFAPattern = 0xA302
|
| 189 |
+
CustomRendered = 0xA401
|
| 190 |
+
ExposureMode = 0xA402
|
| 191 |
+
WhiteBalance = 0xA403
|
| 192 |
+
DigitalZoomRatio = 0xA404
|
| 193 |
+
FocalLengthIn35mmFilm = 0xA405
|
| 194 |
+
SceneCaptureType = 0xA406
|
| 195 |
+
GainControl = 0xA407
|
| 196 |
+
Contrast = 0xA408
|
| 197 |
+
Saturation = 0xA409
|
| 198 |
+
Sharpness = 0xA40A
|
| 199 |
+
DeviceSettingDescription = 0xA40B
|
| 200 |
+
SubjectDistanceRange = 0xA40C
|
| 201 |
+
ImageUniqueID = 0xA420
|
| 202 |
+
CameraOwnerName = 0xA430
|
| 203 |
+
BodySerialNumber = 0xA431
|
| 204 |
+
LensSpecification = 0xA432
|
| 205 |
+
LensMake = 0xA433
|
| 206 |
+
LensModel = 0xA434
|
| 207 |
+
LensSerialNumber = 0xA435
|
| 208 |
+
CompositeImage = 0xA460
|
| 209 |
+
CompositeImageCount = 0xA461
|
| 210 |
+
CompositeImageExposureTimes = 0xA462
|
| 211 |
+
Gamma = 0xA500
|
| 212 |
+
PrintImageMatching = 0xC4A5
|
| 213 |
+
DNGVersion = 0xC612
|
| 214 |
+
DNGBackwardVersion = 0xC613
|
| 215 |
+
UniqueCameraModel = 0xC614
|
| 216 |
+
LocalizedCameraModel = 0xC615
|
| 217 |
+
CFAPlaneColor = 0xC616
|
| 218 |
+
CFALayout = 0xC617
|
| 219 |
+
LinearizationTable = 0xC618
|
| 220 |
+
BlackLevelRepeatDim = 0xC619
|
| 221 |
+
BlackLevel = 0xC61A
|
| 222 |
+
BlackLevelDeltaH = 0xC61B
|
| 223 |
+
BlackLevelDeltaV = 0xC61C
|
| 224 |
+
WhiteLevel = 0xC61D
|
| 225 |
+
DefaultScale = 0xC61E
|
| 226 |
+
DefaultCropOrigin = 0xC61F
|
| 227 |
+
DefaultCropSize = 0xC620
|
| 228 |
+
ColorMatrix1 = 0xC621
|
| 229 |
+
ColorMatrix2 = 0xC622
|
| 230 |
+
CameraCalibration1 = 0xC623
|
| 231 |
+
CameraCalibration2 = 0xC624
|
| 232 |
+
ReductionMatrix1 = 0xC625
|
| 233 |
+
ReductionMatrix2 = 0xC626
|
| 234 |
+
AnalogBalance = 0xC627
|
| 235 |
+
AsShotNeutral = 0xC628
|
| 236 |
+
AsShotWhiteXY = 0xC629
|
| 237 |
+
BaselineExposure = 0xC62A
|
| 238 |
+
BaselineNoise = 0xC62B
|
| 239 |
+
BaselineSharpness = 0xC62C
|
| 240 |
+
BayerGreenSplit = 0xC62D
|
| 241 |
+
LinearResponseLimit = 0xC62E
|
| 242 |
+
CameraSerialNumber = 0xC62F
|
| 243 |
+
LensInfo = 0xC630
|
| 244 |
+
ChromaBlurRadius = 0xC631
|
| 245 |
+
AntiAliasStrength = 0xC632
|
| 246 |
+
ShadowScale = 0xC633
|
| 247 |
+
DNGPrivateData = 0xC634
|
| 248 |
+
MakerNoteSafety = 0xC635
|
| 249 |
+
CalibrationIlluminant1 = 0xC65A
|
| 250 |
+
CalibrationIlluminant2 = 0xC65B
|
| 251 |
+
BestQualityScale = 0xC65C
|
| 252 |
+
RawDataUniqueID = 0xC65D
|
| 253 |
+
OriginalRawFileName = 0xC68B
|
| 254 |
+
OriginalRawFileData = 0xC68C
|
| 255 |
+
ActiveArea = 0xC68D
|
| 256 |
+
MaskedAreas = 0xC68E
|
| 257 |
+
AsShotICCProfile = 0xC68F
|
| 258 |
+
AsShotPreProfileMatrix = 0xC690
|
| 259 |
+
CurrentICCProfile = 0xC691
|
| 260 |
+
CurrentPreProfileMatrix = 0xC692
|
| 261 |
+
ColorimetricReference = 0xC6BF
|
| 262 |
+
CameraCalibrationSignature = 0xC6F3
|
| 263 |
+
ProfileCalibrationSignature = 0xC6F4
|
| 264 |
+
AsShotProfileName = 0xC6F6
|
| 265 |
+
NoiseReductionApplied = 0xC6F7
|
| 266 |
+
ProfileName = 0xC6F8
|
| 267 |
+
ProfileHueSatMapDims = 0xC6F9
|
| 268 |
+
ProfileHueSatMapData1 = 0xC6FA
|
| 269 |
+
ProfileHueSatMapData2 = 0xC6FB
|
| 270 |
+
ProfileToneCurve = 0xC6FC
|
| 271 |
+
ProfileEmbedPolicy = 0xC6FD
|
| 272 |
+
ProfileCopyright = 0xC6FE
|
| 273 |
+
ForwardMatrix1 = 0xC714
|
| 274 |
+
ForwardMatrix2 = 0xC715
|
| 275 |
+
PreviewApplicationName = 0xC716
|
| 276 |
+
PreviewApplicationVersion = 0xC717
|
| 277 |
+
PreviewSettingsName = 0xC718
|
| 278 |
+
PreviewSettingsDigest = 0xC719
|
| 279 |
+
PreviewColorSpace = 0xC71A
|
| 280 |
+
PreviewDateTime = 0xC71B
|
| 281 |
+
RawImageDigest = 0xC71C
|
| 282 |
+
OriginalRawFileDigest = 0xC71D
|
| 283 |
+
SubTileBlockSize = 0xC71E
|
| 284 |
+
RowInterleaveFactor = 0xC71F
|
| 285 |
+
ProfileLookTableDims = 0xC725
|
| 286 |
+
ProfileLookTableData = 0xC726
|
| 287 |
+
OpcodeList1 = 0xC740
|
| 288 |
+
OpcodeList2 = 0xC741
|
| 289 |
+
OpcodeList3 = 0xC74E
|
| 290 |
+
NoiseProfile = 0xC761
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
"""Maps EXIF tags to tag names."""
|
| 294 |
+
TAGS = {
|
| 295 |
+
**{i.value: i.name for i in Base},
|
| 296 |
+
0x920C: "SpatialFrequencyResponse",
|
| 297 |
+
0x9214: "SubjectLocation",
|
| 298 |
+
0x9215: "ExposureIndex",
|
| 299 |
+
0x828E: "CFAPattern",
|
| 300 |
+
0x920B: "FlashEnergy",
|
| 301 |
+
0x9216: "TIFF/EPStandardID",
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
class GPS(IntEnum):
|
| 306 |
+
GPSVersionID = 0x00
|
| 307 |
+
GPSLatitudeRef = 0x01
|
| 308 |
+
GPSLatitude = 0x02
|
| 309 |
+
GPSLongitudeRef = 0x03
|
| 310 |
+
GPSLongitude = 0x04
|
| 311 |
+
GPSAltitudeRef = 0x05
|
| 312 |
+
GPSAltitude = 0x06
|
| 313 |
+
GPSTimeStamp = 0x07
|
| 314 |
+
GPSSatellites = 0x08
|
| 315 |
+
GPSStatus = 0x09
|
| 316 |
+
GPSMeasureMode = 0x0A
|
| 317 |
+
GPSDOP = 0x0B
|
| 318 |
+
GPSSpeedRef = 0x0C
|
| 319 |
+
GPSSpeed = 0x0D
|
| 320 |
+
GPSTrackRef = 0x0E
|
| 321 |
+
GPSTrack = 0x0F
|
| 322 |
+
GPSImgDirectionRef = 0x10
|
| 323 |
+
GPSImgDirection = 0x11
|
| 324 |
+
GPSMapDatum = 0x12
|
| 325 |
+
GPSDestLatitudeRef = 0x13
|
| 326 |
+
GPSDestLatitude = 0x14
|
| 327 |
+
GPSDestLongitudeRef = 0x15
|
| 328 |
+
GPSDestLongitude = 0x16
|
| 329 |
+
GPSDestBearingRef = 0x17
|
| 330 |
+
GPSDestBearing = 0x18
|
| 331 |
+
GPSDestDistanceRef = 0x19
|
| 332 |
+
GPSDestDistance = 0x1A
|
| 333 |
+
GPSProcessingMethod = 0x1B
|
| 334 |
+
GPSAreaInformation = 0x1C
|
| 335 |
+
GPSDateStamp = 0x1D
|
| 336 |
+
GPSDifferential = 0x1E
|
| 337 |
+
GPSHPositioningError = 0x1F
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
"""Maps EXIF GPS tags to tag names."""
|
| 341 |
+
GPSTAGS = {i.value: i.name for i in GPS}
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class Interop(IntEnum):
|
| 345 |
+
InteropIndex = 0x0001
|
| 346 |
+
InteropVersion = 0x0002
|
| 347 |
+
RelatedImageFileFormat = 0x1000
|
| 348 |
+
RelatedImageWidth = 0x1001
|
| 349 |
+
RelatedImageHeight = 0x1002
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
class IFD(IntEnum):
|
| 353 |
+
Exif = 0x8769
|
| 354 |
+
GPSInfo = 0x8825
|
| 355 |
+
MakerNote = 0x927C
|
| 356 |
+
Makernote = 0x927C # Deprecated
|
| 357 |
+
Interop = 0xA005
|
| 358 |
+
IFD1 = -1
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
class LightSource(IntEnum):
|
| 362 |
+
Unknown = 0x00
|
| 363 |
+
Daylight = 0x01
|
| 364 |
+
Fluorescent = 0x02
|
| 365 |
+
Tungsten = 0x03
|
| 366 |
+
Flash = 0x04
|
| 367 |
+
Fine = 0x09
|
| 368 |
+
Cloudy = 0x0A
|
| 369 |
+
Shade = 0x0B
|
| 370 |
+
DaylightFluorescent = 0x0C
|
| 371 |
+
DayWhiteFluorescent = 0x0D
|
| 372 |
+
CoolWhiteFluorescent = 0x0E
|
| 373 |
+
WhiteFluorescent = 0x0F
|
| 374 |
+
StandardLightA = 0x11
|
| 375 |
+
StandardLightB = 0x12
|
| 376 |
+
StandardLightC = 0x13
|
| 377 |
+
D55 = 0x14
|
| 378 |
+
D65 = 0x15
|
| 379 |
+
D75 = 0x16
|
| 380 |
+
D50 = 0x17
|
| 381 |
+
ISO = 0x18
|
| 382 |
+
Other = 0xFF
|
Lib/site-packages/PIL/FitsImagePlugin.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# FITS file handling
|
| 6 |
+
#
|
| 7 |
+
# Copyright (c) 1998-2003 by Fredrik Lundh
|
| 8 |
+
#
|
| 9 |
+
# See the README file for information on usage and redistribution.
|
| 10 |
+
#
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import gzip
|
| 14 |
+
import math
|
| 15 |
+
|
| 16 |
+
from . import Image, ImageFile
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _accept(prefix: bytes) -> bool:
|
| 20 |
+
return prefix[:6] == b"SIMPLE"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class FitsImageFile(ImageFile.ImageFile):
|
| 24 |
+
format = "FITS"
|
| 25 |
+
format_description = "FITS"
|
| 26 |
+
|
| 27 |
+
def _open(self) -> None:
|
| 28 |
+
assert self.fp is not None
|
| 29 |
+
|
| 30 |
+
headers: dict[bytes, bytes] = {}
|
| 31 |
+
header_in_progress = False
|
| 32 |
+
decoder_name = ""
|
| 33 |
+
while True:
|
| 34 |
+
header = self.fp.read(80)
|
| 35 |
+
if not header:
|
| 36 |
+
msg = "Truncated FITS file"
|
| 37 |
+
raise OSError(msg)
|
| 38 |
+
keyword = header[:8].strip()
|
| 39 |
+
if keyword in (b"SIMPLE", b"XTENSION"):
|
| 40 |
+
header_in_progress = True
|
| 41 |
+
elif headers and not header_in_progress:
|
| 42 |
+
# This is now a data unit
|
| 43 |
+
break
|
| 44 |
+
elif keyword == b"END":
|
| 45 |
+
# Seek to the end of the header unit
|
| 46 |
+
self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
|
| 47 |
+
if not decoder_name:
|
| 48 |
+
decoder_name, offset, args = self._parse_headers(headers)
|
| 49 |
+
|
| 50 |
+
header_in_progress = False
|
| 51 |
+
continue
|
| 52 |
+
|
| 53 |
+
if decoder_name:
|
| 54 |
+
# Keep going to read past the headers
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
value = header[8:].split(b"/")[0].strip()
|
| 58 |
+
if value.startswith(b"="):
|
| 59 |
+
value = value[1:].strip()
|
| 60 |
+
if not headers and (not _accept(keyword) or value != b"T"):
|
| 61 |
+
msg = "Not a FITS file"
|
| 62 |
+
raise SyntaxError(msg)
|
| 63 |
+
headers[keyword] = value
|
| 64 |
+
|
| 65 |
+
if not decoder_name:
|
| 66 |
+
msg = "No image data"
|
| 67 |
+
raise ValueError(msg)
|
| 68 |
+
|
| 69 |
+
offset += self.fp.tell() - 80
|
| 70 |
+
self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
|
| 71 |
+
|
| 72 |
+
def _get_size(
|
| 73 |
+
self, headers: dict[bytes, bytes], prefix: bytes
|
| 74 |
+
) -> tuple[int, int] | None:
|
| 75 |
+
naxis = int(headers[prefix + b"NAXIS"])
|
| 76 |
+
if naxis == 0:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
if naxis == 1:
|
| 80 |
+
return 1, int(headers[prefix + b"NAXIS1"])
|
| 81 |
+
else:
|
| 82 |
+
return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
|
| 83 |
+
|
| 84 |
+
def _parse_headers(
|
| 85 |
+
self, headers: dict[bytes, bytes]
|
| 86 |
+
) -> tuple[str, int, tuple[str | int, ...]]:
|
| 87 |
+
prefix = b""
|
| 88 |
+
decoder_name = "raw"
|
| 89 |
+
offset = 0
|
| 90 |
+
if (
|
| 91 |
+
headers.get(b"XTENSION") == b"'BINTABLE'"
|
| 92 |
+
and headers.get(b"ZIMAGE") == b"T"
|
| 93 |
+
and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
|
| 94 |
+
):
|
| 95 |
+
no_prefix_size = self._get_size(headers, prefix) or (0, 0)
|
| 96 |
+
number_of_bits = int(headers[b"BITPIX"])
|
| 97 |
+
offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
|
| 98 |
+
|
| 99 |
+
prefix = b"Z"
|
| 100 |
+
decoder_name = "fits_gzip"
|
| 101 |
+
|
| 102 |
+
size = self._get_size(headers, prefix)
|
| 103 |
+
if not size:
|
| 104 |
+
return "", 0, ()
|
| 105 |
+
|
| 106 |
+
self._size = size
|
| 107 |
+
|
| 108 |
+
number_of_bits = int(headers[prefix + b"BITPIX"])
|
| 109 |
+
if number_of_bits == 8:
|
| 110 |
+
self._mode = "L"
|
| 111 |
+
elif number_of_bits == 16:
|
| 112 |
+
self._mode = "I;16"
|
| 113 |
+
elif number_of_bits == 32:
|
| 114 |
+
self._mode = "I"
|
| 115 |
+
elif number_of_bits in (-32, -64):
|
| 116 |
+
self._mode = "F"
|
| 117 |
+
|
| 118 |
+
args: tuple[str | int, ...]
|
| 119 |
+
if decoder_name == "raw":
|
| 120 |
+
args = (self.mode, 0, -1)
|
| 121 |
+
else:
|
| 122 |
+
args = (number_of_bits,)
|
| 123 |
+
return decoder_name, offset, args
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class FitsGzipDecoder(ImageFile.PyDecoder):
|
| 127 |
+
_pulls_fd = True
|
| 128 |
+
|
| 129 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 130 |
+
assert self.fd is not None
|
| 131 |
+
value = gzip.decompress(self.fd.read())
|
| 132 |
+
|
| 133 |
+
rows = []
|
| 134 |
+
offset = 0
|
| 135 |
+
number_of_bits = min(self.args[0] // 8, 4)
|
| 136 |
+
for y in range(self.state.ysize):
|
| 137 |
+
row = bytearray()
|
| 138 |
+
for x in range(self.state.xsize):
|
| 139 |
+
row += value[offset + (4 - number_of_bits) : offset + 4]
|
| 140 |
+
offset += 4
|
| 141 |
+
rows.append(row)
|
| 142 |
+
self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
|
| 143 |
+
return -1, 0
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# --------------------------------------------------------------------
|
| 147 |
+
# Registry
|
| 148 |
+
|
| 149 |
+
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
|
| 150 |
+
Image.register_decoder("fits_gzip", FitsGzipDecoder)
|
| 151 |
+
|
| 152 |
+
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])
|
Lib/site-packages/PIL/FliImagePlugin.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# FLI/FLC file handling.
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 95-09-01 fl Created
|
| 9 |
+
# 97-01-03 fl Fixed parser, setup decoder tile
|
| 10 |
+
# 98-07-15 fl Renamed offset attribute to avoid name clash
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) Secret Labs AB 1997-98.
|
| 13 |
+
# Copyright (c) Fredrik Lundh 1995-97.
|
| 14 |
+
#
|
| 15 |
+
# See the README file for information on usage and redistribution.
|
| 16 |
+
#
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
|
| 21 |
+
from . import Image, ImageFile, ImagePalette
|
| 22 |
+
from ._binary import i16le as i16
|
| 23 |
+
from ._binary import i32le as i32
|
| 24 |
+
from ._binary import o8
|
| 25 |
+
|
| 26 |
+
#
|
| 27 |
+
# decoder
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _accept(prefix: bytes) -> bool:
|
| 31 |
+
return (
|
| 32 |
+
len(prefix) >= 6
|
| 33 |
+
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
| 34 |
+
and i16(prefix, 14) in [0, 3] # flags
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
##
|
| 39 |
+
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
|
| 40 |
+
# method to load individual frames.
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class FliImageFile(ImageFile.ImageFile):
|
| 44 |
+
format = "FLI"
|
| 45 |
+
format_description = "Autodesk FLI/FLC Animation"
|
| 46 |
+
_close_exclusive_fp_after_loading = False
|
| 47 |
+
|
| 48 |
+
def _open(self) -> None:
|
| 49 |
+
# HEAD
|
| 50 |
+
s = self.fp.read(128)
|
| 51 |
+
if not (_accept(s) and s[20:22] == b"\x00\x00"):
|
| 52 |
+
msg = "not an FLI/FLC file"
|
| 53 |
+
raise SyntaxError(msg)
|
| 54 |
+
|
| 55 |
+
# frames
|
| 56 |
+
self.n_frames = i16(s, 6)
|
| 57 |
+
self.is_animated = self.n_frames > 1
|
| 58 |
+
|
| 59 |
+
# image characteristics
|
| 60 |
+
self._mode = "P"
|
| 61 |
+
self._size = i16(s, 8), i16(s, 10)
|
| 62 |
+
|
| 63 |
+
# animation speed
|
| 64 |
+
duration = i32(s, 16)
|
| 65 |
+
magic = i16(s, 4)
|
| 66 |
+
if magic == 0xAF11:
|
| 67 |
+
duration = (duration * 1000) // 70
|
| 68 |
+
self.info["duration"] = duration
|
| 69 |
+
|
| 70 |
+
# look for palette
|
| 71 |
+
palette = [(a, a, a) for a in range(256)]
|
| 72 |
+
|
| 73 |
+
s = self.fp.read(16)
|
| 74 |
+
|
| 75 |
+
self.__offset = 128
|
| 76 |
+
|
| 77 |
+
if i16(s, 4) == 0xF100:
|
| 78 |
+
# prefix chunk; ignore it
|
| 79 |
+
self.__offset = self.__offset + i32(s)
|
| 80 |
+
self.fp.seek(self.__offset)
|
| 81 |
+
s = self.fp.read(16)
|
| 82 |
+
|
| 83 |
+
if i16(s, 4) == 0xF1FA:
|
| 84 |
+
# look for palette chunk
|
| 85 |
+
number_of_subchunks = i16(s, 6)
|
| 86 |
+
chunk_size: int | None = None
|
| 87 |
+
for _ in range(number_of_subchunks):
|
| 88 |
+
if chunk_size is not None:
|
| 89 |
+
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
|
| 90 |
+
s = self.fp.read(6)
|
| 91 |
+
chunk_type = i16(s, 4)
|
| 92 |
+
if chunk_type in (4, 11):
|
| 93 |
+
self._palette(palette, 2 if chunk_type == 11 else 0)
|
| 94 |
+
break
|
| 95 |
+
chunk_size = i32(s)
|
| 96 |
+
if not chunk_size:
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
self.palette = ImagePalette.raw(
|
| 100 |
+
"RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# set things up to decode first frame
|
| 104 |
+
self.__frame = -1
|
| 105 |
+
self._fp = self.fp
|
| 106 |
+
self.__rewind = self.fp.tell()
|
| 107 |
+
self.seek(0)
|
| 108 |
+
|
| 109 |
+
def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
|
| 110 |
+
# load palette
|
| 111 |
+
|
| 112 |
+
i = 0
|
| 113 |
+
for e in range(i16(self.fp.read(2))):
|
| 114 |
+
s = self.fp.read(2)
|
| 115 |
+
i = i + s[0]
|
| 116 |
+
n = s[1]
|
| 117 |
+
if n == 0:
|
| 118 |
+
n = 256
|
| 119 |
+
s = self.fp.read(n * 3)
|
| 120 |
+
for n in range(0, len(s), 3):
|
| 121 |
+
r = s[n] << shift
|
| 122 |
+
g = s[n + 1] << shift
|
| 123 |
+
b = s[n + 2] << shift
|
| 124 |
+
palette[i] = (r, g, b)
|
| 125 |
+
i += 1
|
| 126 |
+
|
| 127 |
+
def seek(self, frame: int) -> None:
|
| 128 |
+
if not self._seek_check(frame):
|
| 129 |
+
return
|
| 130 |
+
if frame < self.__frame:
|
| 131 |
+
self._seek(0)
|
| 132 |
+
|
| 133 |
+
for f in range(self.__frame + 1, frame + 1):
|
| 134 |
+
self._seek(f)
|
| 135 |
+
|
| 136 |
+
def _seek(self, frame: int) -> None:
|
| 137 |
+
if frame == 0:
|
| 138 |
+
self.__frame = -1
|
| 139 |
+
self._fp.seek(self.__rewind)
|
| 140 |
+
self.__offset = 128
|
| 141 |
+
else:
|
| 142 |
+
# ensure that the previous frame was loaded
|
| 143 |
+
self.load()
|
| 144 |
+
|
| 145 |
+
if frame != self.__frame + 1:
|
| 146 |
+
msg = f"cannot seek to frame {frame}"
|
| 147 |
+
raise ValueError(msg)
|
| 148 |
+
self.__frame = frame
|
| 149 |
+
|
| 150 |
+
# move to next frame
|
| 151 |
+
self.fp = self._fp
|
| 152 |
+
self.fp.seek(self.__offset)
|
| 153 |
+
|
| 154 |
+
s = self.fp.read(4)
|
| 155 |
+
if not s:
|
| 156 |
+
msg = "missing frame size"
|
| 157 |
+
raise EOFError(msg)
|
| 158 |
+
|
| 159 |
+
framesize = i32(s)
|
| 160 |
+
|
| 161 |
+
self.decodermaxblock = framesize
|
| 162 |
+
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]
|
| 163 |
+
|
| 164 |
+
self.__offset += framesize
|
| 165 |
+
|
| 166 |
+
def tell(self) -> int:
|
| 167 |
+
return self.__frame
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
#
|
| 171 |
+
# registry
|
| 172 |
+
|
| 173 |
+
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
| 174 |
+
|
| 175 |
+
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
|
Lib/site-packages/PIL/FontFile.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# base class for raster font file parsers
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1997-06-05 fl created
|
| 9 |
+
# 1997-08-19 fl restrict image width
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) 1997-1998 by Secret Labs AB
|
| 12 |
+
# Copyright (c) 1997-1998 by Fredrik Lundh
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
from typing import BinaryIO
|
| 20 |
+
|
| 21 |
+
from . import Image, _binary
|
| 22 |
+
|
| 23 |
+
WIDTH = 800
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def puti16(
|
| 27 |
+
fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
|
| 28 |
+
) -> None:
|
| 29 |
+
"""Write network order (big-endian) 16-bit sequence"""
|
| 30 |
+
for v in values:
|
| 31 |
+
if v < 0:
|
| 32 |
+
v += 65536
|
| 33 |
+
fp.write(_binary.o16be(v))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class FontFile:
|
| 37 |
+
"""Base class for raster font file handlers."""
|
| 38 |
+
|
| 39 |
+
bitmap: Image.Image | None = None
|
| 40 |
+
|
| 41 |
+
def __init__(self) -> None:
|
| 42 |
+
self.info: dict[bytes, bytes | int] = {}
|
| 43 |
+
self.glyph: list[
|
| 44 |
+
tuple[
|
| 45 |
+
tuple[int, int],
|
| 46 |
+
tuple[int, int, int, int],
|
| 47 |
+
tuple[int, int, int, int],
|
| 48 |
+
Image.Image,
|
| 49 |
+
]
|
| 50 |
+
| None
|
| 51 |
+
] = [None] * 256
|
| 52 |
+
|
| 53 |
+
def __getitem__(self, ix: int) -> (
|
| 54 |
+
tuple[
|
| 55 |
+
tuple[int, int],
|
| 56 |
+
tuple[int, int, int, int],
|
| 57 |
+
tuple[int, int, int, int],
|
| 58 |
+
Image.Image,
|
| 59 |
+
]
|
| 60 |
+
| None
|
| 61 |
+
):
|
| 62 |
+
return self.glyph[ix]
|
| 63 |
+
|
| 64 |
+
def compile(self) -> None:
|
| 65 |
+
"""Create metrics and bitmap"""
|
| 66 |
+
|
| 67 |
+
if self.bitmap:
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
# create bitmap large enough to hold all data
|
| 71 |
+
h = w = maxwidth = 0
|
| 72 |
+
lines = 1
|
| 73 |
+
for glyph in self.glyph:
|
| 74 |
+
if glyph:
|
| 75 |
+
d, dst, src, im = glyph
|
| 76 |
+
h = max(h, src[3] - src[1])
|
| 77 |
+
w = w + (src[2] - src[0])
|
| 78 |
+
if w > WIDTH:
|
| 79 |
+
lines += 1
|
| 80 |
+
w = src[2] - src[0]
|
| 81 |
+
maxwidth = max(maxwidth, w)
|
| 82 |
+
|
| 83 |
+
xsize = maxwidth
|
| 84 |
+
ysize = lines * h
|
| 85 |
+
|
| 86 |
+
if xsize == 0 and ysize == 0:
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
self.ysize = h
|
| 90 |
+
|
| 91 |
+
# paste glyphs into bitmap
|
| 92 |
+
self.bitmap = Image.new("1", (xsize, ysize))
|
| 93 |
+
self.metrics: list[
|
| 94 |
+
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
|
| 95 |
+
| None
|
| 96 |
+
] = [None] * 256
|
| 97 |
+
x = y = 0
|
| 98 |
+
for i in range(256):
|
| 99 |
+
glyph = self[i]
|
| 100 |
+
if glyph:
|
| 101 |
+
d, dst, src, im = glyph
|
| 102 |
+
xx = src[2] - src[0]
|
| 103 |
+
x0, y0 = x, y
|
| 104 |
+
x = x + xx
|
| 105 |
+
if x > WIDTH:
|
| 106 |
+
x, y = 0, y + h
|
| 107 |
+
x0, y0 = x, y
|
| 108 |
+
x = xx
|
| 109 |
+
s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
|
| 110 |
+
self.bitmap.paste(im.crop(src), s)
|
| 111 |
+
self.metrics[i] = d, dst, s
|
| 112 |
+
|
| 113 |
+
def save(self, filename: str) -> None:
|
| 114 |
+
"""Save font"""
|
| 115 |
+
|
| 116 |
+
self.compile()
|
| 117 |
+
|
| 118 |
+
# font data
|
| 119 |
+
if not self.bitmap:
|
| 120 |
+
msg = "No bitmap created"
|
| 121 |
+
raise ValueError(msg)
|
| 122 |
+
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
|
| 123 |
+
|
| 124 |
+
# font metrics
|
| 125 |
+
with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
|
| 126 |
+
fp.write(b"PILfont\n")
|
| 127 |
+
fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
|
| 128 |
+
fp.write(b"DATA\n")
|
| 129 |
+
for id in range(256):
|
| 130 |
+
m = self.metrics[id]
|
| 131 |
+
if not m:
|
| 132 |
+
puti16(fp, (0,) * 10)
|
| 133 |
+
else:
|
| 134 |
+
puti16(fp, m[0] + m[1] + m[2])
|
Lib/site-packages/PIL/FpxImagePlugin.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# THIS IS WORK IN PROGRESS
|
| 3 |
+
#
|
| 4 |
+
# The Python Imaging Library.
|
| 5 |
+
# $Id$
|
| 6 |
+
#
|
| 7 |
+
# FlashPix support for PIL
|
| 8 |
+
#
|
| 9 |
+
# History:
|
| 10 |
+
# 97-01-25 fl Created (reads uncompressed RGB images only)
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 13 |
+
# Copyright (c) Fredrik Lundh 1997.
|
| 14 |
+
#
|
| 15 |
+
# See the README file for information on usage and redistribution.
|
| 16 |
+
#
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import olefile
|
| 20 |
+
|
| 21 |
+
from . import Image, ImageFile
|
| 22 |
+
from ._binary import i32le as i32
|
| 23 |
+
|
| 24 |
+
# we map from colour field tuples to (mode, rawmode) descriptors
|
| 25 |
+
MODES = {
|
| 26 |
+
# opacity
|
| 27 |
+
(0x00007FFE,): ("A", "L"),
|
| 28 |
+
# monochrome
|
| 29 |
+
(0x00010000,): ("L", "L"),
|
| 30 |
+
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
|
| 31 |
+
# photo YCC
|
| 32 |
+
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
|
| 33 |
+
(0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
|
| 34 |
+
# standard RGB (NIFRGB)
|
| 35 |
+
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
|
| 36 |
+
(0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
#
|
| 41 |
+
# --------------------------------------------------------------------
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _accept(prefix: bytes) -> bool:
|
| 45 |
+
return prefix[:8] == olefile.MAGIC
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
##
|
| 49 |
+
# Image plugin for the FlashPix images.
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class FpxImageFile(ImageFile.ImageFile):
|
| 53 |
+
format = "FPX"
|
| 54 |
+
format_description = "FlashPix"
|
| 55 |
+
|
| 56 |
+
def _open(self) -> None:
|
| 57 |
+
#
|
| 58 |
+
# read the OLE directory and see if this is a likely
|
| 59 |
+
# to be a FlashPix file
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
self.ole = olefile.OleFileIO(self.fp)
|
| 63 |
+
except OSError as e:
|
| 64 |
+
msg = "not an FPX file; invalid OLE file"
|
| 65 |
+
raise SyntaxError(msg) from e
|
| 66 |
+
|
| 67 |
+
root = self.ole.root
|
| 68 |
+
if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
|
| 69 |
+
msg = "not an FPX file; bad root CLSID"
|
| 70 |
+
raise SyntaxError(msg)
|
| 71 |
+
|
| 72 |
+
self._open_index(1)
|
| 73 |
+
|
| 74 |
+
def _open_index(self, index: int = 1) -> None:
|
| 75 |
+
#
|
| 76 |
+
# get the Image Contents Property Set
|
| 77 |
+
|
| 78 |
+
prop = self.ole.getproperties(
|
| 79 |
+
[f"Data Object Store {index:06d}", "\005Image Contents"]
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# size (highest resolution)
|
| 83 |
+
|
| 84 |
+
assert isinstance(prop[0x1000002], int)
|
| 85 |
+
assert isinstance(prop[0x1000003], int)
|
| 86 |
+
self._size = prop[0x1000002], prop[0x1000003]
|
| 87 |
+
|
| 88 |
+
size = max(self.size)
|
| 89 |
+
i = 1
|
| 90 |
+
while size > 64:
|
| 91 |
+
size = size // 2
|
| 92 |
+
i += 1
|
| 93 |
+
self.maxid = i - 1
|
| 94 |
+
|
| 95 |
+
# mode. instead of using a single field for this, flashpix
|
| 96 |
+
# requires you to specify the mode for each channel in each
|
| 97 |
+
# resolution subimage, and leaves it to the decoder to make
|
| 98 |
+
# sure that they all match. for now, we'll cheat and assume
|
| 99 |
+
# that this is always the case.
|
| 100 |
+
|
| 101 |
+
id = self.maxid << 16
|
| 102 |
+
|
| 103 |
+
s = prop[0x2000002 | id]
|
| 104 |
+
|
| 105 |
+
if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
|
| 106 |
+
msg = "Invalid number of bands"
|
| 107 |
+
raise OSError(msg)
|
| 108 |
+
|
| 109 |
+
# note: for now, we ignore the "uncalibrated" flag
|
| 110 |
+
colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))
|
| 111 |
+
|
| 112 |
+
self._mode, self.rawmode = MODES[colors]
|
| 113 |
+
|
| 114 |
+
# load JPEG tables, if any
|
| 115 |
+
self.jpeg = {}
|
| 116 |
+
for i in range(256):
|
| 117 |
+
id = 0x3000001 | (i << 16)
|
| 118 |
+
if id in prop:
|
| 119 |
+
self.jpeg[i] = prop[id]
|
| 120 |
+
|
| 121 |
+
self._open_subimage(1, self.maxid)
|
| 122 |
+
|
| 123 |
+
def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
|
| 124 |
+
#
|
| 125 |
+
# setup tile descriptors for a given subimage
|
| 126 |
+
|
| 127 |
+
stream = [
|
| 128 |
+
f"Data Object Store {index:06d}",
|
| 129 |
+
f"Resolution {subimage:04d}",
|
| 130 |
+
"Subimage 0000 Header",
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
fp = self.ole.openstream(stream)
|
| 134 |
+
|
| 135 |
+
# skip prefix
|
| 136 |
+
fp.read(28)
|
| 137 |
+
|
| 138 |
+
# header stream
|
| 139 |
+
s = fp.read(36)
|
| 140 |
+
|
| 141 |
+
size = i32(s, 4), i32(s, 8)
|
| 142 |
+
# tilecount = i32(s, 12)
|
| 143 |
+
tilesize = i32(s, 16), i32(s, 20)
|
| 144 |
+
# channels = i32(s, 24)
|
| 145 |
+
offset = i32(s, 28)
|
| 146 |
+
length = i32(s, 32)
|
| 147 |
+
|
| 148 |
+
if size != self.size:
|
| 149 |
+
msg = "subimage mismatch"
|
| 150 |
+
raise OSError(msg)
|
| 151 |
+
|
| 152 |
+
# get tile descriptors
|
| 153 |
+
fp.seek(28 + offset)
|
| 154 |
+
s = fp.read(i32(s, 12) * length)
|
| 155 |
+
|
| 156 |
+
x = y = 0
|
| 157 |
+
xsize, ysize = size
|
| 158 |
+
xtile, ytile = tilesize
|
| 159 |
+
self.tile = []
|
| 160 |
+
|
| 161 |
+
for i in range(0, len(s), length):
|
| 162 |
+
x1 = min(xsize, x + xtile)
|
| 163 |
+
y1 = min(ysize, y + ytile)
|
| 164 |
+
|
| 165 |
+
compression = i32(s, i + 8)
|
| 166 |
+
|
| 167 |
+
if compression == 0:
|
| 168 |
+
self.tile.append(
|
| 169 |
+
ImageFile._Tile(
|
| 170 |
+
"raw",
|
| 171 |
+
(x, y, x1, y1),
|
| 172 |
+
i32(s, i) + 28,
|
| 173 |
+
self.rawmode,
|
| 174 |
+
)
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
elif compression == 1:
|
| 178 |
+
# FIXME: the fill decoder is not implemented
|
| 179 |
+
self.tile.append(
|
| 180 |
+
ImageFile._Tile(
|
| 181 |
+
"fill",
|
| 182 |
+
(x, y, x1, y1),
|
| 183 |
+
i32(s, i) + 28,
|
| 184 |
+
(self.rawmode, s[12:16]),
|
| 185 |
+
)
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
elif compression == 2:
|
| 189 |
+
internal_color_conversion = s[14]
|
| 190 |
+
jpeg_tables = s[15]
|
| 191 |
+
rawmode = self.rawmode
|
| 192 |
+
|
| 193 |
+
if internal_color_conversion:
|
| 194 |
+
# The image is stored as usual (usually YCbCr).
|
| 195 |
+
if rawmode == "RGBA":
|
| 196 |
+
# For "RGBA", data is stored as YCbCrA based on
|
| 197 |
+
# negative RGB. The following trick works around
|
| 198 |
+
# this problem :
|
| 199 |
+
jpegmode, rawmode = "YCbCrK", "CMYK"
|
| 200 |
+
else:
|
| 201 |
+
jpegmode = None # let the decoder decide
|
| 202 |
+
|
| 203 |
+
else:
|
| 204 |
+
# The image is stored as defined by rawmode
|
| 205 |
+
jpegmode = rawmode
|
| 206 |
+
|
| 207 |
+
self.tile.append(
|
| 208 |
+
ImageFile._Tile(
|
| 209 |
+
"jpeg",
|
| 210 |
+
(x, y, x1, y1),
|
| 211 |
+
i32(s, i) + 28,
|
| 212 |
+
(rawmode, jpegmode),
|
| 213 |
+
)
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# FIXME: jpeg tables are tile dependent; the prefix
|
| 217 |
+
# data must be placed in the tile descriptor itself!
|
| 218 |
+
|
| 219 |
+
if jpeg_tables:
|
| 220 |
+
self.tile_prefix = self.jpeg[jpeg_tables]
|
| 221 |
+
|
| 222 |
+
else:
|
| 223 |
+
msg = "unknown/invalid compression"
|
| 224 |
+
raise OSError(msg)
|
| 225 |
+
|
| 226 |
+
x = x + xtile
|
| 227 |
+
if x >= xsize:
|
| 228 |
+
x, y = 0, y + ytile
|
| 229 |
+
if y >= ysize:
|
| 230 |
+
break # isn't really required
|
| 231 |
+
|
| 232 |
+
self.stream = stream
|
| 233 |
+
self._fp = self.fp
|
| 234 |
+
self.fp = None
|
| 235 |
+
|
| 236 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 237 |
+
if not self.fp:
|
| 238 |
+
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
|
| 239 |
+
|
| 240 |
+
return ImageFile.ImageFile.load(self)
|
| 241 |
+
|
| 242 |
+
def close(self) -> None:
|
| 243 |
+
self.ole.close()
|
| 244 |
+
super().close()
|
| 245 |
+
|
| 246 |
+
def __exit__(self, *args: object) -> None:
|
| 247 |
+
self.ole.close()
|
| 248 |
+
super().__exit__()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
#
|
| 252 |
+
# --------------------------------------------------------------------
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
|
| 256 |
+
|
| 257 |
+
Image.register_extension(FpxImageFile.format, ".fpx")
|
Lib/site-packages/PIL/FtexImagePlugin.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A Pillow loader for .ftc and .ftu files (FTEX)
|
| 3 |
+
Jerome Leclanche <jerome@leclan.ch>
|
| 4 |
+
|
| 5 |
+
The contents of this file are hereby released in the public domain (CC0)
|
| 6 |
+
Full text of the CC0 license:
|
| 7 |
+
https://creativecommons.org/publicdomain/zero/1.0/
|
| 8 |
+
|
| 9 |
+
Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
|
| 10 |
+
|
| 11 |
+
The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
|
| 12 |
+
packed custom format called FTEX. This file format uses file extensions FTC
|
| 13 |
+
and FTU.
|
| 14 |
+
* FTC files are compressed textures (using standard texture compression).
|
| 15 |
+
* FTU files are not compressed.
|
| 16 |
+
Texture File Format
|
| 17 |
+
The FTC and FTU texture files both use the same format. This
|
| 18 |
+
has the following structure:
|
| 19 |
+
{header}
|
| 20 |
+
{format_directory}
|
| 21 |
+
{data}
|
| 22 |
+
Where:
|
| 23 |
+
{header} = {
|
| 24 |
+
u32:magic,
|
| 25 |
+
u32:version,
|
| 26 |
+
u32:width,
|
| 27 |
+
u32:height,
|
| 28 |
+
u32:mipmap_count,
|
| 29 |
+
u32:format_count
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
* The "magic" number is "FTEX".
|
| 33 |
+
* "width" and "height" are the dimensions of the texture.
|
| 34 |
+
* "mipmap_count" is the number of mipmaps in the texture.
|
| 35 |
+
* "format_count" is the number of texture formats (different versions of the
|
| 36 |
+
same texture) in this file.
|
| 37 |
+
|
| 38 |
+
{format_directory} = format_count * { u32:format, u32:where }
|
| 39 |
+
|
| 40 |
+
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB
|
| 41 |
+
uncompressed textures.
|
| 42 |
+
The texture data for a format starts at the position "where" in the file.
|
| 43 |
+
|
| 44 |
+
Each set of texture data in the file has the following structure:
|
| 45 |
+
{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
|
| 46 |
+
* "mipmap_size" is the number of bytes in that mip level. For compressed
|
| 47 |
+
textures this is the size of the texture data compressed with DXT1. For 24 bit
|
| 48 |
+
uncompressed textures, this is 3 * width * height. Following this are the image
|
| 49 |
+
bytes for that mipmap level.
|
| 50 |
+
|
| 51 |
+
Note: All data is stored in little-Endian (Intel) byte order.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
from __future__ import annotations
|
| 55 |
+
|
| 56 |
+
import struct
|
| 57 |
+
from enum import IntEnum
|
| 58 |
+
from io import BytesIO
|
| 59 |
+
|
| 60 |
+
from . import Image, ImageFile
|
| 61 |
+
|
| 62 |
+
MAGIC = b"FTEX"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class Format(IntEnum):
|
| 66 |
+
DXT1 = 0
|
| 67 |
+
UNCOMPRESSED = 1
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class FtexImageFile(ImageFile.ImageFile):
|
| 71 |
+
format = "FTEX"
|
| 72 |
+
format_description = "Texture File Format (IW2:EOC)"
|
| 73 |
+
|
| 74 |
+
def _open(self) -> None:
|
| 75 |
+
if not _accept(self.fp.read(4)):
|
| 76 |
+
msg = "not an FTEX file"
|
| 77 |
+
raise SyntaxError(msg)
|
| 78 |
+
struct.unpack("<i", self.fp.read(4)) # version
|
| 79 |
+
self._size = struct.unpack("<2i", self.fp.read(8))
|
| 80 |
+
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
| 81 |
+
|
| 82 |
+
self._mode = "RGB"
|
| 83 |
+
|
| 84 |
+
# Only support single-format files.
|
| 85 |
+
# I don't know of any multi-format file.
|
| 86 |
+
assert format_count == 1
|
| 87 |
+
|
| 88 |
+
format, where = struct.unpack("<2i", self.fp.read(8))
|
| 89 |
+
self.fp.seek(where)
|
| 90 |
+
(mipmap_size,) = struct.unpack("<i", self.fp.read(4))
|
| 91 |
+
|
| 92 |
+
data = self.fp.read(mipmap_size)
|
| 93 |
+
|
| 94 |
+
if format == Format.DXT1:
|
| 95 |
+
self._mode = "RGBA"
|
| 96 |
+
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
|
| 97 |
+
elif format == Format.UNCOMPRESSED:
|
| 98 |
+
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
|
| 99 |
+
else:
|
| 100 |
+
msg = f"Invalid texture compression format: {repr(format)}"
|
| 101 |
+
raise ValueError(msg)
|
| 102 |
+
|
| 103 |
+
self.fp.close()
|
| 104 |
+
self.fp = BytesIO(data)
|
| 105 |
+
|
| 106 |
+
def load_seek(self, pos: int) -> None:
|
| 107 |
+
pass
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _accept(prefix: bytes) -> bool:
|
| 111 |
+
return prefix[:4] == MAGIC
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
|
| 115 |
+
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])
|
Lib/site-packages/PIL/GbrImagePlugin.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
#
|
| 4 |
+
# load a GIMP brush file
|
| 5 |
+
#
|
| 6 |
+
# History:
|
| 7 |
+
# 96-03-14 fl Created
|
| 8 |
+
# 16-01-08 es Version 2
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 11 |
+
# Copyright (c) Fredrik Lundh 1996.
|
| 12 |
+
# Copyright (c) Eric Soroos 2016.
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
#
|
| 17 |
+
# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for
|
| 18 |
+
# format documentation.
|
| 19 |
+
#
|
| 20 |
+
# This code Interprets version 1 and 2 .gbr files.
|
| 21 |
+
# Version 1 files are obsolete, and should not be used for new
|
| 22 |
+
# brushes.
|
| 23 |
+
# Version 2 files are saved by GIMP v2.8 (at least)
|
| 24 |
+
# Version 3 files have a format specifier of 18 for 16bit floats in
|
| 25 |
+
# the color depth field. This is currently unsupported by Pillow.
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
from . import Image, ImageFile
|
| 29 |
+
from ._binary import i32be as i32
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _accept(prefix: bytes) -> bool:
|
| 33 |
+
return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
##
|
| 37 |
+
# Image plugin for the GIMP brush format.
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class GbrImageFile(ImageFile.ImageFile):
|
| 41 |
+
format = "GBR"
|
| 42 |
+
format_description = "GIMP brush file"
|
| 43 |
+
|
| 44 |
+
def _open(self) -> None:
|
| 45 |
+
header_size = i32(self.fp.read(4))
|
| 46 |
+
if header_size < 20:
|
| 47 |
+
msg = "not a GIMP brush"
|
| 48 |
+
raise SyntaxError(msg)
|
| 49 |
+
version = i32(self.fp.read(4))
|
| 50 |
+
if version not in (1, 2):
|
| 51 |
+
msg = f"Unsupported GIMP brush version: {version}"
|
| 52 |
+
raise SyntaxError(msg)
|
| 53 |
+
|
| 54 |
+
width = i32(self.fp.read(4))
|
| 55 |
+
height = i32(self.fp.read(4))
|
| 56 |
+
color_depth = i32(self.fp.read(4))
|
| 57 |
+
if width <= 0 or height <= 0:
|
| 58 |
+
msg = "not a GIMP brush"
|
| 59 |
+
raise SyntaxError(msg)
|
| 60 |
+
if color_depth not in (1, 4):
|
| 61 |
+
msg = f"Unsupported GIMP brush color depth: {color_depth}"
|
| 62 |
+
raise SyntaxError(msg)
|
| 63 |
+
|
| 64 |
+
if version == 1:
|
| 65 |
+
comment_length = header_size - 20
|
| 66 |
+
else:
|
| 67 |
+
comment_length = header_size - 28
|
| 68 |
+
magic_number = self.fp.read(4)
|
| 69 |
+
if magic_number != b"GIMP":
|
| 70 |
+
msg = "not a GIMP brush, bad magic number"
|
| 71 |
+
raise SyntaxError(msg)
|
| 72 |
+
self.info["spacing"] = i32(self.fp.read(4))
|
| 73 |
+
|
| 74 |
+
comment = self.fp.read(comment_length)[:-1]
|
| 75 |
+
|
| 76 |
+
if color_depth == 1:
|
| 77 |
+
self._mode = "L"
|
| 78 |
+
else:
|
| 79 |
+
self._mode = "RGBA"
|
| 80 |
+
|
| 81 |
+
self._size = width, height
|
| 82 |
+
|
| 83 |
+
self.info["comment"] = comment
|
| 84 |
+
|
| 85 |
+
# Image might not be small
|
| 86 |
+
Image._decompression_bomb_check(self.size)
|
| 87 |
+
|
| 88 |
+
# Data is an uncompressed block of w * h * bytes/pixel
|
| 89 |
+
self._data_size = width * height * color_depth
|
| 90 |
+
|
| 91 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 92 |
+
if self._im is None:
|
| 93 |
+
self.im = Image.core.new(self.mode, self.size)
|
| 94 |
+
self.frombytes(self.fp.read(self._data_size))
|
| 95 |
+
return Image.Image.load(self)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
#
|
| 99 |
+
# registry
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
| 103 |
+
Image.register_extension(GbrImageFile.format, ".gbr")
|
Lib/site-packages/PIL/GdImageFile.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# GD file handling
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-04-12 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) 1997 by Secret Labs AB.
|
| 11 |
+
# Copyright (c) 1996 by Fredrik Lundh.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
.. note::
|
| 19 |
+
This format cannot be automatically recognized, so the
|
| 20 |
+
class is not registered for use with :py:func:`PIL.Image.open()`. To open a
|
| 21 |
+
gd file, use the :py:func:`PIL.GdImageFile.open()` function instead.
|
| 22 |
+
|
| 23 |
+
.. warning::
|
| 24 |
+
THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
|
| 25 |
+
implementation is provided for convenience and demonstrational
|
| 26 |
+
purposes only.
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
from typing import IO
|
| 31 |
+
|
| 32 |
+
from . import ImageFile, ImagePalette, UnidentifiedImageError
|
| 33 |
+
from ._binary import i16be as i16
|
| 34 |
+
from ._binary import i32be as i32
|
| 35 |
+
from ._typing import StrOrBytesPath
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class GdImageFile(ImageFile.ImageFile):
|
| 39 |
+
"""
|
| 40 |
+
Image plugin for the GD uncompressed format. Note that this format
|
| 41 |
+
is not supported by the standard :py:func:`PIL.Image.open()` function. To use
|
| 42 |
+
this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and
|
| 43 |
+
use the :py:func:`PIL.GdImageFile.open()` function.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
format = "GD"
|
| 47 |
+
format_description = "GD uncompressed images"
|
| 48 |
+
|
| 49 |
+
def _open(self) -> None:
|
| 50 |
+
# Header
|
| 51 |
+
assert self.fp is not None
|
| 52 |
+
|
| 53 |
+
s = self.fp.read(1037)
|
| 54 |
+
|
| 55 |
+
if i16(s) not in [65534, 65535]:
|
| 56 |
+
msg = "Not a valid GD 2.x .gd file"
|
| 57 |
+
raise SyntaxError(msg)
|
| 58 |
+
|
| 59 |
+
self._mode = "L" # FIXME: "P"
|
| 60 |
+
self._size = i16(s, 2), i16(s, 4)
|
| 61 |
+
|
| 62 |
+
true_color = s[6]
|
| 63 |
+
true_color_offset = 2 if true_color else 0
|
| 64 |
+
|
| 65 |
+
# transparency index
|
| 66 |
+
tindex = i32(s, 7 + true_color_offset)
|
| 67 |
+
if tindex < 256:
|
| 68 |
+
self.info["transparency"] = tindex
|
| 69 |
+
|
| 70 |
+
self.palette = ImagePalette.raw(
|
| 71 |
+
"XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4]
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.tile = [
|
| 75 |
+
ImageFile._Tile(
|
| 76 |
+
"raw",
|
| 77 |
+
(0, 0) + self.size,
|
| 78 |
+
7 + true_color_offset + 4 + 256 * 4,
|
| 79 |
+
"L",
|
| 80 |
+
)
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile:
|
| 85 |
+
"""
|
| 86 |
+
Load texture from a GD image file.
|
| 87 |
+
|
| 88 |
+
:param fp: GD file name, or an opened file handle.
|
| 89 |
+
:param mode: Optional mode. In this version, if the mode argument
|
| 90 |
+
is given, it must be "r".
|
| 91 |
+
:returns: An image instance.
|
| 92 |
+
:raises OSError: If the image could not be read.
|
| 93 |
+
"""
|
| 94 |
+
if mode != "r":
|
| 95 |
+
msg = "bad mode"
|
| 96 |
+
raise ValueError(msg)
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
return GdImageFile(fp)
|
| 100 |
+
except SyntaxError as e:
|
| 101 |
+
msg = "cannot identify this image file"
|
| 102 |
+
raise UnidentifiedImageError(msg) from e
|
Lib/site-packages/PIL/GifImagePlugin.py
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# GIF file handling
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1995-09-01 fl Created
|
| 9 |
+
# 1996-12-14 fl Added interlace support
|
| 10 |
+
# 1996-12-30 fl Added animation support
|
| 11 |
+
# 1997-01-05 fl Added write support, fixed local colour map bug
|
| 12 |
+
# 1997-02-23 fl Make sure to load raster data in getdata()
|
| 13 |
+
# 1997-07-05 fl Support external decoder (0.4)
|
| 14 |
+
# 1998-07-09 fl Handle all modes when saving (0.5)
|
| 15 |
+
# 1998-07-15 fl Renamed offset attribute to avoid name clash
|
| 16 |
+
# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6)
|
| 17 |
+
# 2001-04-17 fl Added palette optimization (0.7)
|
| 18 |
+
# 2002-06-06 fl Added transparency support for save (0.8)
|
| 19 |
+
# 2004-02-24 fl Disable interlacing for small images
|
| 20 |
+
#
|
| 21 |
+
# Copyright (c) 1997-2004 by Secret Labs AB
|
| 22 |
+
# Copyright (c) 1995-2004 by Fredrik Lundh
|
| 23 |
+
#
|
| 24 |
+
# See the README file for information on usage and redistribution.
|
| 25 |
+
#
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import itertools
|
| 29 |
+
import math
|
| 30 |
+
import os
|
| 31 |
+
import subprocess
|
| 32 |
+
from enum import IntEnum
|
| 33 |
+
from functools import cached_property
|
| 34 |
+
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
|
| 35 |
+
|
| 36 |
+
from . import (
|
| 37 |
+
Image,
|
| 38 |
+
ImageChops,
|
| 39 |
+
ImageFile,
|
| 40 |
+
ImageMath,
|
| 41 |
+
ImageOps,
|
| 42 |
+
ImagePalette,
|
| 43 |
+
ImageSequence,
|
| 44 |
+
)
|
| 45 |
+
from ._binary import i16le as i16
|
| 46 |
+
from ._binary import o8
|
| 47 |
+
from ._binary import o16le as o16
|
| 48 |
+
|
| 49 |
+
if TYPE_CHECKING:
|
| 50 |
+
from . import _imaging
|
| 51 |
+
from ._typing import Buffer
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class LoadingStrategy(IntEnum):
|
| 55 |
+
""".. versionadded:: 9.1.0"""
|
| 56 |
+
|
| 57 |
+
RGB_AFTER_FIRST = 0
|
| 58 |
+
RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
|
| 59 |
+
RGB_ALWAYS = 2
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
#: .. versionadded:: 9.1.0
|
| 63 |
+
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
|
| 64 |
+
|
| 65 |
+
# --------------------------------------------------------------------
|
| 66 |
+
# Identify/read GIF files
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _accept(prefix: bytes) -> bool:
|
| 70 |
+
return prefix[:6] in [b"GIF87a", b"GIF89a"]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
##
|
| 74 |
+
# Image plugin for GIF images. This plugin supports both GIF87 and
|
| 75 |
+
# GIF89 images.
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class GifImageFile(ImageFile.ImageFile):
|
| 79 |
+
format = "GIF"
|
| 80 |
+
format_description = "Compuserve GIF"
|
| 81 |
+
_close_exclusive_fp_after_loading = False
|
| 82 |
+
|
| 83 |
+
global_palette = None
|
| 84 |
+
|
| 85 |
+
def data(self) -> bytes | None:
|
| 86 |
+
s = self.fp.read(1)
|
| 87 |
+
if s and s[0]:
|
| 88 |
+
return self.fp.read(s[0])
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
def _is_palette_needed(self, p: bytes) -> bool:
|
| 92 |
+
for i in range(0, len(p), 3):
|
| 93 |
+
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
| 94 |
+
return True
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
def _open(self) -> None:
|
| 98 |
+
# Screen
|
| 99 |
+
s = self.fp.read(13)
|
| 100 |
+
if not _accept(s):
|
| 101 |
+
msg = "not a GIF file"
|
| 102 |
+
raise SyntaxError(msg)
|
| 103 |
+
|
| 104 |
+
self.info["version"] = s[:6]
|
| 105 |
+
self._size = i16(s, 6), i16(s, 8)
|
| 106 |
+
flags = s[10]
|
| 107 |
+
bits = (flags & 7) + 1
|
| 108 |
+
|
| 109 |
+
if flags & 128:
|
| 110 |
+
# get global palette
|
| 111 |
+
self.info["background"] = s[11]
|
| 112 |
+
# check if palette contains colour indices
|
| 113 |
+
p = self.fp.read(3 << bits)
|
| 114 |
+
if self._is_palette_needed(p):
|
| 115 |
+
p = ImagePalette.raw("RGB", p)
|
| 116 |
+
self.global_palette = self.palette = p
|
| 117 |
+
|
| 118 |
+
self._fp = self.fp # FIXME: hack
|
| 119 |
+
self.__rewind = self.fp.tell()
|
| 120 |
+
self._n_frames: int | None = None
|
| 121 |
+
self._seek(0) # get ready to read first frame
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def n_frames(self) -> int:
|
| 125 |
+
if self._n_frames is None:
|
| 126 |
+
current = self.tell()
|
| 127 |
+
try:
|
| 128 |
+
while True:
|
| 129 |
+
self._seek(self.tell() + 1, False)
|
| 130 |
+
except EOFError:
|
| 131 |
+
self._n_frames = self.tell() + 1
|
| 132 |
+
self.seek(current)
|
| 133 |
+
return self._n_frames
|
| 134 |
+
|
| 135 |
+
@cached_property
|
| 136 |
+
def is_animated(self) -> bool:
|
| 137 |
+
if self._n_frames is not None:
|
| 138 |
+
return self._n_frames != 1
|
| 139 |
+
|
| 140 |
+
current = self.tell()
|
| 141 |
+
if current:
|
| 142 |
+
return True
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
self._seek(1, False)
|
| 146 |
+
is_animated = True
|
| 147 |
+
except EOFError:
|
| 148 |
+
is_animated = False
|
| 149 |
+
|
| 150 |
+
self.seek(current)
|
| 151 |
+
return is_animated
|
| 152 |
+
|
| 153 |
+
def seek(self, frame: int) -> None:
|
| 154 |
+
if not self._seek_check(frame):
|
| 155 |
+
return
|
| 156 |
+
if frame < self.__frame:
|
| 157 |
+
self._im = None
|
| 158 |
+
self._seek(0)
|
| 159 |
+
|
| 160 |
+
last_frame = self.__frame
|
| 161 |
+
for f in range(self.__frame + 1, frame + 1):
|
| 162 |
+
try:
|
| 163 |
+
self._seek(f)
|
| 164 |
+
except EOFError as e:
|
| 165 |
+
self.seek(last_frame)
|
| 166 |
+
msg = "no more images in GIF file"
|
| 167 |
+
raise EOFError(msg) from e
|
| 168 |
+
|
| 169 |
+
def _seek(self, frame: int, update_image: bool = True) -> None:
|
| 170 |
+
if frame == 0:
|
| 171 |
+
# rewind
|
| 172 |
+
self.__offset = 0
|
| 173 |
+
self.dispose: _imaging.ImagingCore | None = None
|
| 174 |
+
self.__frame = -1
|
| 175 |
+
self._fp.seek(self.__rewind)
|
| 176 |
+
self.disposal_method = 0
|
| 177 |
+
if "comment" in self.info:
|
| 178 |
+
del self.info["comment"]
|
| 179 |
+
else:
|
| 180 |
+
# ensure that the previous frame was loaded
|
| 181 |
+
if self.tile and update_image:
|
| 182 |
+
self.load()
|
| 183 |
+
|
| 184 |
+
if frame != self.__frame + 1:
|
| 185 |
+
msg = f"cannot seek to frame {frame}"
|
| 186 |
+
raise ValueError(msg)
|
| 187 |
+
|
| 188 |
+
self.fp = self._fp
|
| 189 |
+
if self.__offset:
|
| 190 |
+
# backup to last frame
|
| 191 |
+
self.fp.seek(self.__offset)
|
| 192 |
+
while self.data():
|
| 193 |
+
pass
|
| 194 |
+
self.__offset = 0
|
| 195 |
+
|
| 196 |
+
s = self.fp.read(1)
|
| 197 |
+
if not s or s == b";":
|
| 198 |
+
msg = "no more images in GIF file"
|
| 199 |
+
raise EOFError(msg)
|
| 200 |
+
|
| 201 |
+
palette: ImagePalette.ImagePalette | Literal[False] | None = None
|
| 202 |
+
|
| 203 |
+
info: dict[str, Any] = {}
|
| 204 |
+
frame_transparency = None
|
| 205 |
+
interlace = None
|
| 206 |
+
frame_dispose_extent = None
|
| 207 |
+
while True:
|
| 208 |
+
if not s:
|
| 209 |
+
s = self.fp.read(1)
|
| 210 |
+
if not s or s == b";":
|
| 211 |
+
break
|
| 212 |
+
|
| 213 |
+
elif s == b"!":
|
| 214 |
+
#
|
| 215 |
+
# extensions
|
| 216 |
+
#
|
| 217 |
+
s = self.fp.read(1)
|
| 218 |
+
block = self.data()
|
| 219 |
+
if s[0] == 249 and block is not None:
|
| 220 |
+
#
|
| 221 |
+
# graphic control extension
|
| 222 |
+
#
|
| 223 |
+
flags = block[0]
|
| 224 |
+
if flags & 1:
|
| 225 |
+
frame_transparency = block[3]
|
| 226 |
+
info["duration"] = i16(block, 1) * 10
|
| 227 |
+
|
| 228 |
+
# disposal method - find the value of bits 4 - 6
|
| 229 |
+
dispose_bits = 0b00011100 & flags
|
| 230 |
+
dispose_bits = dispose_bits >> 2
|
| 231 |
+
if dispose_bits:
|
| 232 |
+
# only set the dispose if it is not
|
| 233 |
+
# unspecified. I'm not sure if this is
|
| 234 |
+
# correct, but it seems to prevent the last
|
| 235 |
+
# frame from looking odd for some animations
|
| 236 |
+
self.disposal_method = dispose_bits
|
| 237 |
+
elif s[0] == 254:
|
| 238 |
+
#
|
| 239 |
+
# comment extension
|
| 240 |
+
#
|
| 241 |
+
comment = b""
|
| 242 |
+
|
| 243 |
+
# Read this comment block
|
| 244 |
+
while block:
|
| 245 |
+
comment += block
|
| 246 |
+
block = self.data()
|
| 247 |
+
|
| 248 |
+
if "comment" in info:
|
| 249 |
+
# If multiple comment blocks in frame, separate with \n
|
| 250 |
+
info["comment"] += b"\n" + comment
|
| 251 |
+
else:
|
| 252 |
+
info["comment"] = comment
|
| 253 |
+
s = None
|
| 254 |
+
continue
|
| 255 |
+
elif s[0] == 255 and frame == 0 and block is not None:
|
| 256 |
+
#
|
| 257 |
+
# application extension
|
| 258 |
+
#
|
| 259 |
+
info["extension"] = block, self.fp.tell()
|
| 260 |
+
if block[:11] == b"NETSCAPE2.0":
|
| 261 |
+
block = self.data()
|
| 262 |
+
if block and len(block) >= 3 and block[0] == 1:
|
| 263 |
+
self.info["loop"] = i16(block, 1)
|
| 264 |
+
while self.data():
|
| 265 |
+
pass
|
| 266 |
+
|
| 267 |
+
elif s == b",":
|
| 268 |
+
#
|
| 269 |
+
# local image
|
| 270 |
+
#
|
| 271 |
+
s = self.fp.read(9)
|
| 272 |
+
|
| 273 |
+
# extent
|
| 274 |
+
x0, y0 = i16(s, 0), i16(s, 2)
|
| 275 |
+
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
|
| 276 |
+
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
|
| 277 |
+
self._size = max(x1, self.size[0]), max(y1, self.size[1])
|
| 278 |
+
Image._decompression_bomb_check(self._size)
|
| 279 |
+
frame_dispose_extent = x0, y0, x1, y1
|
| 280 |
+
flags = s[8]
|
| 281 |
+
|
| 282 |
+
interlace = (flags & 64) != 0
|
| 283 |
+
|
| 284 |
+
if flags & 128:
|
| 285 |
+
bits = (flags & 7) + 1
|
| 286 |
+
p = self.fp.read(3 << bits)
|
| 287 |
+
if self._is_palette_needed(p):
|
| 288 |
+
palette = ImagePalette.raw("RGB", p)
|
| 289 |
+
else:
|
| 290 |
+
palette = False
|
| 291 |
+
|
| 292 |
+
# image data
|
| 293 |
+
bits = self.fp.read(1)[0]
|
| 294 |
+
self.__offset = self.fp.tell()
|
| 295 |
+
break
|
| 296 |
+
s = None
|
| 297 |
+
|
| 298 |
+
if interlace is None:
|
| 299 |
+
msg = "image not found in GIF frame"
|
| 300 |
+
raise EOFError(msg)
|
| 301 |
+
|
| 302 |
+
self.__frame = frame
|
| 303 |
+
if not update_image:
|
| 304 |
+
return
|
| 305 |
+
|
| 306 |
+
self.tile = []
|
| 307 |
+
|
| 308 |
+
if self.dispose:
|
| 309 |
+
self.im.paste(self.dispose, self.dispose_extent)
|
| 310 |
+
|
| 311 |
+
self._frame_palette = palette if palette is not None else self.global_palette
|
| 312 |
+
self._frame_transparency = frame_transparency
|
| 313 |
+
if frame == 0:
|
| 314 |
+
if self._frame_palette:
|
| 315 |
+
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
| 316 |
+
self._mode = "RGBA" if frame_transparency is not None else "RGB"
|
| 317 |
+
else:
|
| 318 |
+
self._mode = "P"
|
| 319 |
+
else:
|
| 320 |
+
self._mode = "L"
|
| 321 |
+
|
| 322 |
+
if palette:
|
| 323 |
+
self.palette = palette
|
| 324 |
+
elif self.global_palette:
|
| 325 |
+
from copy import copy
|
| 326 |
+
|
| 327 |
+
self.palette = copy(self.global_palette)
|
| 328 |
+
else:
|
| 329 |
+
self.palette = None
|
| 330 |
+
else:
|
| 331 |
+
if self.mode == "P":
|
| 332 |
+
if (
|
| 333 |
+
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
| 334 |
+
or palette
|
| 335 |
+
):
|
| 336 |
+
if "transparency" in self.info:
|
| 337 |
+
self.im.putpalettealpha(self.info["transparency"], 0)
|
| 338 |
+
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
| 339 |
+
self._mode = "RGBA"
|
| 340 |
+
del self.info["transparency"]
|
| 341 |
+
else:
|
| 342 |
+
self._mode = "RGB"
|
| 343 |
+
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
| 344 |
+
|
| 345 |
+
def _rgb(color: int) -> tuple[int, int, int]:
|
| 346 |
+
if self._frame_palette:
|
| 347 |
+
if color * 3 + 3 > len(self._frame_palette.palette):
|
| 348 |
+
color = 0
|
| 349 |
+
return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
| 350 |
+
else:
|
| 351 |
+
return (color, color, color)
|
| 352 |
+
|
| 353 |
+
self.dispose = None
|
| 354 |
+
self.dispose_extent = frame_dispose_extent
|
| 355 |
+
if self.dispose_extent and self.disposal_method >= 2:
|
| 356 |
+
try:
|
| 357 |
+
if self.disposal_method == 2:
|
| 358 |
+
# replace with background colour
|
| 359 |
+
|
| 360 |
+
# only dispose the extent in this frame
|
| 361 |
+
x0, y0, x1, y1 = self.dispose_extent
|
| 362 |
+
dispose_size = (x1 - x0, y1 - y0)
|
| 363 |
+
|
| 364 |
+
Image._decompression_bomb_check(dispose_size)
|
| 365 |
+
|
| 366 |
+
# by convention, attempt to use transparency first
|
| 367 |
+
dispose_mode = "P"
|
| 368 |
+
color = self.info.get("transparency", frame_transparency)
|
| 369 |
+
if color is not None:
|
| 370 |
+
if self.mode in ("RGB", "RGBA"):
|
| 371 |
+
dispose_mode = "RGBA"
|
| 372 |
+
color = _rgb(color) + (0,)
|
| 373 |
+
else:
|
| 374 |
+
color = self.info.get("background", 0)
|
| 375 |
+
if self.mode in ("RGB", "RGBA"):
|
| 376 |
+
dispose_mode = "RGB"
|
| 377 |
+
color = _rgb(color)
|
| 378 |
+
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
| 379 |
+
else:
|
| 380 |
+
# replace with previous contents
|
| 381 |
+
if self._im is not None:
|
| 382 |
+
# only dispose the extent in this frame
|
| 383 |
+
self.dispose = self._crop(self.im, self.dispose_extent)
|
| 384 |
+
elif frame_transparency is not None:
|
| 385 |
+
x0, y0, x1, y1 = self.dispose_extent
|
| 386 |
+
dispose_size = (x1 - x0, y1 - y0)
|
| 387 |
+
|
| 388 |
+
Image._decompression_bomb_check(dispose_size)
|
| 389 |
+
dispose_mode = "P"
|
| 390 |
+
color = frame_transparency
|
| 391 |
+
if self.mode in ("RGB", "RGBA"):
|
| 392 |
+
dispose_mode = "RGBA"
|
| 393 |
+
color = _rgb(frame_transparency) + (0,)
|
| 394 |
+
self.dispose = Image.core.fill(
|
| 395 |
+
dispose_mode, dispose_size, color
|
| 396 |
+
)
|
| 397 |
+
except AttributeError:
|
| 398 |
+
pass
|
| 399 |
+
|
| 400 |
+
if interlace is not None:
|
| 401 |
+
transparency = -1
|
| 402 |
+
if frame_transparency is not None:
|
| 403 |
+
if frame == 0:
|
| 404 |
+
if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
|
| 405 |
+
self.info["transparency"] = frame_transparency
|
| 406 |
+
elif self.mode not in ("RGB", "RGBA"):
|
| 407 |
+
transparency = frame_transparency
|
| 408 |
+
self.tile = [
|
| 409 |
+
ImageFile._Tile(
|
| 410 |
+
"gif",
|
| 411 |
+
(x0, y0, x1, y1),
|
| 412 |
+
self.__offset,
|
| 413 |
+
(bits, interlace, transparency),
|
| 414 |
+
)
|
| 415 |
+
]
|
| 416 |
+
|
| 417 |
+
if info.get("comment"):
|
| 418 |
+
self.info["comment"] = info["comment"]
|
| 419 |
+
for k in ["duration", "extension"]:
|
| 420 |
+
if k in info:
|
| 421 |
+
self.info[k] = info[k]
|
| 422 |
+
elif k in self.info:
|
| 423 |
+
del self.info[k]
|
| 424 |
+
|
| 425 |
+
def load_prepare(self) -> None:
|
| 426 |
+
temp_mode = "P" if self._frame_palette else "L"
|
| 427 |
+
self._prev_im = None
|
| 428 |
+
if self.__frame == 0:
|
| 429 |
+
if self._frame_transparency is not None:
|
| 430 |
+
self.im = Image.core.fill(
|
| 431 |
+
temp_mode, self.size, self._frame_transparency
|
| 432 |
+
)
|
| 433 |
+
elif self.mode in ("RGB", "RGBA"):
|
| 434 |
+
self._prev_im = self.im
|
| 435 |
+
if self._frame_palette:
|
| 436 |
+
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
| 437 |
+
self.im.putpalette("RGB", *self._frame_palette.getdata())
|
| 438 |
+
else:
|
| 439 |
+
self._im = None
|
| 440 |
+
if not self._prev_im and self._im is not None and self.size != self.im.size:
|
| 441 |
+
expanded_im = Image.core.fill(self.im.mode, self.size)
|
| 442 |
+
if self._frame_palette:
|
| 443 |
+
expanded_im.putpalette("RGB", *self._frame_palette.getdata())
|
| 444 |
+
expanded_im.paste(self.im, (0, 0) + self.im.size)
|
| 445 |
+
|
| 446 |
+
self.im = expanded_im
|
| 447 |
+
self._mode = temp_mode
|
| 448 |
+
self._frame_palette = None
|
| 449 |
+
|
| 450 |
+
super().load_prepare()
|
| 451 |
+
|
| 452 |
+
def load_end(self) -> None:
|
| 453 |
+
if self.__frame == 0:
|
| 454 |
+
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
| 455 |
+
if self._frame_transparency is not None:
|
| 456 |
+
self.im.putpalettealpha(self._frame_transparency, 0)
|
| 457 |
+
self._mode = "RGBA"
|
| 458 |
+
else:
|
| 459 |
+
self._mode = "RGB"
|
| 460 |
+
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
|
| 461 |
+
return
|
| 462 |
+
if not self._prev_im:
|
| 463 |
+
return
|
| 464 |
+
if self.size != self._prev_im.size:
|
| 465 |
+
if self._frame_transparency is not None:
|
| 466 |
+
expanded_im = Image.core.fill("RGBA", self.size)
|
| 467 |
+
else:
|
| 468 |
+
expanded_im = Image.core.fill("P", self.size)
|
| 469 |
+
expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
|
| 470 |
+
expanded_im = expanded_im.convert("RGB")
|
| 471 |
+
expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)
|
| 472 |
+
|
| 473 |
+
self._prev_im = expanded_im
|
| 474 |
+
assert self._prev_im is not None
|
| 475 |
+
if self._frame_transparency is not None:
|
| 476 |
+
self.im.putpalettealpha(self._frame_transparency, 0)
|
| 477 |
+
frame_im = self.im.convert("RGBA")
|
| 478 |
+
else:
|
| 479 |
+
frame_im = self.im.convert("RGB")
|
| 480 |
+
|
| 481 |
+
assert self.dispose_extent is not None
|
| 482 |
+
frame_im = self._crop(frame_im, self.dispose_extent)
|
| 483 |
+
|
| 484 |
+
self.im = self._prev_im
|
| 485 |
+
self._mode = self.im.mode
|
| 486 |
+
if frame_im.mode == "RGBA":
|
| 487 |
+
self.im.paste(frame_im, self.dispose_extent, frame_im)
|
| 488 |
+
else:
|
| 489 |
+
self.im.paste(frame_im, self.dispose_extent)
|
| 490 |
+
|
| 491 |
+
def tell(self) -> int:
|
| 492 |
+
return self.__frame
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
# --------------------------------------------------------------------
|
| 496 |
+
# Write GIF files
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
RAWMODE = {"1": "L", "L": "L", "P": "P"}
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def _normalize_mode(im: Image.Image) -> Image.Image:
|
| 503 |
+
"""
|
| 504 |
+
Takes an image (or frame), returns an image in a mode that is appropriate
|
| 505 |
+
for saving in a Gif.
|
| 506 |
+
|
| 507 |
+
It may return the original image, or it may return an image converted to
|
| 508 |
+
palette or 'L' mode.
|
| 509 |
+
|
| 510 |
+
:param im: Image object
|
| 511 |
+
:returns: Image object
|
| 512 |
+
"""
|
| 513 |
+
if im.mode in RAWMODE:
|
| 514 |
+
im.load()
|
| 515 |
+
return im
|
| 516 |
+
if Image.getmodebase(im.mode) == "RGB":
|
| 517 |
+
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
|
| 518 |
+
assert im.palette is not None
|
| 519 |
+
if im.palette.mode == "RGBA":
|
| 520 |
+
for rgba in im.palette.colors:
|
| 521 |
+
if rgba[3] == 0:
|
| 522 |
+
im.info["transparency"] = im.palette.colors[rgba]
|
| 523 |
+
break
|
| 524 |
+
return im
|
| 525 |
+
return im.convert("L")
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
def _normalize_palette(
|
| 532 |
+
im: Image.Image, palette: _Palette | None, info: dict[str, Any]
|
| 533 |
+
) -> Image.Image:
|
| 534 |
+
"""
|
| 535 |
+
Normalizes the palette for image.
|
| 536 |
+
- Sets the palette to the incoming palette, if provided.
|
| 537 |
+
- Ensures that there's a palette for L mode images
|
| 538 |
+
- Optimizes the palette if necessary/desired.
|
| 539 |
+
|
| 540 |
+
:param im: Image object
|
| 541 |
+
:param palette: bytes object containing the source palette, or ....
|
| 542 |
+
:param info: encoderinfo
|
| 543 |
+
:returns: Image object
|
| 544 |
+
"""
|
| 545 |
+
source_palette = None
|
| 546 |
+
if palette:
|
| 547 |
+
# a bytes palette
|
| 548 |
+
if isinstance(palette, (bytes, bytearray, list)):
|
| 549 |
+
source_palette = bytearray(palette[:768])
|
| 550 |
+
if isinstance(palette, ImagePalette.ImagePalette):
|
| 551 |
+
source_palette = bytearray(palette.palette)
|
| 552 |
+
|
| 553 |
+
if im.mode == "P":
|
| 554 |
+
if not source_palette:
|
| 555 |
+
im_palette = im.getpalette(None)
|
| 556 |
+
assert im_palette is not None
|
| 557 |
+
source_palette = bytearray(im_palette)
|
| 558 |
+
else: # L-mode
|
| 559 |
+
if not source_palette:
|
| 560 |
+
source_palette = bytearray(i // 3 for i in range(768))
|
| 561 |
+
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
| 562 |
+
assert source_palette is not None
|
| 563 |
+
|
| 564 |
+
if palette:
|
| 565 |
+
used_palette_colors: list[int | None] = []
|
| 566 |
+
assert im.palette is not None
|
| 567 |
+
for i in range(0, len(source_palette), 3):
|
| 568 |
+
source_color = tuple(source_palette[i : i + 3])
|
| 569 |
+
index = im.palette.colors.get(source_color)
|
| 570 |
+
if index in used_palette_colors:
|
| 571 |
+
index = None
|
| 572 |
+
used_palette_colors.append(index)
|
| 573 |
+
for i, index in enumerate(used_palette_colors):
|
| 574 |
+
if index is None:
|
| 575 |
+
for j in range(len(used_palette_colors)):
|
| 576 |
+
if j not in used_palette_colors:
|
| 577 |
+
used_palette_colors[i] = j
|
| 578 |
+
break
|
| 579 |
+
dest_map: list[int] = []
|
| 580 |
+
for index in used_palette_colors:
|
| 581 |
+
assert index is not None
|
| 582 |
+
dest_map.append(index)
|
| 583 |
+
im = im.remap_palette(dest_map)
|
| 584 |
+
else:
|
| 585 |
+
optimized_palette_colors = _get_optimize(im, info)
|
| 586 |
+
if optimized_palette_colors is not None:
|
| 587 |
+
im = im.remap_palette(optimized_palette_colors, source_palette)
|
| 588 |
+
if "transparency" in info:
|
| 589 |
+
try:
|
| 590 |
+
info["transparency"] = optimized_palette_colors.index(
|
| 591 |
+
info["transparency"]
|
| 592 |
+
)
|
| 593 |
+
except ValueError:
|
| 594 |
+
del info["transparency"]
|
| 595 |
+
return im
|
| 596 |
+
|
| 597 |
+
assert im.palette is not None
|
| 598 |
+
im.palette.palette = source_palette
|
| 599 |
+
return im
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def _write_single_frame(
|
| 603 |
+
im: Image.Image,
|
| 604 |
+
fp: IO[bytes],
|
| 605 |
+
palette: _Palette | None,
|
| 606 |
+
) -> None:
|
| 607 |
+
im_out = _normalize_mode(im)
|
| 608 |
+
for k, v in im_out.info.items():
|
| 609 |
+
if isinstance(k, str):
|
| 610 |
+
im.encoderinfo.setdefault(k, v)
|
| 611 |
+
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
|
| 612 |
+
|
| 613 |
+
for s in _get_global_header(im_out, im.encoderinfo):
|
| 614 |
+
fp.write(s)
|
| 615 |
+
|
| 616 |
+
# local image header
|
| 617 |
+
flags = 0
|
| 618 |
+
if get_interlace(im):
|
| 619 |
+
flags = flags | 64
|
| 620 |
+
_write_local_header(fp, im, (0, 0), flags)
|
| 621 |
+
|
| 622 |
+
im_out.encoderconfig = (8, get_interlace(im))
|
| 623 |
+
ImageFile._save(
|
| 624 |
+
im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
fp.write(b"\0") # end of image data
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def _getbbox(
|
| 631 |
+
base_im: Image.Image, im_frame: Image.Image
|
| 632 |
+
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
|
| 633 |
+
palette_bytes = [
|
| 634 |
+
bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
|
| 635 |
+
]
|
| 636 |
+
if palette_bytes[0] != palette_bytes[1]:
|
| 637 |
+
im_frame = im_frame.convert("RGBA")
|
| 638 |
+
base_im = base_im.convert("RGBA")
|
| 639 |
+
delta = ImageChops.subtract_modulo(im_frame, base_im)
|
| 640 |
+
return delta, delta.getbbox(alpha_only=False)
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
class _Frame(NamedTuple):
|
| 644 |
+
im: Image.Image
|
| 645 |
+
bbox: tuple[int, int, int, int] | None
|
| 646 |
+
encoderinfo: dict[str, Any]
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def _write_multiple_frames(
|
| 650 |
+
im: Image.Image, fp: IO[bytes], palette: _Palette | None
|
| 651 |
+
) -> bool:
|
| 652 |
+
duration = im.encoderinfo.get("duration")
|
| 653 |
+
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
| 654 |
+
|
| 655 |
+
im_frames: list[_Frame] = []
|
| 656 |
+
previous_im: Image.Image | None = None
|
| 657 |
+
frame_count = 0
|
| 658 |
+
background_im = None
|
| 659 |
+
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
|
| 660 |
+
for im_frame in ImageSequence.Iterator(imSequence):
|
| 661 |
+
# a copy is required here since seek can still mutate the image
|
| 662 |
+
im_frame = _normalize_mode(im_frame.copy())
|
| 663 |
+
if frame_count == 0:
|
| 664 |
+
for k, v in im_frame.info.items():
|
| 665 |
+
if k == "transparency":
|
| 666 |
+
continue
|
| 667 |
+
if isinstance(k, str):
|
| 668 |
+
im.encoderinfo.setdefault(k, v)
|
| 669 |
+
|
| 670 |
+
encoderinfo = im.encoderinfo.copy()
|
| 671 |
+
if "transparency" in im_frame.info:
|
| 672 |
+
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
|
| 673 |
+
im_frame = _normalize_palette(im_frame, palette, encoderinfo)
|
| 674 |
+
if isinstance(duration, (list, tuple)):
|
| 675 |
+
encoderinfo["duration"] = duration[frame_count]
|
| 676 |
+
elif duration is None and "duration" in im_frame.info:
|
| 677 |
+
encoderinfo["duration"] = im_frame.info["duration"]
|
| 678 |
+
if isinstance(disposal, (list, tuple)):
|
| 679 |
+
encoderinfo["disposal"] = disposal[frame_count]
|
| 680 |
+
frame_count += 1
|
| 681 |
+
|
| 682 |
+
diff_frame = None
|
| 683 |
+
if im_frames and previous_im:
|
| 684 |
+
# delta frame
|
| 685 |
+
delta, bbox = _getbbox(previous_im, im_frame)
|
| 686 |
+
if not bbox:
|
| 687 |
+
# This frame is identical to the previous frame
|
| 688 |
+
if encoderinfo.get("duration"):
|
| 689 |
+
im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
|
| 690 |
+
continue
|
| 691 |
+
if im_frames[-1].encoderinfo.get("disposal") == 2:
|
| 692 |
+
if background_im is None:
|
| 693 |
+
color = im.encoderinfo.get(
|
| 694 |
+
"transparency", im.info.get("transparency", (0, 0, 0))
|
| 695 |
+
)
|
| 696 |
+
background = _get_background(im_frame, color)
|
| 697 |
+
background_im = Image.new("P", im_frame.size, background)
|
| 698 |
+
first_palette = im_frames[0].im.palette
|
| 699 |
+
assert first_palette is not None
|
| 700 |
+
background_im.putpalette(first_palette, first_palette.mode)
|
| 701 |
+
bbox = _getbbox(background_im, im_frame)[1]
|
| 702 |
+
elif encoderinfo.get("optimize") and im_frame.mode != "1":
|
| 703 |
+
if "transparency" not in encoderinfo:
|
| 704 |
+
assert im_frame.palette is not None
|
| 705 |
+
try:
|
| 706 |
+
encoderinfo["transparency"] = (
|
| 707 |
+
im_frame.palette._new_color_index(im_frame)
|
| 708 |
+
)
|
| 709 |
+
except ValueError:
|
| 710 |
+
pass
|
| 711 |
+
if "transparency" in encoderinfo:
|
| 712 |
+
# When the delta is zero, fill the image with transparency
|
| 713 |
+
diff_frame = im_frame.copy()
|
| 714 |
+
fill = Image.new("P", delta.size, encoderinfo["transparency"])
|
| 715 |
+
if delta.mode == "RGBA":
|
| 716 |
+
r, g, b, a = delta.split()
|
| 717 |
+
mask = ImageMath.lambda_eval(
|
| 718 |
+
lambda args: args["convert"](
|
| 719 |
+
args["max"](
|
| 720 |
+
args["max"](
|
| 721 |
+
args["max"](args["r"], args["g"]), args["b"]
|
| 722 |
+
),
|
| 723 |
+
args["a"],
|
| 724 |
+
)
|
| 725 |
+
* 255,
|
| 726 |
+
"1",
|
| 727 |
+
),
|
| 728 |
+
r=r,
|
| 729 |
+
g=g,
|
| 730 |
+
b=b,
|
| 731 |
+
a=a,
|
| 732 |
+
)
|
| 733 |
+
else:
|
| 734 |
+
if delta.mode == "P":
|
| 735 |
+
# Convert to L without considering palette
|
| 736 |
+
delta_l = Image.new("L", delta.size)
|
| 737 |
+
delta_l.putdata(delta.getdata())
|
| 738 |
+
delta = delta_l
|
| 739 |
+
mask = ImageMath.lambda_eval(
|
| 740 |
+
lambda args: args["convert"](args["im"] * 255, "1"),
|
| 741 |
+
im=delta,
|
| 742 |
+
)
|
| 743 |
+
diff_frame.paste(fill, mask=ImageOps.invert(mask))
|
| 744 |
+
else:
|
| 745 |
+
bbox = None
|
| 746 |
+
previous_im = im_frame
|
| 747 |
+
im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
|
| 748 |
+
|
| 749 |
+
if len(im_frames) == 1:
|
| 750 |
+
if "duration" in im.encoderinfo:
|
| 751 |
+
# Since multiple frames will not be written, use the combined duration
|
| 752 |
+
im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
|
| 753 |
+
return False
|
| 754 |
+
|
| 755 |
+
for frame_data in im_frames:
|
| 756 |
+
im_frame = frame_data.im
|
| 757 |
+
if not frame_data.bbox:
|
| 758 |
+
# global header
|
| 759 |
+
for s in _get_global_header(im_frame, frame_data.encoderinfo):
|
| 760 |
+
fp.write(s)
|
| 761 |
+
offset = (0, 0)
|
| 762 |
+
else:
|
| 763 |
+
# compress difference
|
| 764 |
+
if not palette:
|
| 765 |
+
frame_data.encoderinfo["include_color_table"] = True
|
| 766 |
+
|
| 767 |
+
im_frame = im_frame.crop(frame_data.bbox)
|
| 768 |
+
offset = frame_data.bbox[:2]
|
| 769 |
+
_write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
|
| 770 |
+
return True
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 774 |
+
_save(im, fp, filename, save_all=True)
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def _save(
|
| 778 |
+
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
|
| 779 |
+
) -> None:
|
| 780 |
+
# header
|
| 781 |
+
if "palette" in im.encoderinfo or "palette" in im.info:
|
| 782 |
+
palette = im.encoderinfo.get("palette", im.info.get("palette"))
|
| 783 |
+
else:
|
| 784 |
+
palette = None
|
| 785 |
+
im.encoderinfo.setdefault("optimize", True)
|
| 786 |
+
|
| 787 |
+
if not save_all or not _write_multiple_frames(im, fp, palette):
|
| 788 |
+
_write_single_frame(im, fp, palette)
|
| 789 |
+
|
| 790 |
+
fp.write(b";") # end of file
|
| 791 |
+
|
| 792 |
+
if hasattr(fp, "flush"):
|
| 793 |
+
fp.flush()
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
def get_interlace(im: Image.Image) -> int:
|
| 797 |
+
interlace = im.encoderinfo.get("interlace", 1)
|
| 798 |
+
|
| 799 |
+
# workaround for @PIL153
|
| 800 |
+
if min(im.size) < 16:
|
| 801 |
+
interlace = 0
|
| 802 |
+
|
| 803 |
+
return interlace
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _write_local_header(
|
| 807 |
+
fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
|
| 808 |
+
) -> None:
|
| 809 |
+
try:
|
| 810 |
+
transparency = im.encoderinfo["transparency"]
|
| 811 |
+
except KeyError:
|
| 812 |
+
transparency = None
|
| 813 |
+
|
| 814 |
+
if "duration" in im.encoderinfo:
|
| 815 |
+
duration = int(im.encoderinfo["duration"] / 10)
|
| 816 |
+
else:
|
| 817 |
+
duration = 0
|
| 818 |
+
|
| 819 |
+
disposal = int(im.encoderinfo.get("disposal", 0))
|
| 820 |
+
|
| 821 |
+
if transparency is not None or duration != 0 or disposal:
|
| 822 |
+
packed_flag = 1 if transparency is not None else 0
|
| 823 |
+
packed_flag |= disposal << 2
|
| 824 |
+
|
| 825 |
+
fp.write(
|
| 826 |
+
b"!"
|
| 827 |
+
+ o8(249) # extension intro
|
| 828 |
+
+ o8(4) # length
|
| 829 |
+
+ o8(packed_flag) # packed fields
|
| 830 |
+
+ o16(duration) # duration
|
| 831 |
+
+ o8(transparency or 0) # transparency index
|
| 832 |
+
+ o8(0)
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
include_color_table = im.encoderinfo.get("include_color_table")
|
| 836 |
+
if include_color_table:
|
| 837 |
+
palette_bytes = _get_palette_bytes(im)
|
| 838 |
+
color_table_size = _get_color_table_size(palette_bytes)
|
| 839 |
+
if color_table_size:
|
| 840 |
+
flags = flags | 128 # local color table flag
|
| 841 |
+
flags = flags | color_table_size
|
| 842 |
+
|
| 843 |
+
fp.write(
|
| 844 |
+
b","
|
| 845 |
+
+ o16(offset[0]) # offset
|
| 846 |
+
+ o16(offset[1])
|
| 847 |
+
+ o16(im.size[0]) # size
|
| 848 |
+
+ o16(im.size[1])
|
| 849 |
+
+ o8(flags) # flags
|
| 850 |
+
)
|
| 851 |
+
if include_color_table and color_table_size:
|
| 852 |
+
fp.write(_get_header_palette(palette_bytes))
|
| 853 |
+
fp.write(o8(8)) # bits
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 857 |
+
# Unused by default.
|
| 858 |
+
# To use, uncomment the register_save call at the end of the file.
|
| 859 |
+
#
|
| 860 |
+
# If you need real GIF compression and/or RGB quantization, you
|
| 861 |
+
# can use the external NETPBM/PBMPLUS utilities. See comments
|
| 862 |
+
# below for information on how to enable this.
|
| 863 |
+
tempfile = im._dump()
|
| 864 |
+
|
| 865 |
+
try:
|
| 866 |
+
with open(filename, "wb") as f:
|
| 867 |
+
if im.mode != "RGB":
|
| 868 |
+
subprocess.check_call(
|
| 869 |
+
["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
|
| 870 |
+
)
|
| 871 |
+
else:
|
| 872 |
+
# Pipe ppmquant output into ppmtogif
|
| 873 |
+
# "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
|
| 874 |
+
quant_cmd = ["ppmquant", "256", tempfile]
|
| 875 |
+
togif_cmd = ["ppmtogif"]
|
| 876 |
+
quant_proc = subprocess.Popen(
|
| 877 |
+
quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
|
| 878 |
+
)
|
| 879 |
+
togif_proc = subprocess.Popen(
|
| 880 |
+
togif_cmd,
|
| 881 |
+
stdin=quant_proc.stdout,
|
| 882 |
+
stdout=f,
|
| 883 |
+
stderr=subprocess.DEVNULL,
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
|
| 887 |
+
assert quant_proc.stdout is not None
|
| 888 |
+
quant_proc.stdout.close()
|
| 889 |
+
|
| 890 |
+
retcode = quant_proc.wait()
|
| 891 |
+
if retcode:
|
| 892 |
+
raise subprocess.CalledProcessError(retcode, quant_cmd)
|
| 893 |
+
|
| 894 |
+
retcode = togif_proc.wait()
|
| 895 |
+
if retcode:
|
| 896 |
+
raise subprocess.CalledProcessError(retcode, togif_cmd)
|
| 897 |
+
finally:
|
| 898 |
+
try:
|
| 899 |
+
os.unlink(tempfile)
|
| 900 |
+
except OSError:
|
| 901 |
+
pass
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
# Force optimization so that we can test performance against
|
| 905 |
+
# cases where it took lots of memory and time previously.
|
| 906 |
+
_FORCE_OPTIMIZE = False
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
|
| 910 |
+
"""
|
| 911 |
+
Palette optimization is a potentially expensive operation.
|
| 912 |
+
|
| 913 |
+
This function determines if the palette should be optimized using
|
| 914 |
+
some heuristics, then returns the list of palette entries in use.
|
| 915 |
+
|
| 916 |
+
:param im: Image object
|
| 917 |
+
:param info: encoderinfo
|
| 918 |
+
:returns: list of indexes of palette entries in use, or None
|
| 919 |
+
"""
|
| 920 |
+
if im.mode in ("P", "L") and info and info.get("optimize"):
|
| 921 |
+
# Potentially expensive operation.
|
| 922 |
+
|
| 923 |
+
# The palette saves 3 bytes per color not used, but palette
|
| 924 |
+
# lengths are restricted to 3*(2**N) bytes. Max saving would
|
| 925 |
+
# be 768 -> 6 bytes if we went all the way down to 2 colors.
|
| 926 |
+
# * If we're over 128 colors, we can't save any space.
|
| 927 |
+
# * If there aren't any holes, it's not worth collapsing.
|
| 928 |
+
# * If we have a 'large' image, the palette is in the noise.
|
| 929 |
+
|
| 930 |
+
# create the new palette if not every color is used
|
| 931 |
+
optimise = _FORCE_OPTIMIZE or im.mode == "L"
|
| 932 |
+
if optimise or im.width * im.height < 512 * 512:
|
| 933 |
+
# check which colors are used
|
| 934 |
+
used_palette_colors = []
|
| 935 |
+
for i, count in enumerate(im.histogram()):
|
| 936 |
+
if count:
|
| 937 |
+
used_palette_colors.append(i)
|
| 938 |
+
|
| 939 |
+
if optimise or max(used_palette_colors) >= len(used_palette_colors):
|
| 940 |
+
return used_palette_colors
|
| 941 |
+
|
| 942 |
+
assert im.palette is not None
|
| 943 |
+
num_palette_colors = len(im.palette.palette) // Image.getmodebands(
|
| 944 |
+
im.palette.mode
|
| 945 |
+
)
|
| 946 |
+
current_palette_size = 1 << (num_palette_colors - 1).bit_length()
|
| 947 |
+
if (
|
| 948 |
+
# check that the palette would become smaller when saved
|
| 949 |
+
len(used_palette_colors) <= current_palette_size // 2
|
| 950 |
+
# check that the palette is not already the smallest possible size
|
| 951 |
+
and current_palette_size > 2
|
| 952 |
+
):
|
| 953 |
+
return used_palette_colors
|
| 954 |
+
return None
|
| 955 |
+
|
| 956 |
+
|
| 957 |
+
def _get_color_table_size(palette_bytes: bytes) -> int:
|
| 958 |
+
# calculate the palette size for the header
|
| 959 |
+
if not palette_bytes:
|
| 960 |
+
return 0
|
| 961 |
+
elif len(palette_bytes) < 9:
|
| 962 |
+
return 1
|
| 963 |
+
else:
|
| 964 |
+
return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
|
| 965 |
+
|
| 966 |
+
|
| 967 |
+
def _get_header_palette(palette_bytes: bytes) -> bytes:
|
| 968 |
+
"""
|
| 969 |
+
Returns the palette, null padded to the next power of 2 (*3) bytes
|
| 970 |
+
suitable for direct inclusion in the GIF header
|
| 971 |
+
|
| 972 |
+
:param palette_bytes: Unpadded palette bytes, in RGBRGB form
|
| 973 |
+
:returns: Null padded palette
|
| 974 |
+
"""
|
| 975 |
+
color_table_size = _get_color_table_size(palette_bytes)
|
| 976 |
+
|
| 977 |
+
# add the missing amount of bytes
|
| 978 |
+
# the palette has to be 2<<n in size
|
| 979 |
+
actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
|
| 980 |
+
if actual_target_size_diff > 0:
|
| 981 |
+
palette_bytes += o8(0) * 3 * actual_target_size_diff
|
| 982 |
+
return palette_bytes
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
def _get_palette_bytes(im: Image.Image) -> bytes:
|
| 986 |
+
"""
|
| 987 |
+
Gets the palette for inclusion in the gif header
|
| 988 |
+
|
| 989 |
+
:param im: Image object
|
| 990 |
+
:returns: Bytes, len<=768 suitable for inclusion in gif header
|
| 991 |
+
"""
|
| 992 |
+
if not im.palette:
|
| 993 |
+
return b""
|
| 994 |
+
|
| 995 |
+
palette = bytes(im.palette.palette)
|
| 996 |
+
if im.palette.mode == "RGBA":
|
| 997 |
+
palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
|
| 998 |
+
return palette
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
def _get_background(
|
| 1002 |
+
im: Image.Image,
|
| 1003 |
+
info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
|
| 1004 |
+
) -> int:
|
| 1005 |
+
background = 0
|
| 1006 |
+
if info_background:
|
| 1007 |
+
if isinstance(info_background, tuple):
|
| 1008 |
+
# WebPImagePlugin stores an RGBA value in info["background"]
|
| 1009 |
+
# So it must be converted to the same format as GifImagePlugin's
|
| 1010 |
+
# info["background"] - a global color table index
|
| 1011 |
+
assert im.palette is not None
|
| 1012 |
+
try:
|
| 1013 |
+
background = im.palette.getcolor(info_background, im)
|
| 1014 |
+
except ValueError as e:
|
| 1015 |
+
if str(e) not in (
|
| 1016 |
+
# If all 256 colors are in use,
|
| 1017 |
+
# then there is no need for the background color
|
| 1018 |
+
"cannot allocate more than 256 colors",
|
| 1019 |
+
# Ignore non-opaque WebP background
|
| 1020 |
+
"cannot add non-opaque RGBA color to RGB palette",
|
| 1021 |
+
):
|
| 1022 |
+
raise
|
| 1023 |
+
else:
|
| 1024 |
+
background = info_background
|
| 1025 |
+
return background
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
|
| 1029 |
+
"""Return a list of strings representing a GIF header"""
|
| 1030 |
+
|
| 1031 |
+
# Header Block
|
| 1032 |
+
# https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
| 1033 |
+
|
| 1034 |
+
version = b"87a"
|
| 1035 |
+
if im.info.get("version") == b"89a" or (
|
| 1036 |
+
info
|
| 1037 |
+
and (
|
| 1038 |
+
"transparency" in info
|
| 1039 |
+
or info.get("loop") is not None
|
| 1040 |
+
or info.get("duration")
|
| 1041 |
+
or info.get("comment")
|
| 1042 |
+
)
|
| 1043 |
+
):
|
| 1044 |
+
version = b"89a"
|
| 1045 |
+
|
| 1046 |
+
background = _get_background(im, info.get("background"))
|
| 1047 |
+
|
| 1048 |
+
palette_bytes = _get_palette_bytes(im)
|
| 1049 |
+
color_table_size = _get_color_table_size(palette_bytes)
|
| 1050 |
+
|
| 1051 |
+
header = [
|
| 1052 |
+
b"GIF" # signature
|
| 1053 |
+
+ version # version
|
| 1054 |
+
+ o16(im.size[0]) # canvas width
|
| 1055 |
+
+ o16(im.size[1]), # canvas height
|
| 1056 |
+
# Logical Screen Descriptor
|
| 1057 |
+
# size of global color table + global color table flag
|
| 1058 |
+
o8(color_table_size + 128), # packed fields
|
| 1059 |
+
# background + reserved/aspect
|
| 1060 |
+
o8(background) + o8(0),
|
| 1061 |
+
# Global Color Table
|
| 1062 |
+
_get_header_palette(palette_bytes),
|
| 1063 |
+
]
|
| 1064 |
+
if info.get("loop") is not None:
|
| 1065 |
+
header.append(
|
| 1066 |
+
b"!"
|
| 1067 |
+
+ o8(255) # extension intro
|
| 1068 |
+
+ o8(11)
|
| 1069 |
+
+ b"NETSCAPE2.0"
|
| 1070 |
+
+ o8(3)
|
| 1071 |
+
+ o8(1)
|
| 1072 |
+
+ o16(info["loop"]) # number of loops
|
| 1073 |
+
+ o8(0)
|
| 1074 |
+
)
|
| 1075 |
+
if info.get("comment"):
|
| 1076 |
+
comment_block = b"!" + o8(254) # extension intro
|
| 1077 |
+
|
| 1078 |
+
comment = info["comment"]
|
| 1079 |
+
if isinstance(comment, str):
|
| 1080 |
+
comment = comment.encode()
|
| 1081 |
+
for i in range(0, len(comment), 255):
|
| 1082 |
+
subblock = comment[i : i + 255]
|
| 1083 |
+
comment_block += o8(len(subblock)) + subblock
|
| 1084 |
+
|
| 1085 |
+
comment_block += o8(0)
|
| 1086 |
+
header.append(comment_block)
|
| 1087 |
+
return header
|
| 1088 |
+
|
| 1089 |
+
|
| 1090 |
+
def _write_frame_data(
|
| 1091 |
+
fp: IO[bytes],
|
| 1092 |
+
im_frame: Image.Image,
|
| 1093 |
+
offset: tuple[int, int],
|
| 1094 |
+
params: dict[str, Any],
|
| 1095 |
+
) -> None:
|
| 1096 |
+
try:
|
| 1097 |
+
im_frame.encoderinfo = params
|
| 1098 |
+
|
| 1099 |
+
# local image header
|
| 1100 |
+
_write_local_header(fp, im_frame, offset, 0)
|
| 1101 |
+
|
| 1102 |
+
ImageFile._save(
|
| 1103 |
+
im_frame,
|
| 1104 |
+
fp,
|
| 1105 |
+
[ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
|
| 1106 |
+
)
|
| 1107 |
+
|
| 1108 |
+
fp.write(b"\0") # end of image data
|
| 1109 |
+
finally:
|
| 1110 |
+
del im_frame.encoderinfo
|
| 1111 |
+
|
| 1112 |
+
|
| 1113 |
+
# --------------------------------------------------------------------
|
| 1114 |
+
# Legacy GIF utilities
|
| 1115 |
+
|
| 1116 |
+
|
| 1117 |
+
def getheader(
|
| 1118 |
+
im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
|
| 1119 |
+
) -> tuple[list[bytes], list[int] | None]:
|
| 1120 |
+
"""
|
| 1121 |
+
Legacy Method to get Gif data from image.
|
| 1122 |
+
|
| 1123 |
+
Warning:: May modify image data.
|
| 1124 |
+
|
| 1125 |
+
:param im: Image object
|
| 1126 |
+
:param palette: bytes object containing the source palette, or ....
|
| 1127 |
+
:param info: encoderinfo
|
| 1128 |
+
:returns: tuple of(list of header items, optimized palette)
|
| 1129 |
+
|
| 1130 |
+
"""
|
| 1131 |
+
if info is None:
|
| 1132 |
+
info = {}
|
| 1133 |
+
|
| 1134 |
+
used_palette_colors = _get_optimize(im, info)
|
| 1135 |
+
|
| 1136 |
+
if "background" not in info and "background" in im.info:
|
| 1137 |
+
info["background"] = im.info["background"]
|
| 1138 |
+
|
| 1139 |
+
im_mod = _normalize_palette(im, palette, info)
|
| 1140 |
+
im.palette = im_mod.palette
|
| 1141 |
+
im.im = im_mod.im
|
| 1142 |
+
header = _get_global_header(im, info)
|
| 1143 |
+
|
| 1144 |
+
return header, used_palette_colors
|
| 1145 |
+
|
| 1146 |
+
|
| 1147 |
+
def getdata(
|
| 1148 |
+
im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
|
| 1149 |
+
) -> list[bytes]:
|
| 1150 |
+
"""
|
| 1151 |
+
Legacy Method
|
| 1152 |
+
|
| 1153 |
+
Return a list of strings representing this image.
|
| 1154 |
+
The first string is a local image header, the rest contains
|
| 1155 |
+
encoded image data.
|
| 1156 |
+
|
| 1157 |
+
To specify duration, add the time in milliseconds,
|
| 1158 |
+
e.g. ``getdata(im_frame, duration=1000)``
|
| 1159 |
+
|
| 1160 |
+
:param im: Image object
|
| 1161 |
+
:param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
|
| 1162 |
+
:param \\**params: e.g. duration or other encoder info parameters
|
| 1163 |
+
:returns: List of bytes containing GIF encoded frame data
|
| 1164 |
+
|
| 1165 |
+
"""
|
| 1166 |
+
from io import BytesIO
|
| 1167 |
+
|
| 1168 |
+
class Collector(BytesIO):
|
| 1169 |
+
data = []
|
| 1170 |
+
|
| 1171 |
+
def write(self, data: Buffer) -> int:
|
| 1172 |
+
self.data.append(data)
|
| 1173 |
+
return len(data)
|
| 1174 |
+
|
| 1175 |
+
im.load() # make sure raster data is available
|
| 1176 |
+
|
| 1177 |
+
fp = Collector()
|
| 1178 |
+
|
| 1179 |
+
_write_frame_data(fp, im, offset, params)
|
| 1180 |
+
|
| 1181 |
+
return fp.data
|
| 1182 |
+
|
| 1183 |
+
|
| 1184 |
+
# --------------------------------------------------------------------
|
| 1185 |
+
# Registry
|
| 1186 |
+
|
| 1187 |
+
Image.register_open(GifImageFile.format, GifImageFile, _accept)
|
| 1188 |
+
Image.register_save(GifImageFile.format, _save)
|
| 1189 |
+
Image.register_save_all(GifImageFile.format, _save_all)
|
| 1190 |
+
Image.register_extension(GifImageFile.format, ".gif")
|
| 1191 |
+
Image.register_mime(GifImageFile.format, "image/gif")
|
| 1192 |
+
|
| 1193 |
+
#
|
| 1194 |
+
# Uncomment the following line if you wish to use NETPBM/PBMPLUS
|
| 1195 |
+
# instead of the built-in "uncompressed" GIF encoder
|
| 1196 |
+
|
| 1197 |
+
# Image.register_save(GifImageFile.format, _save_netpbm)
|
Lib/site-packages/PIL/GimpGradientFile.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# stuff to read (and render) GIMP gradient files
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 97-08-23 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 11 |
+
# Copyright (c) Fredrik Lundh 1997.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
Stuff to translate curve segments to palette values (derived from
|
| 18 |
+
the corresponding code in GIMP, written by Federico Mena Quintero.
|
| 19 |
+
See the GIMP distribution for more information.)
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
from math import log, pi, sin, sqrt
|
| 24 |
+
from typing import IO, Callable
|
| 25 |
+
|
| 26 |
+
from ._binary import o8
|
| 27 |
+
|
| 28 |
+
EPSILON = 1e-10
|
| 29 |
+
"""""" # Enable auto-doc for data member
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def linear(middle: float, pos: float) -> float:
|
| 33 |
+
if pos <= middle:
|
| 34 |
+
if middle < EPSILON:
|
| 35 |
+
return 0.0
|
| 36 |
+
else:
|
| 37 |
+
return 0.5 * pos / middle
|
| 38 |
+
else:
|
| 39 |
+
pos = pos - middle
|
| 40 |
+
middle = 1.0 - middle
|
| 41 |
+
if middle < EPSILON:
|
| 42 |
+
return 1.0
|
| 43 |
+
else:
|
| 44 |
+
return 0.5 + 0.5 * pos / middle
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def curved(middle: float, pos: float) -> float:
|
| 48 |
+
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def sine(middle: float, pos: float) -> float:
|
| 52 |
+
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def sphere_increasing(middle: float, pos: float) -> float:
|
| 56 |
+
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def sphere_decreasing(middle: float, pos: float) -> float:
|
| 60 |
+
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
| 64 |
+
"""""" # Enable auto-doc for data member
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class GradientFile:
|
| 68 |
+
gradient: (
|
| 69 |
+
list[
|
| 70 |
+
tuple[
|
| 71 |
+
float,
|
| 72 |
+
float,
|
| 73 |
+
float,
|
| 74 |
+
list[float],
|
| 75 |
+
list[float],
|
| 76 |
+
Callable[[float, float], float],
|
| 77 |
+
]
|
| 78 |
+
]
|
| 79 |
+
| None
|
| 80 |
+
) = None
|
| 81 |
+
|
| 82 |
+
def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
|
| 83 |
+
assert self.gradient is not None
|
| 84 |
+
palette = []
|
| 85 |
+
|
| 86 |
+
ix = 0
|
| 87 |
+
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
| 88 |
+
|
| 89 |
+
for i in range(entries):
|
| 90 |
+
x = i / (entries - 1)
|
| 91 |
+
|
| 92 |
+
while x1 < x:
|
| 93 |
+
ix += 1
|
| 94 |
+
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
| 95 |
+
|
| 96 |
+
w = x1 - x0
|
| 97 |
+
|
| 98 |
+
if w < EPSILON:
|
| 99 |
+
scale = segment(0.5, 0.5)
|
| 100 |
+
else:
|
| 101 |
+
scale = segment((xm - x0) / w, (x - x0) / w)
|
| 102 |
+
|
| 103 |
+
# expand to RGBA
|
| 104 |
+
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
|
| 105 |
+
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
|
| 106 |
+
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
|
| 107 |
+
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
|
| 108 |
+
|
| 109 |
+
# add to palette
|
| 110 |
+
palette.append(r + g + b + a)
|
| 111 |
+
|
| 112 |
+
return b"".join(palette), "RGBA"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class GimpGradientFile(GradientFile):
|
| 116 |
+
"""File handler for GIMP's gradient format."""
|
| 117 |
+
|
| 118 |
+
def __init__(self, fp: IO[bytes]) -> None:
|
| 119 |
+
if fp.readline()[:13] != b"GIMP Gradient":
|
| 120 |
+
msg = "not a GIMP gradient file"
|
| 121 |
+
raise SyntaxError(msg)
|
| 122 |
+
|
| 123 |
+
line = fp.readline()
|
| 124 |
+
|
| 125 |
+
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
|
| 126 |
+
if line.startswith(b"Name: "):
|
| 127 |
+
line = fp.readline().strip()
|
| 128 |
+
|
| 129 |
+
count = int(line)
|
| 130 |
+
|
| 131 |
+
self.gradient = []
|
| 132 |
+
|
| 133 |
+
for i in range(count):
|
| 134 |
+
s = fp.readline().split()
|
| 135 |
+
w = [float(x) for x in s[:11]]
|
| 136 |
+
|
| 137 |
+
x0, x1 = w[0], w[2]
|
| 138 |
+
xm = w[1]
|
| 139 |
+
rgb0 = w[3:7]
|
| 140 |
+
rgb1 = w[7:11]
|
| 141 |
+
|
| 142 |
+
segment = SEGMENTS[int(s[11])]
|
| 143 |
+
cspace = int(s[12])
|
| 144 |
+
|
| 145 |
+
if cspace != 0:
|
| 146 |
+
msg = "cannot handle HSV colour space"
|
| 147 |
+
raise OSError(msg)
|
| 148 |
+
|
| 149 |
+
self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
Lib/site-packages/PIL/GimpPaletteFile.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# stuff to read GIMP palette files
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1997-08-23 fl Created
|
| 9 |
+
# 2004-09-07 fl Support GIMP 2.0 palette files.
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
|
| 12 |
+
# Copyright (c) Fredrik Lundh 1997-2004.
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import re
|
| 19 |
+
from typing import IO
|
| 20 |
+
|
| 21 |
+
from ._binary import o8
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class GimpPaletteFile:
|
| 25 |
+
"""File handler for GIMP's palette format."""
|
| 26 |
+
|
| 27 |
+
rawmode = "RGB"
|
| 28 |
+
|
| 29 |
+
def __init__(self, fp: IO[bytes]) -> None:
|
| 30 |
+
palette = [o8(i) * 3 for i in range(256)]
|
| 31 |
+
|
| 32 |
+
if fp.readline()[:12] != b"GIMP Palette":
|
| 33 |
+
msg = "not a GIMP palette file"
|
| 34 |
+
raise SyntaxError(msg)
|
| 35 |
+
|
| 36 |
+
for i in range(256):
|
| 37 |
+
s = fp.readline()
|
| 38 |
+
if not s:
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
# skip fields and comment lines
|
| 42 |
+
if re.match(rb"\w+:|#", s):
|
| 43 |
+
continue
|
| 44 |
+
if len(s) > 100:
|
| 45 |
+
msg = "bad palette file"
|
| 46 |
+
raise SyntaxError(msg)
|
| 47 |
+
|
| 48 |
+
v = tuple(map(int, s.split()[:3]))
|
| 49 |
+
if len(v) != 3:
|
| 50 |
+
msg = "bad palette entry"
|
| 51 |
+
raise ValueError(msg)
|
| 52 |
+
|
| 53 |
+
palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
| 54 |
+
|
| 55 |
+
self.palette = b"".join(palette)
|
| 56 |
+
|
| 57 |
+
def getpalette(self) -> tuple[bytes, str]:
|
| 58 |
+
return self.palette, self.rawmode
|
Lib/site-packages/PIL/GribStubImagePlugin.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# GRIB stub adapter
|
| 6 |
+
#
|
| 7 |
+
# Copyright (c) 1996-2003 by Fredrik Lundh
|
| 8 |
+
#
|
| 9 |
+
# See the README file for information on usage and redistribution.
|
| 10 |
+
#
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import IO
|
| 14 |
+
|
| 15 |
+
from . import Image, ImageFile
|
| 16 |
+
|
| 17 |
+
_handler = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
| 21 |
+
"""
|
| 22 |
+
Install application-specific GRIB image handler.
|
| 23 |
+
|
| 24 |
+
:param handler: Handler object.
|
| 25 |
+
"""
|
| 26 |
+
global _handler
|
| 27 |
+
_handler = handler
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# --------------------------------------------------------------------
|
| 31 |
+
# Image adapter
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _accept(prefix: bytes) -> bool:
|
| 35 |
+
return prefix[:4] == b"GRIB" and prefix[7] == 1
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class GribStubImageFile(ImageFile.StubImageFile):
|
| 39 |
+
format = "GRIB"
|
| 40 |
+
format_description = "GRIB"
|
| 41 |
+
|
| 42 |
+
def _open(self) -> None:
|
| 43 |
+
offset = self.fp.tell()
|
| 44 |
+
|
| 45 |
+
if not _accept(self.fp.read(8)):
|
| 46 |
+
msg = "Not a GRIB file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
self.fp.seek(offset)
|
| 50 |
+
|
| 51 |
+
# make something up
|
| 52 |
+
self._mode = "F"
|
| 53 |
+
self._size = 1, 1
|
| 54 |
+
|
| 55 |
+
loader = self._load()
|
| 56 |
+
if loader:
|
| 57 |
+
loader.open(self)
|
| 58 |
+
|
| 59 |
+
def _load(self) -> ImageFile.StubHandler | None:
|
| 60 |
+
return _handler
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 64 |
+
if _handler is None or not hasattr(_handler, "save"):
|
| 65 |
+
msg = "GRIB save handler not installed"
|
| 66 |
+
raise OSError(msg)
|
| 67 |
+
_handler.save(im, fp, filename)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# --------------------------------------------------------------------
|
| 71 |
+
# Registry
|
| 72 |
+
|
| 73 |
+
Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
|
| 74 |
+
Image.register_save(GribStubImageFile.format, _save)
|
| 75 |
+
|
| 76 |
+
Image.register_extension(GribStubImageFile.format, ".grib")
|
Lib/site-packages/PIL/Hdf5StubImagePlugin.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# HDF5 stub adapter
|
| 6 |
+
#
|
| 7 |
+
# Copyright (c) 2000-2003 by Fredrik Lundh
|
| 8 |
+
#
|
| 9 |
+
# See the README file for information on usage and redistribution.
|
| 10 |
+
#
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import IO
|
| 14 |
+
|
| 15 |
+
from . import Image, ImageFile
|
| 16 |
+
|
| 17 |
+
_handler = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
| 21 |
+
"""
|
| 22 |
+
Install application-specific HDF5 image handler.
|
| 23 |
+
|
| 24 |
+
:param handler: Handler object.
|
| 25 |
+
"""
|
| 26 |
+
global _handler
|
| 27 |
+
_handler = handler
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# --------------------------------------------------------------------
|
| 31 |
+
# Image adapter
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _accept(prefix: bytes) -> bool:
|
| 35 |
+
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class HDF5StubImageFile(ImageFile.StubImageFile):
|
| 39 |
+
format = "HDF5"
|
| 40 |
+
format_description = "HDF5"
|
| 41 |
+
|
| 42 |
+
def _open(self) -> None:
|
| 43 |
+
offset = self.fp.tell()
|
| 44 |
+
|
| 45 |
+
if not _accept(self.fp.read(8)):
|
| 46 |
+
msg = "Not an HDF file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
self.fp.seek(offset)
|
| 50 |
+
|
| 51 |
+
# make something up
|
| 52 |
+
self._mode = "F"
|
| 53 |
+
self._size = 1, 1
|
| 54 |
+
|
| 55 |
+
loader = self._load()
|
| 56 |
+
if loader:
|
| 57 |
+
loader.open(self)
|
| 58 |
+
|
| 59 |
+
def _load(self) -> ImageFile.StubHandler | None:
|
| 60 |
+
return _handler
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 64 |
+
if _handler is None or not hasattr(_handler, "save"):
|
| 65 |
+
msg = "HDF5 save handler not installed"
|
| 66 |
+
raise OSError(msg)
|
| 67 |
+
_handler.save(im, fp, filename)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# --------------------------------------------------------------------
|
| 71 |
+
# Registry
|
| 72 |
+
|
| 73 |
+
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
|
| 74 |
+
Image.register_save(HDF5StubImageFile.format, _save)
|
| 75 |
+
|
| 76 |
+
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])
|
Lib/site-packages/PIL/IcnsImagePlugin.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# macOS icns file decoder, based on icns.py by Bob Ippolito.
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
|
| 9 |
+
# 2020-04-04 Allow saving on all operating systems.
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) 2004 by Bob Ippolito.
|
| 12 |
+
# Copyright (c) 2004 by Secret Labs.
|
| 13 |
+
# Copyright (c) 2004 by Fredrik Lundh.
|
| 14 |
+
# Copyright (c) 2014 by Alastair Houghton.
|
| 15 |
+
# Copyright (c) 2020 by Pan Jing.
|
| 16 |
+
#
|
| 17 |
+
# See the README file for information on usage and redistribution.
|
| 18 |
+
#
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import io
|
| 22 |
+
import os
|
| 23 |
+
import struct
|
| 24 |
+
import sys
|
| 25 |
+
from typing import IO
|
| 26 |
+
|
| 27 |
+
from . import Image, ImageFile, PngImagePlugin, features
|
| 28 |
+
from ._deprecate import deprecate
|
| 29 |
+
|
| 30 |
+
enable_jpeg2k = features.check_codec("jpg_2000")
|
| 31 |
+
if enable_jpeg2k:
|
| 32 |
+
from . import Jpeg2KImagePlugin
|
| 33 |
+
|
| 34 |
+
MAGIC = b"icns"
|
| 35 |
+
HEADERSIZE = 8
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
|
| 39 |
+
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def read_32t(
|
| 43 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 44 |
+
) -> dict[str, Image.Image]:
|
| 45 |
+
# The 128x128 icon seems to have an extra header for some reason.
|
| 46 |
+
(start, length) = start_length
|
| 47 |
+
fobj.seek(start)
|
| 48 |
+
sig = fobj.read(4)
|
| 49 |
+
if sig != b"\x00\x00\x00\x00":
|
| 50 |
+
msg = "Unknown signature, expecting 0x00000000"
|
| 51 |
+
raise SyntaxError(msg)
|
| 52 |
+
return read_32(fobj, (start + 4, length - 4), size)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def read_32(
|
| 56 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 57 |
+
) -> dict[str, Image.Image]:
|
| 58 |
+
"""
|
| 59 |
+
Read a 32bit RGB icon resource. Seems to be either uncompressed or
|
| 60 |
+
an RLE packbits-like scheme.
|
| 61 |
+
"""
|
| 62 |
+
(start, length) = start_length
|
| 63 |
+
fobj.seek(start)
|
| 64 |
+
pixel_size = (size[0] * size[2], size[1] * size[2])
|
| 65 |
+
sizesq = pixel_size[0] * pixel_size[1]
|
| 66 |
+
if length == sizesq * 3:
|
| 67 |
+
# uncompressed ("RGBRGBGB")
|
| 68 |
+
indata = fobj.read(length)
|
| 69 |
+
im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
|
| 70 |
+
else:
|
| 71 |
+
# decode image
|
| 72 |
+
im = Image.new("RGB", pixel_size, None)
|
| 73 |
+
for band_ix in range(3):
|
| 74 |
+
data = []
|
| 75 |
+
bytesleft = sizesq
|
| 76 |
+
while bytesleft > 0:
|
| 77 |
+
byte = fobj.read(1)
|
| 78 |
+
if not byte:
|
| 79 |
+
break
|
| 80 |
+
byte_int = byte[0]
|
| 81 |
+
if byte_int & 0x80:
|
| 82 |
+
blocksize = byte_int - 125
|
| 83 |
+
byte = fobj.read(1)
|
| 84 |
+
for i in range(blocksize):
|
| 85 |
+
data.append(byte)
|
| 86 |
+
else:
|
| 87 |
+
blocksize = byte_int + 1
|
| 88 |
+
data.append(fobj.read(blocksize))
|
| 89 |
+
bytesleft -= blocksize
|
| 90 |
+
if bytesleft <= 0:
|
| 91 |
+
break
|
| 92 |
+
if bytesleft != 0:
|
| 93 |
+
msg = f"Error reading channel [{repr(bytesleft)} left]"
|
| 94 |
+
raise SyntaxError(msg)
|
| 95 |
+
band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
|
| 96 |
+
im.im.putband(band.im, band_ix)
|
| 97 |
+
return {"RGB": im}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def read_mk(
|
| 101 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 102 |
+
) -> dict[str, Image.Image]:
|
| 103 |
+
# Alpha masks seem to be uncompressed
|
| 104 |
+
start = start_length[0]
|
| 105 |
+
fobj.seek(start)
|
| 106 |
+
pixel_size = (size[0] * size[2], size[1] * size[2])
|
| 107 |
+
sizesq = pixel_size[0] * pixel_size[1]
|
| 108 |
+
band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
|
| 109 |
+
return {"A": band}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def read_png_or_jpeg2000(
|
| 113 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 114 |
+
) -> dict[str, Image.Image]:
|
| 115 |
+
(start, length) = start_length
|
| 116 |
+
fobj.seek(start)
|
| 117 |
+
sig = fobj.read(12)
|
| 118 |
+
|
| 119 |
+
im: Image.Image
|
| 120 |
+
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
|
| 121 |
+
fobj.seek(start)
|
| 122 |
+
im = PngImagePlugin.PngImageFile(fobj)
|
| 123 |
+
Image._decompression_bomb_check(im.size)
|
| 124 |
+
return {"RGBA": im}
|
| 125 |
+
elif (
|
| 126 |
+
sig[:4] == b"\xff\x4f\xff\x51"
|
| 127 |
+
or sig[:4] == b"\x0d\x0a\x87\x0a"
|
| 128 |
+
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
| 129 |
+
):
|
| 130 |
+
if not enable_jpeg2k:
|
| 131 |
+
msg = (
|
| 132 |
+
"Unsupported icon subimage format (rebuild PIL "
|
| 133 |
+
"with JPEG 2000 support to fix this)"
|
| 134 |
+
)
|
| 135 |
+
raise ValueError(msg)
|
| 136 |
+
# j2k, jpc or j2c
|
| 137 |
+
fobj.seek(start)
|
| 138 |
+
jp2kstream = fobj.read(length)
|
| 139 |
+
f = io.BytesIO(jp2kstream)
|
| 140 |
+
im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
|
| 141 |
+
Image._decompression_bomb_check(im.size)
|
| 142 |
+
if im.mode != "RGBA":
|
| 143 |
+
im = im.convert("RGBA")
|
| 144 |
+
return {"RGBA": im}
|
| 145 |
+
else:
|
| 146 |
+
msg = "Unsupported icon subimage format"
|
| 147 |
+
raise ValueError(msg)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class IcnsFile:
|
| 151 |
+
SIZES = {
|
| 152 |
+
(512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
|
| 153 |
+
(512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
|
| 154 |
+
(256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
|
| 155 |
+
(256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
|
| 156 |
+
(128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
|
| 157 |
+
(128, 128, 1): [
|
| 158 |
+
(b"ic07", read_png_or_jpeg2000),
|
| 159 |
+
(b"it32", read_32t),
|
| 160 |
+
(b"t8mk", read_mk),
|
| 161 |
+
],
|
| 162 |
+
(64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
|
| 163 |
+
(32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
|
| 164 |
+
(48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
|
| 165 |
+
(32, 32, 1): [
|
| 166 |
+
(b"icp5", read_png_or_jpeg2000),
|
| 167 |
+
(b"il32", read_32),
|
| 168 |
+
(b"l8mk", read_mk),
|
| 169 |
+
],
|
| 170 |
+
(16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
|
| 171 |
+
(16, 16, 1): [
|
| 172 |
+
(b"icp4", read_png_or_jpeg2000),
|
| 173 |
+
(b"is32", read_32),
|
| 174 |
+
(b"s8mk", read_mk),
|
| 175 |
+
],
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
def __init__(self, fobj: IO[bytes]) -> None:
|
| 179 |
+
"""
|
| 180 |
+
fobj is a file-like object as an icns resource
|
| 181 |
+
"""
|
| 182 |
+
# signature : (start, length)
|
| 183 |
+
self.dct = {}
|
| 184 |
+
self.fobj = fobj
|
| 185 |
+
sig, filesize = nextheader(fobj)
|
| 186 |
+
if not _accept(sig):
|
| 187 |
+
msg = "not an icns file"
|
| 188 |
+
raise SyntaxError(msg)
|
| 189 |
+
i = HEADERSIZE
|
| 190 |
+
while i < filesize:
|
| 191 |
+
sig, blocksize = nextheader(fobj)
|
| 192 |
+
if blocksize <= 0:
|
| 193 |
+
msg = "invalid block header"
|
| 194 |
+
raise SyntaxError(msg)
|
| 195 |
+
i += HEADERSIZE
|
| 196 |
+
blocksize -= HEADERSIZE
|
| 197 |
+
self.dct[sig] = (i, blocksize)
|
| 198 |
+
fobj.seek(blocksize, io.SEEK_CUR)
|
| 199 |
+
i += blocksize
|
| 200 |
+
|
| 201 |
+
def itersizes(self) -> list[tuple[int, int, int]]:
|
| 202 |
+
sizes = []
|
| 203 |
+
for size, fmts in self.SIZES.items():
|
| 204 |
+
for fmt, reader in fmts:
|
| 205 |
+
if fmt in self.dct:
|
| 206 |
+
sizes.append(size)
|
| 207 |
+
break
|
| 208 |
+
return sizes
|
| 209 |
+
|
| 210 |
+
def bestsize(self) -> tuple[int, int, int]:
|
| 211 |
+
sizes = self.itersizes()
|
| 212 |
+
if not sizes:
|
| 213 |
+
msg = "No 32bit icon resources found"
|
| 214 |
+
raise SyntaxError(msg)
|
| 215 |
+
return max(sizes)
|
| 216 |
+
|
| 217 |
+
def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
|
| 218 |
+
"""
|
| 219 |
+
Get an icon resource as {channel: array}. Note that
|
| 220 |
+
the arrays are bottom-up like windows bitmaps and will likely
|
| 221 |
+
need to be flipped or transposed in some way.
|
| 222 |
+
"""
|
| 223 |
+
dct = {}
|
| 224 |
+
for code, reader in self.SIZES[size]:
|
| 225 |
+
desc = self.dct.get(code)
|
| 226 |
+
if desc is not None:
|
| 227 |
+
dct.update(reader(self.fobj, desc, size))
|
| 228 |
+
return dct
|
| 229 |
+
|
| 230 |
+
def getimage(
|
| 231 |
+
self, size: tuple[int, int] | tuple[int, int, int] | None = None
|
| 232 |
+
) -> Image.Image:
|
| 233 |
+
if size is None:
|
| 234 |
+
size = self.bestsize()
|
| 235 |
+
elif len(size) == 2:
|
| 236 |
+
size = (size[0], size[1], 1)
|
| 237 |
+
channels = self.dataforsize(size)
|
| 238 |
+
|
| 239 |
+
im = channels.get("RGBA")
|
| 240 |
+
if im:
|
| 241 |
+
return im
|
| 242 |
+
|
| 243 |
+
im = channels["RGB"].copy()
|
| 244 |
+
try:
|
| 245 |
+
im.putalpha(channels["A"])
|
| 246 |
+
except KeyError:
|
| 247 |
+
pass
|
| 248 |
+
return im
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
##
|
| 252 |
+
# Image plugin for Mac OS icons.
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class IcnsImageFile(ImageFile.ImageFile):
|
| 256 |
+
"""
|
| 257 |
+
PIL image support for Mac OS .icns files.
|
| 258 |
+
Chooses the best resolution, but will possibly load
|
| 259 |
+
a different size image if you mutate the size attribute
|
| 260 |
+
before calling 'load'.
|
| 261 |
+
|
| 262 |
+
The info dictionary has a key 'sizes' that is a list
|
| 263 |
+
of sizes that the icns file has.
|
| 264 |
+
"""
|
| 265 |
+
|
| 266 |
+
format = "ICNS"
|
| 267 |
+
format_description = "Mac OS icns resource"
|
| 268 |
+
|
| 269 |
+
def _open(self) -> None:
|
| 270 |
+
self.icns = IcnsFile(self.fp)
|
| 271 |
+
self._mode = "RGBA"
|
| 272 |
+
self.info["sizes"] = self.icns.itersizes()
|
| 273 |
+
self.best_size = self.icns.bestsize()
|
| 274 |
+
self.size = (
|
| 275 |
+
self.best_size[0] * self.best_size[2],
|
| 276 |
+
self.best_size[1] * self.best_size[2],
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
@property # type: ignore[override]
|
| 280 |
+
def size(self) -> tuple[int, int] | tuple[int, int, int]:
|
| 281 |
+
return self._size
|
| 282 |
+
|
| 283 |
+
@size.setter
|
| 284 |
+
def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None:
|
| 285 |
+
if len(value) == 3:
|
| 286 |
+
deprecate("Setting size to (width, height, scale)", 12, "load(scale)")
|
| 287 |
+
if value in self.info["sizes"]:
|
| 288 |
+
self._size = value # type: ignore[assignment]
|
| 289 |
+
return
|
| 290 |
+
else:
|
| 291 |
+
# Check that a matching size exists,
|
| 292 |
+
# or that there is a scale that would create a size that matches
|
| 293 |
+
for size in self.info["sizes"]:
|
| 294 |
+
simple_size = size[0] * size[2], size[1] * size[2]
|
| 295 |
+
scale = simple_size[0] // value[0]
|
| 296 |
+
if simple_size[1] / value[1] == scale:
|
| 297 |
+
self._size = value
|
| 298 |
+
return
|
| 299 |
+
msg = "This is not one of the allowed sizes of this image"
|
| 300 |
+
raise ValueError(msg)
|
| 301 |
+
|
| 302 |
+
def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
|
| 303 |
+
if scale is not None or len(self.size) == 3:
|
| 304 |
+
if scale is None and len(self.size) == 3:
|
| 305 |
+
scale = self.size[2]
|
| 306 |
+
assert scale is not None
|
| 307 |
+
width, height = self.size[:2]
|
| 308 |
+
self.size = width * scale, height * scale
|
| 309 |
+
self.best_size = width, height, scale
|
| 310 |
+
|
| 311 |
+
px = Image.Image.load(self)
|
| 312 |
+
if self._im is not None and self.im.size == self.size:
|
| 313 |
+
# Already loaded
|
| 314 |
+
return px
|
| 315 |
+
self.load_prepare()
|
| 316 |
+
# This is likely NOT the best way to do it, but whatever.
|
| 317 |
+
im = self.icns.getimage(self.best_size)
|
| 318 |
+
|
| 319 |
+
# If this is a PNG or JPEG 2000, it won't be loaded yet
|
| 320 |
+
px = im.load()
|
| 321 |
+
|
| 322 |
+
self.im = im.im
|
| 323 |
+
self._mode = im.mode
|
| 324 |
+
self.size = im.size
|
| 325 |
+
|
| 326 |
+
return px
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 330 |
+
"""
|
| 331 |
+
Saves the image as a series of PNG files,
|
| 332 |
+
that are then combined into a .icns file.
|
| 333 |
+
"""
|
| 334 |
+
if hasattr(fp, "flush"):
|
| 335 |
+
fp.flush()
|
| 336 |
+
|
| 337 |
+
sizes = {
|
| 338 |
+
b"ic07": 128,
|
| 339 |
+
b"ic08": 256,
|
| 340 |
+
b"ic09": 512,
|
| 341 |
+
b"ic10": 1024,
|
| 342 |
+
b"ic11": 32,
|
| 343 |
+
b"ic12": 64,
|
| 344 |
+
b"ic13": 256,
|
| 345 |
+
b"ic14": 512,
|
| 346 |
+
}
|
| 347 |
+
provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
|
| 348 |
+
size_streams = {}
|
| 349 |
+
for size in set(sizes.values()):
|
| 350 |
+
image = (
|
| 351 |
+
provided_images[size]
|
| 352 |
+
if size in provided_images
|
| 353 |
+
else im.resize((size, size))
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
temp = io.BytesIO()
|
| 357 |
+
image.save(temp, "png")
|
| 358 |
+
size_streams[size] = temp.getvalue()
|
| 359 |
+
|
| 360 |
+
entries = []
|
| 361 |
+
for type, size in sizes.items():
|
| 362 |
+
stream = size_streams[size]
|
| 363 |
+
entries.append((type, HEADERSIZE + len(stream), stream))
|
| 364 |
+
|
| 365 |
+
# Header
|
| 366 |
+
fp.write(MAGIC)
|
| 367 |
+
file_length = HEADERSIZE # Header
|
| 368 |
+
file_length += HEADERSIZE + 8 * len(entries) # TOC
|
| 369 |
+
file_length += sum(entry[1] for entry in entries)
|
| 370 |
+
fp.write(struct.pack(">i", file_length))
|
| 371 |
+
|
| 372 |
+
# TOC
|
| 373 |
+
fp.write(b"TOC ")
|
| 374 |
+
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
|
| 375 |
+
for entry in entries:
|
| 376 |
+
fp.write(entry[0])
|
| 377 |
+
fp.write(struct.pack(">i", entry[1]))
|
| 378 |
+
|
| 379 |
+
# Data
|
| 380 |
+
for entry in entries:
|
| 381 |
+
fp.write(entry[0])
|
| 382 |
+
fp.write(struct.pack(">i", entry[1]))
|
| 383 |
+
fp.write(entry[2])
|
| 384 |
+
|
| 385 |
+
if hasattr(fp, "flush"):
|
| 386 |
+
fp.flush()
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def _accept(prefix: bytes) -> bool:
|
| 390 |
+
return prefix[:4] == MAGIC
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
|
| 394 |
+
Image.register_extension(IcnsImageFile.format, ".icns")
|
| 395 |
+
|
| 396 |
+
Image.register_save(IcnsImageFile.format, _save)
|
| 397 |
+
Image.register_mime(IcnsImageFile.format, "image/icns")
|
| 398 |
+
|
| 399 |
+
if __name__ == "__main__":
|
| 400 |
+
if len(sys.argv) < 2:
|
| 401 |
+
print("Syntax: python3 IcnsImagePlugin.py [file]")
|
| 402 |
+
sys.exit()
|
| 403 |
+
|
| 404 |
+
with open(sys.argv[1], "rb") as fp:
|
| 405 |
+
imf = IcnsImageFile(fp)
|
| 406 |
+
for size in imf.info["sizes"]:
|
| 407 |
+
width, height, scale = imf.size = size
|
| 408 |
+
imf.save(f"out-{width}-{height}-{scale}.png")
|
| 409 |
+
with Image.open(sys.argv[1]) as im:
|
| 410 |
+
im.save("out.png")
|
| 411 |
+
if sys.platform == "windows":
|
| 412 |
+
os.startfile("out.png")
|
Lib/site-packages/PIL/IcoImagePlugin.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# Windows Icon support for PIL
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 96-05-27 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 11 |
+
# Copyright (c) Fredrik Lundh 1996.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
|
| 16 |
+
# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
| 17 |
+
# <casadebender@gmail.com>.
|
| 18 |
+
# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
| 19 |
+
#
|
| 20 |
+
# Icon format references:
|
| 21 |
+
# * https://en.wikipedia.org/wiki/ICO_(file_format)
|
| 22 |
+
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import warnings
|
| 26 |
+
from io import BytesIO
|
| 27 |
+
from math import ceil, log
|
| 28 |
+
from typing import IO, NamedTuple
|
| 29 |
+
|
| 30 |
+
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
| 31 |
+
from ._binary import i16le as i16
|
| 32 |
+
from ._binary import i32le as i32
|
| 33 |
+
from ._binary import o8
|
| 34 |
+
from ._binary import o16le as o16
|
| 35 |
+
from ._binary import o32le as o32
|
| 36 |
+
|
| 37 |
+
#
|
| 38 |
+
# --------------------------------------------------------------------
|
| 39 |
+
|
| 40 |
+
_MAGIC = b"\0\0\1\0"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 44 |
+
fp.write(_MAGIC) # (2+2)
|
| 45 |
+
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
| 46 |
+
sizes = im.encoderinfo.get(
|
| 47 |
+
"sizes",
|
| 48 |
+
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
| 49 |
+
)
|
| 50 |
+
frames = []
|
| 51 |
+
provided_ims = [im] + im.encoderinfo.get("append_images", [])
|
| 52 |
+
width, height = im.size
|
| 53 |
+
for size in sorted(set(sizes)):
|
| 54 |
+
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
for provided_im in provided_ims:
|
| 58 |
+
if provided_im.size != size:
|
| 59 |
+
continue
|
| 60 |
+
frames.append(provided_im)
|
| 61 |
+
if bmp:
|
| 62 |
+
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
|
| 63 |
+
bits_used = [bits]
|
| 64 |
+
for other_im in provided_ims:
|
| 65 |
+
if other_im.size != size:
|
| 66 |
+
continue
|
| 67 |
+
bits = BmpImagePlugin.SAVE[other_im.mode][1]
|
| 68 |
+
if bits not in bits_used:
|
| 69 |
+
# Another image has been supplied for this size
|
| 70 |
+
# with a different bit depth
|
| 71 |
+
frames.append(other_im)
|
| 72 |
+
bits_used.append(bits)
|
| 73 |
+
break
|
| 74 |
+
else:
|
| 75 |
+
# TODO: invent a more convenient method for proportional scalings
|
| 76 |
+
frame = provided_im.copy()
|
| 77 |
+
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
| 78 |
+
frames.append(frame)
|
| 79 |
+
fp.write(o16(len(frames))) # idCount(2)
|
| 80 |
+
offset = fp.tell() + len(frames) * 16
|
| 81 |
+
for frame in frames:
|
| 82 |
+
width, height = frame.size
|
| 83 |
+
# 0 means 256
|
| 84 |
+
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
|
| 85 |
+
fp.write(o8(height if height < 256 else 0)) # bHeight(1)
|
| 86 |
+
|
| 87 |
+
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
|
| 88 |
+
fp.write(o8(colors)) # bColorCount(1)
|
| 89 |
+
fp.write(b"\0") # bReserved(1)
|
| 90 |
+
fp.write(b"\0\0") # wPlanes(2)
|
| 91 |
+
fp.write(o16(bits)) # wBitCount(2)
|
| 92 |
+
|
| 93 |
+
image_io = BytesIO()
|
| 94 |
+
if bmp:
|
| 95 |
+
frame.save(image_io, "dib")
|
| 96 |
+
|
| 97 |
+
if bits != 32:
|
| 98 |
+
and_mask = Image.new("1", size)
|
| 99 |
+
ImageFile._save(
|
| 100 |
+
and_mask,
|
| 101 |
+
image_io,
|
| 102 |
+
[ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))],
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
frame.save(image_io, "png")
|
| 106 |
+
image_io.seek(0)
|
| 107 |
+
image_bytes = image_io.read()
|
| 108 |
+
if bmp:
|
| 109 |
+
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
| 110 |
+
bytes_len = len(image_bytes)
|
| 111 |
+
fp.write(o32(bytes_len)) # dwBytesInRes(4)
|
| 112 |
+
fp.write(o32(offset)) # dwImageOffset(4)
|
| 113 |
+
current = fp.tell()
|
| 114 |
+
fp.seek(offset)
|
| 115 |
+
fp.write(image_bytes)
|
| 116 |
+
offset = offset + bytes_len
|
| 117 |
+
fp.seek(current)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _accept(prefix: bytes) -> bool:
|
| 121 |
+
return prefix[:4] == _MAGIC
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class IconHeader(NamedTuple):
|
| 125 |
+
width: int
|
| 126 |
+
height: int
|
| 127 |
+
nb_color: int
|
| 128 |
+
reserved: int
|
| 129 |
+
planes: int
|
| 130 |
+
bpp: int
|
| 131 |
+
size: int
|
| 132 |
+
offset: int
|
| 133 |
+
dim: tuple[int, int]
|
| 134 |
+
square: int
|
| 135 |
+
color_depth: int
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class IcoFile:
|
| 139 |
+
def __init__(self, buf: IO[bytes]) -> None:
|
| 140 |
+
"""
|
| 141 |
+
Parse image from file-like object containing ico file data
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
# check magic
|
| 145 |
+
s = buf.read(6)
|
| 146 |
+
if not _accept(s):
|
| 147 |
+
msg = "not an ICO file"
|
| 148 |
+
raise SyntaxError(msg)
|
| 149 |
+
|
| 150 |
+
self.buf = buf
|
| 151 |
+
self.entry = []
|
| 152 |
+
|
| 153 |
+
# Number of items in file
|
| 154 |
+
self.nb_items = i16(s, 4)
|
| 155 |
+
|
| 156 |
+
# Get headers for each item
|
| 157 |
+
for i in range(self.nb_items):
|
| 158 |
+
s = buf.read(16)
|
| 159 |
+
|
| 160 |
+
# See Wikipedia
|
| 161 |
+
width = s[0] or 256
|
| 162 |
+
height = s[1] or 256
|
| 163 |
+
|
| 164 |
+
# No. of colors in image (0 if >=8bpp)
|
| 165 |
+
nb_color = s[2]
|
| 166 |
+
bpp = i16(s, 6)
|
| 167 |
+
icon_header = IconHeader(
|
| 168 |
+
width=width,
|
| 169 |
+
height=height,
|
| 170 |
+
nb_color=nb_color,
|
| 171 |
+
reserved=s[3],
|
| 172 |
+
planes=i16(s, 4),
|
| 173 |
+
bpp=i16(s, 6),
|
| 174 |
+
size=i32(s, 8),
|
| 175 |
+
offset=i32(s, 12),
|
| 176 |
+
dim=(width, height),
|
| 177 |
+
square=width * height,
|
| 178 |
+
# See Wikipedia notes about color depth.
|
| 179 |
+
# We need this just to differ images with equal sizes
|
| 180 |
+
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
self.entry.append(icon_header)
|
| 184 |
+
|
| 185 |
+
self.entry = sorted(self.entry, key=lambda x: x.color_depth)
|
| 186 |
+
# ICO images are usually squares
|
| 187 |
+
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
|
| 188 |
+
|
| 189 |
+
def sizes(self) -> set[tuple[int, int]]:
|
| 190 |
+
"""
|
| 191 |
+
Get a set of all available icon sizes and color depths.
|
| 192 |
+
"""
|
| 193 |
+
return {(h.width, h.height) for h in self.entry}
|
| 194 |
+
|
| 195 |
+
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
|
| 196 |
+
for i, h in enumerate(self.entry):
|
| 197 |
+
if size == h.dim and (bpp is False or bpp == h.color_depth):
|
| 198 |
+
return i
|
| 199 |
+
return 0
|
| 200 |
+
|
| 201 |
+
def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image:
|
| 202 |
+
"""
|
| 203 |
+
Get an image from the icon
|
| 204 |
+
"""
|
| 205 |
+
return self.frame(self.getentryindex(size, bpp))
|
| 206 |
+
|
| 207 |
+
def frame(self, idx: int) -> Image.Image:
|
| 208 |
+
"""
|
| 209 |
+
Get an image from frame idx
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
header = self.entry[idx]
|
| 213 |
+
|
| 214 |
+
self.buf.seek(header.offset)
|
| 215 |
+
data = self.buf.read(8)
|
| 216 |
+
self.buf.seek(header.offset)
|
| 217 |
+
|
| 218 |
+
im: Image.Image
|
| 219 |
+
if data[:8] == PngImagePlugin._MAGIC:
|
| 220 |
+
# png frame
|
| 221 |
+
im = PngImagePlugin.PngImageFile(self.buf)
|
| 222 |
+
Image._decompression_bomb_check(im.size)
|
| 223 |
+
else:
|
| 224 |
+
# XOR + AND mask bmp frame
|
| 225 |
+
im = BmpImagePlugin.DibImageFile(self.buf)
|
| 226 |
+
Image._decompression_bomb_check(im.size)
|
| 227 |
+
|
| 228 |
+
# change tile dimension to only encompass XOR image
|
| 229 |
+
im._size = (im.size[0], int(im.size[1] / 2))
|
| 230 |
+
d, e, o, a = im.tile[0]
|
| 231 |
+
im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
|
| 232 |
+
|
| 233 |
+
# figure out where AND mask image starts
|
| 234 |
+
if header.bpp == 32:
|
| 235 |
+
# 32-bit color depth icon image allows semitransparent areas
|
| 236 |
+
# PIL's DIB format ignores transparency bits, recover them.
|
| 237 |
+
# The DIB is packed in BGRX byte order where X is the alpha
|
| 238 |
+
# channel.
|
| 239 |
+
|
| 240 |
+
# Back up to start of bmp data
|
| 241 |
+
self.buf.seek(o)
|
| 242 |
+
# extract every 4th byte (eg. 3,7,11,15,...)
|
| 243 |
+
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
|
| 244 |
+
|
| 245 |
+
# convert to an 8bpp grayscale image
|
| 246 |
+
try:
|
| 247 |
+
mask = Image.frombuffer(
|
| 248 |
+
"L", # 8bpp
|
| 249 |
+
im.size, # (w, h)
|
| 250 |
+
alpha_bytes, # source chars
|
| 251 |
+
"raw", # raw decoder
|
| 252 |
+
("L", 0, -1), # 8bpp inverted, unpadded, reversed
|
| 253 |
+
)
|
| 254 |
+
except ValueError:
|
| 255 |
+
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| 256 |
+
mask = None
|
| 257 |
+
else:
|
| 258 |
+
raise
|
| 259 |
+
else:
|
| 260 |
+
# get AND image from end of bitmap
|
| 261 |
+
w = im.size[0]
|
| 262 |
+
if (w % 32) > 0:
|
| 263 |
+
# bitmap row data is aligned to word boundaries
|
| 264 |
+
w += 32 - (im.size[0] % 32)
|
| 265 |
+
|
| 266 |
+
# the total mask data is
|
| 267 |
+
# padded row size * height / bits per char
|
| 268 |
+
|
| 269 |
+
total_bytes = int((w * im.size[1]) / 8)
|
| 270 |
+
and_mask_offset = header.offset + header.size - total_bytes
|
| 271 |
+
|
| 272 |
+
self.buf.seek(and_mask_offset)
|
| 273 |
+
mask_data = self.buf.read(total_bytes)
|
| 274 |
+
|
| 275 |
+
# convert raw data to image
|
| 276 |
+
try:
|
| 277 |
+
mask = Image.frombuffer(
|
| 278 |
+
"1", # 1 bpp
|
| 279 |
+
im.size, # (w, h)
|
| 280 |
+
mask_data, # source chars
|
| 281 |
+
"raw", # raw decoder
|
| 282 |
+
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
|
| 283 |
+
)
|
| 284 |
+
except ValueError:
|
| 285 |
+
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| 286 |
+
mask = None
|
| 287 |
+
else:
|
| 288 |
+
raise
|
| 289 |
+
|
| 290 |
+
# now we have two images, im is XOR image and mask is AND image
|
| 291 |
+
|
| 292 |
+
# apply mask image as alpha channel
|
| 293 |
+
if mask:
|
| 294 |
+
im = im.convert("RGBA")
|
| 295 |
+
im.putalpha(mask)
|
| 296 |
+
|
| 297 |
+
return im
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
##
|
| 301 |
+
# Image plugin for Windows Icon files.
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class IcoImageFile(ImageFile.ImageFile):
|
| 305 |
+
"""
|
| 306 |
+
PIL read-only image support for Microsoft Windows .ico files.
|
| 307 |
+
|
| 308 |
+
By default the largest resolution image in the file will be loaded. This
|
| 309 |
+
can be changed by altering the 'size' attribute before calling 'load'.
|
| 310 |
+
|
| 311 |
+
The info dictionary has a key 'sizes' that is a list of the sizes available
|
| 312 |
+
in the icon file.
|
| 313 |
+
|
| 314 |
+
Handles classic, XP and Vista icon formats.
|
| 315 |
+
|
| 316 |
+
When saving, PNG compression is used. Support for this was only added in
|
| 317 |
+
Windows Vista. If you are unable to view the icon in Windows, convert the
|
| 318 |
+
image to "RGBA" mode before saving.
|
| 319 |
+
|
| 320 |
+
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
| 321 |
+
<casadebender@gmail.com>.
|
| 322 |
+
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
format = "ICO"
|
| 326 |
+
format_description = "Windows Icon"
|
| 327 |
+
|
| 328 |
+
def _open(self) -> None:
|
| 329 |
+
self.ico = IcoFile(self.fp)
|
| 330 |
+
self.info["sizes"] = self.ico.sizes()
|
| 331 |
+
self.size = self.ico.entry[0].dim
|
| 332 |
+
self.load()
|
| 333 |
+
|
| 334 |
+
@property
|
| 335 |
+
def size(self) -> tuple[int, int]:
|
| 336 |
+
return self._size
|
| 337 |
+
|
| 338 |
+
@size.setter
|
| 339 |
+
def size(self, value: tuple[int, int]) -> None:
|
| 340 |
+
if value not in self.info["sizes"]:
|
| 341 |
+
msg = "This is not one of the allowed sizes of this image"
|
| 342 |
+
raise ValueError(msg)
|
| 343 |
+
self._size = value
|
| 344 |
+
|
| 345 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 346 |
+
if self._im is not None and self.im.size == self.size:
|
| 347 |
+
# Already loaded
|
| 348 |
+
return Image.Image.load(self)
|
| 349 |
+
im = self.ico.getimage(self.size)
|
| 350 |
+
# if tile is PNG, it won't really be loaded yet
|
| 351 |
+
im.load()
|
| 352 |
+
self.im = im.im
|
| 353 |
+
self._mode = im.mode
|
| 354 |
+
if im.palette:
|
| 355 |
+
self.palette = im.palette
|
| 356 |
+
if im.size != self.size:
|
| 357 |
+
warnings.warn("Image was not the expected size")
|
| 358 |
+
|
| 359 |
+
index = self.ico.getentryindex(self.size)
|
| 360 |
+
sizes = list(self.info["sizes"])
|
| 361 |
+
sizes[index] = im.size
|
| 362 |
+
self.info["sizes"] = set(sizes)
|
| 363 |
+
|
| 364 |
+
self.size = im.size
|
| 365 |
+
return None
|
| 366 |
+
|
| 367 |
+
def load_seek(self, pos: int) -> None:
|
| 368 |
+
# Flag the ImageFile.Parser so that it
|
| 369 |
+
# just does all the decode at the end.
|
| 370 |
+
pass
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
#
|
| 374 |
+
# --------------------------------------------------------------------
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
|
| 378 |
+
Image.register_save(IcoImageFile.format, _save)
|
| 379 |
+
Image.register_extension(IcoImageFile.format, ".ico")
|
| 380 |
+
|
| 381 |
+
Image.register_mime(IcoImageFile.format, "image/x-icon")
|
Lib/site-packages/PIL/ImImagePlugin.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# IFUNC IM file handling for PIL
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1995-09-01 fl Created.
|
| 9 |
+
# 1997-01-03 fl Save palette images
|
| 10 |
+
# 1997-01-08 fl Added sequence support
|
| 11 |
+
# 1997-01-23 fl Added P and RGB save support
|
| 12 |
+
# 1997-05-31 fl Read floating point images
|
| 13 |
+
# 1997-06-22 fl Save floating point images
|
| 14 |
+
# 1997-08-27 fl Read and save 1-bit images
|
| 15 |
+
# 1998-06-25 fl Added support for RGB+LUT images
|
| 16 |
+
# 1998-07-02 fl Added support for YCC images
|
| 17 |
+
# 1998-07-15 fl Renamed offset attribute to avoid name clash
|
| 18 |
+
# 1998-12-29 fl Added I;16 support
|
| 19 |
+
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
|
| 20 |
+
# 2003-09-26 fl Added LA/PA support
|
| 21 |
+
#
|
| 22 |
+
# Copyright (c) 1997-2003 by Secret Labs AB.
|
| 23 |
+
# Copyright (c) 1995-2001 by Fredrik Lundh.
|
| 24 |
+
#
|
| 25 |
+
# See the README file for information on usage and redistribution.
|
| 26 |
+
#
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import os
|
| 30 |
+
import re
|
| 31 |
+
from typing import IO, Any
|
| 32 |
+
|
| 33 |
+
from . import Image, ImageFile, ImagePalette
|
| 34 |
+
|
| 35 |
+
# --------------------------------------------------------------------
|
| 36 |
+
# Standard tags
|
| 37 |
+
|
| 38 |
+
COMMENT = "Comment"
|
| 39 |
+
DATE = "Date"
|
| 40 |
+
EQUIPMENT = "Digitalization equipment"
|
| 41 |
+
FRAMES = "File size (no of images)"
|
| 42 |
+
LUT = "Lut"
|
| 43 |
+
NAME = "Name"
|
| 44 |
+
SCALE = "Scale (x,y)"
|
| 45 |
+
SIZE = "Image size (x*y)"
|
| 46 |
+
MODE = "Image type"
|
| 47 |
+
|
| 48 |
+
TAGS = {
|
| 49 |
+
COMMENT: 0,
|
| 50 |
+
DATE: 0,
|
| 51 |
+
EQUIPMENT: 0,
|
| 52 |
+
FRAMES: 0,
|
| 53 |
+
LUT: 0,
|
| 54 |
+
NAME: 0,
|
| 55 |
+
SCALE: 0,
|
| 56 |
+
SIZE: 0,
|
| 57 |
+
MODE: 0,
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
OPEN = {
|
| 61 |
+
# ifunc93/p3cfunc formats
|
| 62 |
+
"0 1 image": ("1", "1"),
|
| 63 |
+
"L 1 image": ("1", "1"),
|
| 64 |
+
"Greyscale image": ("L", "L"),
|
| 65 |
+
"Grayscale image": ("L", "L"),
|
| 66 |
+
"RGB image": ("RGB", "RGB;L"),
|
| 67 |
+
"RLB image": ("RGB", "RLB"),
|
| 68 |
+
"RYB image": ("RGB", "RLB"),
|
| 69 |
+
"B1 image": ("1", "1"),
|
| 70 |
+
"B2 image": ("P", "P;2"),
|
| 71 |
+
"B4 image": ("P", "P;4"),
|
| 72 |
+
"X 24 image": ("RGB", "RGB"),
|
| 73 |
+
"L 32 S image": ("I", "I;32"),
|
| 74 |
+
"L 32 F image": ("F", "F;32"),
|
| 75 |
+
# old p3cfunc formats
|
| 76 |
+
"RGB3 image": ("RGB", "RGB;T"),
|
| 77 |
+
"RYB3 image": ("RGB", "RYB;T"),
|
| 78 |
+
# extensions
|
| 79 |
+
"LA image": ("LA", "LA;L"),
|
| 80 |
+
"PA image": ("LA", "PA;L"),
|
| 81 |
+
"RGBA image": ("RGBA", "RGBA;L"),
|
| 82 |
+
"RGBX image": ("RGB", "RGBX;L"),
|
| 83 |
+
"CMYK image": ("CMYK", "CMYK;L"),
|
| 84 |
+
"YCC image": ("YCbCr", "YCbCr;L"),
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
# ifunc95 extensions
|
| 88 |
+
for i in ["8", "8S", "16", "16S", "32", "32F"]:
|
| 89 |
+
OPEN[f"L {i} image"] = ("F", f"F;{i}")
|
| 90 |
+
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
|
| 91 |
+
for i in ["16", "16L", "16B"]:
|
| 92 |
+
OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
|
| 93 |
+
OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
|
| 94 |
+
for i in ["32S"]:
|
| 95 |
+
OPEN[f"L {i} image"] = ("I", f"I;{i}")
|
| 96 |
+
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
|
| 97 |
+
for j in range(2, 33):
|
| 98 |
+
OPEN[f"L*{j} image"] = ("F", f"F;{j}")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# --------------------------------------------------------------------
|
| 102 |
+
# Read IM directory
|
| 103 |
+
|
| 104 |
+
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def number(s: Any) -> float:
|
| 108 |
+
try:
|
| 109 |
+
return int(s)
|
| 110 |
+
except ValueError:
|
| 111 |
+
return float(s)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
##
|
| 115 |
+
# Image plugin for the IFUNC IM file format.
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class ImImageFile(ImageFile.ImageFile):
|
| 119 |
+
format = "IM"
|
| 120 |
+
format_description = "IFUNC Image Memory"
|
| 121 |
+
_close_exclusive_fp_after_loading = False
|
| 122 |
+
|
| 123 |
+
def _open(self) -> None:
|
| 124 |
+
# Quick rejection: if there's not an LF among the first
|
| 125 |
+
# 100 bytes, this is (probably) not a text header.
|
| 126 |
+
|
| 127 |
+
if b"\n" not in self.fp.read(100):
|
| 128 |
+
msg = "not an IM file"
|
| 129 |
+
raise SyntaxError(msg)
|
| 130 |
+
self.fp.seek(0)
|
| 131 |
+
|
| 132 |
+
n = 0
|
| 133 |
+
|
| 134 |
+
# Default values
|
| 135 |
+
self.info[MODE] = "L"
|
| 136 |
+
self.info[SIZE] = (512, 512)
|
| 137 |
+
self.info[FRAMES] = 1
|
| 138 |
+
|
| 139 |
+
self.rawmode = "L"
|
| 140 |
+
|
| 141 |
+
while True:
|
| 142 |
+
s = self.fp.read(1)
|
| 143 |
+
|
| 144 |
+
# Some versions of IFUNC uses \n\r instead of \r\n...
|
| 145 |
+
if s == b"\r":
|
| 146 |
+
continue
|
| 147 |
+
|
| 148 |
+
if not s or s == b"\0" or s == b"\x1A":
|
| 149 |
+
break
|
| 150 |
+
|
| 151 |
+
# FIXME: this may read whole file if not a text file
|
| 152 |
+
s = s + self.fp.readline()
|
| 153 |
+
|
| 154 |
+
if len(s) > 100:
|
| 155 |
+
msg = "not an IM file"
|
| 156 |
+
raise SyntaxError(msg)
|
| 157 |
+
|
| 158 |
+
if s[-2:] == b"\r\n":
|
| 159 |
+
s = s[:-2]
|
| 160 |
+
elif s[-1:] == b"\n":
|
| 161 |
+
s = s[:-1]
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
m = split.match(s)
|
| 165 |
+
except re.error as e:
|
| 166 |
+
msg = "not an IM file"
|
| 167 |
+
raise SyntaxError(msg) from e
|
| 168 |
+
|
| 169 |
+
if m:
|
| 170 |
+
k, v = m.group(1, 2)
|
| 171 |
+
|
| 172 |
+
# Don't know if this is the correct encoding,
|
| 173 |
+
# but a decent guess (I guess)
|
| 174 |
+
k = k.decode("latin-1", "replace")
|
| 175 |
+
v = v.decode("latin-1", "replace")
|
| 176 |
+
|
| 177 |
+
# Convert value as appropriate
|
| 178 |
+
if k in [FRAMES, SCALE, SIZE]:
|
| 179 |
+
v = v.replace("*", ",")
|
| 180 |
+
v = tuple(map(number, v.split(",")))
|
| 181 |
+
if len(v) == 1:
|
| 182 |
+
v = v[0]
|
| 183 |
+
elif k == MODE and v in OPEN:
|
| 184 |
+
v, self.rawmode = OPEN[v]
|
| 185 |
+
|
| 186 |
+
# Add to dictionary. Note that COMMENT tags are
|
| 187 |
+
# combined into a list of strings.
|
| 188 |
+
if k == COMMENT:
|
| 189 |
+
if k in self.info:
|
| 190 |
+
self.info[k].append(v)
|
| 191 |
+
else:
|
| 192 |
+
self.info[k] = [v]
|
| 193 |
+
else:
|
| 194 |
+
self.info[k] = v
|
| 195 |
+
|
| 196 |
+
if k in TAGS:
|
| 197 |
+
n += 1
|
| 198 |
+
|
| 199 |
+
else:
|
| 200 |
+
msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
|
| 201 |
+
raise SyntaxError(msg)
|
| 202 |
+
|
| 203 |
+
if not n:
|
| 204 |
+
msg = "Not an IM file"
|
| 205 |
+
raise SyntaxError(msg)
|
| 206 |
+
|
| 207 |
+
# Basic attributes
|
| 208 |
+
self._size = self.info[SIZE]
|
| 209 |
+
self._mode = self.info[MODE]
|
| 210 |
+
|
| 211 |
+
# Skip forward to start of image data
|
| 212 |
+
while s and s[:1] != b"\x1A":
|
| 213 |
+
s = self.fp.read(1)
|
| 214 |
+
if not s:
|
| 215 |
+
msg = "File truncated"
|
| 216 |
+
raise SyntaxError(msg)
|
| 217 |
+
|
| 218 |
+
if LUT in self.info:
|
| 219 |
+
# convert lookup table to palette or lut attribute
|
| 220 |
+
palette = self.fp.read(768)
|
| 221 |
+
greyscale = 1 # greyscale palette
|
| 222 |
+
linear = 1 # linear greyscale palette
|
| 223 |
+
for i in range(256):
|
| 224 |
+
if palette[i] == palette[i + 256] == palette[i + 512]:
|
| 225 |
+
if palette[i] != i:
|
| 226 |
+
linear = 0
|
| 227 |
+
else:
|
| 228 |
+
greyscale = 0
|
| 229 |
+
if self.mode in ["L", "LA", "P", "PA"]:
|
| 230 |
+
if greyscale:
|
| 231 |
+
if not linear:
|
| 232 |
+
self.lut = list(palette[:256])
|
| 233 |
+
else:
|
| 234 |
+
if self.mode in ["L", "P"]:
|
| 235 |
+
self._mode = self.rawmode = "P"
|
| 236 |
+
elif self.mode in ["LA", "PA"]:
|
| 237 |
+
self._mode = "PA"
|
| 238 |
+
self.rawmode = "PA;L"
|
| 239 |
+
self.palette = ImagePalette.raw("RGB;L", palette)
|
| 240 |
+
elif self.mode == "RGB":
|
| 241 |
+
if not greyscale or not linear:
|
| 242 |
+
self.lut = list(palette)
|
| 243 |
+
|
| 244 |
+
self.frame = 0
|
| 245 |
+
|
| 246 |
+
self.__offset = offs = self.fp.tell()
|
| 247 |
+
|
| 248 |
+
self._fp = self.fp # FIXME: hack
|
| 249 |
+
|
| 250 |
+
if self.rawmode[:2] == "F;":
|
| 251 |
+
# ifunc95 formats
|
| 252 |
+
try:
|
| 253 |
+
# use bit decoder (if necessary)
|
| 254 |
+
bits = int(self.rawmode[2:])
|
| 255 |
+
if bits not in [8, 16, 32]:
|
| 256 |
+
self.tile = [
|
| 257 |
+
ImageFile._Tile(
|
| 258 |
+
"bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
|
| 259 |
+
)
|
| 260 |
+
]
|
| 261 |
+
return
|
| 262 |
+
except ValueError:
|
| 263 |
+
pass
|
| 264 |
+
|
| 265 |
+
if self.rawmode in ["RGB;T", "RYB;T"]:
|
| 266 |
+
# Old LabEye/3PC files. Would be very surprised if anyone
|
| 267 |
+
# ever stumbled upon such a file ;-)
|
| 268 |
+
size = self.size[0] * self.size[1]
|
| 269 |
+
self.tile = [
|
| 270 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
|
| 271 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
|
| 272 |
+
ImageFile._Tile(
|
| 273 |
+
"raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
|
| 274 |
+
),
|
| 275 |
+
]
|
| 276 |
+
else:
|
| 277 |
+
# LabEye/IFUNC files
|
| 278 |
+
self.tile = [
|
| 279 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
| 280 |
+
]
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def n_frames(self) -> int:
|
| 284 |
+
return self.info[FRAMES]
|
| 285 |
+
|
| 286 |
+
@property
|
| 287 |
+
def is_animated(self) -> bool:
|
| 288 |
+
return self.info[FRAMES] > 1
|
| 289 |
+
|
| 290 |
+
def seek(self, frame: int) -> None:
|
| 291 |
+
if not self._seek_check(frame):
|
| 292 |
+
return
|
| 293 |
+
|
| 294 |
+
self.frame = frame
|
| 295 |
+
|
| 296 |
+
if self.mode == "1":
|
| 297 |
+
bits = 1
|
| 298 |
+
else:
|
| 299 |
+
bits = 8 * len(self.mode)
|
| 300 |
+
|
| 301 |
+
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
|
| 302 |
+
offs = self.__offset + frame * size
|
| 303 |
+
|
| 304 |
+
self.fp = self._fp
|
| 305 |
+
|
| 306 |
+
self.tile = [
|
| 307 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
| 308 |
+
]
|
| 309 |
+
|
| 310 |
+
def tell(self) -> int:
|
| 311 |
+
return self.frame
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
#
|
| 315 |
+
# --------------------------------------------------------------------
|
| 316 |
+
# Save IM files
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
SAVE = {
|
| 320 |
+
# mode: (im type, raw mode)
|
| 321 |
+
"1": ("0 1", "1"),
|
| 322 |
+
"L": ("Greyscale", "L"),
|
| 323 |
+
"LA": ("LA", "LA;L"),
|
| 324 |
+
"P": ("Greyscale", "P"),
|
| 325 |
+
"PA": ("LA", "PA;L"),
|
| 326 |
+
"I": ("L 32S", "I;32S"),
|
| 327 |
+
"I;16": ("L 16", "I;16"),
|
| 328 |
+
"I;16L": ("L 16L", "I;16L"),
|
| 329 |
+
"I;16B": ("L 16B", "I;16B"),
|
| 330 |
+
"F": ("L 32F", "F;32F"),
|
| 331 |
+
"RGB": ("RGB", "RGB;L"),
|
| 332 |
+
"RGBA": ("RGBA", "RGBA;L"),
|
| 333 |
+
"RGBX": ("RGBX", "RGBX;L"),
|
| 334 |
+
"CMYK": ("CMYK", "CMYK;L"),
|
| 335 |
+
"YCbCr": ("YCC", "YCbCr;L"),
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 340 |
+
try:
|
| 341 |
+
image_type, rawmode = SAVE[im.mode]
|
| 342 |
+
except KeyError as e:
|
| 343 |
+
msg = f"Cannot save {im.mode} images as IM"
|
| 344 |
+
raise ValueError(msg) from e
|
| 345 |
+
|
| 346 |
+
frames = im.encoderinfo.get("frames", 1)
|
| 347 |
+
|
| 348 |
+
fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
|
| 349 |
+
if filename:
|
| 350 |
+
# Each line must be 100 characters or less,
|
| 351 |
+
# or: SyntaxError("not an IM file")
|
| 352 |
+
# 8 characters are used for "Name: " and "\r\n"
|
| 353 |
+
# Keep just the filename, ditch the potentially overlong path
|
| 354 |
+
if isinstance(filename, bytes):
|
| 355 |
+
filename = filename.decode("ascii")
|
| 356 |
+
name, ext = os.path.splitext(os.path.basename(filename))
|
| 357 |
+
name = "".join([name[: 92 - len(ext)], ext])
|
| 358 |
+
|
| 359 |
+
fp.write(f"Name: {name}\r\n".encode("ascii"))
|
| 360 |
+
fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
|
| 361 |
+
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
|
| 362 |
+
if im.mode in ["P", "PA"]:
|
| 363 |
+
fp.write(b"Lut: 1\r\n")
|
| 364 |
+
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
|
| 365 |
+
if im.mode in ["P", "PA"]:
|
| 366 |
+
im_palette = im.im.getpalette("RGB", "RGB;L")
|
| 367 |
+
colors = len(im_palette) // 3
|
| 368 |
+
palette = b""
|
| 369 |
+
for i in range(3):
|
| 370 |
+
palette += im_palette[colors * i : colors * (i + 1)]
|
| 371 |
+
palette += b"\x00" * (256 - colors)
|
| 372 |
+
fp.write(palette) # 768 bytes
|
| 373 |
+
ImageFile._save(
|
| 374 |
+
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
#
|
| 379 |
+
# --------------------------------------------------------------------
|
| 380 |
+
# Registry
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
Image.register_open(ImImageFile.format, ImImageFile)
|
| 384 |
+
Image.register_save(ImImageFile.format, _save)
|
| 385 |
+
|
| 386 |
+
Image.register_extension(ImImageFile.format, ".im")
|
Lib/site-packages/PIL/Image.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Lib/site-packages/PIL/ImageChops.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# standard channel operations
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-03-24 fl Created
|
| 9 |
+
# 1996-08-13 fl Added logical operations (for "1" images)
|
| 10 |
+
# 2000-10-12 fl Added offset method (from Image.py)
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) 1997-2000 by Secret Labs AB
|
| 13 |
+
# Copyright (c) 1996-2000 by Fredrik Lundh
|
| 14 |
+
#
|
| 15 |
+
# See the README file for information on usage and redistribution.
|
| 16 |
+
#
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from . import Image
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def constant(image: Image.Image, value: int) -> Image.Image:
|
| 24 |
+
"""Fill a channel with a given gray level.
|
| 25 |
+
|
| 26 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
return Image.new("L", image.size, value)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def duplicate(image: Image.Image) -> Image.Image:
|
| 33 |
+
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
|
| 34 |
+
|
| 35 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
return image.copy()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def invert(image: Image.Image) -> Image.Image:
|
| 42 |
+
"""
|
| 43 |
+
Invert an image (channel). ::
|
| 44 |
+
|
| 45 |
+
out = MAX - image
|
| 46 |
+
|
| 47 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
image.load()
|
| 51 |
+
return image._new(image.im.chop_invert())
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 55 |
+
"""
|
| 56 |
+
Compares the two images, pixel by pixel, and returns a new image containing
|
| 57 |
+
the lighter values. ::
|
| 58 |
+
|
| 59 |
+
out = max(image1, image2)
|
| 60 |
+
|
| 61 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
image1.load()
|
| 65 |
+
image2.load()
|
| 66 |
+
return image1._new(image1.im.chop_lighter(image2.im))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 70 |
+
"""
|
| 71 |
+
Compares the two images, pixel by pixel, and returns a new image containing
|
| 72 |
+
the darker values. ::
|
| 73 |
+
|
| 74 |
+
out = min(image1, image2)
|
| 75 |
+
|
| 76 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
image1.load()
|
| 80 |
+
image2.load()
|
| 81 |
+
return image1._new(image1.im.chop_darker(image2.im))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 85 |
+
"""
|
| 86 |
+
Returns the absolute value of the pixel-by-pixel difference between the two
|
| 87 |
+
images. ::
|
| 88 |
+
|
| 89 |
+
out = abs(image1 - image2)
|
| 90 |
+
|
| 91 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
image1.load()
|
| 95 |
+
image2.load()
|
| 96 |
+
return image1._new(image1.im.chop_difference(image2.im))
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 100 |
+
"""
|
| 101 |
+
Superimposes two images on top of each other.
|
| 102 |
+
|
| 103 |
+
If you multiply an image with a solid black image, the result is black. If
|
| 104 |
+
you multiply with a solid white image, the image is unaffected. ::
|
| 105 |
+
|
| 106 |
+
out = image1 * image2 / MAX
|
| 107 |
+
|
| 108 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
image1.load()
|
| 112 |
+
image2.load()
|
| 113 |
+
return image1._new(image1.im.chop_multiply(image2.im))
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 117 |
+
"""
|
| 118 |
+
Superimposes two inverted images on top of each other. ::
|
| 119 |
+
|
| 120 |
+
out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
|
| 121 |
+
|
| 122 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
image1.load()
|
| 126 |
+
image2.load()
|
| 127 |
+
return image1._new(image1.im.chop_screen(image2.im))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 131 |
+
"""
|
| 132 |
+
Superimposes two images on top of each other using the Soft Light algorithm
|
| 133 |
+
|
| 134 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
image1.load()
|
| 138 |
+
image2.load()
|
| 139 |
+
return image1._new(image1.im.chop_soft_light(image2.im))
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 143 |
+
"""
|
| 144 |
+
Superimposes two images on top of each other using the Hard Light algorithm
|
| 145 |
+
|
| 146 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
image1.load()
|
| 150 |
+
image2.load()
|
| 151 |
+
return image1._new(image1.im.chop_hard_light(image2.im))
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 155 |
+
"""
|
| 156 |
+
Superimposes two images on top of each other using the Overlay algorithm
|
| 157 |
+
|
| 158 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
image1.load()
|
| 162 |
+
image2.load()
|
| 163 |
+
return image1._new(image1.im.chop_overlay(image2.im))
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def add(
|
| 167 |
+
image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
|
| 168 |
+
) -> Image.Image:
|
| 169 |
+
"""
|
| 170 |
+
Adds two images, dividing the result by scale and adding the
|
| 171 |
+
offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
| 172 |
+
|
| 173 |
+
out = ((image1 + image2) / scale + offset)
|
| 174 |
+
|
| 175 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
image1.load()
|
| 179 |
+
image2.load()
|
| 180 |
+
return image1._new(image1.im.chop_add(image2.im, scale, offset))
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def subtract(
|
| 184 |
+
image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
|
| 185 |
+
) -> Image.Image:
|
| 186 |
+
"""
|
| 187 |
+
Subtracts two images, dividing the result by scale and adding the offset.
|
| 188 |
+
If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
| 189 |
+
|
| 190 |
+
out = ((image1 - image2) / scale + offset)
|
| 191 |
+
|
| 192 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
image1.load()
|
| 196 |
+
image2.load()
|
| 197 |
+
return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 201 |
+
"""Add two images, without clipping the result. ::
|
| 202 |
+
|
| 203 |
+
out = ((image1 + image2) % MAX)
|
| 204 |
+
|
| 205 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
image1.load()
|
| 209 |
+
image2.load()
|
| 210 |
+
return image1._new(image1.im.chop_add_modulo(image2.im))
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 214 |
+
"""Subtract two images, without clipping the result. ::
|
| 215 |
+
|
| 216 |
+
out = ((image1 - image2) % MAX)
|
| 217 |
+
|
| 218 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
image1.load()
|
| 222 |
+
image2.load()
|
| 223 |
+
return image1._new(image1.im.chop_subtract_modulo(image2.im))
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 227 |
+
"""Logical AND between two images.
|
| 228 |
+
|
| 229 |
+
Both of the images must have mode "1". If you would like to perform a
|
| 230 |
+
logical AND on an image with a mode other than "1", try
|
| 231 |
+
:py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask
|
| 232 |
+
as the second image. ::
|
| 233 |
+
|
| 234 |
+
out = ((image1 and image2) % MAX)
|
| 235 |
+
|
| 236 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
image1.load()
|
| 240 |
+
image2.load()
|
| 241 |
+
return image1._new(image1.im.chop_and(image2.im))
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 245 |
+
"""Logical OR between two images.
|
| 246 |
+
|
| 247 |
+
Both of the images must have mode "1". ::
|
| 248 |
+
|
| 249 |
+
out = ((image1 or image2) % MAX)
|
| 250 |
+
|
| 251 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 252 |
+
"""
|
| 253 |
+
|
| 254 |
+
image1.load()
|
| 255 |
+
image2.load()
|
| 256 |
+
return image1._new(image1.im.chop_or(image2.im))
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
| 260 |
+
"""Logical XOR between two images.
|
| 261 |
+
|
| 262 |
+
Both of the images must have mode "1". ::
|
| 263 |
+
|
| 264 |
+
out = ((bool(image1) != bool(image2)) % MAX)
|
| 265 |
+
|
| 266 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 267 |
+
"""
|
| 268 |
+
|
| 269 |
+
image1.load()
|
| 270 |
+
image2.load()
|
| 271 |
+
return image1._new(image1.im.chop_xor(image2.im))
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
|
| 275 |
+
"""Blend images using constant transparency weight. Alias for
|
| 276 |
+
:py:func:`PIL.Image.blend`.
|
| 277 |
+
|
| 278 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
return Image.blend(image1, image2, alpha)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def composite(
|
| 285 |
+
image1: Image.Image, image2: Image.Image, mask: Image.Image
|
| 286 |
+
) -> Image.Image:
|
| 287 |
+
"""Create composite using transparency mask. Alias for
|
| 288 |
+
:py:func:`PIL.Image.composite`.
|
| 289 |
+
|
| 290 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 291 |
+
"""
|
| 292 |
+
|
| 293 |
+
return Image.composite(image1, image2, mask)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
|
| 297 |
+
"""Returns a copy of the image where data has been offset by the given
|
| 298 |
+
distances. Data wraps around the edges. If ``yoffset`` is omitted, it
|
| 299 |
+
is assumed to be equal to ``xoffset``.
|
| 300 |
+
|
| 301 |
+
:param image: Input image.
|
| 302 |
+
:param xoffset: The horizontal distance.
|
| 303 |
+
:param yoffset: The vertical distance. If omitted, both
|
| 304 |
+
distances are set to the same value.
|
| 305 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 306 |
+
"""
|
| 307 |
+
|
| 308 |
+
if yoffset is None:
|
| 309 |
+
yoffset = xoffset
|
| 310 |
+
image.load()
|
| 311 |
+
return image._new(image.im.offset(xoffset, yoffset))
|
Lib/site-packages/PIL/ImageCms.py
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The Python Imaging Library.
|
| 2 |
+
# $Id$
|
| 3 |
+
|
| 4 |
+
# Optional color management support, based on Kevin Cazabon's PyCMS
|
| 5 |
+
# library.
|
| 6 |
+
|
| 7 |
+
# Originally released under LGPL. Graciously donated to PIL in
|
| 8 |
+
# March 2009, for distribution under the standard PIL license
|
| 9 |
+
|
| 10 |
+
# History:
|
| 11 |
+
|
| 12 |
+
# 2009-03-08 fl Added to PIL.
|
| 13 |
+
|
| 14 |
+
# Copyright (C) 2002-2003 Kevin Cazabon
|
| 15 |
+
# Copyright (c) 2009 by Fredrik Lundh
|
| 16 |
+
# Copyright (c) 2013 by Eric Soroos
|
| 17 |
+
|
| 18 |
+
# See the README file for information on usage and redistribution. See
|
| 19 |
+
# below for the original description.
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import operator
|
| 23 |
+
import sys
|
| 24 |
+
from enum import IntEnum, IntFlag
|
| 25 |
+
from functools import reduce
|
| 26 |
+
from typing import Any, Literal, SupportsFloat, SupportsInt, Union
|
| 27 |
+
|
| 28 |
+
from . import Image, __version__
|
| 29 |
+
from ._deprecate import deprecate
|
| 30 |
+
from ._typing import SupportsRead
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
from . import _imagingcms as core
|
| 34 |
+
|
| 35 |
+
_CmsProfileCompatible = Union[
|
| 36 |
+
str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile"
|
| 37 |
+
]
|
| 38 |
+
except ImportError as ex:
|
| 39 |
+
# Allow error import for doc purposes, but error out when accessing
|
| 40 |
+
# anything in core.
|
| 41 |
+
from ._util import DeferredError
|
| 42 |
+
|
| 43 |
+
core = DeferredError.new(ex)
|
| 44 |
+
|
| 45 |
+
_DESCRIPTION = """
|
| 46 |
+
pyCMS
|
| 47 |
+
|
| 48 |
+
a Python / PIL interface to the littleCMS ICC Color Management System
|
| 49 |
+
Copyright (C) 2002-2003 Kevin Cazabon
|
| 50 |
+
kevin@cazabon.com
|
| 51 |
+
https://www.cazabon.com
|
| 52 |
+
|
| 53 |
+
pyCMS home page: https://www.cazabon.com/pyCMS
|
| 54 |
+
littleCMS home page: https://www.littlecms.com
|
| 55 |
+
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
|
| 56 |
+
|
| 57 |
+
Originally released under LGPL. Graciously donated to PIL in
|
| 58 |
+
March 2009, for distribution under the standard PIL license
|
| 59 |
+
|
| 60 |
+
The pyCMS.py module provides a "clean" interface between Python/PIL and
|
| 61 |
+
pyCMSdll, taking care of some of the more complex handling of the direct
|
| 62 |
+
pyCMSdll functions, as well as error-checking and making sure that all
|
| 63 |
+
relevant data is kept together.
|
| 64 |
+
|
| 65 |
+
While it is possible to call pyCMSdll functions directly, it's not highly
|
| 66 |
+
recommended.
|
| 67 |
+
|
| 68 |
+
Version History:
|
| 69 |
+
|
| 70 |
+
1.0.0 pil Oct 2013 Port to LCMS 2.
|
| 71 |
+
|
| 72 |
+
0.1.0 pil mod March 10, 2009
|
| 73 |
+
|
| 74 |
+
Renamed display profile to proof profile. The proof
|
| 75 |
+
profile is the profile of the device that is being
|
| 76 |
+
simulated, not the profile of the device which is
|
| 77 |
+
actually used to display/print the final simulation
|
| 78 |
+
(that'd be the output profile) - also see LCMSAPI.txt
|
| 79 |
+
input colorspace -> using 'renderingIntent' -> proof
|
| 80 |
+
colorspace -> using 'proofRenderingIntent' -> output
|
| 81 |
+
colorspace
|
| 82 |
+
|
| 83 |
+
Added LCMS FLAGS support.
|
| 84 |
+
Added FLAGS["SOFTPROOFING"] as default flag for
|
| 85 |
+
buildProofTransform (otherwise the proof profile/intent
|
| 86 |
+
would be ignored).
|
| 87 |
+
|
| 88 |
+
0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms
|
| 89 |
+
|
| 90 |
+
0.0.2 alpha Jan 6, 2002
|
| 91 |
+
|
| 92 |
+
Added try/except statements around type() checks of
|
| 93 |
+
potential CObjects... Python won't let you use type()
|
| 94 |
+
on them, and raises a TypeError (stupid, if you ask
|
| 95 |
+
me!)
|
| 96 |
+
|
| 97 |
+
Added buildProofTransformFromOpenProfiles() function.
|
| 98 |
+
Additional fixes in DLL, see DLL code for details.
|
| 99 |
+
|
| 100 |
+
0.0.1 alpha first public release, Dec. 26, 2002
|
| 101 |
+
|
| 102 |
+
Known to-do list with current version (of Python interface, not pyCMSdll):
|
| 103 |
+
|
| 104 |
+
none
|
| 105 |
+
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
_VERSION = "1.0.0 pil"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def __getattr__(name: str) -> Any:
|
| 112 |
+
if name == "DESCRIPTION":
|
| 113 |
+
deprecate("PIL.ImageCms.DESCRIPTION", 12)
|
| 114 |
+
return _DESCRIPTION
|
| 115 |
+
elif name == "VERSION":
|
| 116 |
+
deprecate("PIL.ImageCms.VERSION", 12)
|
| 117 |
+
return _VERSION
|
| 118 |
+
elif name == "FLAGS":
|
| 119 |
+
deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags")
|
| 120 |
+
return _FLAGS
|
| 121 |
+
msg = f"module '{__name__}' has no attribute '{name}'"
|
| 122 |
+
raise AttributeError(msg)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# --------------------------------------------------------------------.
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
#
|
| 129 |
+
# intent/direction values
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class Intent(IntEnum):
|
| 133 |
+
PERCEPTUAL = 0
|
| 134 |
+
RELATIVE_COLORIMETRIC = 1
|
| 135 |
+
SATURATION = 2
|
| 136 |
+
ABSOLUTE_COLORIMETRIC = 3
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class Direction(IntEnum):
|
| 140 |
+
INPUT = 0
|
| 141 |
+
OUTPUT = 1
|
| 142 |
+
PROOF = 2
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
#
|
| 146 |
+
# flags
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class Flags(IntFlag):
|
| 150 |
+
"""Flags and documentation are taken from ``lcms2.h``."""
|
| 151 |
+
|
| 152 |
+
NONE = 0
|
| 153 |
+
NOCACHE = 0x0040
|
| 154 |
+
"""Inhibit 1-pixel cache"""
|
| 155 |
+
NOOPTIMIZE = 0x0100
|
| 156 |
+
"""Inhibit optimizations"""
|
| 157 |
+
NULLTRANSFORM = 0x0200
|
| 158 |
+
"""Don't transform anyway"""
|
| 159 |
+
GAMUTCHECK = 0x1000
|
| 160 |
+
"""Out of Gamut alarm"""
|
| 161 |
+
SOFTPROOFING = 0x4000
|
| 162 |
+
"""Do softproofing"""
|
| 163 |
+
BLACKPOINTCOMPENSATION = 0x2000
|
| 164 |
+
NOWHITEONWHITEFIXUP = 0x0004
|
| 165 |
+
"""Don't fix scum dot"""
|
| 166 |
+
HIGHRESPRECALC = 0x0400
|
| 167 |
+
"""Use more memory to give better accuracy"""
|
| 168 |
+
LOWRESPRECALC = 0x0800
|
| 169 |
+
"""Use less memory to minimize resources"""
|
| 170 |
+
# this should be 8BITS_DEVICELINK, but that is not a valid name in Python:
|
| 171 |
+
USE_8BITS_DEVICELINK = 0x0008
|
| 172 |
+
"""Create 8 bits devicelinks"""
|
| 173 |
+
GUESSDEVICECLASS = 0x0020
|
| 174 |
+
"""Guess device class (for ``transform2devicelink``)"""
|
| 175 |
+
KEEP_SEQUENCE = 0x0080
|
| 176 |
+
"""Keep profile sequence for devicelink creation"""
|
| 177 |
+
FORCE_CLUT = 0x0002
|
| 178 |
+
"""Force CLUT optimization"""
|
| 179 |
+
CLUT_POST_LINEARIZATION = 0x0001
|
| 180 |
+
"""create postlinearization tables if possible"""
|
| 181 |
+
CLUT_PRE_LINEARIZATION = 0x0010
|
| 182 |
+
"""create prelinearization tables if possible"""
|
| 183 |
+
NONEGATIVES = 0x8000
|
| 184 |
+
"""Prevent negative numbers in floating point transforms"""
|
| 185 |
+
COPY_ALPHA = 0x04000000
|
| 186 |
+
"""Alpha channels are copied on ``cmsDoTransform()``"""
|
| 187 |
+
NODEFAULTRESOURCEDEF = 0x01000000
|
| 188 |
+
|
| 189 |
+
_GRIDPOINTS_1 = 1 << 16
|
| 190 |
+
_GRIDPOINTS_2 = 2 << 16
|
| 191 |
+
_GRIDPOINTS_4 = 4 << 16
|
| 192 |
+
_GRIDPOINTS_8 = 8 << 16
|
| 193 |
+
_GRIDPOINTS_16 = 16 << 16
|
| 194 |
+
_GRIDPOINTS_32 = 32 << 16
|
| 195 |
+
_GRIDPOINTS_64 = 64 << 16
|
| 196 |
+
_GRIDPOINTS_128 = 128 << 16
|
| 197 |
+
|
| 198 |
+
@staticmethod
|
| 199 |
+
def GRIDPOINTS(n: int) -> Flags:
|
| 200 |
+
"""
|
| 201 |
+
Fine-tune control over number of gridpoints
|
| 202 |
+
|
| 203 |
+
:param n: :py:class:`int` in range ``0 <= n <= 255``
|
| 204 |
+
"""
|
| 205 |
+
return Flags.NONE | ((n & 0xFF) << 16)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
_MAX_FLAG = reduce(operator.or_, Flags)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
_FLAGS = {
|
| 212 |
+
"MATRIXINPUT": 1,
|
| 213 |
+
"MATRIXOUTPUT": 2,
|
| 214 |
+
"MATRIXONLY": (1 | 2),
|
| 215 |
+
"NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
|
| 216 |
+
# Don't create prelinearization tables on precalculated transforms
|
| 217 |
+
# (internal use):
|
| 218 |
+
"NOPRELINEARIZATION": 16,
|
| 219 |
+
"GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
|
| 220 |
+
"NOTCACHE": 64, # Inhibit 1-pixel cache
|
| 221 |
+
"NOTPRECALC": 256,
|
| 222 |
+
"NULLTRANSFORM": 512, # Don't transform anyway
|
| 223 |
+
"HIGHRESPRECALC": 1024, # Use more memory to give better accuracy
|
| 224 |
+
"LOWRESPRECALC": 2048, # Use less memory to minimize resources
|
| 225 |
+
"WHITEBLACKCOMPENSATION": 8192,
|
| 226 |
+
"BLACKPOINTCOMPENSATION": 8192,
|
| 227 |
+
"GAMUTCHECK": 4096, # Out of Gamut alarm
|
| 228 |
+
"SOFTPROOFING": 16384, # Do softproofing
|
| 229 |
+
"PRESERVEBLACK": 32768, # Black preservation
|
| 230 |
+
"NODEFAULTRESOURCEDEF": 16777216, # CRD special
|
| 231 |
+
"GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# --------------------------------------------------------------------.
|
| 236 |
+
# Experimental PIL-level API
|
| 237 |
+
# --------------------------------------------------------------------.
|
| 238 |
+
|
| 239 |
+
##
|
| 240 |
+
# Profile.
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class ImageCmsProfile:
|
| 244 |
+
def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None:
|
| 245 |
+
"""
|
| 246 |
+
:param profile: Either a string representing a filename,
|
| 247 |
+
a file like object containing a profile or a
|
| 248 |
+
low-level profile object
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
if isinstance(profile, str):
|
| 253 |
+
if sys.platform == "win32":
|
| 254 |
+
profile_bytes_path = profile.encode()
|
| 255 |
+
try:
|
| 256 |
+
profile_bytes_path.decode("ascii")
|
| 257 |
+
except UnicodeDecodeError:
|
| 258 |
+
with open(profile, "rb") as f:
|
| 259 |
+
self._set(core.profile_frombytes(f.read()))
|
| 260 |
+
return
|
| 261 |
+
self._set(core.profile_open(profile), profile)
|
| 262 |
+
elif hasattr(profile, "read"):
|
| 263 |
+
self._set(core.profile_frombytes(profile.read()))
|
| 264 |
+
elif isinstance(profile, core.CmsProfile):
|
| 265 |
+
self._set(profile)
|
| 266 |
+
else:
|
| 267 |
+
msg = "Invalid type for Profile" # type: ignore[unreachable]
|
| 268 |
+
raise TypeError(msg)
|
| 269 |
+
|
| 270 |
+
def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None:
|
| 271 |
+
self.profile = profile
|
| 272 |
+
self.filename = filename
|
| 273 |
+
self.product_name = None # profile.product_name
|
| 274 |
+
self.product_info = None # profile.product_info
|
| 275 |
+
|
| 276 |
+
def tobytes(self) -> bytes:
|
| 277 |
+
"""
|
| 278 |
+
Returns the profile in a format suitable for embedding in
|
| 279 |
+
saved images.
|
| 280 |
+
|
| 281 |
+
:returns: a bytes object containing the ICC profile.
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
return core.profile_tobytes(self.profile)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class ImageCmsTransform(Image.ImagePointHandler):
|
| 288 |
+
"""
|
| 289 |
+
Transform. This can be used with the procedural API, or with the standard
|
| 290 |
+
:py:func:`~PIL.Image.Image.point` method.
|
| 291 |
+
|
| 292 |
+
Will return the output profile in the ``output.info['icc_profile']``.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
def __init__(
|
| 296 |
+
self,
|
| 297 |
+
input: ImageCmsProfile,
|
| 298 |
+
output: ImageCmsProfile,
|
| 299 |
+
input_mode: str,
|
| 300 |
+
output_mode: str,
|
| 301 |
+
intent: Intent = Intent.PERCEPTUAL,
|
| 302 |
+
proof: ImageCmsProfile | None = None,
|
| 303 |
+
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
|
| 304 |
+
flags: Flags = Flags.NONE,
|
| 305 |
+
):
|
| 306 |
+
supported_modes = (
|
| 307 |
+
"RGB",
|
| 308 |
+
"RGBA",
|
| 309 |
+
"RGBX",
|
| 310 |
+
"CMYK",
|
| 311 |
+
"I;16",
|
| 312 |
+
"I;16L",
|
| 313 |
+
"I;16B",
|
| 314 |
+
"YCbCr",
|
| 315 |
+
"LAB",
|
| 316 |
+
"L",
|
| 317 |
+
"1",
|
| 318 |
+
)
|
| 319 |
+
for mode in (input_mode, output_mode):
|
| 320 |
+
if mode not in supported_modes:
|
| 321 |
+
deprecate(
|
| 322 |
+
mode,
|
| 323 |
+
12,
|
| 324 |
+
{
|
| 325 |
+
"L;16": "I;16 or I;16L",
|
| 326 |
+
"L:16B": "I;16B",
|
| 327 |
+
"YCCA": "YCbCr",
|
| 328 |
+
"YCC": "YCbCr",
|
| 329 |
+
}.get(mode),
|
| 330 |
+
)
|
| 331 |
+
if proof is None:
|
| 332 |
+
self.transform = core.buildTransform(
|
| 333 |
+
input.profile, output.profile, input_mode, output_mode, intent, flags
|
| 334 |
+
)
|
| 335 |
+
else:
|
| 336 |
+
self.transform = core.buildProofTransform(
|
| 337 |
+
input.profile,
|
| 338 |
+
output.profile,
|
| 339 |
+
proof.profile,
|
| 340 |
+
input_mode,
|
| 341 |
+
output_mode,
|
| 342 |
+
intent,
|
| 343 |
+
proof_intent,
|
| 344 |
+
flags,
|
| 345 |
+
)
|
| 346 |
+
# Note: inputMode and outputMode are for pyCMS compatibility only
|
| 347 |
+
self.input_mode = self.inputMode = input_mode
|
| 348 |
+
self.output_mode = self.outputMode = output_mode
|
| 349 |
+
|
| 350 |
+
self.output_profile = output
|
| 351 |
+
|
| 352 |
+
def point(self, im: Image.Image) -> Image.Image:
|
| 353 |
+
return self.apply(im)
|
| 354 |
+
|
| 355 |
+
def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
|
| 356 |
+
if imOut is None:
|
| 357 |
+
imOut = Image.new(self.output_mode, im.size, None)
|
| 358 |
+
self.transform.apply(im.getim(), imOut.getim())
|
| 359 |
+
imOut.info["icc_profile"] = self.output_profile.tobytes()
|
| 360 |
+
return imOut
|
| 361 |
+
|
| 362 |
+
def apply_in_place(self, im: Image.Image) -> Image.Image:
|
| 363 |
+
if im.mode != self.output_mode:
|
| 364 |
+
msg = "mode mismatch"
|
| 365 |
+
raise ValueError(msg) # wrong output mode
|
| 366 |
+
self.transform.apply(im.getim(), im.getim())
|
| 367 |
+
im.info["icc_profile"] = self.output_profile.tobytes()
|
| 368 |
+
return im
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None:
|
| 372 |
+
"""
|
| 373 |
+
(experimental) Fetches the profile for the current display device.
|
| 374 |
+
|
| 375 |
+
:returns: ``None`` if the profile is not known.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
if sys.platform != "win32":
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
+
from . import ImageWin # type: ignore[unused-ignore, unreachable]
|
| 382 |
+
|
| 383 |
+
if isinstance(handle, ImageWin.HDC):
|
| 384 |
+
profile = core.get_display_profile_win32(int(handle), 1)
|
| 385 |
+
else:
|
| 386 |
+
profile = core.get_display_profile_win32(int(handle or 0))
|
| 387 |
+
if profile is None:
|
| 388 |
+
return None
|
| 389 |
+
return ImageCmsProfile(profile)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# --------------------------------------------------------------------.
|
| 393 |
+
# pyCMS compatible layer
|
| 394 |
+
# --------------------------------------------------------------------.
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class PyCMSError(Exception):
|
| 398 |
+
"""(pyCMS) Exception class.
|
| 399 |
+
This is used for all errors in the pyCMS API."""
|
| 400 |
+
|
| 401 |
+
pass
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def profileToProfile(
|
| 405 |
+
im: Image.Image,
|
| 406 |
+
inputProfile: _CmsProfileCompatible,
|
| 407 |
+
outputProfile: _CmsProfileCompatible,
|
| 408 |
+
renderingIntent: Intent = Intent.PERCEPTUAL,
|
| 409 |
+
outputMode: str | None = None,
|
| 410 |
+
inPlace: bool = False,
|
| 411 |
+
flags: Flags = Flags.NONE,
|
| 412 |
+
) -> Image.Image | None:
|
| 413 |
+
"""
|
| 414 |
+
(pyCMS) Applies an ICC transformation to a given image, mapping from
|
| 415 |
+
``inputProfile`` to ``outputProfile``.
|
| 416 |
+
|
| 417 |
+
If the input or output profiles specified are not valid filenames, a
|
| 418 |
+
:exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and
|
| 419 |
+
``outputMode != im.mode``, a :exc:`PyCMSError` will be raised.
|
| 420 |
+
If an error occurs during application of the profiles,
|
| 421 |
+
a :exc:`PyCMSError` will be raised.
|
| 422 |
+
If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS),
|
| 423 |
+
a :exc:`PyCMSError` will be raised.
|
| 424 |
+
|
| 425 |
+
This function applies an ICC transformation to im from ``inputProfile``'s
|
| 426 |
+
color space to ``outputProfile``'s color space using the specified rendering
|
| 427 |
+
intent to decide how to handle out-of-gamut colors.
|
| 428 |
+
|
| 429 |
+
``outputMode`` can be used to specify that a color mode conversion is to
|
| 430 |
+
be done using these profiles, but the specified profiles must be able
|
| 431 |
+
to handle that mode. I.e., if converting im from RGB to CMYK using
|
| 432 |
+
profiles, the input profile must handle RGB data, and the output
|
| 433 |
+
profile must handle CMYK data.
|
| 434 |
+
|
| 435 |
+
:param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...)
|
| 436 |
+
or Image.open(...), etc.)
|
| 437 |
+
:param inputProfile: String, as a valid filename path to the ICC input
|
| 438 |
+
profile you wish to use for this image, or a profile object
|
| 439 |
+
:param outputProfile: String, as a valid filename path to the ICC output
|
| 440 |
+
profile you wish to use for this image, or a profile object
|
| 441 |
+
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
| 442 |
+
wish to use for the transform
|
| 443 |
+
|
| 444 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 445 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 446 |
+
ImageCms.Intent.SATURATION = 2
|
| 447 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 448 |
+
|
| 449 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 450 |
+
they do.
|
| 451 |
+
:param outputMode: A valid PIL mode for the output image (i.e. "RGB",
|
| 452 |
+
"CMYK", etc.). Note: if rendering the image "inPlace", outputMode
|
| 453 |
+
MUST be the same mode as the input, or omitted completely. If
|
| 454 |
+
omitted, the outputMode will be the same as the mode of the input
|
| 455 |
+
image (im.mode)
|
| 456 |
+
:param inPlace: Boolean. If ``True``, the original image is modified in-place,
|
| 457 |
+
and ``None`` is returned. If ``False`` (default), a new
|
| 458 |
+
:py:class:`~PIL.Image.Image` object is returned with the transform applied.
|
| 459 |
+
:param flags: Integer (0-...) specifying additional flags
|
| 460 |
+
:returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on
|
| 461 |
+
the value of ``inPlace``
|
| 462 |
+
:exception PyCMSError:
|
| 463 |
+
"""
|
| 464 |
+
|
| 465 |
+
if outputMode is None:
|
| 466 |
+
outputMode = im.mode
|
| 467 |
+
|
| 468 |
+
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
| 469 |
+
msg = "renderingIntent must be an integer between 0 and 3"
|
| 470 |
+
raise PyCMSError(msg)
|
| 471 |
+
|
| 472 |
+
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
| 473 |
+
msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
|
| 474 |
+
raise PyCMSError(msg)
|
| 475 |
+
|
| 476 |
+
try:
|
| 477 |
+
if not isinstance(inputProfile, ImageCmsProfile):
|
| 478 |
+
inputProfile = ImageCmsProfile(inputProfile)
|
| 479 |
+
if not isinstance(outputProfile, ImageCmsProfile):
|
| 480 |
+
outputProfile = ImageCmsProfile(outputProfile)
|
| 481 |
+
transform = ImageCmsTransform(
|
| 482 |
+
inputProfile,
|
| 483 |
+
outputProfile,
|
| 484 |
+
im.mode,
|
| 485 |
+
outputMode,
|
| 486 |
+
renderingIntent,
|
| 487 |
+
flags=flags,
|
| 488 |
+
)
|
| 489 |
+
if inPlace:
|
| 490 |
+
transform.apply_in_place(im)
|
| 491 |
+
imOut = None
|
| 492 |
+
else:
|
| 493 |
+
imOut = transform.apply(im)
|
| 494 |
+
except (OSError, TypeError, ValueError) as v:
|
| 495 |
+
raise PyCMSError(v) from v
|
| 496 |
+
|
| 497 |
+
return imOut
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
def getOpenProfile(
|
| 501 |
+
profileFilename: str | SupportsRead[bytes] | core.CmsProfile,
|
| 502 |
+
) -> ImageCmsProfile:
|
| 503 |
+
"""
|
| 504 |
+
(pyCMS) Opens an ICC profile file.
|
| 505 |
+
|
| 506 |
+
The PyCMSProfile object can be passed back into pyCMS for use in creating
|
| 507 |
+
transforms and such (as in ImageCms.buildTransformFromOpenProfiles()).
|
| 508 |
+
|
| 509 |
+
If ``profileFilename`` is not a valid filename for an ICC profile,
|
| 510 |
+
a :exc:`PyCMSError` will be raised.
|
| 511 |
+
|
| 512 |
+
:param profileFilename: String, as a valid filename path to the ICC profile
|
| 513 |
+
you wish to open, or a file-like object.
|
| 514 |
+
:returns: A CmsProfile class object.
|
| 515 |
+
:exception PyCMSError:
|
| 516 |
+
"""
|
| 517 |
+
|
| 518 |
+
try:
|
| 519 |
+
return ImageCmsProfile(profileFilename)
|
| 520 |
+
except (OSError, TypeError, ValueError) as v:
|
| 521 |
+
raise PyCMSError(v) from v
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def buildTransform(
|
| 525 |
+
inputProfile: _CmsProfileCompatible,
|
| 526 |
+
outputProfile: _CmsProfileCompatible,
|
| 527 |
+
inMode: str,
|
| 528 |
+
outMode: str,
|
| 529 |
+
renderingIntent: Intent = Intent.PERCEPTUAL,
|
| 530 |
+
flags: Flags = Flags.NONE,
|
| 531 |
+
) -> ImageCmsTransform:
|
| 532 |
+
"""
|
| 533 |
+
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
|
| 534 |
+
``outputProfile``. Use applyTransform to apply the transform to a given
|
| 535 |
+
image.
|
| 536 |
+
|
| 537 |
+
If the input or output profiles specified are not valid filenames, a
|
| 538 |
+
:exc:`PyCMSError` will be raised. If an error occurs during creation
|
| 539 |
+
of the transform, a :exc:`PyCMSError` will be raised.
|
| 540 |
+
|
| 541 |
+
If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
|
| 542 |
+
(or by pyCMS), a :exc:`PyCMSError` will be raised.
|
| 543 |
+
|
| 544 |
+
This function builds and returns an ICC transform from the ``inputProfile``
|
| 545 |
+
to the ``outputProfile`` using the ``renderingIntent`` to determine what to do
|
| 546 |
+
with out-of-gamut colors. It will ONLY work for converting images that
|
| 547 |
+
are in ``inMode`` to images that are in ``outMode`` color format (PIL mode,
|
| 548 |
+
i.e. "RGB", "RGBA", "CMYK", etc.).
|
| 549 |
+
|
| 550 |
+
Building the transform is a fair part of the overhead in
|
| 551 |
+
ImageCms.profileToProfile(), so if you're planning on converting multiple
|
| 552 |
+
images using the same input/output settings, this can save you time.
|
| 553 |
+
Once you have a transform object, it can be used with
|
| 554 |
+
ImageCms.applyProfile() to convert images without the need to re-compute
|
| 555 |
+
the lookup table for the transform.
|
| 556 |
+
|
| 557 |
+
The reason pyCMS returns a class object rather than a handle directly
|
| 558 |
+
to the transform is that it needs to keep track of the PIL input/output
|
| 559 |
+
modes that the transform is meant for. These attributes are stored in
|
| 560 |
+
the ``inMode`` and ``outMode`` attributes of the object (which can be
|
| 561 |
+
manually overridden if you really want to, but I don't know of any
|
| 562 |
+
time that would be of use, or would even work).
|
| 563 |
+
|
| 564 |
+
:param inputProfile: String, as a valid filename path to the ICC input
|
| 565 |
+
profile you wish to use for this transform, or a profile object
|
| 566 |
+
:param outputProfile: String, as a valid filename path to the ICC output
|
| 567 |
+
profile you wish to use for this transform, or a profile object
|
| 568 |
+
:param inMode: String, as a valid PIL mode that the appropriate profile
|
| 569 |
+
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
| 570 |
+
:param outMode: String, as a valid PIL mode that the appropriate profile
|
| 571 |
+
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
| 572 |
+
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
| 573 |
+
wish to use for the transform
|
| 574 |
+
|
| 575 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 576 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 577 |
+
ImageCms.Intent.SATURATION = 2
|
| 578 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 579 |
+
|
| 580 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 581 |
+
they do.
|
| 582 |
+
:param flags: Integer (0-...) specifying additional flags
|
| 583 |
+
:returns: A CmsTransform class object.
|
| 584 |
+
:exception PyCMSError:
|
| 585 |
+
"""
|
| 586 |
+
|
| 587 |
+
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
| 588 |
+
msg = "renderingIntent must be an integer between 0 and 3"
|
| 589 |
+
raise PyCMSError(msg)
|
| 590 |
+
|
| 591 |
+
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
| 592 |
+
msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
|
| 593 |
+
raise PyCMSError(msg)
|
| 594 |
+
|
| 595 |
+
try:
|
| 596 |
+
if not isinstance(inputProfile, ImageCmsProfile):
|
| 597 |
+
inputProfile = ImageCmsProfile(inputProfile)
|
| 598 |
+
if not isinstance(outputProfile, ImageCmsProfile):
|
| 599 |
+
outputProfile = ImageCmsProfile(outputProfile)
|
| 600 |
+
return ImageCmsTransform(
|
| 601 |
+
inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags
|
| 602 |
+
)
|
| 603 |
+
except (OSError, TypeError, ValueError) as v:
|
| 604 |
+
raise PyCMSError(v) from v
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def buildProofTransform(
|
| 608 |
+
inputProfile: _CmsProfileCompatible,
|
| 609 |
+
outputProfile: _CmsProfileCompatible,
|
| 610 |
+
proofProfile: _CmsProfileCompatible,
|
| 611 |
+
inMode: str,
|
| 612 |
+
outMode: str,
|
| 613 |
+
renderingIntent: Intent = Intent.PERCEPTUAL,
|
| 614 |
+
proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
|
| 615 |
+
flags: Flags = Flags.SOFTPROOFING,
|
| 616 |
+
) -> ImageCmsTransform:
|
| 617 |
+
"""
|
| 618 |
+
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
|
| 619 |
+
``outputProfile``, but tries to simulate the result that would be
|
| 620 |
+
obtained on the ``proofProfile`` device.
|
| 621 |
+
|
| 622 |
+
If the input, output, or proof profiles specified are not valid
|
| 623 |
+
filenames, a :exc:`PyCMSError` will be raised.
|
| 624 |
+
|
| 625 |
+
If an error occurs during creation of the transform,
|
| 626 |
+
a :exc:`PyCMSError` will be raised.
|
| 627 |
+
|
| 628 |
+
If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
|
| 629 |
+
(or by pyCMS), a :exc:`PyCMSError` will be raised.
|
| 630 |
+
|
| 631 |
+
This function builds and returns an ICC transform from the ``inputProfile``
|
| 632 |
+
to the ``outputProfile``, but tries to simulate the result that would be
|
| 633 |
+
obtained on the ``proofProfile`` device using ``renderingIntent`` and
|
| 634 |
+
``proofRenderingIntent`` to determine what to do with out-of-gamut
|
| 635 |
+
colors. This is known as "soft-proofing". It will ONLY work for
|
| 636 |
+
converting images that are in ``inMode`` to images that are in outMode
|
| 637 |
+
color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.).
|
| 638 |
+
|
| 639 |
+
Usage of the resulting transform object is exactly the same as with
|
| 640 |
+
ImageCms.buildTransform().
|
| 641 |
+
|
| 642 |
+
Proof profiling is generally used when using an output device to get a
|
| 643 |
+
good idea of what the final printed/displayed image would look like on
|
| 644 |
+
the ``proofProfile`` device when it's quicker and easier to use the
|
| 645 |
+
output device for judging color. Generally, this means that the
|
| 646 |
+
output device is a monitor, or a dye-sub printer (etc.), and the simulated
|
| 647 |
+
device is something more expensive, complicated, or time consuming
|
| 648 |
+
(making it difficult to make a real print for color judgement purposes).
|
| 649 |
+
|
| 650 |
+
Soft-proofing basically functions by adjusting the colors on the
|
| 651 |
+
output device to match the colors of the device being simulated. However,
|
| 652 |
+
when the simulated device has a much wider gamut than the output
|
| 653 |
+
device, you may obtain marginal results.
|
| 654 |
+
|
| 655 |
+
:param inputProfile: String, as a valid filename path to the ICC input
|
| 656 |
+
profile you wish to use for this transform, or a profile object
|
| 657 |
+
:param outputProfile: String, as a valid filename path to the ICC output
|
| 658 |
+
(monitor, usually) profile you wish to use for this transform, or a
|
| 659 |
+
profile object
|
| 660 |
+
:param proofProfile: String, as a valid filename path to the ICC proof
|
| 661 |
+
profile you wish to use for this transform, or a profile object
|
| 662 |
+
:param inMode: String, as a valid PIL mode that the appropriate profile
|
| 663 |
+
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
| 664 |
+
:param outMode: String, as a valid PIL mode that the appropriate profile
|
| 665 |
+
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
| 666 |
+
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
| 667 |
+
wish to use for the input->proof (simulated) transform
|
| 668 |
+
|
| 669 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 670 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 671 |
+
ImageCms.Intent.SATURATION = 2
|
| 672 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 673 |
+
|
| 674 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 675 |
+
they do.
|
| 676 |
+
:param proofRenderingIntent: Integer (0-3) specifying the rendering intent
|
| 677 |
+
you wish to use for proof->output transform
|
| 678 |
+
|
| 679 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 680 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 681 |
+
ImageCms.Intent.SATURATION = 2
|
| 682 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 683 |
+
|
| 684 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 685 |
+
they do.
|
| 686 |
+
:param flags: Integer (0-...) specifying additional flags
|
| 687 |
+
:returns: A CmsTransform class object.
|
| 688 |
+
:exception PyCMSError:
|
| 689 |
+
"""
|
| 690 |
+
|
| 691 |
+
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
| 692 |
+
msg = "renderingIntent must be an integer between 0 and 3"
|
| 693 |
+
raise PyCMSError(msg)
|
| 694 |
+
|
| 695 |
+
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
| 696 |
+
msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
|
| 697 |
+
raise PyCMSError(msg)
|
| 698 |
+
|
| 699 |
+
try:
|
| 700 |
+
if not isinstance(inputProfile, ImageCmsProfile):
|
| 701 |
+
inputProfile = ImageCmsProfile(inputProfile)
|
| 702 |
+
if not isinstance(outputProfile, ImageCmsProfile):
|
| 703 |
+
outputProfile = ImageCmsProfile(outputProfile)
|
| 704 |
+
if not isinstance(proofProfile, ImageCmsProfile):
|
| 705 |
+
proofProfile = ImageCmsProfile(proofProfile)
|
| 706 |
+
return ImageCmsTransform(
|
| 707 |
+
inputProfile,
|
| 708 |
+
outputProfile,
|
| 709 |
+
inMode,
|
| 710 |
+
outMode,
|
| 711 |
+
renderingIntent,
|
| 712 |
+
proofProfile,
|
| 713 |
+
proofRenderingIntent,
|
| 714 |
+
flags,
|
| 715 |
+
)
|
| 716 |
+
except (OSError, TypeError, ValueError) as v:
|
| 717 |
+
raise PyCMSError(v) from v
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
buildTransformFromOpenProfiles = buildTransform
|
| 721 |
+
buildProofTransformFromOpenProfiles = buildProofTransform
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def applyTransform(
|
| 725 |
+
im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False
|
| 726 |
+
) -> Image.Image | None:
|
| 727 |
+
"""
|
| 728 |
+
(pyCMS) Applies a transform to a given image.
|
| 729 |
+
|
| 730 |
+
If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised.
|
| 731 |
+
|
| 732 |
+
If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a
|
| 733 |
+
:exc:`PyCMSError` is raised.
|
| 734 |
+
|
| 735 |
+
If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not
|
| 736 |
+
supported by pyCMSdll or the profiles you used for the transform, a
|
| 737 |
+
:exc:`PyCMSError` is raised.
|
| 738 |
+
|
| 739 |
+
If an error occurs while the transform is being applied,
|
| 740 |
+
a :exc:`PyCMSError` is raised.
|
| 741 |
+
|
| 742 |
+
This function applies a pre-calculated transform (from
|
| 743 |
+
ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles())
|
| 744 |
+
to an image. The transform can be used for multiple images, saving
|
| 745 |
+
considerable calculation time if doing the same conversion multiple times.
|
| 746 |
+
|
| 747 |
+
If you want to modify im in-place instead of receiving a new image as
|
| 748 |
+
the return value, set ``inPlace`` to ``True``. This can only be done if
|
| 749 |
+
``transform.input_mode`` and ``transform.output_mode`` are the same, because we
|
| 750 |
+
can't change the mode in-place (the buffer sizes for some modes are
|
| 751 |
+
different). The default behavior is to return a new :py:class:`~PIL.Image.Image`
|
| 752 |
+
object of the same dimensions in mode ``transform.output_mode``.
|
| 753 |
+
|
| 754 |
+
:param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same
|
| 755 |
+
as the ``input_mode`` supported by the transform.
|
| 756 |
+
:param transform: A valid CmsTransform class object
|
| 757 |
+
:param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is
|
| 758 |
+
returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the
|
| 759 |
+
transform applied is returned (and ``im`` is not changed). The default is
|
| 760 |
+
``False``.
|
| 761 |
+
:returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object,
|
| 762 |
+
depending on the value of ``inPlace``. The profile will be returned in
|
| 763 |
+
the image's ``info['icc_profile']``.
|
| 764 |
+
:exception PyCMSError:
|
| 765 |
+
"""
|
| 766 |
+
|
| 767 |
+
try:
|
| 768 |
+
if inPlace:
|
| 769 |
+
transform.apply_in_place(im)
|
| 770 |
+
imOut = None
|
| 771 |
+
else:
|
| 772 |
+
imOut = transform.apply(im)
|
| 773 |
+
except (TypeError, ValueError) as v:
|
| 774 |
+
raise PyCMSError(v) from v
|
| 775 |
+
|
| 776 |
+
return imOut
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
def createProfile(
|
| 780 |
+
colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0
|
| 781 |
+
) -> core.CmsProfile:
|
| 782 |
+
"""
|
| 783 |
+
(pyCMS) Creates a profile.
|
| 784 |
+
|
| 785 |
+
If colorSpace not in ``["LAB", "XYZ", "sRGB"]``,
|
| 786 |
+
a :exc:`PyCMSError` is raised.
|
| 787 |
+
|
| 788 |
+
If using LAB and ``colorTemp`` is not a positive integer,
|
| 789 |
+
a :exc:`PyCMSError` is raised.
|
| 790 |
+
|
| 791 |
+
If an error occurs while creating the profile,
|
| 792 |
+
a :exc:`PyCMSError` is raised.
|
| 793 |
+
|
| 794 |
+
Use this function to create common profiles on-the-fly instead of
|
| 795 |
+
having to supply a profile on disk and knowing the path to it. It
|
| 796 |
+
returns a normal CmsProfile object that can be passed to
|
| 797 |
+
ImageCms.buildTransformFromOpenProfiles() to create a transform to apply
|
| 798 |
+
to images.
|
| 799 |
+
|
| 800 |
+
:param colorSpace: String, the color space of the profile you wish to
|
| 801 |
+
create.
|
| 802 |
+
Currently only "LAB", "XYZ", and "sRGB" are supported.
|
| 803 |
+
:param colorTemp: Positive number for the white point for the profile, in
|
| 804 |
+
degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
|
| 805 |
+
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
|
| 806 |
+
profiles, and is ignored for XYZ and sRGB.
|
| 807 |
+
:returns: A CmsProfile class object
|
| 808 |
+
:exception PyCMSError:
|
| 809 |
+
"""
|
| 810 |
+
|
| 811 |
+
if colorSpace not in ["LAB", "XYZ", "sRGB"]:
|
| 812 |
+
msg = (
|
| 813 |
+
f"Color space not supported for on-the-fly profile creation ({colorSpace})"
|
| 814 |
+
)
|
| 815 |
+
raise PyCMSError(msg)
|
| 816 |
+
|
| 817 |
+
if colorSpace == "LAB":
|
| 818 |
+
try:
|
| 819 |
+
colorTemp = float(colorTemp)
|
| 820 |
+
except (TypeError, ValueError) as e:
|
| 821 |
+
msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
|
| 822 |
+
raise PyCMSError(msg) from e
|
| 823 |
+
|
| 824 |
+
try:
|
| 825 |
+
return core.createProfile(colorSpace, colorTemp)
|
| 826 |
+
except (TypeError, ValueError) as v:
|
| 827 |
+
raise PyCMSError(v) from v
|
| 828 |
+
|
| 829 |
+
|
| 830 |
+
def getProfileName(profile: _CmsProfileCompatible) -> str:
|
| 831 |
+
"""
|
| 832 |
+
|
| 833 |
+
(pyCMS) Gets the internal product name for the given profile.
|
| 834 |
+
|
| 835 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile,
|
| 836 |
+
a :exc:`PyCMSError` is raised If an error occurs while trying
|
| 837 |
+
to obtain the name tag, a :exc:`PyCMSError` is raised.
|
| 838 |
+
|
| 839 |
+
Use this function to obtain the INTERNAL name of the profile (stored
|
| 840 |
+
in an ICC tag in the profile itself), usually the one used when the
|
| 841 |
+
profile was originally created. Sometimes this tag also contains
|
| 842 |
+
additional information supplied by the creator.
|
| 843 |
+
|
| 844 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 845 |
+
filename of an ICC profile.
|
| 846 |
+
:returns: A string containing the internal name of the profile as stored
|
| 847 |
+
in an ICC tag.
|
| 848 |
+
:exception PyCMSError:
|
| 849 |
+
"""
|
| 850 |
+
|
| 851 |
+
try:
|
| 852 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 853 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 854 |
+
profile = ImageCmsProfile(profile)
|
| 855 |
+
# do it in python, not c.
|
| 856 |
+
# // name was "%s - %s" (model, manufacturer) || Description ,
|
| 857 |
+
# // but if the Model and Manufacturer were the same or the model
|
| 858 |
+
# // was long, Just the model, in 1.x
|
| 859 |
+
model = profile.profile.model
|
| 860 |
+
manufacturer = profile.profile.manufacturer
|
| 861 |
+
|
| 862 |
+
if not (model or manufacturer):
|
| 863 |
+
return (profile.profile.profile_description or "") + "\n"
|
| 864 |
+
if not manufacturer or (model and len(model) > 30):
|
| 865 |
+
return f"{model}\n"
|
| 866 |
+
return f"{model} - {manufacturer}\n"
|
| 867 |
+
|
| 868 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 869 |
+
raise PyCMSError(v) from v
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
def getProfileInfo(profile: _CmsProfileCompatible) -> str:
|
| 873 |
+
"""
|
| 874 |
+
(pyCMS) Gets the internal product information for the given profile.
|
| 875 |
+
|
| 876 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile,
|
| 877 |
+
a :exc:`PyCMSError` is raised.
|
| 878 |
+
|
| 879 |
+
If an error occurs while trying to obtain the info tag,
|
| 880 |
+
a :exc:`PyCMSError` is raised.
|
| 881 |
+
|
| 882 |
+
Use this function to obtain the information stored in the profile's
|
| 883 |
+
info tag. This often contains details about the profile, and how it
|
| 884 |
+
was created, as supplied by the creator.
|
| 885 |
+
|
| 886 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 887 |
+
filename of an ICC profile.
|
| 888 |
+
:returns: A string containing the internal profile information stored in
|
| 889 |
+
an ICC tag.
|
| 890 |
+
:exception PyCMSError:
|
| 891 |
+
"""
|
| 892 |
+
|
| 893 |
+
try:
|
| 894 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 895 |
+
profile = ImageCmsProfile(profile)
|
| 896 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 897 |
+
# Python, not C. the white point bits weren't working well,
|
| 898 |
+
# so skipping.
|
| 899 |
+
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
|
| 900 |
+
description = profile.profile.profile_description
|
| 901 |
+
cpright = profile.profile.copyright
|
| 902 |
+
elements = [element for element in (description, cpright) if element]
|
| 903 |
+
return "\r\n\r\n".join(elements) + "\r\n\r\n"
|
| 904 |
+
|
| 905 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 906 |
+
raise PyCMSError(v) from v
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def getProfileCopyright(profile: _CmsProfileCompatible) -> str:
|
| 910 |
+
"""
|
| 911 |
+
(pyCMS) Gets the copyright for the given profile.
|
| 912 |
+
|
| 913 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
|
| 914 |
+
:exc:`PyCMSError` is raised.
|
| 915 |
+
|
| 916 |
+
If an error occurs while trying to obtain the copyright tag,
|
| 917 |
+
a :exc:`PyCMSError` is raised.
|
| 918 |
+
|
| 919 |
+
Use this function to obtain the information stored in the profile's
|
| 920 |
+
copyright tag.
|
| 921 |
+
|
| 922 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 923 |
+
filename of an ICC profile.
|
| 924 |
+
:returns: A string containing the internal profile information stored in
|
| 925 |
+
an ICC tag.
|
| 926 |
+
:exception PyCMSError:
|
| 927 |
+
"""
|
| 928 |
+
try:
|
| 929 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 930 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 931 |
+
profile = ImageCmsProfile(profile)
|
| 932 |
+
return (profile.profile.copyright or "") + "\n"
|
| 933 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 934 |
+
raise PyCMSError(v) from v
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
def getProfileManufacturer(profile: _CmsProfileCompatible) -> str:
|
| 938 |
+
"""
|
| 939 |
+
(pyCMS) Gets the manufacturer for the given profile.
|
| 940 |
+
|
| 941 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
|
| 942 |
+
:exc:`PyCMSError` is raised.
|
| 943 |
+
|
| 944 |
+
If an error occurs while trying to obtain the manufacturer tag, a
|
| 945 |
+
:exc:`PyCMSError` is raised.
|
| 946 |
+
|
| 947 |
+
Use this function to obtain the information stored in the profile's
|
| 948 |
+
manufacturer tag.
|
| 949 |
+
|
| 950 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 951 |
+
filename of an ICC profile.
|
| 952 |
+
:returns: A string containing the internal profile information stored in
|
| 953 |
+
an ICC tag.
|
| 954 |
+
:exception PyCMSError:
|
| 955 |
+
"""
|
| 956 |
+
try:
|
| 957 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 958 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 959 |
+
profile = ImageCmsProfile(profile)
|
| 960 |
+
return (profile.profile.manufacturer or "") + "\n"
|
| 961 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 962 |
+
raise PyCMSError(v) from v
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
def getProfileModel(profile: _CmsProfileCompatible) -> str:
|
| 966 |
+
"""
|
| 967 |
+
(pyCMS) Gets the model for the given profile.
|
| 968 |
+
|
| 969 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
|
| 970 |
+
:exc:`PyCMSError` is raised.
|
| 971 |
+
|
| 972 |
+
If an error occurs while trying to obtain the model tag,
|
| 973 |
+
a :exc:`PyCMSError` is raised.
|
| 974 |
+
|
| 975 |
+
Use this function to obtain the information stored in the profile's
|
| 976 |
+
model tag.
|
| 977 |
+
|
| 978 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 979 |
+
filename of an ICC profile.
|
| 980 |
+
:returns: A string containing the internal profile information stored in
|
| 981 |
+
an ICC tag.
|
| 982 |
+
:exception PyCMSError:
|
| 983 |
+
"""
|
| 984 |
+
|
| 985 |
+
try:
|
| 986 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 987 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 988 |
+
profile = ImageCmsProfile(profile)
|
| 989 |
+
return (profile.profile.model or "") + "\n"
|
| 990 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 991 |
+
raise PyCMSError(v) from v
|
| 992 |
+
|
| 993 |
+
|
| 994 |
+
def getProfileDescription(profile: _CmsProfileCompatible) -> str:
|
| 995 |
+
"""
|
| 996 |
+
(pyCMS) Gets the description for the given profile.
|
| 997 |
+
|
| 998 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
|
| 999 |
+
:exc:`PyCMSError` is raised.
|
| 1000 |
+
|
| 1001 |
+
If an error occurs while trying to obtain the description tag,
|
| 1002 |
+
a :exc:`PyCMSError` is raised.
|
| 1003 |
+
|
| 1004 |
+
Use this function to obtain the information stored in the profile's
|
| 1005 |
+
description tag.
|
| 1006 |
+
|
| 1007 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 1008 |
+
filename of an ICC profile.
|
| 1009 |
+
:returns: A string containing the internal profile information stored in an
|
| 1010 |
+
ICC tag.
|
| 1011 |
+
:exception PyCMSError:
|
| 1012 |
+
"""
|
| 1013 |
+
|
| 1014 |
+
try:
|
| 1015 |
+
# add an extra newline to preserve pyCMS compatibility
|
| 1016 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 1017 |
+
profile = ImageCmsProfile(profile)
|
| 1018 |
+
return (profile.profile.profile_description or "") + "\n"
|
| 1019 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 1020 |
+
raise PyCMSError(v) from v
|
| 1021 |
+
|
| 1022 |
+
|
| 1023 |
+
def getDefaultIntent(profile: _CmsProfileCompatible) -> int:
|
| 1024 |
+
"""
|
| 1025 |
+
(pyCMS) Gets the default intent name for the given profile.
|
| 1026 |
+
|
| 1027 |
+
If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
|
| 1028 |
+
:exc:`PyCMSError` is raised.
|
| 1029 |
+
|
| 1030 |
+
If an error occurs while trying to obtain the default intent, a
|
| 1031 |
+
:exc:`PyCMSError` is raised.
|
| 1032 |
+
|
| 1033 |
+
Use this function to determine the default (and usually best optimized)
|
| 1034 |
+
rendering intent for this profile. Most profiles support multiple
|
| 1035 |
+
rendering intents, but are intended mostly for one type of conversion.
|
| 1036 |
+
If you wish to use a different intent than returned, use
|
| 1037 |
+
ImageCms.isIntentSupported() to verify it will work first.
|
| 1038 |
+
|
| 1039 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 1040 |
+
filename of an ICC profile.
|
| 1041 |
+
:returns: Integer 0-3 specifying the default rendering intent for this
|
| 1042 |
+
profile.
|
| 1043 |
+
|
| 1044 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 1045 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 1046 |
+
ImageCms.Intent.SATURATION = 2
|
| 1047 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 1048 |
+
|
| 1049 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 1050 |
+
they do.
|
| 1051 |
+
:exception PyCMSError:
|
| 1052 |
+
"""
|
| 1053 |
+
|
| 1054 |
+
try:
|
| 1055 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 1056 |
+
profile = ImageCmsProfile(profile)
|
| 1057 |
+
return profile.profile.rendering_intent
|
| 1058 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 1059 |
+
raise PyCMSError(v) from v
|
| 1060 |
+
|
| 1061 |
+
|
| 1062 |
+
def isIntentSupported(
|
| 1063 |
+
profile: _CmsProfileCompatible, intent: Intent, direction: Direction
|
| 1064 |
+
) -> Literal[-1, 1]:
|
| 1065 |
+
"""
|
| 1066 |
+
(pyCMS) Checks if a given intent is supported.
|
| 1067 |
+
|
| 1068 |
+
Use this function to verify that you can use your desired
|
| 1069 |
+
``intent`` with ``profile``, and that ``profile`` can be used for the
|
| 1070 |
+
input/output/proof profile as you desire.
|
| 1071 |
+
|
| 1072 |
+
Some profiles are created specifically for one "direction", can cannot
|
| 1073 |
+
be used for others. Some profiles can only be used for certain
|
| 1074 |
+
rendering intents, so it's best to either verify this before trying
|
| 1075 |
+
to create a transform with them (using this function), or catch the
|
| 1076 |
+
potential :exc:`PyCMSError` that will occur if they don't
|
| 1077 |
+
support the modes you select.
|
| 1078 |
+
|
| 1079 |
+
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
| 1080 |
+
filename of an ICC profile.
|
| 1081 |
+
:param intent: Integer (0-3) specifying the rendering intent you wish to
|
| 1082 |
+
use with this profile
|
| 1083 |
+
|
| 1084 |
+
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
|
| 1085 |
+
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
|
| 1086 |
+
ImageCms.Intent.SATURATION = 2
|
| 1087 |
+
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
|
| 1088 |
+
|
| 1089 |
+
see the pyCMS documentation for details on rendering intents and what
|
| 1090 |
+
they do.
|
| 1091 |
+
:param direction: Integer specifying if the profile is to be used for
|
| 1092 |
+
input, output, or proof
|
| 1093 |
+
|
| 1094 |
+
INPUT = 0 (or use ImageCms.Direction.INPUT)
|
| 1095 |
+
OUTPUT = 1 (or use ImageCms.Direction.OUTPUT)
|
| 1096 |
+
PROOF = 2 (or use ImageCms.Direction.PROOF)
|
| 1097 |
+
|
| 1098 |
+
:returns: 1 if the intent/direction are supported, -1 if they are not.
|
| 1099 |
+
:exception PyCMSError:
|
| 1100 |
+
"""
|
| 1101 |
+
|
| 1102 |
+
try:
|
| 1103 |
+
if not isinstance(profile, ImageCmsProfile):
|
| 1104 |
+
profile = ImageCmsProfile(profile)
|
| 1105 |
+
# FIXME: I get different results for the same data w. different
|
| 1106 |
+
# compilers. Bug in LittleCMS or in the binding?
|
| 1107 |
+
if profile.profile.is_intent_supported(intent, direction):
|
| 1108 |
+
return 1
|
| 1109 |
+
else:
|
| 1110 |
+
return -1
|
| 1111 |
+
except (AttributeError, OSError, TypeError, ValueError) as v:
|
| 1112 |
+
raise PyCMSError(v) from v
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def versions() -> tuple[str, str | None, str, str]:
|
| 1116 |
+
"""
|
| 1117 |
+
(pyCMS) Fetches versions.
|
| 1118 |
+
"""
|
| 1119 |
+
|
| 1120 |
+
deprecate(
|
| 1121 |
+
"PIL.ImageCms.versions()",
|
| 1122 |
+
12,
|
| 1123 |
+
'(PIL.features.version("littlecms2"), sys.version, PIL.__version__)',
|
| 1124 |
+
)
|
| 1125 |
+
return _VERSION, core.littlecms_version, sys.version.split()[0], __version__
|
Lib/site-packages/PIL/ImageColor.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# map CSS3-style colour description strings to RGB
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2002-10-24 fl Added support for CSS-style color strings
|
| 9 |
+
# 2002-12-15 fl Added RGBA support
|
| 10 |
+
# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2
|
| 11 |
+
# 2004-07-19 fl Fixed gray/grey spelling issues
|
| 12 |
+
# 2009-03-05 fl Fixed rounding error in grayscale calculation
|
| 13 |
+
#
|
| 14 |
+
# Copyright (c) 2002-2004 by Secret Labs AB
|
| 15 |
+
# Copyright (c) 2002-2004 by Fredrik Lundh
|
| 16 |
+
#
|
| 17 |
+
# See the README file for information on usage and redistribution.
|
| 18 |
+
#
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import re
|
| 22 |
+
from functools import lru_cache
|
| 23 |
+
|
| 24 |
+
from . import Image
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@lru_cache
|
| 28 |
+
def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
|
| 29 |
+
"""
|
| 30 |
+
Convert a color string to an RGB or RGBA tuple. If the string cannot be
|
| 31 |
+
parsed, this function raises a :py:exc:`ValueError` exception.
|
| 32 |
+
|
| 33 |
+
.. versionadded:: 1.1.4
|
| 34 |
+
|
| 35 |
+
:param color: A color string
|
| 36 |
+
:return: ``(red, green, blue[, alpha])``
|
| 37 |
+
"""
|
| 38 |
+
if len(color) > 100:
|
| 39 |
+
msg = "color specifier is too long"
|
| 40 |
+
raise ValueError(msg)
|
| 41 |
+
color = color.lower()
|
| 42 |
+
|
| 43 |
+
rgb = colormap.get(color, None)
|
| 44 |
+
if rgb:
|
| 45 |
+
if isinstance(rgb, tuple):
|
| 46 |
+
return rgb
|
| 47 |
+
rgb_tuple = getrgb(rgb)
|
| 48 |
+
assert len(rgb_tuple) == 3
|
| 49 |
+
colormap[color] = rgb_tuple
|
| 50 |
+
return rgb_tuple
|
| 51 |
+
|
| 52 |
+
# check for known string formats
|
| 53 |
+
if re.match("#[a-f0-9]{3}$", color):
|
| 54 |
+
return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16)
|
| 55 |
+
|
| 56 |
+
if re.match("#[a-f0-9]{4}$", color):
|
| 57 |
+
return (
|
| 58 |
+
int(color[1] * 2, 16),
|
| 59 |
+
int(color[2] * 2, 16),
|
| 60 |
+
int(color[3] * 2, 16),
|
| 61 |
+
int(color[4] * 2, 16),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
if re.match("#[a-f0-9]{6}$", color):
|
| 65 |
+
return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
|
| 66 |
+
|
| 67 |
+
if re.match("#[a-f0-9]{8}$", color):
|
| 68 |
+
return (
|
| 69 |
+
int(color[1:3], 16),
|
| 70 |
+
int(color[3:5], 16),
|
| 71 |
+
int(color[5:7], 16),
|
| 72 |
+
int(color[7:9], 16),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
| 76 |
+
if m:
|
| 77 |
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
| 78 |
+
|
| 79 |
+
m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
| 80 |
+
if m:
|
| 81 |
+
return (
|
| 82 |
+
int((int(m.group(1)) * 255) / 100.0 + 0.5),
|
| 83 |
+
int((int(m.group(2)) * 255) / 100.0 + 0.5),
|
| 84 |
+
int((int(m.group(3)) * 255) / 100.0 + 0.5),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
m = re.match(
|
| 88 |
+
r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
| 89 |
+
)
|
| 90 |
+
if m:
|
| 91 |
+
from colorsys import hls_to_rgb
|
| 92 |
+
|
| 93 |
+
rgb_floats = hls_to_rgb(
|
| 94 |
+
float(m.group(1)) / 360.0,
|
| 95 |
+
float(m.group(3)) / 100.0,
|
| 96 |
+
float(m.group(2)) / 100.0,
|
| 97 |
+
)
|
| 98 |
+
return (
|
| 99 |
+
int(rgb_floats[0] * 255 + 0.5),
|
| 100 |
+
int(rgb_floats[1] * 255 + 0.5),
|
| 101 |
+
int(rgb_floats[2] * 255 + 0.5),
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
m = re.match(
|
| 105 |
+
r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
| 106 |
+
)
|
| 107 |
+
if m:
|
| 108 |
+
from colorsys import hsv_to_rgb
|
| 109 |
+
|
| 110 |
+
rgb_floats = hsv_to_rgb(
|
| 111 |
+
float(m.group(1)) / 360.0,
|
| 112 |
+
float(m.group(2)) / 100.0,
|
| 113 |
+
float(m.group(3)) / 100.0,
|
| 114 |
+
)
|
| 115 |
+
return (
|
| 116 |
+
int(rgb_floats[0] * 255 + 0.5),
|
| 117 |
+
int(rgb_floats[1] * 255 + 0.5),
|
| 118 |
+
int(rgb_floats[2] * 255 + 0.5),
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
| 122 |
+
if m:
|
| 123 |
+
return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
|
| 124 |
+
msg = f"unknown color specifier: {repr(color)}"
|
| 125 |
+
raise ValueError(msg)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@lru_cache
|
| 129 |
+
def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
|
| 130 |
+
"""
|
| 131 |
+
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
|
| 132 |
+
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
|
| 133 |
+
not color or a palette image, converts the RGB value to a grayscale value.
|
| 134 |
+
If the string cannot be parsed, this function raises a :py:exc:`ValueError`
|
| 135 |
+
exception.
|
| 136 |
+
|
| 137 |
+
.. versionadded:: 1.1.4
|
| 138 |
+
|
| 139 |
+
:param color: A color string
|
| 140 |
+
:param mode: Convert result to this mode
|
| 141 |
+
:return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])``
|
| 142 |
+
"""
|
| 143 |
+
# same as getrgb, but converts the result to the given mode
|
| 144 |
+
rgb, alpha = getrgb(color), 255
|
| 145 |
+
if len(rgb) == 4:
|
| 146 |
+
alpha = rgb[3]
|
| 147 |
+
rgb = rgb[:3]
|
| 148 |
+
|
| 149 |
+
if mode == "HSV":
|
| 150 |
+
from colorsys import rgb_to_hsv
|
| 151 |
+
|
| 152 |
+
r, g, b = rgb
|
| 153 |
+
h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
|
| 154 |
+
return int(h * 255), int(s * 255), int(v * 255)
|
| 155 |
+
elif Image.getmodebase(mode) == "L":
|
| 156 |
+
r, g, b = rgb
|
| 157 |
+
# ITU-R Recommendation 601-2 for nonlinear RGB
|
| 158 |
+
# scaled to 24 bits to match the convert's implementation.
|
| 159 |
+
graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
|
| 160 |
+
if mode[-1] == "A":
|
| 161 |
+
return graylevel, alpha
|
| 162 |
+
return graylevel
|
| 163 |
+
elif mode[-1] == "A":
|
| 164 |
+
return rgb + (alpha,)
|
| 165 |
+
return rgb
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
colormap: dict[str, str | tuple[int, int, int]] = {
|
| 169 |
+
# X11 colour table from https://drafts.csswg.org/css-color-4/, with
|
| 170 |
+
# gray/grey spelling issues fixed. This is a superset of HTML 4.0
|
| 171 |
+
# colour names used in CSS 1.
|
| 172 |
+
"aliceblue": "#f0f8ff",
|
| 173 |
+
"antiquewhite": "#faebd7",
|
| 174 |
+
"aqua": "#00ffff",
|
| 175 |
+
"aquamarine": "#7fffd4",
|
| 176 |
+
"azure": "#f0ffff",
|
| 177 |
+
"beige": "#f5f5dc",
|
| 178 |
+
"bisque": "#ffe4c4",
|
| 179 |
+
"black": "#000000",
|
| 180 |
+
"blanchedalmond": "#ffebcd",
|
| 181 |
+
"blue": "#0000ff",
|
| 182 |
+
"blueviolet": "#8a2be2",
|
| 183 |
+
"brown": "#a52a2a",
|
| 184 |
+
"burlywood": "#deb887",
|
| 185 |
+
"cadetblue": "#5f9ea0",
|
| 186 |
+
"chartreuse": "#7fff00",
|
| 187 |
+
"chocolate": "#d2691e",
|
| 188 |
+
"coral": "#ff7f50",
|
| 189 |
+
"cornflowerblue": "#6495ed",
|
| 190 |
+
"cornsilk": "#fff8dc",
|
| 191 |
+
"crimson": "#dc143c",
|
| 192 |
+
"cyan": "#00ffff",
|
| 193 |
+
"darkblue": "#00008b",
|
| 194 |
+
"darkcyan": "#008b8b",
|
| 195 |
+
"darkgoldenrod": "#b8860b",
|
| 196 |
+
"darkgray": "#a9a9a9",
|
| 197 |
+
"darkgrey": "#a9a9a9",
|
| 198 |
+
"darkgreen": "#006400",
|
| 199 |
+
"darkkhaki": "#bdb76b",
|
| 200 |
+
"darkmagenta": "#8b008b",
|
| 201 |
+
"darkolivegreen": "#556b2f",
|
| 202 |
+
"darkorange": "#ff8c00",
|
| 203 |
+
"darkorchid": "#9932cc",
|
| 204 |
+
"darkred": "#8b0000",
|
| 205 |
+
"darksalmon": "#e9967a",
|
| 206 |
+
"darkseagreen": "#8fbc8f",
|
| 207 |
+
"darkslateblue": "#483d8b",
|
| 208 |
+
"darkslategray": "#2f4f4f",
|
| 209 |
+
"darkslategrey": "#2f4f4f",
|
| 210 |
+
"darkturquoise": "#00ced1",
|
| 211 |
+
"darkviolet": "#9400d3",
|
| 212 |
+
"deeppink": "#ff1493",
|
| 213 |
+
"deepskyblue": "#00bfff",
|
| 214 |
+
"dimgray": "#696969",
|
| 215 |
+
"dimgrey": "#696969",
|
| 216 |
+
"dodgerblue": "#1e90ff",
|
| 217 |
+
"firebrick": "#b22222",
|
| 218 |
+
"floralwhite": "#fffaf0",
|
| 219 |
+
"forestgreen": "#228b22",
|
| 220 |
+
"fuchsia": "#ff00ff",
|
| 221 |
+
"gainsboro": "#dcdcdc",
|
| 222 |
+
"ghostwhite": "#f8f8ff",
|
| 223 |
+
"gold": "#ffd700",
|
| 224 |
+
"goldenrod": "#daa520",
|
| 225 |
+
"gray": "#808080",
|
| 226 |
+
"grey": "#808080",
|
| 227 |
+
"green": "#008000",
|
| 228 |
+
"greenyellow": "#adff2f",
|
| 229 |
+
"honeydew": "#f0fff0",
|
| 230 |
+
"hotpink": "#ff69b4",
|
| 231 |
+
"indianred": "#cd5c5c",
|
| 232 |
+
"indigo": "#4b0082",
|
| 233 |
+
"ivory": "#fffff0",
|
| 234 |
+
"khaki": "#f0e68c",
|
| 235 |
+
"lavender": "#e6e6fa",
|
| 236 |
+
"lavenderblush": "#fff0f5",
|
| 237 |
+
"lawngreen": "#7cfc00",
|
| 238 |
+
"lemonchiffon": "#fffacd",
|
| 239 |
+
"lightblue": "#add8e6",
|
| 240 |
+
"lightcoral": "#f08080",
|
| 241 |
+
"lightcyan": "#e0ffff",
|
| 242 |
+
"lightgoldenrodyellow": "#fafad2",
|
| 243 |
+
"lightgreen": "#90ee90",
|
| 244 |
+
"lightgray": "#d3d3d3",
|
| 245 |
+
"lightgrey": "#d3d3d3",
|
| 246 |
+
"lightpink": "#ffb6c1",
|
| 247 |
+
"lightsalmon": "#ffa07a",
|
| 248 |
+
"lightseagreen": "#20b2aa",
|
| 249 |
+
"lightskyblue": "#87cefa",
|
| 250 |
+
"lightslategray": "#778899",
|
| 251 |
+
"lightslategrey": "#778899",
|
| 252 |
+
"lightsteelblue": "#b0c4de",
|
| 253 |
+
"lightyellow": "#ffffe0",
|
| 254 |
+
"lime": "#00ff00",
|
| 255 |
+
"limegreen": "#32cd32",
|
| 256 |
+
"linen": "#faf0e6",
|
| 257 |
+
"magenta": "#ff00ff",
|
| 258 |
+
"maroon": "#800000",
|
| 259 |
+
"mediumaquamarine": "#66cdaa",
|
| 260 |
+
"mediumblue": "#0000cd",
|
| 261 |
+
"mediumorchid": "#ba55d3",
|
| 262 |
+
"mediumpurple": "#9370db",
|
| 263 |
+
"mediumseagreen": "#3cb371",
|
| 264 |
+
"mediumslateblue": "#7b68ee",
|
| 265 |
+
"mediumspringgreen": "#00fa9a",
|
| 266 |
+
"mediumturquoise": "#48d1cc",
|
| 267 |
+
"mediumvioletred": "#c71585",
|
| 268 |
+
"midnightblue": "#191970",
|
| 269 |
+
"mintcream": "#f5fffa",
|
| 270 |
+
"mistyrose": "#ffe4e1",
|
| 271 |
+
"moccasin": "#ffe4b5",
|
| 272 |
+
"navajowhite": "#ffdead",
|
| 273 |
+
"navy": "#000080",
|
| 274 |
+
"oldlace": "#fdf5e6",
|
| 275 |
+
"olive": "#808000",
|
| 276 |
+
"olivedrab": "#6b8e23",
|
| 277 |
+
"orange": "#ffa500",
|
| 278 |
+
"orangered": "#ff4500",
|
| 279 |
+
"orchid": "#da70d6",
|
| 280 |
+
"palegoldenrod": "#eee8aa",
|
| 281 |
+
"palegreen": "#98fb98",
|
| 282 |
+
"paleturquoise": "#afeeee",
|
| 283 |
+
"palevioletred": "#db7093",
|
| 284 |
+
"papayawhip": "#ffefd5",
|
| 285 |
+
"peachpuff": "#ffdab9",
|
| 286 |
+
"peru": "#cd853f",
|
| 287 |
+
"pink": "#ffc0cb",
|
| 288 |
+
"plum": "#dda0dd",
|
| 289 |
+
"powderblue": "#b0e0e6",
|
| 290 |
+
"purple": "#800080",
|
| 291 |
+
"rebeccapurple": "#663399",
|
| 292 |
+
"red": "#ff0000",
|
| 293 |
+
"rosybrown": "#bc8f8f",
|
| 294 |
+
"royalblue": "#4169e1",
|
| 295 |
+
"saddlebrown": "#8b4513",
|
| 296 |
+
"salmon": "#fa8072",
|
| 297 |
+
"sandybrown": "#f4a460",
|
| 298 |
+
"seagreen": "#2e8b57",
|
| 299 |
+
"seashell": "#fff5ee",
|
| 300 |
+
"sienna": "#a0522d",
|
| 301 |
+
"silver": "#c0c0c0",
|
| 302 |
+
"skyblue": "#87ceeb",
|
| 303 |
+
"slateblue": "#6a5acd",
|
| 304 |
+
"slategray": "#708090",
|
| 305 |
+
"slategrey": "#708090",
|
| 306 |
+
"snow": "#fffafa",
|
| 307 |
+
"springgreen": "#00ff7f",
|
| 308 |
+
"steelblue": "#4682b4",
|
| 309 |
+
"tan": "#d2b48c",
|
| 310 |
+
"teal": "#008080",
|
| 311 |
+
"thistle": "#d8bfd8",
|
| 312 |
+
"tomato": "#ff6347",
|
| 313 |
+
"turquoise": "#40e0d0",
|
| 314 |
+
"violet": "#ee82ee",
|
| 315 |
+
"wheat": "#f5deb3",
|
| 316 |
+
"white": "#ffffff",
|
| 317 |
+
"whitesmoke": "#f5f5f5",
|
| 318 |
+
"yellow": "#ffff00",
|
| 319 |
+
"yellowgreen": "#9acd32",
|
| 320 |
+
}
|
Lib/site-packages/PIL/ImageDraw.py
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# drawing interface operations
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-04-13 fl Created (experimental)
|
| 9 |
+
# 1996-08-07 fl Filled polygons, ellipses.
|
| 10 |
+
# 1996-08-13 fl Added text support
|
| 11 |
+
# 1998-06-28 fl Handle I and F images
|
| 12 |
+
# 1998-12-29 fl Added arc; use arc primitive to draw ellipses
|
| 13 |
+
# 1999-01-10 fl Added shape stuff (experimental)
|
| 14 |
+
# 1999-02-06 fl Added bitmap support
|
| 15 |
+
# 1999-02-11 fl Changed all primitives to take options
|
| 16 |
+
# 1999-02-20 fl Fixed backwards compatibility
|
| 17 |
+
# 2000-10-12 fl Copy on write, when necessary
|
| 18 |
+
# 2001-02-18 fl Use default ink for bitmap/text also in fill mode
|
| 19 |
+
# 2002-10-24 fl Added support for CSS-style color strings
|
| 20 |
+
# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing
|
| 21 |
+
# 2002-12-11 fl Refactored low-level drawing API (work in progress)
|
| 22 |
+
# 2004-08-26 fl Made Draw() a factory function, added getdraw() support
|
| 23 |
+
# 2004-09-04 fl Added width support to line primitive
|
| 24 |
+
# 2004-09-10 fl Added font mode handling
|
| 25 |
+
# 2006-06-19 fl Added font bearing support (getmask2)
|
| 26 |
+
#
|
| 27 |
+
# Copyright (c) 1997-2006 by Secret Labs AB
|
| 28 |
+
# Copyright (c) 1996-2006 by Fredrik Lundh
|
| 29 |
+
#
|
| 30 |
+
# See the README file for information on usage and redistribution.
|
| 31 |
+
#
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import math
|
| 35 |
+
import struct
|
| 36 |
+
from collections.abc import Sequence
|
| 37 |
+
from types import ModuleType
|
| 38 |
+
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast
|
| 39 |
+
|
| 40 |
+
from . import Image, ImageColor
|
| 41 |
+
from ._deprecate import deprecate
|
| 42 |
+
from ._typing import Coords
|
| 43 |
+
|
| 44 |
+
# experimental access to the outline API
|
| 45 |
+
Outline: Callable[[], Image.core._Outline] | None
|
| 46 |
+
try:
|
| 47 |
+
Outline = Image.core.outline
|
| 48 |
+
except AttributeError:
|
| 49 |
+
Outline = None
|
| 50 |
+
|
| 51 |
+
if TYPE_CHECKING:
|
| 52 |
+
from . import ImageDraw2, ImageFont
|
| 53 |
+
|
| 54 |
+
_Ink = Union[float, tuple[int, ...], str]
|
| 55 |
+
|
| 56 |
+
"""
|
| 57 |
+
A simple 2D drawing interface for PIL images.
|
| 58 |
+
<p>
|
| 59 |
+
Application code should use the <b>Draw</b> factory, instead of
|
| 60 |
+
directly.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ImageDraw:
|
| 65 |
+
font: (
|
| 66 |
+
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
|
| 67 |
+
) = None
|
| 68 |
+
|
| 69 |
+
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
|
| 70 |
+
"""
|
| 71 |
+
Create a drawing instance.
|
| 72 |
+
|
| 73 |
+
:param im: The image to draw in.
|
| 74 |
+
:param mode: Optional mode to use for color values. For RGB
|
| 75 |
+
images, this argument can be RGB or RGBA (to blend the
|
| 76 |
+
drawing into the image). For all other modes, this argument
|
| 77 |
+
must be the same as the image mode. If omitted, the mode
|
| 78 |
+
defaults to the mode of the image.
|
| 79 |
+
"""
|
| 80 |
+
im.load()
|
| 81 |
+
if im.readonly:
|
| 82 |
+
im._copy() # make it writeable
|
| 83 |
+
blend = 0
|
| 84 |
+
if mode is None:
|
| 85 |
+
mode = im.mode
|
| 86 |
+
if mode != im.mode:
|
| 87 |
+
if mode == "RGBA" and im.mode == "RGB":
|
| 88 |
+
blend = 1
|
| 89 |
+
else:
|
| 90 |
+
msg = "mode mismatch"
|
| 91 |
+
raise ValueError(msg)
|
| 92 |
+
if mode == "P":
|
| 93 |
+
self.palette = im.palette
|
| 94 |
+
else:
|
| 95 |
+
self.palette = None
|
| 96 |
+
self._image = im
|
| 97 |
+
self.im = im.im
|
| 98 |
+
self.draw = Image.core.draw(self.im, blend)
|
| 99 |
+
self.mode = mode
|
| 100 |
+
if mode in ("I", "F"):
|
| 101 |
+
self.ink = self.draw.draw_ink(1)
|
| 102 |
+
else:
|
| 103 |
+
self.ink = self.draw.draw_ink(-1)
|
| 104 |
+
if mode in ("1", "P", "I", "F"):
|
| 105 |
+
# FIXME: fix Fill2 to properly support matte for I+F images
|
| 106 |
+
self.fontmode = "1"
|
| 107 |
+
else:
|
| 108 |
+
self.fontmode = "L" # aliasing is okay for other modes
|
| 109 |
+
self.fill = False
|
| 110 |
+
|
| 111 |
+
def getfont(
|
| 112 |
+
self,
|
| 113 |
+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
|
| 114 |
+
"""
|
| 115 |
+
Get the current default font.
|
| 116 |
+
|
| 117 |
+
To set the default font for this ImageDraw instance::
|
| 118 |
+
|
| 119 |
+
from PIL import ImageDraw, ImageFont
|
| 120 |
+
draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
| 121 |
+
|
| 122 |
+
To set the default font for all future ImageDraw instances::
|
| 123 |
+
|
| 124 |
+
from PIL import ImageDraw, ImageFont
|
| 125 |
+
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
| 126 |
+
|
| 127 |
+
If the current default font is ``None``,
|
| 128 |
+
it is initialized with ``ImageFont.load_default()``.
|
| 129 |
+
|
| 130 |
+
:returns: An image font."""
|
| 131 |
+
if not self.font:
|
| 132 |
+
# FIXME: should add a font repository
|
| 133 |
+
from . import ImageFont
|
| 134 |
+
|
| 135 |
+
self.font = ImageFont.load_default()
|
| 136 |
+
return self.font
|
| 137 |
+
|
| 138 |
+
def _getfont(
|
| 139 |
+
self, font_size: float | None
|
| 140 |
+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
|
| 141 |
+
if font_size is not None:
|
| 142 |
+
from . import ImageFont
|
| 143 |
+
|
| 144 |
+
return ImageFont.load_default(font_size)
|
| 145 |
+
else:
|
| 146 |
+
return self.getfont()
|
| 147 |
+
|
| 148 |
+
def _getink(
|
| 149 |
+
self, ink: _Ink | None, fill: _Ink | None = None
|
| 150 |
+
) -> tuple[int | None, int | None]:
|
| 151 |
+
result_ink = None
|
| 152 |
+
result_fill = None
|
| 153 |
+
if ink is None and fill is None:
|
| 154 |
+
if self.fill:
|
| 155 |
+
result_fill = self.ink
|
| 156 |
+
else:
|
| 157 |
+
result_ink = self.ink
|
| 158 |
+
else:
|
| 159 |
+
if ink is not None:
|
| 160 |
+
if isinstance(ink, str):
|
| 161 |
+
ink = ImageColor.getcolor(ink, self.mode)
|
| 162 |
+
if self.palette and isinstance(ink, tuple):
|
| 163 |
+
ink = self.palette.getcolor(ink, self._image)
|
| 164 |
+
result_ink = self.draw.draw_ink(ink)
|
| 165 |
+
if fill is not None:
|
| 166 |
+
if isinstance(fill, str):
|
| 167 |
+
fill = ImageColor.getcolor(fill, self.mode)
|
| 168 |
+
if self.palette and isinstance(fill, tuple):
|
| 169 |
+
fill = self.palette.getcolor(fill, self._image)
|
| 170 |
+
result_fill = self.draw.draw_ink(fill)
|
| 171 |
+
return result_ink, result_fill
|
| 172 |
+
|
| 173 |
+
def arc(
|
| 174 |
+
self,
|
| 175 |
+
xy: Coords,
|
| 176 |
+
start: float,
|
| 177 |
+
end: float,
|
| 178 |
+
fill: _Ink | None = None,
|
| 179 |
+
width: int = 1,
|
| 180 |
+
) -> None:
|
| 181 |
+
"""Draw an arc."""
|
| 182 |
+
ink, fill = self._getink(fill)
|
| 183 |
+
if ink is not None:
|
| 184 |
+
self.draw.draw_arc(xy, start, end, ink, width)
|
| 185 |
+
|
| 186 |
+
def bitmap(
|
| 187 |
+
self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
|
| 188 |
+
) -> None:
|
| 189 |
+
"""Draw a bitmap."""
|
| 190 |
+
bitmap.load()
|
| 191 |
+
ink, fill = self._getink(fill)
|
| 192 |
+
if ink is None:
|
| 193 |
+
ink = fill
|
| 194 |
+
if ink is not None:
|
| 195 |
+
self.draw.draw_bitmap(xy, bitmap.im, ink)
|
| 196 |
+
|
| 197 |
+
def chord(
|
| 198 |
+
self,
|
| 199 |
+
xy: Coords,
|
| 200 |
+
start: float,
|
| 201 |
+
end: float,
|
| 202 |
+
fill: _Ink | None = None,
|
| 203 |
+
outline: _Ink | None = None,
|
| 204 |
+
width: int = 1,
|
| 205 |
+
) -> None:
|
| 206 |
+
"""Draw a chord."""
|
| 207 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 208 |
+
if fill_ink is not None:
|
| 209 |
+
self.draw.draw_chord(xy, start, end, fill_ink, 1)
|
| 210 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 211 |
+
self.draw.draw_chord(xy, start, end, ink, 0, width)
|
| 212 |
+
|
| 213 |
+
def ellipse(
|
| 214 |
+
self,
|
| 215 |
+
xy: Coords,
|
| 216 |
+
fill: _Ink | None = None,
|
| 217 |
+
outline: _Ink | None = None,
|
| 218 |
+
width: int = 1,
|
| 219 |
+
) -> None:
|
| 220 |
+
"""Draw an ellipse."""
|
| 221 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 222 |
+
if fill_ink is not None:
|
| 223 |
+
self.draw.draw_ellipse(xy, fill_ink, 1)
|
| 224 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 225 |
+
self.draw.draw_ellipse(xy, ink, 0, width)
|
| 226 |
+
|
| 227 |
+
def circle(
|
| 228 |
+
self,
|
| 229 |
+
xy: Sequence[float],
|
| 230 |
+
radius: float,
|
| 231 |
+
fill: _Ink | None = None,
|
| 232 |
+
outline: _Ink | None = None,
|
| 233 |
+
width: int = 1,
|
| 234 |
+
) -> None:
|
| 235 |
+
"""Draw a circle given center coordinates and a radius."""
|
| 236 |
+
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
|
| 237 |
+
self.ellipse(ellipse_xy, fill, outline, width)
|
| 238 |
+
|
| 239 |
+
def line(
|
| 240 |
+
self,
|
| 241 |
+
xy: Coords,
|
| 242 |
+
fill: _Ink | None = None,
|
| 243 |
+
width: int = 0,
|
| 244 |
+
joint: str | None = None,
|
| 245 |
+
) -> None:
|
| 246 |
+
"""Draw a line, or a connected sequence of line segments."""
|
| 247 |
+
ink = self._getink(fill)[0]
|
| 248 |
+
if ink is not None:
|
| 249 |
+
self.draw.draw_lines(xy, ink, width)
|
| 250 |
+
if joint == "curve" and width > 4:
|
| 251 |
+
points: Sequence[Sequence[float]]
|
| 252 |
+
if isinstance(xy[0], (list, tuple)):
|
| 253 |
+
points = cast(Sequence[Sequence[float]], xy)
|
| 254 |
+
else:
|
| 255 |
+
points = [
|
| 256 |
+
cast(Sequence[float], tuple(xy[i : i + 2]))
|
| 257 |
+
for i in range(0, len(xy), 2)
|
| 258 |
+
]
|
| 259 |
+
for i in range(1, len(points) - 1):
|
| 260 |
+
point = points[i]
|
| 261 |
+
angles = [
|
| 262 |
+
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
|
| 263 |
+
% 360
|
| 264 |
+
for start, end in (
|
| 265 |
+
(points[i - 1], point),
|
| 266 |
+
(point, points[i + 1]),
|
| 267 |
+
)
|
| 268 |
+
]
|
| 269 |
+
if angles[0] == angles[1]:
|
| 270 |
+
# This is a straight line, so no joint is required
|
| 271 |
+
continue
|
| 272 |
+
|
| 273 |
+
def coord_at_angle(
|
| 274 |
+
coord: Sequence[float], angle: float
|
| 275 |
+
) -> tuple[float, ...]:
|
| 276 |
+
x, y = coord
|
| 277 |
+
angle -= 90
|
| 278 |
+
distance = width / 2 - 1
|
| 279 |
+
return tuple(
|
| 280 |
+
p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
|
| 281 |
+
for p, p_d in (
|
| 282 |
+
(x, distance * math.cos(math.radians(angle))),
|
| 283 |
+
(y, distance * math.sin(math.radians(angle))),
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
flipped = (
|
| 288 |
+
angles[1] > angles[0] and angles[1] - 180 > angles[0]
|
| 289 |
+
) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
|
| 290 |
+
coords = [
|
| 291 |
+
(point[0] - width / 2 + 1, point[1] - width / 2 + 1),
|
| 292 |
+
(point[0] + width / 2 - 1, point[1] + width / 2 - 1),
|
| 293 |
+
]
|
| 294 |
+
if flipped:
|
| 295 |
+
start, end = (angles[1] + 90, angles[0] + 90)
|
| 296 |
+
else:
|
| 297 |
+
start, end = (angles[0] - 90, angles[1] - 90)
|
| 298 |
+
self.pieslice(coords, start - 90, end - 90, fill)
|
| 299 |
+
|
| 300 |
+
if width > 8:
|
| 301 |
+
# Cover potential gaps between the line and the joint
|
| 302 |
+
if flipped:
|
| 303 |
+
gap_coords = [
|
| 304 |
+
coord_at_angle(point, angles[0] + 90),
|
| 305 |
+
point,
|
| 306 |
+
coord_at_angle(point, angles[1] + 90),
|
| 307 |
+
]
|
| 308 |
+
else:
|
| 309 |
+
gap_coords = [
|
| 310 |
+
coord_at_angle(point, angles[0] - 90),
|
| 311 |
+
point,
|
| 312 |
+
coord_at_angle(point, angles[1] - 90),
|
| 313 |
+
]
|
| 314 |
+
self.line(gap_coords, fill, width=3)
|
| 315 |
+
|
| 316 |
+
def shape(
|
| 317 |
+
self,
|
| 318 |
+
shape: Image.core._Outline,
|
| 319 |
+
fill: _Ink | None = None,
|
| 320 |
+
outline: _Ink | None = None,
|
| 321 |
+
) -> None:
|
| 322 |
+
"""(Experimental) Draw a shape."""
|
| 323 |
+
shape.close()
|
| 324 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 325 |
+
if fill_ink is not None:
|
| 326 |
+
self.draw.draw_outline(shape, fill_ink, 1)
|
| 327 |
+
if ink is not None and ink != fill_ink:
|
| 328 |
+
self.draw.draw_outline(shape, ink, 0)
|
| 329 |
+
|
| 330 |
+
def pieslice(
|
| 331 |
+
self,
|
| 332 |
+
xy: Coords,
|
| 333 |
+
start: float,
|
| 334 |
+
end: float,
|
| 335 |
+
fill: _Ink | None = None,
|
| 336 |
+
outline: _Ink | None = None,
|
| 337 |
+
width: int = 1,
|
| 338 |
+
) -> None:
|
| 339 |
+
"""Draw a pieslice."""
|
| 340 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 341 |
+
if fill_ink is not None:
|
| 342 |
+
self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
|
| 343 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 344 |
+
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
|
| 345 |
+
|
| 346 |
+
def point(self, xy: Coords, fill: _Ink | None = None) -> None:
|
| 347 |
+
"""Draw one or more individual pixels."""
|
| 348 |
+
ink, fill = self._getink(fill)
|
| 349 |
+
if ink is not None:
|
| 350 |
+
self.draw.draw_points(xy, ink)
|
| 351 |
+
|
| 352 |
+
def polygon(
|
| 353 |
+
self,
|
| 354 |
+
xy: Coords,
|
| 355 |
+
fill: _Ink | None = None,
|
| 356 |
+
outline: _Ink | None = None,
|
| 357 |
+
width: int = 1,
|
| 358 |
+
) -> None:
|
| 359 |
+
"""Draw a polygon."""
|
| 360 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 361 |
+
if fill_ink is not None:
|
| 362 |
+
self.draw.draw_polygon(xy, fill_ink, 1)
|
| 363 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 364 |
+
if width == 1:
|
| 365 |
+
self.draw.draw_polygon(xy, ink, 0, width)
|
| 366 |
+
elif self.im is not None:
|
| 367 |
+
# To avoid expanding the polygon outwards,
|
| 368 |
+
# use the fill as a mask
|
| 369 |
+
mask = Image.new("1", self.im.size)
|
| 370 |
+
mask_ink = self._getink(1)[0]
|
| 371 |
+
|
| 372 |
+
fill_im = mask.copy()
|
| 373 |
+
draw = Draw(fill_im)
|
| 374 |
+
draw.draw.draw_polygon(xy, mask_ink, 1)
|
| 375 |
+
|
| 376 |
+
ink_im = mask.copy()
|
| 377 |
+
draw = Draw(ink_im)
|
| 378 |
+
width = width * 2 - 1
|
| 379 |
+
draw.draw.draw_polygon(xy, mask_ink, 0, width)
|
| 380 |
+
|
| 381 |
+
mask.paste(ink_im, mask=fill_im)
|
| 382 |
+
|
| 383 |
+
im = Image.new(self.mode, self.im.size)
|
| 384 |
+
draw = Draw(im)
|
| 385 |
+
draw.draw.draw_polygon(xy, ink, 0, width)
|
| 386 |
+
self.im.paste(im.im, (0, 0) + im.size, mask.im)
|
| 387 |
+
|
| 388 |
+
def regular_polygon(
|
| 389 |
+
self,
|
| 390 |
+
bounding_circle: Sequence[Sequence[float] | float],
|
| 391 |
+
n_sides: int,
|
| 392 |
+
rotation: float = 0,
|
| 393 |
+
fill: _Ink | None = None,
|
| 394 |
+
outline: _Ink | None = None,
|
| 395 |
+
width: int = 1,
|
| 396 |
+
) -> None:
|
| 397 |
+
"""Draw a regular polygon."""
|
| 398 |
+
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
| 399 |
+
self.polygon(xy, fill, outline, width)
|
| 400 |
+
|
| 401 |
+
def rectangle(
|
| 402 |
+
self,
|
| 403 |
+
xy: Coords,
|
| 404 |
+
fill: _Ink | None = None,
|
| 405 |
+
outline: _Ink | None = None,
|
| 406 |
+
width: int = 1,
|
| 407 |
+
) -> None:
|
| 408 |
+
"""Draw a rectangle."""
|
| 409 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 410 |
+
if fill_ink is not None:
|
| 411 |
+
self.draw.draw_rectangle(xy, fill_ink, 1)
|
| 412 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 413 |
+
self.draw.draw_rectangle(xy, ink, 0, width)
|
| 414 |
+
|
| 415 |
+
def rounded_rectangle(
|
| 416 |
+
self,
|
| 417 |
+
xy: Coords,
|
| 418 |
+
radius: float = 0,
|
| 419 |
+
fill: _Ink | None = None,
|
| 420 |
+
outline: _Ink | None = None,
|
| 421 |
+
width: int = 1,
|
| 422 |
+
*,
|
| 423 |
+
corners: tuple[bool, bool, bool, bool] | None = None,
|
| 424 |
+
) -> None:
|
| 425 |
+
"""Draw a rounded rectangle."""
|
| 426 |
+
if isinstance(xy[0], (list, tuple)):
|
| 427 |
+
(x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
|
| 428 |
+
else:
|
| 429 |
+
x0, y0, x1, y1 = cast(Sequence[float], xy)
|
| 430 |
+
if x1 < x0:
|
| 431 |
+
msg = "x1 must be greater than or equal to x0"
|
| 432 |
+
raise ValueError(msg)
|
| 433 |
+
if y1 < y0:
|
| 434 |
+
msg = "y1 must be greater than or equal to y0"
|
| 435 |
+
raise ValueError(msg)
|
| 436 |
+
if corners is None:
|
| 437 |
+
corners = (True, True, True, True)
|
| 438 |
+
|
| 439 |
+
d = radius * 2
|
| 440 |
+
|
| 441 |
+
x0 = round(x0)
|
| 442 |
+
y0 = round(y0)
|
| 443 |
+
x1 = round(x1)
|
| 444 |
+
y1 = round(y1)
|
| 445 |
+
full_x, full_y = False, False
|
| 446 |
+
if all(corners):
|
| 447 |
+
full_x = d >= x1 - x0 - 1
|
| 448 |
+
if full_x:
|
| 449 |
+
# The two left and two right corners are joined
|
| 450 |
+
d = x1 - x0
|
| 451 |
+
full_y = d >= y1 - y0 - 1
|
| 452 |
+
if full_y:
|
| 453 |
+
# The two top and two bottom corners are joined
|
| 454 |
+
d = y1 - y0
|
| 455 |
+
if full_x and full_y:
|
| 456 |
+
# If all corners are joined, that is a circle
|
| 457 |
+
return self.ellipse(xy, fill, outline, width)
|
| 458 |
+
|
| 459 |
+
if d == 0 or not any(corners):
|
| 460 |
+
# If the corners have no curve,
|
| 461 |
+
# or there are no corners,
|
| 462 |
+
# that is a rectangle
|
| 463 |
+
return self.rectangle(xy, fill, outline, width)
|
| 464 |
+
|
| 465 |
+
r = int(d // 2)
|
| 466 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 467 |
+
|
| 468 |
+
def draw_corners(pieslice: bool) -> None:
|
| 469 |
+
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
|
| 470 |
+
if full_x:
|
| 471 |
+
# Draw top and bottom halves
|
| 472 |
+
parts = (
|
| 473 |
+
((x0, y0, x0 + d, y0 + d), 180, 360),
|
| 474 |
+
((x0, y1 - d, x0 + d, y1), 0, 180),
|
| 475 |
+
)
|
| 476 |
+
elif full_y:
|
| 477 |
+
# Draw left and right halves
|
| 478 |
+
parts = (
|
| 479 |
+
((x0, y0, x0 + d, y0 + d), 90, 270),
|
| 480 |
+
((x1 - d, y0, x1, y0 + d), 270, 90),
|
| 481 |
+
)
|
| 482 |
+
else:
|
| 483 |
+
# Draw four separate corners
|
| 484 |
+
parts = tuple(
|
| 485 |
+
part
|
| 486 |
+
for i, part in enumerate(
|
| 487 |
+
(
|
| 488 |
+
((x0, y0, x0 + d, y0 + d), 180, 270),
|
| 489 |
+
((x1 - d, y0, x1, y0 + d), 270, 360),
|
| 490 |
+
((x1 - d, y1 - d, x1, y1), 0, 90),
|
| 491 |
+
((x0, y1 - d, x0 + d, y1), 90, 180),
|
| 492 |
+
)
|
| 493 |
+
)
|
| 494 |
+
if corners[i]
|
| 495 |
+
)
|
| 496 |
+
for part in parts:
|
| 497 |
+
if pieslice:
|
| 498 |
+
self.draw.draw_pieslice(*(part + (fill_ink, 1)))
|
| 499 |
+
else:
|
| 500 |
+
self.draw.draw_arc(*(part + (ink, width)))
|
| 501 |
+
|
| 502 |
+
if fill_ink is not None:
|
| 503 |
+
draw_corners(True)
|
| 504 |
+
|
| 505 |
+
if full_x:
|
| 506 |
+
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
|
| 507 |
+
elif x1 - r - 1 > x0 + r + 1:
|
| 508 |
+
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
|
| 509 |
+
if not full_x and not full_y:
|
| 510 |
+
left = [x0, y0, x0 + r, y1]
|
| 511 |
+
if corners[0]:
|
| 512 |
+
left[1] += r + 1
|
| 513 |
+
if corners[3]:
|
| 514 |
+
left[3] -= r + 1
|
| 515 |
+
self.draw.draw_rectangle(left, fill_ink, 1)
|
| 516 |
+
|
| 517 |
+
right = [x1 - r, y0, x1, y1]
|
| 518 |
+
if corners[1]:
|
| 519 |
+
right[1] += r + 1
|
| 520 |
+
if corners[2]:
|
| 521 |
+
right[3] -= r + 1
|
| 522 |
+
self.draw.draw_rectangle(right, fill_ink, 1)
|
| 523 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 524 |
+
draw_corners(False)
|
| 525 |
+
|
| 526 |
+
if not full_x:
|
| 527 |
+
top = [x0, y0, x1, y0 + width - 1]
|
| 528 |
+
if corners[0]:
|
| 529 |
+
top[0] += r + 1
|
| 530 |
+
if corners[1]:
|
| 531 |
+
top[2] -= r + 1
|
| 532 |
+
self.draw.draw_rectangle(top, ink, 1)
|
| 533 |
+
|
| 534 |
+
bottom = [x0, y1 - width + 1, x1, y1]
|
| 535 |
+
if corners[3]:
|
| 536 |
+
bottom[0] += r + 1
|
| 537 |
+
if corners[2]:
|
| 538 |
+
bottom[2] -= r + 1
|
| 539 |
+
self.draw.draw_rectangle(bottom, ink, 1)
|
| 540 |
+
if not full_y:
|
| 541 |
+
left = [x0, y0, x0 + width - 1, y1]
|
| 542 |
+
if corners[0]:
|
| 543 |
+
left[1] += r + 1
|
| 544 |
+
if corners[3]:
|
| 545 |
+
left[3] -= r + 1
|
| 546 |
+
self.draw.draw_rectangle(left, ink, 1)
|
| 547 |
+
|
| 548 |
+
right = [x1 - width + 1, y0, x1, y1]
|
| 549 |
+
if corners[1]:
|
| 550 |
+
right[1] += r + 1
|
| 551 |
+
if corners[2]:
|
| 552 |
+
right[3] -= r + 1
|
| 553 |
+
self.draw.draw_rectangle(right, ink, 1)
|
| 554 |
+
|
| 555 |
+
def _multiline_check(self, text: AnyStr) -> bool:
|
| 556 |
+
split_character = "\n" if isinstance(text, str) else b"\n"
|
| 557 |
+
|
| 558 |
+
return split_character in text
|
| 559 |
+
|
| 560 |
+
def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
|
| 561 |
+
return text.split("\n" if isinstance(text, str) else b"\n")
|
| 562 |
+
|
| 563 |
+
def _multiline_spacing(
|
| 564 |
+
self,
|
| 565 |
+
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
|
| 566 |
+
spacing: float,
|
| 567 |
+
stroke_width: float,
|
| 568 |
+
) -> float:
|
| 569 |
+
return (
|
| 570 |
+
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
|
| 571 |
+
+ stroke_width
|
| 572 |
+
+ spacing
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
def text(
|
| 576 |
+
self,
|
| 577 |
+
xy: tuple[float, float],
|
| 578 |
+
text: AnyStr,
|
| 579 |
+
fill: _Ink | None = None,
|
| 580 |
+
font: (
|
| 581 |
+
ImageFont.ImageFont
|
| 582 |
+
| ImageFont.FreeTypeFont
|
| 583 |
+
| ImageFont.TransposedFont
|
| 584 |
+
| None
|
| 585 |
+
) = None,
|
| 586 |
+
anchor: str | None = None,
|
| 587 |
+
spacing: float = 4,
|
| 588 |
+
align: str = "left",
|
| 589 |
+
direction: str | None = None,
|
| 590 |
+
features: list[str] | None = None,
|
| 591 |
+
language: str | None = None,
|
| 592 |
+
stroke_width: float = 0,
|
| 593 |
+
stroke_fill: _Ink | None = None,
|
| 594 |
+
embedded_color: bool = False,
|
| 595 |
+
*args: Any,
|
| 596 |
+
**kwargs: Any,
|
| 597 |
+
) -> None:
|
| 598 |
+
"""Draw text."""
|
| 599 |
+
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
| 600 |
+
msg = "Embedded color supported only in RGB and RGBA modes"
|
| 601 |
+
raise ValueError(msg)
|
| 602 |
+
|
| 603 |
+
if font is None:
|
| 604 |
+
font = self._getfont(kwargs.get("font_size"))
|
| 605 |
+
|
| 606 |
+
if self._multiline_check(text):
|
| 607 |
+
return self.multiline_text(
|
| 608 |
+
xy,
|
| 609 |
+
text,
|
| 610 |
+
fill,
|
| 611 |
+
font,
|
| 612 |
+
anchor,
|
| 613 |
+
spacing,
|
| 614 |
+
align,
|
| 615 |
+
direction,
|
| 616 |
+
features,
|
| 617 |
+
language,
|
| 618 |
+
stroke_width,
|
| 619 |
+
stroke_fill,
|
| 620 |
+
embedded_color,
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
def getink(fill: _Ink | None) -> int:
|
| 624 |
+
ink, fill_ink = self._getink(fill)
|
| 625 |
+
if ink is None:
|
| 626 |
+
assert fill_ink is not None
|
| 627 |
+
return fill_ink
|
| 628 |
+
return ink
|
| 629 |
+
|
| 630 |
+
def draw_text(ink: int, stroke_width: float = 0) -> None:
|
| 631 |
+
mode = self.fontmode
|
| 632 |
+
if stroke_width == 0 and embedded_color:
|
| 633 |
+
mode = "RGBA"
|
| 634 |
+
coord = []
|
| 635 |
+
for i in range(2):
|
| 636 |
+
coord.append(int(xy[i]))
|
| 637 |
+
start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
|
| 638 |
+
try:
|
| 639 |
+
mask, offset = font.getmask2( # type: ignore[union-attr,misc]
|
| 640 |
+
text,
|
| 641 |
+
mode,
|
| 642 |
+
direction=direction,
|
| 643 |
+
features=features,
|
| 644 |
+
language=language,
|
| 645 |
+
stroke_width=stroke_width,
|
| 646 |
+
anchor=anchor,
|
| 647 |
+
ink=ink,
|
| 648 |
+
start=start,
|
| 649 |
+
*args,
|
| 650 |
+
**kwargs,
|
| 651 |
+
)
|
| 652 |
+
coord = [coord[0] + offset[0], coord[1] + offset[1]]
|
| 653 |
+
except AttributeError:
|
| 654 |
+
try:
|
| 655 |
+
mask = font.getmask( # type: ignore[misc]
|
| 656 |
+
text,
|
| 657 |
+
mode,
|
| 658 |
+
direction,
|
| 659 |
+
features,
|
| 660 |
+
language,
|
| 661 |
+
stroke_width,
|
| 662 |
+
anchor,
|
| 663 |
+
ink,
|
| 664 |
+
start=start,
|
| 665 |
+
*args,
|
| 666 |
+
**kwargs,
|
| 667 |
+
)
|
| 668 |
+
except TypeError:
|
| 669 |
+
mask = font.getmask(text)
|
| 670 |
+
if mode == "RGBA":
|
| 671 |
+
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
|
| 672 |
+
# extract mask and set text alpha
|
| 673 |
+
color, mask = mask, mask.getband(3)
|
| 674 |
+
ink_alpha = struct.pack("i", ink)[3]
|
| 675 |
+
color.fillband(3, ink_alpha)
|
| 676 |
+
x, y = coord
|
| 677 |
+
if self.im is not None:
|
| 678 |
+
self.im.paste(
|
| 679 |
+
color, (x, y, x + mask.size[0], y + mask.size[1]), mask
|
| 680 |
+
)
|
| 681 |
+
else:
|
| 682 |
+
self.draw.draw_bitmap(coord, mask, ink)
|
| 683 |
+
|
| 684 |
+
ink = getink(fill)
|
| 685 |
+
if ink is not None:
|
| 686 |
+
stroke_ink = None
|
| 687 |
+
if stroke_width:
|
| 688 |
+
stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
|
| 689 |
+
|
| 690 |
+
if stroke_ink is not None:
|
| 691 |
+
# Draw stroked text
|
| 692 |
+
draw_text(stroke_ink, stroke_width)
|
| 693 |
+
|
| 694 |
+
# Draw normal text
|
| 695 |
+
draw_text(ink, 0)
|
| 696 |
+
else:
|
| 697 |
+
# Only draw normal text
|
| 698 |
+
draw_text(ink)
|
| 699 |
+
|
| 700 |
+
def multiline_text(
|
| 701 |
+
self,
|
| 702 |
+
xy: tuple[float, float],
|
| 703 |
+
text: AnyStr,
|
| 704 |
+
fill: _Ink | None = None,
|
| 705 |
+
font: (
|
| 706 |
+
ImageFont.ImageFont
|
| 707 |
+
| ImageFont.FreeTypeFont
|
| 708 |
+
| ImageFont.TransposedFont
|
| 709 |
+
| None
|
| 710 |
+
) = None,
|
| 711 |
+
anchor: str | None = None,
|
| 712 |
+
spacing: float = 4,
|
| 713 |
+
align: str = "left",
|
| 714 |
+
direction: str | None = None,
|
| 715 |
+
features: list[str] | None = None,
|
| 716 |
+
language: str | None = None,
|
| 717 |
+
stroke_width: float = 0,
|
| 718 |
+
stroke_fill: _Ink | None = None,
|
| 719 |
+
embedded_color: bool = False,
|
| 720 |
+
*,
|
| 721 |
+
font_size: float | None = None,
|
| 722 |
+
) -> None:
|
| 723 |
+
if direction == "ttb":
|
| 724 |
+
msg = "ttb direction is unsupported for multiline text"
|
| 725 |
+
raise ValueError(msg)
|
| 726 |
+
|
| 727 |
+
if anchor is None:
|
| 728 |
+
anchor = "la"
|
| 729 |
+
elif len(anchor) != 2:
|
| 730 |
+
msg = "anchor must be a 2 character string"
|
| 731 |
+
raise ValueError(msg)
|
| 732 |
+
elif anchor[1] in "tb":
|
| 733 |
+
msg = "anchor not supported for multiline text"
|
| 734 |
+
raise ValueError(msg)
|
| 735 |
+
|
| 736 |
+
if font is None:
|
| 737 |
+
font = self._getfont(font_size)
|
| 738 |
+
|
| 739 |
+
widths = []
|
| 740 |
+
max_width: float = 0
|
| 741 |
+
lines = self._multiline_split(text)
|
| 742 |
+
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
|
| 743 |
+
for line in lines:
|
| 744 |
+
line_width = self.textlength(
|
| 745 |
+
line, font, direction=direction, features=features, language=language
|
| 746 |
+
)
|
| 747 |
+
widths.append(line_width)
|
| 748 |
+
max_width = max(max_width, line_width)
|
| 749 |
+
|
| 750 |
+
top = xy[1]
|
| 751 |
+
if anchor[1] == "m":
|
| 752 |
+
top -= (len(lines) - 1) * line_spacing / 2.0
|
| 753 |
+
elif anchor[1] == "d":
|
| 754 |
+
top -= (len(lines) - 1) * line_spacing
|
| 755 |
+
|
| 756 |
+
for idx, line in enumerate(lines):
|
| 757 |
+
left = xy[0]
|
| 758 |
+
width_difference = max_width - widths[idx]
|
| 759 |
+
|
| 760 |
+
# first align left by anchor
|
| 761 |
+
if anchor[0] == "m":
|
| 762 |
+
left -= width_difference / 2.0
|
| 763 |
+
elif anchor[0] == "r":
|
| 764 |
+
left -= width_difference
|
| 765 |
+
|
| 766 |
+
# then align by align parameter
|
| 767 |
+
if align == "left":
|
| 768 |
+
pass
|
| 769 |
+
elif align == "center":
|
| 770 |
+
left += width_difference / 2.0
|
| 771 |
+
elif align == "right":
|
| 772 |
+
left += width_difference
|
| 773 |
+
else:
|
| 774 |
+
msg = 'align must be "left", "center" or "right"'
|
| 775 |
+
raise ValueError(msg)
|
| 776 |
+
|
| 777 |
+
self.text(
|
| 778 |
+
(left, top),
|
| 779 |
+
line,
|
| 780 |
+
fill,
|
| 781 |
+
font,
|
| 782 |
+
anchor,
|
| 783 |
+
direction=direction,
|
| 784 |
+
features=features,
|
| 785 |
+
language=language,
|
| 786 |
+
stroke_width=stroke_width,
|
| 787 |
+
stroke_fill=stroke_fill,
|
| 788 |
+
embedded_color=embedded_color,
|
| 789 |
+
)
|
| 790 |
+
top += line_spacing
|
| 791 |
+
|
| 792 |
+
def textlength(
|
| 793 |
+
self,
|
| 794 |
+
text: AnyStr,
|
| 795 |
+
font: (
|
| 796 |
+
ImageFont.ImageFont
|
| 797 |
+
| ImageFont.FreeTypeFont
|
| 798 |
+
| ImageFont.TransposedFont
|
| 799 |
+
| None
|
| 800 |
+
) = None,
|
| 801 |
+
direction: str | None = None,
|
| 802 |
+
features: list[str] | None = None,
|
| 803 |
+
language: str | None = None,
|
| 804 |
+
embedded_color: bool = False,
|
| 805 |
+
*,
|
| 806 |
+
font_size: float | None = None,
|
| 807 |
+
) -> float:
|
| 808 |
+
"""Get the length of a given string, in pixels with 1/64 precision."""
|
| 809 |
+
if self._multiline_check(text):
|
| 810 |
+
msg = "can't measure length of multiline text"
|
| 811 |
+
raise ValueError(msg)
|
| 812 |
+
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
| 813 |
+
msg = "Embedded color supported only in RGB and RGBA modes"
|
| 814 |
+
raise ValueError(msg)
|
| 815 |
+
|
| 816 |
+
if font is None:
|
| 817 |
+
font = self._getfont(font_size)
|
| 818 |
+
mode = "RGBA" if embedded_color else self.fontmode
|
| 819 |
+
return font.getlength(text, mode, direction, features, language)
|
| 820 |
+
|
| 821 |
+
def textbbox(
|
| 822 |
+
self,
|
| 823 |
+
xy: tuple[float, float],
|
| 824 |
+
text: AnyStr,
|
| 825 |
+
font: (
|
| 826 |
+
ImageFont.ImageFont
|
| 827 |
+
| ImageFont.FreeTypeFont
|
| 828 |
+
| ImageFont.TransposedFont
|
| 829 |
+
| None
|
| 830 |
+
) = None,
|
| 831 |
+
anchor: str | None = None,
|
| 832 |
+
spacing: float = 4,
|
| 833 |
+
align: str = "left",
|
| 834 |
+
direction: str | None = None,
|
| 835 |
+
features: list[str] | None = None,
|
| 836 |
+
language: str | None = None,
|
| 837 |
+
stroke_width: float = 0,
|
| 838 |
+
embedded_color: bool = False,
|
| 839 |
+
*,
|
| 840 |
+
font_size: float | None = None,
|
| 841 |
+
) -> tuple[float, float, float, float]:
|
| 842 |
+
"""Get the bounding box of a given string, in pixels."""
|
| 843 |
+
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
| 844 |
+
msg = "Embedded color supported only in RGB and RGBA modes"
|
| 845 |
+
raise ValueError(msg)
|
| 846 |
+
|
| 847 |
+
if font is None:
|
| 848 |
+
font = self._getfont(font_size)
|
| 849 |
+
|
| 850 |
+
if self._multiline_check(text):
|
| 851 |
+
return self.multiline_textbbox(
|
| 852 |
+
xy,
|
| 853 |
+
text,
|
| 854 |
+
font,
|
| 855 |
+
anchor,
|
| 856 |
+
spacing,
|
| 857 |
+
align,
|
| 858 |
+
direction,
|
| 859 |
+
features,
|
| 860 |
+
language,
|
| 861 |
+
stroke_width,
|
| 862 |
+
embedded_color,
|
| 863 |
+
)
|
| 864 |
+
|
| 865 |
+
mode = "RGBA" if embedded_color else self.fontmode
|
| 866 |
+
bbox = font.getbbox(
|
| 867 |
+
text, mode, direction, features, language, stroke_width, anchor
|
| 868 |
+
)
|
| 869 |
+
return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1]
|
| 870 |
+
|
| 871 |
+
def multiline_textbbox(
|
| 872 |
+
self,
|
| 873 |
+
xy: tuple[float, float],
|
| 874 |
+
text: AnyStr,
|
| 875 |
+
font: (
|
| 876 |
+
ImageFont.ImageFont
|
| 877 |
+
| ImageFont.FreeTypeFont
|
| 878 |
+
| ImageFont.TransposedFont
|
| 879 |
+
| None
|
| 880 |
+
) = None,
|
| 881 |
+
anchor: str | None = None,
|
| 882 |
+
spacing: float = 4,
|
| 883 |
+
align: str = "left",
|
| 884 |
+
direction: str | None = None,
|
| 885 |
+
features: list[str] | None = None,
|
| 886 |
+
language: str | None = None,
|
| 887 |
+
stroke_width: float = 0,
|
| 888 |
+
embedded_color: bool = False,
|
| 889 |
+
*,
|
| 890 |
+
font_size: float | None = None,
|
| 891 |
+
) -> tuple[float, float, float, float]:
|
| 892 |
+
if direction == "ttb":
|
| 893 |
+
msg = "ttb direction is unsupported for multiline text"
|
| 894 |
+
raise ValueError(msg)
|
| 895 |
+
|
| 896 |
+
if anchor is None:
|
| 897 |
+
anchor = "la"
|
| 898 |
+
elif len(anchor) != 2:
|
| 899 |
+
msg = "anchor must be a 2 character string"
|
| 900 |
+
raise ValueError(msg)
|
| 901 |
+
elif anchor[1] in "tb":
|
| 902 |
+
msg = "anchor not supported for multiline text"
|
| 903 |
+
raise ValueError(msg)
|
| 904 |
+
|
| 905 |
+
if font is None:
|
| 906 |
+
font = self._getfont(font_size)
|
| 907 |
+
|
| 908 |
+
widths = []
|
| 909 |
+
max_width: float = 0
|
| 910 |
+
lines = self._multiline_split(text)
|
| 911 |
+
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
|
| 912 |
+
for line in lines:
|
| 913 |
+
line_width = self.textlength(
|
| 914 |
+
line,
|
| 915 |
+
font,
|
| 916 |
+
direction=direction,
|
| 917 |
+
features=features,
|
| 918 |
+
language=language,
|
| 919 |
+
embedded_color=embedded_color,
|
| 920 |
+
)
|
| 921 |
+
widths.append(line_width)
|
| 922 |
+
max_width = max(max_width, line_width)
|
| 923 |
+
|
| 924 |
+
top = xy[1]
|
| 925 |
+
if anchor[1] == "m":
|
| 926 |
+
top -= (len(lines) - 1) * line_spacing / 2.0
|
| 927 |
+
elif anchor[1] == "d":
|
| 928 |
+
top -= (len(lines) - 1) * line_spacing
|
| 929 |
+
|
| 930 |
+
bbox: tuple[float, float, float, float] | None = None
|
| 931 |
+
|
| 932 |
+
for idx, line in enumerate(lines):
|
| 933 |
+
left = xy[0]
|
| 934 |
+
width_difference = max_width - widths[idx]
|
| 935 |
+
|
| 936 |
+
# first align left by anchor
|
| 937 |
+
if anchor[0] == "m":
|
| 938 |
+
left -= width_difference / 2.0
|
| 939 |
+
elif anchor[0] == "r":
|
| 940 |
+
left -= width_difference
|
| 941 |
+
|
| 942 |
+
# then align by align parameter
|
| 943 |
+
if align == "left":
|
| 944 |
+
pass
|
| 945 |
+
elif align == "center":
|
| 946 |
+
left += width_difference / 2.0
|
| 947 |
+
elif align == "right":
|
| 948 |
+
left += width_difference
|
| 949 |
+
else:
|
| 950 |
+
msg = 'align must be "left", "center" or "right"'
|
| 951 |
+
raise ValueError(msg)
|
| 952 |
+
|
| 953 |
+
bbox_line = self.textbbox(
|
| 954 |
+
(left, top),
|
| 955 |
+
line,
|
| 956 |
+
font,
|
| 957 |
+
anchor,
|
| 958 |
+
direction=direction,
|
| 959 |
+
features=features,
|
| 960 |
+
language=language,
|
| 961 |
+
stroke_width=stroke_width,
|
| 962 |
+
embedded_color=embedded_color,
|
| 963 |
+
)
|
| 964 |
+
if bbox is None:
|
| 965 |
+
bbox = bbox_line
|
| 966 |
+
else:
|
| 967 |
+
bbox = (
|
| 968 |
+
min(bbox[0], bbox_line[0]),
|
| 969 |
+
min(bbox[1], bbox_line[1]),
|
| 970 |
+
max(bbox[2], bbox_line[2]),
|
| 971 |
+
max(bbox[3], bbox_line[3]),
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
top += line_spacing
|
| 975 |
+
|
| 976 |
+
if bbox is None:
|
| 977 |
+
return xy[0], xy[1], xy[0], xy[1]
|
| 978 |
+
return bbox
|
| 979 |
+
|
| 980 |
+
|
| 981 |
+
def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
|
| 982 |
+
"""
|
| 983 |
+
A simple 2D drawing interface for PIL images.
|
| 984 |
+
|
| 985 |
+
:param im: The image to draw in.
|
| 986 |
+
:param mode: Optional mode to use for color values. For RGB
|
| 987 |
+
images, this argument can be RGB or RGBA (to blend the
|
| 988 |
+
drawing into the image). For all other modes, this argument
|
| 989 |
+
must be the same as the image mode. If omitted, the mode
|
| 990 |
+
defaults to the mode of the image.
|
| 991 |
+
"""
|
| 992 |
+
try:
|
| 993 |
+
return getattr(im, "getdraw")(mode)
|
| 994 |
+
except AttributeError:
|
| 995 |
+
return ImageDraw(im, mode)
|
| 996 |
+
|
| 997 |
+
|
| 998 |
+
def getdraw(
|
| 999 |
+
im: Image.Image | None = None, hints: list[str] | None = None
|
| 1000 |
+
) -> tuple[ImageDraw2.Draw | None, ModuleType]:
|
| 1001 |
+
"""
|
| 1002 |
+
:param im: The image to draw in.
|
| 1003 |
+
:param hints: An optional list of hints. Deprecated.
|
| 1004 |
+
:returns: A (drawing context, drawing resource factory) tuple.
|
| 1005 |
+
"""
|
| 1006 |
+
if hints is not None:
|
| 1007 |
+
deprecate("'hints' parameter", 12)
|
| 1008 |
+
from . import ImageDraw2
|
| 1009 |
+
|
| 1010 |
+
draw = ImageDraw2.Draw(im) if im is not None else None
|
| 1011 |
+
return draw, ImageDraw2
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
def floodfill(
|
| 1015 |
+
image: Image.Image,
|
| 1016 |
+
xy: tuple[int, int],
|
| 1017 |
+
value: float | tuple[int, ...],
|
| 1018 |
+
border: float | tuple[int, ...] | None = None,
|
| 1019 |
+
thresh: float = 0,
|
| 1020 |
+
) -> None:
|
| 1021 |
+
"""
|
| 1022 |
+
.. warning:: This method is experimental.
|
| 1023 |
+
|
| 1024 |
+
Fills a bounded region with a given color.
|
| 1025 |
+
|
| 1026 |
+
:param image: Target image.
|
| 1027 |
+
:param xy: Seed position (a 2-item coordinate tuple). See
|
| 1028 |
+
:ref:`coordinate-system`.
|
| 1029 |
+
:param value: Fill color.
|
| 1030 |
+
:param border: Optional border value. If given, the region consists of
|
| 1031 |
+
pixels with a color different from the border color. If not given,
|
| 1032 |
+
the region consists of pixels having the same color as the seed
|
| 1033 |
+
pixel.
|
| 1034 |
+
:param thresh: Optional threshold value which specifies a maximum
|
| 1035 |
+
tolerable difference of a pixel value from the 'background' in
|
| 1036 |
+
order for it to be replaced. Useful for filling regions of
|
| 1037 |
+
non-homogeneous, but similar, colors.
|
| 1038 |
+
"""
|
| 1039 |
+
# based on an implementation by Eric S. Raymond
|
| 1040 |
+
# amended by yo1995 @20180806
|
| 1041 |
+
pixel = image.load()
|
| 1042 |
+
assert pixel is not None
|
| 1043 |
+
x, y = xy
|
| 1044 |
+
try:
|
| 1045 |
+
background = pixel[x, y]
|
| 1046 |
+
if _color_diff(value, background) <= thresh:
|
| 1047 |
+
return # seed point already has fill color
|
| 1048 |
+
pixel[x, y] = value
|
| 1049 |
+
except (ValueError, IndexError):
|
| 1050 |
+
return # seed point outside image
|
| 1051 |
+
edge = {(x, y)}
|
| 1052 |
+
# use a set to keep record of current and previous edge pixels
|
| 1053 |
+
# to reduce memory consumption
|
| 1054 |
+
full_edge = set()
|
| 1055 |
+
while edge:
|
| 1056 |
+
new_edge = set()
|
| 1057 |
+
for x, y in edge: # 4 adjacent method
|
| 1058 |
+
for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
|
| 1059 |
+
# If already processed, or if a coordinate is negative, skip
|
| 1060 |
+
if (s, t) in full_edge or s < 0 or t < 0:
|
| 1061 |
+
continue
|
| 1062 |
+
try:
|
| 1063 |
+
p = pixel[s, t]
|
| 1064 |
+
except (ValueError, IndexError):
|
| 1065 |
+
pass
|
| 1066 |
+
else:
|
| 1067 |
+
full_edge.add((s, t))
|
| 1068 |
+
if border is None:
|
| 1069 |
+
fill = _color_diff(p, background) <= thresh
|
| 1070 |
+
else:
|
| 1071 |
+
fill = p not in (value, border)
|
| 1072 |
+
if fill:
|
| 1073 |
+
pixel[s, t] = value
|
| 1074 |
+
new_edge.add((s, t))
|
| 1075 |
+
full_edge = edge # discard pixels processed
|
| 1076 |
+
edge = new_edge
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def _compute_regular_polygon_vertices(
|
| 1080 |
+
bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
|
| 1081 |
+
) -> list[tuple[float, float]]:
|
| 1082 |
+
"""
|
| 1083 |
+
Generate a list of vertices for a 2D regular polygon.
|
| 1084 |
+
|
| 1085 |
+
:param bounding_circle: The bounding circle is a sequence defined
|
| 1086 |
+
by a point and radius. The polygon is inscribed in this circle.
|
| 1087 |
+
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
|
| 1088 |
+
:param n_sides: Number of sides
|
| 1089 |
+
(e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
|
| 1090 |
+
:param rotation: Apply an arbitrary rotation to the polygon
|
| 1091 |
+
(e.g. ``rotation=90``, applies a 90 degree rotation)
|
| 1092 |
+
:return: List of regular polygon vertices
|
| 1093 |
+
(e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
|
| 1094 |
+
|
| 1095 |
+
How are the vertices computed?
|
| 1096 |
+
1. Compute the following variables
|
| 1097 |
+
- theta: Angle between the apothem & the nearest polygon vertex
|
| 1098 |
+
- side_length: Length of each polygon edge
|
| 1099 |
+
- centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
|
| 1100 |
+
- polygon_radius: Polygon radius (last element of bounding_circle)
|
| 1101 |
+
- angles: Location of each polygon vertex in polar grid
|
| 1102 |
+
(e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
|
| 1103 |
+
|
| 1104 |
+
2. For each angle in angles, get the polygon vertex at that angle
|
| 1105 |
+
The vertex is computed using the equation below.
|
| 1106 |
+
X= xcos(φ) + ysin(φ)
|
| 1107 |
+
Y= −xsin(φ) + ycos(φ)
|
| 1108 |
+
|
| 1109 |
+
Note:
|
| 1110 |
+
φ = angle in degrees
|
| 1111 |
+
x = 0
|
| 1112 |
+
y = polygon_radius
|
| 1113 |
+
|
| 1114 |
+
The formula above assumes rotation around the origin.
|
| 1115 |
+
In our case, we are rotating around the centroid.
|
| 1116 |
+
To account for this, we use the formula below
|
| 1117 |
+
X = xcos(φ) + ysin(φ) + centroid_x
|
| 1118 |
+
Y = −xsin(φ) + ycos(φ) + centroid_y
|
| 1119 |
+
"""
|
| 1120 |
+
# 1. Error Handling
|
| 1121 |
+
# 1.1 Check `n_sides` has an appropriate value
|
| 1122 |
+
if not isinstance(n_sides, int):
|
| 1123 |
+
msg = "n_sides should be an int" # type: ignore[unreachable]
|
| 1124 |
+
raise TypeError(msg)
|
| 1125 |
+
if n_sides < 3:
|
| 1126 |
+
msg = "n_sides should be an int > 2"
|
| 1127 |
+
raise ValueError(msg)
|
| 1128 |
+
|
| 1129 |
+
# 1.2 Check `bounding_circle` has an appropriate value
|
| 1130 |
+
if not isinstance(bounding_circle, (list, tuple)):
|
| 1131 |
+
msg = "bounding_circle should be a sequence"
|
| 1132 |
+
raise TypeError(msg)
|
| 1133 |
+
|
| 1134 |
+
if len(bounding_circle) == 3:
|
| 1135 |
+
if not all(isinstance(i, (int, float)) for i in bounding_circle):
|
| 1136 |
+
msg = "bounding_circle should only contain numeric data"
|
| 1137 |
+
raise ValueError(msg)
|
| 1138 |
+
|
| 1139 |
+
*centroid, polygon_radius = cast(list[float], list(bounding_circle))
|
| 1140 |
+
elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
|
| 1141 |
+
if not all(
|
| 1142 |
+
isinstance(i, (int, float)) for i in bounding_circle[0]
|
| 1143 |
+
) or not isinstance(bounding_circle[1], (int, float)):
|
| 1144 |
+
msg = "bounding_circle should only contain numeric data"
|
| 1145 |
+
raise ValueError(msg)
|
| 1146 |
+
|
| 1147 |
+
if len(bounding_circle[0]) != 2:
|
| 1148 |
+
msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
|
| 1149 |
+
raise ValueError(msg)
|
| 1150 |
+
|
| 1151 |
+
centroid = cast(list[float], list(bounding_circle[0]))
|
| 1152 |
+
polygon_radius = cast(float, bounding_circle[1])
|
| 1153 |
+
else:
|
| 1154 |
+
msg = (
|
| 1155 |
+
"bounding_circle should contain 2D coordinates "
|
| 1156 |
+
"and a radius (e.g. (x, y, r) or ((x, y), r) )"
|
| 1157 |
+
)
|
| 1158 |
+
raise ValueError(msg)
|
| 1159 |
+
|
| 1160 |
+
if polygon_radius <= 0:
|
| 1161 |
+
msg = "bounding_circle radius should be > 0"
|
| 1162 |
+
raise ValueError(msg)
|
| 1163 |
+
|
| 1164 |
+
# 1.3 Check `rotation` has an appropriate value
|
| 1165 |
+
if not isinstance(rotation, (int, float)):
|
| 1166 |
+
msg = "rotation should be an int or float" # type: ignore[unreachable]
|
| 1167 |
+
raise ValueError(msg)
|
| 1168 |
+
|
| 1169 |
+
# 2. Define Helper Functions
|
| 1170 |
+
def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
|
| 1171 |
+
return (
|
| 1172 |
+
round(
|
| 1173 |
+
point[0] * math.cos(math.radians(360 - degrees))
|
| 1174 |
+
- point[1] * math.sin(math.radians(360 - degrees))
|
| 1175 |
+
+ centroid[0],
|
| 1176 |
+
2,
|
| 1177 |
+
),
|
| 1178 |
+
round(
|
| 1179 |
+
point[1] * math.cos(math.radians(360 - degrees))
|
| 1180 |
+
+ point[0] * math.sin(math.radians(360 - degrees))
|
| 1181 |
+
+ centroid[1],
|
| 1182 |
+
2,
|
| 1183 |
+
),
|
| 1184 |
+
)
|
| 1185 |
+
|
| 1186 |
+
def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
|
| 1187 |
+
start_point = [polygon_radius, 0]
|
| 1188 |
+
return _apply_rotation(start_point, angle)
|
| 1189 |
+
|
| 1190 |
+
def _get_angles(n_sides: int, rotation: float) -> list[float]:
|
| 1191 |
+
angles = []
|
| 1192 |
+
degrees = 360 / n_sides
|
| 1193 |
+
# Start with the bottom left polygon vertex
|
| 1194 |
+
current_angle = (270 - 0.5 * degrees) + rotation
|
| 1195 |
+
for _ in range(0, n_sides):
|
| 1196 |
+
angles.append(current_angle)
|
| 1197 |
+
current_angle += degrees
|
| 1198 |
+
if current_angle > 360:
|
| 1199 |
+
current_angle -= 360
|
| 1200 |
+
return angles
|
| 1201 |
+
|
| 1202 |
+
# 3. Variable Declarations
|
| 1203 |
+
angles = _get_angles(n_sides, rotation)
|
| 1204 |
+
|
| 1205 |
+
# 4. Compute Vertices
|
| 1206 |
+
return [_compute_polygon_vertex(angle) for angle in angles]
|
| 1207 |
+
|
| 1208 |
+
|
| 1209 |
+
def _color_diff(
|
| 1210 |
+
color1: float | tuple[int, ...], color2: float | tuple[int, ...]
|
| 1211 |
+
) -> float:
|
| 1212 |
+
"""
|
| 1213 |
+
Uses 1-norm distance to calculate difference between two values.
|
| 1214 |
+
"""
|
| 1215 |
+
first = color1 if isinstance(color1, tuple) else (color1,)
|
| 1216 |
+
second = color2 if isinstance(color2, tuple) else (color2,)
|
| 1217 |
+
|
| 1218 |
+
return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
|
Lib/site-packages/PIL/ImageDraw2.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# WCK-style drawing interface operations
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2003-12-07 fl created
|
| 9 |
+
# 2005-05-15 fl updated; added to PIL as ImageDraw2
|
| 10 |
+
# 2005-05-15 fl added text support
|
| 11 |
+
# 2005-05-20 fl added arc/chord/pieslice support
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) 2003-2005 by Secret Labs AB
|
| 14 |
+
# Copyright (c) 2003-2005 by Fredrik Lundh
|
| 15 |
+
#
|
| 16 |
+
# See the README file for information on usage and redistribution.
|
| 17 |
+
#
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
(Experimental) WCK-style drawing interface operations
|
| 22 |
+
|
| 23 |
+
.. seealso:: :py:mod:`PIL.ImageDraw`
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
from typing import Any, AnyStr, BinaryIO
|
| 28 |
+
|
| 29 |
+
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
| 30 |
+
from ._typing import Coords, StrOrBytesPath
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Pen:
|
| 34 |
+
"""Stores an outline color and width."""
|
| 35 |
+
|
| 36 |
+
def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
|
| 37 |
+
self.color = ImageColor.getrgb(color)
|
| 38 |
+
self.width = width
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Brush:
|
| 42 |
+
"""Stores a fill color"""
|
| 43 |
+
|
| 44 |
+
def __init__(self, color: str, opacity: int = 255) -> None:
|
| 45 |
+
self.color = ImageColor.getrgb(color)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Font:
|
| 49 |
+
"""Stores a TrueType font and color"""
|
| 50 |
+
|
| 51 |
+
def __init__(
|
| 52 |
+
self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
|
| 53 |
+
) -> None:
|
| 54 |
+
# FIXME: add support for bitmap fonts
|
| 55 |
+
self.color = ImageColor.getrgb(color)
|
| 56 |
+
self.font = ImageFont.truetype(file, size)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class Draw:
|
| 60 |
+
"""
|
| 61 |
+
(Experimental) WCK-style drawing interface
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
image: Image.Image | str,
|
| 67 |
+
size: tuple[int, int] | list[int] | None = None,
|
| 68 |
+
color: float | tuple[float, ...] | str | None = None,
|
| 69 |
+
) -> None:
|
| 70 |
+
if isinstance(image, str):
|
| 71 |
+
if size is None:
|
| 72 |
+
msg = "If image argument is mode string, size must be a list or tuple"
|
| 73 |
+
raise ValueError(msg)
|
| 74 |
+
image = Image.new(image, size, color)
|
| 75 |
+
self.draw = ImageDraw.Draw(image)
|
| 76 |
+
self.image = image
|
| 77 |
+
self.transform: tuple[float, float, float, float, float, float] | None = None
|
| 78 |
+
|
| 79 |
+
def flush(self) -> Image.Image:
|
| 80 |
+
return self.image
|
| 81 |
+
|
| 82 |
+
def render(
|
| 83 |
+
self,
|
| 84 |
+
op: str,
|
| 85 |
+
xy: Coords,
|
| 86 |
+
pen: Pen | Brush | None,
|
| 87 |
+
brush: Brush | Pen | None = None,
|
| 88 |
+
**kwargs: Any,
|
| 89 |
+
) -> None:
|
| 90 |
+
# handle color arguments
|
| 91 |
+
outline = fill = None
|
| 92 |
+
width = 1
|
| 93 |
+
if isinstance(pen, Pen):
|
| 94 |
+
outline = pen.color
|
| 95 |
+
width = pen.width
|
| 96 |
+
elif isinstance(brush, Pen):
|
| 97 |
+
outline = brush.color
|
| 98 |
+
width = brush.width
|
| 99 |
+
if isinstance(brush, Brush):
|
| 100 |
+
fill = brush.color
|
| 101 |
+
elif isinstance(pen, Brush):
|
| 102 |
+
fill = pen.color
|
| 103 |
+
# handle transformation
|
| 104 |
+
if self.transform:
|
| 105 |
+
path = ImagePath.Path(xy)
|
| 106 |
+
path.transform(self.transform)
|
| 107 |
+
xy = path
|
| 108 |
+
# render the item
|
| 109 |
+
if op in ("arc", "line"):
|
| 110 |
+
kwargs.setdefault("fill", outline)
|
| 111 |
+
else:
|
| 112 |
+
kwargs.setdefault("fill", fill)
|
| 113 |
+
kwargs.setdefault("outline", outline)
|
| 114 |
+
if op == "line":
|
| 115 |
+
kwargs.setdefault("width", width)
|
| 116 |
+
getattr(self.draw, op)(xy, **kwargs)
|
| 117 |
+
|
| 118 |
+
def settransform(self, offset: tuple[float, float]) -> None:
|
| 119 |
+
"""Sets a transformation offset."""
|
| 120 |
+
(xoffset, yoffset) = offset
|
| 121 |
+
self.transform = (1, 0, xoffset, 0, 1, yoffset)
|
| 122 |
+
|
| 123 |
+
def arc(
|
| 124 |
+
self,
|
| 125 |
+
xy: Coords,
|
| 126 |
+
pen: Pen | Brush | None,
|
| 127 |
+
start: float,
|
| 128 |
+
end: float,
|
| 129 |
+
*options: Any,
|
| 130 |
+
) -> None:
|
| 131 |
+
"""
|
| 132 |
+
Draws an arc (a portion of a circle outline) between the start and end
|
| 133 |
+
angles, inside the given bounding box.
|
| 134 |
+
|
| 135 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc`
|
| 136 |
+
"""
|
| 137 |
+
self.render("arc", xy, pen, *options, start=start, end=end)
|
| 138 |
+
|
| 139 |
+
def chord(
|
| 140 |
+
self,
|
| 141 |
+
xy: Coords,
|
| 142 |
+
pen: Pen | Brush | None,
|
| 143 |
+
start: float,
|
| 144 |
+
end: float,
|
| 145 |
+
*options: Any,
|
| 146 |
+
) -> None:
|
| 147 |
+
"""
|
| 148 |
+
Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points
|
| 149 |
+
with a straight line.
|
| 150 |
+
|
| 151 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord`
|
| 152 |
+
"""
|
| 153 |
+
self.render("chord", xy, pen, *options, start=start, end=end)
|
| 154 |
+
|
| 155 |
+
def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
|
| 156 |
+
"""
|
| 157 |
+
Draws an ellipse inside the given bounding box.
|
| 158 |
+
|
| 159 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
|
| 160 |
+
"""
|
| 161 |
+
self.render("ellipse", xy, pen, *options)
|
| 162 |
+
|
| 163 |
+
def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
|
| 164 |
+
"""
|
| 165 |
+
Draws a line between the coordinates in the ``xy`` list.
|
| 166 |
+
|
| 167 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
|
| 168 |
+
"""
|
| 169 |
+
self.render("line", xy, pen, *options)
|
| 170 |
+
|
| 171 |
+
def pieslice(
|
| 172 |
+
self,
|
| 173 |
+
xy: Coords,
|
| 174 |
+
pen: Pen | Brush | None,
|
| 175 |
+
start: float,
|
| 176 |
+
end: float,
|
| 177 |
+
*options: Any,
|
| 178 |
+
) -> None:
|
| 179 |
+
"""
|
| 180 |
+
Same as arc, but also draws straight lines between the end points and the
|
| 181 |
+
center of the bounding box.
|
| 182 |
+
|
| 183 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice`
|
| 184 |
+
"""
|
| 185 |
+
self.render("pieslice", xy, pen, *options, start=start, end=end)
|
| 186 |
+
|
| 187 |
+
def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
|
| 188 |
+
"""
|
| 189 |
+
Draws a polygon.
|
| 190 |
+
|
| 191 |
+
The polygon outline consists of straight lines between the given
|
| 192 |
+
coordinates, plus a straight line between the last and the first
|
| 193 |
+
coordinate.
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon`
|
| 197 |
+
"""
|
| 198 |
+
self.render("polygon", xy, pen, *options)
|
| 199 |
+
|
| 200 |
+
def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
|
| 201 |
+
"""
|
| 202 |
+
Draws a rectangle.
|
| 203 |
+
|
| 204 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
|
| 205 |
+
"""
|
| 206 |
+
self.render("rectangle", xy, pen, *options)
|
| 207 |
+
|
| 208 |
+
def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None:
|
| 209 |
+
"""
|
| 210 |
+
Draws the string at the given position.
|
| 211 |
+
|
| 212 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text`
|
| 213 |
+
"""
|
| 214 |
+
if self.transform:
|
| 215 |
+
path = ImagePath.Path(xy)
|
| 216 |
+
path.transform(self.transform)
|
| 217 |
+
xy = path
|
| 218 |
+
self.draw.text(xy, text, font=font.font, fill=font.color)
|
| 219 |
+
|
| 220 |
+
def textbbox(
|
| 221 |
+
self, xy: tuple[float, float], text: AnyStr, font: Font
|
| 222 |
+
) -> tuple[float, float, float, float]:
|
| 223 |
+
"""
|
| 224 |
+
Returns bounding box (in pixels) of given text.
|
| 225 |
+
|
| 226 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 227 |
+
|
| 228 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox`
|
| 229 |
+
"""
|
| 230 |
+
if self.transform:
|
| 231 |
+
path = ImagePath.Path(xy)
|
| 232 |
+
path.transform(self.transform)
|
| 233 |
+
xy = path
|
| 234 |
+
return self.draw.textbbox(xy, text, font=font.font)
|
| 235 |
+
|
| 236 |
+
def textlength(self, text: AnyStr, font: Font) -> float:
|
| 237 |
+
"""
|
| 238 |
+
Returns length (in pixels) of given text.
|
| 239 |
+
This is the amount by which following text should be offset.
|
| 240 |
+
|
| 241 |
+
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength`
|
| 242 |
+
"""
|
| 243 |
+
return self.draw.textlength(text, font=font.font)
|
Lib/site-packages/PIL/ImageEnhance.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# image enhancement classes
|
| 6 |
+
#
|
| 7 |
+
# For a background, see "Image Processing By Interpolation and
|
| 8 |
+
# Extrapolation", Paul Haeberli and Douglas Voorhies. Available
|
| 9 |
+
# at http://www.graficaobscura.com/interp/index.html
|
| 10 |
+
#
|
| 11 |
+
# History:
|
| 12 |
+
# 1996-03-23 fl Created
|
| 13 |
+
# 2009-06-16 fl Fixed mean calculation
|
| 14 |
+
#
|
| 15 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 16 |
+
# Copyright (c) Fredrik Lundh 1996.
|
| 17 |
+
#
|
| 18 |
+
# See the README file for information on usage and redistribution.
|
| 19 |
+
#
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
from . import Image, ImageFilter, ImageStat
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _Enhance:
|
| 26 |
+
image: Image.Image
|
| 27 |
+
degenerate: Image.Image
|
| 28 |
+
|
| 29 |
+
def enhance(self, factor: float) -> Image.Image:
|
| 30 |
+
"""
|
| 31 |
+
Returns an enhanced image.
|
| 32 |
+
|
| 33 |
+
:param factor: A floating point value controlling the enhancement.
|
| 34 |
+
Factor 1.0 always returns a copy of the original image,
|
| 35 |
+
lower factors mean less color (brightness, contrast,
|
| 36 |
+
etc), and higher values more. There are no restrictions
|
| 37 |
+
on this value.
|
| 38 |
+
:rtype: :py:class:`~PIL.Image.Image`
|
| 39 |
+
"""
|
| 40 |
+
return Image.blend(self.degenerate, self.image, factor)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Color(_Enhance):
|
| 44 |
+
"""Adjust image color balance.
|
| 45 |
+
|
| 46 |
+
This class can be used to adjust the colour balance of an image, in
|
| 47 |
+
a manner similar to the controls on a colour TV set. An enhancement
|
| 48 |
+
factor of 0.0 gives a black and white image. A factor of 1.0 gives
|
| 49 |
+
the original image.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, image: Image.Image) -> None:
|
| 53 |
+
self.image = image
|
| 54 |
+
self.intermediate_mode = "L"
|
| 55 |
+
if "A" in image.getbands():
|
| 56 |
+
self.intermediate_mode = "LA"
|
| 57 |
+
|
| 58 |
+
if self.intermediate_mode != image.mode:
|
| 59 |
+
image = image.convert(self.intermediate_mode).convert(image.mode)
|
| 60 |
+
self.degenerate = image
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class Contrast(_Enhance):
|
| 64 |
+
"""Adjust image contrast.
|
| 65 |
+
|
| 66 |
+
This class can be used to control the contrast of an image, similar
|
| 67 |
+
to the contrast control on a TV set. An enhancement factor of 0.0
|
| 68 |
+
gives a solid gray image. A factor of 1.0 gives the original image.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def __init__(self, image: Image.Image) -> None:
|
| 72 |
+
self.image = image
|
| 73 |
+
if image.mode != "L":
|
| 74 |
+
image = image.convert("L")
|
| 75 |
+
mean = int(ImageStat.Stat(image).mean[0] + 0.5)
|
| 76 |
+
self.degenerate = Image.new("L", image.size, mean)
|
| 77 |
+
if self.degenerate.mode != self.image.mode:
|
| 78 |
+
self.degenerate = self.degenerate.convert(self.image.mode)
|
| 79 |
+
|
| 80 |
+
if "A" in self.image.getbands():
|
| 81 |
+
self.degenerate.putalpha(self.image.getchannel("A"))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class Brightness(_Enhance):
|
| 85 |
+
"""Adjust image brightness.
|
| 86 |
+
|
| 87 |
+
This class can be used to control the brightness of an image. An
|
| 88 |
+
enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the
|
| 89 |
+
original image.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self, image: Image.Image) -> None:
|
| 93 |
+
self.image = image
|
| 94 |
+
self.degenerate = Image.new(image.mode, image.size, 0)
|
| 95 |
+
|
| 96 |
+
if "A" in image.getbands():
|
| 97 |
+
self.degenerate.putalpha(image.getchannel("A"))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class Sharpness(_Enhance):
|
| 101 |
+
"""Adjust image sharpness.
|
| 102 |
+
|
| 103 |
+
This class can be used to adjust the sharpness of an image. An
|
| 104 |
+
enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the
|
| 105 |
+
original image, and a factor of 2.0 gives a sharpened image.
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
def __init__(self, image: Image.Image) -> None:
|
| 109 |
+
self.image = image
|
| 110 |
+
self.degenerate = image.filter(ImageFilter.SMOOTH)
|
| 111 |
+
|
| 112 |
+
if "A" in image.getbands():
|
| 113 |
+
self.degenerate.putalpha(image.getchannel("A"))
|
Lib/site-packages/PIL/ImageFile.py
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# base class for image file handlers
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1995-09-09 fl Created
|
| 9 |
+
# 1996-03-11 fl Fixed load mechanism.
|
| 10 |
+
# 1996-04-15 fl Added pcx/xbm decoders.
|
| 11 |
+
# 1996-04-30 fl Added encoders.
|
| 12 |
+
# 1996-12-14 fl Added load helpers
|
| 13 |
+
# 1997-01-11 fl Use encode_to_file where possible
|
| 14 |
+
# 1997-08-27 fl Flush output in _save
|
| 15 |
+
# 1998-03-05 fl Use memory mapping for some modes
|
| 16 |
+
# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B"
|
| 17 |
+
# 1999-05-31 fl Added image parser
|
| 18 |
+
# 2000-10-12 fl Set readonly flag on memory-mapped images
|
| 19 |
+
# 2002-03-20 fl Use better messages for common decoder errors
|
| 20 |
+
# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available
|
| 21 |
+
# 2003-10-30 fl Added StubImageFile class
|
| 22 |
+
# 2004-02-25 fl Made incremental parser more robust
|
| 23 |
+
#
|
| 24 |
+
# Copyright (c) 1997-2004 by Secret Labs AB
|
| 25 |
+
# Copyright (c) 1995-2004 by Fredrik Lundh
|
| 26 |
+
#
|
| 27 |
+
# See the README file for information on usage and redistribution.
|
| 28 |
+
#
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import abc
|
| 32 |
+
import io
|
| 33 |
+
import itertools
|
| 34 |
+
import os
|
| 35 |
+
import struct
|
| 36 |
+
import sys
|
| 37 |
+
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
|
| 38 |
+
|
| 39 |
+
from . import Image
|
| 40 |
+
from ._deprecate import deprecate
|
| 41 |
+
from ._util import is_path
|
| 42 |
+
|
| 43 |
+
if TYPE_CHECKING:
|
| 44 |
+
from ._typing import StrOrBytesPath
|
| 45 |
+
|
| 46 |
+
MAXBLOCK = 65536
|
| 47 |
+
|
| 48 |
+
SAFEBLOCK = 1024 * 1024
|
| 49 |
+
|
| 50 |
+
LOAD_TRUNCATED_IMAGES = False
|
| 51 |
+
"""Whether or not to load truncated image files. User code may change this."""
|
| 52 |
+
|
| 53 |
+
ERRORS = {
|
| 54 |
+
-1: "image buffer overrun error",
|
| 55 |
+
-2: "decoding error",
|
| 56 |
+
-3: "unknown error",
|
| 57 |
+
-8: "bad configuration",
|
| 58 |
+
-9: "out of memory error",
|
| 59 |
+
}
|
| 60 |
+
"""
|
| 61 |
+
Dict of known error codes returned from :meth:`.PyDecoder.decode`,
|
| 62 |
+
:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
|
| 63 |
+
:meth:`.PyEncoder.encode_to_file`.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
#
|
| 68 |
+
# --------------------------------------------------------------------
|
| 69 |
+
# Helpers
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _get_oserror(error: int, *, encoder: bool) -> OSError:
|
| 73 |
+
try:
|
| 74 |
+
msg = Image.core.getcodecstatus(error)
|
| 75 |
+
except AttributeError:
|
| 76 |
+
msg = ERRORS.get(error)
|
| 77 |
+
if not msg:
|
| 78 |
+
msg = f"{'encoder' if encoder else 'decoder'} error {error}"
|
| 79 |
+
msg += f" when {'writing' if encoder else 'reading'} image file"
|
| 80 |
+
return OSError(msg)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def raise_oserror(error: int) -> OSError:
|
| 84 |
+
deprecate(
|
| 85 |
+
"raise_oserror",
|
| 86 |
+
12,
|
| 87 |
+
action="It is only useful for translating error codes returned by a codec's "
|
| 88 |
+
"decode() method, which ImageFile already does automatically.",
|
| 89 |
+
)
|
| 90 |
+
raise _get_oserror(error, encoder=False)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _tilesort(t: _Tile) -> int:
|
| 94 |
+
# sort on offset
|
| 95 |
+
return t[2]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class _Tile(NamedTuple):
|
| 99 |
+
codec_name: str
|
| 100 |
+
extents: tuple[int, int, int, int] | None
|
| 101 |
+
offset: int = 0
|
| 102 |
+
args: tuple[Any, ...] | str | None = None
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
#
|
| 106 |
+
# --------------------------------------------------------------------
|
| 107 |
+
# ImageFile base class
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class ImageFile(Image.Image):
|
| 111 |
+
"""Base class for image file format handlers."""
|
| 112 |
+
|
| 113 |
+
def __init__(
|
| 114 |
+
self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
|
| 115 |
+
) -> None:
|
| 116 |
+
super().__init__()
|
| 117 |
+
|
| 118 |
+
self._min_frame = 0
|
| 119 |
+
|
| 120 |
+
self.custom_mimetype: str | None = None
|
| 121 |
+
|
| 122 |
+
self.tile: list[_Tile] = []
|
| 123 |
+
""" A list of tile descriptors """
|
| 124 |
+
|
| 125 |
+
self.readonly = 1 # until we know better
|
| 126 |
+
|
| 127 |
+
self.decoderconfig: tuple[Any, ...] = ()
|
| 128 |
+
self.decodermaxblock = MAXBLOCK
|
| 129 |
+
|
| 130 |
+
if is_path(fp):
|
| 131 |
+
# filename
|
| 132 |
+
self.fp = open(fp, "rb")
|
| 133 |
+
self.filename = os.fspath(fp)
|
| 134 |
+
self._exclusive_fp = True
|
| 135 |
+
else:
|
| 136 |
+
# stream
|
| 137 |
+
self.fp = cast(IO[bytes], fp)
|
| 138 |
+
self.filename = filename if filename is not None else ""
|
| 139 |
+
# can be overridden
|
| 140 |
+
self._exclusive_fp = False
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
try:
|
| 144 |
+
self._open()
|
| 145 |
+
except (
|
| 146 |
+
IndexError, # end of data
|
| 147 |
+
TypeError, # end of data (ord)
|
| 148 |
+
KeyError, # unsupported mode
|
| 149 |
+
EOFError, # got header but not the first frame
|
| 150 |
+
struct.error,
|
| 151 |
+
) as v:
|
| 152 |
+
raise SyntaxError(v) from v
|
| 153 |
+
|
| 154 |
+
if not self.mode or self.size[0] <= 0 or self.size[1] <= 0:
|
| 155 |
+
msg = "not identified by this driver"
|
| 156 |
+
raise SyntaxError(msg)
|
| 157 |
+
except BaseException:
|
| 158 |
+
# close the file only if we have opened it this constructor
|
| 159 |
+
if self._exclusive_fp:
|
| 160 |
+
self.fp.close()
|
| 161 |
+
raise
|
| 162 |
+
|
| 163 |
+
def _open(self) -> None:
|
| 164 |
+
pass
|
| 165 |
+
|
| 166 |
+
def get_format_mimetype(self) -> str | None:
|
| 167 |
+
if self.custom_mimetype:
|
| 168 |
+
return self.custom_mimetype
|
| 169 |
+
if self.format is not None:
|
| 170 |
+
return Image.MIME.get(self.format.upper())
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
def __setstate__(self, state: list[Any]) -> None:
|
| 174 |
+
self.tile = []
|
| 175 |
+
super().__setstate__(state)
|
| 176 |
+
|
| 177 |
+
def verify(self) -> None:
|
| 178 |
+
"""Check file integrity"""
|
| 179 |
+
|
| 180 |
+
# raise exception if something's wrong. must be called
|
| 181 |
+
# directly after open, and closes file when finished.
|
| 182 |
+
if self._exclusive_fp:
|
| 183 |
+
self.fp.close()
|
| 184 |
+
self.fp = None
|
| 185 |
+
|
| 186 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 187 |
+
"""Load image data based on tile list"""
|
| 188 |
+
|
| 189 |
+
if not self.tile and self._im is None:
|
| 190 |
+
msg = "cannot load this image"
|
| 191 |
+
raise OSError(msg)
|
| 192 |
+
|
| 193 |
+
pixel = Image.Image.load(self)
|
| 194 |
+
if not self.tile:
|
| 195 |
+
return pixel
|
| 196 |
+
|
| 197 |
+
self.map: mmap.mmap | None = None
|
| 198 |
+
use_mmap = self.filename and len(self.tile) == 1
|
| 199 |
+
# As of pypy 2.1.0, memory mapping was failing here.
|
| 200 |
+
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
|
| 201 |
+
|
| 202 |
+
readonly = 0
|
| 203 |
+
|
| 204 |
+
# look for read/seek overrides
|
| 205 |
+
if hasattr(self, "load_read"):
|
| 206 |
+
read = self.load_read
|
| 207 |
+
# don't use mmap if there are custom read/seek functions
|
| 208 |
+
use_mmap = False
|
| 209 |
+
else:
|
| 210 |
+
read = self.fp.read
|
| 211 |
+
|
| 212 |
+
if hasattr(self, "load_seek"):
|
| 213 |
+
seek = self.load_seek
|
| 214 |
+
use_mmap = False
|
| 215 |
+
else:
|
| 216 |
+
seek = self.fp.seek
|
| 217 |
+
|
| 218 |
+
if use_mmap:
|
| 219 |
+
# try memory mapping
|
| 220 |
+
decoder_name, extents, offset, args = self.tile[0]
|
| 221 |
+
if isinstance(args, str):
|
| 222 |
+
args = (args, 0, 1)
|
| 223 |
+
if (
|
| 224 |
+
decoder_name == "raw"
|
| 225 |
+
and isinstance(args, tuple)
|
| 226 |
+
and len(args) >= 3
|
| 227 |
+
and args[0] == self.mode
|
| 228 |
+
and args[0] in Image._MAPMODES
|
| 229 |
+
):
|
| 230 |
+
try:
|
| 231 |
+
# use mmap, if possible
|
| 232 |
+
import mmap
|
| 233 |
+
|
| 234 |
+
with open(self.filename) as fp:
|
| 235 |
+
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
| 236 |
+
if offset + self.size[1] * args[1] > self.map.size():
|
| 237 |
+
msg = "buffer is not large enough"
|
| 238 |
+
raise OSError(msg)
|
| 239 |
+
self.im = Image.core.map_buffer(
|
| 240 |
+
self.map, self.size, decoder_name, offset, args
|
| 241 |
+
)
|
| 242 |
+
readonly = 1
|
| 243 |
+
# After trashing self.im,
|
| 244 |
+
# we might need to reload the palette data.
|
| 245 |
+
if self.palette:
|
| 246 |
+
self.palette.dirty = 1
|
| 247 |
+
except (AttributeError, OSError, ImportError):
|
| 248 |
+
self.map = None
|
| 249 |
+
|
| 250 |
+
self.load_prepare()
|
| 251 |
+
err_code = -3 # initialize to unknown error
|
| 252 |
+
if not self.map:
|
| 253 |
+
# sort tiles in file order
|
| 254 |
+
self.tile.sort(key=_tilesort)
|
| 255 |
+
|
| 256 |
+
# FIXME: This is a hack to handle TIFF's JpegTables tag.
|
| 257 |
+
prefix = getattr(self, "tile_prefix", b"")
|
| 258 |
+
|
| 259 |
+
# Remove consecutive duplicates that only differ by their offset
|
| 260 |
+
self.tile = [
|
| 261 |
+
list(tiles)[-1]
|
| 262 |
+
for _, tiles in itertools.groupby(
|
| 263 |
+
self.tile, lambda tile: (tile[0], tile[1], tile[3])
|
| 264 |
+
)
|
| 265 |
+
]
|
| 266 |
+
for decoder_name, extents, offset, args in self.tile:
|
| 267 |
+
seek(offset)
|
| 268 |
+
decoder = Image._getdecoder(
|
| 269 |
+
self.mode, decoder_name, args, self.decoderconfig
|
| 270 |
+
)
|
| 271 |
+
try:
|
| 272 |
+
decoder.setimage(self.im, extents)
|
| 273 |
+
if decoder.pulls_fd:
|
| 274 |
+
decoder.setfd(self.fp)
|
| 275 |
+
err_code = decoder.decode(b"")[1]
|
| 276 |
+
else:
|
| 277 |
+
b = prefix
|
| 278 |
+
while True:
|
| 279 |
+
try:
|
| 280 |
+
s = read(self.decodermaxblock)
|
| 281 |
+
except (IndexError, struct.error) as e:
|
| 282 |
+
# truncated png/gif
|
| 283 |
+
if LOAD_TRUNCATED_IMAGES:
|
| 284 |
+
break
|
| 285 |
+
else:
|
| 286 |
+
msg = "image file is truncated"
|
| 287 |
+
raise OSError(msg) from e
|
| 288 |
+
|
| 289 |
+
if not s: # truncated jpeg
|
| 290 |
+
if LOAD_TRUNCATED_IMAGES:
|
| 291 |
+
break
|
| 292 |
+
else:
|
| 293 |
+
msg = (
|
| 294 |
+
"image file is truncated "
|
| 295 |
+
f"({len(b)} bytes not processed)"
|
| 296 |
+
)
|
| 297 |
+
raise OSError(msg)
|
| 298 |
+
|
| 299 |
+
b = b + s
|
| 300 |
+
n, err_code = decoder.decode(b)
|
| 301 |
+
if n < 0:
|
| 302 |
+
break
|
| 303 |
+
b = b[n:]
|
| 304 |
+
finally:
|
| 305 |
+
# Need to cleanup here to prevent leaks
|
| 306 |
+
decoder.cleanup()
|
| 307 |
+
|
| 308 |
+
self.tile = []
|
| 309 |
+
self.readonly = readonly
|
| 310 |
+
|
| 311 |
+
self.load_end()
|
| 312 |
+
|
| 313 |
+
if self._exclusive_fp and self._close_exclusive_fp_after_loading:
|
| 314 |
+
self.fp.close()
|
| 315 |
+
self.fp = None
|
| 316 |
+
|
| 317 |
+
if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
|
| 318 |
+
# still raised if decoder fails to return anything
|
| 319 |
+
raise _get_oserror(err_code, encoder=False)
|
| 320 |
+
|
| 321 |
+
return Image.Image.load(self)
|
| 322 |
+
|
| 323 |
+
def load_prepare(self) -> None:
|
| 324 |
+
# create image memory if necessary
|
| 325 |
+
if self._im is None:
|
| 326 |
+
self.im = Image.core.new(self.mode, self.size)
|
| 327 |
+
# create palette (optional)
|
| 328 |
+
if self.mode == "P":
|
| 329 |
+
Image.Image.load(self)
|
| 330 |
+
|
| 331 |
+
def load_end(self) -> None:
|
| 332 |
+
# may be overridden
|
| 333 |
+
pass
|
| 334 |
+
|
| 335 |
+
# may be defined for contained formats
|
| 336 |
+
# def load_seek(self, pos: int) -> None:
|
| 337 |
+
# pass
|
| 338 |
+
|
| 339 |
+
# may be defined for blocked formats (e.g. PNG)
|
| 340 |
+
# def load_read(self, read_bytes: int) -> bytes:
|
| 341 |
+
# pass
|
| 342 |
+
|
| 343 |
+
def _seek_check(self, frame: int) -> bool:
|
| 344 |
+
if (
|
| 345 |
+
frame < self._min_frame
|
| 346 |
+
# Only check upper limit on frames if additional seek operations
|
| 347 |
+
# are not required to do so
|
| 348 |
+
or (
|
| 349 |
+
not (hasattr(self, "_n_frames") and self._n_frames is None)
|
| 350 |
+
and frame >= getattr(self, "n_frames") + self._min_frame
|
| 351 |
+
)
|
| 352 |
+
):
|
| 353 |
+
msg = "attempt to seek outside sequence"
|
| 354 |
+
raise EOFError(msg)
|
| 355 |
+
|
| 356 |
+
return self.tell() != frame
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class StubHandler:
|
| 360 |
+
def open(self, im: StubImageFile) -> None:
|
| 361 |
+
pass
|
| 362 |
+
|
| 363 |
+
@abc.abstractmethod
|
| 364 |
+
def load(self, im: StubImageFile) -> Image.Image:
|
| 365 |
+
pass
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class StubImageFile(ImageFile):
|
| 369 |
+
"""
|
| 370 |
+
Base class for stub image loaders.
|
| 371 |
+
|
| 372 |
+
A stub loader is an image loader that can identify files of a
|
| 373 |
+
certain format, but relies on external code to load the file.
|
| 374 |
+
"""
|
| 375 |
+
|
| 376 |
+
def _open(self) -> None:
|
| 377 |
+
msg = "StubImageFile subclass must implement _open"
|
| 378 |
+
raise NotImplementedError(msg)
|
| 379 |
+
|
| 380 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 381 |
+
loader = self._load()
|
| 382 |
+
if loader is None:
|
| 383 |
+
msg = f"cannot find loader for this {self.format} file"
|
| 384 |
+
raise OSError(msg)
|
| 385 |
+
image = loader.load(self)
|
| 386 |
+
assert image is not None
|
| 387 |
+
# become the other object (!)
|
| 388 |
+
self.__class__ = image.__class__ # type: ignore[assignment]
|
| 389 |
+
self.__dict__ = image.__dict__
|
| 390 |
+
return image.load()
|
| 391 |
+
|
| 392 |
+
def _load(self) -> StubHandler | None:
|
| 393 |
+
"""(Hook) Find actual image loader."""
|
| 394 |
+
msg = "StubImageFile subclass must implement _load"
|
| 395 |
+
raise NotImplementedError(msg)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
class Parser:
|
| 399 |
+
"""
|
| 400 |
+
Incremental image parser. This class implements the standard
|
| 401 |
+
feed/close consumer interface.
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
incremental = None
|
| 405 |
+
image: Image.Image | None = None
|
| 406 |
+
data: bytes | None = None
|
| 407 |
+
decoder: Image.core.ImagingDecoder | PyDecoder | None = None
|
| 408 |
+
offset = 0
|
| 409 |
+
finished = 0
|
| 410 |
+
|
| 411 |
+
def reset(self) -> None:
|
| 412 |
+
"""
|
| 413 |
+
(Consumer) Reset the parser. Note that you can only call this
|
| 414 |
+
method immediately after you've created a parser; parser
|
| 415 |
+
instances cannot be reused.
|
| 416 |
+
"""
|
| 417 |
+
assert self.data is None, "cannot reuse parsers"
|
| 418 |
+
|
| 419 |
+
def feed(self, data: bytes) -> None:
|
| 420 |
+
"""
|
| 421 |
+
(Consumer) Feed data to the parser.
|
| 422 |
+
|
| 423 |
+
:param data: A string buffer.
|
| 424 |
+
:exception OSError: If the parser failed to parse the image file.
|
| 425 |
+
"""
|
| 426 |
+
# collect data
|
| 427 |
+
|
| 428 |
+
if self.finished:
|
| 429 |
+
return
|
| 430 |
+
|
| 431 |
+
if self.data is None:
|
| 432 |
+
self.data = data
|
| 433 |
+
else:
|
| 434 |
+
self.data = self.data + data
|
| 435 |
+
|
| 436 |
+
# parse what we have
|
| 437 |
+
if self.decoder:
|
| 438 |
+
if self.offset > 0:
|
| 439 |
+
# skip header
|
| 440 |
+
skip = min(len(self.data), self.offset)
|
| 441 |
+
self.data = self.data[skip:]
|
| 442 |
+
self.offset = self.offset - skip
|
| 443 |
+
if self.offset > 0 or not self.data:
|
| 444 |
+
return
|
| 445 |
+
|
| 446 |
+
n, e = self.decoder.decode(self.data)
|
| 447 |
+
|
| 448 |
+
if n < 0:
|
| 449 |
+
# end of stream
|
| 450 |
+
self.data = None
|
| 451 |
+
self.finished = 1
|
| 452 |
+
if e < 0:
|
| 453 |
+
# decoding error
|
| 454 |
+
self.image = None
|
| 455 |
+
raise _get_oserror(e, encoder=False)
|
| 456 |
+
else:
|
| 457 |
+
# end of image
|
| 458 |
+
return
|
| 459 |
+
self.data = self.data[n:]
|
| 460 |
+
|
| 461 |
+
elif self.image:
|
| 462 |
+
# if we end up here with no decoder, this file cannot
|
| 463 |
+
# be incrementally parsed. wait until we've gotten all
|
| 464 |
+
# available data
|
| 465 |
+
pass
|
| 466 |
+
|
| 467 |
+
else:
|
| 468 |
+
# attempt to open this file
|
| 469 |
+
try:
|
| 470 |
+
with io.BytesIO(self.data) as fp:
|
| 471 |
+
im = Image.open(fp)
|
| 472 |
+
except OSError:
|
| 473 |
+
pass # not enough data
|
| 474 |
+
else:
|
| 475 |
+
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
| 476 |
+
if flag or len(im.tile) != 1:
|
| 477 |
+
# custom load code, or multiple tiles
|
| 478 |
+
self.decode = None
|
| 479 |
+
else:
|
| 480 |
+
# initialize decoder
|
| 481 |
+
im.load_prepare()
|
| 482 |
+
d, e, o, a = im.tile[0]
|
| 483 |
+
im.tile = []
|
| 484 |
+
self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig)
|
| 485 |
+
self.decoder.setimage(im.im, e)
|
| 486 |
+
|
| 487 |
+
# calculate decoder offset
|
| 488 |
+
self.offset = o
|
| 489 |
+
if self.offset <= len(self.data):
|
| 490 |
+
self.data = self.data[self.offset :]
|
| 491 |
+
self.offset = 0
|
| 492 |
+
|
| 493 |
+
self.image = im
|
| 494 |
+
|
| 495 |
+
def __enter__(self) -> Parser:
|
| 496 |
+
return self
|
| 497 |
+
|
| 498 |
+
def __exit__(self, *args: object) -> None:
|
| 499 |
+
self.close()
|
| 500 |
+
|
| 501 |
+
def close(self) -> Image.Image:
|
| 502 |
+
"""
|
| 503 |
+
(Consumer) Close the stream.
|
| 504 |
+
|
| 505 |
+
:returns: An image object.
|
| 506 |
+
:exception OSError: If the parser failed to parse the image file either
|
| 507 |
+
because it cannot be identified or cannot be
|
| 508 |
+
decoded.
|
| 509 |
+
"""
|
| 510 |
+
# finish decoding
|
| 511 |
+
if self.decoder:
|
| 512 |
+
# get rid of what's left in the buffers
|
| 513 |
+
self.feed(b"")
|
| 514 |
+
self.data = self.decoder = None
|
| 515 |
+
if not self.finished:
|
| 516 |
+
msg = "image was incomplete"
|
| 517 |
+
raise OSError(msg)
|
| 518 |
+
if not self.image:
|
| 519 |
+
msg = "cannot parse this image"
|
| 520 |
+
raise OSError(msg)
|
| 521 |
+
if self.data:
|
| 522 |
+
# incremental parsing not possible; reopen the file
|
| 523 |
+
# not that we have all data
|
| 524 |
+
with io.BytesIO(self.data) as fp:
|
| 525 |
+
try:
|
| 526 |
+
self.image = Image.open(fp)
|
| 527 |
+
finally:
|
| 528 |
+
self.image.load()
|
| 529 |
+
return self.image
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
# --------------------------------------------------------------------
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None:
|
| 536 |
+
"""Helper to save image based on tile list
|
| 537 |
+
|
| 538 |
+
:param im: Image object.
|
| 539 |
+
:param fp: File object.
|
| 540 |
+
:param tile: Tile list.
|
| 541 |
+
:param bufsize: Optional buffer size
|
| 542 |
+
"""
|
| 543 |
+
|
| 544 |
+
im.load()
|
| 545 |
+
if not hasattr(im, "encoderconfig"):
|
| 546 |
+
im.encoderconfig = ()
|
| 547 |
+
tile.sort(key=_tilesort)
|
| 548 |
+
# FIXME: make MAXBLOCK a configuration parameter
|
| 549 |
+
# It would be great if we could have the encoder specify what it needs
|
| 550 |
+
# But, it would need at least the image size in most cases. RawEncode is
|
| 551 |
+
# a tricky case.
|
| 552 |
+
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
| 553 |
+
try:
|
| 554 |
+
fh = fp.fileno()
|
| 555 |
+
fp.flush()
|
| 556 |
+
_encode_tile(im, fp, tile, bufsize, fh)
|
| 557 |
+
except (AttributeError, io.UnsupportedOperation) as exc:
|
| 558 |
+
_encode_tile(im, fp, tile, bufsize, None, exc)
|
| 559 |
+
if hasattr(fp, "flush"):
|
| 560 |
+
fp.flush()
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def _encode_tile(
|
| 564 |
+
im: Image.Image,
|
| 565 |
+
fp: IO[bytes],
|
| 566 |
+
tile: list[_Tile],
|
| 567 |
+
bufsize: int,
|
| 568 |
+
fh: int | None,
|
| 569 |
+
exc: BaseException | None = None,
|
| 570 |
+
) -> None:
|
| 571 |
+
for encoder_name, extents, offset, args in tile:
|
| 572 |
+
if offset > 0:
|
| 573 |
+
fp.seek(offset)
|
| 574 |
+
encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
|
| 575 |
+
try:
|
| 576 |
+
encoder.setimage(im.im, extents)
|
| 577 |
+
if encoder.pushes_fd:
|
| 578 |
+
encoder.setfd(fp)
|
| 579 |
+
errcode = encoder.encode_to_pyfd()[1]
|
| 580 |
+
else:
|
| 581 |
+
if exc:
|
| 582 |
+
# compress to Python file-compatible object
|
| 583 |
+
while True:
|
| 584 |
+
errcode, data = encoder.encode(bufsize)[1:]
|
| 585 |
+
fp.write(data)
|
| 586 |
+
if errcode:
|
| 587 |
+
break
|
| 588 |
+
else:
|
| 589 |
+
# slight speedup: compress to real file object
|
| 590 |
+
assert fh is not None
|
| 591 |
+
errcode = encoder.encode_to_file(fh, bufsize)
|
| 592 |
+
if errcode < 0:
|
| 593 |
+
raise _get_oserror(errcode, encoder=True) from exc
|
| 594 |
+
finally:
|
| 595 |
+
encoder.cleanup()
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def _safe_read(fp: IO[bytes], size: int) -> bytes:
|
| 599 |
+
"""
|
| 600 |
+
Reads large blocks in a safe way. Unlike fp.read(n), this function
|
| 601 |
+
doesn't trust the user. If the requested size is larger than
|
| 602 |
+
SAFEBLOCK, the file is read block by block.
|
| 603 |
+
|
| 604 |
+
:param fp: File handle. Must implement a <b>read</b> method.
|
| 605 |
+
:param size: Number of bytes to read.
|
| 606 |
+
:returns: A string containing <i>size</i> bytes of data.
|
| 607 |
+
|
| 608 |
+
Raises an OSError if the file is truncated and the read cannot be completed
|
| 609 |
+
|
| 610 |
+
"""
|
| 611 |
+
if size <= 0:
|
| 612 |
+
return b""
|
| 613 |
+
if size <= SAFEBLOCK:
|
| 614 |
+
data = fp.read(size)
|
| 615 |
+
if len(data) < size:
|
| 616 |
+
msg = "Truncated File Read"
|
| 617 |
+
raise OSError(msg)
|
| 618 |
+
return data
|
| 619 |
+
blocks: list[bytes] = []
|
| 620 |
+
remaining_size = size
|
| 621 |
+
while remaining_size > 0:
|
| 622 |
+
block = fp.read(min(remaining_size, SAFEBLOCK))
|
| 623 |
+
if not block:
|
| 624 |
+
break
|
| 625 |
+
blocks.append(block)
|
| 626 |
+
remaining_size -= len(block)
|
| 627 |
+
if sum(len(block) for block in blocks) < size:
|
| 628 |
+
msg = "Truncated File Read"
|
| 629 |
+
raise OSError(msg)
|
| 630 |
+
return b"".join(blocks)
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
class PyCodecState:
|
| 634 |
+
def __init__(self) -> None:
|
| 635 |
+
self.xsize = 0
|
| 636 |
+
self.ysize = 0
|
| 637 |
+
self.xoff = 0
|
| 638 |
+
self.yoff = 0
|
| 639 |
+
|
| 640 |
+
def extents(self) -> tuple[int, int, int, int]:
|
| 641 |
+
return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
class PyCodec:
|
| 645 |
+
fd: IO[bytes] | None
|
| 646 |
+
|
| 647 |
+
def __init__(self, mode: str, *args: Any) -> None:
|
| 648 |
+
self.im: Image.core.ImagingCore | None = None
|
| 649 |
+
self.state = PyCodecState()
|
| 650 |
+
self.fd = None
|
| 651 |
+
self.mode = mode
|
| 652 |
+
self.init(args)
|
| 653 |
+
|
| 654 |
+
def init(self, args: tuple[Any, ...]) -> None:
|
| 655 |
+
"""
|
| 656 |
+
Override to perform codec specific initialization
|
| 657 |
+
|
| 658 |
+
:param args: Tuple of arg items from the tile entry
|
| 659 |
+
:returns: None
|
| 660 |
+
"""
|
| 661 |
+
self.args = args
|
| 662 |
+
|
| 663 |
+
def cleanup(self) -> None:
|
| 664 |
+
"""
|
| 665 |
+
Override to perform codec specific cleanup
|
| 666 |
+
|
| 667 |
+
:returns: None
|
| 668 |
+
"""
|
| 669 |
+
pass
|
| 670 |
+
|
| 671 |
+
def setfd(self, fd: IO[bytes]) -> None:
|
| 672 |
+
"""
|
| 673 |
+
Called from ImageFile to set the Python file-like object
|
| 674 |
+
|
| 675 |
+
:param fd: A Python file-like object
|
| 676 |
+
:returns: None
|
| 677 |
+
"""
|
| 678 |
+
self.fd = fd
|
| 679 |
+
|
| 680 |
+
def setimage(
|
| 681 |
+
self,
|
| 682 |
+
im: Image.core.ImagingCore,
|
| 683 |
+
extents: tuple[int, int, int, int] | None = None,
|
| 684 |
+
) -> None:
|
| 685 |
+
"""
|
| 686 |
+
Called from ImageFile to set the core output image for the codec
|
| 687 |
+
|
| 688 |
+
:param im: A core image object
|
| 689 |
+
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
|
| 690 |
+
for this tile
|
| 691 |
+
:returns: None
|
| 692 |
+
"""
|
| 693 |
+
|
| 694 |
+
# following c code
|
| 695 |
+
self.im = im
|
| 696 |
+
|
| 697 |
+
if extents:
|
| 698 |
+
(x0, y0, x1, y1) = extents
|
| 699 |
+
else:
|
| 700 |
+
(x0, y0, x1, y1) = (0, 0, 0, 0)
|
| 701 |
+
|
| 702 |
+
if x0 == 0 and x1 == 0:
|
| 703 |
+
self.state.xsize, self.state.ysize = self.im.size
|
| 704 |
+
else:
|
| 705 |
+
self.state.xoff = x0
|
| 706 |
+
self.state.yoff = y0
|
| 707 |
+
self.state.xsize = x1 - x0
|
| 708 |
+
self.state.ysize = y1 - y0
|
| 709 |
+
|
| 710 |
+
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
| 711 |
+
msg = "Size cannot be negative"
|
| 712 |
+
raise ValueError(msg)
|
| 713 |
+
|
| 714 |
+
if (
|
| 715 |
+
self.state.xsize + self.state.xoff > self.im.size[0]
|
| 716 |
+
or self.state.ysize + self.state.yoff > self.im.size[1]
|
| 717 |
+
):
|
| 718 |
+
msg = "Tile cannot extend outside image"
|
| 719 |
+
raise ValueError(msg)
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
class PyDecoder(PyCodec):
|
| 723 |
+
"""
|
| 724 |
+
Python implementation of a format decoder. Override this class and
|
| 725 |
+
add the decoding logic in the :meth:`decode` method.
|
| 726 |
+
|
| 727 |
+
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
|
| 728 |
+
"""
|
| 729 |
+
|
| 730 |
+
_pulls_fd = False
|
| 731 |
+
|
| 732 |
+
@property
|
| 733 |
+
def pulls_fd(self) -> bool:
|
| 734 |
+
return self._pulls_fd
|
| 735 |
+
|
| 736 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 737 |
+
"""
|
| 738 |
+
Override to perform the decoding process.
|
| 739 |
+
|
| 740 |
+
:param buffer: A bytes object with the data to be decoded.
|
| 741 |
+
:returns: A tuple of ``(bytes consumed, errcode)``.
|
| 742 |
+
If finished with decoding return -1 for the bytes consumed.
|
| 743 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 744 |
+
"""
|
| 745 |
+
msg = "unavailable in base decoder"
|
| 746 |
+
raise NotImplementedError(msg)
|
| 747 |
+
|
| 748 |
+
def set_as_raw(
|
| 749 |
+
self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = ()
|
| 750 |
+
) -> None:
|
| 751 |
+
"""
|
| 752 |
+
Convenience method to set the internal image from a stream of raw data
|
| 753 |
+
|
| 754 |
+
:param data: Bytes to be set
|
| 755 |
+
:param rawmode: The rawmode to be used for the decoder.
|
| 756 |
+
If not specified, it will default to the mode of the image
|
| 757 |
+
:param extra: Extra arguments for the decoder.
|
| 758 |
+
:returns: None
|
| 759 |
+
"""
|
| 760 |
+
|
| 761 |
+
if not rawmode:
|
| 762 |
+
rawmode = self.mode
|
| 763 |
+
d = Image._getdecoder(self.mode, "raw", rawmode, extra)
|
| 764 |
+
assert self.im is not None
|
| 765 |
+
d.setimage(self.im, self.state.extents())
|
| 766 |
+
s = d.decode(data)
|
| 767 |
+
|
| 768 |
+
if s[0] >= 0:
|
| 769 |
+
msg = "not enough image data"
|
| 770 |
+
raise ValueError(msg)
|
| 771 |
+
if s[1] != 0:
|
| 772 |
+
msg = "cannot decode image data"
|
| 773 |
+
raise ValueError(msg)
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
class PyEncoder(PyCodec):
|
| 777 |
+
"""
|
| 778 |
+
Python implementation of a format encoder. Override this class and
|
| 779 |
+
add the decoding logic in the :meth:`encode` method.
|
| 780 |
+
|
| 781 |
+
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
|
| 782 |
+
"""
|
| 783 |
+
|
| 784 |
+
_pushes_fd = False
|
| 785 |
+
|
| 786 |
+
@property
|
| 787 |
+
def pushes_fd(self) -> bool:
|
| 788 |
+
return self._pushes_fd
|
| 789 |
+
|
| 790 |
+
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
| 791 |
+
"""
|
| 792 |
+
Override to perform the encoding process.
|
| 793 |
+
|
| 794 |
+
:param bufsize: Buffer size.
|
| 795 |
+
:returns: A tuple of ``(bytes encoded, errcode, bytes)``.
|
| 796 |
+
If finished with encoding return 1 for the error code.
|
| 797 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 798 |
+
"""
|
| 799 |
+
msg = "unavailable in base encoder"
|
| 800 |
+
raise NotImplementedError(msg)
|
| 801 |
+
|
| 802 |
+
def encode_to_pyfd(self) -> tuple[int, int]:
|
| 803 |
+
"""
|
| 804 |
+
If ``pushes_fd`` is ``True``, then this method will be used,
|
| 805 |
+
and ``encode()`` will only be called once.
|
| 806 |
+
|
| 807 |
+
:returns: A tuple of ``(bytes consumed, errcode)``.
|
| 808 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 809 |
+
"""
|
| 810 |
+
if not self.pushes_fd:
|
| 811 |
+
return 0, -8 # bad configuration
|
| 812 |
+
bytes_consumed, errcode, data = self.encode(0)
|
| 813 |
+
if data:
|
| 814 |
+
assert self.fd is not None
|
| 815 |
+
self.fd.write(data)
|
| 816 |
+
return bytes_consumed, errcode
|
| 817 |
+
|
| 818 |
+
def encode_to_file(self, fh: int, bufsize: int) -> int:
|
| 819 |
+
"""
|
| 820 |
+
:param fh: File handle.
|
| 821 |
+
:param bufsize: Buffer size.
|
| 822 |
+
|
| 823 |
+
:returns: If finished successfully, return 0.
|
| 824 |
+
Otherwise, return an error code. Err codes are from
|
| 825 |
+
:data:`.ImageFile.ERRORS`.
|
| 826 |
+
"""
|
| 827 |
+
errcode = 0
|
| 828 |
+
while errcode == 0:
|
| 829 |
+
status, errcode, buf = self.encode(bufsize)
|
| 830 |
+
if status > 0:
|
| 831 |
+
os.write(fh, buf[status:])
|
| 832 |
+
return errcode
|
Lib/site-packages/PIL/ImageFilter.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# standard filters
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1995-11-27 fl Created
|
| 9 |
+
# 2002-06-08 fl Added rank and mode filters
|
| 10 |
+
# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) 1997-2003 by Secret Labs AB.
|
| 13 |
+
# Copyright (c) 1995-2002 by Fredrik Lundh.
|
| 14 |
+
#
|
| 15 |
+
# See the README file for information on usage and redistribution.
|
| 16 |
+
#
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import abc
|
| 20 |
+
import functools
|
| 21 |
+
from collections.abc import Sequence
|
| 22 |
+
from types import ModuleType
|
| 23 |
+
from typing import TYPE_CHECKING, Any, Callable, cast
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from . import _imaging
|
| 27 |
+
from ._typing import NumpyArray
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Filter:
|
| 31 |
+
@abc.abstractmethod
|
| 32 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 33 |
+
pass
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class MultibandFilter(Filter):
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class BuiltinFilter(MultibandFilter):
|
| 41 |
+
filterargs: tuple[Any, ...]
|
| 42 |
+
|
| 43 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 44 |
+
if image.mode == "P":
|
| 45 |
+
msg = "cannot filter palette images"
|
| 46 |
+
raise ValueError(msg)
|
| 47 |
+
return image.filter(*self.filterargs)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Kernel(BuiltinFilter):
|
| 51 |
+
"""
|
| 52 |
+
Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating
|
| 53 |
+
point kernels.
|
| 54 |
+
|
| 55 |
+
Kernels can only be applied to "L" and "RGB" images.
|
| 56 |
+
|
| 57 |
+
:param size: Kernel size, given as (width, height). This must be (3,3) or (5,5).
|
| 58 |
+
:param kernel: A sequence containing kernel weights. The kernel will be flipped
|
| 59 |
+
vertically before being applied to the image.
|
| 60 |
+
:param scale: Scale factor. If given, the result for each pixel is divided by this
|
| 61 |
+
value. The default is the sum of the kernel weights.
|
| 62 |
+
:param offset: Offset. If given, this value is added to the result, after it has
|
| 63 |
+
been divided by the scale factor.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
name = "Kernel"
|
| 67 |
+
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
size: tuple[int, int],
|
| 71 |
+
kernel: Sequence[float],
|
| 72 |
+
scale: float | None = None,
|
| 73 |
+
offset: float = 0,
|
| 74 |
+
) -> None:
|
| 75 |
+
if scale is None:
|
| 76 |
+
# default scale is sum of kernel
|
| 77 |
+
scale = functools.reduce(lambda a, b: a + b, kernel)
|
| 78 |
+
if size[0] * size[1] != len(kernel):
|
| 79 |
+
msg = "not enough coefficients in kernel"
|
| 80 |
+
raise ValueError(msg)
|
| 81 |
+
self.filterargs = size, scale, offset, kernel
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class RankFilter(Filter):
|
| 85 |
+
"""
|
| 86 |
+
Create a rank filter. The rank filter sorts all pixels in
|
| 87 |
+
a window of the given size, and returns the ``rank``'th value.
|
| 88 |
+
|
| 89 |
+
:param size: The kernel size, in pixels.
|
| 90 |
+
:param rank: What pixel value to pick. Use 0 for a min filter,
|
| 91 |
+
``size * size / 2`` for a median filter, ``size * size - 1``
|
| 92 |
+
for a max filter, etc.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
name = "Rank"
|
| 96 |
+
|
| 97 |
+
def __init__(self, size: int, rank: int) -> None:
|
| 98 |
+
self.size = size
|
| 99 |
+
self.rank = rank
|
| 100 |
+
|
| 101 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 102 |
+
if image.mode == "P":
|
| 103 |
+
msg = "cannot filter palette images"
|
| 104 |
+
raise ValueError(msg)
|
| 105 |
+
image = image.expand(self.size // 2, self.size // 2)
|
| 106 |
+
return image.rankfilter(self.size, self.rank)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class MedianFilter(RankFilter):
|
| 110 |
+
"""
|
| 111 |
+
Create a median filter. Picks the median pixel value in a window with the
|
| 112 |
+
given size.
|
| 113 |
+
|
| 114 |
+
:param size: The kernel size, in pixels.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
name = "Median"
|
| 118 |
+
|
| 119 |
+
def __init__(self, size: int = 3) -> None:
|
| 120 |
+
self.size = size
|
| 121 |
+
self.rank = size * size // 2
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class MinFilter(RankFilter):
|
| 125 |
+
"""
|
| 126 |
+
Create a min filter. Picks the lowest pixel value in a window with the
|
| 127 |
+
given size.
|
| 128 |
+
|
| 129 |
+
:param size: The kernel size, in pixels.
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
name = "Min"
|
| 133 |
+
|
| 134 |
+
def __init__(self, size: int = 3) -> None:
|
| 135 |
+
self.size = size
|
| 136 |
+
self.rank = 0
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class MaxFilter(RankFilter):
|
| 140 |
+
"""
|
| 141 |
+
Create a max filter. Picks the largest pixel value in a window with the
|
| 142 |
+
given size.
|
| 143 |
+
|
| 144 |
+
:param size: The kernel size, in pixels.
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
name = "Max"
|
| 148 |
+
|
| 149 |
+
def __init__(self, size: int = 3) -> None:
|
| 150 |
+
self.size = size
|
| 151 |
+
self.rank = size * size - 1
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class ModeFilter(Filter):
|
| 155 |
+
"""
|
| 156 |
+
Create a mode filter. Picks the most frequent pixel value in a box with the
|
| 157 |
+
given size. Pixel values that occur only once or twice are ignored; if no
|
| 158 |
+
pixel value occurs more than twice, the original pixel value is preserved.
|
| 159 |
+
|
| 160 |
+
:param size: The kernel size, in pixels.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
name = "Mode"
|
| 164 |
+
|
| 165 |
+
def __init__(self, size: int = 3) -> None:
|
| 166 |
+
self.size = size
|
| 167 |
+
|
| 168 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 169 |
+
return image.modefilter(self.size)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class GaussianBlur(MultibandFilter):
|
| 173 |
+
"""Blurs the image with a sequence of extended box filters, which
|
| 174 |
+
approximates a Gaussian kernel. For details on accuracy see
|
| 175 |
+
<https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf>
|
| 176 |
+
|
| 177 |
+
:param radius: Standard deviation of the Gaussian kernel. Either a sequence of two
|
| 178 |
+
numbers for x and y, or a single number for both.
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
name = "GaussianBlur"
|
| 182 |
+
|
| 183 |
+
def __init__(self, radius: float | Sequence[float] = 2) -> None:
|
| 184 |
+
self.radius = radius
|
| 185 |
+
|
| 186 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 187 |
+
xy = self.radius
|
| 188 |
+
if isinstance(xy, (int, float)):
|
| 189 |
+
xy = (xy, xy)
|
| 190 |
+
if xy == (0, 0):
|
| 191 |
+
return image.copy()
|
| 192 |
+
return image.gaussian_blur(xy)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class BoxBlur(MultibandFilter):
|
| 196 |
+
"""Blurs the image by setting each pixel to the average value of the pixels
|
| 197 |
+
in a square box extending radius pixels in each direction.
|
| 198 |
+
Supports float radius of arbitrary size. Uses an optimized implementation
|
| 199 |
+
which runs in linear time relative to the size of the image
|
| 200 |
+
for any radius value.
|
| 201 |
+
|
| 202 |
+
:param radius: Size of the box in a direction. Either a sequence of two numbers for
|
| 203 |
+
x and y, or a single number for both.
|
| 204 |
+
|
| 205 |
+
Radius 0 does not blur, returns an identical image.
|
| 206 |
+
Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
name = "BoxBlur"
|
| 210 |
+
|
| 211 |
+
def __init__(self, radius: float | Sequence[float]) -> None:
|
| 212 |
+
xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
|
| 213 |
+
if xy[0] < 0 or xy[1] < 0:
|
| 214 |
+
msg = "radius must be >= 0"
|
| 215 |
+
raise ValueError(msg)
|
| 216 |
+
self.radius = radius
|
| 217 |
+
|
| 218 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 219 |
+
xy = self.radius
|
| 220 |
+
if isinstance(xy, (int, float)):
|
| 221 |
+
xy = (xy, xy)
|
| 222 |
+
if xy == (0, 0):
|
| 223 |
+
return image.copy()
|
| 224 |
+
return image.box_blur(xy)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class UnsharpMask(MultibandFilter):
|
| 228 |
+
"""Unsharp mask filter.
|
| 229 |
+
|
| 230 |
+
See Wikipedia's entry on `digital unsharp masking`_ for an explanation of
|
| 231 |
+
the parameters.
|
| 232 |
+
|
| 233 |
+
:param radius: Blur Radius
|
| 234 |
+
:param percent: Unsharp strength, in percent
|
| 235 |
+
:param threshold: Threshold controls the minimum brightness change that
|
| 236 |
+
will be sharpened
|
| 237 |
+
|
| 238 |
+
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
|
| 239 |
+
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
name = "UnsharpMask"
|
| 243 |
+
|
| 244 |
+
def __init__(
|
| 245 |
+
self, radius: float = 2, percent: int = 150, threshold: int = 3
|
| 246 |
+
) -> None:
|
| 247 |
+
self.radius = radius
|
| 248 |
+
self.percent = percent
|
| 249 |
+
self.threshold = threshold
|
| 250 |
+
|
| 251 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 252 |
+
return image.unsharp_mask(self.radius, self.percent, self.threshold)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class BLUR(BuiltinFilter):
|
| 256 |
+
name = "Blur"
|
| 257 |
+
# fmt: off
|
| 258 |
+
filterargs = (5, 5), 16, 0, (
|
| 259 |
+
1, 1, 1, 1, 1,
|
| 260 |
+
1, 0, 0, 0, 1,
|
| 261 |
+
1, 0, 0, 0, 1,
|
| 262 |
+
1, 0, 0, 0, 1,
|
| 263 |
+
1, 1, 1, 1, 1,
|
| 264 |
+
)
|
| 265 |
+
# fmt: on
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class CONTOUR(BuiltinFilter):
|
| 269 |
+
name = "Contour"
|
| 270 |
+
# fmt: off
|
| 271 |
+
filterargs = (3, 3), 1, 255, (
|
| 272 |
+
-1, -1, -1,
|
| 273 |
+
-1, 8, -1,
|
| 274 |
+
-1, -1, -1,
|
| 275 |
+
)
|
| 276 |
+
# fmt: on
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class DETAIL(BuiltinFilter):
|
| 280 |
+
name = "Detail"
|
| 281 |
+
# fmt: off
|
| 282 |
+
filterargs = (3, 3), 6, 0, (
|
| 283 |
+
0, -1, 0,
|
| 284 |
+
-1, 10, -1,
|
| 285 |
+
0, -1, 0,
|
| 286 |
+
)
|
| 287 |
+
# fmt: on
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
class EDGE_ENHANCE(BuiltinFilter):
|
| 291 |
+
name = "Edge-enhance"
|
| 292 |
+
# fmt: off
|
| 293 |
+
filterargs = (3, 3), 2, 0, (
|
| 294 |
+
-1, -1, -1,
|
| 295 |
+
-1, 10, -1,
|
| 296 |
+
-1, -1, -1,
|
| 297 |
+
)
|
| 298 |
+
# fmt: on
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class EDGE_ENHANCE_MORE(BuiltinFilter):
|
| 302 |
+
name = "Edge-enhance More"
|
| 303 |
+
# fmt: off
|
| 304 |
+
filterargs = (3, 3), 1, 0, (
|
| 305 |
+
-1, -1, -1,
|
| 306 |
+
-1, 9, -1,
|
| 307 |
+
-1, -1, -1,
|
| 308 |
+
)
|
| 309 |
+
# fmt: on
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class EMBOSS(BuiltinFilter):
|
| 313 |
+
name = "Emboss"
|
| 314 |
+
# fmt: off
|
| 315 |
+
filterargs = (3, 3), 1, 128, (
|
| 316 |
+
-1, 0, 0,
|
| 317 |
+
0, 1, 0,
|
| 318 |
+
0, 0, 0,
|
| 319 |
+
)
|
| 320 |
+
# fmt: on
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
class FIND_EDGES(BuiltinFilter):
|
| 324 |
+
name = "Find Edges"
|
| 325 |
+
# fmt: off
|
| 326 |
+
filterargs = (3, 3), 1, 0, (
|
| 327 |
+
-1, -1, -1,
|
| 328 |
+
-1, 8, -1,
|
| 329 |
+
-1, -1, -1,
|
| 330 |
+
)
|
| 331 |
+
# fmt: on
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
class SHARPEN(BuiltinFilter):
|
| 335 |
+
name = "Sharpen"
|
| 336 |
+
# fmt: off
|
| 337 |
+
filterargs = (3, 3), 16, 0, (
|
| 338 |
+
-2, -2, -2,
|
| 339 |
+
-2, 32, -2,
|
| 340 |
+
-2, -2, -2,
|
| 341 |
+
)
|
| 342 |
+
# fmt: on
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
class SMOOTH(BuiltinFilter):
|
| 346 |
+
name = "Smooth"
|
| 347 |
+
# fmt: off
|
| 348 |
+
filterargs = (3, 3), 13, 0, (
|
| 349 |
+
1, 1, 1,
|
| 350 |
+
1, 5, 1,
|
| 351 |
+
1, 1, 1,
|
| 352 |
+
)
|
| 353 |
+
# fmt: on
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
class SMOOTH_MORE(BuiltinFilter):
|
| 357 |
+
name = "Smooth More"
|
| 358 |
+
# fmt: off
|
| 359 |
+
filterargs = (5, 5), 100, 0, (
|
| 360 |
+
1, 1, 1, 1, 1,
|
| 361 |
+
1, 5, 5, 5, 1,
|
| 362 |
+
1, 5, 44, 5, 1,
|
| 363 |
+
1, 5, 5, 5, 1,
|
| 364 |
+
1, 1, 1, 1, 1,
|
| 365 |
+
)
|
| 366 |
+
# fmt: on
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
class Color3DLUT(MultibandFilter):
|
| 370 |
+
"""Three-dimensional color lookup table.
|
| 371 |
+
|
| 372 |
+
Transforms 3-channel pixels using the values of the channels as coordinates
|
| 373 |
+
in the 3D lookup table and interpolating the nearest elements.
|
| 374 |
+
|
| 375 |
+
This method allows you to apply almost any color transformation
|
| 376 |
+
in constant time by using pre-calculated decimated tables.
|
| 377 |
+
|
| 378 |
+
.. versionadded:: 5.2.0
|
| 379 |
+
|
| 380 |
+
:param size: Size of the table. One int or tuple of (int, int, int).
|
| 381 |
+
Minimal size in any dimension is 2, maximum is 65.
|
| 382 |
+
:param table: Flat lookup table. A list of ``channels * size**3``
|
| 383 |
+
float elements or a list of ``size**3`` channels-sized
|
| 384 |
+
tuples with floats. Channels are changed first,
|
| 385 |
+
then first dimension, then second, then third.
|
| 386 |
+
Value 0.0 corresponds lowest value of output, 1.0 highest.
|
| 387 |
+
:param channels: Number of channels in the table. Could be 3 or 4.
|
| 388 |
+
Default is 3.
|
| 389 |
+
:param target_mode: A mode for the result image. Should have not less
|
| 390 |
+
than ``channels`` channels. Default is ``None``,
|
| 391 |
+
which means that mode wouldn't be changed.
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
name = "Color 3D LUT"
|
| 395 |
+
|
| 396 |
+
def __init__(
|
| 397 |
+
self,
|
| 398 |
+
size: int | tuple[int, int, int],
|
| 399 |
+
table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
|
| 400 |
+
channels: int = 3,
|
| 401 |
+
target_mode: str | None = None,
|
| 402 |
+
**kwargs: bool,
|
| 403 |
+
) -> None:
|
| 404 |
+
if channels not in (3, 4):
|
| 405 |
+
msg = "Only 3 or 4 output channels are supported"
|
| 406 |
+
raise ValueError(msg)
|
| 407 |
+
self.size = size = self._check_size(size)
|
| 408 |
+
self.channels = channels
|
| 409 |
+
self.mode = target_mode
|
| 410 |
+
|
| 411 |
+
# Hidden flag `_copy_table=False` could be used to avoid extra copying
|
| 412 |
+
# of the table if the table is specially made for the constructor.
|
| 413 |
+
copy_table = kwargs.get("_copy_table", True)
|
| 414 |
+
items = size[0] * size[1] * size[2]
|
| 415 |
+
wrong_size = False
|
| 416 |
+
|
| 417 |
+
numpy: ModuleType | None = None
|
| 418 |
+
if hasattr(table, "shape"):
|
| 419 |
+
try:
|
| 420 |
+
import numpy
|
| 421 |
+
except ImportError:
|
| 422 |
+
pass
|
| 423 |
+
|
| 424 |
+
if numpy and isinstance(table, numpy.ndarray):
|
| 425 |
+
numpy_table: NumpyArray = table
|
| 426 |
+
if copy_table:
|
| 427 |
+
numpy_table = numpy_table.copy()
|
| 428 |
+
|
| 429 |
+
if numpy_table.shape in [
|
| 430 |
+
(items * channels,),
|
| 431 |
+
(items, channels),
|
| 432 |
+
(size[2], size[1], size[0], channels),
|
| 433 |
+
]:
|
| 434 |
+
table = numpy_table.reshape(items * channels)
|
| 435 |
+
else:
|
| 436 |
+
wrong_size = True
|
| 437 |
+
|
| 438 |
+
else:
|
| 439 |
+
if copy_table:
|
| 440 |
+
table = list(table)
|
| 441 |
+
|
| 442 |
+
# Convert to a flat list
|
| 443 |
+
if table and isinstance(table[0], (list, tuple)):
|
| 444 |
+
raw_table = cast(Sequence[Sequence[int]], table)
|
| 445 |
+
flat_table: list[int] = []
|
| 446 |
+
for pixel in raw_table:
|
| 447 |
+
if len(pixel) != channels:
|
| 448 |
+
msg = (
|
| 449 |
+
"The elements of the table should "
|
| 450 |
+
f"have a length of {channels}."
|
| 451 |
+
)
|
| 452 |
+
raise ValueError(msg)
|
| 453 |
+
flat_table.extend(pixel)
|
| 454 |
+
table = flat_table
|
| 455 |
+
|
| 456 |
+
if wrong_size or len(table) != items * channels:
|
| 457 |
+
msg = (
|
| 458 |
+
"The table should have either channels * size**3 float items "
|
| 459 |
+
"or size**3 items of channels-sized tuples with floats. "
|
| 460 |
+
f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. "
|
| 461 |
+
f"Actual length: {len(table)}"
|
| 462 |
+
)
|
| 463 |
+
raise ValueError(msg)
|
| 464 |
+
self.table = table
|
| 465 |
+
|
| 466 |
+
@staticmethod
|
| 467 |
+
def _check_size(size: Any) -> tuple[int, int, int]:
|
| 468 |
+
try:
|
| 469 |
+
_, _, _ = size
|
| 470 |
+
except ValueError as e:
|
| 471 |
+
msg = "Size should be either an integer or a tuple of three integers."
|
| 472 |
+
raise ValueError(msg) from e
|
| 473 |
+
except TypeError:
|
| 474 |
+
size = (size, size, size)
|
| 475 |
+
size = tuple(int(x) for x in size)
|
| 476 |
+
for size_1d in size:
|
| 477 |
+
if not 2 <= size_1d <= 65:
|
| 478 |
+
msg = "Size should be in [2, 65] range."
|
| 479 |
+
raise ValueError(msg)
|
| 480 |
+
return size
|
| 481 |
+
|
| 482 |
+
@classmethod
|
| 483 |
+
def generate(
|
| 484 |
+
cls,
|
| 485 |
+
size: int | tuple[int, int, int],
|
| 486 |
+
callback: Callable[[float, float, float], tuple[float, ...]],
|
| 487 |
+
channels: int = 3,
|
| 488 |
+
target_mode: str | None = None,
|
| 489 |
+
) -> Color3DLUT:
|
| 490 |
+
"""Generates new LUT using provided callback.
|
| 491 |
+
|
| 492 |
+
:param size: Size of the table. Passed to the constructor.
|
| 493 |
+
:param callback: Function with three parameters which correspond
|
| 494 |
+
three color channels. Will be called ``size**3``
|
| 495 |
+
times with values from 0.0 to 1.0 and should return
|
| 496 |
+
a tuple with ``channels`` elements.
|
| 497 |
+
:param channels: The number of channels which should return callback.
|
| 498 |
+
:param target_mode: Passed to the constructor of the resulting
|
| 499 |
+
lookup table.
|
| 500 |
+
"""
|
| 501 |
+
size_1d, size_2d, size_3d = cls._check_size(size)
|
| 502 |
+
if channels not in (3, 4):
|
| 503 |
+
msg = "Only 3 or 4 output channels are supported"
|
| 504 |
+
raise ValueError(msg)
|
| 505 |
+
|
| 506 |
+
table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
|
| 507 |
+
idx_out = 0
|
| 508 |
+
for b in range(size_3d):
|
| 509 |
+
for g in range(size_2d):
|
| 510 |
+
for r in range(size_1d):
|
| 511 |
+
table[idx_out : idx_out + channels] = callback(
|
| 512 |
+
r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1)
|
| 513 |
+
)
|
| 514 |
+
idx_out += channels
|
| 515 |
+
|
| 516 |
+
return cls(
|
| 517 |
+
(size_1d, size_2d, size_3d),
|
| 518 |
+
table,
|
| 519 |
+
channels=channels,
|
| 520 |
+
target_mode=target_mode,
|
| 521 |
+
_copy_table=False,
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
def transform(
|
| 525 |
+
self,
|
| 526 |
+
callback: Callable[..., tuple[float, ...]],
|
| 527 |
+
with_normals: bool = False,
|
| 528 |
+
channels: int | None = None,
|
| 529 |
+
target_mode: str | None = None,
|
| 530 |
+
) -> Color3DLUT:
|
| 531 |
+
"""Transforms the table values using provided callback and returns
|
| 532 |
+
a new LUT with altered values.
|
| 533 |
+
|
| 534 |
+
:param callback: A function which takes old lookup table values
|
| 535 |
+
and returns a new set of values. The number
|
| 536 |
+
of arguments which function should take is
|
| 537 |
+
``self.channels`` or ``3 + self.channels``
|
| 538 |
+
if ``with_normals`` flag is set.
|
| 539 |
+
Should return a tuple of ``self.channels`` or
|
| 540 |
+
``channels`` elements if it is set.
|
| 541 |
+
:param with_normals: If true, ``callback`` will be called with
|
| 542 |
+
coordinates in the color cube as the first
|
| 543 |
+
three arguments. Otherwise, ``callback``
|
| 544 |
+
will be called only with actual color values.
|
| 545 |
+
:param channels: The number of channels in the resulting lookup table.
|
| 546 |
+
:param target_mode: Passed to the constructor of the resulting
|
| 547 |
+
lookup table.
|
| 548 |
+
"""
|
| 549 |
+
if channels not in (None, 3, 4):
|
| 550 |
+
msg = "Only 3 or 4 output channels are supported"
|
| 551 |
+
raise ValueError(msg)
|
| 552 |
+
ch_in = self.channels
|
| 553 |
+
ch_out = channels or ch_in
|
| 554 |
+
size_1d, size_2d, size_3d = self.size
|
| 555 |
+
|
| 556 |
+
table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
|
| 557 |
+
idx_in = 0
|
| 558 |
+
idx_out = 0
|
| 559 |
+
for b in range(size_3d):
|
| 560 |
+
for g in range(size_2d):
|
| 561 |
+
for r in range(size_1d):
|
| 562 |
+
values = self.table[idx_in : idx_in + ch_in]
|
| 563 |
+
if with_normals:
|
| 564 |
+
values = callback(
|
| 565 |
+
r / (size_1d - 1),
|
| 566 |
+
g / (size_2d - 1),
|
| 567 |
+
b / (size_3d - 1),
|
| 568 |
+
*values,
|
| 569 |
+
)
|
| 570 |
+
else:
|
| 571 |
+
values = callback(*values)
|
| 572 |
+
table[idx_out : idx_out + ch_out] = values
|
| 573 |
+
idx_in += ch_in
|
| 574 |
+
idx_out += ch_out
|
| 575 |
+
|
| 576 |
+
return type(self)(
|
| 577 |
+
self.size,
|
| 578 |
+
table,
|
| 579 |
+
channels=ch_out,
|
| 580 |
+
target_mode=target_mode or self.mode,
|
| 581 |
+
_copy_table=False,
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
def __repr__(self) -> str:
|
| 585 |
+
r = [
|
| 586 |
+
f"{self.__class__.__name__} from {self.table.__class__.__name__}",
|
| 587 |
+
"size={:d}x{:d}x{:d}".format(*self.size),
|
| 588 |
+
f"channels={self.channels:d}",
|
| 589 |
+
]
|
| 590 |
+
if self.mode:
|
| 591 |
+
r.append(f"target_mode={self.mode}")
|
| 592 |
+
return "<{}>".format(" ".join(r))
|
| 593 |
+
|
| 594 |
+
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
| 595 |
+
from . import Image
|
| 596 |
+
|
| 597 |
+
return image.color_lut_3d(
|
| 598 |
+
self.mode or image.mode,
|
| 599 |
+
Image.Resampling.BILINEAR,
|
| 600 |
+
self.channels,
|
| 601 |
+
self.size[0],
|
| 602 |
+
self.size[1],
|
| 603 |
+
self.size[2],
|
| 604 |
+
self.table,
|
| 605 |
+
)
|
Lib/site-packages/PIL/ImageFont.py
ADDED
|
@@ -0,0 +1,1338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# PIL raster font management
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-08-07 fl created (experimental)
|
| 9 |
+
# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3
|
| 10 |
+
# 1999-02-06 fl rewrote most font management stuff in C
|
| 11 |
+
# 1999-03-17 fl take pth files into account in load_path (from Richard Jones)
|
| 12 |
+
# 2001-02-17 fl added freetype support
|
| 13 |
+
# 2001-05-09 fl added TransposedFont wrapper class
|
| 14 |
+
# 2002-03-04 fl make sure we have a "L" or "1" font
|
| 15 |
+
# 2002-12-04 fl skip non-directory entries in the system path
|
| 16 |
+
# 2003-04-29 fl add embedded default font
|
| 17 |
+
# 2003-09-27 fl added support for truetype charmap encodings
|
| 18 |
+
#
|
| 19 |
+
# Todo:
|
| 20 |
+
# Adapt to PILFONT2 format (16-bit fonts, compressed, single file)
|
| 21 |
+
#
|
| 22 |
+
# Copyright (c) 1997-2003 by Secret Labs AB
|
| 23 |
+
# Copyright (c) 1996-2003 by Fredrik Lundh
|
| 24 |
+
#
|
| 25 |
+
# See the README file for information on usage and redistribution.
|
| 26 |
+
#
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import base64
|
| 31 |
+
import os
|
| 32 |
+
import sys
|
| 33 |
+
import warnings
|
| 34 |
+
from enum import IntEnum
|
| 35 |
+
from io import BytesIO
|
| 36 |
+
from types import ModuleType
|
| 37 |
+
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
|
| 38 |
+
|
| 39 |
+
from . import Image, features
|
| 40 |
+
from ._typing import StrOrBytesPath
|
| 41 |
+
from ._util import DeferredError, is_path
|
| 42 |
+
|
| 43 |
+
if TYPE_CHECKING:
|
| 44 |
+
from . import ImageFile
|
| 45 |
+
from ._imaging import ImagingFont
|
| 46 |
+
from ._imagingft import Font
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Axis(TypedDict):
|
| 50 |
+
minimum: int | None
|
| 51 |
+
default: int | None
|
| 52 |
+
maximum: int | None
|
| 53 |
+
name: bytes | None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class Layout(IntEnum):
|
| 57 |
+
BASIC = 0
|
| 58 |
+
RAQM = 1
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
MAX_STRING_LENGTH = 1_000_000
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
core: ModuleType | DeferredError
|
| 65 |
+
try:
|
| 66 |
+
from . import _imagingft as core
|
| 67 |
+
except ImportError as ex:
|
| 68 |
+
core = DeferredError.new(ex)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _string_length_check(text: str | bytes | bytearray) -> None:
|
| 72 |
+
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
|
| 73 |
+
msg = "too many characters in string"
|
| 74 |
+
raise ValueError(msg)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# FIXME: add support for pilfont2 format (see FontFile.py)
|
| 78 |
+
|
| 79 |
+
# --------------------------------------------------------------------
|
| 80 |
+
# Font metrics format:
|
| 81 |
+
# "PILfont" LF
|
| 82 |
+
# fontdescriptor LF
|
| 83 |
+
# (optional) key=value... LF
|
| 84 |
+
# "DATA" LF
|
| 85 |
+
# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox)
|
| 86 |
+
#
|
| 87 |
+
# To place a character, cut out srcbox and paste at dstbox,
|
| 88 |
+
# relative to the character position. Then move the character
|
| 89 |
+
# position according to dx, dy.
|
| 90 |
+
# --------------------------------------------------------------------
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class ImageFont:
|
| 94 |
+
"""PIL font wrapper"""
|
| 95 |
+
|
| 96 |
+
font: ImagingFont
|
| 97 |
+
|
| 98 |
+
def _load_pilfont(self, filename: str) -> None:
|
| 99 |
+
with open(filename, "rb") as fp:
|
| 100 |
+
image: ImageFile.ImageFile | None = None
|
| 101 |
+
root = os.path.splitext(filename)[0]
|
| 102 |
+
|
| 103 |
+
for ext in (".png", ".gif", ".pbm"):
|
| 104 |
+
if image:
|
| 105 |
+
image.close()
|
| 106 |
+
try:
|
| 107 |
+
fullname = root + ext
|
| 108 |
+
image = Image.open(fullname)
|
| 109 |
+
except Exception:
|
| 110 |
+
pass
|
| 111 |
+
else:
|
| 112 |
+
if image and image.mode in ("1", "L"):
|
| 113 |
+
break
|
| 114 |
+
else:
|
| 115 |
+
if image:
|
| 116 |
+
image.close()
|
| 117 |
+
|
| 118 |
+
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
|
| 119 |
+
raise OSError(msg)
|
| 120 |
+
|
| 121 |
+
self.file = fullname
|
| 122 |
+
|
| 123 |
+
self._load_pilfont_data(fp, image)
|
| 124 |
+
image.close()
|
| 125 |
+
|
| 126 |
+
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
|
| 127 |
+
# read PILfont header
|
| 128 |
+
if file.readline() != b"PILfont\n":
|
| 129 |
+
msg = "Not a PILfont file"
|
| 130 |
+
raise SyntaxError(msg)
|
| 131 |
+
file.readline().split(b";")
|
| 132 |
+
self.info = [] # FIXME: should be a dictionary
|
| 133 |
+
while True:
|
| 134 |
+
s = file.readline()
|
| 135 |
+
if not s or s == b"DATA\n":
|
| 136 |
+
break
|
| 137 |
+
self.info.append(s)
|
| 138 |
+
|
| 139 |
+
# read PILfont metrics
|
| 140 |
+
data = file.read(256 * 20)
|
| 141 |
+
|
| 142 |
+
# check image
|
| 143 |
+
if image.mode not in ("1", "L"):
|
| 144 |
+
msg = "invalid font image mode"
|
| 145 |
+
raise TypeError(msg)
|
| 146 |
+
|
| 147 |
+
image.load()
|
| 148 |
+
|
| 149 |
+
self.font = Image.core.font(image.im, data)
|
| 150 |
+
|
| 151 |
+
def getmask(
|
| 152 |
+
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
|
| 153 |
+
) -> Image.core.ImagingCore:
|
| 154 |
+
"""
|
| 155 |
+
Create a bitmap for the text.
|
| 156 |
+
|
| 157 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 158 |
+
maximum value of 255. Otherwise, it should have mode ``1``.
|
| 159 |
+
|
| 160 |
+
:param text: Text to render.
|
| 161 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 162 |
+
driver prefers; if empty, the renderer may return either
|
| 163 |
+
mode. Note that the mode is always a string, to simplify
|
| 164 |
+
C-level implementations.
|
| 165 |
+
|
| 166 |
+
.. versionadded:: 1.1.5
|
| 167 |
+
|
| 168 |
+
:return: An internal PIL storage memory instance as defined by the
|
| 169 |
+
:py:mod:`PIL.Image.core` interface module.
|
| 170 |
+
"""
|
| 171 |
+
_string_length_check(text)
|
| 172 |
+
Image._decompression_bomb_check(self.font.getsize(text))
|
| 173 |
+
return self.font.getmask(text, mode)
|
| 174 |
+
|
| 175 |
+
def getbbox(
|
| 176 |
+
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
|
| 177 |
+
) -> tuple[int, int, int, int]:
|
| 178 |
+
"""
|
| 179 |
+
Returns bounding box (in pixels) of given text.
|
| 180 |
+
|
| 181 |
+
.. versionadded:: 9.2.0
|
| 182 |
+
|
| 183 |
+
:param text: Text to render.
|
| 184 |
+
|
| 185 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 186 |
+
"""
|
| 187 |
+
_string_length_check(text)
|
| 188 |
+
width, height = self.font.getsize(text)
|
| 189 |
+
return 0, 0, width, height
|
| 190 |
+
|
| 191 |
+
def getlength(
|
| 192 |
+
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
|
| 193 |
+
) -> int:
|
| 194 |
+
"""
|
| 195 |
+
Returns length (in pixels) of given text.
|
| 196 |
+
This is the amount by which following text should be offset.
|
| 197 |
+
|
| 198 |
+
.. versionadded:: 9.2.0
|
| 199 |
+
"""
|
| 200 |
+
_string_length_check(text)
|
| 201 |
+
width, height = self.font.getsize(text)
|
| 202 |
+
return width
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
##
|
| 206 |
+
# Wrapper for FreeType fonts. Application code should use the
|
| 207 |
+
# <b>truetype</b> factory function to create font objects.
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class FreeTypeFont:
|
| 211 |
+
"""FreeType font wrapper (requires _imagingft service)"""
|
| 212 |
+
|
| 213 |
+
font: Font
|
| 214 |
+
font_bytes: bytes
|
| 215 |
+
|
| 216 |
+
def __init__(
|
| 217 |
+
self,
|
| 218 |
+
font: StrOrBytesPath | BinaryIO,
|
| 219 |
+
size: float = 10,
|
| 220 |
+
index: int = 0,
|
| 221 |
+
encoding: str = "",
|
| 222 |
+
layout_engine: Layout | None = None,
|
| 223 |
+
) -> None:
|
| 224 |
+
# FIXME: use service provider instead
|
| 225 |
+
|
| 226 |
+
if isinstance(core, DeferredError):
|
| 227 |
+
raise core.ex
|
| 228 |
+
|
| 229 |
+
if size <= 0:
|
| 230 |
+
msg = f"font size must be greater than 0, not {size}"
|
| 231 |
+
raise ValueError(msg)
|
| 232 |
+
|
| 233 |
+
self.path = font
|
| 234 |
+
self.size = size
|
| 235 |
+
self.index = index
|
| 236 |
+
self.encoding = encoding
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
from packaging.version import parse as parse_version
|
| 240 |
+
except ImportError:
|
| 241 |
+
pass
|
| 242 |
+
else:
|
| 243 |
+
if freetype_version := features.version_module("freetype2"):
|
| 244 |
+
if parse_version(freetype_version) < parse_version("2.9.1"):
|
| 245 |
+
warnings.warn(
|
| 246 |
+
"Support for FreeType 2.9.0 is deprecated and will be removed "
|
| 247 |
+
"in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 "
|
| 248 |
+
"or newer, preferably FreeType 2.10.4 which fixes "
|
| 249 |
+
"CVE-2020-15999.",
|
| 250 |
+
DeprecationWarning,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
if layout_engine not in (Layout.BASIC, Layout.RAQM):
|
| 254 |
+
layout_engine = Layout.BASIC
|
| 255 |
+
if core.HAVE_RAQM:
|
| 256 |
+
layout_engine = Layout.RAQM
|
| 257 |
+
elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
|
| 258 |
+
warnings.warn(
|
| 259 |
+
"Raqm layout was requested, but Raqm is not available. "
|
| 260 |
+
"Falling back to basic layout."
|
| 261 |
+
)
|
| 262 |
+
layout_engine = Layout.BASIC
|
| 263 |
+
|
| 264 |
+
self.layout_engine = layout_engine
|
| 265 |
+
|
| 266 |
+
def load_from_bytes(f: IO[bytes]) -> None:
|
| 267 |
+
self.font_bytes = f.read()
|
| 268 |
+
self.font = core.getfont(
|
| 269 |
+
"", size, index, encoding, self.font_bytes, layout_engine
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
if is_path(font):
|
| 273 |
+
font = os.fspath(font)
|
| 274 |
+
if sys.platform == "win32":
|
| 275 |
+
font_bytes_path = font if isinstance(font, bytes) else font.encode()
|
| 276 |
+
try:
|
| 277 |
+
font_bytes_path.decode("ascii")
|
| 278 |
+
except UnicodeDecodeError:
|
| 279 |
+
# FreeType cannot load fonts with non-ASCII characters on Windows
|
| 280 |
+
# So load it into memory first
|
| 281 |
+
with open(font, "rb") as f:
|
| 282 |
+
load_from_bytes(f)
|
| 283 |
+
return
|
| 284 |
+
self.font = core.getfont(
|
| 285 |
+
font, size, index, encoding, layout_engine=layout_engine
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
load_from_bytes(cast(IO[bytes], font))
|
| 289 |
+
|
| 290 |
+
def __getstate__(self) -> list[Any]:
|
| 291 |
+
return [self.path, self.size, self.index, self.encoding, self.layout_engine]
|
| 292 |
+
|
| 293 |
+
def __setstate__(self, state: list[Any]) -> None:
|
| 294 |
+
path, size, index, encoding, layout_engine = state
|
| 295 |
+
FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine)
|
| 296 |
+
|
| 297 |
+
def getname(self) -> tuple[str | None, str | None]:
|
| 298 |
+
"""
|
| 299 |
+
:return: A tuple of the font family (e.g. Helvetica) and the font style
|
| 300 |
+
(e.g. Bold)
|
| 301 |
+
"""
|
| 302 |
+
return self.font.family, self.font.style
|
| 303 |
+
|
| 304 |
+
def getmetrics(self) -> tuple[int, int]:
|
| 305 |
+
"""
|
| 306 |
+
:return: A tuple of the font ascent (the distance from the baseline to
|
| 307 |
+
the highest outline point) and descent (the distance from the
|
| 308 |
+
baseline to the lowest outline point, a negative value)
|
| 309 |
+
"""
|
| 310 |
+
return self.font.ascent, self.font.descent
|
| 311 |
+
|
| 312 |
+
def getlength(
|
| 313 |
+
self,
|
| 314 |
+
text: str | bytes,
|
| 315 |
+
mode: str = "",
|
| 316 |
+
direction: str | None = None,
|
| 317 |
+
features: list[str] | None = None,
|
| 318 |
+
language: str | None = None,
|
| 319 |
+
) -> float:
|
| 320 |
+
"""
|
| 321 |
+
Returns length (in pixels with 1/64 precision) of given text when rendered
|
| 322 |
+
in font with provided direction, features, and language.
|
| 323 |
+
|
| 324 |
+
This is the amount by which following text should be offset.
|
| 325 |
+
Text bounding box may extend past the length in some fonts,
|
| 326 |
+
e.g. when using italics or accents.
|
| 327 |
+
|
| 328 |
+
The result is returned as a float; it is a whole number if using basic layout.
|
| 329 |
+
|
| 330 |
+
Note that the sum of two lengths may not equal the length of a concatenated
|
| 331 |
+
string due to kerning. If you need to adjust for kerning, include the following
|
| 332 |
+
character and subtract its length.
|
| 333 |
+
|
| 334 |
+
For example, instead of ::
|
| 335 |
+
|
| 336 |
+
hello = font.getlength("Hello")
|
| 337 |
+
world = font.getlength("World")
|
| 338 |
+
hello_world = hello + world # not adjusted for kerning
|
| 339 |
+
assert hello_world == font.getlength("HelloWorld") # may fail
|
| 340 |
+
|
| 341 |
+
use ::
|
| 342 |
+
|
| 343 |
+
hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning
|
| 344 |
+
world = font.getlength("World")
|
| 345 |
+
hello_world = hello + world # adjusted for kerning
|
| 346 |
+
assert hello_world == font.getlength("HelloWorld") # True
|
| 347 |
+
|
| 348 |
+
or disable kerning with (requires libraqm) ::
|
| 349 |
+
|
| 350 |
+
hello = draw.textlength("Hello", font, features=["-kern"])
|
| 351 |
+
world = draw.textlength("World", font, features=["-kern"])
|
| 352 |
+
hello_world = hello + world # kerning is disabled, no need to adjust
|
| 353 |
+
assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"])
|
| 354 |
+
|
| 355 |
+
.. versionadded:: 8.0.0
|
| 356 |
+
|
| 357 |
+
:param text: Text to measure.
|
| 358 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 359 |
+
driver prefers; if empty, the renderer may return either
|
| 360 |
+
mode. Note that the mode is always a string, to simplify
|
| 361 |
+
C-level implementations.
|
| 362 |
+
|
| 363 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 364 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 365 |
+
Requires libraqm.
|
| 366 |
+
|
| 367 |
+
:param features: A list of OpenType font features to be used during text
|
| 368 |
+
layout. This is usually used to turn on optional
|
| 369 |
+
font features that are not enabled by default,
|
| 370 |
+
for example 'dlig' or 'ss01', but can be also
|
| 371 |
+
used to turn off default font features for
|
| 372 |
+
example '-liga' to disable ligatures or '-kern'
|
| 373 |
+
to disable kerning. To get all supported
|
| 374 |
+
features, see
|
| 375 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 376 |
+
Requires libraqm.
|
| 377 |
+
|
| 378 |
+
:param language: Language of the text. Different languages may use
|
| 379 |
+
different glyph shapes or ligatures. This parameter tells
|
| 380 |
+
the font which language the text is in, and to apply the
|
| 381 |
+
correct substitutions as appropriate, if available.
|
| 382 |
+
It should be a `BCP 47 language code
|
| 383 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 384 |
+
Requires libraqm.
|
| 385 |
+
|
| 386 |
+
:return: Either width for horizontal text, or height for vertical text.
|
| 387 |
+
"""
|
| 388 |
+
_string_length_check(text)
|
| 389 |
+
return self.font.getlength(text, mode, direction, features, language) / 64
|
| 390 |
+
|
| 391 |
+
def getbbox(
|
| 392 |
+
self,
|
| 393 |
+
text: str | bytes,
|
| 394 |
+
mode: str = "",
|
| 395 |
+
direction: str | None = None,
|
| 396 |
+
features: list[str] | None = None,
|
| 397 |
+
language: str | None = None,
|
| 398 |
+
stroke_width: float = 0,
|
| 399 |
+
anchor: str | None = None,
|
| 400 |
+
) -> tuple[float, float, float, float]:
|
| 401 |
+
"""
|
| 402 |
+
Returns bounding box (in pixels) of given text relative to given anchor
|
| 403 |
+
when rendered in font with provided direction, features, and language.
|
| 404 |
+
|
| 405 |
+
Use :py:meth:`getlength()` to get the offset of following text with
|
| 406 |
+
1/64 pixel precision. The bounding box includes extra margins for
|
| 407 |
+
some fonts, e.g. italics or accents.
|
| 408 |
+
|
| 409 |
+
.. versionadded:: 8.0.0
|
| 410 |
+
|
| 411 |
+
:param text: Text to render.
|
| 412 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 413 |
+
driver prefers; if empty, the renderer may return either
|
| 414 |
+
mode. Note that the mode is always a string, to simplify
|
| 415 |
+
C-level implementations.
|
| 416 |
+
|
| 417 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 418 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 419 |
+
Requires libraqm.
|
| 420 |
+
|
| 421 |
+
:param features: A list of OpenType font features to be used during text
|
| 422 |
+
layout. This is usually used to turn on optional
|
| 423 |
+
font features that are not enabled by default,
|
| 424 |
+
for example 'dlig' or 'ss01', but can be also
|
| 425 |
+
used to turn off default font features for
|
| 426 |
+
example '-liga' to disable ligatures or '-kern'
|
| 427 |
+
to disable kerning. To get all supported
|
| 428 |
+
features, see
|
| 429 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 430 |
+
Requires libraqm.
|
| 431 |
+
|
| 432 |
+
:param language: Language of the text. Different languages may use
|
| 433 |
+
different glyph shapes or ligatures. This parameter tells
|
| 434 |
+
the font which language the text is in, and to apply the
|
| 435 |
+
correct substitutions as appropriate, if available.
|
| 436 |
+
It should be a `BCP 47 language code
|
| 437 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 438 |
+
Requires libraqm.
|
| 439 |
+
|
| 440 |
+
:param stroke_width: The width of the text stroke.
|
| 441 |
+
|
| 442 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 443 |
+
the anchor to the text. The default alignment is top left,
|
| 444 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 445 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 446 |
+
|
| 447 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 448 |
+
"""
|
| 449 |
+
_string_length_check(text)
|
| 450 |
+
size, offset = self.font.getsize(
|
| 451 |
+
text, mode, direction, features, language, anchor
|
| 452 |
+
)
|
| 453 |
+
left, top = offset[0] - stroke_width, offset[1] - stroke_width
|
| 454 |
+
width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width
|
| 455 |
+
return left, top, left + width, top + height
|
| 456 |
+
|
| 457 |
+
def getmask(
|
| 458 |
+
self,
|
| 459 |
+
text: str | bytes,
|
| 460 |
+
mode: str = "",
|
| 461 |
+
direction: str | None = None,
|
| 462 |
+
features: list[str] | None = None,
|
| 463 |
+
language: str | None = None,
|
| 464 |
+
stroke_width: float = 0,
|
| 465 |
+
anchor: str | None = None,
|
| 466 |
+
ink: int = 0,
|
| 467 |
+
start: tuple[float, float] | None = None,
|
| 468 |
+
) -> Image.core.ImagingCore:
|
| 469 |
+
"""
|
| 470 |
+
Create a bitmap for the text.
|
| 471 |
+
|
| 472 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 473 |
+
maximum value of 255. If the font has embedded color data, the bitmap
|
| 474 |
+
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
| 475 |
+
|
| 476 |
+
:param text: Text to render.
|
| 477 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 478 |
+
driver prefers; if empty, the renderer may return either
|
| 479 |
+
mode. Note that the mode is always a string, to simplify
|
| 480 |
+
C-level implementations.
|
| 481 |
+
|
| 482 |
+
.. versionadded:: 1.1.5
|
| 483 |
+
|
| 484 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 485 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 486 |
+
Requires libraqm.
|
| 487 |
+
|
| 488 |
+
.. versionadded:: 4.2.0
|
| 489 |
+
|
| 490 |
+
:param features: A list of OpenType font features to be used during text
|
| 491 |
+
layout. This is usually used to turn on optional
|
| 492 |
+
font features that are not enabled by default,
|
| 493 |
+
for example 'dlig' or 'ss01', but can be also
|
| 494 |
+
used to turn off default font features for
|
| 495 |
+
example '-liga' to disable ligatures or '-kern'
|
| 496 |
+
to disable kerning. To get all supported
|
| 497 |
+
features, see
|
| 498 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 499 |
+
Requires libraqm.
|
| 500 |
+
|
| 501 |
+
.. versionadded:: 4.2.0
|
| 502 |
+
|
| 503 |
+
:param language: Language of the text. Different languages may use
|
| 504 |
+
different glyph shapes or ligatures. This parameter tells
|
| 505 |
+
the font which language the text is in, and to apply the
|
| 506 |
+
correct substitutions as appropriate, if available.
|
| 507 |
+
It should be a `BCP 47 language code
|
| 508 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 509 |
+
Requires libraqm.
|
| 510 |
+
|
| 511 |
+
.. versionadded:: 6.0.0
|
| 512 |
+
|
| 513 |
+
:param stroke_width: The width of the text stroke.
|
| 514 |
+
|
| 515 |
+
.. versionadded:: 6.2.0
|
| 516 |
+
|
| 517 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 518 |
+
the anchor to the text. The default alignment is top left,
|
| 519 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 520 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 521 |
+
|
| 522 |
+
.. versionadded:: 8.0.0
|
| 523 |
+
|
| 524 |
+
:param ink: Foreground ink for rendering in RGBA mode.
|
| 525 |
+
|
| 526 |
+
.. versionadded:: 8.0.0
|
| 527 |
+
|
| 528 |
+
:param start: Tuple of horizontal and vertical offset, as text may render
|
| 529 |
+
differently when starting at fractional coordinates.
|
| 530 |
+
|
| 531 |
+
.. versionadded:: 9.4.0
|
| 532 |
+
|
| 533 |
+
:return: An internal PIL storage memory instance as defined by the
|
| 534 |
+
:py:mod:`PIL.Image.core` interface module.
|
| 535 |
+
"""
|
| 536 |
+
return self.getmask2(
|
| 537 |
+
text,
|
| 538 |
+
mode,
|
| 539 |
+
direction=direction,
|
| 540 |
+
features=features,
|
| 541 |
+
language=language,
|
| 542 |
+
stroke_width=stroke_width,
|
| 543 |
+
anchor=anchor,
|
| 544 |
+
ink=ink,
|
| 545 |
+
start=start,
|
| 546 |
+
)[0]
|
| 547 |
+
|
| 548 |
+
def getmask2(
|
| 549 |
+
self,
|
| 550 |
+
text: str | bytes,
|
| 551 |
+
mode: str = "",
|
| 552 |
+
direction: str | None = None,
|
| 553 |
+
features: list[str] | None = None,
|
| 554 |
+
language: str | None = None,
|
| 555 |
+
stroke_width: float = 0,
|
| 556 |
+
anchor: str | None = None,
|
| 557 |
+
ink: int = 0,
|
| 558 |
+
start: tuple[float, float] | None = None,
|
| 559 |
+
*args: Any,
|
| 560 |
+
**kwargs: Any,
|
| 561 |
+
) -> tuple[Image.core.ImagingCore, tuple[int, int]]:
|
| 562 |
+
"""
|
| 563 |
+
Create a bitmap for the text.
|
| 564 |
+
|
| 565 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 566 |
+
maximum value of 255. If the font has embedded color data, the bitmap
|
| 567 |
+
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
| 568 |
+
|
| 569 |
+
:param text: Text to render.
|
| 570 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 571 |
+
driver prefers; if empty, the renderer may return either
|
| 572 |
+
mode. Note that the mode is always a string, to simplify
|
| 573 |
+
C-level implementations.
|
| 574 |
+
|
| 575 |
+
.. versionadded:: 1.1.5
|
| 576 |
+
|
| 577 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 578 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 579 |
+
Requires libraqm.
|
| 580 |
+
|
| 581 |
+
.. versionadded:: 4.2.0
|
| 582 |
+
|
| 583 |
+
:param features: A list of OpenType font features to be used during text
|
| 584 |
+
layout. This is usually used to turn on optional
|
| 585 |
+
font features that are not enabled by default,
|
| 586 |
+
for example 'dlig' or 'ss01', but can be also
|
| 587 |
+
used to turn off default font features for
|
| 588 |
+
example '-liga' to disable ligatures or '-kern'
|
| 589 |
+
to disable kerning. To get all supported
|
| 590 |
+
features, see
|
| 591 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 592 |
+
Requires libraqm.
|
| 593 |
+
|
| 594 |
+
.. versionadded:: 4.2.0
|
| 595 |
+
|
| 596 |
+
:param language: Language of the text. Different languages may use
|
| 597 |
+
different glyph shapes or ligatures. This parameter tells
|
| 598 |
+
the font which language the text is in, and to apply the
|
| 599 |
+
correct substitutions as appropriate, if available.
|
| 600 |
+
It should be a `BCP 47 language code
|
| 601 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 602 |
+
Requires libraqm.
|
| 603 |
+
|
| 604 |
+
.. versionadded:: 6.0.0
|
| 605 |
+
|
| 606 |
+
:param stroke_width: The width of the text stroke.
|
| 607 |
+
|
| 608 |
+
.. versionadded:: 6.2.0
|
| 609 |
+
|
| 610 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 611 |
+
the anchor to the text. The default alignment is top left,
|
| 612 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 613 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 614 |
+
|
| 615 |
+
.. versionadded:: 8.0.0
|
| 616 |
+
|
| 617 |
+
:param ink: Foreground ink for rendering in RGBA mode.
|
| 618 |
+
|
| 619 |
+
.. versionadded:: 8.0.0
|
| 620 |
+
|
| 621 |
+
:param start: Tuple of horizontal and vertical offset, as text may render
|
| 622 |
+
differently when starting at fractional coordinates.
|
| 623 |
+
|
| 624 |
+
.. versionadded:: 9.4.0
|
| 625 |
+
|
| 626 |
+
:return: A tuple of an internal PIL storage memory instance as defined by the
|
| 627 |
+
:py:mod:`PIL.Image.core` interface module, and the text offset, the
|
| 628 |
+
gap between the starting coordinate and the first marking
|
| 629 |
+
"""
|
| 630 |
+
_string_length_check(text)
|
| 631 |
+
if start is None:
|
| 632 |
+
start = (0, 0)
|
| 633 |
+
|
| 634 |
+
def fill(width: int, height: int) -> Image.core.ImagingCore:
|
| 635 |
+
size = (width, height)
|
| 636 |
+
Image._decompression_bomb_check(size)
|
| 637 |
+
return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
|
| 638 |
+
|
| 639 |
+
return self.font.render(
|
| 640 |
+
text,
|
| 641 |
+
fill,
|
| 642 |
+
mode,
|
| 643 |
+
direction,
|
| 644 |
+
features,
|
| 645 |
+
language,
|
| 646 |
+
stroke_width,
|
| 647 |
+
anchor,
|
| 648 |
+
ink,
|
| 649 |
+
start[0],
|
| 650 |
+
start[1],
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
def font_variant(
|
| 654 |
+
self,
|
| 655 |
+
font: StrOrBytesPath | BinaryIO | None = None,
|
| 656 |
+
size: float | None = None,
|
| 657 |
+
index: int | None = None,
|
| 658 |
+
encoding: str | None = None,
|
| 659 |
+
layout_engine: Layout | None = None,
|
| 660 |
+
) -> FreeTypeFont:
|
| 661 |
+
"""
|
| 662 |
+
Create a copy of this FreeTypeFont object,
|
| 663 |
+
using any specified arguments to override the settings.
|
| 664 |
+
|
| 665 |
+
Parameters are identical to the parameters used to initialize this
|
| 666 |
+
object.
|
| 667 |
+
|
| 668 |
+
:return: A FreeTypeFont object.
|
| 669 |
+
"""
|
| 670 |
+
if font is None:
|
| 671 |
+
try:
|
| 672 |
+
font = BytesIO(self.font_bytes)
|
| 673 |
+
except AttributeError:
|
| 674 |
+
font = self.path
|
| 675 |
+
return FreeTypeFont(
|
| 676 |
+
font=font,
|
| 677 |
+
size=self.size if size is None else size,
|
| 678 |
+
index=self.index if index is None else index,
|
| 679 |
+
encoding=self.encoding if encoding is None else encoding,
|
| 680 |
+
layout_engine=layout_engine or self.layout_engine,
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
def get_variation_names(self) -> list[bytes]:
|
| 684 |
+
"""
|
| 685 |
+
:returns: A list of the named styles in a variation font.
|
| 686 |
+
:exception OSError: If the font is not a variation font.
|
| 687 |
+
"""
|
| 688 |
+
try:
|
| 689 |
+
names = self.font.getvarnames()
|
| 690 |
+
except AttributeError as e:
|
| 691 |
+
msg = "FreeType 2.9.1 or greater is required"
|
| 692 |
+
raise NotImplementedError(msg) from e
|
| 693 |
+
return [name.replace(b"\x00", b"") for name in names]
|
| 694 |
+
|
| 695 |
+
def set_variation_by_name(self, name: str | bytes) -> None:
|
| 696 |
+
"""
|
| 697 |
+
:param name: The name of the style.
|
| 698 |
+
:exception OSError: If the font is not a variation font.
|
| 699 |
+
"""
|
| 700 |
+
names = self.get_variation_names()
|
| 701 |
+
if not isinstance(name, bytes):
|
| 702 |
+
name = name.encode()
|
| 703 |
+
index = names.index(name) + 1
|
| 704 |
+
|
| 705 |
+
if index == getattr(self, "_last_variation_index", None):
|
| 706 |
+
# When the same name is set twice in a row,
|
| 707 |
+
# there is an 'unknown freetype error'
|
| 708 |
+
# https://savannah.nongnu.org/bugs/?56186
|
| 709 |
+
return
|
| 710 |
+
self._last_variation_index = index
|
| 711 |
+
|
| 712 |
+
self.font.setvarname(index)
|
| 713 |
+
|
| 714 |
+
def get_variation_axes(self) -> list[Axis]:
|
| 715 |
+
"""
|
| 716 |
+
:returns: A list of the axes in a variation font.
|
| 717 |
+
:exception OSError: If the font is not a variation font.
|
| 718 |
+
"""
|
| 719 |
+
try:
|
| 720 |
+
axes = self.font.getvaraxes()
|
| 721 |
+
except AttributeError as e:
|
| 722 |
+
msg = "FreeType 2.9.1 or greater is required"
|
| 723 |
+
raise NotImplementedError(msg) from e
|
| 724 |
+
for axis in axes:
|
| 725 |
+
if axis["name"]:
|
| 726 |
+
axis["name"] = axis["name"].replace(b"\x00", b"")
|
| 727 |
+
return axes
|
| 728 |
+
|
| 729 |
+
def set_variation_by_axes(self, axes: list[float]) -> None:
|
| 730 |
+
"""
|
| 731 |
+
:param axes: A list of values for each axis.
|
| 732 |
+
:exception OSError: If the font is not a variation font.
|
| 733 |
+
"""
|
| 734 |
+
try:
|
| 735 |
+
self.font.setvaraxes(axes)
|
| 736 |
+
except AttributeError as e:
|
| 737 |
+
msg = "FreeType 2.9.1 or greater is required"
|
| 738 |
+
raise NotImplementedError(msg) from e
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
class TransposedFont:
|
| 742 |
+
"""Wrapper for writing rotated or mirrored text"""
|
| 743 |
+
|
| 744 |
+
def __init__(
|
| 745 |
+
self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None
|
| 746 |
+
):
|
| 747 |
+
"""
|
| 748 |
+
Wrapper that creates a transposed font from any existing font
|
| 749 |
+
object.
|
| 750 |
+
|
| 751 |
+
:param font: A font object.
|
| 752 |
+
:param orientation: An optional orientation. If given, this should
|
| 753 |
+
be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM,
|
| 754 |
+
Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or
|
| 755 |
+
Image.Transpose.ROTATE_270.
|
| 756 |
+
"""
|
| 757 |
+
self.font = font
|
| 758 |
+
self.orientation = orientation # any 'transpose' argument, or None
|
| 759 |
+
|
| 760 |
+
def getmask(
|
| 761 |
+
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
|
| 762 |
+
) -> Image.core.ImagingCore:
|
| 763 |
+
im = self.font.getmask(text, mode, *args, **kwargs)
|
| 764 |
+
if self.orientation is not None:
|
| 765 |
+
return im.transpose(self.orientation)
|
| 766 |
+
return im
|
| 767 |
+
|
| 768 |
+
def getbbox(
|
| 769 |
+
self, text: str | bytes, *args: Any, **kwargs: Any
|
| 770 |
+
) -> tuple[int, int, float, float]:
|
| 771 |
+
# TransposedFont doesn't support getmask2, move top-left point to (0, 0)
|
| 772 |
+
# this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
|
| 773 |
+
left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
|
| 774 |
+
width = right - left
|
| 775 |
+
height = bottom - top
|
| 776 |
+
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
|
| 777 |
+
return 0, 0, height, width
|
| 778 |
+
return 0, 0, width, height
|
| 779 |
+
|
| 780 |
+
def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
|
| 781 |
+
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
|
| 782 |
+
msg = "text length is undefined for text rotated by 90 or 270 degrees"
|
| 783 |
+
raise ValueError(msg)
|
| 784 |
+
return self.font.getlength(text, *args, **kwargs)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def load(filename: str) -> ImageFont:
|
| 788 |
+
"""
|
| 789 |
+
Load a font file. This function loads a font object from the given
|
| 790 |
+
bitmap font file, and returns the corresponding font object. For loading TrueType
|
| 791 |
+
or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
|
| 792 |
+
|
| 793 |
+
:param filename: Name of font file.
|
| 794 |
+
:return: A font object.
|
| 795 |
+
:exception OSError: If the file could not be read.
|
| 796 |
+
"""
|
| 797 |
+
f = ImageFont()
|
| 798 |
+
f._load_pilfont(filename)
|
| 799 |
+
return f
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
def truetype(
|
| 803 |
+
font: StrOrBytesPath | BinaryIO,
|
| 804 |
+
size: float = 10,
|
| 805 |
+
index: int = 0,
|
| 806 |
+
encoding: str = "",
|
| 807 |
+
layout_engine: Layout | None = None,
|
| 808 |
+
) -> FreeTypeFont:
|
| 809 |
+
"""
|
| 810 |
+
Load a TrueType or OpenType font from a file or file-like object,
|
| 811 |
+
and create a font object. This function loads a font object from the given
|
| 812 |
+
file or file-like object, and creates a font object for a font of the given
|
| 813 |
+
size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
|
| 814 |
+
and :py:func:`~PIL.ImageFont.load_path`.
|
| 815 |
+
|
| 816 |
+
Pillow uses FreeType to open font files. On Windows, be aware that FreeType
|
| 817 |
+
will keep the file open as long as the FreeTypeFont object exists. Windows
|
| 818 |
+
limits the number of files that can be open in C at once to 512, so if many
|
| 819 |
+
fonts are opened simultaneously and that limit is approached, an
|
| 820 |
+
``OSError`` may be thrown, reporting that FreeType "cannot open resource".
|
| 821 |
+
A workaround would be to copy the file(s) into memory, and open that instead.
|
| 822 |
+
|
| 823 |
+
This function requires the _imagingft service.
|
| 824 |
+
|
| 825 |
+
:param font: A filename or file-like object containing a TrueType font.
|
| 826 |
+
If the file is not found in this filename, the loader may also
|
| 827 |
+
search in other directories, such as:
|
| 828 |
+
|
| 829 |
+
* The :file:`fonts/` directory on Windows,
|
| 830 |
+
* :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
|
| 831 |
+
and :file:`~/Library/Fonts/` on macOS.
|
| 832 |
+
* :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
|
| 833 |
+
and :file:`/usr/share/fonts` on Linux; or those specified by
|
| 834 |
+
the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
|
| 835 |
+
for user-installed and system-wide fonts, respectively.
|
| 836 |
+
|
| 837 |
+
:param size: The requested size, in pixels.
|
| 838 |
+
:param index: Which font face to load (default is first available face).
|
| 839 |
+
:param encoding: Which font encoding to use (default is Unicode). Possible
|
| 840 |
+
encodings include (see the FreeType documentation for more
|
| 841 |
+
information):
|
| 842 |
+
|
| 843 |
+
* "unic" (Unicode)
|
| 844 |
+
* "symb" (Microsoft Symbol)
|
| 845 |
+
* "ADOB" (Adobe Standard)
|
| 846 |
+
* "ADBE" (Adobe Expert)
|
| 847 |
+
* "ADBC" (Adobe Custom)
|
| 848 |
+
* "armn" (Apple Roman)
|
| 849 |
+
* "sjis" (Shift JIS)
|
| 850 |
+
* "gb " (PRC)
|
| 851 |
+
* "big5"
|
| 852 |
+
* "wans" (Extended Wansung)
|
| 853 |
+
* "joha" (Johab)
|
| 854 |
+
* "lat1" (Latin-1)
|
| 855 |
+
|
| 856 |
+
This specifies the character set to use. It does not alter the
|
| 857 |
+
encoding of any text provided in subsequent operations.
|
| 858 |
+
:param layout_engine: Which layout engine to use, if available:
|
| 859 |
+
:attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
|
| 860 |
+
If it is available, Raqm layout will be used by default.
|
| 861 |
+
Otherwise, basic layout will be used.
|
| 862 |
+
|
| 863 |
+
Raqm layout is recommended for all non-English text. If Raqm layout
|
| 864 |
+
is not required, basic layout will have better performance.
|
| 865 |
+
|
| 866 |
+
You can check support for Raqm layout using
|
| 867 |
+
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.
|
| 868 |
+
|
| 869 |
+
.. versionadded:: 4.2.0
|
| 870 |
+
:return: A font object.
|
| 871 |
+
:exception OSError: If the file could not be read.
|
| 872 |
+
:exception ValueError: If the font size is not greater than zero.
|
| 873 |
+
"""
|
| 874 |
+
|
| 875 |
+
def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont:
|
| 876 |
+
return FreeTypeFont(font, size, index, encoding, layout_engine)
|
| 877 |
+
|
| 878 |
+
try:
|
| 879 |
+
return freetype(font)
|
| 880 |
+
except OSError:
|
| 881 |
+
if not is_path(font):
|
| 882 |
+
raise
|
| 883 |
+
ttf_filename = os.path.basename(font)
|
| 884 |
+
|
| 885 |
+
dirs = []
|
| 886 |
+
if sys.platform == "win32":
|
| 887 |
+
# check the windows font repository
|
| 888 |
+
# NOTE: must use uppercase WINDIR, to work around bugs in
|
| 889 |
+
# 1.5.2's os.environ.get()
|
| 890 |
+
windir = os.environ.get("WINDIR")
|
| 891 |
+
if windir:
|
| 892 |
+
dirs.append(os.path.join(windir, "fonts"))
|
| 893 |
+
elif sys.platform in ("linux", "linux2"):
|
| 894 |
+
data_home = os.environ.get("XDG_DATA_HOME")
|
| 895 |
+
if not data_home:
|
| 896 |
+
# The freedesktop spec defines the following default directory for
|
| 897 |
+
# when XDG_DATA_HOME is unset or empty. This user-level directory
|
| 898 |
+
# takes precedence over system-level directories.
|
| 899 |
+
data_home = os.path.expanduser("~/.local/share")
|
| 900 |
+
xdg_dirs = [data_home]
|
| 901 |
+
|
| 902 |
+
data_dirs = os.environ.get("XDG_DATA_DIRS")
|
| 903 |
+
if not data_dirs:
|
| 904 |
+
# Similarly, defaults are defined for the system-level directories
|
| 905 |
+
data_dirs = "/usr/local/share:/usr/share"
|
| 906 |
+
xdg_dirs += data_dirs.split(":")
|
| 907 |
+
|
| 908 |
+
dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
|
| 909 |
+
elif sys.platform == "darwin":
|
| 910 |
+
dirs += [
|
| 911 |
+
"/Library/Fonts",
|
| 912 |
+
"/System/Library/Fonts",
|
| 913 |
+
os.path.expanduser("~/Library/Fonts"),
|
| 914 |
+
]
|
| 915 |
+
|
| 916 |
+
ext = os.path.splitext(ttf_filename)[1]
|
| 917 |
+
first_font_with_a_different_extension = None
|
| 918 |
+
for directory in dirs:
|
| 919 |
+
for walkroot, walkdir, walkfilenames in os.walk(directory):
|
| 920 |
+
for walkfilename in walkfilenames:
|
| 921 |
+
if ext and walkfilename == ttf_filename:
|
| 922 |
+
return freetype(os.path.join(walkroot, walkfilename))
|
| 923 |
+
elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
|
| 924 |
+
fontpath = os.path.join(walkroot, walkfilename)
|
| 925 |
+
if os.path.splitext(fontpath)[1] == ".ttf":
|
| 926 |
+
return freetype(fontpath)
|
| 927 |
+
if not ext and first_font_with_a_different_extension is None:
|
| 928 |
+
first_font_with_a_different_extension = fontpath
|
| 929 |
+
if first_font_with_a_different_extension:
|
| 930 |
+
return freetype(first_font_with_a_different_extension)
|
| 931 |
+
raise
|
| 932 |
+
|
| 933 |
+
|
| 934 |
+
def load_path(filename: str | bytes) -> ImageFont:
|
| 935 |
+
"""
|
| 936 |
+
Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
|
| 937 |
+
bitmap font along the Python path.
|
| 938 |
+
|
| 939 |
+
:param filename: Name of font file.
|
| 940 |
+
:return: A font object.
|
| 941 |
+
:exception OSError: If the file could not be read.
|
| 942 |
+
"""
|
| 943 |
+
if not isinstance(filename, str):
|
| 944 |
+
filename = filename.decode("utf-8")
|
| 945 |
+
for directory in sys.path:
|
| 946 |
+
try:
|
| 947 |
+
return load(os.path.join(directory, filename))
|
| 948 |
+
except OSError:
|
| 949 |
+
pass
|
| 950 |
+
msg = f'cannot find font file "{filename}" in sys.path'
|
| 951 |
+
if os.path.exists(filename):
|
| 952 |
+
msg += f', did you mean ImageFont.load("{filename}") instead?'
|
| 953 |
+
|
| 954 |
+
raise OSError(msg)
|
| 955 |
+
|
| 956 |
+
|
| 957 |
+
def load_default_imagefont() -> ImageFont:
|
| 958 |
+
f = ImageFont()
|
| 959 |
+
f._load_pilfont_data(
|
| 960 |
+
# courB08
|
| 961 |
+
BytesIO(
|
| 962 |
+
base64.b64decode(
|
| 963 |
+
b"""
|
| 964 |
+
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 965 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 966 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 967 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 968 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 969 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 970 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 971 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 972 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 973 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 974 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 975 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
|
| 976 |
+
BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
|
| 977 |
+
AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
|
| 978 |
+
AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
|
| 979 |
+
ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
|
| 980 |
+
BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
|
| 981 |
+
//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
|
| 982 |
+
AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
|
| 983 |
+
AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
|
| 984 |
+
ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
|
| 985 |
+
AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
|
| 986 |
+
/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
|
| 987 |
+
AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
|
| 988 |
+
AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
|
| 989 |
+
AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
|
| 990 |
+
BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
|
| 991 |
+
AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
|
| 992 |
+
2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
|
| 993 |
+
AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
|
| 994 |
+
+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
|
| 995 |
+
////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
|
| 996 |
+
BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
|
| 997 |
+
AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
|
| 998 |
+
AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
|
| 999 |
+
AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
|
| 1000 |
+
BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
|
| 1001 |
+
//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
|
| 1002 |
+
AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
|
| 1003 |
+
AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
|
| 1004 |
+
mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
|
| 1005 |
+
AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
|
| 1006 |
+
AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
|
| 1007 |
+
AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
|
| 1008 |
+
Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
|
| 1009 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1010 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1011 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1012 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1013 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1014 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1015 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1016 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1017 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1018 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1019 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1020 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
|
| 1021 |
+
//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
|
| 1022 |
+
AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
|
| 1023 |
+
AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
|
| 1024 |
+
DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
|
| 1025 |
+
AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
|
| 1026 |
+
+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
|
| 1027 |
+
AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
|
| 1028 |
+
///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
|
| 1029 |
+
AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
|
| 1030 |
+
BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
|
| 1031 |
+
Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
|
| 1032 |
+
eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
|
| 1033 |
+
AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
|
| 1034 |
+
+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
|
| 1035 |
+
////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
|
| 1036 |
+
BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
|
| 1037 |
+
AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
|
| 1038 |
+
AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
|
| 1039 |
+
Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
|
| 1040 |
+
Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
|
| 1041 |
+
//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
|
| 1042 |
+
AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
|
| 1043 |
+
AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
|
| 1044 |
+
LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
|
| 1045 |
+
AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
|
| 1046 |
+
AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
|
| 1047 |
+
AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
|
| 1048 |
+
AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
|
| 1049 |
+
AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
|
| 1050 |
+
EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
|
| 1051 |
+
AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
|
| 1052 |
+
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
|
| 1053 |
+
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
|
| 1054 |
+
+QAGAAIAzgAKANUAEw==
|
| 1055 |
+
"""
|
| 1056 |
+
)
|
| 1057 |
+
),
|
| 1058 |
+
Image.open(
|
| 1059 |
+
BytesIO(
|
| 1060 |
+
base64.b64decode(
|
| 1061 |
+
b"""
|
| 1062 |
+
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
|
| 1063 |
+
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
|
| 1064 |
+
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
|
| 1065 |
+
LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
|
| 1066 |
+
IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
|
| 1067 |
+
Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
|
| 1068 |
+
NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
|
| 1069 |
+
in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
|
| 1070 |
+
SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
|
| 1071 |
+
AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
|
| 1072 |
+
y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
|
| 1073 |
+
ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
|
| 1074 |
+
lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
|
| 1075 |
+
/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
|
| 1076 |
+
AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
|
| 1077 |
+
c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
|
| 1078 |
+
/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
|
| 1079 |
+
pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
|
| 1080 |
+
oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
|
| 1081 |
+
evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
|
| 1082 |
+
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
|
| 1083 |
+
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
|
| 1084 |
+
w7IkEbzhVQAAAABJRU5ErkJggg==
|
| 1085 |
+
"""
|
| 1086 |
+
)
|
| 1087 |
+
)
|
| 1088 |
+
),
|
| 1089 |
+
)
|
| 1090 |
+
return f
|
| 1091 |
+
|
| 1092 |
+
|
| 1093 |
+
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
|
| 1094 |
+
"""If FreeType support is available, load a version of Aileron Regular,
|
| 1095 |
+
https://dotcolon.net/font/aileron, with a more limited character set.
|
| 1096 |
+
|
| 1097 |
+
Otherwise, load a "better than nothing" font.
|
| 1098 |
+
|
| 1099 |
+
.. versionadded:: 1.1.4
|
| 1100 |
+
|
| 1101 |
+
:param size: The font size of Aileron Regular.
|
| 1102 |
+
|
| 1103 |
+
.. versionadded:: 10.1.0
|
| 1104 |
+
|
| 1105 |
+
:return: A font object.
|
| 1106 |
+
"""
|
| 1107 |
+
if isinstance(core, ModuleType) or size is not None:
|
| 1108 |
+
return truetype(
|
| 1109 |
+
BytesIO(
|
| 1110 |
+
base64.b64decode(
|
| 1111 |
+
b"""
|
| 1112 |
+
AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
|
| 1113 |
+
AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
|
| 1114 |
+
MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
|
| 1115 |
+
tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk
|
| 1116 |
+
OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/
|
| 1117 |
+
2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ
|
| 1118 |
+
AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI
|
| 1119 |
+
BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA
|
| 1120 |
+
AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ
|
| 1121 |
+
AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk
|
| 1122 |
+
QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB
|
| 1123 |
+
kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC
|
| 1124 |
+
ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA
|
| 1125 |
+
EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg
|
| 1126 |
+
JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y
|
| 1127 |
+
AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q
|
| 1128 |
+
AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq
|
| 1129 |
+
QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB//
|
| 1130 |
+
//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1131 |
+
AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT
|
| 1132 |
+
FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT
|
| 1133 |
+
U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA
|
| 1134 |
+
AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9
|
| 1135 |
+
ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO
|
| 1136 |
+
AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ
|
| 1137 |
+
gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG
|
| 1138 |
+
oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz
|
| 1139 |
+
qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA
|
| 1140 |
+
DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA
|
| 1141 |
+
P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA
|
| 1142 |
+
LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc
|
| 1143 |
+
jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb
|
| 1144 |
+
2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ
|
| 1145 |
+
icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ
|
| 1146 |
+
ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA
|
| 1147 |
+
dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c
|
| 1148 |
+
OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/
|
| 1149 |
+
/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg
|
| 1150 |
+
ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp
|
| 1151 |
+
COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA
|
| 1152 |
+
EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q
|
| 1153 |
+
EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx
|
| 1154 |
+
ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj
|
| 1155 |
+
OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA
|
| 1156 |
+
AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H
|
| 1157 |
+
gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg
|
| 1158 |
+
KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM
|
| 1159 |
+
iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA
|
| 1160 |
+
AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA
|
| 1161 |
+
YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg
|
| 1162 |
+
pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4
|
| 1163 |
+
rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv
|
| 1164 |
+
d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA
|
| 1165 |
+
sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA
|
| 1166 |
+
IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY
|
| 1167 |
+
AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2
|
| 1168 |
+
Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS
|
| 1169 |
+
0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC
|
| 1170 |
+
MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp
|
| 1171 |
+
7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS
|
| 1172 |
+
MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA
|
| 1173 |
+
AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS
|
| 1174 |
+
UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8
|
| 1175 |
+
AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA
|
| 1176 |
+
ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J
|
| 1177 |
+
CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj
|
| 1178 |
+
Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY
|
| 1179 |
+
Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74
|
| 1180 |
+
EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA
|
| 1181 |
+
AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA
|
| 1182 |
+
EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt
|
| 1183 |
+
hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA
|
| 1184 |
+
ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A
|
| 1185 |
+
sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi
|
| 1186 |
+
sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI
|
| 1187 |
+
vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh
|
| 1188 |
+
FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH
|
| 1189 |
+
wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq
|
| 1190 |
+
N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA
|
| 1191 |
+
AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2
|
| 1192 |
+
NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA
|
| 1193 |
+
wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j
|
| 1194 |
+
VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7
|
| 1195 |
+
MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR
|
| 1196 |
+
MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN
|
| 1197 |
+
jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg
|
| 1198 |
+
EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU
|
| 1199 |
+
V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx
|
| 1200 |
+
UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA
|
| 1201 |
+
CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv
|
| 1202 |
+
6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM
|
| 1203 |
+
uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9
|
| 1204 |
+
Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE
|
| 1205 |
+
SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA
|
| 1206 |
+
IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA
|
| 1207 |
+
hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi
|
| 1208 |
+
kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY
|
| 1209 |
+
re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A
|
| 1210 |
+
EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA
|
| 1211 |
+
BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+
|
| 1212 |
+
HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE
|
| 1213 |
+
wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg
|
| 1214 |
+
ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI
|
| 1215 |
+
XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf
|
| 1216 |
+
J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH
|
| 1217 |
+
QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe//
|
| 1218 |
+
IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB
|
| 1219 |
+
oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm
|
| 1220 |
+
IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA
|
| 1221 |
+
B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI
|
| 1222 |
+
WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU
|
| 1223 |
+
zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi
|
| 1224 |
+
AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd
|
| 1225 |
+
NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED
|
| 1226 |
+
RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs
|
| 1227 |
+
6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm
|
| 1228 |
+
NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN
|
| 1229 |
+
RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC
|
| 1230 |
+
EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM
|
| 1231 |
+
iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn
|
| 1232 |
+
JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI
|
| 1233 |
+
jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg
|
| 1234 |
+
YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI
|
| 1235 |
+
sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A
|
| 1236 |
+
AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV
|
| 1237 |
+
igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ
|
| 1238 |
+
cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd
|
| 1239 |
+
4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe
|
| 1240 |
+
B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL
|
| 1241 |
+
gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE
|
| 1242 |
+
BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM
|
| 1243 |
+
BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy
|
| 1244 |
+
Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA
|
| 1245 |
+
AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW
|
| 1246 |
+
Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq
|
| 1247 |
+
8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7
|
| 1248 |
+
2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA
|
| 1249 |
+
QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR
|
| 1250 |
+
QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk
|
| 1251 |
+
WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6
|
| 1252 |
+
yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF
|
| 1253 |
+
AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh
|
| 1254 |
+
YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4
|
| 1255 |
+
bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX
|
| 1256 |
+
IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX
|
| 1257 |
+
HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw
|
| 1258 |
+
cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY
|
| 1259 |
+
yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1
|
| 1260 |
+
MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA
|
| 1261 |
+
AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw
|
| 1262 |
+
UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po
|
| 1263 |
+
AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O
|
| 1264 |
+
XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A
|
| 1265 |
+
AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC
|
| 1266 |
+
Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA
|
| 1267 |
+
AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy
|
| 1268 |
+
AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl
|
| 1269 |
+
CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj
|
| 1270 |
+
k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI
|
| 1271 |
+
mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa
|
| 1272 |
+
EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA
|
| 1273 |
+
QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA
|
| 1274 |
+
AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA
|
| 1275 |
+
BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A
|
| 1276 |
+
AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA
|
| 1277 |
+
gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm
|
| 1278 |
+
lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV
|
| 1279 |
+
ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy
|
| 1280 |
+
AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA
|
| 1281 |
+
HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg
|
| 1282 |
+
B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk
|
| 1283 |
+
AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41
|
| 1284 |
+
ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA
|
| 1285 |
+
HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3
|
| 1286 |
+
JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB
|
| 1287 |
+
odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs
|
| 1288 |
+
AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA
|
| 1289 |
+
AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB
|
| 1290 |
+
QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA
|
| 1291 |
+
xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A
|
| 1292 |
+
TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A
|
| 1293 |
+
LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA
|
| 1294 |
+
AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ
|
| 1295 |
+
ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG
|
| 1296 |
+
AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE
|
| 1297 |
+
AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE
|
| 1298 |
+
kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ
|
| 1299 |
+
PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA
|
| 1300 |
+
AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA
|
| 1301 |
+
AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1302 |
+
AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1303 |
+
AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA
|
| 1304 |
+
ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA
|
| 1305 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA
|
| 1306 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA
|
| 1307 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA
|
| 1308 |
+
AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1309 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA
|
| 1310 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD
|
| 1311 |
+
/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA
|
| 1312 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1313 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA
|
| 1314 |
+
AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1315 |
+
AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA
|
| 1316 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA
|
| 1317 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA
|
| 1318 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1319 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1320 |
+
AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1321 |
+
AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1322 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA
|
| 1323 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA
|
| 1324 |
+
BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA
|
| 1325 |
+
AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA
|
| 1326 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1327 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ
|
| 1328 |
+
ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
|
| 1329 |
+
gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
|
| 1330 |
+
YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
|
| 1331 |
+
AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
|
| 1332 |
+
"""
|
| 1333 |
+
)
|
| 1334 |
+
),
|
| 1335 |
+
10 if size is None else size,
|
| 1336 |
+
layout_engine=Layout.BASIC,
|
| 1337 |
+
)
|
| 1338 |
+
return load_default_imagefont()
|