Add imagecrunch.

This commit is contained in:
Ethan Dalool 2019-08-03 00:49:15 -07:00
parent 1c4a73ca01
commit b47701ec3a
103 changed files with 25166 additions and 0 deletions

View file

@ -0,0 +1,133 @@
#
# The Python Imaging Library
# $Id$
#
# bitmap distribution font (bdf) file parser
#
# history:
# 1996-05-16 fl created (as bdf2pil)
# 1997-08-25 fl converted to FontFile driver
# 2001-05-25 fl removed bogus __init__ call
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
# 2003-04-22 fl more robustification (from Graham Dumpleton)
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1997-2003 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
from __future__ import print_function
from . import Image, FontFile
# --------------------------------------------------------------------
# parse X Bitmap Distribution Format (BDF)
# --------------------------------------------------------------------
bdf_slant = {
"R": "Roman",
"I": "Italic",
"O": "Oblique",
"RI": "Reverse Italic",
"RO": "Reverse Oblique",
"OT": "Other"
}
bdf_spacing = {
"P": "Proportional",
"M": "Monospaced",
"C": "Cell"
}
def bdf_char(f):
# skip to STARTCHAR
while True:
s = f.readline()
if not s:
return None
if s[:9] == b"STARTCHAR":
break
id = s[9:].strip().decode('ascii')
# load symbol properties
props = {}
while True:
s = f.readline()
if not s or s[:6] == b"BITMAP":
break
i = s.find(b" ")
props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii')
# load bitmap
bitmap = []
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
break
bitmap.append(s[:-1])
bitmap = b"".join(bitmap)
[x, y, l, d] = [int(p) for p in props["BBX"].split()]
[dx, dy] = [int(p) for p in props["DWIDTH"].split()]
bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y)
try:
im = Image.frombytes("1", (x, y), bitmap, "hex", "1")
except ValueError:
# deal with zero-width characters
im = Image.new("1", (x, y))
return id, int(props["ENCODING"]), bbox, im
##
# Font file plugin for the X11 BDF format.
class BdfFontFile(FontFile.FontFile):
def __init__(self, fp):
FontFile.FontFile.__init__(self)
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
raise SyntaxError("not a valid BDF file")
props = {}
comments = []
while True:
s = fp.readline()
if not s or s[:13] == b"ENDPROPERTIES":
break
i = s.find(b" ")
props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii')
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
if s.find(b"LogicalFontDescription") < 0:
comments.append(s[i+1:-1].decode('ascii'))
# font = props["FONT"].split("-")
# font[4] = bdf_slant[font[4].upper()]
# font[11] = bdf_spacing[font[11].upper()]
# ascent = int(props["FONT_ASCENT"])
# descent = int(props["FONT_DESCENT"])
# fontname = ";".join(font[1:])
# print("#", fontname)
# for i in comments:
# print("#", i)
while True:
c = bdf_char(fp)
if not c:
break
id, ch, (xy, dst, src), im = c
if 0 <= ch < len(self.glyph):
self.glyph[ch] = xy, dst, src, im

View file

@ -0,0 +1,435 @@
"""
Blizzard Mipmap Format (.blp)
Jerome Leclanche <jerome@leclan.ch>
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
BLP1 files, used mostly in Warcraft III, are not fully supported.
All types of BLP2 files used in World of Warcraft are supported.
The BLP file structure consists of a header, up to 16 mipmaps of the
texture
Texture sizes must be powers of two, though the two dimensions do
not have to be equal; 512x256 is valid, but 512x200 is not.
The first mipmap (mipmap #0) is the full size image; each subsequent
mipmap halves both dimensions. The final mipmap should be 1x1.
BLP files come in many different flavours:
* JPEG-compressed (type == 0) - only supported for BLP1.
* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
array of 8-bit values, one per pixel, left to right, top to bottom.
Each value is an index to the palette.
* DXT-compressed (type == 1, encoding == 2):
- DXT1 compression is used if alpha_encoding == 0.
- An additional alpha bit is used if alpha_depth == 1.
- DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7.
"""
import struct
from io import BytesIO
from . import Image, ImageFile
BLP_FORMAT_JPEG = 0
BLP_ENCODING_UNCOMPRESSED = 1
BLP_ENCODING_DXT = 2
BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3
BLP_ALPHA_ENCODING_DXT1 = 0
BLP_ALPHA_ENCODING_DXT3 = 1
BLP_ALPHA_ENCODING_DXT5 = 7
def unpack_565(i):
return (
((i >> 11) & 0x1f) << 3,
((i >> 5) & 0x3f) << 2,
(i & 0x1f) << 3
)
def decode_dxt1(data, alpha=False):
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
# Decode next 8-byte block.
idx = block * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
# Decode this block into 4x4 pixels
# Accumulate the results onto our 4 row accumulators
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
control = bits & 3
bits = bits >> 2
a = 0xFF
if control == 0:
r, g, b = r0, g0, b0
elif control == 1:
r, g, b = r1, g1, b1
elif control == 2:
if color0 > color1:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
else:
r = (r0 + r1) // 2
g = (g0 + g1) // 2
b = (b0 + b1) // 2
elif control == 3:
if color0 > color1:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
else:
r, g, b, a = 0, 0, 0, 0
if alpha:
ret[j].extend([r, g, b, a])
else:
ret[j].extend([r, g, b])
return ret
def decode_dxt3(data):
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
block = data[idx:idx + 16]
# Decode next 16-byte block.
bits = struct.unpack_from("<8B", block)
color0, color1 = struct.unpack_from("<HH", block, 8)
code, = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
high = False # Do we want the higher bits?
for i in range(4):
alphacode_index = (4 * j + i) // 2
a = bits[alphacode_index]
if high:
high = False
a >>= 4
else:
high = True
a &= 0xf
a *= 17 # We get a value between 0 and 15
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
def decode_dxt5(data):
"""
input: one "row" of data (i.e. will produce 4 * width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
block = data[idx:idx + 16]
# Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block)
bits = struct.unpack_from("<6B", block, 2)
alphacode1 = (
bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24)
)
alphacode2 = bits[0] | (bits[1] << 8)
color0, color1 = struct.unpack_from("<HH", block, 8)
code, = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
alphacode_index = 3 * (4 * j + i)
if alphacode_index <= 12:
alphacode = (alphacode2 >> alphacode_index) & 0x07
elif alphacode_index == 15:
alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
else: # alphacode_index >= 18 and alphacode_index <= 45
alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
if alphacode == 0:
a = a0
elif alphacode == 1:
a = a1
elif a0 > a1:
a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
elif alphacode == 6:
a = 0
elif alphacode == 7:
a = 255
else:
a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
class BLPFormatError(NotImplementedError):
pass
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
"""
format = "BLP"
format_description = "Blizzard Mipmap Format"
def _open(self):
self.magic = self.fp.read(4)
self._read_blp_header()
if self.magic == b"BLP1":
decoder = "BLP1"
self.mode = "RGB"
elif self.magic == b"BLP2":
decoder = "BLP2"
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
else:
raise BLPFormatError("Bad BLP magic %r" % (self.magic))
self.tile = [
(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
]
def _read_blp_header(self):
self._blp_compression, = struct.unpack("<i", self.fp.read(4))
self._blp_encoding, = struct.unpack("<b", self.fp.read(1))
self._blp_alpha_depth, = struct.unpack("<b", self.fp.read(1))
self._blp_alpha_encoding, = struct.unpack("<b", self.fp.read(1))
self._blp_mips, = struct.unpack("<b", self.fp.read(1))
self.size = struct.unpack("<II", self.fp.read(8))
if self.magic == b"BLP1":
# Only present for BLP1
self._blp_encoding, = struct.unpack("<i", self.fp.read(4))
self._blp_subtype, = struct.unpack("<i", self.fp.read(4))
self._blp_offsets = struct.unpack("<16I", self.fp.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fp.read(16 * 4))
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
try:
self.fd.seek(0)
self.magic = self.fd.read(4)
self._read_blp_header()
self._load()
except struct.error:
raise IOError("Truncated Blp file")
return 0, 0
def _read_palette(self):
ret = []
for i in range(256):
try:
b, g, r, a = struct.unpack("<4B", self.fd.read(4))
except struct.error:
break
ret.append((b, g, r, a))
return ret
def _read_blp_header(self):
self._blp_compression, = struct.unpack("<i", self.fd.read(4))
self._blp_encoding, = struct.unpack("<b", self.fd.read(1))
self._blp_alpha_depth, = struct.unpack("<b", self.fd.read(1))
self._blp_alpha_encoding, = struct.unpack("<b", self.fd.read(1))
self._blp_mips, = struct.unpack("<b", self.fd.read(1))
self.size = struct.unpack("<II", self.fd.read(8))
if self.magic == b"BLP1":
# Only present for BLP1
self._blp_encoding, = struct.unpack("<i", self.fd.read(4))
self._blp_subtype, = struct.unpack("<i", self.fd.read(4))
self._blp_offsets = struct.unpack("<16I", self.fd.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fd.read(16 * 4))
class BLP1Decoder(_BLPBaseDecoder):
def _load(self):
if self._blp_compression == BLP_FORMAT_JPEG:
self._decode_jpeg_stream()
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette()
_data = BytesIO(self.fd.read(self._blp_lengths[0]))
while True:
try:
offset, = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend([r, g, b])
self.set_as_raw(bytes(data))
else:
raise BLPFormatError(
"Unsupported BLP encoding %r" % (self._blp_encoding)
)
else:
raise BLPFormatError(
"Unsupported BLP compression %r" % (self._blp_encoding)
)
def _decode_jpeg_stream(self):
from PIL.JpegImagePlugin import JpegImageFile
jpeg_header_size, = struct.unpack("<I", self.fd.read(4))
jpeg_header = self.fd.read(jpeg_header_size)
self.fd.read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self.fd.read(self._blp_lengths[0])
data = jpeg_header + data
data = BytesIO(data)
image = JpegImageFile(data)
self.tile = image.tile # :/
self.fd = image.fp
self.mode = image.mode
class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
palette = self._read_palette()
data = bytearray()
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == BLP_ENCODING_UNCOMPRESSED:
_data = BytesIO(self.fd.read(self._blp_lengths[0]))
while True:
try:
offset, = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend((r, g, b))
elif self._blp_encoding == BLP_ENCODING_DXT:
if self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
self.fd.read(linesize),
alpha=bool(self._blp_alpha_depth)
):
data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt3(self.fd.read(linesize)):
data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt5(self.fd.read(linesize)):
data += d
else:
raise BLPFormatError("Unsupported alpha encoding %r" % (
self._blp_alpha_encoding
))
else:
raise BLPFormatError(
"Unknown BLP encoding %r" % (self._blp_encoding)
)
else:
raise BLPFormatError(
"Unknown BLP compression %r" % (self._blp_compression)
)
self.set_as_raw(bytes(data))
Image.register_open(
BlpImageFile.format, BlpImageFile, lambda p: p[:4] in (b"BLP1", b"BLP2")
)
Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder)

View file

@ -0,0 +1,289 @@
#
# The Python Imaging Library.
# $Id$
#
# BMP file handler
#
# Windows (and OS/2) native bitmap storage format.
#
# history:
# 1995-09-01 fl Created
# 1996-04-30 fl Added save
# 1997-08-27 fl Fixed save of 1-bit images
# 1998-03-06 fl Load P images as L where possible
# 1998-07-03 fl Load P images as 1 where possible
# 1998-12-29 fl Handle small palettes
# 2002-12-30 fl Fixed load of 1-bit palette images
# 2003-04-21 fl Fixed load of 1-bit monochrome images
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
#
# Copyright (c) 1997-2003 by Secret Labs AB
# Copyright (c) 1995-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile, ImagePalette
from ._binary import i8, i16le as i16, i32le as i32, \
o8, o16le as o16, o32le as o32
import math
__version__ = "0.7"
#
# --------------------------------------------------------------------
# Read BMP file
BIT2MODE = {
# bits => mode, rawmode
1: ("P", "P;1"),
4: ("P", "P;4"),
8: ("P", "P"),
16: ("RGB", "BGR;15"),
24: ("RGB", "BGR"),
32: ("RGB", "BGRX"),
}
def _accept(prefix):
return prefix[:2] == b"BM"
# ==============================================================================
# Image plugin for the Windows BMP format.
# ==============================================================================
class BmpImageFile(ImageFile.ImageFile):
""" Image plugin for the Windows Bitmap format (BMP) """
# -------------------------------------------------------------- Description
format_description = "Windows Bitmap"
format = "BMP"
# --------------------------------------------------- BMP Compression values
COMPRESSIONS = {'RAW': 0, 'RLE8': 1, 'RLE4': 2, 'BITFIELDS': 3, 'JPEG': 4, 'PNG': 5}
RAW, RLE8, RLE4, BITFIELDS, JPEG, PNG = 0, 1, 2, 3, 4, 5
def _bitmap(self, header=0, offset=0):
""" Read relevant info about the BMP """
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
file_info = {}
file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size)
file_info['direction'] = -1
# --------------------- If requested, read header at a specific position
header_data = ImageFile._safe_read(self.fp, file_info['header_size'] - 4) # read the rest of the bmp header, without its size
# --------------------------------------------------- IBM OS/2 Bitmap v1
# ------ This format has different offsets because of width/height types
if file_info['header_size'] == 12:
file_info['width'] = i16(header_data[0:2])
file_info['height'] = i16(header_data[2:4])
file_info['planes'] = i16(header_data[4:6])
file_info['bits'] = i16(header_data[6:8])
file_info['compression'] = self.RAW
file_info['palette_padding'] = 3
# ---------------------------------------------- Windows Bitmap v2 to v5
elif file_info['header_size'] in (40, 64, 108, 124): # v3, OS/2 v2, v4, v5
if file_info['header_size'] >= 40: # v3 and OS/2
file_info['y_flip'] = i8(header_data[7]) == 0xff
file_info['direction'] = 1 if file_info['y_flip'] else -1
file_info['width'] = i32(header_data[0:4])
file_info['height'] = i32(header_data[4:8]) if not file_info['y_flip'] else 2**32 - i32(header_data[4:8])
file_info['planes'] = i16(header_data[8:10])
file_info['bits'] = i16(header_data[10:12])
file_info['compression'] = i32(header_data[12:16])
file_info['data_size'] = i32(header_data[16:20]) # byte size of pixel data
file_info['pixels_per_meter'] = (i32(header_data[20:24]), i32(header_data[24:28]))
file_info['colors'] = i32(header_data[28:32])
file_info['palette_padding'] = 4
self.info["dpi"] = tuple(
map(lambda x: int(math.ceil(x / 39.3701)),
file_info['pixels_per_meter']))
if file_info['compression'] == self.BITFIELDS:
if len(header_data) >= 52:
for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']):
file_info[mask] = i32(header_data[36+idx*4:40+idx*4])
else:
# 40 byte headers only have the three components in the bitfields masks,
# ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
# See also https://github.com/python-pillow/Pillow/issues/1293
# There is a 4th component in the RGBQuad, in the alpha location, but it
# is listed as a reserved component, and it is not generally an alpha channel
file_info['a_mask'] = 0x0
for mask in ['r_mask', 'g_mask', 'b_mask']:
file_info[mask] = i32(read(4))
file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'])
file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask'])
else:
raise IOError("Unsupported BMP header type (%d)" % file_info['header_size'])
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
self.size = file_info['width'], file_info['height']
# -------- If color count was not found in the header, compute from bits
file_info['colors'] = file_info['colors'] if file_info.get('colors', 0) else (1 << file_info['bits'])
# -------------------------------- Check abnormal values for DOS attacks
if file_info['width'] * file_info['height'] > 2**31:
raise IOError("Unsupported BMP Size: (%dx%d)" % self.size)
# ----------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None))
if self.mode is None:
raise IOError("Unsupported BMP pixel depth (%d)" % file_info['bits'])
# ----------------- Process BMP with Bitfields compression (not palette)
if file_info['compression'] == self.BITFIELDS:
SUPPORTED = {
32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0)],
24: [(0xff0000, 0xff00, 0xff)],
16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)]
}
MASK_MODES = {
(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
(32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
(32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xff0000, 0xff00, 0xff)): "BGR",
(16, (0xf800, 0x7e0, 0x1f)): "BGR;16",
(16, (0x7c00, 0x3e0, 0x1f)): "BGR;15"
}
if file_info['bits'] in SUPPORTED:
if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]:
raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])]
self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode
elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]:
raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])]
else:
raise IOError("Unsupported BMP bitfields layout")
else:
raise IOError("Unsupported BMP bitfields layout")
elif file_info['compression'] == self.RAW:
if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA"
else:
raise IOError("Unsupported BMP compression (%d)" % file_info['compression'])
# ---------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ----------------------------------------------------- 1-bit images
if not (0 < file_info['colors'] <= 65536):
raise IOError("Unsupported BMP Palette size (%d)" % file_info['colors'])
else:
padding = file_info['palette_padding']
palette = read(padding * file_info['colors'])
greyscale = True
indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors']))
# ------------------ Check if greyscale and ignore palette if so
for ind, val in enumerate(indices):
rgb = palette[ind*padding:ind*padding + 3]
if rgb != o8(val) * 3:
greyscale = False
# -------- If all colors are grey, white or black, ditch palette
if greyscale:
self.mode = "1" if file_info['colors'] == 2 else "L"
raw_mode = self.mode
else:
self.mode = "P"
self.palette = ImagePalette.raw("BGRX" if padding == 4 else "BGR", palette)
# ----------------------------- Finally set the tile data for the plugin
self.info['compression'] = file_info['compression']
self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), offset or self.fp.tell(),
(raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction'])
)]
def _open(self):
""" Open file, check magic number and read header """
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if head_data[0:2] != b"BM":
raise SyntaxError("Not a BMP file")
# read the start position of the BMP image data (u32)
offset = i32(head_data[10:14])
# load bitmap information (offset=raster info)
self._bitmap(offset=offset)
# ==============================================================================
# Image plugin for the DIB format (BMP alias)
# ==============================================================================
class DibImageFile(BmpImageFile):
format = "DIB"
format_description = "Windows Bitmap"
def _open(self):
self._bitmap()
#
# --------------------------------------------------------------------
# Write BMP file
SAVE = {
"1": ("1", 1, 2),
"L": ("L", 8, 256),
"P": ("P", 8, 256),
"RGB": ("BGR", 24, 0),
"RGBA": ("BGRA", 32, 0),
}
def _save(im, fp, filename):
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError:
raise IOError("cannot write mode %s as BMP" % im.mode)
info = im.encoderinfo
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
stride = ((im.size[0]*bits+7)//8+3) & (~3)
header = 40 # or 64 for OS/2 version 2
offset = 14 + header + colors * 4
image = stride * im.size[1]
# bitmap header
fp.write(b"BM" + # file type (magic)
o32(offset+image) + # file size
o32(0) + # reserved
o32(offset)) # image data offset
# bitmap info header
fp.write(o32(header) + # info header size
o32(im.size[0]) + # width
o32(im.size[1]) + # height
o16(1) + # planes
o16(bits) + # depth
o32(0) + # compression (0=uncompressed)
o32(image) + # size of bitmap
o32(ppm[0]) + o32(ppm[1]) + # resolution
o32(colors) + # colors used
o32(colors)) # colors important
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if im.mode == "1":
for i in (0, 255):
fp.write(o8(i) * 4)
elif im.mode == "L":
for i in range(256):
fp.write(o8(i) * 4)
elif im.mode == "P":
fp.write(im.im.getpalette("RGB", "BGRX"))
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0,
(rawmode, stride, -1))])
#
# --------------------------------------------------------------------
# Registry
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
Image.register_save(BmpImageFile.format, _save)
Image.register_extension(BmpImageFile.format, ".bmp")
Image.register_mime(BmpImageFile.format, "image/bmp")

View file

