la-zorra-negra commited on
Commit
edc8b84
·
verified ·
1 Parent(s): 7f84d37

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +209 -0
  2. Lib/site-packages/.DS_Store +0 -0
  3. Lib/site-packages/GitPython-3.1.44.dist-info/AUTHORS +59 -0
  4. Lib/site-packages/GitPython-3.1.44.dist-info/INSTALLER +1 -0
  5. Lib/site-packages/GitPython-3.1.44.dist-info/LICENSE +29 -0
  6. Lib/site-packages/GitPython-3.1.44.dist-info/METADATA +295 -0
  7. Lib/site-packages/GitPython-3.1.44.dist-info/RECORD +82 -0
  8. Lib/site-packages/GitPython-3.1.44.dist-info/WHEEL +5 -0
  9. Lib/site-packages/GitPython-3.1.44.dist-info/top_level.txt +1 -0
  10. Lib/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER +1 -0
  11. Lib/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt +28 -0
  12. Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA +92 -0
  13. Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD +14 -0
  14. Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL +5 -0
  15. Lib/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt +1 -0
  16. Lib/site-packages/PIL/BdfFontFile.py +133 -0
  17. Lib/site-packages/PIL/BlpImagePlugin.py +501 -0
  18. Lib/site-packages/PIL/BmpImagePlugin.py +511 -0
  19. Lib/site-packages/PIL/BufrStubImagePlugin.py +76 -0
  20. Lib/site-packages/PIL/ContainerIO.py +173 -0
  21. Lib/site-packages/PIL/CurImagePlugin.py +75 -0
  22. Lib/site-packages/PIL/DcxImagePlugin.py +80 -0
  23. Lib/site-packages/PIL/DdsImagePlugin.py +573 -0
  24. Lib/site-packages/PIL/EpsImagePlugin.py +474 -0
  25. Lib/site-packages/PIL/ExifTags.py +382 -0
  26. Lib/site-packages/PIL/FitsImagePlugin.py +152 -0
  27. Lib/site-packages/PIL/FliImagePlugin.py +175 -0
  28. Lib/site-packages/PIL/FontFile.py +134 -0
  29. Lib/site-packages/PIL/FpxImagePlugin.py +257 -0
  30. Lib/site-packages/PIL/FtexImagePlugin.py +115 -0
  31. Lib/site-packages/PIL/GbrImagePlugin.py +103 -0
  32. Lib/site-packages/PIL/GdImageFile.py +102 -0
  33. Lib/site-packages/PIL/GifImagePlugin.py +1197 -0
  34. Lib/site-packages/PIL/GimpGradientFile.py +149 -0
  35. Lib/site-packages/PIL/GimpPaletteFile.py +58 -0
  36. Lib/site-packages/PIL/GribStubImagePlugin.py +76 -0
  37. Lib/site-packages/PIL/Hdf5StubImagePlugin.py +76 -0
  38. Lib/site-packages/PIL/IcnsImagePlugin.py +412 -0
  39. Lib/site-packages/PIL/IcoImagePlugin.py +381 -0
  40. Lib/site-packages/PIL/ImImagePlugin.py +386 -0
  41. Lib/site-packages/PIL/Image.py +0 -0
  42. Lib/site-packages/PIL/ImageChops.py +311 -0
  43. Lib/site-packages/PIL/ImageCms.py +1125 -0
  44. Lib/site-packages/PIL/ImageColor.py +320 -0
  45. Lib/site-packages/PIL/ImageDraw.py +1218 -0
  46. Lib/site-packages/PIL/ImageDraw2.py +243 -0
  47. Lib/site-packages/PIL/ImageEnhance.py +113 -0
  48. Lib/site-packages/PIL/ImageFile.py +832 -0
  49. Lib/site-packages/PIL/ImageFilter.py +605 -0
  50. 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
+ ![Python package](https://github.com/gitpython-developers/GitPython/workflows/Python%20package/badge.svg)
50
+ [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
51
+ [![Packaging status](https://repology.org/badge/tiny-repos/python:gitpython.svg)](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('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
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>&#34;World&#34;</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()