Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- PIL/AvifImagePlugin.py +293 -0
- PIL/BdfFontFile.py +122 -0
- PIL/BlpImagePlugin.py +498 -0
- PIL/BmpImagePlugin.py +517 -0
- PIL/ContainerIO.py +173 -0
- PIL/CurImagePlugin.py +75 -0
- PIL/DcxImagePlugin.py +84 -0
- PIL/DdsImagePlugin.py +625 -0
- PIL/EpsImagePlugin.py +481 -0
- PIL/FitsImagePlugin.py +152 -0
- PIL/FliImagePlugin.py +184 -0
- PIL/FtexImagePlugin.py +115 -0
- PIL/GbrImagePlugin.py +103 -0
- PIL/GdImageFile.py +102 -0
- PIL/GimpGradientFile.py +153 -0
- PIL/GimpPaletteFile.py +75 -0
- PIL/GribStubImagePlugin.py +76 -0
- PIL/Hdf5StubImagePlugin.py +76 -0
- PIL/IcnsImagePlugin.py +402 -0
- PIL/IcoImagePlugin.py +396 -0
- PIL/ImImagePlugin.py +390 -0
- PIL/Image.py +0 -0
- PIL/ImageColor.py +320 -0
- PIL/ImageDraw.py +1036 -0
- PIL/ImageDraw2.py +243 -0
- PIL/ImageEnhance.py +113 -0
- PIL/ImageFile.py +938 -0
- PIL/ImageFont.py +1320 -0
- PIL/ImageGrab.py +224 -0
- PIL/ImageMode.py +85 -0
- PIL/ImageMorph.py +317 -0
- PIL/ImageOps.py +746 -0
- PIL/ImagePalette.py +287 -0
- PIL/ImagePath.py +20 -0
- PIL/ImageSequence.py +88 -0
- PIL/ImageShow.py +362 -0
- PIL/ImageStat.py +167 -0
- PIL/ImageText.py +320 -0
- PIL/ImageTk.py +266 -0
- PIL/ImageTransform.py +136 -0
- PIL/ImageWin.py +247 -0
- PIL/IptcImagePlugin.py +233 -0
- PIL/Jpeg2KImagePlugin.py +448 -0
- PIL/JpegImagePlugin.py +895 -0
- PIL/JpegPresets.py +242 -0
- PIL/McIdasImagePlugin.py +78 -0
- PIL/MicImagePlugin.py +103 -0
- PIL/MpegImagePlugin.py +84 -0
- PIL/MpoImagePlugin.py +204 -0
- PIL/MspImagePlugin.py +200 -0
PIL/AvifImagePlugin.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
from typing import IO
|
| 6 |
+
|
| 7 |
+
from . import ExifTags, Image, ImageFile
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from . import _avif
|
| 11 |
+
|
| 12 |
+
SUPPORTED = True
|
| 13 |
+
except ImportError:
|
| 14 |
+
SUPPORTED = False
|
| 15 |
+
|
| 16 |
+
# Decoder options as module globals, until there is a way to pass parameters
|
| 17 |
+
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
|
| 18 |
+
DECODE_CODEC_CHOICE = "auto"
|
| 19 |
+
DEFAULT_MAX_THREADS = 0
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_codec_version(codec_name: str) -> str | None:
|
| 23 |
+
versions = _avif.codec_versions()
|
| 24 |
+
for version in versions.split(", "):
|
| 25 |
+
if version.split(" [")[0] == codec_name:
|
| 26 |
+
return version.split(":")[-1].split(" ")[0]
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _accept(prefix: bytes) -> bool | str:
|
| 31 |
+
if prefix[4:8] != b"ftyp":
|
| 32 |
+
return False
|
| 33 |
+
major_brand = prefix[8:12]
|
| 34 |
+
if major_brand in (
|
| 35 |
+
# coding brands
|
| 36 |
+
b"avif",
|
| 37 |
+
b"avis",
|
| 38 |
+
# We accept files with AVIF container brands; we can't yet know if
|
| 39 |
+
# the ftyp box has the correct compatible brands, but if it doesn't
|
| 40 |
+
# then the plugin will raise a SyntaxError which Pillow will catch
|
| 41 |
+
# before moving on to the next plugin that accepts the file.
|
| 42 |
+
#
|
| 43 |
+
# Also, because this file might not actually be an AVIF file, we
|
| 44 |
+
# don't raise an error if AVIF support isn't properly compiled.
|
| 45 |
+
b"mif1",
|
| 46 |
+
b"msf1",
|
| 47 |
+
):
|
| 48 |
+
if not SUPPORTED:
|
| 49 |
+
return (
|
| 50 |
+
"image file could not be identified because AVIF support not installed"
|
| 51 |
+
)
|
| 52 |
+
return True
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _get_default_max_threads() -> int:
|
| 57 |
+
if DEFAULT_MAX_THREADS:
|
| 58 |
+
return DEFAULT_MAX_THREADS
|
| 59 |
+
if hasattr(os, "sched_getaffinity"):
|
| 60 |
+
return len(os.sched_getaffinity(0))
|
| 61 |
+
else:
|
| 62 |
+
return os.cpu_count() or 1
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class AvifImageFile(ImageFile.ImageFile):
|
| 66 |
+
format = "AVIF"
|
| 67 |
+
format_description = "AVIF image"
|
| 68 |
+
__frame = -1
|
| 69 |
+
|
| 70 |
+
def _open(self) -> None:
|
| 71 |
+
if not SUPPORTED:
|
| 72 |
+
msg = "image file could not be opened because AVIF support not installed"
|
| 73 |
+
raise SyntaxError(msg)
|
| 74 |
+
|
| 75 |
+
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
|
| 76 |
+
DECODE_CODEC_CHOICE
|
| 77 |
+
):
|
| 78 |
+
msg = "Invalid opening codec"
|
| 79 |
+
raise ValueError(msg)
|
| 80 |
+
|
| 81 |
+
assert self.fp is not None
|
| 82 |
+
self._decoder = _avif.AvifDecoder(
|
| 83 |
+
self.fp.read(),
|
| 84 |
+
DECODE_CODEC_CHOICE,
|
| 85 |
+
_get_default_max_threads(),
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Get info from decoder
|
| 89 |
+
self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
|
| 90 |
+
self._decoder.get_info()
|
| 91 |
+
)
|
| 92 |
+
self.is_animated = self.n_frames > 1
|
| 93 |
+
|
| 94 |
+
if icc:
|
| 95 |
+
self.info["icc_profile"] = icc
|
| 96 |
+
if xmp:
|
| 97 |
+
self.info["xmp"] = xmp
|
| 98 |
+
|
| 99 |
+
if exif_orientation != 1 or exif:
|
| 100 |
+
exif_data = Image.Exif()
|
| 101 |
+
if exif:
|
| 102 |
+
exif_data.load(exif)
|
| 103 |
+
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
|
| 104 |
+
else:
|
| 105 |
+
original_orientation = 1
|
| 106 |
+
if exif_orientation != original_orientation:
|
| 107 |
+
exif_data[ExifTags.Base.Orientation] = exif_orientation
|
| 108 |
+
exif = exif_data.tobytes()
|
| 109 |
+
if exif:
|
| 110 |
+
self.info["exif"] = exif
|
| 111 |
+
self.seek(0)
|
| 112 |
+
|
| 113 |
+
def seek(self, frame: int) -> None:
|
| 114 |
+
if not self._seek_check(frame):
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
# Set tile
|
| 118 |
+
self.__frame = frame
|
| 119 |
+
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
|
| 120 |
+
|
| 121 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 122 |
+
if self.tile:
|
| 123 |
+
# We need to load the image data for this frame
|
| 124 |
+
data, timescale, pts_in_timescales, duration_in_timescales = (
|
| 125 |
+
self._decoder.get_frame(self.__frame)
|
| 126 |
+
)
|
| 127 |
+
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
|
| 128 |
+
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
|
| 129 |
+
|
| 130 |
+
if self.fp and self._exclusive_fp:
|
| 131 |
+
self.fp.close()
|
| 132 |
+
self.fp = BytesIO(data)
|
| 133 |
+
|
| 134 |
+
return super().load()
|
| 135 |
+
|
| 136 |
+
def load_seek(self, pos: int) -> None:
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
+
def tell(self) -> int:
|
| 140 |
+
return self.__frame
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 144 |
+
_save(im, fp, filename, save_all=True)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _save(
|
| 148 |
+
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
|
| 149 |
+
) -> None:
|
| 150 |
+
info = im.encoderinfo.copy()
|
| 151 |
+
if save_all:
|
| 152 |
+
append_images = list(info.get("append_images", []))
|
| 153 |
+
else:
|
| 154 |
+
append_images = []
|
| 155 |
+
|
| 156 |
+
total = 0
|
| 157 |
+
for ims in [im] + append_images:
|
| 158 |
+
total += getattr(ims, "n_frames", 1)
|
| 159 |
+
|
| 160 |
+
quality = info.get("quality", 75)
|
| 161 |
+
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
| 162 |
+
msg = "Invalid quality setting"
|
| 163 |
+
raise ValueError(msg)
|
| 164 |
+
|
| 165 |
+
duration = info.get("duration", 0)
|
| 166 |
+
subsampling = info.get("subsampling", "4:2:0")
|
| 167 |
+
speed = info.get("speed", 6)
|
| 168 |
+
max_threads = info.get("max_threads", _get_default_max_threads())
|
| 169 |
+
codec = info.get("codec", "auto")
|
| 170 |
+
if codec != "auto" and not _avif.encoder_codec_available(codec):
|
| 171 |
+
msg = "Invalid saving codec"
|
| 172 |
+
raise ValueError(msg)
|
| 173 |
+
range_ = info.get("range", "full")
|
| 174 |
+
tile_rows_log2 = info.get("tile_rows", 0)
|
| 175 |
+
tile_cols_log2 = info.get("tile_cols", 0)
|
| 176 |
+
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
|
| 177 |
+
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
|
| 178 |
+
|
| 179 |
+
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
|
| 180 |
+
exif_orientation = 1
|
| 181 |
+
if exif := info.get("exif"):
|
| 182 |
+
if isinstance(exif, Image.Exif):
|
| 183 |
+
exif_data = exif
|
| 184 |
+
else:
|
| 185 |
+
exif_data = Image.Exif()
|
| 186 |
+
exif_data.load(exif)
|
| 187 |
+
if ExifTags.Base.Orientation in exif_data:
|
| 188 |
+
exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
|
| 189 |
+
exif = exif_data.tobytes() if exif_data else b""
|
| 190 |
+
elif isinstance(exif, Image.Exif):
|
| 191 |
+
exif = exif_data.tobytes()
|
| 192 |
+
|
| 193 |
+
xmp = info.get("xmp")
|
| 194 |
+
|
| 195 |
+
if isinstance(xmp, str):
|
| 196 |
+
xmp = xmp.encode("utf-8")
|
| 197 |
+
|
| 198 |
+
advanced = info.get("advanced")
|
| 199 |
+
if advanced is not None:
|
| 200 |
+
if isinstance(advanced, dict):
|
| 201 |
+
advanced = advanced.items()
|
| 202 |
+
try:
|
| 203 |
+
advanced = tuple(advanced)
|
| 204 |
+
except TypeError:
|
| 205 |
+
invalid = True
|
| 206 |
+
else:
|
| 207 |
+
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
|
| 208 |
+
if invalid:
|
| 209 |
+
msg = (
|
| 210 |
+
"advanced codec options must be a dict of key-value string "
|
| 211 |
+
"pairs or a series of key-value two-tuples"
|
| 212 |
+
)
|
| 213 |
+
raise ValueError(msg)
|
| 214 |
+
|
| 215 |
+
# Setup the AVIF encoder
|
| 216 |
+
enc = _avif.AvifEncoder(
|
| 217 |
+
im.size,
|
| 218 |
+
subsampling,
|
| 219 |
+
quality,
|
| 220 |
+
speed,
|
| 221 |
+
max_threads,
|
| 222 |
+
codec,
|
| 223 |
+
range_,
|
| 224 |
+
tile_rows_log2,
|
| 225 |
+
tile_cols_log2,
|
| 226 |
+
alpha_premultiplied,
|
| 227 |
+
autotiling,
|
| 228 |
+
icc_profile or b"",
|
| 229 |
+
exif or b"",
|
| 230 |
+
exif_orientation,
|
| 231 |
+
xmp or b"",
|
| 232 |
+
advanced,
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Add each frame
|
| 236 |
+
frame_idx = 0
|
| 237 |
+
frame_duration = 0
|
| 238 |
+
cur_idx = im.tell()
|
| 239 |
+
is_single_frame = total == 1
|
| 240 |
+
try:
|
| 241 |
+
for ims in [im] + append_images:
|
| 242 |
+
# Get number of frames in this image
|
| 243 |
+
nfr = getattr(ims, "n_frames", 1)
|
| 244 |
+
|
| 245 |
+
for idx in range(nfr):
|
| 246 |
+
ims.seek(idx)
|
| 247 |
+
|
| 248 |
+
# Make sure image mode is supported
|
| 249 |
+
frame = ims
|
| 250 |
+
rawmode = ims.mode
|
| 251 |
+
if ims.mode not in {"RGB", "RGBA"}:
|
| 252 |
+
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
|
| 253 |
+
frame = ims.convert(rawmode)
|
| 254 |
+
|
| 255 |
+
# Update frame duration
|
| 256 |
+
if isinstance(duration, (list, tuple)):
|
| 257 |
+
frame_duration = duration[frame_idx]
|
| 258 |
+
else:
|
| 259 |
+
frame_duration = duration
|
| 260 |
+
|
| 261 |
+
# Append the frame to the animation encoder
|
| 262 |
+
enc.add(
|
| 263 |
+
frame.tobytes("raw", rawmode),
|
| 264 |
+
frame_duration,
|
| 265 |
+
frame.size,
|
| 266 |
+
rawmode,
|
| 267 |
+
is_single_frame,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# Update frame index
|
| 271 |
+
frame_idx += 1
|
| 272 |
+
|
| 273 |
+
if not save_all:
|
| 274 |
+
break
|
| 275 |
+
|
| 276 |
+
finally:
|
| 277 |
+
im.seek(cur_idx)
|
| 278 |
+
|
| 279 |
+
# Get the final output from the encoder
|
| 280 |
+
data = enc.finish()
|
| 281 |
+
if data is None:
|
| 282 |
+
msg = "cannot write file as AVIF (encoder returned None)"
|
| 283 |
+
raise OSError(msg)
|
| 284 |
+
|
| 285 |
+
fp.write(data)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
|
| 289 |
+
if SUPPORTED:
|
| 290 |
+
Image.register_save(AvifImageFile.format, _save)
|
| 291 |
+
Image.register_save_all(AvifImageFile.format, _save_all)
|
| 292 |
+
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
|
| 293 |
+
Image.register_mime(AvifImageFile.format, "image/avif")
|
PIL/BdfFontFile.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 30 |
+
def bdf_char(
|
| 31 |
+
f: BinaryIO,
|
| 32 |
+
) -> (
|
| 33 |
+
tuple[
|
| 34 |
+
str,
|
| 35 |
+
int,
|
| 36 |
+
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
|
| 37 |
+
Image.Image,
|
| 38 |
+
]
|
| 39 |
+
| None
|
| 40 |
+
):
|
| 41 |
+
# skip to STARTCHAR
|
| 42 |
+
while True:
|
| 43 |
+
s = f.readline()
|
| 44 |
+
if not s:
|
| 45 |
+
return None
|
| 46 |
+
if s.startswith(b"STARTCHAR"):
|
| 47 |
+
break
|
| 48 |
+
id = s[9:].strip().decode("ascii")
|
| 49 |
+
|
| 50 |
+
# load symbol properties
|
| 51 |
+
props = {}
|
| 52 |
+
while True:
|
| 53 |
+
s = f.readline()
|
| 54 |
+
if not s or s.startswith(b"BITMAP"):
|
| 55 |
+
break
|
| 56 |
+
i = s.find(b" ")
|
| 57 |
+
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
| 58 |
+
|
| 59 |
+
# load bitmap
|
| 60 |
+
bitmap = bytearray()
|
| 61 |
+
while True:
|
| 62 |
+
s = f.readline()
|
| 63 |
+
if not s or s.startswith(b"ENDCHAR"):
|
| 64 |
+
break
|
| 65 |
+
bitmap += s[:-1]
|
| 66 |
+
|
| 67 |
+
# The word BBX
|
| 68 |
+
# followed by the width in x (BBw), height in y (BBh),
|
| 69 |
+
# and x and y displacement (BBxoff0, BByoff0)
|
| 70 |
+
# of the lower left corner from the origin of the character.
|
| 71 |
+
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
| 72 |
+
|
| 73 |
+
# The word DWIDTH
|
| 74 |
+
# followed by the width in x and y of the character in device pixels.
|
| 75 |
+
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
| 76 |
+
|
| 77 |
+
bbox = (
|
| 78 |
+
(dwx, dwy),
|
| 79 |
+
(x_disp, -y_disp - height, width + x_disp, -y_disp),
|
| 80 |
+
(0, 0, width, height),
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
|
| 85 |
+
except ValueError:
|
| 86 |
+
# deal with zero-width characters
|
| 87 |
+
im = Image.new("1", (width, height))
|
| 88 |
+
|
| 89 |
+
return id, int(props["ENCODING"]), bbox, im
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class BdfFontFile(FontFile.FontFile):
|
| 93 |
+
"""Font file plugin for the X11 BDF format."""
|
| 94 |
+
|
| 95 |
+
def __init__(self, fp: BinaryIO) -> None:
|
| 96 |
+
super().__init__()
|
| 97 |
+
|
| 98 |
+
s = fp.readline()
|
| 99 |
+
if not s.startswith(b"STARTFONT 2.1"):
|
| 100 |
+
msg = "not a valid BDF file"
|
| 101 |
+
raise SyntaxError(msg)
|
| 102 |
+
|
| 103 |
+
props = {}
|
| 104 |
+
comments = []
|
| 105 |
+
|
| 106 |
+
while True:
|
| 107 |
+
s = fp.readline()
|
| 108 |
+
if not s or s.startswith(b"ENDPROPERTIES"):
|
| 109 |
+
break
|
| 110 |
+
i = s.find(b" ")
|
| 111 |
+
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
| 112 |
+
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
|
| 113 |
+
if s.find(b"LogicalFontDescription") < 0:
|
| 114 |
+
comments.append(s[i + 1 : -1].decode("ascii"))
|
| 115 |
+
|
| 116 |
+
while True:
|
| 117 |
+
c = bdf_char(fp)
|
| 118 |
+
if not c:
|
| 119 |
+
break
|
| 120 |
+
id, ch, (xy, dst, src), im = c
|
| 121 |
+
if 0 <= ch < len(self.glyph):
|
| 122 |
+
self.glyph[ch] = xy, dst, src, im
|
PIL/BlpImagePlugin.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.startswith((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 |
+
assert self.fp is not None
|
| 262 |
+
self.magic = self.fp.read(4)
|
| 263 |
+
if not _accept(self.magic):
|
| 264 |
+
msg = f"Bad BLP magic {repr(self.magic)}"
|
| 265 |
+
raise BLPFormatError(msg)
|
| 266 |
+
|
| 267 |
+
compression = struct.unpack("<i", self.fp.read(4))[0]
|
| 268 |
+
if self.magic == b"BLP1":
|
| 269 |
+
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
|
| 270 |
+
else:
|
| 271 |
+
encoding = struct.unpack("<b", self.fp.read(1))[0]
|
| 272 |
+
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
|
| 273 |
+
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
|
| 274 |
+
self.fp.seek(1, os.SEEK_CUR) # mips
|
| 275 |
+
|
| 276 |
+
self._size = struct.unpack("<II", self.fp.read(8))
|
| 277 |
+
|
| 278 |
+
args: tuple[int, int, bool] | tuple[int, int, bool, int]
|
| 279 |
+
if self.magic == b"BLP1":
|
| 280 |
+
encoding = struct.unpack("<i", self.fp.read(4))[0]
|
| 281 |
+
self.fp.seek(4, os.SEEK_CUR) # subtype
|
| 282 |
+
|
| 283 |
+
args = (compression, encoding, alpha)
|
| 284 |
+
offset = 28
|
| 285 |
+
else:
|
| 286 |
+
args = (compression, encoding, alpha, alpha_encoding)
|
| 287 |
+
offset = 20
|
| 288 |
+
|
| 289 |
+
decoder = self.magic.decode()
|
| 290 |
+
|
| 291 |
+
self._mode = "RGBA" if alpha else "RGB"
|
| 292 |
+
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
|
| 296 |
+
_pulls_fd = True
|
| 297 |
+
|
| 298 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 299 |
+
try:
|
| 300 |
+
self._read_header()
|
| 301 |
+
self._load()
|
| 302 |
+
except struct.error as e:
|
| 303 |
+
msg = "Truncated BLP file"
|
| 304 |
+
raise OSError(msg) from e
|
| 305 |
+
return -1, 0
|
| 306 |
+
|
| 307 |
+
@abc.abstractmethod
|
| 308 |
+
def _load(self) -> None:
|
| 309 |
+
pass
|
| 310 |
+
|
| 311 |
+
def _read_header(self) -> None:
|
| 312 |
+
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
| 313 |
+
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
| 314 |
+
|
| 315 |
+
def _safe_read(self, length: int) -> bytes:
|
| 316 |
+
assert self.fd is not None
|
| 317 |
+
return ImageFile._safe_read(self.fd, length)
|
| 318 |
+
|
| 319 |
+
def _read_palette(self) -> list[tuple[int, int, int, int]]:
|
| 320 |
+
ret = []
|
| 321 |
+
for i in range(256):
|
| 322 |
+
try:
|
| 323 |
+
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
|
| 324 |
+
except struct.error:
|
| 325 |
+
break
|
| 326 |
+
ret.append((b, g, r, a))
|
| 327 |
+
return ret
|
| 328 |
+
|
| 329 |
+
def _read_bgra(
|
| 330 |
+
self, palette: list[tuple[int, int, int, int]], alpha: bool
|
| 331 |
+
) -> bytearray:
|
| 332 |
+
data = bytearray()
|
| 333 |
+
_data = BytesIO(self._safe_read(self._lengths[0]))
|
| 334 |
+
while True:
|
| 335 |
+
try:
|
| 336 |
+
(offset,) = struct.unpack("<B", _data.read(1))
|
| 337 |
+
except struct.error:
|
| 338 |
+
break
|
| 339 |
+
b, g, r, a = palette[offset]
|
| 340 |
+
d: tuple[int, ...] = (r, g, b)
|
| 341 |
+
if alpha:
|
| 342 |
+
d += (a,)
|
| 343 |
+
data.extend(d)
|
| 344 |
+
return data
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
class BLP1Decoder(_BLPBaseDecoder):
|
| 348 |
+
def _load(self) -> None:
|
| 349 |
+
self._compression, self._encoding, alpha = self.args
|
| 350 |
+
|
| 351 |
+
if self._compression == Format.JPEG:
|
| 352 |
+
self._decode_jpeg_stream()
|
| 353 |
+
|
| 354 |
+
elif self._compression == 1:
|
| 355 |
+
if self._encoding in (4, 5):
|
| 356 |
+
palette = self._read_palette()
|
| 357 |
+
data = self._read_bgra(palette, alpha)
|
| 358 |
+
self.set_as_raw(data)
|
| 359 |
+
else:
|
| 360 |
+
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
|
| 361 |
+
raise BLPFormatError(msg)
|
| 362 |
+
else:
|
| 363 |
+
msg = f"Unsupported BLP compression {repr(self._encoding)}"
|
| 364 |
+
raise BLPFormatError(msg)
|
| 365 |
+
|
| 366 |
+
def _decode_jpeg_stream(self) -> None:
|
| 367 |
+
from .JpegImagePlugin import JpegImageFile
|
| 368 |
+
|
| 369 |
+
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
| 370 |
+
jpeg_header = self._safe_read(jpeg_header_size)
|
| 371 |
+
assert self.fd is not None
|
| 372 |
+
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
|
| 373 |
+
data = self._safe_read(self._lengths[0])
|
| 374 |
+
data = jpeg_header + data
|
| 375 |
+
image = JpegImageFile(BytesIO(data))
|
| 376 |
+
Image._decompression_bomb_check(image.size)
|
| 377 |
+
if image.mode == "CMYK":
|
| 378 |
+
args = image.tile[0].args
|
| 379 |
+
assert isinstance(args, tuple)
|
| 380 |
+
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
|
| 381 |
+
self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
class BLP2Decoder(_BLPBaseDecoder):
|
| 385 |
+
def _load(self) -> None:
|
| 386 |
+
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
|
| 387 |
+
|
| 388 |
+
palette = self._read_palette()
|
| 389 |
+
|
| 390 |
+
assert self.fd is not None
|
| 391 |
+
self.fd.seek(self._offsets[0])
|
| 392 |
+
|
| 393 |
+
if self._compression == 1:
|
| 394 |
+
# Uncompressed or DirectX compression
|
| 395 |
+
|
| 396 |
+
if self._encoding == Encoding.UNCOMPRESSED:
|
| 397 |
+
data = self._read_bgra(palette, alpha)
|
| 398 |
+
|
| 399 |
+
elif self._encoding == Encoding.DXT:
|
| 400 |
+
data = bytearray()
|
| 401 |
+
if self._alpha_encoding == AlphaEncoding.DXT1:
|
| 402 |
+
linesize = (self.state.xsize + 3) // 4 * 8
|
| 403 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 404 |
+
for d in decode_dxt1(self._safe_read(linesize), alpha):
|
| 405 |
+
data += d
|
| 406 |
+
|
| 407 |
+
elif self._alpha_encoding == AlphaEncoding.DXT3:
|
| 408 |
+
linesize = (self.state.xsize + 3) // 4 * 16
|
| 409 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 410 |
+
for d in decode_dxt3(self._safe_read(linesize)):
|
| 411 |
+
data += d
|
| 412 |
+
|
| 413 |
+
elif self._alpha_encoding == AlphaEncoding.DXT5:
|
| 414 |
+
linesize = (self.state.xsize + 3) // 4 * 16
|
| 415 |
+
for yb in range((self.state.ysize + 3) // 4):
|
| 416 |
+
for d in decode_dxt5(self._safe_read(linesize)):
|
| 417 |
+
data += d
|
| 418 |
+
else:
|
| 419 |
+
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
|
| 420 |
+
raise BLPFormatError(msg)
|
| 421 |
+
else:
|
| 422 |
+
msg = f"Unknown BLP encoding {repr(self._encoding)}"
|
| 423 |
+
raise BLPFormatError(msg)
|
| 424 |
+
|
| 425 |
+
else:
|
| 426 |
+
msg = f"Unknown BLP compression {repr(self._compression)}"
|
| 427 |
+
raise BLPFormatError(msg)
|
| 428 |
+
|
| 429 |
+
self.set_as_raw(data)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
class BLPEncoder(ImageFile.PyEncoder):
|
| 433 |
+
_pushes_fd = True
|
| 434 |
+
|
| 435 |
+
def _write_palette(self) -> bytes:
|
| 436 |
+
data = b""
|
| 437 |
+
assert self.im is not None
|
| 438 |
+
palette = self.im.getpalette("RGBA", "RGBA")
|
| 439 |
+
for i in range(len(palette) // 4):
|
| 440 |
+
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
| 441 |
+
data += struct.pack("<4B", b, g, r, a)
|
| 442 |
+
while len(data) < 256 * 4:
|
| 443 |
+
data += b"\x00" * 4
|
| 444 |
+
return data
|
| 445 |
+
|
| 446 |
+
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
| 447 |
+
palette_data = self._write_palette()
|
| 448 |
+
|
| 449 |
+
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
| 450 |
+
data = struct.pack("<16I", offset, *((0,) * 15))
|
| 451 |
+
|
| 452 |
+
assert self.im is not None
|
| 453 |
+
w, h = self.im.size
|
| 454 |
+
data += struct.pack("<16I", w * h, *((0,) * 15))
|
| 455 |
+
|
| 456 |
+
data += palette_data
|
| 457 |
+
|
| 458 |
+
for y in range(h):
|
| 459 |
+
for x in range(w):
|
| 460 |
+
data += struct.pack("<B", self.im.getpixel((x, y)))
|
| 461 |
+
|
| 462 |
+
return len(data), 0, data
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 466 |
+
if im.mode != "P":
|
| 467 |
+
msg = "Unsupported BLP image mode"
|
| 468 |
+
raise ValueError(msg)
|
| 469 |
+
|
| 470 |
+
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
|
| 471 |
+
fp.write(magic)
|
| 472 |
+
|
| 473 |
+
assert im.palette is not None
|
| 474 |
+
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
| 475 |
+
|
| 476 |
+
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
|
| 477 |
+
if magic == b"BLP1":
|
| 478 |
+
fp.write(struct.pack("<L", alpha_depth))
|
| 479 |
+
else:
|
| 480 |
+
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
| 481 |
+
fp.write(struct.pack("<b", alpha_depth))
|
| 482 |
+
fp.write(struct.pack("<b", 0)) # alpha encoding
|
| 483 |
+
fp.write(struct.pack("<b", 0)) # mips
|
| 484 |
+
fp.write(struct.pack("<II", *im.size))
|
| 485 |
+
if magic == b"BLP1":
|
| 486 |
+
fp.write(struct.pack("<i", 5))
|
| 487 |
+
fp.write(struct.pack("<i", 0))
|
| 488 |
+
|
| 489 |
+
ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
|
| 493 |
+
Image.register_extension(BlpImageFile.format, ".blp")
|
| 494 |
+
Image.register_decoder("BLP1", BLP1Decoder)
|
| 495 |
+
Image.register_decoder("BLP2", BLP2Decoder)
|
| 496 |
+
|
| 497 |
+
Image.register_save(BlpImageFile.format, _save)
|
| 498 |
+
Image.register_encoder("BLP", BLPEncoder)
|
PIL/BmpImagePlugin.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
USE_RAW_ALPHA = False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _accept(prefix: bytes) -> bool:
|
| 55 |
+
return prefix.startswith(b"BM")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _dib_accept(prefix: bytes) -> bool:
|
| 59 |
+
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# =============================================================================
|
| 63 |
+
# Image plugin for the Windows BMP format.
|
| 64 |
+
# =============================================================================
|
| 65 |
+
class BmpImageFile(ImageFile.ImageFile):
|
| 66 |
+
"""Image plugin for the Windows Bitmap format (BMP)"""
|
| 67 |
+
|
| 68 |
+
# ------------------------------------------------------------- Description
|
| 69 |
+
format_description = "Windows Bitmap"
|
| 70 |
+
format = "BMP"
|
| 71 |
+
|
| 72 |
+
# -------------------------------------------------- BMP Compression values
|
| 73 |
+
COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
|
| 74 |
+
for k, v in COMPRESSIONS.items():
|
| 75 |
+
vars()[k] = v
|
| 76 |
+
|
| 77 |
+
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
|
| 78 |
+
"""Read relevant info about the BMP"""
|
| 79 |
+
assert self.fp is not None
|
| 80 |
+
read, seek = self.fp.read, self.fp.seek
|
| 81 |
+
if header:
|
| 82 |
+
seek(header)
|
| 83 |
+
# read bmp header size @offset 14 (this is part of the header size)
|
| 84 |
+
file_info: dict[str, bool | int | tuple[int, ...]] = {
|
| 85 |
+
"header_size": i32(read(4)),
|
| 86 |
+
"direction": -1,
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
# -------------------- If requested, read header at a specific position
|
| 90 |
+
# read the rest of the bmp header, without its size
|
| 91 |
+
assert isinstance(file_info["header_size"], int)
|
| 92 |
+
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
|
| 93 |
+
|
| 94 |
+
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
|
| 95 |
+
# ----- This format has different offsets because of width/height types
|
| 96 |
+
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
|
| 97 |
+
if file_info["header_size"] == 12:
|
| 98 |
+
file_info["width"] = i16(header_data, 0)
|
| 99 |
+
file_info["height"] = i16(header_data, 2)
|
| 100 |
+
file_info["planes"] = i16(header_data, 4)
|
| 101 |
+
file_info["bits"] = i16(header_data, 6)
|
| 102 |
+
file_info["compression"] = self.COMPRESSIONS["RAW"]
|
| 103 |
+
file_info["palette_padding"] = 3
|
| 104 |
+
|
| 105 |
+
# --------------------------------------------- Windows Bitmap v3 to v5
|
| 106 |
+
# 40: BITMAPINFOHEADER
|
| 107 |
+
# 52: BITMAPV2HEADER
|
| 108 |
+
# 56: BITMAPV3HEADER
|
| 109 |
+
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
|
| 110 |
+
# 108: BITMAPV4HEADER
|
| 111 |
+
# 124: BITMAPV5HEADER
|
| 112 |
+
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
|
| 113 |
+
file_info["y_flip"] = header_data[7] == 0xFF
|
| 114 |
+
file_info["direction"] = 1 if file_info["y_flip"] else -1
|
| 115 |
+
file_info["width"] = i32(header_data, 0)
|
| 116 |
+
file_info["height"] = (
|
| 117 |
+
i32(header_data, 4)
|
| 118 |
+
if not file_info["y_flip"]
|
| 119 |
+
else 2**32 - i32(header_data, 4)
|
| 120 |
+
)
|
| 121 |
+
file_info["planes"] = i16(header_data, 8)
|
| 122 |
+
file_info["bits"] = i16(header_data, 10)
|
| 123 |
+
file_info["compression"] = i32(header_data, 12)
|
| 124 |
+
# byte size of pixel data
|
| 125 |
+
file_info["data_size"] = i32(header_data, 16)
|
| 126 |
+
file_info["pixels_per_meter"] = (
|
| 127 |
+
i32(header_data, 20),
|
| 128 |
+
i32(header_data, 24),
|
| 129 |
+
)
|
| 130 |
+
file_info["colors"] = i32(header_data, 28)
|
| 131 |
+
file_info["palette_padding"] = 4
|
| 132 |
+
assert isinstance(file_info["pixels_per_meter"], tuple)
|
| 133 |
+
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
|
| 134 |
+
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
| 135 |
+
masks = ["r_mask", "g_mask", "b_mask"]
|
| 136 |
+
if len(header_data) >= 48:
|
| 137 |
+
if len(header_data) >= 52:
|
| 138 |
+
masks.append("a_mask")
|
| 139 |
+
else:
|
| 140 |
+
file_info["a_mask"] = 0x0
|
| 141 |
+
for idx, mask in enumerate(masks):
|
| 142 |
+
file_info[mask] = i32(header_data, 36 + idx * 4)
|
| 143 |
+
else:
|
| 144 |
+
# 40 byte headers only have the three components in the
|
| 145 |
+
# bitfields masks, ref:
|
| 146 |
+
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
| 147 |
+
# See also
|
| 148 |
+
# https://github.com/python-pillow/Pillow/issues/1293
|
| 149 |
+
# There is a 4th component in the RGBQuad, in the alpha
|
| 150 |
+
# location, but it is listed as a reserved component,
|
| 151 |
+
# and it is not generally an alpha channel
|
| 152 |
+
file_info["a_mask"] = 0x0
|
| 153 |
+
for mask in masks:
|
| 154 |
+
file_info[mask] = i32(read(4))
|
| 155 |
+
assert isinstance(file_info["r_mask"], int)
|
| 156 |
+
assert isinstance(file_info["g_mask"], int)
|
| 157 |
+
assert isinstance(file_info["b_mask"], int)
|
| 158 |
+
assert isinstance(file_info["a_mask"], int)
|
| 159 |
+
file_info["rgb_mask"] = (
|
| 160 |
+
file_info["r_mask"],
|
| 161 |
+
file_info["g_mask"],
|
| 162 |
+
file_info["b_mask"],
|
| 163 |
+
)
|
| 164 |
+
file_info["rgba_mask"] = (
|
| 165 |
+
file_info["r_mask"],
|
| 166 |
+
file_info["g_mask"],
|
| 167 |
+
file_info["b_mask"],
|
| 168 |
+
file_info["a_mask"],
|
| 169 |
+
)
|
| 170 |
+
else:
|
| 171 |
+
msg = f"Unsupported BMP header type ({file_info['header_size']})"
|
| 172 |
+
raise OSError(msg)
|
| 173 |
+
|
| 174 |
+
# ------------------ Special case : header is reported 40, which
|
| 175 |
+
# ---------------------- is shorter than real size for bpp >= 16
|
| 176 |
+
assert isinstance(file_info["width"], int)
|
| 177 |
+
assert isinstance(file_info["height"], int)
|
| 178 |
+
self._size = file_info["width"], file_info["height"]
|
| 179 |
+
|
| 180 |
+
# ------- If color count was not found in the header, compute from bits
|
| 181 |
+
assert isinstance(file_info["bits"], int)
|
| 182 |
+
file_info["colors"] = (
|
| 183 |
+
file_info["colors"]
|
| 184 |
+
if file_info.get("colors", 0)
|
| 185 |
+
else (1 << file_info["bits"])
|
| 186 |
+
)
|
| 187 |
+
assert isinstance(file_info["colors"], int)
|
| 188 |
+
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
|
| 189 |
+
offset += 4 * file_info["colors"]
|
| 190 |
+
|
| 191 |
+
# ---------------------- Check bit depth for unusual unsupported values
|
| 192 |
+
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
|
| 193 |
+
if not self.mode:
|
| 194 |
+
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
|
| 195 |
+
raise OSError(msg)
|
| 196 |
+
|
| 197 |
+
# ---------------- Process BMP with Bitfields compression (not palette)
|
| 198 |
+
decoder_name = "raw"
|
| 199 |
+
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
| 200 |
+
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
|
| 201 |
+
32: [
|
| 202 |
+
(0xFF0000, 0xFF00, 0xFF, 0x0),
|
| 203 |
+
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
|
| 204 |
+
(0xFF000000, 0xFF00, 0xFF, 0x0),
|
| 205 |
+
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
|
| 206 |
+
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
|
| 207 |
+
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
|
| 208 |
+
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
|
| 209 |
+
(0x0, 0x0, 0x0, 0x0),
|
| 210 |
+
],
|
| 211 |
+
24: [(0xFF0000, 0xFF00, 0xFF)],
|
| 212 |
+
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
|
| 213 |
+
}
|
| 214 |
+
MASK_MODES = {
|
| 215 |
+
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
|
| 216 |
+
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
|
| 217 |
+
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
|
| 218 |
+
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
|
| 219 |
+
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
|
| 220 |
+
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
|
| 221 |
+
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
|
| 222 |
+
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
| 223 |
+
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
|
| 224 |
+
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
|
| 225 |
+
(16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
|
| 226 |
+
}
|
| 227 |
+
if file_info["bits"] in SUPPORTED:
|
| 228 |
+
if (
|
| 229 |
+
file_info["bits"] == 32
|
| 230 |
+
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
|
| 231 |
+
):
|
| 232 |
+
assert isinstance(file_info["rgba_mask"], tuple)
|
| 233 |
+
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
|
| 234 |
+
self._mode = "RGBA" if "A" in raw_mode else self.mode
|
| 235 |
+
elif (
|
| 236 |
+
file_info["bits"] in (24, 16)
|
| 237 |
+
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
|
| 238 |
+
):
|
| 239 |
+
assert isinstance(file_info["rgb_mask"], tuple)
|
| 240 |
+
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
|
| 241 |
+
else:
|
| 242 |
+
msg = "Unsupported BMP bitfields layout"
|
| 243 |
+
raise OSError(msg)
|
| 244 |
+
else:
|
| 245 |
+
msg = "Unsupported BMP bitfields layout"
|
| 246 |
+
raise OSError(msg)
|
| 247 |
+
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
|
| 248 |
+
if file_info["bits"] == 32 and (
|
| 249 |
+
header == 22 or USE_RAW_ALPHA # 32-bit .cur offset
|
| 250 |
+
):
|
| 251 |
+
raw_mode, self._mode = "BGRA", "RGBA"
|
| 252 |
+
elif file_info["compression"] in (
|
| 253 |
+
self.COMPRESSIONS["RLE8"],
|
| 254 |
+
self.COMPRESSIONS["RLE4"],
|
| 255 |
+
):
|
| 256 |
+
decoder_name = "bmp_rle"
|
| 257 |
+
else:
|
| 258 |
+
msg = f"Unsupported BMP compression ({file_info['compression']})"
|
| 259 |
+
raise OSError(msg)
|
| 260 |
+
|
| 261 |
+
# --------------- Once the header is processed, process the palette/LUT
|
| 262 |
+
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
|
| 263 |
+
# ---------------------------------------------------- 1-bit images
|
| 264 |
+
if not (0 < file_info["colors"] <= 65536):
|
| 265 |
+
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
|
| 266 |
+
raise OSError(msg)
|
| 267 |
+
else:
|
| 268 |
+
assert isinstance(file_info["palette_padding"], int)
|
| 269 |
+
padding = file_info["palette_padding"]
|
| 270 |
+
palette = read(padding * file_info["colors"])
|
| 271 |
+
grayscale = True
|
| 272 |
+
indices = (
|
| 273 |
+
(0, 255)
|
| 274 |
+
if file_info["colors"] == 2
|
| 275 |
+
else list(range(file_info["colors"]))
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# ----------------- Check if grayscale and ignore palette if so
|
| 279 |
+
for ind, val in enumerate(indices):
|
| 280 |
+
rgb = palette[ind * padding : ind * padding + 3]
|
| 281 |
+
if rgb != o8(val) * 3:
|
| 282 |
+
grayscale = False
|
| 283 |
+
|
| 284 |
+
# ------- If all colors are gray, white or black, ditch palette
|
| 285 |
+
if grayscale:
|
| 286 |
+
self._mode = "1" if file_info["colors"] == 2 else "L"
|
| 287 |
+
raw_mode = self.mode
|
| 288 |
+
else:
|
| 289 |
+
self._mode = "P"
|
| 290 |
+
self.palette = ImagePalette.raw(
|
| 291 |
+
"BGRX" if padding == 4 else "BGR", palette
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# ---------------------------- Finally set the tile data for the plugin
|
| 295 |
+
self.info["compression"] = file_info["compression"]
|
| 296 |
+
args: list[Any] = [raw_mode]
|
| 297 |
+
if decoder_name == "bmp_rle":
|
| 298 |
+
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
|
| 299 |
+
else:
|
| 300 |
+
assert isinstance(file_info["width"], int)
|
| 301 |
+
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
|
| 302 |
+
args.append(file_info["direction"])
|
| 303 |
+
self.tile = [
|
| 304 |
+
ImageFile._Tile(
|
| 305 |
+
decoder_name,
|
| 306 |
+
(0, 0, file_info["width"], file_info["height"]),
|
| 307 |
+
offset or self.fp.tell(),
|
| 308 |
+
tuple(args),
|
| 309 |
+
)
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
def _open(self) -> None:
|
| 313 |
+
"""Open file, check magic number and read header"""
|
| 314 |
+
# read 14 bytes: magic number, filesize, reserved, header final offset
|
| 315 |
+
assert self.fp is not None
|
| 316 |
+
head_data = self.fp.read(14)
|
| 317 |
+
# choke if the file does not have the required magic bytes
|
| 318 |
+
if not _accept(head_data):
|
| 319 |
+
msg = "Not a BMP file"
|
| 320 |
+
raise SyntaxError(msg)
|
| 321 |
+
# read the start position of the BMP image data (u32)
|
| 322 |
+
offset = i32(head_data, 10)
|
| 323 |
+
# load bitmap information (offset=raster info)
|
| 324 |
+
self._bitmap(offset=offset)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
class BmpRleDecoder(ImageFile.PyDecoder):
|
| 328 |
+
_pulls_fd = True
|
| 329 |
+
|
| 330 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 331 |
+
assert self.fd is not None
|
| 332 |
+
rle4 = self.args[1]
|
| 333 |
+
data = bytearray()
|
| 334 |
+
x = 0
|
| 335 |
+
dest_length = self.state.xsize * self.state.ysize
|
| 336 |
+
while len(data) < dest_length:
|
| 337 |
+
pixels = self.fd.read(1)
|
| 338 |
+
byte = self.fd.read(1)
|
| 339 |
+
if not pixels or not byte:
|
| 340 |
+
break
|
| 341 |
+
num_pixels = pixels[0]
|
| 342 |
+
if num_pixels:
|
| 343 |
+
# encoded mode
|
| 344 |
+
if x + num_pixels > self.state.xsize:
|
| 345 |
+
# Too much data for row
|
| 346 |
+
num_pixels = max(0, self.state.xsize - x)
|
| 347 |
+
if rle4:
|
| 348 |
+
first_pixel = o8(byte[0] >> 4)
|
| 349 |
+
second_pixel = o8(byte[0] & 0x0F)
|
| 350 |
+
for index in range(num_pixels):
|
| 351 |
+
if index % 2 == 0:
|
| 352 |
+
data += first_pixel
|
| 353 |
+
else:
|
| 354 |
+
data += second_pixel
|
| 355 |
+
else:
|
| 356 |
+
data += byte * num_pixels
|
| 357 |
+
x += num_pixels
|
| 358 |
+
else:
|
| 359 |
+
if byte[0] == 0:
|
| 360 |
+
# end of line
|
| 361 |
+
while len(data) % self.state.xsize != 0:
|
| 362 |
+
data += b"\x00"
|
| 363 |
+
x = 0
|
| 364 |
+
elif byte[0] == 1:
|
| 365 |
+
# end of bitmap
|
| 366 |
+
break
|
| 367 |
+
elif byte[0] == 2:
|
| 368 |
+
# delta
|
| 369 |
+
bytes_read = self.fd.read(2)
|
| 370 |
+
if len(bytes_read) < 2:
|
| 371 |
+
break
|
| 372 |
+
right, up = self.fd.read(2)
|
| 373 |
+
data += b"\x00" * (right + up * self.state.xsize)
|
| 374 |
+
x = len(data) % self.state.xsize
|
| 375 |
+
else:
|
| 376 |
+
# absolute mode
|
| 377 |
+
if rle4:
|
| 378 |
+
# 2 pixels per byte
|
| 379 |
+
byte_count = byte[0] // 2
|
| 380 |
+
bytes_read = self.fd.read(byte_count)
|
| 381 |
+
for byte_read in bytes_read:
|
| 382 |
+
data += o8(byte_read >> 4)
|
| 383 |
+
data += o8(byte_read & 0x0F)
|
| 384 |
+
else:
|
| 385 |
+
byte_count = byte[0]
|
| 386 |
+
bytes_read = self.fd.read(byte_count)
|
| 387 |
+
data += bytes_read
|
| 388 |
+
if len(bytes_read) < byte_count:
|
| 389 |
+
break
|
| 390 |
+
x += byte[0]
|
| 391 |
+
|
| 392 |
+
# align to 16-bit word boundary
|
| 393 |
+
if self.fd.tell() % 2 != 0:
|
| 394 |
+
self.fd.seek(1, os.SEEK_CUR)
|
| 395 |
+
rawmode = "L" if self.mode == "L" else "P"
|
| 396 |
+
self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
|
| 397 |
+
return -1, 0
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
# =============================================================================
|
| 401 |
+
# Image plugin for the DIB format (BMP alias)
|
| 402 |
+
# =============================================================================
|
| 403 |
+
class DibImageFile(BmpImageFile):
|
| 404 |
+
format = "DIB"
|
| 405 |
+
format_description = "Windows Bitmap"
|
| 406 |
+
|
| 407 |
+
def _open(self) -> None:
|
| 408 |
+
self._bitmap()
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
#
|
| 412 |
+
# --------------------------------------------------------------------
|
| 413 |
+
# Write BMP file
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
SAVE = {
|
| 417 |
+
"1": ("1", 1, 2),
|
| 418 |
+
"L": ("L", 8, 256),
|
| 419 |
+
"P": ("P", 8, 256),
|
| 420 |
+
"RGB": ("BGR", 24, 0),
|
| 421 |
+
"RGBA": ("BGRA", 32, 0),
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 426 |
+
_save(im, fp, filename, False)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def _save(
|
| 430 |
+
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
|
| 431 |
+
) -> None:
|
| 432 |
+
try:
|
| 433 |
+
rawmode, bits, colors = SAVE[im.mode]
|
| 434 |
+
except KeyError as e:
|
| 435 |
+
msg = f"cannot write mode {im.mode} as BMP"
|
| 436 |
+
raise OSError(msg) from e
|
| 437 |
+
|
| 438 |
+
info = im.encoderinfo
|
| 439 |
+
|
| 440 |
+
dpi = info.get("dpi", (96, 96))
|
| 441 |
+
|
| 442 |
+
# 1 meter == 39.3701 inches
|
| 443 |
+
ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
|
| 444 |
+
|
| 445 |
+
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
| 446 |
+
header = 40 # or 64 for OS/2 version 2
|
| 447 |
+
image = stride * im.size[1]
|
| 448 |
+
|
| 449 |
+
if im.mode == "1":
|
| 450 |
+
palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255))
|
| 451 |
+
elif im.mode == "L":
|
| 452 |
+
palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256))
|
| 453 |
+
elif im.mode == "P":
|
| 454 |
+
palette = im.im.getpalette("RGB", "BGRX")
|
| 455 |
+
colors = len(palette) // 4
|
| 456 |
+
else:
|
| 457 |
+
palette = None
|
| 458 |
+
|
| 459 |
+
# bitmap header
|
| 460 |
+
if bitmap_header:
|
| 461 |
+
offset = 14 + header + colors * 4
|
| 462 |
+
file_size = offset + image
|
| 463 |
+
if file_size > 2**32 - 1:
|
| 464 |
+
msg = "File size is too large for the BMP format"
|
| 465 |
+
raise ValueError(msg)
|
| 466 |
+
fp.write(
|
| 467 |
+
b"BM" # file type (magic)
|
| 468 |
+
+ o32(file_size) # file size
|
| 469 |
+
+ o32(0) # reserved
|
| 470 |
+
+ o32(offset) # image data offset
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
# bitmap info header
|
| 474 |
+
fp.write(
|
| 475 |
+
o32(header) # info header size
|
| 476 |
+
+ o32(im.size[0]) # width
|
| 477 |
+
+ o32(im.size[1]) # height
|
| 478 |
+
+ o16(1) # planes
|
| 479 |
+
+ o16(bits) # depth
|
| 480 |
+
+ o32(0) # compression (0=uncompressed)
|
| 481 |
+
+ o32(image) # size of bitmap
|
| 482 |
+
+ o32(ppm[0]) # resolution
|
| 483 |
+
+ o32(ppm[1]) # resolution
|
| 484 |
+
+ o32(colors) # colors used
|
| 485 |
+
+ o32(colors) # colors important
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
| 489 |
+
|
| 490 |
+
if palette:
|
| 491 |
+
fp.write(palette)
|
| 492 |
+
|
| 493 |
+
ImageFile._save(
|
| 494 |
+
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
#
|
| 499 |
+
# --------------------------------------------------------------------
|
| 500 |
+
# Registry
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
|
| 504 |
+
Image.register_save(BmpImageFile.format, _save)
|
| 505 |
+
|
| 506 |
+
Image.register_extension(BmpImageFile.format, ".bmp")
|
| 507 |
+
|
| 508 |
+
Image.register_mime(BmpImageFile.format, "image/bmp")
|
| 509 |
+
|
| 510 |
+
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
| 511 |
+
|
| 512 |
+
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
| 513 |
+
Image.register_save(DibImageFile.format, _dib_save)
|
| 514 |
+
|
| 515 |
+
Image.register_extension(DibImageFile.format, ".dib")
|
| 516 |
+
|
| 517 |
+
Image.register_mime(DibImageFile.format, "image/bmp")
|
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()
|
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
|
| 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.startswith(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 |
+
assert self.fp is not None
|
| 42 |
+
offset = self.fp.tell()
|
| 43 |
+
|
| 44 |
+
# check magic
|
| 45 |
+
s = self.fp.read(6)
|
| 46 |
+
if not _accept(s):
|
| 47 |
+
msg = "not a CUR file"
|
| 48 |
+
raise SyntaxError(msg)
|
| 49 |
+
|
| 50 |
+
# pick the largest cursor in the file
|
| 51 |
+
m = b""
|
| 52 |
+
for i in range(i16(s, 4)):
|
| 53 |
+
s = self.fp.read(16)
|
| 54 |
+
if not m:
|
| 55 |
+
m = s
|
| 56 |
+
elif s[0] > m[0] and s[1] > m[1]:
|
| 57 |
+
m = s
|
| 58 |
+
if not m:
|
| 59 |
+
msg = "No cursors were found"
|
| 60 |
+
raise TypeError(msg)
|
| 61 |
+
|
| 62 |
+
# load as bitmap
|
| 63 |
+
self._bitmap(i32(m, 12) + offset)
|
| 64 |
+
|
| 65 |
+
# patch up the bitmap height
|
| 66 |
+
self._size = self.size[0], self.size[1] // 2
|
| 67 |
+
self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
#
|
| 71 |
+
# --------------------------------------------------------------------
|
| 72 |
+
|
| 73 |
+
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
| 74 |
+
|
| 75 |
+
Image.register_extension(CurImageFile.format, ".cur")
|
PIL/DcxImagePlugin.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ._util import DeferredError
|
| 28 |
+
from .PcxImagePlugin import PcxImageFile
|
| 29 |
+
|
| 30 |
+
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _accept(prefix: bytes) -> bool:
|
| 34 |
+
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
##
|
| 38 |
+
# Image plugin for the Intel DCX format.
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DcxImageFile(PcxImageFile):
|
| 42 |
+
format = "DCX"
|
| 43 |
+
format_description = "Intel DCX"
|
| 44 |
+
_close_exclusive_fp_after_loading = False
|
| 45 |
+
|
| 46 |
+
def _open(self) -> None:
|
| 47 |
+
# Header
|
| 48 |
+
assert self.fp is not None
|
| 49 |
+
s = self.fp.read(4)
|
| 50 |
+
if not _accept(s):
|
| 51 |
+
msg = "not a DCX file"
|
| 52 |
+
raise SyntaxError(msg)
|
| 53 |
+
|
| 54 |
+
# Component directory
|
| 55 |
+
self._offset = []
|
| 56 |
+
for i in range(1024):
|
| 57 |
+
offset = i32(self.fp.read(4))
|
| 58 |
+
if not offset:
|
| 59 |
+
break
|
| 60 |
+
self._offset.append(offset)
|
| 61 |
+
|
| 62 |
+
self._fp = self.fp
|
| 63 |
+
self.frame = -1
|
| 64 |
+
self.n_frames = len(self._offset)
|
| 65 |
+
self.is_animated = self.n_frames > 1
|
| 66 |
+
self.seek(0)
|
| 67 |
+
|
| 68 |
+
def seek(self, frame: int) -> None:
|
| 69 |
+
if not self._seek_check(frame):
|
| 70 |
+
return
|
| 71 |
+
if isinstance(self._fp, DeferredError):
|
| 72 |
+
raise self._fp.ex
|
| 73 |
+
self.frame = frame
|
| 74 |
+
self.fp = self._fp
|
| 75 |
+
self.fp.seek(self._offset[frame])
|
| 76 |
+
PcxImageFile._open(self)
|
| 77 |
+
|
| 78 |
+
def tell(self) -> int:
|
| 79 |
+
return self.frame
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
| 83 |
+
|
| 84 |
+
Image.register_extension(DcxImageFile.format, ".dcx")
|
PIL/DdsImagePlugin.py
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A Pillow plugin 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 struct
|
| 16 |
+
import sys
|
| 17 |
+
from enum import IntEnum, IntFlag
|
| 18 |
+
from typing import IO
|
| 19 |
+
|
| 20 |
+
from . import Image, ImageFile, ImagePalette
|
| 21 |
+
from ._binary import i32le as i32
|
| 22 |
+
from ._binary import o8
|
| 23 |
+
from ._binary import o32le as o32
|
| 24 |
+
|
| 25 |
+
# Magic ("DDS ")
|
| 26 |
+
DDS_MAGIC = 0x20534444
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# DDS flags
|
| 30 |
+
class DDSD(IntFlag):
|
| 31 |
+
CAPS = 0x1
|
| 32 |
+
HEIGHT = 0x2
|
| 33 |
+
WIDTH = 0x4
|
| 34 |
+
PITCH = 0x8
|
| 35 |
+
PIXELFORMAT = 0x1000
|
| 36 |
+
MIPMAPCOUNT = 0x20000
|
| 37 |
+
LINEARSIZE = 0x80000
|
| 38 |
+
DEPTH = 0x800000
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# DDS caps
|
| 42 |
+
class DDSCAPS(IntFlag):
|
| 43 |
+
COMPLEX = 0x8
|
| 44 |
+
TEXTURE = 0x1000
|
| 45 |
+
MIPMAP = 0x400000
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class DDSCAPS2(IntFlag):
|
| 49 |
+
CUBEMAP = 0x200
|
| 50 |
+
CUBEMAP_POSITIVEX = 0x400
|
| 51 |
+
CUBEMAP_NEGATIVEX = 0x800
|
| 52 |
+
CUBEMAP_POSITIVEY = 0x1000
|
| 53 |
+
CUBEMAP_NEGATIVEY = 0x2000
|
| 54 |
+
CUBEMAP_POSITIVEZ = 0x4000
|
| 55 |
+
CUBEMAP_NEGATIVEZ = 0x8000
|
| 56 |
+
VOLUME = 0x200000
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Pixel Format
|
| 60 |
+
class DDPF(IntFlag):
|
| 61 |
+
ALPHAPIXELS = 0x1
|
| 62 |
+
ALPHA = 0x2
|
| 63 |
+
FOURCC = 0x4
|
| 64 |
+
PALETTEINDEXED8 = 0x20
|
| 65 |
+
RGB = 0x40
|
| 66 |
+
LUMINANCE = 0x20000
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# dxgiformat.h
|
| 70 |
+
class DXGI_FORMAT(IntEnum):
|
| 71 |
+
UNKNOWN = 0
|
| 72 |
+
R32G32B32A32_TYPELESS = 1
|
| 73 |
+
R32G32B32A32_FLOAT = 2
|
| 74 |
+
R32G32B32A32_UINT = 3
|
| 75 |
+
R32G32B32A32_SINT = 4
|
| 76 |
+
R32G32B32_TYPELESS = 5
|
| 77 |
+
R32G32B32_FLOAT = 6
|
| 78 |
+
R32G32B32_UINT = 7
|
| 79 |
+
R32G32B32_SINT = 8
|
| 80 |
+
R16G16B16A16_TYPELESS = 9
|
| 81 |
+
R16G16B16A16_FLOAT = 10
|
| 82 |
+
R16G16B16A16_UNORM = 11
|
| 83 |
+
R16G16B16A16_UINT = 12
|
| 84 |
+
R16G16B16A16_SNORM = 13
|
| 85 |
+
R16G16B16A16_SINT = 14
|
| 86 |
+
R32G32_TYPELESS = 15
|
| 87 |
+
R32G32_FLOAT = 16
|
| 88 |
+
R32G32_UINT = 17
|
| 89 |
+
R32G32_SINT = 18
|
| 90 |
+
R32G8X24_TYPELESS = 19
|
| 91 |
+
D32_FLOAT_S8X24_UINT = 20
|
| 92 |
+
R32_FLOAT_X8X24_TYPELESS = 21
|
| 93 |
+
X32_TYPELESS_G8X24_UINT = 22
|
| 94 |
+
R10G10B10A2_TYPELESS = 23
|
| 95 |
+
R10G10B10A2_UNORM = 24
|
| 96 |
+
R10G10B10A2_UINT = 25
|
| 97 |
+
R11G11B10_FLOAT = 26
|
| 98 |
+
R8G8B8A8_TYPELESS = 27
|
| 99 |
+
R8G8B8A8_UNORM = 28
|
| 100 |
+
R8G8B8A8_UNORM_SRGB = 29
|
| 101 |
+
R8G8B8A8_UINT = 30
|
| 102 |
+
R8G8B8A8_SNORM = 31
|
| 103 |
+
R8G8B8A8_SINT = 32
|
| 104 |
+
R16G16_TYPELESS = 33
|
| 105 |
+
R16G16_FLOAT = 34
|
| 106 |
+
R16G16_UNORM = 35
|
| 107 |
+
R16G16_UINT = 36
|
| 108 |
+
R16G16_SNORM = 37
|
| 109 |
+
R16G16_SINT = 38
|
| 110 |
+
R32_TYPELESS = 39
|
| 111 |
+
D32_FLOAT = 40
|
| 112 |
+
R32_FLOAT = 41
|
| 113 |
+
R32_UINT = 42
|
| 114 |
+
R32_SINT = 43
|
| 115 |
+
R24G8_TYPELESS = 44
|
| 116 |
+
D24_UNORM_S8_UINT = 45
|
| 117 |
+
R24_UNORM_X8_TYPELESS = 46
|
| 118 |
+
X24_TYPELESS_G8_UINT = 47
|
| 119 |
+
R8G8_TYPELESS = 48
|
| 120 |
+
R8G8_UNORM = 49
|
| 121 |
+
R8G8_UINT = 50
|
| 122 |
+
R8G8_SNORM = 51
|
| 123 |
+
R8G8_SINT = 52
|
| 124 |
+
R16_TYPELESS = 53
|
| 125 |
+
R16_FLOAT = 54
|
| 126 |
+
D16_UNORM = 55
|
| 127 |
+
R16_UNORM = 56
|
| 128 |
+
R16_UINT = 57
|
| 129 |
+
R16_SNORM = 58
|
| 130 |
+
R16_SINT = 59
|
| 131 |
+
R8_TYPELESS = 60
|
| 132 |
+
R8_UNORM = 61
|
| 133 |
+
R8_UINT = 62
|
| 134 |
+
R8_SNORM = 63
|
| 135 |
+
R8_SINT = 64
|
| 136 |
+
A8_UNORM = 65
|
| 137 |
+
R1_UNORM = 66
|
| 138 |
+
R9G9B9E5_SHAREDEXP = 67
|
| 139 |
+
R8G8_B8G8_UNORM = 68
|
| 140 |
+
G8R8_G8B8_UNORM = 69
|
| 141 |
+
BC1_TYPELESS = 70
|
| 142 |
+
BC1_UNORM = 71
|
| 143 |
+
BC1_UNORM_SRGB = 72
|
| 144 |
+
BC2_TYPELESS = 73
|
| 145 |
+
BC2_UNORM = 74
|
| 146 |
+
BC2_UNORM_SRGB = 75
|
| 147 |
+
BC3_TYPELESS = 76
|
| 148 |
+
BC3_UNORM = 77
|
| 149 |
+
BC3_UNORM_SRGB = 78
|
| 150 |
+
BC4_TYPELESS = 79
|
| 151 |
+
BC4_UNORM = 80
|
| 152 |
+
BC4_SNORM = 81
|
| 153 |
+
BC5_TYPELESS = 82
|
| 154 |
+
BC5_UNORM = 83
|
| 155 |
+
BC5_SNORM = 84
|
| 156 |
+
B5G6R5_UNORM = 85
|
| 157 |
+
B5G5R5A1_UNORM = 86
|
| 158 |
+
B8G8R8A8_UNORM = 87
|
| 159 |
+
B8G8R8X8_UNORM = 88
|
| 160 |
+
R10G10B10_XR_BIAS_A2_UNORM = 89
|
| 161 |
+
B8G8R8A8_TYPELESS = 90
|
| 162 |
+
B8G8R8A8_UNORM_SRGB = 91
|
| 163 |
+
B8G8R8X8_TYPELESS = 92
|
| 164 |
+
B8G8R8X8_UNORM_SRGB = 93
|
| 165 |
+
BC6H_TYPELESS = 94
|
| 166 |
+
BC6H_UF16 = 95
|
| 167 |
+
BC6H_SF16 = 96
|
| 168 |
+
BC7_TYPELESS = 97
|
| 169 |
+
BC7_UNORM = 98
|
| 170 |
+
BC7_UNORM_SRGB = 99
|
| 171 |
+
AYUV = 100
|
| 172 |
+
Y410 = 101
|
| 173 |
+
Y416 = 102
|
| 174 |
+
NV12 = 103
|
| 175 |
+
P010 = 104
|
| 176 |
+
P016 = 105
|
| 177 |
+
OPAQUE_420 = 106
|
| 178 |
+
YUY2 = 107
|
| 179 |
+
Y210 = 108
|
| 180 |
+
Y216 = 109
|
| 181 |
+
NV11 = 110
|
| 182 |
+
AI44 = 111
|
| 183 |
+
IA44 = 112
|
| 184 |
+
P8 = 113
|
| 185 |
+
A8P8 = 114
|
| 186 |
+
B4G4R4A4_UNORM = 115
|
| 187 |
+
P208 = 130
|
| 188 |
+
V208 = 131
|
| 189 |
+
V408 = 132
|
| 190 |
+
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
|
| 191 |
+
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class D3DFMT(IntEnum):
|
| 195 |
+
UNKNOWN = 0
|
| 196 |
+
R8G8B8 = 20
|
| 197 |
+
A8R8G8B8 = 21
|
| 198 |
+
X8R8G8B8 = 22
|
| 199 |
+
R5G6B5 = 23
|
| 200 |
+
X1R5G5B5 = 24
|
| 201 |
+
A1R5G5B5 = 25
|
| 202 |
+
A4R4G4B4 = 26
|
| 203 |
+
R3G3B2 = 27
|
| 204 |
+
A8 = 28
|
| 205 |
+
A8R3G3B2 = 29
|
| 206 |
+
X4R4G4B4 = 30
|
| 207 |
+
A2B10G10R10 = 31
|
| 208 |
+
A8B8G8R8 = 32
|
| 209 |
+
X8B8G8R8 = 33
|
| 210 |
+
G16R16 = 34
|
| 211 |
+
A2R10G10B10 = 35
|
| 212 |
+
A16B16G16R16 = 36
|
| 213 |
+
A8P8 = 40
|
| 214 |
+
P8 = 41
|
| 215 |
+
L8 = 50
|
| 216 |
+
A8L8 = 51
|
| 217 |
+
A4L4 = 52
|
| 218 |
+
V8U8 = 60
|
| 219 |
+
L6V5U5 = 61
|
| 220 |
+
X8L8V8U8 = 62
|
| 221 |
+
Q8W8V8U8 = 63
|
| 222 |
+
V16U16 = 64
|
| 223 |
+
A2W10V10U10 = 67
|
| 224 |
+
D16_LOCKABLE = 70
|
| 225 |
+
D32 = 71
|
| 226 |
+
D15S1 = 73
|
| 227 |
+
D24S8 = 75
|
| 228 |
+
D24X8 = 77
|
| 229 |
+
D24X4S4 = 79
|
| 230 |
+
D16 = 80
|
| 231 |
+
D32F_LOCKABLE = 82
|
| 232 |
+
D24FS8 = 83
|
| 233 |
+
D32_LOCKABLE = 84
|
| 234 |
+
S8_LOCKABLE = 85
|
| 235 |
+
L16 = 81
|
| 236 |
+
VERTEXDATA = 100
|
| 237 |
+
INDEX16 = 101
|
| 238 |
+
INDEX32 = 102
|
| 239 |
+
Q16W16V16U16 = 110
|
| 240 |
+
R16F = 111
|
| 241 |
+
G16R16F = 112
|
| 242 |
+
A16B16G16R16F = 113
|
| 243 |
+
R32F = 114
|
| 244 |
+
G32R32F = 115
|
| 245 |
+
A32B32G32R32F = 116
|
| 246 |
+
CxV8U8 = 117
|
| 247 |
+
A1 = 118
|
| 248 |
+
A2B10G10R10_XR_BIAS = 119
|
| 249 |
+
BINARYBUFFER = 199
|
| 250 |
+
|
| 251 |
+
UYVY = i32(b"UYVY")
|
| 252 |
+
R8G8_B8G8 = i32(b"RGBG")
|
| 253 |
+
YUY2 = i32(b"YUY2")
|
| 254 |
+
G8R8_G8B8 = i32(b"GRGB")
|
| 255 |
+
DXT1 = i32(b"DXT1")
|
| 256 |
+
DXT2 = i32(b"DXT2")
|
| 257 |
+
DXT3 = i32(b"DXT3")
|
| 258 |
+
DXT4 = i32(b"DXT4")
|
| 259 |
+
DXT5 = i32(b"DXT5")
|
| 260 |
+
DX10 = i32(b"DX10")
|
| 261 |
+
BC4S = i32(b"BC4S")
|
| 262 |
+
BC4U = i32(b"BC4U")
|
| 263 |
+
BC5S = i32(b"BC5S")
|
| 264 |
+
BC5U = i32(b"BC5U")
|
| 265 |
+
ATI1 = i32(b"ATI1")
|
| 266 |
+
ATI2 = i32(b"ATI2")
|
| 267 |
+
MULTI2_ARGB8 = i32(b"MET1")
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# Backward compatibility layer
|
| 271 |
+
module = sys.modules[__name__]
|
| 272 |
+
for item in DDSD:
|
| 273 |
+
assert item.name is not None
|
| 274 |
+
setattr(module, f"DDSD_{item.name}", item.value)
|
| 275 |
+
for item1 in DDSCAPS:
|
| 276 |
+
assert item1.name is not None
|
| 277 |
+
setattr(module, f"DDSCAPS_{item1.name}", item1.value)
|
| 278 |
+
for item2 in DDSCAPS2:
|
| 279 |
+
assert item2.name is not None
|
| 280 |
+
setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
|
| 281 |
+
for item3 in DDPF:
|
| 282 |
+
assert item3.name is not None
|
| 283 |
+
setattr(module, f"DDPF_{item3.name}", item3.value)
|
| 284 |
+
|
| 285 |
+
DDS_FOURCC = DDPF.FOURCC
|
| 286 |
+
DDS_RGB = DDPF.RGB
|
| 287 |
+
DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
|
| 288 |
+
DDS_LUMINANCE = DDPF.LUMINANCE
|
| 289 |
+
DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
|
| 290 |
+
DDS_ALPHA = DDPF.ALPHA
|
| 291 |
+
DDS_PAL8 = DDPF.PALETTEINDEXED8
|
| 292 |
+
|
| 293 |
+
DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
|
| 294 |
+
DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
|
| 295 |
+
DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
|
| 296 |
+
DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
|
| 297 |
+
DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
|
| 298 |
+
|
| 299 |
+
DDS_HEIGHT = DDSD.HEIGHT
|
| 300 |
+
DDS_WIDTH = DDSD.WIDTH
|
| 301 |
+
|
| 302 |
+
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
|
| 303 |
+
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
|
| 304 |
+
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
|
| 305 |
+
|
| 306 |
+
DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
|
| 307 |
+
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
|
| 308 |
+
DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
|
| 309 |
+
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
|
| 310 |
+
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
|
| 311 |
+
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
|
| 312 |
+
|
| 313 |
+
DXT1_FOURCC = D3DFMT.DXT1
|
| 314 |
+
DXT3_FOURCC = D3DFMT.DXT3
|
| 315 |
+
DXT5_FOURCC = D3DFMT.DXT5
|
| 316 |
+
|
| 317 |
+
DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
|
| 318 |
+
DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
|
| 319 |
+
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
|
| 320 |
+
DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
|
| 321 |
+
DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
|
| 322 |
+
DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
|
| 323 |
+
DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
|
| 324 |
+
DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
|
| 325 |
+
DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
|
| 326 |
+
DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
|
| 327 |
+
DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class DdsImageFile(ImageFile.ImageFile):
|
| 331 |
+
format = "DDS"
|
| 332 |
+
format_description = "DirectDraw Surface"
|
| 333 |
+
|
| 334 |
+
def _open(self) -> None:
|
| 335 |
+
assert self.fp is not 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 = self.fp.read(header_size - 4)
|
| 344 |
+
if len(header) != 120:
|
| 345 |
+
msg = f"Incomplete header: {len(header)} bytes"
|
| 346 |
+
raise OSError(msg)
|
| 347 |
+
|
| 348 |
+
flags, height, width = struct.unpack("<3I", header[:12])
|
| 349 |
+
self._size = (width, height)
|
| 350 |
+
extents = (0, 0) + self.size
|
| 351 |
+
|
| 352 |
+
pitch, depth, mipmaps = struct.unpack("<3I", header[12:24])
|
| 353 |
+
struct.unpack("<11I", header[24:68]) # reserved
|
| 354 |
+
|
| 355 |
+
# pixel format
|
| 356 |
+
pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header[68:84])
|
| 357 |
+
n = 0
|
| 358 |
+
rawmode = None
|
| 359 |
+
if pfflags & DDPF.RGB:
|
| 360 |
+
# Texture contains uncompressed RGB data
|
| 361 |
+
if pfflags & DDPF.ALPHAPIXELS:
|
| 362 |
+
self._mode = "RGBA"
|
| 363 |
+
mask_count = 4
|
| 364 |
+
else:
|
| 365 |
+
self._mode = "RGB"
|
| 366 |
+
mask_count = 3
|
| 367 |
+
|
| 368 |
+
masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4])
|
| 369 |
+
self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
|
| 370 |
+
return
|
| 371 |
+
elif pfflags & DDPF.LUMINANCE:
|
| 372 |
+
if bitcount == 8:
|
| 373 |
+
self._mode = "L"
|
| 374 |
+
elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
|
| 375 |
+
self._mode = "LA"
|
| 376 |
+
else:
|
| 377 |
+
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
|
| 378 |
+
raise OSError(msg)
|
| 379 |
+
elif pfflags & DDPF.PALETTEINDEXED8:
|
| 380 |
+
self._mode = "P"
|
| 381 |
+
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
| 382 |
+
self.palette.mode = "RGBA"
|
| 383 |
+
elif pfflags & DDPF.FOURCC:
|
| 384 |
+
offset = header_size + 4
|
| 385 |
+
if fourcc == D3DFMT.DXT1:
|
| 386 |
+
self._mode = "RGBA"
|
| 387 |
+
self.pixel_format = "DXT1"
|
| 388 |
+
n = 1
|
| 389 |
+
elif fourcc == D3DFMT.DXT3:
|
| 390 |
+
self._mode = "RGBA"
|
| 391 |
+
self.pixel_format = "DXT3"
|
| 392 |
+
n = 2
|
| 393 |
+
elif fourcc == D3DFMT.DXT5:
|
| 394 |
+
self._mode = "RGBA"
|
| 395 |
+
self.pixel_format = "DXT5"
|
| 396 |
+
n = 3
|
| 397 |
+
elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
|
| 398 |
+
self._mode = "L"
|
| 399 |
+
self.pixel_format = "BC4"
|
| 400 |
+
n = 4
|
| 401 |
+
elif fourcc == D3DFMT.BC5S:
|
| 402 |
+
self._mode = "RGB"
|
| 403 |
+
self.pixel_format = "BC5S"
|
| 404 |
+
n = 5
|
| 405 |
+
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
|
| 406 |
+
self._mode = "RGB"
|
| 407 |
+
self.pixel_format = "BC5"
|
| 408 |
+
n = 5
|
| 409 |
+
elif fourcc == D3DFMT.DX10:
|
| 410 |
+
offset += 20
|
| 411 |
+
# ignoring flags which pertain to volume textures and cubemaps
|
| 412 |
+
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
|
| 413 |
+
self.fp.read(16)
|
| 414 |
+
if dxgi_format in (
|
| 415 |
+
DXGI_FORMAT.BC1_UNORM,
|
| 416 |
+
DXGI_FORMAT.BC1_TYPELESS,
|
| 417 |
+
):
|
| 418 |
+
self._mode = "RGBA"
|
| 419 |
+
self.pixel_format = "BC1"
|
| 420 |
+
n = 1
|
| 421 |
+
elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM):
|
| 422 |
+
self._mode = "RGBA"
|
| 423 |
+
self.pixel_format = "BC2"
|
| 424 |
+
n = 2
|
| 425 |
+
elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM):
|
| 426 |
+
self._mode = "RGBA"
|
| 427 |
+
self.pixel_format = "BC3"
|
| 428 |
+
n = 3
|
| 429 |
+
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
|
| 430 |
+
self._mode = "L"
|
| 431 |
+
self.pixel_format = "BC4"
|
| 432 |
+
n = 4
|
| 433 |
+
elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
|
| 434 |
+
self._mode = "RGB"
|
| 435 |
+
self.pixel_format = "BC5"
|
| 436 |
+
n = 5
|
| 437 |
+
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
|
| 438 |
+
self._mode = "RGB"
|
| 439 |
+
self.pixel_format = "BC5S"
|
| 440 |
+
n = 5
|
| 441 |
+
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
|
| 442 |
+
self._mode = "RGB"
|
| 443 |
+
self.pixel_format = "BC6H"
|
| 444 |
+
n = 6
|
| 445 |
+
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
|
| 446 |
+
self._mode = "RGB"
|
| 447 |
+
self.pixel_format = "BC6HS"
|
| 448 |
+
n = 6
|
| 449 |
+
elif dxgi_format in (
|
| 450 |
+
DXGI_FORMAT.BC7_TYPELESS,
|
| 451 |
+
DXGI_FORMAT.BC7_UNORM,
|
| 452 |
+
DXGI_FORMAT.BC7_UNORM_SRGB,
|
| 453 |
+
):
|
| 454 |
+
self._mode = "RGBA"
|
| 455 |
+
self.pixel_format = "BC7"
|
| 456 |
+
n = 7
|
| 457 |
+
if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
|
| 458 |
+
self.info["gamma"] = 1 / 2.2
|
| 459 |
+
elif dxgi_format in (
|
| 460 |
+
DXGI_FORMAT.R8G8B8A8_TYPELESS,
|
| 461 |
+
DXGI_FORMAT.R8G8B8A8_UNORM,
|
| 462 |
+
DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
|
| 463 |
+
):
|
| 464 |
+
self._mode = "RGBA"
|
| 465 |
+
if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
|
| 466 |
+
self.info["gamma"] = 1 / 2.2
|
| 467 |
+
else:
|
| 468 |
+
msg = f"Unimplemented DXGI format {dxgi_format}"
|
| 469 |
+
raise NotImplementedError(msg)
|
| 470 |
+
else:
|
| 471 |
+
msg = f"Unimplemented pixel format {repr(fourcc)}"
|
| 472 |
+
raise NotImplementedError(msg)
|
| 473 |
+
else:
|
| 474 |
+
msg = f"Unknown pixel format flags {pfflags}"
|
| 475 |
+
raise NotImplementedError(msg)
|
| 476 |
+
|
| 477 |
+
if n:
|
| 478 |
+
self.tile = [
|
| 479 |
+
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
| 480 |
+
]
|
| 481 |
+
else:
|
| 482 |
+
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
|
| 483 |
+
|
| 484 |
+
def load_seek(self, pos: int) -> None:
|
| 485 |
+
pass
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
class DdsRgbDecoder(ImageFile.PyDecoder):
|
| 489 |
+
_pulls_fd = True
|
| 490 |
+
|
| 491 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 492 |
+
assert self.fd is not None
|
| 493 |
+
bitcount, masks = self.args
|
| 494 |
+
|
| 495 |
+
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
|
| 496 |
+
# Calculate how many zeros each mask is padded with
|
| 497 |
+
mask_offsets = []
|
| 498 |
+
# And the maximum value of each channel without the padding
|
| 499 |
+
mask_totals = []
|
| 500 |
+
for mask in masks:
|
| 501 |
+
offset = 0
|
| 502 |
+
if mask != 0:
|
| 503 |
+
while mask >> (offset + 1) << (offset + 1) == mask:
|
| 504 |
+
offset += 1
|
| 505 |
+
mask_offsets.append(offset)
|
| 506 |
+
mask_totals.append(mask >> offset)
|
| 507 |
+
|
| 508 |
+
data = bytearray()
|
| 509 |
+
bytecount = bitcount // 8
|
| 510 |
+
dest_length = self.state.xsize * self.state.ysize * len(masks)
|
| 511 |
+
while len(data) < dest_length:
|
| 512 |
+
value = int.from_bytes(self.fd.read(bytecount), "little")
|
| 513 |
+
for i, mask in enumerate(masks):
|
| 514 |
+
masked_value = value & mask
|
| 515 |
+
# Remove the zero padding, and scale it to 8 bits
|
| 516 |
+
data += o8(
|
| 517 |
+
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
|
| 518 |
+
if mask_totals[i]
|
| 519 |
+
else 0
|
| 520 |
+
)
|
| 521 |
+
self.set_as_raw(data)
|
| 522 |
+
return -1, 0
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 526 |
+
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
| 527 |
+
msg = f"cannot write mode {im.mode} as DDS"
|
| 528 |
+
raise OSError(msg)
|
| 529 |
+
|
| 530 |
+
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
|
| 531 |
+
bitcount = len(im.getbands()) * 8
|
| 532 |
+
pixel_format = im.encoderinfo.get("pixel_format")
|
| 533 |
+
args: tuple[int] | str
|
| 534 |
+
if pixel_format:
|
| 535 |
+
codec_name = "bcn"
|
| 536 |
+
flags |= DDSD.LINEARSIZE
|
| 537 |
+
pitch = (im.width + 3) * 4
|
| 538 |
+
rgba_mask = [0, 0, 0, 0]
|
| 539 |
+
pixel_flags = DDPF.FOURCC
|
| 540 |
+
if pixel_format == "DXT1":
|
| 541 |
+
fourcc = D3DFMT.DXT1
|
| 542 |
+
args = (1,)
|
| 543 |
+
elif pixel_format == "DXT3":
|
| 544 |
+
fourcc = D3DFMT.DXT3
|
| 545 |
+
args = (2,)
|
| 546 |
+
elif pixel_format == "DXT5":
|
| 547 |
+
fourcc = D3DFMT.DXT5
|
| 548 |
+
args = (3,)
|
| 549 |
+
else:
|
| 550 |
+
fourcc = D3DFMT.DX10
|
| 551 |
+
if pixel_format == "BC2":
|
| 552 |
+
args = (2,)
|
| 553 |
+
dxgi_format = DXGI_FORMAT.BC2_TYPELESS
|
| 554 |
+
elif pixel_format == "BC3":
|
| 555 |
+
args = (3,)
|
| 556 |
+
dxgi_format = DXGI_FORMAT.BC3_TYPELESS
|
| 557 |
+
elif pixel_format == "BC5":
|
| 558 |
+
args = (5,)
|
| 559 |
+
dxgi_format = DXGI_FORMAT.BC5_TYPELESS
|
| 560 |
+
if im.mode != "RGB":
|
| 561 |
+
msg = "only RGB mode can be written as BC5"
|
| 562 |
+
raise OSError(msg)
|
| 563 |
+
else:
|
| 564 |
+
msg = f"cannot write pixel format {pixel_format}"
|
| 565 |
+
raise OSError(msg)
|
| 566 |
+
else:
|
| 567 |
+
codec_name = "raw"
|
| 568 |
+
flags |= DDSD.PITCH
|
| 569 |
+
pitch = (im.width * bitcount + 7) // 8
|
| 570 |
+
|
| 571 |
+
alpha = im.mode[-1] == "A"
|
| 572 |
+
if im.mode[0] == "L":
|
| 573 |
+
pixel_flags = DDPF.LUMINANCE
|
| 574 |
+
args = im.mode
|
| 575 |
+
if alpha:
|
| 576 |
+
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
|
| 577 |
+
else:
|
| 578 |
+
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
|
| 579 |
+
else:
|
| 580 |
+
pixel_flags = DDPF.RGB
|
| 581 |
+
args = im.mode[::-1]
|
| 582 |
+
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
|
| 583 |
+
|
| 584 |
+
if alpha:
|
| 585 |
+
r, g, b, a = im.split()
|
| 586 |
+
im = Image.merge("RGBA", (a, r, g, b))
|
| 587 |
+
if alpha:
|
| 588 |
+
pixel_flags |= DDPF.ALPHAPIXELS
|
| 589 |
+
rgba_mask.append(0xFF000000 if alpha else 0)
|
| 590 |
+
|
| 591 |
+
fourcc = D3DFMT.UNKNOWN
|
| 592 |
+
fp.write(
|
| 593 |
+
o32(DDS_MAGIC)
|
| 594 |
+
+ struct.pack(
|
| 595 |
+
"<7I",
|
| 596 |
+
124, # header size
|
| 597 |
+
flags, # flags
|
| 598 |
+
im.height,
|
| 599 |
+
im.width,
|
| 600 |
+
pitch,
|
| 601 |
+
0, # depth
|
| 602 |
+
0, # mipmaps
|
| 603 |
+
)
|
| 604 |
+
+ struct.pack("11I", *((0,) * 11)) # reserved
|
| 605 |
+
# pfsize, pfflags, fourcc, bitcount
|
| 606 |
+
+ struct.pack("<4I", 32, pixel_flags, fourcc, bitcount)
|
| 607 |
+
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
|
| 608 |
+
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
|
| 609 |
+
)
|
| 610 |
+
if fourcc == D3DFMT.DX10:
|
| 611 |
+
fp.write(
|
| 612 |
+
# dxgi_format, 2D resource, misc, array size, straight alpha
|
| 613 |
+
struct.pack("<5I", dxgi_format, 3, 0, 0, 1)
|
| 614 |
+
)
|
| 615 |
+
ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)])
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
def _accept(prefix: bytes) -> bool:
|
| 619 |
+
return prefix.startswith(b"DDS ")
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
| 623 |
+
Image.register_decoder("dds_rgb", DdsRgbDecoder)
|
| 624 |
+
Image.register_save(DdsImageFile.format, _save)
|
| 625 |
+
Image.register_extension(DdsImageFile.format, ".dds")
|
PIL/EpsImagePlugin.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.startswith(b"%!PS") or (
|
| 174 |
+
len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
##
|
| 179 |
+
# Image plugin for Encapsulated PostScript. This plugin supports only
|
| 180 |
+
# a few variants of this format.
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class EpsImageFile(ImageFile.ImageFile):
|
| 184 |
+
"""EPS File Parser for the Python Imaging Library"""
|
| 185 |
+
|
| 186 |
+
format = "EPS"
|
| 187 |
+
format_description = "Encapsulated Postscript"
|
| 188 |
+
|
| 189 |
+
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
|
| 190 |
+
|
| 191 |
+
def _open(self) -> None:
|
| 192 |
+
assert self.fp is not None
|
| 193 |
+
(length, offset) = self._find_offset(self.fp)
|
| 194 |
+
|
| 195 |
+
# go to offset - start of "%!PS"
|
| 196 |
+
self.fp.seek(offset)
|
| 197 |
+
|
| 198 |
+
self._mode = "RGB"
|
| 199 |
+
|
| 200 |
+
# When reading header comments, the first comment is used.
|
| 201 |
+
# When reading trailer comments, the last comment is used.
|
| 202 |
+
bounding_box: list[int] | None = None
|
| 203 |
+
imagedata_size: tuple[int, int] | None = None
|
| 204 |
+
|
| 205 |
+
byte_arr = bytearray(255)
|
| 206 |
+
bytes_mv = memoryview(byte_arr)
|
| 207 |
+
bytes_read = 0
|
| 208 |
+
reading_header_comments = True
|
| 209 |
+
reading_trailer_comments = False
|
| 210 |
+
trailer_reached = False
|
| 211 |
+
|
| 212 |
+
def check_required_header_comments() -> None:
|
| 213 |
+
"""
|
| 214 |
+
The EPS specification requires that some headers exist.
|
| 215 |
+
This should be checked when the header comments formally end,
|
| 216 |
+
when image data starts, or when the file ends, whichever comes first.
|
| 217 |
+
"""
|
| 218 |
+
if "PS-Adobe" not in self.info:
|
| 219 |
+
msg = 'EPS header missing "%!PS-Adobe" comment'
|
| 220 |
+
raise SyntaxError(msg)
|
| 221 |
+
if "BoundingBox" not in self.info:
|
| 222 |
+
msg = 'EPS header missing "%%BoundingBox" comment'
|
| 223 |
+
raise SyntaxError(msg)
|
| 224 |
+
|
| 225 |
+
def read_comment(s: str) -> bool:
|
| 226 |
+
nonlocal bounding_box, reading_trailer_comments
|
| 227 |
+
try:
|
| 228 |
+
m = split.match(s)
|
| 229 |
+
except re.error as e:
|
| 230 |
+
msg = "not an EPS file"
|
| 231 |
+
raise SyntaxError(msg) from e
|
| 232 |
+
|
| 233 |
+
if not m:
|
| 234 |
+
return False
|
| 235 |
+
|
| 236 |
+
k, v = m.group(1, 2)
|
| 237 |
+
self.info[k] = v
|
| 238 |
+
if k == "BoundingBox":
|
| 239 |
+
if v == "(atend)":
|
| 240 |
+
reading_trailer_comments = True
|
| 241 |
+
elif not bounding_box or (trailer_reached and reading_trailer_comments):
|
| 242 |
+
try:
|
| 243 |
+
# Note: The DSC spec says that BoundingBox
|
| 244 |
+
# fields should be integers, but some drivers
|
| 245 |
+
# put floating point values there anyway.
|
| 246 |
+
bounding_box = [int(float(i)) for i in v.split()]
|
| 247 |
+
except Exception:
|
| 248 |
+
pass
|
| 249 |
+
return True
|
| 250 |
+
|
| 251 |
+
while True:
|
| 252 |
+
byte = self.fp.read(1)
|
| 253 |
+
if byte == b"":
|
| 254 |
+
# if we didn't read a byte we must be at the end of the file
|
| 255 |
+
if bytes_read == 0:
|
| 256 |
+
if reading_header_comments:
|
| 257 |
+
check_required_header_comments()
|
| 258 |
+
break
|
| 259 |
+
elif byte in b"\r\n":
|
| 260 |
+
# if we read a line ending character, ignore it and parse what
|
| 261 |
+
# we have already read. if we haven't read any other characters,
|
| 262 |
+
# continue reading
|
| 263 |
+
if bytes_read == 0:
|
| 264 |
+
continue
|
| 265 |
+
else:
|
| 266 |
+
# ASCII/hexadecimal lines in an EPS file must not exceed
|
| 267 |
+
# 255 characters, not including line ending characters
|
| 268 |
+
if bytes_read >= 255:
|
| 269 |
+
# only enforce this for lines starting with a "%",
|
| 270 |
+
# otherwise assume it's binary data
|
| 271 |
+
if byte_arr[0] == ord("%"):
|
| 272 |
+
msg = "not an EPS file"
|
| 273 |
+
raise SyntaxError(msg)
|
| 274 |
+
else:
|
| 275 |
+
if reading_header_comments:
|
| 276 |
+
check_required_header_comments()
|
| 277 |
+
reading_header_comments = False
|
| 278 |
+
# reset bytes_read so we can keep reading
|
| 279 |
+
# data until the end of the line
|
| 280 |
+
bytes_read = 0
|
| 281 |
+
byte_arr[bytes_read] = byte[0]
|
| 282 |
+
bytes_read += 1
|
| 283 |
+
continue
|
| 284 |
+
|
| 285 |
+
if reading_header_comments:
|
| 286 |
+
# Load EPS header
|
| 287 |
+
|
| 288 |
+
# if this line doesn't start with a "%",
|
| 289 |
+
# or does start with "%%EndComments",
|
| 290 |
+
# then we've reached the end of the header/comments
|
| 291 |
+
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
| 292 |
+
check_required_header_comments()
|
| 293 |
+
reading_header_comments = False
|
| 294 |
+
continue
|
| 295 |
+
|
| 296 |
+
s = str(bytes_mv[:bytes_read], "latin-1")
|
| 297 |
+
if not read_comment(s):
|
| 298 |
+
m = field.match(s)
|
| 299 |
+
if m:
|
| 300 |
+
k = m.group(1)
|
| 301 |
+
if k.startswith("PS-Adobe"):
|
| 302 |
+
self.info["PS-Adobe"] = k[9:]
|
| 303 |
+
else:
|
| 304 |
+
self.info[k] = ""
|
| 305 |
+
elif s[0] == "%":
|
| 306 |
+
# handle non-DSC PostScript comments that some
|
| 307 |
+
# tools mistakenly put in the Comments section
|
| 308 |
+
pass
|
| 309 |
+
else:
|
| 310 |
+
msg = "bad EPS header"
|
| 311 |
+
raise OSError(msg)
|
| 312 |
+
elif bytes_mv[:11] == b"%ImageData:":
|
| 313 |
+
# Check for an "ImageData" descriptor
|
| 314 |
+
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
|
| 315 |
+
|
| 316 |
+
# If we've already read an "ImageData" descriptor,
|
| 317 |
+
# don't read another one.
|
| 318 |
+
if imagedata_size:
|
| 319 |
+
bytes_read = 0
|
| 320 |
+
continue
|
| 321 |
+
|
| 322 |
+
# Values:
|
| 323 |
+
# columns
|
| 324 |
+
# rows
|
| 325 |
+
# bit depth (1 or 8)
|
| 326 |
+
# mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
|
| 327 |
+
# number of padding channels
|
| 328 |
+
# block size (number of bytes per row per channel)
|
| 329 |
+
# binary/ascii (1: binary, 2: ascii)
|
| 330 |
+
# data start identifier (the image data follows after a single line
|
| 331 |
+
# consisting only of this quoted value)
|
| 332 |
+
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
| 333 |
+
columns, rows, bit_depth, mode_id = (
|
| 334 |
+
int(value) for value in image_data_values[:4]
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
if bit_depth == 1:
|
| 338 |
+
self._mode = "1"
|
| 339 |
+
elif bit_depth == 8:
|
| 340 |
+
try:
|
| 341 |
+
self._mode = self.mode_map[mode_id]
|
| 342 |
+
except ValueError:
|
| 343 |
+
break
|
| 344 |
+
else:
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
# Parse the columns and rows after checking the bit depth and mode
|
| 348 |
+
# in case the bit depth and/or mode are invalid.
|
| 349 |
+
imagedata_size = columns, rows
|
| 350 |
+
elif bytes_mv[:5] == b"%%EOF":
|
| 351 |
+
break
|
| 352 |
+
elif trailer_reached and reading_trailer_comments:
|
| 353 |
+
# Load EPS trailer
|
| 354 |
+
s = str(bytes_mv[:bytes_read], "latin-1")
|
| 355 |
+
read_comment(s)
|
| 356 |
+
elif bytes_mv[:9] == b"%%Trailer":
|
| 357 |
+
trailer_reached = True
|
| 358 |
+
elif bytes_mv[:14] == b"%%BeginBinary:":
|
| 359 |
+
bytecount = int(byte_arr[14:bytes_read])
|
| 360 |
+
self.fp.seek(bytecount, os.SEEK_CUR)
|
| 361 |
+
bytes_read = 0
|
| 362 |
+
|
| 363 |
+
# A "BoundingBox" is always required,
|
| 364 |
+
# even if an "ImageData" descriptor size exists.
|
| 365 |
+
if not bounding_box:
|
| 366 |
+
msg = "cannot determine EPS bounding box"
|
| 367 |
+
raise OSError(msg)
|
| 368 |
+
|
| 369 |
+
# An "ImageData" size takes precedence over the "BoundingBox".
|
| 370 |
+
self._size = imagedata_size or (
|
| 371 |
+
bounding_box[2] - bounding_box[0],
|
| 372 |
+
bounding_box[3] - bounding_box[1],
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
self.tile = [
|
| 376 |
+
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
|
| 377 |
+
]
|
| 378 |
+
|
| 379 |
+
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
|
| 380 |
+
s = fp.read(4)
|
| 381 |
+
|
| 382 |
+
if s == b"%!PS":
|
| 383 |
+
# for HEAD without binary preview
|
| 384 |
+
fp.seek(0, io.SEEK_END)
|
| 385 |
+
length = fp.tell()
|
| 386 |
+
offset = 0
|
| 387 |
+
elif i32(s) == 0xC6D3D0C5:
|
| 388 |
+
# FIX for: Some EPS file not handled correctly / issue #302
|
| 389 |
+
# EPS can contain binary data
|
| 390 |
+
# or start directly with latin coding
|
| 391 |
+
# more info see:
|
| 392 |
+
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
| 393 |
+
s = fp.read(8)
|
| 394 |
+
offset = i32(s)
|
| 395 |
+
length = i32(s, 4)
|
| 396 |
+
else:
|
| 397 |
+
msg = "not an EPS file"
|
| 398 |
+
raise SyntaxError(msg)
|
| 399 |
+
|
| 400 |
+
return length, offset
|
| 401 |
+
|
| 402 |
+
def load(
|
| 403 |
+
self, scale: int = 1, transparency: bool = False
|
| 404 |
+
) -> Image.core.PixelAccess | None:
|
| 405 |
+
# Load EPS via Ghostscript
|
| 406 |
+
if self.tile:
|
| 407 |
+
assert self.fp is not None
|
| 408 |
+
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
| 409 |
+
self._mode = self.im.mode
|
| 410 |
+
self._size = self.im.size
|
| 411 |
+
self.tile = []
|
| 412 |
+
return Image.Image.load(self)
|
| 413 |
+
|
| 414 |
+
def load_seek(self, pos: int) -> None:
|
| 415 |
+
# we can't incrementally load, so force ImageFile.parser to
|
| 416 |
+
# use our custom load method by defining this method.
|
| 417 |
+
pass
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
# --------------------------------------------------------------------
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
|
| 424 |
+
"""EPS Writer for the Python Imaging Library."""
|
| 425 |
+
|
| 426 |
+
# make sure image data is available
|
| 427 |
+
im.load()
|
| 428 |
+
|
| 429 |
+
# determine PostScript image mode
|
| 430 |
+
if im.mode == "L":
|
| 431 |
+
operator = (8, 1, b"image")
|
| 432 |
+
elif im.mode == "RGB":
|
| 433 |
+
operator = (8, 3, b"false 3 colorimage")
|
| 434 |
+
elif im.mode == "CMYK":
|
| 435 |
+
operator = (8, 4, b"false 4 colorimage")
|
| 436 |
+
else:
|
| 437 |
+
msg = "image mode is not supported"
|
| 438 |
+
raise ValueError(msg)
|
| 439 |
+
|
| 440 |
+
if eps:
|
| 441 |
+
# write EPS header
|
| 442 |
+
fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
|
| 443 |
+
fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
|
| 444 |
+
# fp.write("%%CreationDate: %s"...)
|
| 445 |
+
fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
|
| 446 |
+
fp.write(b"%%Pages: 1\n")
|
| 447 |
+
fp.write(b"%%EndComments\n")
|
| 448 |
+
fp.write(b"%%Page: 1 1\n")
|
| 449 |
+
fp.write(b"%%ImageData: %d %d " % im.size)
|
| 450 |
+
fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
|
| 451 |
+
|
| 452 |
+
# image header
|
| 453 |
+
fp.write(b"gsave\n")
|
| 454 |
+
fp.write(b"10 dict begin\n")
|
| 455 |
+
fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
|
| 456 |
+
fp.write(b"%d %d scale\n" % im.size)
|
| 457 |
+
fp.write(b"%d %d 8\n" % im.size) # <= bits
|
| 458 |
+
fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
|
| 459 |
+
fp.write(b"{ currentfile buf readhexstring pop } bind\n")
|
| 460 |
+
fp.write(operator[2] + b"\n")
|
| 461 |
+
if hasattr(fp, "flush"):
|
| 462 |
+
fp.flush()
|
| 463 |
+
|
| 464 |
+
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
|
| 465 |
+
|
| 466 |
+
fp.write(b"\n%%%%EndBinary\n")
|
| 467 |
+
fp.write(b"grestore end\n")
|
| 468 |
+
if hasattr(fp, "flush"):
|
| 469 |
+
fp.flush()
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
# --------------------------------------------------------------------
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
|
| 476 |
+
|
| 477 |
+
Image.register_save(EpsImageFile.format, _save)
|
| 478 |
+
|
| 479 |
+
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
|
| 480 |
+
|
| 481 |
+
Image.register_mime(EpsImageFile.format, "application/postscript")
|
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.startswith(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"])
|
PIL/FliImagePlugin.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
from ._util import DeferredError
|
| 26 |
+
|
| 27 |
+
#
|
| 28 |
+
# decoder
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _accept(prefix: bytes) -> bool:
|
| 32 |
+
return (
|
| 33 |
+
len(prefix) >= 16
|
| 34 |
+
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
| 35 |
+
and i16(prefix, 14) in [0, 3] # flags
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
##
|
| 40 |
+
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
|
| 41 |
+
# method to load individual frames.
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class FliImageFile(ImageFile.ImageFile):
|
| 45 |
+
format = "FLI"
|
| 46 |
+
format_description = "Autodesk FLI/FLC Animation"
|
| 47 |
+
_close_exclusive_fp_after_loading = False
|
| 48 |
+
|
| 49 |
+
def _open(self) -> None:
|
| 50 |
+
# HEAD
|
| 51 |
+
assert self.fp is not None
|
| 52 |
+
s = self.fp.read(128)
|
| 53 |
+
if not (
|
| 54 |
+
_accept(s)
|
| 55 |
+
and s[20:22] == b"\x00" * 2
|
| 56 |
+
and s[42:80] == b"\x00" * 38
|
| 57 |
+
and s[88:] == b"\x00" * 40
|
| 58 |
+
):
|
| 59 |
+
msg = "not an FLI/FLC file"
|
| 60 |
+
raise SyntaxError(msg)
|
| 61 |
+
|
| 62 |
+
# frames
|
| 63 |
+
self.n_frames = i16(s, 6)
|
| 64 |
+
self.is_animated = self.n_frames > 1
|
| 65 |
+
|
| 66 |
+
# image characteristics
|
| 67 |
+
self._mode = "P"
|
| 68 |
+
self._size = i16(s, 8), i16(s, 10)
|
| 69 |
+
|
| 70 |
+
# animation speed
|
| 71 |
+
duration = i32(s, 16)
|
| 72 |
+
magic = i16(s, 4)
|
| 73 |
+
if magic == 0xAF11:
|
| 74 |
+
duration = (duration * 1000) // 70
|
| 75 |
+
self.info["duration"] = duration
|
| 76 |
+
|
| 77 |
+
# look for palette
|
| 78 |
+
palette = [(a, a, a) for a in range(256)]
|
| 79 |
+
|
| 80 |
+
s = self.fp.read(16)
|
| 81 |
+
|
| 82 |
+
self.__offset = 128
|
| 83 |
+
|
| 84 |
+
if i16(s, 4) == 0xF100:
|
| 85 |
+
# prefix chunk; ignore it
|
| 86 |
+
self.fp.seek(self.__offset + i32(s))
|
| 87 |
+
s = self.fp.read(16)
|
| 88 |
+
|
| 89 |
+
if i16(s, 4) == 0xF1FA:
|
| 90 |
+
# look for palette chunk
|
| 91 |
+
number_of_subchunks = i16(s, 6)
|
| 92 |
+
chunk_size: int | None = None
|
| 93 |
+
for _ in range(number_of_subchunks):
|
| 94 |
+
if chunk_size is not None:
|
| 95 |
+
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
|
| 96 |
+
s = self.fp.read(6)
|
| 97 |
+
chunk_type = i16(s, 4)
|
| 98 |
+
if chunk_type in (4, 11):
|
| 99 |
+
self._palette(palette, 2 if chunk_type == 11 else 0)
|
| 100 |
+
break
|
| 101 |
+
chunk_size = i32(s)
|
| 102 |
+
if not chunk_size:
|
| 103 |
+
break
|
| 104 |
+
|
| 105 |
+
self.palette = ImagePalette.raw(
|
| 106 |
+
"RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# set things up to decode first frame
|
| 110 |
+
self.__frame = -1
|
| 111 |
+
self._fp = self.fp
|
| 112 |
+
self.__rewind = self.fp.tell()
|
| 113 |
+
self.seek(0)
|
| 114 |
+
|
| 115 |
+
def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
|
| 116 |
+
# load palette
|
| 117 |
+
|
| 118 |
+
i = 0
|
| 119 |
+
assert self.fp is not None
|
| 120 |
+
for e in range(i16(self.fp.read(2))):
|
| 121 |
+
s = self.fp.read(2)
|
| 122 |
+
i = i + s[0]
|
| 123 |
+
n = s[1]
|
| 124 |
+
if n == 0:
|
| 125 |
+
n = 256
|
| 126 |
+
s = self.fp.read(n * 3)
|
| 127 |
+
for n in range(0, len(s), 3):
|
| 128 |
+
r = s[n] << shift
|
| 129 |
+
g = s[n + 1] << shift
|
| 130 |
+
b = s[n + 2] << shift
|
| 131 |
+
palette[i] = (r, g, b)
|
| 132 |
+
i += 1
|
| 133 |
+
|
| 134 |
+
def seek(self, frame: int) -> None:
|
| 135 |
+
if not self._seek_check(frame):
|
| 136 |
+
return
|
| 137 |
+
if frame < self.__frame:
|
| 138 |
+
self._seek(0)
|
| 139 |
+
|
| 140 |
+
for f in range(self.__frame + 1, frame + 1):
|
| 141 |
+
self._seek(f)
|
| 142 |
+
|
| 143 |
+
def _seek(self, frame: int) -> None:
|
| 144 |
+
if isinstance(self._fp, DeferredError):
|
| 145 |
+
raise self._fp.ex
|
| 146 |
+
if frame == 0:
|
| 147 |
+
self.__frame = -1
|
| 148 |
+
self._fp.seek(self.__rewind)
|
| 149 |
+
self.__offset = 128
|
| 150 |
+
else:
|
| 151 |
+
# ensure that the previous frame was loaded
|
| 152 |
+
self.load()
|
| 153 |
+
|
| 154 |
+
if frame != self.__frame + 1:
|
| 155 |
+
msg = f"cannot seek to frame {frame}"
|
| 156 |
+
raise ValueError(msg)
|
| 157 |
+
self.__frame = frame
|
| 158 |
+
|
| 159 |
+
# move to next frame
|
| 160 |
+
self.fp = self._fp
|
| 161 |
+
self.fp.seek(self.__offset)
|
| 162 |
+
|
| 163 |
+
s = self.fp.read(4)
|
| 164 |
+
if not s:
|
| 165 |
+
msg = "missing frame size"
|
| 166 |
+
raise EOFError(msg)
|
| 167 |
+
|
| 168 |
+
framesize = i32(s)
|
| 169 |
+
|
| 170 |
+
self.decodermaxblock = framesize
|
| 171 |
+
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]
|
| 172 |
+
|
| 173 |
+
self.__offset += framesize
|
| 174 |
+
|
| 175 |
+
def tell(self) -> int:
|
| 176 |
+
return self.__frame
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
#
|
| 180 |
+
# registry
|
| 181 |
+
|
| 182 |
+
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
| 183 |
+
|
| 184 |
+
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
|
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 |
+
assert self.fp is not None
|
| 76 |
+
if not _accept(self.fp.read(4)):
|
| 77 |
+
msg = "not an FTEX file"
|
| 78 |
+
raise SyntaxError(msg)
|
| 79 |
+
struct.unpack("<i", self.fp.read(4)) # version
|
| 80 |
+
self._size = struct.unpack("<2i", self.fp.read(8))
|
| 81 |
+
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
| 82 |
+
|
| 83 |
+
# Only support single-format files.
|
| 84 |
+
# I don't know of any multi-format file.
|
| 85 |
+
assert format_count == 1
|
| 86 |
+
|
| 87 |
+
format, where = struct.unpack("<2i", self.fp.read(8))
|
| 88 |
+
self.fp.seek(where)
|
| 89 |
+
(mipmap_size,) = struct.unpack("<i", self.fp.read(4))
|
| 90 |
+
|
| 91 |
+
data = self.fp.read(mipmap_size)
|
| 92 |
+
|
| 93 |
+
if format == Format.DXT1:
|
| 94 |
+
self._mode = "RGBA"
|
| 95 |
+
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
|
| 96 |
+
elif format == Format.UNCOMPRESSED:
|
| 97 |
+
self._mode = "RGB"
|
| 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.startswith(MAGIC)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
|
| 115 |
+
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])
|
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 |
+
assert self.fp is not None
|
| 46 |
+
header_size = i32(self.fp.read(4))
|
| 47 |
+
if header_size < 20:
|
| 48 |
+
msg = "not a GIMP brush"
|
| 49 |
+
raise SyntaxError(msg)
|
| 50 |
+
version = i32(self.fp.read(4))
|
| 51 |
+
if version not in (1, 2):
|
| 52 |
+
msg = f"Unsupported GIMP brush version: {version}"
|
| 53 |
+
raise SyntaxError(msg)
|
| 54 |
+
|
| 55 |
+
width = i32(self.fp.read(4))
|
| 56 |
+
height = i32(self.fp.read(4))
|
| 57 |
+
color_depth = i32(self.fp.read(4))
|
| 58 |
+
if width == 0 or height == 0:
|
| 59 |
+
msg = "not a GIMP brush"
|
| 60 |
+
raise SyntaxError(msg)
|
| 61 |
+
if color_depth not in (1, 4):
|
| 62 |
+
msg = f"Unsupported GIMP brush color depth: {color_depth}"
|
| 63 |
+
raise SyntaxError(msg)
|
| 64 |
+
|
| 65 |
+
if version == 1:
|
| 66 |
+
comment_length = header_size - 20
|
| 67 |
+
else:
|
| 68 |
+
comment_length = header_size - 28
|
| 69 |
+
magic_number = self.fp.read(4)
|
| 70 |
+
if magic_number != b"GIMP":
|
| 71 |
+
msg = "not a GIMP brush, bad magic number"
|
| 72 |
+
raise SyntaxError(msg)
|
| 73 |
+
self.info["spacing"] = i32(self.fp.read(4))
|
| 74 |
+
|
| 75 |
+
self.info["comment"] = self.fp.read(comment_length)[:-1]
|
| 76 |
+
|
| 77 |
+
if color_depth == 1:
|
| 78 |
+
self._mode = "L"
|
| 79 |
+
else:
|
| 80 |
+
self._mode = "RGBA"
|
| 81 |
+
|
| 82 |
+
self._size = width, height
|
| 83 |
+
|
| 84 |
+
# Image might not be small
|
| 85 |
+
Image._decompression_bomb_check(self.size)
|
| 86 |
+
|
| 87 |
+
# Data is an uncompressed block of w * h * bytes/pixel
|
| 88 |
+
self._data_size = width * height * color_depth
|
| 89 |
+
|
| 90 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 91 |
+
if self._im is None:
|
| 92 |
+
assert self.fp is not 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")
|
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 = "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 |
+
"RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4]
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.tile = [
|
| 75 |
+
ImageFile._Tile(
|
| 76 |
+
"raw",
|
| 77 |
+
(0, 0) + self.size,
|
| 78 |
+
7 + true_color_offset + 6 + 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
|
PIL/GimpGradientFile.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 25 |
+
from ._binary import o8
|
| 26 |
+
|
| 27 |
+
TYPE_CHECKING = False
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from collections.abc import Callable
|
| 30 |
+
from typing import IO
|
| 31 |
+
|
| 32 |
+
EPSILON = 1e-10
|
| 33 |
+
"""""" # Enable auto-doc for data member
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def linear(middle: float, pos: float) -> float:
|
| 37 |
+
if pos <= middle:
|
| 38 |
+
if middle < EPSILON:
|
| 39 |
+
return 0.0
|
| 40 |
+
else:
|
| 41 |
+
return 0.5 * pos / middle
|
| 42 |
+
else:
|
| 43 |
+
pos = pos - middle
|
| 44 |
+
middle = 1.0 - middle
|
| 45 |
+
if middle < EPSILON:
|
| 46 |
+
return 1.0
|
| 47 |
+
else:
|
| 48 |
+
return 0.5 + 0.5 * pos / middle
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def curved(middle: float, pos: float) -> float:
|
| 52 |
+
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def sine(middle: float, pos: float) -> float:
|
| 56 |
+
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def sphere_increasing(middle: float, pos: float) -> float:
|
| 60 |
+
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def sphere_decreasing(middle: float, pos: float) -> float:
|
| 64 |
+
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
| 68 |
+
"""""" # Enable auto-doc for data member
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class GradientFile:
|
| 72 |
+
gradient: (
|
| 73 |
+
list[
|
| 74 |
+
tuple[
|
| 75 |
+
float,
|
| 76 |
+
float,
|
| 77 |
+
float,
|
| 78 |
+
list[float],
|
| 79 |
+
list[float],
|
| 80 |
+
Callable[[float, float], float],
|
| 81 |
+
]
|
| 82 |
+
]
|
| 83 |
+
| None
|
| 84 |
+
) = None
|
| 85 |
+
|
| 86 |
+
def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
|
| 87 |
+
assert self.gradient is not None
|
| 88 |
+
palette = []
|
| 89 |
+
|
| 90 |
+
ix = 0
|
| 91 |
+
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
| 92 |
+
|
| 93 |
+
for i in range(entries):
|
| 94 |
+
x = i / (entries - 1)
|
| 95 |
+
|
| 96 |
+
while x1 < x:
|
| 97 |
+
ix += 1
|
| 98 |
+
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
| 99 |
+
|
| 100 |
+
w = x1 - x0
|
| 101 |
+
|
| 102 |
+
if w < EPSILON:
|
| 103 |
+
scale = segment(0.5, 0.5)
|
| 104 |
+
else:
|
| 105 |
+
scale = segment((xm - x0) / w, (x - x0) / w)
|
| 106 |
+
|
| 107 |
+
# expand to RGBA
|
| 108 |
+
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
|
| 109 |
+
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
|
| 110 |
+
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
|
| 111 |
+
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
|
| 112 |
+
|
| 113 |
+
# add to palette
|
| 114 |
+
palette.append(r + g + b + a)
|
| 115 |
+
|
| 116 |
+
return b"".join(palette), "RGBA"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class GimpGradientFile(GradientFile):
|
| 120 |
+
"""File handler for GIMP's gradient format."""
|
| 121 |
+
|
| 122 |
+
def __init__(self, fp: IO[bytes]) -> None:
|
| 123 |
+
if not fp.readline().startswith(b"GIMP Gradient"):
|
| 124 |
+
msg = "not a GIMP gradient file"
|
| 125 |
+
raise SyntaxError(msg)
|
| 126 |
+
|
| 127 |
+
line = fp.readline()
|
| 128 |
+
|
| 129 |
+
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
|
| 130 |
+
if line.startswith(b"Name: "):
|
| 131 |
+
line = fp.readline().strip()
|
| 132 |
+
|
| 133 |
+
count = int(line)
|
| 134 |
+
|
| 135 |
+
self.gradient = []
|
| 136 |
+
|
| 137 |
+
for i in range(count):
|
| 138 |
+
s = fp.readline().split()
|
| 139 |
+
w = [float(x) for x in s[:11]]
|
| 140 |
+
|
| 141 |
+
x0, x1 = w[0], w[2]
|
| 142 |
+
xm = w[1]
|
| 143 |
+
rgb0 = w[3:7]
|
| 144 |
+
rgb1 = w[7:11]
|
| 145 |
+
|
| 146 |
+
segment = SEGMENTS[int(s[11])]
|
| 147 |
+
cspace = int(s[12])
|
| 148 |
+
|
| 149 |
+
if cspace != 0:
|
| 150 |
+
msg = "cannot handle HSV colour space"
|
| 151 |
+
raise OSError(msg)
|
| 152 |
+
|
| 153 |
+
self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
PIL/GimpPaletteFile.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 io import BytesIO
|
| 20 |
+
|
| 21 |
+
TYPE_CHECKING = False
|
| 22 |
+
if TYPE_CHECKING:
|
| 23 |
+
from typing import IO
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class GimpPaletteFile:
|
| 27 |
+
"""File handler for GIMP's palette format."""
|
| 28 |
+
|
| 29 |
+
rawmode = "RGB"
|
| 30 |
+
|
| 31 |
+
def _read(self, fp: IO[bytes], limit: bool = True) -> None:
|
| 32 |
+
if not fp.readline().startswith(b"GIMP Palette"):
|
| 33 |
+
msg = "not a GIMP palette file"
|
| 34 |
+
raise SyntaxError(msg)
|
| 35 |
+
|
| 36 |
+
palette: list[int] = []
|
| 37 |
+
i = 0
|
| 38 |
+
while True:
|
| 39 |
+
if limit and i == 256 + 3:
|
| 40 |
+
break
|
| 41 |
+
|
| 42 |
+
i += 1
|
| 43 |
+
s = fp.readline()
|
| 44 |
+
if not s:
|
| 45 |
+
break
|
| 46 |
+
|
| 47 |
+
# skip fields and comment lines
|
| 48 |
+
if re.match(rb"\w+:|#", s):
|
| 49 |
+
continue
|
| 50 |
+
if limit and len(s) > 100:
|
| 51 |
+
msg = "bad palette file"
|
| 52 |
+
raise SyntaxError(msg)
|
| 53 |
+
|
| 54 |
+
v = s.split(maxsplit=3)
|
| 55 |
+
if len(v) < 3:
|
| 56 |
+
msg = "bad palette entry"
|
| 57 |
+
raise ValueError(msg)
|
| 58 |
+
|
| 59 |
+
palette += (int(v[i]) for i in range(3))
|
| 60 |
+
if limit and len(palette) == 768:
|
| 61 |
+
break
|
| 62 |
+
|
| 63 |
+
self.palette = bytes(palette)
|
| 64 |
+
|
| 65 |
+
def __init__(self, fp: IO[bytes]) -> None:
|
| 66 |
+
self._read(fp)
|
| 67 |
+
|
| 68 |
+
@classmethod
|
| 69 |
+
def frombytes(cls, data: bytes) -> GimpPaletteFile:
|
| 70 |
+
self = cls.__new__(cls)
|
| 71 |
+
self._read(BytesIO(data), False)
|
| 72 |
+
return self
|
| 73 |
+
|
| 74 |
+
def getpalette(self) -> tuple[bytes, str]:
|
| 75 |
+
return self.palette, self.rawmode
|
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 |
+
import os
|
| 14 |
+
from typing import IO
|
| 15 |
+
|
| 16 |
+
from . import Image, ImageFile
|
| 17 |
+
|
| 18 |
+
_handler = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
| 22 |
+
"""
|
| 23 |
+
Install application-specific GRIB image handler.
|
| 24 |
+
|
| 25 |
+
:param handler: Handler object.
|
| 26 |
+
"""
|
| 27 |
+
global _handler
|
| 28 |
+
_handler = handler
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# --------------------------------------------------------------------
|
| 32 |
+
# Image adapter
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _accept(prefix: bytes) -> bool:
|
| 36 |
+
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class GribStubImageFile(ImageFile.StubImageFile):
|
| 40 |
+
format = "GRIB"
|
| 41 |
+
format_description = "GRIB"
|
| 42 |
+
|
| 43 |
+
def _open(self) -> None:
|
| 44 |
+
assert self.fp is not None
|
| 45 |
+
if not _accept(self.fp.read(8)):
|
| 46 |
+
msg = "Not a GRIB file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
self.fp.seek(-8, os.SEEK_CUR)
|
| 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")
|
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 |
+
import os
|
| 14 |
+
from typing import IO
|
| 15 |
+
|
| 16 |
+
from . import Image, ImageFile
|
| 17 |
+
|
| 18 |
+
_handler = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
| 22 |
+
"""
|
| 23 |
+
Install application-specific HDF5 image handler.
|
| 24 |
+
|
| 25 |
+
:param handler: Handler object.
|
| 26 |
+
"""
|
| 27 |
+
global _handler
|
| 28 |
+
_handler = handler
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# --------------------------------------------------------------------
|
| 32 |
+
# Image adapter
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _accept(prefix: bytes) -> bool:
|
| 36 |
+
return prefix.startswith(b"\x89HDF\r\n\x1a\n")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class HDF5StubImageFile(ImageFile.StubImageFile):
|
| 40 |
+
format = "HDF5"
|
| 41 |
+
format_description = "HDF5"
|
| 42 |
+
|
| 43 |
+
def _open(self) -> None:
|
| 44 |
+
assert self.fp is not None
|
| 45 |
+
if not _accept(self.fp.read(8)):
|
| 46 |
+
msg = "Not an HDF file"
|
| 47 |
+
raise SyntaxError(msg)
|
| 48 |
+
|
| 49 |
+
self.fp.seek(-8, os.SEEK_CUR)
|
| 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"])
|
PIL/IcnsImagePlugin.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 29 |
+
enable_jpeg2k = features.check_codec("jpg_2000")
|
| 30 |
+
if enable_jpeg2k:
|
| 31 |
+
from . import Jpeg2KImagePlugin
|
| 32 |
+
|
| 33 |
+
MAGIC = b"icns"
|
| 34 |
+
HEADERSIZE = 8
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
|
| 38 |
+
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def read_32t(
|
| 42 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 43 |
+
) -> dict[str, Image.Image]:
|
| 44 |
+
# The 128x128 icon seems to have an extra header for some reason.
|
| 45 |
+
(start, length) = start_length
|
| 46 |
+
fobj.seek(start)
|
| 47 |
+
sig = fobj.read(4)
|
| 48 |
+
if sig != b"\x00\x00\x00\x00":
|
| 49 |
+
msg = "Unknown signature, expecting 0x00000000"
|
| 50 |
+
raise SyntaxError(msg)
|
| 51 |
+
return read_32(fobj, (start + 4, length - 4), size)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def read_32(
|
| 55 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 56 |
+
) -> dict[str, Image.Image]:
|
| 57 |
+
"""
|
| 58 |
+
Read a 32bit RGB icon resource. Seems to be either uncompressed or
|
| 59 |
+
an RLE packbits-like scheme.
|
| 60 |
+
"""
|
| 61 |
+
(start, length) = start_length
|
| 62 |
+
fobj.seek(start)
|
| 63 |
+
pixel_size = (size[0] * size[2], size[1] * size[2])
|
| 64 |
+
sizesq = pixel_size[0] * pixel_size[1]
|
| 65 |
+
if length == sizesq * 3:
|
| 66 |
+
# uncompressed ("RGBRGBGB")
|
| 67 |
+
indata = fobj.read(length)
|
| 68 |
+
im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
|
| 69 |
+
else:
|
| 70 |
+
# decode image
|
| 71 |
+
im = Image.new("RGB", pixel_size, None)
|
| 72 |
+
for band_ix in range(3):
|
| 73 |
+
data = []
|
| 74 |
+
bytesleft = sizesq
|
| 75 |
+
while bytesleft > 0:
|
| 76 |
+
byte = fobj.read(1)
|
| 77 |
+
if not byte:
|
| 78 |
+
break
|
| 79 |
+
byte_int = byte[0]
|
| 80 |
+
if byte_int & 0x80:
|
| 81 |
+
blocksize = byte_int - 125
|
| 82 |
+
byte = fobj.read(1)
|
| 83 |
+
for i in range(blocksize):
|
| 84 |
+
data.append(byte)
|
| 85 |
+
else:
|
| 86 |
+
blocksize = byte_int + 1
|
| 87 |
+
data.append(fobj.read(blocksize))
|
| 88 |
+
bytesleft -= blocksize
|
| 89 |
+
if bytesleft <= 0:
|
| 90 |
+
break
|
| 91 |
+
if bytesleft != 0:
|
| 92 |
+
msg = f"Error reading channel [{repr(bytesleft)} left]"
|
| 93 |
+
raise SyntaxError(msg)
|
| 94 |
+
band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
|
| 95 |
+
im.im.putband(band.im, band_ix)
|
| 96 |
+
return {"RGB": im}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def read_mk(
|
| 100 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 101 |
+
) -> dict[str, Image.Image]:
|
| 102 |
+
# Alpha masks seem to be uncompressed
|
| 103 |
+
start = start_length[0]
|
| 104 |
+
fobj.seek(start)
|
| 105 |
+
pixel_size = (size[0] * size[2], size[1] * size[2])
|
| 106 |
+
sizesq = pixel_size[0] * pixel_size[1]
|
| 107 |
+
band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
|
| 108 |
+
return {"A": band}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def read_png_or_jpeg2000(
|
| 112 |
+
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
| 113 |
+
) -> dict[str, Image.Image]:
|
| 114 |
+
(start, length) = start_length
|
| 115 |
+
fobj.seek(start)
|
| 116 |
+
sig = fobj.read(12)
|
| 117 |
+
|
| 118 |
+
im: Image.Image
|
| 119 |
+
if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"):
|
| 120 |
+
fobj.seek(start)
|
| 121 |
+
im = PngImagePlugin.PngImageFile(fobj)
|
| 122 |
+
Image._decompression_bomb_check(im.size)
|
| 123 |
+
return {"RGBA": im}
|
| 124 |
+
elif (
|
| 125 |
+
sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
|
| 126 |
+
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
| 127 |
+
):
|
| 128 |
+
if not enable_jpeg2k:
|
| 129 |
+
msg = (
|
| 130 |
+
"Unsupported icon subimage format (rebuild PIL "
|
| 131 |
+
"with JPEG 2000 support to fix this)"
|
| 132 |
+
)
|
| 133 |
+
raise ValueError(msg)
|
| 134 |
+
# j2k, jpc or j2c
|
| 135 |
+
fobj.seek(start)
|
| 136 |
+
jp2kstream = fobj.read(length)
|
| 137 |
+
f = io.BytesIO(jp2kstream)
|
| 138 |
+
im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
|
| 139 |
+
Image._decompression_bomb_check(im.size)
|
| 140 |
+
if im.mode != "RGBA":
|
| 141 |
+
im = im.convert("RGBA")
|
| 142 |
+
return {"RGBA": im}
|
| 143 |
+
else:
|
| 144 |
+
msg = "Unsupported icon subimage format"
|
| 145 |
+
raise ValueError(msg)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class IcnsFile:
|
| 149 |
+
SIZES = {
|
| 150 |
+
(512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
|
| 151 |
+
(512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
|
| 152 |
+
(256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
|
| 153 |
+
(256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
|
| 154 |
+
(128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
|
| 155 |
+
(128, 128, 1): [
|
| 156 |
+
(b"ic07", read_png_or_jpeg2000),
|
| 157 |
+
(b"it32", read_32t),
|
| 158 |
+
(b"t8mk", read_mk),
|
| 159 |
+
],
|
| 160 |
+
(64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
|
| 161 |
+
(32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
|
| 162 |
+
(48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
|
| 163 |
+
(32, 32, 1): [
|
| 164 |
+
(b"icp5", read_png_or_jpeg2000),
|
| 165 |
+
(b"il32", read_32),
|
| 166 |
+
(b"l8mk", read_mk),
|
| 167 |
+
],
|
| 168 |
+
(16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
|
| 169 |
+
(16, 16, 1): [
|
| 170 |
+
(b"icp4", read_png_or_jpeg2000),
|
| 171 |
+
(b"is32", read_32),
|
| 172 |
+
(b"s8mk", read_mk),
|
| 173 |
+
],
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
def __init__(self, fobj: IO[bytes]) -> None:
|
| 177 |
+
"""
|
| 178 |
+
fobj is a file-like object as an icns resource
|
| 179 |
+
"""
|
| 180 |
+
# signature : (start, length)
|
| 181 |
+
self.dct = {}
|
| 182 |
+
self.fobj = fobj
|
| 183 |
+
sig, filesize = nextheader(fobj)
|
| 184 |
+
if not _accept(sig):
|
| 185 |
+
msg = "not an icns file"
|
| 186 |
+
raise SyntaxError(msg)
|
| 187 |
+
i = HEADERSIZE
|
| 188 |
+
while i < filesize:
|
| 189 |
+
sig, blocksize = nextheader(fobj)
|
| 190 |
+
if blocksize <= 0:
|
| 191 |
+
msg = "invalid block header"
|
| 192 |
+
raise SyntaxError(msg)
|
| 193 |
+
i += HEADERSIZE
|
| 194 |
+
blocksize -= HEADERSIZE
|
| 195 |
+
self.dct[sig] = (i, blocksize)
|
| 196 |
+
fobj.seek(blocksize, io.SEEK_CUR)
|
| 197 |
+
i += blocksize
|
| 198 |
+
|
| 199 |
+
def itersizes(self) -> list[tuple[int, int, int]]:
|
| 200 |
+
sizes = []
|
| 201 |
+
for size, fmts in self.SIZES.items():
|
| 202 |
+
for fmt, reader in fmts:
|
| 203 |
+
if fmt in self.dct:
|
| 204 |
+
sizes.append(size)
|
| 205 |
+
break
|
| 206 |
+
return sizes
|
| 207 |
+
|
| 208 |
+
def bestsize(self) -> tuple[int, int, int]:
|
| 209 |
+
sizes = self.itersizes()
|
| 210 |
+
if not sizes:
|
| 211 |
+
msg = "No 32bit icon resources found"
|
| 212 |
+
raise SyntaxError(msg)
|
| 213 |
+
return max(sizes)
|
| 214 |
+
|
| 215 |
+
def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
|
| 216 |
+
"""
|
| 217 |
+
Get an icon resource as {channel: array}. Note that
|
| 218 |
+
the arrays are bottom-up like windows bitmaps and will likely
|
| 219 |
+
need to be flipped or transposed in some way.
|
| 220 |
+
"""
|
| 221 |
+
dct = {}
|
| 222 |
+
for code, reader in self.SIZES[size]:
|
| 223 |
+
desc = self.dct.get(code)
|
| 224 |
+
if desc is not None:
|
| 225 |
+
dct.update(reader(self.fobj, desc, size))
|
| 226 |
+
return dct
|
| 227 |
+
|
| 228 |
+
def getimage(
|
| 229 |
+
self, size: tuple[int, int] | tuple[int, int, int] | None = None
|
| 230 |
+
) -> Image.Image:
|
| 231 |
+
if size is None:
|
| 232 |
+
size = self.bestsize()
|
| 233 |
+
elif len(size) == 2:
|
| 234 |
+
size = (size[0], size[1], 1)
|
| 235 |
+
channels = self.dataforsize(size)
|
| 236 |
+
|
| 237 |
+
im = channels.get("RGBA")
|
| 238 |
+
if im:
|
| 239 |
+
return im
|
| 240 |
+
|
| 241 |
+
im = channels["RGB"].copy()
|
| 242 |
+
try:
|
| 243 |
+
im.putalpha(channels["A"])
|
| 244 |
+
except KeyError:
|
| 245 |
+
pass
|
| 246 |
+
return im
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
##
|
| 250 |
+
# Image plugin for Mac OS icons.
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
class IcnsImageFile(ImageFile.ImageFile):
|
| 254 |
+
"""
|
| 255 |
+
PIL image support for Mac OS .icns files.
|
| 256 |
+
Chooses the best resolution, but will possibly load
|
| 257 |
+
a different size image if you mutate the size attribute
|
| 258 |
+
before calling 'load'.
|
| 259 |
+
|
| 260 |
+
The info dictionary has a key 'sizes' that is a list
|
| 261 |
+
of sizes that the icns file has.
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
format = "ICNS"
|
| 265 |
+
format_description = "Mac OS icns resource"
|
| 266 |
+
|
| 267 |
+
def _open(self) -> None:
|
| 268 |
+
assert self.fp is not None
|
| 269 |
+
self.icns = IcnsFile(self.fp)
|
| 270 |
+
self._mode = "RGBA"
|
| 271 |
+
self.info["sizes"] = self.icns.itersizes()
|
| 272 |
+
self.best_size = self.icns.bestsize()
|
| 273 |
+
self.size = (
|
| 274 |
+
self.best_size[0] * self.best_size[2],
|
| 275 |
+
self.best_size[1] * self.best_size[2],
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def size(self) -> tuple[int, int]:
|
| 280 |
+
return self._size
|
| 281 |
+
|
| 282 |
+
@size.setter
|
| 283 |
+
def size(self, value: tuple[int, int]) -> None:
|
| 284 |
+
# Check that a matching size exists,
|
| 285 |
+
# or that there is a scale that would create a size that matches
|
| 286 |
+
for size in self.info["sizes"]:
|
| 287 |
+
simple_size = size[0] * size[2], size[1] * size[2]
|
| 288 |
+
scale = simple_size[0] // value[0]
|
| 289 |
+
if simple_size[1] / value[1] == scale:
|
| 290 |
+
self._size = value
|
| 291 |
+
return
|
| 292 |
+
msg = "This is not one of the allowed sizes of this image"
|
| 293 |
+
raise ValueError(msg)
|
| 294 |
+
|
| 295 |
+
def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
|
| 296 |
+
if scale is not None:
|
| 297 |
+
width, height = self.size[:2]
|
| 298 |
+
self.size = width * scale, height * scale
|
| 299 |
+
self.best_size = width, height, scale
|
| 300 |
+
|
| 301 |
+
px = Image.Image.load(self)
|
| 302 |
+
if self._im is not None and self.im.size == self.size:
|
| 303 |
+
# Already loaded
|
| 304 |
+
return px
|
| 305 |
+
self.load_prepare()
|
| 306 |
+
# This is likely NOT the best way to do it, but whatever.
|
| 307 |
+
im = self.icns.getimage(self.best_size)
|
| 308 |
+
|
| 309 |
+
# If this is a PNG or JPEG 2000, it won't be loaded yet
|
| 310 |
+
px = im.load()
|
| 311 |
+
|
| 312 |
+
self.im = im.im
|
| 313 |
+
self._mode = im.mode
|
| 314 |
+
self.size = im.size
|
| 315 |
+
|
| 316 |
+
return px
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 320 |
+
"""
|
| 321 |
+
Saves the image as a series of PNG files,
|
| 322 |
+
that are then combined into a .icns file.
|
| 323 |
+
"""
|
| 324 |
+
if hasattr(fp, "flush"):
|
| 325 |
+
fp.flush()
|
| 326 |
+
|
| 327 |
+
sizes = {
|
| 328 |
+
b"ic07": 128,
|
| 329 |
+
b"ic08": 256,
|
| 330 |
+
b"ic09": 512,
|
| 331 |
+
b"ic10": 1024,
|
| 332 |
+
b"ic11": 32,
|
| 333 |
+
b"ic12": 64,
|
| 334 |
+
b"ic13": 256,
|
| 335 |
+
b"ic14": 512,
|
| 336 |
+
}
|
| 337 |
+
provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
|
| 338 |
+
size_streams = {}
|
| 339 |
+
for size in set(sizes.values()):
|
| 340 |
+
image = (
|
| 341 |
+
provided_images[size]
|
| 342 |
+
if size in provided_images
|
| 343 |
+
else im.resize((size, size))
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
temp = io.BytesIO()
|
| 347 |
+
image.save(temp, "png")
|
| 348 |
+
size_streams[size] = temp.getvalue()
|
| 349 |
+
|
| 350 |
+
entries = []
|
| 351 |
+
for type, size in sizes.items():
|
| 352 |
+
stream = size_streams[size]
|
| 353 |
+
entries.append((type, HEADERSIZE + len(stream), stream))
|
| 354 |
+
|
| 355 |
+
# Header
|
| 356 |
+
fp.write(MAGIC)
|
| 357 |
+
file_length = HEADERSIZE # Header
|
| 358 |
+
file_length += HEADERSIZE + 8 * len(entries) # TOC
|
| 359 |
+
file_length += sum(entry[1] for entry in entries)
|
| 360 |
+
fp.write(struct.pack(">i", file_length))
|
| 361 |
+
|
| 362 |
+
# TOC
|
| 363 |
+
fp.write(b"TOC ")
|
| 364 |
+
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
|
| 365 |
+
for entry in entries:
|
| 366 |
+
fp.write(entry[0])
|
| 367 |
+
fp.write(struct.pack(">i", entry[1]))
|
| 368 |
+
|
| 369 |
+
# Data
|
| 370 |
+
for entry in entries:
|
| 371 |
+
fp.write(entry[0])
|
| 372 |
+
fp.write(struct.pack(">i", entry[1]))
|
| 373 |
+
fp.write(entry[2])
|
| 374 |
+
|
| 375 |
+
if hasattr(fp, "flush"):
|
| 376 |
+
fp.flush()
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def _accept(prefix: bytes) -> bool:
|
| 380 |
+
return prefix.startswith(MAGIC)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
|
| 384 |
+
Image.register_extension(IcnsImageFile.format, ".icns")
|
| 385 |
+
|
| 386 |
+
Image.register_save(IcnsImageFile.format, _save)
|
| 387 |
+
Image.register_mime(IcnsImageFile.format, "image/icns")
|
| 388 |
+
|
| 389 |
+
if __name__ == "__main__":
|
| 390 |
+
if len(sys.argv) < 2:
|
| 391 |
+
print("Syntax: python3 IcnsImagePlugin.py [file]")
|
| 392 |
+
sys.exit()
|
| 393 |
+
|
| 394 |
+
with open(sys.argv[1], "rb") as fp:
|
| 395 |
+
imf = IcnsImageFile(fp)
|
| 396 |
+
for size in imf.info["sizes"]:
|
| 397 |
+
width, height, scale = imf.size = size
|
| 398 |
+
imf.save(f"out-{width}-{height}-{scale}.png")
|
| 399 |
+
with Image.open(sys.argv[1]) as im:
|
| 400 |
+
im.save("out.png")
|
| 401 |
+
if sys.platform == "windows":
|
| 402 |
+
os.startfile("out.png")
|
PIL/IcoImagePlugin.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Copyright 2008 Bryan Davis
|
| 21 |
+
#
|
| 22 |
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
| 23 |
+
# not use this file except in compliance with the License. You may obtain
|
| 24 |
+
# a copy of the License at
|
| 25 |
+
#
|
| 26 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 27 |
+
#
|
| 28 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 29 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 30 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 31 |
+
# See the License for the specific language governing permissions and
|
| 32 |
+
# limitations under the License.
|
| 33 |
+
|
| 34 |
+
# Icon format references:
|
| 35 |
+
# * https://en.wikipedia.org/wiki/ICO_(file_format)
|
| 36 |
+
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
import warnings
|
| 40 |
+
from io import BytesIO
|
| 41 |
+
from math import ceil, log
|
| 42 |
+
from typing import IO, NamedTuple
|
| 43 |
+
|
| 44 |
+
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
| 45 |
+
from ._binary import i16le as i16
|
| 46 |
+
from ._binary import i32le as i32
|
| 47 |
+
from ._binary import o8
|
| 48 |
+
from ._binary import o16le as o16
|
| 49 |
+
from ._binary import o32le as o32
|
| 50 |
+
|
| 51 |
+
#
|
| 52 |
+
# --------------------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
_MAGIC = b"\0\0\1\0"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 58 |
+
fp.write(_MAGIC) # (2+2)
|
| 59 |
+
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
| 60 |
+
sizes = im.encoderinfo.get(
|
| 61 |
+
"sizes",
|
| 62 |
+
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
| 63 |
+
)
|
| 64 |
+
frames = []
|
| 65 |
+
provided_ims = [im] + im.encoderinfo.get("append_images", [])
|
| 66 |
+
width, height = im.size
|
| 67 |
+
for size in sorted(set(sizes)):
|
| 68 |
+
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
| 69 |
+
continue
|
| 70 |
+
|
| 71 |
+
for provided_im in provided_ims:
|
| 72 |
+
if provided_im.size != size:
|
| 73 |
+
continue
|
| 74 |
+
frames.append(provided_im)
|
| 75 |
+
if bmp:
|
| 76 |
+
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
|
| 77 |
+
bits_used = [bits]
|
| 78 |
+
for other_im in provided_ims:
|
| 79 |
+
if other_im.size != size:
|
| 80 |
+
continue
|
| 81 |
+
bits = BmpImagePlugin.SAVE[other_im.mode][1]
|
| 82 |
+
if bits not in bits_used:
|
| 83 |
+
# Another image has been supplied for this size
|
| 84 |
+
# with a different bit depth
|
| 85 |
+
frames.append(other_im)
|
| 86 |
+
bits_used.append(bits)
|
| 87 |
+
break
|
| 88 |
+
else:
|
| 89 |
+
# TODO: invent a more convenient method for proportional scalings
|
| 90 |
+
frame = provided_im.copy()
|
| 91 |
+
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
| 92 |
+
frames.append(frame)
|
| 93 |
+
fp.write(o16(len(frames))) # idCount(2)
|
| 94 |
+
offset = fp.tell() + len(frames) * 16
|
| 95 |
+
for frame in frames:
|
| 96 |
+
width, height = frame.size
|
| 97 |
+
# 0 means 256
|
| 98 |
+
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
|
| 99 |
+
fp.write(o8(height if height < 256 else 0)) # bHeight(1)
|
| 100 |
+
|
| 101 |
+
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
|
| 102 |
+
fp.write(o8(colors)) # bColorCount(1)
|
| 103 |
+
fp.write(b"\0") # bReserved(1)
|
| 104 |
+
fp.write(b"\0\0") # wPlanes(2)
|
| 105 |
+
fp.write(o16(bits)) # wBitCount(2)
|
| 106 |
+
|
| 107 |
+
image_io = BytesIO()
|
| 108 |
+
if bmp:
|
| 109 |
+
frame.save(image_io, "dib")
|
| 110 |
+
|
| 111 |
+
if bits != 32:
|
| 112 |
+
and_mask = Image.new("1", size)
|
| 113 |
+
ImageFile._save(
|
| 114 |
+
and_mask,
|
| 115 |
+
image_io,
|
| 116 |
+
[ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))],
|
| 117 |
+
)
|
| 118 |
+
else:
|
| 119 |
+
frame.save(image_io, "png")
|
| 120 |
+
image_io.seek(0)
|
| 121 |
+
image_bytes = image_io.read()
|
| 122 |
+
if bmp:
|
| 123 |
+
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
| 124 |
+
bytes_len = len(image_bytes)
|
| 125 |
+
fp.write(o32(bytes_len)) # dwBytesInRes(4)
|
| 126 |
+
fp.write(o32(offset)) # dwImageOffset(4)
|
| 127 |
+
current = fp.tell()
|
| 128 |
+
fp.seek(offset)
|
| 129 |
+
fp.write(image_bytes)
|
| 130 |
+
offset = offset + bytes_len
|
| 131 |
+
fp.seek(current)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _accept(prefix: bytes) -> bool:
|
| 135 |
+
return prefix.startswith(_MAGIC)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class IconHeader(NamedTuple):
|
| 139 |
+
width: int
|
| 140 |
+
height: int
|
| 141 |
+
nb_color: int
|
| 142 |
+
reserved: int
|
| 143 |
+
planes: int
|
| 144 |
+
bpp: int
|
| 145 |
+
size: int
|
| 146 |
+
offset: int
|
| 147 |
+
dim: tuple[int, int]
|
| 148 |
+
square: int
|
| 149 |
+
color_depth: int
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class IcoFile:
|
| 153 |
+
def __init__(self, buf: IO[bytes]) -> None:
|
| 154 |
+
"""
|
| 155 |
+
Parse image from file-like object containing ico file data
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
# check magic
|
| 159 |
+
s = buf.read(6)
|
| 160 |
+
if not _accept(s):
|
| 161 |
+
msg = "not an ICO file"
|
| 162 |
+
raise SyntaxError(msg)
|
| 163 |
+
|
| 164 |
+
self.buf = buf
|
| 165 |
+
self.entry = []
|
| 166 |
+
|
| 167 |
+
# Number of items in file
|
| 168 |
+
self.nb_items = i16(s, 4)
|
| 169 |
+
|
| 170 |
+
# Get headers for each item
|
| 171 |
+
for i in range(self.nb_items):
|
| 172 |
+
s = buf.read(16)
|
| 173 |
+
|
| 174 |
+
# See Wikipedia
|
| 175 |
+
width = s[0] or 256
|
| 176 |
+
height = s[1] or 256
|
| 177 |
+
|
| 178 |
+
# No. of colors in image (0 if >=8bpp)
|
| 179 |
+
nb_color = s[2]
|
| 180 |
+
bpp = i16(s, 6)
|
| 181 |
+
icon_header = IconHeader(
|
| 182 |
+
width=width,
|
| 183 |
+
height=height,
|
| 184 |
+
nb_color=nb_color,
|
| 185 |
+
reserved=s[3],
|
| 186 |
+
planes=i16(s, 4),
|
| 187 |
+
bpp=i16(s, 6),
|
| 188 |
+
size=i32(s, 8),
|
| 189 |
+
offset=i32(s, 12),
|
| 190 |
+
dim=(width, height),
|
| 191 |
+
square=width * height,
|
| 192 |
+
# See Wikipedia notes about color depth.
|
| 193 |
+
# We need this just to differ images with equal sizes
|
| 194 |
+
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
self.entry.append(icon_header)
|
| 198 |
+
|
| 199 |
+
self.entry = sorted(self.entry, key=lambda x: x.color_depth)
|
| 200 |
+
# ICO images are usually squares
|
| 201 |
+
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
|
| 202 |
+
|
| 203 |
+
def sizes(self) -> set[tuple[int, int]]:
|
| 204 |
+
"""
|
| 205 |
+
Get a set of all available icon sizes and color depths.
|
| 206 |
+
"""
|
| 207 |
+
return {(h.width, h.height) for h in self.entry}
|
| 208 |
+
|
| 209 |
+
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
|
| 210 |
+
for i, h in enumerate(self.entry):
|
| 211 |
+
if size == h.dim and (bpp is False or bpp == h.color_depth):
|
| 212 |
+
return i
|
| 213 |
+
return 0
|
| 214 |
+
|
| 215 |
+
def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image:
|
| 216 |
+
"""
|
| 217 |
+
Get an image from the icon
|
| 218 |
+
"""
|
| 219 |
+
return self.frame(self.getentryindex(size, bpp))
|
| 220 |
+
|
| 221 |
+
def frame(self, idx: int) -> Image.Image:
|
| 222 |
+
"""
|
| 223 |
+
Get an image from frame idx
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
header = self.entry[idx]
|
| 227 |
+
|
| 228 |
+
self.buf.seek(header.offset)
|
| 229 |
+
data = self.buf.read(8)
|
| 230 |
+
self.buf.seek(header.offset)
|
| 231 |
+
|
| 232 |
+
im: Image.Image
|
| 233 |
+
if data[:8] == PngImagePlugin._MAGIC:
|
| 234 |
+
# png frame
|
| 235 |
+
im = PngImagePlugin.PngImageFile(self.buf)
|
| 236 |
+
Image._decompression_bomb_check(im.size)
|
| 237 |
+
else:
|
| 238 |
+
# XOR + AND mask bmp frame
|
| 239 |
+
im = BmpImagePlugin.DibImageFile(self.buf)
|
| 240 |
+
Image._decompression_bomb_check(im.size)
|
| 241 |
+
|
| 242 |
+
# change tile dimension to only encompass XOR image
|
| 243 |
+
im._size = (im.size[0], int(im.size[1] / 2))
|
| 244 |
+
d, e, o, a = im.tile[0]
|
| 245 |
+
im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
|
| 246 |
+
|
| 247 |
+
# figure out where AND mask image starts
|
| 248 |
+
if header.bpp == 32:
|
| 249 |
+
# 32-bit color depth icon image allows semitransparent areas
|
| 250 |
+
# PIL's DIB format ignores transparency bits, recover them.
|
| 251 |
+
# The DIB is packed in BGRX byte order where X is the alpha
|
| 252 |
+
# channel.
|
| 253 |
+
|
| 254 |
+
# Back up to start of bmp data
|
| 255 |
+
self.buf.seek(o)
|
| 256 |
+
# extract every 4th byte (eg. 3,7,11,15,...)
|
| 257 |
+
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
|
| 258 |
+
|
| 259 |
+
# convert to an 8bpp grayscale image
|
| 260 |
+
try:
|
| 261 |
+
mask = Image.frombuffer(
|
| 262 |
+
"L", # 8bpp
|
| 263 |
+
im.size, # (w, h)
|
| 264 |
+
alpha_bytes, # source chars
|
| 265 |
+
"raw", # raw decoder
|
| 266 |
+
("L", 0, -1), # 8bpp inverted, unpadded, reversed
|
| 267 |
+
)
|
| 268 |
+
except ValueError:
|
| 269 |
+
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| 270 |
+
mask = None
|
| 271 |
+
else:
|
| 272 |
+
raise
|
| 273 |
+
else:
|
| 274 |
+
# get AND image from end of bitmap
|
| 275 |
+
w = im.size[0]
|
| 276 |
+
if (w % 32) > 0:
|
| 277 |
+
# bitmap row data is aligned to word boundaries
|
| 278 |
+
w += 32 - (im.size[0] % 32)
|
| 279 |
+
|
| 280 |
+
# the total mask data is
|
| 281 |
+
# padded row size * height / bits per char
|
| 282 |
+
|
| 283 |
+
total_bytes = int((w * im.size[1]) / 8)
|
| 284 |
+
and_mask_offset = header.offset + header.size - total_bytes
|
| 285 |
+
|
| 286 |
+
self.buf.seek(and_mask_offset)
|
| 287 |
+
mask_data = self.buf.read(total_bytes)
|
| 288 |
+
|
| 289 |
+
# convert raw data to image
|
| 290 |
+
try:
|
| 291 |
+
mask = Image.frombuffer(
|
| 292 |
+
"1", # 1 bpp
|
| 293 |
+
im.size, # (w, h)
|
| 294 |
+
mask_data, # source chars
|
| 295 |
+
"raw", # raw decoder
|
| 296 |
+
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
|
| 297 |
+
)
|
| 298 |
+
except ValueError:
|
| 299 |
+
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
| 300 |
+
mask = None
|
| 301 |
+
else:
|
| 302 |
+
raise
|
| 303 |
+
|
| 304 |
+
# now we have two images, im is XOR image and mask is AND image
|
| 305 |
+
|
| 306 |
+
# apply mask image as alpha channel
|
| 307 |
+
if mask:
|
| 308 |
+
im = im.convert("RGBA")
|
| 309 |
+
im.putalpha(mask)
|
| 310 |
+
|
| 311 |
+
return im
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
##
|
| 315 |
+
# Image plugin for Windows Icon files.
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
class IcoImageFile(ImageFile.ImageFile):
|
| 319 |
+
"""
|
| 320 |
+
PIL read-only image support for Microsoft Windows .ico files.
|
| 321 |
+
|
| 322 |
+
By default the largest resolution image in the file will be loaded. This
|
| 323 |
+
can be changed by altering the 'size' attribute before calling 'load'.
|
| 324 |
+
|
| 325 |
+
The info dictionary has a key 'sizes' that is a list of the sizes available
|
| 326 |
+
in the icon file.
|
| 327 |
+
|
| 328 |
+
Handles classic, XP and Vista icon formats.
|
| 329 |
+
|
| 330 |
+
When saving, PNG compression is used. Support for this was only added in
|
| 331 |
+
Windows Vista. If you are unable to view the icon in Windows, convert the
|
| 332 |
+
image to "RGBA" mode before saving.
|
| 333 |
+
|
| 334 |
+
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
| 335 |
+
<casadebender@gmail.com>.
|
| 336 |
+
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
format = "ICO"
|
| 340 |
+
format_description = "Windows Icon"
|
| 341 |
+
|
| 342 |
+
def _open(self) -> None:
|
| 343 |
+
assert self.fp is not None
|
| 344 |
+
self.ico = IcoFile(self.fp)
|
| 345 |
+
self.info["sizes"] = self.ico.sizes()
|
| 346 |
+
self.size = self.ico.entry[0].dim
|
| 347 |
+
self.load()
|
| 348 |
+
|
| 349 |
+
@property
|
| 350 |
+
def size(self) -> tuple[int, int]:
|
| 351 |
+
return self._size
|
| 352 |
+
|
| 353 |
+
@size.setter
|
| 354 |
+
def size(self, value: tuple[int, int]) -> None:
|
| 355 |
+
if value not in self.info["sizes"]:
|
| 356 |
+
msg = "This is not one of the allowed sizes of this image"
|
| 357 |
+
raise ValueError(msg)
|
| 358 |
+
self._size = value
|
| 359 |
+
|
| 360 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 361 |
+
if self._im is not None and self.im.size == self.size:
|
| 362 |
+
# Already loaded
|
| 363 |
+
return Image.Image.load(self)
|
| 364 |
+
im = self.ico.getimage(self.size)
|
| 365 |
+
# if tile is PNG, it won't really be loaded yet
|
| 366 |
+
im.load()
|
| 367 |
+
self.im = im.im
|
| 368 |
+
self._mode = im.mode
|
| 369 |
+
if im.palette:
|
| 370 |
+
self.palette = im.palette
|
| 371 |
+
if im.size != self.size:
|
| 372 |
+
warnings.warn("Image was not the expected size")
|
| 373 |
+
|
| 374 |
+
index = self.ico.getentryindex(self.size)
|
| 375 |
+
sizes = list(self.info["sizes"])
|
| 376 |
+
sizes[index] = im.size
|
| 377 |
+
self.info["sizes"] = set(sizes)
|
| 378 |
+
|
| 379 |
+
self.size = im.size
|
| 380 |
+
return Image.Image.load(self)
|
| 381 |
+
|
| 382 |
+
def load_seek(self, pos: int) -> None:
|
| 383 |
+
# Flag the ImageFile.Parser so that it
|
| 384 |
+
# just does all the decode at the end.
|
| 385 |
+
pass
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
#
|
| 389 |
+
# --------------------------------------------------------------------
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
|
| 393 |
+
Image.register_save(IcoImageFile.format, _save)
|
| 394 |
+
Image.register_extension(IcoImageFile.format, ".ico")
|
| 395 |
+
|
| 396 |
+
Image.register_mime(IcoImageFile.format, "image/x-icon")
|
PIL/ImImagePlugin.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
from ._util import DeferredError
|
| 35 |
+
|
| 36 |
+
# --------------------------------------------------------------------
|
| 37 |
+
# Standard tags
|
| 38 |
+
|
| 39 |
+
COMMENT = "Comment"
|
| 40 |
+
DATE = "Date"
|
| 41 |
+
EQUIPMENT = "Digitalization equipment"
|
| 42 |
+
FRAMES = "File size (no of images)"
|
| 43 |
+
LUT = "Lut"
|
| 44 |
+
NAME = "Name"
|
| 45 |
+
SCALE = "Scale (x,y)"
|
| 46 |
+
SIZE = "Image size (x*y)"
|
| 47 |
+
MODE = "Image type"
|
| 48 |
+
|
| 49 |
+
TAGS = {
|
| 50 |
+
COMMENT: 0,
|
| 51 |
+
DATE: 0,
|
| 52 |
+
EQUIPMENT: 0,
|
| 53 |
+
FRAMES: 0,
|
| 54 |
+
LUT: 0,
|
| 55 |
+
NAME: 0,
|
| 56 |
+
SCALE: 0,
|
| 57 |
+
SIZE: 0,
|
| 58 |
+
MODE: 0,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
OPEN = {
|
| 62 |
+
# ifunc93/p3cfunc formats
|
| 63 |
+
"0 1 image": ("1", "1"),
|
| 64 |
+
"L 1 image": ("1", "1"),
|
| 65 |
+
"Greyscale image": ("L", "L"),
|
| 66 |
+
"Grayscale image": ("L", "L"),
|
| 67 |
+
"RGB image": ("RGB", "RGB;L"),
|
| 68 |
+
"RLB image": ("RGB", "RLB"),
|
| 69 |
+
"RYB image": ("RGB", "RLB"),
|
| 70 |
+
"B1 image": ("1", "1"),
|
| 71 |
+
"B2 image": ("P", "P;2"),
|
| 72 |
+
"B4 image": ("P", "P;4"),
|
| 73 |
+
"X 24 image": ("RGB", "RGB"),
|
| 74 |
+
"L 32 S image": ("I", "I;32"),
|
| 75 |
+
"L 32 F image": ("F", "F;32"),
|
| 76 |
+
# old p3cfunc formats
|
| 77 |
+
"RGB3 image": ("RGB", "RGB;T"),
|
| 78 |
+
"RYB3 image": ("RGB", "RYB;T"),
|
| 79 |
+
# extensions
|
| 80 |
+
"LA image": ("LA", "LA;L"),
|
| 81 |
+
"PA image": ("LA", "PA;L"),
|
| 82 |
+
"RGBA image": ("RGBA", "RGBA;L"),
|
| 83 |
+
"RGBX image": ("RGB", "RGBX;L"),
|
| 84 |
+
"CMYK image": ("CMYK", "CMYK;L"),
|
| 85 |
+
"YCC image": ("YCbCr", "YCbCr;L"),
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# ifunc95 extensions
|
| 89 |
+
for i in ["8", "8S", "16", "16S", "32", "32F"]:
|
| 90 |
+
OPEN[f"L {i} image"] = ("F", f"F;{i}")
|
| 91 |
+
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
|
| 92 |
+
for i in ["16", "16L", "16B"]:
|
| 93 |
+
OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
|
| 94 |
+
OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
|
| 95 |
+
for i in ["32S"]:
|
| 96 |
+
OPEN[f"L {i} image"] = ("I", f"I;{i}")
|
| 97 |
+
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
|
| 98 |
+
for j in range(2, 33):
|
| 99 |
+
OPEN[f"L*{j} image"] = ("F", f"F;{j}")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# --------------------------------------------------------------------
|
| 103 |
+
# Read IM directory
|
| 104 |
+
|
| 105 |
+
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def number(s: Any) -> float:
|
| 109 |
+
try:
|
| 110 |
+
return int(s)
|
| 111 |
+
except ValueError:
|
| 112 |
+
return float(s)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
##
|
| 116 |
+
# Image plugin for the IFUNC IM file format.
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class ImImageFile(ImageFile.ImageFile):
|
| 120 |
+
format = "IM"
|
| 121 |
+
format_description = "IFUNC Image Memory"
|
| 122 |
+
_close_exclusive_fp_after_loading = False
|
| 123 |
+
|
| 124 |
+
def _open(self) -> None:
|
| 125 |
+
# Quick rejection: if there's not an LF among the first
|
| 126 |
+
# 100 bytes, this is (probably) not a text header.
|
| 127 |
+
|
| 128 |
+
assert self.fp is not None
|
| 129 |
+
if b"\n" not in self.fp.read(100):
|
| 130 |
+
msg = "not an IM file"
|
| 131 |
+
raise SyntaxError(msg)
|
| 132 |
+
self.fp.seek(0)
|
| 133 |
+
|
| 134 |
+
n = 0
|
| 135 |
+
|
| 136 |
+
# Default values
|
| 137 |
+
self.info[MODE] = "L"
|
| 138 |
+
self.info[SIZE] = (512, 512)
|
| 139 |
+
self.info[FRAMES] = 1
|
| 140 |
+
|
| 141 |
+
self.rawmode = "L"
|
| 142 |
+
|
| 143 |
+
while True:
|
| 144 |
+
s = self.fp.read(1)
|
| 145 |
+
|
| 146 |
+
# Some versions of IFUNC uses \n\r instead of \r\n...
|
| 147 |
+
if s == b"\r":
|
| 148 |
+
continue
|
| 149 |
+
|
| 150 |
+
if not s or s == b"\0" or s == b"\x1a":
|
| 151 |
+
break
|
| 152 |
+
|
| 153 |
+
# FIXME: this may read whole file if not a text file
|
| 154 |
+
s = s + self.fp.readline()
|
| 155 |
+
|
| 156 |
+
if len(s) > 100:
|
| 157 |
+
msg = "not an IM file"
|
| 158 |
+
raise SyntaxError(msg)
|
| 159 |
+
|
| 160 |
+
if s.endswith(b"\r\n"):
|
| 161 |
+
s = s[:-2]
|
| 162 |
+
elif s.endswith(b"\n"):
|
| 163 |
+
s = s[:-1]
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
m = split.match(s)
|
| 167 |
+
except re.error as e:
|
| 168 |
+
msg = "not an IM file"
|
| 169 |
+
raise SyntaxError(msg) from e
|
| 170 |
+
|
| 171 |
+
if m:
|
| 172 |
+
k, v = m.group(1, 2)
|
| 173 |
+
|
| 174 |
+
# Don't know if this is the correct encoding,
|
| 175 |
+
# but a decent guess (I guess)
|
| 176 |
+
k = k.decode("latin-1", "replace")
|
| 177 |
+
v = v.decode("latin-1", "replace")
|
| 178 |
+
|
| 179 |
+
# Convert value as appropriate
|
| 180 |
+
if k in [FRAMES, SCALE, SIZE]:
|
| 181 |
+
v = v.replace("*", ",")
|
| 182 |
+
v = tuple(map(number, v.split(",")))
|
| 183 |
+
if len(v) == 1:
|
| 184 |
+
v = v[0]
|
| 185 |
+
elif k == MODE and v in OPEN:
|
| 186 |
+
v, self.rawmode = OPEN[v]
|
| 187 |
+
|
| 188 |
+
# Add to dictionary. Note that COMMENT tags are
|
| 189 |
+
# combined into a list of strings.
|
| 190 |
+
if k == COMMENT:
|
| 191 |
+
if k in self.info:
|
| 192 |
+
self.info[k].append(v)
|
| 193 |
+
else:
|
| 194 |
+
self.info[k] = [v]
|
| 195 |
+
else:
|
| 196 |
+
self.info[k] = v
|
| 197 |
+
|
| 198 |
+
if k in TAGS:
|
| 199 |
+
n += 1
|
| 200 |
+
|
| 201 |
+
else:
|
| 202 |
+
msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
|
| 203 |
+
raise SyntaxError(msg)
|
| 204 |
+
|
| 205 |
+
if not n:
|
| 206 |
+
msg = "Not an IM file"
|
| 207 |
+
raise SyntaxError(msg)
|
| 208 |
+
|
| 209 |
+
# Basic attributes
|
| 210 |
+
self._size = self.info[SIZE]
|
| 211 |
+
self._mode = self.info[MODE]
|
| 212 |
+
|
| 213 |
+
# Skip forward to start of image data
|
| 214 |
+
while s and not s.startswith(b"\x1a"):
|
| 215 |
+
s = self.fp.read(1)
|
| 216 |
+
if not s:
|
| 217 |
+
msg = "File truncated"
|
| 218 |
+
raise SyntaxError(msg)
|
| 219 |
+
|
| 220 |
+
if LUT in self.info:
|
| 221 |
+
# convert lookup table to palette or lut attribute
|
| 222 |
+
palette = self.fp.read(768)
|
| 223 |
+
greyscale = 1 # greyscale palette
|
| 224 |
+
linear = 1 # linear greyscale palette
|
| 225 |
+
for i in range(256):
|
| 226 |
+
if palette[i] == palette[i + 256] == palette[i + 512]:
|
| 227 |
+
if palette[i] != i:
|
| 228 |
+
linear = 0
|
| 229 |
+
else:
|
| 230 |
+
greyscale = 0
|
| 231 |
+
if self.mode in ["L", "LA", "P", "PA"]:
|
| 232 |
+
if greyscale:
|
| 233 |
+
if not linear:
|
| 234 |
+
self.lut = list(palette[:256])
|
| 235 |
+
else:
|
| 236 |
+
if self.mode in ["L", "P"]:
|
| 237 |
+
self._mode = self.rawmode = "P"
|
| 238 |
+
elif self.mode in ["LA", "PA"]:
|
| 239 |
+
self._mode = "PA"
|
| 240 |
+
self.rawmode = "PA;L"
|
| 241 |
+
self.palette = ImagePalette.raw("RGB;L", palette)
|
| 242 |
+
elif self.mode == "RGB":
|
| 243 |
+
if not greyscale or not linear:
|
| 244 |
+
self.lut = list(palette)
|
| 245 |
+
|
| 246 |
+
self.frame = 0
|
| 247 |
+
|
| 248 |
+
self.__offset = offs = self.fp.tell()
|
| 249 |
+
|
| 250 |
+
self._fp = self.fp # FIXME: hack
|
| 251 |
+
|
| 252 |
+
if self.rawmode.startswith("F;"):
|
| 253 |
+
# ifunc95 formats
|
| 254 |
+
try:
|
| 255 |
+
# use bit decoder (if necessary)
|
| 256 |
+
bits = int(self.rawmode[2:])
|
| 257 |
+
if bits not in [8, 16, 32]:
|
| 258 |
+
self.tile = [
|
| 259 |
+
ImageFile._Tile(
|
| 260 |
+
"bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
|
| 261 |
+
)
|
| 262 |
+
]
|
| 263 |
+
return
|
| 264 |
+
except ValueError:
|
| 265 |
+
pass
|
| 266 |
+
|
| 267 |
+
if self.rawmode in ["RGB;T", "RYB;T"]:
|
| 268 |
+
# Old LabEye/3PC files. Would be very surprised if anyone
|
| 269 |
+
# ever stumbled upon such a file ;-)
|
| 270 |
+
size = self.size[0] * self.size[1]
|
| 271 |
+
self.tile = [
|
| 272 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
|
| 273 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
|
| 274 |
+
ImageFile._Tile(
|
| 275 |
+
"raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
|
| 276 |
+
),
|
| 277 |
+
]
|
| 278 |
+
else:
|
| 279 |
+
# LabEye/IFUNC files
|
| 280 |
+
self.tile = [
|
| 281 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
| 282 |
+
]
|
| 283 |
+
|
| 284 |
+
@property
|
| 285 |
+
def n_frames(self) -> int:
|
| 286 |
+
return self.info[FRAMES]
|
| 287 |
+
|
| 288 |
+
@property
|
| 289 |
+
def is_animated(self) -> bool:
|
| 290 |
+
return self.info[FRAMES] > 1
|
| 291 |
+
|
| 292 |
+
def seek(self, frame: int) -> None:
|
| 293 |
+
if not self._seek_check(frame):
|
| 294 |
+
return
|
| 295 |
+
if isinstance(self._fp, DeferredError):
|
| 296 |
+
raise self._fp.ex
|
| 297 |
+
|
| 298 |
+
self.frame = frame
|
| 299 |
+
|
| 300 |
+
if self.mode == "1":
|
| 301 |
+
bits = 1
|
| 302 |
+
else:
|
| 303 |
+
bits = 8 * len(self.mode)
|
| 304 |
+
|
| 305 |
+
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
|
| 306 |
+
offs = self.__offset + frame * size
|
| 307 |
+
|
| 308 |
+
self.fp = self._fp
|
| 309 |
+
|
| 310 |
+
self.tile = [
|
| 311 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
def tell(self) -> int:
|
| 315 |
+
return self.frame
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
#
|
| 319 |
+
# --------------------------------------------------------------------
|
| 320 |
+
# Save IM files
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
SAVE = {
|
| 324 |
+
# mode: (im type, raw mode)
|
| 325 |
+
"1": ("0 1", "1"),
|
| 326 |
+
"L": ("Greyscale", "L"),
|
| 327 |
+
"LA": ("LA", "LA;L"),
|
| 328 |
+
"P": ("Greyscale", "P"),
|
| 329 |
+
"PA": ("LA", "PA;L"),
|
| 330 |
+
"I": ("L 32S", "I;32S"),
|
| 331 |
+
"I;16": ("L 16", "I;16"),
|
| 332 |
+
"I;16L": ("L 16L", "I;16L"),
|
| 333 |
+
"I;16B": ("L 16B", "I;16B"),
|
| 334 |
+
"F": ("L 32F", "F;32F"),
|
| 335 |
+
"RGB": ("RGB", "RGB;L"),
|
| 336 |
+
"RGBA": ("RGBA", "RGBA;L"),
|
| 337 |
+
"RGBX": ("RGBX", "RGBX;L"),
|
| 338 |
+
"CMYK": ("CMYK", "CMYK;L"),
|
| 339 |
+
"YCbCr": ("YCC", "YCbCr;L"),
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 344 |
+
try:
|
| 345 |
+
image_type, rawmode = SAVE[im.mode]
|
| 346 |
+
except KeyError as e:
|
| 347 |
+
msg = f"Cannot save {im.mode} images as IM"
|
| 348 |
+
raise ValueError(msg) from e
|
| 349 |
+
|
| 350 |
+
frames = im.encoderinfo.get("frames", 1)
|
| 351 |
+
|
| 352 |
+
fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
|
| 353 |
+
if filename:
|
| 354 |
+
# Each line must be 100 characters or less,
|
| 355 |
+
# or: SyntaxError("not an IM file")
|
| 356 |
+
# 8 characters are used for "Name: " and "\r\n"
|
| 357 |
+
# Keep just the filename, ditch the potentially overlong path
|
| 358 |
+
if isinstance(filename, bytes):
|
| 359 |
+
filename = filename.decode("ascii")
|
| 360 |
+
name, ext = os.path.splitext(os.path.basename(filename))
|
| 361 |
+
name = "".join([name[: 92 - len(ext)], ext])
|
| 362 |
+
|
| 363 |
+
fp.write(f"Name: {name}\r\n".encode("ascii"))
|
| 364 |
+
fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
|
| 365 |
+
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
|
| 366 |
+
if im.mode in ["P", "PA"]:
|
| 367 |
+
fp.write(b"Lut: 1\r\n")
|
| 368 |
+
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
|
| 369 |
+
if im.mode in ["P", "PA"]:
|
| 370 |
+
im_palette = im.im.getpalette("RGB", "RGB;L")
|
| 371 |
+
colors = len(im_palette) // 3
|
| 372 |
+
palette = b""
|
| 373 |
+
for i in range(3):
|
| 374 |
+
palette += im_palette[colors * i : colors * (i + 1)]
|
| 375 |
+
palette += b"\x00" * (256 - colors)
|
| 376 |
+
fp.write(palette) # 768 bytes
|
| 377 |
+
ImageFile._save(
|
| 378 |
+
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
#
|
| 383 |
+
# --------------------------------------------------------------------
|
| 384 |
+
# Registry
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
Image.register_open(ImImageFile.format, ImImageFile)
|
| 388 |
+
Image.register_save(ImImageFile.format, _save)
|
| 389 |
+
|
| 390 |
+
Image.register_extension(ImImageFile.format, ".im")
|
PIL/Image.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
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 |
+
}
|
PIL/ImageDraw.py
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 typing import cast
|
| 38 |
+
|
| 39 |
+
from . import Image, ImageColor, ImageText
|
| 40 |
+
|
| 41 |
+
TYPE_CHECKING = False
|
| 42 |
+
if TYPE_CHECKING:
|
| 43 |
+
from collections.abc import Callable
|
| 44 |
+
from types import ModuleType
|
| 45 |
+
from typing import Any, AnyStr
|
| 46 |
+
|
| 47 |
+
from . import ImageDraw2, ImageFont
|
| 48 |
+
from ._typing import Coords, _Ink
|
| 49 |
+
|
| 50 |
+
# experimental access to the outline API
|
| 51 |
+
Outline: Callable[[], Image.core._Outline] = Image.core.outline
|
| 52 |
+
|
| 53 |
+
"""
|
| 54 |
+
A simple 2D drawing interface for PIL images.
|
| 55 |
+
<p>
|
| 56 |
+
Application code should use the <b>Draw</b> factory, instead of
|
| 57 |
+
directly.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class ImageDraw:
|
| 62 |
+
font: (
|
| 63 |
+
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
|
| 64 |
+
) = None
|
| 65 |
+
|
| 66 |
+
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Create a drawing instance.
|
| 69 |
+
|
| 70 |
+
:param im: The image to draw in.
|
| 71 |
+
:param mode: Optional mode to use for color values. For RGB
|
| 72 |
+
images, this argument can be RGB or RGBA (to blend the
|
| 73 |
+
drawing into the image). For all other modes, this argument
|
| 74 |
+
must be the same as the image mode. If omitted, the mode
|
| 75 |
+
defaults to the mode of the image.
|
| 76 |
+
"""
|
| 77 |
+
im._ensure_mutable()
|
| 78 |
+
blend = 0
|
| 79 |
+
if mode is None:
|
| 80 |
+
mode = im.mode
|
| 81 |
+
if mode != im.mode:
|
| 82 |
+
if mode == "RGBA" and im.mode == "RGB":
|
| 83 |
+
blend = 1
|
| 84 |
+
else:
|
| 85 |
+
msg = "mode mismatch"
|
| 86 |
+
raise ValueError(msg)
|
| 87 |
+
if mode == "P":
|
| 88 |
+
self.palette = im.palette
|
| 89 |
+
else:
|
| 90 |
+
self.palette = None
|
| 91 |
+
self._image = im
|
| 92 |
+
self.im = im.im
|
| 93 |
+
self.draw = Image.core.draw(self.im, blend)
|
| 94 |
+
self.mode = mode
|
| 95 |
+
if mode in ("I", "F"):
|
| 96 |
+
self.ink = self.draw.draw_ink(1)
|
| 97 |
+
else:
|
| 98 |
+
self.ink = self.draw.draw_ink(-1)
|
| 99 |
+
if mode in ("1", "P", "I", "F"):
|
| 100 |
+
# FIXME: fix Fill2 to properly support matte for I+F images
|
| 101 |
+
self.fontmode = "1"
|
| 102 |
+
else:
|
| 103 |
+
self.fontmode = "L" # aliasing is okay for other modes
|
| 104 |
+
self.fill = False
|
| 105 |
+
|
| 106 |
+
def getfont(
|
| 107 |
+
self,
|
| 108 |
+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
|
| 109 |
+
"""
|
| 110 |
+
Get the current default font.
|
| 111 |
+
|
| 112 |
+
To set the default font for this ImageDraw instance::
|
| 113 |
+
|
| 114 |
+
from PIL import ImageDraw, ImageFont
|
| 115 |
+
draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
| 116 |
+
|
| 117 |
+
To set the default font for all future ImageDraw instances::
|
| 118 |
+
|
| 119 |
+
from PIL import ImageDraw, ImageFont
|
| 120 |
+
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
| 121 |
+
|
| 122 |
+
If the current default font is ``None``,
|
| 123 |
+
it is initialized with ``ImageFont.load_default()``.
|
| 124 |
+
|
| 125 |
+
:returns: An image font."""
|
| 126 |
+
if not self.font:
|
| 127 |
+
# FIXME: should add a font repository
|
| 128 |
+
from . import ImageFont
|
| 129 |
+
|
| 130 |
+
self.font = ImageFont.load_default()
|
| 131 |
+
return self.font
|
| 132 |
+
|
| 133 |
+
def _getfont(
|
| 134 |
+
self, font_size: float | None
|
| 135 |
+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
|
| 136 |
+
if font_size is not None:
|
| 137 |
+
from . import ImageFont
|
| 138 |
+
|
| 139 |
+
return ImageFont.load_default(font_size)
|
| 140 |
+
else:
|
| 141 |
+
return self.getfont()
|
| 142 |
+
|
| 143 |
+
def _getink(
|
| 144 |
+
self, ink: _Ink | None, fill: _Ink | None = None
|
| 145 |
+
) -> tuple[int | None, int | None]:
|
| 146 |
+
result_ink = None
|
| 147 |
+
result_fill = None
|
| 148 |
+
if ink is None and fill is None:
|
| 149 |
+
if self.fill:
|
| 150 |
+
result_fill = self.ink
|
| 151 |
+
else:
|
| 152 |
+
result_ink = self.ink
|
| 153 |
+
else:
|
| 154 |
+
if ink is not None:
|
| 155 |
+
if isinstance(ink, str):
|
| 156 |
+
ink = ImageColor.getcolor(ink, self.mode)
|
| 157 |
+
if self.palette and isinstance(ink, tuple):
|
| 158 |
+
ink = self.palette.getcolor(ink, self._image)
|
| 159 |
+
result_ink = self.draw.draw_ink(ink)
|
| 160 |
+
if fill is not None:
|
| 161 |
+
if isinstance(fill, str):
|
| 162 |
+
fill = ImageColor.getcolor(fill, self.mode)
|
| 163 |
+
if self.palette and isinstance(fill, tuple):
|
| 164 |
+
fill = self.palette.getcolor(fill, self._image)
|
| 165 |
+
result_fill = self.draw.draw_ink(fill)
|
| 166 |
+
return result_ink, result_fill
|
| 167 |
+
|
| 168 |
+
def arc(
|
| 169 |
+
self,
|
| 170 |
+
xy: Coords,
|
| 171 |
+
start: float,
|
| 172 |
+
end: float,
|
| 173 |
+
fill: _Ink | None = None,
|
| 174 |
+
width: int = 1,
|
| 175 |
+
) -> None:
|
| 176 |
+
"""Draw an arc."""
|
| 177 |
+
ink, fill = self._getink(fill)
|
| 178 |
+
if ink is not None:
|
| 179 |
+
self.draw.draw_arc(xy, start, end, ink, width)
|
| 180 |
+
|
| 181 |
+
def bitmap(
|
| 182 |
+
self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
|
| 183 |
+
) -> None:
|
| 184 |
+
"""Draw a bitmap."""
|
| 185 |
+
bitmap.load()
|
| 186 |
+
ink, fill = self._getink(fill)
|
| 187 |
+
if ink is None:
|
| 188 |
+
ink = fill
|
| 189 |
+
if ink is not None:
|
| 190 |
+
self.draw.draw_bitmap(xy, bitmap.im, ink)
|
| 191 |
+
|
| 192 |
+
def chord(
|
| 193 |
+
self,
|
| 194 |
+
xy: Coords,
|
| 195 |
+
start: float,
|
| 196 |
+
end: float,
|
| 197 |
+
fill: _Ink | None = None,
|
| 198 |
+
outline: _Ink | None = None,
|
| 199 |
+
width: int = 1,
|
| 200 |
+
) -> None:
|
| 201 |
+
"""Draw a chord."""
|
| 202 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 203 |
+
if fill_ink is not None:
|
| 204 |
+
self.draw.draw_chord(xy, start, end, fill_ink, 1)
|
| 205 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 206 |
+
self.draw.draw_chord(xy, start, end, ink, 0, width)
|
| 207 |
+
|
| 208 |
+
def ellipse(
|
| 209 |
+
self,
|
| 210 |
+
xy: Coords,
|
| 211 |
+
fill: _Ink | None = None,
|
| 212 |
+
outline: _Ink | None = None,
|
| 213 |
+
width: int = 1,
|
| 214 |
+
) -> None:
|
| 215 |
+
"""Draw an ellipse."""
|
| 216 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 217 |
+
if fill_ink is not None:
|
| 218 |
+
self.draw.draw_ellipse(xy, fill_ink, 1)
|
| 219 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 220 |
+
self.draw.draw_ellipse(xy, ink, 0, width)
|
| 221 |
+
|
| 222 |
+
def circle(
|
| 223 |
+
self,
|
| 224 |
+
xy: Sequence[float],
|
| 225 |
+
radius: float,
|
| 226 |
+
fill: _Ink | None = None,
|
| 227 |
+
outline: _Ink | None = None,
|
| 228 |
+
width: int = 1,
|
| 229 |
+
) -> None:
|
| 230 |
+
"""Draw a circle given center coordinates and a radius."""
|
| 231 |
+
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
|
| 232 |
+
self.ellipse(ellipse_xy, fill, outline, width)
|
| 233 |
+
|
| 234 |
+
def line(
|
| 235 |
+
self,
|
| 236 |
+
xy: Coords,
|
| 237 |
+
fill: _Ink | None = None,
|
| 238 |
+
width: int = 0,
|
| 239 |
+
joint: str | None = None,
|
| 240 |
+
) -> None:
|
| 241 |
+
"""Draw a line, or a connected sequence of line segments."""
|
| 242 |
+
ink = self._getink(fill)[0]
|
| 243 |
+
if ink is not None:
|
| 244 |
+
self.draw.draw_lines(xy, ink, width)
|
| 245 |
+
if joint == "curve" and width > 4:
|
| 246 |
+
points: Sequence[Sequence[float]]
|
| 247 |
+
if isinstance(xy[0], (list, tuple)):
|
| 248 |
+
points = cast(Sequence[Sequence[float]], xy)
|
| 249 |
+
else:
|
| 250 |
+
points = [
|
| 251 |
+
cast(Sequence[float], tuple(xy[i : i + 2]))
|
| 252 |
+
for i in range(0, len(xy), 2)
|
| 253 |
+
]
|
| 254 |
+
for i in range(1, len(points) - 1):
|
| 255 |
+
point = points[i]
|
| 256 |
+
angles = [
|
| 257 |
+
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
|
| 258 |
+
% 360
|
| 259 |
+
for start, end in (
|
| 260 |
+
(points[i - 1], point),
|
| 261 |
+
(point, points[i + 1]),
|
| 262 |
+
)
|
| 263 |
+
]
|
| 264 |
+
if angles[0] == angles[1]:
|
| 265 |
+
# This is a straight line, so no joint is required
|
| 266 |
+
continue
|
| 267 |
+
|
| 268 |
+
def coord_at_angle(
|
| 269 |
+
coord: Sequence[float], angle: float
|
| 270 |
+
) -> tuple[float, ...]:
|
| 271 |
+
x, y = coord
|
| 272 |
+
angle -= 90
|
| 273 |
+
distance = width / 2 - 1
|
| 274 |
+
return tuple(
|
| 275 |
+
p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
|
| 276 |
+
for p, p_d in (
|
| 277 |
+
(x, distance * math.cos(math.radians(angle))),
|
| 278 |
+
(y, distance * math.sin(math.radians(angle))),
|
| 279 |
+
)
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
flipped = (
|
| 283 |
+
angles[1] > angles[0] and angles[1] - 180 > angles[0]
|
| 284 |
+
) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
|
| 285 |
+
coords = [
|
| 286 |
+
(point[0] - width / 2 + 1, point[1] - width / 2 + 1),
|
| 287 |
+
(point[0] + width / 2 - 1, point[1] + width / 2 - 1),
|
| 288 |
+
]
|
| 289 |
+
if flipped:
|
| 290 |
+
start, end = (angles[1] + 90, angles[0] + 90)
|
| 291 |
+
else:
|
| 292 |
+
start, end = (angles[0] - 90, angles[1] - 90)
|
| 293 |
+
self.pieslice(coords, start - 90, end - 90, fill)
|
| 294 |
+
|
| 295 |
+
if width > 8:
|
| 296 |
+
# Cover potential gaps between the line and the joint
|
| 297 |
+
if flipped:
|
| 298 |
+
gap_coords = [
|
| 299 |
+
coord_at_angle(point, angles[0] + 90),
|
| 300 |
+
point,
|
| 301 |
+
coord_at_angle(point, angles[1] + 90),
|
| 302 |
+
]
|
| 303 |
+
else:
|
| 304 |
+
gap_coords = [
|
| 305 |
+
coord_at_angle(point, angles[0] - 90),
|
| 306 |
+
point,
|
| 307 |
+
coord_at_angle(point, angles[1] - 90),
|
| 308 |
+
]
|
| 309 |
+
self.line(gap_coords, fill, width=3)
|
| 310 |
+
|
| 311 |
+
def shape(
|
| 312 |
+
self,
|
| 313 |
+
shape: Image.core._Outline,
|
| 314 |
+
fill: _Ink | None = None,
|
| 315 |
+
outline: _Ink | None = None,
|
| 316 |
+
) -> None:
|
| 317 |
+
"""(Experimental) Draw a shape."""
|
| 318 |
+
shape.close()
|
| 319 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 320 |
+
if fill_ink is not None:
|
| 321 |
+
self.draw.draw_outline(shape, fill_ink, 1)
|
| 322 |
+
if ink is not None and ink != fill_ink:
|
| 323 |
+
self.draw.draw_outline(shape, ink, 0)
|
| 324 |
+
|
| 325 |
+
def pieslice(
|
| 326 |
+
self,
|
| 327 |
+
xy: Coords,
|
| 328 |
+
start: float,
|
| 329 |
+
end: float,
|
| 330 |
+
fill: _Ink | None = None,
|
| 331 |
+
outline: _Ink | None = None,
|
| 332 |
+
width: int = 1,
|
| 333 |
+
) -> None:
|
| 334 |
+
"""Draw a pieslice."""
|
| 335 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 336 |
+
if fill_ink is not None:
|
| 337 |
+
self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
|
| 338 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 339 |
+
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
|
| 340 |
+
|
| 341 |
+
def point(self, xy: Coords, fill: _Ink | None = None) -> None:
|
| 342 |
+
"""Draw one or more individual pixels."""
|
| 343 |
+
ink, fill = self._getink(fill)
|
| 344 |
+
if ink is not None:
|
| 345 |
+
self.draw.draw_points(xy, ink)
|
| 346 |
+
|
| 347 |
+
def polygon(
|
| 348 |
+
self,
|
| 349 |
+
xy: Coords,
|
| 350 |
+
fill: _Ink | None = None,
|
| 351 |
+
outline: _Ink | None = None,
|
| 352 |
+
width: int = 1,
|
| 353 |
+
) -> None:
|
| 354 |
+
"""Draw a polygon."""
|
| 355 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 356 |
+
if fill_ink is not None:
|
| 357 |
+
self.draw.draw_polygon(xy, fill_ink, 1)
|
| 358 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 359 |
+
if width == 1:
|
| 360 |
+
self.draw.draw_polygon(xy, ink, 0, width)
|
| 361 |
+
elif self.im is not None:
|
| 362 |
+
# To avoid expanding the polygon outwards,
|
| 363 |
+
# use the fill as a mask
|
| 364 |
+
mask = Image.new("1", self.im.size)
|
| 365 |
+
mask_ink = self._getink(1)[0]
|
| 366 |
+
draw = Draw(mask)
|
| 367 |
+
draw.draw.draw_polygon(xy, mask_ink, 1)
|
| 368 |
+
|
| 369 |
+
self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
|
| 370 |
+
|
| 371 |
+
def regular_polygon(
|
| 372 |
+
self,
|
| 373 |
+
bounding_circle: Sequence[Sequence[float] | float],
|
| 374 |
+
n_sides: int,
|
| 375 |
+
rotation: float = 0,
|
| 376 |
+
fill: _Ink | None = None,
|
| 377 |
+
outline: _Ink | None = None,
|
| 378 |
+
width: int = 1,
|
| 379 |
+
) -> None:
|
| 380 |
+
"""Draw a regular polygon."""
|
| 381 |
+
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
| 382 |
+
self.polygon(xy, fill, outline, width)
|
| 383 |
+
|
| 384 |
+
def rectangle(
|
| 385 |
+
self,
|
| 386 |
+
xy: Coords,
|
| 387 |
+
fill: _Ink | None = None,
|
| 388 |
+
outline: _Ink | None = None,
|
| 389 |
+
width: int = 1,
|
| 390 |
+
) -> None:
|
| 391 |
+
"""Draw a rectangle."""
|
| 392 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 393 |
+
if fill_ink is not None:
|
| 394 |
+
self.draw.draw_rectangle(xy, fill_ink, 1)
|
| 395 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 396 |
+
self.draw.draw_rectangle(xy, ink, 0, width)
|
| 397 |
+
|
| 398 |
+
def rounded_rectangle(
|
| 399 |
+
self,
|
| 400 |
+
xy: Coords,
|
| 401 |
+
radius: float = 0,
|
| 402 |
+
fill: _Ink | None = None,
|
| 403 |
+
outline: _Ink | None = None,
|
| 404 |
+
width: int = 1,
|
| 405 |
+
*,
|
| 406 |
+
corners: tuple[bool, bool, bool, bool] | None = None,
|
| 407 |
+
) -> None:
|
| 408 |
+
"""Draw a rounded rectangle."""
|
| 409 |
+
if isinstance(xy[0], (list, tuple)):
|
| 410 |
+
(x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
|
| 411 |
+
else:
|
| 412 |
+
x0, y0, x1, y1 = cast(Sequence[float], xy)
|
| 413 |
+
if x1 < x0:
|
| 414 |
+
msg = "x1 must be greater than or equal to x0"
|
| 415 |
+
raise ValueError(msg)
|
| 416 |
+
if y1 < y0:
|
| 417 |
+
msg = "y1 must be greater than or equal to y0"
|
| 418 |
+
raise ValueError(msg)
|
| 419 |
+
if corners is None:
|
| 420 |
+
corners = (True, True, True, True)
|
| 421 |
+
|
| 422 |
+
d = radius * 2
|
| 423 |
+
|
| 424 |
+
x0 = round(x0)
|
| 425 |
+
y0 = round(y0)
|
| 426 |
+
x1 = round(x1)
|
| 427 |
+
y1 = round(y1)
|
| 428 |
+
full_x, full_y = False, False
|
| 429 |
+
if all(corners):
|
| 430 |
+
full_x = d >= x1 - x0 - 1
|
| 431 |
+
if full_x:
|
| 432 |
+
# The two left and two right corners are joined
|
| 433 |
+
d = x1 - x0
|
| 434 |
+
full_y = d >= y1 - y0 - 1
|
| 435 |
+
if full_y:
|
| 436 |
+
# The two top and two bottom corners are joined
|
| 437 |
+
d = y1 - y0
|
| 438 |
+
if full_x and full_y:
|
| 439 |
+
# If all corners are joined, that is a circle
|
| 440 |
+
return self.ellipse(xy, fill, outline, width)
|
| 441 |
+
|
| 442 |
+
if d == 0 or not any(corners):
|
| 443 |
+
# If the corners have no curve,
|
| 444 |
+
# or there are no corners,
|
| 445 |
+
# that is a rectangle
|
| 446 |
+
return self.rectangle(xy, fill, outline, width)
|
| 447 |
+
|
| 448 |
+
r = int(d // 2)
|
| 449 |
+
ink, fill_ink = self._getink(outline, fill)
|
| 450 |
+
|
| 451 |
+
def draw_corners(pieslice: bool) -> None:
|
| 452 |
+
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
|
| 453 |
+
if full_x:
|
| 454 |
+
# Draw top and bottom halves
|
| 455 |
+
parts = (
|
| 456 |
+
((x0, y0, x0 + d, y0 + d), 180, 360),
|
| 457 |
+
((x0, y1 - d, x0 + d, y1), 0, 180),
|
| 458 |
+
)
|
| 459 |
+
elif full_y:
|
| 460 |
+
# Draw left and right halves
|
| 461 |
+
parts = (
|
| 462 |
+
((x0, y0, x0 + d, y0 + d), 90, 270),
|
| 463 |
+
((x1 - d, y0, x1, y0 + d), 270, 90),
|
| 464 |
+
)
|
| 465 |
+
else:
|
| 466 |
+
# Draw four separate corners
|
| 467 |
+
parts = tuple(
|
| 468 |
+
part
|
| 469 |
+
for i, part in enumerate(
|
| 470 |
+
(
|
| 471 |
+
((x0, y0, x0 + d, y0 + d), 180, 270),
|
| 472 |
+
((x1 - d, y0, x1, y0 + d), 270, 360),
|
| 473 |
+
((x1 - d, y1 - d, x1, y1), 0, 90),
|
| 474 |
+
((x0, y1 - d, x0 + d, y1), 90, 180),
|
| 475 |
+
)
|
| 476 |
+
)
|
| 477 |
+
if corners[i]
|
| 478 |
+
)
|
| 479 |
+
for part in parts:
|
| 480 |
+
if pieslice:
|
| 481 |
+
self.draw.draw_pieslice(*(part + (fill_ink, 1)))
|
| 482 |
+
else:
|
| 483 |
+
self.draw.draw_arc(*(part + (ink, width)))
|
| 484 |
+
|
| 485 |
+
if fill_ink is not None:
|
| 486 |
+
draw_corners(True)
|
| 487 |
+
|
| 488 |
+
if full_x:
|
| 489 |
+
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
|
| 490 |
+
elif x1 - r - 1 > x0 + r + 1:
|
| 491 |
+
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
|
| 492 |
+
if not full_x and not full_y:
|
| 493 |
+
left = [x0, y0, x0 + r, y1]
|
| 494 |
+
if corners[0]:
|
| 495 |
+
left[1] += r + 1
|
| 496 |
+
if corners[3]:
|
| 497 |
+
left[3] -= r + 1
|
| 498 |
+
self.draw.draw_rectangle(left, fill_ink, 1)
|
| 499 |
+
|
| 500 |
+
right = [x1 - r, y0, x1, y1]
|
| 501 |
+
if corners[1]:
|
| 502 |
+
right[1] += r + 1
|
| 503 |
+
if corners[2]:
|
| 504 |
+
right[3] -= r + 1
|
| 505 |
+
self.draw.draw_rectangle(right, fill_ink, 1)
|
| 506 |
+
if ink is not None and ink != fill_ink and width != 0:
|
| 507 |
+
draw_corners(False)
|
| 508 |
+
|
| 509 |
+
if not full_x:
|
| 510 |
+
top = [x0, y0, x1, y0 + width - 1]
|
| 511 |
+
if corners[0]:
|
| 512 |
+
top[0] += r + 1
|
| 513 |
+
if corners[1]:
|
| 514 |
+
top[2] -= r + 1
|
| 515 |
+
self.draw.draw_rectangle(top, ink, 1)
|
| 516 |
+
|
| 517 |
+
bottom = [x0, y1 - width + 1, x1, y1]
|
| 518 |
+
if corners[3]:
|
| 519 |
+
bottom[0] += r + 1
|
| 520 |
+
if corners[2]:
|
| 521 |
+
bottom[2] -= r + 1
|
| 522 |
+
self.draw.draw_rectangle(bottom, ink, 1)
|
| 523 |
+
if not full_y:
|
| 524 |
+
left = [x0, y0, x0 + width - 1, y1]
|
| 525 |
+
if corners[0]:
|
| 526 |
+
left[1] += r + 1
|
| 527 |
+
if corners[3]:
|
| 528 |
+
left[3] -= r + 1
|
| 529 |
+
self.draw.draw_rectangle(left, ink, 1)
|
| 530 |
+
|
| 531 |
+
right = [x1 - width + 1, y0, x1, y1]
|
| 532 |
+
if corners[1]:
|
| 533 |
+
right[1] += r + 1
|
| 534 |
+
if corners[2]:
|
| 535 |
+
right[3] -= r + 1
|
| 536 |
+
self.draw.draw_rectangle(right, ink, 1)
|
| 537 |
+
|
| 538 |
+
def text(
|
| 539 |
+
self,
|
| 540 |
+
xy: tuple[float, float],
|
| 541 |
+
text: AnyStr | ImageText.Text,
|
| 542 |
+
fill: _Ink | None = None,
|
| 543 |
+
font: (
|
| 544 |
+
ImageFont.ImageFont
|
| 545 |
+
| ImageFont.FreeTypeFont
|
| 546 |
+
| ImageFont.TransposedFont
|
| 547 |
+
| None
|
| 548 |
+
) = None,
|
| 549 |
+
anchor: str | None = None,
|
| 550 |
+
spacing: float = 4,
|
| 551 |
+
align: str = "left",
|
| 552 |
+
direction: str | None = None,
|
| 553 |
+
features: list[str] | None = None,
|
| 554 |
+
language: str | None = None,
|
| 555 |
+
stroke_width: float = 0,
|
| 556 |
+
stroke_fill: _Ink | None = None,
|
| 557 |
+
embedded_color: bool = False,
|
| 558 |
+
*args: Any,
|
| 559 |
+
**kwargs: Any,
|
| 560 |
+
) -> None:
|
| 561 |
+
"""Draw text."""
|
| 562 |
+
if isinstance(text, ImageText.Text):
|
| 563 |
+
image_text = text
|
| 564 |
+
else:
|
| 565 |
+
if font is None:
|
| 566 |
+
font = self._getfont(kwargs.get("font_size"))
|
| 567 |
+
image_text = ImageText.Text(
|
| 568 |
+
text, font, self.mode, spacing, direction, features, language
|
| 569 |
+
)
|
| 570 |
+
if embedded_color:
|
| 571 |
+
image_text.embed_color()
|
| 572 |
+
if stroke_width:
|
| 573 |
+
image_text.stroke(stroke_width, stroke_fill)
|
| 574 |
+
|
| 575 |
+
def getink(fill: _Ink | None) -> int:
|
| 576 |
+
ink, fill_ink = self._getink(fill)
|
| 577 |
+
if ink is None:
|
| 578 |
+
assert fill_ink is not None
|
| 579 |
+
return fill_ink
|
| 580 |
+
return ink
|
| 581 |
+
|
| 582 |
+
ink = getink(fill)
|
| 583 |
+
if ink is None:
|
| 584 |
+
return
|
| 585 |
+
|
| 586 |
+
stroke_ink = None
|
| 587 |
+
if image_text.stroke_width:
|
| 588 |
+
stroke_ink = (
|
| 589 |
+
getink(image_text.stroke_fill)
|
| 590 |
+
if image_text.stroke_fill is not None
|
| 591 |
+
else ink
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
for xy, anchor, line in image_text._split(xy, anchor, align):
|
| 595 |
+
|
| 596 |
+
def draw_text(ink: int, stroke_width: float = 0) -> None:
|
| 597 |
+
mode = self.fontmode
|
| 598 |
+
if stroke_width == 0 and embedded_color:
|
| 599 |
+
mode = "RGBA"
|
| 600 |
+
coord = []
|
| 601 |
+
for i in range(2):
|
| 602 |
+
coord.append(int(xy[i]))
|
| 603 |
+
start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
|
| 604 |
+
try:
|
| 605 |
+
mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc]
|
| 606 |
+
line,
|
| 607 |
+
mode,
|
| 608 |
+
direction=direction,
|
| 609 |
+
features=features,
|
| 610 |
+
language=language,
|
| 611 |
+
stroke_width=stroke_width,
|
| 612 |
+
stroke_filled=True,
|
| 613 |
+
anchor=anchor,
|
| 614 |
+
ink=ink,
|
| 615 |
+
start=start,
|
| 616 |
+
*args,
|
| 617 |
+
**kwargs,
|
| 618 |
+
)
|
| 619 |
+
coord = [coord[0] + offset[0], coord[1] + offset[1]]
|
| 620 |
+
except AttributeError:
|
| 621 |
+
try:
|
| 622 |
+
mask = image_text.font.getmask( # type: ignore[misc]
|
| 623 |
+
line,
|
| 624 |
+
mode,
|
| 625 |
+
direction,
|
| 626 |
+
features,
|
| 627 |
+
language,
|
| 628 |
+
stroke_width,
|
| 629 |
+
anchor,
|
| 630 |
+
ink,
|
| 631 |
+
start=start,
|
| 632 |
+
*args,
|
| 633 |
+
**kwargs,
|
| 634 |
+
)
|
| 635 |
+
except TypeError:
|
| 636 |
+
mask = image_text.font.getmask(line)
|
| 637 |
+
if mode == "RGBA":
|
| 638 |
+
# image_text.font.getmask2(mode="RGBA")
|
| 639 |
+
# returns color in RGB bands and mask in A
|
| 640 |
+
# extract mask and set text alpha
|
| 641 |
+
color, mask = mask, mask.getband(3)
|
| 642 |
+
ink_alpha = struct.pack("i", ink)[3]
|
| 643 |
+
color.fillband(3, ink_alpha)
|
| 644 |
+
x, y = coord
|
| 645 |
+
if self.im is not None:
|
| 646 |
+
self.im.paste(
|
| 647 |
+
color, (x, y, x + mask.size[0], y + mask.size[1]), mask
|
| 648 |
+
)
|
| 649 |
+
else:
|
| 650 |
+
self.draw.draw_bitmap(coord, mask, ink)
|
| 651 |
+
|
| 652 |
+
if stroke_ink is not None:
|
| 653 |
+
# Draw stroked text
|
| 654 |
+
draw_text(stroke_ink, image_text.stroke_width)
|
| 655 |
+
|
| 656 |
+
# Draw normal text
|
| 657 |
+
if ink != stroke_ink:
|
| 658 |
+
draw_text(ink)
|
| 659 |
+
else:
|
| 660 |
+
# Only draw normal text
|
| 661 |
+
draw_text(ink)
|
| 662 |
+
|
| 663 |
+
def multiline_text(
|
| 664 |
+
self,
|
| 665 |
+
xy: tuple[float, float],
|
| 666 |
+
text: AnyStr,
|
| 667 |
+
fill: _Ink | None = None,
|
| 668 |
+
font: (
|
| 669 |
+
ImageFont.ImageFont
|
| 670 |
+
| ImageFont.FreeTypeFont
|
| 671 |
+
| ImageFont.TransposedFont
|
| 672 |
+
| None
|
| 673 |
+
) = None,
|
| 674 |
+
anchor: str | None = None,
|
| 675 |
+
spacing: float = 4,
|
| 676 |
+
align: str = "left",
|
| 677 |
+
direction: str | None = None,
|
| 678 |
+
features: list[str] | None = None,
|
| 679 |
+
language: str | None = None,
|
| 680 |
+
stroke_width: float = 0,
|
| 681 |
+
stroke_fill: _Ink | None = None,
|
| 682 |
+
embedded_color: bool = False,
|
| 683 |
+
*,
|
| 684 |
+
font_size: float | None = None,
|
| 685 |
+
) -> None:
|
| 686 |
+
return self.text(
|
| 687 |
+
xy,
|
| 688 |
+
text,
|
| 689 |
+
fill,
|
| 690 |
+
font,
|
| 691 |
+
anchor,
|
| 692 |
+
spacing,
|
| 693 |
+
align,
|
| 694 |
+
direction,
|
| 695 |
+
features,
|
| 696 |
+
language,
|
| 697 |
+
stroke_width,
|
| 698 |
+
stroke_fill,
|
| 699 |
+
embedded_color,
|
| 700 |
+
font_size=font_size,
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
def textlength(
|
| 704 |
+
self,
|
| 705 |
+
text: AnyStr,
|
| 706 |
+
font: (
|
| 707 |
+
ImageFont.ImageFont
|
| 708 |
+
| ImageFont.FreeTypeFont
|
| 709 |
+
| ImageFont.TransposedFont
|
| 710 |
+
| None
|
| 711 |
+
) = None,
|
| 712 |
+
direction: str | None = None,
|
| 713 |
+
features: list[str] | None = None,
|
| 714 |
+
language: str | None = None,
|
| 715 |
+
embedded_color: bool = False,
|
| 716 |
+
*,
|
| 717 |
+
font_size: float | None = None,
|
| 718 |
+
) -> float:
|
| 719 |
+
"""Get the length of a given string, in pixels with 1/64 precision."""
|
| 720 |
+
if font is None:
|
| 721 |
+
font = self._getfont(font_size)
|
| 722 |
+
image_text = ImageText.Text(
|
| 723 |
+
text,
|
| 724 |
+
font,
|
| 725 |
+
self.mode,
|
| 726 |
+
direction=direction,
|
| 727 |
+
features=features,
|
| 728 |
+
language=language,
|
| 729 |
+
)
|
| 730 |
+
if embedded_color:
|
| 731 |
+
image_text.embed_color()
|
| 732 |
+
return image_text.get_length()
|
| 733 |
+
|
| 734 |
+
def textbbox(
|
| 735 |
+
self,
|
| 736 |
+
xy: tuple[float, float],
|
| 737 |
+
text: AnyStr,
|
| 738 |
+
font: (
|
| 739 |
+
ImageFont.ImageFont
|
| 740 |
+
| ImageFont.FreeTypeFont
|
| 741 |
+
| ImageFont.TransposedFont
|
| 742 |
+
| None
|
| 743 |
+
) = None,
|
| 744 |
+
anchor: str | None = None,
|
| 745 |
+
spacing: float = 4,
|
| 746 |
+
align: str = "left",
|
| 747 |
+
direction: str | None = None,
|
| 748 |
+
features: list[str] | None = None,
|
| 749 |
+
language: str | None = None,
|
| 750 |
+
stroke_width: float = 0,
|
| 751 |
+
embedded_color: bool = False,
|
| 752 |
+
*,
|
| 753 |
+
font_size: float | None = None,
|
| 754 |
+
) -> tuple[float, float, float, float]:
|
| 755 |
+
"""Get the bounding box of a given string, in pixels."""
|
| 756 |
+
if font is None:
|
| 757 |
+
font = self._getfont(font_size)
|
| 758 |
+
image_text = ImageText.Text(
|
| 759 |
+
text, font, self.mode, spacing, direction, features, language
|
| 760 |
+
)
|
| 761 |
+
if embedded_color:
|
| 762 |
+
image_text.embed_color()
|
| 763 |
+
if stroke_width:
|
| 764 |
+
image_text.stroke(stroke_width)
|
| 765 |
+
return image_text.get_bbox(xy, anchor, align)
|
| 766 |
+
|
| 767 |
+
def multiline_textbbox(
|
| 768 |
+
self,
|
| 769 |
+
xy: tuple[float, float],
|
| 770 |
+
text: AnyStr,
|
| 771 |
+
font: (
|
| 772 |
+
ImageFont.ImageFont
|
| 773 |
+
| ImageFont.FreeTypeFont
|
| 774 |
+
| ImageFont.TransposedFont
|
| 775 |
+
| None
|
| 776 |
+
) = None,
|
| 777 |
+
anchor: str | None = None,
|
| 778 |
+
spacing: float = 4,
|
| 779 |
+
align: str = "left",
|
| 780 |
+
direction: str | None = None,
|
| 781 |
+
features: list[str] | None = None,
|
| 782 |
+
language: str | None = None,
|
| 783 |
+
stroke_width: float = 0,
|
| 784 |
+
embedded_color: bool = False,
|
| 785 |
+
*,
|
| 786 |
+
font_size: float | None = None,
|
| 787 |
+
) -> tuple[float, float, float, float]:
|
| 788 |
+
return self.textbbox(
|
| 789 |
+
xy,
|
| 790 |
+
text,
|
| 791 |
+
font,
|
| 792 |
+
anchor,
|
| 793 |
+
spacing,
|
| 794 |
+
align,
|
| 795 |
+
direction,
|
| 796 |
+
features,
|
| 797 |
+
language,
|
| 798 |
+
stroke_width,
|
| 799 |
+
embedded_color,
|
| 800 |
+
font_size=font_size,
|
| 801 |
+
)
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
|
| 805 |
+
"""
|
| 806 |
+
A simple 2D drawing interface for PIL images.
|
| 807 |
+
|
| 808 |
+
:param im: The image to draw in.
|
| 809 |
+
:param mode: Optional mode to use for color values. For RGB
|
| 810 |
+
images, this argument can be RGB or RGBA (to blend the
|
| 811 |
+
drawing into the image). For all other modes, this argument
|
| 812 |
+
must be the same as the image mode. If omitted, the mode
|
| 813 |
+
defaults to the mode of the image.
|
| 814 |
+
"""
|
| 815 |
+
try:
|
| 816 |
+
return getattr(im, "getdraw")(mode)
|
| 817 |
+
except AttributeError:
|
| 818 |
+
return ImageDraw(im, mode)
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
|
| 822 |
+
"""
|
| 823 |
+
:param im: The image to draw in.
|
| 824 |
+
:returns: A (drawing context, drawing resource factory) tuple.
|
| 825 |
+
"""
|
| 826 |
+
from . import ImageDraw2
|
| 827 |
+
|
| 828 |
+
draw = ImageDraw2.Draw(im) if im is not None else None
|
| 829 |
+
return draw, ImageDraw2
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
def floodfill(
|
| 833 |
+
image: Image.Image,
|
| 834 |
+
xy: tuple[int, int],
|
| 835 |
+
value: float | tuple[int, ...],
|
| 836 |
+
border: float | tuple[int, ...] | None = None,
|
| 837 |
+
thresh: float = 0,
|
| 838 |
+
) -> None:
|
| 839 |
+
"""
|
| 840 |
+
.. warning:: This method is experimental.
|
| 841 |
+
|
| 842 |
+
Fills a bounded region with a given color.
|
| 843 |
+
|
| 844 |
+
:param image: Target image.
|
| 845 |
+
:param xy: Seed position (a 2-item coordinate tuple). See
|
| 846 |
+
:ref:`coordinate-system`.
|
| 847 |
+
:param value: Fill color.
|
| 848 |
+
:param border: Optional border value. If given, the region consists of
|
| 849 |
+
pixels with a color different from the border color. If not given,
|
| 850 |
+
the region consists of pixels having the same color as the seed
|
| 851 |
+
pixel.
|
| 852 |
+
:param thresh: Optional threshold value which specifies a maximum
|
| 853 |
+
tolerable difference of a pixel value from the 'background' in
|
| 854 |
+
order for it to be replaced. Useful for filling regions of
|
| 855 |
+
non-homogeneous, but similar, colors.
|
| 856 |
+
"""
|
| 857 |
+
# based on an implementation by Eric S. Raymond
|
| 858 |
+
# amended by yo1995 @20180806
|
| 859 |
+
pixel = image.load()
|
| 860 |
+
assert pixel is not None
|
| 861 |
+
x, y = xy
|
| 862 |
+
try:
|
| 863 |
+
background = pixel[x, y]
|
| 864 |
+
if _color_diff(value, background) <= thresh:
|
| 865 |
+
return # seed point already has fill color
|
| 866 |
+
pixel[x, y] = value
|
| 867 |
+
except (ValueError, IndexError):
|
| 868 |
+
return # seed point outside image
|
| 869 |
+
edge = {(x, y)}
|
| 870 |
+
# use a set to keep record of current and previous edge pixels
|
| 871 |
+
# to reduce memory consumption
|
| 872 |
+
full_edge = set()
|
| 873 |
+
while edge:
|
| 874 |
+
new_edge = set()
|
| 875 |
+
for x, y in edge: # 4 adjacent method
|
| 876 |
+
for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
|
| 877 |
+
# If already processed, or if a coordinate is negative, skip
|
| 878 |
+
if (s, t) in full_edge or s < 0 or t < 0:
|
| 879 |
+
continue
|
| 880 |
+
try:
|
| 881 |
+
p = pixel[s, t]
|
| 882 |
+
except (ValueError, IndexError):
|
| 883 |
+
pass
|
| 884 |
+
else:
|
| 885 |
+
full_edge.add((s, t))
|
| 886 |
+
if border is None:
|
| 887 |
+
fill = _color_diff(p, background) <= thresh
|
| 888 |
+
else:
|
| 889 |
+
fill = p not in (value, border)
|
| 890 |
+
if fill:
|
| 891 |
+
pixel[s, t] = value
|
| 892 |
+
new_edge.add((s, t))
|
| 893 |
+
full_edge = edge # discard pixels processed
|
| 894 |
+
edge = new_edge
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def _compute_regular_polygon_vertices(
|
| 898 |
+
bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
|
| 899 |
+
) -> list[tuple[float, float]]:
|
| 900 |
+
"""
|
| 901 |
+
Generate a list of vertices for a 2D regular polygon.
|
| 902 |
+
|
| 903 |
+
:param bounding_circle: The bounding circle is a sequence defined
|
| 904 |
+
by a point and radius. The polygon is inscribed in this circle.
|
| 905 |
+
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
|
| 906 |
+
:param n_sides: Number of sides
|
| 907 |
+
(e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
|
| 908 |
+
:param rotation: Apply an arbitrary rotation to the polygon
|
| 909 |
+
(e.g. ``rotation=90``, applies a 90 degree rotation)
|
| 910 |
+
:return: List of regular polygon vertices
|
| 911 |
+
(e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
|
| 912 |
+
|
| 913 |
+
How are the vertices computed?
|
| 914 |
+
1. Compute the following variables
|
| 915 |
+
- theta: Angle between the apothem & the nearest polygon vertex
|
| 916 |
+
- side_length: Length of each polygon edge
|
| 917 |
+
- centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
|
| 918 |
+
- polygon_radius: Polygon radius (last element of bounding_circle)
|
| 919 |
+
- angles: Location of each polygon vertex in polar grid
|
| 920 |
+
(e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
|
| 921 |
+
|
| 922 |
+
2. For each angle in angles, get the polygon vertex at that angle
|
| 923 |
+
The vertex is computed using the equation below.
|
| 924 |
+
X= xcos(φ) + ysin(φ)
|
| 925 |
+
Y= −xsin(φ) + ycos(φ)
|
| 926 |
+
|
| 927 |
+
Note:
|
| 928 |
+
φ = angle in degrees
|
| 929 |
+
x = 0
|
| 930 |
+
y = polygon_radius
|
| 931 |
+
|
| 932 |
+
The formula above assumes rotation around the origin.
|
| 933 |
+
In our case, we are rotating around the centroid.
|
| 934 |
+
To account for this, we use the formula below
|
| 935 |
+
X = xcos(φ) + ysin(φ) + centroid_x
|
| 936 |
+
Y = −xsin(φ) + ycos(φ) + centroid_y
|
| 937 |
+
"""
|
| 938 |
+
# 1. Error Handling
|
| 939 |
+
# 1.1 Check `n_sides` has an appropriate value
|
| 940 |
+
if not isinstance(n_sides, int):
|
| 941 |
+
msg = "n_sides should be an int" # type: ignore[unreachable]
|
| 942 |
+
raise TypeError(msg)
|
| 943 |
+
if n_sides < 3:
|
| 944 |
+
msg = "n_sides should be an int > 2"
|
| 945 |
+
raise ValueError(msg)
|
| 946 |
+
|
| 947 |
+
# 1.2 Check `bounding_circle` has an appropriate value
|
| 948 |
+
if not isinstance(bounding_circle, (list, tuple)):
|
| 949 |
+
msg = "bounding_circle should be a sequence"
|
| 950 |
+
raise TypeError(msg)
|
| 951 |
+
|
| 952 |
+
if len(bounding_circle) == 3:
|
| 953 |
+
if not all(isinstance(i, (int, float)) for i in bounding_circle):
|
| 954 |
+
msg = "bounding_circle should only contain numeric data"
|
| 955 |
+
raise ValueError(msg)
|
| 956 |
+
|
| 957 |
+
*centroid, polygon_radius = cast(list[float], list(bounding_circle))
|
| 958 |
+
elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
|
| 959 |
+
if not all(
|
| 960 |
+
isinstance(i, (int, float)) for i in bounding_circle[0]
|
| 961 |
+
) or not isinstance(bounding_circle[1], (int, float)):
|
| 962 |
+
msg = "bounding_circle should only contain numeric data"
|
| 963 |
+
raise ValueError(msg)
|
| 964 |
+
|
| 965 |
+
if len(bounding_circle[0]) != 2:
|
| 966 |
+
msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
|
| 967 |
+
raise ValueError(msg)
|
| 968 |
+
|
| 969 |
+
centroid = cast(list[float], list(bounding_circle[0]))
|
| 970 |
+
polygon_radius = cast(float, bounding_circle[1])
|
| 971 |
+
else:
|
| 972 |
+
msg = (
|
| 973 |
+
"bounding_circle should contain 2D coordinates "
|
| 974 |
+
"and a radius (e.g. (x, y, r) or ((x, y), r) )"
|
| 975 |
+
)
|
| 976 |
+
raise ValueError(msg)
|
| 977 |
+
|
| 978 |
+
if polygon_radius <= 0:
|
| 979 |
+
msg = "bounding_circle radius should be > 0"
|
| 980 |
+
raise ValueError(msg)
|
| 981 |
+
|
| 982 |
+
# 1.3 Check `rotation` has an appropriate value
|
| 983 |
+
if not isinstance(rotation, (int, float)):
|
| 984 |
+
msg = "rotation should be an int or float" # type: ignore[unreachable]
|
| 985 |
+
raise ValueError(msg)
|
| 986 |
+
|
| 987 |
+
# 2. Define Helper Functions
|
| 988 |
+
def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
|
| 989 |
+
return (
|
| 990 |
+
round(
|
| 991 |
+
point[0] * math.cos(math.radians(360 - degrees))
|
| 992 |
+
- point[1] * math.sin(math.radians(360 - degrees))
|
| 993 |
+
+ centroid[0],
|
| 994 |
+
2,
|
| 995 |
+
),
|
| 996 |
+
round(
|
| 997 |
+
point[1] * math.cos(math.radians(360 - degrees))
|
| 998 |
+
+ point[0] * math.sin(math.radians(360 - degrees))
|
| 999 |
+
+ centroid[1],
|
| 1000 |
+
2,
|
| 1001 |
+
),
|
| 1002 |
+
)
|
| 1003 |
+
|
| 1004 |
+
def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
|
| 1005 |
+
start_point = [polygon_radius, 0]
|
| 1006 |
+
return _apply_rotation(start_point, angle)
|
| 1007 |
+
|
| 1008 |
+
def _get_angles(n_sides: int, rotation: float) -> list[float]:
|
| 1009 |
+
angles = []
|
| 1010 |
+
degrees = 360 / n_sides
|
| 1011 |
+
# Start with the bottom left polygon vertex
|
| 1012 |
+
current_angle = (270 - 0.5 * degrees) + rotation
|
| 1013 |
+
for _ in range(n_sides):
|
| 1014 |
+
angles.append(current_angle)
|
| 1015 |
+
current_angle += degrees
|
| 1016 |
+
if current_angle > 360:
|
| 1017 |
+
current_angle -= 360
|
| 1018 |
+
return angles
|
| 1019 |
+
|
| 1020 |
+
# 3. Variable Declarations
|
| 1021 |
+
angles = _get_angles(n_sides, rotation)
|
| 1022 |
+
|
| 1023 |
+
# 4. Compute Vertices
|
| 1024 |
+
return [_compute_polygon_vertex(angle) for angle in angles]
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
def _color_diff(
|
| 1028 |
+
color1: float | tuple[int, ...], color2: float | tuple[int, ...]
|
| 1029 |
+
) -> float:
|
| 1030 |
+
"""
|
| 1031 |
+
Uses 1-norm distance to calculate difference between two values.
|
| 1032 |
+
"""
|
| 1033 |
+
first = color1 if isinstance(color1, tuple) else (color1,)
|
| 1034 |
+
second = color2 if isinstance(color2, tuple) else (color2,)
|
| 1035 |
+
|
| 1036 |
+
return sum(abs(first[i] - second[i]) for i in range(len(second)))
|
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)
|
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"))
|
PIL/ImageFile.py
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 logging
|
| 35 |
+
import os
|
| 36 |
+
import struct
|
| 37 |
+
from typing import IO, Any, NamedTuple, cast
|
| 38 |
+
|
| 39 |
+
from . import ExifTags, Image
|
| 40 |
+
from ._util import DeferredError, is_path
|
| 41 |
+
|
| 42 |
+
TYPE_CHECKING = False
|
| 43 |
+
if TYPE_CHECKING:
|
| 44 |
+
from ._typing import StrOrBytesPath
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
MAXBLOCK = 65536
|
| 49 |
+
"""
|
| 50 |
+
By default, Pillow processes image data in blocks. This helps to prevent excessive use
|
| 51 |
+
of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
|
| 52 |
+
|
| 53 |
+
When reading an image, this is the number of bytes to read at once.
|
| 54 |
+
|
| 55 |
+
When writing an image, this is the number of bytes to write at once.
|
| 56 |
+
If the image width times 4 is greater, then that will be used instead.
|
| 57 |
+
Plugins may also set a greater number.
|
| 58 |
+
|
| 59 |
+
User code may set this to another number.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
SAFEBLOCK = 1024 * 1024
|
| 63 |
+
|
| 64 |
+
LOAD_TRUNCATED_IMAGES = False
|
| 65 |
+
"""Whether or not to load truncated image files. User code may change this."""
|
| 66 |
+
|
| 67 |
+
ERRORS = {
|
| 68 |
+
-1: "image buffer overrun error",
|
| 69 |
+
-2: "decoding error",
|
| 70 |
+
-3: "unknown error",
|
| 71 |
+
-8: "bad configuration",
|
| 72 |
+
-9: "out of memory error",
|
| 73 |
+
}
|
| 74 |
+
"""
|
| 75 |
+
Dict of known error codes returned from :meth:`.PyDecoder.decode`,
|
| 76 |
+
:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
|
| 77 |
+
:meth:`.PyEncoder.encode_to_file`.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
#
|
| 82 |
+
# --------------------------------------------------------------------
|
| 83 |
+
# Helpers
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _get_oserror(error: int, *, encoder: bool) -> OSError:
|
| 87 |
+
try:
|
| 88 |
+
msg = Image.core.getcodecstatus(error)
|
| 89 |
+
except AttributeError:
|
| 90 |
+
msg = ERRORS.get(error)
|
| 91 |
+
if not msg:
|
| 92 |
+
msg = f"{'encoder' if encoder else 'decoder'} error {error}"
|
| 93 |
+
msg += f" when {'writing' if encoder else 'reading'} image file"
|
| 94 |
+
return OSError(msg)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _tilesort(t: _Tile) -> int:
|
| 98 |
+
# sort on offset
|
| 99 |
+
return t[2]
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class _Tile(NamedTuple):
|
| 103 |
+
codec_name: str
|
| 104 |
+
extents: tuple[int, int, int, int] | None
|
| 105 |
+
offset: int = 0
|
| 106 |
+
args: tuple[Any, ...] | str | None = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
#
|
| 110 |
+
# --------------------------------------------------------------------
|
| 111 |
+
# ImageFile base class
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class ImageFile(Image.Image):
|
| 115 |
+
"""Base class for image file format handlers."""
|
| 116 |
+
|
| 117 |
+
def __init__(
|
| 118 |
+
self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
|
| 119 |
+
) -> None:
|
| 120 |
+
super().__init__()
|
| 121 |
+
|
| 122 |
+
self._min_frame = 0
|
| 123 |
+
|
| 124 |
+
self.custom_mimetype: str | None = None
|
| 125 |
+
|
| 126 |
+
self.tile: list[_Tile] = []
|
| 127 |
+
""" A list of tile descriptors """
|
| 128 |
+
|
| 129 |
+
self.readonly = 1 # until we know better
|
| 130 |
+
|
| 131 |
+
self.decoderconfig: tuple[Any, ...] = ()
|
| 132 |
+
self.decodermaxblock = MAXBLOCK
|
| 133 |
+
|
| 134 |
+
self.fp: IO[bytes] | None
|
| 135 |
+
self._fp: IO[bytes] | DeferredError
|
| 136 |
+
if is_path(fp):
|
| 137 |
+
# filename
|
| 138 |
+
self.fp = open(fp, "rb")
|
| 139 |
+
self.filename = os.fspath(fp)
|
| 140 |
+
self._exclusive_fp = True
|
| 141 |
+
else:
|
| 142 |
+
# stream
|
| 143 |
+
self.fp = cast(IO[bytes], fp)
|
| 144 |
+
self.filename = filename if filename is not None else ""
|
| 145 |
+
# can be overridden
|
| 146 |
+
self._exclusive_fp = False
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
try:
|
| 150 |
+
self._open()
|
| 151 |
+
except (
|
| 152 |
+
IndexError, # end of data
|
| 153 |
+
TypeError, # end of data (ord)
|
| 154 |
+
KeyError, # unsupported mode
|
| 155 |
+
EOFError, # got header but not the first frame
|
| 156 |
+
struct.error,
|
| 157 |
+
) as v:
|
| 158 |
+
raise SyntaxError(v) from v
|
| 159 |
+
|
| 160 |
+
if not self.mode or self.size[0] <= 0 or self.size[1] <= 0:
|
| 161 |
+
msg = "not identified by this driver"
|
| 162 |
+
raise SyntaxError(msg)
|
| 163 |
+
except BaseException:
|
| 164 |
+
# close the file only if we have opened it this constructor
|
| 165 |
+
if self._exclusive_fp:
|
| 166 |
+
self.fp.close()
|
| 167 |
+
raise
|
| 168 |
+
|
| 169 |
+
def _open(self) -> None:
|
| 170 |
+
pass
|
| 171 |
+
|
| 172 |
+
# Context manager support
|
| 173 |
+
def __enter__(self) -> ImageFile:
|
| 174 |
+
return self
|
| 175 |
+
|
| 176 |
+
def _close_fp(self) -> None:
|
| 177 |
+
if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
|
| 178 |
+
if self._fp != self.fp:
|
| 179 |
+
self._fp.close()
|
| 180 |
+
self._fp = DeferredError(ValueError("Operation on closed image"))
|
| 181 |
+
if self.fp:
|
| 182 |
+
self.fp.close()
|
| 183 |
+
|
| 184 |
+
def __exit__(self, *args: object) -> None:
|
| 185 |
+
if getattr(self, "_exclusive_fp", False):
|
| 186 |
+
self._close_fp()
|
| 187 |
+
self.fp = None
|
| 188 |
+
|
| 189 |
+
def close(self) -> None:
|
| 190 |
+
"""
|
| 191 |
+
Closes the file pointer, if possible.
|
| 192 |
+
|
| 193 |
+
This operation will destroy the image core and release its memory.
|
| 194 |
+
The image data will be unusable afterward.
|
| 195 |
+
|
| 196 |
+
This function is required to close images that have multiple frames or
|
| 197 |
+
have not had their file read and closed by the
|
| 198 |
+
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
|
| 199 |
+
more information.
|
| 200 |
+
"""
|
| 201 |
+
try:
|
| 202 |
+
self._close_fp()
|
| 203 |
+
self.fp = None
|
| 204 |
+
except Exception as msg:
|
| 205 |
+
logger.debug("Error closing: %s", msg)
|
| 206 |
+
|
| 207 |
+
super().close()
|
| 208 |
+
|
| 209 |
+
def get_child_images(self) -> list[ImageFile]:
|
| 210 |
+
child_images = []
|
| 211 |
+
exif = self.getexif()
|
| 212 |
+
ifds = []
|
| 213 |
+
if ExifTags.Base.SubIFDs in exif:
|
| 214 |
+
subifd_offsets = exif[ExifTags.Base.SubIFDs]
|
| 215 |
+
if subifd_offsets:
|
| 216 |
+
if not isinstance(subifd_offsets, tuple):
|
| 217 |
+
subifd_offsets = (subifd_offsets,)
|
| 218 |
+
for subifd_offset in subifd_offsets:
|
| 219 |
+
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
|
| 220 |
+
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
|
| 221 |
+
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
|
| 222 |
+
assert exif._info is not None
|
| 223 |
+
ifds.append((ifd1, exif._info.next))
|
| 224 |
+
|
| 225 |
+
offset = None
|
| 226 |
+
for ifd, ifd_offset in ifds:
|
| 227 |
+
assert self.fp is not None
|
| 228 |
+
current_offset = self.fp.tell()
|
| 229 |
+
if offset is None:
|
| 230 |
+
offset = current_offset
|
| 231 |
+
|
| 232 |
+
fp = self.fp
|
| 233 |
+
if ifd is not None:
|
| 234 |
+
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
|
| 235 |
+
if thumbnail_offset is not None:
|
| 236 |
+
thumbnail_offset += getattr(self, "_exif_offset", 0)
|
| 237 |
+
self.fp.seek(thumbnail_offset)
|
| 238 |
+
|
| 239 |
+
length = ifd.get(ExifTags.Base.JpegIFByteCount)
|
| 240 |
+
assert isinstance(length, int)
|
| 241 |
+
data = self.fp.read(length)
|
| 242 |
+
fp = io.BytesIO(data)
|
| 243 |
+
|
| 244 |
+
with Image.open(fp) as im:
|
| 245 |
+
from . import TiffImagePlugin
|
| 246 |
+
|
| 247 |
+
if thumbnail_offset is None and isinstance(
|
| 248 |
+
im, TiffImagePlugin.TiffImageFile
|
| 249 |
+
):
|
| 250 |
+
im._frame_pos = [ifd_offset]
|
| 251 |
+
im._seek(0)
|
| 252 |
+
im.load()
|
| 253 |
+
child_images.append(im)
|
| 254 |
+
|
| 255 |
+
if offset is not None:
|
| 256 |
+
assert self.fp is not None
|
| 257 |
+
self.fp.seek(offset)
|
| 258 |
+
return child_images
|
| 259 |
+
|
| 260 |
+
def get_format_mimetype(self) -> str | None:
|
| 261 |
+
if self.custom_mimetype:
|
| 262 |
+
return self.custom_mimetype
|
| 263 |
+
if self.format is not None:
|
| 264 |
+
return Image.MIME.get(self.format.upper())
|
| 265 |
+
return None
|
| 266 |
+
|
| 267 |
+
def __getstate__(self) -> list[Any]:
|
| 268 |
+
return super().__getstate__() + [self.filename]
|
| 269 |
+
|
| 270 |
+
def __setstate__(self, state: list[Any]) -> None:
|
| 271 |
+
self.tile = []
|
| 272 |
+
if len(state) > 5:
|
| 273 |
+
self.filename = state[5]
|
| 274 |
+
super().__setstate__(state)
|
| 275 |
+
|
| 276 |
+
def verify(self) -> None:
|
| 277 |
+
"""Check file integrity"""
|
| 278 |
+
|
| 279 |
+
# raise exception if something's wrong. must be called
|
| 280 |
+
# directly after open, and closes file when finished.
|
| 281 |
+
if self._exclusive_fp and self.fp:
|
| 282 |
+
self.fp.close()
|
| 283 |
+
self.fp = None
|
| 284 |
+
|
| 285 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 286 |
+
"""Load image data based on tile list"""
|
| 287 |
+
|
| 288 |
+
if not self.tile and self._im is None:
|
| 289 |
+
msg = "cannot load this image"
|
| 290 |
+
raise OSError(msg)
|
| 291 |
+
|
| 292 |
+
pixel = Image.Image.load(self)
|
| 293 |
+
if not self.tile:
|
| 294 |
+
return pixel
|
| 295 |
+
|
| 296 |
+
self.map: mmap.mmap | None = None
|
| 297 |
+
use_mmap = self.filename and len(self.tile) == 1
|
| 298 |
+
|
| 299 |
+
assert self.fp is not None
|
| 300 |
+
readonly = 0
|
| 301 |
+
|
| 302 |
+
# look for read/seek overrides
|
| 303 |
+
if hasattr(self, "load_read"):
|
| 304 |
+
read = self.load_read
|
| 305 |
+
# don't use mmap if there are custom read/seek functions
|
| 306 |
+
use_mmap = False
|
| 307 |
+
else:
|
| 308 |
+
read = self.fp.read
|
| 309 |
+
|
| 310 |
+
if hasattr(self, "load_seek"):
|
| 311 |
+
seek = self.load_seek
|
| 312 |
+
use_mmap = False
|
| 313 |
+
else:
|
| 314 |
+
seek = self.fp.seek
|
| 315 |
+
|
| 316 |
+
if use_mmap:
|
| 317 |
+
# try memory mapping
|
| 318 |
+
decoder_name, extents, offset, args = self.tile[0]
|
| 319 |
+
if isinstance(args, str):
|
| 320 |
+
args = (args, 0, 1)
|
| 321 |
+
if (
|
| 322 |
+
decoder_name == "raw"
|
| 323 |
+
and isinstance(args, tuple)
|
| 324 |
+
and len(args) >= 3
|
| 325 |
+
and args[0] == self.mode
|
| 326 |
+
and args[0] in Image._MAPMODES
|
| 327 |
+
):
|
| 328 |
+
if offset < 0:
|
| 329 |
+
msg = "Tile offset cannot be negative"
|
| 330 |
+
raise ValueError(msg)
|
| 331 |
+
try:
|
| 332 |
+
# use mmap, if possible
|
| 333 |
+
import mmap
|
| 334 |
+
|
| 335 |
+
with open(self.filename) as fp:
|
| 336 |
+
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
| 337 |
+
if offset + self.size[1] * args[1] > self.map.size():
|
| 338 |
+
msg = "buffer is not large enough"
|
| 339 |
+
raise OSError(msg)
|
| 340 |
+
self.im = Image.core.map_buffer(
|
| 341 |
+
self.map, self.size, decoder_name, offset, args
|
| 342 |
+
)
|
| 343 |
+
readonly = 1
|
| 344 |
+
# After trashing self.im,
|
| 345 |
+
# we might need to reload the palette data.
|
| 346 |
+
if self.palette:
|
| 347 |
+
self.palette.dirty = 1
|
| 348 |
+
except (AttributeError, OSError, ImportError):
|
| 349 |
+
self.map = None
|
| 350 |
+
|
| 351 |
+
self.load_prepare()
|
| 352 |
+
err_code = -3 # initialize to unknown error
|
| 353 |
+
if not self.map:
|
| 354 |
+
# sort tiles in file order
|
| 355 |
+
self.tile.sort(key=_tilesort)
|
| 356 |
+
|
| 357 |
+
# FIXME: This is a hack to handle TIFF's JpegTables tag.
|
| 358 |
+
prefix = getattr(self, "tile_prefix", b"")
|
| 359 |
+
|
| 360 |
+
# Remove consecutive duplicates that only differ by their offset
|
| 361 |
+
self.tile = [
|
| 362 |
+
list(tiles)[-1]
|
| 363 |
+
for _, tiles in itertools.groupby(
|
| 364 |
+
self.tile, lambda tile: (tile[0], tile[1], tile[3])
|
| 365 |
+
)
|
| 366 |
+
]
|
| 367 |
+
for i, (decoder_name, extents, offset, args) in enumerate(self.tile):
|
| 368 |
+
seek(offset)
|
| 369 |
+
decoder = Image._getdecoder(
|
| 370 |
+
self.mode, decoder_name, args, self.decoderconfig
|
| 371 |
+
)
|
| 372 |
+
try:
|
| 373 |
+
decoder.setimage(self.im, extents)
|
| 374 |
+
if decoder.pulls_fd:
|
| 375 |
+
decoder.setfd(self.fp)
|
| 376 |
+
err_code = decoder.decode(b"")[1]
|
| 377 |
+
else:
|
| 378 |
+
b = prefix
|
| 379 |
+
while True:
|
| 380 |
+
read_bytes = self.decodermaxblock
|
| 381 |
+
if i + 1 < len(self.tile):
|
| 382 |
+
next_offset = self.tile[i + 1].offset
|
| 383 |
+
if next_offset > offset:
|
| 384 |
+
read_bytes = next_offset - offset
|
| 385 |
+
try:
|
| 386 |
+
s = read(read_bytes)
|
| 387 |
+
except (IndexError, struct.error) as e:
|
| 388 |
+
# truncated png/gif
|
| 389 |
+
if LOAD_TRUNCATED_IMAGES:
|
| 390 |
+
break
|
| 391 |
+
else:
|
| 392 |
+
msg = "image file is truncated"
|
| 393 |
+
raise OSError(msg) from e
|
| 394 |
+
|
| 395 |
+
if not s: # truncated jpeg
|
| 396 |
+
if LOAD_TRUNCATED_IMAGES:
|
| 397 |
+
break
|
| 398 |
+
else:
|
| 399 |
+
msg = (
|
| 400 |
+
"image file is truncated "
|
| 401 |
+
f"({len(b)} bytes not processed)"
|
| 402 |
+
)
|
| 403 |
+
raise OSError(msg)
|
| 404 |
+
|
| 405 |
+
b = b + s
|
| 406 |
+
n, err_code = decoder.decode(b)
|
| 407 |
+
if n < 0:
|
| 408 |
+
break
|
| 409 |
+
b = b[n:]
|
| 410 |
+
finally:
|
| 411 |
+
# Need to cleanup here to prevent leaks
|
| 412 |
+
decoder.cleanup()
|
| 413 |
+
|
| 414 |
+
self.tile = []
|
| 415 |
+
self.readonly = readonly
|
| 416 |
+
|
| 417 |
+
self.load_end()
|
| 418 |
+
|
| 419 |
+
if self._exclusive_fp and self._close_exclusive_fp_after_loading:
|
| 420 |
+
self.fp.close()
|
| 421 |
+
self.fp = None
|
| 422 |
+
|
| 423 |
+
if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
|
| 424 |
+
# still raised if decoder fails to return anything
|
| 425 |
+
raise _get_oserror(err_code, encoder=False)
|
| 426 |
+
|
| 427 |
+
return Image.Image.load(self)
|
| 428 |
+
|
| 429 |
+
def load_prepare(self) -> None:
|
| 430 |
+
# create image memory if necessary
|
| 431 |
+
if self._im is None:
|
| 432 |
+
self.im = Image.core.new(self.mode, self.size)
|
| 433 |
+
# create palette (optional)
|
| 434 |
+
if self.mode == "P":
|
| 435 |
+
Image.Image.load(self)
|
| 436 |
+
|
| 437 |
+
def load_end(self) -> None:
|
| 438 |
+
# may be overridden
|
| 439 |
+
pass
|
| 440 |
+
|
| 441 |
+
# may be defined for contained formats
|
| 442 |
+
# def load_seek(self, pos: int) -> None:
|
| 443 |
+
# pass
|
| 444 |
+
|
| 445 |
+
# may be defined for blocked formats (e.g. PNG)
|
| 446 |
+
# def load_read(self, read_bytes: int) -> bytes:
|
| 447 |
+
# pass
|
| 448 |
+
|
| 449 |
+
def _seek_check(self, frame: int) -> bool:
|
| 450 |
+
if (
|
| 451 |
+
frame < self._min_frame
|
| 452 |
+
# Only check upper limit on frames if additional seek operations
|
| 453 |
+
# are not required to do so
|
| 454 |
+
or (
|
| 455 |
+
not (hasattr(self, "_n_frames") and self._n_frames is None)
|
| 456 |
+
and frame >= getattr(self, "n_frames") + self._min_frame
|
| 457 |
+
)
|
| 458 |
+
):
|
| 459 |
+
msg = "attempt to seek outside sequence"
|
| 460 |
+
raise EOFError(msg)
|
| 461 |
+
|
| 462 |
+
return self.tell() != frame
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
class StubHandler(abc.ABC):
|
| 466 |
+
def open(self, im: StubImageFile) -> None:
|
| 467 |
+
pass
|
| 468 |
+
|
| 469 |
+
@abc.abstractmethod
|
| 470 |
+
def load(self, im: StubImageFile) -> Image.Image:
|
| 471 |
+
pass
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
class StubImageFile(ImageFile, metaclass=abc.ABCMeta):
|
| 475 |
+
"""
|
| 476 |
+
Base class for stub image loaders.
|
| 477 |
+
|
| 478 |
+
A stub loader is an image loader that can identify files of a
|
| 479 |
+
certain format, but relies on external code to load the file.
|
| 480 |
+
"""
|
| 481 |
+
|
| 482 |
+
@abc.abstractmethod
|
| 483 |
+
def _open(self) -> None:
|
| 484 |
+
pass
|
| 485 |
+
|
| 486 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 487 |
+
loader = self._load()
|
| 488 |
+
if loader is None:
|
| 489 |
+
msg = f"cannot find loader for this {self.format} file"
|
| 490 |
+
raise OSError(msg)
|
| 491 |
+
image = loader.load(self)
|
| 492 |
+
assert image is not None
|
| 493 |
+
# become the other object (!)
|
| 494 |
+
self.__class__ = image.__class__ # type: ignore[assignment]
|
| 495 |
+
self.__dict__ = image.__dict__
|
| 496 |
+
return image.load()
|
| 497 |
+
|
| 498 |
+
@abc.abstractmethod
|
| 499 |
+
def _load(self) -> StubHandler | None:
|
| 500 |
+
"""(Hook) Find actual image loader."""
|
| 501 |
+
pass
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
class Parser:
|
| 505 |
+
"""
|
| 506 |
+
Incremental image parser. This class implements the standard
|
| 507 |
+
feed/close consumer interface.
|
| 508 |
+
"""
|
| 509 |
+
|
| 510 |
+
incremental = None
|
| 511 |
+
image: Image.Image | None = None
|
| 512 |
+
data: bytes | None = None
|
| 513 |
+
decoder: Image.core.ImagingDecoder | PyDecoder | None = None
|
| 514 |
+
offset = 0
|
| 515 |
+
finished = 0
|
| 516 |
+
|
| 517 |
+
def reset(self) -> None:
|
| 518 |
+
"""
|
| 519 |
+
(Consumer) Reset the parser. Note that you can only call this
|
| 520 |
+
method immediately after you've created a parser; parser
|
| 521 |
+
instances cannot be reused.
|
| 522 |
+
"""
|
| 523 |
+
assert self.data is None, "cannot reuse parsers"
|
| 524 |
+
|
| 525 |
+
def feed(self, data: bytes) -> None:
|
| 526 |
+
"""
|
| 527 |
+
(Consumer) Feed data to the parser.
|
| 528 |
+
|
| 529 |
+
:param data: A string buffer.
|
| 530 |
+
:exception OSError: If the parser failed to parse the image file.
|
| 531 |
+
"""
|
| 532 |
+
# collect data
|
| 533 |
+
|
| 534 |
+
if self.finished:
|
| 535 |
+
return
|
| 536 |
+
|
| 537 |
+
if self.data is None:
|
| 538 |
+
self.data = data
|
| 539 |
+
else:
|
| 540 |
+
self.data = self.data + data
|
| 541 |
+
|
| 542 |
+
# parse what we have
|
| 543 |
+
if self.decoder:
|
| 544 |
+
if self.offset > 0:
|
| 545 |
+
# skip header
|
| 546 |
+
skip = min(len(self.data), self.offset)
|
| 547 |
+
self.data = self.data[skip:]
|
| 548 |
+
self.offset = self.offset - skip
|
| 549 |
+
if self.offset > 0 or not self.data:
|
| 550 |
+
return
|
| 551 |
+
|
| 552 |
+
n, e = self.decoder.decode(self.data)
|
| 553 |
+
|
| 554 |
+
if n < 0:
|
| 555 |
+
# end of stream
|
| 556 |
+
self.data = None
|
| 557 |
+
self.finished = 1
|
| 558 |
+
if e < 0:
|
| 559 |
+
# decoding error
|
| 560 |
+
self.image = None
|
| 561 |
+
raise _get_oserror(e, encoder=False)
|
| 562 |
+
else:
|
| 563 |
+
# end of image
|
| 564 |
+
return
|
| 565 |
+
self.data = self.data[n:]
|
| 566 |
+
|
| 567 |
+
elif self.image:
|
| 568 |
+
# if we end up here with no decoder, this file cannot
|
| 569 |
+
# be incrementally parsed. wait until we've gotten all
|
| 570 |
+
# available data
|
| 571 |
+
pass
|
| 572 |
+
|
| 573 |
+
else:
|
| 574 |
+
# attempt to open this file
|
| 575 |
+
try:
|
| 576 |
+
with io.BytesIO(self.data) as fp:
|
| 577 |
+
im = Image.open(fp)
|
| 578 |
+
except OSError:
|
| 579 |
+
pass # not enough data
|
| 580 |
+
else:
|
| 581 |
+
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
| 582 |
+
if flag or len(im.tile) != 1:
|
| 583 |
+
# custom load code, or multiple tiles
|
| 584 |
+
self.decode = None
|
| 585 |
+
else:
|
| 586 |
+
# initialize decoder
|
| 587 |
+
im.load_prepare()
|
| 588 |
+
d, e, o, a = im.tile[0]
|
| 589 |
+
im.tile = []
|
| 590 |
+
self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig)
|
| 591 |
+
self.decoder.setimage(im.im, e)
|
| 592 |
+
|
| 593 |
+
# calculate decoder offset
|
| 594 |
+
self.offset = o
|
| 595 |
+
if self.offset <= len(self.data):
|
| 596 |
+
self.data = self.data[self.offset :]
|
| 597 |
+
self.offset = 0
|
| 598 |
+
|
| 599 |
+
self.image = im
|
| 600 |
+
|
| 601 |
+
def __enter__(self) -> Parser:
|
| 602 |
+
return self
|
| 603 |
+
|
| 604 |
+
def __exit__(self, *args: object) -> None:
|
| 605 |
+
self.close()
|
| 606 |
+
|
| 607 |
+
def close(self) -> Image.Image:
|
| 608 |
+
"""
|
| 609 |
+
(Consumer) Close the stream.
|
| 610 |
+
|
| 611 |
+
:returns: An image object.
|
| 612 |
+
:exception OSError: If the parser failed to parse the image file either
|
| 613 |
+
because it cannot be identified or cannot be
|
| 614 |
+
decoded.
|
| 615 |
+
"""
|
| 616 |
+
# finish decoding
|
| 617 |
+
if self.decoder:
|
| 618 |
+
# get rid of what's left in the buffers
|
| 619 |
+
self.feed(b"")
|
| 620 |
+
self.data = self.decoder = None
|
| 621 |
+
if not self.finished:
|
| 622 |
+
msg = "image was incomplete"
|
| 623 |
+
raise OSError(msg)
|
| 624 |
+
if not self.image:
|
| 625 |
+
msg = "cannot parse this image"
|
| 626 |
+
raise OSError(msg)
|
| 627 |
+
if self.data:
|
| 628 |
+
# incremental parsing not possible; reopen the file
|
| 629 |
+
# not that we have all data
|
| 630 |
+
with io.BytesIO(self.data) as fp:
|
| 631 |
+
try:
|
| 632 |
+
self.image = Image.open(fp)
|
| 633 |
+
finally:
|
| 634 |
+
self.image.load()
|
| 635 |
+
return self.image
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
# --------------------------------------------------------------------
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None:
|
| 642 |
+
"""Helper to save image based on tile list
|
| 643 |
+
|
| 644 |
+
:param im: Image object.
|
| 645 |
+
:param fp: File object.
|
| 646 |
+
:param tile: Tile list.
|
| 647 |
+
:param bufsize: Optional buffer size
|
| 648 |
+
"""
|
| 649 |
+
|
| 650 |
+
im.load()
|
| 651 |
+
if not hasattr(im, "encoderconfig"):
|
| 652 |
+
im.encoderconfig = ()
|
| 653 |
+
tile.sort(key=_tilesort)
|
| 654 |
+
# FIXME: make MAXBLOCK a configuration parameter
|
| 655 |
+
# It would be great if we could have the encoder specify what it needs
|
| 656 |
+
# But, it would need at least the image size in most cases. RawEncode is
|
| 657 |
+
# a tricky case.
|
| 658 |
+
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
| 659 |
+
try:
|
| 660 |
+
fh = fp.fileno()
|
| 661 |
+
fp.flush()
|
| 662 |
+
_encode_tile(im, fp, tile, bufsize, fh)
|
| 663 |
+
except (AttributeError, io.UnsupportedOperation) as exc:
|
| 664 |
+
_encode_tile(im, fp, tile, bufsize, None, exc)
|
| 665 |
+
if hasattr(fp, "flush"):
|
| 666 |
+
fp.flush()
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def _encode_tile(
|
| 670 |
+
im: Image.Image,
|
| 671 |
+
fp: IO[bytes],
|
| 672 |
+
tile: list[_Tile],
|
| 673 |
+
bufsize: int,
|
| 674 |
+
fh: int | None,
|
| 675 |
+
exc: BaseException | None = None,
|
| 676 |
+
) -> None:
|
| 677 |
+
for encoder_name, extents, offset, args in tile:
|
| 678 |
+
if offset > 0:
|
| 679 |
+
fp.seek(offset)
|
| 680 |
+
encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
|
| 681 |
+
try:
|
| 682 |
+
encoder.setimage(im.im, extents)
|
| 683 |
+
if encoder.pushes_fd:
|
| 684 |
+
encoder.setfd(fp)
|
| 685 |
+
errcode = encoder.encode_to_pyfd()[1]
|
| 686 |
+
else:
|
| 687 |
+
if exc:
|
| 688 |
+
# compress to Python file-compatible object
|
| 689 |
+
while True:
|
| 690 |
+
errcode, data = encoder.encode(bufsize)[1:]
|
| 691 |
+
fp.write(data)
|
| 692 |
+
if errcode:
|
| 693 |
+
break
|
| 694 |
+
else:
|
| 695 |
+
# slight speedup: compress to real file object
|
| 696 |
+
assert fh is not None
|
| 697 |
+
errcode = encoder.encode_to_file(fh, bufsize)
|
| 698 |
+
if errcode < 0:
|
| 699 |
+
raise _get_oserror(errcode, encoder=True) from exc
|
| 700 |
+
finally:
|
| 701 |
+
encoder.cleanup()
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
def _safe_read(fp: IO[bytes], size: int) -> bytes:
|
| 705 |
+
"""
|
| 706 |
+
Reads large blocks in a safe way. Unlike fp.read(n), this function
|
| 707 |
+
doesn't trust the user. If the requested size is larger than
|
| 708 |
+
SAFEBLOCK, the file is read block by block.
|
| 709 |
+
|
| 710 |
+
:param fp: File handle. Must implement a <b>read</b> method.
|
| 711 |
+
:param size: Number of bytes to read.
|
| 712 |
+
:returns: A string containing <i>size</i> bytes of data.
|
| 713 |
+
|
| 714 |
+
Raises an OSError if the file is truncated and the read cannot be completed
|
| 715 |
+
|
| 716 |
+
"""
|
| 717 |
+
if size <= 0:
|
| 718 |
+
return b""
|
| 719 |
+
if size <= SAFEBLOCK:
|
| 720 |
+
data = fp.read(size)
|
| 721 |
+
if len(data) < size:
|
| 722 |
+
msg = "Truncated File Read"
|
| 723 |
+
raise OSError(msg)
|
| 724 |
+
return data
|
| 725 |
+
blocks: list[bytes] = []
|
| 726 |
+
remaining_size = size
|
| 727 |
+
while remaining_size > 0:
|
| 728 |
+
block = fp.read(min(remaining_size, SAFEBLOCK))
|
| 729 |
+
if not block:
|
| 730 |
+
break
|
| 731 |
+
blocks.append(block)
|
| 732 |
+
remaining_size -= len(block)
|
| 733 |
+
if sum(len(block) for block in blocks) < size:
|
| 734 |
+
msg = "Truncated File Read"
|
| 735 |
+
raise OSError(msg)
|
| 736 |
+
return b"".join(blocks)
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
class PyCodecState:
|
| 740 |
+
def __init__(self) -> None:
|
| 741 |
+
self.xsize = 0
|
| 742 |
+
self.ysize = 0
|
| 743 |
+
self.xoff = 0
|
| 744 |
+
self.yoff = 0
|
| 745 |
+
|
| 746 |
+
def extents(self) -> tuple[int, int, int, int]:
|
| 747 |
+
return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
class PyCodec:
|
| 751 |
+
fd: IO[bytes] | None
|
| 752 |
+
|
| 753 |
+
def __init__(self, mode: str, *args: Any) -> None:
|
| 754 |
+
self.im: Image.core.ImagingCore | None = None
|
| 755 |
+
self.state = PyCodecState()
|
| 756 |
+
self.fd = None
|
| 757 |
+
self.mode = mode
|
| 758 |
+
self.init(args)
|
| 759 |
+
|
| 760 |
+
def init(self, args: tuple[Any, ...]) -> None:
|
| 761 |
+
"""
|
| 762 |
+
Override to perform codec specific initialization
|
| 763 |
+
|
| 764 |
+
:param args: Tuple of arg items from the tile entry
|
| 765 |
+
:returns: None
|
| 766 |
+
"""
|
| 767 |
+
self.args = args
|
| 768 |
+
|
| 769 |
+
def cleanup(self) -> None:
|
| 770 |
+
"""
|
| 771 |
+
Override to perform codec specific cleanup
|
| 772 |
+
|
| 773 |
+
:returns: None
|
| 774 |
+
"""
|
| 775 |
+
pass
|
| 776 |
+
|
| 777 |
+
def setfd(self, fd: IO[bytes]) -> None:
|
| 778 |
+
"""
|
| 779 |
+
Called from ImageFile to set the Python file-like object
|
| 780 |
+
|
| 781 |
+
:param fd: A Python file-like object
|
| 782 |
+
:returns: None
|
| 783 |
+
"""
|
| 784 |
+
self.fd = fd
|
| 785 |
+
|
| 786 |
+
def setimage(
|
| 787 |
+
self,
|
| 788 |
+
im: Image.core.ImagingCore,
|
| 789 |
+
extents: tuple[int, int, int, int] | None = None,
|
| 790 |
+
) -> None:
|
| 791 |
+
"""
|
| 792 |
+
Called from ImageFile to set the core output image for the codec
|
| 793 |
+
|
| 794 |
+
:param im: A core image object
|
| 795 |
+
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
|
| 796 |
+
for this tile
|
| 797 |
+
:returns: None
|
| 798 |
+
"""
|
| 799 |
+
|
| 800 |
+
# following c code
|
| 801 |
+
self.im = im
|
| 802 |
+
|
| 803 |
+
if extents:
|
| 804 |
+
(x0, y0, x1, y1) = extents
|
| 805 |
+
else:
|
| 806 |
+
(x0, y0, x1, y1) = (0, 0, 0, 0)
|
| 807 |
+
|
| 808 |
+
if x0 == 0 and x1 == 0:
|
| 809 |
+
self.state.xsize, self.state.ysize = self.im.size
|
| 810 |
+
else:
|
| 811 |
+
self.state.xoff = x0
|
| 812 |
+
self.state.yoff = y0
|
| 813 |
+
self.state.xsize = x1 - x0
|
| 814 |
+
self.state.ysize = y1 - y0
|
| 815 |
+
|
| 816 |
+
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
| 817 |
+
msg = "Size cannot be negative"
|
| 818 |
+
raise ValueError(msg)
|
| 819 |
+
|
| 820 |
+
if (
|
| 821 |
+
self.state.xsize + self.state.xoff > self.im.size[0]
|
| 822 |
+
or self.state.ysize + self.state.yoff > self.im.size[1]
|
| 823 |
+
):
|
| 824 |
+
msg = "Tile cannot extend outside image"
|
| 825 |
+
raise ValueError(msg)
|
| 826 |
+
|
| 827 |
+
|
| 828 |
+
class PyDecoder(PyCodec):
|
| 829 |
+
"""
|
| 830 |
+
Python implementation of a format decoder. Override this class and
|
| 831 |
+
add the decoding logic in the :meth:`decode` method.
|
| 832 |
+
|
| 833 |
+
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
|
| 834 |
+
"""
|
| 835 |
+
|
| 836 |
+
_pulls_fd = False
|
| 837 |
+
|
| 838 |
+
@property
|
| 839 |
+
def pulls_fd(self) -> bool:
|
| 840 |
+
return self._pulls_fd
|
| 841 |
+
|
| 842 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 843 |
+
"""
|
| 844 |
+
Override to perform the decoding process.
|
| 845 |
+
|
| 846 |
+
:param buffer: A bytes object with the data to be decoded.
|
| 847 |
+
:returns: A tuple of ``(bytes consumed, errcode)``.
|
| 848 |
+
If finished with decoding return -1 for the bytes consumed.
|
| 849 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 850 |
+
"""
|
| 851 |
+
msg = "unavailable in base decoder"
|
| 852 |
+
raise NotImplementedError(msg)
|
| 853 |
+
|
| 854 |
+
def set_as_raw(
|
| 855 |
+
self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = ()
|
| 856 |
+
) -> None:
|
| 857 |
+
"""
|
| 858 |
+
Convenience method to set the internal image from a stream of raw data
|
| 859 |
+
|
| 860 |
+
:param data: Bytes to be set
|
| 861 |
+
:param rawmode: The rawmode to be used for the decoder.
|
| 862 |
+
If not specified, it will default to the mode of the image
|
| 863 |
+
:param extra: Extra arguments for the decoder.
|
| 864 |
+
:returns: None
|
| 865 |
+
"""
|
| 866 |
+
|
| 867 |
+
if not rawmode:
|
| 868 |
+
rawmode = self.mode
|
| 869 |
+
d = Image._getdecoder(self.mode, "raw", rawmode, extra)
|
| 870 |
+
assert self.im is not None
|
| 871 |
+
d.setimage(self.im, self.state.extents())
|
| 872 |
+
s = d.decode(data)
|
| 873 |
+
|
| 874 |
+
if s[0] >= 0:
|
| 875 |
+
msg = "not enough image data"
|
| 876 |
+
raise ValueError(msg)
|
| 877 |
+
if s[1] != 0:
|
| 878 |
+
msg = "cannot decode image data"
|
| 879 |
+
raise ValueError(msg)
|
| 880 |
+
|
| 881 |
+
|
| 882 |
+
class PyEncoder(PyCodec):
|
| 883 |
+
"""
|
| 884 |
+
Python implementation of a format encoder. Override this class and
|
| 885 |
+
add the decoding logic in the :meth:`encode` method.
|
| 886 |
+
|
| 887 |
+
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
|
| 888 |
+
"""
|
| 889 |
+
|
| 890 |
+
_pushes_fd = False
|
| 891 |
+
|
| 892 |
+
@property
|
| 893 |
+
def pushes_fd(self) -> bool:
|
| 894 |
+
return self._pushes_fd
|
| 895 |
+
|
| 896 |
+
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
| 897 |
+
"""
|
| 898 |
+
Override to perform the encoding process.
|
| 899 |
+
|
| 900 |
+
:param bufsize: Buffer size.
|
| 901 |
+
:returns: A tuple of ``(bytes encoded, errcode, bytes)``.
|
| 902 |
+
If finished with encoding return 1 for the error code.
|
| 903 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 904 |
+
"""
|
| 905 |
+
msg = "unavailable in base encoder"
|
| 906 |
+
raise NotImplementedError(msg)
|
| 907 |
+
|
| 908 |
+
def encode_to_pyfd(self) -> tuple[int, int]:
|
| 909 |
+
"""
|
| 910 |
+
If ``pushes_fd`` is ``True``, then this method will be used,
|
| 911 |
+
and ``encode()`` will only be called once.
|
| 912 |
+
|
| 913 |
+
:returns: A tuple of ``(bytes consumed, errcode)``.
|
| 914 |
+
Err codes are from :data:`.ImageFile.ERRORS`.
|
| 915 |
+
"""
|
| 916 |
+
if not self.pushes_fd:
|
| 917 |
+
return 0, -8 # bad configuration
|
| 918 |
+
bytes_consumed, errcode, data = self.encode(0)
|
| 919 |
+
if data:
|
| 920 |
+
assert self.fd is not None
|
| 921 |
+
self.fd.write(data)
|
| 922 |
+
return bytes_consumed, errcode
|
| 923 |
+
|
| 924 |
+
def encode_to_file(self, fh: int, bufsize: int) -> int:
|
| 925 |
+
"""
|
| 926 |
+
:param fh: File handle.
|
| 927 |
+
:param bufsize: Buffer size.
|
| 928 |
+
|
| 929 |
+
:returns: If finished successfully, return 0.
|
| 930 |
+
Otherwise, return an error code. Err codes are from
|
| 931 |
+
:data:`.ImageFile.ERRORS`.
|
| 932 |
+
"""
|
| 933 |
+
errcode = 0
|
| 934 |
+
while errcode == 0:
|
| 935 |
+
status, errcode, buf = self.encode(bufsize)
|
| 936 |
+
if status > 0:
|
| 937 |
+
os.write(fh, buf[status:])
|
| 938 |
+
return errcode
|
PIL/ImageFont.py
ADDED
|
@@ -0,0 +1,1320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, Any, BinaryIO, TypedDict, cast
|
| 38 |
+
|
| 39 |
+
from . import Image
|
| 40 |
+
from ._typing import StrOrBytesPath
|
| 41 |
+
from ._util import DeferredError, is_path
|
| 42 |
+
|
| 43 |
+
TYPE_CHECKING = False
|
| 44 |
+
if TYPE_CHECKING:
|
| 45 |
+
from . import ImageFile
|
| 46 |
+
from ._imaging import ImagingFont
|
| 47 |
+
from ._imagingft import Font
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Axis(TypedDict):
|
| 51 |
+
minimum: int | None
|
| 52 |
+
default: int | None
|
| 53 |
+
maximum: int | None
|
| 54 |
+
name: bytes | None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class Layout(IntEnum):
|
| 58 |
+
BASIC = 0
|
| 59 |
+
RAQM = 1
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
MAX_STRING_LENGTH = 1_000_000
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
core: ModuleType | DeferredError
|
| 66 |
+
try:
|
| 67 |
+
from . import _imagingft as core
|
| 68 |
+
except ImportError as ex:
|
| 69 |
+
core = DeferredError.new(ex)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _string_length_check(text: str | bytes | bytearray) -> None:
|
| 73 |
+
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
|
| 74 |
+
msg = "too many characters in string"
|
| 75 |
+
raise ValueError(msg)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# FIXME: add support for pilfont2 format (see FontFile.py)
|
| 79 |
+
|
| 80 |
+
# --------------------------------------------------------------------
|
| 81 |
+
# Font metrics format:
|
| 82 |
+
# "PILfont" LF
|
| 83 |
+
# fontdescriptor LF
|
| 84 |
+
# (optional) key=value... LF
|
| 85 |
+
# "DATA" LF
|
| 86 |
+
# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox)
|
| 87 |
+
#
|
| 88 |
+
# To place a character, cut out srcbox and paste at dstbox,
|
| 89 |
+
# relative to the character position. Then move the character
|
| 90 |
+
# position according to dx, dy.
|
| 91 |
+
# --------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class ImageFont:
|
| 95 |
+
"""PIL font wrapper"""
|
| 96 |
+
|
| 97 |
+
font: ImagingFont
|
| 98 |
+
|
| 99 |
+
def _load_pilfont(self, filename: str) -> None:
|
| 100 |
+
with open(filename, "rb") as fp:
|
| 101 |
+
image: ImageFile.ImageFile | None = None
|
| 102 |
+
root = os.path.splitext(filename)[0]
|
| 103 |
+
|
| 104 |
+
for ext in (".png", ".gif", ".pbm"):
|
| 105 |
+
if image:
|
| 106 |
+
image.close()
|
| 107 |
+
try:
|
| 108 |
+
fullname = root + ext
|
| 109 |
+
image = Image.open(fullname)
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
+
else:
|
| 113 |
+
if image and image.mode in ("1", "L"):
|
| 114 |
+
break
|
| 115 |
+
else:
|
| 116 |
+
if image:
|
| 117 |
+
image.close()
|
| 118 |
+
|
| 119 |
+
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
|
| 120 |
+
raise OSError(msg)
|
| 121 |
+
|
| 122 |
+
self.file = fullname
|
| 123 |
+
|
| 124 |
+
self._load_pilfont_data(fp, image)
|
| 125 |
+
image.close()
|
| 126 |
+
|
| 127 |
+
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
|
| 128 |
+
# check image
|
| 129 |
+
if image.mode not in ("1", "L"):
|
| 130 |
+
image.close()
|
| 131 |
+
|
| 132 |
+
msg = "invalid font image mode"
|
| 133 |
+
raise TypeError(msg)
|
| 134 |
+
|
| 135 |
+
# read PILfont header
|
| 136 |
+
if file.read(8) != b"PILfont\n":
|
| 137 |
+
image.close()
|
| 138 |
+
|
| 139 |
+
msg = "Not a PILfont file"
|
| 140 |
+
raise SyntaxError(msg)
|
| 141 |
+
file.readline()
|
| 142 |
+
self.info = [] # FIXME: should be a dictionary
|
| 143 |
+
while True:
|
| 144 |
+
s = file.readline()
|
| 145 |
+
if not s or s == b"DATA\n":
|
| 146 |
+
break
|
| 147 |
+
self.info.append(s)
|
| 148 |
+
|
| 149 |
+
# read PILfont metrics
|
| 150 |
+
data = file.read(256 * 20)
|
| 151 |
+
|
| 152 |
+
image.load()
|
| 153 |
+
|
| 154 |
+
self.font = Image.core.font(image.im, data)
|
| 155 |
+
|
| 156 |
+
def getmask(
|
| 157 |
+
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
|
| 158 |
+
) -> Image.core.ImagingCore:
|
| 159 |
+
"""
|
| 160 |
+
Create a bitmap for the text.
|
| 161 |
+
|
| 162 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 163 |
+
maximum value of 255. Otherwise, it should have mode ``1``.
|
| 164 |
+
|
| 165 |
+
:param text: Text to render.
|
| 166 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 167 |
+
driver prefers; if empty, the renderer may return either
|
| 168 |
+
mode. Note that the mode is always a string, to simplify
|
| 169 |
+
C-level implementations.
|
| 170 |
+
|
| 171 |
+
.. versionadded:: 1.1.5
|
| 172 |
+
|
| 173 |
+
:return: An internal PIL storage memory instance as defined by the
|
| 174 |
+
:py:mod:`PIL.Image.core` interface module.
|
| 175 |
+
"""
|
| 176 |
+
_string_length_check(text)
|
| 177 |
+
Image._decompression_bomb_check(self.font.getsize(text))
|
| 178 |
+
return self.font.getmask(text, mode)
|
| 179 |
+
|
| 180 |
+
def getbbox(
|
| 181 |
+
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
|
| 182 |
+
) -> tuple[int, int, int, int]:
|
| 183 |
+
"""
|
| 184 |
+
Returns bounding box (in pixels) of given text.
|
| 185 |
+
|
| 186 |
+
.. versionadded:: 9.2.0
|
| 187 |
+
|
| 188 |
+
:param text: Text to render.
|
| 189 |
+
|
| 190 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 191 |
+
"""
|
| 192 |
+
_string_length_check(text)
|
| 193 |
+
width, height = self.font.getsize(text)
|
| 194 |
+
return 0, 0, width, height
|
| 195 |
+
|
| 196 |
+
def getlength(
|
| 197 |
+
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
|
| 198 |
+
) -> int:
|
| 199 |
+
"""
|
| 200 |
+
Returns length (in pixels) of given text.
|
| 201 |
+
This is the amount by which following text should be offset.
|
| 202 |
+
|
| 203 |
+
.. versionadded:: 9.2.0
|
| 204 |
+
"""
|
| 205 |
+
_string_length_check(text)
|
| 206 |
+
width, height = self.font.getsize(text)
|
| 207 |
+
return width
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
##
|
| 211 |
+
# Wrapper for FreeType fonts. Application code should use the
|
| 212 |
+
# <b>truetype</b> factory function to create font objects.
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class FreeTypeFont:
|
| 216 |
+
"""FreeType font wrapper (requires _imagingft service)"""
|
| 217 |
+
|
| 218 |
+
font: Font
|
| 219 |
+
font_bytes: bytes
|
| 220 |
+
|
| 221 |
+
def __init__(
|
| 222 |
+
self,
|
| 223 |
+
font: StrOrBytesPath | BinaryIO,
|
| 224 |
+
size: float = 10,
|
| 225 |
+
index: int = 0,
|
| 226 |
+
encoding: str = "",
|
| 227 |
+
layout_engine: Layout | None = None,
|
| 228 |
+
) -> None:
|
| 229 |
+
# FIXME: use service provider instead
|
| 230 |
+
|
| 231 |
+
if isinstance(core, DeferredError):
|
| 232 |
+
raise core.ex
|
| 233 |
+
|
| 234 |
+
if size <= 0:
|
| 235 |
+
msg = f"font size must be greater than 0, not {size}"
|
| 236 |
+
raise ValueError(msg)
|
| 237 |
+
|
| 238 |
+
self.path = font
|
| 239 |
+
self.size = size
|
| 240 |
+
self.index = index
|
| 241 |
+
self.encoding = encoding
|
| 242 |
+
|
| 243 |
+
if layout_engine not in (Layout.BASIC, Layout.RAQM):
|
| 244 |
+
layout_engine = Layout.BASIC
|
| 245 |
+
if core.HAVE_RAQM:
|
| 246 |
+
layout_engine = Layout.RAQM
|
| 247 |
+
elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
|
| 248 |
+
warnings.warn(
|
| 249 |
+
"Raqm layout was requested, but Raqm is not available. "
|
| 250 |
+
"Falling back to basic layout."
|
| 251 |
+
)
|
| 252 |
+
layout_engine = Layout.BASIC
|
| 253 |
+
|
| 254 |
+
self.layout_engine = layout_engine
|
| 255 |
+
|
| 256 |
+
def load_from_bytes(f: IO[bytes]) -> None:
|
| 257 |
+
self.font_bytes = f.read()
|
| 258 |
+
self.font = core.getfont(
|
| 259 |
+
"", size, index, encoding, self.font_bytes, layout_engine
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
if is_path(font):
|
| 263 |
+
font = os.fspath(font)
|
| 264 |
+
if sys.platform == "win32":
|
| 265 |
+
font_bytes_path = font if isinstance(font, bytes) else font.encode()
|
| 266 |
+
try:
|
| 267 |
+
font_bytes_path.decode("ascii")
|
| 268 |
+
except UnicodeDecodeError:
|
| 269 |
+
# FreeType cannot load fonts with non-ASCII characters on Windows
|
| 270 |
+
# So load it into memory first
|
| 271 |
+
with open(font, "rb") as f:
|
| 272 |
+
load_from_bytes(f)
|
| 273 |
+
return
|
| 274 |
+
self.font = core.getfont(
|
| 275 |
+
font, size, index, encoding, layout_engine=layout_engine
|
| 276 |
+
)
|
| 277 |
+
else:
|
| 278 |
+
load_from_bytes(cast(IO[bytes], font))
|
| 279 |
+
|
| 280 |
+
def __getstate__(self) -> list[Any]:
|
| 281 |
+
return [self.path, self.size, self.index, self.encoding, self.layout_engine]
|
| 282 |
+
|
| 283 |
+
def __setstate__(self, state: list[Any]) -> None:
|
| 284 |
+
path, size, index, encoding, layout_engine = state
|
| 285 |
+
FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine)
|
| 286 |
+
|
| 287 |
+
def getname(self) -> tuple[str | None, str | None]:
|
| 288 |
+
"""
|
| 289 |
+
:return: A tuple of the font family (e.g. Helvetica) and the font style
|
| 290 |
+
(e.g. Bold)
|
| 291 |
+
"""
|
| 292 |
+
return self.font.family, self.font.style
|
| 293 |
+
|
| 294 |
+
def getmetrics(self) -> tuple[int, int]:
|
| 295 |
+
"""
|
| 296 |
+
:return: A tuple of the font ascent (the distance from the baseline to
|
| 297 |
+
the highest outline point) and descent (the distance from the
|
| 298 |
+
baseline to the lowest outline point, a negative value)
|
| 299 |
+
"""
|
| 300 |
+
return self.font.ascent, self.font.descent
|
| 301 |
+
|
| 302 |
+
def getlength(
|
| 303 |
+
self,
|
| 304 |
+
text: str | bytes,
|
| 305 |
+
mode: str = "",
|
| 306 |
+
direction: str | None = None,
|
| 307 |
+
features: list[str] | None = None,
|
| 308 |
+
language: str | None = None,
|
| 309 |
+
) -> float:
|
| 310 |
+
"""
|
| 311 |
+
Returns length (in pixels with 1/64 precision) of given text when rendered
|
| 312 |
+
in font with provided direction, features, and language.
|
| 313 |
+
|
| 314 |
+
This is the amount by which following text should be offset.
|
| 315 |
+
Text bounding box may extend past the length in some fonts,
|
| 316 |
+
e.g. when using italics or accents.
|
| 317 |
+
|
| 318 |
+
The result is returned as a float; it is a whole number if using basic layout.
|
| 319 |
+
|
| 320 |
+
Note that the sum of two lengths may not equal the length of a concatenated
|
| 321 |
+
string due to kerning. If you need to adjust for kerning, include the following
|
| 322 |
+
character and subtract its length.
|
| 323 |
+
|
| 324 |
+
For example, instead of ::
|
| 325 |
+
|
| 326 |
+
hello = font.getlength("Hello")
|
| 327 |
+
world = font.getlength("World")
|
| 328 |
+
hello_world = hello + world # not adjusted for kerning
|
| 329 |
+
assert hello_world == font.getlength("HelloWorld") # may fail
|
| 330 |
+
|
| 331 |
+
use ::
|
| 332 |
+
|
| 333 |
+
hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning
|
| 334 |
+
world = font.getlength("World")
|
| 335 |
+
hello_world = hello + world # adjusted for kerning
|
| 336 |
+
assert hello_world == font.getlength("HelloWorld") # True
|
| 337 |
+
|
| 338 |
+
or disable kerning with (requires libraqm) ::
|
| 339 |
+
|
| 340 |
+
hello = draw.textlength("Hello", font, features=["-kern"])
|
| 341 |
+
world = draw.textlength("World", font, features=["-kern"])
|
| 342 |
+
hello_world = hello + world # kerning is disabled, no need to adjust
|
| 343 |
+
assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"])
|
| 344 |
+
|
| 345 |
+
.. versionadded:: 8.0.0
|
| 346 |
+
|
| 347 |
+
:param text: Text to measure.
|
| 348 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 349 |
+
driver prefers; if empty, the renderer may return either
|
| 350 |
+
mode. Note that the mode is always a string, to simplify
|
| 351 |
+
C-level implementations.
|
| 352 |
+
|
| 353 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 354 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 355 |
+
Requires libraqm.
|
| 356 |
+
|
| 357 |
+
:param features: A list of OpenType font features to be used during text
|
| 358 |
+
layout. This is usually used to turn on optional
|
| 359 |
+
font features that are not enabled by default,
|
| 360 |
+
for example 'dlig' or 'ss01', but can be also
|
| 361 |
+
used to turn off default font features for
|
| 362 |
+
example '-liga' to disable ligatures or '-kern'
|
| 363 |
+
to disable kerning. To get all supported
|
| 364 |
+
features, see
|
| 365 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 366 |
+
Requires libraqm.
|
| 367 |
+
|
| 368 |
+
:param language: Language of the text. Different languages may use
|
| 369 |
+
different glyph shapes or ligatures. This parameter tells
|
| 370 |
+
the font which language the text is in, and to apply the
|
| 371 |
+
correct substitutions as appropriate, if available.
|
| 372 |
+
It should be a `BCP 47 language code
|
| 373 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 374 |
+
Requires libraqm.
|
| 375 |
+
|
| 376 |
+
:return: Either width for horizontal text, or height for vertical text.
|
| 377 |
+
"""
|
| 378 |
+
_string_length_check(text)
|
| 379 |
+
return self.font.getlength(text, mode, direction, features, language) / 64
|
| 380 |
+
|
| 381 |
+
def getbbox(
|
| 382 |
+
self,
|
| 383 |
+
text: str | bytes,
|
| 384 |
+
mode: str = "",
|
| 385 |
+
direction: str | None = None,
|
| 386 |
+
features: list[str] | None = None,
|
| 387 |
+
language: str | None = None,
|
| 388 |
+
stroke_width: float = 0,
|
| 389 |
+
anchor: str | None = None,
|
| 390 |
+
) -> tuple[float, float, float, float]:
|
| 391 |
+
"""
|
| 392 |
+
Returns bounding box (in pixels) of given text relative to given anchor
|
| 393 |
+
when rendered in font with provided direction, features, and language.
|
| 394 |
+
|
| 395 |
+
Use :py:meth:`getlength()` to get the offset of following text with
|
| 396 |
+
1/64 pixel precision. The bounding box includes extra margins for
|
| 397 |
+
some fonts, e.g. italics or accents.
|
| 398 |
+
|
| 399 |
+
.. versionadded:: 8.0.0
|
| 400 |
+
|
| 401 |
+
:param text: Text to render.
|
| 402 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 403 |
+
driver prefers; if empty, the renderer may return either
|
| 404 |
+
mode. Note that the mode is always a string, to simplify
|
| 405 |
+
C-level implementations.
|
| 406 |
+
|
| 407 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 408 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 409 |
+
Requires libraqm.
|
| 410 |
+
|
| 411 |
+
:param features: A list of OpenType font features to be used during text
|
| 412 |
+
layout. This is usually used to turn on optional
|
| 413 |
+
font features that are not enabled by default,
|
| 414 |
+
for example 'dlig' or 'ss01', but can be also
|
| 415 |
+
used to turn off default font features for
|
| 416 |
+
example '-liga' to disable ligatures or '-kern'
|
| 417 |
+
to disable kerning. To get all supported
|
| 418 |
+
features, see
|
| 419 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 420 |
+
Requires libraqm.
|
| 421 |
+
|
| 422 |
+
:param language: Language of the text. Different languages may use
|
| 423 |
+
different glyph shapes or ligatures. This parameter tells
|
| 424 |
+
the font which language the text is in, and to apply the
|
| 425 |
+
correct substitutions as appropriate, if available.
|
| 426 |
+
It should be a `BCP 47 language code
|
| 427 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 428 |
+
Requires libraqm.
|
| 429 |
+
|
| 430 |
+
:param stroke_width: The width of the text stroke.
|
| 431 |
+
|
| 432 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 433 |
+
the anchor to the text. The default alignment is top left,
|
| 434 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 435 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 436 |
+
|
| 437 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 438 |
+
"""
|
| 439 |
+
_string_length_check(text)
|
| 440 |
+
size, offset = self.font.getsize(
|
| 441 |
+
text, mode, direction, features, language, anchor
|
| 442 |
+
)
|
| 443 |
+
left, top = offset[0] - stroke_width, offset[1] - stroke_width
|
| 444 |
+
width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width
|
| 445 |
+
return left, top, left + width, top + height
|
| 446 |
+
|
| 447 |
+
def getmask(
|
| 448 |
+
self,
|
| 449 |
+
text: str | bytes,
|
| 450 |
+
mode: str = "",
|
| 451 |
+
direction: str | None = None,
|
| 452 |
+
features: list[str] | None = None,
|
| 453 |
+
language: str | None = None,
|
| 454 |
+
stroke_width: float = 0,
|
| 455 |
+
anchor: str | None = None,
|
| 456 |
+
ink: int = 0,
|
| 457 |
+
start: tuple[float, float] | None = None,
|
| 458 |
+
) -> Image.core.ImagingCore:
|
| 459 |
+
"""
|
| 460 |
+
Create a bitmap for the text.
|
| 461 |
+
|
| 462 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 463 |
+
maximum value of 255. If the font has embedded color data, the bitmap
|
| 464 |
+
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
| 465 |
+
|
| 466 |
+
:param text: Text to render.
|
| 467 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 468 |
+
driver prefers; if empty, the renderer may return either
|
| 469 |
+
mode. Note that the mode is always a string, to simplify
|
| 470 |
+
C-level implementations.
|
| 471 |
+
|
| 472 |
+
.. versionadded:: 1.1.5
|
| 473 |
+
|
| 474 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 475 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 476 |
+
Requires libraqm.
|
| 477 |
+
|
| 478 |
+
.. versionadded:: 4.2.0
|
| 479 |
+
|
| 480 |
+
:param features: A list of OpenType font features to be used during text
|
| 481 |
+
layout. This is usually used to turn on optional
|
| 482 |
+
font features that are not enabled by default,
|
| 483 |
+
for example 'dlig' or 'ss01', but can be also
|
| 484 |
+
used to turn off default font features for
|
| 485 |
+
example '-liga' to disable ligatures or '-kern'
|
| 486 |
+
to disable kerning. To get all supported
|
| 487 |
+
features, see
|
| 488 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 489 |
+
Requires libraqm.
|
| 490 |
+
|
| 491 |
+
.. versionadded:: 4.2.0
|
| 492 |
+
|
| 493 |
+
:param language: Language of the text. Different languages may use
|
| 494 |
+
different glyph shapes or ligatures. This parameter tells
|
| 495 |
+
the font which language the text is in, and to apply the
|
| 496 |
+
correct substitutions as appropriate, if available.
|
| 497 |
+
It should be a `BCP 47 language code
|
| 498 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 499 |
+
Requires libraqm.
|
| 500 |
+
|
| 501 |
+
.. versionadded:: 6.0.0
|
| 502 |
+
|
| 503 |
+
:param stroke_width: The width of the text stroke.
|
| 504 |
+
|
| 505 |
+
.. versionadded:: 6.2.0
|
| 506 |
+
|
| 507 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 508 |
+
the anchor to the text. The default alignment is top left,
|
| 509 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 510 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 511 |
+
|
| 512 |
+
.. versionadded:: 8.0.0
|
| 513 |
+
|
| 514 |
+
:param ink: Foreground ink for rendering in RGBA mode.
|
| 515 |
+
|
| 516 |
+
.. versionadded:: 8.0.0
|
| 517 |
+
|
| 518 |
+
:param start: Tuple of horizontal and vertical offset, as text may render
|
| 519 |
+
differently when starting at fractional coordinates.
|
| 520 |
+
|
| 521 |
+
.. versionadded:: 9.4.0
|
| 522 |
+
|
| 523 |
+
:return: An internal PIL storage memory instance as defined by the
|
| 524 |
+
:py:mod:`PIL.Image.core` interface module.
|
| 525 |
+
"""
|
| 526 |
+
return self.getmask2(
|
| 527 |
+
text,
|
| 528 |
+
mode,
|
| 529 |
+
direction=direction,
|
| 530 |
+
features=features,
|
| 531 |
+
language=language,
|
| 532 |
+
stroke_width=stroke_width,
|
| 533 |
+
anchor=anchor,
|
| 534 |
+
ink=ink,
|
| 535 |
+
start=start,
|
| 536 |
+
)[0]
|
| 537 |
+
|
| 538 |
+
def getmask2(
|
| 539 |
+
self,
|
| 540 |
+
text: str | bytes,
|
| 541 |
+
mode: str = "",
|
| 542 |
+
direction: str | None = None,
|
| 543 |
+
features: list[str] | None = None,
|
| 544 |
+
language: str | None = None,
|
| 545 |
+
stroke_width: float = 0,
|
| 546 |
+
anchor: str | None = None,
|
| 547 |
+
ink: int = 0,
|
| 548 |
+
start: tuple[float, float] | None = None,
|
| 549 |
+
*args: Any,
|
| 550 |
+
**kwargs: Any,
|
| 551 |
+
) -> tuple[Image.core.ImagingCore, tuple[int, int]]:
|
| 552 |
+
"""
|
| 553 |
+
Create a bitmap for the text.
|
| 554 |
+
|
| 555 |
+
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
| 556 |
+
maximum value of 255. If the font has embedded color data, the bitmap
|
| 557 |
+
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
| 558 |
+
|
| 559 |
+
:param text: Text to render.
|
| 560 |
+
:param mode: Used by some graphics drivers to indicate what mode the
|
| 561 |
+
driver prefers; if empty, the renderer may return either
|
| 562 |
+
mode. Note that the mode is always a string, to simplify
|
| 563 |
+
C-level implementations.
|
| 564 |
+
|
| 565 |
+
.. versionadded:: 1.1.5
|
| 566 |
+
|
| 567 |
+
:param direction: Direction of the text. It can be 'rtl' (right to
|
| 568 |
+
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
| 569 |
+
Requires libraqm.
|
| 570 |
+
|
| 571 |
+
.. versionadded:: 4.2.0
|
| 572 |
+
|
| 573 |
+
:param features: A list of OpenType font features to be used during text
|
| 574 |
+
layout. This is usually used to turn on optional
|
| 575 |
+
font features that are not enabled by default,
|
| 576 |
+
for example 'dlig' or 'ss01', but can be also
|
| 577 |
+
used to turn off default font features for
|
| 578 |
+
example '-liga' to disable ligatures or '-kern'
|
| 579 |
+
to disable kerning. To get all supported
|
| 580 |
+
features, see
|
| 581 |
+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
| 582 |
+
Requires libraqm.
|
| 583 |
+
|
| 584 |
+
.. versionadded:: 4.2.0
|
| 585 |
+
|
| 586 |
+
:param language: Language of the text. Different languages may use
|
| 587 |
+
different glyph shapes or ligatures. This parameter tells
|
| 588 |
+
the font which language the text is in, and to apply the
|
| 589 |
+
correct substitutions as appropriate, if available.
|
| 590 |
+
It should be a `BCP 47 language code
|
| 591 |
+
<https://www.w3.org/International/articles/language-tags/>`_
|
| 592 |
+
Requires libraqm.
|
| 593 |
+
|
| 594 |
+
.. versionadded:: 6.0.0
|
| 595 |
+
|
| 596 |
+
:param stroke_width: The width of the text stroke.
|
| 597 |
+
|
| 598 |
+
.. versionadded:: 6.2.0
|
| 599 |
+
|
| 600 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 601 |
+
the anchor to the text. The default alignment is top left,
|
| 602 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 603 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 604 |
+
|
| 605 |
+
.. versionadded:: 8.0.0
|
| 606 |
+
|
| 607 |
+
:param ink: Foreground ink for rendering in RGBA mode.
|
| 608 |
+
|
| 609 |
+
.. versionadded:: 8.0.0
|
| 610 |
+
|
| 611 |
+
:param start: Tuple of horizontal and vertical offset, as text may render
|
| 612 |
+
differently when starting at fractional coordinates.
|
| 613 |
+
|
| 614 |
+
.. versionadded:: 9.4.0
|
| 615 |
+
|
| 616 |
+
:return: A tuple of an internal PIL storage memory instance as defined by the
|
| 617 |
+
:py:mod:`PIL.Image.core` interface module, and the text offset, the
|
| 618 |
+
gap between the starting coordinate and the first marking
|
| 619 |
+
"""
|
| 620 |
+
_string_length_check(text)
|
| 621 |
+
if start is None:
|
| 622 |
+
start = (0, 0)
|
| 623 |
+
|
| 624 |
+
def fill(width: int, height: int) -> Image.core.ImagingCore:
|
| 625 |
+
size = (width, height)
|
| 626 |
+
Image._decompression_bomb_check(size)
|
| 627 |
+
return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
|
| 628 |
+
|
| 629 |
+
return self.font.render(
|
| 630 |
+
text,
|
| 631 |
+
fill,
|
| 632 |
+
mode,
|
| 633 |
+
direction,
|
| 634 |
+
features,
|
| 635 |
+
language,
|
| 636 |
+
stroke_width,
|
| 637 |
+
kwargs.get("stroke_filled", False),
|
| 638 |
+
anchor,
|
| 639 |
+
ink,
|
| 640 |
+
start,
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
def font_variant(
|
| 644 |
+
self,
|
| 645 |
+
font: StrOrBytesPath | BinaryIO | None = None,
|
| 646 |
+
size: float | None = None,
|
| 647 |
+
index: int | None = None,
|
| 648 |
+
encoding: str | None = None,
|
| 649 |
+
layout_engine: Layout | None = None,
|
| 650 |
+
) -> FreeTypeFont:
|
| 651 |
+
"""
|
| 652 |
+
Create a copy of this FreeTypeFont object,
|
| 653 |
+
using any specified arguments to override the settings.
|
| 654 |
+
|
| 655 |
+
Parameters are identical to the parameters used to initialize this
|
| 656 |
+
object.
|
| 657 |
+
|
| 658 |
+
:return: A FreeTypeFont object.
|
| 659 |
+
"""
|
| 660 |
+
if font is None:
|
| 661 |
+
try:
|
| 662 |
+
font = BytesIO(self.font_bytes)
|
| 663 |
+
except AttributeError:
|
| 664 |
+
font = self.path
|
| 665 |
+
return FreeTypeFont(
|
| 666 |
+
font=font,
|
| 667 |
+
size=self.size if size is None else size,
|
| 668 |
+
index=self.index if index is None else index,
|
| 669 |
+
encoding=self.encoding if encoding is None else encoding,
|
| 670 |
+
layout_engine=layout_engine or self.layout_engine,
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
def get_variation_names(self) -> list[bytes]:
|
| 674 |
+
"""
|
| 675 |
+
:returns: A list of the named styles in a variation font.
|
| 676 |
+
:exception OSError: If the font is not a variation font.
|
| 677 |
+
"""
|
| 678 |
+
names = []
|
| 679 |
+
for name in self.font.getvarnames():
|
| 680 |
+
name = name.replace(b"\x00", b"")
|
| 681 |
+
if name not in names:
|
| 682 |
+
names.append(name)
|
| 683 |
+
return names
|
| 684 |
+
|
| 685 |
+
def set_variation_by_name(self, name: str | bytes) -> None:
|
| 686 |
+
"""
|
| 687 |
+
:param name: The name of the style.
|
| 688 |
+
:exception OSError: If the font is not a variation font.
|
| 689 |
+
"""
|
| 690 |
+
names = self.get_variation_names()
|
| 691 |
+
if not isinstance(name, bytes):
|
| 692 |
+
name = name.encode()
|
| 693 |
+
index = names.index(name) + 1
|
| 694 |
+
|
| 695 |
+
if index == getattr(self, "_last_variation_index", None):
|
| 696 |
+
# When the same name is set twice in a row,
|
| 697 |
+
# there is an 'unknown freetype error'
|
| 698 |
+
# https://savannah.nongnu.org/bugs/?56186
|
| 699 |
+
return
|
| 700 |
+
self._last_variation_index = index
|
| 701 |
+
|
| 702 |
+
self.font.setvarname(index)
|
| 703 |
+
|
| 704 |
+
def get_variation_axes(self) -> list[Axis]:
|
| 705 |
+
"""
|
| 706 |
+
:returns: A list of the axes in a variation font.
|
| 707 |
+
:exception OSError: If the font is not a variation font.
|
| 708 |
+
"""
|
| 709 |
+
axes = self.font.getvaraxes()
|
| 710 |
+
for axis in axes:
|
| 711 |
+
if axis["name"]:
|
| 712 |
+
axis["name"] = axis["name"].replace(b"\x00", b"")
|
| 713 |
+
return axes
|
| 714 |
+
|
| 715 |
+
def set_variation_by_axes(self, axes: list[float]) -> None:
|
| 716 |
+
"""
|
| 717 |
+
:param axes: A list of values for each axis.
|
| 718 |
+
:exception OSError: If the font is not a variation font.
|
| 719 |
+
"""
|
| 720 |
+
self.font.setvaraxes(axes)
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
class TransposedFont:
|
| 724 |
+
"""Wrapper for writing rotated or mirrored text"""
|
| 725 |
+
|
| 726 |
+
def __init__(
|
| 727 |
+
self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None
|
| 728 |
+
):
|
| 729 |
+
"""
|
| 730 |
+
Wrapper that creates a transposed font from any existing font
|
| 731 |
+
object.
|
| 732 |
+
|
| 733 |
+
:param font: A font object.
|
| 734 |
+
:param orientation: An optional orientation. If given, this should
|
| 735 |
+
be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM,
|
| 736 |
+
Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or
|
| 737 |
+
Image.Transpose.ROTATE_270.
|
| 738 |
+
"""
|
| 739 |
+
self.font = font
|
| 740 |
+
self.orientation = orientation # any 'transpose' argument, or None
|
| 741 |
+
|
| 742 |
+
def getmask(
|
| 743 |
+
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
|
| 744 |
+
) -> Image.core.ImagingCore:
|
| 745 |
+
im = self.font.getmask(text, mode, *args, **kwargs)
|
| 746 |
+
if self.orientation is not None:
|
| 747 |
+
return im.transpose(self.orientation)
|
| 748 |
+
return im
|
| 749 |
+
|
| 750 |
+
def getbbox(
|
| 751 |
+
self, text: str | bytes, *args: Any, **kwargs: Any
|
| 752 |
+
) -> tuple[int, int, float, float]:
|
| 753 |
+
# TransposedFont doesn't support getmask2, move top-left point to (0, 0)
|
| 754 |
+
# this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
|
| 755 |
+
left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
|
| 756 |
+
width = right - left
|
| 757 |
+
height = bottom - top
|
| 758 |
+
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
|
| 759 |
+
return 0, 0, height, width
|
| 760 |
+
return 0, 0, width, height
|
| 761 |
+
|
| 762 |
+
def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
|
| 763 |
+
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
|
| 764 |
+
msg = "text length is undefined for text rotated by 90 or 270 degrees"
|
| 765 |
+
raise ValueError(msg)
|
| 766 |
+
return self.font.getlength(text, *args, **kwargs)
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def load(filename: str) -> ImageFont:
|
| 770 |
+
"""
|
| 771 |
+
Load a font file. This function loads a font object from the given
|
| 772 |
+
bitmap font file, and returns the corresponding font object. For loading TrueType
|
| 773 |
+
or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
|
| 774 |
+
|
| 775 |
+
:param filename: Name of font file.
|
| 776 |
+
:return: A font object.
|
| 777 |
+
:exception OSError: If the file could not be read.
|
| 778 |
+
"""
|
| 779 |
+
f = ImageFont()
|
| 780 |
+
f._load_pilfont(filename)
|
| 781 |
+
return f
|
| 782 |
+
|
| 783 |
+
|
| 784 |
+
def truetype(
|
| 785 |
+
font: StrOrBytesPath | BinaryIO,
|
| 786 |
+
size: float = 10,
|
| 787 |
+
index: int = 0,
|
| 788 |
+
encoding: str = "",
|
| 789 |
+
layout_engine: Layout | None = None,
|
| 790 |
+
) -> FreeTypeFont:
|
| 791 |
+
"""
|
| 792 |
+
Load a TrueType or OpenType font from a file or file-like object,
|
| 793 |
+
and create a font object. This function loads a font object from the given
|
| 794 |
+
file or file-like object, and creates a font object for a font of the given
|
| 795 |
+
size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
|
| 796 |
+
and :py:func:`~PIL.ImageFont.load_path`.
|
| 797 |
+
|
| 798 |
+
Pillow uses FreeType to open font files. On Windows, be aware that FreeType
|
| 799 |
+
will keep the file open as long as the FreeTypeFont object exists. Windows
|
| 800 |
+
limits the number of files that can be open in C at once to 512, so if many
|
| 801 |
+
fonts are opened simultaneously and that limit is approached, an
|
| 802 |
+
``OSError`` may be thrown, reporting that FreeType "cannot open resource".
|
| 803 |
+
A workaround would be to copy the file(s) into memory, and open that instead.
|
| 804 |
+
|
| 805 |
+
This function requires the _imagingft service.
|
| 806 |
+
|
| 807 |
+
:param font: A filename or file-like object containing a TrueType font.
|
| 808 |
+
If the file is not found in this filename, the loader may also
|
| 809 |
+
search in other directories, such as:
|
| 810 |
+
|
| 811 |
+
* The :file:`fonts/` directory on Windows,
|
| 812 |
+
* :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
|
| 813 |
+
and :file:`~/Library/Fonts/` on macOS.
|
| 814 |
+
* :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
|
| 815 |
+
and :file:`/usr/share/fonts` on Linux; or those specified by
|
| 816 |
+
the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
|
| 817 |
+
for user-installed and system-wide fonts, respectively.
|
| 818 |
+
|
| 819 |
+
:param size: The requested size, in pixels.
|
| 820 |
+
:param index: Which font face to load (default is first available face).
|
| 821 |
+
:param encoding: Which font encoding to use (default is Unicode). Possible
|
| 822 |
+
encodings include (see the FreeType documentation for more
|
| 823 |
+
information):
|
| 824 |
+
|
| 825 |
+
* "unic" (Unicode)
|
| 826 |
+
* "symb" (Microsoft Symbol)
|
| 827 |
+
* "ADOB" (Adobe Standard)
|
| 828 |
+
* "ADBE" (Adobe Expert)
|
| 829 |
+
* "ADBC" (Adobe Custom)
|
| 830 |
+
* "armn" (Apple Roman)
|
| 831 |
+
* "sjis" (Shift JIS)
|
| 832 |
+
* "gb " (PRC)
|
| 833 |
+
* "big5"
|
| 834 |
+
* "wans" (Extended Wansung)
|
| 835 |
+
* "joha" (Johab)
|
| 836 |
+
* "lat1" (Latin-1)
|
| 837 |
+
|
| 838 |
+
This specifies the character set to use. It does not alter the
|
| 839 |
+
encoding of any text provided in subsequent operations.
|
| 840 |
+
:param layout_engine: Which layout engine to use, if available:
|
| 841 |
+
:attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
|
| 842 |
+
If it is available, Raqm layout will be used by default.
|
| 843 |
+
Otherwise, basic layout will be used.
|
| 844 |
+
|
| 845 |
+
Raqm layout is recommended for all non-English text. If Raqm layout
|
| 846 |
+
is not required, basic layout will have better performance.
|
| 847 |
+
|
| 848 |
+
You can check support for Raqm layout using
|
| 849 |
+
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.
|
| 850 |
+
|
| 851 |
+
.. versionadded:: 4.2.0
|
| 852 |
+
:return: A font object.
|
| 853 |
+
:exception OSError: If the file could not be read.
|
| 854 |
+
:exception ValueError: If the font size is not greater than zero.
|
| 855 |
+
"""
|
| 856 |
+
|
| 857 |
+
def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont:
|
| 858 |
+
return FreeTypeFont(font, size, index, encoding, layout_engine)
|
| 859 |
+
|
| 860 |
+
try:
|
| 861 |
+
return freetype(font)
|
| 862 |
+
except OSError:
|
| 863 |
+
if not is_path(font):
|
| 864 |
+
raise
|
| 865 |
+
ttf_filename = os.path.basename(font)
|
| 866 |
+
|
| 867 |
+
dirs = []
|
| 868 |
+
if sys.platform == "win32":
|
| 869 |
+
# check the windows font repository
|
| 870 |
+
# NOTE: must use uppercase WINDIR, to work around bugs in
|
| 871 |
+
# 1.5.2's os.environ.get()
|
| 872 |
+
windir = os.environ.get("WINDIR")
|
| 873 |
+
if windir:
|
| 874 |
+
dirs.append(os.path.join(windir, "fonts"))
|
| 875 |
+
elif sys.platform in ("linux", "linux2"):
|
| 876 |
+
data_home = os.environ.get("XDG_DATA_HOME")
|
| 877 |
+
if not data_home:
|
| 878 |
+
# The freedesktop spec defines the following default directory for
|
| 879 |
+
# when XDG_DATA_HOME is unset or empty. This user-level directory
|
| 880 |
+
# takes precedence over system-level directories.
|
| 881 |
+
data_home = os.path.expanduser("~/.local/share")
|
| 882 |
+
xdg_dirs = [data_home]
|
| 883 |
+
|
| 884 |
+
data_dirs = os.environ.get("XDG_DATA_DIRS")
|
| 885 |
+
if not data_dirs:
|
| 886 |
+
# Similarly, defaults are defined for the system-level directories
|
| 887 |
+
data_dirs = "/usr/local/share:/usr/share"
|
| 888 |
+
xdg_dirs += data_dirs.split(":")
|
| 889 |
+
|
| 890 |
+
dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
|
| 891 |
+
elif sys.platform == "darwin":
|
| 892 |
+
dirs += [
|
| 893 |
+
"/Library/Fonts",
|
| 894 |
+
"/System/Library/Fonts",
|
| 895 |
+
os.path.expanduser("~/Library/Fonts"),
|
| 896 |
+
]
|
| 897 |
+
|
| 898 |
+
ext = os.path.splitext(ttf_filename)[1]
|
| 899 |
+
first_font_with_a_different_extension = None
|
| 900 |
+
for directory in dirs:
|
| 901 |
+
for walkroot, walkdir, walkfilenames in os.walk(directory):
|
| 902 |
+
for walkfilename in walkfilenames:
|
| 903 |
+
if ext and walkfilename == ttf_filename:
|
| 904 |
+
return freetype(os.path.join(walkroot, walkfilename))
|
| 905 |
+
elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
|
| 906 |
+
fontpath = os.path.join(walkroot, walkfilename)
|
| 907 |
+
if os.path.splitext(fontpath)[1] == ".ttf":
|
| 908 |
+
return freetype(fontpath)
|
| 909 |
+
if not ext and first_font_with_a_different_extension is None:
|
| 910 |
+
first_font_with_a_different_extension = fontpath
|
| 911 |
+
if first_font_with_a_different_extension:
|
| 912 |
+
return freetype(first_font_with_a_different_extension)
|
| 913 |
+
raise
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
def load_path(filename: str | bytes) -> ImageFont:
|
| 917 |
+
"""
|
| 918 |
+
Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
|
| 919 |
+
bitmap font along the Python path.
|
| 920 |
+
|
| 921 |
+
:param filename: Name of font file.
|
| 922 |
+
:return: A font object.
|
| 923 |
+
:exception OSError: If the file could not be read.
|
| 924 |
+
"""
|
| 925 |
+
if not isinstance(filename, str):
|
| 926 |
+
filename = filename.decode("utf-8")
|
| 927 |
+
for directory in sys.path:
|
| 928 |
+
try:
|
| 929 |
+
return load(os.path.join(directory, filename))
|
| 930 |
+
except OSError:
|
| 931 |
+
pass
|
| 932 |
+
msg = f'cannot find font file "{filename}" in sys.path'
|
| 933 |
+
if os.path.exists(filename):
|
| 934 |
+
msg += f', did you mean ImageFont.load("{filename}") instead?'
|
| 935 |
+
|
| 936 |
+
raise OSError(msg)
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
def load_default_imagefont() -> ImageFont:
|
| 940 |
+
f = ImageFont()
|
| 941 |
+
f._load_pilfont_data(
|
| 942 |
+
# courB08
|
| 943 |
+
BytesIO(
|
| 944 |
+
base64.b64decode(
|
| 945 |
+
b"""
|
| 946 |
+
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 947 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 948 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 949 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 950 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 951 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 952 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 953 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 954 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 955 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 956 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 957 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
|
| 958 |
+
BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
|
| 959 |
+
AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
|
| 960 |
+
AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
|
| 961 |
+
ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
|
| 962 |
+
BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
|
| 963 |
+
//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
|
| 964 |
+
AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
|
| 965 |
+
AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
|
| 966 |
+
ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
|
| 967 |
+
AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
|
| 968 |
+
/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
|
| 969 |
+
AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
|
| 970 |
+
AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
|
| 971 |
+
AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
|
| 972 |
+
BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
|
| 973 |
+
AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
|
| 974 |
+
2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
|
| 975 |
+
AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
|
| 976 |
+
+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
|
| 977 |
+
////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
|
| 978 |
+
BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
|
| 979 |
+
AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
|
| 980 |
+
AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
|
| 981 |
+
AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
|
| 982 |
+
BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
|
| 983 |
+
//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
|
| 984 |
+
AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
|
| 985 |
+
AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
|
| 986 |
+
mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
|
| 987 |
+
AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
|
| 988 |
+
AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
|
| 989 |
+
AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
|
| 990 |
+
Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
|
| 991 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 992 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 993 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 994 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 995 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 996 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 997 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 998 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 999 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1000 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1001 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1002 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
|
| 1003 |
+
//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
|
| 1004 |
+
AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
|
| 1005 |
+
AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
|
| 1006 |
+
DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
|
| 1007 |
+
AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
|
| 1008 |
+
+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
|
| 1009 |
+
AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
|
| 1010 |
+
///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
|
| 1011 |
+
AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
|
| 1012 |
+
BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
|
| 1013 |
+
Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
|
| 1014 |
+
eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
|
| 1015 |
+
AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
|
| 1016 |
+
+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
|
| 1017 |
+
////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
|
| 1018 |
+
BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
|
| 1019 |
+
AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
|
| 1020 |
+
AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
|
| 1021 |
+
Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
|
| 1022 |
+
Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
|
| 1023 |
+
//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
|
| 1024 |
+
AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
|
| 1025 |
+
AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
|
| 1026 |
+
LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
|
| 1027 |
+
AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
|
| 1028 |
+
AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
|
| 1029 |
+
AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
|
| 1030 |
+
AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
|
| 1031 |
+
AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
|
| 1032 |
+
EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
|
| 1033 |
+
AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
|
| 1034 |
+
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
|
| 1035 |
+
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
|
| 1036 |
+
+QAGAAIAzgAKANUAEw==
|
| 1037 |
+
"""
|
| 1038 |
+
)
|
| 1039 |
+
),
|
| 1040 |
+
Image.open(
|
| 1041 |
+
BytesIO(
|
| 1042 |
+
base64.b64decode(
|
| 1043 |
+
b"""
|
| 1044 |
+
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
|
| 1045 |
+
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
|
| 1046 |
+
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
|
| 1047 |
+
LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
|
| 1048 |
+
IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
|
| 1049 |
+
Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
|
| 1050 |
+
NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
|
| 1051 |
+
in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
|
| 1052 |
+
SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
|
| 1053 |
+
AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
|
| 1054 |
+
y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
|
| 1055 |
+
ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
|
| 1056 |
+
lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
|
| 1057 |
+
/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
|
| 1058 |
+
AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
|
| 1059 |
+
c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
|
| 1060 |
+
/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
|
| 1061 |
+
pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
|
| 1062 |
+
oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
|
| 1063 |
+
evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
|
| 1064 |
+
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
|
| 1065 |
+
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
|
| 1066 |
+
w7IkEbzhVQAAAABJRU5ErkJggg==
|
| 1067 |
+
"""
|
| 1068 |
+
)
|
| 1069 |
+
)
|
| 1070 |
+
),
|
| 1071 |
+
)
|
| 1072 |
+
return f
|
| 1073 |
+
|
| 1074 |
+
|
| 1075 |
+
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
|
| 1076 |
+
"""If FreeType support is available, load a version of Aileron Regular,
|
| 1077 |
+
https://dotcolon.net/fonts/aileron, with a more limited character set.
|
| 1078 |
+
|
| 1079 |
+
Otherwise, load a "better than nothing" font.
|
| 1080 |
+
|
| 1081 |
+
.. versionadded:: 1.1.4
|
| 1082 |
+
|
| 1083 |
+
:param size: The font size of Aileron Regular.
|
| 1084 |
+
|
| 1085 |
+
.. versionadded:: 10.1.0
|
| 1086 |
+
|
| 1087 |
+
:return: A font object.
|
| 1088 |
+
"""
|
| 1089 |
+
if isinstance(core, ModuleType) or size is not None:
|
| 1090 |
+
return truetype(
|
| 1091 |
+
BytesIO(
|
| 1092 |
+
base64.b64decode(
|
| 1093 |
+
b"""
|
| 1094 |
+
AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
|
| 1095 |
+
AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
|
| 1096 |
+
MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
|
| 1097 |
+
tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk
|
| 1098 |
+
OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/
|
| 1099 |
+
2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ
|
| 1100 |
+
AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI
|
| 1101 |
+
BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA
|
| 1102 |
+
AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ
|
| 1103 |
+
AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk
|
| 1104 |
+
QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB
|
| 1105 |
+
kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC
|
| 1106 |
+
ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA
|
| 1107 |
+
EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg
|
| 1108 |
+
JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y
|
| 1109 |
+
AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q
|
| 1110 |
+
AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq
|
| 1111 |
+
QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB//
|
| 1112 |
+
//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1113 |
+
AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT
|
| 1114 |
+
FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT
|
| 1115 |
+
U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA
|
| 1116 |
+
AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9
|
| 1117 |
+
ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO
|
| 1118 |
+
AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ
|
| 1119 |
+
gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG
|
| 1120 |
+
oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz
|
| 1121 |
+
qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA
|
| 1122 |
+
DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA
|
| 1123 |
+
P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA
|
| 1124 |
+
LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc
|
| 1125 |
+
jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb
|
| 1126 |
+
2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ
|
| 1127 |
+
icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ
|
| 1128 |
+
ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA
|
| 1129 |
+
dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c
|
| 1130 |
+
OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/
|
| 1131 |
+
/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg
|
| 1132 |
+
ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp
|
| 1133 |
+
COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA
|
| 1134 |
+
EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q
|
| 1135 |
+
EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx
|
| 1136 |
+
ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj
|
| 1137 |
+
OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA
|
| 1138 |
+
AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H
|
| 1139 |
+
gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg
|
| 1140 |
+
KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM
|
| 1141 |
+
iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA
|
| 1142 |
+
AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA
|
| 1143 |
+
YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg
|
| 1144 |
+
pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4
|
| 1145 |
+
rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv
|
| 1146 |
+
d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA
|
| 1147 |
+
sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA
|
| 1148 |
+
IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY
|
| 1149 |
+
AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2
|
| 1150 |
+
Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS
|
| 1151 |
+
0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC
|
| 1152 |
+
MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp
|
| 1153 |
+
7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS
|
| 1154 |
+
MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA
|
| 1155 |
+
AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS
|
| 1156 |
+
UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8
|
| 1157 |
+
AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA
|
| 1158 |
+
ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J
|
| 1159 |
+
CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj
|
| 1160 |
+
Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY
|
| 1161 |
+
Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74
|
| 1162 |
+
EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA
|
| 1163 |
+
AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA
|
| 1164 |
+
EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt
|
| 1165 |
+
hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA
|
| 1166 |
+
ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A
|
| 1167 |
+
sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi
|
| 1168 |
+
sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI
|
| 1169 |
+
vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh
|
| 1170 |
+
FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH
|
| 1171 |
+
wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq
|
| 1172 |
+
N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA
|
| 1173 |
+
AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2
|
| 1174 |
+
NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA
|
| 1175 |
+
wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j
|
| 1176 |
+
VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7
|
| 1177 |
+
MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR
|
| 1178 |
+
MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN
|
| 1179 |
+
jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg
|
| 1180 |
+
EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU
|
| 1181 |
+
V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx
|
| 1182 |
+
UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA
|
| 1183 |
+
CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv
|
| 1184 |
+
6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM
|
| 1185 |
+
uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9
|
| 1186 |
+
Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE
|
| 1187 |
+
SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA
|
| 1188 |
+
IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA
|
| 1189 |
+
hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi
|
| 1190 |
+
kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY
|
| 1191 |
+
re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A
|
| 1192 |
+
EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA
|
| 1193 |
+
BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+
|
| 1194 |
+
HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE
|
| 1195 |
+
wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg
|
| 1196 |
+
ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI
|
| 1197 |
+
XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf
|
| 1198 |
+
J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH
|
| 1199 |
+
QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe//
|
| 1200 |
+
IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB
|
| 1201 |
+
oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm
|
| 1202 |
+
IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA
|
| 1203 |
+
B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI
|
| 1204 |
+
WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU
|
| 1205 |
+
zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi
|
| 1206 |
+
AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd
|
| 1207 |
+
NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED
|
| 1208 |
+
RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs
|
| 1209 |
+
6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm
|
| 1210 |
+
NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN
|
| 1211 |
+
RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC
|
| 1212 |
+
EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM
|
| 1213 |
+
iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn
|
| 1214 |
+
JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI
|
| 1215 |
+
jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg
|
| 1216 |
+
YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI
|
| 1217 |
+
sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A
|
| 1218 |
+
AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV
|
| 1219 |
+
igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ
|
| 1220 |
+
cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd
|
| 1221 |
+
4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe
|
| 1222 |
+
B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL
|
| 1223 |
+
gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE
|
| 1224 |
+
BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM
|
| 1225 |
+
BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy
|
| 1226 |
+
Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA
|
| 1227 |
+
AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW
|
| 1228 |
+
Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq
|
| 1229 |
+
8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7
|
| 1230 |
+
2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA
|
| 1231 |
+
QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR
|
| 1232 |
+
QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk
|
| 1233 |
+
WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6
|
| 1234 |
+
yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF
|
| 1235 |
+
AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh
|
| 1236 |
+
YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4
|
| 1237 |
+
bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX
|
| 1238 |
+
IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX
|
| 1239 |
+
HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw
|
| 1240 |
+
cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY
|
| 1241 |
+
yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1
|
| 1242 |
+
MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA
|
| 1243 |
+
AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw
|
| 1244 |
+
UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po
|
| 1245 |
+
AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O
|
| 1246 |
+
XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A
|
| 1247 |
+
AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC
|
| 1248 |
+
Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA
|
| 1249 |
+
AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy
|
| 1250 |
+
AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl
|
| 1251 |
+
CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj
|
| 1252 |
+
k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI
|
| 1253 |
+
mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa
|
| 1254 |
+
EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA
|
| 1255 |
+
QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA
|
| 1256 |
+
AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA
|
| 1257 |
+
BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A
|
| 1258 |
+
AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA
|
| 1259 |
+
gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm
|
| 1260 |
+
lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV
|
| 1261 |
+
ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy
|
| 1262 |
+
AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA
|
| 1263 |
+
HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg
|
| 1264 |
+
B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk
|
| 1265 |
+
AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41
|
| 1266 |
+
ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA
|
| 1267 |
+
HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3
|
| 1268 |
+
JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB
|
| 1269 |
+
odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs
|
| 1270 |
+
AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA
|
| 1271 |
+
AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB
|
| 1272 |
+
QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA
|
| 1273 |
+
xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A
|
| 1274 |
+
TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A
|
| 1275 |
+
LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA
|
| 1276 |
+
AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ
|
| 1277 |
+
ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG
|
| 1278 |
+
AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE
|
| 1279 |
+
AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE
|
| 1280 |
+
kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ
|
| 1281 |
+
PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA
|
| 1282 |
+
AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA
|
| 1283 |
+
AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1284 |
+
AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1285 |
+
AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA
|
| 1286 |
+
ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA
|
| 1287 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA
|
| 1288 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA
|
| 1289 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA
|
| 1290 |
+
AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1291 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA
|
| 1292 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD
|
| 1293 |
+
/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA
|
| 1294 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1295 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA
|
| 1296 |
+
AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1297 |
+
AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA
|
| 1298 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA
|
| 1299 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA
|
| 1300 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1301 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1302 |
+
AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1303 |
+
AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1304 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA
|
| 1305 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA
|
| 1306 |
+
BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA
|
| 1307 |
+
AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA
|
| 1308 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA
|
| 1309 |
+
AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ
|
| 1310 |
+
ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
|
| 1311 |
+
gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
|
| 1312 |
+
YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
|
| 1313 |
+
AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
|
| 1314 |
+
"""
|
| 1315 |
+
)
|
| 1316 |
+
),
|
| 1317 |
+
10 if size is None else size,
|
| 1318 |
+
layout_engine=Layout.BASIC,
|
| 1319 |
+
)
|
| 1320 |
+
return load_default_imagefont()
|
PIL/ImageGrab.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# screen grabber
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2001-04-26 fl created
|
| 9 |
+
# 2001-09-17 fl use builtin driver, if present
|
| 10 |
+
# 2002-11-19 fl added grabclipboard support
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) 2001-2002 by Secret Labs AB
|
| 13 |
+
# Copyright (c) 2001-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 io
|
| 20 |
+
import os
|
| 21 |
+
import shutil
|
| 22 |
+
import subprocess
|
| 23 |
+
import sys
|
| 24 |
+
import tempfile
|
| 25 |
+
|
| 26 |
+
from . import Image
|
| 27 |
+
|
| 28 |
+
TYPE_CHECKING = False
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from . import ImageWin
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def grab(
|
| 34 |
+
bbox: tuple[int, int, int, int] | None = None,
|
| 35 |
+
include_layered_windows: bool = False,
|
| 36 |
+
all_screens: bool = False,
|
| 37 |
+
xdisplay: str | None = None,
|
| 38 |
+
window: int | ImageWin.HWND | None = None,
|
| 39 |
+
) -> Image.Image:
|
| 40 |
+
im: Image.Image
|
| 41 |
+
if xdisplay is None:
|
| 42 |
+
if sys.platform == "darwin":
|
| 43 |
+
fh, filepath = tempfile.mkstemp(".png")
|
| 44 |
+
os.close(fh)
|
| 45 |
+
args = ["screencapture"]
|
| 46 |
+
if window:
|
| 47 |
+
args += ["-l", str(window)]
|
| 48 |
+
elif bbox:
|
| 49 |
+
left, top, right, bottom = bbox
|
| 50 |
+
args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
|
| 51 |
+
subprocess.call(args + ["-x", filepath])
|
| 52 |
+
im = Image.open(filepath)
|
| 53 |
+
im.load()
|
| 54 |
+
os.unlink(filepath)
|
| 55 |
+
if bbox:
|
| 56 |
+
if window:
|
| 57 |
+
# Determine if the window was in Retina mode or not
|
| 58 |
+
# by capturing it without the shadow,
|
| 59 |
+
# and checking how different the width is
|
| 60 |
+
fh, filepath = tempfile.mkstemp(".png")
|
| 61 |
+
os.close(fh)
|
| 62 |
+
subprocess.call(
|
| 63 |
+
["screencapture", "-l", str(window), "-o", "-x", filepath]
|
| 64 |
+
)
|
| 65 |
+
with Image.open(filepath) as im_no_shadow:
|
| 66 |
+
retina = im.width - im_no_shadow.width > 100
|
| 67 |
+
os.unlink(filepath)
|
| 68 |
+
|
| 69 |
+
# Since screencapture's -R does not work with -l,
|
| 70 |
+
# crop the image manually
|
| 71 |
+
if retina:
|
| 72 |
+
left, top, right, bottom = bbox
|
| 73 |
+
im_cropped = im.resize(
|
| 74 |
+
(right - left, bottom - top),
|
| 75 |
+
box=tuple(coord * 2 for coord in bbox),
|
| 76 |
+
)
|
| 77 |
+
else:
|
| 78 |
+
im_cropped = im.crop(bbox)
|
| 79 |
+
im.close()
|
| 80 |
+
return im_cropped
|
| 81 |
+
else:
|
| 82 |
+
im_resized = im.resize((right - left, bottom - top))
|
| 83 |
+
im.close()
|
| 84 |
+
return im_resized
|
| 85 |
+
return im
|
| 86 |
+
elif sys.platform == "win32":
|
| 87 |
+
if window is not None:
|
| 88 |
+
all_screens = -1
|
| 89 |
+
offset, size, data = Image.core.grabscreen_win32(
|
| 90 |
+
include_layered_windows,
|
| 91 |
+
all_screens,
|
| 92 |
+
int(window) if window is not None else 0,
|
| 93 |
+
)
|
| 94 |
+
im = Image.frombytes(
|
| 95 |
+
"RGB",
|
| 96 |
+
size,
|
| 97 |
+
data,
|
| 98 |
+
# RGB, 32-bit line padding, origin lower left corner
|
| 99 |
+
"raw",
|
| 100 |
+
"BGR",
|
| 101 |
+
(size[0] * 3 + 3) & -4,
|
| 102 |
+
-1,
|
| 103 |
+
)
|
| 104 |
+
if bbox:
|
| 105 |
+
x0, y0 = offset
|
| 106 |
+
left, top, right, bottom = bbox
|
| 107 |
+
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
|
| 108 |
+
return im
|
| 109 |
+
# Cast to Optional[str] needed for Windows and macOS.
|
| 110 |
+
display_name: str | None = xdisplay
|
| 111 |
+
try:
|
| 112 |
+
if not Image.core.HAVE_XCB:
|
| 113 |
+
msg = "Pillow was built without XCB support"
|
| 114 |
+
raise OSError(msg)
|
| 115 |
+
size, data = Image.core.grabscreen_x11(display_name)
|
| 116 |
+
except OSError:
|
| 117 |
+
if display_name is None and sys.platform not in ("darwin", "win32"):
|
| 118 |
+
if shutil.which("gnome-screenshot"):
|
| 119 |
+
args = ["gnome-screenshot", "-f"]
|
| 120 |
+
elif shutil.which("grim"):
|
| 121 |
+
args = ["grim"]
|
| 122 |
+
elif shutil.which("spectacle"):
|
| 123 |
+
args = ["spectacle", "-n", "-b", "-f", "-o"]
|
| 124 |
+
else:
|
| 125 |
+
raise
|
| 126 |
+
fh, filepath = tempfile.mkstemp(".png")
|
| 127 |
+
os.close(fh)
|
| 128 |
+
subprocess.call(args + [filepath])
|
| 129 |
+
im = Image.open(filepath)
|
| 130 |
+
im.load()
|
| 131 |
+
os.unlink(filepath)
|
| 132 |
+
if bbox:
|
| 133 |
+
im_cropped = im.crop(bbox)
|
| 134 |
+
im.close()
|
| 135 |
+
return im_cropped
|
| 136 |
+
return im
|
| 137 |
+
else:
|
| 138 |
+
raise
|
| 139 |
+
else:
|
| 140 |
+
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
|
| 141 |
+
if bbox:
|
| 142 |
+
im = im.crop(bbox)
|
| 143 |
+
return im
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def grabclipboard() -> Image.Image | list[str] | None:
|
| 147 |
+
if sys.platform == "darwin":
|
| 148 |
+
p = subprocess.run(
|
| 149 |
+
["osascript", "-e", "get the clipboard as «class PNGf»"],
|
| 150 |
+
capture_output=True,
|
| 151 |
+
)
|
| 152 |
+
if p.returncode != 0:
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
import binascii
|
| 156 |
+
|
| 157 |
+
data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
|
| 158 |
+
return Image.open(data)
|
| 159 |
+
elif sys.platform == "win32":
|
| 160 |
+
fmt, data = Image.core.grabclipboard_win32()
|
| 161 |
+
if fmt == "file": # CF_HDROP
|
| 162 |
+
import struct
|
| 163 |
+
|
| 164 |
+
o = struct.unpack_from("I", data)[0]
|
| 165 |
+
if data[16] == 0:
|
| 166 |
+
files = data[o:].decode("mbcs").split("\0")
|
| 167 |
+
else:
|
| 168 |
+
files = data[o:].decode("utf-16le").split("\0")
|
| 169 |
+
return files[: files.index("")]
|
| 170 |
+
if isinstance(data, bytes):
|
| 171 |
+
data = io.BytesIO(data)
|
| 172 |
+
if fmt == "png":
|
| 173 |
+
from . import PngImagePlugin
|
| 174 |
+
|
| 175 |
+
return PngImagePlugin.PngImageFile(data)
|
| 176 |
+
elif fmt == "DIB":
|
| 177 |
+
from . import BmpImagePlugin
|
| 178 |
+
|
| 179 |
+
return BmpImagePlugin.DibImageFile(data)
|
| 180 |
+
return None
|
| 181 |
+
else:
|
| 182 |
+
if os.getenv("WAYLAND_DISPLAY"):
|
| 183 |
+
session_type = "wayland"
|
| 184 |
+
elif os.getenv("DISPLAY"):
|
| 185 |
+
session_type = "x11"
|
| 186 |
+
else: # Session type check failed
|
| 187 |
+
session_type = None
|
| 188 |
+
|
| 189 |
+
if shutil.which("wl-paste") and session_type in ("wayland", None):
|
| 190 |
+
args = ["wl-paste", "-t", "image"]
|
| 191 |
+
elif shutil.which("xclip") and session_type in ("x11", None):
|
| 192 |
+
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
|
| 193 |
+
else:
|
| 194 |
+
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
|
| 195 |
+
raise NotImplementedError(msg)
|
| 196 |
+
|
| 197 |
+
p = subprocess.run(args, capture_output=True)
|
| 198 |
+
if p.returncode != 0:
|
| 199 |
+
err = p.stderr
|
| 200 |
+
for silent_error in [
|
| 201 |
+
# wl-paste, when the clipboard is empty
|
| 202 |
+
b"Nothing is copied",
|
| 203 |
+
# Ubuntu/Debian wl-paste, when the clipboard is empty
|
| 204 |
+
b"No selection",
|
| 205 |
+
# Ubuntu/Debian wl-paste, when an image isn't available
|
| 206 |
+
b"No suitable type of content copied",
|
| 207 |
+
# wl-paste or Ubuntu/Debian xclip, when an image isn't available
|
| 208 |
+
b" not available",
|
| 209 |
+
# xclip, when an image isn't available
|
| 210 |
+
b"cannot convert ",
|
| 211 |
+
# xclip, when the clipboard isn't initialized
|
| 212 |
+
b"xclip: Error: There is no owner for the ",
|
| 213 |
+
]:
|
| 214 |
+
if silent_error in err:
|
| 215 |
+
return None
|
| 216 |
+
msg = f"{args[0]} error"
|
| 217 |
+
if err:
|
| 218 |
+
msg += f": {err.strip().decode()}"
|
| 219 |
+
raise ChildProcessError(msg)
|
| 220 |
+
|
| 221 |
+
data = io.BytesIO(p.stdout)
|
| 222 |
+
im = Image.open(data)
|
| 223 |
+
im.load()
|
| 224 |
+
return im
|
PIL/ImageMode.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# standard mode descriptors
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2006-03-20 fl Added
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) 2006 by Secret Labs AB.
|
| 11 |
+
# Copyright (c) 2006 by Fredrik Lundh.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
from functools import lru_cache
|
| 19 |
+
from typing import NamedTuple
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ModeDescriptor(NamedTuple):
|
| 23 |
+
"""Wrapper for mode strings."""
|
| 24 |
+
|
| 25 |
+
mode: str
|
| 26 |
+
bands: tuple[str, ...]
|
| 27 |
+
basemode: str
|
| 28 |
+
basetype: str
|
| 29 |
+
typestr: str
|
| 30 |
+
|
| 31 |
+
def __str__(self) -> str:
|
| 32 |
+
return self.mode
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@lru_cache
|
| 36 |
+
def getmode(mode: str) -> ModeDescriptor:
|
| 37 |
+
"""Gets a mode descriptor for the given mode."""
|
| 38 |
+
endian = "<" if sys.byteorder == "little" else ">"
|
| 39 |
+
|
| 40 |
+
modes = {
|
| 41 |
+
# core modes
|
| 42 |
+
# Bits need to be extended to bytes
|
| 43 |
+
"1": ("L", "L", ("1",), "|b1"),
|
| 44 |
+
"L": ("L", "L", ("L",), "|u1"),
|
| 45 |
+
"I": ("L", "I", ("I",), f"{endian}i4"),
|
| 46 |
+
"F": ("L", "F", ("F",), f"{endian}f4"),
|
| 47 |
+
"P": ("P", "L", ("P",), "|u1"),
|
| 48 |
+
"RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
|
| 49 |
+
"RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
|
| 50 |
+
"RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"),
|
| 51 |
+
"CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"),
|
| 52 |
+
"YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"),
|
| 53 |
+
# UNDONE - unsigned |u1i1i1
|
| 54 |
+
"LAB": ("RGB", "L", ("L", "A", "B"), "|u1"),
|
| 55 |
+
"HSV": ("RGB", "L", ("H", "S", "V"), "|u1"),
|
| 56 |
+
# extra experimental modes
|
| 57 |
+
"RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"),
|
| 58 |
+
"LA": ("L", "L", ("L", "A"), "|u1"),
|
| 59 |
+
"La": ("L", "L", ("L", "a"), "|u1"),
|
| 60 |
+
"PA": ("RGB", "L", ("P", "A"), "|u1"),
|
| 61 |
+
}
|
| 62 |
+
if mode in modes:
|
| 63 |
+
base_mode, base_type, bands, type_str = modes[mode]
|
| 64 |
+
return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
|
| 65 |
+
|
| 66 |
+
mapping_modes = {
|
| 67 |
+
# I;16 == I;16L, and I;32 == I;32L
|
| 68 |
+
"I;16": "<u2",
|
| 69 |
+
"I;16S": "<i2",
|
| 70 |
+
"I;16L": "<u2",
|
| 71 |
+
"I;16LS": "<i2",
|
| 72 |
+
"I;16B": ">u2",
|
| 73 |
+
"I;16BS": ">i2",
|
| 74 |
+
"I;16N": f"{endian}u2",
|
| 75 |
+
"I;16NS": f"{endian}i2",
|
| 76 |
+
"I;32": "<u4",
|
| 77 |
+
"I;32B": ">u4",
|
| 78 |
+
"I;32L": "<u4",
|
| 79 |
+
"I;32S": "<i4",
|
| 80 |
+
"I;32BS": ">i4",
|
| 81 |
+
"I;32LS": "<i4",
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
type_str = mapping_modes[mode]
|
| 85 |
+
return ModeDescriptor(mode, ("I",), "L", "L", type_str)
|
PIL/ImageMorph.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A binary morphology add-on for the Python Imaging Library
|
| 2 |
+
#
|
| 3 |
+
# History:
|
| 4 |
+
# 2014-06-04 Initial version.
|
| 5 |
+
#
|
| 6 |
+
# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
from . import Image, _imagingmorph
|
| 12 |
+
|
| 13 |
+
LUT_SIZE = 1 << 9
|
| 14 |
+
|
| 15 |
+
# fmt: off
|
| 16 |
+
ROTATION_MATRIX = [
|
| 17 |
+
6, 3, 0,
|
| 18 |
+
7, 4, 1,
|
| 19 |
+
8, 5, 2,
|
| 20 |
+
]
|
| 21 |
+
MIRROR_MATRIX = [
|
| 22 |
+
2, 1, 0,
|
| 23 |
+
5, 4, 3,
|
| 24 |
+
8, 7, 6,
|
| 25 |
+
]
|
| 26 |
+
# fmt: on
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class LutBuilder:
|
| 30 |
+
"""A class for building a MorphLut from a descriptive language
|
| 31 |
+
|
| 32 |
+
The input patterns is a list of a strings sequences like these::
|
| 33 |
+
|
| 34 |
+
4:(...
|
| 35 |
+
.1.
|
| 36 |
+
111)->1
|
| 37 |
+
|
| 38 |
+
(whitespaces including linebreaks are ignored). The option 4
|
| 39 |
+
describes a series of symmetry operations (in this case a
|
| 40 |
+
4-rotation), the pattern is described by:
|
| 41 |
+
|
| 42 |
+
- . or X - Ignore
|
| 43 |
+
- 1 - Pixel is on
|
| 44 |
+
- 0 - Pixel is off
|
| 45 |
+
|
| 46 |
+
The result of the operation is described after "->" string.
|
| 47 |
+
|
| 48 |
+
The default is to return the current pixel value, which is
|
| 49 |
+
returned if no other match is found.
|
| 50 |
+
|
| 51 |
+
Operations:
|
| 52 |
+
|
| 53 |
+
- 4 - 4 way rotation
|
| 54 |
+
- N - Negate
|
| 55 |
+
- 1 - Dummy op for no other operation (an op must always be given)
|
| 56 |
+
- M - Mirroring
|
| 57 |
+
|
| 58 |
+
Example::
|
| 59 |
+
|
| 60 |
+
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
| 61 |
+
lut = lb.build_lut()
|
| 62 |
+
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self, patterns: list[str] | None = None, op_name: str | None = None
|
| 67 |
+
) -> None:
|
| 68 |
+
"""
|
| 69 |
+
:param patterns: A list of input patterns, or None.
|
| 70 |
+
:param op_name: The name of a known pattern. One of "corner", "dilation4",
|
| 71 |
+
"dilation8", "erosion4", "erosion8" or "edge".
|
| 72 |
+
:exception Exception: If the op_name is not recognized.
|
| 73 |
+
"""
|
| 74 |
+
self.lut: bytearray | None = None
|
| 75 |
+
if op_name is not None:
|
| 76 |
+
known_patterns = {
|
| 77 |
+
"corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
|
| 78 |
+
"dilation4": ["4:(... .0. .1.)->1"],
|
| 79 |
+
"dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"],
|
| 80 |
+
"erosion4": ["4:(... .1. .0.)->0"],
|
| 81 |
+
"erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"],
|
| 82 |
+
"edge": [
|
| 83 |
+
"1:(... ... ...)->0",
|
| 84 |
+
"4:(.0. .1. ...)->1",
|
| 85 |
+
"4:(01. .1. ...)->1",
|
| 86 |
+
],
|
| 87 |
+
}
|
| 88 |
+
if op_name not in known_patterns:
|
| 89 |
+
msg = f"Unknown pattern {op_name}!"
|
| 90 |
+
raise Exception(msg)
|
| 91 |
+
|
| 92 |
+
self.patterns = known_patterns[op_name]
|
| 93 |
+
elif patterns is not None:
|
| 94 |
+
self.patterns = patterns
|
| 95 |
+
else:
|
| 96 |
+
self.patterns = []
|
| 97 |
+
|
| 98 |
+
def add_patterns(self, patterns: list[str]) -> None:
|
| 99 |
+
"""
|
| 100 |
+
Append to list of patterns.
|
| 101 |
+
|
| 102 |
+
:param patterns: Additional patterns.
|
| 103 |
+
"""
|
| 104 |
+
self.patterns += patterns
|
| 105 |
+
|
| 106 |
+
def build_default_lut(self) -> bytearray:
|
| 107 |
+
"""
|
| 108 |
+
Set the current LUT, and return it.
|
| 109 |
+
|
| 110 |
+
This is the default LUT that patterns will be applied against when building.
|
| 111 |
+
"""
|
| 112 |
+
symbols = [0, 1]
|
| 113 |
+
m = 1 << 4 # pos of current pixel
|
| 114 |
+
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
|
| 115 |
+
return self.lut
|
| 116 |
+
|
| 117 |
+
def get_lut(self) -> bytearray | None:
|
| 118 |
+
"""
|
| 119 |
+
Returns the current LUT
|
| 120 |
+
"""
|
| 121 |
+
return self.lut
|
| 122 |
+
|
| 123 |
+
def _string_permute(self, pattern: str, permutation: list[int]) -> str:
|
| 124 |
+
"""Takes a pattern and a permutation and returns the
|
| 125 |
+
string permuted according to the permutation list.
|
| 126 |
+
"""
|
| 127 |
+
assert len(permutation) == 9
|
| 128 |
+
return "".join(pattern[p] for p in permutation)
|
| 129 |
+
|
| 130 |
+
def _pattern_permute(
|
| 131 |
+
self, basic_pattern: str, options: str, basic_result: int
|
| 132 |
+
) -> list[tuple[str, int]]:
|
| 133 |
+
"""Takes a basic pattern and its result and clones
|
| 134 |
+
the pattern according to the modifications described in the $options
|
| 135 |
+
parameter. It returns a list of all cloned patterns."""
|
| 136 |
+
patterns = [(basic_pattern, basic_result)]
|
| 137 |
+
|
| 138 |
+
# rotations
|
| 139 |
+
if "4" in options:
|
| 140 |
+
res = patterns[-1][1]
|
| 141 |
+
for i in range(4):
|
| 142 |
+
patterns.append(
|
| 143 |
+
(self._string_permute(patterns[-1][0], ROTATION_MATRIX), res)
|
| 144 |
+
)
|
| 145 |
+
# mirror
|
| 146 |
+
if "M" in options:
|
| 147 |
+
n = len(patterns)
|
| 148 |
+
for pattern, res in patterns[:n]:
|
| 149 |
+
patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res))
|
| 150 |
+
|
| 151 |
+
# negate
|
| 152 |
+
if "N" in options:
|
| 153 |
+
n = len(patterns)
|
| 154 |
+
for pattern, res in patterns[:n]:
|
| 155 |
+
# Swap 0 and 1
|
| 156 |
+
pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1")
|
| 157 |
+
res = 1 - int(res)
|
| 158 |
+
patterns.append((pattern, res))
|
| 159 |
+
|
| 160 |
+
return patterns
|
| 161 |
+
|
| 162 |
+
def build_lut(self) -> bytearray:
|
| 163 |
+
"""Compile all patterns into a morphology LUT, and return it.
|
| 164 |
+
|
| 165 |
+
This is the data to be passed into MorphOp."""
|
| 166 |
+
self.build_default_lut()
|
| 167 |
+
assert self.lut is not None
|
| 168 |
+
patterns = []
|
| 169 |
+
|
| 170 |
+
# Parse and create symmetries of the patterns strings
|
| 171 |
+
for p in self.patterns:
|
| 172 |
+
m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
|
| 173 |
+
if not m:
|
| 174 |
+
msg = 'Syntax error in pattern "' + p + '"'
|
| 175 |
+
raise Exception(msg)
|
| 176 |
+
options = m.group(1)
|
| 177 |
+
pattern = m.group(2)
|
| 178 |
+
result = int(m.group(3))
|
| 179 |
+
|
| 180 |
+
# Get rid of spaces
|
| 181 |
+
pattern = pattern.replace(" ", "").replace("\n", "")
|
| 182 |
+
|
| 183 |
+
patterns += self._pattern_permute(pattern, options, result)
|
| 184 |
+
|
| 185 |
+
# Compile the patterns into regular expressions for speed
|
| 186 |
+
compiled_patterns = []
|
| 187 |
+
for pattern in patterns:
|
| 188 |
+
p = pattern[0].replace(".", "X").replace("X", "[01]")
|
| 189 |
+
compiled_patterns.append((re.compile(p), pattern[1]))
|
| 190 |
+
|
| 191 |
+
# Step through table and find patterns that match.
|
| 192 |
+
# Note that all the patterns are searched. The last one found takes priority
|
| 193 |
+
for i in range(LUT_SIZE):
|
| 194 |
+
# Build the bit pattern
|
| 195 |
+
bitpattern = bin(i)[2:]
|
| 196 |
+
bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
|
| 197 |
+
|
| 198 |
+
for pattern, r in compiled_patterns:
|
| 199 |
+
if pattern.match(bitpattern):
|
| 200 |
+
self.lut[i] = [0, 1][r]
|
| 201 |
+
|
| 202 |
+
return self.lut
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
class MorphOp:
|
| 206 |
+
"""A class for binary morphological operators"""
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self,
|
| 210 |
+
lut: bytearray | None = None,
|
| 211 |
+
op_name: str | None = None,
|
| 212 |
+
patterns: list[str] | None = None,
|
| 213 |
+
) -> None:
|
| 214 |
+
"""Create a binary morphological operator.
|
| 215 |
+
|
| 216 |
+
If the LUT is not provided, then it is built using LutBuilder from the op_name
|
| 217 |
+
or the patterns.
|
| 218 |
+
|
| 219 |
+
:param lut: The LUT data.
|
| 220 |
+
:param patterns: A list of input patterns, or None.
|
| 221 |
+
:param op_name: The name of a known pattern. One of "corner", "dilation4",
|
| 222 |
+
"dilation8", "erosion4", "erosion8", "edge".
|
| 223 |
+
:exception Exception: If the op_name is not recognized.
|
| 224 |
+
"""
|
| 225 |
+
if patterns is None and op_name is None:
|
| 226 |
+
self.lut = lut
|
| 227 |
+
else:
|
| 228 |
+
self.lut = LutBuilder(patterns, op_name).build_lut()
|
| 229 |
+
|
| 230 |
+
def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
|
| 231 |
+
"""Run a single morphological operation on an image.
|
| 232 |
+
|
| 233 |
+
Returns a tuple of the number of changed pixels and the
|
| 234 |
+
morphed image.
|
| 235 |
+
|
| 236 |
+
:param image: A 1-mode or L-mode image.
|
| 237 |
+
:exception Exception: If the current operator is None.
|
| 238 |
+
:exception ValueError: If the image is not 1 or L mode."""
|
| 239 |
+
if self.lut is None:
|
| 240 |
+
msg = "No operator loaded"
|
| 241 |
+
raise Exception(msg)
|
| 242 |
+
|
| 243 |
+
if image.mode not in ("1", "L"):
|
| 244 |
+
msg = "Image mode must be 1 or L"
|
| 245 |
+
raise ValueError(msg)
|
| 246 |
+
outimage = Image.new(image.mode, image.size)
|
| 247 |
+
count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
|
| 248 |
+
return count, outimage
|
| 249 |
+
|
| 250 |
+
def match(self, image: Image.Image) -> list[tuple[int, int]]:
|
| 251 |
+
"""Get a list of coordinates matching the morphological operation on
|
| 252 |
+
an image.
|
| 253 |
+
|
| 254 |
+
Returns a list of tuples of (x,y) coordinates of all matching pixels. See
|
| 255 |
+
:ref:`coordinate-system`.
|
| 256 |
+
|
| 257 |
+
:param image: A 1-mode or L-mode image.
|
| 258 |
+
:exception Exception: If the current operator is None.
|
| 259 |
+
:exception ValueError: If the image is not 1 or L mode."""
|
| 260 |
+
if self.lut is None:
|
| 261 |
+
msg = "No operator loaded"
|
| 262 |
+
raise Exception(msg)
|
| 263 |
+
|
| 264 |
+
if image.mode not in ("1", "L"):
|
| 265 |
+
msg = "Image mode must be 1 or L"
|
| 266 |
+
raise ValueError(msg)
|
| 267 |
+
return _imagingmorph.match(bytes(self.lut), image.getim())
|
| 268 |
+
|
| 269 |
+
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
|
| 270 |
+
"""Get a list of all turned on pixels in a 1 or L mode image.
|
| 271 |
+
|
| 272 |
+
Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See
|
| 273 |
+
:ref:`coordinate-system`.
|
| 274 |
+
|
| 275 |
+
:param image: A 1-mode or L-mode image.
|
| 276 |
+
:exception ValueError: If the image is not 1 or L mode."""
|
| 277 |
+
|
| 278 |
+
if image.mode not in ("1", "L"):
|
| 279 |
+
msg = "Image mode must be 1 or L"
|
| 280 |
+
raise ValueError(msg)
|
| 281 |
+
return _imagingmorph.get_on_pixels(image.getim())
|
| 282 |
+
|
| 283 |
+
def load_lut(self, filename: str) -> None:
|
| 284 |
+
"""
|
| 285 |
+
Load an operator from an mrl file
|
| 286 |
+
|
| 287 |
+
:param filename: The file to read from.
|
| 288 |
+
:exception Exception: If the length of the file data is not 512.
|
| 289 |
+
"""
|
| 290 |
+
with open(filename, "rb") as f:
|
| 291 |
+
self.lut = bytearray(f.read())
|
| 292 |
+
|
| 293 |
+
if len(self.lut) != LUT_SIZE:
|
| 294 |
+
self.lut = None
|
| 295 |
+
msg = "Wrong size operator file!"
|
| 296 |
+
raise Exception(msg)
|
| 297 |
+
|
| 298 |
+
def save_lut(self, filename: str) -> None:
|
| 299 |
+
"""
|
| 300 |
+
Save an operator to an mrl file.
|
| 301 |
+
|
| 302 |
+
:param filename: The destination file.
|
| 303 |
+
:exception Exception: If the current operator is None.
|
| 304 |
+
"""
|
| 305 |
+
if self.lut is None:
|
| 306 |
+
msg = "No operator loaded"
|
| 307 |
+
raise Exception(msg)
|
| 308 |
+
with open(filename, "wb") as f:
|
| 309 |
+
f.write(self.lut)
|
| 310 |
+
|
| 311 |
+
def set_lut(self, lut: bytearray | None) -> None:
|
| 312 |
+
"""
|
| 313 |
+
Set the LUT from an external source
|
| 314 |
+
|
| 315 |
+
:param lut: A new LUT.
|
| 316 |
+
"""
|
| 317 |
+
self.lut = lut
|
PIL/ImageOps.py
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# standard image operations
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2001-10-20 fl Created
|
| 9 |
+
# 2001-10-23 fl Added autocontrast operator
|
| 10 |
+
# 2001-12-18 fl Added Kevin's fit operator
|
| 11 |
+
# 2004-03-14 fl Fixed potential division by zero in equalize
|
| 12 |
+
# 2005-05-05 fl Fixed equalize for low number of values
|
| 13 |
+
#
|
| 14 |
+
# Copyright (c) 2001-2004 by Secret Labs AB
|
| 15 |
+
# Copyright (c) 2001-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 functools
|
| 22 |
+
import operator
|
| 23 |
+
import re
|
| 24 |
+
from collections.abc import Sequence
|
| 25 |
+
from typing import Literal, Protocol, cast, overload
|
| 26 |
+
|
| 27 |
+
from . import ExifTags, Image, ImagePalette
|
| 28 |
+
|
| 29 |
+
#
|
| 30 |
+
# helpers
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
|
| 34 |
+
if isinstance(border, tuple):
|
| 35 |
+
if len(border) == 2:
|
| 36 |
+
left, top = right, bottom = border
|
| 37 |
+
elif len(border) == 4:
|
| 38 |
+
left, top, right, bottom = border
|
| 39 |
+
else:
|
| 40 |
+
left = top = right = bottom = border
|
| 41 |
+
return left, top, right, bottom
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
|
| 45 |
+
if isinstance(color, str):
|
| 46 |
+
from . import ImageColor
|
| 47 |
+
|
| 48 |
+
color = ImageColor.getcolor(color, mode)
|
| 49 |
+
return color
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
|
| 53 |
+
if image.mode == "P":
|
| 54 |
+
# FIXME: apply to lookup table, not image data
|
| 55 |
+
msg = "mode P support coming soon"
|
| 56 |
+
raise NotImplementedError(msg)
|
| 57 |
+
elif image.mode in ("L", "RGB"):
|
| 58 |
+
if image.mode == "RGB" and len(lut) == 256:
|
| 59 |
+
lut = lut + lut + lut
|
| 60 |
+
return image.point(lut)
|
| 61 |
+
else:
|
| 62 |
+
msg = f"not supported for mode {image.mode}"
|
| 63 |
+
raise OSError(msg)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
#
|
| 67 |
+
# actions
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def autocontrast(
|
| 71 |
+
image: Image.Image,
|
| 72 |
+
cutoff: float | tuple[float, float] = 0,
|
| 73 |
+
ignore: int | Sequence[int] | None = None,
|
| 74 |
+
mask: Image.Image | None = None,
|
| 75 |
+
preserve_tone: bool = False,
|
| 76 |
+
) -> Image.Image:
|
| 77 |
+
"""
|
| 78 |
+
Maximize (normalize) image contrast. This function calculates a
|
| 79 |
+
histogram of the input image (or mask region), removes ``cutoff`` percent of the
|
| 80 |
+
lightest and darkest pixels from the histogram, and remaps the image
|
| 81 |
+
so that the darkest pixel becomes black (0), and the lightest
|
| 82 |
+
becomes white (255).
|
| 83 |
+
|
| 84 |
+
:param image: The image to process.
|
| 85 |
+
:param cutoff: The percent to cut off from the histogram on the low and
|
| 86 |
+
high ends. Either a tuple of (low, high), or a single
|
| 87 |
+
number for both.
|
| 88 |
+
:param ignore: The background pixel value (use None for no background).
|
| 89 |
+
:param mask: Histogram used in contrast operation is computed using pixels
|
| 90 |
+
within the mask. If no mask is given the entire image is used
|
| 91 |
+
for histogram computation.
|
| 92 |
+
:param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
|
| 93 |
+
|
| 94 |
+
.. versionadded:: 8.2.0
|
| 95 |
+
|
| 96 |
+
:return: An image.
|
| 97 |
+
"""
|
| 98 |
+
if preserve_tone:
|
| 99 |
+
histogram = image.convert("L").histogram(mask)
|
| 100 |
+
else:
|
| 101 |
+
histogram = image.histogram(mask)
|
| 102 |
+
|
| 103 |
+
lut = []
|
| 104 |
+
for layer in range(0, len(histogram), 256):
|
| 105 |
+
h = histogram[layer : layer + 256]
|
| 106 |
+
if ignore is not None:
|
| 107 |
+
# get rid of outliers
|
| 108 |
+
if isinstance(ignore, int):
|
| 109 |
+
h[ignore] = 0
|
| 110 |
+
else:
|
| 111 |
+
for ix in ignore:
|
| 112 |
+
h[ix] = 0
|
| 113 |
+
if cutoff:
|
| 114 |
+
# cut off pixels from both ends of the histogram
|
| 115 |
+
if not isinstance(cutoff, tuple):
|
| 116 |
+
cutoff = (cutoff, cutoff)
|
| 117 |
+
# get number of pixels
|
| 118 |
+
n = 0
|
| 119 |
+
for ix in range(256):
|
| 120 |
+
n = n + h[ix]
|
| 121 |
+
# remove cutoff% pixels from the low end
|
| 122 |
+
cut = int(n * cutoff[0] // 100)
|
| 123 |
+
for lo in range(256):
|
| 124 |
+
if cut > h[lo]:
|
| 125 |
+
cut = cut - h[lo]
|
| 126 |
+
h[lo] = 0
|
| 127 |
+
else:
|
| 128 |
+
h[lo] -= cut
|
| 129 |
+
cut = 0
|
| 130 |
+
if cut <= 0:
|
| 131 |
+
break
|
| 132 |
+
# remove cutoff% samples from the high end
|
| 133 |
+
cut = int(n * cutoff[1] // 100)
|
| 134 |
+
for hi in range(255, -1, -1):
|
| 135 |
+
if cut > h[hi]:
|
| 136 |
+
cut = cut - h[hi]
|
| 137 |
+
h[hi] = 0
|
| 138 |
+
else:
|
| 139 |
+
h[hi] -= cut
|
| 140 |
+
cut = 0
|
| 141 |
+
if cut <= 0:
|
| 142 |
+
break
|
| 143 |
+
# find lowest/highest samples after preprocessing
|
| 144 |
+
for lo in range(256):
|
| 145 |
+
if h[lo]:
|
| 146 |
+
break
|
| 147 |
+
for hi in range(255, -1, -1):
|
| 148 |
+
if h[hi]:
|
| 149 |
+
break
|
| 150 |
+
if hi <= lo:
|
| 151 |
+
# don't bother
|
| 152 |
+
lut.extend(list(range(256)))
|
| 153 |
+
else:
|
| 154 |
+
scale = 255.0 / (hi - lo)
|
| 155 |
+
offset = -lo * scale
|
| 156 |
+
for ix in range(256):
|
| 157 |
+
ix = int(ix * scale + offset)
|
| 158 |
+
if ix < 0:
|
| 159 |
+
ix = 0
|
| 160 |
+
elif ix > 255:
|
| 161 |
+
ix = 255
|
| 162 |
+
lut.append(ix)
|
| 163 |
+
return _lut(image, lut)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def colorize(
|
| 167 |
+
image: Image.Image,
|
| 168 |
+
black: str | tuple[int, ...],
|
| 169 |
+
white: str | tuple[int, ...],
|
| 170 |
+
mid: str | int | tuple[int, ...] | None = None,
|
| 171 |
+
blackpoint: int = 0,
|
| 172 |
+
whitepoint: int = 255,
|
| 173 |
+
midpoint: int = 127,
|
| 174 |
+
) -> Image.Image:
|
| 175 |
+
"""
|
| 176 |
+
Colorize grayscale image.
|
| 177 |
+
This function calculates a color wedge which maps all black pixels in
|
| 178 |
+
the source image to the first color and all white pixels to the
|
| 179 |
+
second color. If ``mid`` is specified, it uses three-color mapping.
|
| 180 |
+
The ``black`` and ``white`` arguments should be RGB tuples or color names;
|
| 181 |
+
optionally you can use three-color mapping by also specifying ``mid``.
|
| 182 |
+
Mapping positions for any of the colors can be specified
|
| 183 |
+
(e.g. ``blackpoint``), where these parameters are the integer
|
| 184 |
+
value corresponding to where the corresponding color should be mapped.
|
| 185 |
+
These parameters must have logical order, such that
|
| 186 |
+
``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
|
| 187 |
+
|
| 188 |
+
:param image: The image to colorize.
|
| 189 |
+
:param black: The color to use for black input pixels.
|
| 190 |
+
:param white: The color to use for white input pixels.
|
| 191 |
+
:param mid: The color to use for midtone input pixels.
|
| 192 |
+
:param blackpoint: an int value [0, 255] for the black mapping.
|
| 193 |
+
:param whitepoint: an int value [0, 255] for the white mapping.
|
| 194 |
+
:param midpoint: an int value [0, 255] for the midtone mapping.
|
| 195 |
+
:return: An image.
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
# Initial asserts
|
| 199 |
+
assert image.mode == "L"
|
| 200 |
+
if mid is None:
|
| 201 |
+
assert 0 <= blackpoint <= whitepoint <= 255
|
| 202 |
+
else:
|
| 203 |
+
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
|
| 204 |
+
|
| 205 |
+
# Define colors from arguments
|
| 206 |
+
rgb_black = cast(Sequence[int], _color(black, "RGB"))
|
| 207 |
+
rgb_white = cast(Sequence[int], _color(white, "RGB"))
|
| 208 |
+
rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
|
| 209 |
+
|
| 210 |
+
# Empty lists for the mapping
|
| 211 |
+
red = []
|
| 212 |
+
green = []
|
| 213 |
+
blue = []
|
| 214 |
+
|
| 215 |
+
# Create the low-end values
|
| 216 |
+
for i in range(blackpoint):
|
| 217 |
+
red.append(rgb_black[0])
|
| 218 |
+
green.append(rgb_black[1])
|
| 219 |
+
blue.append(rgb_black[2])
|
| 220 |
+
|
| 221 |
+
# Create the mapping (2-color)
|
| 222 |
+
if rgb_mid is None:
|
| 223 |
+
range_map = range(whitepoint - blackpoint)
|
| 224 |
+
|
| 225 |
+
for i in range_map:
|
| 226 |
+
red.append(
|
| 227 |
+
rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
|
| 228 |
+
)
|
| 229 |
+
green.append(
|
| 230 |
+
rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
|
| 231 |
+
)
|
| 232 |
+
blue.append(
|
| 233 |
+
rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Create the mapping (3-color)
|
| 237 |
+
else:
|
| 238 |
+
range_map1 = range(midpoint - blackpoint)
|
| 239 |
+
range_map2 = range(whitepoint - midpoint)
|
| 240 |
+
|
| 241 |
+
for i in range_map1:
|
| 242 |
+
red.append(
|
| 243 |
+
rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
|
| 244 |
+
)
|
| 245 |
+
green.append(
|
| 246 |
+
rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
|
| 247 |
+
)
|
| 248 |
+
blue.append(
|
| 249 |
+
rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
|
| 250 |
+
)
|
| 251 |
+
for i in range_map2:
|
| 252 |
+
red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
|
| 253 |
+
green.append(
|
| 254 |
+
rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
|
| 255 |
+
)
|
| 256 |
+
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
|
| 257 |
+
|
| 258 |
+
# Create the high-end values
|
| 259 |
+
for i in range(256 - whitepoint):
|
| 260 |
+
red.append(rgb_white[0])
|
| 261 |
+
green.append(rgb_white[1])
|
| 262 |
+
blue.append(rgb_white[2])
|
| 263 |
+
|
| 264 |
+
# Return converted image
|
| 265 |
+
image = image.convert("RGB")
|
| 266 |
+
return _lut(image, red + green + blue)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def contain(
|
| 270 |
+
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
|
| 271 |
+
) -> Image.Image:
|
| 272 |
+
"""
|
| 273 |
+
Returns a resized version of the image, set to the maximum width and height
|
| 274 |
+
within the requested size, while maintaining the original aspect ratio.
|
| 275 |
+
|
| 276 |
+
:param image: The image to resize.
|
| 277 |
+
:param size: The requested output size in pixels, given as a
|
| 278 |
+
(width, height) tuple.
|
| 279 |
+
:param method: Resampling method to use. Default is
|
| 280 |
+
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
| 281 |
+
See :ref:`concept-filters`.
|
| 282 |
+
:return: An image.
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
im_ratio = image.width / image.height
|
| 286 |
+
dest_ratio = size[0] / size[1]
|
| 287 |
+
|
| 288 |
+
if im_ratio != dest_ratio:
|
| 289 |
+
if im_ratio > dest_ratio:
|
| 290 |
+
new_height = round(image.height / image.width * size[0])
|
| 291 |
+
if new_height != size[1]:
|
| 292 |
+
size = (size[0], new_height)
|
| 293 |
+
else:
|
| 294 |
+
new_width = round(image.width / image.height * size[1])
|
| 295 |
+
if new_width != size[0]:
|
| 296 |
+
size = (new_width, size[1])
|
| 297 |
+
return image.resize(size, resample=method)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def cover(
|
| 301 |
+
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
|
| 302 |
+
) -> Image.Image:
|
| 303 |
+
"""
|
| 304 |
+
Returns a resized version of the image, so that the requested size is
|
| 305 |
+
covered, while maintaining the original aspect ratio.
|
| 306 |
+
|
| 307 |
+
:param image: The image to resize.
|
| 308 |
+
:param size: The requested output size in pixels, given as a
|
| 309 |
+
(width, height) tuple.
|
| 310 |
+
:param method: Resampling method to use. Default is
|
| 311 |
+
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
| 312 |
+
See :ref:`concept-filters`.
|
| 313 |
+
:return: An image.
|
| 314 |
+
"""
|
| 315 |
+
|
| 316 |
+
im_ratio = image.width / image.height
|
| 317 |
+
dest_ratio = size[0] / size[1]
|
| 318 |
+
|
| 319 |
+
if im_ratio != dest_ratio:
|
| 320 |
+
if im_ratio < dest_ratio:
|
| 321 |
+
new_height = round(image.height / image.width * size[0])
|
| 322 |
+
if new_height != size[1]:
|
| 323 |
+
size = (size[0], new_height)
|
| 324 |
+
else:
|
| 325 |
+
new_width = round(image.width / image.height * size[1])
|
| 326 |
+
if new_width != size[0]:
|
| 327 |
+
size = (new_width, size[1])
|
| 328 |
+
return image.resize(size, resample=method)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def pad(
|
| 332 |
+
image: Image.Image,
|
| 333 |
+
size: tuple[int, int],
|
| 334 |
+
method: int = Image.Resampling.BICUBIC,
|
| 335 |
+
color: str | int | tuple[int, ...] | None = None,
|
| 336 |
+
centering: tuple[float, float] = (0.5, 0.5),
|
| 337 |
+
) -> Image.Image:
|
| 338 |
+
"""
|
| 339 |
+
Returns a resized and padded version of the image, expanded to fill the
|
| 340 |
+
requested aspect ratio and size.
|
| 341 |
+
|
| 342 |
+
:param image: The image to resize and crop.
|
| 343 |
+
:param size: The requested output size in pixels, given as a
|
| 344 |
+
(width, height) tuple.
|
| 345 |
+
:param method: Resampling method to use. Default is
|
| 346 |
+
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
| 347 |
+
See :ref:`concept-filters`.
|
| 348 |
+
:param color: The background color of the padded image.
|
| 349 |
+
:param centering: Control the position of the original image within the
|
| 350 |
+
padded version.
|
| 351 |
+
|
| 352 |
+
(0.5, 0.5) will keep the image centered
|
| 353 |
+
(0, 0) will keep the image aligned to the top left
|
| 354 |
+
(1, 1) will keep the image aligned to the bottom
|
| 355 |
+
right
|
| 356 |
+
:return: An image.
|
| 357 |
+
"""
|
| 358 |
+
|
| 359 |
+
resized = contain(image, size, method)
|
| 360 |
+
if resized.size == size:
|
| 361 |
+
out = resized
|
| 362 |
+
else:
|
| 363 |
+
out = Image.new(image.mode, size, color)
|
| 364 |
+
if resized.palette:
|
| 365 |
+
palette = resized.getpalette()
|
| 366 |
+
if palette is not None:
|
| 367 |
+
out.putpalette(palette)
|
| 368 |
+
if resized.width != size[0]:
|
| 369 |
+
x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
|
| 370 |
+
out.paste(resized, (x, 0))
|
| 371 |
+
else:
|
| 372 |
+
y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
|
| 373 |
+
out.paste(resized, (0, y))
|
| 374 |
+
return out
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def crop(image: Image.Image, border: int = 0) -> Image.Image:
|
| 378 |
+
"""
|
| 379 |
+
Remove border from image. The same amount of pixels are removed
|
| 380 |
+
from all four sides. This function works on all image modes.
|
| 381 |
+
|
| 382 |
+
.. seealso:: :py:meth:`~PIL.Image.Image.crop`
|
| 383 |
+
|
| 384 |
+
:param image: The image to crop.
|
| 385 |
+
:param border: The number of pixels to remove.
|
| 386 |
+
:return: An image.
|
| 387 |
+
"""
|
| 388 |
+
left, top, right, bottom = _border(border)
|
| 389 |
+
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def scale(
|
| 393 |
+
image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
|
| 394 |
+
) -> Image.Image:
|
| 395 |
+
"""
|
| 396 |
+
Returns a rescaled image by a specific factor given in parameter.
|
| 397 |
+
A factor greater than 1 expands the image, between 0 and 1 contracts the
|
| 398 |
+
image.
|
| 399 |
+
|
| 400 |
+
:param image: The image to rescale.
|
| 401 |
+
:param factor: The expansion factor, as a float.
|
| 402 |
+
:param resample: Resampling method to use. Default is
|
| 403 |
+
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
| 404 |
+
See :ref:`concept-filters`.
|
| 405 |
+
:returns: An :py:class:`~PIL.Image.Image` object.
|
| 406 |
+
"""
|
| 407 |
+
if factor == 1:
|
| 408 |
+
return image.copy()
|
| 409 |
+
elif factor <= 0:
|
| 410 |
+
msg = "the factor must be greater than 0"
|
| 411 |
+
raise ValueError(msg)
|
| 412 |
+
else:
|
| 413 |
+
size = (round(factor * image.width), round(factor * image.height))
|
| 414 |
+
return image.resize(size, resample)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
class SupportsGetMesh(Protocol):
|
| 418 |
+
"""
|
| 419 |
+
An object that supports the ``getmesh`` method, taking an image as an
|
| 420 |
+
argument, and returning a list of tuples. Each tuple contains two tuples,
|
| 421 |
+
the source box as a tuple of 4 integers, and a tuple of 8 integers for the
|
| 422 |
+
final quadrilateral, in order of top left, bottom left, bottom right, top
|
| 423 |
+
right.
|
| 424 |
+
"""
|
| 425 |
+
|
| 426 |
+
def getmesh(
|
| 427 |
+
self, image: Image.Image
|
| 428 |
+
) -> list[
|
| 429 |
+
tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
|
| 430 |
+
]: ...
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def deform(
|
| 434 |
+
image: Image.Image,
|
| 435 |
+
deformer: SupportsGetMesh,
|
| 436 |
+
resample: int = Image.Resampling.BILINEAR,
|
| 437 |
+
) -> Image.Image:
|
| 438 |
+
"""
|
| 439 |
+
Deform the image.
|
| 440 |
+
|
| 441 |
+
:param image: The image to deform.
|
| 442 |
+
:param deformer: A deformer object. Any object that implements a
|
| 443 |
+
``getmesh`` method can be used.
|
| 444 |
+
:param resample: An optional resampling filter. Same values possible as
|
| 445 |
+
in the PIL.Image.transform function.
|
| 446 |
+
:return: An image.
|
| 447 |
+
"""
|
| 448 |
+
return image.transform(
|
| 449 |
+
image.size, Image.Transform.MESH, deformer.getmesh(image), resample
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
|
| 454 |
+
"""
|
| 455 |
+
Equalize the image histogram. This function applies a non-linear
|
| 456 |
+
mapping to the input image, in order to create a uniform
|
| 457 |
+
distribution of grayscale values in the output image.
|
| 458 |
+
|
| 459 |
+
:param image: The image to equalize.
|
| 460 |
+
:param mask: An optional mask. If given, only the pixels selected by
|
| 461 |
+
the mask are included in the analysis.
|
| 462 |
+
:return: An image.
|
| 463 |
+
"""
|
| 464 |
+
if image.mode == "P":
|
| 465 |
+
image = image.convert("RGB")
|
| 466 |
+
h = image.histogram(mask)
|
| 467 |
+
lut = []
|
| 468 |
+
for b in range(0, len(h), 256):
|
| 469 |
+
histo = [_f for _f in h[b : b + 256] if _f]
|
| 470 |
+
if len(histo) <= 1:
|
| 471 |
+
lut.extend(list(range(256)))
|
| 472 |
+
else:
|
| 473 |
+
step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
|
| 474 |
+
if not step:
|
| 475 |
+
lut.extend(list(range(256)))
|
| 476 |
+
else:
|
| 477 |
+
n = step // 2
|
| 478 |
+
for i in range(256):
|
| 479 |
+
lut.append(n // step)
|
| 480 |
+
n = n + h[i + b]
|
| 481 |
+
return _lut(image, lut)
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def expand(
|
| 485 |
+
image: Image.Image,
|
| 486 |
+
border: int | tuple[int, ...] = 0,
|
| 487 |
+
fill: str | int | tuple[int, ...] = 0,
|
| 488 |
+
) -> Image.Image:
|
| 489 |
+
"""
|
| 490 |
+
Add border to the image
|
| 491 |
+
|
| 492 |
+
:param image: The image to expand.
|
| 493 |
+
:param border: Border width, in pixels.
|
| 494 |
+
:param fill: Pixel fill value (a color value). Default is 0 (black).
|
| 495 |
+
:return: An image.
|
| 496 |
+
"""
|
| 497 |
+
left, top, right, bottom = _border(border)
|
| 498 |
+
width = left + image.size[0] + right
|
| 499 |
+
height = top + image.size[1] + bottom
|
| 500 |
+
color = _color(fill, image.mode)
|
| 501 |
+
if image.palette:
|
| 502 |
+
mode = image.palette.mode
|
| 503 |
+
palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
|
| 504 |
+
if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
|
| 505 |
+
color = palette.getcolor(color)
|
| 506 |
+
else:
|
| 507 |
+
palette = None
|
| 508 |
+
out = Image.new(image.mode, (width, height), color)
|
| 509 |
+
if palette:
|
| 510 |
+
out.putpalette(palette.palette, mode)
|
| 511 |
+
out.paste(image, (left, top))
|
| 512 |
+
return out
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def fit(
|
| 516 |
+
image: Image.Image,
|
| 517 |
+
size: tuple[int, int],
|
| 518 |
+
method: int = Image.Resampling.BICUBIC,
|
| 519 |
+
bleed: float = 0.0,
|
| 520 |
+
centering: tuple[float, float] = (0.5, 0.5),
|
| 521 |
+
) -> Image.Image:
|
| 522 |
+
"""
|
| 523 |
+
Returns a resized and cropped version of the image, cropped to the
|
| 524 |
+
requested aspect ratio and size.
|
| 525 |
+
|
| 526 |
+
This function was contributed by Kevin Cazabon.
|
| 527 |
+
|
| 528 |
+
:param image: The image to resize and crop.
|
| 529 |
+
:param size: The requested output size in pixels, given as a
|
| 530 |
+
(width, height) tuple.
|
| 531 |
+
:param method: Resampling method to use. Default is
|
| 532 |
+
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
| 533 |
+
See :ref:`concept-filters`.
|
| 534 |
+
:param bleed: Remove a border around the outside of the image from all
|
| 535 |
+
four edges. The value is a decimal percentage (use 0.01 for
|
| 536 |
+
one percent). The default value is 0 (no border).
|
| 537 |
+
Cannot be greater than or equal to 0.5.
|
| 538 |
+
:param centering: Control the cropping position. Use (0.5, 0.5) for
|
| 539 |
+
center cropping (e.g. if cropping the width, take 50% off
|
| 540 |
+
of the left side, and therefore 50% off the right side).
|
| 541 |
+
(0.0, 0.0) will crop from the top left corner (i.e. if
|
| 542 |
+
cropping the width, take all of the crop off of the right
|
| 543 |
+
side, and if cropping the height, take all of it off the
|
| 544 |
+
bottom). (1.0, 0.0) will crop from the bottom left
|
| 545 |
+
corner, etc. (i.e. if cropping the width, take all of the
|
| 546 |
+
crop off the left side, and if cropping the height take
|
| 547 |
+
none from the top, and therefore all off the bottom).
|
| 548 |
+
:return: An image.
|
| 549 |
+
"""
|
| 550 |
+
|
| 551 |
+
# by Kevin Cazabon, Feb 17/2000
|
| 552 |
+
# kevin@cazabon.com
|
| 553 |
+
# https://www.cazabon.com
|
| 554 |
+
|
| 555 |
+
centering_x, centering_y = centering
|
| 556 |
+
|
| 557 |
+
if not 0.0 <= centering_x <= 1.0:
|
| 558 |
+
centering_x = 0.5
|
| 559 |
+
if not 0.0 <= centering_y <= 1.0:
|
| 560 |
+
centering_y = 0.5
|
| 561 |
+
|
| 562 |
+
if not 0.0 <= bleed < 0.5:
|
| 563 |
+
bleed = 0.0
|
| 564 |
+
|
| 565 |
+
# calculate the area to use for resizing and cropping, subtracting
|
| 566 |
+
# the 'bleed' around the edges
|
| 567 |
+
|
| 568 |
+
# number of pixels to trim off on Top and Bottom, Left and Right
|
| 569 |
+
bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
|
| 570 |
+
|
| 571 |
+
live_size = (
|
| 572 |
+
image.size[0] - bleed_pixels[0] * 2,
|
| 573 |
+
image.size[1] - bleed_pixels[1] * 2,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
# calculate the aspect ratio of the live_size
|
| 577 |
+
live_size_ratio = live_size[0] / live_size[1]
|
| 578 |
+
|
| 579 |
+
# calculate the aspect ratio of the output image
|
| 580 |
+
output_ratio = size[0] / size[1]
|
| 581 |
+
|
| 582 |
+
# figure out if the sides or top/bottom will be cropped off
|
| 583 |
+
if live_size_ratio == output_ratio:
|
| 584 |
+
# live_size is already the needed ratio
|
| 585 |
+
crop_width = live_size[0]
|
| 586 |
+
crop_height = live_size[1]
|
| 587 |
+
elif live_size_ratio >= output_ratio:
|
| 588 |
+
# live_size is wider than what's needed, crop the sides
|
| 589 |
+
crop_width = output_ratio * live_size[1]
|
| 590 |
+
crop_height = live_size[1]
|
| 591 |
+
else:
|
| 592 |
+
# live_size is taller than what's needed, crop the top and bottom
|
| 593 |
+
crop_width = live_size[0]
|
| 594 |
+
crop_height = live_size[0] / output_ratio
|
| 595 |
+
|
| 596 |
+
# make the crop
|
| 597 |
+
crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
|
| 598 |
+
crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
|
| 599 |
+
|
| 600 |
+
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
|
| 601 |
+
|
| 602 |
+
# resize the image and return it
|
| 603 |
+
return image.resize(size, method, box=crop)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
def flip(image: Image.Image) -> Image.Image:
|
| 607 |
+
"""
|
| 608 |
+
Flip the image vertically (top to bottom).
|
| 609 |
+
|
| 610 |
+
:param image: The image to flip.
|
| 611 |
+
:return: An image.
|
| 612 |
+
"""
|
| 613 |
+
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def grayscale(image: Image.Image) -> Image.Image:
|
| 617 |
+
"""
|
| 618 |
+
Convert the image to grayscale.
|
| 619 |
+
|
| 620 |
+
:param image: The image to convert.
|
| 621 |
+
:return: An image.
|
| 622 |
+
"""
|
| 623 |
+
return image.convert("L")
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def invert(image: Image.Image) -> Image.Image:
|
| 627 |
+
"""
|
| 628 |
+
Invert (negate) the image.
|
| 629 |
+
|
| 630 |
+
:param image: The image to invert.
|
| 631 |
+
:return: An image.
|
| 632 |
+
"""
|
| 633 |
+
lut = list(range(255, -1, -1))
|
| 634 |
+
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def mirror(image: Image.Image) -> Image.Image:
|
| 638 |
+
"""
|
| 639 |
+
Flip image horizontally (left to right).
|
| 640 |
+
|
| 641 |
+
:param image: The image to mirror.
|
| 642 |
+
:return: An image.
|
| 643 |
+
"""
|
| 644 |
+
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def posterize(image: Image.Image, bits: int) -> Image.Image:
|
| 648 |
+
"""
|
| 649 |
+
Reduce the number of bits for each color channel.
|
| 650 |
+
|
| 651 |
+
:param image: The image to posterize.
|
| 652 |
+
:param bits: The number of bits to keep for each channel (1-8).
|
| 653 |
+
:return: An image.
|
| 654 |
+
"""
|
| 655 |
+
mask = ~(2 ** (8 - bits) - 1)
|
| 656 |
+
lut = [i & mask for i in range(256)]
|
| 657 |
+
return _lut(image, lut)
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
|
| 661 |
+
"""
|
| 662 |
+
Invert all pixel values above a threshold.
|
| 663 |
+
|
| 664 |
+
:param image: The image to solarize.
|
| 665 |
+
:param threshold: All pixels above this grayscale level are inverted.
|
| 666 |
+
:return: An image.
|
| 667 |
+
"""
|
| 668 |
+
lut = []
|
| 669 |
+
for i in range(256):
|
| 670 |
+
if i < threshold:
|
| 671 |
+
lut.append(i)
|
| 672 |
+
else:
|
| 673 |
+
lut.append(255 - i)
|
| 674 |
+
return _lut(image, lut)
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
@overload
|
| 678 |
+
def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
@overload
|
| 682 |
+
def exif_transpose(
|
| 683 |
+
image: Image.Image, *, in_place: Literal[False] = False
|
| 684 |
+
) -> Image.Image: ...
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
|
| 688 |
+
"""
|
| 689 |
+
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
| 690 |
+
accordingly, and remove the orientation data.
|
| 691 |
+
|
| 692 |
+
:param image: The image to transpose.
|
| 693 |
+
:param in_place: Boolean. Keyword-only argument.
|
| 694 |
+
If ``True``, the original image is modified in-place, and ``None`` is returned.
|
| 695 |
+
If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
|
| 696 |
+
with the transposition applied. If there is no transposition, a copy of the
|
| 697 |
+
image will be returned.
|
| 698 |
+
"""
|
| 699 |
+
image.load()
|
| 700 |
+
image_exif = image.getexif()
|
| 701 |
+
orientation = image_exif.get(ExifTags.Base.Orientation, 1)
|
| 702 |
+
method = {
|
| 703 |
+
2: Image.Transpose.FLIP_LEFT_RIGHT,
|
| 704 |
+
3: Image.Transpose.ROTATE_180,
|
| 705 |
+
4: Image.Transpose.FLIP_TOP_BOTTOM,
|
| 706 |
+
5: Image.Transpose.TRANSPOSE,
|
| 707 |
+
6: Image.Transpose.ROTATE_270,
|
| 708 |
+
7: Image.Transpose.TRANSVERSE,
|
| 709 |
+
8: Image.Transpose.ROTATE_90,
|
| 710 |
+
}.get(orientation)
|
| 711 |
+
if method is not None:
|
| 712 |
+
if in_place:
|
| 713 |
+
image.im = image.im.transpose(method)
|
| 714 |
+
image._size = image.im.size
|
| 715 |
+
else:
|
| 716 |
+
transposed_image = image.transpose(method)
|
| 717 |
+
exif_image = image if in_place else transposed_image
|
| 718 |
+
|
| 719 |
+
exif = exif_image.getexif()
|
| 720 |
+
if ExifTags.Base.Orientation in exif:
|
| 721 |
+
del exif[ExifTags.Base.Orientation]
|
| 722 |
+
if "exif" in exif_image.info:
|
| 723 |
+
exif_image.info["exif"] = exif.tobytes()
|
| 724 |
+
elif "Raw profile type exif" in exif_image.info:
|
| 725 |
+
exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
|
| 726 |
+
for key in ("XML:com.adobe.xmp", "xmp"):
|
| 727 |
+
if key in exif_image.info:
|
| 728 |
+
for pattern in (
|
| 729 |
+
r'tiff:Orientation="([0-9])"',
|
| 730 |
+
r"<tiff:Orientation>([0-9])</tiff:Orientation>",
|
| 731 |
+
):
|
| 732 |
+
value = exif_image.info[key]
|
| 733 |
+
if isinstance(value, str):
|
| 734 |
+
value = re.sub(pattern, "", value)
|
| 735 |
+
elif isinstance(value, tuple):
|
| 736 |
+
value = tuple(
|
| 737 |
+
re.sub(pattern.encode(), b"", v) for v in value
|
| 738 |
+
)
|
| 739 |
+
else:
|
| 740 |
+
value = re.sub(pattern.encode(), b"", value)
|
| 741 |
+
exif_image.info[key] = value
|
| 742 |
+
if not in_place:
|
| 743 |
+
return transposed_image
|
| 744 |
+
elif not in_place:
|
| 745 |
+
return image.copy()
|
| 746 |
+
return None
|
PIL/ImagePalette.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# image palette object
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-03-11 fl Rewritten.
|
| 9 |
+
# 1997-01-03 fl Up and running.
|
| 10 |
+
# 1997-08-23 fl Added load hack
|
| 11 |
+
# 2001-04-16 fl Fixed randint shadow bug in random()
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) 1997-2001 by Secret Labs AB
|
| 14 |
+
# Copyright (c) 1996-1997 by Fredrik Lundh
|
| 15 |
+
#
|
| 16 |
+
# See the README file for information on usage and redistribution.
|
| 17 |
+
#
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import array
|
| 21 |
+
from collections.abc import Sequence
|
| 22 |
+
from typing import IO
|
| 23 |
+
|
| 24 |
+
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
| 25 |
+
|
| 26 |
+
TYPE_CHECKING = False
|
| 27 |
+
if TYPE_CHECKING:
|
| 28 |
+
from . import Image
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ImagePalette:
|
| 32 |
+
"""
|
| 33 |
+
Color palette for palette mapped images
|
| 34 |
+
|
| 35 |
+
:param mode: The mode to use for the palette. See:
|
| 36 |
+
:ref:`concept-modes`. Defaults to "RGB"
|
| 37 |
+
:param palette: An optional palette. If given, it must be a bytearray,
|
| 38 |
+
an array or a list of ints between 0-255. The list must consist of
|
| 39 |
+
all channels for one color followed by the next color (e.g. RGBRGBRGB).
|
| 40 |
+
Defaults to an empty palette.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
mode: str = "RGB",
|
| 46 |
+
palette: Sequence[int] | bytes | bytearray | None = None,
|
| 47 |
+
) -> None:
|
| 48 |
+
self.mode = mode
|
| 49 |
+
self.rawmode: str | None = None # if set, palette contains raw data
|
| 50 |
+
self.palette = palette or bytearray()
|
| 51 |
+
self.dirty: int | None = None
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def palette(self) -> Sequence[int] | bytes | bytearray:
|
| 55 |
+
return self._palette
|
| 56 |
+
|
| 57 |
+
@palette.setter
|
| 58 |
+
def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
|
| 59 |
+
self._colors: dict[tuple[int, ...], int] | None = None
|
| 60 |
+
self._palette = palette
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def colors(self) -> dict[tuple[int, ...], int]:
|
| 64 |
+
if self._colors is None:
|
| 65 |
+
mode_len = len(self.mode)
|
| 66 |
+
self._colors = {}
|
| 67 |
+
for i in range(0, len(self.palette), mode_len):
|
| 68 |
+
color = tuple(self.palette[i : i + mode_len])
|
| 69 |
+
if color in self._colors:
|
| 70 |
+
continue
|
| 71 |
+
self._colors[color] = i // mode_len
|
| 72 |
+
return self._colors
|
| 73 |
+
|
| 74 |
+
@colors.setter
|
| 75 |
+
def colors(self, colors: dict[tuple[int, ...], int]) -> None:
|
| 76 |
+
self._colors = colors
|
| 77 |
+
|
| 78 |
+
def copy(self) -> ImagePalette:
|
| 79 |
+
new = ImagePalette()
|
| 80 |
+
|
| 81 |
+
new.mode = self.mode
|
| 82 |
+
new.rawmode = self.rawmode
|
| 83 |
+
if self.palette is not None:
|
| 84 |
+
new.palette = self.palette[:]
|
| 85 |
+
new.dirty = self.dirty
|
| 86 |
+
|
| 87 |
+
return new
|
| 88 |
+
|
| 89 |
+
def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
|
| 90 |
+
"""
|
| 91 |
+
Get palette contents in format suitable for the low-level
|
| 92 |
+
``im.putpalette`` primitive.
|
| 93 |
+
|
| 94 |
+
.. warning:: This method is experimental.
|
| 95 |
+
"""
|
| 96 |
+
if self.rawmode:
|
| 97 |
+
return self.rawmode, self.palette
|
| 98 |
+
return self.mode, self.tobytes()
|
| 99 |
+
|
| 100 |
+
def tobytes(self) -> bytes:
|
| 101 |
+
"""Convert palette to bytes.
|
| 102 |
+
|
| 103 |
+
.. warning:: This method is experimental.
|
| 104 |
+
"""
|
| 105 |
+
if self.rawmode:
|
| 106 |
+
msg = "palette contains raw palette data"
|
| 107 |
+
raise ValueError(msg)
|
| 108 |
+
if isinstance(self.palette, bytes):
|
| 109 |
+
return self.palette
|
| 110 |
+
arr = array.array("B", self.palette)
|
| 111 |
+
return arr.tobytes()
|
| 112 |
+
|
| 113 |
+
# Declare tostring as an alias for tobytes
|
| 114 |
+
tostring = tobytes
|
| 115 |
+
|
| 116 |
+
def _new_color_index(
|
| 117 |
+
self, image: Image.Image | None = None, e: Exception | None = None
|
| 118 |
+
) -> int:
|
| 119 |
+
if not isinstance(self.palette, bytearray):
|
| 120 |
+
self._palette = bytearray(self.palette)
|
| 121 |
+
index = len(self.palette) // len(self.mode)
|
| 122 |
+
special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
|
| 123 |
+
if image:
|
| 124 |
+
special_colors = (
|
| 125 |
+
image.info.get("background"),
|
| 126 |
+
image.info.get("transparency"),
|
| 127 |
+
)
|
| 128 |
+
while index in special_colors:
|
| 129 |
+
index += 1
|
| 130 |
+
if index >= 256:
|
| 131 |
+
if image:
|
| 132 |
+
# Search for an unused index
|
| 133 |
+
for i, count in reversed(list(enumerate(image.histogram()))):
|
| 134 |
+
if count == 0 and i not in special_colors:
|
| 135 |
+
index = i
|
| 136 |
+
break
|
| 137 |
+
if index >= 256:
|
| 138 |
+
msg = "cannot allocate more than 256 colors"
|
| 139 |
+
raise ValueError(msg) from e
|
| 140 |
+
return index
|
| 141 |
+
|
| 142 |
+
def getcolor(
|
| 143 |
+
self,
|
| 144 |
+
color: tuple[int, ...],
|
| 145 |
+
image: Image.Image | None = None,
|
| 146 |
+
) -> int:
|
| 147 |
+
"""Given an rgb tuple, allocate palette entry.
|
| 148 |
+
|
| 149 |
+
.. warning:: This method is experimental.
|
| 150 |
+
"""
|
| 151 |
+
if self.rawmode:
|
| 152 |
+
msg = "palette contains raw palette data"
|
| 153 |
+
raise ValueError(msg)
|
| 154 |
+
if isinstance(color, tuple):
|
| 155 |
+
if self.mode == "RGB":
|
| 156 |
+
if len(color) == 4:
|
| 157 |
+
if color[3] != 255:
|
| 158 |
+
msg = "cannot add non-opaque RGBA color to RGB palette"
|
| 159 |
+
raise ValueError(msg)
|
| 160 |
+
color = color[:3]
|
| 161 |
+
elif self.mode == "RGBA":
|
| 162 |
+
if len(color) == 3:
|
| 163 |
+
color += (255,)
|
| 164 |
+
try:
|
| 165 |
+
return self.colors[color]
|
| 166 |
+
except KeyError as e:
|
| 167 |
+
# allocate new color slot
|
| 168 |
+
index = self._new_color_index(image, e)
|
| 169 |
+
assert isinstance(self._palette, bytearray)
|
| 170 |
+
self.colors[color] = index
|
| 171 |
+
mode_len = len(self.mode)
|
| 172 |
+
if index * mode_len < len(self.palette):
|
| 173 |
+
self._palette = (
|
| 174 |
+
self._palette[: index * mode_len]
|
| 175 |
+
+ bytes(color)
|
| 176 |
+
+ self._palette[index * mode_len + mode_len :]
|
| 177 |
+
)
|
| 178 |
+
else:
|
| 179 |
+
self._palette += bytes(color)
|
| 180 |
+
self.dirty = 1
|
| 181 |
+
return index
|
| 182 |
+
else:
|
| 183 |
+
msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
|
| 184 |
+
raise ValueError(msg)
|
| 185 |
+
|
| 186 |
+
def save(self, fp: str | IO[str]) -> None:
|
| 187 |
+
"""Save palette to text file.
|
| 188 |
+
|
| 189 |
+
.. warning:: This method is experimental.
|
| 190 |
+
"""
|
| 191 |
+
if self.rawmode:
|
| 192 |
+
msg = "palette contains raw palette data"
|
| 193 |
+
raise ValueError(msg)
|
| 194 |
+
if isinstance(fp, str):
|
| 195 |
+
fp = open(fp, "w")
|
| 196 |
+
fp.write("# Palette\n")
|
| 197 |
+
fp.write(f"# Mode: {self.mode}\n")
|
| 198 |
+
for i in range(256):
|
| 199 |
+
fp.write(f"{i}")
|
| 200 |
+
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
|
| 201 |
+
try:
|
| 202 |
+
fp.write(f" {self.palette[j]}")
|
| 203 |
+
except IndexError:
|
| 204 |
+
fp.write(" 0")
|
| 205 |
+
fp.write("\n")
|
| 206 |
+
fp.close()
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# --------------------------------------------------------------------
|
| 210 |
+
# Internal
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
|
| 214 |
+
palette = ImagePalette()
|
| 215 |
+
palette.rawmode = rawmode
|
| 216 |
+
palette.palette = data
|
| 217 |
+
palette.dirty = 1
|
| 218 |
+
return palette
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
# --------------------------------------------------------------------
|
| 222 |
+
# Factories
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def make_linear_lut(black: int, white: float) -> list[int]:
|
| 226 |
+
if black == 0:
|
| 227 |
+
return [int(white * i // 255) for i in range(256)]
|
| 228 |
+
|
| 229 |
+
msg = "unavailable when black is non-zero"
|
| 230 |
+
raise NotImplementedError(msg) # FIXME
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def make_gamma_lut(exp: float) -> list[int]:
|
| 234 |
+
return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def negative(mode: str = "RGB") -> ImagePalette:
|
| 238 |
+
palette = list(range(256 * len(mode)))
|
| 239 |
+
palette.reverse()
|
| 240 |
+
return ImagePalette(mode, [i // len(mode) for i in palette])
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def random(mode: str = "RGB") -> ImagePalette:
|
| 244 |
+
from random import randint
|
| 245 |
+
|
| 246 |
+
palette = [randint(0, 255) for _ in range(256 * len(mode))]
|
| 247 |
+
return ImagePalette(mode, palette)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def sepia(white: str = "#fff0c0") -> ImagePalette:
|
| 251 |
+
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
|
| 252 |
+
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def wedge(mode: str = "RGB") -> ImagePalette:
|
| 256 |
+
palette = list(range(256 * len(mode)))
|
| 257 |
+
return ImagePalette(mode, [i // len(mode) for i in palette])
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def load(filename: str) -> tuple[bytes, str]:
|
| 261 |
+
# FIXME: supports GIMP gradients only
|
| 262 |
+
|
| 263 |
+
with open(filename, "rb") as fp:
|
| 264 |
+
paletteHandlers: list[
|
| 265 |
+
type[
|
| 266 |
+
GimpPaletteFile.GimpPaletteFile
|
| 267 |
+
| GimpGradientFile.GimpGradientFile
|
| 268 |
+
| PaletteFile.PaletteFile
|
| 269 |
+
]
|
| 270 |
+
] = [
|
| 271 |
+
GimpPaletteFile.GimpPaletteFile,
|
| 272 |
+
GimpGradientFile.GimpGradientFile,
|
| 273 |
+
PaletteFile.PaletteFile,
|
| 274 |
+
]
|
| 275 |
+
for paletteHandler in paletteHandlers:
|
| 276 |
+
try:
|
| 277 |
+
fp.seek(0)
|
| 278 |
+
lut = paletteHandler(fp).getpalette()
|
| 279 |
+
if lut:
|
| 280 |
+
break
|
| 281 |
+
except (SyntaxError, ValueError):
|
| 282 |
+
pass
|
| 283 |
+
else:
|
| 284 |
+
msg = "cannot load palette"
|
| 285 |
+
raise OSError(msg)
|
| 286 |
+
|
| 287 |
+
return lut # data, rawmode
|
PIL/ImagePath.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# path interface
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-11-04 fl Created
|
| 9 |
+
# 2002-04-14 fl Added documentation stub class
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 12 |
+
# Copyright (c) Fredrik Lundh 1996.
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from . import Image
|
| 19 |
+
|
| 20 |
+
Path = Image.core.path
|
PIL/ImageSequence.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# sequence support classes
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1997-02-20 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) 1997 by Secret Labs AB.
|
| 11 |
+
# Copyright (c) 1997 by Fredrik Lundh.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
|
| 16 |
+
##
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from . import Image
|
| 20 |
+
|
| 21 |
+
TYPE_CHECKING = False
|
| 22 |
+
if TYPE_CHECKING:
|
| 23 |
+
from collections.abc import Callable
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Iterator:
|
| 27 |
+
"""
|
| 28 |
+
This class implements an iterator object that can be used to loop
|
| 29 |
+
over an image sequence.
|
| 30 |
+
|
| 31 |
+
You can use the ``[]`` operator to access elements by index. This operator
|
| 32 |
+
will raise an :py:exc:`IndexError` if you try to access a nonexistent
|
| 33 |
+
frame.
|
| 34 |
+
|
| 35 |
+
:param im: An image object.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, im: Image.Image) -> None:
|
| 39 |
+
if not hasattr(im, "seek"):
|
| 40 |
+
msg = "im must have seek method"
|
| 41 |
+
raise AttributeError(msg)
|
| 42 |
+
self.im = im
|
| 43 |
+
self.position = getattr(self.im, "_min_frame", 0)
|
| 44 |
+
|
| 45 |
+
def __getitem__(self, ix: int) -> Image.Image:
|
| 46 |
+
try:
|
| 47 |
+
self.im.seek(ix)
|
| 48 |
+
return self.im
|
| 49 |
+
except EOFError as e:
|
| 50 |
+
msg = "end of sequence"
|
| 51 |
+
raise IndexError(msg) from e
|
| 52 |
+
|
| 53 |
+
def __iter__(self) -> Iterator:
|
| 54 |
+
return self
|
| 55 |
+
|
| 56 |
+
def __next__(self) -> Image.Image:
|
| 57 |
+
try:
|
| 58 |
+
self.im.seek(self.position)
|
| 59 |
+
self.position += 1
|
| 60 |
+
return self.im
|
| 61 |
+
except EOFError as e:
|
| 62 |
+
msg = "end of sequence"
|
| 63 |
+
raise StopIteration(msg) from e
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def all_frames(
|
| 67 |
+
im: Image.Image | list[Image.Image],
|
| 68 |
+
func: Callable[[Image.Image], Image.Image] | None = None,
|
| 69 |
+
) -> list[Image.Image]:
|
| 70 |
+
"""
|
| 71 |
+
Applies a given function to all frames in an image or a list of images.
|
| 72 |
+
The frames are returned as a list of separate images.
|
| 73 |
+
|
| 74 |
+
:param im: An image, or a list of images.
|
| 75 |
+
:param func: The function to apply to all of the image frames.
|
| 76 |
+
:returns: A list of images.
|
| 77 |
+
"""
|
| 78 |
+
if not isinstance(im, list):
|
| 79 |
+
im = [im]
|
| 80 |
+
|
| 81 |
+
ims = []
|
| 82 |
+
for imSequence in im:
|
| 83 |
+
current = imSequence.tell()
|
| 84 |
+
|
| 85 |
+
ims += [im_frame.copy() for im_frame in Iterator(imSequence)]
|
| 86 |
+
|
| 87 |
+
imSequence.seek(current)
|
| 88 |
+
return [func(im) for im in ims] if func else ims
|
PIL/ImageShow.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# im.show() drivers
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2008-04-06 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) Secret Labs AB 2008.
|
| 11 |
+
#
|
| 12 |
+
# See the README file for information on usage and redistribution.
|
| 13 |
+
#
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import abc
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import subprocess
|
| 20 |
+
import sys
|
| 21 |
+
from shlex import quote
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
from . import Image
|
| 25 |
+
|
| 26 |
+
_viewers = []
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None:
|
| 30 |
+
"""
|
| 31 |
+
The :py:func:`register` function is used to register additional viewers::
|
| 32 |
+
|
| 33 |
+
from PIL import ImageShow
|
| 34 |
+
ImageShow.register(MyViewer()) # MyViewer will be used as a last resort
|
| 35 |
+
ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised
|
| 36 |
+
ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised
|
| 37 |
+
|
| 38 |
+
:param viewer: The viewer to be registered.
|
| 39 |
+
:param order:
|
| 40 |
+
Zero or a negative integer to prepend this viewer to the list,
|
| 41 |
+
a positive integer to append it.
|
| 42 |
+
"""
|
| 43 |
+
if isinstance(viewer, type) and issubclass(viewer, Viewer):
|
| 44 |
+
viewer = viewer()
|
| 45 |
+
if order > 0:
|
| 46 |
+
_viewers.append(viewer)
|
| 47 |
+
else:
|
| 48 |
+
_viewers.insert(0, viewer)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
|
| 52 |
+
r"""
|
| 53 |
+
Display a given image.
|
| 54 |
+
|
| 55 |
+
:param image: An image object.
|
| 56 |
+
:param title: Optional title. Not all viewers can display the title.
|
| 57 |
+
:param \**options: Additional viewer options.
|
| 58 |
+
:returns: ``True`` if a suitable viewer was found, ``False`` otherwise.
|
| 59 |
+
"""
|
| 60 |
+
for viewer in _viewers:
|
| 61 |
+
if viewer.show(image, title=title, **options):
|
| 62 |
+
return True
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Viewer:
|
| 67 |
+
"""Base class for viewers."""
|
| 68 |
+
|
| 69 |
+
# main api
|
| 70 |
+
|
| 71 |
+
def show(self, image: Image.Image, **options: Any) -> int:
|
| 72 |
+
"""
|
| 73 |
+
The main function for displaying an image.
|
| 74 |
+
Converts the given image to the target format and displays it.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
if not (
|
| 78 |
+
image.mode in ("1", "RGBA")
|
| 79 |
+
or (self.format == "PNG" and image.mode in ("I;16", "LA"))
|
| 80 |
+
):
|
| 81 |
+
base = Image.getmodebase(image.mode)
|
| 82 |
+
if image.mode != base:
|
| 83 |
+
image = image.convert(base)
|
| 84 |
+
|
| 85 |
+
return self.show_image(image, **options)
|
| 86 |
+
|
| 87 |
+
# hook methods
|
| 88 |
+
|
| 89 |
+
format: str | None = None
|
| 90 |
+
"""The format to convert the image into."""
|
| 91 |
+
options: dict[str, Any] = {}
|
| 92 |
+
"""Additional options used to convert the image."""
|
| 93 |
+
|
| 94 |
+
def get_format(self, image: Image.Image) -> str | None:
|
| 95 |
+
"""Return format name, or ``None`` to save as PGM/PPM."""
|
| 96 |
+
return self.format
|
| 97 |
+
|
| 98 |
+
def get_command(self, file: str, **options: Any) -> str:
|
| 99 |
+
"""
|
| 100 |
+
Returns the command used to display the file.
|
| 101 |
+
Not implemented in the base class.
|
| 102 |
+
"""
|
| 103 |
+
msg = "unavailable in base viewer"
|
| 104 |
+
raise NotImplementedError(msg)
|
| 105 |
+
|
| 106 |
+
def save_image(self, image: Image.Image) -> str:
|
| 107 |
+
"""Save to temporary file and return filename."""
|
| 108 |
+
return image._dump(format=self.get_format(image), **self.options)
|
| 109 |
+
|
| 110 |
+
def show_image(self, image: Image.Image, **options: Any) -> int:
|
| 111 |
+
"""Display the given image."""
|
| 112 |
+
return self.show_file(self.save_image(image), **options)
|
| 113 |
+
|
| 114 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 115 |
+
"""
|
| 116 |
+
Display given file.
|
| 117 |
+
"""
|
| 118 |
+
if not os.path.exists(path):
|
| 119 |
+
raise FileNotFoundError
|
| 120 |
+
os.system(self.get_command(path, **options)) # nosec
|
| 121 |
+
return 1
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# --------------------------------------------------------------------
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class WindowsViewer(Viewer):
|
| 128 |
+
"""The default viewer on Windows is the default system application for PNG files."""
|
| 129 |
+
|
| 130 |
+
format = "PNG"
|
| 131 |
+
options = {"compress_level": 1, "save_all": True}
|
| 132 |
+
|
| 133 |
+
def get_command(self, file: str, **options: Any) -> str:
|
| 134 |
+
return (
|
| 135 |
+
f'start "Pillow" /WAIT "{file}" '
|
| 136 |
+
"&& ping -n 4 127.0.0.1 >NUL "
|
| 137 |
+
f'&& del /f "{file}"'
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 141 |
+
"""
|
| 142 |
+
Display given file.
|
| 143 |
+
"""
|
| 144 |
+
if not os.path.exists(path):
|
| 145 |
+
raise FileNotFoundError
|
| 146 |
+
subprocess.Popen(
|
| 147 |
+
self.get_command(path, **options),
|
| 148 |
+
shell=True,
|
| 149 |
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
|
| 150 |
+
) # nosec
|
| 151 |
+
return 1
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
if sys.platform == "win32":
|
| 155 |
+
register(WindowsViewer)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class MacViewer(Viewer):
|
| 159 |
+
"""The default viewer on macOS using ``Preview.app``."""
|
| 160 |
+
|
| 161 |
+
format = "PNG"
|
| 162 |
+
options = {"compress_level": 1, "save_all": True}
|
| 163 |
+
|
| 164 |
+
def get_command(self, file: str, **options: Any) -> str:
|
| 165 |
+
# on darwin open returns immediately resulting in the temp
|
| 166 |
+
# file removal while app is opening
|
| 167 |
+
command = "open -a Preview.app"
|
| 168 |
+
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
|
| 169 |
+
return command
|
| 170 |
+
|
| 171 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 172 |
+
"""
|
| 173 |
+
Display given file.
|
| 174 |
+
"""
|
| 175 |
+
if not os.path.exists(path):
|
| 176 |
+
raise FileNotFoundError
|
| 177 |
+
subprocess.call(["open", "-a", "Preview.app", path])
|
| 178 |
+
|
| 179 |
+
pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
|
| 180 |
+
executable = (not pyinstaller and sys.executable) or shutil.which("python3")
|
| 181 |
+
if executable:
|
| 182 |
+
subprocess.Popen(
|
| 183 |
+
[
|
| 184 |
+
executable,
|
| 185 |
+
"-c",
|
| 186 |
+
"import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
|
| 187 |
+
path,
|
| 188 |
+
]
|
| 189 |
+
)
|
| 190 |
+
return 1
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
if sys.platform == "darwin":
|
| 194 |
+
register(MacViewer)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class UnixViewer(abc.ABC, Viewer):
|
| 198 |
+
format = "PNG"
|
| 199 |
+
options = {"compress_level": 1, "save_all": True}
|
| 200 |
+
|
| 201 |
+
@abc.abstractmethod
|
| 202 |
+
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
|
| 203 |
+
pass
|
| 204 |
+
|
| 205 |
+
def get_command(self, file: str, **options: Any) -> str:
|
| 206 |
+
command = self.get_command_ex(file, **options)[0]
|
| 207 |
+
return f"{command} {quote(file)}"
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class XDGViewer(UnixViewer):
|
| 211 |
+
"""
|
| 212 |
+
The freedesktop.org ``xdg-open`` command.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
|
| 216 |
+
command = executable = "xdg-open"
|
| 217 |
+
return command, executable
|
| 218 |
+
|
| 219 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 220 |
+
"""
|
| 221 |
+
Display given file.
|
| 222 |
+
"""
|
| 223 |
+
if not os.path.exists(path):
|
| 224 |
+
raise FileNotFoundError
|
| 225 |
+
subprocess.Popen(["xdg-open", path])
|
| 226 |
+
return 1
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class DisplayViewer(UnixViewer):
|
| 230 |
+
"""
|
| 231 |
+
The ImageMagick ``display`` command.
|
| 232 |
+
This viewer supports the ``title`` parameter.
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
+
def get_command_ex(
|
| 236 |
+
self, file: str, title: str | None = None, **options: Any
|
| 237 |
+
) -> tuple[str, str]:
|
| 238 |
+
command = executable = "display"
|
| 239 |
+
if title:
|
| 240 |
+
command += f" -title {quote(title)}"
|
| 241 |
+
return command, executable
|
| 242 |
+
|
| 243 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 244 |
+
"""
|
| 245 |
+
Display given file.
|
| 246 |
+
"""
|
| 247 |
+
if not os.path.exists(path):
|
| 248 |
+
raise FileNotFoundError
|
| 249 |
+
args = ["display"]
|
| 250 |
+
title = options.get("title")
|
| 251 |
+
if title:
|
| 252 |
+
args += ["-title", title]
|
| 253 |
+
args.append(path)
|
| 254 |
+
|
| 255 |
+
subprocess.Popen(args)
|
| 256 |
+
return 1
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class GmDisplayViewer(UnixViewer):
|
| 260 |
+
"""The GraphicsMagick ``gm display`` command."""
|
| 261 |
+
|
| 262 |
+
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
|
| 263 |
+
executable = "gm"
|
| 264 |
+
command = "gm display"
|
| 265 |
+
return command, executable
|
| 266 |
+
|
| 267 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 268 |
+
"""
|
| 269 |
+
Display given file.
|
| 270 |
+
"""
|
| 271 |
+
if not os.path.exists(path):
|
| 272 |
+
raise FileNotFoundError
|
| 273 |
+
subprocess.Popen(["gm", "display", path])
|
| 274 |
+
return 1
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class EogViewer(UnixViewer):
|
| 278 |
+
"""The GNOME Image Viewer ``eog`` command."""
|
| 279 |
+
|
| 280 |
+
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
|
| 281 |
+
executable = "eog"
|
| 282 |
+
command = "eog -n"
|
| 283 |
+
return command, executable
|
| 284 |
+
|
| 285 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 286 |
+
"""
|
| 287 |
+
Display given file.
|
| 288 |
+
"""
|
| 289 |
+
if not os.path.exists(path):
|
| 290 |
+
raise FileNotFoundError
|
| 291 |
+
subprocess.Popen(["eog", "-n", path])
|
| 292 |
+
return 1
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
class XVViewer(UnixViewer):
|
| 296 |
+
"""
|
| 297 |
+
The X Viewer ``xv`` command.
|
| 298 |
+
This viewer supports the ``title`` parameter.
|
| 299 |
+
"""
|
| 300 |
+
|
| 301 |
+
def get_command_ex(
|
| 302 |
+
self, file: str, title: str | None = None, **options: Any
|
| 303 |
+
) -> tuple[str, str]:
|
| 304 |
+
# note: xv is pretty outdated. most modern systems have
|
| 305 |
+
# imagemagick's display command instead.
|
| 306 |
+
command = executable = "xv"
|
| 307 |
+
if title:
|
| 308 |
+
command += f" -name {quote(title)}"
|
| 309 |
+
return command, executable
|
| 310 |
+
|
| 311 |
+
def show_file(self, path: str, **options: Any) -> int:
|
| 312 |
+
"""
|
| 313 |
+
Display given file.
|
| 314 |
+
"""
|
| 315 |
+
if not os.path.exists(path):
|
| 316 |
+
raise FileNotFoundError
|
| 317 |
+
args = ["xv"]
|
| 318 |
+
title = options.get("title")
|
| 319 |
+
if title:
|
| 320 |
+
args += ["-name", title]
|
| 321 |
+
args.append(path)
|
| 322 |
+
|
| 323 |
+
subprocess.Popen(args)
|
| 324 |
+
return 1
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
if sys.platform not in ("win32", "darwin"): # unixoids
|
| 328 |
+
if shutil.which("xdg-open"):
|
| 329 |
+
register(XDGViewer)
|
| 330 |
+
if shutil.which("display"):
|
| 331 |
+
register(DisplayViewer)
|
| 332 |
+
if shutil.which("gm"):
|
| 333 |
+
register(GmDisplayViewer)
|
| 334 |
+
if shutil.which("eog"):
|
| 335 |
+
register(EogViewer)
|
| 336 |
+
if shutil.which("xv"):
|
| 337 |
+
register(XVViewer)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class IPythonViewer(Viewer):
|
| 341 |
+
"""The viewer for IPython frontends."""
|
| 342 |
+
|
| 343 |
+
def show_image(self, image: Image.Image, **options: Any) -> int:
|
| 344 |
+
ipython_display(image)
|
| 345 |
+
return 1
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
try:
|
| 349 |
+
from IPython.display import display as ipython_display
|
| 350 |
+
except ImportError:
|
| 351 |
+
pass
|
| 352 |
+
else:
|
| 353 |
+
register(IPythonViewer)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
if __name__ == "__main__":
|
| 357 |
+
if len(sys.argv) < 2:
|
| 358 |
+
print("Syntax: python3 ImageShow.py imagefile [title]")
|
| 359 |
+
sys.exit()
|
| 360 |
+
|
| 361 |
+
with Image.open(sys.argv[1]) as im:
|
| 362 |
+
print(show(im, *sys.argv[2:]))
|
PIL/ImageStat.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# global image statistics
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-04-05 fl Created
|
| 9 |
+
# 1997-05-21 fl Added mask; added rms, var, stddev attributes
|
| 10 |
+
# 1997-08-05 fl Added median
|
| 11 |
+
# 1998-07-05 hk Fixed integer overflow error
|
| 12 |
+
#
|
| 13 |
+
# Notes:
|
| 14 |
+
# This class shows how to implement delayed evaluation of attributes.
|
| 15 |
+
# To get a certain value, simply access the corresponding attribute.
|
| 16 |
+
# The __getattr__ dispatcher takes care of the rest.
|
| 17 |
+
#
|
| 18 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 19 |
+
# Copyright (c) Fredrik Lundh 1996-97.
|
| 20 |
+
#
|
| 21 |
+
# See the README file for information on usage and redistribution.
|
| 22 |
+
#
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import math
|
| 26 |
+
from functools import cached_property
|
| 27 |
+
|
| 28 |
+
from . import Image
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Stat:
|
| 32 |
+
def __init__(
|
| 33 |
+
self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None
|
| 34 |
+
) -> None:
|
| 35 |
+
"""
|
| 36 |
+
Calculate statistics for the given image. If a mask is included,
|
| 37 |
+
only the regions covered by that mask are included in the
|
| 38 |
+
statistics. You can also pass in a previously calculated histogram.
|
| 39 |
+
|
| 40 |
+
:param image: A PIL image, or a precalculated histogram.
|
| 41 |
+
|
| 42 |
+
.. note::
|
| 43 |
+
|
| 44 |
+
For a PIL image, calculations rely on the
|
| 45 |
+
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
| 46 |
+
grouped into 256 bins, even if the image has more than 8 bits per
|
| 47 |
+
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
| 48 |
+
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
| 49 |
+
of more than 255.
|
| 50 |
+
|
| 51 |
+
:param mask: An optional mask.
|
| 52 |
+
"""
|
| 53 |
+
if isinstance(image_or_list, Image.Image):
|
| 54 |
+
self.h = image_or_list.histogram(mask)
|
| 55 |
+
elif isinstance(image_or_list, list):
|
| 56 |
+
self.h = image_or_list
|
| 57 |
+
else:
|
| 58 |
+
msg = "first argument must be image or list" # type: ignore[unreachable]
|
| 59 |
+
raise TypeError(msg)
|
| 60 |
+
self.bands = list(range(len(self.h) // 256))
|
| 61 |
+
|
| 62 |
+
@cached_property
|
| 63 |
+
def extrema(self) -> list[tuple[int, int]]:
|
| 64 |
+
"""
|
| 65 |
+
Min/max values for each band in the image.
|
| 66 |
+
|
| 67 |
+
.. note::
|
| 68 |
+
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
|
| 69 |
+
simply returns the low and high bins used. This is correct for
|
| 70 |
+
images with 8 bits per channel, but fails for other modes such as
|
| 71 |
+
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
|
| 72 |
+
return per-band extrema for the image. This is more correct and
|
| 73 |
+
efficient because, for non-8-bit modes, the histogram method uses
|
| 74 |
+
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def minmax(histogram: list[int]) -> tuple[int, int]:
|
| 78 |
+
res_min, res_max = 255, 0
|
| 79 |
+
for i in range(256):
|
| 80 |
+
if histogram[i]:
|
| 81 |
+
res_min = i
|
| 82 |
+
break
|
| 83 |
+
for i in range(255, -1, -1):
|
| 84 |
+
if histogram[i]:
|
| 85 |
+
res_max = i
|
| 86 |
+
break
|
| 87 |
+
return res_min, res_max
|
| 88 |
+
|
| 89 |
+
return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
|
| 90 |
+
|
| 91 |
+
@cached_property
|
| 92 |
+
def count(self) -> list[int]:
|
| 93 |
+
"""Total number of pixels for each band in the image."""
|
| 94 |
+
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
|
| 95 |
+
|
| 96 |
+
@cached_property
|
| 97 |
+
def sum(self) -> list[float]:
|
| 98 |
+
"""Sum of all pixels for each band in the image."""
|
| 99 |
+
|
| 100 |
+
v = []
|
| 101 |
+
for i in range(0, len(self.h), 256):
|
| 102 |
+
layer_sum = 0.0
|
| 103 |
+
for j in range(256):
|
| 104 |
+
layer_sum += j * self.h[i + j]
|
| 105 |
+
v.append(layer_sum)
|
| 106 |
+
return v
|
| 107 |
+
|
| 108 |
+
@cached_property
|
| 109 |
+
def sum2(self) -> list[float]:
|
| 110 |
+
"""Squared sum of all pixels for each band in the image."""
|
| 111 |
+
|
| 112 |
+
v = []
|
| 113 |
+
for i in range(0, len(self.h), 256):
|
| 114 |
+
sum2 = 0.0
|
| 115 |
+
for j in range(256):
|
| 116 |
+
sum2 += (j**2) * float(self.h[i + j])
|
| 117 |
+
v.append(sum2)
|
| 118 |
+
return v
|
| 119 |
+
|
| 120 |
+
@cached_property
|
| 121 |
+
def mean(self) -> list[float]:
|
| 122 |
+
"""Average (arithmetic mean) pixel level for each band in the image."""
|
| 123 |
+
return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
|
| 124 |
+
|
| 125 |
+
@cached_property
|
| 126 |
+
def median(self) -> list[int]:
|
| 127 |
+
"""Median pixel level for each band in the image."""
|
| 128 |
+
|
| 129 |
+
v = []
|
| 130 |
+
for i in self.bands:
|
| 131 |
+
s = 0
|
| 132 |
+
half = self.count[i] // 2
|
| 133 |
+
b = i * 256
|
| 134 |
+
for j in range(256):
|
| 135 |
+
s = s + self.h[b + j]
|
| 136 |
+
if s > half:
|
| 137 |
+
break
|
| 138 |
+
v.append(j)
|
| 139 |
+
return v
|
| 140 |
+
|
| 141 |
+
@cached_property
|
| 142 |
+
def rms(self) -> list[float]:
|
| 143 |
+
"""RMS (root-mean-square) for each band in the image."""
|
| 144 |
+
return [
|
| 145 |
+
math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
|
| 146 |
+
for i in self.bands
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
@cached_property
|
| 150 |
+
def var(self) -> list[float]:
|
| 151 |
+
"""Variance for each band in the image."""
|
| 152 |
+
return [
|
| 153 |
+
(
|
| 154 |
+
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
| 155 |
+
if self.count[i]
|
| 156 |
+
else 0
|
| 157 |
+
)
|
| 158 |
+
for i in self.bands
|
| 159 |
+
]
|
| 160 |
+
|
| 161 |
+
@cached_property
|
| 162 |
+
def stddev(self) -> list[float]:
|
| 163 |
+
"""Standard deviation for each band in the image."""
|
| 164 |
+
return [math.sqrt(self.var[i]) for i in self.bands]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
Global = Stat # compatibility
|
PIL/ImageText.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from . import ImageFont
|
| 4 |
+
from ._typing import _Ink
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Text:
|
| 8 |
+
def __init__(
|
| 9 |
+
self,
|
| 10 |
+
text: str | bytes,
|
| 11 |
+
font: (
|
| 12 |
+
ImageFont.ImageFont
|
| 13 |
+
| ImageFont.FreeTypeFont
|
| 14 |
+
| ImageFont.TransposedFont
|
| 15 |
+
| None
|
| 16 |
+
) = None,
|
| 17 |
+
mode: str = "RGB",
|
| 18 |
+
spacing: float = 4,
|
| 19 |
+
direction: str | None = None,
|
| 20 |
+
features: list[str] | None = None,
|
| 21 |
+
language: str | None = None,
|
| 22 |
+
) -> None:
|
| 23 |
+
"""
|
| 24 |
+
:param text: String to be drawn.
|
| 25 |
+
:param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance,
|
| 26 |
+
:py:class:`~PIL.ImageFont.FreeTypeFont` instance,
|
| 27 |
+
:py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If
|
| 28 |
+
``None``, the default font from :py:meth:`.ImageFont.load_default`
|
| 29 |
+
will be used.
|
| 30 |
+
:param mode: The image mode this will be used with.
|
| 31 |
+
:param spacing: The number of pixels between lines.
|
| 32 |
+
:param direction: Direction of the text. It can be ``"rtl"`` (right to left),
|
| 33 |
+
``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
| 34 |
+
Requires libraqm.
|
| 35 |
+
:param features: A list of OpenType font features to be used during text
|
| 36 |
+
layout. This is usually used to turn on optional font features
|
| 37 |
+
that are not enabled by default, for example ``"dlig"`` or
|
| 38 |
+
``"ss01"``, but can be also used to turn off default font
|
| 39 |
+
features, for example ``"-liga"`` to disable ligatures or
|
| 40 |
+
``"-kern"`` to disable kerning. To get all supported
|
| 41 |
+
features, see `OpenType docs`_.
|
| 42 |
+
Requires libraqm.
|
| 43 |
+
:param language: Language of the text. Different languages may use
|
| 44 |
+
different glyph shapes or ligatures. This parameter tells
|
| 45 |
+
the font which language the text is in, and to apply the
|
| 46 |
+
correct substitutions as appropriate, if available.
|
| 47 |
+
It should be a `BCP 47 language code`_.
|
| 48 |
+
Requires libraqm.
|
| 49 |
+
"""
|
| 50 |
+
self.text = text
|
| 51 |
+
self.font = font or ImageFont.load_default()
|
| 52 |
+
|
| 53 |
+
self.mode = mode
|
| 54 |
+
self.spacing = spacing
|
| 55 |
+
self.direction = direction
|
| 56 |
+
self.features = features
|
| 57 |
+
self.language = language
|
| 58 |
+
|
| 59 |
+
self.embedded_color = False
|
| 60 |
+
|
| 61 |
+
self.stroke_width: float = 0
|
| 62 |
+
self.stroke_fill: _Ink | None = None
|
| 63 |
+
|
| 64 |
+
def embed_color(self) -> None:
|
| 65 |
+
"""
|
| 66 |
+
Use embedded color glyphs (COLR, CBDT, SBIX).
|
| 67 |
+
"""
|
| 68 |
+
if self.mode not in ("RGB", "RGBA"):
|
| 69 |
+
msg = "Embedded color supported only in RGB and RGBA modes"
|
| 70 |
+
raise ValueError(msg)
|
| 71 |
+
self.embedded_color = True
|
| 72 |
+
|
| 73 |
+
def stroke(self, width: float = 0, fill: _Ink | None = None) -> None:
|
| 74 |
+
"""
|
| 75 |
+
:param width: The width of the text stroke.
|
| 76 |
+
:param fill: Color to use for the text stroke when drawing. If not given, will
|
| 77 |
+
default to the ``fill`` parameter from
|
| 78 |
+
:py:meth:`.ImageDraw.ImageDraw.text`.
|
| 79 |
+
"""
|
| 80 |
+
self.stroke_width = width
|
| 81 |
+
self.stroke_fill = fill
|
| 82 |
+
|
| 83 |
+
def _get_fontmode(self) -> str:
|
| 84 |
+
if self.mode in ("1", "P", "I", "F"):
|
| 85 |
+
return "1"
|
| 86 |
+
elif self.embedded_color:
|
| 87 |
+
return "RGBA"
|
| 88 |
+
else:
|
| 89 |
+
return "L"
|
| 90 |
+
|
| 91 |
+
def get_length(self) -> float:
|
| 92 |
+
"""
|
| 93 |
+
Returns length (in pixels with 1/64 precision) of text.
|
| 94 |
+
|
| 95 |
+
This is the amount by which following text should be offset.
|
| 96 |
+
Text bounding box may extend past the length in some fonts,
|
| 97 |
+
e.g. when using italics or accents.
|
| 98 |
+
|
| 99 |
+
The result is returned as a float; it is a whole number if using basic layout.
|
| 100 |
+
|
| 101 |
+
Note that the sum of two lengths may not equal the length of a concatenated
|
| 102 |
+
string due to kerning. If you need to adjust for kerning, include the following
|
| 103 |
+
character and subtract its length.
|
| 104 |
+
|
| 105 |
+
For example, instead of::
|
| 106 |
+
|
| 107 |
+
hello = ImageText.Text("Hello", font).get_length()
|
| 108 |
+
world = ImageText.Text("World", font).get_length()
|
| 109 |
+
helloworld = ImageText.Text("HelloWorld", font).get_length()
|
| 110 |
+
assert hello + world == helloworld
|
| 111 |
+
|
| 112 |
+
use::
|
| 113 |
+
|
| 114 |
+
hello = (
|
| 115 |
+
ImageText.Text("HelloW", font).get_length() -
|
| 116 |
+
ImageText.Text("W", font).get_length()
|
| 117 |
+
) # adjusted for kerning
|
| 118 |
+
world = ImageText.Text("World", font).get_length()
|
| 119 |
+
helloworld = ImageText.Text("HelloWorld", font).get_length()
|
| 120 |
+
assert hello + world == helloworld
|
| 121 |
+
|
| 122 |
+
or disable kerning with (requires libraqm)::
|
| 123 |
+
|
| 124 |
+
hello = ImageText.Text("Hello", font, features=["-kern"]).get_length()
|
| 125 |
+
world = ImageText.Text("World", font, features=["-kern"]).get_length()
|
| 126 |
+
helloworld = ImageText.Text(
|
| 127 |
+
"HelloWorld", font, features=["-kern"]
|
| 128 |
+
).get_length()
|
| 129 |
+
assert hello + world == helloworld
|
| 130 |
+
|
| 131 |
+
:return: Either width for horizontal text, or height for vertical text.
|
| 132 |
+
"""
|
| 133 |
+
if isinstance(self.text, str):
|
| 134 |
+
multiline = "\n" in self.text
|
| 135 |
+
else:
|
| 136 |
+
multiline = b"\n" in self.text
|
| 137 |
+
if multiline:
|
| 138 |
+
msg = "can't measure length of multiline text"
|
| 139 |
+
raise ValueError(msg)
|
| 140 |
+
return self.font.getlength(
|
| 141 |
+
self.text,
|
| 142 |
+
self._get_fontmode(),
|
| 143 |
+
self.direction,
|
| 144 |
+
self.features,
|
| 145 |
+
self.language,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
def _split(
|
| 149 |
+
self, xy: tuple[float, float], anchor: str | None, align: str
|
| 150 |
+
) -> list[tuple[tuple[float, float], str, str | bytes]]:
|
| 151 |
+
if anchor is None:
|
| 152 |
+
anchor = "lt" if self.direction == "ttb" else "la"
|
| 153 |
+
elif len(anchor) != 2:
|
| 154 |
+
msg = "anchor must be a 2 character string"
|
| 155 |
+
raise ValueError(msg)
|
| 156 |
+
|
| 157 |
+
lines = (
|
| 158 |
+
self.text.split("\n")
|
| 159 |
+
if isinstance(self.text, str)
|
| 160 |
+
else self.text.split(b"\n")
|
| 161 |
+
)
|
| 162 |
+
if len(lines) == 1:
|
| 163 |
+
return [(xy, anchor, self.text)]
|
| 164 |
+
|
| 165 |
+
if anchor[1] in "tb" and self.direction != "ttb":
|
| 166 |
+
msg = "anchor not supported for multiline text"
|
| 167 |
+
raise ValueError(msg)
|
| 168 |
+
|
| 169 |
+
fontmode = self._get_fontmode()
|
| 170 |
+
line_spacing = (
|
| 171 |
+
self.font.getbbox(
|
| 172 |
+
"A",
|
| 173 |
+
fontmode,
|
| 174 |
+
None,
|
| 175 |
+
self.features,
|
| 176 |
+
self.language,
|
| 177 |
+
self.stroke_width,
|
| 178 |
+
)[3]
|
| 179 |
+
+ self.stroke_width
|
| 180 |
+
+ self.spacing
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
top = xy[1]
|
| 184 |
+
parts = []
|
| 185 |
+
if self.direction == "ttb":
|
| 186 |
+
left = xy[0]
|
| 187 |
+
for line in lines:
|
| 188 |
+
parts.append(((left, top), anchor, line))
|
| 189 |
+
left += line_spacing
|
| 190 |
+
else:
|
| 191 |
+
widths = []
|
| 192 |
+
max_width: float = 0
|
| 193 |
+
for line in lines:
|
| 194 |
+
line_width = self.font.getlength(
|
| 195 |
+
line, fontmode, self.direction, self.features, self.language
|
| 196 |
+
)
|
| 197 |
+
widths.append(line_width)
|
| 198 |
+
max_width = max(max_width, line_width)
|
| 199 |
+
|
| 200 |
+
if anchor[1] == "m":
|
| 201 |
+
top -= (len(lines) - 1) * line_spacing / 2.0
|
| 202 |
+
elif anchor[1] == "d":
|
| 203 |
+
top -= (len(lines) - 1) * line_spacing
|
| 204 |
+
|
| 205 |
+
idx = -1
|
| 206 |
+
for line in lines:
|
| 207 |
+
left = xy[0]
|
| 208 |
+
idx += 1
|
| 209 |
+
width_difference = max_width - widths[idx]
|
| 210 |
+
|
| 211 |
+
# align by align parameter
|
| 212 |
+
if align in ("left", "justify"):
|
| 213 |
+
pass
|
| 214 |
+
elif align == "center":
|
| 215 |
+
left += width_difference / 2.0
|
| 216 |
+
elif align == "right":
|
| 217 |
+
left += width_difference
|
| 218 |
+
else:
|
| 219 |
+
msg = 'align must be "left", "center", "right" or "justify"'
|
| 220 |
+
raise ValueError(msg)
|
| 221 |
+
|
| 222 |
+
if (
|
| 223 |
+
align == "justify"
|
| 224 |
+
and width_difference != 0
|
| 225 |
+
and idx != len(lines) - 1
|
| 226 |
+
):
|
| 227 |
+
words = (
|
| 228 |
+
line.split(" ") if isinstance(line, str) else line.split(b" ")
|
| 229 |
+
)
|
| 230 |
+
if len(words) > 1:
|
| 231 |
+
# align left by anchor
|
| 232 |
+
if anchor[0] == "m":
|
| 233 |
+
left -= max_width / 2.0
|
| 234 |
+
elif anchor[0] == "r":
|
| 235 |
+
left -= max_width
|
| 236 |
+
|
| 237 |
+
word_widths = [
|
| 238 |
+
self.font.getlength(
|
| 239 |
+
word,
|
| 240 |
+
fontmode,
|
| 241 |
+
self.direction,
|
| 242 |
+
self.features,
|
| 243 |
+
self.language,
|
| 244 |
+
)
|
| 245 |
+
for word in words
|
| 246 |
+
]
|
| 247 |
+
word_anchor = "l" + anchor[1]
|
| 248 |
+
width_difference = max_width - sum(word_widths)
|
| 249 |
+
i = 0
|
| 250 |
+
for word in words:
|
| 251 |
+
parts.append(((left, top), word_anchor, word))
|
| 252 |
+
left += word_widths[i] + width_difference / (len(words) - 1)
|
| 253 |
+
i += 1
|
| 254 |
+
top += line_spacing
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
# align left by anchor
|
| 258 |
+
if anchor[0] == "m":
|
| 259 |
+
left -= width_difference / 2.0
|
| 260 |
+
elif anchor[0] == "r":
|
| 261 |
+
left -= width_difference
|
| 262 |
+
parts.append(((left, top), anchor, line))
|
| 263 |
+
top += line_spacing
|
| 264 |
+
|
| 265 |
+
return parts
|
| 266 |
+
|
| 267 |
+
def get_bbox(
|
| 268 |
+
self,
|
| 269 |
+
xy: tuple[float, float] = (0, 0),
|
| 270 |
+
anchor: str | None = None,
|
| 271 |
+
align: str = "left",
|
| 272 |
+
) -> tuple[float, float, float, float]:
|
| 273 |
+
"""
|
| 274 |
+
Returns bounding box (in pixels) of text.
|
| 275 |
+
|
| 276 |
+
Use :py:meth:`get_length` to get the offset of following text with 1/64 pixel
|
| 277 |
+
precision. The bounding box includes extra margins for some fonts, e.g. italics
|
| 278 |
+
or accents.
|
| 279 |
+
|
| 280 |
+
:param xy: The anchor coordinates of the text.
|
| 281 |
+
:param anchor: The text anchor alignment. Determines the relative location of
|
| 282 |
+
the anchor to the text. The default alignment is top left,
|
| 283 |
+
specifically ``la`` for horizontal text and ``lt`` for
|
| 284 |
+
vertical text. See :ref:`text-anchors` for details.
|
| 285 |
+
:param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or
|
| 286 |
+
``"justify"`` determines the relative alignment of lines. Use the
|
| 287 |
+
``anchor`` parameter to specify the alignment to ``xy``.
|
| 288 |
+
|
| 289 |
+
:return: ``(left, top, right, bottom)`` bounding box
|
| 290 |
+
"""
|
| 291 |
+
bbox: tuple[float, float, float, float] | None = None
|
| 292 |
+
fontmode = self._get_fontmode()
|
| 293 |
+
for xy, anchor, line in self._split(xy, anchor, align):
|
| 294 |
+
bbox_line = self.font.getbbox(
|
| 295 |
+
line,
|
| 296 |
+
fontmode,
|
| 297 |
+
self.direction,
|
| 298 |
+
self.features,
|
| 299 |
+
self.language,
|
| 300 |
+
self.stroke_width,
|
| 301 |
+
anchor,
|
| 302 |
+
)
|
| 303 |
+
bbox_line = (
|
| 304 |
+
bbox_line[0] + xy[0],
|
| 305 |
+
bbox_line[1] + xy[1],
|
| 306 |
+
bbox_line[2] + xy[0],
|
| 307 |
+
bbox_line[3] + xy[1],
|
| 308 |
+
)
|
| 309 |
+
if bbox is None:
|
| 310 |
+
bbox = bbox_line
|
| 311 |
+
else:
|
| 312 |
+
bbox = (
|
| 313 |
+
min(bbox[0], bbox_line[0]),
|
| 314 |
+
min(bbox[1], bbox_line[1]),
|
| 315 |
+
max(bbox[2], bbox_line[2]),
|
| 316 |
+
max(bbox[3], bbox_line[3]),
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
assert bbox is not None
|
| 320 |
+
return bbox
|
PIL/ImageTk.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# a Tk display interface
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 96-04-08 fl Created
|
| 9 |
+
# 96-09-06 fl Added getimage method
|
| 10 |
+
# 96-11-01 fl Rewritten, removed image attribute and crop method
|
| 11 |
+
# 97-05-09 fl Use PyImagingPaste method instead of image type
|
| 12 |
+
# 97-05-12 fl Minor tweaks to match the IFUNC95 interface
|
| 13 |
+
# 97-05-17 fl Support the "pilbitmap" booster patch
|
| 14 |
+
# 97-06-05 fl Added file= and data= argument to image constructors
|
| 15 |
+
# 98-03-09 fl Added width and height methods to Image classes
|
| 16 |
+
# 98-07-02 fl Use default mode for "P" images without palette attribute
|
| 17 |
+
# 98-07-02 fl Explicitly destroy Tkinter image objects
|
| 18 |
+
# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch)
|
| 19 |
+
# 99-07-26 fl Automatically hook into Tkinter (if possible)
|
| 20 |
+
# 99-08-15 fl Hook uses _imagingtk instead of _imaging
|
| 21 |
+
#
|
| 22 |
+
# Copyright (c) 1997-1999 by Secret Labs AB
|
| 23 |
+
# Copyright (c) 1996-1997 by Fredrik Lundh
|
| 24 |
+
#
|
| 25 |
+
# See the README file for information on usage and redistribution.
|
| 26 |
+
#
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import tkinter
|
| 30 |
+
from io import BytesIO
|
| 31 |
+
from typing import Any
|
| 32 |
+
|
| 33 |
+
from . import Image, ImageFile
|
| 34 |
+
|
| 35 |
+
TYPE_CHECKING = False
|
| 36 |
+
if TYPE_CHECKING:
|
| 37 |
+
from ._typing import CapsuleType
|
| 38 |
+
|
| 39 |
+
# --------------------------------------------------------------------
|
| 40 |
+
# Check for Tkinter interface hooks
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
|
| 44 |
+
source = None
|
| 45 |
+
if "file" in kw:
|
| 46 |
+
source = kw.pop("file")
|
| 47 |
+
elif "data" in kw:
|
| 48 |
+
source = BytesIO(kw.pop("data"))
|
| 49 |
+
if not source:
|
| 50 |
+
return None
|
| 51 |
+
return Image.open(source)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _pyimagingtkcall(
|
| 55 |
+
command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
|
| 56 |
+
) -> None:
|
| 57 |
+
tk = photo.tk
|
| 58 |
+
try:
|
| 59 |
+
tk.call(command, photo, repr(ptr))
|
| 60 |
+
except tkinter.TclError:
|
| 61 |
+
# activate Tkinter hook
|
| 62 |
+
# may raise an error if it cannot attach to Tkinter
|
| 63 |
+
from . import _imagingtk
|
| 64 |
+
|
| 65 |
+
_imagingtk.tkinit(tk.interpaddr())
|
| 66 |
+
tk.call(command, photo, repr(ptr))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# --------------------------------------------------------------------
|
| 70 |
+
# PhotoImage
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class PhotoImage:
|
| 74 |
+
"""
|
| 75 |
+
A Tkinter-compatible photo image. This can be used
|
| 76 |
+
everywhere Tkinter expects an image object. If the image is an RGBA
|
| 77 |
+
image, pixels having alpha 0 are treated as transparent.
|
| 78 |
+
|
| 79 |
+
The constructor takes either a PIL image, or a mode and a size.
|
| 80 |
+
Alternatively, you can use the ``file`` or ``data`` options to initialize
|
| 81 |
+
the photo image object.
|
| 82 |
+
|
| 83 |
+
:param image: Either a PIL image, or a mode string. If a mode string is
|
| 84 |
+
used, a size must also be given.
|
| 85 |
+
:param size: If the first argument is a mode string, this defines the size
|
| 86 |
+
of the image.
|
| 87 |
+
:keyword file: A filename to load the image from (using
|
| 88 |
+
``Image.open(file)``).
|
| 89 |
+
:keyword data: An 8-bit string containing image data (as loaded from an
|
| 90 |
+
image file).
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def __init__(
|
| 94 |
+
self,
|
| 95 |
+
image: Image.Image | str | None = None,
|
| 96 |
+
size: tuple[int, int] | None = None,
|
| 97 |
+
**kw: Any,
|
| 98 |
+
) -> None:
|
| 99 |
+
# Tk compatibility: file or data
|
| 100 |
+
if image is None:
|
| 101 |
+
image = _get_image_from_kw(kw)
|
| 102 |
+
|
| 103 |
+
if image is None:
|
| 104 |
+
msg = "Image is required"
|
| 105 |
+
raise ValueError(msg)
|
| 106 |
+
elif isinstance(image, str):
|
| 107 |
+
mode = image
|
| 108 |
+
image = None
|
| 109 |
+
|
| 110 |
+
if size is None:
|
| 111 |
+
msg = "If first argument is mode, size is required"
|
| 112 |
+
raise ValueError(msg)
|
| 113 |
+
else:
|
| 114 |
+
# got an image instead of a mode
|
| 115 |
+
mode = image.mode
|
| 116 |
+
if mode == "P":
|
| 117 |
+
# palette mapped data
|
| 118 |
+
image.apply_transparency()
|
| 119 |
+
image.load()
|
| 120 |
+
mode = image.palette.mode if image.palette else "RGB"
|
| 121 |
+
size = image.size
|
| 122 |
+
kw["width"], kw["height"] = size
|
| 123 |
+
|
| 124 |
+
if mode not in ["1", "L", "RGB", "RGBA"]:
|
| 125 |
+
mode = Image.getmodebase(mode)
|
| 126 |
+
|
| 127 |
+
self.__mode = mode
|
| 128 |
+
self.__size = size
|
| 129 |
+
self.__photo = tkinter.PhotoImage(**kw)
|
| 130 |
+
self.tk = self.__photo.tk
|
| 131 |
+
if image:
|
| 132 |
+
self.paste(image)
|
| 133 |
+
|
| 134 |
+
def __del__(self) -> None:
|
| 135 |
+
try:
|
| 136 |
+
name = self.__photo.name
|
| 137 |
+
except AttributeError:
|
| 138 |
+
return
|
| 139 |
+
self.__photo.name = None
|
| 140 |
+
try:
|
| 141 |
+
self.__photo.tk.call("image", "delete", name)
|
| 142 |
+
except Exception:
|
| 143 |
+
pass # ignore internal errors
|
| 144 |
+
|
| 145 |
+
def __str__(self) -> str:
|
| 146 |
+
"""
|
| 147 |
+
Get the Tkinter photo image identifier. This method is automatically
|
| 148 |
+
called by Tkinter whenever a PhotoImage object is passed to a Tkinter
|
| 149 |
+
method.
|
| 150 |
+
|
| 151 |
+
:return: A Tkinter photo image identifier (a string).
|
| 152 |
+
"""
|
| 153 |
+
return str(self.__photo)
|
| 154 |
+
|
| 155 |
+
def width(self) -> int:
|
| 156 |
+
"""
|
| 157 |
+
Get the width of the image.
|
| 158 |
+
|
| 159 |
+
:return: The width, in pixels.
|
| 160 |
+
"""
|
| 161 |
+
return self.__size[0]
|
| 162 |
+
|
| 163 |
+
def height(self) -> int:
|
| 164 |
+
"""
|
| 165 |
+
Get the height of the image.
|
| 166 |
+
|
| 167 |
+
:return: The height, in pixels.
|
| 168 |
+
"""
|
| 169 |
+
return self.__size[1]
|
| 170 |
+
|
| 171 |
+
def paste(self, im: Image.Image) -> None:
|
| 172 |
+
"""
|
| 173 |
+
Paste a PIL image into the photo image. Note that this can
|
| 174 |
+
be very slow if the photo image is displayed.
|
| 175 |
+
|
| 176 |
+
:param im: A PIL image. The size must match the target region. If the
|
| 177 |
+
mode does not match, the image is converted to the mode of
|
| 178 |
+
the bitmap image.
|
| 179 |
+
"""
|
| 180 |
+
# convert to blittable
|
| 181 |
+
ptr = im.getim()
|
| 182 |
+
image = im.im
|
| 183 |
+
if not image.isblock() or im.mode != self.__mode:
|
| 184 |
+
block = Image.core.new_block(self.__mode, im.size)
|
| 185 |
+
image.convert2(block, image) # convert directly between buffers
|
| 186 |
+
ptr = block.ptr
|
| 187 |
+
|
| 188 |
+
_pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# --------------------------------------------------------------------
|
| 192 |
+
# BitmapImage
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class BitmapImage:
|
| 196 |
+
"""
|
| 197 |
+
A Tkinter-compatible bitmap image. This can be used everywhere Tkinter
|
| 198 |
+
expects an image object.
|
| 199 |
+
|
| 200 |
+
The given image must have mode "1". Pixels having value 0 are treated as
|
| 201 |
+
transparent. Options, if any, are passed on to Tkinter. The most commonly
|
| 202 |
+
used option is ``foreground``, which is used to specify the color for the
|
| 203 |
+
non-transparent parts. See the Tkinter documentation for information on
|
| 204 |
+
how to specify colours.
|
| 205 |
+
|
| 206 |
+
:param image: A PIL image.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
|
| 210 |
+
# Tk compatibility: file or data
|
| 211 |
+
if image is None:
|
| 212 |
+
image = _get_image_from_kw(kw)
|
| 213 |
+
|
| 214 |
+
if image is None:
|
| 215 |
+
msg = "Image is required"
|
| 216 |
+
raise ValueError(msg)
|
| 217 |
+
self.__mode = image.mode
|
| 218 |
+
self.__size = image.size
|
| 219 |
+
|
| 220 |
+
self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
|
| 221 |
+
|
| 222 |
+
def __del__(self) -> None:
|
| 223 |
+
try:
|
| 224 |
+
name = self.__photo.name
|
| 225 |
+
except AttributeError:
|
| 226 |
+
return
|
| 227 |
+
self.__photo.name = None
|
| 228 |
+
try:
|
| 229 |
+
self.__photo.tk.call("image", "delete", name)
|
| 230 |
+
except Exception:
|
| 231 |
+
pass # ignore internal errors
|
| 232 |
+
|
| 233 |
+
def width(self) -> int:
|
| 234 |
+
"""
|
| 235 |
+
Get the width of the image.
|
| 236 |
+
|
| 237 |
+
:return: The width, in pixels.
|
| 238 |
+
"""
|
| 239 |
+
return self.__size[0]
|
| 240 |
+
|
| 241 |
+
def height(self) -> int:
|
| 242 |
+
"""
|
| 243 |
+
Get the height of the image.
|
| 244 |
+
|
| 245 |
+
:return: The height, in pixels.
|
| 246 |
+
"""
|
| 247 |
+
return self.__size[1]
|
| 248 |
+
|
| 249 |
+
def __str__(self) -> str:
|
| 250 |
+
"""
|
| 251 |
+
Get the Tkinter bitmap image identifier. This method is automatically
|
| 252 |
+
called by Tkinter whenever a BitmapImage object is passed to a Tkinter
|
| 253 |
+
method.
|
| 254 |
+
|
| 255 |
+
:return: A Tkinter bitmap image identifier (a string).
|
| 256 |
+
"""
|
| 257 |
+
return str(self.__photo)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def getimage(photo: PhotoImage) -> Image.Image:
|
| 261 |
+
"""Copies the contents of a PhotoImage to a PIL image memory."""
|
| 262 |
+
im = Image.new("RGBA", (photo.width(), photo.height()))
|
| 263 |
+
|
| 264 |
+
_pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
|
| 265 |
+
|
| 266 |
+
return im
|
PIL/ImageTransform.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# transform wrappers
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2002-04-08 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) 2002 by Secret Labs AB
|
| 11 |
+
# Copyright (c) 2002 by Fredrik Lundh
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from collections.abc import Sequence
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
from . import Image
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Transform(Image.ImageTransformHandler):
|
| 24 |
+
"""Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""
|
| 25 |
+
|
| 26 |
+
method: Image.Transform
|
| 27 |
+
|
| 28 |
+
def __init__(self, data: Sequence[Any]) -> None:
|
| 29 |
+
self.data = data
|
| 30 |
+
|
| 31 |
+
def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
|
| 32 |
+
return self.method, self.data
|
| 33 |
+
|
| 34 |
+
def transform(
|
| 35 |
+
self,
|
| 36 |
+
size: tuple[int, int],
|
| 37 |
+
image: Image.Image,
|
| 38 |
+
**options: Any,
|
| 39 |
+
) -> Image.Image:
|
| 40 |
+
"""Perform the transform. Called from :py:meth:`.Image.transform`."""
|
| 41 |
+
# can be overridden
|
| 42 |
+
method, data = self.getdata()
|
| 43 |
+
return image.transform(size, method, data, **options)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class AffineTransform(Transform):
|
| 47 |
+
"""
|
| 48 |
+
Define an affine image transform.
|
| 49 |
+
|
| 50 |
+
This function takes a 6-tuple (a, b, c, d, e, f) which contain the first
|
| 51 |
+
two rows from the inverse of an affine transform matrix. For each pixel
|
| 52 |
+
(x, y) in the output image, the new value is taken from a position (a x +
|
| 53 |
+
b y + c, d x + e y + f) in the input image, rounded to nearest pixel.
|
| 54 |
+
|
| 55 |
+
This function can be used to scale, translate, rotate, and shear the
|
| 56 |
+
original image.
|
| 57 |
+
|
| 58 |
+
See :py:meth:`.Image.transform`
|
| 59 |
+
|
| 60 |
+
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
|
| 61 |
+
from the inverse of an affine transform matrix.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
method = Image.Transform.AFFINE
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class PerspectiveTransform(Transform):
|
| 68 |
+
"""
|
| 69 |
+
Define a perspective image transform.
|
| 70 |
+
|
| 71 |
+
This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel
|
| 72 |
+
(x, y) in the output image, the new value is taken from a position
|
| 73 |
+
((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in
|
| 74 |
+
the input image, rounded to nearest pixel.
|
| 75 |
+
|
| 76 |
+
This function can be used to scale, translate, rotate, and shear the
|
| 77 |
+
original image.
|
| 78 |
+
|
| 79 |
+
See :py:meth:`.Image.transform`
|
| 80 |
+
|
| 81 |
+
:param matrix: An 8-tuple (a, b, c, d, e, f, g, h).
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
method = Image.Transform.PERSPECTIVE
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class ExtentTransform(Transform):
|
| 88 |
+
"""
|
| 89 |
+
Define a transform to extract a subregion from an image.
|
| 90 |
+
|
| 91 |
+
Maps a rectangle (defined by two corners) from the image to a rectangle of
|
| 92 |
+
the given size. The resulting image will contain data sampled from between
|
| 93 |
+
the corners, such that (x0, y0) in the input image will end up at (0,0) in
|
| 94 |
+
the output image, and (x1, y1) at size.
|
| 95 |
+
|
| 96 |
+
This method can be used to crop, stretch, shrink, or mirror an arbitrary
|
| 97 |
+
rectangle in the current image. It is slightly slower than crop, but about
|
| 98 |
+
as fast as a corresponding resize operation.
|
| 99 |
+
|
| 100 |
+
See :py:meth:`.Image.transform`
|
| 101 |
+
|
| 102 |
+
:param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
|
| 103 |
+
input image's coordinate system. See :ref:`coordinate-system`.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
method = Image.Transform.EXTENT
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class QuadTransform(Transform):
|
| 110 |
+
"""
|
| 111 |
+
Define a quad image transform.
|
| 112 |
+
|
| 113 |
+
Maps a quadrilateral (a region defined by four corners) from the image to a
|
| 114 |
+
rectangle of the given size.
|
| 115 |
+
|
| 116 |
+
See :py:meth:`.Image.transform`
|
| 117 |
+
|
| 118 |
+
:param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the
|
| 119 |
+
upper left, lower left, lower right, and upper right corner of the
|
| 120 |
+
source quadrilateral.
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
method = Image.Transform.QUAD
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class MeshTransform(Transform):
|
| 127 |
+
"""
|
| 128 |
+
Define a mesh image transform. A mesh transform consists of one or more
|
| 129 |
+
individual quad transforms.
|
| 130 |
+
|
| 131 |
+
See :py:meth:`.Image.transform`
|
| 132 |
+
|
| 133 |
+
:param data: A list of (bbox, quad) tuples.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
method = Image.Transform.MESH
|
PIL/ImageWin.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# a Windows DIB display interface
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1996-05-20 fl Created
|
| 9 |
+
# 1996-09-20 fl Fixed subregion exposure
|
| 10 |
+
# 1997-09-21 fl Added draw primitive (for tzPrint)
|
| 11 |
+
# 2003-05-21 fl Added experimental Window/ImageWindow classes
|
| 12 |
+
# 2003-09-05 fl Added fromstring/tostring methods
|
| 13 |
+
#
|
| 14 |
+
# Copyright (c) Secret Labs AB 1997-2003.
|
| 15 |
+
# Copyright (c) Fredrik Lundh 1996-2003.
|
| 16 |
+
#
|
| 17 |
+
# See the README file for information on usage and redistribution.
|
| 18 |
+
#
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
from . import Image
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class HDC:
|
| 25 |
+
"""
|
| 26 |
+
Wraps an HDC integer. The resulting object can be passed to the
|
| 27 |
+
:py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose`
|
| 28 |
+
methods.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, dc: int) -> None:
|
| 32 |
+
self.dc = dc
|
| 33 |
+
|
| 34 |
+
def __int__(self) -> int:
|
| 35 |
+
return self.dc
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class HWND:
|
| 39 |
+
"""
|
| 40 |
+
Wraps an HWND integer. The resulting object can be passed to the
|
| 41 |
+
:py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose`
|
| 42 |
+
methods, instead of a DC.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, wnd: int) -> None:
|
| 46 |
+
self.wnd = wnd
|
| 47 |
+
|
| 48 |
+
def __int__(self) -> int:
|
| 49 |
+
return self.wnd
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class Dib:
|
| 53 |
+
"""
|
| 54 |
+
A Windows bitmap with the given mode and size. The mode can be one of "1",
|
| 55 |
+
"L", "P", or "RGB".
|
| 56 |
+
|
| 57 |
+
If the display requires a palette, this constructor creates a suitable
|
| 58 |
+
palette and associates it with the image. For an "L" image, 128 graylevels
|
| 59 |
+
are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together
|
| 60 |
+
with 20 graylevels.
|
| 61 |
+
|
| 62 |
+
To make sure that palettes work properly under Windows, you must call the
|
| 63 |
+
``palette`` method upon certain events from Windows.
|
| 64 |
+
|
| 65 |
+
:param image: Either a PIL image, or a mode string. If a mode string is
|
| 66 |
+
used, a size must also be given. The mode can be one of "1",
|
| 67 |
+
"L", "P", or "RGB".
|
| 68 |
+
:param size: If the first argument is a mode string, this
|
| 69 |
+
defines the size of the image.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self, image: Image.Image | str, size: tuple[int, int] | None = None
|
| 74 |
+
) -> None:
|
| 75 |
+
if isinstance(image, str):
|
| 76 |
+
mode = image
|
| 77 |
+
image = ""
|
| 78 |
+
if size is None:
|
| 79 |
+
msg = "If first argument is mode, size is required"
|
| 80 |
+
raise ValueError(msg)
|
| 81 |
+
else:
|
| 82 |
+
mode = image.mode
|
| 83 |
+
size = image.size
|
| 84 |
+
if mode not in ["1", "L", "P", "RGB"]:
|
| 85 |
+
mode = Image.getmodebase(mode)
|
| 86 |
+
self.image = Image.core.display(mode, size)
|
| 87 |
+
self.mode = mode
|
| 88 |
+
self.size = size
|
| 89 |
+
if image:
|
| 90 |
+
assert not isinstance(image, str)
|
| 91 |
+
self.paste(image)
|
| 92 |
+
|
| 93 |
+
def expose(self, handle: int | HDC | HWND) -> None:
|
| 94 |
+
"""
|
| 95 |
+
Copy the bitmap contents to a device context.
|
| 96 |
+
|
| 97 |
+
:param handle: Device context (HDC), cast to a Python integer, or an
|
| 98 |
+
HDC or HWND instance. In PythonWin, you can use
|
| 99 |
+
``CDC.GetHandleAttrib()`` to get a suitable handle.
|
| 100 |
+
"""
|
| 101 |
+
handle_int = int(handle)
|
| 102 |
+
if isinstance(handle, HWND):
|
| 103 |
+
dc = self.image.getdc(handle_int)
|
| 104 |
+
try:
|
| 105 |
+
self.image.expose(dc)
|
| 106 |
+
finally:
|
| 107 |
+
self.image.releasedc(handle_int, dc)
|
| 108 |
+
else:
|
| 109 |
+
self.image.expose(handle_int)
|
| 110 |
+
|
| 111 |
+
def draw(
|
| 112 |
+
self,
|
| 113 |
+
handle: int | HDC | HWND,
|
| 114 |
+
dst: tuple[int, int, int, int],
|
| 115 |
+
src: tuple[int, int, int, int] | None = None,
|
| 116 |
+
) -> None:
|
| 117 |
+
"""
|
| 118 |
+
Same as expose, but allows you to specify where to draw the image, and
|
| 119 |
+
what part of it to draw.
|
| 120 |
+
|
| 121 |
+
The destination and source areas are given as 4-tuple rectangles. If
|
| 122 |
+
the source is omitted, the entire image is copied. If the source and
|
| 123 |
+
the destination have different sizes, the image is resized as
|
| 124 |
+
necessary.
|
| 125 |
+
"""
|
| 126 |
+
if src is None:
|
| 127 |
+
src = (0, 0) + self.size
|
| 128 |
+
handle_int = int(handle)
|
| 129 |
+
if isinstance(handle, HWND):
|
| 130 |
+
dc = self.image.getdc(handle_int)
|
| 131 |
+
try:
|
| 132 |
+
self.image.draw(dc, dst, src)
|
| 133 |
+
finally:
|
| 134 |
+
self.image.releasedc(handle_int, dc)
|
| 135 |
+
else:
|
| 136 |
+
self.image.draw(handle_int, dst, src)
|
| 137 |
+
|
| 138 |
+
def query_palette(self, handle: int | HDC | HWND) -> int:
|
| 139 |
+
"""
|
| 140 |
+
Installs the palette associated with the image in the given device
|
| 141 |
+
context.
|
| 142 |
+
|
| 143 |
+
This method should be called upon **QUERYNEWPALETTE** and
|
| 144 |
+
**PALETTECHANGED** events from Windows. If this method returns a
|
| 145 |
+
non-zero value, one or more display palette entries were changed, and
|
| 146 |
+
the image should be redrawn.
|
| 147 |
+
|
| 148 |
+
:param handle: Device context (HDC), cast to a Python integer, or an
|
| 149 |
+
HDC or HWND instance.
|
| 150 |
+
:return: The number of entries that were changed (if one or more entries,
|
| 151 |
+
this indicates that the image should be redrawn).
|
| 152 |
+
"""
|
| 153 |
+
handle_int = int(handle)
|
| 154 |
+
if isinstance(handle, HWND):
|
| 155 |
+
handle = self.image.getdc(handle_int)
|
| 156 |
+
try:
|
| 157 |
+
result = self.image.query_palette(handle)
|
| 158 |
+
finally:
|
| 159 |
+
self.image.releasedc(handle, handle)
|
| 160 |
+
else:
|
| 161 |
+
result = self.image.query_palette(handle_int)
|
| 162 |
+
return result
|
| 163 |
+
|
| 164 |
+
def paste(
|
| 165 |
+
self, im: Image.Image, box: tuple[int, int, int, int] | None = None
|
| 166 |
+
) -> None:
|
| 167 |
+
"""
|
| 168 |
+
Paste a PIL image into the bitmap image.
|
| 169 |
+
|
| 170 |
+
:param im: A PIL image. The size must match the target region.
|
| 171 |
+
If the mode does not match, the image is converted to the
|
| 172 |
+
mode of the bitmap image.
|
| 173 |
+
:param box: A 4-tuple defining the left, upper, right, and
|
| 174 |
+
lower pixel coordinate. See :ref:`coordinate-system`. If
|
| 175 |
+
None is given instead of a tuple, all of the image is
|
| 176 |
+
assumed.
|
| 177 |
+
"""
|
| 178 |
+
im.load()
|
| 179 |
+
if self.mode != im.mode:
|
| 180 |
+
im = im.convert(self.mode)
|
| 181 |
+
if box:
|
| 182 |
+
self.image.paste(im.im, box)
|
| 183 |
+
else:
|
| 184 |
+
self.image.paste(im.im)
|
| 185 |
+
|
| 186 |
+
def frombytes(self, buffer: bytes) -> None:
|
| 187 |
+
"""
|
| 188 |
+
Load display memory contents from byte data.
|
| 189 |
+
|
| 190 |
+
:param buffer: A buffer containing display data (usually
|
| 191 |
+
data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`)
|
| 192 |
+
"""
|
| 193 |
+
self.image.frombytes(buffer)
|
| 194 |
+
|
| 195 |
+
def tobytes(self) -> bytes:
|
| 196 |
+
"""
|
| 197 |
+
Copy display memory contents to bytes object.
|
| 198 |
+
|
| 199 |
+
:return: A bytes object containing display data.
|
| 200 |
+
"""
|
| 201 |
+
return self.image.tobytes()
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class Window:
|
| 205 |
+
"""Create a Window with the given title size."""
|
| 206 |
+
|
| 207 |
+
def __init__(
|
| 208 |
+
self, title: str = "PIL", width: int | None = None, height: int | None = None
|
| 209 |
+
) -> None:
|
| 210 |
+
self.hwnd = Image.core.createwindow(
|
| 211 |
+
title, self.__dispatcher, width or 0, height or 0
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
def __dispatcher(self, action: str, *args: int) -> None:
|
| 215 |
+
getattr(self, f"ui_handle_{action}")(*args)
|
| 216 |
+
|
| 217 |
+
def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
|
| 218 |
+
pass
|
| 219 |
+
|
| 220 |
+
def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None:
|
| 221 |
+
pass
|
| 222 |
+
|
| 223 |
+
def ui_handle_destroy(self) -> None:
|
| 224 |
+
pass
|
| 225 |
+
|
| 226 |
+
def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
|
| 227 |
+
pass
|
| 228 |
+
|
| 229 |
+
def ui_handle_resize(self, width: int, height: int) -> None:
|
| 230 |
+
pass
|
| 231 |
+
|
| 232 |
+
def mainloop(self) -> None:
|
| 233 |
+
Image.core.eventloop()
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
class ImageWindow(Window):
|
| 237 |
+
"""Create an image window which displays the given image."""
|
| 238 |
+
|
| 239 |
+
def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None:
|
| 240 |
+
if not isinstance(image, Dib):
|
| 241 |
+
image = Dib(image)
|
| 242 |
+
self.image = image
|
| 243 |
+
width, height = image.size
|
| 244 |
+
super().__init__(title, width=width, height=height)
|
| 245 |
+
|
| 246 |
+
def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
|
| 247 |
+
self.image.draw(dc, (x0, y0, x1, y1))
|
PIL/IptcImagePlugin.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# IPTC/NAA file handling
|
| 6 |
+
#
|
| 7 |
+
# history:
|
| 8 |
+
# 1995-10-01 fl Created
|
| 9 |
+
# 1998-03-09 fl Cleaned up and added to PIL
|
| 10 |
+
# 2002-06-18 fl Added getiptcinfo helper
|
| 11 |
+
#
|
| 12 |
+
# Copyright (c) Secret Labs AB 1997-2002.
|
| 13 |
+
# Copyright (c) Fredrik Lundh 1995.
|
| 14 |
+
#
|
| 15 |
+
# See the README file for information on usage and redistribution.
|
| 16 |
+
#
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from io import BytesIO
|
| 20 |
+
from typing import cast
|
| 21 |
+
|
| 22 |
+
from . import Image, ImageFile
|
| 23 |
+
from ._binary import i16be as i16
|
| 24 |
+
from ._binary import i32be as i32
|
| 25 |
+
|
| 26 |
+
COMPRESSION = {1: "raw", 5: "jpeg"}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
#
|
| 30 |
+
# Helpers
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _i(c: bytes) -> int:
|
| 34 |
+
return i32((b"\0\0\0\0" + c)[-4:])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
##
|
| 38 |
+
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
| 39 |
+
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class IptcImageFile(ImageFile.ImageFile):
|
| 43 |
+
format = "IPTC"
|
| 44 |
+
format_description = "IPTC/NAA"
|
| 45 |
+
|
| 46 |
+
def getint(self, key: tuple[int, int]) -> int:
|
| 47 |
+
return _i(self.info[key])
|
| 48 |
+
|
| 49 |
+
def field(self) -> tuple[tuple[int, int] | None, int]:
|
| 50 |
+
#
|
| 51 |
+
# get a IPTC field header
|
| 52 |
+
assert self.fp is not None
|
| 53 |
+
s = self.fp.read(5)
|
| 54 |
+
if not s.strip(b"\x00"):
|
| 55 |
+
return None, 0
|
| 56 |
+
|
| 57 |
+
tag = s[1], s[2]
|
| 58 |
+
|
| 59 |
+
# syntax
|
| 60 |
+
if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
|
| 61 |
+
msg = "invalid IPTC/NAA file"
|
| 62 |
+
raise SyntaxError(msg)
|
| 63 |
+
|
| 64 |
+
# field size
|
| 65 |
+
size = s[3]
|
| 66 |
+
if size > 132:
|
| 67 |
+
msg = "illegal field length in IPTC/NAA file"
|
| 68 |
+
raise OSError(msg)
|
| 69 |
+
elif size == 128:
|
| 70 |
+
size = 0
|
| 71 |
+
elif size > 128:
|
| 72 |
+
size = _i(self.fp.read(size - 128))
|
| 73 |
+
else:
|
| 74 |
+
size = i16(s, 3)
|
| 75 |
+
|
| 76 |
+
return tag, size
|
| 77 |
+
|
| 78 |
+
def _open(self) -> None:
|
| 79 |
+
# load descriptive fields
|
| 80 |
+
assert self.fp is not None
|
| 81 |
+
while True:
|
| 82 |
+
offset = self.fp.tell()
|
| 83 |
+
tag, size = self.field()
|
| 84 |
+
if not tag or tag == (8, 10):
|
| 85 |
+
break
|
| 86 |
+
if size:
|
| 87 |
+
tagdata = self.fp.read(size)
|
| 88 |
+
else:
|
| 89 |
+
tagdata = None
|
| 90 |
+
if tag in self.info:
|
| 91 |
+
if isinstance(self.info[tag], list):
|
| 92 |
+
self.info[tag].append(tagdata)
|
| 93 |
+
else:
|
| 94 |
+
self.info[tag] = [self.info[tag], tagdata]
|
| 95 |
+
else:
|
| 96 |
+
self.info[tag] = tagdata
|
| 97 |
+
|
| 98 |
+
# mode
|
| 99 |
+
layers = self.info[(3, 60)][0]
|
| 100 |
+
component = self.info[(3, 60)][1]
|
| 101 |
+
if layers == 1 and not component:
|
| 102 |
+
self._mode = "L"
|
| 103 |
+
band = None
|
| 104 |
+
else:
|
| 105 |
+
if layers == 3 and component:
|
| 106 |
+
self._mode = "RGB"
|
| 107 |
+
elif layers == 4 and component:
|
| 108 |
+
self._mode = "CMYK"
|
| 109 |
+
if (3, 65) in self.info:
|
| 110 |
+
band = self.info[(3, 65)][0] - 1
|
| 111 |
+
else:
|
| 112 |
+
band = 0
|
| 113 |
+
|
| 114 |
+
# size
|
| 115 |
+
self._size = self.getint((3, 20)), self.getint((3, 30))
|
| 116 |
+
|
| 117 |
+
# compression
|
| 118 |
+
try:
|
| 119 |
+
compression = COMPRESSION[self.getint((3, 120))]
|
| 120 |
+
except KeyError as e:
|
| 121 |
+
msg = "Unknown IPTC image compression"
|
| 122 |
+
raise OSError(msg) from e
|
| 123 |
+
|
| 124 |
+
# tile
|
| 125 |
+
if tag == (8, 10):
|
| 126 |
+
self.tile = [
|
| 127 |
+
ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 131 |
+
if self.tile:
|
| 132 |
+
args = self.tile[0].args
|
| 133 |
+
assert isinstance(args, tuple)
|
| 134 |
+
compression, band = args
|
| 135 |
+
|
| 136 |
+
assert self.fp is not None
|
| 137 |
+
self.fp.seek(self.tile[0].offset)
|
| 138 |
+
|
| 139 |
+
# Copy image data to temporary file
|
| 140 |
+
o = BytesIO()
|
| 141 |
+
if compression == "raw":
|
| 142 |
+
# To simplify access to the extracted file,
|
| 143 |
+
# prepend a PPM header
|
| 144 |
+
o.write(b"P5\n%d %d\n255\n" % self.size)
|
| 145 |
+
while True:
|
| 146 |
+
type, size = self.field()
|
| 147 |
+
if type != (8, 10):
|
| 148 |
+
break
|
| 149 |
+
while size > 0:
|
| 150 |
+
s = self.fp.read(min(size, 8192))
|
| 151 |
+
if not s:
|
| 152 |
+
break
|
| 153 |
+
o.write(s)
|
| 154 |
+
size -= len(s)
|
| 155 |
+
|
| 156 |
+
with Image.open(o) as _im:
|
| 157 |
+
if band is not None:
|
| 158 |
+
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
|
| 159 |
+
bands[band] = _im
|
| 160 |
+
im = Image.merge(self.mode, bands)
|
| 161 |
+
else:
|
| 162 |
+
im = _im
|
| 163 |
+
im.load()
|
| 164 |
+
self.im = im.im
|
| 165 |
+
self.tile = []
|
| 166 |
+
return ImageFile.ImageFile.load(self)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
Image.register_open(IptcImageFile.format, IptcImageFile)
|
| 170 |
+
|
| 171 |
+
Image.register_extension(IptcImageFile.format, ".iim")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def getiptcinfo(
|
| 175 |
+
im: ImageFile.ImageFile,
|
| 176 |
+
) -> dict[tuple[int, int], bytes | list[bytes]] | None:
|
| 177 |
+
"""
|
| 178 |
+
Get IPTC information from TIFF, JPEG, or IPTC file.
|
| 179 |
+
|
| 180 |
+
:param im: An image containing IPTC data.
|
| 181 |
+
:returns: A dictionary containing IPTC information, or None if
|
| 182 |
+
no IPTC information block was found.
|
| 183 |
+
"""
|
| 184 |
+
from . import JpegImagePlugin, TiffImagePlugin
|
| 185 |
+
|
| 186 |
+
data = None
|
| 187 |
+
|
| 188 |
+
info: dict[tuple[int, int], bytes | list[bytes]] = {}
|
| 189 |
+
if isinstance(im, IptcImageFile):
|
| 190 |
+
# return info dictionary right away
|
| 191 |
+
for k, v in im.info.items():
|
| 192 |
+
if isinstance(k, tuple):
|
| 193 |
+
info[k] = v
|
| 194 |
+
return info
|
| 195 |
+
|
| 196 |
+
elif isinstance(im, JpegImagePlugin.JpegImageFile):
|
| 197 |
+
# extract the IPTC/NAA resource
|
| 198 |
+
photoshop = im.info.get("photoshop")
|
| 199 |
+
if photoshop:
|
| 200 |
+
data = photoshop.get(0x0404)
|
| 201 |
+
|
| 202 |
+
elif isinstance(im, TiffImagePlugin.TiffImageFile):
|
| 203 |
+
# get raw data from the IPTC/NAA tag (PhotoShop tags the data
|
| 204 |
+
# as 4-byte integers, so we cannot use the get method...)
|
| 205 |
+
try:
|
| 206 |
+
data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
|
| 207 |
+
except KeyError:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
if data is None:
|
| 211 |
+
return None # no properties
|
| 212 |
+
|
| 213 |
+
# create an IptcImagePlugin object without initializing it
|
| 214 |
+
class FakeImage:
|
| 215 |
+
pass
|
| 216 |
+
|
| 217 |
+
fake_im = FakeImage()
|
| 218 |
+
fake_im.__class__ = IptcImageFile # type: ignore[assignment]
|
| 219 |
+
iptc_im = cast(IptcImageFile, fake_im)
|
| 220 |
+
|
| 221 |
+
# parse the IPTC information chunk
|
| 222 |
+
iptc_im.info = {}
|
| 223 |
+
iptc_im.fp = BytesIO(data)
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
iptc_im._open()
|
| 227 |
+
except (IndexError, KeyError):
|
| 228 |
+
pass # expected failure
|
| 229 |
+
|
| 230 |
+
for k, v in iptc_im.info.items():
|
| 231 |
+
if isinstance(k, tuple):
|
| 232 |
+
info[k] = v
|
| 233 |
+
return info
|
PIL/Jpeg2KImagePlugin.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# JPEG2000 file handling
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 2014-03-12 ajh Created
|
| 9 |
+
# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
|
| 10 |
+
#
|
| 11 |
+
# Copyright (c) 2014 Coriolis Systems Limited
|
| 12 |
+
# Copyright (c) 2014 Alastair Houghton
|
| 13 |
+
#
|
| 14 |
+
# See the README file for information on usage and redistribution.
|
| 15 |
+
#
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import io
|
| 19 |
+
import os
|
| 20 |
+
import struct
|
| 21 |
+
from typing import cast
|
| 22 |
+
|
| 23 |
+
from . import Image, ImageFile, ImagePalette, _binary
|
| 24 |
+
|
| 25 |
+
TYPE_CHECKING = False
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from collections.abc import Callable
|
| 28 |
+
from typing import IO
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class BoxReader:
|
| 32 |
+
"""
|
| 33 |
+
A small helper class to read fields stored in JPEG2000 header boxes
|
| 34 |
+
and to easily step into and read sub-boxes.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self, fp: IO[bytes], length: int = -1) -> None:
|
| 38 |
+
self.fp = fp
|
| 39 |
+
self.has_length = length >= 0
|
| 40 |
+
self.length = length
|
| 41 |
+
self.remaining_in_box = -1
|
| 42 |
+
|
| 43 |
+
def _can_read(self, num_bytes: int) -> bool:
|
| 44 |
+
if self.has_length and self.fp.tell() + num_bytes > self.length:
|
| 45 |
+
# Outside box: ensure we don't read past the known file length
|
| 46 |
+
return False
|
| 47 |
+
if self.remaining_in_box >= 0:
|
| 48 |
+
# Inside box contents: ensure read does not go past box boundaries
|
| 49 |
+
return num_bytes <= self.remaining_in_box
|
| 50 |
+
else:
|
| 51 |
+
return True # No length known, just read
|
| 52 |
+
|
| 53 |
+
def _read_bytes(self, num_bytes: int) -> bytes:
|
| 54 |
+
if not self._can_read(num_bytes):
|
| 55 |
+
msg = "Not enough data in header"
|
| 56 |
+
raise SyntaxError(msg)
|
| 57 |
+
|
| 58 |
+
data = self.fp.read(num_bytes)
|
| 59 |
+
if len(data) < num_bytes:
|
| 60 |
+
msg = f"Expected to read {num_bytes} bytes but only got {len(data)}."
|
| 61 |
+
raise OSError(msg)
|
| 62 |
+
|
| 63 |
+
if self.remaining_in_box > 0:
|
| 64 |
+
self.remaining_in_box -= num_bytes
|
| 65 |
+
return data
|
| 66 |
+
|
| 67 |
+
def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
|
| 68 |
+
size = struct.calcsize(field_format)
|
| 69 |
+
data = self._read_bytes(size)
|
| 70 |
+
return struct.unpack(field_format, data)
|
| 71 |
+
|
| 72 |
+
def read_boxes(self) -> BoxReader:
|
| 73 |
+
size = self.remaining_in_box
|
| 74 |
+
data = self._read_bytes(size)
|
| 75 |
+
return BoxReader(io.BytesIO(data), size)
|
| 76 |
+
|
| 77 |
+
def has_next_box(self) -> bool:
|
| 78 |
+
if self.has_length:
|
| 79 |
+
return self.fp.tell() + self.remaining_in_box < self.length
|
| 80 |
+
else:
|
| 81 |
+
return True
|
| 82 |
+
|
| 83 |
+
def next_box_type(self) -> bytes:
|
| 84 |
+
# Skip the rest of the box if it has not been read
|
| 85 |
+
if self.remaining_in_box > 0:
|
| 86 |
+
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
|
| 87 |
+
self.remaining_in_box = -1
|
| 88 |
+
|
| 89 |
+
# Read the length and type of the next box
|
| 90 |
+
lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s"))
|
| 91 |
+
if lbox == 1:
|
| 92 |
+
lbox = cast(int, self.read_fields(">Q")[0])
|
| 93 |
+
hlen = 16
|
| 94 |
+
else:
|
| 95 |
+
hlen = 8
|
| 96 |
+
|
| 97 |
+
if lbox < hlen or not self._can_read(lbox - hlen):
|
| 98 |
+
msg = "Invalid header length"
|
| 99 |
+
raise SyntaxError(msg)
|
| 100 |
+
|
| 101 |
+
self.remaining_in_box = lbox - hlen
|
| 102 |
+
return tbox
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
|
| 106 |
+
"""Parse the JPEG 2000 codestream to extract the size and component
|
| 107 |
+
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
| 108 |
+
|
| 109 |
+
hdr = fp.read(2)
|
| 110 |
+
lsiz = _binary.i16be(hdr)
|
| 111 |
+
siz = hdr + fp.read(lsiz - 2)
|
| 112 |
+
lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
|
| 113 |
+
">HHIIIIIIIIH", siz
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
size = (xsiz - xosiz, ysiz - yosiz)
|
| 117 |
+
if csiz == 1:
|
| 118 |
+
ssiz = struct.unpack_from(">B", siz, 38)
|
| 119 |
+
if (ssiz[0] & 0x7F) + 1 > 8:
|
| 120 |
+
mode = "I;16"
|
| 121 |
+
else:
|
| 122 |
+
mode = "L"
|
| 123 |
+
elif csiz == 2:
|
| 124 |
+
mode = "LA"
|
| 125 |
+
elif csiz == 3:
|
| 126 |
+
mode = "RGB"
|
| 127 |
+
elif csiz == 4:
|
| 128 |
+
mode = "RGBA"
|
| 129 |
+
else:
|
| 130 |
+
msg = "unable to determine J2K image mode"
|
| 131 |
+
raise SyntaxError(msg)
|
| 132 |
+
|
| 133 |
+
return size, mode
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
|
| 137 |
+
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
|
| 138 |
+
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
| 139 |
+
to floating-point dots per inch."""
|
| 140 |
+
if denom == 0:
|
| 141 |
+
return None
|
| 142 |
+
return (254 * num * (10**exp)) / (10000 * denom)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _parse_jp2_header(
|
| 146 |
+
fp: IO[bytes],
|
| 147 |
+
) -> tuple[
|
| 148 |
+
tuple[int, int],
|
| 149 |
+
str,
|
| 150 |
+
str | None,
|
| 151 |
+
tuple[float, float] | None,
|
| 152 |
+
ImagePalette.ImagePalette | None,
|
| 153 |
+
]:
|
| 154 |
+
"""Parse the JP2 header box to extract size, component count,
|
| 155 |
+
color space information, and optionally DPI information,
|
| 156 |
+
returning a (size, mode, mimetype, dpi) tuple."""
|
| 157 |
+
|
| 158 |
+
# Find the JP2 header box
|
| 159 |
+
reader = BoxReader(fp)
|
| 160 |
+
header = None
|
| 161 |
+
mimetype = None
|
| 162 |
+
while reader.has_next_box():
|
| 163 |
+
tbox = reader.next_box_type()
|
| 164 |
+
|
| 165 |
+
if tbox == b"jp2h":
|
| 166 |
+
header = reader.read_boxes()
|
| 167 |
+
break
|
| 168 |
+
elif tbox == b"ftyp":
|
| 169 |
+
if reader.read_fields(">4s")[0] == b"jpx ":
|
| 170 |
+
mimetype = "image/jpx"
|
| 171 |
+
assert header is not None
|
| 172 |
+
|
| 173 |
+
size = None
|
| 174 |
+
mode = None
|
| 175 |
+
bpc = None
|
| 176 |
+
nc = None
|
| 177 |
+
dpi = None # 2-tuple of DPI info, or None
|
| 178 |
+
palette = None
|
| 179 |
+
|
| 180 |
+
while header.has_next_box():
|
| 181 |
+
tbox = header.next_box_type()
|
| 182 |
+
|
| 183 |
+
if tbox == b"ihdr":
|
| 184 |
+
height, width, nc, bpc = header.read_fields(">IIHB")
|
| 185 |
+
assert isinstance(height, int)
|
| 186 |
+
assert isinstance(width, int)
|
| 187 |
+
assert isinstance(bpc, int)
|
| 188 |
+
size = (width, height)
|
| 189 |
+
if nc == 1 and (bpc & 0x7F) > 8:
|
| 190 |
+
mode = "I;16"
|
| 191 |
+
elif nc == 1:
|
| 192 |
+
mode = "L"
|
| 193 |
+
elif nc == 2:
|
| 194 |
+
mode = "LA"
|
| 195 |
+
elif nc == 3:
|
| 196 |
+
mode = "RGB"
|
| 197 |
+
elif nc == 4:
|
| 198 |
+
mode = "RGBA"
|
| 199 |
+
elif tbox == b"colr" and nc == 4:
|
| 200 |
+
meth, _, _, enumcs = header.read_fields(">BBBI")
|
| 201 |
+
if meth == 1 and enumcs == 12:
|
| 202 |
+
mode = "CMYK"
|
| 203 |
+
elif tbox == b"pclr" and mode in ("L", "LA"):
|
| 204 |
+
ne, npc = header.read_fields(">HB")
|
| 205 |
+
assert isinstance(ne, int)
|
| 206 |
+
assert isinstance(npc, int)
|
| 207 |
+
max_bitdepth = 0
|
| 208 |
+
for bitdepth in header.read_fields(">" + ("B" * npc)):
|
| 209 |
+
assert isinstance(bitdepth, int)
|
| 210 |
+
if bitdepth > max_bitdepth:
|
| 211 |
+
max_bitdepth = bitdepth
|
| 212 |
+
if max_bitdepth <= 8:
|
| 213 |
+
palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB")
|
| 214 |
+
for i in range(ne):
|
| 215 |
+
color: list[int] = []
|
| 216 |
+
for value in header.read_fields(">" + ("B" * npc)):
|
| 217 |
+
assert isinstance(value, int)
|
| 218 |
+
color.append(value)
|
| 219 |
+
palette.getcolor(tuple(color))
|
| 220 |
+
mode = "P" if mode == "L" else "PA"
|
| 221 |
+
elif tbox == b"res ":
|
| 222 |
+
res = header.read_boxes()
|
| 223 |
+
while res.has_next_box():
|
| 224 |
+
tres = res.next_box_type()
|
| 225 |
+
if tres == b"resc":
|
| 226 |
+
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
|
| 227 |
+
assert isinstance(vrcn, int)
|
| 228 |
+
assert isinstance(vrcd, int)
|
| 229 |
+
assert isinstance(hrcn, int)
|
| 230 |
+
assert isinstance(hrcd, int)
|
| 231 |
+
assert isinstance(vrce, int)
|
| 232 |
+
assert isinstance(hrce, int)
|
| 233 |
+
hres = _res_to_dpi(hrcn, hrcd, hrce)
|
| 234 |
+
vres = _res_to_dpi(vrcn, vrcd, vrce)
|
| 235 |
+
if hres is not None and vres is not None:
|
| 236 |
+
dpi = (hres, vres)
|
| 237 |
+
break
|
| 238 |
+
|
| 239 |
+
if size is None or mode is None:
|
| 240 |
+
msg = "Malformed JP2 header"
|
| 241 |
+
raise SyntaxError(msg)
|
| 242 |
+
|
| 243 |
+
return size, mode, mimetype, dpi, palette
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
##
|
| 247 |
+
# Image plugin for JPEG2000 images.
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
class Jpeg2KImageFile(ImageFile.ImageFile):
|
| 251 |
+
format = "JPEG2000"
|
| 252 |
+
format_description = "JPEG 2000 (ISO 15444)"
|
| 253 |
+
|
| 254 |
+
def _open(self) -> None:
|
| 255 |
+
assert self.fp is not None
|
| 256 |
+
sig = self.fp.read(4)
|
| 257 |
+
if sig == b"\xff\x4f\xff\x51":
|
| 258 |
+
self.codec = "j2k"
|
| 259 |
+
self._size, self._mode = _parse_codestream(self.fp)
|
| 260 |
+
self._parse_comment()
|
| 261 |
+
else:
|
| 262 |
+
sig = sig + self.fp.read(8)
|
| 263 |
+
|
| 264 |
+
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
|
| 265 |
+
self.codec = "jp2"
|
| 266 |
+
header = _parse_jp2_header(self.fp)
|
| 267 |
+
self._size, self._mode, self.custom_mimetype, dpi, self.palette = header
|
| 268 |
+
if dpi is not None:
|
| 269 |
+
self.info["dpi"] = dpi
|
| 270 |
+
if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
|
| 271 |
+
hdr = self.fp.read(2)
|
| 272 |
+
length = _binary.i16be(hdr)
|
| 273 |
+
self.fp.seek(length - 2, os.SEEK_CUR)
|
| 274 |
+
self._parse_comment()
|
| 275 |
+
else:
|
| 276 |
+
msg = "not a JPEG 2000 file"
|
| 277 |
+
raise SyntaxError(msg)
|
| 278 |
+
|
| 279 |
+
self._reduce = 0
|
| 280 |
+
self.layers = 0
|
| 281 |
+
|
| 282 |
+
fd = -1
|
| 283 |
+
length = -1
|
| 284 |
+
|
| 285 |
+
try:
|
| 286 |
+
fd = self.fp.fileno()
|
| 287 |
+
length = os.fstat(fd).st_size
|
| 288 |
+
except Exception:
|
| 289 |
+
fd = -1
|
| 290 |
+
try:
|
| 291 |
+
pos = self.fp.tell()
|
| 292 |
+
self.fp.seek(0, io.SEEK_END)
|
| 293 |
+
length = self.fp.tell()
|
| 294 |
+
self.fp.seek(pos)
|
| 295 |
+
except Exception:
|
| 296 |
+
length = -1
|
| 297 |
+
|
| 298 |
+
self.tile = [
|
| 299 |
+
ImageFile._Tile(
|
| 300 |
+
"jpeg2k",
|
| 301 |
+
(0, 0) + self.size,
|
| 302 |
+
0,
|
| 303 |
+
(self.codec, self._reduce, self.layers, fd, length),
|
| 304 |
+
)
|
| 305 |
+
]
|
| 306 |
+
|
| 307 |
+
def _parse_comment(self) -> None:
|
| 308 |
+
assert self.fp is not None
|
| 309 |
+
while True:
|
| 310 |
+
marker = self.fp.read(2)
|
| 311 |
+
if not marker:
|
| 312 |
+
break
|
| 313 |
+
typ = marker[1]
|
| 314 |
+
if typ in (0x90, 0xD9):
|
| 315 |
+
# Start of tile or end of codestream
|
| 316 |
+
break
|
| 317 |
+
hdr = self.fp.read(2)
|
| 318 |
+
length = _binary.i16be(hdr)
|
| 319 |
+
if typ == 0x64:
|
| 320 |
+
# Comment
|
| 321 |
+
self.info["comment"] = self.fp.read(length - 2)[2:]
|
| 322 |
+
break
|
| 323 |
+
else:
|
| 324 |
+
self.fp.seek(length - 2, os.SEEK_CUR)
|
| 325 |
+
|
| 326 |
+
@property # type: ignore[override]
|
| 327 |
+
def reduce(
|
| 328 |
+
self,
|
| 329 |
+
) -> (
|
| 330 |
+
Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image]
|
| 331 |
+
| int
|
| 332 |
+
):
|
| 333 |
+
# https://github.com/python-pillow/Pillow/issues/4343 found that the
|
| 334 |
+
# new Image 'reduce' method was shadowed by this plugin's 'reduce'
|
| 335 |
+
# property. This attempts to allow for both scenarios
|
| 336 |
+
return self._reduce or super().reduce
|
| 337 |
+
|
| 338 |
+
@reduce.setter
|
| 339 |
+
def reduce(self, value: int) -> None:
|
| 340 |
+
self._reduce = value
|
| 341 |
+
|
| 342 |
+
def load(self) -> Image.core.PixelAccess | None:
|
| 343 |
+
if self.tile and self._reduce:
|
| 344 |
+
power = 1 << self._reduce
|
| 345 |
+
adjust = power >> 1
|
| 346 |
+
self._size = (
|
| 347 |
+
int((self.size[0] + adjust) / power),
|
| 348 |
+
int((self.size[1] + adjust) / power),
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Update the reduce and layers settings
|
| 352 |
+
t = self.tile[0]
|
| 353 |
+
assert isinstance(t[3], tuple)
|
| 354 |
+
t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
|
| 355 |
+
self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
|
| 356 |
+
|
| 357 |
+
return ImageFile.ImageFile.load(self)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def _accept(prefix: bytes) -> bool:
|
| 361 |
+
return prefix.startswith(
|
| 362 |
+
(b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a")
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
# ------------------------------------------------------------
|
| 367 |
+
# Save support
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 371 |
+
# Get the keyword arguments
|
| 372 |
+
info = im.encoderinfo
|
| 373 |
+
|
| 374 |
+
if isinstance(filename, str):
|
| 375 |
+
filename = filename.encode()
|
| 376 |
+
if filename.endswith(b".j2k") or info.get("no_jp2", False):
|
| 377 |
+
kind = "j2k"
|
| 378 |
+
else:
|
| 379 |
+
kind = "jp2"
|
| 380 |
+
|
| 381 |
+
offset = info.get("offset", None)
|
| 382 |
+
tile_offset = info.get("tile_offset", None)
|
| 383 |
+
tile_size = info.get("tile_size", None)
|
| 384 |
+
quality_mode = info.get("quality_mode", "rates")
|
| 385 |
+
quality_layers = info.get("quality_layers", None)
|
| 386 |
+
if quality_layers is not None and not (
|
| 387 |
+
isinstance(quality_layers, (list, tuple))
|
| 388 |
+
and all(
|
| 389 |
+
isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
|
| 390 |
+
)
|
| 391 |
+
):
|
| 392 |
+
msg = "quality_layers must be a sequence of numbers"
|
| 393 |
+
raise ValueError(msg)
|
| 394 |
+
|
| 395 |
+
num_resolutions = info.get("num_resolutions", 0)
|
| 396 |
+
cblk_size = info.get("codeblock_size", None)
|
| 397 |
+
precinct_size = info.get("precinct_size", None)
|
| 398 |
+
irreversible = info.get("irreversible", False)
|
| 399 |
+
progression = info.get("progression", "LRCP")
|
| 400 |
+
cinema_mode = info.get("cinema_mode", "no")
|
| 401 |
+
mct = info.get("mct", 0)
|
| 402 |
+
signed = info.get("signed", False)
|
| 403 |
+
comment = info.get("comment")
|
| 404 |
+
if isinstance(comment, str):
|
| 405 |
+
comment = comment.encode()
|
| 406 |
+
plt = info.get("plt", False)
|
| 407 |
+
|
| 408 |
+
fd = -1
|
| 409 |
+
if hasattr(fp, "fileno"):
|
| 410 |
+
try:
|
| 411 |
+
fd = fp.fileno()
|
| 412 |
+
except Exception:
|
| 413 |
+
fd = -1
|
| 414 |
+
|
| 415 |
+
im.encoderconfig = (
|
| 416 |
+
offset,
|
| 417 |
+
tile_offset,
|
| 418 |
+
tile_size,
|
| 419 |
+
quality_mode,
|
| 420 |
+
quality_layers,
|
| 421 |
+
num_resolutions,
|
| 422 |
+
cblk_size,
|
| 423 |
+
precinct_size,
|
| 424 |
+
irreversible,
|
| 425 |
+
progression,
|
| 426 |
+
cinema_mode,
|
| 427 |
+
mct,
|
| 428 |
+
signed,
|
| 429 |
+
fd,
|
| 430 |
+
comment,
|
| 431 |
+
plt,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)])
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
# ------------------------------------------------------------
|
| 438 |
+
# Registry stuff
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept)
|
| 442 |
+
Image.register_save(Jpeg2KImageFile.format, _save)
|
| 443 |
+
|
| 444 |
+
Image.register_extensions(
|
| 445 |
+
Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
Image.register_mime(Jpeg2KImageFile.format, "image/jp2")
|
PIL/JpegImagePlugin.py
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# JPEG (JFIF) file handling
|
| 6 |
+
#
|
| 7 |
+
# See "Digital Compression and Coding of Continuous-Tone Still Images,
|
| 8 |
+
# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1)
|
| 9 |
+
#
|
| 10 |
+
# History:
|
| 11 |
+
# 1995-09-09 fl Created
|
| 12 |
+
# 1995-09-13 fl Added full parser
|
| 13 |
+
# 1996-03-25 fl Added hack to use the IJG command line utilities
|
| 14 |
+
# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug
|
| 15 |
+
# 1996-05-28 fl Added draft support, JFIF version (0.1)
|
| 16 |
+
# 1996-12-30 fl Added encoder options, added progression property (0.2)
|
| 17 |
+
# 1997-08-27 fl Save mode 1 images as BW (0.3)
|
| 18 |
+
# 1998-07-12 fl Added YCbCr to draft and save methods (0.4)
|
| 19 |
+
# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1)
|
| 20 |
+
# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2)
|
| 21 |
+
# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3)
|
| 22 |
+
# 2003-04-25 fl Added experimental EXIF decoder (0.5)
|
| 23 |
+
# 2003-06-06 fl Added experimental EXIF GPSinfo decoder
|
| 24 |
+
# 2003-09-13 fl Extract COM markers
|
| 25 |
+
# 2009-09-06 fl Added icc_profile support (from Florian Hoech)
|
| 26 |
+
# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6)
|
| 27 |
+
# 2009-03-08 fl Added subsampling support (from Justin Huff).
|
| 28 |
+
#
|
| 29 |
+
# Copyright (c) 1997-2003 by Secret Labs AB.
|
| 30 |
+
# Copyright (c) 1995-1996 by Fredrik Lundh.
|
| 31 |
+
#
|
| 32 |
+
# See the README file for information on usage and redistribution.
|
| 33 |
+
#
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import array
|
| 37 |
+
import io
|
| 38 |
+
import math
|
| 39 |
+
import os
|
| 40 |
+
import struct
|
| 41 |
+
import subprocess
|
| 42 |
+
import sys
|
| 43 |
+
import tempfile
|
| 44 |
+
import warnings
|
| 45 |
+
|
| 46 |
+
from . import Image, ImageFile
|
| 47 |
+
from ._binary import i16be as i16
|
| 48 |
+
from ._binary import i32be as i32
|
| 49 |
+
from ._binary import o8
|
| 50 |
+
from ._binary import o16be as o16
|
| 51 |
+
from .JpegPresets import presets
|
| 52 |
+
|
| 53 |
+
TYPE_CHECKING = False
|
| 54 |
+
if TYPE_CHECKING:
|
| 55 |
+
from typing import IO, Any
|
| 56 |
+
|
| 57 |
+
from .MpoImagePlugin import MpoImageFile
|
| 58 |
+
|
| 59 |
+
#
|
| 60 |
+
# Parser
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def Skip(self: JpegImageFile, marker: int) -> None:
|
| 64 |
+
assert self.fp is not None
|
| 65 |
+
n = i16(self.fp.read(2)) - 2
|
| 66 |
+
ImageFile._safe_read(self.fp, n)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def APP(self: JpegImageFile, marker: int) -> None:
|
| 70 |
+
#
|
| 71 |
+
# Application marker. Store these in the APP dictionary.
|
| 72 |
+
# Also look for well-known application markers.
|
| 73 |
+
|
| 74 |
+
assert self.fp is not None
|
| 75 |
+
n = i16(self.fp.read(2)) - 2
|
| 76 |
+
s = ImageFile._safe_read(self.fp, n)
|
| 77 |
+
|
| 78 |
+
app = f"APP{marker & 15}"
|
| 79 |
+
|
| 80 |
+
self.app[app] = s # compatibility
|
| 81 |
+
self.applist.append((app, s))
|
| 82 |
+
|
| 83 |
+
if marker == 0xFFE0 and s.startswith(b"JFIF"):
|
| 84 |
+
# extract JFIF information
|
| 85 |
+
self.info["jfif"] = version = i16(s, 5) # version
|
| 86 |
+
self.info["jfif_version"] = divmod(version, 256)
|
| 87 |
+
# extract JFIF properties
|
| 88 |
+
try:
|
| 89 |
+
jfif_unit = s[7]
|
| 90 |
+
jfif_density = i16(s, 8), i16(s, 10)
|
| 91 |
+
except Exception:
|
| 92 |
+
pass
|
| 93 |
+
else:
|
| 94 |
+
if jfif_unit == 1:
|
| 95 |
+
self.info["dpi"] = jfif_density
|
| 96 |
+
elif jfif_unit == 2: # cm
|
| 97 |
+
# 1 dpcm = 2.54 dpi
|
| 98 |
+
self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
|
| 99 |
+
self.info["jfif_unit"] = jfif_unit
|
| 100 |
+
self.info["jfif_density"] = jfif_density
|
| 101 |
+
elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"):
|
| 102 |
+
# extract EXIF information
|
| 103 |
+
if "exif" in self.info:
|
| 104 |
+
self.info["exif"] += s[6:]
|
| 105 |
+
else:
|
| 106 |
+
self.info["exif"] = s
|
| 107 |
+
self._exif_offset = self.fp.tell() - n + 6
|
| 108 |
+
elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"):
|
| 109 |
+
self.info["xmp"] = s.split(b"\x00", 1)[1]
|
| 110 |
+
elif marker == 0xFFE2 and s.startswith(b"FPXR\0"):
|
| 111 |
+
# extract FlashPix information (incomplete)
|
| 112 |
+
self.info["flashpix"] = s # FIXME: value will change
|
| 113 |
+
elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"):
|
| 114 |
+
# Since an ICC profile can be larger than the maximum size of
|
| 115 |
+
# a JPEG marker (64K), we need provisions to split it into
|
| 116 |
+
# multiple markers. The format defined by the ICC specifies
|
| 117 |
+
# one or more APP2 markers containing the following data:
|
| 118 |
+
# Identifying string ASCII "ICC_PROFILE\0" (12 bytes)
|
| 119 |
+
# Marker sequence number 1, 2, etc (1 byte)
|
| 120 |
+
# Number of markers Total of APP2's used (1 byte)
|
| 121 |
+
# Profile data (remainder of APP2 data)
|
| 122 |
+
# Decoders should use the marker sequence numbers to
|
| 123 |
+
# reassemble the profile, rather than assuming that the APP2
|
| 124 |
+
# markers appear in the correct sequence.
|
| 125 |
+
self.icclist.append(s)
|
| 126 |
+
elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"):
|
| 127 |
+
# parse the image resource block
|
| 128 |
+
offset = 14
|
| 129 |
+
photoshop = self.info.setdefault("photoshop", {})
|
| 130 |
+
while s[offset : offset + 4] == b"8BIM":
|
| 131 |
+
try:
|
| 132 |
+
offset += 4
|
| 133 |
+
# resource code
|
| 134 |
+
code = i16(s, offset)
|
| 135 |
+
offset += 2
|
| 136 |
+
# resource name (usually empty)
|
| 137 |
+
name_len = s[offset]
|
| 138 |
+
# name = s[offset+1:offset+1+name_len]
|
| 139 |
+
offset += 1 + name_len
|
| 140 |
+
offset += offset & 1 # align
|
| 141 |
+
# resource data block
|
| 142 |
+
size = i32(s, offset)
|
| 143 |
+
offset += 4
|
| 144 |
+
data = s[offset : offset + size]
|
| 145 |
+
if code == 0x03ED: # ResolutionInfo
|
| 146 |
+
photoshop[code] = {
|
| 147 |
+
"XResolution": i32(data, 0) / 65536,
|
| 148 |
+
"DisplayedUnitsX": i16(data, 4),
|
| 149 |
+
"YResolution": i32(data, 8) / 65536,
|
| 150 |
+
"DisplayedUnitsY": i16(data, 12),
|
| 151 |
+
}
|
| 152 |
+
else:
|
| 153 |
+
photoshop[code] = data
|
| 154 |
+
offset += size
|
| 155 |
+
offset += offset & 1 # align
|
| 156 |
+
except struct.error:
|
| 157 |
+
break # insufficient data
|
| 158 |
+
|
| 159 |
+
elif marker == 0xFFEE and s.startswith(b"Adobe"):
|
| 160 |
+
self.info["adobe"] = i16(s, 5)
|
| 161 |
+
# extract Adobe custom properties
|
| 162 |
+
try:
|
| 163 |
+
adobe_transform = s[11]
|
| 164 |
+
except IndexError:
|
| 165 |
+
pass
|
| 166 |
+
else:
|
| 167 |
+
self.info["adobe_transform"] = adobe_transform
|
| 168 |
+
elif marker == 0xFFE2 and s.startswith(b"MPF\0"):
|
| 169 |
+
# extract MPO information
|
| 170 |
+
self.info["mp"] = s[4:]
|
| 171 |
+
# offset is current location minus buffer size
|
| 172 |
+
# plus constant header size
|
| 173 |
+
self.info["mpoffset"] = self.fp.tell() - n + 4
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def COM(self: JpegImageFile, marker: int) -> None:
|
| 177 |
+
#
|
| 178 |
+
# Comment marker. Store these in the APP dictionary.
|
| 179 |
+
assert self.fp is not None
|
| 180 |
+
n = i16(self.fp.read(2)) - 2
|
| 181 |
+
s = ImageFile._safe_read(self.fp, n)
|
| 182 |
+
|
| 183 |
+
self.info["comment"] = s
|
| 184 |
+
self.app["COM"] = s # compatibility
|
| 185 |
+
self.applist.append(("COM", s))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def SOF(self: JpegImageFile, marker: int) -> None:
|
| 189 |
+
#
|
| 190 |
+
# Start of frame marker. Defines the size and mode of the
|
| 191 |
+
# image. JPEG is colour blind, so we use some simple
|
| 192 |
+
# heuristics to map the number of layers to an appropriate
|
| 193 |
+
# mode. Note that this could be made a bit brighter, by
|
| 194 |
+
# looking for JFIF and Adobe APP markers.
|
| 195 |
+
|
| 196 |
+
assert self.fp is not None
|
| 197 |
+
n = i16(self.fp.read(2)) - 2
|
| 198 |
+
s = ImageFile._safe_read(self.fp, n)
|
| 199 |
+
self._size = i16(s, 3), i16(s, 1)
|
| 200 |
+
if self._im is not None and self.size != self.im.size:
|
| 201 |
+
self._im = None
|
| 202 |
+
|
| 203 |
+
self.bits = s[0]
|
| 204 |
+
if self.bits != 8:
|
| 205 |
+
msg = f"cannot handle {self.bits}-bit layers"
|
| 206 |
+
raise SyntaxError(msg)
|
| 207 |
+
|
| 208 |
+
self.layers = s[5]
|
| 209 |
+
if self.layers == 1:
|
| 210 |
+
self._mode = "L"
|
| 211 |
+
elif self.layers == 3:
|
| 212 |
+
self._mode = "RGB"
|
| 213 |
+
elif self.layers == 4:
|
| 214 |
+
self._mode = "CMYK"
|
| 215 |
+
else:
|
| 216 |
+
msg = f"cannot handle {self.layers}-layer images"
|
| 217 |
+
raise SyntaxError(msg)
|
| 218 |
+
|
| 219 |
+
if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
|
| 220 |
+
self.info["progressive"] = self.info["progression"] = 1
|
| 221 |
+
|
| 222 |
+
if self.icclist:
|
| 223 |
+
# fixup icc profile
|
| 224 |
+
self.icclist.sort() # sort by sequence number
|
| 225 |
+
if self.icclist[0][13] == len(self.icclist):
|
| 226 |
+
profile = [p[14:] for p in self.icclist]
|
| 227 |
+
icc_profile = b"".join(profile)
|
| 228 |
+
else:
|
| 229 |
+
icc_profile = None # wrong number of fragments
|
| 230 |
+
self.info["icc_profile"] = icc_profile
|
| 231 |
+
self.icclist = []
|
| 232 |
+
|
| 233 |
+
for i in range(6, len(s), 3):
|
| 234 |
+
t = s[i : i + 3]
|
| 235 |
+
# 4-tuples: id, vsamp, hsamp, qtable
|
| 236 |
+
self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2]))
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def DQT(self: JpegImageFile, marker: int) -> None:
|
| 240 |
+
#
|
| 241 |
+
# Define quantization table. Note that there might be more
|
| 242 |
+
# than one table in each marker.
|
| 243 |
+
|
| 244 |
+
# FIXME: The quantization tables can be used to estimate the
|
| 245 |
+
# compression quality.
|
| 246 |
+
|
| 247 |
+
assert self.fp is not None
|
| 248 |
+
n = i16(self.fp.read(2)) - 2
|
| 249 |
+
s = ImageFile._safe_read(self.fp, n)
|
| 250 |
+
while len(s):
|
| 251 |
+
v = s[0]
|
| 252 |
+
precision = 1 if (v // 16 == 0) else 2 # in bytes
|
| 253 |
+
qt_length = 1 + precision * 64
|
| 254 |
+
if len(s) < qt_length:
|
| 255 |
+
msg = "bad quantization table marker"
|
| 256 |
+
raise SyntaxError(msg)
|
| 257 |
+
data = array.array("B" if precision == 1 else "H", s[1:qt_length])
|
| 258 |
+
if sys.byteorder == "little" and precision > 1:
|
| 259 |
+
data.byteswap() # the values are always big-endian
|
| 260 |
+
self.quantization[v & 15] = [data[i] for i in zigzag_index]
|
| 261 |
+
s = s[qt_length:]
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
#
|
| 265 |
+
# JPEG marker table
|
| 266 |
+
|
| 267 |
+
MARKER = {
|
| 268 |
+
0xFFC0: ("SOF0", "Baseline DCT", SOF),
|
| 269 |
+
0xFFC1: ("SOF1", "Extended Sequential DCT", SOF),
|
| 270 |
+
0xFFC2: ("SOF2", "Progressive DCT", SOF),
|
| 271 |
+
0xFFC3: ("SOF3", "Spatial lossless", SOF),
|
| 272 |
+
0xFFC4: ("DHT", "Define Huffman table", Skip),
|
| 273 |
+
0xFFC5: ("SOF5", "Differential sequential DCT", SOF),
|
| 274 |
+
0xFFC6: ("SOF6", "Differential progressive DCT", SOF),
|
| 275 |
+
0xFFC7: ("SOF7", "Differential spatial", SOF),
|
| 276 |
+
0xFFC8: ("JPG", "Extension", None),
|
| 277 |
+
0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF),
|
| 278 |
+
0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF),
|
| 279 |
+
0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF),
|
| 280 |
+
0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip),
|
| 281 |
+
0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF),
|
| 282 |
+
0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF),
|
| 283 |
+
0xFFCF: ("SOF15", "Differential spatial (AC)", SOF),
|
| 284 |
+
0xFFD0: ("RST0", "Restart 0", None),
|
| 285 |
+
0xFFD1: ("RST1", "Restart 1", None),
|
| 286 |
+
0xFFD2: ("RST2", "Restart 2", None),
|
| 287 |
+
0xFFD3: ("RST3", "Restart 3", None),
|
| 288 |
+
0xFFD4: ("RST4", "Restart 4", None),
|
| 289 |
+
0xFFD5: ("RST5", "Restart 5", None),
|
| 290 |
+
0xFFD6: ("RST6", "Restart 6", None),
|
| 291 |
+
0xFFD7: ("RST7", "Restart 7", None),
|
| 292 |
+
0xFFD8: ("SOI", "Start of image", None),
|
| 293 |
+
0xFFD9: ("EOI", "End of image", None),
|
| 294 |
+
0xFFDA: ("SOS", "Start of scan", Skip),
|
| 295 |
+
0xFFDB: ("DQT", "Define quantization table", DQT),
|
| 296 |
+
0xFFDC: ("DNL", "Define number of lines", Skip),
|
| 297 |
+
0xFFDD: ("DRI", "Define restart interval", Skip),
|
| 298 |
+
0xFFDE: ("DHP", "Define hierarchical progression", SOF),
|
| 299 |
+
0xFFDF: ("EXP", "Expand reference component", Skip),
|
| 300 |
+
0xFFE0: ("APP0", "Application segment 0", APP),
|
| 301 |
+
0xFFE1: ("APP1", "Application segment 1", APP),
|
| 302 |
+
0xFFE2: ("APP2", "Application segment 2", APP),
|
| 303 |
+
0xFFE3: ("APP3", "Application segment 3", APP),
|
| 304 |
+
0xFFE4: ("APP4", "Application segment 4", APP),
|
| 305 |
+
0xFFE5: ("APP5", "Application segment 5", APP),
|
| 306 |
+
0xFFE6: ("APP6", "Application segment 6", APP),
|
| 307 |
+
0xFFE7: ("APP7", "Application segment 7", APP),
|
| 308 |
+
0xFFE8: ("APP8", "Application segment 8", APP),
|
| 309 |
+
0xFFE9: ("APP9", "Application segment 9", APP),
|
| 310 |
+
0xFFEA: ("APP10", "Application segment 10", APP),
|
| 311 |
+
0xFFEB: ("APP11", "Application segment 11", APP),
|
| 312 |
+
0xFFEC: ("APP12", "Application segment 12", APP),
|
| 313 |
+
0xFFED: ("APP13", "Application segment 13", APP),
|
| 314 |
+
0xFFEE: ("APP14", "Application segment 14", APP),
|
| 315 |
+
0xFFEF: ("APP15", "Application segment 15", APP),
|
| 316 |
+
0xFFF0: ("JPG0", "Extension 0", None),
|
| 317 |
+
0xFFF1: ("JPG1", "Extension 1", None),
|
| 318 |
+
0xFFF2: ("JPG2", "Extension 2", None),
|
| 319 |
+
0xFFF3: ("JPG3", "Extension 3", None),
|
| 320 |
+
0xFFF4: ("JPG4", "Extension 4", None),
|
| 321 |
+
0xFFF5: ("JPG5", "Extension 5", None),
|
| 322 |
+
0xFFF6: ("JPG6", "Extension 6", None),
|
| 323 |
+
0xFFF7: ("JPG7", "Extension 7", None),
|
| 324 |
+
0xFFF8: ("JPG8", "Extension 8", None),
|
| 325 |
+
0xFFF9: ("JPG9", "Extension 9", None),
|
| 326 |
+
0xFFFA: ("JPG10", "Extension 10", None),
|
| 327 |
+
0xFFFB: ("JPG11", "Extension 11", None),
|
| 328 |
+
0xFFFC: ("JPG12", "Extension 12", None),
|
| 329 |
+
0xFFFD: ("JPG13", "Extension 13", None),
|
| 330 |
+
0xFFFE: ("COM", "Comment", COM),
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def _accept(prefix: bytes) -> bool:
|
| 335 |
+
# Magic number was taken from https://en.wikipedia.org/wiki/JPEG
|
| 336 |
+
return prefix.startswith(b"\xff\xd8\xff")
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
##
|
| 340 |
+
# Image plugin for JPEG and JFIF images.
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class JpegImageFile(ImageFile.ImageFile):
|
| 344 |
+
format = "JPEG"
|
| 345 |
+
format_description = "JPEG (ISO 10918)"
|
| 346 |
+
|
| 347 |
+
def _open(self) -> None:
|
| 348 |
+
assert self.fp is not None
|
| 349 |
+
s = self.fp.read(3)
|
| 350 |
+
|
| 351 |
+
if not _accept(s):
|
| 352 |
+
msg = "not a JPEG file"
|
| 353 |
+
raise SyntaxError(msg)
|
| 354 |
+
s = b"\xff"
|
| 355 |
+
|
| 356 |
+
# Create attributes
|
| 357 |
+
self.bits = self.layers = 0
|
| 358 |
+
self._exif_offset = 0
|
| 359 |
+
|
| 360 |
+
# JPEG specifics (internal)
|
| 361 |
+
self.layer: list[tuple[int, int, int, int]] = []
|
| 362 |
+
self._huffman_dc: dict[Any, Any] = {}
|
| 363 |
+
self._huffman_ac: dict[Any, Any] = {}
|
| 364 |
+
self.quantization: dict[int, list[int]] = {}
|
| 365 |
+
self.app: dict[str, bytes] = {} # compatibility
|
| 366 |
+
self.applist: list[tuple[str, bytes]] = []
|
| 367 |
+
self.icclist: list[bytes] = []
|
| 368 |
+
|
| 369 |
+
while True:
|
| 370 |
+
i = s[0]
|
| 371 |
+
if i == 0xFF:
|
| 372 |
+
s = s + self.fp.read(1)
|
| 373 |
+
i = i16(s)
|
| 374 |
+
else:
|
| 375 |
+
# Skip non-0xFF junk
|
| 376 |
+
s = self.fp.read(1)
|
| 377 |
+
continue
|
| 378 |
+
|
| 379 |
+
if i in MARKER:
|
| 380 |
+
name, description, handler = MARKER[i]
|
| 381 |
+
if handler is not None:
|
| 382 |
+
handler(self, i)
|
| 383 |
+
if i == 0xFFDA: # start of scan
|
| 384 |
+
rawmode = self.mode
|
| 385 |
+
if self.mode == "CMYK":
|
| 386 |
+
rawmode = "CMYK;I" # assume adobe conventions
|
| 387 |
+
self.tile = [
|
| 388 |
+
ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
|
| 389 |
+
]
|
| 390 |
+
# self.__offset = self.fp.tell()
|
| 391 |
+
break
|
| 392 |
+
s = self.fp.read(1)
|
| 393 |
+
elif i in {0, 0xFFFF}:
|
| 394 |
+
# padded marker or junk; move on
|
| 395 |
+
s = b"\xff"
|
| 396 |
+
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
| 397 |
+
s = self.fp.read(1)
|
| 398 |
+
else:
|
| 399 |
+
msg = "no marker found"
|
| 400 |
+
raise SyntaxError(msg)
|
| 401 |
+
|
| 402 |
+
self._read_dpi_from_exif()
|
| 403 |
+
|
| 404 |
+
def __getstate__(self) -> list[Any]:
|
| 405 |
+
return super().__getstate__() + [self.layers, self.layer]
|
| 406 |
+
|
| 407 |
+
def __setstate__(self, state: list[Any]) -> None:
|
| 408 |
+
self.layers, self.layer = state[6:]
|
| 409 |
+
super().__setstate__(state)
|
| 410 |
+
|
| 411 |
+
def load_read(self, read_bytes: int) -> bytes:
|
| 412 |
+
"""
|
| 413 |
+
internal: read more image data
|
| 414 |
+
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
|
| 415 |
+
so libjpeg can finish decoding
|
| 416 |
+
"""
|
| 417 |
+
assert self.fp is not None
|
| 418 |
+
s = self.fp.read(read_bytes)
|
| 419 |
+
|
| 420 |
+
if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"):
|
| 421 |
+
# Premature EOF.
|
| 422 |
+
# Pretend file is finished adding EOI marker
|
| 423 |
+
self._ended = True
|
| 424 |
+
return b"\xff\xd9"
|
| 425 |
+
|
| 426 |
+
return s
|
| 427 |
+
|
| 428 |
+
def draft(
|
| 429 |
+
self, mode: str | None, size: tuple[int, int] | None
|
| 430 |
+
) -> tuple[str, tuple[int, int, float, float]] | None:
|
| 431 |
+
if len(self.tile) != 1:
|
| 432 |
+
return None
|
| 433 |
+
|
| 434 |
+
# Protect from second call
|
| 435 |
+
if self.decoderconfig:
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
d, e, o, a = self.tile[0]
|
| 439 |
+
scale = 1
|
| 440 |
+
original_size = self.size
|
| 441 |
+
|
| 442 |
+
assert isinstance(a, tuple)
|
| 443 |
+
if a[0] == "RGB" and mode in ["L", "YCbCr"]:
|
| 444 |
+
self._mode = mode
|
| 445 |
+
a = mode, ""
|
| 446 |
+
|
| 447 |
+
if size:
|
| 448 |
+
scale = min(self.size[0] // size[0], self.size[1] // size[1])
|
| 449 |
+
for s in [8, 4, 2, 1]:
|
| 450 |
+
if scale >= s:
|
| 451 |
+
break
|
| 452 |
+
assert e is not None
|
| 453 |
+
e = (
|
| 454 |
+
e[0],
|
| 455 |
+
e[1],
|
| 456 |
+
(e[2] - e[0] + s - 1) // s + e[0],
|
| 457 |
+
(e[3] - e[1] + s - 1) // s + e[1],
|
| 458 |
+
)
|
| 459 |
+
self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
|
| 460 |
+
scale = s
|
| 461 |
+
|
| 462 |
+
self.tile = [ImageFile._Tile(d, e, o, a)]
|
| 463 |
+
self.decoderconfig = (scale, 0)
|
| 464 |
+
|
| 465 |
+
box = (0, 0, original_size[0] / scale, original_size[1] / scale)
|
| 466 |
+
return self.mode, box
|
| 467 |
+
|
| 468 |
+
def load_djpeg(self) -> None:
|
| 469 |
+
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
|
| 470 |
+
|
| 471 |
+
f, path = tempfile.mkstemp()
|
| 472 |
+
os.close(f)
|
| 473 |
+
if os.path.exists(self.filename):
|
| 474 |
+
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
|
| 475 |
+
else:
|
| 476 |
+
try:
|
| 477 |
+
os.unlink(path)
|
| 478 |
+
except OSError:
|
| 479 |
+
pass
|
| 480 |
+
|
| 481 |
+
msg = "Invalid Filename"
|
| 482 |
+
raise ValueError(msg)
|
| 483 |
+
|
| 484 |
+
try:
|
| 485 |
+
with Image.open(path) as _im:
|
| 486 |
+
_im.load()
|
| 487 |
+
self.im = _im.im
|
| 488 |
+
finally:
|
| 489 |
+
try:
|
| 490 |
+
os.unlink(path)
|
| 491 |
+
except OSError:
|
| 492 |
+
pass
|
| 493 |
+
|
| 494 |
+
self._mode = self.im.mode
|
| 495 |
+
self._size = self.im.size
|
| 496 |
+
|
| 497 |
+
self.tile = []
|
| 498 |
+
|
| 499 |
+
def _getexif(self) -> dict[int, Any] | None:
|
| 500 |
+
return _getexif(self)
|
| 501 |
+
|
| 502 |
+
def _read_dpi_from_exif(self) -> None:
|
| 503 |
+
# If DPI isn't in JPEG header, fetch from EXIF
|
| 504 |
+
if "dpi" in self.info or "exif" not in self.info:
|
| 505 |
+
return
|
| 506 |
+
try:
|
| 507 |
+
exif = self.getexif()
|
| 508 |
+
resolution_unit = exif[0x0128]
|
| 509 |
+
x_resolution = exif[0x011A]
|
| 510 |
+
try:
|
| 511 |
+
dpi = float(x_resolution[0]) / x_resolution[1]
|
| 512 |
+
except TypeError:
|
| 513 |
+
dpi = x_resolution
|
| 514 |
+
if math.isnan(dpi):
|
| 515 |
+
msg = "DPI is not a number"
|
| 516 |
+
raise ValueError(msg)
|
| 517 |
+
if resolution_unit == 3: # cm
|
| 518 |
+
# 1 dpcm = 2.54 dpi
|
| 519 |
+
dpi *= 2.54
|
| 520 |
+
self.info["dpi"] = dpi, dpi
|
| 521 |
+
except (
|
| 522 |
+
struct.error, # truncated EXIF
|
| 523 |
+
KeyError, # dpi not included
|
| 524 |
+
SyntaxError, # invalid/unreadable EXIF
|
| 525 |
+
TypeError, # dpi is an invalid float
|
| 526 |
+
ValueError, # dpi is an invalid float
|
| 527 |
+
ZeroDivisionError, # invalid dpi rational value
|
| 528 |
+
):
|
| 529 |
+
self.info["dpi"] = 72, 72
|
| 530 |
+
|
| 531 |
+
def _getmp(self) -> dict[int, Any] | None:
|
| 532 |
+
return _getmp(self)
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def _getexif(self: JpegImageFile) -> dict[int, Any] | None:
|
| 536 |
+
if "exif" not in self.info:
|
| 537 |
+
return None
|
| 538 |
+
return self.getexif()._get_merged_dict()
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
|
| 542 |
+
# Extract MP information. This method was inspired by the "highly
|
| 543 |
+
# experimental" _getexif version that's been in use for years now,
|
| 544 |
+
# itself based on the ImageFileDirectory class in the TIFF plugin.
|
| 545 |
+
|
| 546 |
+
# The MP record essentially consists of a TIFF file embedded in a JPEG
|
| 547 |
+
# application marker.
|
| 548 |
+
try:
|
| 549 |
+
data = self.info["mp"]
|
| 550 |
+
except KeyError:
|
| 551 |
+
return None
|
| 552 |
+
file_contents = io.BytesIO(data)
|
| 553 |
+
head = file_contents.read(8)
|
| 554 |
+
endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<"
|
| 555 |
+
# process dictionary
|
| 556 |
+
from . import TiffImagePlugin
|
| 557 |
+
|
| 558 |
+
try:
|
| 559 |
+
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
| 560 |
+
file_contents.seek(info.next)
|
| 561 |
+
info.load(file_contents)
|
| 562 |
+
mp = dict(info)
|
| 563 |
+
except Exception as e:
|
| 564 |
+
msg = "malformed MP Index (unreadable directory)"
|
| 565 |
+
raise SyntaxError(msg) from e
|
| 566 |
+
# it's an error not to have a number of images
|
| 567 |
+
try:
|
| 568 |
+
quant = mp[0xB001]
|
| 569 |
+
except KeyError as e:
|
| 570 |
+
msg = "malformed MP Index (no number of images)"
|
| 571 |
+
raise SyntaxError(msg) from e
|
| 572 |
+
# get MP entries
|
| 573 |
+
mpentries = []
|
| 574 |
+
try:
|
| 575 |
+
rawmpentries = mp[0xB002]
|
| 576 |
+
for entrynum in range(quant):
|
| 577 |
+
unpackedentry = struct.unpack_from(
|
| 578 |
+
f"{endianness}LLLHH", rawmpentries, entrynum * 16
|
| 579 |
+
)
|
| 580 |
+
labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2")
|
| 581 |
+
mpentry = dict(zip(labels, unpackedentry))
|
| 582 |
+
mpentryattr = {
|
| 583 |
+
"DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)),
|
| 584 |
+
"DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)),
|
| 585 |
+
"RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)),
|
| 586 |
+
"Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27,
|
| 587 |
+
"ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24,
|
| 588 |
+
"MPType": mpentry["Attribute"] & 0x00FFFFFF,
|
| 589 |
+
}
|
| 590 |
+
if mpentryattr["ImageDataFormat"] == 0:
|
| 591 |
+
mpentryattr["ImageDataFormat"] = "JPEG"
|
| 592 |
+
else:
|
| 593 |
+
msg = "unsupported picture format in MPO"
|
| 594 |
+
raise SyntaxError(msg)
|
| 595 |
+
mptypemap = {
|
| 596 |
+
0x000000: "Undefined",
|
| 597 |
+
0x010001: "Large Thumbnail (VGA Equivalent)",
|
| 598 |
+
0x010002: "Large Thumbnail (Full HD Equivalent)",
|
| 599 |
+
0x020001: "Multi-Frame Image (Panorama)",
|
| 600 |
+
0x020002: "Multi-Frame Image: (Disparity)",
|
| 601 |
+
0x020003: "Multi-Frame Image: (Multi-Angle)",
|
| 602 |
+
0x030000: "Baseline MP Primary Image",
|
| 603 |
+
}
|
| 604 |
+
mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown")
|
| 605 |
+
mpentry["Attribute"] = mpentryattr
|
| 606 |
+
mpentries.append(mpentry)
|
| 607 |
+
mp[0xB002] = mpentries
|
| 608 |
+
except KeyError as e:
|
| 609 |
+
msg = "malformed MP Index (bad MP Entry)"
|
| 610 |
+
raise SyntaxError(msg) from e
|
| 611 |
+
# Next we should try and parse the individual image unique ID list;
|
| 612 |
+
# we don't because I've never seen this actually used in a real MPO
|
| 613 |
+
# file and so can't test it.
|
| 614 |
+
return mp
|
| 615 |
+
|
| 616 |
+
|
| 617 |
+
# --------------------------------------------------------------------
|
| 618 |
+
# stuff to save JPEG files
|
| 619 |
+
|
| 620 |
+
RAWMODE = {
|
| 621 |
+
"1": "L",
|
| 622 |
+
"L": "L",
|
| 623 |
+
"RGB": "RGB",
|
| 624 |
+
"RGBX": "RGB",
|
| 625 |
+
"CMYK": "CMYK;I", # assume adobe conventions
|
| 626 |
+
"YCbCr": "YCbCr",
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
# fmt: off
|
| 630 |
+
zigzag_index = (
|
| 631 |
+
0, 1, 5, 6, 14, 15, 27, 28,
|
| 632 |
+
2, 4, 7, 13, 16, 26, 29, 42,
|
| 633 |
+
3, 8, 12, 17, 25, 30, 41, 43,
|
| 634 |
+
9, 11, 18, 24, 31, 40, 44, 53,
|
| 635 |
+
10, 19, 23, 32, 39, 45, 52, 54,
|
| 636 |
+
20, 22, 33, 38, 46, 51, 55, 60,
|
| 637 |
+
21, 34, 37, 47, 50, 56, 59, 61,
|
| 638 |
+
35, 36, 48, 49, 57, 58, 62, 63,
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
samplings = {
|
| 642 |
+
(1, 1, 1, 1, 1, 1): 0,
|
| 643 |
+
(2, 1, 1, 1, 1, 1): 1,
|
| 644 |
+
(2, 2, 1, 1, 1, 1): 2,
|
| 645 |
+
}
|
| 646 |
+
# fmt: on
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def get_sampling(im: Image.Image) -> int:
|
| 650 |
+
# There's no subsampling when images have only 1 layer
|
| 651 |
+
# (grayscale images) or when they are CMYK (4 layers),
|
| 652 |
+
# so set subsampling to the default value.
|
| 653 |
+
#
|
| 654 |
+
# NOTE: currently Pillow can't encode JPEG to YCCK format.
|
| 655 |
+
# If YCCK support is added in the future, subsampling code will have
|
| 656 |
+
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
|
| 657 |
+
if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
|
| 658 |
+
return -1
|
| 659 |
+
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
|
| 660 |
+
return samplings.get(sampling, -1)
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 664 |
+
if im.width == 0 or im.height == 0:
|
| 665 |
+
msg = "cannot write empty image as JPEG"
|
| 666 |
+
raise ValueError(msg)
|
| 667 |
+
|
| 668 |
+
try:
|
| 669 |
+
rawmode = RAWMODE[im.mode]
|
| 670 |
+
except KeyError as e:
|
| 671 |
+
msg = f"cannot write mode {im.mode} as JPEG"
|
| 672 |
+
raise OSError(msg) from e
|
| 673 |
+
|
| 674 |
+
info = im.encoderinfo
|
| 675 |
+
|
| 676 |
+
dpi = [round(x) for x in info.get("dpi", (0, 0))]
|
| 677 |
+
|
| 678 |
+
quality = info.get("quality", -1)
|
| 679 |
+
subsampling = info.get("subsampling", -1)
|
| 680 |
+
qtables = info.get("qtables")
|
| 681 |
+
|
| 682 |
+
if quality == "keep":
|
| 683 |
+
quality = -1
|
| 684 |
+
subsampling = "keep"
|
| 685 |
+
qtables = "keep"
|
| 686 |
+
elif quality in presets:
|
| 687 |
+
preset = presets[quality]
|
| 688 |
+
quality = -1
|
| 689 |
+
subsampling = preset.get("subsampling", -1)
|
| 690 |
+
qtables = preset.get("quantization")
|
| 691 |
+
elif not isinstance(quality, int):
|
| 692 |
+
msg = "Invalid quality setting"
|
| 693 |
+
raise ValueError(msg)
|
| 694 |
+
else:
|
| 695 |
+
if subsampling in presets:
|
| 696 |
+
subsampling = presets[subsampling].get("subsampling", -1)
|
| 697 |
+
if isinstance(qtables, str) and qtables in presets:
|
| 698 |
+
qtables = presets[qtables].get("quantization")
|
| 699 |
+
|
| 700 |
+
if subsampling == "4:4:4":
|
| 701 |
+
subsampling = 0
|
| 702 |
+
elif subsampling == "4:2:2":
|
| 703 |
+
subsampling = 1
|
| 704 |
+
elif subsampling == "4:2:0":
|
| 705 |
+
subsampling = 2
|
| 706 |
+
elif subsampling == "4:1:1":
|
| 707 |
+
# For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
|
| 708 |
+
# Set 4:2:0 if someone is still using that value.
|
| 709 |
+
subsampling = 2
|
| 710 |
+
elif subsampling == "keep":
|
| 711 |
+
if im.format != "JPEG":
|
| 712 |
+
msg = "Cannot use 'keep' when original image is not a JPEG"
|
| 713 |
+
raise ValueError(msg)
|
| 714 |
+
subsampling = get_sampling(im)
|
| 715 |
+
|
| 716 |
+
def validate_qtables(
|
| 717 |
+
qtables: (
|
| 718 |
+
str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
|
| 719 |
+
),
|
| 720 |
+
) -> list[list[int]] | None:
|
| 721 |
+
if qtables is None:
|
| 722 |
+
return qtables
|
| 723 |
+
if isinstance(qtables, str):
|
| 724 |
+
try:
|
| 725 |
+
lines = [
|
| 726 |
+
int(num)
|
| 727 |
+
for line in qtables.splitlines()
|
| 728 |
+
for num in line.split("#", 1)[0].split()
|
| 729 |
+
]
|
| 730 |
+
except ValueError as e:
|
| 731 |
+
msg = "Invalid quantization table"
|
| 732 |
+
raise ValueError(msg) from e
|
| 733 |
+
else:
|
| 734 |
+
qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
|
| 735 |
+
if isinstance(qtables, (tuple, list, dict)):
|
| 736 |
+
if isinstance(qtables, dict):
|
| 737 |
+
qtables = [
|
| 738 |
+
qtables[key] for key in range(len(qtables)) if key in qtables
|
| 739 |
+
]
|
| 740 |
+
elif isinstance(qtables, tuple):
|
| 741 |
+
qtables = list(qtables)
|
| 742 |
+
if not (0 < len(qtables) < 5):
|
| 743 |
+
msg = "None or too many quantization tables"
|
| 744 |
+
raise ValueError(msg)
|
| 745 |
+
for idx, table in enumerate(qtables):
|
| 746 |
+
try:
|
| 747 |
+
if len(table) != 64:
|
| 748 |
+
msg = "Invalid quantization table"
|
| 749 |
+
raise TypeError(msg)
|
| 750 |
+
table_array = array.array("H", table)
|
| 751 |
+
except TypeError as e:
|
| 752 |
+
msg = "Invalid quantization table"
|
| 753 |
+
raise ValueError(msg) from e
|
| 754 |
+
else:
|
| 755 |
+
qtables[idx] = list(table_array)
|
| 756 |
+
return qtables
|
| 757 |
+
|
| 758 |
+
if qtables == "keep":
|
| 759 |
+
if im.format != "JPEG":
|
| 760 |
+
msg = "Cannot use 'keep' when original image is not a JPEG"
|
| 761 |
+
raise ValueError(msg)
|
| 762 |
+
qtables = getattr(im, "quantization", None)
|
| 763 |
+
qtables = validate_qtables(qtables)
|
| 764 |
+
|
| 765 |
+
extra = info.get("extra", b"")
|
| 766 |
+
|
| 767 |
+
MAX_BYTES_IN_MARKER = 65533
|
| 768 |
+
if xmp := info.get("xmp"):
|
| 769 |
+
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
|
| 770 |
+
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
| 771 |
+
if len(xmp) > max_data_bytes_in_marker:
|
| 772 |
+
msg = "XMP data is too long"
|
| 773 |
+
raise ValueError(msg)
|
| 774 |
+
size = o16(2 + overhead_len + len(xmp))
|
| 775 |
+
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
|
| 776 |
+
|
| 777 |
+
if icc_profile := info.get("icc_profile"):
|
| 778 |
+
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
|
| 779 |
+
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
| 780 |
+
markers = []
|
| 781 |
+
while icc_profile:
|
| 782 |
+
markers.append(icc_profile[:max_data_bytes_in_marker])
|
| 783 |
+
icc_profile = icc_profile[max_data_bytes_in_marker:]
|
| 784 |
+
i = 1
|
| 785 |
+
for marker in markers:
|
| 786 |
+
size = o16(2 + overhead_len + len(marker))
|
| 787 |
+
extra += (
|
| 788 |
+
b"\xff\xe2"
|
| 789 |
+
+ size
|
| 790 |
+
+ b"ICC_PROFILE\0"
|
| 791 |
+
+ o8(i)
|
| 792 |
+
+ o8(len(markers))
|
| 793 |
+
+ marker
|
| 794 |
+
)
|
| 795 |
+
i += 1
|
| 796 |
+
|
| 797 |
+
comment = info.get("comment", im.info.get("comment"))
|
| 798 |
+
|
| 799 |
+
# "progressive" is the official name, but older documentation
|
| 800 |
+
# says "progression"
|
| 801 |
+
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
|
| 802 |
+
progressive = info.get("progressive", False) or info.get("progression", False)
|
| 803 |
+
|
| 804 |
+
optimize = info.get("optimize", False)
|
| 805 |
+
|
| 806 |
+
exif = info.get("exif", b"")
|
| 807 |
+
if isinstance(exif, Image.Exif):
|
| 808 |
+
exif = exif.tobytes()
|
| 809 |
+
if len(exif) > MAX_BYTES_IN_MARKER:
|
| 810 |
+
msg = "EXIF data is too long"
|
| 811 |
+
raise ValueError(msg)
|
| 812 |
+
|
| 813 |
+
# get keyword arguments
|
| 814 |
+
im.encoderconfig = (
|
| 815 |
+
quality,
|
| 816 |
+
progressive,
|
| 817 |
+
info.get("smooth", 0),
|
| 818 |
+
optimize,
|
| 819 |
+
info.get("keep_rgb", False),
|
| 820 |
+
info.get("streamtype", 0),
|
| 821 |
+
dpi,
|
| 822 |
+
subsampling,
|
| 823 |
+
info.get("restart_marker_blocks", 0),
|
| 824 |
+
info.get("restart_marker_rows", 0),
|
| 825 |
+
qtables,
|
| 826 |
+
comment,
|
| 827 |
+
extra,
|
| 828 |
+
exif,
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
# if we optimize, libjpeg needs a buffer big enough to hold the whole image
|
| 832 |
+
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
|
| 833 |
+
# channels*size, this is a value that's been used in a django patch.
|
| 834 |
+
# https://github.com/matthewwithanm/django-imagekit/issues/50
|
| 835 |
+
if optimize or progressive:
|
| 836 |
+
# CMYK can be bigger
|
| 837 |
+
if im.mode == "CMYK":
|
| 838 |
+
bufsize = 4 * im.size[0] * im.size[1]
|
| 839 |
+
# keep sets quality to -1, but the actual value may be high.
|
| 840 |
+
elif quality >= 95 or quality == -1:
|
| 841 |
+
bufsize = 2 * im.size[0] * im.size[1]
|
| 842 |
+
else:
|
| 843 |
+
bufsize = im.size[0] * im.size[1]
|
| 844 |
+
if exif:
|
| 845 |
+
bufsize += len(exif) + 5
|
| 846 |
+
if extra:
|
| 847 |
+
bufsize += len(extra) + 1
|
| 848 |
+
else:
|
| 849 |
+
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
|
| 850 |
+
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
| 851 |
+
bufsize = max(len(exif) + 5, len(extra) + 1)
|
| 852 |
+
|
| 853 |
+
ImageFile._save(
|
| 854 |
+
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
##
|
| 859 |
+
# Factory for making JPEG and MPO instances
|
| 860 |
+
def jpeg_factory(
|
| 861 |
+
fp: IO[bytes], filename: str | bytes | None = None
|
| 862 |
+
) -> JpegImageFile | MpoImageFile:
|
| 863 |
+
im = JpegImageFile(fp, filename)
|
| 864 |
+
try:
|
| 865 |
+
mpheader = im._getmp()
|
| 866 |
+
if mpheader is not None and mpheader[45057] > 1:
|
| 867 |
+
for segment, content in im.applist:
|
| 868 |
+
if segment == "APP1" and b' hdrgm:Version="' in content:
|
| 869 |
+
# Ultra HDR images are not yet supported
|
| 870 |
+
return im
|
| 871 |
+
# It's actually an MPO
|
| 872 |
+
from .MpoImagePlugin import MpoImageFile
|
| 873 |
+
|
| 874 |
+
# Don't reload everything, just convert it.
|
| 875 |
+
im = MpoImageFile.adopt(im, mpheader)
|
| 876 |
+
except (TypeError, IndexError):
|
| 877 |
+
# It is really a JPEG
|
| 878 |
+
pass
|
| 879 |
+
except SyntaxError:
|
| 880 |
+
warnings.warn(
|
| 881 |
+
"Image appears to be a malformed MPO file, it will be "
|
| 882 |
+
"interpreted as a base JPEG file"
|
| 883 |
+
)
|
| 884 |
+
return im
|
| 885 |
+
|
| 886 |
+
|
| 887 |
+
# ---------------------------------------------------------------------
|
| 888 |
+
# Registry stuff
|
| 889 |
+
|
| 890 |
+
Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
|
| 891 |
+
Image.register_save(JpegImageFile.format, _save)
|
| 892 |
+
|
| 893 |
+
Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"])
|
| 894 |
+
|
| 895 |
+
Image.register_mime(JpegImageFile.format, "image/jpeg")
|
PIL/JpegPresets.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
JPEG quality settings equivalent to the Photoshop settings.
|
| 3 |
+
Can be used when saving JPEG files.
|
| 4 |
+
|
| 5 |
+
The following presets are available by default:
|
| 6 |
+
``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``,
|
| 7 |
+
``low``, ``medium``, ``high``, ``maximum``.
|
| 8 |
+
More presets can be added to the :py:data:`presets` dict if needed.
|
| 9 |
+
|
| 10 |
+
To apply the preset, specify::
|
| 11 |
+
|
| 12 |
+
quality="preset_name"
|
| 13 |
+
|
| 14 |
+
To apply only the quantization table::
|
| 15 |
+
|
| 16 |
+
qtables="preset_name"
|
| 17 |
+
|
| 18 |
+
To apply only the subsampling setting::
|
| 19 |
+
|
| 20 |
+
subsampling="preset_name"
|
| 21 |
+
|
| 22 |
+
Example::
|
| 23 |
+
|
| 24 |
+
im.save("image_name.jpg", quality="web_high")
|
| 25 |
+
|
| 26 |
+
Subsampling
|
| 27 |
+
-----------
|
| 28 |
+
|
| 29 |
+
Subsampling is the practice of encoding images by implementing less resolution
|
| 30 |
+
for chroma information than for luma information.
|
| 31 |
+
(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling)
|
| 32 |
+
|
| 33 |
+
Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and
|
| 34 |
+
4:2:0.
|
| 35 |
+
|
| 36 |
+
You can get the subsampling of a JPEG with the
|
| 37 |
+
:func:`.JpegImagePlugin.get_sampling` function.
|
| 38 |
+
|
| 39 |
+
In JPEG compressed data a JPEG marker is used instead of an EXIF tag.
|
| 40 |
+
(ref.: https://exiv2.org/tags.html)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
Quantization tables
|
| 44 |
+
-------------------
|
| 45 |
+
|
| 46 |
+
They are values use by the DCT (Discrete cosine transform) to remove
|
| 47 |
+
*unnecessary* information from the image (the lossy part of the compression).
|
| 48 |
+
(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices,
|
| 49 |
+
https://en.wikipedia.org/wiki/JPEG#Quantization)
|
| 50 |
+
|
| 51 |
+
You can get the quantization tables of a JPEG with::
|
| 52 |
+
|
| 53 |
+
im.quantization
|
| 54 |
+
|
| 55 |
+
This will return a dict with a number of lists. You can pass this dict
|
| 56 |
+
directly as the qtables argument when saving a JPEG.
|
| 57 |
+
|
| 58 |
+
The quantization table format in presets is a list with sublists. These formats
|
| 59 |
+
are interchangeable.
|
| 60 |
+
|
| 61 |
+
Libjpeg ref.:
|
| 62 |
+
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
|
| 63 |
+
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
from __future__ import annotations
|
| 67 |
+
|
| 68 |
+
# fmt: off
|
| 69 |
+
presets = {
|
| 70 |
+
'web_low': {'subsampling': 2, # "4:2:0"
|
| 71 |
+
'quantization': [
|
| 72 |
+
[20, 16, 25, 39, 50, 46, 62, 68,
|
| 73 |
+
16, 18, 23, 38, 38, 53, 65, 68,
|
| 74 |
+
25, 23, 31, 38, 53, 65, 68, 68,
|
| 75 |
+
39, 38, 38, 53, 65, 68, 68, 68,
|
| 76 |
+
50, 38, 53, 65, 68, 68, 68, 68,
|
| 77 |
+
46, 53, 65, 68, 68, 68, 68, 68,
|
| 78 |
+
62, 65, 68, 68, 68, 68, 68, 68,
|
| 79 |
+
68, 68, 68, 68, 68, 68, 68, 68],
|
| 80 |
+
[21, 25, 32, 38, 54, 68, 68, 68,
|
| 81 |
+
25, 28, 24, 38, 54, 68, 68, 68,
|
| 82 |
+
32, 24, 32, 43, 66, 68, 68, 68,
|
| 83 |
+
38, 38, 43, 53, 68, 68, 68, 68,
|
| 84 |
+
54, 54, 66, 68, 68, 68, 68, 68,
|
| 85 |
+
68, 68, 68, 68, 68, 68, 68, 68,
|
| 86 |
+
68, 68, 68, 68, 68, 68, 68, 68,
|
| 87 |
+
68, 68, 68, 68, 68, 68, 68, 68]
|
| 88 |
+
]},
|
| 89 |
+
'web_medium': {'subsampling': 2, # "4:2:0"
|
| 90 |
+
'quantization': [
|
| 91 |
+
[16, 11, 11, 16, 23, 27, 31, 30,
|
| 92 |
+
11, 12, 12, 15, 20, 23, 23, 30,
|
| 93 |
+
11, 12, 13, 16, 23, 26, 35, 47,
|
| 94 |
+
16, 15, 16, 23, 26, 37, 47, 64,
|
| 95 |
+
23, 20, 23, 26, 39, 51, 64, 64,
|
| 96 |
+
27, 23, 26, 37, 51, 64, 64, 64,
|
| 97 |
+
31, 23, 35, 47, 64, 64, 64, 64,
|
| 98 |
+
30, 30, 47, 64, 64, 64, 64, 64],
|
| 99 |
+
[17, 15, 17, 21, 20, 26, 38, 48,
|
| 100 |
+
15, 19, 18, 17, 20, 26, 35, 43,
|
| 101 |
+
17, 18, 20, 22, 26, 30, 46, 53,
|
| 102 |
+
21, 17, 22, 28, 30, 39, 53, 64,
|
| 103 |
+
20, 20, 26, 30, 39, 48, 64, 64,
|
| 104 |
+
26, 26, 30, 39, 48, 63, 64, 64,
|
| 105 |
+
38, 35, 46, 53, 64, 64, 64, 64,
|
| 106 |
+
48, 43, 53, 64, 64, 64, 64, 64]
|
| 107 |
+
]},
|
| 108 |
+
'web_high': {'subsampling': 0, # "4:4:4"
|
| 109 |
+
'quantization': [
|
| 110 |
+
[6, 4, 4, 6, 9, 11, 12, 16,
|
| 111 |
+
4, 5, 5, 6, 8, 10, 12, 12,
|
| 112 |
+
4, 5, 5, 6, 10, 12, 14, 19,
|
| 113 |
+
6, 6, 6, 11, 12, 15, 19, 28,
|
| 114 |
+
9, 8, 10, 12, 16, 20, 27, 31,
|
| 115 |
+
11, 10, 12, 15, 20, 27, 31, 31,
|
| 116 |
+
12, 12, 14, 19, 27, 31, 31, 31,
|
| 117 |
+
16, 12, 19, 28, 31, 31, 31, 31],
|
| 118 |
+
[7, 7, 13, 24, 26, 31, 31, 31,
|
| 119 |
+
7, 12, 16, 21, 31, 31, 31, 31,
|
| 120 |
+
13, 16, 17, 31, 31, 31, 31, 31,
|
| 121 |
+
24, 21, 31, 31, 31, 31, 31, 31,
|
| 122 |
+
26, 31, 31, 31, 31, 31, 31, 31,
|
| 123 |
+
31, 31, 31, 31, 31, 31, 31, 31,
|
| 124 |
+
31, 31, 31, 31, 31, 31, 31, 31,
|
| 125 |
+
31, 31, 31, 31, 31, 31, 31, 31]
|
| 126 |
+
]},
|
| 127 |
+
'web_very_high': {'subsampling': 0, # "4:4:4"
|
| 128 |
+
'quantization': [
|
| 129 |
+
[2, 2, 2, 2, 3, 4, 5, 6,
|
| 130 |
+
2, 2, 2, 2, 3, 4, 5, 6,
|
| 131 |
+
2, 2, 2, 2, 4, 5, 7, 9,
|
| 132 |
+
2, 2, 2, 4, 5, 7, 9, 12,
|
| 133 |
+
3, 3, 4, 5, 8, 10, 12, 12,
|
| 134 |
+
4, 4, 5, 7, 10, 12, 12, 12,
|
| 135 |
+
5, 5, 7, 9, 12, 12, 12, 12,
|
| 136 |
+
6, 6, 9, 12, 12, 12, 12, 12],
|
| 137 |
+
[3, 3, 5, 9, 13, 15, 15, 15,
|
| 138 |
+
3, 4, 6, 11, 14, 12, 12, 12,
|
| 139 |
+
5, 6, 9, 14, 12, 12, 12, 12,
|
| 140 |
+
9, 11, 14, 12, 12, 12, 12, 12,
|
| 141 |
+
13, 14, 12, 12, 12, 12, 12, 12,
|
| 142 |
+
15, 12, 12, 12, 12, 12, 12, 12,
|
| 143 |
+
15, 12, 12, 12, 12, 12, 12, 12,
|
| 144 |
+
15, 12, 12, 12, 12, 12, 12, 12]
|
| 145 |
+
]},
|
| 146 |
+
'web_maximum': {'subsampling': 0, # "4:4:4"
|
| 147 |
+
'quantization': [
|
| 148 |
+
[1, 1, 1, 1, 1, 1, 1, 1,
|
| 149 |
+
1, 1, 1, 1, 1, 1, 1, 1,
|
| 150 |
+
1, 1, 1, 1, 1, 1, 1, 2,
|
| 151 |
+
1, 1, 1, 1, 1, 1, 2, 2,
|
| 152 |
+
1, 1, 1, 1, 1, 2, 2, 3,
|
| 153 |
+
1, 1, 1, 1, 2, 2, 3, 3,
|
| 154 |
+
1, 1, 1, 2, 2, 3, 3, 3,
|
| 155 |
+
1, 1, 2, 2, 3, 3, 3, 3],
|
| 156 |
+
[1, 1, 1, 2, 2, 3, 3, 3,
|
| 157 |
+
1, 1, 1, 2, 3, 3, 3, 3,
|
| 158 |
+
1, 1, 1, 3, 3, 3, 3, 3,
|
| 159 |
+
2, 2, 3, 3, 3, 3, 3, 3,
|
| 160 |
+
2, 3, 3, 3, 3, 3, 3, 3,
|
| 161 |
+
3, 3, 3, 3, 3, 3, 3, 3,
|
| 162 |
+
3, 3, 3, 3, 3, 3, 3, 3,
|
| 163 |
+
3, 3, 3, 3, 3, 3, 3, 3]
|
| 164 |
+
]},
|
| 165 |
+
'low': {'subsampling': 2, # "4:2:0"
|
| 166 |
+
'quantization': [
|
| 167 |
+
[18, 14, 14, 21, 30, 35, 34, 17,
|
| 168 |
+
14, 16, 16, 19, 26, 23, 12, 12,
|
| 169 |
+
14, 16, 17, 21, 23, 12, 12, 12,
|
| 170 |
+
21, 19, 21, 23, 12, 12, 12, 12,
|
| 171 |
+
30, 26, 23, 12, 12, 12, 12, 12,
|
| 172 |
+
35, 23, 12, 12, 12, 12, 12, 12,
|
| 173 |
+
34, 12, 12, 12, 12, 12, 12, 12,
|
| 174 |
+
17, 12, 12, 12, 12, 12, 12, 12],
|
| 175 |
+
[20, 19, 22, 27, 20, 20, 17, 17,
|
| 176 |
+
19, 25, 23, 14, 14, 12, 12, 12,
|
| 177 |
+
22, 23, 14, 14, 12, 12, 12, 12,
|
| 178 |
+
27, 14, 14, 12, 12, 12, 12, 12,
|
| 179 |
+
20, 14, 12, 12, 12, 12, 12, 12,
|
| 180 |
+
20, 12, 12, 12, 12, 12, 12, 12,
|
| 181 |
+
17, 12, 12, 12, 12, 12, 12, 12,
|
| 182 |
+
17, 12, 12, 12, 12, 12, 12, 12]
|
| 183 |
+
]},
|
| 184 |
+
'medium': {'subsampling': 2, # "4:2:0"
|
| 185 |
+
'quantization': [
|
| 186 |
+
[12, 8, 8, 12, 17, 21, 24, 17,
|
| 187 |
+
8, 9, 9, 11, 15, 19, 12, 12,
|
| 188 |
+
8, 9, 10, 12, 19, 12, 12, 12,
|
| 189 |
+
12, 11, 12, 21, 12, 12, 12, 12,
|
| 190 |
+
17, 15, 19, 12, 12, 12, 12, 12,
|
| 191 |
+
21, 19, 12, 12, 12, 12, 12, 12,
|
| 192 |
+
24, 12, 12, 12, 12, 12, 12, 12,
|
| 193 |
+
17, 12, 12, 12, 12, 12, 12, 12],
|
| 194 |
+
[13, 11, 13, 16, 20, 20, 17, 17,
|
| 195 |
+
11, 14, 14, 14, 14, 12, 12, 12,
|
| 196 |
+
13, 14, 14, 14, 12, 12, 12, 12,
|
| 197 |
+
16, 14, 14, 12, 12, 12, 12, 12,
|
| 198 |
+
20, 14, 12, 12, 12, 12, 12, 12,
|
| 199 |
+
20, 12, 12, 12, 12, 12, 12, 12,
|
| 200 |
+
17, 12, 12, 12, 12, 12, 12, 12,
|
| 201 |
+
17, 12, 12, 12, 12, 12, 12, 12]
|
| 202 |
+
]},
|
| 203 |
+
'high': {'subsampling': 0, # "4:4:4"
|
| 204 |
+
'quantization': [
|
| 205 |
+
[6, 4, 4, 6, 9, 11, 12, 16,
|
| 206 |
+
4, 5, 5, 6, 8, 10, 12, 12,
|
| 207 |
+
4, 5, 5, 6, 10, 12, 12, 12,
|
| 208 |
+
6, 6, 6, 11, 12, 12, 12, 12,
|
| 209 |
+
9, 8, 10, 12, 12, 12, 12, 12,
|
| 210 |
+
11, 10, 12, 12, 12, 12, 12, 12,
|
| 211 |
+
12, 12, 12, 12, 12, 12, 12, 12,
|
| 212 |
+
16, 12, 12, 12, 12, 12, 12, 12],
|
| 213 |
+
[7, 7, 13, 24, 20, 20, 17, 17,
|
| 214 |
+
7, 12, 16, 14, 14, 12, 12, 12,
|
| 215 |
+
13, 16, 14, 14, 12, 12, 12, 12,
|
| 216 |
+
24, 14, 14, 12, 12, 12, 12, 12,
|
| 217 |
+
20, 14, 12, 12, 12, 12, 12, 12,
|
| 218 |
+
20, 12, 12, 12, 12, 12, 12, 12,
|
| 219 |
+
17, 12, 12, 12, 12, 12, 12, 12,
|
| 220 |
+
17, 12, 12, 12, 12, 12, 12, 12]
|
| 221 |
+
]},
|
| 222 |
+
'maximum': {'subsampling': 0, # "4:4:4"
|
| 223 |
+
'quantization': [
|
| 224 |
+
[2, 2, 2, 2, 3, 4, 5, 6,
|
| 225 |
+
2, 2, 2, 2, 3, 4, 5, 6,
|
| 226 |
+
2, 2, 2, 2, 4, 5, 7, 9,
|
| 227 |
+
2, 2, 2, 4, 5, 7, 9, 12,
|
| 228 |
+
3, 3, 4, 5, 8, 10, 12, 12,
|
| 229 |
+
4, 4, 5, 7, 10, 12, 12, 12,
|
| 230 |
+
5, 5, 7, 9, 12, 12, 12, 12,
|
| 231 |
+
6, 6, 9, 12, 12, 12, 12, 12],
|
| 232 |
+
[3, 3, 5, 9, 13, 15, 15, 15,
|
| 233 |
+
3, 4, 6, 10, 14, 12, 12, 12,
|
| 234 |
+
5, 6, 9, 14, 12, 12, 12, 12,
|
| 235 |
+
9, 10, 14, 12, 12, 12, 12, 12,
|
| 236 |
+
13, 14, 12, 12, 12, 12, 12, 12,
|
| 237 |
+
15, 12, 12, 12, 12, 12, 12, 12,
|
| 238 |
+
15, 12, 12, 12, 12, 12, 12, 12,
|
| 239 |
+
15, 12, 12, 12, 12, 12, 12, 12]
|
| 240 |
+
]},
|
| 241 |
+
}
|
| 242 |
+
# fmt: on
|
PIL/McIdasImagePlugin.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# Basic McIdas support for PIL
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 1997-05-05 fl Created (8-bit images only)
|
| 9 |
+
# 2009-03-08 fl Added 16/32-bit support.
|
| 10 |
+
#
|
| 11 |
+
# Thanks to Richard Jones and Craig Swank for specs and samples.
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 14 |
+
# Copyright (c) Fredrik Lundh 1997.
|
| 15 |
+
#
|
| 16 |
+
# See the README file for information on usage and redistribution.
|
| 17 |
+
#
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import struct
|
| 21 |
+
|
| 22 |
+
from . import Image, ImageFile
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _accept(prefix: bytes) -> bool:
|
| 26 |
+
return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
##
|
| 30 |
+
# Image plugin for McIdas area images.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class McIdasImageFile(ImageFile.ImageFile):
|
| 34 |
+
format = "MCIDAS"
|
| 35 |
+
format_description = "McIdas area file"
|
| 36 |
+
|
| 37 |
+
def _open(self) -> None:
|
| 38 |
+
# parse area file directory
|
| 39 |
+
assert self.fp is not None
|
| 40 |
+
|
| 41 |
+
s = self.fp.read(256)
|
| 42 |
+
if not _accept(s) or len(s) != 256:
|
| 43 |
+
msg = "not an McIdas area file"
|
| 44 |
+
raise SyntaxError(msg)
|
| 45 |
+
|
| 46 |
+
self.area_descriptor_raw = s
|
| 47 |
+
self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
|
| 48 |
+
|
| 49 |
+
# get mode
|
| 50 |
+
if w[11] == 1:
|
| 51 |
+
mode = rawmode = "L"
|
| 52 |
+
elif w[11] == 2:
|
| 53 |
+
mode = rawmode = "I;16B"
|
| 54 |
+
elif w[11] == 4:
|
| 55 |
+
# FIXME: add memory map support
|
| 56 |
+
mode = "I"
|
| 57 |
+
rawmode = "I;32B"
|
| 58 |
+
else:
|
| 59 |
+
msg = "unsupported McIdas format"
|
| 60 |
+
raise SyntaxError(msg)
|
| 61 |
+
|
| 62 |
+
self._mode = mode
|
| 63 |
+
self._size = w[10], w[9]
|
| 64 |
+
|
| 65 |
+
offset = w[34] + w[15]
|
| 66 |
+
stride = w[15] + w[10] * w[11] * w[14]
|
| 67 |
+
|
| 68 |
+
self.tile = [
|
| 69 |
+
ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# --------------------------------------------------------------------
|
| 74 |
+
# registry
|
| 75 |
+
|
| 76 |
+
Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept)
|
| 77 |
+
|
| 78 |
+
# no default extension
|
PIL/MicImagePlugin.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# Microsoft Image Composer support for PIL
|
| 6 |
+
#
|
| 7 |
+
# Notes:
|
| 8 |
+
# uses TiffImagePlugin.py to read the actual image streams
|
| 9 |
+
#
|
| 10 |
+
# History:
|
| 11 |
+
# 97-01-20 fl Created
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 14 |
+
# Copyright (c) Fredrik Lundh 1997.
|
| 15 |
+
#
|
| 16 |
+
# See the README file for information on usage and redistribution.
|
| 17 |
+
#
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import olefile
|
| 21 |
+
|
| 22 |
+
from . import Image, TiffImagePlugin
|
| 23 |
+
|
| 24 |
+
#
|
| 25 |
+
# --------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _accept(prefix: bytes) -> bool:
|
| 29 |
+
return prefix.startswith(olefile.MAGIC)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
##
|
| 33 |
+
# Image plugin for Microsoft's Image Composer file format.
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class MicImageFile(TiffImagePlugin.TiffImageFile):
|
| 37 |
+
format = "MIC"
|
| 38 |
+
format_description = "Microsoft Image Composer"
|
| 39 |
+
_close_exclusive_fp_after_loading = False
|
| 40 |
+
|
| 41 |
+
def _open(self) -> None:
|
| 42 |
+
# read the OLE directory and see if this is a likely
|
| 43 |
+
# to be a Microsoft Image Composer file
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
self.ole = olefile.OleFileIO(self.fp)
|
| 47 |
+
except OSError as e:
|
| 48 |
+
msg = "not an MIC file; invalid OLE file"
|
| 49 |
+
raise SyntaxError(msg) from e
|
| 50 |
+
|
| 51 |
+
# find ACI subfiles with Image members (maybe not the
|
| 52 |
+
# best way to identify MIC files, but what the... ;-)
|
| 53 |
+
|
| 54 |
+
self.images = [
|
| 55 |
+
path
|
| 56 |
+
for path in self.ole.listdir()
|
| 57 |
+
if path[1:] and path[0].endswith(".ACI") and path[1] == "Image"
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
# if we didn't find any images, this is probably not
|
| 61 |
+
# an MIC file.
|
| 62 |
+
if not self.images:
|
| 63 |
+
msg = "not an MIC file; no image entries"
|
| 64 |
+
raise SyntaxError(msg)
|
| 65 |
+
|
| 66 |
+
self.frame = -1
|
| 67 |
+
self._n_frames = len(self.images)
|
| 68 |
+
self.is_animated = self._n_frames > 1
|
| 69 |
+
|
| 70 |
+
assert self.fp is not None
|
| 71 |
+
self.__fp = self.fp
|
| 72 |
+
self.seek(0)
|
| 73 |
+
|
| 74 |
+
def seek(self, frame: int) -> None:
|
| 75 |
+
if not self._seek_check(frame):
|
| 76 |
+
return
|
| 77 |
+
filename = self.images[frame]
|
| 78 |
+
self.fp = self.ole.openstream(filename)
|
| 79 |
+
|
| 80 |
+
TiffImagePlugin.TiffImageFile._open(self)
|
| 81 |
+
|
| 82 |
+
self.frame = frame
|
| 83 |
+
|
| 84 |
+
def tell(self) -> int:
|
| 85 |
+
return self.frame
|
| 86 |
+
|
| 87 |
+
def close(self) -> None:
|
| 88 |
+
self.__fp.close()
|
| 89 |
+
self.ole.close()
|
| 90 |
+
super().close()
|
| 91 |
+
|
| 92 |
+
def __exit__(self, *args: object) -> None:
|
| 93 |
+
self.__fp.close()
|
| 94 |
+
self.ole.close()
|
| 95 |
+
super().__exit__()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
#
|
| 99 |
+
# --------------------------------------------------------------------
|
| 100 |
+
|
| 101 |
+
Image.register_open(MicImageFile.format, MicImageFile, _accept)
|
| 102 |
+
|
| 103 |
+
Image.register_extension(MicImageFile.format, ".mic")
|
PIL/MpegImagePlugin.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# MPEG file handling
|
| 6 |
+
#
|
| 7 |
+
# History:
|
| 8 |
+
# 95-09-09 fl Created
|
| 9 |
+
#
|
| 10 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 11 |
+
# Copyright (c) Fredrik Lundh 1995.
|
| 12 |
+
#
|
| 13 |
+
# See the README file for information on usage and redistribution.
|
| 14 |
+
#
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from . import Image, ImageFile
|
| 18 |
+
from ._binary import i8
|
| 19 |
+
from ._typing import SupportsRead
|
| 20 |
+
|
| 21 |
+
#
|
| 22 |
+
# Bitstream parser
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class BitStream:
|
| 26 |
+
def __init__(self, fp: SupportsRead[bytes]) -> None:
|
| 27 |
+
self.fp = fp
|
| 28 |
+
self.bits = 0
|
| 29 |
+
self.bitbuffer = 0
|
| 30 |
+
|
| 31 |
+
def next(self) -> int:
|
| 32 |
+
return i8(self.fp.read(1))
|
| 33 |
+
|
| 34 |
+
def peek(self, bits: int) -> int:
|
| 35 |
+
while self.bits < bits:
|
| 36 |
+
self.bitbuffer = (self.bitbuffer << 8) + self.next()
|
| 37 |
+
self.bits += 8
|
| 38 |
+
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
|
| 39 |
+
|
| 40 |
+
def skip(self, bits: int) -> None:
|
| 41 |
+
while self.bits < bits:
|
| 42 |
+
self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
|
| 43 |
+
self.bits += 8
|
| 44 |
+
self.bits = self.bits - bits
|
| 45 |
+
|
| 46 |
+
def read(self, bits: int) -> int:
|
| 47 |
+
v = self.peek(bits)
|
| 48 |
+
self.bits = self.bits - bits
|
| 49 |
+
return v
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _accept(prefix: bytes) -> bool:
|
| 53 |
+
return prefix.startswith(b"\x00\x00\x01\xb3")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
##
|
| 57 |
+
# Image plugin for MPEG streams. This plugin can identify a stream,
|
| 58 |
+
# but it cannot read it.
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class MpegImageFile(ImageFile.ImageFile):
|
| 62 |
+
format = "MPEG"
|
| 63 |
+
format_description = "MPEG"
|
| 64 |
+
|
| 65 |
+
def _open(self) -> None:
|
| 66 |
+
assert self.fp is not None
|
| 67 |
+
|
| 68 |
+
s = BitStream(self.fp)
|
| 69 |
+
if s.read(32) != 0x1B3:
|
| 70 |
+
msg = "not an MPEG file"
|
| 71 |
+
raise SyntaxError(msg)
|
| 72 |
+
|
| 73 |
+
self._mode = "RGB"
|
| 74 |
+
self._size = s.read(12), s.read(12)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# --------------------------------------------------------------------
|
| 78 |
+
# Registry stuff
|
| 79 |
+
|
| 80 |
+
Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
|
| 81 |
+
|
| 82 |
+
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])
|
| 83 |
+
|
| 84 |
+
Image.register_mime(MpegImageFile.format, "video/mpeg")
|
PIL/MpoImagePlugin.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
# $Id$
|
| 4 |
+
#
|
| 5 |
+
# MPO file handling
|
| 6 |
+
#
|
| 7 |
+
# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the
|
| 8 |
+
# Camera & Imaging Products Association)
|
| 9 |
+
#
|
| 10 |
+
# The multi-picture object combines multiple JPEG images (with a modified EXIF
|
| 11 |
+
# data format) into a single file. While it can theoretically be used much like
|
| 12 |
+
# a GIF animation, it is commonly used to represent 3D photographs and is (as
|
| 13 |
+
# of this writing) the most commonly used format by 3D cameras.
|
| 14 |
+
#
|
| 15 |
+
# History:
|
| 16 |
+
# 2014-03-13 Feneric Created
|
| 17 |
+
#
|
| 18 |
+
# See the README file for information on usage and redistribution.
|
| 19 |
+
#
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import os
|
| 23 |
+
import struct
|
| 24 |
+
from typing import IO, Any, cast
|
| 25 |
+
|
| 26 |
+
from . import (
|
| 27 |
+
Image,
|
| 28 |
+
ImageFile,
|
| 29 |
+
ImageSequence,
|
| 30 |
+
JpegImagePlugin,
|
| 31 |
+
TiffImagePlugin,
|
| 32 |
+
)
|
| 33 |
+
from ._binary import o32le
|
| 34 |
+
from ._util import DeferredError
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 38 |
+
JpegImagePlugin._save(im, fp, filename)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 42 |
+
append_images = im.encoderinfo.get("append_images", [])
|
| 43 |
+
if not append_images and not getattr(im, "is_animated", False):
|
| 44 |
+
_save(im, fp, filename)
|
| 45 |
+
return
|
| 46 |
+
|
| 47 |
+
mpf_offset = 28
|
| 48 |
+
offsets: list[int] = []
|
| 49 |
+
im_sequences = [im, *append_images]
|
| 50 |
+
total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
|
| 51 |
+
for im_sequence in im_sequences:
|
| 52 |
+
for im_frame in ImageSequence.Iterator(im_sequence):
|
| 53 |
+
if not offsets:
|
| 54 |
+
# APP2 marker
|
| 55 |
+
ifd_length = 66 + 16 * total
|
| 56 |
+
im_frame.encoderinfo["extra"] = (
|
| 57 |
+
b"\xff\xe2"
|
| 58 |
+
+ struct.pack(">H", 6 + ifd_length)
|
| 59 |
+
+ b"MPF\0"
|
| 60 |
+
+ b" " * ifd_length
|
| 61 |
+
)
|
| 62 |
+
exif = im_frame.encoderinfo.get("exif")
|
| 63 |
+
if isinstance(exif, Image.Exif):
|
| 64 |
+
exif = exif.tobytes()
|
| 65 |
+
im_frame.encoderinfo["exif"] = exif
|
| 66 |
+
if exif:
|
| 67 |
+
mpf_offset += 4 + len(exif)
|
| 68 |
+
|
| 69 |
+
JpegImagePlugin._save(im_frame, fp, filename)
|
| 70 |
+
offsets.append(fp.tell())
|
| 71 |
+
else:
|
| 72 |
+
encoderinfo = im_frame._attach_default_encoderinfo(im)
|
| 73 |
+
im_frame.save(fp, "JPEG")
|
| 74 |
+
im_frame.encoderinfo = encoderinfo
|
| 75 |
+
offsets.append(fp.tell() - offsets[-1])
|
| 76 |
+
|
| 77 |
+
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
| 78 |
+
ifd[0xB000] = b"0100"
|
| 79 |
+
ifd[0xB001] = len(offsets)
|
| 80 |
+
|
| 81 |
+
mpentries = b""
|
| 82 |
+
data_offset = 0
|
| 83 |
+
for i, size in enumerate(offsets):
|
| 84 |
+
if i == 0:
|
| 85 |
+
mptype = 0x030000 # Baseline MP Primary Image
|
| 86 |
+
else:
|
| 87 |
+
mptype = 0x000000 # Undefined
|
| 88 |
+
mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
|
| 89 |
+
if i == 0:
|
| 90 |
+
data_offset -= mpf_offset
|
| 91 |
+
data_offset += size
|
| 92 |
+
ifd[0xB002] = mpentries
|
| 93 |
+
|
| 94 |
+
fp.seek(mpf_offset)
|
| 95 |
+
fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
|
| 96 |
+
fp.seek(0, os.SEEK_END)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
##
|
| 100 |
+
# Image plugin for MPO images.
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
| 104 |
+
format = "MPO"
|
| 105 |
+
format_description = "MPO (CIPA DC-007)"
|
| 106 |
+
_close_exclusive_fp_after_loading = False
|
| 107 |
+
|
| 108 |
+
def _open(self) -> None:
|
| 109 |
+
assert self.fp is not None
|
| 110 |
+
self.fp.seek(0) # prep the fp in order to pass the JPEG test
|
| 111 |
+
JpegImagePlugin.JpegImageFile._open(self)
|
| 112 |
+
self._after_jpeg_open()
|
| 113 |
+
|
| 114 |
+
def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
|
| 115 |
+
self.mpinfo = mpheader if mpheader is not None else self._getmp()
|
| 116 |
+
if self.mpinfo is None:
|
| 117 |
+
msg = "Image appears to be a malformed MPO file"
|
| 118 |
+
raise ValueError(msg)
|
| 119 |
+
self.n_frames = self.mpinfo[0xB001]
|
| 120 |
+
self.__mpoffsets = [
|
| 121 |
+
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
|
| 122 |
+
]
|
| 123 |
+
self.__mpoffsets[0] = 0
|
| 124 |
+
# Note that the following assertion will only be invalid if something
|
| 125 |
+
# gets broken within JpegImagePlugin.
|
| 126 |
+
assert self.n_frames == len(self.__mpoffsets)
|
| 127 |
+
del self.info["mpoffset"] # no longer needed
|
| 128 |
+
self.is_animated = self.n_frames > 1
|
| 129 |
+
assert self.fp is not None
|
| 130 |
+
self._fp = self.fp # FIXME: hack
|
| 131 |
+
self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
|
| 132 |
+
self.__frame = 0
|
| 133 |
+
self.offset = 0
|
| 134 |
+
# for now we can only handle reading and individual frame extraction
|
| 135 |
+
self.readonly = 1
|
| 136 |
+
|
| 137 |
+
def load_seek(self, pos: int) -> None:
|
| 138 |
+
if isinstance(self._fp, DeferredError):
|
| 139 |
+
raise self._fp.ex
|
| 140 |
+
self._fp.seek(pos)
|
| 141 |
+
|
| 142 |
+
def seek(self, frame: int) -> None:
|
| 143 |
+
if not self._seek_check(frame):
|
| 144 |
+
return
|
| 145 |
+
if isinstance(self._fp, DeferredError):
|
| 146 |
+
raise self._fp.ex
|
| 147 |
+
self.fp = self._fp
|
| 148 |
+
self.offset = self.__mpoffsets[frame]
|
| 149 |
+
|
| 150 |
+
original_exif = self.info.get("exif")
|
| 151 |
+
if "exif" in self.info:
|
| 152 |
+
del self.info["exif"]
|
| 153 |
+
|
| 154 |
+
self.fp.seek(self.offset + 2) # skip SOI marker
|
| 155 |
+
if not self.fp.read(2):
|
| 156 |
+
msg = "No data found for frame"
|
| 157 |
+
raise ValueError(msg)
|
| 158 |
+
self.fp.seek(self.offset)
|
| 159 |
+
JpegImagePlugin.JpegImageFile._open(self)
|
| 160 |
+
if self.info.get("exif") != original_exif:
|
| 161 |
+
self._reload_exif()
|
| 162 |
+
|
| 163 |
+
self.tile = [
|
| 164 |
+
ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
|
| 165 |
+
]
|
| 166 |
+
self.__frame = frame
|
| 167 |
+
|
| 168 |
+
def tell(self) -> int:
|
| 169 |
+
return self.__frame
|
| 170 |
+
|
| 171 |
+
@staticmethod
|
| 172 |
+
def adopt(
|
| 173 |
+
jpeg_instance: JpegImagePlugin.JpegImageFile,
|
| 174 |
+
mpheader: dict[int, Any] | None = None,
|
| 175 |
+
) -> MpoImageFile:
|
| 176 |
+
"""
|
| 177 |
+
Transform the instance of JpegImageFile into
|
| 178 |
+
an instance of MpoImageFile.
|
| 179 |
+
After the call, the JpegImageFile is extended
|
| 180 |
+
to be an MpoImageFile.
|
| 181 |
+
|
| 182 |
+
This is essentially useful when opening a JPEG
|
| 183 |
+
file that reveals itself as an MPO, to avoid
|
| 184 |
+
double call to _open.
|
| 185 |
+
"""
|
| 186 |
+
jpeg_instance.__class__ = MpoImageFile
|
| 187 |
+
mpo_instance = cast(MpoImageFile, jpeg_instance)
|
| 188 |
+
mpo_instance._after_jpeg_open(mpheader)
|
| 189 |
+
return mpo_instance
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ---------------------------------------------------------------------
|
| 193 |
+
# Registry stuff
|
| 194 |
+
|
| 195 |
+
# Note that since MPO shares a factory with JPEG, we do not need to do a
|
| 196 |
+
# separate registration for it here.
|
| 197 |
+
# Image.register_open(MpoImageFile.format,
|
| 198 |
+
# JpegImagePlugin.jpeg_factory, _accept)
|
| 199 |
+
Image.register_save(MpoImageFile.format, _save)
|
| 200 |
+
Image.register_save_all(MpoImageFile.format, _save_all)
|
| 201 |
+
|
| 202 |
+
Image.register_extension(MpoImageFile.format, ".mpo")
|
| 203 |
+
|
| 204 |
+
Image.register_mime(MpoImageFile.format, "image/mpo")
|
PIL/MspImagePlugin.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# The Python Imaging Library.
|
| 3 |
+
#
|
| 4 |
+
# MSP file handling
|
| 5 |
+
#
|
| 6 |
+
# This is the format used by the Paint program in Windows 1 and 2.
|
| 7 |
+
#
|
| 8 |
+
# History:
|
| 9 |
+
# 95-09-05 fl Created
|
| 10 |
+
# 97-01-03 fl Read/write MSP images
|
| 11 |
+
# 17-02-21 es Fixed RLE interpretation
|
| 12 |
+
#
|
| 13 |
+
# Copyright (c) Secret Labs AB 1997.
|
| 14 |
+
# Copyright (c) Fredrik Lundh 1995-97.
|
| 15 |
+
# Copyright (c) Eric Soroos 2017.
|
| 16 |
+
#
|
| 17 |
+
# See the README file for information on usage and redistribution.
|
| 18 |
+
#
|
| 19 |
+
# More info on this format: https://archive.org/details/gg243631
|
| 20 |
+
# Page 313:
|
| 21 |
+
# Figure 205. Windows Paint Version 1: "DanM" Format
|
| 22 |
+
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
|
| 23 |
+
#
|
| 24 |
+
# See also: https://www.fileformat.info/format/mspaint/egff.htm
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import io
|
| 28 |
+
import struct
|
| 29 |
+
from typing import IO
|
| 30 |
+
|
| 31 |
+
from . import Image, ImageFile
|
| 32 |
+
from ._binary import i16le as i16
|
| 33 |
+
from ._binary import o16le as o16
|
| 34 |
+
|
| 35 |
+
#
|
| 36 |
+
# read MSP files
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _accept(prefix: bytes) -> bool:
|
| 40 |
+
return prefix.startswith((b"DanM", b"LinS"))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
##
|
| 44 |
+
# Image plugin for Windows MSP images. This plugin supports both
|
| 45 |
+
# uncompressed (Windows 1.0).
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class MspImageFile(ImageFile.ImageFile):
|
| 49 |
+
format = "MSP"
|
| 50 |
+
format_description = "Windows Paint"
|
| 51 |
+
|
| 52 |
+
def _open(self) -> None:
|
| 53 |
+
# Header
|
| 54 |
+
assert self.fp is not None
|
| 55 |
+
|
| 56 |
+
s = self.fp.read(32)
|
| 57 |
+
if not _accept(s):
|
| 58 |
+
msg = "not an MSP file"
|
| 59 |
+
raise SyntaxError(msg)
|
| 60 |
+
|
| 61 |
+
# Header checksum
|
| 62 |
+
checksum = 0
|
| 63 |
+
for i in range(0, 32, 2):
|
| 64 |
+
checksum = checksum ^ i16(s, i)
|
| 65 |
+
if checksum != 0:
|
| 66 |
+
msg = "bad MSP checksum"
|
| 67 |
+
raise SyntaxError(msg)
|
| 68 |
+
|
| 69 |
+
self._mode = "1"
|
| 70 |
+
self._size = i16(s, 4), i16(s, 6)
|
| 71 |
+
|
| 72 |
+
if s.startswith(b"DanM"):
|
| 73 |
+
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
|
| 74 |
+
else:
|
| 75 |
+
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class MspDecoder(ImageFile.PyDecoder):
|
| 79 |
+
# The algo for the MSP decoder is from
|
| 80 |
+
# https://www.fileformat.info/format/mspaint/egff.htm
|
| 81 |
+
# cc-by-attribution -- That page references is taken from the
|
| 82 |
+
# Encyclopedia of Graphics File Formats and is licensed by
|
| 83 |
+
# O'Reilly under the Creative Common/Attribution license
|
| 84 |
+
#
|
| 85 |
+
# For RLE encoded files, the 32byte header is followed by a scan
|
| 86 |
+
# line map, encoded as one 16bit word of encoded byte length per
|
| 87 |
+
# line.
|
| 88 |
+
#
|
| 89 |
+
# NOTE: the encoded length of the line can be 0. This was not
|
| 90 |
+
# handled in the previous version of this encoder, and there's no
|
| 91 |
+
# mention of how to handle it in the documentation. From the few
|
| 92 |
+
# examples I've seen, I've assumed that it is a fill of the
|
| 93 |
+
# background color, in this case, white.
|
| 94 |
+
#
|
| 95 |
+
#
|
| 96 |
+
# Pseudocode of the decoder:
|
| 97 |
+
# Read a BYTE value as the RunType
|
| 98 |
+
# If the RunType value is zero
|
| 99 |
+
# Read next byte as the RunCount
|
| 100 |
+
# Read the next byte as the RunValue
|
| 101 |
+
# Write the RunValue byte RunCount times
|
| 102 |
+
# If the RunType value is non-zero
|
| 103 |
+
# Use this value as the RunCount
|
| 104 |
+
# Read and write the next RunCount bytes literally
|
| 105 |
+
#
|
| 106 |
+
# e.g.:
|
| 107 |
+
# 0x00 03 ff 05 00 01 02 03 04
|
| 108 |
+
# would yield the bytes:
|
| 109 |
+
# 0xff ff ff 00 01 02 03 04
|
| 110 |
+
#
|
| 111 |
+
# which are then interpreted as a bit packed mode '1' image
|
| 112 |
+
|
| 113 |
+
_pulls_fd = True
|
| 114 |
+
|
| 115 |
+
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
| 116 |
+
assert self.fd is not None
|
| 117 |
+
|
| 118 |
+
img = io.BytesIO()
|
| 119 |
+
blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
|
| 120 |
+
try:
|
| 121 |
+
self.fd.seek(32)
|
| 122 |
+
rowmap = struct.unpack_from(
|
| 123 |
+
f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2)
|
| 124 |
+
)
|
| 125 |
+
except struct.error as e:
|
| 126 |
+
msg = "Truncated MSP file in row map"
|
| 127 |
+
raise OSError(msg) from e
|
| 128 |
+
|
| 129 |
+
for x, rowlen in enumerate(rowmap):
|
| 130 |
+
try:
|
| 131 |
+
if rowlen == 0:
|
| 132 |
+
img.write(blank_line)
|
| 133 |
+
continue
|
| 134 |
+
row = self.fd.read(rowlen)
|
| 135 |
+
if len(row) != rowlen:
|
| 136 |
+
msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}"
|
| 137 |
+
raise OSError(msg)
|
| 138 |
+
idx = 0
|
| 139 |
+
while idx < rowlen:
|
| 140 |
+
runtype = row[idx]
|
| 141 |
+
idx += 1
|
| 142 |
+
if runtype == 0:
|
| 143 |
+
(runcount, runval) = struct.unpack_from("Bc", row, idx)
|
| 144 |
+
img.write(runval * runcount)
|
| 145 |
+
idx += 2
|
| 146 |
+
else:
|
| 147 |
+
runcount = runtype
|
| 148 |
+
img.write(row[idx : idx + runcount])
|
| 149 |
+
idx += runcount
|
| 150 |
+
|
| 151 |
+
except struct.error as e:
|
| 152 |
+
msg = f"Corrupted MSP file in row {x}"
|
| 153 |
+
raise OSError(msg) from e
|
| 154 |
+
|
| 155 |
+
self.set_as_raw(img.getvalue(), "1")
|
| 156 |
+
|
| 157 |
+
return -1, 0
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
Image.register_decoder("MSP", MspDecoder)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
#
|
| 164 |
+
# write MSP files (uncompressed only)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
| 168 |
+
if im.mode != "1":
|
| 169 |
+
msg = f"cannot write mode {im.mode} as MSP"
|
| 170 |
+
raise OSError(msg)
|
| 171 |
+
|
| 172 |
+
# create MSP header
|
| 173 |
+
header = [0] * 16
|
| 174 |
+
|
| 175 |
+
header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1
|
| 176 |
+
header[2], header[3] = im.size
|
| 177 |
+
header[4], header[5] = 1, 1
|
| 178 |
+
header[6], header[7] = 1, 1
|
| 179 |
+
header[8], header[9] = im.size
|
| 180 |
+
|
| 181 |
+
checksum = 0
|
| 182 |
+
for h in header:
|
| 183 |
+
checksum = checksum ^ h
|
| 184 |
+
header[12] = checksum # FIXME: is this the right field?
|
| 185 |
+
|
| 186 |
+
# header
|
| 187 |
+
for h in header:
|
| 188 |
+
fp.write(o16(h))
|
| 189 |
+
|
| 190 |
+
# image body
|
| 191 |
+
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
#
|
| 195 |
+
# registry
|
| 196 |
+
|
| 197 |
+
Image.register_open(MspImageFile.format, MspImageFile, _accept)
|
| 198 |
+
Image.register_save(MspImageFile.format, _save)
|
| 199 |
+
|
| 200 |
+
Image.register_extension(MspImageFile.format, ".msp")
|