@ -0,0 +1,72 @@
#
# The Python Imaging Library
# $Id$
#
# BUFR stub adapter
#
# Copyright (c) 1996-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific BUFR image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR"
format_description = "BUFR"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
raise SyntaxError("Not a BUFR file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self.size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
raise IOError("BUFR save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
Image.register_save(BufrStubImageFile.format, _save)
Image.register_extension(BufrStubImageFile.format, ".bufr")

View file

@ -0,0 +1,116 @@
#
# The Python Imaging Library.
# $Id$
#
# a class to read from a container file
#
# History:
# 1995-06-18 fl Created
# 1995-09-07 fl Added readline(), readlines()
#
# Copyright (c) 1997-2001 by Secret Labs AB
# Copyright (c) 1995 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
##
# A file object that provides read access to a part of an existing
# file (for example a TAR file).
class ContainerIO(object):
def __init__(self, file, offset, length):
"""
Create file object.
:param file: Existing file.
:param offset: Start of region, in bytes.
:param length: Size of region, in bytes.
"""
self.fh = file
self.pos = 0
self.offset = offset
self.length = length
self.fh.seek(offset)
##
# Always false.
def isatty(self):
return 0
def seek(self, offset, mode=0):
"""
Move file pointer.
:param offset: Offset in bytes.
:param mode: Starting position. Use 0 for beginning of region, 1
for current offset, and 2 for end of region. You cannot move
the pointer outside the defined region.
"""
if mode == 1:
self.pos = self.pos + offset
elif mode == 2:
self.pos = self.length + offset
else:
self.pos = offset
# clamp
self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos)
def tell(self):
"""
Get current file pointer.
:returns: Offset from start of region, in bytes.
"""
return self.pos
def read(self, n=0):
"""
Read data.
:param n: Number of bytes to read. If omitted or zero,
read until end of region.
:returns: An 8-bit string.
"""
if n:
n = min(n, self.length - self.pos)
else:
n = self.length - self.pos
if not n: # EOF
return ""
self.pos = self.pos + n
return self.fh.read(n)
def readline(self):
"""
Read a line of text.
:returns: An 8-bit string.
"""
s = ""
while True:
c = self.read(1)
if not c:
break
s = s + c
if c == "\n":
break
return s
def readlines(self):
"""
Read multiple lines of text.
:returns: A list of 8-bit strings.
"""
l = []
while True:
s = self.readline()
if not s:
break
l.append(s)
return l

View file

@ -0,0 +1,86 @@
#
# The Python Imaging Library.
# $Id$
#
# Windows Cursor support for PIL
#
# notes:
# uses BmpImagePlugin.py to read the bitmap data.
#
# history:
# 96-05-27 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from __future__ import print_function
from . import Image, BmpImagePlugin
from ._binary import i8, i16le as i16, i32le as i32
__version__ = "0.1"
#
# --------------------------------------------------------------------
def _accept(prefix):
return prefix[:4] == b"\0\0\2\0"
##
# Image plugin for Windows Cursor files.
class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR"
format_description = "Windows Cursor"
def _open(self):
offset = self.fp.tell()
# check magic
s = self.fp.read(6)
if not _accept(s):
raise SyntaxError("not a CUR file")
# pick the largest cursor in the file
m = b""
for i in range(i16(s[4:])):
s = self.fp.read(16)
if not m:
m = s
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
m = s
# print("width", i8(s[0]))
# print("height", i8(s[1]))
# print("colors", i8(s[2]))
# print("reserved", i8(s[3]))
# print("hotspot x", i16(s[4:]))
# print("hotspot y", i16(s[6:]))
# print("bytes", i32(s[8:]))
# print("offset", i32(s[12:]))
if not m:
raise TypeError("No cursors were found")
# load as bitmap
self._bitmap(i32(m[12:]) + offset)
# patch up the bitmap height
self.size = self.size[0], self.size[1]//2
d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0)+self.size, o, a
return
#
# --------------------------------------------------------------------
Image.register_open(CurImageFile.format, CurImageFile, _accept)
Image.register_extension(CurImageFile.format, ".cur")

View file

@ -0,0 +1,87 @@
#
# The Python Imaging Library.
# $Id$
#
# DCX file handling
#
# DCX is a container file format defined by Intel, commonly used
# for fax applications. Each DCX file consists of a directory
# (a list of file offsets) followed by a set of (usually 1-bit)
# PCX files.
#
# History:
# 1995-09-09 fl Created
# 1996-03-20 fl Properly derived from PcxImageFile.
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 2002-07-30 fl Fixed file handling
#
# Copyright (c) 1997-98 by Secret Labs AB.
# Copyright (c) 1995-96 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
from . import Image
from ._binary import i32le as i32
from .PcxImagePlugin import PcxImageFile
__version__ = "0.2"
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix):
return len(prefix) >= 4 and i32(prefix) == MAGIC
##
# Image plugin for the Intel DCX format.
class DcxImageFile(PcxImageFile):
format = "DCX"
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
def _open(self):
# Header
s = self.fp.read(4)
if i32(s) != MAGIC:
raise SyntaxError("not a DCX file")
# Component directory
self._offset = []
for i in range(1024):
offset = i32(self.fp.read(4))
if not offset:
break
self._offset.append(offset)
self.__fp = self.fp
self.frame = None
self.seek(0)
@property
def n_frames(self):
return len(self._offset)
@property
def is_animated(self):
return len(self._offset) > 1
def seek(self, frame):
if not self._seek_check(frame):
return
self.frame = frame
self.fp = self.__fp
self.fp.seek(self._offset[frame])
PcxImageFile._open(self)
def tell(self):
return self.frame
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
Image.register_extension(DcxImageFile.format, ".dcx")

View file

@ -0,0 +1,172 @@
"""
A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch>
Documentation:
https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
"""
import struct
from io import BytesIO
from . import Image, ImageFile
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
# DDS flags
DDSD_CAPS = 0x1
DDSD_HEIGHT = 0x2
DDSD_WIDTH = 0x4
DDSD_PITCH = 0x8
DDSD_PIXELFORMAT = 0x1000
DDSD_MIPMAPCOUNT = 0x20000
DDSD_LINEARSIZE = 0x80000
DDSD_DEPTH = 0x800000
# DDS caps
DDSCAPS_COMPLEX = 0x8
DDSCAPS_TEXTURE = 0x1000
DDSCAPS_MIPMAP = 0x400000
DDSCAPS2_CUBEMAP = 0x200
DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
DDSCAPS2_VOLUME = 0x200000
# Pixel Format
DDPF_ALPHAPIXELS = 0x1
DDPF_ALPHA = 0x2
DDPF_FOURCC = 0x4
DDPF_PALETTEINDEXED8 = 0x20
DDPF_RGB = 0x40
DDPF_LUMINANCE = 0x20000
# dds.h
DDS_FOURCC = DDPF_FOURCC
DDS_RGB = DDPF_RGB
DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
DDS_LUMINANCE = DDPF_LUMINANCE
DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
DDS_ALPHA = DDPF_ALPHA
DDS_PAL8 = DDPF_PALETTEINDEXED8
DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH |
DDSD_PIXELFORMAT)
DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
DDS_HEIGHT = DDSD_HEIGHT
DDS_WIDTH = DDSD_WIDTH
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
# DXT1
DXT1_FOURCC = 0x31545844
# DXT3
DXT3_FOURCC = 0x33545844
# DXT5
DXT5_FOURCC = 0x35545844
# dxgiformat.h
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"
def _open(self):
magic, header_size = struct.unpack("<II", self.fp.read(8))
if header_size != 124:
raise IOError("Unsupported header size %r" % (header_size))
header_bytes = self.fp.read(header_size - 4)
if len(header_bytes) != 120:
raise IOError("Incomplete header: %s bytes" % len(header_bytes))
header = BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12))
self.size = (width, height)
self.mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
reserved = struct.unpack("<11I", header.read(44))
# pixel format
pfsize, pfflags = struct.unpack("<2I", header.read(8))
fourcc = header.read(4)
bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I",
header.read(20))
data_start = header_size + 4
n = 0
if fourcc == b"DXT1":
self.pixel_format = "DXT1"
n = 1
elif fourcc == b"DXT3":
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
dxt10 = BytesIO(self.fp.read(20))
dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.im_info["gamma"] = 1/2.2
n = 7
else:
raise NotImplementedError("Unimplemented DXGI format %d" %
(dxgi_format))
else:
raise NotImplementedError("Unimplemented pixel format %r" %
(fourcc))
self.tile = [
("bcn", (0, 0) + self.size, data_start, (n))
]
def load_seek(self, pos):
pass
def _validate(prefix):
return prefix[:4] == b"DDS "
Image.register_open(DdsImageFile.format, DdsImageFile, _validate)
Image.register_extension(DdsImageFile.format, ".dds")

View file

@ -0,0 +1,429 @@
#
# The Python Imaging Library.
# $Id$
#
# EPS file handling
#
# History:
# 1995-09-01 fl Created (0.1)
# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
# 1996-08-22 fl Don't choke on floating point BoundingBox values
# 1996-08-23 fl Handle files from Macintosh (0.3)
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
# resizing
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1995-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import re
import io
import os
import sys
from . import Image, ImageFile
from ._binary import i32le as i32
from ._util import py3
__version__ = "0.5"
#
# --------------------------------------------------------------------
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
gs_windows_binary = None
if sys.platform.startswith('win'):
import shutil
if hasattr(shutil, 'which'):
which = shutil.which
else:
# Python < 3.3
import distutils.spawn
which = distutils.spawn.find_executable
for binary in ('gswin32c', 'gswin64c', 'gs'):
if which(binary) is not None:
gs_windows_binary = binary
break
else:
gs_windows_binary = False
def has_ghostscript():
if gs_windows_binary:
return True
if not sys.platform.startswith('win'):
import subprocess
try:
with open(os.devnull, 'wb') as devnull:
subprocess.check_call(['gs', '--version'], stdout=devnull)
return True
except OSError:
# no ghostscript
pass
return False
def Ghostscript(tile, size, fp, scale=1):
"""Render an image using Ghostscript"""
# Unpack decoder tile
decoder, tile, offset, data = tile[0]
length, bbox = data
# Hack to support hi-res rendering
scale = int(scale) or 1
# orig_size = size
# orig_bbox = bbox
size = (size[0] * scale, size[1] * scale)
# resolution is dependent on bbox and size
res = (float((72.0 * size[0]) / (bbox[2]-bbox[0])),
float((72.0 * size[1]) / (bbox[3]-bbox[1])))
# print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res)
import subprocess
import tempfile
out_fd, outfile = tempfile.mkstemp()
os.close(out_fd)
infile_temp = None
if hasattr(fp, 'name') and os.path.exists(fp.name):
infile = fp.name
else:
in_fd, infile_temp = tempfile.mkstemp()
os.close(in_fd)
infile = infile_temp
# ignore length and offset!
# ghostscript can read it
# copy whole file to read in ghostscript
with open(infile_temp, 'wb') as f:
# fetch length of fp
fp.seek(0, 2)
fsize = fp.tell()
# ensure start position
# go back
fp.seek(0)
lengthfile = fsize
while lengthfile > 0:
s = fp.read(min(lengthfile, 100*1024))
if not s:
break
lengthfile -= len(s)
f.write(s)
# Build ghostscript command
command = ["gs",
"-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch)
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages,
"-dSAFER", # safe mode
"-sDEVICE=ppmraw", # ppm driver
"-sOutputFile=%s" % outfile, # output file
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
# adjust for image origin
"-f", infile, # input file
"-c", "showpage", # showpage (see: https://bugs.ghostscript.com/show_bug.cgi?id=698272)
]
if gs_windows_binary is not None:
if not gs_windows_binary:
raise WindowsError('Unable to locate Ghostscript on paths')
command[0] = gs_windows_binary
# push data through ghostscript
try:
with open(os.devnull, 'w+b') as devnull:
subprocess.check_call(command, stdin=devnull, stdout=devnull)
im = Image.open(outfile)
im.load()
finally:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
except OSError:
pass
return im.im.copy()
class PSFile(object):
"""
Wrapper for bytesio object that treats either CR or LF as end of line.
"""
def __init__(self, fp):
self.fp = fp
self.char = None
def seek(self, offset, whence=0):
self.char = None
self.fp.seek(offset, whence)
def readline(self):
s = self.char or b""
self.char = None
c = self.fp.read(1)
while c not in b"\r\n":
s = s + c
c = self.fp.read(1)
self.char = self.fp.read(1)
# line endings can be 1 or 2 of \r \n, in either order
if self.char in b"\r\n":
self.char = None
return s.decode('latin-1')
def _accept(prefix):
return prefix[:4] == b"%!PS" or \
(len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
##
# Image plugin for Encapsulated Postscript. This plugin supports only
# a few variants of this format.
class EpsImageFile(ImageFile.ImageFile):
"""EPS File Parser for the Python Imaging Library"""
format = "EPS"
format_description = "Encapsulated Postscript"
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self):
(length, offset) = self._find_offset(self.fp)
# Rewrap the open file pointer in something that will
# convert line endings and decode to latin-1.
try:
if py3:
# Python3, can use bare open command.
fp = open(self.fp.name, "Ur", encoding='latin-1')
else:
# Python2, no encoding conversion necessary
fp = open(self.fp.name, "Ur")
except:
# Expect this for bytesio/stringio
fp = PSFile(self.fp)
# go to offset - start of "%!PS"
fp.seek(offset)
box = None
self.mode = "RGB"
self.size = 1, 1 # FIXME: huh?
#
# Load EPS header
s_raw = fp.readline()
s = s_raw.strip('\r\n')
while s_raw:
if s:
if len(s) > 255:
raise SyntaxError("not an EPS file")
try:
m = split.match(s)
except re.error as v:
raise SyntaxError("not an EPS file")
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self.size = box[2] - box[0], box[3] - box[1]
self.tile = [("eps", (0, 0) + self.size, offset,
(length, box))]
except:
pass
else:
m = field.match(s)
if m:
k = m.group(1)
if k == "EndComments":
break
if k[:8] == "PS-Adobe":
self.info[k[:8]] = k[9:]
else:
self.info[k] = ""
elif s[0] == '%':
# handle non-DSC Postscript comments that some
# tools mistakenly put in the Comments section
pass
else:
raise IOError("bad EPS header")
s_raw = fp.readline()
s = s_raw.strip('\r\n')
if s and s[:1] != "%":
break
#
# Scan for an "ImageData" descriptor
while s[:1] == "%":
if len(s) > 255:
raise SyntaxError("not an EPS file")
if s[:11] == "%ImageData:":
# Encoded bitmapped image.
x, y, bi, mo = s[11:].split(None, 7)[:4]
if int(bi) != 8:
break
try:
self.mode = self.mode_map[int(mo)]
except ValueError:
break
self.size = int(x), int(y)
return
s = fp.readline().strip('\r\n')
if not s:
break
if not box:
raise IOError("cannot determine EPS bounding box")
def _find_offset(self, fp):
s = fp.read(160)
if s[:4] == b"%!PS":
# for HEAD without binary preview
fp.seek(0, 2)
length = fp.tell()
offset = 0
elif i32(s[0:4]) == 0xC6D3D0C5:
# FIX for: Some EPS file not handled correctly / issue #302
# EPS can contain binary data
# or start directly with latin coding
# more info see:
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
offset = i32(s[4:8])
length = i32(s[8:12])
else:
raise SyntaxError("not an EPS file")
return (length, offset)
def load(self, scale=1):
# Load EPS via Ghostscript
if not self.tile:
return
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
self.mode = self.im.mode
self.size = self.im.size
self.tile = []
def load_seek(self, *args, **kwargs):
# we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method.
pass
#
# --------------------------------------------------------------------
def _save(im, fp, filename, eps=1):
"""EPS Writer for the Python Imaging Library."""
#
# make sure image data is available
im.load()
#
# determine postscript image mode
if im.mode == "L":
operator = (8, 1, "image")
elif im.mode == "RGB":
operator = (8, 3, "false 3 colorimage")
elif im.mode == "CMYK":
operator = (8, 4, "false 4 colorimage")
else:
raise ValueError("image mode is not supported")
class NoCloseStream(object):
def __init__(self, fp):
self.fp = fp
def __getattr__(self, name):
return getattr(self.fp, name)
def close(self):
pass
base_fp = fp
if fp != sys.stdout:
fp = NoCloseStream(fp)
if sys.version_info.major > 2:
fp = io.TextIOWrapper(fp, encoding='latin-1')
if eps:
#
# write EPS header
fp.write("%!PS-Adobe-3.0 EPSF-3.0\n")
fp.write("%%Creator: PIL 0.1 EpsEncode\n")
# fp.write("%%CreationDate: %s"...)
fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size)
fp.write("%%Pages: 1\n")
fp.write("%%EndComments\n")
fp.write("%%Page: 1 1\n")
fp.write("%%ImageData: %d %d " % im.size)
fp.write("%d %d 0 1 1 \"%s\"\n" % operator)
#
# image header
fp.write("gsave\n")
fp.write("10 dict begin\n")
fp.write("/buf %d string def\n" % (im.size[0] * operator[1]))
fp.write("%d %d scale\n" % im.size)
fp.write("%d %d 8\n" % im.size) # <= bits
fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
fp.write("{ currentfile buf readhexstring pop } bind\n")
fp.write(operator[2] + "\n")
if hasattr(fp, "flush"):
fp.flush()
ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)])
fp.write("\n%%%%EndBinary\n")
fp.write("grestore end\n")
if hasattr(fp, "flush"):
fp.flush()
#
# --------------------------------------------------------------------
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
Image.register_save(EpsImageFile.format, _save)
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
Image.register_mime(EpsImageFile.format, "application/postscript")

315
imagecrunch/PIL/ExifTags.py Normal file
View file

@ -0,0 +1,315 @@
#
# The Python Imaging Library.
# $Id$
#
# EXIF tags
#
# Copyright (c) 2003 by Secret Labs AB
#
# See the README file for information on usage and redistribution.
#
##
# This module provides constants and clear-text names for various
# well-known EXIF tags.
##
##
# Maps EXIF tags to tag names.
TAGS = {
# possibly incomplete
0x000b: "ProcessingSoftware",
0x00fe: "NewSubfileType",
0x00ff: "SubfileType",
0x0100: "ImageWidth",
0x0101: "ImageLength",
0x0102: "BitsPerSample",
0x0103: "Compression",
0x0106: "PhotometricInterpretation",
0x0107: "Thresholding",
0x0108: "CellWidth",
0x0109: "CellLength",
0x010a: "FillOrder",
0x010d: "DocumentName",
0x010e: "ImageDescription",
0x010f: "Make",
0x0110: "Model",
0x0111: "StripOffsets",
0x0112: "Orientation",
0x0115: "SamplesPerPixel",
0x0116: "RowsPerStrip",
0x0117: "StripByteCounts",
0x0118: "MinSampleValue",
0x0119: "MaxSampleValue",
0x011a: "XResolution",
0x011b: "YResolution",
0x011c: "PlanarConfiguration",
0x011d: "PageName",
0x0120: "FreeOffsets",
0x0121: "FreeByteCounts",
0x0122: "GrayResponseUnit",
0x0123: "GrayResponseCurve",
0x0124: "T4Options",
0x0125: "T6Options",
0x0128: "ResolutionUnit",
0x0129: "PageNumber",
0x012d: "TransferFunction",
0x0131: "Software",
0x0132: "DateTime",
0x013b: "Artist",
0x013c: "HostComputer",
0x013d: "Predictor",
0x013e: "WhitePoint",
0x013f: "PrimaryChromaticities",
0x0140: "ColorMap",
0x0141: "HalftoneHints",
0x0142: "TileWidth",
0x0143: "TileLength",
0x0144: "TileOffsets",
0x0145: "TileByteCounts",
0x014a: "SubIFDs",
0x014c: "InkSet",
0x014d: "InkNames",
0x014e: "NumberOfInks",
0x0150: "DotRange",
0x0151: "TargetPrinter",
0x0152: "ExtraSamples",
0x0153: "SampleFormat",
0x0154: "SMinSampleValue",
0x0155: "SMaxSampleValue",
0x0156: "TransferRange",
0x0157: "ClipPath",
0x0158: "XClipPathUnits",
0x0159: "YClipPathUnits",
0x015a: "Indexed",
0x015b: "JPEGTables",
0x015f: "OPIProxy",
0x0200: "JPEGProc",
0x0201: "JpegIFOffset",
0x0202: "JpegIFByteCount",
0x0203: "JpegRestartInterval",
0x0205: "JpegLosslessPredictors",
0x0206: "JpegPointTransforms",
0x0207: "JpegQTables",
0x0208: "JpegDCTables",
0x0209: "JpegACTables",
0x0211: "YCbCrCoefficients",
0x0212: "YCbCrSubSampling",
0x0213: "YCbCrPositioning",
0x0214: "ReferenceBlackWhite",
0x02bc: "XMLPacket",
0x1000: "RelatedImageFileFormat",
0x1001: "RelatedImageWidth",
0x1002: "RelatedImageLength",
0x4746: "Rating",
0x4749: "RatingPercent",
0x800d: "ImageID",
0x828d: "CFARepeatPatternDim",
0x828e: "CFAPattern",
0x828f: "BatteryLevel",
0x8298: "Copyright",
0x829a: "ExposureTime",
0x829d: "FNumber",
0x83bb: "IPTCNAA",
0x8649: "ImageResources",
0x8769: "ExifOffset",
0x8773: "InterColorProfile",
0x8822: "ExposureProgram",
0x8824: "SpectralSensitivity",
0x8825: "GPSInfo",
0x8827: "ISOSpeedRatings",
0x8828: "OECF",
0x8829: "Interlace",
0x882a: "TimeZoneOffset",
0x882b: "SelfTimerMode",
0x9000: "ExifVersion",
0x9003: "DateTimeOriginal",
0x9004: "DateTimeDigitized",
0x9101: "ComponentsConfiguration",
0x9102: "CompressedBitsPerPixel",
0x9201: "ShutterSpeedValue",
0x9202: "ApertureValue",
0x9203: "BrightnessValue",
0x9204: "ExposureBiasValue",
0x9205: "MaxApertureValue",
0x9206: "SubjectDistance",
0x9207: "MeteringMode",
0x9208: "LightSource",
0x9209: "Flash",
0x920a: "FocalLength",
0x920b: "FlashEnergy",
0x920c: "SpatialFrequencyResponse",
0x920d: "Noise",
0x9211: "ImageNumber",
0x9212: "SecurityClassification",
0x9213: "ImageHistory",
0x9214: "SubjectLocation",
0x9215: "ExposureIndex",
0x9216: "TIFF/EPStandardID",
0x927c: "MakerNote",
0x9286: "UserComment",
0x9290: "SubsecTime",
0x9291: "SubsecTimeOriginal",
0x9292: "SubsecTimeDigitized",
0x9c9b: "XPTitle",
0x9c9c: "XPComment",
0x9c9d: "XPAuthor",
0x9c9e: "XPKeywords",
0x9c9f: "XPSubject",
0xa000: "FlashPixVersion",
0xa001: "ColorSpace",
0xa002: "ExifImageWidth",
0xa003: "ExifImageHeight",
0xa004: "RelatedSoundFile",
0xa005: "ExifInteroperabilityOffset",
0xa20b: "FlashEnergy",
0xa20c: "SpatialFrequencyResponse",
0xa20e: "FocalPlaneXResolution",
0xa20f: "FocalPlaneYResolution",
0xa210: "FocalPlaneResolutionUnit",
0xa214: "SubjectLocation",
0xa215: "ExposureIndex",
0xa217: "SensingMethod",
0xa300: "FileSource",
0xa301: "SceneType",
0xa302: "CFAPattern",
0xa401: "CustomRendered",
0xa402: "ExposureMode",
0xa403: "WhiteBalance",
0xa404: "DigitalZoomRatio",
0xa405: "FocalLengthIn35mmFilm",
0xa406: "SceneCaptureType",
0xa407: "GainControl",
0xa408: "Contrast",
0xa409: "Saturation",
0xa40a: "Sharpness",
0xa40b: "DeviceSettingDescription",
0xa40c: "SubjectDistanceRange",
0xa420: "ImageUniqueID",
0xa430: "CameraOwnerName",
0xa431: "BodySerialNumber",
0xa432: "LensSpecification",
0xa433: "LensMake",
0xa434: "LensModel",
0xa435: "LensSerialNumber",
0xa500: "Gamma",
0xc4a5: "PrintImageMatching",
0xc612: "DNGVersion",
0xc613: "DNGBackwardVersion",
0xc614: "UniqueCameraModel",
0xc615: "LocalizedCameraModel",
0xc616: "CFAPlaneColor",
0xc617: "CFALayout",
0xc618: "LinearizationTable",
0xc619: "BlackLevelRepeatDim",
0xc61a: "BlackLevel",
0xc61b: "BlackLevelDeltaH",
0xc61c: "BlackLevelDeltaV",
0xc61d: "WhiteLevel",
0xc61e: "DefaultScale",
0xc61f: "DefaultCropOrigin",
0xc620: "DefaultCropSize",
0xc621: "ColorMatrix1",
0xc622: "ColorMatrix2",
0xc623: "CameraCalibration1",
0xc624: "CameraCalibration2",
0xc625: "ReductionMatrix1",
0xc626: "ReductionMatrix2",
0xc627: "AnalogBalance",
0xc628: "AsShotNeutral",
0xc629: "AsShotWhiteXY",
0xc62a: "BaselineExposure",
0xc62b: "BaselineNoise",
0xc62c: "BaselineSharpness",
0xc62d: "BayerGreenSplit",
0xc62e: "LinearResponseLimit",
0xc62f: "CameraSerialNumber",
0xc630: "LensInfo",
0xc631: "ChromaBlurRadius",
0xc632: "AntiAliasStrength",
0xc633: "ShadowScale",
0xc634: "DNGPrivateData",
0xc635: "MakerNoteSafety",
0xc65a: "CalibrationIlluminant1",
0xc65b: "CalibrationIlluminant2",
0xc65c: "BestQualityScale",
0xc65d: "RawDataUniqueID",
0xc68b: "OriginalRawFileName",
0xc68c: "OriginalRawFileData",
0xc68d: "ActiveArea",
0xc68e: "MaskedAreas",
0xc68f: "AsShotICCProfile",
0xc690: "AsShotPreProfileMatrix",
0xc691: "CurrentICCProfile",
0xc692: "CurrentPreProfileMatrix",
0xc6bf: "ColorimetricReference",
0xc6f3: "CameraCalibrationSignature",
0xc6f4: "ProfileCalibrationSignature",
0xc6f6: "AsShotProfileName",
0xc6f7: "NoiseReductionApplied",
0xc6f8: "ProfileName",
0xc6f9: "ProfileHueSatMapDims",
0xc6fa: "ProfileHueSatMapData1",
0xc6fb: "ProfileHueSatMapData2",
0xc6fc: "ProfileToneCurve",
0xc6fd: "ProfileEmbedPolicy",
0xc6fe: "ProfileCopyright",
0xc714: "ForwardMatrix1",
0xc715: "ForwardMatrix2",
0xc716: "PreviewApplicationName",
0xc717: "PreviewApplicationVersion",
0xc718: "PreviewSettingsName",
0xc719: "PreviewSettingsDigest",
0xc71a: "PreviewColorSpace",
0xc71b: "PreviewDateTime",
0xc71c: "RawImageDigest",
0xc71d: "OriginalRawFileDigest",
0xc71e: "SubTileBlockSize",
0xc71f: "RowInterleaveFactor",
0xc725: "ProfileLookTableDims",
0xc726: "ProfileLookTableData",
0xc740: "OpcodeList1",
0xc741: "OpcodeList2",
0xc74e: "OpcodeList3",
0xc761: "NoiseProfile"
}
##
# Maps EXIF GPS tags to tag names.
GPSTAGS = {
0: "GPSVersionID",
1: "GPSLatitudeRef",
2: "GPSLatitude",
3: "GPSLongitudeRef",
4: "GPSLongitude",
5: "GPSAltitudeRef",
6: "GPSAltitude",
7: "GPSTimeStamp",
8: "GPSSatellites",
9: "GPSStatus",
10: "GPSMeasureMode",
11: "GPSDOP",
12: "GPSSpeedRef",
13: "GPSSpeed",
14: "GPSTrackRef",
15: "GPSTrack",
16: "GPSImgDirectionRef",
17: "GPSImgDirection",
18: "GPSMapDatum",
19: "GPSDestLatitudeRef",
20: "GPSDestLatitude",
21: "GPSDestLongitudeRef",
22: "GPSDestLongitude",
23: "GPSDestBearingRef",
24: "GPSDestBearing",
25: "GPSDestDistanceRef",
26: "GPSDestDistance",
27: "GPSProcessingMethod",
28: "GPSAreaInformation",
29: "GPSDateStamp",
30: "GPSDifferential",
31: "GPSHPositioningError",
}

View file

@ -0,0 +1,75 @@
#
# The Python Imaging Library
# $Id$
#
# FITS stub adapter
#
# Copyright (c) 1998-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific FITS image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:6] == b"SIMPLE"
class FITSStubImageFile(ImageFile.StubImageFile):
format = "FITS"
format_description = "FITS"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(6)):
raise SyntaxError("Not a FITS file")
# FIXME: add more sanity checks here; mandatory header items
# include SIMPLE, BITPIX, NAXIS, etc.
self.fp.seek(offset)
# make something up
self.mode = "F"
self.size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
raise IOError("FITS save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
Image.register_save(FITSStubImageFile.format, _save)
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])

View file

@ -0,0 +1,163 @@
#
# The Python Imaging Library.
# $Id$
#
# FLI/FLC file handling.
#
# History:
# 95-09-01 fl Created
# 97-01-03 fl Fixed parser, setup decoder tile
# 98-07-15 fl Renamed offset attribute to avoid name clash
#
# Copyright (c) Secret Labs AB 1997-98.
# Copyright (c) Fredrik Lundh 1995-97.
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile, ImagePalette
from ._binary import i8, i16le as i16, i32le as i32, o8
__version__ = "0.2"
#
# decoder
def _accept(prefix):
return len(prefix) >= 6 and i16(prefix[4:6]) in [0xAF11, 0xAF12]
##
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
# method to load individual frames.
class FliImageFile(ImageFile.ImageFile):
format = "FLI"
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
def _open(self):
# HEAD
s = self.fp.read(128)
magic = i16(s[4:6])
if not (magic in [0xAF11, 0xAF12] and
i16(s[14:16]) in [0, 3] and # flags
s[20:22] == b"\x00\x00"): # reserved
raise SyntaxError("not an FLI/FLC file")
# frames
self.__framecount = i16(s[6:8])
# image characteristics
self.mode = "P"
self.size = i16(s[8:10]), i16(s[10:12])
# animation speed
duration = i32(s[16:20])
if magic == 0xAF11:
duration = (duration * 1000) // 70
self.info["duration"] = duration
# look for palette
palette = [(a, a, a) for a in range(256)]
s = self.fp.read(16)
self.__offset = 128
if i16(s[4:6]) == 0xF100:
# prefix chunk; ignore it
self.__offset = self.__offset + i32(s)
s = self.fp.read(16)
if i16(s[4:6]) == 0xF1FA:
# look for palette chunk
s = self.fp.read(6)
if i16(s[4:6]) == 11:
self._palette(palette, 2)
elif i16(s[4:6]) == 4:
self._palette(palette, 0)
palette = [o8(r)+o8(g)+o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))
# set things up to decode first frame
self.__frame = -1
self.__fp = self.fp
self.__rewind = self.fp.tell()
self.seek(0)
def _palette(self, palette, shift):
# load palette
i = 0
for e in range(i16(self.fp.read(2))):
s = self.fp.read(2)
i = i + i8(s[0])
n = i8(s[1])
if n == 0:
n = 256
s = self.fp.read(n * 3)
for n in range(0, len(s), 3):
r = i8(s[n]) << shift
g = i8(s[n+1]) << shift
b = i8(s[n+2]) << shift
palette[i] = (r, g, b)
i += 1
@property
def n_frames(self):
return self.__framecount
@property
def is_animated(self):
return self.__framecount > 1
def seek(self, frame):
if not self._seek_check(frame):
return
if frame < self.__frame:
self._seek(0)
for f in range(self.__frame + 1, frame + 1):
self._seek(f)
def _seek(self, frame):
if frame == 0:
self.__frame = -1
self.__fp.seek(self.__rewind)
self.__offset = 128
if frame != self.__frame + 1:
raise ValueError("cannot seek to frame %d" % frame)
self.__frame = frame
# move to next frame
self.fp = self.__fp
self.fp.seek(self.__offset)
s = self.fp.read(4)
if not s:
raise EOFError
framesize = i32(s)
self.decodermaxblock = framesize
self.tile = [("fli", (0, 0)+self.size, self.__offset, None)]
self.__offset += framesize
def tell(self):
return self.__frame
#
# registry
Image.register_open(FliImageFile.format, FliImageFile, _accept)
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])

114
imagecrunch/PIL/FontFile.py Normal file
View file

@ -0,0 +1,114 @@
#
# The Python Imaging Library
# $Id$
#
# base class for raster font file parsers
#
# history:
# 1997-06-05 fl created
# 1997-08-19 fl restrict image width
#
# Copyright (c) 1997-1998 by Secret Labs AB
# Copyright (c) 1997-1998 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from __future__ import print_function
import os
from . import Image, _binary
WIDTH = 800
def puti16(fp, values):
# write network order (big-endian) 16-bit sequence
for v in values:
if v < 0:
v += 65536
fp.write(_binary.o16be(v))
##
# Base class for raster font file handlers.
class FontFile(object):
bitmap = None
def __init__(self):
self.info = {}
self.glyph = [None] * 256
def __getitem__(self, ix):
return self.glyph[ix]
def compile(self):
"Create metrics and bitmap"
if self.bitmap:
return
# create bitmap large enough to hold all data
h = w = maxwidth = 0
lines = 1
for glyph in self:
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1])
w = w + (src[2] - src[0])
if w > WIDTH:
lines += 1
w = (src[2] - src[0])
maxwidth = max(maxwidth, w)
xsize = maxwidth
ysize = lines * h
if xsize == 0 and ysize == 0:
return ""
self.ysize = h
# paste glyphs into bitmap
self.bitmap = Image.new("1", (xsize, ysize))
self.metrics = [None] * 256
x = y = 0
for i in range(256):
glyph = self[i]
if glyph:
d, dst, src, im = glyph
xx = src[2] - src[0]
# yy = src[3] - src[1]
x0, y0 = x, y
x = x + xx
if x > WIDTH:
x, y = 0, y + h
x0, y0 = x, y
x = xx
s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
self.bitmap.paste(im.crop(src), s)
# print(chr(i), dst, s)
self.metrics[i] = d, dst, s
def save(self, filename):
"Save font"
self.compile()
# font data
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
# font metrics
with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
fp.write(b"PILfont\n")
fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!!
fp.write(b"DATA\n")
for id in range(256):
m = self.metrics[id]
if not m:
puti16(fp, [0] * 10)
else:
puti16(fp, m[0] + m[1] + m[2])

View file

@ -0,0 +1,229 @@
#
# THIS IS WORK IN PROGRESS
#
# The Python Imaging Library.
# $Id$
#
# FlashPix support for PIL
#
# History:
# 97-01-25 fl Created (reads uncompressed RGB images only)
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1997.
#
# See the README file for information on usage and redistribution.
#
from __future__ import print_function
from . import Image, ImageFile
from ._binary import i32le as i32, i8
import olefile
__version__ = "0.1"
# we map from colour field tuples to (mode, rawmode) descriptors
MODES = {
# opacity
(0x00007ffe): ("A", "L"),
# monochrome
(0x00010000,): ("L", "L"),
(0x00018000, 0x00017ffe): ("RGBA", "LA"),
# photo YCC
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
(0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"),
# standard RGB (NIFRGB)
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
(0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA", "RGBA"),
}
#
# --------------------------------------------------------------------
def _accept(prefix):
return prefix[:8] == olefile.MAGIC
##
# Image plugin for the FlashPix images.
class FpxImageFile(ImageFile.ImageFile):
format = "FPX"
format_description = "FlashPix"
def _open(self):
#
# read the OLE directory and see if this is a likely
# to be a FlashPix file
try:
self.ole = olefile.OleFileIO(self.fp)
except IOError:
raise SyntaxError("not an FPX file; invalid OLE file")
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
raise SyntaxError("not an FPX file; bad root CLSID")
self._open_index(1)
def _open_index(self, index=1):
#
# get the Image Contents Property Set
prop = self.ole.getproperties([
"Data Object Store %06d" % index,
"\005Image Contents"
])
# size (highest resolution)
self.size = prop[0x1000002], prop[0x1000003]
size = max(self.size)
i = 1
while size > 64:
size = size / 2
i += 1
self.maxid = i - 1
# mode. instead of using a single field for this, flashpix
# requires you to specify the mode for each channel in each
# resolution subimage, and leaves it to the decoder to make
# sure that they all match. for now, we'll cheat and assume
# that this is always the case.
id = self.maxid << 16
s = prop[0x2000002 | id]
colors = []
for i in range(i32(s, 4)):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8+i*4) & 0x7fffffff)
self.mode, self.rawmode = MODES[tuple(colors)]
# load JPEG tables, if any
self.jpeg = {}
for i in range(256):
id = 0x3000001 | (i << 16)
if id in prop:
self.jpeg[i] = prop[id]
# print(len(self.jpeg), "tables loaded")
self._open_subimage(1, self.maxid)
def _open_subimage(self, index=1, subimage=0):
#
# setup tile descriptors for a given subimage
stream = [
"Data Object Store %06d" % index,
"Resolution %04d" % subimage,
"Subimage 0000 Header"
]
fp = self.ole.openstream(stream)
# skip prefix
fp.read(28)
# header stream
s = fp.read(36)
size = i32(s, 4), i32(s, 8)
# tilecount = i32(s, 12)
tilesize = i32(s, 16), i32(s, 20)
# channels = i32(s, 24)
offset = i32(s, 28)
length = i32(s, 32)
# print(size, self.mode, self.rawmode)
if size != self.size:
raise IOError("subimage mismatch")
# get tile descriptors
fp.seek(28 + offset)
s = fp.read(i32(s, 12) * length)
x = y = 0
xsize, ysize = size
xtile, ytile = tilesize
self.tile = []
for i in range(0, len(s), length):
compression = i32(s, i+8)
if compression == 0:
self.tile.append(("raw", (x, y, x+xtile, y+ytile),
i32(s, i) + 28, (self.rawmode)))
elif compression == 1:
# FIXME: the fill decoder is not implemented
self.tile.append(("fill", (x, y, x+xtile, y+ytile),
i32(s, i) + 28, (self.rawmode, s[12:16])))
elif compression == 2:
internal_color_conversion = i8(s[14])
jpeg_tables = i8(s[15])
rawmode = self.rawmode
if internal_color_conversion:
# The image is stored as usual (usually YCbCr).
if rawmode == "RGBA":
# For "RGBA", data is stored as YCbCrA based on
# negative RGB. The following trick works around
# this problem :
jpegmode, rawmode = "YCbCrK", "CMYK"
else:
jpegmode = None # let the decoder decide
else:
# The image is stored as defined by rawmode
jpegmode = rawmode
self.tile.append(("jpeg", (x, y, x+xtile, y+ytile),
i32(s, i) + 28, (rawmode, jpegmode)))
# FIXME: jpeg tables are tile dependent; the prefix
# data must be placed in the tile descriptor itself!
if jpeg_tables:
self.tile_prefix = self.jpeg[jpeg_tables]
else:
raise IOError("unknown/invalid compression")
x = x + xtile
if x >= xsize:
x, y = 0, y + ytile
if y >= ysize:
break # isn't really required
self.stream = stream
self.fp = None
def load(self):
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] +
["Subimage 0000 Data"])
return ImageFile.ImageFile.load(self)
#
# --------------------------------------------------------------------
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
Image.register_extension(FpxImageFile.format, ".fpx")

View file

@ -0,0 +1,94 @@
"""
A Pillow loader for .ftc and .ftu files (FTEX)
Jerome Leclanche <jerome@leclan.ch>
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
packed custom format called FTEX. This file format uses file extensions FTC and FTU.
* FTC files are compressed textures (using standard texture compression).
* FTU files are not compressed.
Texture File Format
The FTC and FTU texture files both use the same format. This
has the following structure:
{header}
{format_directory}
{data}
Where:
{header} = { u32:magic, u32:version, u32:width, u32:height, u32:mipmap_count, u32:format_count }
* The "magic" number is "FTEX".
* "width" and "height" are the dimensions of the texture.
* "mipmap_count" is the number of mipmaps in the texture.
* "format_count" is the number of texture formats (different versions of the same texture) in this file.
{format_directory} = format_count * { u32:format, u32:where }
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB uncompressed textures.
The texture data for a format starts at the position "where" in the file.
Each set of texture data in the file has the following structure:
{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
* "mipmap_size" is the number of bytes in that mip level. For compressed textures this is the
size of the texture data compressed with DXT1. For 24 bit uncompressed textures, this is 3 * width * height.
Following this are the image bytes for that mipmap level.
Note: All data is stored in little-Endian (Intel) byte order.
"""
import struct
from io import BytesIO
from . import Image, ImageFile
MAGIC = b"FTEX"
FORMAT_DXT1 = 0
FORMAT_UNCOMPRESSED = 1
class FtexImageFile(ImageFile.ImageFile):
format = "FTEX"
format_description = "Texture File Format (IW2:EOC)"
def _open(self):
magic = struct.unpack("<I", self.fp.read(4))
version = struct.unpack("<i", self.fp.read(4))
self.size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
self.mode = "RGB"
# Only support single-format files. I don't know of any multi-format file.
assert format_count == 1
format, where = struct.unpack("<2i", self.fp.read(8))
self.fp.seek(where)
mipmap_size, = struct.unpack("<i", self.fp.read(4))
data = self.fp.read(mipmap_size)
if format == FORMAT_DXT1:
self.mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, (1))]
elif format == FORMAT_UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ('RGB', 0, 1))]
else:
raise ValueError("Invalid texture compression format: %r" % (format))
self.fp.close()
self.fp = BytesIO(data)
def load_seek(self, pos):
pass
def _validate(prefix):
return prefix[:4] == MAGIC
Image.register_open(FtexImageFile.format, FtexImageFile, _validate)
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])

View file

@ -0,0 +1,94 @@
#
# The Python Imaging Library
#
# load a GIMP brush file
#
# History:
# 96-03-14 fl Created
# 16-01-08 es Version 2
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
# Copyright (c) Eric Soroos 2016.
#
# See the README file for information on usage and redistribution.
#
#
# See https://github.com/GNOME/gimp/blob/master/devel-docs/gbr.txt for
# format documentation.
#
# This code Interprets version 1 and 2 .gbr files.
# Version 1 files are obsolete, and should not be used for new
# brushes.
# Version 2 files are saved by GIMP v2.8 (at least)
# Version 3 files have a format specifier of 18 for 16bit floats in
# the color depth field. This is currently unsupported by Pillow.
from . import Image, ImageFile
from ._binary import i32be as i32
def _accept(prefix):
return len(prefix) >= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2)
##
# Image plugin for the GIMP brush format.
class GbrImageFile(ImageFile.ImageFile):
format = "GBR"
format_description = "GIMP brush file"
def _open(self):
header_size = i32(self.fp.read(4))
version = i32(self.fp.read(4))
if header_size < 20:
raise SyntaxError("not a GIMP brush")
if version not in (1, 2):
raise SyntaxError("Unsupported GIMP brush version: %s" % version)
width = i32(self.fp.read(4))
height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4))
if width <= 0 or height <= 0:
raise SyntaxError("not a GIMP brush")
if color_depth not in (1, 4):
raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth)
if version == 1:
comment_length = header_size-20
else:
comment_length = header_size-28
magic_number = self.fp.read(4)
if magic_number != b'GIMP':
raise SyntaxError("not a GIMP brush, bad magic number")
self.info['spacing'] = i32(self.fp.read(4))
comment = self.fp.read(comment_length)[:-1]
if color_depth == 1:
self.mode = "L"
else:
self.mode = 'RGBA'
self.size = width, height
self.info["comment"] = comment
# Image might not be small
Image._decompression_bomb_check(self.size)
# Data is an uncompressed block of w * h * bytes/pixel
self._data_size = width * height * color_depth
def load(self):
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
#
# registry
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
Image.register_extension(GbrImageFile.format, ".gbr")

View file

@ -0,0 +1,83 @@
#
# The Python Imaging Library.
# $Id$
#
# GD file handling
#
# History:
# 1996-04-12 fl Created
#
# Copyright (c) 1997 by Secret Labs AB.
# Copyright (c) 1996 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
# NOTE: This format cannot be automatically recognized, so the
# class is not registered for use with Image.open(). To open a
# gd file, use the GdImageFile.open() function instead.
# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
# implementation is provided for convenience and demonstrational
# purposes only.
from . import ImageFile, ImagePalette
from ._binary import i8, i16be as i16, i32be as i32
__version__ = "0.1"
##
# Image plugin for the GD uncompressed format. Note that this format
# is not supported by the standard <b>Image.open</b> function. To use
# this plugin, you have to import the <b>GdImageFile</b> module and
# use the <b>GdImageFile.open</b> function.
class GdImageFile(ImageFile.ImageFile):
format = "GD"
format_description = "GD uncompressed images"
def _open(self):
# Header
s = self.fp.read(1037)
if not i16(s[:2]) in [65534, 65535]:
raise SyntaxError("Not a valid GD 2.x .gd file")
self.mode = "L" # FIXME: "P"
self.size = i16(s[2:4]), i16(s[4:6])
trueColor = i8(s[6])
trueColorOffset = 2 if trueColor else 0
# transparency index
tindex = i32(s[7+trueColorOffset:7+trueColorOffset+4])
if tindex < 256:
self.info["transparency"] = tindex
self.palette = ImagePalette.raw("XBGR", s[7+trueColorOffset+4:7+trueColorOffset+4+256*4])
self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4, ("L", 0, 1))]
def open(fp, mode="r"):
"""
Load texture from a GD image file.
:param filename: GD file name, or an opened file handle.
:param mode: Optional mode. In this version, if the mode argument
is given, it must be "r".
:returns: An image instance.
:raises IOError: If the image could not be read.
"""
if mode != "r":
raise ValueError("bad mode")
try:
return GdImageFile(fp)
except SyntaxError:
raise IOError("cannot identify this image file")

View file

@ -0,0 +1,814 @@
#
# The Python Imaging Library.
# $Id$
#
# GIF file handling
#
# History:
# 1995-09-01 fl Created
# 1996-12-14 fl Added interlace support
# 1996-12-30 fl Added animation support
# 1997-01-05 fl Added write support, fixed local colour map bug
# 1997-02-23 fl Make sure to load raster data in getdata()
# 1997-07-05 fl Support external decoder (0.4)
# 1998-07-09 fl Handle all modes when saving (0.5)
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6)
# 2001-04-17 fl Added palette optimization (0.7)
# 2002-06-06 fl Added transparency support for save (0.8)
# 2004-02-24 fl Disable interlacing for small images
#
# Copyright (c) 1997-2004 by Secret Labs AB
# Copyright (c) 1995-2004 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence
from ._binary import i8, i16le as i16, o8, o16le as o16
import itertools
__version__ = "0.9"
# --------------------------------------------------------------------
# Identify/read GIF files
def _accept(prefix):
return prefix[:6] in [b"GIF87a", b"GIF89a"]
##
# Image plugin for GIF images. This plugin supports both GIF87 and
# GIF89 images.
class GifImageFile(ImageFile.ImageFile):
format = "GIF"
format_description = "Compuserve GIF"
_close_exclusive_fp_after_loading = False
global_palette = None
def data(self):
s = self.fp.read(1)
if s and i8(s):
return self.fp.read(i8(s))
return None
def _open(self):
# Screen
s = self.fp.read(13)
if s[:6] not in [b"GIF87a", b"GIF89a"]:
raise SyntaxError("not a GIF file")
self.info["version"] = s[:6]
self.size = i16(s[6:]), i16(s[8:])
self.tile = []
flags = i8(s[10])
bits = (flags & 7) + 1
if flags & 128:
# get global palette
self.info["background"] = i8(s[11])
# check if palette contains colour indices
p = self.fp.read(3 << bits)
for i in range(0, len(p), 3):
if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):
p = ImagePalette.raw("RGB", p)
self.global_palette = self.palette = p
break
self.__fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell()
self._n_frames = None
self._is_animated = None
self._seek(0) # get ready to read first frame
@property
def n_frames(self):
if self._n_frames is None:
current = self.tell()
try:
while True:
self.seek(self.tell() + 1)
except EOFError:
self._n_frames = self.tell() + 1
self.seek(current)
return self._n_frames
@property
def is_animated(self):
if self._is_animated is None:
if self._n_frames is not None:
self._is_animated = self._n_frames != 1
else:
current = self.tell()
try:
self.seek(1)
self._is_animated = True
except EOFError:
self._is_animated = False
self.seek(current)
return self._is_animated
def seek(self, frame):
if not self._seek_check(frame):
return
if frame < self.__frame:
self._seek(0)
last_frame = self.__frame
for f in range(self.__frame + 1, frame + 1):
try:
self._seek(f)
except EOFError:
self.seek(last_frame)
raise EOFError("no more images in GIF file")
def _seek(self, frame):
if frame == 0:
# rewind
self.__offset = 0
self.dispose = None
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
self.__frame = -1
self.__fp.seek(self.__rewind)
self._prev_im = None
self.disposal_method = 0
else:
# ensure that the previous frame was loaded
if not self.im:
self.load()
if frame != self.__frame + 1:
raise ValueError("cannot seek to frame %d" % frame)
self.__frame = frame
self.tile = []
self.fp = self.__fp
if self.__offset:
# backup to last frame
self.fp.seek(self.__offset)
while self.data():
pass
self.__offset = 0
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)
from copy import copy
self.palette = copy(self.global_palette)
while True:
s = self.fp.read(1)
if not s or s == b";":
break
elif s == b"!":
#
# extensions
#
s = self.fp.read(1)
block = self.data()
if i8(s) == 249:
#
# graphic control extension
#
flags = i8(block[0])
if flags & 1:
self.info["transparency"] = i8(block[3])
self.info["duration"] = i16(block[1:3]) * 10
# disposal method - find the value of bits 4 - 6
dispose_bits = 0b00011100 & flags
dispose_bits = dispose_bits >> 2
if dispose_bits:
# only set the dispose if it is not
# unspecified. I'm not sure if this is
# correct, but it seems to prevent the last
# frame from looking odd for some animations
self.disposal_method = dispose_bits
elif i8(s) == 254:
#
# comment extension
#
self.info["comment"] = block
elif i8(s) == 255:
#
# application extension
#
self.info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0":
block = self.data()
if len(block) >= 3 and i8(block[0]) == 1:
self.info["loop"] = i16(block[1:3])
while self.data():
pass
elif s == b",":
#
# local image
#
s = self.fp.read(9)
# extent
x0, y0 = i16(s[0:]), i16(s[2:])
x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
self.dispose_extent = x0, y0, x1, y1
flags = i8(s[8])
interlace = (flags & 64) != 0
if flags & 128:
bits = (flags & 7) + 1
self.palette =\
ImagePalette.raw("RGB", self.fp.read(3 << bits))
# image data
bits = i8(self.fp.read(1))
self.__offset = self.fp.tell()
self.tile = [("gif",
(x0, y0, x1, y1),
self.__offset,
(bits, interlace))]
break
else:
pass
# raise IOError, "illegal GIF tag `%x`" % i8(s)
try:
if self.disposal_method < 2:
# do not dispose or none specified
self.dispose = None
elif self.disposal_method == 2:
# replace with background colour
self.dispose = Image.core.fill("P", self.size,
self.info["background"])
else:
# replace with previous contents
if self.im:
self.dispose = self.im.copy()
# only dispose the extent in this frame
if self.dispose:
self.dispose = self._crop(self.dispose, self.dispose_extent)
except (AttributeError, KeyError):
pass
if not self.tile:
# self.__fp = None
raise EOFError
self.mode = "L"
if self.palette:
self.mode = "P"
def tell(self):
return self.__frame
def load_end(self):
ImageFile.ImageFile.load_end(self)
# if the disposal method is 'do not dispose', transparent
# pixels should show the content of the previous frame
if self._prev_im and self.disposal_method == 1:
# we do this by pasting the updated area onto the previous
# frame which we then use as the current image content
updated = self._crop(self.im, self.dispose_extent)
self._prev_im.paste(updated, self.dispose_extent,
updated.convert('RGBA'))
self.im = self._prev_im
self._prev_im = self.im.copy()
# --------------------------------------------------------------------
# Write GIF files
RAWMODE = {
"1": "L",
"L": "L",
"P": "P"
}
def _normalize_mode(im, initial_call=False):
"""
Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif.
It may return the original image, or it may return an image converted to
palette or 'L' mode.
UNDONE: What is the point of mucking with the initial call palette, for
an image that shouldn't have a palette, or it would be a mode 'P' and
get returned in the RAWMODE clause.
:param im: Image object
:param initial_call: Default false, set to true for a single frame.
:returns: Image object
"""
if im.mode in RAWMODE:
im.load()
return im
if Image.getmodebase(im.mode) == "RGB":
if initial_call:
palette_size = 256
if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3
return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
else:
return im.convert("P")
return im.convert("L")
def _normalize_palette(im, palette, info):
"""
Normalizes the palette for image.
- Sets the palette to the incoming palette, if provided.
- Ensures that there's a palette for L mode images
- Optimizes the palette if necessary/desired.
:param im: Image object
:param palette: bytes object containing the source palette, or ....
:param info: encoderinfo
:returns: Image object
"""
source_palette = None
if palette:
# a bytes palette
if isinstance(palette, (bytes, bytearray, list)):
source_palette = bytearray(palette[:768])
if isinstance(palette, ImagePalette.ImagePalette):
source_palette = bytearray(itertools.chain.from_iterable(
zip(palette.palette[:256],
palette.palette[256:512],
palette.palette[512:768])))
if im.mode == "P":
if not source_palette:
source_palette = im.im.getpalette("RGB")[:768]
else: # L-mode
if not source_palette:
source_palette = bytearray(i//3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB",
palette=source_palette)
used_palette_colors = _get_optimize(im, info)
if used_palette_colors is not None:
return im.remap_palette(used_palette_colors, source_palette)
im.palette.palette = source_palette
return im
def _write_single_frame(im, fp, palette):
im_out = _normalize_mode(im, True)
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
for s in _get_global_header(im_out, im.encoderinfo):
fp.write(s)
# local image header
flags = 0
if get_interlace(im):
flags = flags | 64
_write_local_header(fp, im, (0, 0), flags)
im_out.encoderconfig = (8, get_interlace(im))
ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0,
RAWMODE[im_out.mode])])
fp.write(b"\0") # end of image data
def _write_multiple_frames(im, fp, palette):
duration = im.encoderinfo.get("duration", None)
disposal = im.encoderinfo.get('disposal', None)
im_frames = []
frame_count = 0
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
for im_frame in ImageSequence.Iterator(imSequence):
# a copy is required here since seek can still mutate the image
im_frame = _normalize_mode(im_frame.copy())
im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo['duration'] = duration[frame_count]
if isinstance(disposal, (list, tuple)):
encoderinfo["disposal"] = disposal[frame_count]
frame_count += 1
if im_frames:
# delta frame
previous = im_frames[-1]
if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):
delta = ImageChops.subtract_modulo(im_frame,
previous['im'])
else:
delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),
previous['im'].convert('RGB'))
bbox = delta.getbbox()
if not bbox:
# This frame is identical to the previous frame
if duration:
previous['encoderinfo']['duration'] += encoderinfo['duration']
continue
else:
bbox = None
im_frames.append({
'im': im_frame,
'bbox': bbox,
'encoderinfo': encoderinfo
})
if len(im_frames) > 1:
for frame_data in im_frames:
im_frame = frame_data['im']
if not frame_data['bbox']:
# global header
for s in _get_global_header(im_frame,
frame_data['encoderinfo']):
fp.write(s)
offset = (0, 0)
else:
# compress difference
frame_data['encoderinfo']['include_color_table'] = True
im_frame = im_frame.crop(frame_data['bbox'])
offset = frame_data['bbox'][:2]
_write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])
return True
def _save_all(im, fp, filename):
_save(im, fp, filename, save_all=True)
def _save(im, fp, filename, save_all=False):
for k, v in im.info.items():
im.encoderinfo.setdefault(k, v)
# header
try:
palette = im.encoderinfo["palette"]
except KeyError:
palette = None
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
if not save_all or not _write_multiple_frames(im, fp, palette):
_write_single_frame(im, fp, palette)
fp.write(b";") # end of file
if hasattr(fp, "flush"):
fp.flush()
def get_interlace(im):
interlace = im.encoderinfo.get("interlace", 1)
# workaround for @PIL153
if min(im.size) < 16:
interlace = 0
return interlace
def _write_local_header(fp, im, offset, flags):
transparent_color_exists = False
try:
transparency = im.encoderinfo["transparency"]
except KeyError:
pass
else:
transparency = int(transparency)
# optimize the block away if transparent color is not used
transparent_color_exists = True
used_palette_colors = _get_optimize(im, im.encoderinfo)
if used_palette_colors is not None:
# adjust the transparency index after optimize
try:
transparency = used_palette_colors.index(transparency)
except ValueError:
transparent_color_exists = False
if "duration" in im.encoderinfo:
duration = int(im.encoderinfo["duration"] / 10)
else:
duration = 0
disposal = int(im.encoderinfo.get('disposal', 0))
if transparent_color_exists or duration != 0 or disposal:
packed_flag = 1 if transparent_color_exists else 0
packed_flag |= disposal << 2
if not transparent_color_exists:
transparency = 0
fp.write(b"!" +
o8(249) + # extension intro
o8(4) + # length
o8(packed_flag) + # packed fields
o16(duration) + # duration
o8(transparency) + # transparency index
o8(0))
if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255:
fp.write(b"!" +
o8(254) + # extension intro
o8(len(im.encoderinfo["comment"])) +
im.encoderinfo["comment"] +
o8(0))
if "loop" in im.encoderinfo:
number_of_loops = im.encoderinfo["loop"]
fp.write(b"!" +
o8(255) + # extension intro
o8(11) +
b"NETSCAPE2.0" +
o8(3) +
o8(1) +
o16(number_of_loops) + # number of loops
o8(0))
include_color_table = im.encoderinfo.get('include_color_table')
if include_color_table:
palette = im.encoderinfo.get("palette", None)
palette_bytes = _get_palette_bytes(im)
color_table_size = _get_color_table_size(palette_bytes)
if color_table_size:
flags = flags | 128 # local color table flag
flags = flags | color_table_size
fp.write(b"," +
o16(offset[0]) + # offset
o16(offset[1]) +
o16(im.size[0]) + # size
o16(im.size[1]) +
o8(flags)) # flags
if include_color_table and color_table_size:
fp.write(_get_header_palette(palette_bytes))
fp.write(o8(8)) # bits
def _save_netpbm(im, fp, filename):
# Unused by default.
# To use, uncomment the register_save call at the end of the file.
#
# If you need real GIF compression and/or RGB quantization, you
# can use the external NETPBM/PBMPLUS utilities. See comments
# below for information on how to enable this.
import os
from subprocess import Popen, check_call, PIPE, CalledProcessError
file = im._dump()
with open(filename, 'wb') as f:
if im.mode != "RGB":
with open(os.devnull, 'wb') as devnull:
check_call(["ppmtogif", file], stdout=f, stderr=devnull)
else:
# Pipe ppmquant output into ppmtogif
# "ppmquant 256 %s | ppmtogif > %s" % (file, filename)
quant_cmd = ["ppmquant", "256", file]
togif_cmd = ["ppmtogif"]
with open(os.devnull, 'wb') as devnull:
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull)
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout,
stdout=f, stderr=devnull)
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
quant_proc.stdout.close()
retcode = quant_proc.wait()
if retcode:
raise CalledProcessError(retcode, quant_cmd)
retcode = togif_proc.wait()
if retcode:
raise CalledProcessError(retcode, togif_cmd)
try:
os.unlink(file)
except OSError:
pass
# Force optimization so that we can test performance against
# cases where it took lots of memory and time previously.
_FORCE_OPTIMIZE = False
def _get_optimize(im, info):
"""
Palette optimization is a potentially expensive operation.
This function determines if the palette should be optimized using
some heuristics, then returns the list of palette entries in use.
:param im: Image object
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
if im.mode in ("P", "L") and info and info.get("optimize", 0):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette
# lengths are restricted to 3*(2**N) bytes. Max saving would
# be 768 -> 6 bytes if we went all the way down to 2 colors.
# * If we're over 128 colors, we can't save any space.
# * If there aren't any holes, it's not worth collapsing.
# * If we have a 'large' image, the palette is in the noise.
# create the new palette if not every color is used
optimise = _FORCE_OPTIMIZE or im.mode == 'L'
if optimise or im.width * im.height < 512 * 512:
# check which colors are used
used_palette_colors = []
for i, count in enumerate(im.histogram()):
if count:
used_palette_colors.append(i)
if optimise or (len(used_palette_colors) <= 128 and
max(used_palette_colors) > len(used_palette_colors)):
return used_palette_colors
def _get_color_table_size(palette_bytes):
# calculate the palette size for the header
import math
color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
if color_table_size < 0:
color_table_size = 0
return color_table_size
def _get_header_palette(palette_bytes):
"""
Returns the palette, null padded to the next power of 2 (*3) bytes
suitable for direct inclusion in the GIF header
:param palette_bytes: Unpadded palette bytes, in RGBRGB form
:returns: Null padded palette
"""
color_table_size = _get_color_table_size(palette_bytes)
# add the missing amount of bytes
# the palette has to be 2<<n in size
actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3
if actual_target_size_diff > 0:
palette_bytes += o8(0) * 3 * actual_target_size_diff
return palette_bytes
def _get_palette_bytes(im):
"""
Gets the palette for inclusion in the gif header
:param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header
"""
return im.palette.palette
def _get_global_header(im, info):
"""Return a list of strings representing a GIF header"""
# Header Block
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
version = b"87a"
for extensionKey in ["transparency", "duration", "loop", "comment"]:
if info and extensionKey in info:
if ((extensionKey == "duration" and info[extensionKey] == 0) or
(extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
continue
version = b"89a"
break
else:
if im.info.get("version") == b"89a":
version = b"89a"
palette_bytes = _get_palette_bytes(im)
color_table_size = _get_color_table_size(palette_bytes)
background = info["background"] if "background" in info else 0
return [
b"GIF"+version + # signature + version
o16(im.size[0]) + # canvas width
o16(im.size[1]), # canvas height
# Logical Screen Descriptor
# size of global color table + global color table flag
o8(color_table_size + 128), # packed fields
# background + reserved/aspect
o8(background) + o8(0),
# Global Color Table
_get_header_palette(palette_bytes)
]
def _write_frame_data(fp, im_frame, offset, params):
try:
im_frame.encoderinfo = params
# local image header
_write_local_header(fp, im_frame, offset, 0)
ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0,
RAWMODE[im_frame.mode])])
fp.write(b"\0") # end of image data
finally:
del im_frame.encoderinfo
# --------------------------------------------------------------------
# Legacy GIF utilities
def getheader(im, palette=None, info=None):
"""
Legacy Method to get Gif data from image.
Warning:: May modify image data.
:param im: Image object
:param palette: bytes object containing the source palette, or ....
:param info: encoderinfo
:returns: tuple of(list of header items, optimized palette)
"""
used_palette_colors = _get_optimize(im, info)
if info is None:
info = {}
if "background" not in info and "background" in im.info:
info["background"] = im.info["background"]
im_mod = _normalize_palette(im, palette, info)
im.palette = im_mod.palette
im.im = im_mod.im
header = _get_global_header(im, info)
return header, used_palette_colors
# To specify duration, add the time in milliseconds to getdata(),
# e.g. getdata(im_frame, duration=1000)
def getdata(im, offset=(0, 0), **params):
"""
Legacy Method
Return a list of strings representing this image.
The first string is a local image header, the rest contains
encoded image data.
:param im: Image object
:param offset: Tuple of (x, y) pixels. Defaults to (0,0)
:param \\**params: E.g. duration or other encoder info parameters
:returns: List of Bytes containing gif encoded frame data
"""
class Collector(object):
data = []
def write(self, data):
self.data.append(data)
im.load() # make sure raster data is available
fp = Collector()
_write_frame_data(fp, im, offset, params)
return fp.data
# --------------------------------------------------------------------
# Registry
Image.register_open(GifImageFile.format, GifImageFile, _accept)
Image.register_save(GifImageFile.format, _save)
Image.register_save_all(GifImageFile.format, _save_all)
Image.register_extension(GifImageFile.format, ".gif")
Image.register_mime(GifImageFile.format, "image/gif")
#
# Uncomment the following line if you wish to use NETPBM/PBMPLUS
# instead of the built-in "uncompressed" GIF encoder
# Image.register_save(GifImageFile.format, _save_netpbm)

View file

@ -0,0 +1,138 @@
#
# Python Imaging Library
# $Id$
#
# stuff to read (and render) GIMP gradient files
#
# History:
# 97-08-23 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1997.
#
# See the README file for information on usage and redistribution.
#
from math import pi, log, sin, sqrt
from ._binary import o8
# --------------------------------------------------------------------
# Stuff to translate curve segments to palette values (derived from
# the corresponding code in GIMP, written by Federico Mena Quintero.
# See the GIMP distribution for more information.)
#
EPSILON = 1e-10
def linear(middle, pos):
if pos <= middle:
if middle < EPSILON:
return 0.0
else:
return 0.5 * pos / middle
else:
pos = pos - middle
middle = 1.0 - middle
if middle < EPSILON:
return 1.0
else:
return 0.5 + 0.5 * pos / middle
def curved(middle, pos):
return pos ** (log(0.5) / log(max(middle, EPSILON)))
def sine(middle, pos):
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
def sphere_increasing(middle, pos):
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
def sphere_decreasing(middle, pos):
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
class GradientFile(object):
gradient = None
def getpalette(self, entries=256):
palette = []
ix = 0
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
for i in range(entries):
x = i / float(entries-1)
while x1 < x:
ix += 1
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
w = x1 - x0
if w < EPSILON:
scale = segment(0.5, 0.5)
else:
scale = segment((xm - x0) / w, (x - x0) / w)
# expand to RGBA
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
# add to palette
palette.append(r + g + b + a)
return b"".join(palette), "RGBA"
##
# File handler for GIMP's gradient format.
class GimpGradientFile(GradientFile):
def __init__(self, fp):
if fp.readline()[:13] != b"GIMP Gradient":
raise SyntaxError("not a GIMP gradient file")
line = fp.readline()
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
if line.startswith(b"Name: "):
line = fp.readline().strip()
count = int(line)
gradient = []
for i in range(count):
s = fp.readline().split()
w = [float(x) for x in s[:11]]
x0, x1 = w[0], w[2]
xm = w[1]
rgb0 = w[3:7]
rgb1 = w[7:11]
segment = SEGMENTS[int(s[11])]
cspace = int(s[12])
if cspace != 0:
raise IOError("cannot handle HSV colour space")
gradient.append((x0, x1, xm, rgb0, rgb1, segment))
self.gradient = gradient

View file

@ -0,0 +1,62 @@
#
# Python Imaging Library
# $Id$
#
# stuff to read GIMP palette files
#
# History:
# 1997-08-23 fl Created
# 2004-09-07 fl Support GIMP 2.0 palette files.
#
# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
# Copyright (c) Fredrik Lundh 1997-2004.
#
# See the README file for information on usage and redistribution.
#
import re
from ._binary import o8
##
# File handler for GIMP's palette format.
class GimpPaletteFile(object):
rawmode = "RGB"
def __init__(self, fp):
self.palette = [o8(i)*3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
raise SyntaxError("not a GIMP palette file")
i = 0
while i <= 255:
s = fp.readline()
if not s:
break
# skip fields and comment lines
if re.match(br"\w+:|#", s):
continue
if len(s) > 100:
raise SyntaxError("bad palette file")
v = tuple(map(int, s.split()[:3]))
if len(v) != 3:
raise ValueError("bad palette entry")
if 0 <= i <= 255:
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
i += 1
self.palette = b"".join(self.palette)
def getpalette(self):
return self.palette, self.rawmode

View file

@ -0,0 +1,73 @@
#
# The Python Imaging Library
# $Id$
#
# GRIB stub adapter
#
# Copyright (c) 1996-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
from ._binary import i8
_handler = None
def register_handler(handler):
"""
Install application-specific GRIB image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1
class GribStubImageFile(ImageFile.StubImageFile):
format = "GRIB"
format_description = "GRIB"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
raise SyntaxError("Not a GRIB file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self.size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
raise IOError("GRIB save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
Image.register_save(GribStubImageFile.format, _save)
Image.register_extension(GribStubImageFile.format, ".grib")

View file

@ -0,0 +1,72 @@
#
# The Python Imaging Library
# $Id$
#
# HDF5 stub adapter
#
# Copyright (c) 2000-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific HDF5 image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
class HDF5StubImageFile(ImageFile.StubImageFile):
format = "HDF5"
format_description = "HDF5"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
raise SyntaxError("Not an HDF file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self.size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
raise IOError("HDF5 save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
Image.register_save(HDF5StubImageFile.format, _save)
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])

View file

@ -0,0 +1,374 @@
#
# The Python Imaging Library.
# $Id$
#
# macOS icns file decoder, based on icns.py by Bob Ippolito.
#
# history:
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
#
# Copyright (c) 2004 by Bob Ippolito.
# Copyright (c) 2004 by Secret Labs.
# Copyright (c) 2004 by Fredrik Lundh.
# Copyright (c) 2014 by Alastair Houghton.
#
# See the README file for information on usage and redistribution.
#
from PIL import Image, ImageFile, PngImagePlugin
from PIL._binary import i8
import io
import os
import shutil
import struct
import sys
import tempfile
enable_jpeg2k = hasattr(Image.core, 'jp2klib_version')
if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin
HEADERSIZE = 8
def nextheader(fobj):
return struct.unpack('>4sI', fobj.read(HEADERSIZE))
def read_32t(fobj, start_length, size):
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(4)
if sig != b'\x00\x00\x00\x00':
raise SyntaxError('Unknown signature, expecting 0x00000000')
return read_32(fobj, (start + 4, length - 4), size)
def read_32(fobj, start_length, size):
"""
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
"""
(start, length) = start_length
fobj.seek(start)
pixel_size = (size[0] * size[2], size[1] * size[2])
sizesq = pixel_size[0] * pixel_size[1]
if length == sizesq * 3:
# uncompressed ("RGBRGBGB")
indata = fobj.read(length)
im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
else:
# decode image
im = Image.new("RGB", pixel_size, None)
for band_ix in range(3):
data = []
bytesleft = sizesq
while bytesleft > 0:
byte = fobj.read(1)
if not byte:
break
byte = i8(byte)
if byte & 0x80:
blocksize = byte - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
else:
blocksize = byte + 1
data.append(fobj.read(blocksize))
bytesleft -= blocksize
if bytesleft <= 0:
break
if bytesleft != 0:
raise SyntaxError(
"Error reading channel [%r left]" % bytesleft
)
band = Image.frombuffer(
"L", pixel_size, b"".join(data), "raw", "L", 0, 1
)
im.im.putband(band.im, band_ix)
return {"RGB": im}
def read_mk(fobj, start_length, size):
# Alpha masks seem to be uncompressed
start = start_length[0]
fobj.seek(start)
pixel_size = (size[0] * size[2], size[1] * size[2])
sizesq = pixel_size[0] * pixel_size[1]
band = Image.frombuffer(
"L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1
)
return {"A": band}
def read_png_or_jpeg2000(fobj, start_length, size):
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(12)
if sig[:8] == b'\x89PNG\x0d\x0a\x1a\x0a':
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
return {"RGBA": im}
elif sig[:4] == b'\xff\x4f\xff\x51' \
or sig[:4] == b'\x0d\x0a\x87\x0a' \
or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a':
if not enable_jpeg2k:
raise ValueError('Unsupported icon subimage format (rebuild PIL '
'with JPEG 2000 support to fix this)')
# j2k, jpc or j2c
fobj.seek(start)
jp2kstream = fobj.read(length)
f = io.BytesIO(jp2kstream)
im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
if im.mode != 'RGBA':
im = im.convert('RGBA')
return {"RGBA": im}
else:
raise ValueError('Unsupported icon subimage format')
class IcnsFile(object):
SIZES = {
(512, 512, 2): [
(b'ic10', read_png_or_jpeg2000),
],
(512, 512, 1): [
(b'ic09', read_png_or_jpeg2000),
],
(256, 256, 2): [
(b'ic14', read_png_or_jpeg2000),
],
(256, 256, 1): [
(b'ic08', read_png_or_jpeg2000),
],
(128, 128, 2): [
(b'ic13', read_png_or_jpeg2000),
],
(128, 128, 1): [
(b'ic07', read_png_or_jpeg2000),
(b'it32', read_32t),
(b't8mk', read_mk),
],
(64, 64, 1): [
(b'icp6', read_png_or_jpeg2000),
],
(32, 32, 2): [
(b'ic12', read_png_or_jpeg2000),
],
(48, 48, 1): [
(b'ih32', read_32),
(b'h8mk', read_mk),
],
(32, 32, 1): [
(b'icp5', read_png_or_jpeg2000),
(b'il32', read_32),
(b'l8mk', read_mk),
],
(16, 16, 2): [
(b'ic11', read_png_or_jpeg2000),
],
(16, 16, 1): [
(b'icp4', read_png_or_jpeg2000),
(b'is32', read_32),
(b's8mk', read_mk),
],
}
def __init__(self, fobj):
"""
fobj is a file-like object as an icns resource
"""
# signature : (start, length)
self.dct = dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if sig != b'icns':
raise SyntaxError('not an icns file')
i = HEADERSIZE
while i < filesize:
sig, blocksize = nextheader(fobj)
if blocksize <= 0:
raise SyntaxError('invalid block header')
i += HEADERSIZE
blocksize -= HEADERSIZE
dct[sig] = (i, blocksize)
fobj.seek(blocksize, 1)
i += blocksize
def itersizes(self):
sizes = []
for size, fmts in self.SIZES.items():
for (fmt, reader) in fmts:
if fmt in self.dct:
sizes.append(size)
break
return sizes
def bestsize(self):
sizes = self.itersizes()
if not sizes:
raise SyntaxError("No 32bit icon resources found")
return max(sizes)
def dataforsize(self, size):
"""
Get an icon resource as {channel: array}. Note that
the arrays are bottom-up like windows bitmaps and will likely
need to be flipped or transposed in some way.
"""
dct = {}
for code, reader in self.SIZES[size]:
desc = self.dct.get(code)
if desc is not None:
dct.update(reader(self.fobj, desc, size))
return dct
def getimage(self, size=None):
if size is None:
size = self.bestsize()
if len(size) == 2:
size = (size[0], size[1], 1)
channels = self.dataforsize(size)
im = channels.get('RGBA', None)
if im:
return im
im = channels.get("RGB").copy()
try:
im.putalpha(channels["A"])
except KeyError:
pass
return im
##
# Image plugin for Mac OS icons.
class IcnsImageFile(ImageFile.ImageFile):
"""
PIL image support for Mac OS .icns files.
Chooses the best resolution, but will possibly load
a different size image if you mutate the size attribute
before calling 'load'.
The info dictionary has a key 'sizes' that is a list
of sizes that the icns file has.
"""
format = "ICNS"
format_description = "Mac OS icns resource"
def _open(self):
self.icns = IcnsFile(self.fp)
self.mode = 'RGBA'
self.best_size = self.icns.bestsize()
self.size = (self.best_size[0] * self.best_size[2],
self.best_size[1] * self.best_size[2])
self.info['sizes'] = self.icns.itersizes()
# Just use this to see if it's loaded or not yet.
self.tile = ('',)
def load(self):
if len(self.size) == 3:
self.best_size = self.size
self.size = (self.best_size[0] * self.best_size[2],
self.best_size[1] * self.best_size[2])
Image.Image.load(self)
if not self.tile:
return
self.load_prepare()
# This is likely NOT the best way to do it, but whatever.
im = self.icns.getimage(self.best_size)
# If this is a PNG or JPEG 2000, it won't be loaded yet
im.load()
self.im = im.im
self.mode = im.mode
self.size = im.size
self.fp = None
self.icns = None
self.tile = ()
self.load_end()
def _save(im, fp, filename):
"""
Saves the image as a series of PNG files,
that are then converted to a .icns file
using the macOS command line utility 'iconutil'.
macOS only.
"""
if hasattr(fp, "flush"):
fp.flush()
# create the temporary set of pngs
iconset = tempfile.mkdtemp('.iconset')
provided_images = {im.width: im
for im in im.encoderinfo.get("append_images", [])}
last_w = None
for w in [16, 32, 128, 256, 512]:
prefix = 'icon_{}x{}'.format(w, w)
first_path = os.path.join(iconset, prefix+'.png')
if last_w == w:
shutil.copyfile(second_path, first_path)
else:
im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS))
im_w.save(first_path)
second_path = os.path.join(iconset, prefix+'@2x.png')
im_w2 = provided_images.get(w*2, im.resize((w*2, w*2), Image.LANCZOS))
im_w2.save(second_path)
last_w = w*2
# iconutil -c icns -o {} {}
from subprocess import Popen, PIPE, CalledProcessError
convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset]
with open(os.devnull, 'wb') as devnull:
convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull)
convert_proc.stdout.close()
retcode = convert_proc.wait()
# remove the temporary files
shutil.rmtree(iconset)
if retcode:
raise CalledProcessError(retcode, convert_cmd)
Image.register_open(IcnsImageFile.format, IcnsImageFile,
lambda x: x[:4] == b'icns')
Image.register_extension(IcnsImageFile.format, '.icns')
if sys.platform == 'darwin':
Image.register_save(IcnsImageFile.format, _save)
Image.register_mime(IcnsImageFile.format, "image/icns")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Syntax: python IcnsImagePlugin.py [file]")
sys.exit()
imf = IcnsImageFile(open(sys.argv[1], 'rb'))
for size in imf.info['sizes']:
imf.size = size
imf.load()
im = imf.im
im.save('out-%s-%s-%s.png' % size)
im = Image.open(sys.argv[1])
im.save("out.png")
if sys.platform == 'windows':
os.startfile("out.png")

View file

@ -0,0 +1,284 @@
#
# The Python Imaging Library.
# $Id$
#
# Windows Icon support for PIL
#
# History:
# 96-05-27 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
# <casadebender@gmail.com>.
# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
#
# Icon format references:
# * https://en.wikipedia.org/wiki/ICO_(file_format)
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
import struct
from io import BytesIO
from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin
from ._binary import i8, i16le as i16, i32le as i32
from math import log, ceil
__version__ = "0.1"
#
# --------------------------------------------------------------------
_MAGIC = b"\0\0\1\0"
def _save(im, fp, filename):
fp.write(_MAGIC) # (2+2)
sizes = im.encoderinfo.get("sizes",
[(16, 16), (24, 24), (32, 32), (48, 48),
(64, 64), (128, 128), (256, 256)])
width, height = im.size
sizes = filter(lambda x: False if (x[0] > width or x[1] > height or
x[0] > 256 or x[1] > 256) else True,
sizes)
sizes = list(sizes)
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
offset = fp.tell() + len(sizes)*16
for size in sizes:
width, height = size
# 0 means 256
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
fp.write(b"\0") # bColorCount(1)
fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2)
fp.write(struct.pack("<H", 32)) # wBitCount(2)
image_io = BytesIO()
tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS)
tmp.save(image_io, "png")
image_io.seek(0)
image_bytes = image_io.read()
bytes_len = len(image_bytes)
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
current = fp.tell()
fp.seek(offset)
fp.write(image_bytes)
offset = offset + bytes_len
fp.seek(current)
def _accept(prefix):
return prefix[:4] == _MAGIC
class IcoFile(object):
def __init__(self, buf):
"""
Parse image from file-like object containing ico file data
"""
# check magic
s = buf.read(6)
if not _accept(s):
raise SyntaxError("not an ICO file")
self.buf = buf
self.entry = []
# Number of items in file
self.nb_items = i16(s[4:])
# Get headers for each item
for i in range(self.nb_items):
s = buf.read(16)
icon_header = {
'width': i8(s[0]),
'height': i8(s[1]),
'nb_color': i8(s[2]), # No. of colors in image (0 if >=8bpp)
'reserved': i8(s[3]),
'planes': i16(s[4:]),
'bpp': i16(s[6:]),
'size': i32(s[8:]),
'offset': i32(s[12:])
}
# See Wikipedia
for j in ('width', 'height'):
if not icon_header[j]:
icon_header[j] = 256
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
icon_header['color_depth'] = (icon_header['bpp'] or
(icon_header['nb_color'] != 0 and
ceil(log(icon_header['nb_color'],
2))) or 256)
icon_header['dim'] = (icon_header['width'], icon_header['height'])
icon_header['square'] = (icon_header['width'] *
icon_header['height'])
self.entry.append(icon_header)
self.entry = sorted(self.entry, key=lambda x: x['color_depth'])
# ICO images are usually squares
# self.entry = sorted(self.entry, key=lambda x: x['width'])
self.entry = sorted(self.entry, key=lambda x: x['square'])
self.entry.reverse()
def sizes(self):
"""
Get a list of all available icon sizes and color depths.
"""
return {(h['width'], h['height']) for h in self.entry}
def getimage(self, size, bpp=False):
"""
Get an image from the icon
"""
for (i, h) in enumerate(self.entry):
if size == h['dim'] and (bpp is False or bpp == h['color_depth']):
return self.frame(i)
return self.frame(0)
def frame(self, idx):
"""
Get an image from frame idx
"""
header = self.entry[idx]
self.buf.seek(header['offset'])
data = self.buf.read(8)
self.buf.seek(header['offset'])
if data[:8] == PngImagePlugin._MAGIC:
# png frame
im = PngImagePlugin.PngImageFile(self.buf)
else:
# XOR + AND mask bmp frame
im = BmpImagePlugin.DibImageFile(self.buf)
# change tile dimension to only encompass XOR image
im.size = (im.size[0], int(im.size[1] / 2))
d, e, o, a = im.tile[0]
im.tile[0] = d, (0, 0) + im.size, o, a
# figure out where AND mask image starts
mode = a[0]
bpp = 8
for k, v in BmpImagePlugin.BIT2MODE.items():
if mode == v[1]:
bpp = k
break
if 32 == bpp:
# 32-bit color depth icon image allows semitransparent areas
# PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha
# channel.
# Back up to start of bmp data
self.buf.seek(o)
# extract every 4th byte (eg. 3,7,11,15,...)
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
# convert to an 8bpp grayscale image
mask = Image.frombuffer(
'L', # 8bpp
im.size, # (w, h)
alpha_bytes, # source chars
'raw', # raw decoder
('L', 0, -1) # 8bpp inverted, unpadded, reversed
)
else:
# get AND image from end of bitmap
w = im.size[0]
if (w % 32) > 0:
# bitmap row data is aligned to word boundaries
w += 32 - (im.size[0] % 32)
# the total mask data is
# padded row size * height / bits per char
and_mask_offset = o + int(im.size[0] * im.size[1] *
(bpp / 8.0))
total_bytes = int((w * im.size[1]) / 8)
self.buf.seek(and_mask_offset)
mask_data = self.buf.read(total_bytes)
# convert raw data to image
mask = Image.frombuffer(
'1', # 1 bpp
im.size, # (w, h)
mask_data, # source chars
'raw', # raw decoder
('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed
)
# now we have two images, im is XOR image and mask is AND image
# apply mask image as alpha channel
im = im.convert('RGBA')
im.putalpha(mask)
return im
##
# Image plugin for Windows Icon files.
class IcoImageFile(ImageFile.ImageFile):
"""
PIL read-only image support for Microsoft Windows .ico files.
By default the largest resolution image in the file will be loaded. This
can be changed by altering the 'size' attribute before calling 'load'.
The info dictionary has a key 'sizes' that is a list of the sizes available
in the icon file.
Handles classic, XP and Vista icon formats.
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
<casadebender@gmail.com>.
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
"""
format = "ICO"
format_description = "Windows Icon"
def _open(self):
self.ico = IcoFile(self.fp)
self.info['sizes'] = self.ico.sizes()
self.size = self.ico.entry[0]['dim']
self.load()
def load(self):
im = self.ico.getimage(self.size)
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
self.mode = im.mode
self.size = im.size
def load_seek(self):
# Flag the ImageFile.Parser so that it
# just does all the decode at the end.
pass
#
# --------------------------------------------------------------------
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
Image.register_save(IcoImageFile.format, _save)
Image.register_extension(IcoImageFile.format, ".ico")

View file

@ -0,0 +1,347 @@
#
# The Python Imaging Library.
# $Id$
#
# IFUNC IM file handling for PIL
#
# history:
# 1995-09-01 fl Created.
# 1997-01-03 fl Save palette images
# 1997-01-08 fl Added sequence support
# 1997-01-23 fl Added P and RGB save support
# 1997-05-31 fl Read floating point images
# 1997-06-22 fl Save floating point images
# 1997-08-27 fl Read and save 1-bit images
# 1998-06-25 fl Added support for RGB+LUT images
# 1998-07-02 fl Added support for YCC images
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 1998-12-29 fl Added I;16 support
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
# 2003-09-26 fl Added LA/PA support
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1995-2001 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
import re
from . import Image, ImageFile, ImagePalette
from ._binary import i8
__version__ = "0.7"
# --------------------------------------------------------------------
# Standard tags
COMMENT = "Comment"
DATE = "Date"
EQUIPMENT = "Digitalization equipment"
FRAMES = "File size (no of images)"
LUT = "Lut"
NAME = "Name"
SCALE = "Scale (x,y)"
SIZE = "Image size (x*y)"
MODE = "Image type"
TAGS = {COMMENT: 0, DATE: 0, EQUIPMENT: 0, FRAMES: 0, LUT: 0, NAME: 0,
SCALE: 0, SIZE: 0, MODE: 0}
OPEN = {
# ifunc93/p3cfunc formats
"0 1 image": ("1", "1"),
"L 1 image": ("1", "1"),
"Greyscale image": ("L", "L"),
"Grayscale image": ("L", "L"),
"RGB image": ("RGB", "RGB;L"),
"RLB image": ("RGB", "RLB"),
"RYB image": ("RGB", "RLB"),
"B1 image": ("1", "1"),
"B2 image": ("P", "P;2"),
"B4 image": ("P", "P;4"),
"X 24 image": ("RGB", "RGB"),
"L 32 S image": ("I", "I;32"),
"L 32 F image": ("F", "F;32"),
# old p3cfunc formats
"RGB3 image": ("RGB", "RGB;T"),
"RYB3 image": ("RGB", "RYB;T"),
# extensions
"LA image": ("LA", "LA;L"),
"RGBA image": ("RGBA", "RGBA;L"),
"RGBX image": ("RGBX", "RGBX;L"),
"CMYK image": ("CMYK", "CMYK;L"),
"YCC image": ("YCbCr", "YCbCr;L"),
}
# ifunc95 extensions
for i in ["8", "8S", "16", "16S", "32", "32F"]:
OPEN["L %s image" % i] = ("F", "F;%s" % i)
OPEN["L*%s image" % i] = ("F", "F;%s" % i)
for i in ["16", "16L", "16B"]:
OPEN["L %s image" % i] = ("I;%s" % i, "I;%s" % i)
OPEN["L*%s image" % i] = ("I;%s" % i, "I;%s" % i)
for i in ["32S"]:
OPEN["L %s image" % i] = ("I", "I;%s" % i)
OPEN["L*%s image" % i] = ("I", "I;%s" % i)
for i in range(2, 33):
OPEN["L*%s image" % i] = ("F", "F;%s" % i)
# --------------------------------------------------------------------
# Read IM directory
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s):
try:
return int(s)
except ValueError:
return float(s)
##
# Image plugin for the IFUNC IM file format.
class ImImageFile(ImageFile.ImageFile):
format = "IM"
format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False
def _open(self):
# Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header.
if b"\n" not in self.fp.read(100):
raise SyntaxError("not an IM file")
self.fp.seek(0)
n = 0
# Default values
self.info[MODE] = "L"
self.info[SIZE] = (512, 512)
self.info[FRAMES] = 1
self.rawmode = "L"
while True:
s = self.fp.read(1)
# Some versions of IFUNC uses \n\r instead of \r\n...
if s == b"\r":
continue
if not s or s == b'\0' or s == b'\x1A':
break
# FIXME: this may read whole file if not a text file
s = s + self.fp.readline()
if len(s) > 100:
raise SyntaxError("not an IM file")
if s[-2:] == b'\r\n':
s = s[:-2]
elif s[-1:] == b'\n':
s = s[:-1]
try:
m = split.match(s)
except re.error as v:
raise SyntaxError("not an IM file")
if m:
k, v = m.group(1, 2)
# Don't know if this is the correct encoding,
# but a decent guess (I guess)
k = k.decode('latin-1', 'replace')
v = v.decode('latin-1', 'replace')
# Convert value as appropriate
if k in [FRAMES, SCALE, SIZE]:
v = v.replace("*", ",")
v = tuple(map(number, v.split(",")))
if len(v) == 1:
v = v[0]
elif k == MODE and v in OPEN:
v, self.rawmode = OPEN[v]
# Add to dictionary. Note that COMMENT tags are
# combined into a list of strings.
if k == COMMENT:
if k in self.info:
self.info[k].append(v)
else:
self.info[k] = [v]
else:
self.info[k] = v
if k in TAGS:
n += 1
else:
raise SyntaxError("Syntax error in IM header: " +
s.decode('ascii', 'replace'))
if not n:
raise SyntaxError("Not an IM file")
# Basic attributes
self.size = self.info[SIZE]
self.mode = self.info[MODE]
# Skip forward to start of image data
while s and s[0:1] != b'\x1A':
s = self.fp.read(1)
if not s:
raise SyntaxError("File truncated")
if LUT in self.info:
# convert lookup table to palette or lut attribute
palette = self.fp.read(768)
greyscale = 1 # greyscale palette
linear = 1 # linear greyscale palette
for i in range(256):
if palette[i] == palette[i+256] == palette[i+512]:
if i8(palette[i]) != i:
linear = 0
else:
greyscale = 0
if self.mode == "L" or self.mode == "LA":
if greyscale:
if not linear:
self.lut = [i8(c) for c in palette[:256]]
else:
if self.mode == "L":
self.mode = self.rawmode = "P"
elif self.mode == "LA":
self.mode = self.rawmode = "PA"
self.palette = ImagePalette.raw("RGB;L", palette)
elif self.mode == "RGB":
if not greyscale or not linear:
self.lut = [i8(c) for c in palette]
self.frame = 0
self.__offset = offs = self.fp.tell()
self.__fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
# ifunc95 formats
try:
# use bit decoder (if necessary)
bits = int(self.rawmode[2:])
if bits not in [8, 16, 32]:
self.tile = [("bit", (0, 0)+self.size, offs,
(bits, 8, 3, 0, -1))]
return
except ValueError:
pass
if self.rawmode in ["RGB;T", "RYB;T"]:
# Old LabEye/3PC files. Would be very surprised if anyone
# ever stumbled upon such a file ;-)
size = self.size[0] * self.size[1]
self.tile = [("raw", (0, 0)+self.size, offs, ("G", 0, -1)),
("raw", (0, 0)+self.size, offs+size, ("R", 0, -1)),
("raw", (0, 0)+self.size, offs+2*size, ("B", 0, -1))]
else:
# LabEye/IFUNC files
self.tile = [("raw", (0, 0)+self.size, offs,
(self.rawmode, 0, -1))]
@property
def n_frames(self):
return self.info[FRAMES]
@property
def is_animated(self):
return self.info[FRAMES] > 1
def seek(self, frame):
if not self._seek_check(frame):
return
self.frame = frame
if self.mode == "1":
bits = 1
else:
bits = 8 * len(self.mode)
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
offs = self.__offset + frame * size
self.fp = self.__fp
self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))]
def tell(self):
return self.frame
#
# --------------------------------------------------------------------
# Save IM files
SAVE = {
# mode: (im type, raw mode)
"1": ("0 1", "1"),
"L": ("Greyscale", "L"),
"LA": ("LA", "LA;L"),
"P": ("Greyscale", "P"),
"PA": ("LA", "PA;L"),
"I": ("L 32S", "I;32S"),
"I;16": ("L 16", "I;16"),
"I;16L": ("L 16L", "I;16L"),
"I;16B": ("L 16B", "I;16B"),
"F": ("L 32F", "F;32F"),
"RGB": ("RGB", "RGB;L"),
"RGBA": ("RGBA", "RGBA;L"),
"RGBX": ("RGBX", "RGBX;L"),
"CMYK": ("CMYK", "CMYK;L"),
"YCbCr": ("YCC", "YCbCr;L")
}
def _save(im, fp, filename):
try:
image_type, rawmode = SAVE[im.mode]
except KeyError:
raise ValueError("Cannot save %s images as IM" % im.mode)
frames = im.encoderinfo.get("frames", 1)
fp.write(("Image type: %s image\r\n" % image_type).encode('ascii'))
if filename:
fp.write(("Name: %s\r\n" % filename).encode('ascii'))
fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii'))
fp.write(("File size (no of images): %d\r\n" % frames).encode('ascii'))
if im.mode == "P":
fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511-fp.tell()) + b"\032")
if im.mode == "P":
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))])
#
# --------------------------------------------------------------------
# Registry
Image.register_open(ImImageFile.format, ImImageFile)
Image.register_save(ImImageFile.format, _save)
Image.register_extension(ImImageFile.format, ".im")

2930
imagecrunch/PIL/Image.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,283 @@
#
# The Python Imaging Library.
# $Id$
#
# standard channel operations
#
# History:
# 1996-03-24 fl Created
# 1996-08-13 fl Added logical operations (for "1" images)
# 2000-10-12 fl Added offset method (from Image.py)
#
# Copyright (c) 1997-2000 by Secret Labs AB
# Copyright (c) 1996-2000 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image
def constant(image, value):
"""Fill a channel with a given grey level.
:rtype: :py:class:`~PIL.Image.Image`
"""
return Image.new("L", image.size, value)
def duplicate(image):
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
:rtype: :py:class:`~PIL.Image.Image`
"""
return image.copy()
def invert(image):
"""
Invert an image (channel).
.. code-block:: python
out = MAX - image
:rtype: :py:class:`~PIL.Image.Image`
"""
image.load()
return image._new(image.im.chop_invert())
def lighter(image1, image2):
"""
Compares the two images, pixel by pixel, and returns a new image containing
the lighter values.
.. code-block:: python
out = max(image1, image2)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_lighter(image2.im))
def darker(image1, image2):
"""
Compares the two images, pixel by pixel, and returns a new image
containing the darker values.
.. code-block:: python
out = min(image1, image2)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_darker(image2.im))
def difference(image1, image2):
"""
Returns the absolute value of the pixel-by-pixel difference between the two
images.
.. code-block:: python
out = abs(image1 - image2)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_difference(image2.im))
def multiply(image1, image2):
"""
Superimposes two images on top of each other.
If you multiply an image with a solid black image, the result is black. If
you multiply with a solid white image, the image is unaffected.
.. code-block:: python
out = image1 * image2 / MAX
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_multiply(image2.im))
def screen(image1, image2):
"""
Superimposes two inverted images on top of each other.
.. code-block:: python
out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_screen(image2.im))
def add(image1, image2, scale=1.0, offset=0):
"""
Adds two images, dividing the result by scale and adding the
offset. If omitted, scale defaults to 1.0, and offset to 0.0.
.. code-block:: python
out = ((image1 + image2) / scale + offset)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_add(image2.im, scale, offset))
def subtract(image1, image2, scale=1.0, offset=0):
"""
Subtracts two images, dividing the result by scale and adding the
offset. If omitted, scale defaults to 1.0, and offset to 0.0.
.. code-block:: python
out = ((image1 - image2) / scale + offset)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
def add_modulo(image1, image2):
"""Add two images, without clipping the result.
.. code-block:: python
out = ((image1 + image2) % MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_add_modulo(image2.im))
def subtract_modulo(image1, image2):
"""Subtract two images, without clipping the result.
.. code-block:: python
out = ((image1 - image2) % MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_subtract_modulo(image2.im))
def logical_and(image1, image2):
"""Logical AND between two images.
.. code-block:: python
out = ((image1 and image2) % MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_and(image2.im))
def logical_or(image1, image2):
"""Logical OR between two images.
.. code-block:: python
out = ((image1 or image2) % MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_or(image2.im))
def logical_xor(image1, image2):
"""Logical XOR between two images.
.. code-block:: python
out = ((bool(image1) != bool(image2)) % MAX)
:rtype: :py:class:`~PIL.Image.Image`
"""
image1.load()
image2.load()
return image1._new(image1.im.chop_xor(image2.im))
def blend(image1, image2, alpha):
"""Blend images using constant transparency weight. Alias for
:py:meth:`PIL.Image.Image.blend`.
:rtype: :py:class:`~PIL.Image.Image`
"""
return Image.blend(image1, image2, alpha)
def composite(image1, image2, mask):
"""Create composite using transparency mask. Alias for
:py:meth:`PIL.Image.Image.composite`.
:rtype: :py:class:`~PIL.Image.Image`
"""
return Image.composite(image1, image2, mask)
def offset(image, xoffset, yoffset=None):
"""Returns a copy of the image where data has been offset by the given
distances. Data wraps around the edges. If **yoffset** is omitted, it
is assumed to be equal to **xoffset**.
:param xoffset: The horizontal distance.
:param yoffset: The vertical distance. If omitted, both
distances are set to the same value.
:rtype: :py:class:`~PIL.Image.Image`
"""
if yoffset is None:
yoffset = xoffset
image.load()
return image._new(image.im.offset(xoffset, yoffset))

955
imagecrunch/PIL/ImageCms.py Normal file
View file

@ -0,0 +1,955 @@
# The Python Imaging Library.
# $Id$
# Optional color management support, based on Kevin Cazabon's PyCMS
# library.
# History:
# 2009-03-08 fl Added to PIL.
# Copyright (C) 2002-2003 Kevin Cazabon
# Copyright (c) 2009 by Fredrik Lundh
# Copyright (c) 2013 by Eric Soroos
# See the README file for information on usage and redistribution. See
# below for the original description.
from __future__ import print_function
import sys
from PIL import Image
try:
from PIL import _imagingcms
except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing
# anything in core.
from _util import deferred_error
_imagingcms = deferred_error(ex)
from PIL._util import isStringType
DESCRIPTION = """
pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System
Copyright (C) 2002-2003 Kevin Cazabon
kevin@cazabon.com
http://www.cazabon.com
pyCMS home page: http://www.cazabon.com/pyCMS
littleCMS home page: http://www.littlecms.com
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
Originally released under LGPL. Graciously donated to PIL in
March 2009, for distribution under the standard PIL license
The pyCMS.py module provides a "clean" interface between Python/PIL and
pyCMSdll, taking care of some of the more complex handling of the direct
pyCMSdll functions, as well as error-checking and making sure that all
relevant data is kept together.
While it is possible to call pyCMSdll functions directly, it's not highly
recommended.
Version History:
1.0.0 pil Oct 2013 Port to LCMS 2.
0.1.0 pil mod March 10, 2009
Renamed display profile to proof profile. The proof
profile is the profile of the device that is being
simulated, not the profile of the device which is
actually used to display/print the final simulation
(that'd be the output profile) - also see LCMSAPI.txt
input colorspace -> using 'renderingIntent' -> proof
colorspace -> using 'proofRenderingIntent' -> output
colorspace
Added LCMS FLAGS support.
Added FLAGS["SOFTPROOFING"] as default flag for
buildProofTransform (otherwise the proof profile/intent
would be ignored).
0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms
0.0.2 alpha Jan 6, 2002
Added try/except statements around type() checks of
potential CObjects... Python won't let you use type()
on them, and raises a TypeError (stupid, if you ask
me!)
Added buildProofTransformFromOpenProfiles() function.
Additional fixes in DLL, see DLL code for details.
0.0.1 alpha first public release, Dec. 26, 2002
Known to-do list with current version (of Python interface, not pyCMSdll):
none
"""
VERSION = "1.0.0 pil"
# --------------------------------------------------------------------.
core = _imagingcms
#
# intent/direction values
INTENT_PERCEPTUAL = 0
INTENT_RELATIVE_COLORIMETRIC = 1
INTENT_SATURATION = 2
INTENT_ABSOLUTE_COLORIMETRIC = 3
DIRECTION_INPUT = 0
DIRECTION_OUTPUT = 1
DIRECTION_PROOF = 2
#
# flags
FLAGS = {
"MATRIXINPUT": 1,
"MATRIXOUTPUT": 2,
"MATRIXONLY": (1 | 2),
"NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
# Don't create prelinearization tables on precalculated transforms
# (internal use):
"NOPRELINEARIZATION": 16,
"GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
"NOTCACHE": 64, # Inhibit 1-pixel cache
"NOTPRECALC": 256,
"NULLTRANSFORM": 512, # Don't transform anyway
"HIGHRESPRECALC": 1024, # Use more memory to give better accuracy
"LOWRESPRECALC": 2048, # Use less memory to minimize resources
"WHITEBLACKCOMPENSATION": 8192,
"BLACKPOINTCOMPENSATION": 8192,
"GAMUTCHECK": 4096, # Out of Gamut alarm
"SOFTPROOFING": 16384, # Do softproofing
"PRESERVEBLACK": 32768, # Black preservation
"NODEFAULTRESOURCEDEF": 16777216, # CRD special
"GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints
}
_MAX_FLAG = 0
for flag in FLAGS.values():
if isinstance(flag, int):
_MAX_FLAG = _MAX_FLAG | flag
# --------------------------------------------------------------------.
# Experimental PIL-level API
# --------------------------------------------------------------------.
##
# Profile.
class ImageCmsProfile(object):
def __init__(self, profile):
"""
:param profile: Either a string representing a filename,
a file like object containing a profile or a
low-level profile object
"""
if isStringType(profile):
self._set(core.profile_open(profile), profile)
elif hasattr(profile, "read"):
self._set(core.profile_frombytes(profile.read()))
elif isinstance(profile, _imagingcms.CmsProfile):
self._set(profile)
else:
raise TypeError("Invalid type for Profile")
def _set(self, profile, filename=None):
self.profile = profile
self.filename = filename
if profile:
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
else:
self.product_name = None
self.product_info = None
def tobytes(self):
"""
Returns the profile in a format suitable for embedding in
saved images.
:returns: a bytes object containing the ICC profile.
"""
return core.profile_tobytes(self.profile)
class ImageCmsTransform(Image.ImagePointHandler):
"""
Transform. This can be used with the procedural API, or with the standard
Image.point() method.
Will return the output profile in the output.info['icc_profile'].
"""
def __init__(self, input, output, input_mode, output_mode,
intent=INTENT_PERCEPTUAL, proof=None,
proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0):
if proof is None:
self.transform = core.buildTransform(
input.profile, output.profile,
input_mode, output_mode,
intent,
flags
)
else:
self.transform = core.buildProofTransform(
input.profile, output.profile, proof.profile,
input_mode, output_mode,
intent, proof_intent,
flags
)
# Note: inputMode and outputMode are for pyCMS compatibility only
self.input_mode = self.inputMode = input_mode
self.output_mode = self.outputMode = output_mode
self.output_profile = output
def point(self, im):
return self.apply(im)
def apply(self, im, imOut=None):
im.load()
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.im.id, imOut.im.id)
imOut.info['icc_profile'] = self.output_profile.tobytes()
return imOut
def apply_in_place(self, im):
im.load()
if im.mode != self.output_mode:
raise ValueError("mode mismatch") # wrong output mode
self.transform.apply(im.im.id, im.im.id)
im.info['icc_profile'] = self.output_profile.tobytes()
return im
def get_display_profile(handle=None):
""" (experimental) Fetches the profile for the current display device.
:returns: None if the profile is not known.
"""
if sys.platform == "win32":
from PIL import ImageWin
if isinstance(handle, ImageWin.HDC):
profile = core.get_display_profile_win32(handle, 1)
else:
profile = core.get_display_profile_win32(handle or 0)
else:
try:
get = _imagingcms.get_display_profile
except AttributeError:
return None
else:
profile = get()
return ImageCmsProfile(profile)
# --------------------------------------------------------------------.
# pyCMS compatible layer
# --------------------------------------------------------------------.
class PyCMSError(Exception):
""" (pyCMS) Exception class.
This is used for all errors in the pyCMS API. """
pass
def profileToProfile(
im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL,
outputMode=None, inPlace=0, flags=0):
"""
(pyCMS) Applies an ICC transformation to a given image, mapping from
inputProfile to outputProfile.
If the input or output profiles specified are not valid filenames, a
PyCMSError will be raised. If inPlace == TRUE and outputMode != im.mode,
a PyCMSError will be raised. If an error occurs during application of
the profiles, a PyCMSError will be raised. If outputMode is not a mode
supported by the outputProfile (or by pyCMS), a PyCMSError will be
raised.
This function applies an ICC transformation to im from inputProfile's
color space to outputProfile's color space using the specified rendering
intent to decide how to handle out-of-gamut colors.
OutputMode can be used to specify that a color mode conversion is to
be done using these profiles, but the specified profiles must be able
to handle that mode. I.e., if converting im from RGB to CMYK using
profiles, the input profile must handle RGB data, and the output
profile must handle CMYK data.
:param im: An open PIL image object (i.e. Image.new(...) or
Image.open(...), etc.)
:param inputProfile: String, as a valid filename path to the ICC input
profile you wish to use for this image, or a profile object
:param outputProfile: String, as a valid filename path to the ICC output
profile you wish to use for this image, or a profile object
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:param outputMode: A valid PIL mode for the output image (i.e. "RGB",
"CMYK", etc.). Note: if rendering the image "inPlace", outputMode
MUST be the same mode as the input, or omitted completely. If
omitted, the outputMode will be the same as the mode of the input
image (im.mode)
:param inPlace: Boolean (1 = True, None or 0 = False). If True, the
original image is modified in-place, and None is returned. If False
(default), a new Image object is returned with the transform applied.
:param flags: Integer (0-...) specifying additional flags
:returns: Either None or a new PIL image object, depending on value of
inPlace
:exception PyCMSError:
"""
if outputMode is None:
outputMode = im.mode
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
raise PyCMSError(
"flags must be an integer between 0 and %s" + _MAX_FLAG)
try:
if not isinstance(inputProfile, ImageCmsProfile):
inputProfile = ImageCmsProfile(inputProfile)
if not isinstance(outputProfile, ImageCmsProfile):
outputProfile = ImageCmsProfile(outputProfile)
transform = ImageCmsTransform(
inputProfile, outputProfile, im.mode, outputMode,
renderingIntent, flags=flags
)
if inPlace:
transform.apply_in_place(im)
imOut = None
else:
imOut = transform.apply(im)
except (IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
return imOut
def getOpenProfile(profileFilename):
"""
(pyCMS) Opens an ICC profile file.
The PyCMSProfile object can be passed back into pyCMS for use in creating
transforms and such (as in ImageCms.buildTransformFromOpenProfiles()).
If profileFilename is not a valid filename for an ICC profile, a PyCMSError
will be raised.
:param profileFilename: String, as a valid filename path to the ICC profile
you wish to open, or a file-like object.
:returns: A CmsProfile class object.
:exception PyCMSError:
"""
try:
return ImageCmsProfile(profileFilename)
except (IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def buildTransform(
inputProfile, outputProfile, inMode, outMode,
renderingIntent=INTENT_PERCEPTUAL, flags=0):
"""
(pyCMS) Builds an ICC transform mapping from the inputProfile to the
outputProfile. Use applyTransform to apply the transform to a given
image.
If the input or output profiles specified are not valid filenames, a
PyCMSError will be raised. If an error occurs during creation of the
transform, a PyCMSError will be raised.
If inMode or outMode are not a mode supported by the outputProfile (or
by pyCMS), a PyCMSError will be raised.
This function builds and returns an ICC transform from the inputProfile
to the outputProfile using the renderingIntent to determine what to do
with out-of-gamut colors. It will ONLY work for converting images that
are in inMode to images that are in outMode color format (PIL mode,
i.e. "RGB", "RGBA", "CMYK", etc.).
Building the transform is a fair part of the overhead in
ImageCms.profileToProfile(), so if you're planning on converting multiple
images using the same input/output settings, this can save you time.
Once you have a transform object, it can be used with
ImageCms.applyProfile() to convert images without the need to re-compute
the lookup table for the transform.
The reason pyCMS returns a class object rather than a handle directly
to the transform is that it needs to keep track of the PIL input/output
modes that the transform is meant for. These attributes are stored in
the "inMode" and "outMode" attributes of the object (which can be
manually overridden if you really want to, but I don't know of any
time that would be of use, or would even work).
:param inputProfile: String, as a valid filename path to the ICC input
profile you wish to use for this transform, or a profile object
:param outputProfile: String, as a valid filename path to the ICC output
profile you wish to use for this transform, or a profile object
:param inMode: String, as a valid PIL mode that the appropriate profile
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
:param outMode: String, as a valid PIL mode that the appropriate profile
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:param flags: Integer (0-...) specifying additional flags
:returns: A CmsTransform class object.
:exception PyCMSError:
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
raise PyCMSError(
"flags must be an integer between 0 and %s" + _MAX_FLAG)
try:
if not isinstance(inputProfile, ImageCmsProfile):
inputProfile = ImageCmsProfile(inputProfile)
if not isinstance(outputProfile, ImageCmsProfile):
outputProfile = ImageCmsProfile(outputProfile)
return ImageCmsTransform(
inputProfile, outputProfile, inMode, outMode,
renderingIntent, flags=flags)
except (IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def buildProofTransform(
inputProfile, outputProfile, proofProfile, inMode, outMode,
renderingIntent=INTENT_PERCEPTUAL,
proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC,
flags=FLAGS["SOFTPROOFING"]):
"""
(pyCMS) Builds an ICC transform mapping from the inputProfile to the
outputProfile, but tries to simulate the result that would be
obtained on the proofProfile device.
If the input, output, or proof profiles specified are not valid
filenames, a PyCMSError will be raised.
If an error occurs during creation of the transform, a PyCMSError will
be raised.
If inMode or outMode are not a mode supported by the outputProfile
(or by pyCMS), a PyCMSError will be raised.
This function builds and returns an ICC transform from the inputProfile
to the outputProfile, but tries to simulate the result that would be
obtained on the proofProfile device using renderingIntent and
proofRenderingIntent to determine what to do with out-of-gamut
colors. This is known as "soft-proofing". It will ONLY work for
converting images that are in inMode to images that are in outMode
color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.).
Usage of the resulting transform object is exactly the same as with
ImageCms.buildTransform().
Proof profiling is generally used when using an output device to get a
good idea of what the final printed/displayed image would look like on
the proofProfile device when it's quicker and easier to use the
output device for judging color. Generally, this means that the
output device is a monitor, or a dye-sub printer (etc.), and the simulated
device is something more expensive, complicated, or time consuming
(making it difficult to make a real print for color judgement purposes).
Soft-proofing basically functions by adjusting the colors on the
output device to match the colors of the device being simulated. However,
when the simulated device has a much wider gamut than the output
device, you may obtain marginal results.
:param inputProfile: String, as a valid filename path to the ICC input
profile you wish to use for this transform, or a profile object
:param outputProfile: String, as a valid filename path to the ICC output
(monitor, usually) profile you wish to use for this transform, or a
profile object
:param proofProfile: String, as a valid filename path to the ICC proof
profile you wish to use for this transform, or a profile object
:param inMode: String, as a valid PIL mode that the appropriate profile
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
:param outMode: String, as a valid PIL mode that the appropriate profile
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the input->proof (simulated) transform
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:param proofRenderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for proof->output transform
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:param flags: Integer (0-...) specifying additional flags
:returns: A CmsTransform class object.
:exception PyCMSError:
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
raise PyCMSError(
"flags must be an integer between 0 and %s" + _MAX_FLAG)
try:
if not isinstance(inputProfile, ImageCmsProfile):
inputProfile = ImageCmsProfile(inputProfile)
if not isinstance(outputProfile, ImageCmsProfile):
outputProfile = ImageCmsProfile(outputProfile)
if not isinstance(proofProfile, ImageCmsProfile):
proofProfile = ImageCmsProfile(proofProfile)
return ImageCmsTransform(
inputProfile, outputProfile, inMode, outMode, renderingIntent,
proofProfile, proofRenderingIntent, flags)
except (IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
buildTransformFromOpenProfiles = buildTransform
buildProofTransformFromOpenProfiles = buildProofTransform
def applyTransform(im, transform, inPlace=0):
"""
(pyCMS) Applies a transform to a given image.
If im.mode != transform.inMode, a PyCMSError is raised.
If inPlace == TRUE and transform.inMode != transform.outMode, a
PyCMSError is raised.
If im.mode, transfer.inMode, or transfer.outMode is not supported by
pyCMSdll or the profiles you used for the transform, a PyCMSError is
raised.
If an error occurs while the transform is being applied, a PyCMSError
is raised.
This function applies a pre-calculated transform (from
ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles())
to an image. The transform can be used for multiple images, saving
considerable calculation time if doing the same conversion multiple times.
If you want to modify im in-place instead of receiving a new image as
the return value, set inPlace to TRUE. This can only be done if
transform.inMode and transform.outMode are the same, because we can't
change the mode in-place (the buffer sizes for some modes are
different). The default behavior is to return a new Image object of
the same dimensions in mode transform.outMode.
:param im: A PIL Image object, and im.mode must be the same as the inMode
supported by the transform.
:param transform: A valid CmsTransform class object
:param inPlace: Bool (1 == True, 0 or None == False). If True, im is
modified in place and None is returned, if False, a new Image object
with the transform applied is returned (and im is not changed). The
default is False.
:returns: Either None, or a new PIL Image object, depending on the value of
inPlace. The profile will be returned in the image's
info['icc_profile'].
:exception PyCMSError:
"""
try:
if inPlace:
transform.apply_in_place(im)
imOut = None
else:
imOut = transform.apply(im)
except (TypeError, ValueError) as v:
raise PyCMSError(v)
return imOut
def createProfile(colorSpace, colorTemp=-1):
"""
(pyCMS) Creates a profile.
If colorSpace not in ["LAB", "XYZ", "sRGB"], a PyCMSError is raised
If using LAB and colorTemp != a positive integer, a PyCMSError is raised.
If an error occurs while creating the profile, a PyCMSError is raised.
Use this function to create common profiles on-the-fly instead of
having to supply a profile on disk and knowing the path to it. It
returns a normal CmsProfile object that can be passed to
ImageCms.buildTransformFromOpenProfiles() to create a transform to apply
to images.
:param colorSpace: String, the color space of the profile you wish to
create.
Currently only "LAB", "XYZ", and "sRGB" are supported.
:param colorTemp: Positive integer for the white point for the profile, in
degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
profiles, and is ignored for XYZ and sRGB.
:returns: A CmsProfile class object
:exception PyCMSError:
"""
if colorSpace not in ["LAB", "XYZ", "sRGB"]:
raise PyCMSError(
"Color space not supported for on-the-fly profile creation (%s)"
% colorSpace)
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except:
raise PyCMSError(
"Color temperature must be numeric, \"%s\" not valid"
% colorTemp)
try:
return core.createProfile(colorSpace, colorTemp)
except (TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileName(profile):
"""
(pyCMS) Gets the internal product name for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised If an error occurs while trying to obtain the
name tag, a PyCMSError is raised.
Use this function to obtain the INTERNAL name of the profile (stored
in an ICC tag in the profile itself), usually the one used when the
profile was originally created. Sometimes this tag also contains
additional information supplied by the creator.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal name of the profile as stored
in an ICC tag.
:exception PyCMSError:
"""
try:
# add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
# do it in python, not c.
# // name was "%s - %s" (model, manufacturer) || Description ,
# // but if the Model and Manufacturer were the same or the model
# // was long, Just the model, in 1.x
model = profile.profile.product_model
manufacturer = profile.profile.product_manufacturer
if not (model or manufacturer):
return profile.profile.product_description + "\n"
if not manufacturer or len(model) > 30:
return model + "\n"
return "%s - %s\n" % (model, manufacturer)
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileInfo(profile):
"""
(pyCMS) Gets the internal product information for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the info tag, a PyCMSError
is raised
Use this function to obtain the information stored in the profile's
info tag. This often contains details about the profile, and how it
was created, as supplied by the creator.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal profile information stored in
an ICC tag.
:exception PyCMSError:
"""
try:
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
# add an extra newline to preserve pyCMS compatibility
# Python, not C. the white point bits weren't working well,
# so skipping.
# // info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.product_description
cpright = profile.profile.product_copyright
arr = []
for elt in (description, cpright):
if elt:
arr.append(elt)
return "\r\n\r\n".join(arr) + "\r\n\r\n"
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileCopyright(profile):
"""
(pyCMS) Gets the copyright for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the copyright tag, a PyCMSError
is raised
Use this function to obtain the information stored in the profile's
copyright tag.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal profile information stored in
an ICC tag.
:exception PyCMSError:
"""
try:
# add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
return profile.profile.product_copyright + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileManufacturer(profile):
"""
(pyCMS) Gets the manufacturer for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the manufacturer tag, a
PyCMSError is raised
Use this function to obtain the information stored in the profile's
manufacturer tag.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal profile information stored in
an ICC tag.
:exception PyCMSError:
"""
try:
# add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
return profile.profile.product_manufacturer + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileModel(profile):
"""
(pyCMS) Gets the model for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the model tag, a PyCMSError
is raised
Use this function to obtain the information stored in the profile's
model tag.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal profile information stored in
an ICC tag.
:exception PyCMSError:
"""
try:
# add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
return profile.profile.product_model + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getProfileDescription(profile):
"""
(pyCMS) Gets the description for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the description tag, a PyCMSError
is raised
Use this function to obtain the information stored in the profile's
description tag.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: A string containing the internal profile information stored in an
ICC tag.
:exception PyCMSError:
"""
try:
# add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
return profile.profile.product_description + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def getDefaultIntent(profile):
"""
(pyCMS) Gets the default intent name for the given profile.
If profile isn't a valid CmsProfile object or filename to a profile,
a PyCMSError is raised.
If an error occurs while trying to obtain the default intent, a
PyCMSError is raised.
Use this function to determine the default (and usually best optimized)
rendering intent for this profile. Most profiles support multiple
rendering intents, but are intended mostly for one type of conversion.
If you wish to use a different intent than returned, use
ImageCms.isIntentSupported() to verify it will work first.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:returns: Integer 0-3 specifying the default rendering intent for this
profile.
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:exception PyCMSError:
"""
try:
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
return profile.profile.rendering_intent
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def isIntentSupported(profile, intent, direction):
"""
(pyCMS) Checks if a given intent is supported.
Use this function to verify that you can use your desired
renderingIntent with profile, and that profile can be used for the
input/output/proof profile as you desire.
Some profiles are created specifically for one "direction", can cannot
be used for others. Some profiles can only be used for certain
rendering intents... so it's best to either verify this before trying
to create a transform with them (using this function), or catch the
potential PyCMSError that will occur if they don't support the modes
you select.
:param profile: EITHER a valid CmsProfile object, OR a string of the
filename of an ICC profile.
:param intent: Integer (0-3) specifying the rendering intent you wish to
use with this profile
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
see the pyCMS documentation for details on rendering intents and what
they do.
:param direction: Integer specifying if the profile is to be used for input,
output, or proof
INPUT = 0 (or use ImageCms.DIRECTION_INPUT)
OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT)
PROOF = 2 (or use ImageCms.DIRECTION_PROOF)
:returns: 1 if the intent/direction are supported, -1 if they are not.
:exception PyCMSError:
"""
try:
if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile)
# FIXME: I get different results for the same data w. different
# compilers. Bug in LittleCMS or in the binding?
if profile.profile.is_intent_supported(intent, direction):
return 1
else:
return -1
except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v)
def versions():
"""
(pyCMS) Fetches versions.
"""
return (
VERSION, core.littlecms_version,
sys.version.split()[0], Image.VERSION
)

View file

@ -0,0 +1,309 @@
#
# The Python Imaging Library
# $Id$
#
# map CSS3-style colour description strings to RGB
#
# History:
# 2002-10-24 fl Added support for CSS-style color strings
# 2002-12-15 fl Added RGBA support
# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2
# 2004-07-19 fl Fixed gray/grey spelling issues
# 2009-03-05 fl Fixed rounding error in grayscale calculation
#
# Copyright (c) 2002-2004 by Secret Labs AB
# Copyright (c) 2002-2004 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image
import re
def getrgb(color):
"""
Convert a color string to an RGB tuple. If the string cannot be parsed,
this function raises a :py:exc:`ValueError` exception.
.. versionadded:: 1.1.4
:param color: A color string
:return: ``(red, green, blue[, alpha])``
"""
color = color.lower()
rgb = colormap.get(color, None)
if rgb:
if isinstance(rgb, tuple):
return rgb
colormap[color] = rgb = getrgb(rgb)
return rgb
# check for known string formats
if re.match('#[a-f0-9]{3}$', color):
return (
int(color[1]*2, 16),
int(color[2]*2, 16),
int(color[3]*2, 16),
)
if re.match('#[a-f0-9]{4}$', color):
return (
int(color[1]*2, 16),
int(color[2]*2, 16),
int(color[3]*2, 16),
int(color[4]*2, 16),
)
if re.match('#[a-f0-9]{6}$', color):
return (
int(color[1:3], 16),
int(color[3:5], 16),
int(color[5:7], 16),
)
if re.match('#[a-f0-9]{8}$', color):
return (
int(color[1:3], 16),
int(color[3:5], 16),
int(color[5:7], 16),
int(color[7:9], 16),
)
m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
if m:
return (
int(m.group(1)),
int(m.group(2)),
int(m.group(3))
)
m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
if m:
return (
int((int(m.group(1)) * 255) / 100.0 + 0.5),
int((int(m.group(2)) * 255) / 100.0 + 0.5),
int((int(m.group(3)) * 255) / 100.0 + 0.5)
)
m = re.match(r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color)
if m:
from colorsys import hls_to_rgb
rgb = hls_to_rgb(
float(m.group(1)) / 360.0,
float(m.group(3)) / 100.0,
float(m.group(2)) / 100.0,
)
return (
int(rgb[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5)
)
m = re.match(r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color)
if m:
from colorsys import hsv_to_rgb
rgb = hsv_to_rgb(
float(m.group(1)) / 360.0,
float(m.group(2)) / 100.0,
float(m.group(3)) / 100.0,
)
return (
int(rgb[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5)
)
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$",
color)
if m:
return (
int(m.group(1)),
int(m.group(2)),
int(m.group(3)),
int(m.group(4))
)
raise ValueError("unknown color specifier: %r" % color)
def getcolor(color, mode):
"""
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
greyscale value if the mode is not color or a palette image. If the string
cannot be parsed, this function raises a :py:exc:`ValueError` exception.
.. versionadded:: 1.1.4
:param color: A color string
:return: ``(graylevel [, alpha]) or (red, green, blue[, alpha])``
"""
# same as getrgb, but converts the result to the given mode
color, alpha = getrgb(color), 255
if len(color) == 4:
color, alpha = color[0:3], color[3]
if Image.getmodebase(mode) == "L":
r, g, b = color
color = (r*299 + g*587 + b*114)//1000
if mode[-1] == 'A':
return (color, alpha)
else:
if mode[-1] == 'A':
return color + (alpha,)
return color
colormap = {
# X11 colour table from https://drafts.csswg.org/css-color-4/, with
# gray/grey spelling issues fixed. This is a superset of HTML 4.0
# colour names used in CSS 1.
"aliceblue": "#f0f8ff",
"antiquewhite": "#faebd7",
"aqua": "#00ffff",
"aquamarine": "#7fffd4",
"azure": "#f0ffff",
"beige": "#f5f5dc",
"bisque": "#ffe4c4",
"black": "#000000",
"blanchedalmond": "#ffebcd",
"blue": "#0000ff",
"blueviolet": "#8a2be2",
"brown": "#a52a2a",
"burlywood": "#deb887",
"cadetblue": "#5f9ea0",
"chartreuse": "#7fff00",
"chocolate": "#d2691e",
"coral": "#ff7f50",
"cornflowerblue": "#6495ed",
"cornsilk": "#fff8dc",
"crimson": "#dc143c",
"cyan": "#00ffff",
"darkblue": "#00008b",
"darkcyan": "#008b8b",
"darkgoldenrod": "#b8860b",
"darkgray": "#a9a9a9",
"darkgrey": "#a9a9a9",
"darkgreen": "#006400",
"darkkhaki": "#bdb76b",
"darkmagenta": "#8b008b",
"darkolivegreen": "#556b2f",
"darkorange": "#ff8c00",
"darkorchid": "#9932cc",
"darkred": "#8b0000",
"darksalmon": "#e9967a",
"darkseagreen": "#8fbc8f",
"darkslateblue": "#483d8b",
"darkslategray": "#2f4f4f",
"darkslategrey": "#2f4f4f",
"darkturquoise": "#00ced1",
"darkviolet": "#9400d3",
"deeppink": "#ff1493",
"deepskyblue": "#00bfff",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1e90ff",
"firebrick": "#b22222",
"floralwhite": "#fffaf0",
"forestgreen": "#228b22",
"fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc",
"ghostwhite": "#f8f8ff",
"gold": "#ffd700",
"goldenrod": "#daa520",
"gray": "#808080",
"grey": "#808080",
"green": "#008000",
"greenyellow": "#adff2f",
"honeydew": "#f0fff0",
"hotpink": "#ff69b4",
"indianred": "#cd5c5c",
"indigo": "#4b0082",
"ivory": "#fffff0",
"khaki": "#f0e68c",
"lavender": "#e6e6fa",
"lavenderblush": "#fff0f5",
"lawngreen": "#7cfc00",
"lemonchiffon": "#fffacd",
"lightblue": "#add8e6",
"lightcoral": "#f08080",
"lightcyan": "#e0ffff",
"lightgoldenrodyellow": "#fafad2",
"lightgreen": "#90ee90",
"lightgray": "#d3d3d3",
"lightgrey": "#d3d3d3",
"lightpink": "#ffb6c1",
"lightsalmon": "#ffa07a",
"lightseagreen": "#20b2aa",
"lightskyblue": "#87cefa",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0",
"lime": "#00ff00",
"limegreen": "#32cd32",
"linen": "#faf0e6",
"magenta": "#ff00ff",
"maroon": "#800000",
"mediumaquamarine": "#66cdaa",
"mediumblue": "#0000cd",
"mediumorchid": "#ba55d3",
"mediumpurple": "#9370db",
"mediumseagreen": "#3cb371",
"mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a",
"mediumturquoise": "#48d1cc",
"mediumvioletred": "#c71585",
"midnightblue": "#191970",
"mintcream": "#f5fffa",
"mistyrose": "#ffe4e1",
"moccasin": "#ffe4b5",
"navajowhite": "#ffdead",
"navy": "#000080",
"oldlace": "#fdf5e6",
"olive": "#808000",
"olivedrab": "#6b8e23",
"orange": "#ffa500",
"orangered": "#ff4500",
"orchid": "#da70d6",
"palegoldenrod": "#eee8aa",
"palegreen": "#98fb98",
"paleturquoise": "#afeeee",
"palevioletred": "#db7093",
"papayawhip": "#ffefd5",
"peachpuff": "#ffdab9",
"peru": "#cd853f",
"pink": "#ffc0cb",
"plum": "#dda0dd",
"powderblue": "#b0e0e6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#ff0000",
"rosybrown": "#bc8f8f",
"royalblue": "#4169e1",
"saddlebrown": "#8b4513",
"salmon": "#fa8072",
"sandybrown": "#f4a460",
"seagreen": "#2e8b57",
"seashell": "#fff5ee",
"sienna": "#a0522d",
"silver": "#c0c0c0",
"skyblue": "#87ceeb",
"slateblue": "#6a5acd",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#fffafa",
"springgreen": "#00ff7f",
"steelblue": "#4682b4",
"tan": "#d2b48c",
"teal": "#008080",
"thistle": "#d8bfd8",
"tomato": "#ff6347",
"turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3",
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32",
}

View file

@ -0,0 +1,387 @@
#
# The Python Imaging Library
# $Id$
#
# drawing interface operations
#
# History:
# 1996-04-13 fl Created (experimental)
# 1996-08-07 fl Filled polygons, ellipses.
# 1996-08-13 fl Added text support
# 1998-06-28 fl Handle I and F images
# 1998-12-29 fl Added arc; use arc primitive to draw ellipses
# 1999-01-10 fl Added shape stuff (experimental)
# 1999-02-06 fl Added bitmap support
# 1999-02-11 fl Changed all primitives to take options
# 1999-02-20 fl Fixed backwards compatibility
# 2000-10-12 fl Copy on write, when necessary
# 2001-02-18 fl Use default ink for bitmap/text also in fill mode
# 2002-10-24 fl Added support for CSS-style color strings
# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing
# 2002-12-11 fl Refactored low-level drawing API (work in progress)
# 2004-08-26 fl Made Draw() a factory function, added getdraw() support
# 2004-09-04 fl Added width support to line primitive
# 2004-09-10 fl Added font mode handling
# 2006-06-19 fl Added font bearing support (getmask2)
#
# Copyright (c) 1997-2006 by Secret Labs AB
# Copyright (c) 1996-2006 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import numbers
from . import Image, ImageColor
from ._util import isStringType
"""
A simple 2D drawing interface for PIL images.
<p>
Application code should use the <b>Draw</b> factory, instead of
directly.
"""
class ImageDraw(object):
def __init__(self, im, mode=None):
"""
Create a drawing instance.
:param im: The image to draw in.
:param mode: Optional mode to use for color values. For RGB
images, this argument can be RGB or RGBA (to blend the
drawing into the image). For all other modes, this argument
must be the same as the image mode. If omitted, the mode
defaults to the mode of the image.
"""
im.load()
if im.readonly:
im._copy() # make it writeable
blend = 0
if mode is None:
mode = im.mode
if mode != im.mode:
if mode == "RGBA" and im.mode == "RGB":
blend = 1
else:
raise ValueError("mode mismatch")
if mode == "P":
self.palette = im.palette
else:
self.palette = None
self.im = im.im
self.draw = Image.core.draw(self.im, blend)
self.mode = mode
if mode in ("I", "F"):
self.ink = self.draw.draw_ink(1, mode)
else:
self.ink = self.draw.draw_ink(-1, mode)
if mode in ("1", "P", "I", "F"):
# FIXME: fix Fill2 to properly support matte for I+F images
self.fontmode = "1"
else:
self.fontmode = "L" # aliasing is okay for other modes
self.fill = 0
self.font = None
def getfont(self):
"""
Get the current default font.
:returns: An image font."""
if not self.font:
# FIXME: should add a font repository
from . import ImageFont
self.font = ImageFont.load_default()
return self.font
def _getink(self, ink, fill=None):
if ink is None and fill is None:
if self.fill:
fill = self.ink
else:
ink = self.ink
else:
if ink is not None:
if isStringType(ink):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
ink = self.palette.getcolor(ink)
ink = self.draw.draw_ink(ink, self.mode)
if fill is not None:
if isStringType(fill):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
fill = self.palette.getcolor(fill)
fill = self.draw.draw_ink(fill, self.mode)
return ink, fill
def arc(self, xy, start, end, fill=None):
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_arc(xy, start, end, ink)
def bitmap(self, xy, bitmap, fill=None):
"""Draw a bitmap."""
bitmap.load()
ink, fill = self._getink(fill)
if ink is None:
ink = fill
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
def chord(self, xy, start, end, fill=None, outline=None):
"""Draw a chord."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_chord(xy, start, end, fill, 1)
if ink is not None:
self.draw.draw_chord(xy, start, end, ink, 0)
def ellipse(self, xy, fill=None, outline=None):
"""Draw an ellipse."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_ellipse(xy, fill, 1)
if ink is not None:
self.draw.draw_ellipse(xy, ink, 0)
def line(self, xy, fill=None, width=0):
"""Draw a line, or a connected sequence of line segments."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_lines(xy, ink, width)
def shape(self, shape, fill=None, outline=None):
"""(Experimental) Draw a shape."""
shape.close()
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_outline(shape, fill, 1)
if ink is not None:
self.draw.draw_outline(shape, ink, 0)
def pieslice(self, xy, start, end, fill=None, outline=None):
"""Draw a pieslice."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_pieslice(xy, start, end, fill, 1)
if ink is not None:
self.draw.draw_pieslice(xy, start, end, ink, 0)
def point(self, xy, fill=None):
"""Draw one or more individual pixels."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_points(xy, ink)
def polygon(self, xy, fill=None, outline=None):
"""Draw a polygon."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_polygon(xy, fill, 1)
if ink is not None:
self.draw.draw_polygon(xy, ink, 0)
def rectangle(self, xy, fill=None, outline=None):
"""Draw a rectangle."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_rectangle(xy, fill, 1)
if ink is not None:
self.draw.draw_rectangle(xy, ink, 0)
def _multiline_check(self, text):
"""Draw text."""
split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text
def _multiline_split(self, text):
split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character)
def text(self, xy, text, fill=None, font=None, anchor=None,
*args, **kwargs):
if self._multiline_check(text):
return self.multiline_text(xy, text, fill, font, anchor,
*args, **kwargs)
ink, fill = self._getink(fill)
if font is None:
font = self.getfont()
if ink is None:
ink = fill
if ink is not None:
try:
mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs)
xy = xy[0] + offset[0], xy[1] + offset[1]
except AttributeError:
try:
mask = font.getmask(text, self.fontmode, *args, **kwargs)
except TypeError:
mask = font.getmask(text)
self.draw.draw_bitmap(xy, mask, ink)
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
spacing=4, align="left", direction=None, features=None):
widths = []
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines:
line_width, line_height = self.textsize(line, font)
widths.append(line_width)
max_width = max(max_width, line_width)
left, top = xy
for idx, line in enumerate(lines):
if align == "left":
pass # left = x
elif align == "center":
left += (max_width - widths[idx]) / 2.0
elif align == "right":
left += (max_width - widths[idx])
else:
assert False, 'align must be "left", "center" or "right"'
self.text((left, top), line, fill, font, anchor,
direction=direction, features=features)
top += line_spacing
left = xy[0]
def textsize(self, text, font=None, spacing=4, direction=None,
features=None):
"""Get the size of a given string, in pixels."""
if self._multiline_check(text):
return self.multiline_textsize(text, font, spacing,
direction, features)
if font is None:
font = self.getfont()
return font.getsize(text, direction, features)
def multiline_textsize(self, text, font=None, spacing=4, direction=None,
features=None):
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines:
line_width, line_height = self.textsize(line, font, spacing,
direction, features)
max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing - spacing
def Draw(im, mode=None):
"""
A simple 2D drawing interface for PIL images.
:param im: The image to draw in.
:param mode: Optional mode to use for color values. For RGB
images, this argument can be RGB or RGBA (to blend the
drawing into the image). For all other modes, this argument
must be the same as the image mode. If omitted, the mode
defaults to the mode of the image.
"""
try:
return im.getdraw(mode)
except AttributeError:
return ImageDraw(im, mode)
# experimental access to the outline API
try:
Outline = Image.core.outline
except AttributeError:
Outline = None
def getdraw(im=None, hints=None):
"""
(Experimental) A more advanced 2D drawing interface for PIL images,
based on the WCK interface.
:param im: The image to draw in.
:param hints: An optional list of hints.
:returns: A (drawing context, drawing resource factory) tuple.
"""
# FIXME: this needs more work!
# FIXME: come up with a better 'hints' scheme.
handler = None
if not hints or "nicest" in hints:
try:
from . import _imagingagg as handler
except ImportError:
pass
if handler is None:
from . import ImageDraw2 as handler
if im:
im = handler.Draw(im)
return im, handler
def floodfill(image, xy, value, border=None, thresh=0):
"""
(experimental) Fills a bounded region with a given color.
:param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple). See
:ref:`coordinate-system`.
:param value: Fill color.
:param border: Optional border value. If given, the region consists of
pixels with a color different from the border color. If not given,
the region consists of pixels having the same color as the seed
pixel.
:param thresh: Optional threshold value which specifies a maximum
tolerable difference of a pixel value from the 'background' in
order for it to be replaced. Useful for filling regions of non-
homogeneous, but similar, colors.
"""
# based on an implementation by Eric S. Raymond
pixel = image.load()
x, y = xy
try:
background = pixel[x, y]
if _color_diff(value, background) <= thresh:
return # seed point already has fill color
pixel[x, y] = value
except (ValueError, IndexError):
return # seed point outside image
edge = [(x, y)]
if border is None:
while edge:
newedge = []
for (x, y) in edge:
for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
try:
p = pixel[s, t]
except IndexError:
pass
else:
if _color_diff(p, background) <= thresh:
pixel[s, t] = value
newedge.append((s, t))
edge = newedge
else:
while edge:
newedge = []
for (x, y) in edge:
for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
try:
p = pixel[s, t]
except IndexError:
pass
else:
if p != value and p != border:
pixel[s, t] = value
newedge.append((s, t))
edge = newedge
def _color_diff(rgb1, rgb2):
"""
Uses 1-norm distance to calculate difference between two rgb values.
"""
return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2])

View file

@ -0,0 +1,108 @@
#
# The Python Imaging Library
# $Id$
#
# WCK-style drawing interface operations
#
# History:
# 2003-12-07 fl created
# 2005-05-15 fl updated; added to PIL as ImageDraw2
# 2005-05-15 fl added text support
# 2005-05-20 fl added arc/chord/pieslice support
#
# Copyright (c) 2003-2005 by Secret Labs AB
# Copyright (c) 2003-2005 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
class Pen(object):
def __init__(self, color, width=1, opacity=255):
self.color = ImageColor.getrgb(color)
self.width = width
class Brush(object):
def __init__(self, color, opacity=255):
self.color = ImageColor.getrgb(color)
class Font(object):
def __init__(self, color, file, size=12):
# FIXME: add support for bitmap fonts
self.color = ImageColor.getrgb(color)
self.font = ImageFont.truetype(file, size)
class Draw(object):
def __init__(self, image, size=None, color=None):
if not hasattr(image, "im"):
image = Image.new(image, size, color)
self.draw = ImageDraw.Draw(image)
self.image = image
self.transform = None
def flush(self):
return self.image
def render(self, op, xy, pen, brush=None):
# handle color arguments
outline = fill = None
width = 1
if isinstance(pen, Pen):
outline = pen.color
width = pen.width
elif isinstance(brush, Pen):
outline = brush.color
width = brush.width
if isinstance(brush, Brush):
fill = brush.color
elif isinstance(pen, Brush):
fill = pen.color
# handle transformation
if self.transform:
xy = ImagePath.Path(xy)
xy.transform(self.transform)
# render the item
if op == "line":
self.draw.line(xy, fill=outline, width=width)
else:
getattr(self.draw, op)(xy, fill=fill, outline=outline)
def settransform(self, offset):
(xoffset, yoffset) = offset
self.transform = (1, 0, xoffset, 0, 1, yoffset)
def arc(self, xy, start, end, *options):
self.render("arc", xy, start, end, *options)
def chord(self, xy, start, end, *options):
self.render("chord", xy, start, end, *options)
def ellipse(self, xy, *options):
self.render("ellipse", xy, *options)
def line(self, xy, *options):
self.render("line", xy, *options)
def pieslice(self, xy, start, end, *options):
self.render("pieslice", xy, start, end, *options)
def polygon(self, xy, *options):
self.render("polygon", xy, *options)
def rectangle(self, xy, *options):
self.render("rectangle", xy, *options)
def text(self, xy, text, font):
if self.transform:
xy = ImagePath.Path(xy)
xy.transform(self.transform)
self.draw.text(xy, text, font=font.font, fill=font.color)
def textsize(self, text, font):
return self.draw.textsize(text, font=font.font)

View file

@ -0,0 +1,100 @@
#
# The Python Imaging Library.
# $Id$
#
# image enhancement classes
#
# For a background, see "Image Processing By Interpolation and
# Extrapolation", Paul Haeberli and Douglas Voorhies. Available
# at http://www.graficaobscura.com/interp/index.html
#
# History:
# 1996-03-23 fl Created
# 2009-06-16 fl Fixed mean calculation
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFilter, ImageStat
class _Enhance(object):
def enhance(self, factor):
"""
Returns an enhanced image.
:param factor: A floating point value controlling the enhancement.
Factor 1.0 always returns a copy of the original image,
lower factors mean less color (brightness, contrast,
etc), and higher values more. There are no restrictions
on this value.
:rtype: :py:class:`~PIL.Image.Image`
"""
return Image.blend(self.degenerate, self.image, factor)
class Color(_Enhance):
"""Adjust image color balance.
This class can be used to adjust the colour balance of an image, in
a manner similar to the controls on a colour TV set. An enhancement
factor of 0.0 gives a black and white image. A factor of 1.0 gives
the original image.
"""
def __init__(self, image):
self.image = image
self.intermediate_mode = 'L'
if 'A' in image.getbands():
self.intermediate_mode = 'LA'
self.degenerate = image.convert(self.intermediate_mode).convert(image.mode)
class Contrast(_Enhance):
"""Adjust image contrast.
This class can be used to control the contrast of an image, similar
to the contrast control on a TV set. An enhancement factor of 0.0
gives a solid grey image. A factor of 1.0 gives the original image.
"""
def __init__(self, image):
self.image = image
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
if 'A' in image.getbands():
self.degenerate.putalpha(image.getchannel('A'))
class Brightness(_Enhance):
"""Adjust image brightness.
This class can be used to control the brightness of an image. An
enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the
original image.
"""
def __init__(self, image):
self.image = image
self.degenerate = Image.new(image.mode, image.size, 0)
if 'A' in image.getbands():
self.degenerate.putalpha(image.getchannel('A'))
class Sharpness(_Enhance):
"""Adjust image sharpness.
This class can be used to adjust the sharpness of an image. An
enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the
original image, and a factor of 2.0 gives a sharpened image.
"""
def __init__(self, image):
self.image = image
self.degenerate = image.filter(ImageFilter.SMOOTH)
if 'A' in image.getbands():
self.degenerate.putalpha(image.getchannel('A'))

View file

@ -0,0 +1,667 @@
#
# The Python Imaging Library.
# $Id$
#
# base class for image file handlers
#
# history:
# 1995-09-09 fl Created
# 1996-03-11 fl Fixed load mechanism.
# 1996-04-15 fl Added pcx/xbm decoders.
# 1996-04-30 fl Added encoders.
# 1996-12-14 fl Added load helpers
# 1997-01-11 fl Use encode_to_file where possible
# 1997-08-27 fl Flush output in _save
# 1998-03-05 fl Use memory mapping for some modes
# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B"
# 1999-05-31 fl Added image parser
# 2000-10-12 fl Set readonly flag on memory-mapped images
# 2002-03-20 fl Use better messages for common decoder errors
# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available
# 2003-10-30 fl Added StubImageFile class
# 2004-02-25 fl Made incremental parser more robust
#
# Copyright (c) 1997-2004 by Secret Labs AB
# Copyright (c) 1995-2004 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image
from ._util import isPath
import io
import os
import sys
import struct
MAXBLOCK = 65536
SAFEBLOCK = 1024*1024
LOAD_TRUNCATED_IMAGES = False
ERRORS = {
-1: "image buffer overrun error",
-2: "decoding error",
-3: "unknown error",
-8: "bad configuration",
-9: "out of memory error"
}
def raise_ioerror(error):
try:
message = Image.core.getcodecstatus(error)
except AttributeError:
message = ERRORS.get(error)
if not message:
message = "decoder error %d" % error
raise IOError(message + " when reading image file")
#
# --------------------------------------------------------------------
# Helpers
def _tilesort(t):
# sort on offset
return t[2]
#
# --------------------------------------------------------------------
# ImageFile base class
class ImageFile(Image.Image):
"Base class for image file format handlers."
def __init__(self, fp=None, filename=None):
Image.Image.__init__(self)
self._min_frame = 0
self.tile = None
self.readonly = 1 # until we know better
self.decoderconfig = ()
self.decodermaxblock = MAXBLOCK
if isPath(fp):
# filename
self.fp = open(fp, "rb")
self.filename = fp
self._exclusive_fp = True
else:
# stream
self.fp = fp
self.filename = filename
# can be overridden
self._exclusive_fp = None
try:
self._open()
except (IndexError, # end of data
TypeError, # end of data (ord)
KeyError, # unsupported mode
EOFError, # got header but not the first frame
struct.error) as v:
# close the file only if we have opened it this constructor
if self._exclusive_fp:
self.fp.close()
raise SyntaxError(v)
if not self.mode or self.size[0] <= 0:
raise SyntaxError("not identified by this driver")
def draft(self, mode, size):
"Set draft mode"
pass
def get_format_mimetype(self):
if self.format is None:
return
return Image.MIME.get(self.format.upper())
def verify(self):
"Check file integrity"
# raise exception if something's wrong. must be called
# directly after open, and closes file when finished.
if self._exclusive_fp:
self.fp.close()
self.fp = None
def load(self):
"Load image data based on tile list"
pixel = Image.Image.load(self)
if self.tile is None:
raise IOError("cannot load this image")
if not self.tile:
return pixel
self.map = None
use_mmap = self.filename and len(self.tile) == 1
# As of pypy 2.1.0, memory mapping was failing here.
use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info')