update windows build to Python 3.7
This commit is contained in:
parent
73105fa71e
commit
ddc59ab92d
5761 changed files with 750298 additions and 213405 deletions
|
|
@ -17,8 +17,9 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL import FontFile
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, FontFile
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -109,20 +110,6 @@ class BdfFontFile(FontFile.FontFile):
|
|||
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:
|
||||
|
|
|
|||
435
Lib/site-packages/PIL/BlpImagePlugin.py
Normal file
435
Lib/site-packages/PIL/BlpImagePlugin.py
Normal 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)
|
||||
|
|
@ -24,18 +24,13 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
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"
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16le
|
||||
o32 = _binary.o32le
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Read BMP file
|
||||
|
|
@ -55,17 +50,25 @@ 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
|
||||
# ------------------------------------------------------------- Description
|
||||
format_description = "Windows Bitmap"
|
||||
format = "BMP"
|
||||
# --------------------------------------------------- BMP Compression values
|
||||
COMPRESSIONS = {'RAW': 0, 'RLE8': 1, 'RLE4': 2, 'BITFIELDS': 3, 'JPEG': 4, 'PNG': 5}
|
||||
|
||||
# -------------------------------------------------- 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):
|
||||
|
|
@ -73,13 +76,18 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
read, seek = self.fp.read, self.fp.seek
|
||||
if header:
|
||||
seek(header)
|
||||
file_info = dict()
|
||||
file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size)
|
||||
file_info = {}
|
||||
# read bmp header size @offset 14 (this is part of the header size)
|
||||
file_info['header_size'] = i32(read(4))
|
||||
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 requested, read header at a specific position
|
||||
# read the rest of the bmp header, without its size
|
||||
header_data = ImageFile._safe_read(self.fp,
|
||||
file_info['header_size'] - 4)
|
||||
|
||||
# -------------------------------------------------- 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])
|
||||
|
|
@ -87,18 +95,24 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
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
|
||||
|
||||
# --------------------------------------------- Windows Bitmap v2 to v5
|
||||
# v3, OS/2 v2, v4, v5
|
||||
elif file_info['header_size'] in (40, 64, 108, 124):
|
||||
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['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]))
|
||||
# byte size of pixel data
|
||||
file_info['data_size'] = i32(header_data[16:20])
|
||||
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(
|
||||
|
|
@ -106,36 +120,68 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
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])
|
||||
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:
|
||||
for mask in ['r_mask', 'g_mask', 'b_mask', 'a_mask']:
|
||||
# 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'])
|
||||
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'])
|
||||
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
|
||||
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
|
||||
|
||||
# ---------------------- 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)
|
||||
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)],
|
||||
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",
|
||||
|
|
@ -143,11 +189,17 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
(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'])]
|
||||
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'])]
|
||||
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:
|
||||
|
|
@ -156,35 +208,48 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
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
|
||||
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
|
||||
|
||||
# ---------------------------------------------------- 1-bit images
|
||||
if not (0 < file_info['colors'] <= 65536):
|
||||
raise IOError("Unsupported BMP Palette size (%d)" % file_info['colors'])
|
||||
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
|
||||
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 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)
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGRX" if padding == 4 else "BGR", palette)
|
||||
|
||||
# ----------------------------- Finally set the tile data for the plugin
|
||||
# ---------------------------- 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'])
|
||||
)]
|
||||
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 """
|
||||
|
|
@ -199,9 +264,9 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
self._bitmap(offset=offset)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# =============================================================================
|
||||
# Image plugin for the DIB format (BMP alias)
|
||||
# ==============================================================================
|
||||
# =============================================================================
|
||||
class DibImageFile(BmpImageFile):
|
||||
|
||||
format = "DIB"
|
||||
|
|
@ -214,6 +279,7 @@ class DibImageFile(BmpImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
# Write BMP file
|
||||
|
||||
|
||||
SAVE = {
|
||||
"1": ("1", 1, 2),
|
||||
"L": ("L", 8, 256),
|
||||
|
|
@ -223,15 +289,12 @@ SAVE = {
|
|||
}
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=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)
|
||||
|
||||
if check:
|
||||
return check
|
||||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (96, 96))
|
||||
|
|
@ -280,6 +343,7 @@ def _save(im, fp, filename, check=0):
|
|||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
|
||||
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
|
||||
Image.register_save(BmpImageFile.format, _save)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
##
|
||||
# Install application-specific BUFR image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific BUFR image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
|
@ -40,14 +40,14 @@ class BufrStubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
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
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@
|
|||
|
||||
class ContainerIO(object):
|
||||
|
||||
##
|
||||
# Create file object.
|
||||
#
|
||||
# @param file Existing file.
|
||||
# @param offset Start of region, in bytes.
|
||||
# @param length Size of region, in bytes.
|
||||
|
||||
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
|
||||
|
|
@ -41,15 +41,15 @@ class ContainerIO(object):
|
|||
def isatty(self):
|
||||
return 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.
|
||||
|
||||
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:
|
||||
|
|
@ -60,23 +60,22 @@ class ContainerIO(object):
|
|||
self.pos = max(0, min(self.pos, self.length))
|
||||
self.fh.seek(self.offset + self.pos)
|
||||
|
||||
##
|
||||
# Get current file pointer.
|
||||
#
|
||||
# @return Offset from start of region, in bytes.
|
||||
|
||||
def tell(self):
|
||||
"""
|
||||
Get current file pointer.
|
||||
|
||||
:returns: Offset from start of region, in bytes.
|
||||
"""
|
||||
return self.pos
|
||||
|
||||
##
|
||||
# Read data.
|
||||
#
|
||||
# @def read(bytes=0)
|
||||
# @param bytes Number of bytes to read. If omitted or zero,
|
||||
# read until end of region.
|
||||
# @return An 8-bit string.
|
||||
|
||||
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:
|
||||
|
|
@ -86,12 +85,12 @@ class ContainerIO(object):
|
|||
self.pos = self.pos + n
|
||||
return self.fh.read(n)
|
||||
|
||||
##
|
||||
# Read a line of text.
|
||||
#
|
||||
# @return An 8-bit string.
|
||||
|
||||
def readline(self):
|
||||
"""
|
||||
Read a line of text.
|
||||
|
||||
:returns: An 8-bit string.
|
||||
"""
|
||||
s = ""
|
||||
while True:
|
||||
c = self.read(1)
|
||||
|
|
@ -102,16 +101,16 @@ class ContainerIO(object):
|
|||
break
|
||||
return s
|
||||
|
||||
##
|
||||
# Read multiple lines of text.
|
||||
#
|
||||
# @return A list of 8-bit strings.
|
||||
|
||||
def readlines(self):
|
||||
l = []
|
||||
"""
|
||||
Read multiple lines of text.
|
||||
|
||||
:returns: A list of 8-bit strings.
|
||||
"""
|
||||
lines = []
|
||||
while True:
|
||||
s = self.readline()
|
||||
if not s:
|
||||
break
|
||||
l.append(s)
|
||||
return l
|
||||
lines.append(s)
|
||||
return lines
|
||||
|
|
|
|||
|
|
@ -16,18 +16,16 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image, BmpImagePlugin, _binary
|
||||
from . import Image, BmpImagePlugin
|
||||
from ._binary import i8, i16le as i16, i32le as i32
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"\0\0\2\0"
|
||||
|
|
@ -58,14 +56,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
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")
|
||||
|
||||
|
|
@ -73,7 +63,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
self._bitmap(i32(m[12:]) + offset)
|
||||
|
||||
# patch up the bitmap height
|
||||
self.size = self.size[0], self.size[1]//2
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,14 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, _binary
|
||||
from PIL.PcxImagePlugin import PcxImageFile
|
||||
from . import Image
|
||||
from ._binary import i32le as i32
|
||||
from .PcxImagePlugin import PcxImageFile
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
i32 = _binary.i32le
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
||||
|
|
@ -42,6 +41,7 @@ class DcxImageFile(PcxImageFile):
|
|||
|
||||
format = "DCX"
|
||||
format_description = "Intel DCX"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
|
||||
|
|
@ -59,6 +59,7 @@ class DcxImageFile(PcxImageFile):
|
|||
self._offset.append(offset)
|
||||
|
||||
self.__fp = self.fp
|
||||
self.frame = None
|
||||
self.seek(0)
|
||||
|
||||
@property
|
||||
|
|
@ -70,8 +71,8 @@ class DcxImageFile(PcxImageFile):
|
|||
return len(self._offset) > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if frame >= len(self._offset):
|
||||
raise EOFError("attempt to seek outside DCX directory")
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.frame = frame
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self._offset[frame])
|
||||
|
|
@ -80,6 +81,15 @@ class DcxImageFile(PcxImageFile):
|
|||
def tell(self):
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
||||
|
||||
|
|
|
|||
173
Lib/site-packages/PIL/DdsImagePlugin.py
Normal file
173
Lib/site-packages/PIL/DdsImagePlugin.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
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))
|
||||
struct.unpack("<11I", header.read(44)) # reserved
|
||||
|
||||
# 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")
|
||||
|
|
@ -22,17 +22,16 @@
|
|||
|
||||
import re
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
|
||||
__version__ = "0.5"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i32 = _binary.i32le
|
||||
o32 = _binary.o32le
|
||||
|
||||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ if sys.platform.startswith('win'):
|
|||
if hasattr(shutil, 'which'):
|
||||
which = shutil.which
|
||||
else:
|
||||
# Python < 3.3
|
||||
# Python 2
|
||||
import distutils.spawn
|
||||
which = distutils.spawn.find_executable
|
||||
for binary in ('gswin32c', 'gswin64c', 'gs'):
|
||||
|
|
@ -59,11 +58,11 @@ def has_ghostscript():
|
|||
if not sys.platform.startswith('win'):
|
||||
import subprocess
|
||||
try:
|
||||
gs = subprocess.Popen(['gs', '--version'], stdout=subprocess.PIPE)
|
||||
gs.stdout.read()
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
subprocess.check_call(['gs', '--version'], stdout=devnull)
|
||||
return True
|
||||
except OSError:
|
||||
# no ghostscript
|
||||
# No Ghostscript
|
||||
pass
|
||||
return False
|
||||
|
||||
|
|
@ -83,9 +82,7 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
# 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 os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
|
@ -100,9 +97,9 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
os.close(in_fd)
|
||||
infile = infile_temp
|
||||
|
||||
# ignore length and offset!
|
||||
# ghostscript can read it
|
||||
# copy whole file to read in ghostscript
|
||||
# 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)
|
||||
|
|
@ -118,18 +115,21 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
lengthfile -= len(s)
|
||||
f.write(s)
|
||||
|
||||
# Build ghostscript command
|
||||
# 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)
|
||||
"-dNOPAUSE", # don't pause between pages,
|
||||
"-dBATCH", # exit after processing
|
||||
"-dNOPAUSE", # don't pause between pages
|
||||
"-dSAFER", # safe mode
|
||||
"-sDEVICE=ppmraw", # ppm driver
|
||||
"-sOutputFile=%s" % outfile, # output file
|
||||
# adjust for image origin
|
||||
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
|
||||
# adjust for image origin
|
||||
"-f", infile, # input file
|
||||
# showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
|
||||
"-c", "showpage",
|
||||
]
|
||||
|
||||
if gs_windows_binary is not None:
|
||||
|
|
@ -137,15 +137,17 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
raise WindowsError('Unable to locate Ghostscript on paths')
|
||||
command[0] = gs_windows_binary
|
||||
|
||||
# push data through ghostscript
|
||||
# push data through Ghostscript
|
||||
try:
|
||||
gs = subprocess.Popen(command, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
gs.stdin.close()
|
||||
status = gs.wait()
|
||||
if status:
|
||||
raise IOError("gs failed (status %d)" % status)
|
||||
im = Image.core.open_ppm(outfile)
|
||||
with open(os.devnull, 'w+b') as devnull:
|
||||
startupinfo = None
|
||||
if sys.platform.startswith('win'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
subprocess.check_call(command, stdin=devnull, stdout=devnull,
|
||||
startupinfo=startupinfo)
|
||||
im = Image.open(outfile)
|
||||
im.load()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
|
|
@ -154,7 +156,7 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
return im
|
||||
return im.im.copy()
|
||||
|
||||
|
||||
class PSFile(object):
|
||||
|
|
@ -201,23 +203,14 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
format = "EPS"
|
||||
format_description = "Encapsulated Postscript"
|
||||
|
||||
mode_map = {1: "L", 2: "LAB", 3: "RGB"}
|
||||
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 bytes is str:
|
||||
# Python2, no encoding conversion necessary
|
||||
fp = open(self.fp.name, "Ur")
|
||||
else:
|
||||
# Python3, can use bare open command.
|
||||
fp = open(self.fp.name, "Ur", encoding='latin-1')
|
||||
except:
|
||||
# Expect this for bytesio/stringio
|
||||
fp = PSFile(self.fp)
|
||||
fp = PSFile(self.fp)
|
||||
|
||||
# go to offset - start of "%!PS"
|
||||
fp.seek(offset)
|
||||
|
|
@ -225,58 +218,61 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
box = None
|
||||
|
||||
self.mode = "RGB"
|
||||
self.size = 1, 1 # FIXME: huh?
|
||||
self._size = 1, 1 # FIXME: huh?
|
||||
|
||||
#
|
||||
# Load EPS header
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
s_raw = fp.readline()
|
||||
s = s_raw.strip('\r\n')
|
||||
|
||||
while s:
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
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")
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error:
|
||||
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)
|
||||
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 Exception:
|
||||
pass
|
||||
|
||||
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")
|
||||
m = field.match(s)
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
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")
|
||||
|
||||
if s[:1] != "%":
|
||||
s_raw = fp.readline()
|
||||
s = s_raw.strip('\r\n')
|
||||
|
||||
if s and s[:1] != "%":
|
||||
break
|
||||
|
||||
#
|
||||
|
|
@ -298,7 +294,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
except ValueError:
|
||||
break
|
||||
|
||||
self.size = int(x), int(y)
|
||||
self._size = int(x), int(y)
|
||||
return
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
|
|
@ -322,7 +318,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# EPS can contain binary data
|
||||
# or start directly with latin coding
|
||||
# more info see:
|
||||
# http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||
# 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:
|
||||
|
|
@ -336,7 +332,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
return
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
|
||||
self.mode = self.im.mode
|
||||
self.size = self.im.size
|
||||
self._size = self.im.size
|
||||
self.tile = []
|
||||
|
||||
def load_seek(self, *args, **kwargs):
|
||||
|
|
@ -366,63 +362,58 @@ def _save(im, fp, filename, eps=1):
|
|||
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
|
||||
wrapped_fp = False
|
||||
if fp != sys.stdout:
|
||||
fp = NoCloseStream(fp)
|
||||
if sys.version_info[0] > 2:
|
||||
if sys.version_info.major > 2:
|
||||
fp = io.TextIOWrapper(fp, encoding='latin-1')
|
||||
wrapped_fp = True
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
#
|
||||
# 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)])
|
||||
|
||||
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()
|
||||
fp.write("\n%%%%EndBinary\n")
|
||||
fp.write("grestore end\n")
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
finally:
|
||||
if wrapped_fp:
|
||||
fp.detach()
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
|
||||
|
||||
Image.register_save(EpsImageFile.format, _save)
|
||||
|
||||
Image.register_extension(EpsImageFile.format, ".ps")
|
||||
Image.register_extension(EpsImageFile.format, ".eps")
|
||||
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
|
||||
|
||||
Image.register_mime(EpsImageFile.format, "application/postscript")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
TAGS = {
|
||||
|
||||
# possibly incomplete
|
||||
0x000b: "ProcessingSoftware",
|
||||
0x00fe: "NewSubfileType",
|
||||
0x00ff: "SubfileType",
|
||||
0x0100: "ImageWidth",
|
||||
|
|
@ -27,12 +28,11 @@ TAGS = {
|
|||
0x0102: "BitsPerSample",
|
||||
0x0103: "Compression",
|
||||
0x0106: "PhotometricInterpretation",
|
||||
0x0107: "Threshholding",
|
||||
0x0107: "Thresholding",
|
||||
0x0108: "CellWidth",
|
||||
0x0109: "CellLenght",
|
||||
0x0109: "CellLength",
|
||||
0x010a: "FillOrder",
|
||||
0x010d: "DocumentName",
|
||||
0x011d: "PageName",
|
||||
0x010e: "ImageDescription",
|
||||
0x010f: "Make",
|
||||
0x0110: "Model",
|
||||
|
|
@ -40,41 +40,80 @@ TAGS = {
|
|||
0x0112: "Orientation",
|
||||
0x0115: "SamplesPerPixel",
|
||||
0x0116: "RowsPerStrip",
|
||||
0x0117: "StripByteConunts",
|
||||
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",
|
||||
|
|
@ -114,6 +153,11 @@ TAGS = {
|
|||
0x9290: "SubsecTime",
|
||||
0x9291: "SubsecTimeOriginal",
|
||||
0x9292: "SubsecTimeDigitized",
|
||||
0x9c9b: "XPTitle",
|
||||
0x9c9c: "XPComment",
|
||||
0x9c9d: "XPAuthor",
|
||||
0x9c9e: "XPKeywords",
|
||||
0x9c9f: "XPSubject",
|
||||
0xa000: "FlashPixVersion",
|
||||
0xa001: "ColorSpace",
|
||||
0xa002: "ExifImageWidth",
|
||||
|
|
@ -151,7 +195,85 @@ TAGS = {
|
|||
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"
|
||||
}
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
##
|
||||
# Install application-specific FITS image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific FITS image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ class FITSStubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
|
|
@ -72,5 +72,4 @@ def _save(im, fp, filename):
|
|||
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
|
||||
Image.register_save(FITSStubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(FITSStubImageFile.format, ".fit")
|
||||
Image.register_extension(FITSStubImageFile.format, ".fits")
|
||||
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])
|
||||
|
|
|
|||
|
|
@ -16,15 +16,11 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, i32le as i32, o8
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
o8 = _binary.o8
|
||||
|
||||
|
||||
#
|
||||
# decoder
|
||||
|
|
@ -41,6 +37,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
format = "FLI"
|
||||
format_description = "Autodesk FLI/FLC Animation"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
|
||||
|
|
@ -52,14 +49,17 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
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])
|
||||
self._size = i16(s[8:10]), i16(s[10:12])
|
||||
|
||||
# animation speed
|
||||
duration = i32(s[16:20])
|
||||
if magic == 0xAF11:
|
||||
duration = (duration * 1000) / 70
|
||||
duration = (duration * 1000) // 70
|
||||
self.info["duration"] = duration
|
||||
|
||||
# look for palette
|
||||
|
|
@ -89,8 +89,6 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
self.__frame = -1
|
||||
self.__fp = self.fp
|
||||
self.__rewind = self.fp.tell()
|
||||
self._n_frames = None
|
||||
self._is_animated = None
|
||||
self.seek(0)
|
||||
|
||||
def _palette(self, palette, shift):
|
||||
|
|
@ -113,49 +111,29 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
@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
|
||||
return self.__framecount
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
return self.__framecount > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if frame == 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 FLI file")
|
||||
self._seek(f)
|
||||
|
||||
def _seek(self, frame):
|
||||
if frame == 0:
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.__offset = 128
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError("cannot seek to frame %d" % frame)
|
||||
|
|
@ -179,10 +157,19 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
||||
|
||||
Image.register_extension(FliImageFile.format, ".fli")
|
||||
Image.register_extension(FliImageFile.format, ".flc")
|
||||
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
from PIL import Image, _binary
|
||||
from . import Image, _binary
|
||||
|
||||
WIDTH = 800
|
||||
|
||||
|
|
@ -88,7 +90,6 @@ class FontFile(object):
|
|||
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):
|
||||
|
|
@ -100,16 +101,13 @@ class FontFile(object):
|
|||
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
|
||||
|
||||
# font metrics
|
||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||
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])
|
||||
fp.close()
|
||||
|
||||
# End of file
|
||||
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])
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from PIL.OleFileIO import i8, i32, MAGIC, OleFileIO
|
||||
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
|
||||
|
|
@ -42,7 +44,7 @@ MODES = {
|
|||
# --------------------------------------------------------------------
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == MAGIC
|
||||
return prefix[:8] == olefile.MAGIC
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -59,7 +61,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
# to be a FlashPix file
|
||||
|
||||
try:
|
||||
self.ole = OleFileIO(self.fp)
|
||||
self.ole = olefile.OleFileIO(self.fp)
|
||||
except IOError:
|
||||
raise SyntaxError("not an FPX file; invalid OLE file")
|
||||
|
||||
|
|
@ -79,7 +81,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
|
||||
# size (highest resolution)
|
||||
|
||||
self.size = prop[0x1000002], prop[0x1000003]
|
||||
self._size = prop[0x1000002], prop[0x1000003]
|
||||
|
||||
size = max(self.size)
|
||||
i = 1
|
||||
|
|
@ -112,8 +114,6 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
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):
|
||||
|
|
@ -141,8 +141,6 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
offset = i32(s, 28)
|
||||
length = i32(s, 32)
|
||||
|
||||
# print size, self.mode, self.rawmode
|
||||
|
||||
if size != self.size:
|
||||
raise IOError("subimage mismatch")
|
||||
|
||||
|
|
@ -216,11 +214,12 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
self.fp = self.ole.openstream(self.stream[:2] +
|
||||
["Subimage 0000 Data"])
|
||||
|
||||
ImageFile.ImageFile.load(self)
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
|
||||
|
||||
Image.register_extension(FpxImageFile.format, ".fpx")
|
||||
|
|
|
|||
107
Lib/site-packages/PIL/FtexImagePlugin.py
Normal file
107
Lib/site-packages/PIL/FtexImagePlugin.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""
|
||||
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):
|
||||
struct.unpack("<I", self.fp.read(4)) # magic
|
||||
struct.unpack("<i", self.fp.read(4)) # version
|
||||
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"])
|
||||
|
|
@ -1,25 +1,36 @@
|
|||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# 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 PIL import Image, ImageFile, _binary
|
||||
|
||||
i32 = _binary.i32be
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32be as i32
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 8 and i32(prefix) >= 20 and i32(prefix[4:8]) == 1
|
||||
return len(prefix) >= 8 and \
|
||||
i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2)
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -31,41 +42,55 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
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 or version != 1:
|
||||
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 or color_depth != 1:
|
||||
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)
|
||||
|
||||
comment = self.fp.read(header_size - 20)[:-1]
|
||||
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))
|
||||
|
||||
self.mode = "L"
|
||||
self.size = width, height
|
||||
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
|
||||
|
||||
# Since the brush is so small, we read the data immediately
|
||||
self.data = self.fp.read(width * height)
|
||||
# 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):
|
||||
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
# create an image out of the brush data block
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.im.frombytes(self.data)
|
||||
self.data = b""
|
||||
self.frombytes(self.fp.read(self._data_size))
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
||||
|
||||
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
||||
Image.register_extension(GbrImageFile.format, ".gbr")
|
||||
|
|
|
|||
|
|
@ -23,19 +23,11 @@
|
|||
# purposes only.
|
||||
|
||||
|
||||
from PIL import ImageFile, ImagePalette, _binary
|
||||
from PIL._util import isPath
|
||||
from . import ImageFile, ImagePalette
|
||||
from ._binary import i8, i16be as i16, i32be as i32
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
i16 = _binary.i16be
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the GD uncompressed format. Note that this format
|
||||
|
|
@ -51,42 +43,43 @@ class GdImageFile(ImageFile.ImageFile):
|
|||
def _open(self):
|
||||
|
||||
# Header
|
||||
s = self.fp.read(775)
|
||||
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[0:2]), i16(s[2:4])
|
||||
self._size = i16(s[2:4]), i16(s[4:6])
|
||||
|
||||
trueColor = i8(s[6])
|
||||
trueColorOffset = 2 if trueColor else 0
|
||||
|
||||
# transparency index
|
||||
tindex = i16(s[5:7])
|
||||
tindex = i32(s[7+trueColorOffset:7+trueColorOffset+4])
|
||||
if tindex < 256:
|
||||
self.info["transparent"] = tindex
|
||||
self.info["transparency"] = tindex
|
||||
|
||||
self.palette = ImagePalette.raw("RGB", s[7:])
|
||||
self.palette = ImagePalette.raw(
|
||||
"XBGR", s[7+trueColorOffset+4:7+trueColorOffset+4+256*4])
|
||||
|
||||
self.tile = [("raw", (0, 0)+self.size, 775, ("L", 0, -1))]
|
||||
self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4,
|
||||
("L", 0, 1))]
|
||||
|
||||
|
||||
##
|
||||
# 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".
|
||||
# @return An image instance.
|
||||
# @exception IOError If the image could not be read.
|
||||
|
||||
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")
|
||||
|
||||
if isPath(fp):
|
||||
filename = fp
|
||||
fp = builtins.open(fp, "rb")
|
||||
else:
|
||||
filename = ""
|
||||
|
||||
try:
|
||||
return GdImageFile(fp, filename)
|
||||
return GdImageFile(fp)
|
||||
except SyntaxError:
|
||||
raise IOError("cannot identify this image file")
|
||||
|
|
|
|||
|
|
@ -24,21 +24,14 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, \
|
||||
ImageChops, ImageSequence, _binary
|
||||
from . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence
|
||||
from ._binary import i8, i16le as i16, o8, o16le as o16
|
||||
|
||||
import itertools
|
||||
|
||||
__version__ = "0.9"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16le
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Identify/read GIF files
|
||||
|
||||
|
|
@ -54,6 +47,8 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
|
||||
format = "GIF"
|
||||
format_description = "Compuserve GIF"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
global_palette = None
|
||||
|
||||
def data(self):
|
||||
|
|
@ -70,7 +65,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("not a GIF file")
|
||||
|
||||
self.info["version"] = s[:6]
|
||||
self.size = i16(s[6:]), i16(s[8:])
|
||||
self._size = i16(s[6:]), i16(s[8:])
|
||||
self.tile = []
|
||||
flags = i8(s[10])
|
||||
bits = (flags & 7) + 1
|
||||
|
|
@ -107,19 +102,22 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
current = self.tell()
|
||||
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
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
|
||||
def seek(self, frame):
|
||||
if frame == self.__frame:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
if frame < self.__frame:
|
||||
self._seek(0)
|
||||
|
|
@ -168,6 +166,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
from copy import copy
|
||||
self.palette = copy(self.global_palette)
|
||||
|
||||
info = {}
|
||||
while True:
|
||||
|
||||
s = self.fp.read(1)
|
||||
|
|
@ -186,8 +185,8 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
#
|
||||
flags = i8(block[0])
|
||||
if flags & 1:
|
||||
self.info["transparency"] = i8(block[3])
|
||||
self.info["duration"] = i16(block[1:3]) * 10
|
||||
info["transparency"] = i8(block[3])
|
||||
info["duration"] = i16(block[1:3]) * 10
|
||||
|
||||
# disposal method - find the value of bits 4 - 6
|
||||
dispose_bits = 0b00011100 & flags
|
||||
|
|
@ -198,15 +197,26 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# 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
|
||||
#
|
||||
while block:
|
||||
if "comment" in info:
|
||||
info["comment"] += block
|
||||
else:
|
||||
info["comment"] = block
|
||||
block = self.data()
|
||||
continue
|
||||
elif i8(s) == 255:
|
||||
#
|
||||
# application extension
|
||||
#
|
||||
self.info["extension"] = block, self.fp.tell()
|
||||
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])
|
||||
info["loop"] = i16(block[1:3])
|
||||
while self.data():
|
||||
pass
|
||||
|
||||
|
|
@ -257,7 +267,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
|
||||
# only dispose the extent in this frame
|
||||
if self.dispose:
|
||||
self.dispose = self.dispose.crop(self.dispose_extent)
|
||||
self.dispose = self._crop(self.dispose, self.dispose_extent)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
|
|
@ -265,6 +275,12 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# self.__fp = None
|
||||
raise EOFError
|
||||
|
||||
for k in ["transparency", "duration", "comment", "extension", "loop"]:
|
||||
if k in info:
|
||||
self.info[k] = info[k]
|
||||
elif k in self.info:
|
||||
del self.info[k]
|
||||
|
||||
self.mode = "L"
|
||||
if self.palette:
|
||||
self.mode = "P"
|
||||
|
|
@ -280,120 +296,206 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
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.im.crop(self.dispose_extent)
|
||||
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()
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Write GIF files
|
||||
|
||||
try:
|
||||
import _imaging_gif
|
||||
except ImportError:
|
||||
_imaging_gif = None
|
||||
|
||||
RAWMODE = {
|
||||
"1": "L",
|
||||
"L": "L",
|
||||
"P": "P",
|
||||
"P": "P"
|
||||
}
|
||||
|
||||
|
||||
def _convert_mode(im, initial_call=False):
|
||||
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
||||
# should automatically convert images on save...)
|
||||
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=1, colors=palette_size)
|
||||
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)
|
||||
for k, v in im_out.info.items():
|
||||
im.encoderinfo.setdefault(k, v)
|
||||
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", im.info.get("duration"))
|
||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
||||
|
||||
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())
|
||||
if frame_count == 0:
|
||||
for k, v in im_frame.info.items():
|
||||
im.encoderinfo.setdefault(k, v)
|
||||
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):
|
||||
|
||||
im.encoderinfo.update(im.info)
|
||||
if _imaging_gif:
|
||||
# call external driver
|
||||
try:
|
||||
_imaging_gif.save(im, fp, filename)
|
||||
return
|
||||
except IOError:
|
||||
pass # write uncompressed file
|
||||
|
||||
if im.mode in RAWMODE:
|
||||
im_out = im.copy()
|
||||
else:
|
||||
im_out = _convert_mode(im, True)
|
||||
|
||||
# header
|
||||
try:
|
||||
palette = im.encoderinfo["palette"]
|
||||
except KeyError:
|
||||
if "palette" in im.encoderinfo or "palette" in im.info:
|
||||
palette = im.encoderinfo.get("palette", im.info.get("palette"))
|
||||
else:
|
||||
palette = None
|
||||
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
||||
|
||||
if save_all:
|
||||
previous = None
|
||||
|
||||
first_frame = None
|
||||
for im_frame in ImageSequence.Iterator(im):
|
||||
im_frame = _convert_mode(im_frame)
|
||||
|
||||
# To specify duration, add the time in milliseconds to getdata(),
|
||||
# e.g. getdata(im_frame, duration=1000)
|
||||
if not previous:
|
||||
# global header
|
||||
first_frame = getheader(im_frame, palette, im.encoderinfo)[0]
|
||||
first_frame += getdata(im_frame, (0, 0), **im.encoderinfo)
|
||||
else:
|
||||
if first_frame:
|
||||
for s in first_frame:
|
||||
fp.write(s)
|
||||
first_frame = None
|
||||
|
||||
# delta frame
|
||||
delta = ImageChops.subtract_modulo(im_frame, previous.copy())
|
||||
bbox = delta.getbbox()
|
||||
|
||||
if bbox:
|
||||
# compress difference
|
||||
for s in getdata(im_frame.crop(bbox),
|
||||
bbox[:2], **im.encoderinfo):
|
||||
fp.write(s)
|
||||
else:
|
||||
# FIXME: what should we do in this case?
|
||||
pass
|
||||
previous = im_frame
|
||||
if first_frame:
|
||||
save_all = False
|
||||
if not save_all:
|
||||
header = getheader(im_out, palette, im.encoderinfo)[0]
|
||||
for s in header:
|
||||
fp.write(s)
|
||||
|
||||
flags = 0
|
||||
|
||||
if get_interlace(im):
|
||||
flags = flags | 64
|
||||
|
||||
# local image header
|
||||
_get_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
|
||||
if not save_all or not _write_multiple_frames(im, fp, palette):
|
||||
_write_single_frame(im, fp, palette)
|
||||
|
||||
fp.write(b";") # end of file
|
||||
|
||||
|
|
@ -402,10 +504,7 @@ def _save(im, fp, filename, save_all=False):
|
|||
|
||||
|
||||
def get_interlace(im):
|
||||
try:
|
||||
interlace = im.encoderinfo["interlace"]
|
||||
except KeyError:
|
||||
interlace = 1
|
||||
interlace = im.encoderinfo.get("interlace", 1)
|
||||
|
||||
# workaround for @PIL153
|
||||
if min(im.size) < 16:
|
||||
|
|
@ -414,7 +513,7 @@ def get_interlace(im):
|
|||
return interlace
|
||||
|
||||
|
||||
def _get_local_header(fp, im, offset, flags):
|
||||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
|
|
@ -425,36 +524,44 @@ def _get_local_header(fp, im, offset, flags):
|
|||
# optimize the block away if transparent color is not used
|
||||
transparent_color_exists = True
|
||||
|
||||
if _get_optimize(im, im.encoderinfo):
|
||||
used_palette_colors = _get_used_palette_colors(im)
|
||||
|
||||
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
||||
if used_palette_colors is not None:
|
||||
# adjust the transparency index after optimize
|
||||
if len(used_palette_colors) < 256:
|
||||
for i in range(len(used_palette_colors)):
|
||||
if used_palette_colors[i] == transparency:
|
||||
transparency = i
|
||||
transparent_color_exists = True
|
||||
break
|
||||
else:
|
||||
transparent_color_exists = False
|
||||
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
|
||||
if transparent_color_exists or duration != 0:
|
||||
transparency_flag = 1 if transparent_color_exists else 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(transparency_flag) + # transparency info present
|
||||
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"]):
|
||||
fp.write(b"!" +
|
||||
o8(254)) # extension intro
|
||||
for i in range(0, len(im.encoderinfo["comment"]), 255):
|
||||
subblock = im.encoderinfo["comment"][i:i+255]
|
||||
fp.write(o8(len(subblock)) +
|
||||
subblock)
|
||||
fp.write(o8(0))
|
||||
if "loop" in im.encoderinfo:
|
||||
number_of_loops = im.encoderinfo["loop"]
|
||||
fp.write(b"!" +
|
||||
|
|
@ -465,17 +572,29 @@ def _get_local_header(fp, im, offset, flags):
|
|||
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_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
|
||||
o8(8)) # bits
|
||||
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
|
||||
|
|
@ -483,25 +602,21 @@ def _save_netpbm(im, fp, filename):
|
|||
|
||||
import os
|
||||
from subprocess import Popen, check_call, PIPE, CalledProcessError
|
||||
import tempfile
|
||||
file = im._dump()
|
||||
|
||||
if im.mode != "RGB":
|
||||
with open(filename, 'wb') as f:
|
||||
stderr = tempfile.TemporaryFile()
|
||||
check_call(["ppmtogif", file], stdout=f, stderr=stderr)
|
||||
else:
|
||||
with open(filename, 'wb') as f:
|
||||
|
||||
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"]
|
||||
stderr = tempfile.TemporaryFile()
|
||||
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr)
|
||||
stderr = tempfile.TemporaryFile()
|
||||
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f,
|
||||
stderr=stderr)
|
||||
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()
|
||||
|
|
@ -520,129 +635,192 @@ def _save_netpbm(im, fp, filename):
|
|||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# GIF utilities
|
||||
# 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):
|
||||
return im.mode in ("P", "L") and info and info.get("optimize", 0)
|
||||
"""
|
||||
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.
|
||||
|
||||
def _get_used_palette_colors(im):
|
||||
used_palette_colors = []
|
||||
: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.
|
||||
|
||||
# check which colors are used
|
||||
i = 0
|
||||
for count in im.histogram():
|
||||
if count:
|
||||
used_palette_colors.append(i)
|
||||
i += 1
|
||||
|
||||
return used_palette_colors
|
||||
|
||||
|
||||
def getheader(im, palette=None, info=None):
|
||||
"""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"]:
|
||||
if info and extensionKey in info and \
|
||||
not (extensionKey == "duration" and info[extensionKey] == 0):
|
||||
version = b"89a"
|
||||
break
|
||||
else:
|
||||
if im.info.get("version") == "89a":
|
||||
version = b"89a"
|
||||
|
||||
header = [
|
||||
b"GIF"+version + # signature + version
|
||||
o16(im.size[0]) + # canvas width
|
||||
o16(im.size[1]) # canvas height
|
||||
]
|
||||
|
||||
if im.mode == "P":
|
||||
if palette and isinstance(palette, bytes):
|
||||
source_palette = palette[:768]
|
||||
else:
|
||||
source_palette = im.im.getpalette("RGB")[:768]
|
||||
else: # L-mode
|
||||
if palette and isinstance(palette, bytes):
|
||||
source_palette = palette[:768]
|
||||
else:
|
||||
source_palette = bytearray([i//3 for i in range(768)])
|
||||
|
||||
used_palette_colors = palette_bytes = None
|
||||
|
||||
if _get_optimize(im, info):
|
||||
used_palette_colors = _get_used_palette_colors(im)
|
||||
# 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
|
||||
if len(used_palette_colors) < 256:
|
||||
palette_bytes = b""
|
||||
new_positions = {}
|
||||
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)
|
||||
|
||||
i = 0
|
||||
# pick only the used colors from the palette
|
||||
for oldPosition in used_palette_colors:
|
||||
palette_bytes += source_palette[oldPosition*3:oldPosition*3+3]
|
||||
new_positions[oldPosition] = i
|
||||
i += 1
|
||||
if optimise or (len(used_palette_colors) <= 128 and
|
||||
max(used_palette_colors) > len(used_palette_colors)):
|
||||
return used_palette_colors
|
||||
|
||||
# replace the palette color id of all pixel with the new id
|
||||
image_bytes = bytearray(im.tobytes())
|
||||
for i in range(len(image_bytes)):
|
||||
image_bytes[i] = new_positions[image_bytes[i]]
|
||||
im.frombytes(bytes(image_bytes))
|
||||
new_palette_bytes = (palette_bytes +
|
||||
(768 - len(palette_bytes)) * b'\x00')
|
||||
im.putpalette(new_palette_bytes)
|
||||
im.palette = ImagePalette.ImagePalette("RGB",
|
||||
palette=palette_bytes,
|
||||
size=len(palette_bytes))
|
||||
|
||||
if not palette_bytes:
|
||||
palette_bytes = source_palette
|
||||
|
||||
# Logical Screen Descriptor
|
||||
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
|
||||
# size of global color table + global color table flag
|
||||
header.append(o8(color_table_size + 128))
|
||||
# background + reserved/aspect
|
||||
if info and "background" in info:
|
||||
background = info["background"]
|
||||
elif "background" in im.info:
|
||||
# This elif is redundant within GifImagePlugin
|
||||
# since im.info parameters are bundled into the info dictionary
|
||||
# However, external scripts may call getheader directly
|
||||
# So this maintains earlier behaviour
|
||||
background = im.info["background"]
|
||||
else:
|
||||
background = 0
|
||||
header.append(o8(background) + o8(0))
|
||||
# end of Logical Screen Descriptor
|
||||
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"
|
||||
|
||||
background = 0
|
||||
if "background" in info:
|
||||
background = info["background"]
|
||||
if isinstance(background, tuple):
|
||||
# WebPImagePlugin stores an RGBA value in info["background"]
|
||||
# So it must be converted to the same format as GifImagePlugin's
|
||||
# info["background"] - a global color table index
|
||||
background = im.palette.getcolor(background)
|
||||
|
||||
palette_bytes = _get_palette_bytes(im)
|
||||
color_table_size = _get_color_table_size(palette_bytes)
|
||||
|
||||
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)
|
||||
|
||||
# Header + Logical Screen Descriptor + Global Color Table
|
||||
header.append(palette_bytes)
|
||||
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):
|
||||
"""Return a list of strings representing this image.
|
||||
The first string is a local image header, the rest contains
|
||||
encoded image data."""
|
||||
"""
|
||||
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 = []
|
||||
|
||||
|
|
@ -653,18 +831,7 @@ def getdata(im, offset=(0, 0), **params):
|
|||
|
||||
fp = Collector()
|
||||
|
||||
try:
|
||||
im.encoderinfo = params
|
||||
|
||||
# local image header
|
||||
_get_local_header(fp, im, offset, 0)
|
||||
|
||||
ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])])
|
||||
|
||||
fp.write(b"\0") # end of image data
|
||||
|
||||
finally:
|
||||
del im.encoderinfo
|
||||
_write_frame_data(fp, im, offset, params)
|
||||
|
||||
return fp.data
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
#
|
||||
|
||||
from math import pi, log, sin, sqrt
|
||||
from PIL._binary import o8
|
||||
from ._binary import o8
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Stuff to translate curve segments to palette values (derived from
|
||||
|
|
@ -55,6 +55,7 @@ def sphere_increasing(middle, pos):
|
|||
def sphere_decreasing(middle, pos):
|
||||
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
||||
|
||||
|
||||
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
#
|
||||
|
||||
import re
|
||||
from PIL._binary import o8
|
||||
from ._binary import o8
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -41,7 +41,7 @@ class GimpPaletteFile(object):
|
|||
if not s:
|
||||
break
|
||||
# skip fields and comment lines
|
||||
if re.match(b"\w+:|#", s):
|
||||
if re.match(br"\w+:|#", s):
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
|
|
|||
|
|
@ -9,17 +9,18 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
##
|
||||
# Install application-specific GRIB image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific GRIB image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ def register_handler(handler):
|
|||
# Image adapter
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[0:4] == b"GRIB" and prefix[7] == b'\x01'
|
||||
return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1
|
||||
|
||||
|
||||
class GribStubImageFile(ImageFile.StubImageFile):
|
||||
|
|
@ -47,7 +48,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
##
|
||||
# Install application-specific HDF5 image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific HDF5 image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
|
|
@ -69,5 +69,4 @@ def _save(im, fp, filename):
|
|||
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
|
||||
Image.register_save(HDF5StubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(HDF5StubImageFile.format, ".h5")
|
||||
Image.register_extension(HDF5StubImageFile.format, ".hdf")
|
||||
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# Mac OS X icns file decoder, based on icns.py by Bob Ippolito.
|
||||
# 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.
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, PngImagePlugin, _binary
|
||||
from PIL import Image, ImageFile, PngImagePlugin
|
||||
from PIL._binary import i8
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
|
|
@ -27,8 +28,6 @@ enable_jpeg2k = hasattr(Image.core, 'jp2klib_version')
|
|||
if enable_jpeg2k:
|
||||
from PIL import Jpeg2KImagePlugin
|
||||
|
||||
i8 = _binary.i8
|
||||
|
||||
HEADERSIZE = 8
|
||||
|
||||
|
||||
|
|
@ -266,13 +265,33 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
def _open(self):
|
||||
self.icns = IcnsFile(self.fp)
|
||||
self.mode = 'RGBA'
|
||||
self.info['sizes'] = self.icns.itersizes()
|
||||
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 = ('',)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
info_size = value
|
||||
if info_size not in self.info['sizes'] and len(info_size) == 2:
|
||||
info_size = (info_size[0], info_size[1], 1)
|
||||
if info_size not in self.info['sizes'] and len(info_size) == 3 and \
|
||||
info_size[2] == 1:
|
||||
simple_sizes = [(size[0] * size[2], size[1] * size[2])
|
||||
for size in self.info['sizes']]
|
||||
if value in simple_sizes:
|
||||
info_size = self.info['sizes'][simple_sizes.index(value)]
|
||||
if info_size not in self.info['sizes']:
|
||||
raise ValueError(
|
||||
"This is not one of the allowed sizes of this image")
|
||||
self._size = value
|
||||
|
||||
def load(self):
|
||||
if len(self.size) == 3:
|
||||
self.best_size = self.size
|
||||
|
|
@ -292,6 +311,8 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
self.size = im.size
|
||||
if self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
self.icns = None
|
||||
self.tile = ()
|
||||
|
|
@ -302,36 +323,40 @@ def _save(im, fp, filename):
|
|||
"""
|
||||
Saves the image as a series of PNG files,
|
||||
that are then converted to a .icns file
|
||||
using the OS X command line utility 'iconutil'.
|
||||
using the macOS command line utility 'iconutil'.
|
||||
|
||||
OS X only.
|
||||
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
|
||||
last_im = None
|
||||
second_path = 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:
|
||||
im_scaled = last_im
|
||||
shutil.copyfile(second_path, first_path)
|
||||
else:
|
||||
im_scaled = im.resize((w, w), Image.LANCZOS)
|
||||
im_scaled.save(os.path.join(iconset, prefix+'.png'))
|
||||
im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS))
|
||||
im_w.save(first_path)
|
||||
|
||||
im_scaled = im.resize((w*2, w*2), Image.LANCZOS)
|
||||
im_scaled.save(os.path.join(iconset, prefix+'@2x.png'))
|
||||
last_im = im_scaled
|
||||
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]
|
||||
stderr = tempfile.TemporaryFile()
|
||||
convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=stderr)
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull)
|
||||
|
||||
convert_proc.stdout.close()
|
||||
|
||||
|
|
@ -343,6 +368,7 @@ def _save(im, fp, filename):
|
|||
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')
|
||||
|
|
@ -354,13 +380,18 @@ if sys.platform == 'darwin':
|
|||
|
||||
|
||||
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(open(sys.argv[1], "rb"))
|
||||
im = Image.open(sys.argv[1])
|
||||
im.save("out.png")
|
||||
if sys.platform == 'windows':
|
||||
os.startfile("out.png")
|
||||
|
|
|
|||
|
|
@ -15,17 +15,18 @@
|
|||
|
||||
# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
||||
# <casadebender@gmail.com>.
|
||||
# https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin
|
||||
# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
||||
#
|
||||
# Icon format references:
|
||||
# * https://en.wikipedia.org/wiki/ICO_(file_format)
|
||||
# * http://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary
|
||||
from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin
|
||||
from ._binary import i8, i16le as i16, i32le as i32
|
||||
from math import log, ceil
|
||||
|
||||
__version__ = "0.1"
|
||||
|
|
@ -33,10 +34,6 @@ __version__ = "0.1"
|
|||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
|
||||
_MAGIC = b"\0\0\1\0"
|
||||
|
||||
|
||||
|
|
@ -44,16 +41,19 @@ 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), (255, 255)])
|
||||
(64, 64), (128, 128), (256, 256)])
|
||||
width, height = im.size
|
||||
filter(lambda x: False if (x[0] > width or x[1] > height or
|
||||
x[0] > 255 or x[1] > 255) else True, sizes)
|
||||
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
|
||||
fp.write(struct.pack("B", width)) # bWidth(1)
|
||||
fp.write(struct.pack("B", height)) # bHeight(1)
|
||||
# 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)
|
||||
|
|
@ -139,7 +139,7 @@ class IcoFile(object):
|
|||
"""
|
||||
Get a list of all available icon sizes and color depths.
|
||||
"""
|
||||
return set((h['width'], h['height']) for h in self.entry)
|
||||
return {(h['width'], h['height']) for h in self.entry}
|
||||
|
||||
def getimage(self, size, bpp=False):
|
||||
"""
|
||||
|
|
@ -169,15 +169,15 @@ class IcoFile(object):
|
|||
im = BmpImagePlugin.DibImageFile(self.buf)
|
||||
|
||||
# change tile dimension to only encompass XOR image
|
||||
im.size = (im.size[0], int(im.size[1] / 2))
|
||||
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 in BmpImagePlugin.BIT2MODE.keys():
|
||||
if mode == BmpImagePlugin.BIT2MODE[k][1]:
|
||||
for k, v in BmpImagePlugin.BIT2MODE.items():
|
||||
if mode == v[1]:
|
||||
bpp = k
|
||||
break
|
||||
|
||||
|
|
@ -215,13 +215,13 @@ class IcoFile(object):
|
|||
total_bytes = int((w * im.size[1]) / 8)
|
||||
|
||||
self.buf.seek(and_mask_offset)
|
||||
maskData = self.buf.read(total_bytes)
|
||||
mask_data = self.buf.read(total_bytes)
|
||||
|
||||
# convert raw data to image
|
||||
mask = Image.frombuffer(
|
||||
'1', # 1 bpp
|
||||
im.size, # (w, h)
|
||||
maskData, # source chars
|
||||
mask_data, # source chars
|
||||
'raw', # raw decoder
|
||||
('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed
|
||||
)
|
||||
|
|
@ -252,7 +252,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
|
||||
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
||||
<casadebender@gmail.com>.
|
||||
https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin
|
||||
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
||||
"""
|
||||
format = "ICO"
|
||||
format_description = "Windows Icon"
|
||||
|
|
@ -263,6 +263,17 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
self.size = self.ico.entry[0]['dim']
|
||||
self.load()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
if value not in self.info['sizes']:
|
||||
raise ValueError(
|
||||
"This is not one of the allowed sizes of this image")
|
||||
self._size = value
|
||||
|
||||
def load(self):
|
||||
im = self.ico.getimage(self.size)
|
||||
# if tile is PNG, it won't really be loaded yet
|
||||
|
|
@ -278,6 +289,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
|
||||
Image.register_save(IcoImageFile.format, _save)
|
||||
Image.register_extension(IcoImageFile.format, ".ico")
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@
|
|||
|
||||
|
||||
import re
|
||||
from PIL import Image, ImageFile, ImagePalette
|
||||
from PIL._binary import i8
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
|
|
@ -109,6 +109,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
|
||||
format = "IM"
|
||||
format_description = "IFUNC Image Memory"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
|
||||
|
|
@ -152,7 +153,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as v:
|
||||
except re.error:
|
||||
raise SyntaxError("not an IM file")
|
||||
|
||||
if m:
|
||||
|
|
@ -195,7 +196,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("Not an IM file")
|
||||
|
||||
# Basic attributes
|
||||
self.size = self.info[SIZE]
|
||||
self._size = self.info[SIZE]
|
||||
self.mode = self.info[MODE]
|
||||
|
||||
# Skip forward to start of image data
|
||||
|
|
@ -269,11 +270,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
return self.info[FRAMES] > 1
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
if frame < 0 or frame >= self.info[FRAMES]:
|
||||
raise EOFError("seek outside sequence")
|
||||
|
||||
if self.frame == frame:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
|
||||
self.frame = frame
|
||||
|
|
@ -291,13 +288,22 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))]
|
||||
|
||||
def tell(self):
|
||||
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Save IM files
|
||||
|
||||
|
||||
SAVE = {
|
||||
# mode: (im type, raw mode)
|
||||
"1": ("0 1", "1"),
|
||||
|
|
@ -318,20 +324,14 @@ SAVE = {
|
|||
}
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
def _save(im, fp, filename):
|
||||
|
||||
try:
|
||||
image_type, rawmode = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise ValueError("Cannot save %s images as IM" % im.mode)
|
||||
|
||||
try:
|
||||
frames = im.encoderinfo["frames"]
|
||||
except KeyError:
|
||||
frames = 1
|
||||
|
||||
if check:
|
||||
return check
|
||||
frames = im.encoderinfo.get("frames", 1)
|
||||
|
||||
fp.write(("Image type: %s image\r\n" % image_type).encode('ascii'))
|
||||
if filename:
|
||||
|
|
@ -349,6 +349,7 @@ def _save(im, fp, filename, check=0):
|
|||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
|
||||
Image.register_open(ImImageFile.format, ImImageFile)
|
||||
Image.register_save(ImImageFile.format, _save)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
|
||||
def constant(image, value):
|
||||
|
|
@ -54,7 +54,7 @@ def invert(image):
|
|||
def lighter(image1, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the lighter values.
|
||||
the lighter values. At least one of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -70,8 +70,8 @@ def lighter(image1, image2):
|
|||
|
||||
def darker(image1, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image
|
||||
containing the darker values.
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the darker values. At least one of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ def darker(image1, image2):
|
|||
def difference(image1, image2):
|
||||
"""
|
||||
Returns the absolute value of the pixel-by-pixel difference between the two
|
||||
images.
|
||||
images. At least one of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -107,7 +107,8 @@ 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.
|
||||
you multiply with a solid white image, the image is unaffected. At least
|
||||
one of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -123,7 +124,8 @@ def multiply(image1, image2):
|
|||
|
||||
def screen(image1, image2):
|
||||
"""
|
||||
Superimposes two inverted images on top of each other.
|
||||
Superimposes two inverted images on top of each other. At least one of the
|
||||
images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -141,6 +143,7 @@ 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.
|
||||
At least one of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -156,8 +159,9 @@ def add(image1, image2, scale=1.0, offset=0):
|
|||
|
||||
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.
|
||||
Subtracts two images, dividing the result by scale and adding the offset.
|
||||
If omitted, scale defaults to 1.0, and offset to 0.0. At least one of the
|
||||
images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -172,7 +176,8 @@ def subtract(image1, image2, scale=1.0, offset=0):
|
|||
|
||||
|
||||
def add_modulo(image1, image2):
|
||||
"""Add two images, without clipping the result.
|
||||
"""Add two images, without clipping the result. At least one of the images
|
||||
must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -187,7 +192,8 @@ def add_modulo(image1, image2):
|
|||
|
||||
|
||||
def subtract_modulo(image1, image2):
|
||||
"""Subtract two images, without clipping the result.
|
||||
"""Subtract two images, without clipping the result. At least one of the
|
||||
images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -202,7 +208,8 @@ def subtract_modulo(image1, image2):
|
|||
|
||||
|
||||
def logical_and(image1, image2):
|
||||
"""Logical AND between two images.
|
||||
"""Logical AND between two images. At least one of the images must have
|
||||
mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -217,7 +224,8 @@ def logical_and(image1, image2):
|
|||
|
||||
|
||||
def logical_or(image1, image2):
|
||||
"""Logical OR between two images.
|
||||
"""Logical OR between two images. At least one of the images must have
|
||||
mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -232,7 +240,8 @@ def logical_or(image1, image2):
|
|||
|
||||
|
||||
def logical_xor(image1, image2):
|
||||
"""Logical XOR between two images.
|
||||
"""Logical XOR between two images. At least one of the images must have
|
||||
mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
|
||||
# Optional color managment support, based on Kevin Cazabon's PyCMS
|
||||
# Optional color management support, based on Kevin Cazabon's PyCMS
|
||||
# library.
|
||||
|
||||
# History:
|
||||
|
|
@ -18,6 +18,16 @@
|
|||
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
|
||||
|
||||
|
|
@ -85,16 +95,6 @@ VERSION = "1.0.0 pil"
|
|||
|
||||
# --------------------------------------------------------------------.
|
||||
|
||||
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
|
||||
|
||||
core = _imagingcms
|
||||
|
||||
#
|
||||
|
|
@ -162,8 +162,10 @@ class ImageCmsProfile(object):
|
|||
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:
|
||||
self._set(profile) # assume it's already a profile
|
||||
raise TypeError("Invalid type for Profile")
|
||||
|
||||
def _set(self, profile, filename=None):
|
||||
self.profile = profile
|
||||
|
|
@ -188,10 +190,12 @@ class ImageCmsProfile(object):
|
|||
|
||||
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'].
|
||||
"""
|
||||
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,
|
||||
|
|
@ -301,10 +305,10 @@ def profileToProfile(
|
|||
: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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
|
|
@ -359,7 +363,7 @@ def getOpenProfile(profileFilename):
|
|||
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 vaild filename for an ICC profile, a PyCMSError
|
||||
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
|
||||
|
|
@ -420,10 +424,10 @@ def buildTransform(
|
|||
: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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
|
|
@ -508,20 +512,20 @@ def buildProofTransform(
|
|||
: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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
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
|
||||
: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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
|
|
@ -550,6 +554,7 @@ def buildProofTransform(
|
|||
except (IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
buildTransformFromOpenProfiles = buildTransform
|
||||
buildProofTransformFromOpenProfiles = buildProofTransform
|
||||
|
||||
|
|
@ -590,7 +595,8 @@ def applyTransform(im, transform, inPlace=0):
|
|||
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'].
|
||||
inPlace. The profile will be returned in the image's
|
||||
info['icc_profile'].
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
|
@ -641,7 +647,7 @@ def createProfile(colorSpace, colorTemp=-1):
|
|||
if colorSpace == "LAB":
|
||||
try:
|
||||
colorTemp = float(colorTemp)
|
||||
except:
|
||||
except (TypeError, ValueError):
|
||||
raise PyCMSError(
|
||||
"Color temperature must be numeric, \"%s\" not valid"
|
||||
% colorTemp)
|
||||
|
|
@ -721,7 +727,7 @@ def getProfileInfo(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
|
||||
# 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 = []
|
||||
|
|
@ -869,10 +875,10 @@ def getDefaultIntent(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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
|
|
@ -907,15 +913,15 @@ def isIntentSupported(profile, intent, direction):
|
|||
: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)
|
||||
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
|
||||
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
ImageCms.INTENT_SATURATION = 2
|
||||
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
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
|
||||
: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)
|
||||
|
|
@ -947,24 +953,3 @@ def versions():
|
|||
VERSION, core.littlecms_version,
|
||||
sys.version.split()[0], Image.VERSION
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create a cheap manual from the __doc__ strings for the functions above
|
||||
|
||||
print(__doc__)
|
||||
|
||||
for f in dir(sys.modules[__name__]):
|
||||
doc = None
|
||||
try:
|
||||
exec("doc = %s.__doc__" % (f))
|
||||
if "pyCMS" in doc:
|
||||
# so we don't get the __doc__ string for imported modules
|
||||
print("=" * 80)
|
||||
print("%s" % f)
|
||||
print(doc)
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
# End of file
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
import re
|
||||
|
||||
|
||||
|
|
@ -31,50 +31,66 @@ def getrgb(color):
|
|||
:param color: A color string
|
||||
:return: ``(red, green, blue[, alpha])``
|
||||
"""
|
||||
try:
|
||||
rgb = colormap[color]
|
||||
except KeyError:
|
||||
try:
|
||||
# fall back on case-insensitive lookup
|
||||
rgb = colormap[color.lower()]
|
||||
except KeyError:
|
||||
rgb = None
|
||||
# found color in cache
|
||||
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
|
||||
m = re.match("#\w\w\w$", color)
|
||||
if m:
|
||||
if re.match('#[a-f0-9]{3}$', color):
|
||||
return (
|
||||
int(color[1]*2, 16),
|
||||
int(color[2]*2, 16),
|
||||
int(color[3]*2, 16)
|
||||
int(color[3]*2, 16),
|
||||
)
|
||||
m = re.match("#\w\w\w\w\w\w$", color)
|
||||
if m:
|
||||
|
||||
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)
|
||||
int(color[5:7], 16),
|
||||
)
|
||||
m = re.match("rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
||||
|
||||
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("rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
|
||||
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("hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
|
||||
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(
|
||||
|
|
@ -87,7 +103,25 @@ def getrgb(color):
|
|||
int(rgb[1] * 255 + 0.5),
|
||||
int(rgb[2] * 255 + 0.5)
|
||||
)
|
||||
m = re.match("rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$",
|
||||
|
||||
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 (
|
||||
|
|
@ -125,8 +159,9 @@ def getcolor(color, mode):
|
|||
return color + (alpha,)
|
||||
return color
|
||||
|
||||
|
||||
colormap = {
|
||||
# X11 colour table (from "CSS3 module: Color working draft"), with
|
||||
# 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",
|
||||
|
|
@ -248,6 +283,7 @@ colormap = {
|
|||
"plum": "#dda0dd",
|
||||
"powderblue": "#b0e0e6",
|
||||
"purple": "#800080",
|
||||
"rebeccapurple": "#663399",
|
||||
"red": "#ff0000",
|
||||
"rosybrown": "#bc8f8f",
|
||||
"royalblue": "#4169e1",
|
||||
|
|
|
|||
|
|
@ -30,32 +30,33 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
import math
|
||||
import numbers
|
||||
import warnings
|
||||
|
||||
from PIL import Image, ImageColor
|
||||
from PIL._util import isStringType
|
||||
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.
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
<p>
|
||||
Application code should use the <b>Draw</b> factory, instead of
|
||||
directly.
|
||||
"""
|
||||
|
||||
|
||||
class ImageDraw(object):
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
|
@ -86,27 +87,14 @@ class ImageDraw(object):
|
|||
self.fill = 0
|
||||
self.font = None
|
||||
|
||||
def setink(self, ink):
|
||||
raise Exception("setink() has been removed. " +
|
||||
"Please use keyword arguments instead.")
|
||||
|
||||
def setfill(self, onoff):
|
||||
raise Exception("setfill() has been removed. " +
|
||||
"Please use keyword arguments instead.")
|
||||
|
||||
def setfont(self, font):
|
||||
warnings.warn("setfont() is deprecated. " +
|
||||
"Please set the attribute directly instead.")
|
||||
# compatibility
|
||||
self.font = font
|
||||
|
||||
##
|
||||
# Get the current default font.
|
||||
|
||||
def getfont(self):
|
||||
"""
|
||||
Get the current default font.
|
||||
|
||||
:returns: An image font."""
|
||||
if not self.font:
|
||||
# FIXME: should add a font repository
|
||||
from PIL import ImageFont
|
||||
from . import ImageFont
|
||||
self.font = ImageFont.load_default()
|
||||
return self.font
|
||||
|
||||
|
|
@ -131,18 +119,14 @@ class ImageDraw(object):
|
|||
fill = self.draw.draw_ink(fill, self.mode)
|
||||
return ink, fill
|
||||
|
||||
##
|
||||
# Draw an arc.
|
||||
|
||||
def arc(self, xy, start, end, fill=None):
|
||||
def arc(self, xy, start, end, fill=None, width=0):
|
||||
"""Draw an arc."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_arc(xy, start, end, ink)
|
||||
|
||||
##
|
||||
# Draw a bitmap.
|
||||
self.draw.draw_arc(xy, start, end, ink, width)
|
||||
|
||||
def bitmap(self, xy, bitmap, fill=None):
|
||||
"""Draw a bitmap."""
|
||||
bitmap.load()
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is None:
|
||||
|
|
@ -150,101 +134,136 @@ class ImageDraw(object):
|
|||
if ink is not None:
|
||||
self.draw.draw_bitmap(xy, bitmap.im, ink)
|
||||
|
||||
##
|
||||
# Draw a chord.
|
||||
|
||||
def chord(self, xy, start, end, fill=None, outline=None):
|
||||
def chord(self, xy, start, end, fill=None, outline=None, width=0):
|
||||
"""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)
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_chord(xy, start, end, ink, 0, width)
|
||||
|
||||
##
|
||||
# Draw an ellipse.
|
||||
|
||||
def ellipse(self, xy, fill=None, outline=None):
|
||||
def ellipse(self, xy, fill=None, outline=None, width=0):
|
||||
"""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)
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_ellipse(xy, ink, 0, width)
|
||||
|
||||
##
|
||||
# Draw a line, or a connected sequence of line segments.
|
||||
|
||||
def line(self, xy, fill=None, width=0):
|
||||
ink, fill = self._getink(fill)
|
||||
def line(self, xy, fill=None, width=0, joint=None):
|
||||
"""Draw a line, or a connected sequence of line segments."""
|
||||
ink = self._getink(fill)[0]
|
||||
if ink is not None:
|
||||
self.draw.draw_lines(xy, ink, width)
|
||||
if joint == "curve" and width > 4:
|
||||
for i in range(1, len(xy)-1):
|
||||
point = xy[i]
|
||||
angles = [
|
||||
math.degrees(math.atan2(
|
||||
end[0] - start[0], start[1] - end[1]
|
||||
)) % 360
|
||||
for start, end in ((xy[i-1], point), (point, xy[i+1]))
|
||||
]
|
||||
if angles[0] == angles[1]:
|
||||
# This is a straight line, so no joint is required
|
||||
continue
|
||||
|
||||
##
|
||||
# (Experimental) Draw a shape.
|
||||
def coord_at_angle(coord, angle):
|
||||
x, y = coord
|
||||
angle -= 90
|
||||
distance = width/2 - 1
|
||||
return tuple([
|
||||
p +
|
||||
(math.floor(p_d) if p_d > 0 else math.ceil(p_d))
|
||||
for p, p_d in
|
||||
((x, distance * math.cos(math.radians(angle))),
|
||||
(y, distance * math.sin(math.radians(angle))))
|
||||
])
|
||||
flipped = ((angles[1] > angles[0] and
|
||||
angles[1] - 180 > angles[0]) or
|
||||
(angles[1] < angles[0] and
|
||||
angles[1] + 180 > angles[0]))
|
||||
coords = [
|
||||
(point[0] - width/2 + 1, point[1] - width/2 + 1),
|
||||
(point[0] + width/2 - 1, point[1] + width/2 - 1)
|
||||
]
|
||||
if flipped:
|
||||
start, end = (angles[1] + 90, angles[0] + 90)
|
||||
else:
|
||||
start, end = (angles[0] - 90, angles[1] - 90)
|
||||
self.pieslice(coords, start - 90, end - 90, fill)
|
||||
|
||||
if width > 8:
|
||||
# Cover potential gaps between the line and the joint
|
||||
if flipped:
|
||||
gapCoords = [
|
||||
coord_at_angle(point, angles[0]+90),
|
||||
point,
|
||||
coord_at_angle(point, angles[1]+90)
|
||||
]
|
||||
else:
|
||||
gapCoords = [
|
||||
coord_at_angle(point, angles[0]-90),
|
||||
point,
|
||||
coord_at_angle(point, angles[1]-90)
|
||||
]
|
||||
self.line(gapCoords, fill, width=3)
|
||||
|
||||
def shape(self, shape, fill=None, outline=None):
|
||||
# experimental
|
||||
"""(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:
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_outline(shape, ink, 0)
|
||||
|
||||
##
|
||||
# Draw a pieslice.
|
||||
|
||||
def pieslice(self, xy, start, end, fill=None, outline=None):
|
||||
def pieslice(self, xy, start, end, fill=None, outline=None, width=0):
|
||||
"""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)
|
||||
|
||||
##
|
||||
# Draw one or more individual pixels.
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
|
||||
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw a polygon.
|
||||
|
||||
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:
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_polygon(xy, ink, 0)
|
||||
|
||||
##
|
||||
# Draw a rectangle.
|
||||
|
||||
def rectangle(self, xy, fill=None, outline=None):
|
||||
def rectangle(self, xy, fill=None, outline=None, width=0):
|
||||
"""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)
|
||||
|
||||
##
|
||||
# Draw text.
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_rectangle(xy, ink, 0, width)
|
||||
|
||||
def _multiline_check(self, text):
|
||||
split_character = "\n" if isinstance(text, type("")) else b"\n"
|
||||
"""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, type("")) else b"\n"
|
||||
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):
|
||||
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)
|
||||
|
||||
return self.multiline_text(xy, text, fill, font, anchor,
|
||||
*args, **kwargs)
|
||||
ink, fill = self._getink(fill)
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
|
|
@ -252,17 +271,18 @@ class ImageDraw(object):
|
|||
ink = fill
|
||||
if ink is not None:
|
||||
try:
|
||||
mask, offset = font.getmask2(text, self.fontmode)
|
||||
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)
|
||||
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"):
|
||||
spacing=4, align="left", direction=None, features=None):
|
||||
widths = []
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
|
|
@ -280,48 +300,52 @@ class ImageDraw(object):
|
|||
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)
|
||||
raise ValueError('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]
|
||||
|
||||
##
|
||||
# Get the size of a given string, in pixels.
|
||||
|
||||
def textsize(self, text, font=None):
|
||||
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)
|
||||
return self.multiline_textsize(text, font, spacing,
|
||||
direction, features)
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
return font.getsize(text)
|
||||
return font.getsize(text, direction, features)
|
||||
|
||||
def multiline_textsize(self, text, font=None, spacing=4):
|
||||
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)
|
||||
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
|
||||
return max_width, len(lines)*line_spacing - spacing
|
||||
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
|
@ -329,79 +353,90 @@ except AttributeError:
|
|||
Outline = 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.
|
||||
# @return A (drawing context, drawing resource factory) tuple.
|
||||
|
||||
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 PIL import _imagingagg as handler
|
||||
from . import _imagingagg as handler
|
||||
except ImportError:
|
||||
pass
|
||||
if handler is None:
|
||||
from PIL import ImageDraw2 as handler
|
||||
from . import ImageDraw2 as handler
|
||||
if im:
|
||||
im = handler.Draw(im)
|
||||
return im, handler
|
||||
|
||||
|
||||
##
|
||||
# (experimental) Fills a bounded region with a given color.
|
||||
#
|
||||
# @param image Target image.
|
||||
# @param xy Seed position (a 2-item coordinate tuple).
|
||||
# @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.
|
||||
def floodfill(image, xy, value, border=None, thresh=0):
|
||||
"""
|
||||
(experimental) Fills a bounded region with a given color.
|
||||
|
||||
def floodfill(image, xy, value, border=None):
|
||||
"Fill bounded region."
|
||||
: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
|
||||
# amended by yo1995 @20180806
|
||||
pixel = image.load()
|
||||
x, y = xy
|
||||
try:
|
||||
background = pixel[x, y]
|
||||
if background == value:
|
||||
if _color_diff(value, background) <= thresh:
|
||||
return # seed point already has fill color
|
||||
pixel[x, y] = value
|
||||
except IndexError:
|
||||
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
|
||||
edge = {(x, y)}
|
||||
# use a set to keep record of current and previous edge pixels
|
||||
# to reduce memory consumption
|
||||
full_edge = set()
|
||||
while edge:
|
||||
new_edge = set()
|
||||
for (x, y) in edge: # 4 adjacent method
|
||||
for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
|
||||
if (s, t) in full_edge:
|
||||
continue # if already processed, skip
|
||||
try:
|
||||
p = pixel[s, t]
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
else:
|
||||
full_edge.add((s, t))
|
||||
if border is None:
|
||||
fill = _color_diff(p, background) <= thresh
|
||||
else:
|
||||
if p == background:
|
||||
pixel[s, t] = value
|
||||
newedge.append((s, t))
|
||||
edge = newedge
|
||||
fill = p != value and p != border
|
||||
if fill:
|
||||
pixel[s, t] = value
|
||||
new_edge.add((s, t))
|
||||
full_edge = edge # discard pixels processed
|
||||
edge = new_edge
|
||||
|
||||
|
||||
def _color_diff(color1, color2):
|
||||
"""
|
||||
Uses 1-norm distance to calculate difference between two values.
|
||||
"""
|
||||
if isinstance(color2, tuple):
|
||||
return sum([abs(color1[i]-color2[i]) for i in range(0, len(color2))])
|
||||
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
|
||||
return abs(color1-color2)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
|
||||
|
||||
class Pen(object):
|
||||
|
|
@ -98,9 +98,6 @@ class Draw(object):
|
|||
def rectangle(self, xy, *options):
|
||||
self.render("rectangle", xy, *options)
|
||||
|
||||
def symbol(self, xy, symbol, *options):
|
||||
raise NotImplementedError("not in this version")
|
||||
|
||||
def text(self, xy, text, font):
|
||||
if self.transform:
|
||||
xy = ImagePath.Path(xy)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFilter, ImageStat
|
||||
from . import Image, ImageFilter, ImageStat
|
||||
|
||||
|
||||
class _Enhance(object):
|
||||
|
|
@ -51,7 +51,8 @@ class Color(_Enhance):
|
|||
if 'A' in image.getbands():
|
||||
self.intermediate_mode = 'LA'
|
||||
|
||||
self.degenerate = image.convert(self.intermediate_mode).convert(image.mode)
|
||||
self.degenerate = image.convert(
|
||||
self.intermediate_mode).convert(image.mode)
|
||||
|
||||
|
||||
class Contrast(_Enhance):
|
||||
|
|
@ -67,7 +68,7 @@ class Contrast(_Enhance):
|
|||
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
|
||||
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
self.degenerate.putalpha(image.getchannel('A'))
|
||||
|
||||
|
||||
class Brightness(_Enhance):
|
||||
|
|
@ -82,7 +83,7 @@ class Brightness(_Enhance):
|
|||
self.degenerate = Image.new(image.mode, image.size, 0)
|
||||
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
self.degenerate.putalpha(image.getchannel('A'))
|
||||
|
||||
|
||||
class Sharpness(_Enhance):
|
||||
|
|
@ -97,4 +98,4 @@ class Sharpness(_Enhance):
|
|||
self.degenerate = image.filter(ImageFilter.SMOOTH)
|
||||
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
self.degenerate.putalpha(image.getchannel('A'))
|
||||
|
|
|
|||
|
|
@ -27,10 +27,9 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL._util import isPath
|
||||
from . import Image
|
||||
from ._util import isPath
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import struct
|
||||
|
||||
|
|
@ -78,6 +77,10 @@ class ImageFile(Image.Image):
|
|||
def __init__(self, fp=None, filename=None):
|
||||
Image.Image.__init__(self)
|
||||
|
||||
self._min_frame = 0
|
||||
|
||||
self.custom_mimetype = None
|
||||
|
||||
self.tile = None
|
||||
self.readonly = 1 # until we know better
|
||||
|
||||
|
|
@ -88,10 +91,13 @@ class ImageFile(Image.Image):
|
|||
# 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()
|
||||
|
|
@ -100,6 +106,9 @@ class ImageFile(Image.Image):
|
|||
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:
|
||||
|
|
@ -110,11 +119,18 @@ class ImageFile(Image.Image):
|
|||
|
||||
pass
|
||||
|
||||
def get_format_mimetype(self):
|
||||
if self.format is None:
|
||||
return
|
||||
return self.custom_mimetype or 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):
|
||||
|
|
@ -150,32 +166,37 @@ class ImageFile(Image.Image):
|
|||
|
||||
if use_mmap:
|
||||
# try memory mapping
|
||||
d, e, o, a = self.tile[0]
|
||||
if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES:
|
||||
decoder_name, extents, offset, args = self.tile[0]
|
||||
if decoder_name == "raw" and len(args) >= 3 and \
|
||||
args[0] == self.mode and \
|
||||
args[0] in Image._MAPMODES:
|
||||
try:
|
||||
if hasattr(Image.core, "map"):
|
||||
# use built-in mapper
|
||||
# use built-in mapper WIN32 only
|
||||
self.map = Image.core.map(self.filename)
|
||||
self.map.seek(o)
|
||||
self.map.seek(offset)
|
||||
self.im = self.map.readimage(
|
||||
self.mode, self.size, a[1], a[2]
|
||||
self.mode, self.size, args[1], args[2]
|
||||
)
|
||||
else:
|
||||
# use mmap, if possible
|
||||
import mmap
|
||||
fp = open(self.filename, "r+")
|
||||
size = os.path.getsize(self.filename)
|
||||
# FIXME: on Unix, use PROT_READ etc
|
||||
self.map = mmap.mmap(fp.fileno(), size)
|
||||
with open(self.filename, "r") as fp:
|
||||
self.map = mmap.mmap(fp.fileno(), 0,
|
||||
access=mmap.ACCESS_READ)
|
||||
self.im = Image.core.map_buffer(
|
||||
self.map, self.size, d, e, o, a
|
||||
)
|
||||
self.map, self.size, decoder_name, extents,
|
||||
offset, args)
|
||||
readonly = 1
|
||||
# After trashing self.im,
|
||||
# we might need to reload the palette data.
|
||||
if self.palette:
|
||||
self.palette.dirty = 1
|
||||
except (AttributeError, EnvironmentError, ImportError):
|
||||
self.map = None
|
||||
|
||||
self.load_prepare()
|
||||
|
||||
err_code = -3 # initialize to unknown error
|
||||
if not self.map:
|
||||
# sort tiles in file order
|
||||
self.tile.sort(key=_tilesort)
|
||||
|
|
@ -186,62 +207,58 @@ class ImageFile(Image.Image):
|
|||
except AttributeError:
|
||||
prefix = b""
|
||||
|
||||
for d, e, o, a in self.tile:
|
||||
d = Image._getdecoder(self.mode, d, a, self.decoderconfig)
|
||||
seek(o)
|
||||
for decoder_name, extents, offset, args in self.tile:
|
||||
decoder = Image._getdecoder(self.mode, decoder_name,
|
||||
args, self.decoderconfig)
|
||||
try:
|
||||
d.setimage(self.im, e)
|
||||
except ValueError:
|
||||
continue
|
||||
b = prefix
|
||||
while True:
|
||||
try:
|
||||
s = read(self.decodermaxblock)
|
||||
except (IndexError, struct.error): # truncated png/gif
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated")
|
||||
seek(offset)
|
||||
decoder.setimage(self.im, extents)
|
||||
if decoder.pulls_fd:
|
||||
decoder.setfd(self.fp)
|
||||
status, err_code = decoder.decode(b"")
|
||||
else:
|
||||
b = prefix
|
||||
while True:
|
||||
try:
|
||||
s = read(self.decodermaxblock)
|
||||
except (IndexError, struct.error):
|
||||
# truncated png/gif
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated")
|
||||
|
||||
if not s and not d.handles_eof: # truncated jpeg
|
||||
self.tile = []
|
||||
if not s: # truncated jpeg
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
self.tile = []
|
||||
raise IOError("image file is truncated "
|
||||
"(%d bytes not processed)" %
|
||||
len(b))
|
||||
|
||||
# JpegDecode needs to clean things up here either way
|
||||
# If we don't destroy the decompressor,
|
||||
# we have a memory leak.
|
||||
d.cleanup()
|
||||
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated "
|
||||
"(%d bytes not processed)" % len(b))
|
||||
|
||||
b = b + s
|
||||
n, e = d.decode(b)
|
||||
if n < 0:
|
||||
break
|
||||
b = b[n:]
|
||||
# Need to cleanup here to prevent leaks in PyPy
|
||||
d.cleanup()
|
||||
b = b + s
|
||||
n, err_code = decoder.decode(b)
|
||||
if n < 0:
|
||||
break
|
||||
b = b[n:]
|
||||
finally:
|
||||
# Need to cleanup here to prevent leaks
|
||||
decoder.cleanup()
|
||||
|
||||
self.tile = []
|
||||
self.readonly = readonly
|
||||
|
||||
self.fp = None # might be shared
|
||||
|
||||
if not self.map and not LOAD_TRUNCATED_IMAGES and e < 0:
|
||||
# still raised if decoder fails to return anything
|
||||
raise_ioerror(e)
|
||||
|
||||
# post processing
|
||||
if hasattr(self, "tile_post_rotate"):
|
||||
# FIXME: This is a hack to handle rotated PCD's
|
||||
self.im = self.im.rotate(self.tile_post_rotate)
|
||||
self.size = self.im.size
|
||||
|
||||
self.load_end()
|
||||
|
||||
if self._exclusive_fp and self._close_exclusive_fp_after_loading:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
|
||||
if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
|
||||
# still raised if decoder fails to return anything
|
||||
raise_ioerror(err_code)
|
||||
|
||||
return Image.Image.load(self)
|
||||
|
||||
def load_prepare(self):
|
||||
|
|
@ -265,6 +282,16 @@ class ImageFile(Image.Image):
|
|||
# def load_read(self, bytes):
|
||||
# pass
|
||||
|
||||
def _seek_check(self, frame):
|
||||
if (frame < self._min_frame or
|
||||
# Only check upper limit on frames if additional seek operations
|
||||
# are not required to do so
|
||||
(not (hasattr(self, "_n_frames") and self._n_frames is None) and
|
||||
frame >= self.n_frames+self._min_frame)):
|
||||
raise EOFError("attempt to seek outside sequence")
|
||||
|
||||
return self.tell() != frame
|
||||
|
||||
|
||||
class StubImageFile(ImageFile):
|
||||
"""
|
||||
|
|
@ -300,8 +327,6 @@ class Parser(object):
|
|||
"""
|
||||
Incremental image parser. This class implements the standard
|
||||
feed/close consumer interface.
|
||||
|
||||
In Python 2.x, this is an old-style class.
|
||||
"""
|
||||
incremental = None
|
||||
image = None
|
||||
|
|
@ -372,11 +397,8 @@ class Parser(object):
|
|||
|
||||
# attempt to open this file
|
||||
try:
|
||||
try:
|
||||
fp = io.BytesIO(self.data)
|
||||
with io.BytesIO(self.data) as fp:
|
||||
im = Image.open(fp)
|
||||
finally:
|
||||
fp.close() # explicitly close the virtual file
|
||||
except IOError:
|
||||
# traceback.print_exc()
|
||||
pass # not enough data
|
||||
|
|
@ -403,6 +425,12 @@ class Parser(object):
|
|||
|
||||
self.image = im
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
(Consumer) Close the stream.
|
||||
|
|
@ -424,12 +452,11 @@ class Parser(object):
|
|||
if self.data:
|
||||
# incremental parsing not possible; reopen the file
|
||||
# not that we have all data
|
||||
try:
|
||||
fp = io.BytesIO(self.data)
|
||||
self.image = Image.open(fp)
|
||||
finally:
|
||||
self.image.load()
|
||||
fp.close() # explicitly close the virtual file
|
||||
with io.BytesIO(self.data) as fp:
|
||||
try:
|
||||
self.image = Image.open(fp)
|
||||
finally:
|
||||
self.image.load()
|
||||
return self.image
|
||||
|
||||
|
||||
|
|
@ -466,11 +493,15 @@ def _save(im, fp, tile, bufsize=0):
|
|||
if o > 0:
|
||||
fp.seek(o, 0)
|
||||
e.setimage(im.im, b)
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
|
|
@ -481,7 +512,11 @@ def _save(im, fp, tile, bufsize=0):
|
|||
if o > 0:
|
||||
fp.seek(o, 0)
|
||||
e.setimage(im.im, b)
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
|
|
@ -511,3 +546,130 @@ def _safe_read(fp, size):
|
|||
data.append(block)
|
||||
size -= len(block)
|
||||
return b"".join(data)
|
||||
|
||||
|
||||
class PyCodecState(object):
|
||||
def __init__(self):
|
||||
self.xsize = 0
|
||||
self.ysize = 0
|
||||
self.xoff = 0
|
||||
self.yoff = 0
|
||||
|
||||
def extents(self):
|
||||
return (self.xoff, self.yoff,
|
||||
self.xoff+self.xsize, self.yoff+self.ysize)
|
||||
|
||||
|
||||
class PyDecoder(object):
|
||||
"""
|
||||
Python implementation of a format decoder. Override this class and
|
||||
add the decoding logic in the `decode` method.
|
||||
|
||||
See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
|
||||
"""
|
||||
|
||||
_pulls_fd = False
|
||||
|
||||
def __init__(self, mode, *args):
|
||||
self.im = None
|
||||
self.state = PyCodecState()
|
||||
self.fd = None
|
||||
self.mode = mode
|
||||
self.init(args)
|
||||
|
||||
def init(self, args):
|
||||
"""
|
||||
Override to perform decoder specific initialization
|
||||
|
||||
:param args: Array of args items from the tile entry
|
||||
:returns: None
|
||||
"""
|
||||
self.args = args
|
||||
|
||||
@property
|
||||
def pulls_fd(self):
|
||||
return self._pulls_fd
|
||||
|
||||
def decode(self, buffer):
|
||||
"""
|
||||
Override to perform the decoding process.
|
||||
|
||||
:param buffer: A bytes object with the data to be decoded.
|
||||
If `handles_eof` is set, then `buffer` will be empty and `self.fd`
|
||||
will be set.
|
||||
:returns: A tuple of (bytes consumed, errcode).
|
||||
If finished with decoding return <0 for the bytes consumed.
|
||||
Err codes are from `ERRORS`
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Override to perform decoder specific cleanup
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
pass
|
||||
|
||||
def setfd(self, fd):
|
||||
"""
|
||||
Called from ImageFile to set the python file-like object
|
||||
|
||||
:param fd: A python file-like object
|
||||
:returns: None
|
||||
"""
|
||||
self.fd = fd
|
||||
|
||||
def setimage(self, im, extents=None):
|
||||
"""
|
||||
Called from ImageFile to set the core output image for the decoder
|
||||
|
||||
:param im: A core image object
|
||||
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
|
||||
for this tile
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
# following c code
|
||||
self.im = im
|
||||
|
||||
if extents:
|
||||
(x0, y0, x1, y1) = extents
|
||||
else:
|
||||
(x0, y0, x1, y1) = (0, 0, 0, 0)
|
||||
|
||||
if x0 == 0 and x1 == 0:
|
||||
self.state.xsize, self.state.ysize = self.im.size
|
||||
else:
|
||||
self.state.xoff = x0
|
||||
self.state.yoff = y0
|
||||
self.state.xsize = x1 - x0
|
||||
self.state.ysize = y1 - y0
|
||||
|
||||
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
||||
raise ValueError("Size cannot be negative")
|
||||
|
||||
if (self.state.xsize + self.state.xoff > self.im.size[0] or
|
||||
self.state.ysize + self.state.yoff > self.im.size[1]):
|
||||
raise ValueError("Tile cannot extend outside image")
|
||||
|
||||
def set_as_raw(self, data, rawmode=None):
|
||||
"""
|
||||
Convenience method to set the internal image from a stream of raw data
|
||||
|
||||
:param data: Bytes to be set
|
||||
:param rawmode: The rawmode to be used for the decoder.
|
||||
If not specified, it will default to the mode of the image
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
if not rawmode:
|
||||
rawmode = self.mode
|
||||
d = Image._getdecoder(self.mode, 'raw', (rawmode))
|
||||
d.setimage(self.im, self.state.extents())
|
||||
s = d.decode(data)
|
||||
|
||||
if s[0] >= 0:
|
||||
raise ValueError("not enough image data")
|
||||
if s[1] != 0:
|
||||
raise ValueError("cannot decode image data")
|
||||
|
|
|
|||
|
|
@ -15,14 +15,32 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import functools
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError: # pragma: no cover
|
||||
numpy = None
|
||||
|
||||
|
||||
class Filter(object):
|
||||
pass
|
||||
|
||||
|
||||
class Kernel(Filter):
|
||||
class MultibandFilter(Filter):
|
||||
pass
|
||||
|
||||
|
||||
class BuiltinFilter(MultibandFilter):
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
return image.filter(*self.filterargs)
|
||||
|
||||
|
||||
class Kernel(BuiltinFilter):
|
||||
"""
|
||||
Create a convolution kernel. The current version only
|
||||
supports 3x3 and 5x5 integer and floating point kernels.
|
||||
|
|
@ -39,6 +57,7 @@ class Kernel(Filter):
|
|||
:param offset: Offset. If given, this value is added to the result,
|
||||
after it has been divided by the scale factor.
|
||||
"""
|
||||
name = "Kernel"
|
||||
|
||||
def __init__(self, size, kernel, scale=None, offset=0):
|
||||
if scale is None:
|
||||
|
|
@ -48,16 +67,6 @@ class Kernel(Filter):
|
|||
raise ValueError("not enough coefficients in kernel")
|
||||
self.filterargs = size, scale, offset, kernel
|
||||
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
return image.filter(*self.filterargs)
|
||||
|
||||
|
||||
class BuiltinFilter(Kernel):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class RankFilter(Filter):
|
||||
"""
|
||||
|
|
@ -126,7 +135,6 @@ class MaxFilter(RankFilter):
|
|||
|
||||
class ModeFilter(Filter):
|
||||
"""
|
||||
|
||||
Create a mode filter. Picks the most frequent pixel value in a box with the
|
||||
given size. Pixel values that occur only once or twice are ignored; if no
|
||||
pixel value occurs more than twice, the original pixel value is preserved.
|
||||
|
|
@ -142,7 +150,7 @@ class ModeFilter(Filter):
|
|||
return image.modefilter(self.size)
|
||||
|
||||
|
||||
class GaussianBlur(Filter):
|
||||
class GaussianBlur(MultibandFilter):
|
||||
"""Gaussian blur filter.
|
||||
|
||||
:param radius: Blur radius.
|
||||
|
|
@ -156,7 +164,27 @@ class GaussianBlur(Filter):
|
|||
return image.gaussian_blur(self.radius)
|
||||
|
||||
|
||||
class UnsharpMask(Filter):
|
||||
class BoxBlur(MultibandFilter):
|
||||
"""Blurs the image by setting each pixel to the average value of the pixels
|
||||
in a square box extending radius pixels in each direction.
|
||||
Supports float radius of arbitrary size. Uses an optimized implementation
|
||||
which runs in linear time relative to the size of the image
|
||||
for any radius value.
|
||||
|
||||
:param radius: Size of the box in one direction. Radius 0 does not blur,
|
||||
returns an identical image. Radius 1 takes 1 pixel
|
||||
in each direction, i.e. 9 pixels in total.
|
||||
"""
|
||||
name = "BoxBlur"
|
||||
|
||||
def __init__(self, radius):
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.box_blur(self.radius)
|
||||
|
||||
|
||||
class UnsharpMask(MultibandFilter):
|
||||
"""Unsharp mask filter.
|
||||
|
||||
See Wikipedia's entry on `digital unsharp masking`_ for an explanation of
|
||||
|
|
@ -169,7 +197,7 @@ class UnsharpMask(Filter):
|
|||
|
||||
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
|
||||
|
||||
"""
|
||||
""" # noqa: E501
|
||||
name = "UnsharpMask"
|
||||
|
||||
def __init__(self, radius=2, percent=150, threshold=3):
|
||||
|
|
@ -246,6 +274,15 @@ class FIND_EDGES(BuiltinFilter):
|
|||
)
|
||||
|
||||
|
||||
class SHARPEN(BuiltinFilter):
|
||||
name = "Sharpen"
|
||||
filterargs = (3, 3), 16, 0, (
|
||||
-2, -2, -2,
|
||||
-2, 32, -2,
|
||||
-2, -2, -2
|
||||
)
|
||||
|
||||
|
||||
class SMOOTH(BuiltinFilter):
|
||||
name = "Smooth"
|
||||
filterargs = (3, 3), 13, 0, (
|
||||
|
|
@ -266,10 +303,181 @@ class SMOOTH_MORE(BuiltinFilter):
|
|||
)
|
||||
|
||||
|
||||
class SHARPEN(BuiltinFilter):
|
||||
name = "Sharpen"
|
||||
filterargs = (3, 3), 16, 0, (
|
||||
-2, -2, -2,
|
||||
-2, 32, -2,
|
||||
-2, -2, -2
|
||||
)
|
||||
class Color3DLUT(MultibandFilter):
|
||||
"""Three-dimensional color lookup table.
|
||||
|
||||
Transforms 3-channel pixels using the values of the channels as coordinates
|
||||
in the 3D lookup table and interpolating the nearest elements.
|
||||
|
||||
This method allows you to apply almost any color transformation
|
||||
in constant time by using pre-calculated decimated tables.
|
||||
|
||||
.. versionadded:: 5.2.0
|
||||
|
||||
:param size: Size of the table. One int or tuple of (int, int, int).
|
||||
Minimal size in any dimension is 2, maximum is 65.
|
||||
:param table: Flat lookup table. A list of ``channels * size**3``
|
||||
float elements or a list of ``size**3`` channels-sized
|
||||
tuples with floats. Channels are changed first,
|
||||
then first dimension, then second, then third.
|
||||
Value 0.0 corresponds lowest value of output, 1.0 highest.
|
||||
:param channels: Number of channels in the table. Could be 3 or 4.
|
||||
Default is 3.
|
||||
:param target_mode: A mode for the result image. Should have not less
|
||||
than ``channels`` channels. Default is ``None``,
|
||||
which means that mode wouldn't be changed.
|
||||
"""
|
||||
name = "Color 3D LUT"
|
||||
|
||||
def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
|
||||
if channels not in (3, 4):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
self.size = size = self._check_size(size)
|
||||
self.channels = channels
|
||||
self.mode = target_mode
|
||||
|
||||
# Hidden flag `_copy_table=False` could be used to avoid extra copying
|
||||
# of the table if the table is specially made for the constructor.
|
||||
copy_table = kwargs.get('_copy_table', True)
|
||||
items = size[0] * size[1] * size[2]
|
||||
wrong_size = False
|
||||
|
||||
if numpy and isinstance(table, numpy.ndarray):
|
||||
if copy_table:
|
||||
table = table.copy()
|
||||
|
||||
if table.shape in [(items * channels,), (items, channels),
|
||||
(size[2], size[1], size[0], channels)]:
|
||||
table = table.reshape(items * channels)
|
||||
else:
|
||||
wrong_size = True
|
||||
|
||||
else:
|
||||
if copy_table:
|
||||
table = list(table)
|
||||
|
||||
# Convert to a flat list
|
||||
if table and isinstance(table[0], (list, tuple)):
|
||||
table, raw_table = [], table
|
||||
for pixel in raw_table:
|
||||
if len(pixel) != channels:
|
||||
raise ValueError(
|
||||
"The elements of the table should "
|
||||
"have a length of {}.".format(channels))
|
||||
table.extend(pixel)
|
||||
|
||||
if wrong_size or len(table) != items * channels:
|
||||
raise ValueError(
|
||||
"The table should have either channels * size**3 float items "
|
||||
"or size**3 items of channels-sized tuples with floats. "
|
||||
"Table should be: {}x{}x{}x{}. Actual length: {}".format(
|
||||
channels, size[0], size[1], size[2], len(table)))
|
||||
self.table = table
|
||||
|
||||
@staticmethod
|
||||
def _check_size(size):
|
||||
try:
|
||||
_, _, _ = size
|
||||
except ValueError:
|
||||
raise ValueError("Size should be either an integer or "
|
||||
"a tuple of three integers.")
|
||||
except TypeError:
|
||||
size = (size, size, size)
|
||||
size = [int(x) for x in size]
|
||||
for size1D in size:
|
||||
if not 2 <= size1D <= 65:
|
||||
raise ValueError("Size should be in [2, 65] range.")
|
||||
return size
|
||||
|
||||
@classmethod
|
||||
def generate(cls, size, callback, channels=3, target_mode=None):
|
||||
"""Generates new LUT using provided callback.
|
||||
|
||||
:param size: Size of the table. Passed to the constructor.
|
||||
:param callback: Function with three parameters which correspond
|
||||
three color channels. Will be called ``size**3``
|
||||
times with values from 0.0 to 1.0 and should return
|
||||
a tuple with ``channels`` elements.
|
||||
:param channels: The number of channels which should return callback.
|
||||
:param target_mode: Passed to the constructor of the resulting
|
||||
lookup table.
|
||||
"""
|
||||
size1D, size2D, size3D = cls._check_size(size)
|
||||
if channels not in (3, 4):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
|
||||
table = [0] * (size1D * size2D * size3D * channels)
|
||||
idx_out = 0
|
||||
for b in range(size3D):
|
||||
for g in range(size2D):
|
||||
for r in range(size1D):
|
||||
table[idx_out:idx_out + channels] = callback(
|
||||
r / (size1D-1), g / (size2D-1), b / (size3D-1))
|
||||
idx_out += channels
|
||||
|
||||
return cls((size1D, size2D, size3D), table, channels=channels,
|
||||
target_mode=target_mode, _copy_table=False)
|
||||
|
||||
def transform(self, callback, with_normals=False, channels=None,
|
||||
target_mode=None):
|
||||
"""Transforms the table values using provided callback and returns
|
||||
a new LUT with altered values.
|
||||
|
||||
:param callback: A function which takes old lookup table values
|
||||
and returns a new set of values. The number
|
||||
of arguments which function should take is
|
||||
``self.channels`` or ``3 + self.channels``
|
||||
if ``with_normals`` flag is set.
|
||||
Should return a tuple of ``self.channels`` or
|
||||
``channels`` elements if it is set.
|
||||
:param with_normals: If true, ``callback`` will be called with
|
||||
coordinates in the color cube as the first
|
||||
three arguments. Otherwise, ``callback``
|
||||
will be called only with actual color values.
|
||||
:param channels: The number of channels in the resulting lookup table.
|
||||
:param target_mode: Passed to the constructor of the resulting
|
||||
lookup table.
|
||||
"""
|
||||
if channels not in (None, 3, 4):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
ch_in = self.channels
|
||||
ch_out = channels or ch_in
|
||||
size1D, size2D, size3D = self.size
|
||||
|
||||
table = [0] * (size1D * size2D * size3D * ch_out)
|
||||
idx_in = 0
|
||||
idx_out = 0
|
||||
for b in range(size3D):
|
||||
for g in range(size2D):
|
||||
for r in range(size1D):
|
||||
values = self.table[idx_in:idx_in + ch_in]
|
||||
if with_normals:
|
||||
values = callback(r / (size1D-1), g / (size2D-1),
|
||||
b / (size3D-1), *values)
|
||||
else:
|
||||
values = callback(*values)
|
||||
table[idx_out:idx_out + ch_out] = values
|
||||
idx_in += ch_in
|
||||
idx_out += ch_out
|
||||
|
||||
return type(self)(self.size, table, channels=ch_out,
|
||||
target_mode=target_mode or self.mode,
|
||||
_copy_table=False)
|
||||
|
||||
def __repr__(self):
|
||||
r = [
|
||||
"{} from {}".format(self.__class__.__name__,
|
||||
self.table.__class__.__name__),
|
||||
"size={:d}x{:d}x{:d}".format(*self.size),
|
||||
"channels={:d}".format(self.channels),
|
||||
]
|
||||
if self.mode:
|
||||
r.append("target_mode={}".format(self.mode))
|
||||
return "<{}>".format(" ".join(r))
|
||||
|
||||
def filter(self, image):
|
||||
from . import Image
|
||||
|
||||
return image.color_lut_3d(
|
||||
self.mode or image.mode, Image.LINEAR, self.channels,
|
||||
self.size[0], self.size[1], self.size[2], self.table)
|
||||
|
|
|
|||
|
|
@ -25,22 +25,27 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL._util import isDirectory, isPath
|
||||
from . import Image
|
||||
from ._util import isDirectory, isPath, py3
|
||||
import os
|
||||
import sys
|
||||
|
||||
LAYOUT_BASIC = 0
|
||||
LAYOUT_RAQM = 1
|
||||
|
||||
|
||||
class _imagingft_not_installed(object):
|
||||
# module placeholder
|
||||
def __getattr__(self, id):
|
||||
raise ImportError("The _imagingft C module is not installed")
|
||||
|
||||
|
||||
try:
|
||||
from PIL import _imagingft as core
|
||||
from . import _imagingft as core
|
||||
except ImportError:
|
||||
core = _imagingft_not_installed()
|
||||
|
||||
|
||||
# FIXME: add support for pilfont2 format (see FontFile.py)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -62,23 +67,22 @@ class ImageFont(object):
|
|||
|
||||
def _load_pilfont(self, filename):
|
||||
|
||||
fp = open(filename, "rb")
|
||||
|
||||
for ext in (".png", ".gif", ".pbm"):
|
||||
try:
|
||||
fullname = os.path.splitext(filename)[0] + ext
|
||||
image = Image.open(fullname)
|
||||
except:
|
||||
pass
|
||||
with open(filename, "rb") as fp:
|
||||
for ext in (".png", ".gif", ".pbm"):
|
||||
try:
|
||||
fullname = os.path.splitext(filename)[0] + ext
|
||||
image = Image.open(fullname)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if image and image.mode in ("1", "L"):
|
||||
break
|
||||
else:
|
||||
if image and image.mode in ("1", "L"):
|
||||
break
|
||||
else:
|
||||
raise IOError("cannot find glyph data file")
|
||||
raise IOError("cannot find glyph data file")
|
||||
|
||||
self.file = fullname
|
||||
self.file = fullname
|
||||
|
||||
return self._load_pilfont_data(fp, image)
|
||||
return self._load_pilfont_data(fp, image)
|
||||
|
||||
def _load_pilfont_data(self, file, image):
|
||||
|
||||
|
|
@ -104,9 +108,11 @@ class ImageFont(object):
|
|||
|
||||
self.font = Image.core.font(image.im, data)
|
||||
|
||||
# delegate critical operations to internal type
|
||||
self.getsize = self.font.getsize
|
||||
self.getmask = self.font.getmask
|
||||
def getsize(self, text, *args, **kwargs):
|
||||
return self.font.getsize(text)
|
||||
|
||||
def getmask(self, text, mode="", *args, **kwargs):
|
||||
return self.font.getmask(text, mode)
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -116,7 +122,8 @@ class ImageFont(object):
|
|||
class FreeTypeFont(object):
|
||||
"FreeType font wrapper (requires _imagingft service)"
|
||||
|
||||
def __init__(self, font=None, size=10, index=0, encoding=""):
|
||||
def __init__(self, font=None, size=10, index=0, encoding="",
|
||||
layout_engine=None):
|
||||
# FIXME: use service provider instead
|
||||
|
||||
self.path = font
|
||||
|
|
@ -124,12 +131,26 @@ class FreeTypeFont(object):
|
|||
self.index = index
|
||||
self.encoding = encoding
|
||||
|
||||
if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM):
|
||||
layout_engine = LAYOUT_BASIC
|
||||
if core.HAVE_RAQM:
|
||||
layout_engine = LAYOUT_RAQM
|
||||
if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM:
|
||||
layout_engine = LAYOUT_BASIC
|
||||
|
||||
self.layout_engine = layout_engine
|
||||
|
||||
if isPath(font):
|
||||
self.font = core.getfont(font, size, index, encoding)
|
||||
self.font = core.getfont(font, size, index, encoding,
|
||||
layout_engine=layout_engine)
|
||||
else:
|
||||
self.font_bytes = font.read()
|
||||
self.font = core.getfont(
|
||||
"", size, index, encoding, self.font_bytes)
|
||||
"", size, index, encoding, self.font_bytes, layout_engine)
|
||||
|
||||
def _multiline_split(self, text):
|
||||
split_character = "\n" if isinstance(text, str) else b"\n"
|
||||
return text.split(split_character)
|
||||
|
||||
def getname(self):
|
||||
return self.font.family, self.font.style
|
||||
|
|
@ -137,23 +158,37 @@ class FreeTypeFont(object):
|
|||
def getmetrics(self):
|
||||
return self.font.ascent, self.font.descent
|
||||
|
||||
def getsize(self, text):
|
||||
size, offset = self.font.getsize(text)
|
||||
def getsize(self, text, direction=None, features=None):
|
||||
size, offset = self.font.getsize(text, direction, features)
|
||||
return (size[0] + offset[0], size[1] + offset[1])
|
||||
|
||||
def getsize_multiline(self, text, direction=None,
|
||||
spacing=4, features=None):
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.getsize('A')[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.getsize(line, direction, features)
|
||||
max_width = max(max_width, line_width)
|
||||
|
||||
return max_width, len(lines)*line_spacing - spacing
|
||||
|
||||
def getoffset(self, text):
|
||||
return self.font.getsize(text)[1]
|
||||
|
||||
def getmask(self, text, mode=""):
|
||||
return self.getmask2(text, mode)[0]
|
||||
def getmask(self, text, mode="", direction=None, features=None):
|
||||
return self.getmask2(text, mode, direction=direction,
|
||||
features=features)[0]
|
||||
|
||||
def getmask2(self, text, mode="", fill=Image.core.fill):
|
||||
size, offset = self.font.getsize(text)
|
||||
def getmask2(self, text, mode="", fill=Image.core.fill, direction=None,
|
||||
features=None, *args, **kwargs):
|
||||
size, offset = self.font.getsize(text, direction, features)
|
||||
im = fill("L", size, 0)
|
||||
self.font.render(text, im.id, mode == "1")
|
||||
self.font.render(text, im.id, mode == "1", direction, features)
|
||||
return im, offset
|
||||
|
||||
def font_variant(self, font=None, size=None, index=None, encoding=None):
|
||||
def font_variant(self, font=None, size=None, index=None, encoding=None,
|
||||
layout_engine=None):
|
||||
"""
|
||||
Create a copy of this FreeTypeFont object,
|
||||
using any specified arguments to override the settings.
|
||||
|
|
@ -163,37 +198,39 @@ class FreeTypeFont(object):
|
|||
|
||||
:return: A FreeTypeFont object.
|
||||
"""
|
||||
return FreeTypeFont(font=self.path if font is None else font,
|
||||
size=self.size if size is None else size,
|
||||
index=self.index if index is None else index,
|
||||
encoding=self.encoding if encoding is None else
|
||||
encoding)
|
||||
|
||||
##
|
||||
# Wrapper that creates a transposed font from any existing font
|
||||
# object.
|
||||
#
|
||||
# @param font A font object.
|
||||
# @param orientation An optional orientation. If given, this should
|
||||
# be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
||||
# Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
||||
return FreeTypeFont(
|
||||
font=self.path if font is None else font,
|
||||
size=self.size if size is None else size,
|
||||
index=self.index if index is None else index,
|
||||
encoding=self.encoding if encoding is None else encoding,
|
||||
layout_engine=layout_engine or self.layout_engine
|
||||
)
|
||||
|
||||
|
||||
class TransposedFont(object):
|
||||
"Wrapper for writing rotated or mirrored text"
|
||||
|
||||
def __init__(self, font, orientation=None):
|
||||
"""
|
||||
Wrapper that creates a transposed font from any existing font
|
||||
object.
|
||||
|
||||
:param font: A font object.
|
||||
:param orientation: An optional orientation. If given, this should
|
||||
be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
||||
Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
||||
"""
|
||||
self.font = font
|
||||
self.orientation = orientation # any 'transpose' argument, or None
|
||||
|
||||
def getsize(self, text):
|
||||
def getsize(self, text, *args, **kwargs):
|
||||
w, h = self.font.getsize(text)
|
||||
if self.orientation in (Image.ROTATE_90, Image.ROTATE_270):
|
||||
return h, w
|
||||
return w, h
|
||||
|
||||
def getmask(self, text, mode=""):
|
||||
im = self.font.getmask(text, mode)
|
||||
def getmask(self, text, mode="", *args, **kwargs):
|
||||
im = self.font.getmask(text, mode, *args, **kwargs)
|
||||
if self.orientation is not None:
|
||||
return im.transpose(self.orientation)
|
||||
return im
|
||||
|
|
@ -213,17 +250,19 @@ def load(filename):
|
|||
return f
|
||||
|
||||
|
||||
def truetype(font=None, size=10, index=0, encoding=""):
|
||||
def truetype(font=None, size=10, index=0, encoding="",
|
||||
layout_engine=None):
|
||||
"""
|
||||
Load a TrueType or OpenType font file, and create a font object.
|
||||
This function loads a font object from the given file, and creates
|
||||
a font object for a font of the given size.
|
||||
Load a TrueType or OpenType font from a file or file-like object,
|
||||
and create a font object.
|
||||
This function loads a font object from the given file or file-like
|
||||
object, and creates a font object for a font of the given size.
|
||||
|
||||
This function requires the _imagingft service.
|
||||
|
||||
:param font: A truetype font file. Under Windows, if the file
|
||||
is not found in this filename, the loader also looks in
|
||||
Windows :file:`fonts/` directory.
|
||||
:param font: A filename or file-like object containing a TrueType font.
|
||||
Under Windows, if the file is not found in this filename,
|
||||
the loader also looks in Windows :file:`fonts/` directory.
|
||||
:param size: The requested size, in points.
|
||||
:param index: Which font face to load (default is first available face).
|
||||
:param encoding: Which font encoding to use (default is Unicode). Common
|
||||
|
|
@ -231,12 +270,14 @@ def truetype(font=None, size=10, index=0, encoding=""):
|
|||
Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert),
|
||||
and "armn" (Apple Roman). See the FreeType documentation
|
||||
for more information.
|
||||
:param layout_engine: Which layout engine to use, if available:
|
||||
`ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.
|
||||
:return: A font object.
|
||||
:exception IOError: If the file could not be read.
|
||||
"""
|
||||
|
||||
try:
|
||||
return FreeTypeFont(font, size, index, encoding)
|
||||
return FreeTypeFont(font, size, index, encoding, layout_engine)
|
||||
except IOError:
|
||||
ttf_filename = os.path.basename(font)
|
||||
|
||||
|
|
@ -267,16 +308,20 @@ def truetype(font=None, size=10, index=0, encoding=""):
|
|||
for walkfilename in walkfilenames:
|
||||
if ext and walkfilename == ttf_filename:
|
||||
fontpath = os.path.join(walkroot, walkfilename)
|
||||
return FreeTypeFont(fontpath, size, index, encoding)
|
||||
elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
|
||||
return FreeTypeFont(fontpath, size, index,
|
||||
encoding, layout_engine)
|
||||
elif (not ext and
|
||||
os.path.splitext(walkfilename)[0] == ttf_filename):
|
||||
fontpath = os.path.join(walkroot, walkfilename)
|
||||
if os.path.splitext(fontpath)[1] == '.ttf':
|
||||
return FreeTypeFont(fontpath, size, index, encoding)
|
||||
if not ext and first_font_with_a_different_extension is None:
|
||||
return FreeTypeFont(fontpath, size, index,
|
||||
encoding, layout_engine)
|
||||
if not ext \
|
||||
and first_font_with_a_different_extension is None:
|
||||
first_font_with_a_different_extension = fontpath
|
||||
if first_font_with_a_different_extension:
|
||||
return FreeTypeFont(first_font_with_a_different_extension, size,
|
||||
index, encoding)
|
||||
index, encoding, layout_engine)
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -292,10 +337,10 @@ def load_path(filename):
|
|||
for directory in sys.path:
|
||||
if isDirectory(directory):
|
||||
if not isinstance(filename, str):
|
||||
if bytes is str:
|
||||
filename = filename.encode("utf-8")
|
||||
else:
|
||||
if py3:
|
||||
filename = filename.decode("utf-8")
|
||||
else:
|
||||
filename = filename.encode("utf-8")
|
||||
try:
|
||||
return load(os.path.join(directory, filename))
|
||||
except IOError:
|
||||
|
|
@ -315,7 +360,7 @@ def load_default():
|
|||
f = ImageFont()
|
||||
f._load_pilfont_data(
|
||||
# courB08
|
||||
BytesIO(base64.decodestring(b'''
|
||||
BytesIO(base64.b64decode(b'''
|
||||
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
|
@ -407,7 +452,7 @@ AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
|
|||
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
|
||||
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
|
||||
+QAGAAIAzgAKANUAEw==
|
||||
''')), Image.open(BytesIO(base64.decodestring(b'''
|
||||
''')), Image.open(BytesIO(base64.b64decode(b'''
|
||||
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
|
||||
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
|
||||
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
|
||||
|
|
@ -433,5 +478,3 @@ Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
|
|||
w7IkEbzhVQAAAABJRU5ErkJggg==
|
||||
'''))))
|
||||
return f
|
||||
|
||||
# End of file
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# screen grabber (OS X and Windows only)
|
||||
# screen grabber (macOS and Windows only)
|
||||
#
|
||||
# History:
|
||||
# 2001-04-26 fl created
|
||||
|
|
@ -15,11 +15,11 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
import sys
|
||||
if sys.platform not in ["win32", "darwin"]:
|
||||
raise ImportError("ImageGrab is OS X and Windows only")
|
||||
raise ImportError("ImageGrab is macOS and Windows only")
|
||||
|
||||
if sys.platform == "win32":
|
||||
grabber = Image.core.grabscreen
|
||||
|
|
@ -31,17 +31,17 @@ elif sys.platform == "darwin":
|
|||
|
||||
def grab(bbox=None):
|
||||
if sys.platform == "darwin":
|
||||
f, file = tempfile.mkstemp('.png')
|
||||
os.close(f)
|
||||
subprocess.call(['screencapture', '-x', file])
|
||||
im = Image.open(file)
|
||||
fh, filepath = tempfile.mkstemp('.png')
|
||||
os.close(fh)
|
||||
subprocess.call(['screencapture', '-x', filepath])
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(file)
|
||||
os.unlink(filepath)
|
||||
else:
|
||||
size, data = grabber()
|
||||
im = Image.frombytes(
|
||||
"RGB", size, data,
|
||||
# RGB, 32-bit line padding, origo in lower left corner
|
||||
# RGB, 32-bit line padding, origin lower left corner
|
||||
"raw", "BGR", (size[0]*3 + 3) & -4, -1
|
||||
)
|
||||
if bbox:
|
||||
|
|
@ -51,11 +51,31 @@ def grab(bbox=None):
|
|||
|
||||
def grabclipboard():
|
||||
if sys.platform == "darwin":
|
||||
raise NotImplementedError("Method is not implemented on OS X")
|
||||
debug = 0 # temporary interface
|
||||
data = Image.core.grabclipboard(debug)
|
||||
if isinstance(data, bytes):
|
||||
from PIL import BmpImagePlugin
|
||||
import io
|
||||
return BmpImagePlugin.DibImageFile(io.BytesIO(data))
|
||||
return data
|
||||
fh, filepath = tempfile.mkstemp('.jpg')
|
||||
os.close(fh)
|
||||
commands = [
|
||||
"set theFile to (open for access POSIX file \""
|
||||
+ filepath + "\" with write permission)",
|
||||
"try",
|
||||
" write (the clipboard as JPEG picture) to theFile",
|
||||
"end try",
|
||||
"close access theFile"
|
||||
]
|
||||
script = ["osascript"]
|
||||
for command in commands:
|
||||
script += ["-e", command]
|
||||
subprocess.call(script)
|
||||
|
||||
im = None
|
||||
if os.stat(filepath).st_size != 0:
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
return im
|
||||
else:
|
||||
data = Image.core.grabclipboard()
|
||||
if isinstance(data, bytes):
|
||||
from . import BmpImagePlugin
|
||||
import io
|
||||
return BmpImagePlugin.DibImageFile(io.BytesIO(data))
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL import _imagingmath
|
||||
from . import Image, _imagingmath
|
||||
from ._util import py3
|
||||
|
||||
try:
|
||||
import builtins
|
||||
|
|
@ -32,7 +32,7 @@ def _isconstant(v):
|
|||
|
||||
|
||||
class _Operand(object):
|
||||
# wraps an image operand, providing standard operators
|
||||
"""Wraps an image operand, providing standard operators"""
|
||||
|
||||
def __init__(self, im):
|
||||
self.im = im
|
||||
|
|
@ -101,7 +101,7 @@ class _Operand(object):
|
|||
# an image is "true" if it contains at least one non-zero pixel
|
||||
return self.im.getbbox() is not None
|
||||
|
||||
if bytes is str:
|
||||
if not py3:
|
||||
# Provide __nonzero__ for pre-Py3k
|
||||
__nonzero__ = __bool__
|
||||
del __bool__
|
||||
|
|
@ -152,7 +152,7 @@ class _Operand(object):
|
|||
def __rpow__(self, other):
|
||||
return self.apply("pow", other, self)
|
||||
|
||||
if bytes is str:
|
||||
if not py3:
|
||||
# Provide __div__ and __rdiv__ for pre-Py3k
|
||||
__div__ = __truediv__
|
||||
__rdiv__ = __rtruediv__
|
||||
|
|
@ -236,6 +236,7 @@ def imagemath_max(self, other):
|
|||
def imagemath_convert(self, mode):
|
||||
return _Operand(self.im.convert(mode))
|
||||
|
||||
|
||||
ops = {}
|
||||
for k, v in list(globals().items()):
|
||||
if k[:10] == "imagemath_":
|
||||
|
|
|
|||
|
|
@ -14,13 +14,11 @@
|
|||
#
|
||||
|
||||
# mode descriptor cache
|
||||
_modes = {}
|
||||
_modes = None
|
||||
|
||||
|
||||
##
|
||||
# Wrapper for mode strings.
|
||||
|
||||
class ModeDescriptor(object):
|
||||
"""Wrapper for mode strings."""
|
||||
|
||||
def __init__(self, mode, bands, basemode, basetype):
|
||||
self.mode = mode
|
||||
|
|
@ -32,21 +30,27 @@ class ModeDescriptor(object):
|
|||
return self.mode
|
||||
|
||||
|
||||
##
|
||||
# Gets a mode descriptor for the given mode.
|
||||
|
||||
def getmode(mode):
|
||||
"""Gets a mode descriptor for the given mode."""
|
||||
global _modes
|
||||
if not _modes:
|
||||
# initialize mode cache
|
||||
from PIL import Image
|
||||
|
||||
from . import Image
|
||||
modes = {}
|
||||
# core modes
|
||||
for m, (basemode, basetype, bands) in Image._MODEINFO.items():
|
||||
_modes[m] = ModeDescriptor(m, bands, basemode, basetype)
|
||||
modes[m] = ModeDescriptor(m, bands, basemode, basetype)
|
||||
# extra experimental modes
|
||||
_modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L")
|
||||
_modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L")
|
||||
modes["RGBa"] = ModeDescriptor("RGBa",
|
||||
("R", "G", "B", "a"), "RGB", "L")
|
||||
modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L")
|
||||
modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L")
|
||||
modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L")
|
||||
# mapping modes
|
||||
_modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L")
|
||||
_modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L")
|
||||
_modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L")
|
||||
modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L")
|
||||
modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L")
|
||||
modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L")
|
||||
# set global mode cache atomically
|
||||
_modes = modes
|
||||
return _modes[mode]
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
#
|
||||
# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
|
||||
|
||||
from PIL import Image
|
||||
from PIL import _imagingmorph
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, _imagingmorph
|
||||
import re
|
||||
|
||||
LUT_SIZE = 1 << 9
|
||||
|
|
@ -78,7 +79,7 @@ class LutBuilder(object):
|
|||
def build_default_lut(self):
|
||||
symbols = [0, 1]
|
||||
m = 1 << 4 # pos of current pixel
|
||||
self.lut = bytearray([symbols[(i & m) > 0] for i in range(LUT_SIZE)])
|
||||
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
|
||||
|
||||
def get_lut(self):
|
||||
return self.lut
|
||||
|
|
@ -88,7 +89,7 @@ class LutBuilder(object):
|
|||
string permuted according to the permutation list.
|
||||
"""
|
||||
assert(len(permutation) == 9)
|
||||
return ''.join([pattern[p] for p in permutation])
|
||||
return ''.join(pattern[p] for p in permutation)
|
||||
|
||||
def _pattern_permute(self, basic_pattern, options, basic_result):
|
||||
"""pattern_permute takes a basic pattern and its result and clones
|
||||
|
|
@ -122,7 +123,7 @@ class LutBuilder(object):
|
|||
.replace('0', 'Z')
|
||||
.replace('1', '0')
|
||||
.replace('Z', '1'))
|
||||
res = '%d' % (1-int(res))
|
||||
res = 1-int(res)
|
||||
patterns.append((pattern, res))
|
||||
|
||||
return patterns
|
||||
|
|
@ -150,16 +151,11 @@ class LutBuilder(object):
|
|||
|
||||
patterns += self._pattern_permute(pattern, options, result)
|
||||
|
||||
# # Debugging
|
||||
# for p,r in patterns:
|
||||
# print p,r
|
||||
# print '--'
|
||||
|
||||
# compile the patterns into regular expressions for speed
|
||||
for i in range(len(patterns)):
|
||||
p = patterns[i][0].replace('.', 'X').replace('X', '[01]')
|
||||
for i, pattern in enumerate(patterns):
|
||||
p = pattern[0].replace('.', 'X').replace('X', '[01]')
|
||||
p = re.compile(p)
|
||||
patterns[i] = (p, patterns[i][1])
|
||||
patterns[i] = (p, pattern[1])
|
||||
|
||||
# Step through table and find patterns that match.
|
||||
# Note that all the patterns are searched. The last one
|
||||
|
|
@ -210,7 +206,7 @@ class MorphOp(object):
|
|||
an image.
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels."""
|
||||
of all matching pixels. See :ref:`coordinate-system`."""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
|
||||
|
|
@ -222,7 +218,7 @@ class MorphOp(object):
|
|||
"""Get a list of all turned on pixels in a binary image
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels."""
|
||||
of all matching pixels. See :ref:`coordinate-system`."""
|
||||
|
||||
if image.mode != 'L':
|
||||
raise Exception('Image must be binary, meaning it must use mode L')
|
||||
|
|
@ -233,7 +229,7 @@ class MorphOp(object):
|
|||
with open(filename, 'rb') as f:
|
||||
self.lut = bytearray(f.read())
|
||||
|
||||
if len(self.lut) != 8192:
|
||||
if len(self.lut) != LUT_SIZE:
|
||||
self.lut = None
|
||||
raise Exception('Wrong size operator file!')
|
||||
|
||||
|
|
@ -247,5 +243,3 @@ class MorphOp(object):
|
|||
def set_lut(self, lut):
|
||||
"""Set the lut from an external source"""
|
||||
self.lut = lut
|
||||
|
||||
# End of file
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL._util import isStringType
|
||||
from . import Image
|
||||
from ._util import isStringType
|
||||
import operator
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -39,7 +40,7 @@ def _border(border):
|
|||
|
||||
def _color(color, mode):
|
||||
if isStringType(color):
|
||||
from PIL import ImageColor
|
||||
from . import ImageColor
|
||||
color = ImageColor.getcolor(color, mode)
|
||||
return color
|
||||
|
||||
|
|
@ -135,32 +136,135 @@ def autocontrast(image, cutoff=0, ignore=None):
|
|||
return _lut(image, lut)
|
||||
|
||||
|
||||
def colorize(image, black, white):
|
||||
def colorize(image, black, white, mid=None, blackpoint=0,
|
||||
whitepoint=255, midpoint=127):
|
||||
"""
|
||||
Colorize grayscale image. The **black** and **white**
|
||||
arguments should be RGB tuples; this function calculates a color
|
||||
wedge mapping all black pixels in the source image to the first
|
||||
color, and all white pixels to the second color.
|
||||
Colorize grayscale image.
|
||||
This function calculates a color wedge which maps all black pixels in
|
||||
the source image to the first color and all white pixels to the
|
||||
second color. If **mid** is specified, it uses three-color mapping.
|
||||
The **black** and **white** arguments should be RGB tuples or color names;
|
||||
optionally you can use three-color mapping by also specifying **mid**.
|
||||
Mapping positions for any of the colors can be specified
|
||||
(e.g. **blackpoint**), where these parameters are the integer
|
||||
value corresponding to where the corresponding color should be mapped.
|
||||
These parameters must have logical order, such that
|
||||
**blackpoint** <= **midpoint** <= **whitepoint** (if **mid** is specified).
|
||||
|
||||
:param image: The image to colorize.
|
||||
:param black: The color to use for black input pixels.
|
||||
:param white: The color to use for white input pixels.
|
||||
:param mid: The color to use for midtone input pixels.
|
||||
:param blackpoint: an int value [0, 255] for the black mapping.
|
||||
:param whitepoint: an int value [0, 255] for the white mapping.
|
||||
:param midpoint: an int value [0, 255] for the midtone mapping.
|
||||
:return: An image.
|
||||
"""
|
||||
|
||||
# Initial asserts
|
||||
assert image.mode == "L"
|
||||
if mid is None:
|
||||
assert 0 <= blackpoint <= whitepoint <= 255
|
||||
else:
|
||||
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
|
||||
|
||||
# Define colors from arguments
|
||||
black = _color(black, "RGB")
|
||||
white = _color(white, "RGB")
|
||||
if mid is not None:
|
||||
mid = _color(mid, "RGB")
|
||||
|
||||
# Empty lists for the mapping
|
||||
red = []
|
||||
green = []
|
||||
blue = []
|
||||
for i in range(256):
|
||||
red.append(black[0]+i*(white[0]-black[0])//255)
|
||||
green.append(black[1]+i*(white[1]-black[1])//255)
|
||||
blue.append(black[2]+i*(white[2]-black[2])//255)
|
||||
|
||||
# Create the low-end values
|
||||
for i in range(0, blackpoint):
|
||||
red.append(black[0])
|
||||
green.append(black[1])
|
||||
blue.append(black[2])
|
||||
|
||||
# Create the mapping (2-color)
|
||||
if mid is None:
|
||||
|
||||
range_map = range(0, whitepoint - blackpoint)
|
||||
|
||||
for i in range_map:
|
||||
red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
|
||||
green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
|
||||
blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
|
||||
|
||||
# Create the mapping (3-color)
|
||||
else:
|
||||
|
||||
range_map1 = range(0, midpoint - blackpoint)
|
||||
range_map2 = range(0, whitepoint - midpoint)
|
||||
|
||||
for i in range_map1:
|
||||
red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
|
||||
green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
|
||||
blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
|
||||
for i in range_map2:
|
||||
red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
|
||||
green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
|
||||
blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
|
||||
|
||||
# Create the high-end values
|
||||
for i in range(0, 256 - whitepoint):
|
||||
red.append(white[0])
|
||||
green.append(white[1])
|
||||
blue.append(white[2])
|
||||
|
||||
# Return converted image
|
||||
image = image.convert("RGB")
|
||||
return _lut(image, red + green + blue)
|
||||
|
||||
|
||||
def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)):
|
||||
"""
|
||||
Returns a sized and padded version of the image, expanded to fill the
|
||||
requested aspect ratio and size.
|
||||
|
||||
:param image: The image to size and crop.
|
||||
:param size: The requested output size in pixels, given as a
|
||||
(width, height) tuple.
|
||||
:param method: What resampling method to use. Default is
|
||||
:py:attr:`PIL.Image.NEAREST`.
|
||||
:param color: The background color of the padded image.
|
||||
:param centering: Control the position of the original image within the
|
||||
padded version.
|
||||
(0.5, 0.5) will keep the image centered
|
||||
(0, 0) will keep the image aligned to the top left
|
||||
(1, 1) will keep the image aligned to the bottom
|
||||
right
|
||||
:return: An image.
|
||||
"""
|
||||
|
||||
im_ratio = image.width / image.height
|
||||
dest_ratio = float(size[0]) / size[1]
|
||||
|
||||
if im_ratio == dest_ratio:
|
||||
out = image.resize(size, resample=method)
|
||||
else:
|
||||
out = Image.new(image.mode, size, color)
|
||||
if im_ratio > dest_ratio:
|
||||
new_height = int(image.height / image.width * size[0])
|
||||
if new_height != size[1]:
|
||||
image = image.resize((size[0], new_height), resample=method)
|
||||
|
||||
y = int((size[1] - new_height) * max(0, min(centering[1], 1)))
|
||||
out.paste(image, (0, y))
|
||||
else:
|
||||
new_width = int(image.width / image.height * size[1])
|
||||
if new_width != size[0]:
|
||||
image = image.resize((new_width, size[1]), resample=method)
|
||||
|
||||
x = int((size[0] - new_width) * max(0, min(centering[0], 1)))
|
||||
out.paste(image, (x, 0))
|
||||
return out
|
||||
|
||||
|
||||
def crop(image, border=0):
|
||||
"""
|
||||
Remove border from image. The same amount of pixels are removed
|
||||
|
|
@ -178,6 +282,28 @@ def crop(image, border=0):
|
|||
)
|
||||
|
||||
|
||||
def scale(image, factor, resample=Image.NEAREST):
|
||||
"""
|
||||
Returns a rescaled image by a specific factor given in parameter.
|
||||
A factor greater than 1 expands the image, between 0 and 1 contracts the
|
||||
image.
|
||||
|
||||
:param image: The image to rescale.
|
||||
:param factor: The expansion factor, as a float.
|
||||
:param resample: An optional resampling filter. Same values possible as
|
||||
in the PIL.Image.resize function.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
"""
|
||||
if factor == 1:
|
||||
return image.copy()
|
||||
elif factor <= 0:
|
||||
raise ValueError("the factor must be greater than 0")
|
||||
else:
|
||||
size = (int(round(factor * image.width)),
|
||||
int(round(factor * image.height)))
|
||||
return image.resize(size, resample)
|
||||
|
||||
|
||||
def deform(image, deformer, resample=Image.BILINEAR):
|
||||
"""
|
||||
Deform the image.
|
||||
|
|
@ -185,7 +311,8 @@ def deform(image, deformer, resample=Image.BILINEAR):
|
|||
:param image: The image to deform.
|
||||
:param deformer: A deformer object. Any object that implements a
|
||||
**getmesh** method can be used.
|
||||
:param resample: What resampling filter to use.
|
||||
:param resample: An optional resampling filter. Same values possible as
|
||||
in the PIL.Image.transform function.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transform(
|
||||
|
|
@ -248,13 +375,15 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)):
|
|||
|
||||
This function was contributed by Kevin Cazabon.
|
||||
|
||||
:param image: The image to size and crop.
|
||||
:param size: The requested output size in pixels, given as a
|
||||
(width, height) tuple.
|
||||
:param method: What resampling method to use. Default is
|
||||
:py:attr:`PIL.Image.NEAREST`.
|
||||
:param bleed: Remove a border around the outside of the image (from all
|
||||
:param bleed: Remove a border around the outside of the image from all
|
||||
four edges. The value is a decimal percentage (use 0.01 for
|
||||
one percent). The default value is 0 (no border).
|
||||
Cannot be greater than or equal to 0.5.
|
||||
:param centering: Control the cropping position. Use (0.5, 0.5) for
|
||||
center cropping (e.g. if cropping the width, take 50% off
|
||||
of the left side, and therefore 50% off the right side).
|
||||
|
|
@ -272,66 +401,53 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)):
|
|||
# kevin@cazabon.com
|
||||
# http://www.cazabon.com
|
||||
|
||||
# ensure inputs are valid
|
||||
if not isinstance(centering, list):
|
||||
centering = [centering[0], centering[1]]
|
||||
# ensure centering is mutable
|
||||
centering = list(centering)
|
||||
|
||||
if centering[0] > 1.0 or centering[0] < 0.0:
|
||||
centering[0] = 0.50
|
||||
if centering[1] > 1.0 or centering[1] < 0.0:
|
||||
centering[1] = 0.50
|
||||
if not 0.0 <= centering[0] <= 1.0:
|
||||
centering[0] = 0.5
|
||||
if not 0.0 <= centering[1] <= 1.0:
|
||||
centering[1] = 0.5
|
||||
|
||||
if bleed > 0.49999 or bleed < 0.0:
|
||||
if not 0.0 <= bleed < 0.5:
|
||||
bleed = 0.0
|
||||
|
||||
# calculate the area to use for resizing and cropping, subtracting
|
||||
# the 'bleed' around the edges
|
||||
|
||||
# number of pixels to trim off on Top and Bottom, Left and Right
|
||||
bleedPixels = (
|
||||
int((float(bleed) * float(image.size[0])) + 0.5),
|
||||
int((float(bleed) * float(image.size[1])) + 0.5)
|
||||
)
|
||||
bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
|
||||
|
||||
liveArea = (0, 0, image.size[0], image.size[1])
|
||||
if bleed > 0.0:
|
||||
liveArea = (
|
||||
bleedPixels[0], bleedPixels[1], image.size[0] - bleedPixels[0] - 1,
|
||||
image.size[1] - bleedPixels[1] - 1
|
||||
)
|
||||
live_size = (image.size[0] - bleed_pixels[0] * 2,
|
||||
image.size[1] - bleed_pixels[1] * 2)
|
||||
|
||||
liveSize = (liveArea[2] - liveArea[0], liveArea[3] - liveArea[1])
|
||||
|
||||
# calculate the aspect ratio of the liveArea
|
||||
liveAreaAspectRatio = float(liveSize[0])/float(liveSize[1])
|
||||
# calculate the aspect ratio of the live_size
|
||||
live_size_ratio = float(live_size[0]) / live_size[1]
|
||||
|
||||
# calculate the aspect ratio of the output image
|
||||
aspectRatio = float(size[0]) / float(size[1])
|
||||
output_ratio = float(size[0]) / size[1]
|
||||
|
||||
# figure out if the sides or top/bottom will be cropped off
|
||||
if liveAreaAspectRatio >= aspectRatio:
|
||||
# liveArea is wider than what's needed, crop the sides
|
||||
cropWidth = int((aspectRatio * float(liveSize[1])) + 0.5)
|
||||
cropHeight = liveSize[1]
|
||||
if live_size_ratio >= output_ratio:
|
||||
# live_size is wider than what's needed, crop the sides
|
||||
crop_width = output_ratio * live_size[1]
|
||||
crop_height = live_size[1]
|
||||
else:
|
||||
# liveArea is taller than what's needed, crop the top and bottom
|
||||
cropWidth = liveSize[0]
|
||||
cropHeight = int((float(liveSize[0])/aspectRatio) + 0.5)
|
||||
# live_size is taller than what's needed, crop the top and bottom
|
||||
crop_width = live_size[0]
|
||||
crop_height = live_size[0] / output_ratio
|
||||
|
||||
# make the crop
|
||||
leftSide = int(liveArea[0] + (float(liveSize[0]-cropWidth) * centering[0]))
|
||||
if leftSide < 0:
|
||||
leftSide = 0
|
||||
topSide = int(liveArea[1] + (float(liveSize[1]-cropHeight) * centering[1]))
|
||||
if topSide < 0:
|
||||
topSide = 0
|
||||
crop_left = bleed_pixels[0] + (live_size[0]-crop_width) * centering[0]
|
||||
crop_top = bleed_pixels[1] + (live_size[1]-crop_height) * centering[1]
|
||||
|
||||
out = image.crop(
|
||||
(leftSide, topSide, leftSide + cropWidth, topSide + cropHeight)
|
||||
)
|
||||
crop = (
|
||||
crop_left, crop_top,
|
||||
crop_left + crop_width, crop_top + crop_height
|
||||
)
|
||||
|
||||
# resize the image and return it
|
||||
return out.resize(size, method)
|
||||
return image.resize(size, method, box=crop)
|
||||
|
||||
|
||||
def flip(image):
|
||||
|
|
@ -415,6 +531,13 @@ def solarize(image, threshold=128):
|
|||
def gaussian_blur(im, radius=None):
|
||||
""" PIL_usm.gblur(im, [radius])"""
|
||||
|
||||
warnings.warn(
|
||||
'PIL.ImageOps.gaussian_blur is deprecated. '
|
||||
'Use PIL.ImageFilter.GaussianBlur instead. '
|
||||
'This function will be removed in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
if radius is None:
|
||||
radius = 5.0
|
||||
|
||||
|
|
@ -422,12 +545,30 @@ def gaussian_blur(im, radius=None):
|
|||
|
||||
return im.im.gaussian_blur(radius)
|
||||
|
||||
gblur = gaussian_blur
|
||||
|
||||
def gblur(im, radius=None):
|
||||
""" PIL_usm.gblur(im, [radius])"""
|
||||
|
||||
warnings.warn(
|
||||
'PIL.ImageOps.gblur is deprecated. '
|
||||
'Use PIL.ImageFilter.GaussianBlur instead. '
|
||||
'This function will be removed in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
return gaussian_blur(im, radius)
|
||||
|
||||
|
||||
def unsharp_mask(im, radius=None, percent=None, threshold=None):
|
||||
""" PIL_usm.usm(im, [radius, percent, threshold])"""
|
||||
|
||||
warnings.warn(
|
||||
'PIL.ImageOps.unsharp_mask is deprecated. '
|
||||
'Use PIL.ImageFilter.UnsharpMask instead. '
|
||||
'This function will be removed in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
if radius is None:
|
||||
radius = 5.0
|
||||
if percent is None:
|
||||
|
|
@ -439,7 +580,18 @@ def unsharp_mask(im, radius=None, percent=None, threshold=None):
|
|||
|
||||
return im.im.unsharp_mask(radius, percent, threshold)
|
||||
|
||||
usm = unsharp_mask
|
||||
|
||||
def usm(im, radius=None, percent=None, threshold=None):
|
||||
""" PIL_usm.usm(im, [radius, percent, threshold])"""
|
||||
|
||||
warnings.warn(
|
||||
'PIL.ImageOps.usm is deprecated. '
|
||||
'Use PIL.ImageFilter.UnsharpMask instead. '
|
||||
'This function will be removed in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
return unsharp_mask(im, radius, percent, threshold)
|
||||
|
||||
|
||||
def box_blur(image, radius):
|
||||
|
|
@ -456,6 +608,13 @@ def box_blur(image, radius):
|
|||
in each direction, i.e. 9 pixels in total.
|
||||
:return: An image.
|
||||
"""
|
||||
warnings.warn(
|
||||
'PIL.ImageOps.box_blur is deprecated. '
|
||||
'Use PIL.ImageFilter.BoxBlur instead. '
|
||||
'This function will be removed in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
image.load()
|
||||
|
||||
return image._new(image.im.box_blur(radius))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
#
|
||||
|
||||
import array
|
||||
from PIL import ImageColor
|
||||
from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile
|
||||
|
||||
|
||||
class ImagePalette(object):
|
||||
|
|
@ -38,7 +38,7 @@ class ImagePalette(object):
|
|||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or list(range(256))*len(self.mode)
|
||||
self.palette = palette or bytearray(range(256))*len(self.mode)
|
||||
self.colors = {}
|
||||
self.dirty = None
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
|
|
@ -59,7 +59,7 @@ class ImagePalette(object):
|
|||
|
||||
def getdata(self):
|
||||
"""
|
||||
Get palette contents in format suitable # for the low-level
|
||||
Get palette contents in format suitable for the low-level
|
||||
``im.putpalette`` primitive.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
|
|
@ -98,7 +98,7 @@ class ImagePalette(object):
|
|||
except KeyError:
|
||||
# allocate new color slot
|
||||
if isinstance(self.palette, bytes):
|
||||
self.palette = [int(x) for x in self.palette]
|
||||
self.palette = bytearray(self.palette)
|
||||
index = len(self.colors)
|
||||
if index >= 256:
|
||||
raise ValueError("cannot allocate more than 256 colors")
|
||||
|
|
@ -194,44 +194,23 @@ def load(filename):
|
|||
|
||||
# FIXME: supports GIMP gradients only
|
||||
|
||||
fp = open(filename, "rb")
|
||||
with open(filename, "rb") as fp:
|
||||
|
||||
lut = None
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import GimpPaletteFile
|
||||
fp.seek(0)
|
||||
p = GimpPaletteFile.GimpPaletteFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import GimpGradientFile
|
||||
fp.seek(0)
|
||||
p = GimpGradientFile.GimpGradientFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import PaletteFile
|
||||
fp.seek(0)
|
||||
p = PaletteFile.PaletteFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
raise IOError("cannot load palette")
|
||||
for paletteHandler in [
|
||||
GimpPaletteFile.GimpPaletteFile,
|
||||
GimpGradientFile.GimpGradientFile,
|
||||
PaletteFile.PaletteFile
|
||||
]:
|
||||
try:
|
||||
fp.seek(0)
|
||||
lut = paletteHandler(fp).getpalette()
|
||||
if lut:
|
||||
break
|
||||
except (SyntaxError, ValueError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
else:
|
||||
raise IOError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
||||
|
|
|
|||
|
|
@ -14,53 +14,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
|
||||
# the Python class below is overridden by the C implementation.
|
||||
|
||||
|
||||
class Path(object):
|
||||
|
||||
def __init__(self, xy):
|
||||
pass
|
||||
|
||||
##
|
||||
# Compacts the path, by removing points that are close to each
|
||||
# other. This method modifies the path in place.
|
||||
|
||||
def compact(self, distance=2):
|
||||
pass
|
||||
|
||||
##
|
||||
# Gets the bounding box.
|
||||
|
||||
def getbbox(self):
|
||||
pass
|
||||
|
||||
##
|
||||
# Maps the path through a function.
|
||||
|
||||
def map(self, function):
|
||||
pass
|
||||
|
||||
##
|
||||
# Converts the path to Python list.
|
||||
#
|
||||
# @param flat By default, this function returns a list of 2-tuples
|
||||
# [(x, y), ...]. If this argument is true, it returns a flat
|
||||
# list [x, y, ...] instead.
|
||||
# @return A list of coordinates.
|
||||
|
||||
def tolist(self, flat=0):
|
||||
pass
|
||||
|
||||
##
|
||||
# Transforms the path.
|
||||
|
||||
def transform(self, matrix):
|
||||
pass
|
||||
|
||||
|
||||
# override with C implementation
|
||||
Path = Image.core.path
|
||||
|
|
|
|||
|
|
@ -16,28 +16,41 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL._util import isPath
|
||||
from . import Image
|
||||
from ._util import isPath, py3
|
||||
from io import BytesIO
|
||||
import sys
|
||||
|
||||
qt_is_installed = True
|
||||
qt_version = None
|
||||
try:
|
||||
from PyQt5.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
qt_version = '5'
|
||||
except ImportError:
|
||||
qt_versions = [
|
||||
['5', 'PyQt5'],
|
||||
['side2', 'PySide2'],
|
||||
['4', 'PyQt4'],
|
||||
['side', 'PySide']
|
||||
]
|
||||
# If a version has already been imported, attempt it first
|
||||
qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules,
|
||||
reverse=True)
|
||||
for qt_version, qt_module in qt_versions:
|
||||
try:
|
||||
from PyQt4.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt4.QtCore import QBuffer, QIODevice
|
||||
qt_version = '4'
|
||||
except ImportError:
|
||||
try:
|
||||
if qt_module == 'PyQt5':
|
||||
from PyQt5.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
elif qt_module == 'PySide2':
|
||||
from PySide2.QtGui import QImage, qRgba, QPixmap
|
||||
from PySide2.QtCore import QBuffer, QIODevice
|
||||
elif qt_module == 'PyQt4':
|
||||
from PyQt4.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt4.QtCore import QBuffer, QIODevice
|
||||
elif qt_module == 'PySide':
|
||||
from PySide.QtGui import QImage, qRgba, QPixmap
|
||||
from PySide.QtCore import QBuffer, QIODevice
|
||||
qt_version = 'side'
|
||||
except ImportError:
|
||||
qt_is_installed = False
|
||||
except (ImportError, RuntimeError):
|
||||
continue
|
||||
qt_is_installed = True
|
||||
break
|
||||
else:
|
||||
qt_is_installed = False
|
||||
qt_version = None
|
||||
|
||||
|
||||
def rgb(r, g, b, a=255):
|
||||
|
|
@ -47,10 +60,11 @@ def rgb(r, g, b, a=255):
|
|||
return (qRgba(r, g, b, a) & 0xffffffff)
|
||||
|
||||
|
||||
# :param im A PIL Image object, or a file name
|
||||
# (given either as Python string or a PyQt string object)
|
||||
|
||||
def fromqimage(im):
|
||||
"""
|
||||
:param im: A PIL Image object, or a file name
|
||||
(given either as Python string or a PyQt string object)
|
||||
"""
|
||||
buffer = QBuffer()
|
||||
buffer.open(QIODevice.ReadWrite)
|
||||
# preserve alha channel with png
|
||||
|
|
@ -110,7 +124,8 @@ def align8to32(bytes, width, mode):
|
|||
|
||||
new_data = []
|
||||
for i in range(len(bytes) // bytes_per_line):
|
||||
new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\x00' * extra_padding)
|
||||
new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line]
|
||||
+ b'\x00' * extra_padding)
|
||||
|
||||
return b''.join(new_data)
|
||||
|
||||
|
|
@ -122,10 +137,10 @@ def _toqclass_helper(im):
|
|||
# handle filename, if given instead of image name
|
||||
if hasattr(im, "toUtf8"):
|
||||
# FIXME - is this really the best way to do this?
|
||||
if str is bytes:
|
||||
im = unicode(im.toUtf8(), "utf-8")
|
||||
else:
|
||||
if py3:
|
||||
im = str(im.toUtf8(), "utf-8")
|
||||
else:
|
||||
im = unicode(im.toUtf8(), "utf-8") # noqa: F821
|
||||
if isPath(im):
|
||||
im = Image.open(im)
|
||||
|
||||
|
|
@ -156,26 +171,31 @@ def _toqclass_helper(im):
|
|||
else:
|
||||
raise ValueError("unsupported image mode %r" % im.mode)
|
||||
|
||||
# must keep a reference, or Qt will crash!
|
||||
__data = data or align8to32(im.tobytes(), im.size[0], im.mode)
|
||||
return {
|
||||
'data': __data, 'im': im, 'format': format, 'colortable': colortable
|
||||
}
|
||||
|
||||
##
|
||||
# An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
||||
# class.
|
||||
#
|
||||
# @param im A PIL Image object, or a file name (given either as Python
|
||||
# string or a PyQt string object).
|
||||
|
||||
if qt_is_installed:
|
||||
class ImageQt(QImage):
|
||||
|
||||
def __init__(self, im):
|
||||
"""
|
||||
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
||||
class.
|
||||
|
||||
:param im: A PIL Image object, or a file name (given either as
|
||||
Python string or a PyQt string object).
|
||||
"""
|
||||
im_data = _toqclass_helper(im)
|
||||
# must keep a reference, or Qt will crash!
|
||||
# All QImage constructors that take data operate on an existing
|
||||
# buffer, so this buffer has to hang on for the life of the image.
|
||||
# Fixes https://github.com/python-pillow/Pillow/issues/1370
|
||||
self.__data = im_data['data']
|
||||
QImage.__init__(self,
|
||||
im_data['data'], im_data['im'].size[0],
|
||||
self.__data, im_data['im'].size[0],
|
||||
im_data['im'].size[1], im_data['format'])
|
||||
if im_data['colortable']:
|
||||
self.setColorTable(im_data['colortable'])
|
||||
|
|
|
|||
|
|
@ -32,11 +32,25 @@ class Iterator(object):
|
|||
if not hasattr(im, "seek"):
|
||||
raise AttributeError("im must have seek method")
|
||||
self.im = im
|
||||
self.position = 0
|
||||
|
||||
def __getitem__(self, ix):
|
||||
try:
|
||||
if ix:
|
||||
self.im.seek(ix)
|
||||
self.im.seek(ix)
|
||||
return self.im
|
||||
except EOFError:
|
||||
raise IndexError # end of sequence
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
try:
|
||||
self.im.seek(self.position)
|
||||
self.position += 1
|
||||
return self.im
|
||||
except EOFError:
|
||||
raise StopIteration
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ from __future__ import print_function
|
|||
from PIL import Image
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
if sys.version_info.major >= 3:
|
||||
from shlex import quote
|
||||
else:
|
||||
from pipes import quote
|
||||
|
|
@ -38,25 +40,23 @@ def register(viewer, order=1):
|
|||
_viewers.insert(0, viewer)
|
||||
|
||||
|
||||
##
|
||||
# Displays a given image.
|
||||
#
|
||||
# @param image An image object.
|
||||
# @param title Optional title. Not all viewers can display the title.
|
||||
# @param **options Additional viewer options.
|
||||
# @return True if a suitable viewer was found, false otherwise.
|
||||
|
||||
def show(image, title=None, **options):
|
||||
r"""
|
||||
Display a given image.
|
||||
|
||||
:param image: An image object.
|
||||
:param title: Optional title. Not all viewers can display the title.
|
||||
:param \**options: Additional viewer options.
|
||||
:returns: True if a suitable viewer was found, false otherwise.
|
||||
"""
|
||||
for viewer in _viewers:
|
||||
if viewer.show(image, title=title, **options):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
##
|
||||
# Base class for viewers.
|
||||
|
||||
class Viewer(object):
|
||||
"""Base class for viewers."""
|
||||
|
||||
# main api
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ class Viewer(object):
|
|||
# FIXME: auto-contrast if max() > 255?
|
||||
else:
|
||||
base = Image.getmodebase(image.mode)
|
||||
if base != image.mode and image.mode != "1":
|
||||
if base != image.mode and image.mode != "1" and image.mode != "RGBA":
|
||||
image = image.convert(base)
|
||||
|
||||
return self.show_image(image, **options)
|
||||
|
|
@ -79,29 +79,31 @@ class Viewer(object):
|
|||
# hook methods
|
||||
|
||||
format = None
|
||||
options = {}
|
||||
|
||||
def get_format(self, image):
|
||||
# return format name, or None to save as PGM/PPM
|
||||
"""Return format name, or None to save as PGM/PPM"""
|
||||
return self.format
|
||||
|
||||
def get_command(self, file, **options):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_image(self, image):
|
||||
# save to temporary file, and return filename
|
||||
return image._dump(format=self.get_format(image))
|
||||
"""Save to temporary file, and return filename"""
|
||||
return image._dump(format=self.get_format(image), **self.options)
|
||||
|
||||
def show_image(self, image, **options):
|
||||
# display given image
|
||||
"""Display given image"""
|
||||
return self.show_file(self.save_image(image), **options)
|
||||
|
||||
def show_file(self, file, **options):
|
||||
# display given file
|
||||
"""Display given file"""
|
||||
os.system(self.get_command(file, **options))
|
||||
return 1
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
||||
class WindowsViewer(Viewer):
|
||||
|
|
@ -117,7 +119,8 @@ if sys.platform == "win32":
|
|||
elif sys.platform == "darwin":
|
||||
|
||||
class MacViewer(Viewer):
|
||||
format = "BMP"
|
||||
format = "PNG"
|
||||
options = {'compress_level': 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
# on darwin open returns immediately resulting in the temp
|
||||
|
|
@ -127,6 +130,21 @@ elif sys.platform == "darwin":
|
|||
quote(file))
|
||||
return command
|
||||
|
||||
def show_file(self, file, **options):
|
||||
"""Display given file"""
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
f.write(file)
|
||||
with open(path, "r") as f:
|
||||
subprocess.Popen([
|
||||
'im=$(cat);'
|
||||
'open -a /Applications/Preview.app $im;'
|
||||
'sleep 20;'
|
||||
'rm -f $im'
|
||||
], shell=True, stdin=f)
|
||||
os.remove(path)
|
||||
return 1
|
||||
|
||||
register(MacViewer)
|
||||
|
||||
else:
|
||||
|
|
@ -139,17 +157,31 @@ else:
|
|||
return None
|
||||
for dirname in path.split(os.pathsep):
|
||||
filename = os.path.join(dirname, executable)
|
||||
if os.path.isfile(filename):
|
||||
# FIXME: make sure it's executable
|
||||
if os.path.isfile(filename) and os.access(filename, os.X_OK):
|
||||
return filename
|
||||
return None
|
||||
|
||||
class UnixViewer(Viewer):
|
||||
format = "PNG"
|
||||
options = {'compress_level': 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
return "(%s %s; rm -f %s)&" % (command, quote(file), quote(file))
|
||||
|
||||
def show_file(self, file, **options):
|
||||
command, executable = self.get_command_ex(file, **options)
|
||||
command = "(%s %s; rm -f %s)&" % (command, quote(file),
|
||||
quote(file))
|
||||
os.system(command)
|
||||
"""Display given file"""
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
f.write(file)
|
||||
with open(path, "r") as f:
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
subprocess.Popen([
|
||||
'im=$(cat);' +
|
||||
command+' $im;'
|
||||
'rm -f $im'
|
||||
], shell=True, stdin=f)
|
||||
os.remove(path)
|
||||
return 1
|
||||
|
||||
# implementations
|
||||
|
|
@ -162,6 +194,14 @@ else:
|
|||
if which("display"):
|
||||
register(DisplayViewer)
|
||||
|
||||
class EogViewer(UnixViewer):
|
||||
def get_command_ex(self, file, **options):
|
||||
command = executable = "eog"
|
||||
return command, executable
|
||||
|
||||
if which("eog"):
|
||||
register(EogViewer)
|
||||
|
||||
class XVViewer(UnixViewer):
|
||||
def get_command_ex(self, file, title=None, **options):
|
||||
# note: xv is pretty outdated. most modern systems have
|
||||
|
|
@ -175,5 +215,9 @@ else:
|
|||
register(XVViewer)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# usage: python ImageShow.py imagefile [title]
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Syntax: python ImageShow.py imagefile [title]")
|
||||
sys.exit()
|
||||
|
||||
print(show(Image.open(sys.argv[1]), *sys.argv[2:]))
|
||||
|
|
|
|||
|
|
@ -110,11 +110,11 @@ class Stat(object):
|
|||
v = []
|
||||
for i in self.bands:
|
||||
s = 0
|
||||
l = self.count[i]//2
|
||||
half = self.count[i]//2
|
||||
b = i * 256
|
||||
for j in range(256):
|
||||
s = s + self.h[b+j]
|
||||
if s > l:
|
||||
if s > half:
|
||||
break
|
||||
v.append(j)
|
||||
return v
|
||||
|
|
@ -144,4 +144,5 @@ class Stat(object):
|
|||
v.append(math.sqrt(self.var[i]))
|
||||
return v
|
||||
|
||||
|
||||
Global = Stat # compatibility
|
||||
|
|
|
|||
|
|
@ -25,14 +25,15 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
try:
|
||||
import tkinter
|
||||
except ImportError:
|
||||
import Tkinter
|
||||
tkinter = Tkinter
|
||||
del Tkinter
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
if sys.version_info.major > 2:
|
||||
import tkinter
|
||||
else:
|
||||
import Tkinter as tkinter
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -53,6 +54,16 @@ def _pilbitmap_check():
|
|||
return _pilbitmap_ok
|
||||
|
||||
|
||||
def _get_image_from_kw(kw):
|
||||
source = None
|
||||
if "file" in kw:
|
||||
source = kw.pop("file")
|
||||
elif "data" in kw:
|
||||
source = BytesIO(kw.pop("data"))
|
||||
if source:
|
||||
return Image.open(source)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PhotoImage
|
||||
|
||||
|
|
@ -80,13 +91,7 @@ class PhotoImage(object):
|
|||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
if "file" in kw:
|
||||
image = Image.open(kw["file"])
|
||||
del kw["file"]
|
||||
elif "data" in kw:
|
||||
from io import BytesIO
|
||||
image = Image.open(BytesIO(kw["data"]))
|
||||
del kw["data"]
|
||||
image = _get_image_from_kw(kw)
|
||||
|
||||
if hasattr(image, "mode") and hasattr(image, "size"):
|
||||
# got an image instead of a mode
|
||||
|
|
@ -119,7 +124,7 @@ class PhotoImage(object):
|
|||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except:
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -157,8 +162,8 @@ class PhotoImage(object):
|
|||
mode does not match, the image is converted to the mode of
|
||||
the bitmap image.
|
||||
:param box: A 4-tuple defining the left, upper, right, and lower pixel
|
||||
coordinate. If None is given instead of a tuple, all of
|
||||
the image is assumed.
|
||||
coordinate. See :ref:`coordinate-system`. If None is given
|
||||
instead of a tuple, all of the image is assumed.
|
||||
"""
|
||||
|
||||
# convert to blittable
|
||||
|
|
@ -177,9 +182,20 @@ class PhotoImage(object):
|
|||
except tkinter.TclError:
|
||||
# activate Tkinter hook
|
||||
try:
|
||||
from PIL import _imagingtk
|
||||
from . import _imagingtk
|
||||
try:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
if hasattr(tk, 'interp'):
|
||||
# Required for PyPy, which always has CFFI installed
|
||||
from cffi import FFI
|
||||
ffi = FFI()
|
||||
|
||||
# PyPy is using an FFI CDATA element
|
||||
# (Pdb) self.tk.interp
|
||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||
_imagingtk.tkinit(
|
||||
int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||
else:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
|
|
@ -192,7 +208,6 @@ class PhotoImage(object):
|
|||
|
||||
class BitmapImage(object):
|
||||
"""
|
||||
|
||||
A Tkinter-compatible bitmap image. This can be used everywhere Tkinter
|
||||
expects an image object.
|
||||
|
||||
|
|
@ -209,13 +224,7 @@ class BitmapImage(object):
|
|||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
if "file" in kw:
|
||||
image = Image.open(kw["file"])
|
||||
del kw["file"]
|
||||
elif "data" in kw:
|
||||
from io import BytesIO
|
||||
image = Image.open(BytesIO(kw["data"]))
|
||||
del kw["data"]
|
||||
image = _get_image_from_kw(kw)
|
||||
|
||||
self.__mode = image.mode
|
||||
self.__size = image.size
|
||||
|
|
@ -235,7 +244,7 @@ class BitmapImage(object):
|
|||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except:
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
|
||||
def width(self):
|
||||
|
|
@ -266,14 +275,14 @@ class BitmapImage(object):
|
|||
|
||||
|
||||
def getimage(photo):
|
||||
""" This function is unimplemented """
|
||||
|
||||
"""Copies the contents of a PhotoImage to a PIL image memory."""
|
||||
photo.tk.call("PyImagingPhotoGet", photo)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Helper for the Image.show method.
|
||||
|
||||
def _show(image, title):
|
||||
"""Helper for the Image.show method."""
|
||||
|
||||
class UI(tkinter.Label):
|
||||
def __init__(self, master, im):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
|
||||
class Transform(Image.ImageTransformHandler):
|
||||
|
|
@ -29,75 +29,70 @@ class Transform(Image.ImageTransformHandler):
|
|||
return image.transform(size, method, data, **options)
|
||||
|
||||
|
||||
##
|
||||
# Define an affine image transform.
|
||||
# <p>
|
||||
# This function takes a 6-tuple (<i>a, b, c, d, e, f</i>) which
|
||||
# contain the first two rows from an affine transform matrix. For
|
||||
# each pixel (<i>x, y</i>) in the output image, the new value is
|
||||
# taken from a position (a <i>x</i> + b <i>y</i> + c,
|
||||
# d <i>x</i> + e <i>y</i> + f) in the input image, rounded to
|
||||
# nearest pixel.
|
||||
# <p>
|
||||
# This function can be used to scale, translate, rotate, and shear the
|
||||
# original image.
|
||||
#
|
||||
# @def AffineTransform(matrix)
|
||||
# @param matrix A 6-tuple (<i>a, b, c, d, e, f</i>) containing
|
||||
# the first two rows from an affine transform matrix.
|
||||
# @see Image#Image.transform
|
||||
|
||||
|
||||
class AffineTransform(Transform):
|
||||
"""
|
||||
Define an affine image transform.
|
||||
|
||||
This function takes a 6-tuple (a, b, c, d, e, f) which contain the first
|
||||
two rows from an affine transform matrix. For each pixel (x, y) in the
|
||||
output image, the new value is taken from a position (a x + b y + c,
|
||||
d x + e y + f) in the input image, rounded to nearest pixel.
|
||||
|
||||
This function can be used to scale, translate, rotate, and shear the
|
||||
original image.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.transform`
|
||||
|
||||
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
|
||||
from an affine transform matrix.
|
||||
"""
|
||||
method = Image.AFFINE
|
||||
|
||||
|
||||
##
|
||||
# Define a transform to extract a subregion from an image.
|
||||
# <p>
|
||||
# Maps a rectangle (defined by two corners) from the image to a
|
||||
# rectangle of the given size. The resulting image will contain
|
||||
# data sampled from between the corners, such that (<i>x0, y0</i>)
|
||||
# in the input image will end up at (0,0) in the output image,
|
||||
# and (<i>x1, y1</i>) at <i>size</i>.
|
||||
# <p>
|
||||
# This method can be used to crop, stretch, shrink, or mirror an
|
||||
# arbitrary rectangle in the current image. It is slightly slower than
|
||||
# <b>crop</b>, but about as fast as a corresponding <b>resize</b>
|
||||
# operation.
|
||||
#
|
||||
# @def ExtentTransform(bbox)
|
||||
# @param bbox A 4-tuple (<i>x0, y0, x1, y1</i>) which specifies
|
||||
# two points in the input image's coordinate system.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class ExtentTransform(Transform):
|
||||
"""
|
||||
Define a transform to extract a subregion from an image.
|
||||
|
||||
Maps a rectangle (defined by two corners) from the image to a rectangle of
|
||||
the given size. The resulting image will contain data sampled from between
|
||||
the corners, such that (x0, y0) in the input image will end up at (0,0) in
|
||||
the output image, and (x1, y1) at size.
|
||||
|
||||
This method can be used to crop, stretch, shrink, or mirror an arbitrary
|
||||
rectangle in the current image. It is slightly slower than crop, but about
|
||||
as fast as a corresponding resize operation.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.transform`
|
||||
|
||||
:param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
|
||||
input image's coordinate system. See :ref:`coordinate-system`.
|
||||
"""
|
||||
method = Image.EXTENT
|
||||
|
||||
|
||||
##
|
||||
# Define an quad image transform.
|
||||
# <p>
|
||||
# Maps a quadrilateral (a region defined by four corners) from the
|
||||
# image to a rectangle of the given size.
|
||||
#
|
||||
# @def QuadTransform(xy)
|
||||
# @param xy An 8-tuple (<i>x0, y0, x1, y1, x2, y2, y3, y3</i>) which
|
||||
# contain the upper left, lower left, lower right, and upper right
|
||||
# corner of the source quadrilateral.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class QuadTransform(Transform):
|
||||
"""
|
||||
Define a quad image transform.
|
||||
|
||||
Maps a quadrilateral (a region defined by four corners) from the image to a
|
||||
rectangle of the given size.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.transform`
|
||||
|
||||
:param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the
|
||||
upper left, lower left, lower right, and upper right corner of the
|
||||
source quadrilateral.
|
||||
"""
|
||||
method = Image.QUAD
|
||||
|
||||
|
||||
##
|
||||
# Define an mesh image transform. A mesh transform consists of one
|
||||
# or more individual quad transforms.
|
||||
#
|
||||
# @def MeshTransform(data)
|
||||
# @param data A list of (bbox, quad) tuples.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class MeshTransform(Transform):
|
||||
"""
|
||||
Define a mesh image transform. A mesh transform consists of one or more
|
||||
individual quad transforms.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.transform`
|
||||
|
||||
:param data: A list of (bbox, quad) tuples.
|
||||
"""
|
||||
method = Image.MESH
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
|
||||
class HDC(object):
|
||||
|
|
@ -154,8 +154,9 @@ class Dib(object):
|
|||
If the mode does not match, the image is converted to the
|
||||
mode of the bitmap image.
|
||||
:param box: A 4-tuple defining the left, upper, right, and
|
||||
lower pixel coordinate. If None is given instead of a
|
||||
tuple, all of the image is assumed.
|
||||
lower pixel coordinate. See :ref:`coordinate-system`. If
|
||||
None is given instead of a tuple, all of the image is
|
||||
assumed.
|
||||
"""
|
||||
im.load()
|
||||
if self.mode != im.mode:
|
||||
|
|
@ -182,19 +183,9 @@ class Dib(object):
|
|||
"""
|
||||
return self.image.tobytes()
|
||||
|
||||
def fromstring(self, *args, **kw):
|
||||
raise Exception("fromstring() has been removed. " +
|
||||
"Please use frombytes() instead.")
|
||||
|
||||
def tostring(self, *args, **kw):
|
||||
raise Exception("tostring() has been removed. " +
|
||||
"Please use tobytes() instead.")
|
||||
|
||||
|
||||
##
|
||||
# Create a Window with the given title size.
|
||||
|
||||
class Window(object):
|
||||
"""Create a Window with the given title size."""
|
||||
|
||||
def __init__(self, title="PIL", width=None, height=None):
|
||||
self.hwnd = Image.core.createwindow(
|
||||
|
|
@ -223,10 +214,8 @@ class Window(object):
|
|||
Image.core.eventloop()
|
||||
|
||||
|
||||
##
|
||||
# Create an image window which displays the given image.
|
||||
|
||||
class ImageWindow(Window):
|
||||
"""Create an image window which displays the given image."""
|
||||
|
||||
def __init__(self, image, title="PIL"):
|
||||
if not isinstance(image, Dib):
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import re
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ class ImtImageFile(ImageFile.ImageFile):
|
|||
s = s + self.fp.readline()
|
||||
if len(s) == 1 or len(s) > 100:
|
||||
break
|
||||
if s[0] == b"*":
|
||||
if s[0] == ord(b"*"):
|
||||
continue # comment
|
||||
|
||||
m = field.match(s)
|
||||
|
|
@ -78,10 +78,10 @@ class ImtImageFile(ImageFile.ImageFile):
|
|||
k, v = m.group(1, 2)
|
||||
if k == "width":
|
||||
xsize = int(v)
|
||||
self.size = xsize, ysize
|
||||
self._size = xsize, ysize
|
||||
elif k == "height":
|
||||
ysize = int(v)
|
||||
self.size = xsize, ysize
|
||||
self._size = xsize, ysize
|
||||
elif k == "pixel" and v == "n8":
|
||||
self.mode = "L"
|
||||
|
||||
|
|
|
|||
|
|
@ -17,17 +17,13 @@
|
|||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8, i16be as i16, i32be as i32, o8
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
__version__ = "0.3"
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16be
|
||||
i32 = _binary.i32be
|
||||
o8 = _binary.o8
|
||||
|
||||
COMPRESSION = {
|
||||
1: "raw",
|
||||
5: "jpeg"
|
||||
|
|
@ -99,7 +95,7 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
tagdata = self.fp.read(size)
|
||||
else:
|
||||
tagdata = None
|
||||
if tag in list(self.info.keys()):
|
||||
if tag in self.info:
|
||||
if isinstance(self.info[tag], list):
|
||||
self.info[tag].append(tagdata)
|
||||
else:
|
||||
|
|
@ -107,8 +103,6 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
self.info[tag] = tagdata
|
||||
|
||||
# print tag, self.info[tag]
|
||||
|
||||
# mode
|
||||
layers = i8(self.info[(3, 60)][0])
|
||||
component = i8(self.info[(3, 60)][1])
|
||||
|
|
@ -124,7 +118,7 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
self.mode = "CMYK"[id]
|
||||
|
||||
# size
|
||||
self.size = self.getint((3, 20)), self.getint((3, 30))
|
||||
self._size = self.getint((3, 20)), self.getint((3, 30))
|
||||
|
||||
# compression
|
||||
try:
|
||||
|
|
@ -168,14 +162,9 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
o.close()
|
||||
|
||||
try:
|
||||
try:
|
||||
# fast
|
||||
self.im = Image.core.open_ppm(outfile)
|
||||
except:
|
||||
# slightly slower
|
||||
im = Image.open(outfile)
|
||||
im.load()
|
||||
self.im = im.im
|
||||
_im = Image.open(outfile)
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
|
|
@ -188,16 +177,15 @@ Image.register_open(IptcImageFile.format, IptcImageFile)
|
|||
Image.register_extension(IptcImageFile.format, ".iim")
|
||||
|
||||
|
||||
##
|
||||
# Get IPTC information from TIFF, JPEG, or IPTC file.
|
||||
#
|
||||
# @param im An image containing IPTC data.
|
||||
# @return A dictionary containing IPTC information, or None if
|
||||
# no IPTC information block was found.
|
||||
|
||||
def getiptcinfo(im):
|
||||
"""
|
||||
Get IPTC information from TIFF, JPEG, or IPTC file.
|
||||
|
||||
from PIL import TiffImagePlugin, JpegImagePlugin
|
||||
:param im: An image containing IPTC data.
|
||||
:returns: A dictionary containing IPTC information, or None if
|
||||
no IPTC information block was found.
|
||||
"""
|
||||
from . import TiffImagePlugin, JpegImagePlugin
|
||||
import io
|
||||
|
||||
data = None
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
import struct
|
||||
import os
|
||||
import io
|
||||
|
|
@ -29,13 +29,13 @@ def _parse_codestream(fp):
|
|||
siz = hdr + fp.read(lsiz - 2)
|
||||
lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \
|
||||
xtosiz, ytosiz, csiz \
|
||||
= struct.unpack('>HHIIIIIIIIH', siz[:38])
|
||||
= struct.unpack_from('>HHIIIIIIIIH', siz)
|
||||
ssiz = [None]*csiz
|
||||
xrsiz = [None]*csiz
|
||||
yrsiz = [None]*csiz
|
||||
for i in range(csiz):
|
||||
ssiz[i], xrsiz[i], yrsiz[i] \
|
||||
= struct.unpack('>BBB', siz[36 + 3 * i:39 + 3 * i])
|
||||
= struct.unpack_from('>BBB', siz, 36 + 3 * i)
|
||||
|
||||
size = (xsiz - xosiz, ysiz - yosiz)
|
||||
if csiz == 1:
|
||||
|
|
@ -84,6 +84,7 @@ def _parse_jp2_header(fp):
|
|||
size = None
|
||||
mode = None
|
||||
bpc = None
|
||||
nc = None
|
||||
|
||||
hio = io.BytesIO(header)
|
||||
while True:
|
||||
|
|
@ -113,9 +114,9 @@ def _parse_jp2_header(fp):
|
|||
mode = 'RGBA'
|
||||
break
|
||||
elif tbox == b'colr':
|
||||
meth, prec, approx = struct.unpack('>BBB', content[:3])
|
||||
meth, prec, approx = struct.unpack_from('>BBB', content)
|
||||
if meth == 1:
|
||||
cs = struct.unpack('>I', content[3:7])[0]
|
||||
cs = struct.unpack_from('>I', content, 3)[0]
|
||||
if cs == 16: # sRGB
|
||||
if nc == 1 and (bpc & 0x7f) > 8:
|
||||
mode = 'I;16'
|
||||
|
|
@ -141,6 +142,9 @@ def _parse_jp2_header(fp):
|
|||
mode = 'RGBA'
|
||||
break
|
||||
|
||||
if size is None or mode is None:
|
||||
raise SyntaxError("Malformed jp2 header")
|
||||
|
||||
return (size, mode)
|
||||
|
||||
##
|
||||
|
|
@ -155,13 +159,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
sig = self.fp.read(4)
|
||||
if sig == b'\xff\x4f\xff\x51':
|
||||
self.codec = "j2k"
|
||||
self.size, self.mode = _parse_codestream(self.fp)
|
||||
self._size, self.mode = _parse_codestream(self.fp)
|
||||
else:
|
||||
sig = sig + self.fp.read(8)
|
||||
|
||||
if sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a':
|
||||
self.codec = "jp2"
|
||||
self.size, self.mode = _parse_jp2_header(self.fp)
|
||||
self._size, self.mode = _parse_jp2_header(self.fp)
|
||||
else:
|
||||
raise SyntaxError('not a JPEG 2000 file')
|
||||
|
||||
|
|
@ -177,14 +181,14 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
try:
|
||||
fd = self.fp.fileno()
|
||||
length = os.fstat(fd).st_size
|
||||
except:
|
||||
except Exception:
|
||||
fd = -1
|
||||
try:
|
||||
pos = self.fp.tell()
|
||||
self.fp.seek(0, 2)
|
||||
length = self.fp.tell()
|
||||
self.fp.seek(pos, 0)
|
||||
except:
|
||||
except Exception:
|
||||
length = -1
|
||||
|
||||
self.tile = [('jpeg2k', (0, 0) + self.size, 0,
|
||||
|
|
@ -194,8 +198,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if self.reduce:
|
||||
power = 1 << self.reduce
|
||||
adjust = power >> 1
|
||||
self.size = (int((self.size[0] + adjust) / power),
|
||||
int((self.size[1] + adjust) / power))
|
||||
self._size = (int((self.size[0] + adjust) / power),
|
||||
int((self.size[1] + adjust) / power))
|
||||
|
||||
if self.tile:
|
||||
# Update the reduce and layers settings
|
||||
|
|
@ -203,7 +207,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4])
|
||||
self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
|
||||
|
||||
ImageFile.ImageFile.load(self)
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
|
|
@ -228,6 +232,13 @@ def _save(im, fp, filename):
|
|||
tile_size = info.get('tile_size', None)
|
||||
quality_mode = info.get('quality_mode', 'rates')
|
||||
quality_layers = info.get('quality_layers', None)
|
||||
if quality_layers is not None and not (
|
||||
isinstance(quality_layers, (list, tuple)) and
|
||||
all([isinstance(quality_layer, (int, float))
|
||||
for quality_layer in quality_layers])
|
||||
):
|
||||
raise ValueError('quality_layers must be a sequence of numbers')
|
||||
|
||||
num_resolutions = info.get('num_resolutions', 0)
|
||||
cblk_size = info.get('codeblock_size', None)
|
||||
precinct_size = info.get('precinct_size', None)
|
||||
|
|
@ -239,7 +250,7 @@ def _save(im, fp, filename):
|
|||
if hasattr(fp, "fileno"):
|
||||
try:
|
||||
fd = fp.fileno()
|
||||
except:
|
||||
except Exception:
|
||||
fd = -1
|
||||
|
||||
im.encoderconfig = (
|
||||
|
|
@ -262,15 +273,12 @@ def _save(im, fp, filename):
|
|||
# ------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
|
||||
Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept)
|
||||
Image.register_save(Jpeg2KImageFile.format, _save)
|
||||
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.jp2')
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.j2k')
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.jpc')
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.jpf')
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.jpx')
|
||||
Image.register_extension(Jpeg2KImageFile.format, '.j2c')
|
||||
Image.register_extensions(Jpeg2KImageFile.format,
|
||||
[".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"])
|
||||
|
||||
Image.register_mime(Jpeg2KImageFile.format, 'image/jp2')
|
||||
Image.register_mime(Jpeg2KImageFile.format, 'image/jpx')
|
||||
|
|
|
|||
|
|
@ -32,19 +32,16 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import array
|
||||
import struct
|
||||
import io
|
||||
import warnings
|
||||
from struct import unpack_from
|
||||
from PIL import Image, ImageFile, TiffImagePlugin, _binary
|
||||
from PIL.JpegPresets import presets
|
||||
from PIL._util import isStringType
|
||||
|
||||
i8 = _binary.i8
|
||||
o8 = _binary.o8
|
||||
i16 = _binary.i16be
|
||||
i32 = _binary.i32be
|
||||
from . import Image, ImageFile, TiffImagePlugin
|
||||
from ._binary import i8, o8, i16be as i16
|
||||
from .JpegPresets import presets
|
||||
from ._util import isStringType
|
||||
|
||||
__version__ = "0.6"
|
||||
|
||||
|
|
@ -78,7 +75,7 @@ def APP(self, marker):
|
|||
try:
|
||||
jfif_unit = i8(s[7])
|
||||
jfif_density = i16(s, 8), i16(s, 10)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if jfif_unit == 1:
|
||||
|
|
@ -86,8 +83,9 @@ def APP(self, marker):
|
|||
self.info["jfif_unit"] = jfif_unit
|
||||
self.info["jfif_density"] = jfif_density
|
||||
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
|
||||
# extract Exif information (incomplete)
|
||||
self.info["exif"] = s # FIXME: value will change
|
||||
if "exif" not in self.info:
|
||||
# extract Exif information (incomplete)
|
||||
self.info["exif"] = s # FIXME: value will change
|
||||
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
||||
# extract FlashPix information (incomplete)
|
||||
self.info["flashpix"] = s # FIXME: value will change
|
||||
|
|
@ -109,7 +107,7 @@ def APP(self, marker):
|
|||
# extract Adobe custom properties
|
||||
try:
|
||||
adobe_transform = i8(s[1])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.info["adobe_transform"] = adobe_transform
|
||||
|
|
@ -120,6 +118,26 @@ def APP(self, marker):
|
|||
# plus constant header size
|
||||
self.info["mpoffset"] = self.fp.tell() - n + 4
|
||||
|
||||
# If DPI isn't in JPEG header, fetch from EXIF
|
||||
if "dpi" not in self.info and "exif" in self.info:
|
||||
try:
|
||||
exif = self._getexif()
|
||||
resolution_unit = exif[0x0128]
|
||||
x_resolution = exif[0x011A]
|
||||
try:
|
||||
dpi = x_resolution[0] / x_resolution[1]
|
||||
except TypeError:
|
||||
dpi = x_resolution
|
||||
if resolution_unit == 3: # cm
|
||||
# 1 dpcm = 2.54 dpi
|
||||
dpi *= 2.54
|
||||
self.info["dpi"] = dpi, dpi
|
||||
except (KeyError, SyntaxError, ZeroDivisionError):
|
||||
# SyntaxError for invalid/unreadable exif
|
||||
# KeyError for dpi not included
|
||||
# ZeroDivisionError for invalid dpi rational value
|
||||
self.info["dpi"] = 72, 72
|
||||
|
||||
|
||||
def COM(self, marker):
|
||||
#
|
||||
|
|
@ -141,7 +159,7 @@ def SOF(self, marker):
|
|||
|
||||
n = i16(self.fp.read(2))-2
|
||||
s = ImageFile._safe_read(self.fp, n)
|
||||
self.size = i16(s[3:]), i16(s[1:])
|
||||
self._size = i16(s[3:]), i16(s[1:])
|
||||
|
||||
self.bits = i8(s[0])
|
||||
if self.bits != 8:
|
||||
|
|
@ -195,7 +213,7 @@ def DQT(self, marker):
|
|||
raise SyntaxError("bad quantization table marker")
|
||||
v = i8(s[0])
|
||||
if v//16 == 0:
|
||||
self.quantization[v & 15] = array.array("b", s[1:65])
|
||||
self.quantization[v & 15] = array.array("B", s[1:65])
|
||||
s = s[65:]
|
||||
else:
|
||||
return # FIXME: add code to read 16-bit tables!
|
||||
|
|
@ -316,7 +334,6 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
|
||||
if i in MARKER:
|
||||
name, description, handler = MARKER[i]
|
||||
# print hex(i), name, description
|
||||
if handler is not None:
|
||||
handler(self, i)
|
||||
if i == 0xFFDA: # start of scan
|
||||
|
|
@ -331,14 +348,35 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
elif i == 0 or i == 0xFFFF:
|
||||
# padded marker or junk; move on
|
||||
s = b"\xff"
|
||||
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
||||
s = self.fp.read(1)
|
||||
else:
|
||||
raise SyntaxError("no marker found")
|
||||
|
||||
def load_read(self, read_bytes):
|
||||
"""
|
||||
internal: read more image data
|
||||
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
|
||||
so libjpeg can finish decoding
|
||||
"""
|
||||
s = self.fp.read(read_bytes)
|
||||
|
||||
if not s and ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
# Premature EOF.
|
||||
# Pretend file is finished adding EOI marker
|
||||
return b"\xFF\xD9"
|
||||
|
||||
return s
|
||||
|
||||
def draft(self, mode, size):
|
||||
|
||||
if len(self.tile) != 1:
|
||||
return
|
||||
|
||||
# Protect from second call
|
||||
if self.decoderconfig:
|
||||
return
|
||||
|
||||
d, e, o, a = self.tile[0]
|
||||
scale = 0
|
||||
|
||||
|
|
@ -347,12 +385,12 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
a = mode, ""
|
||||
|
||||
if size:
|
||||
scale = max(self.size[0] // size[0], self.size[1] // size[1])
|
||||
scale = min(self.size[0] // size[0], self.size[1] // size[1])
|
||||
for s in [8, 4, 2, 1]:
|
||||
if scale >= s:
|
||||
break
|
||||
e = e[0], e[1], (e[2]-e[0]+s-1)//s+e[0], (e[3]-e[1]+s-1)//s+e[1]
|
||||
self.size = ((self.size[0]+s-1)//s, (self.size[1]+s-1)//s)
|
||||
self._size = ((self.size[0]+s-1)//s, (self.size[1]+s-1)//s)
|
||||
scale = s
|
||||
|
||||
self.tile = [(d, e, o, a)]
|
||||
|
|
@ -375,7 +413,9 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
raise ValueError("Invalid Filename")
|
||||
|
||||
try:
|
||||
self.im = Image.core.open_ppm(path)
|
||||
_im = Image.open(path)
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
finally:
|
||||
try:
|
||||
os.unlink(path)
|
||||
|
|
@ -383,7 +423,7 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
|
||||
self.mode = self.im.mode
|
||||
self.size = self.im.size
|
||||
self._size = self.im.size
|
||||
|
||||
self.tile = []
|
||||
|
||||
|
|
@ -399,12 +439,13 @@ def _fixup_dict(src_dict):
|
|||
# returns a dict with any single item tuples/lists as individual values
|
||||
def _fixup(value):
|
||||
try:
|
||||
if len(value) == 1 and type(value) != type({}):
|
||||
if len(value) == 1 and not isinstance(value, dict):
|
||||
return value[0]
|
||||
except: pass
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
return dict([(k, _fixup(v)) for k, v in src_dict.items()])
|
||||
return {k: _fixup(v) for k, v in src_dict.items()}
|
||||
|
||||
|
||||
def _getexif(self):
|
||||
|
|
@ -448,7 +489,7 @@ def _getexif(self):
|
|||
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
||||
info.load(file)
|
||||
exif[0x8825] = _fixup_dict(info)
|
||||
|
||||
|
||||
return exif
|
||||
|
||||
|
||||
|
|
@ -471,7 +512,7 @@ def _getmp(self):
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||
info.load(file_contents)
|
||||
mp = dict(info)
|
||||
except:
|
||||
except Exception:
|
||||
raise SyntaxError("malformed MP Index (unreadable directory)")
|
||||
# it's an error not to have a number of images
|
||||
try:
|
||||
|
|
@ -483,8 +524,8 @@ def _getmp(self):
|
|||
try:
|
||||
rawmpentries = mp[0xB002]
|
||||
for entrynum in range(0, quant):
|
||||
unpackedentry = unpack_from(
|
||||
'{0}LLLHH'.format(endianness), rawmpentries, entrynum * 16)
|
||||
unpackedentry = struct.unpack_from(
|
||||
'{}LLLHH'.format(endianness), rawmpentries, entrynum * 16)
|
||||
labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1',
|
||||
'EntryNo2')
|
||||
mpentry = dict(zip(labels, unpackedentry))
|
||||
|
|
@ -532,13 +573,12 @@ RAWMODE = {
|
|||
"1": "L",
|
||||
"L": "L",
|
||||
"RGB": "RGB",
|
||||
"RGBA": "RGB",
|
||||
"RGBX": "RGB",
|
||||
"CMYK": "CMYK;I", # assume adobe conventions
|
||||
"YCbCr": "YCbCr",
|
||||
}
|
||||
|
||||
zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28,
|
||||
zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28, # noqa: E128
|
||||
2, 4, 7, 13, 16, 26, 29, 42,
|
||||
3, 8, 12, 17, 25, 30, 41, 43,
|
||||
9, 11, 18, 24, 31, 40, 44, 53,
|
||||
|
|
@ -583,7 +623,7 @@ def _save(im, fp, filename):
|
|||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (0, 0))
|
||||
dpi = [int(round(x)) for x in info.get("dpi", (0, 0))]
|
||||
|
||||
quality = info.get("quality", 0)
|
||||
subsampling = info.get("subsampling", -1)
|
||||
|
|
@ -610,7 +650,11 @@ def _save(im, fp, filename):
|
|||
subsampling = 0
|
||||
elif subsampling == "4:2:2":
|
||||
subsampling = 1
|
||||
elif subsampling == "4:2:0":
|
||||
subsampling = 2
|
||||
elif subsampling == "4:1:1":
|
||||
# For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
|
||||
# Set 4:2:0 if someone is still using that value.
|
||||
subsampling = 2
|
||||
elif subsampling == "keep":
|
||||
if im.format != "JPEG":
|
||||
|
|
@ -639,8 +683,8 @@ def _save(im, fp, filename):
|
|||
for idx, table in enumerate(qtables):
|
||||
try:
|
||||
if len(table) != 64:
|
||||
raise
|
||||
table = array.array('b', table)
|
||||
raise TypeError
|
||||
table = array.array('B', table)
|
||||
except TypeError:
|
||||
raise ValueError("Invalid quantization table")
|
||||
else:
|
||||
|
|
@ -672,15 +716,20 @@ def _save(im, fp, filename):
|
|||
o8(len(markers)) + marker)
|
||||
i += 1
|
||||
|
||||
# "progressive" is the official name, but older documentation
|
||||
# says "progression"
|
||||
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
|
||||
progressive = (info.get("progressive", False) or
|
||||
info.get("progression", False))
|
||||
|
||||
optimize = info.get("optimize", False)
|
||||
|
||||
# get keyword arguments
|
||||
im.encoderconfig = (
|
||||
quality,
|
||||
# "progressive" is the official name, but older documentation
|
||||
# says "progression"
|
||||
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
|
||||
"progressive" in info or "progression" in info,
|
||||
progressive,
|
||||
info.get("smooth", 0),
|
||||
"optimize" in info,
|
||||
optimize,
|
||||
info.get("streamtype", 0),
|
||||
dpi[0], dpi[1],
|
||||
subsampling,
|
||||
|
|
@ -690,20 +739,24 @@ def _save(im, fp, filename):
|
|||
)
|
||||
|
||||
# if we optimize, libjpeg needs a buffer big enough to hold the whole image
|
||||
# in a shot. Guessing on the size, at im.size bytes. (raw pizel size is
|
||||
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
|
||||
# channels*size, this is a value that's been used in a django patch.
|
||||
# https://github.com/matthewwithanm/django-imagekit/issues/50
|
||||
bufsize = 0
|
||||
if "optimize" in info or "progressive" in info or "progression" in info:
|
||||
if optimize or progressive:
|
||||
# CMYK can be bigger
|
||||
if im.mode == 'CMYK':
|
||||
bufsize = 4 * im.size[0] * im.size[1]
|
||||
# keep sets quality to 0, but the actual value may be high.
|
||||
if quality >= 95 or quality == 0:
|
||||
elif quality >= 95 or quality == 0:
|
||||
bufsize = 2 * im.size[0] * im.size[1]
|
||||
else:
|
||||
bufsize = im.size[0] * im.size[1]
|
||||
|
||||
# The exif info needs to be written as one block, + APP1, + one spare byte.
|
||||
# Ensure that our buffer is big enough
|
||||
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5)
|
||||
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
||||
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5,
|
||||
len(extra) + 1)
|
||||
|
||||
ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize)
|
||||
|
||||
|
|
@ -739,15 +792,13 @@ def jpeg_factory(fp=None, filename=None):
|
|||
return im
|
||||
|
||||
|
||||
# -------------------------------------------------------------------q-
|
||||
# ---------------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
|
||||
Image.register_save(JpegImageFile.format, _save)
|
||||
|
||||
Image.register_extension(JpegImageFile.format, ".jfif")
|
||||
Image.register_extension(JpegImageFile.format, ".jpe")
|
||||
Image.register_extension(JpegImageFile.format, ".jpg")
|
||||
Image.register_extension(JpegImageFile.format, ".jpeg")
|
||||
Image.register_extensions(JpegImageFile.format,
|
||||
[".jfif", ".jpe", ".jpg", ".jpeg"])
|
||||
|
||||
Image.register_mime(JpegImageFile.format, "image/jpeg")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ for chroma information than for luma information.
|
|||
(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling)
|
||||
|
||||
Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and
|
||||
4:1:1 (or 4:2:0?).
|
||||
4:2:0.
|
||||
|
||||
You can get the subsampling of a JPEG with the
|
||||
`JpegImagePlugin.get_subsampling(im)` function.
|
||||
|
|
@ -62,12 +62,13 @@ The tables format between im.quantization and quantization in presets differ in
|
|||
You can convert the dict format to the preset format with the
|
||||
`JpegImagePlugin.convert_dict_qtables(dict_qtables)` function.
|
||||
|
||||
Libjpeg ref.: http://www.jpegcameras.com/libjpeg/libjpeg-3.html
|
||||
Libjpeg ref.:
|
||||
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
|
||||
|
||||
"""
|
||||
|
||||
presets = {
|
||||
'web_low': {'subsampling': 2, # "4:1:1"
|
||||
presets = { # noqa: E128
|
||||
'web_low': {'subsampling': 2, # "4:2:0"
|
||||
'quantization': [
|
||||
[20, 16, 25, 39, 50, 46, 62, 68,
|
||||
16, 18, 23, 38, 38, 53, 65, 68,
|
||||
|
|
@ -86,7 +87,7 @@ presets = {
|
|||
68, 68, 68, 68, 68, 68, 68, 68,
|
||||
68, 68, 68, 68, 68, 68, 68, 68]
|
||||
]},
|
||||
'web_medium': {'subsampling': 2, # "4:1:1"
|
||||
'web_medium': {'subsampling': 2, # "4:2:0"
|
||||
'quantization': [
|
||||
[16, 11, 11, 16, 23, 27, 31, 30,
|
||||
11, 12, 12, 15, 20, 23, 23, 30,
|
||||
|
|
@ -162,7 +163,7 @@ presets = {
|
|||
3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3]
|
||||
]},
|
||||
'low': {'subsampling': 2, # "4:1:1"
|
||||
'low': {'subsampling': 2, # "4:2:0"
|
||||
'quantization': [
|
||||
[18, 14, 14, 21, 30, 35, 34, 17,
|
||||
14, 16, 16, 19, 26, 23, 12, 12,
|
||||
|
|
@ -181,7 +182,7 @@ presets = {
|
|||
17, 12, 12, 12, 12, 12, 12, 12,
|
||||
17, 12, 12, 12, 12, 12, 12, 12]
|
||||
]},
|
||||
'medium': {'subsampling': 2, # "4:1:1"
|
||||
'medium': {'subsampling': 2, # "4:2:0"
|
||||
'quantization': [
|
||||
[12, 8, 8, 12, 17, 21, 24, 17,
|
||||
8, 9, 9, 11, 15, 19, 12, 12,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
#
|
||||
|
||||
import struct
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
|
|
@ -59,13 +59,14 @@ class McIdasImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("unsupported McIdas format")
|
||||
|
||||
self.mode = mode
|
||||
self.size = w[10], w[9]
|
||||
self._size = w[10], w[9]
|
||||
|
||||
offset = w[34] + w[15]
|
||||
stride = w[15] + w[10]*w[11]*w[14]
|
||||
|
||||
self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# registry
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, TiffImagePlugin
|
||||
from PIL.OleFileIO import MAGIC, OleFileIO
|
||||
from . import Image, TiffImagePlugin
|
||||
|
||||
import olefile
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ __version__ = "0.1"
|
|||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == MAGIC
|
||||
return prefix[:8] == olefile.MAGIC
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -38,6 +39,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
|||
|
||||
format = "MIC"
|
||||
format_description = "Microsoft Image Composer"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
|||
# to be a Microsoft Image Composer file
|
||||
|
||||
try:
|
||||
self.ole = OleFileIO(self.fp)
|
||||
self.ole = olefile.OleFileIO(self.fp)
|
||||
except IOError:
|
||||
raise SyntaxError("not an MIC file; invalid OLE file")
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
|||
raise SyntaxError("not an MIC file; no image entries")
|
||||
|
||||
self.__fp = self.fp
|
||||
self.frame = 0
|
||||
self.frame = None
|
||||
|
||||
if len(self.images) > 1:
|
||||
self.category = Image.CONTAINER
|
||||
|
|
@ -79,7 +81,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
|||
return len(self.images) > 1
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
try:
|
||||
filename = self.images[frame]
|
||||
except IndexError:
|
||||
|
|
@ -92,9 +95,18 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
|||
self.frame = frame
|
||||
|
||||
def tell(self):
|
||||
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from PIL._binary import i8
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ class MpegImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("not an MPEG file")
|
||||
|
||||
self.mode = "RGB"
|
||||
self.size = s.read(12), s.read(12)
|
||||
self._size = s.read(12), s.read(12)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -80,7 +80,6 @@ class MpegImageFile(ImageFile.ImageFile):
|
|||
|
||||
Image.register_open(MpegImageFile.format, MpegImageFile)
|
||||
|
||||
Image.register_extension(MpegImageFile.format, ".mpg")
|
||||
Image.register_extension(MpegImageFile.format, ".mpeg")
|
||||
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])
|
||||
|
||||
Image.register_mime(MpegImageFile.format, "video/mpeg")
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, JpegImagePlugin
|
||||
from . import Image, JpegImagePlugin
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
|
@ -39,6 +39,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
|
||||
format = "MPO"
|
||||
format_description = "MPO (CIPA DC-007)"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
self.fp.seek(0) # prep the fp in order to pass the JPEG test
|
||||
|
|
@ -71,21 +72,29 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
return self.__framecount > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if frame < 0 or frame >= self.__framecount:
|
||||
raise EOFError("no more images in MPO file")
|
||||
else:
|
||||
self.fp = self.__fp
|
||||
self.offset = self.__mpoffsets[frame]
|
||||
self.tile = [
|
||||
("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
|
||||
]
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.fp = self.__fp
|
||||
self.offset = self.__mpoffsets[frame]
|
||||
self.tile = [
|
||||
("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
|
||||
]
|
||||
self.__frame = frame
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
# -------------------------------------------------------------------q-
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
# Note that since MPO shares a factory with JPEG, we do not need to do a
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# MSP file handling
|
||||
#
|
||||
|
|
@ -9,15 +8,25 @@
|
|||
# History:
|
||||
# 95-09-05 fl Created
|
||||
# 97-01-03 fl Read/write MSP images
|
||||
# 17-02-21 es Fixed RLE interpretation
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1995-97.
|
||||
# Copyright (c) Eric Soroos 2017.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
# More info on this format: https://archive.org/details/gg243631
|
||||
# Page 313:
|
||||
# Figure 205. Windows Paint Version 1: "DanM" Format
|
||||
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
|
||||
#
|
||||
# See also: http://www.fileformat.info/format/mspaint/egff.htm
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16le as i16, o16le as o16, i8
|
||||
import struct
|
||||
import io
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
|
@ -25,8 +34,6 @@ __version__ = "0.1"
|
|||
#
|
||||
# read MSP files
|
||||
|
||||
i16 = _binary.i16le
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] in [b"DanM", b"LinS"]
|
||||
|
|
@ -56,18 +63,99 @@ class MspImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("bad MSP checksum")
|
||||
|
||||
self.mode = "1"
|
||||
self.size = i16(s[4:]), i16(s[6:])
|
||||
self._size = i16(s[4:]), i16(s[6:])
|
||||
|
||||
if s[:4] == b"DanM":
|
||||
self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))]
|
||||
else:
|
||||
self.tile = [("msp", (0, 0)+self.size, 32+2*self.size[1], None)]
|
||||
self.tile = [("MSP", (0, 0)+self.size, 32, None)]
|
||||
|
||||
|
||||
class MspDecoder(ImageFile.PyDecoder):
|
||||
# The algo for the MSP decoder is from
|
||||
# http://www.fileformat.info/format/mspaint/egff.htm
|
||||
# cc-by-attribution -- That page references is taken from the
|
||||
# Encyclopedia of Graphics File Formats and is licensed by
|
||||
# O'Reilly under the Creative Common/Attribution license
|
||||
#
|
||||
# For RLE encoded files, the 32byte header is followed by a scan
|
||||
# line map, encoded as one 16bit word of encoded byte length per
|
||||
# line.
|
||||
#
|
||||
# NOTE: the encoded length of the line can be 0. This was not
|
||||
# handled in the previous version of this encoder, and there's no
|
||||
# mention of how to handle it in the documentation. From the few
|
||||
# examples I've seen, I've assumed that it is a fill of the
|
||||
# background color, in this case, white.
|
||||
#
|
||||
#
|
||||
# Pseudocode of the decoder:
|
||||
# Read a BYTE value as the RunType
|
||||
# If the RunType value is zero
|
||||
# Read next byte as the RunCount
|
||||
# Read the next byte as the RunValue
|
||||
# Write the RunValue byte RunCount times
|
||||
# If the RunType value is non-zero
|
||||
# Use this value as the RunCount
|
||||
# Read and write the next RunCount bytes literally
|
||||
#
|
||||
# e.g.:
|
||||
# 0x00 03 ff 05 00 01 02 03 04
|
||||
# would yield the bytes:
|
||||
# 0xff ff ff 00 01 02 03 04
|
||||
#
|
||||
# which are then interpreted as a bit packed mode '1' image
|
||||
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
|
||||
img = io.BytesIO()
|
||||
blank_line = bytearray((0xff,)*((self.state.xsize+7)//8))
|
||||
try:
|
||||
self.fd.seek(32)
|
||||
rowmap = struct.unpack_from("<%dH" % (self.state.ysize),
|
||||
self.fd.read(self.state.ysize*2))
|
||||
except struct.error:
|
||||
raise IOError("Truncated MSP file in row map")
|
||||
|
||||
for x, rowlen in enumerate(rowmap):
|
||||
try:
|
||||
if rowlen == 0:
|
||||
img.write(blank_line)
|
||||
continue
|
||||
row = self.fd.read(rowlen)
|
||||
if len(row) != rowlen:
|
||||
raise IOError(
|
||||
"Truncated MSP file, expected %d bytes on row %s",
|
||||
(rowlen, x))
|
||||
idx = 0
|
||||
while idx < rowlen:
|
||||
runtype = i8(row[idx])
|
||||
idx += 1
|
||||
if runtype == 0:
|
||||
(runcount, runval) = struct.unpack_from("Bc", row, idx)
|
||||
img.write(runval * runcount)
|
||||
idx += 2
|
||||
else:
|
||||
runcount = runtype
|
||||
img.write(row[idx:idx+runcount])
|
||||
idx += runcount
|
||||
|
||||
except struct.error:
|
||||
raise IOError("Corrupted MSP file in row %d" % x)
|
||||
|
||||
self.set_as_raw(img.getvalue(), ("1", 0, 1))
|
||||
|
||||
return 0, 0
|
||||
|
||||
|
||||
Image.register_decoder('MSP', MspDecoder)
|
||||
|
||||
|
||||
#
|
||||
# write MSP files (uncompressed only)
|
||||
|
||||
o16 = _binary.o16le
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
|
||||
|
|
@ -95,6 +183,7 @@ def _save(im, fp, filename):
|
|||
# image body
|
||||
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))])
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
olefile (formerly OleFileIO_PL)
|
||||
===============================
|
||||
|
||||
[olefile](http://www.decalage.info/olefile) is a Python package to parse, read and write
|
||||
[Microsoft OLE2 files](http://en.wikipedia.org/wiki/Compound_File_Binary_Format)
|
||||
(also called Structured Storage, Compound File Binary Format or Compound Document File Format),
|
||||
such as Microsoft Office 97-2003 documents, vbaProject.bin in MS Office 2007+ files, Image Composer
|
||||
and FlashPix files, Outlook messages, StickyNotes, several Microscopy file formats, McAfee antivirus quarantine files,
|
||||
etc.
|
||||
|
||||
|
||||
**Quick links:** [Home page](http://www.decalage.info/olefile) -
|
||||
[Download/Install](https://bitbucket.org/decalage/olefileio_pl/wiki/Install) -
|
||||
[Documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) -
|
||||
[Report Issues/Suggestions/Questions](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open) -
|
||||
[Contact the author](http://decalage.info/contact) -
|
||||
[Repository](https://bitbucket.org/decalage/olefileio_pl) -
|
||||
[Updates on Twitter](https://twitter.com/decalage2)
|
||||
|
||||
|
||||
News
|
||||
----
|
||||
|
||||
Follow all updates and news on Twitter: <https://twitter.com/decalage2>
|
||||
|
||||
- **2015-01-25 v0.42**: improved handling of special characters in stream/storage names on Python 2.x (using UTF-8
|
||||
instead of Latin-1), fixed bug in listdir with empty storages.
|
||||
- 2014-11-25 v0.41: OleFileIO.open and isOleFile now support OLE files stored in byte strings, fixed installer for
|
||||
python 3, added support for Jython (Niko Ehrenfeuchter)
|
||||
- 2014-10-01 v0.40: renamed OleFileIO_PL to olefile, added initial write support for streams >4K, updated doc and
|
||||
license, improved the setup script.
|
||||
- 2014-07-27 v0.31: fixed support for large files with 4K sectors, thanks to Niko Ehrenfeuchter, Martijn Berger and
|
||||
Dave Jones. Added test scripts from Pillow (by hugovk). Fixed setup for Python 3 (Martin Panter)
|
||||
- 2014-02-04 v0.30: now compatible with Python 3.x, thanks to Martin Panter who did most of the hard work.
|
||||
- 2013-07-24 v0.26: added methods to parse stream/storage timestamps, improved listdir to include storages, fixed
|
||||
parsing of direntry timestamps
|
||||
- 2013-05-27 v0.25: improved metadata extraction, properties parsing and exception handling, fixed
|
||||
[issue #12](https://bitbucket.org/decalage/olefileio_pl/issue/12/error-when-converting-timestamps-in-ole)
|
||||
- 2013-05-07 v0.24: new features to extract metadata (get\_metadata method and OleMetadata class), improved
|
||||
getproperties to convert timestamps to Python datetime
|
||||
- 2012-10-09: published [python-oletools](http://www.decalage.info/python/oletools), a package of analysis tools based
|
||||
on OleFileIO_PL
|
||||
- 2012-09-11 v0.23: added support for file-like objects, fixed [issue #8](https://bitbucket.org/decalage/olefileio_pl/issue/8/bug-with-file-object)
|
||||
- 2012-02-17 v0.22: fixed issues #7 (bug in getproperties) and #2 (added close method)
|
||||
- 2011-10-20: code hosted on bitbucket to ease contributions and bug tracking
|
||||
- 2010-01-24 v0.21: fixed support for big-endian CPUs, such as PowerPC Macs.
|
||||
- 2009-12-11 v0.20: small bugfix in OleFileIO.open when filename is not plain str.
|
||||
- 2009-12-10 v0.19: fixed support for 64 bits platforms (thanks to Ben G. and Martijn for reporting the bug)
|
||||
- see changelog in source code for more info.
|
||||
|
||||
Download/Install
|
||||
----------------
|
||||
|
||||
If you have pip or setuptools installed (pip is included in Python 2.7.9+), you may simply run **pip install olefile**
|
||||
or **easy_install olefile** for the first installation.
|
||||
|
||||
To update olefile, run **pip install -U olefile**.
|
||||
|
||||
Otherwise, see https://bitbucket.org/decalage/olefileio_pl/wiki/Install
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Parse, read and write any OLE file such as Microsoft Office 97-2003 legacy document formats (Word .doc, Excel .xls,
|
||||
PowerPoint .ppt, Visio .vsd, Project .mpp), Image Composer and FlashPix files, Outlook messages, StickyNotes,
|
||||
Zeiss AxioVision ZVI files, Olympus FluoView OIB files, etc
|
||||
- List all the streams and storages contained in an OLE file
|
||||
- Open streams as files
|
||||
- Parse and read property streams, containing metadata of the file
|
||||
- Portable, pure Python module, no dependency
|
||||
|
||||
olefile can be used as an independent package or with PIL/Pillow.
|
||||
|
||||
olefile is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data (especially
|
||||
for security purposes such as malware analysis and forensics), then please also check my
|
||||
[python-oletools](http://www.decalage.info/python/oletools), which are built upon olefile and provide a higher-level interface.
|
||||
|
||||
|
||||
History
|
||||
-------
|
||||
|
||||
olefile is based on the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent
|
||||
Python Imaging Library, created and maintained by Fredrik Lundh. The olefile API is still compatible with PIL, but
|
||||
since 2005 I have improved the internal implementation significantly, with new features, bugfixes and a more robust
|
||||
design. From 2005 to 2014 the project was called OleFileIO_PL, and in 2014 I changed its name to olefile to celebrate
|
||||
its 9 years and its new write features.
|
||||
|
||||
As far as I know, olefile is the most complete and robust Python implementation to read MS OLE2 files, portable on
|
||||
several operating systems. (please tell me if you know other similar Python modules)
|
||||
|
||||
Since 2014 olefile/OleFileIO_PL has been integrated into [Pillow](http://python-imaging.github.io/), the friendly fork
|
||||
of PIL. olefile will continue to be improved as a separate project, and new versions will be merged into Pillow
|
||||
regularly.
|
||||
|
||||
|
||||
Main improvements over the original version of OleFileIO in PIL:
|
||||
----------------------------------------------------------------
|
||||
|
||||
- Compatible with Python 3.x and 2.6+
|
||||
- Many bug fixes
|
||||
- Support for files larger than 6.8MB
|
||||
- Support for 64 bits platforms and big-endian CPUs
|
||||
- Robust: many checks to detect malformed files
|
||||
- Runtime option to choose if malformed files should be parsed or raise exceptions
|
||||
- Improved API
|
||||
- Metadata extraction, stream/storage timestamps (e.g. for document forensics)
|
||||
- Can open file-like objects
|
||||
- Added setup.py and install.bat to ease installation
|
||||
- More convenient slash-based syntax for stream paths
|
||||
- Write features
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Please see the [online documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) for more information,
|
||||
especially the [OLE overview](https://bitbucket.org/decalage/olefileio_pl/wiki/OLE_Overview) and the
|
||||
[API page](https://bitbucket.org/decalage/olefileio_pl/wiki/API) which describe how to use olefile in Python applications.
|
||||
A copy of the same documentation is also provided in the doc subfolder of the olefile package.
|
||||
|
||||
|
||||
## Real-life examples ##
|
||||
|
||||
A real-life example: [using OleFileIO_PL for malware analysis and forensics](http://blog.gregback.net/2011/03/using-remnux-for-forensic-puzzle-6/).
|
||||
|
||||
See also [this paper](https://computer-forensics.sans.org/community/papers/gcfa/grow-forensic-tools-taxonomy-python-libraries-helpful-forensic-analysis_6879) about python tools for forensics, which features olefile.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec
|
||||
([http://www.decalage.info](http://www.decalage.info))
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
----------
|
||||
|
||||
olefile is based on source code from the OleFileIO module of the Python Imaging Library (PIL) published by Fredrik
|
||||
Lundh under the following license:
|
||||
|
||||
The Python Imaging Library (PIL) is
|
||||
|
||||
- Copyright (c) 1997-2005 by Secret Labs AB
|
||||
- Copyright (c) 1995-2005 by Fredrik Lundh
|
||||
|
||||
By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read,
|
||||
understood, and will comply with the following terms and conditions:
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and
|
||||
without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that
|
||||
copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or
|
||||
the author not be used in advertising or publicity pertaining to distribution of the software without specific, written
|
||||
prior permission.
|
||||
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
SOFTWARE.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import EpsImagePlugin
|
||||
from . import EpsImagePlugin
|
||||
from ._util import py3
|
||||
import sys
|
||||
|
||||
##
|
||||
|
|
@ -24,7 +25,7 @@ import sys
|
|||
|
||||
class PSDraw(object):
|
||||
"""
|
||||
Sets up printing to the given file. If **file** is omitted,
|
||||
Sets up printing to the given file. If **fp** is omitted,
|
||||
:py:attr:`sys.stdout` is assumed.
|
||||
"""
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ class PSDraw(object):
|
|||
self.fp = fp
|
||||
|
||||
def _fp_write(self, to_write):
|
||||
if bytes is str or self.fp == sys.stdout:
|
||||
if not py3 or self.fp == sys.stdout:
|
||||
self.fp.write(to_write)
|
||||
else:
|
||||
self.fp.write(bytes(to_write, 'UTF-8'))
|
||||
|
|
@ -153,6 +154,7 @@ class PSDraw(object):
|
|||
# Copyright (c) Fredrik Lundh 1994.
|
||||
#
|
||||
|
||||
|
||||
EDROFF_PS = """\
|
||||
/S { show } bind def
|
||||
/P { moveto show } bind def
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL._binary import o8
|
||||
from ._binary import o8
|
||||
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
# Image plugin for Palm pixmap images (output only).
|
||||
##
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import o8, o16be as o16b
|
||||
|
||||
__version__ = "1.0"
|
||||
|
||||
_Palm8BitColormapValues = (
|
||||
_Palm8BitColormapValues = ( # noqa: E131
|
||||
(255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255),
|
||||
(255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204),
|
||||
(255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204),
|
||||
|
|
@ -80,16 +81,16 @@ _Palm8BitColormapValues = (
|
|||
|
||||
# so build a prototype image to be used for palette resampling
|
||||
def build_prototype_image():
|
||||
image = Image.new("L", (1, len(_Palm8BitColormapValues),))
|
||||
image = Image.new("L", (1, len(_Palm8BitColormapValues)))
|
||||
image.putdata(list(range(len(_Palm8BitColormapValues))))
|
||||
palettedata = ()
|
||||
for i in range(len(_Palm8BitColormapValues)):
|
||||
palettedata = palettedata + _Palm8BitColormapValues[i]
|
||||
for i in range(256 - len(_Palm8BitColormapValues)):
|
||||
palettedata = palettedata + (0, 0, 0)
|
||||
for colormapValue in _Palm8BitColormapValues:
|
||||
palettedata += colormapValue
|
||||
palettedata += (0, 0, 0)*(256 - len(_Palm8BitColormapValues))
|
||||
image.putpalette(palettedata)
|
||||
return image
|
||||
|
||||
|
||||
Palm8BitColormapImage = build_prototype_image()
|
||||
|
||||
# OK, we now have in Palm8BitColormapImage,
|
||||
|
|
@ -109,9 +110,6 @@ _COMPRESSION_TYPES = {
|
|||
"scanline": 0x00,
|
||||
}
|
||||
|
||||
o8 = _binary.o8
|
||||
o16b = _binary.o16be
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -119,7 +117,7 @@ o16b = _binary.o16be
|
|||
##
|
||||
# (Internal) Image save plugin for the Palm format.
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
def _save(im, fp, filename):
|
||||
|
||||
if im.mode == "P":
|
||||
|
||||
|
|
@ -168,9 +166,6 @@ def _save(im, fp, filename, check=0):
|
|||
|
||||
raise IOError("cannot write mode %s as Palm" % im.mode)
|
||||
|
||||
if check:
|
||||
return check
|
||||
|
||||
#
|
||||
# make sure image data is available
|
||||
im.load()
|
||||
|
|
|
|||
|
|
@ -15,12 +15,11 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
i8 = _binary.i8
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for PhotoCD images. This plugin only reads the 768x512
|
||||
|
|
@ -42,15 +41,23 @@ class PcdImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError("not a PCD file")
|
||||
|
||||
orientation = i8(s[1538]) & 3
|
||||
self.tile_post_rotate = None
|
||||
if orientation == 1:
|
||||
self.tile_post_rotate = 90 # hack
|
||||
self.tile_post_rotate = 90
|
||||
elif orientation == 3:
|
||||
self.tile_post_rotate = -90
|
||||
|
||||
self.mode = "RGB"
|
||||
self.size = 768, 512 # FIXME: not correct for rotated images!
|
||||
self._size = 768, 512 # FIXME: not correct for rotated images!
|
||||
self.tile = [("pcd", (0, 0)+self.size, 96*2048, None)]
|
||||
|
||||
def load_end(self):
|
||||
if self.tile_post_rotate:
|
||||
# Handle rotated PCDs
|
||||
self.im = self.im.rotate(self.tile_post_rotate)
|
||||
self._size = self.im.size
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
from PIL import FontFile
|
||||
from PIL import _binary
|
||||
from . import Image, FontFile
|
||||
from ._binary import i8, i16le as l16, i32le as l32, i16be as b16, i32be as b32
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# declarations
|
||||
|
|
@ -42,12 +41,6 @@ BYTES_PER_ROW = [
|
|||
lambda bits: ((bits+63) >> 3) & ~7,
|
||||
]
|
||||
|
||||
i8 = _binary.i8
|
||||
l16 = _binary.i16le
|
||||
l32 = _binary.i32le
|
||||
b16 = _binary.i16be
|
||||
b32 = _binary.i32be
|
||||
|
||||
|
||||
def sz(s, o):
|
||||
return s[o:s.index(b"\0", o)]
|
||||
|
|
@ -237,7 +230,7 @@ class PcfFontFile(FontFile.FontFile):
|
|||
firstCol, lastCol = i16(fp.read(2)), i16(fp.read(2))
|
||||
firstRow, lastRow = i16(fp.read(2)), i16(fp.read(2))
|
||||
|
||||
default = i16(fp.read(2))
|
||||
i16(fp.read(2)) # default
|
||||
|
||||
nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,17 +25,12 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, o8, o16le as o16
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
o8 = _binary.o8
|
||||
|
||||
__version__ = "0.6"
|
||||
|
||||
|
||||
|
|
@ -105,7 +100,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
|||
raise IOError("unknown PCX mode")
|
||||
|
||||
self.mode = mode
|
||||
self.size = bbox[2]-bbox[0], bbox[3]-bbox[1]
|
||||
self._size = bbox[2]-bbox[0], bbox[3]-bbox[1]
|
||||
|
||||
bbox = (0, 0) + self.size
|
||||
logger.debug("size: %sx%s", *self.size)
|
||||
|
|
@ -115,6 +110,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
# save PCX files
|
||||
|
||||
|
||||
SAVE = {
|
||||
# mode: (version, bits, planes, raw mode)
|
||||
"1": (2, 1, 1, "1"),
|
||||
|
|
@ -123,19 +119,14 @@ SAVE = {
|
|||
"RGB": (5, 8, 3, "RGB;L"),
|
||||
}
|
||||
|
||||
o16 = _binary.o16le
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
def _save(im, fp, filename):
|
||||
|
||||
try:
|
||||
version, bits, planes, rawmode = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise ValueError("Cannot save %s images as PCX" % im.mode)
|
||||
|
||||
if check:
|
||||
return check
|
||||
|
||||
# bytes per plane
|
||||
stride = (im.size[0] * bits + 7) // 8
|
||||
# stride should be even
|
||||
|
|
@ -181,6 +172,7 @@ def _save(im, fp, filename, check=0):
|
|||
# --------------------------------------------------------------------
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(PcxImageFile.format, PcxImageFile, _accept)
|
||||
Image.register_save(PcxImageFile.format, _save)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@
|
|||
# Image plugin for PDF images (output only).
|
||||
##
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from PIL._binary import i8
|
||||
from . import Image, ImageFile, ImageSequence, PdfParser
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
|
||||
__version__ = "0.4"
|
||||
__version__ = "0.5"
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -37,19 +38,6 @@ __version__ = "0.4"
|
|||
# 4. page
|
||||
# 5. page contents
|
||||
|
||||
def _obj(fp, obj, **dict):
|
||||
fp.write("%d 0 obj\n" % obj)
|
||||
if dict:
|
||||
fp.write("<<\n")
|
||||
for k, v in dict.items():
|
||||
if v is not None:
|
||||
fp.write("/%s %s\n" % (k, v))
|
||||
fp.write(">>\n")
|
||||
|
||||
|
||||
def _endobj(fp):
|
||||
fp.write("endobj\n")
|
||||
|
||||
|
||||
def _save_all(im, fp, filename):
|
||||
_save(im, fp, filename, save_all=True)
|
||||
|
|
@ -59,197 +47,197 @@ def _save_all(im, fp, filename):
|
|||
# (Internal) Image save plugin for the PDF format.
|
||||
|
||||
def _save(im, fp, filename, save_all=False):
|
||||
is_appending = im.encoderinfo.get("append", False)
|
||||
if is_appending:
|
||||
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b")
|
||||
else:
|
||||
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b")
|
||||
|
||||
resolution = im.encoderinfo.get("resolution", 72.0)
|
||||
|
||||
info = {
|
||||
"title": None if is_appending else os.path.splitext(
|
||||
os.path.basename(filename)
|
||||
)[0],
|
||||
"author": None,
|
||||
"subject": None,
|
||||
"keywords": None,
|
||||
"creator": None,
|
||||
"producer": None,
|
||||
"creationDate": None if is_appending else time.gmtime(),
|
||||
"modDate": None if is_appending else time.gmtime()
|
||||
}
|
||||
for k, default in info.items():
|
||||
v = im.encoderinfo.get(k) if k in im.encoderinfo else default
|
||||
if v:
|
||||
existing_pdf.info[k[0].upper() + k[1:]] = v
|
||||
|
||||
#
|
||||
# make sure image data is available
|
||||
im.load()
|
||||
|
||||
xref = [0]
|
||||
|
||||
class TextWriter(object):
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.fp, name)
|
||||
|
||||
def write(self, value):
|
||||
self.fp.write(value.encode('latin-1'))
|
||||
|
||||
fp = TextWriter(fp)
|
||||
|
||||
fp.write("%PDF-1.2\n")
|
||||
fp.write("% created by PIL PDF driver " + __version__ + "\n")
|
||||
|
||||
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits)
|
||||
# or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports
|
||||
# Flatedecode (zip compression).
|
||||
|
||||
bits = 8
|
||||
params = None
|
||||
|
||||
if im.mode == "1":
|
||||
filter = "/ASCIIHexDecode"
|
||||
colorspace = "/DeviceGray"
|
||||
procset = "/ImageB" # grayscale
|
||||
bits = 1
|
||||
elif im.mode == "L":
|
||||
filter = "/DCTDecode"
|
||||
# params = "<< /Predictor 15 /Columns %d >>" % (width-2)
|
||||
colorspace = "/DeviceGray"
|
||||
procset = "/ImageB" # grayscale
|
||||
elif im.mode == "P":
|
||||
filter = "/ASCIIHexDecode"
|
||||
colorspace = "[ /Indexed /DeviceRGB 255 <"
|
||||
palette = im.im.getpalette("RGB")
|
||||
for i in range(256):
|
||||
r = i8(palette[i*3])
|
||||
g = i8(palette[i*3+1])
|
||||
b = i8(palette[i*3+2])
|
||||
colorspace += "%02x%02x%02x " % (r, g, b)
|
||||
colorspace += "> ]"
|
||||
procset = "/ImageI" # indexed color
|
||||
elif im.mode == "RGB":
|
||||
filter = "/DCTDecode"
|
||||
colorspace = "/DeviceRGB"
|
||||
procset = "/ImageC" # color images
|
||||
elif im.mode == "CMYK":
|
||||
filter = "/DCTDecode"
|
||||
colorspace = "/DeviceCMYK"
|
||||
procset = "/ImageC" # color images
|
||||
else:
|
||||
raise ValueError("cannot save mode %s" % im.mode)
|
||||
|
||||
#
|
||||
# catalogue
|
||||
|
||||
xref.append(fp.tell())
|
||||
_obj(
|
||||
fp, 1,
|
||||
Type="/Catalog",
|
||||
Pages="2 0 R")
|
||||
_endobj(fp)
|
||||
existing_pdf.start_writing()
|
||||
existing_pdf.write_header()
|
||||
existing_pdf.write_comment("created by PIL PDF driver " + __version__)
|
||||
|
||||
#
|
||||
# pages
|
||||
numberOfPages = 1
|
||||
ims = [im]
|
||||
if save_all:
|
||||
try:
|
||||
numberOfPages = im.n_frames
|
||||
except AttributeError:
|
||||
# Image format does not have n_frames. It is a single frame image
|
||||
pass
|
||||
pages = [str(pageNumber*3+4)+" 0 R"
|
||||
for pageNumber in range(0, numberOfPages)]
|
||||
append_images = im.encoderinfo.get("append_images", [])
|
||||
for append_im in append_images:
|
||||
append_im.encoderinfo = im.encoderinfo.copy()
|
||||
ims.append(append_im)
|
||||
numberOfPages = 0
|
||||
image_refs = []
|
||||
page_refs = []
|
||||
contents_refs = []
|
||||
for im in ims:
|
||||
im_numberOfPages = 1
|
||||
if save_all:
|
||||
try:
|
||||
im_numberOfPages = im.n_frames
|
||||
except AttributeError:
|
||||
# Image format does not have n_frames.
|
||||
# It is a single frame image
|
||||
pass
|
||||
numberOfPages += im_numberOfPages
|
||||
for i in range(im_numberOfPages):
|
||||
image_refs.append(existing_pdf.next_object_id(0))
|
||||
page_refs.append(existing_pdf.next_object_id(0))
|
||||
contents_refs.append(existing_pdf.next_object_id(0))
|
||||
existing_pdf.pages.append(page_refs[-1])
|
||||
|
||||
xref.append(fp.tell())
|
||||
_obj(
|
||||
fp, 2,
|
||||
Type="/Pages",
|
||||
Count=len(pages),
|
||||
Kids="["+"\n".join(pages)+"]")
|
||||
_endobj(fp)
|
||||
#
|
||||
# catalog and list of pages
|
||||
existing_pdf.write_catalog()
|
||||
|
||||
for pageNumber in range(0, numberOfPages):
|
||||
im.seek(pageNumber)
|
||||
pageNumber = 0
|
||||
for imSequence in ims:
|
||||
im_pages = ImageSequence.Iterator(imSequence) if save_all else [imSequence]
|
||||
for im in im_pages:
|
||||
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode
|
||||
# (packbits) or LZWDecode (tiff/lzw compression). Note that
|
||||
# PDF 1.2 also supports Flatedecode (zip compression).
|
||||
|
||||
#
|
||||
# image
|
||||
bits = 8
|
||||
params = None
|
||||
|
||||
op = io.BytesIO()
|
||||
if im.mode == "1":
|
||||
filter = "ASCIIHexDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
bits = 1
|
||||
elif im.mode == "L":
|
||||
filter = "DCTDecode"
|
||||
# params = "<< /Predictor 15 /Columns %d >>" % (width-2)
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
elif im.mode == "P":
|
||||
filter = "ASCIIHexDecode"
|
||||
palette = im.im.getpalette("RGB")
|
||||
colorspace = [
|
||||
PdfParser.PdfName("Indexed"),
|
||||
PdfParser.PdfName("DeviceRGB"),
|
||||
255,
|
||||
PdfParser.PdfBinary(palette)
|
||||
]
|
||||
procset = "ImageI" # indexed color
|
||||
elif im.mode == "RGB":
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceRGB")
|
||||
procset = "ImageC" # color images
|
||||
elif im.mode == "CMYK":
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceCMYK")
|
||||
procset = "ImageC" # color images
|
||||
else:
|
||||
raise ValueError("cannot save mode %s" % im.mode)
|
||||
|
||||
if filter == "/ASCIIHexDecode":
|
||||
if bits == 1:
|
||||
# FIXME: the hex encoder doesn't support packed 1-bit
|
||||
# images; do things the hard way...
|
||||
data = im.tobytes("raw", "1")
|
||||
im = Image.new("L", (len(data), 1), None)
|
||||
im.putdata(data)
|
||||
ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
|
||||
elif filter == "/DCTDecode":
|
||||
Image.SAVE["JPEG"](im, op, filename)
|
||||
elif filter == "/FlateDecode":
|
||||
ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)])
|
||||
elif filter == "/RunLengthDecode":
|
||||
ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)])
|
||||
else:
|
||||
raise ValueError("unsupported PDF filter (%s)" % filter)
|
||||
#
|
||||
# image
|
||||
|
||||
#
|
||||
# Get image characteristics
|
||||
op = io.BytesIO()
|
||||
|
||||
width, height = im.size
|
||||
if filter == "ASCIIHexDecode":
|
||||
if bits == 1:
|
||||
# FIXME: the hex encoder doesn't support packed 1-bit
|
||||
# images; do things the hard way...
|
||||
data = im.tobytes("raw", "1")
|
||||
im = Image.new("L", (len(data), 1), None)
|
||||
im.putdata(data)
|
||||
ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
|
||||
elif filter == "DCTDecode":
|
||||
Image.SAVE["JPEG"](im, op, filename)
|
||||
elif filter == "FlateDecode":
|
||||
ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)])
|
||||
elif filter == "RunLengthDecode":
|
||||
ImageFile._save(im, op,
|
||||
[("packbits", (0, 0)+im.size, 0, im.mode)])
|
||||
else:
|
||||
raise ValueError("unsupported PDF filter (%s)" % filter)
|
||||
|
||||
xref.append(fp.tell())
|
||||
_obj(
|
||||
fp, pageNumber*3+3,
|
||||
Type="/XObject",
|
||||
Subtype="/Image",
|
||||
Width=width, # * 72.0 / resolution,
|
||||
Height=height, # * 72.0 / resolution,
|
||||
Length=len(op.getvalue()),
|
||||
Filter=filter,
|
||||
BitsPerComponent=bits,
|
||||
DecodeParams=params,
|
||||
ColorSpace=colorspace)
|
||||
#
|
||||
# Get image characteristics
|
||||
|
||||
fp.write("stream\n")
|
||||
fp.fp.write(op.getvalue())
|
||||
fp.write("\nendstream\n")
|
||||
width, height = im.size
|
||||
|
||||
_endobj(fp)
|
||||
existing_pdf.write_obj(image_refs[pageNumber],
|
||||
stream=op.getvalue(),
|
||||
Type=PdfParser.PdfName("XObject"),
|
||||
Subtype=PdfParser.PdfName("Image"),
|
||||
Width=width, # * 72.0 / resolution,
|
||||
Height=height, # * 72.0 / resolution,
|
||||
Filter=PdfParser.PdfName(filter),
|
||||
BitsPerComponent=bits,
|
||||
DecodeParams=params,
|
||||
ColorSpace=colorspace)
|
||||
|
||||
#
|
||||
# page
|
||||
#
|
||||
# page
|
||||
|
||||
xref.append(fp.tell())
|
||||
_obj(fp, pageNumber*3+4)
|
||||
fp.write(
|
||||
"<<\n/Type /Page\n/Parent 2 0 R\n"
|
||||
"/Resources <<\n/ProcSet [ /PDF %s ]\n"
|
||||
"/XObject << /image %d 0 R >>\n>>\n"
|
||||
"/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % (
|
||||
procset,
|
||||
pageNumber*3+3,
|
||||
int(width * 72.0 / resolution),
|
||||
int(height * 72.0 / resolution),
|
||||
pageNumber*3+5))
|
||||
_endobj(fp)
|
||||
existing_pdf.write_page(page_refs[pageNumber],
|
||||
Resources=PdfParser.PdfDict(
|
||||
ProcSet=[
|
||||
PdfParser.PdfName("PDF"),
|
||||
PdfParser.PdfName(procset)
|
||||
],
|
||||
XObject=PdfParser.PdfDict(
|
||||
image=image_refs[pageNumber]
|
||||
)
|
||||
),
|
||||
MediaBox=[
|
||||
0,
|
||||
0,
|
||||
int(width * 72.0 / resolution),
|
||||
int(height * 72.0 / resolution)
|
||||
],
|
||||
Contents=contents_refs[pageNumber])
|
||||
|
||||
#
|
||||
# page contents
|
||||
#
|
||||
# page contents
|
||||
|
||||
op = TextWriter(io.BytesIO())
|
||||
page_contents = PdfParser.make_bytes(
|
||||
"q %d 0 0 %d 0 0 cm /image Do Q\n" % (
|
||||
int(width * 72.0 / resolution),
|
||||
int(height * 72.0 / resolution)))
|
||||
|
||||
op.write(
|
||||
"q %d 0 0 %d 0 0 cm /image Do Q\n" % (
|
||||
int(width * 72.0 / resolution),
|
||||
int(height * 72.0 / resolution)))
|
||||
existing_pdf.write_obj(contents_refs[pageNumber],
|
||||
stream=page_contents)
|
||||
|
||||
xref.append(fp.tell())
|
||||
_obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue()))
|
||||
|
||||
fp.write("stream\n")
|
||||
fp.fp.write(op.fp.getvalue())
|
||||
fp.write("\nendstream\n")
|
||||
|
||||
_endobj(fp)
|
||||
pageNumber += 1
|
||||
|
||||
#
|
||||
# trailer
|
||||
startxref = fp.tell()
|
||||
fp.write("xref\n0 %d\n0000000000 65535 f \n" % len(xref))
|
||||
for x in xref[1:]:
|
||||
fp.write("%010d 00000 n \n" % x)
|
||||
fp.write("trailer\n<<\n/Size %d\n/Root 1 0 R\n>>\n" % len(xref))
|
||||
fp.write("startxref\n%d\n%%%%EOF\n" % startxref)
|
||||
existing_pdf.write_xref_and_trailer()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
existing_pdf.close()
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_save("PDF", _save)
|
||||
Image.register_save_all("PDF", _save_all)
|
||||
|
||||
|
|
|
|||
976
Lib/site-packages/PIL/PdfParser.py
Normal file
976
Lib/site-packages/PIL/PdfParser.py
Normal file
|
|
@ -0,0 +1,976 @@
|
|||
import calendar
|
||||
import codecs
|
||||
import collections
|
||||
import mmap
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import zlib
|
||||
from ._util import py3
|
||||
|
||||
try:
|
||||
from UserDict import UserDict # Python 2.x
|
||||
except ImportError:
|
||||
UserDict = collections.UserDict # Python 3.x
|
||||
|
||||
|
||||
if py3: # Python 3.x
|
||||
def make_bytes(s):
|
||||
return s.encode("us-ascii")
|
||||
else: # Python 2.x
|
||||
def make_bytes(s): # pragma: no cover
|
||||
return s # pragma: no cover
|
||||
|
||||
|
||||
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
||||
# on page 656
|
||||
def encode_text(s):
|
||||
return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
|
||||
|
||||
|
||||
PDFDocEncoding = {
|
||||
0x16: u"\u0017",
|
||||
0x18: u"\u02D8",
|
||||
0x19: u"\u02C7",
|
||||
0x1A: u"\u02C6",
|
||||
0x1B: u"\u02D9",
|
||||
0x1C: u"\u02DD",
|
||||
0x1D: u"\u02DB",
|
||||
0x1E: u"\u02DA",
|
||||
0x1F: u"\u02DC",
|
||||
0x80: u"\u2022",
|
||||
0x81: u"\u2020",
|
||||
0x82: u"\u2021",
|
||||
0x83: u"\u2026",
|
||||
0x84: u"\u2014",
|
||||
0x85: u"\u2013",
|
||||
0x86: u"\u0192",
|
||||
0x87: u"\u2044",
|
||||
0x88: u"\u2039",
|
||||
0x89: u"\u203A",
|
||||
0x8A: u"\u2212",
|
||||
0x8B: u"\u2030",
|
||||
0x8C: u"\u201E",
|
||||
0x8D: u"\u201C",
|
||||
0x8E: u"\u201D",
|
||||
0x8F: u"\u2018",
|
||||
0x90: u"\u2019",
|
||||
0x91: u"\u201A",
|
||||
0x92: u"\u2122",
|
||||
0x93: u"\uFB01",
|
||||
0x94: u"\uFB02",
|
||||
0x95: u"\u0141",
|
||||
0x96: u"\u0152",
|
||||
0x97: u"\u0160",
|
||||
0x98: u"\u0178",
|
||||
0x99: u"\u017D",
|
||||
0x9A: u"\u0131",
|
||||
0x9B: u"\u0142",
|
||||
0x9C: u"\u0153",
|
||||
0x9D: u"\u0161",
|
||||
0x9E: u"\u017E",
|
||||
0xA0: u"\u20AC",
|
||||
}
|
||||
|
||||
|
||||
def decode_text(b):
|
||||
if b[:len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
|
||||
return b[len(codecs.BOM_UTF16_BE):].decode("utf_16_be")
|
||||
elif py3: # Python 3.x
|
||||
return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b)
|
||||
else: # Python 2.x
|
||||
return u"".join(PDFDocEncoding.get(ord(byte), byte) for byte in b)
|
||||
|
||||
|
||||
class PdfFormatError(RuntimeError):
|
||||
"""An error that probably indicates a syntactic or semantic error in the
|
||||
PDF file structure"""
|
||||
pass
|
||||
|
||||
|
||||
def check_format_condition(condition, error_message):
|
||||
if not condition:
|
||||
raise PdfFormatError(error_message)
|
||||
|
||||
|
||||
class IndirectReference(collections.namedtuple("IndirectReferenceTuple",
|
||||
["object_id", "generation"])):
|
||||
def __str__(self):
|
||||
return "%s %s R" % self
|
||||
|
||||
def __bytes__(self):
|
||||
return self.__str__().encode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return other.__class__ is self.__class__ and \
|
||||
other.object_id == self.object_id and \
|
||||
other.generation == self.generation
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.object_id, self.generation))
|
||||
|
||||
|
||||
class IndirectObjectDef(IndirectReference):
|
||||
def __str__(self):
|
||||
return "%s %s obj" % self
|
||||
|
||||
|
||||
class XrefTable:
|
||||
def __init__(self):
|
||||
self.existing_entries = {} # object ID => (offset, generation)
|
||||
self.new_entries = {} # object ID => (offset, generation)
|
||||
self.deleted_entries = {0: 65536} # object ID => generation
|
||||
self.reading_finished = False
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if self.reading_finished:
|
||||
self.new_entries[key] = value
|
||||
else:
|
||||
self.existing_entries[key] = value
|
||||
if key in self.deleted_entries:
|
||||
del self.deleted_entries[key]
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self.new_entries[key]
|
||||
except KeyError:
|
||||
return self.existing_entries[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key in self.new_entries:
|
||||
generation = self.new_entries[key][1] + 1
|
||||
del self.new_entries[key]
|
||||
self.deleted_entries[key] = generation
|
||||
elif key in self.existing_entries:
|
||||
generation = self.existing_entries[key][1] + 1
|
||||
self.deleted_entries[key] = generation
|
||||
elif key in self.deleted_entries:
|
||||
generation = self.deleted_entries[key]
|
||||
else:
|
||||
raise IndexError("object ID " + str(key) +
|
||||
" cannot be deleted because it doesn't exist")
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.existing_entries or key in self.new_entries
|
||||
|
||||
def __len__(self):
|
||||
return len(set(self.existing_entries.keys()) |
|
||||
set(self.new_entries.keys()) |
|
||||
set(self.deleted_entries.keys()))
|
||||
|
||||
def keys(self):
|
||||
return (
|
||||
set(self.existing_entries.keys()) -
|
||||
set(self.deleted_entries.keys())
|
||||
) | set(self.new_entries.keys())
|
||||
|
||||
def write(self, f):
|
||||
keys = sorted(set(self.new_entries.keys()) |
|
||||
set(self.deleted_entries.keys()))
|
||||
deleted_keys = sorted(set(self.deleted_entries.keys()))
|
||||
startxref = f.tell()
|
||||
f.write(b"xref\n")
|
||||
while keys:
|
||||
# find a contiguous sequence of object IDs
|
||||
prev = None
|
||||
for index, key in enumerate(keys):
|
||||
if prev is None or prev+1 == key:
|
||||
prev = key
|
||||
else:
|
||||
contiguous_keys = keys[:index]
|
||||
keys = keys[index:]
|
||||
break
|
||||
else:
|
||||
contiguous_keys = keys
|
||||
keys = None
|
||||
f.write(make_bytes("%d %d\n" %
|
||||
(contiguous_keys[0], len(contiguous_keys))))
|
||||
for object_id in contiguous_keys:
|
||||
if object_id in self.new_entries:
|
||||
f.write(make_bytes("%010d %05d n \n" %
|
||||
self.new_entries[object_id]))
|
||||
else:
|
||||
this_deleted_object_id = deleted_keys.pop(0)
|
||||
check_format_condition(object_id == this_deleted_object_id,
|
||||
"expected the next deleted object "
|
||||
"ID to be %s, instead found %s" %
|
||||
(object_id, this_deleted_object_id))
|
||||
try:
|
||||
next_in_linked_list = deleted_keys[0]
|
||||
except IndexError:
|
||||
next_in_linked_list = 0
|
||||
f.write(make_bytes("%010d %05d f \n" %
|
||||
(next_in_linked_list,
|
||||
self.deleted_entries[object_id])))
|
||||
return startxref
|
||||
|
||||
|
||||
class PdfName:
|
||||
def __init__(self, name):
|
||||
if isinstance(name, PdfName):
|
||||
self.name = name.name
|
||||
elif isinstance(name, bytes):
|
||||
self.name = name
|
||||
else:
|
||||
self.name = name.encode("us-ascii")
|
||||
|
||||
def name_as_str(self):
|
||||
return self.name.decode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, PdfName) and other.name == self.name) or \
|
||||
other == self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "PdfName(%s)" % repr(self.name)
|
||||
|
||||
@classmethod
|
||||
def from_pdf_stream(cls, data):
|
||||
return cls(PdfParser.interpret_name(data))
|
||||
|
||||
allowed_chars = set(range(33, 127)) - set(ord(c) for c in "#%/()<>[]{}")
|
||||
|
||||
def __bytes__(self):
|
||||
result = bytearray(b"/")
|
||||
for b in self.name:
|
||||
if py3: # Python 3.x
|
||||
if b in self.allowed_chars:
|
||||
result.append(b)
|
||||
else:
|
||||
result.extend(make_bytes("#%02X" % b))
|
||||
else: # Python 2.x
|
||||
if ord(b) in self.allowed_chars:
|
||||
result.append(b)
|
||||
else:
|
||||
result.extend(b"#%02X" % ord(b))
|
||||
return bytes(result)
|
||||
|
||||
__str__ = __bytes__
|
||||
|
||||
|
||||
class PdfArray(list):
|
||||
def __bytes__(self):
|
||||
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
||||
|
||||
__str__ = __bytes__
|
||||
|
||||
|
||||
class PdfDict(UserDict):
|
||||
def __setattr__(self, key, value):
|
||||
if key == "data":
|
||||
if hasattr(UserDict, "__setattr__"):
|
||||
UserDict.__setattr__(self, key, value)
|
||||
else:
|
||||
self.__dict__[key] = value
|
||||
else:
|
||||
if isinstance(key, str):
|
||||
key = key.encode("us-ascii")
|
||||
self[key] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
value = self[key]
|
||||
except KeyError:
|
||||
try:
|
||||
value = self[key.encode("us-ascii")]
|
||||
except KeyError:
|
||||
raise AttributeError(key)
|
||||
if isinstance(value, bytes):
|
||||
value = decode_text(value)
|
||||
if key.endswith("Date"):
|
||||
if value.startswith("D:"):
|
||||
value = value[2:]
|
||||
|
||||
relationship = 'Z'
|
||||
if len(value) > 17:
|
||||
relationship = value[14]
|
||||
offset = int(value[15:17]) * 60
|
||||
if len(value) > 20:
|
||||
offset += int(value[18:20])
|
||||
|
||||
format = '%Y%m%d%H%M%S'[:len(value) - 2]
|
||||
value = time.strptime(value[:len(format)+2], format)
|
||||
if relationship in ['+', '-']:
|
||||
offset *= 60
|
||||
if relationship == '+':
|
||||
offset *= -1
|
||||
value = time.gmtime(calendar.timegm(value) + offset)
|
||||
return value
|
||||
|
||||
def __bytes__(self):
|
||||
out = bytearray(b"<<")
|
||||
for key, value in self.items():
|
||||
if value is None:
|
||||
continue
|
||||
value = pdf_repr(value)
|
||||
out.extend(b"\n")
|
||||
out.extend(bytes(PdfName(key)))
|
||||
out.extend(b" ")
|
||||
out.extend(value)
|
||||
out.extend(b"\n>>")
|
||||
return bytes(out)
|
||||
|
||||
if not py3:
|
||||
__str__ = __bytes__
|
||||
|
||||
|
||||
class PdfBinary:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
if py3: # Python 3.x
|
||||
def __bytes__(self):
|
||||
return make_bytes("<%s>" % "".join("%02X" % b for b in self.data))
|
||||
else: # Python 2.x
|
||||
def __str__(self):
|
||||
return "<%s>" % "".join("%02X" % ord(b) for b in self.data)
|
||||
|
||||
|
||||
class PdfStream:
|
||||
def __init__(self, dictionary, buf):
|
||||
self.dictionary = dictionary
|
||||
self.buf = buf
|
||||
|
||||
def decode(self):
|
||||
try:
|
||||
filter = self.dictionary.Filter
|
||||
except AttributeError:
|
||||
return self.buf
|
||||
if filter == b"FlateDecode":
|
||||
try:
|
||||
expected_length = self.dictionary.DL
|
||||
except AttributeError:
|
||||
expected_length = self.dictionary.Length
|
||||
return zlib.decompress(self.buf, bufsize=int(expected_length))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"stream filter %s unknown/unsupported" %
|
||||
repr(self.dictionary.Filter))
|
||||
|
||||
|
||||
def pdf_repr(x):
|
||||
if x is True:
|
||||
return b"true"
|
||||
elif x is False:
|
||||
return b"false"
|
||||
elif x is None:
|
||||
return b"null"
|
||||
elif (isinstance(x, PdfName) or isinstance(x, PdfDict) or
|
||||
isinstance(x, PdfArray) or isinstance(x, PdfBinary)):
|
||||
return bytes(x)
|
||||
elif isinstance(x, int):
|
||||
return str(x).encode("us-ascii")
|
||||
elif isinstance(x, time.struct_time):
|
||||
return b'(D:'+time.strftime('%Y%m%d%H%M%SZ', x).encode("us-ascii")+b')'
|
||||
elif isinstance(x, dict):
|
||||
return bytes(PdfDict(x))
|
||||
elif isinstance(x, list):
|
||||
return bytes(PdfArray(x))
|
||||
elif ((py3 and isinstance(x, str)) or
|
||||
(not py3 and isinstance(x, unicode))): # noqa: F821
|
||||
return pdf_repr(encode_text(x))
|
||||
elif isinstance(x, bytes):
|
||||
# XXX escape more chars? handle binary garbage
|
||||
x = x.replace(b"\\", b"\\\\")
|
||||
x = x.replace(b"(", b"\\(")
|
||||
x = x.replace(b")", b"\\)")
|
||||
return b"(" + x + b")"
|
||||
else:
|
||||
return bytes(x)
|
||||
|
||||
|
||||
class PdfParser:
|
||||
"""Based on
|
||||
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
|
||||
Supports PDF up to 1.4
|
||||
"""
|
||||
|
||||
def __init__(self, filename=None, f=None,
|
||||
buf=None, start_offset=0, mode="rb"):
|
||||
# type: (PdfParser, str, file, Union[bytes, bytearray], int, str)
|
||||
# -> None
|
||||
if buf and f:
|
||||
raise RuntimeError(
|
||||
"specify buf or f or filename, but not both buf and f")
|
||||
self.filename = filename
|
||||
self.buf = buf
|
||||
self.f = f
|
||||
self.start_offset = start_offset
|
||||
self.should_close_buf = False
|
||||
self.should_close_file = False
|
||||
if filename is not None and f is None:
|
||||
self.f = f = open(filename, mode)
|
||||
self.should_close_file = True
|
||||
if f is not None:
|
||||
self.buf = buf = self.get_buf_from_file(f)
|
||||
self.should_close_buf = True
|
||||
if not filename and hasattr(f, "name"):
|
||||
self.filename = f.name
|
||||
self.cached_objects = {}
|
||||
if buf:
|
||||
self.read_pdf_info()
|
||||
else:
|
||||
self.file_size_total = self.file_size_this = 0
|
||||
self.root = PdfDict()
|
||||
self.root_ref = None
|
||||
self.info = PdfDict()
|
||||
self.info_ref = None
|
||||
self.page_tree_root = {}
|
||||
self.pages = []
|
||||
self.orig_pages = []
|
||||
self.pages_ref = None
|
||||
self.last_xref_section_offset = None
|
||||
self.trailer_dict = {}
|
||||
self.xref_table = XrefTable()
|
||||
self.xref_table.reading_finished = True
|
||||
if f:
|
||||
self.seek_end()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
return False # do not suppress exceptions
|
||||
|
||||
def start_writing(self):
|
||||
self.close_buf()
|
||||
self.seek_end()
|
||||
|
||||
def close_buf(self):
|
||||
try:
|
||||
self.buf.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
self.buf = None
|
||||
|
||||
def close(self):
|
||||
if self.should_close_buf:
|
||||
self.close_buf()
|
||||
if self.f is not None and self.should_close_file:
|
||||
self.f.close()
|
||||
self.f = None
|
||||
|
||||
def seek_end(self):
|
||||
self.f.seek(0, os.SEEK_END)
|
||||
|
||||
def write_header(self):
|
||||
self.f.write(b"%PDF-1.4\n")
|
||||
|
||||
def write_comment(self, s):
|
||||
self.f.write(("%% %s\n" % (s,)).encode("utf-8"))
|
||||
|
||||
def write_catalog(self):
|
||||
self.del_root()
|
||||
self.root_ref = self.next_object_id(self.f.tell())
|
||||
self.pages_ref = self.next_object_id(0)
|
||||
self.rewrite_pages()
|
||||
self.write_obj(self.root_ref,
|
||||
Type=PdfName(b"Catalog"),
|
||||
Pages=self.pages_ref)
|
||||
self.write_obj(self.pages_ref,
|
||||
Type=PdfName(b"Pages"),
|
||||
Count=len(self.pages),
|
||||
Kids=self.pages)
|
||||
return self.root_ref
|
||||
|
||||
def rewrite_pages(self):
|
||||
pages_tree_nodes_to_delete = []
|
||||
for i, page_ref in enumerate(self.orig_pages):
|
||||
page_info = self.cached_objects[page_ref]
|
||||
del self.xref_table[page_ref.object_id]
|
||||
pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")])
|
||||
if page_ref not in self.pages:
|
||||
# the page has been deleted
|
||||
continue
|
||||
# make dict keys into strings for passing to write_page
|
||||
stringified_page_info = {}
|
||||
for key, value in page_info.items():
|
||||
# key should be a PdfName
|
||||
stringified_page_info[key.name_as_str()] = value
|
||||
stringified_page_info["Parent"] = self.pages_ref
|
||||
new_page_ref = self.write_page(None, **stringified_page_info)
|
||||
for j, cur_page_ref in enumerate(self.pages):
|
||||
if cur_page_ref == page_ref:
|
||||
# replace the page reference with the new one
|
||||
self.pages[j] = new_page_ref
|
||||
# delete redundant Pages tree nodes from xref table
|
||||
for pages_tree_node_ref in pages_tree_nodes_to_delete:
|
||||
while pages_tree_node_ref:
|
||||
pages_tree_node = self.cached_objects[pages_tree_node_ref]
|
||||
if pages_tree_node_ref.object_id in self.xref_table:
|
||||
del self.xref_table[pages_tree_node_ref.object_id]
|
||||
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
|
||||
self.orig_pages = []
|
||||
|
||||
def write_xref_and_trailer(self, new_root_ref=None):
|
||||
if new_root_ref:
|
||||
self.del_root()
|
||||
self.root_ref = new_root_ref
|
||||
if self.info:
|
||||
self.info_ref = self.write_obj(None, self.info)
|
||||
start_xref = self.xref_table.write(self.f)
|
||||
num_entries = len(self.xref_table)
|
||||
trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
|
||||
if self.last_xref_section_offset is not None:
|
||||
trailer_dict[b"Prev"] = self.last_xref_section_offset
|
||||
if self.info:
|
||||
trailer_dict[b"Info"] = self.info_ref
|
||||
self.last_xref_section_offset = start_xref
|
||||
self.f.write(b"trailer\n" + bytes(PdfDict(trailer_dict)) +
|
||||
make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref))
|
||||
|
||||
def write_page(self, ref, *objs, **dict_obj):
|
||||
if isinstance(ref, int):
|
||||
ref = self.pages[ref]
|
||||
if "Type" not in dict_obj:
|
||||
dict_obj["Type"] = PdfName(b"Page")
|
||||
if "Parent" not in dict_obj:
|
||||
dict_obj["Parent"] = self.pages_ref
|
||||
return self.write_obj(ref, *objs, **dict_obj)
|
||||
|
||||
def write_obj(self, ref, *objs, **dict_obj):
|
||||
f = self.f
|
||||
if ref is None:
|
||||
ref = self.next_object_id(f.tell())
|
||||
else:
|
||||
self.xref_table[ref.object_id] = (f.tell(), ref.generation)
|
||||
f.write(bytes(IndirectObjectDef(*ref)))
|
||||
stream = dict_obj.pop("stream", None)
|
||||
if stream is not None:
|
||||
dict_obj["Length"] = len(stream)
|
||||
if dict_obj:
|
||||
f.write(pdf_repr(dict_obj))
|
||||
for obj in objs:
|
||||
f.write(pdf_repr(obj))
|
||||
if stream is not None:
|
||||
f.write(b"stream\n")
|
||||
f.write(stream)
|
||||
f.write(b"\nendstream\n")
|
||||
f.write(b"endobj\n")
|
||||
return ref
|
||||
|
||||
def del_root(self):
|
||||
if self.root_ref is None:
|
||||
return
|
||||
del self.xref_table[self.root_ref.object_id]
|
||||
del self.xref_table[self.root[b"Pages"].object_id]
|
||||
|
||||
@staticmethod
|
||||
def get_buf_from_file(f):
|
||||
if hasattr(f, "getbuffer"):
|
||||
return f.getbuffer()
|
||||
elif hasattr(f, "getvalue"):
|
||||
return f.getvalue()
|
||||
else:
|
||||
try:
|
||||
return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
except ValueError: # cannot mmap an empty file
|
||||
return b""
|
||||
|
||||
def read_pdf_info(self):
|
||||
self.file_size_total = len(self.buf)
|
||||
self.file_size_this = self.file_size_total - self.start_offset
|
||||
self.read_trailer()
|
||||
self.root_ref = self.trailer_dict[b"Root"]
|
||||
self.info_ref = self.trailer_dict.get(b"Info", None)
|
||||
self.root = PdfDict(self.read_indirect(self.root_ref))
|
||||
if self.info_ref is None:
|
||||
self.info = PdfDict()
|
||||
else:
|
||||
self.info = PdfDict(self.read_indirect(self.info_ref))
|
||||
check_format_condition(b"Type" in self.root, "/Type missing in Root")
|
||||
check_format_condition(self.root[b"Type"] == b"Catalog",
|
||||
"/Type in Root is not /Catalog")
|
||||
check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
|
||||
check_format_condition(isinstance(self.root[b"Pages"],
|
||||
IndirectReference),
|
||||
"/Pages in Root is not an indirect reference")
|
||||
self.pages_ref = self.root[b"Pages"]
|
||||
self.page_tree_root = self.read_indirect(self.pages_ref)
|
||||
self.pages = self.linearize_page_tree(self.page_tree_root)
|
||||
# save the original list of page references
|
||||
# in case the user modifies, adds or deletes some pages
|
||||
# and we need to rewrite the pages and their list
|
||||
self.orig_pages = self.pages[:]
|
||||
|
||||
def next_object_id(self, offset=None):
|
||||
try:
|
||||
# TODO: support reuse of deleted objects
|
||||
reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
|
||||
except ValueError:
|
||||
reference = IndirectReference(1, 0)
|
||||
if offset is not None:
|
||||
self.xref_table[reference.object_id] = (offset, 0)
|
||||
return reference
|
||||
|
||||
delimiter = br"[][()<>{}/%]"
|
||||
delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]"
|
||||
whitespace = br"[\000\011\012\014\015\040]"
|
||||
whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]"
|
||||
whitespace_optional = whitespace + b"*"
|
||||
whitespace_mandatory = whitespace + b"+"
|
||||
newline_only = br"[\r\n]+"
|
||||
newline = whitespace_optional + newline_only + whitespace_optional
|
||||
re_trailer_end = re.compile(
|
||||
whitespace_mandatory + br"trailer" + whitespace_optional +
|
||||
br"\<\<(.*\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" +
|
||||
newline + br"%%EOF" + whitespace_optional + br"$", re.DOTALL)
|
||||
re_trailer_prev = re.compile(
|
||||
whitespace_optional + br"trailer" + whitespace_optional +
|
||||
br"\<\<(.*?\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" +
|
||||
newline + br"%%EOF" + whitespace_optional, re.DOTALL)
|
||||
|
||||
def read_trailer(self):
|
||||
search_start_offset = len(self.buf) - 16384
|
||||
if search_start_offset < self.start_offset:
|
||||
search_start_offset = self.start_offset
|
||||
m = self.re_trailer_end.search(self.buf, search_start_offset)
|
||||
check_format_condition(m, "trailer end not found")
|
||||
# make sure we found the LAST trailer
|
||||
last_match = m
|
||||
while m:
|
||||
last_match = m
|
||||
m = self.re_trailer_end.search(self.buf, m.start()+16)
|
||||
if not m:
|
||||
m = last_match
|
||||
trailer_data = m.group(1)
|
||||
self.last_xref_section_offset = int(m.group(2))
|
||||
self.trailer_dict = self.interpret_trailer(trailer_data)
|
||||
self.xref_table = XrefTable()
|
||||
self.read_xref_table(xref_section_offset=self.last_xref_section_offset)
|
||||
if b"Prev" in self.trailer_dict:
|
||||
self.read_prev_trailer(self.trailer_dict[b"Prev"])
|
||||
|
||||
def read_prev_trailer(self, xref_section_offset):
|
||||
trailer_offset = self.read_xref_table(
|
||||
xref_section_offset=xref_section_offset)
|
||||
m = self.re_trailer_prev.search(
|
||||
self.buf[trailer_offset:trailer_offset+16384])
|
||||
check_format_condition(m, "previous trailer not found")
|
||||
trailer_data = m.group(1)
|
||||
check_format_condition(int(m.group(2)) == xref_section_offset,
|
||||
"xref section offset in previous trailer "
|
||||
"doesn't match what was expected")
|
||||
trailer_dict = self.interpret_trailer(trailer_data)
|
||||
if b"Prev" in trailer_dict:
|
||||
self.read_prev_trailer(trailer_dict[b"Prev"])
|
||||
|
||||
re_whitespace_optional = re.compile(whitespace_optional)
|
||||
re_name = re.compile(
|
||||
whitespace_optional + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" +
|
||||
delimiter_or_ws + br")")
|
||||
re_dict_start = re.compile(whitespace_optional + br"\<\<")
|
||||
re_dict_end = re.compile(
|
||||
whitespace_optional + br"\>\>" + whitespace_optional)
|
||||
|
||||
@classmethod
|
||||
def interpret_trailer(cls, trailer_data):
|
||||
trailer = {}
|
||||
offset = 0
|
||||
while True:
|
||||
m = cls.re_name.match(trailer_data, offset)
|
||||
if not m:
|
||||
m = cls.re_dict_end.match(trailer_data, offset)
|
||||
check_format_condition(
|
||||
m and m.end() == len(trailer_data),
|
||||
"name not found in trailer, remaining data: " +
|
||||
repr(trailer_data[offset:]))
|
||||
break
|
||||
key = cls.interpret_name(m.group(1))
|
||||
value, offset = cls.get_value(trailer_data, m.end())
|
||||
trailer[key] = value
|
||||
check_format_condition(
|
||||
b"Size" in trailer and isinstance(trailer[b"Size"], int),
|
||||
"/Size not in trailer or not an integer")
|
||||
check_format_condition(
|
||||
b"Root" in trailer and
|
||||
isinstance(trailer[b"Root"], IndirectReference),
|
||||
"/Root not in trailer or not an indirect reference")
|
||||
return trailer
|
||||
|
||||
re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?")
|
||||
|
||||
@classmethod
|
||||
def interpret_name(cls, raw, as_text=False):
|
||||
name = b""
|
||||
for m in cls.re_hashes_in_name.finditer(raw):
|
||||
if m.group(3):
|
||||
name += m.group(1) + \
|
||||
bytearray.fromhex(m.group(3).decode("us-ascii"))
|
||||
else:
|
||||
name += m.group(1)
|
||||
if as_text:
|
||||
return name.decode("utf-8")
|
||||
else:
|
||||
return bytes(name)
|
||||
|
||||
re_null = re.compile(
|
||||
whitespace_optional + br"null(?=" + delimiter_or_ws + br")")
|
||||
re_true = re.compile(
|
||||
whitespace_optional + br"true(?=" + delimiter_or_ws + br")")
|
||||
re_false = re.compile(
|
||||
whitespace_optional + br"false(?=" + delimiter_or_ws + br")")
|
||||
re_int = re.compile(
|
||||
whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")")
|
||||
re_real = re.compile(
|
||||
whitespace_optional + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" +
|
||||
delimiter_or_ws + br")")
|
||||
re_array_start = re.compile(whitespace_optional + br"\[")
|
||||
re_array_end = re.compile(whitespace_optional + br"]")
|
||||
re_string_hex = re.compile(
|
||||
whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>")
|
||||
re_string_lit = re.compile(whitespace_optional + br"\(")
|
||||
re_indirect_reference = re.compile(
|
||||
whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory +
|
||||
br"([-+]?[0-9]+)" + whitespace_mandatory + br"R(?=" + delimiter_or_ws +
|
||||
br")")
|
||||
re_indirect_def_start = re.compile(
|
||||
whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory +
|
||||
br"([-+]?[0-9]+)" + whitespace_mandatory + br"obj(?=" +
|
||||
delimiter_or_ws + br")")
|
||||
re_indirect_def_end = re.compile(
|
||||
whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")")
|
||||
re_comment = re.compile(
|
||||
br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*")
|
||||
re_stream_start = re.compile(whitespace_optional + br"stream\r?\n")
|
||||
re_stream_end = re.compile(
|
||||
whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")")
|
||||
|
||||
@classmethod
|
||||
def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
|
||||
if max_nesting == 0:
|
||||
return None, None
|
||||
m = cls.re_comment.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
m = cls.re_indirect_def_start.match(data, offset)
|
||||
if m:
|
||||
check_format_condition(
|
||||
int(m.group(1)) > 0,
|
||||
"indirect object definition: object ID must be greater than 0")
|
||||
check_format_condition(
|
||||
int(m.group(2)) >= 0,
|
||||
"indirect object definition: generation must be non-negative")
|
||||
check_format_condition(
|
||||
expect_indirect is None or expect_indirect ==
|
||||
IndirectReference(int(m.group(1)), int(m.group(2))),
|
||||
"indirect object definition different than expected")
|
||||
object, offset = cls.get_value(
|
||||
data, m.end(), max_nesting=max_nesting-1)
|
||||
if offset is None:
|
||||
return object, None
|
||||
m = cls.re_indirect_def_end.match(data, offset)
|
||||
check_format_condition(
|
||||
m, "indirect object definition end not found")
|
||||
return object, m.end()
|
||||
check_format_condition(
|
||||
not expect_indirect, "indirect object definition not found")
|
||||
m = cls.re_indirect_reference.match(data, offset)
|
||||
if m:
|
||||
check_format_condition(
|
||||
int(m.group(1)) > 0,
|
||||
"indirect object reference: object ID must be greater than 0")
|
||||
check_format_condition(
|
||||
int(m.group(2)) >= 0,
|
||||
"indirect object reference: generation must be non-negative")
|
||||
return IndirectReference(int(m.group(1)), int(m.group(2))), m.end()
|
||||
m = cls.re_dict_start.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
result = {}
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
while not m:
|
||||
key, offset = cls.get_value(
|
||||
data, offset, max_nesting=max_nesting-1)
|
||||
if offset is None:
|
||||
return result, None
|
||||
value, offset = cls.get_value(
|
||||
data, offset, max_nesting=max_nesting-1)
|
||||
result[key] = value
|
||||
if offset is None:
|
||||
return result, None
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
offset = m.end()
|
||||
m = cls.re_stream_start.match(data, offset)
|
||||
if m:
|
||||
try:
|
||||
stream_len = int(result[b"Length"])
|
||||
except (TypeError, KeyError, ValueError):
|
||||
raise PdfFormatError(
|
||||
"bad or missing Length in stream dict (%r)" %
|
||||
result.get(b"Length", None))
|
||||
stream_data = data[m.end():m.end() + stream_len]
|
||||
m = cls.re_stream_end.match(data, m.end() + stream_len)
|
||||
check_format_condition(m, "stream end not found")
|
||||
offset = m.end()
|
||||
result = PdfStream(PdfDict(result), stream_data)
|
||||
else:
|
||||
result = PdfDict(result)
|
||||
return result, offset
|
||||
m = cls.re_array_start.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
result = []
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
while not m:
|
||||
value, offset = cls.get_value(
|
||||
data, offset, max_nesting=max_nesting-1)
|
||||
result.append(value)
|
||||
if offset is None:
|
||||
return result, None
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
return result, m.end()
|
||||
m = cls.re_null.match(data, offset)
|
||||
if m:
|
||||
return None, m.end()
|
||||
m = cls.re_true.match(data, offset)
|
||||
if m:
|
||||
return True, m.end()
|
||||
m = cls.re_false.match(data, offset)
|
||||
if m:
|
||||
return False, m.end()
|
||||
m = cls.re_name.match(data, offset)
|
||||
if m:
|
||||
return PdfName(cls.interpret_name(m.group(1))), m.end()
|
||||
m = cls.re_int.match(data, offset)
|
||||
if m:
|
||||
return int(m.group(1)), m.end()
|
||||
m = cls.re_real.match(data, offset)
|
||||
if m:
|
||||
# XXX Decimal instead of float???
|
||||
return float(m.group(1)), m.end()
|
||||
m = cls.re_string_hex.match(data, offset)
|
||||
if m:
|
||||
# filter out whitespace
|
||||
hex_string = bytearray([
|
||||
b for b in m.group(1)
|
||||
if b in b"0123456789abcdefABCDEF"
|
||||
])
|
||||
if len(hex_string) % 2 == 1:
|
||||
# append a 0 if the length is not even - yes, at the end
|
||||
hex_string.append(ord(b"0"))
|
||||
return bytearray.fromhex(hex_string.decode("us-ascii")), m.end()
|
||||
m = cls.re_string_lit.match(data, offset)
|
||||
if m:
|
||||
return cls.get_literal_string(data, m.end())
|
||||
# return None, offset # fallback (only for debugging)
|
||||
raise PdfFormatError(
|
||||
"unrecognized object: " + repr(data[offset:offset+32]))
|
||||
|
||||
re_lit_str_token = re.compile(
|
||||
br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))")
|
||||
escaped_chars = {
|
||||
b"n": b"\n",
|
||||
b"r": b"\r",
|
||||
b"t": b"\t",
|
||||
b"b": b"\b",
|
||||
b"f": b"\f",
|
||||
b"(": b"(",
|
||||
b")": b")",
|
||||
b"\\": b"\\",
|
||||
ord(b"n"): b"\n",
|
||||
ord(b"r"): b"\r",
|
||||
ord(b"t"): b"\t",
|
||||
ord(b"b"): b"\b",
|
||||
ord(b"f"): b"\f",
|
||||
ord(b"("): b"(",
|
||||
ord(b")"): b")",
|
||||
ord(b"\\"): b"\\",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_literal_string(cls, data, offset):
|
||||
nesting_depth = 0
|
||||
result = bytearray()
|
||||
for m in cls.re_lit_str_token.finditer(data, offset):
|
||||
result.extend(data[offset:m.start()])
|
||||
if m.group(1):
|
||||
result.extend(cls.escaped_chars[m.group(1)[1]])
|
||||
elif m.group(2):
|
||||
result.append(int(m.group(2)[1:], 8))
|
||||
elif m.group(3):
|
||||
pass
|
||||
elif m.group(5):
|
||||
result.extend(b"\n")
|
||||
elif m.group(6):
|
||||
result.extend(b"(")
|
||||
nesting_depth += 1
|
||||
elif m.group(7):
|
||||
if nesting_depth == 0:
|
||||
return bytes(result), m.end()
|
||||
result.extend(b")")
|
||||
nesting_depth -= 1
|
||||
offset = m.end()
|
||||
raise PdfFormatError("unfinished literal string")
|
||||
|
||||
re_xref_section_start = re.compile(
|
||||
whitespace_optional + br"xref" + newline)
|
||||
re_xref_subsection_start = re.compile(
|
||||
whitespace_optional + br"([0-9]+)" + whitespace_mandatory +
|
||||
br"([0-9]+)" + whitespace_optional + newline_only)
|
||||
re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
|
||||
|
||||
def read_xref_table(self, xref_section_offset):
|
||||
subsection_found = False
|
||||
m = self.re_xref_section_start.match(
|
||||
self.buf, xref_section_offset + self.start_offset)
|
||||
check_format_condition(m, "xref section start not found")
|
||||
offset = m.end()
|
||||
while True:
|
||||
m = self.re_xref_subsection_start.match(self.buf, offset)
|
||||
if not m:
|
||||
check_format_condition(
|
||||
subsection_found, "xref subsection start not found")
|
||||
break
|
||||
subsection_found = True
|
||||
offset = m.end()
|
||||
first_object = int(m.group(1))
|
||||
num_objects = int(m.group(2))
|
||||
for i in range(first_object, first_object+num_objects):
|
||||
m = self.re_xref_entry.match(self.buf, offset)
|
||||
check_format_condition(m, "xref entry not found")
|
||||
offset = m.end()
|
||||
is_free = m.group(3) == b"f"
|
||||
generation = int(m.group(2))
|
||||
if not is_free:
|
||||
new_entry = (int(m.group(1)), generation)
|
||||
check_format_condition(
|
||||
i not in self.xref_table or
|
||||
self.xref_table[i] == new_entry,
|
||||
"xref entry duplicated (and not identical)")
|
||||
self.xref_table[i] = new_entry
|
||||
return offset
|
||||
|
||||
def read_indirect(self, ref, max_nesting=-1):
|
||||
offset, generation = self.xref_table[ref[0]]
|
||||
check_format_condition(
|
||||
generation == ref[1],
|
||||
"expected to find generation %s for object ID %s in xref table, "
|
||||
"instead found generation %s at offset %s"
|
||||
% (ref[1], ref[0], generation, offset))
|
||||
value = self.get_value(self.buf, offset + self.start_offset,
|
||||
expect_indirect=IndirectReference(*ref),
|
||||
max_nesting=max_nesting)[0]
|
||||
self.cached_objects[ref] = value
|
||||
return value
|
||||
|
||||
def linearize_page_tree(self, node=None):
|
||||
if node is None:
|
||||
node = self.page_tree_root
|
||||
check_format_condition(
|
||||
node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages")
|
||||
pages = []
|
||||
for kid in node[b"Kids"]:
|
||||
kid_object = self.read_indirect(kid)
|
||||
if kid_object[b"Type"] == b"Page":
|
||||
pages.append(kid)
|
||||
else:
|
||||
pages.extend(self.linearize_page_tree(node=kid_object))
|
||||
return pages
|
||||
|
|
@ -19,14 +19,17 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16le as i16
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
i16 = _binary.i16le
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"\200\350\000\000"
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -39,7 +42,7 @@ class PixarImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self):
|
||||
|
||||
# assuming a 4-byte magic label (FIXME: add "_accept" hook)
|
||||
# assuming a 4-byte magic label
|
||||
s = self.fp.read(4)
|
||||
if s != b"\200\350\000\000":
|
||||
raise SyntaxError("not a PIXAR file")
|
||||
|
|
@ -47,7 +50,7 @@ class PixarImageFile(ImageFile.ImageFile):
|
|||
# read rest of header
|
||||
s = s + self.fp.read(508)
|
||||
|
||||
self.size = i16(s[418:420]), i16(s[416:418])
|
||||
self._size = i16(s[418:420]), i16(s[416:418])
|
||||
|
||||
# get channel/depth descriptions
|
||||
mode = i16(s[424:426]), i16(s[426:428])
|
||||
|
|
@ -59,10 +62,10 @@ class PixarImageFile(ImageFile.ImageFile):
|
|||
# create tile descriptor (assuming "dumped")
|
||||
self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))]
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(PixarImageFile.format, PixarImageFile)
|
||||
Image.register_open(PixarImageFile.format, PixarImageFile, _accept)
|
||||
|
||||
#
|
||||
# FIXME: what's the standard extension?
|
||||
Image.register_extension(PixarImageFile.format, ".pxr")
|
||||
|
|
|
|||
|
|
@ -31,23 +31,20 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import re
|
||||
import zlib
|
||||
import struct
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16be as i16, i32be as i32, o16be as o16, o32be as o32
|
||||
from ._util import py3
|
||||
|
||||
__version__ = "0.9"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16be
|
||||
i32 = _binary.i32be
|
||||
|
||||
is_cid = re.compile(b"\w\w\w\w").match
|
||||
is_cid = re.compile(br"\w\w\w\w").match
|
||||
|
||||
|
||||
_MAGIC = b"\211PNG\r\n\032\n"
|
||||
|
|
@ -73,8 +70,7 @@ _MODES = {
|
|||
}
|
||||
|
||||
|
||||
_simple_palette = re.compile(b'^\xff+\x00\xff*$')
|
||||
_null_palette = re.compile(b'^\x00*$')
|
||||
_simple_palette = re.compile(b'^\xff*\x00\xff*$')
|
||||
|
||||
# Maximum decompressed size for a iTXt or zTXt chunk.
|
||||
# Eliminates decompression bombs where compressed chunks can expand 1000x
|
||||
|
|
@ -91,6 +87,10 @@ def _safe_zlib_decompress(s):
|
|||
return plaintext
|
||||
|
||||
|
||||
def _crc32(data, seed=0):
|
||||
return zlib.crc32(data, seed) & 0xffffffff
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Support classes. Suitable for PNG and related formats like MNG etc.
|
||||
|
||||
|
|
@ -101,15 +101,12 @@ class ChunkStream(object):
|
|||
self.fp = fp
|
||||
self.queue = []
|
||||
|
||||
if not hasattr(Image.core, "crc32"):
|
||||
self.crc = self.crc_skip
|
||||
|
||||
def read(self):
|
||||
"Fetch a new chunk. Returns header information."
|
||||
cid = None
|
||||
|
||||
if self.queue:
|
||||
cid, pos, length = self.queue[-1]
|
||||
del self.queue[-1]
|
||||
cid, pos, length = self.queue.pop()
|
||||
self.fp.seek(pos)
|
||||
else:
|
||||
s = self.fp.read(8)
|
||||
|
|
@ -118,10 +115,17 @@ class ChunkStream(object):
|
|||
length = i32(s)
|
||||
|
||||
if not is_cid(cid):
|
||||
raise SyntaxError("broken PNG file (chunk %s)" % repr(cid))
|
||||
if not ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
raise SyntaxError("broken PNG file (chunk %s)" % repr(cid))
|
||||
|
||||
return cid, pos, length
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
self.queue = self.crc = self.fp = None
|
||||
|
||||
|
|
@ -132,17 +136,28 @@ class ChunkStream(object):
|
|||
def call(self, cid, pos, length):
|
||||
"Call the appropriate chunk handler"
|
||||
|
||||
logger.debug("STREAM %s %s %s", cid, pos, length)
|
||||
logger.debug("STREAM %r %s %s", cid, pos, length)
|
||||
return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length)
|
||||
|
||||
def crc(self, cid, data):
|
||||
"Read and verify checksum"
|
||||
|
||||
crc1 = Image.core.crc32(data, Image.core.crc32(cid))
|
||||
crc2 = i16(self.fp.read(2)), i16(self.fp.read(2))
|
||||
if crc1 != crc2:
|
||||
raise SyntaxError("broken PNG file"
|
||||
"(bad header checksum in %s)" % cid)
|
||||
# Skip CRC checks for ancillary chunks if allowed to load truncated
|
||||
# images
|
||||
# 5th byte of first char is 1 [specs, section 5.4]
|
||||
if ImageFile.LOAD_TRUNCATED_IMAGES and (i8(cid[0]) >> 5 & 1):
|
||||
self.crc_skip(cid, data)
|
||||
return
|
||||
|
||||
try:
|
||||
crc1 = _crc32(data, _crc32(cid))
|
||||
crc2 = i32(self.fp.read(4))
|
||||
if crc1 != crc2:
|
||||
raise SyntaxError("broken PNG file (bad header checksum in %r)"
|
||||
% cid)
|
||||
except struct.error:
|
||||
raise SyntaxError("broken PNG file (incomplete checksum in %r)"
|
||||
% cid)
|
||||
|
||||
def crc_skip(self, cid, data):
|
||||
"Read checksum. Used if the C module is not present"
|
||||
|
|
@ -157,7 +172,11 @@ class ChunkStream(object):
|
|||
cids = []
|
||||
|
||||
while True:
|
||||
cid, pos, length = self.read()
|
||||
try:
|
||||
cid, pos, length = self.read()
|
||||
except struct.error:
|
||||
raise IOError("truncated PNG file")
|
||||
|
||||
if cid == endchunk:
|
||||
break
|
||||
self.crc(cid, ImageFile._safe_read(self.fp, length))
|
||||
|
|
@ -175,7 +194,8 @@ class iTXt(str):
|
|||
@staticmethod
|
||||
def __new__(cls, text, lang, tkey):
|
||||
"""
|
||||
:param value: value for this key
|
||||
:param cls: the class to use when creating the instance
|
||||
:param text: value for this key
|
||||
:param lang: language code
|
||||
:param tkey: UTF-8 version of the key name
|
||||
"""
|
||||
|
|
@ -232,7 +252,7 @@ class PngInfo(object):
|
|||
self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" +
|
||||
value)
|
||||
|
||||
def add_text(self, key, value, zip=0):
|
||||
def add_text(self, key, value, zip=False):
|
||||
"""Appends a text chunk.
|
||||
|
||||
:param key: latin-1 encodable text key name
|
||||
|
|
@ -242,14 +262,14 @@ class PngInfo(object):
|
|||
|
||||
"""
|
||||
if isinstance(value, iTXt):
|
||||
return self.add_itxt(key, value, value.lang, value.tkey, bool(zip))
|
||||
return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)
|
||||
|
||||
# The tEXt chunk stores latin-1 text
|
||||
if not isinstance(value, bytes):
|
||||
try:
|
||||
value = value.encode('latin-1', 'strict')
|
||||
except UnicodeError:
|
||||
return self.add_itxt(key, value, zip=bool(zip))
|
||||
return self.add_itxt(key, value, zip=zip)
|
||||
|
||||
if not isinstance(key, bytes):
|
||||
key = key.encode('latin-1', 'strict')
|
||||
|
|
@ -276,14 +296,15 @@ class PngStream(ChunkStream):
|
|||
self.im_mode = None
|
||||
self.im_tile = None
|
||||
self.im_palette = None
|
||||
self.im_custom_mimetype = None
|
||||
|
||||
self.text_memory = 0
|
||||
|
||||
def check_text_memory(self, chunklen):
|
||||
self.text_memory += chunklen
|
||||
if self.text_memory > MAX_TEXT_MEMORY:
|
||||
raise ValueError("Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" %
|
||||
self.text_memory)
|
||||
raise ValueError("Too much memory used in text chunks: "
|
||||
"%s>MAX_TEXT_MEMORY" % self.text_memory)
|
||||
|
||||
def chunk_iCCP(self, pos, length):
|
||||
|
||||
|
|
@ -295,7 +316,7 @@ class PngStream(ChunkStream):
|
|||
# Compression method 1 byte (0)
|
||||
# Compressed profile n bytes (zlib with deflate compression)
|
||||
i = s.find(b"\0")
|
||||
logger.debug("iCCP profile name %s", s[:i])
|
||||
logger.debug("iCCP profile name %r", s[:i])
|
||||
logger.debug("Compression method %s", i8(s[i]))
|
||||
comp_method = i8(s[i])
|
||||
if comp_method != 0:
|
||||
|
|
@ -303,6 +324,11 @@ class PngStream(ChunkStream):
|
|||
comp_method)
|
||||
try:
|
||||
icc_profile = _safe_zlib_decompress(s[i+2:])
|
||||
except ValueError:
|
||||
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
icc_profile = None
|
||||
else:
|
||||
raise
|
||||
except zlib.error:
|
||||
icc_profile = None # FIXME
|
||||
self.im_info["icc_profile"] = icc_profile
|
||||
|
|
@ -315,7 +341,7 @@ class PngStream(ChunkStream):
|
|||
self.im_size = i32(s), i32(s[4:])
|
||||
try:
|
||||
self.im_mode, self.im_rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if i8(s[12]):
|
||||
self.im_info["interlace"] = 1
|
||||
|
|
@ -349,12 +375,14 @@ class PngStream(ChunkStream):
|
|||
s = ImageFile._safe_read(self.fp, length)
|
||||
if self.im_mode == "P":
|
||||
if _simple_palette.match(s):
|
||||
# tRNS contains only one full-transparent entry,
|
||||
# other entries are full opaque
|
||||
i = s.find(b"\0")
|
||||
if i >= 0:
|
||||
self.im_info["transparency"] = i
|
||||
elif _null_palette.match(s):
|
||||
self.im_info["transparency"] = 0
|
||||
else:
|
||||
# otherwise, we have a byte string with one alpha value
|
||||
# for each palette entry
|
||||
self.im_info["transparency"] = s
|
||||
elif self.im_mode == "L":
|
||||
self.im_info["transparency"] = i16(s)
|
||||
|
|
@ -363,12 +391,31 @@ class PngStream(ChunkStream):
|
|||
return s
|
||||
|
||||
def chunk_gAMA(self, pos, length):
|
||||
|
||||
# gamma setting
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
self.im_info["gamma"] = i32(s) / 100000.0
|
||||
return s
|
||||
|
||||
def chunk_cHRM(self, pos, length):
|
||||
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000
|
||||
# WP x,y, Red x,y, Green x,y Blue x,y
|
||||
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
raw_vals = struct.unpack('>%dI' % (len(s) // 4), s)
|
||||
self.im_info['chromaticity'] = tuple(elt/100000.0 for elt in raw_vals)
|
||||
return s
|
||||
|
||||
def chunk_sRGB(self, pos, length):
|
||||
# srgb rendering intent, 1 byte
|
||||
# 0 perceptual
|
||||
# 1 relative colorimetric
|
||||
# 2 saturation
|
||||
# 3 absolute colorimetric
|
||||
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
self.im_info['srgb'] = i8(s)
|
||||
return s
|
||||
|
||||
def chunk_pHYs(self, pos, length):
|
||||
|
||||
# pixels per unit
|
||||
|
|
@ -393,7 +440,7 @@ class PngStream(ChunkStream):
|
|||
k = s
|
||||
v = b""
|
||||
if k:
|
||||
if bytes is not str:
|
||||
if py3:
|
||||
k = k.decode('latin-1', 'strict')
|
||||
v = v.decode('latin-1', 'replace')
|
||||
|
||||
|
|
@ -420,11 +467,16 @@ class PngStream(ChunkStream):
|
|||
comp_method)
|
||||
try:
|
||||
v = _safe_zlib_decompress(v[1:])
|
||||
except ValueError:
|
||||
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
v = b""
|
||||
else:
|
||||
raise
|
||||
except zlib.error:
|
||||
v = b""
|
||||
|
||||
if k:
|
||||
if bytes is not str:
|
||||
if py3:
|
||||
k = k.decode('latin-1', 'strict')
|
||||
v = v.decode('latin-1', 'replace')
|
||||
|
||||
|
|
@ -452,11 +504,16 @@ class PngStream(ChunkStream):
|
|||
if cm == 0:
|
||||
try:
|
||||
v = _safe_zlib_decompress(v)
|
||||
except ValueError:
|
||||
if ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
return s
|
||||
else:
|
||||
raise
|
||||
except zlib.error:
|
||||
return s
|
||||
else:
|
||||
return s
|
||||
if bytes is not str:
|
||||
if py3:
|
||||
try:
|
||||
k = k.decode("latin-1", "strict")
|
||||
lang = lang.decode("utf-8", "strict")
|
||||
|
|
@ -470,6 +527,20 @@ class PngStream(ChunkStream):
|
|||
|
||||
return s
|
||||
|
||||
# APNG chunks
|
||||
def chunk_acTL(self, pos, length):
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
self.im_custom_mimetype = 'image/apng'
|
||||
return s
|
||||
|
||||
def chunk_fcTL(self, pos, length):
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
return s
|
||||
|
||||
def chunk_fdAT(self, pos, length):
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
return s
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PNG reader
|
||||
|
|
@ -508,7 +579,7 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
except EOFError:
|
||||
break
|
||||
except AttributeError:
|
||||
logger.debug("%s %s %s (unknown)", cid, pos, length)
|
||||
logger.debug("%r %s %s (unknown)", cid, pos, length)
|
||||
s = ImageFile._safe_read(self.fp, length)
|
||||
|
||||
self.png.crc(cid, s)
|
||||
|
|
@ -521,10 +592,11 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
# (believe me, I've tried ;-)
|
||||
|
||||
self.mode = self.png.im_mode
|
||||
self.size = self.png.im_size
|
||||
self._size = self.png.im_size
|
||||
self.info = self.png.im_info
|
||||
self.text = self.png.im_text # experimental
|
||||
self._text = None
|
||||
self.tile = self.png.im_tile
|
||||
self.custom_mimetype = self.png.im_custom_mimetype
|
||||
|
||||
if self.png.im_palette:
|
||||
rawmode, data = self.png.im_palette
|
||||
|
|
@ -532,6 +604,15 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
|
||||
self.__idat = length # used by load_read()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
# experimental
|
||||
if self._text is None:
|
||||
# iTxt, tEXt and zTXt chunks may appear at the end of the file
|
||||
# So load the file to ensure that they are read
|
||||
self.load()
|
||||
return self._text
|
||||
|
||||
def verify(self):
|
||||
"Verify PNG file"
|
||||
|
||||
|
|
@ -544,6 +625,8 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
self.png.verify()
|
||||
self.png.close()
|
||||
|
||||
if self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
|
||||
def load_prepare(self):
|
||||
|
|
@ -582,7 +665,24 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
|
||||
def load_end(self):
|
||||
"internal: finished reading image data"
|
||||
while True:
|
||||
self.fp.read(4) # CRC
|
||||
|
||||
try:
|
||||
cid, pos, length = self.png.read()
|
||||
except (struct.error, SyntaxError):
|
||||
break
|
||||
|
||||
if cid == b"IEND":
|
||||
break
|
||||
|
||||
try:
|
||||
self.png.call(cid, pos, length)
|
||||
except UnicodeDecodeError:
|
||||
break
|
||||
except EOFError:
|
||||
ImageFile._safe_read(self.fp, length)
|
||||
self._text = self.png.im_text
|
||||
self.png.close()
|
||||
self.png = None
|
||||
|
||||
|
|
@ -590,10 +690,6 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
# PNG writer
|
||||
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16be
|
||||
o32 = _binary.o32be
|
||||
|
||||
_OUTMODES = {
|
||||
# supported PIL modes, and corresponding rawmodes/bits/color combinations
|
||||
"1": ("1", b'\x01\x00'),
|
||||
|
|
@ -613,14 +709,14 @@ _OUTMODES = {
|
|||
|
||||
|
||||
def putchunk(fp, cid, *data):
|
||||
"Write a PNG chunk (including CRC field)"
|
||||
"""Write a PNG chunk (including CRC field)"""
|
||||
|
||||
data = b"".join(data)
|
||||
|
||||
fp.write(o32(len(data)) + cid)
|
||||
fp.write(data)
|
||||
hi, lo = Image.core.crc32(data, Image.core.crc32(cid))
|
||||
fp.write(o16(hi) + o16(lo))
|
||||
crc = _crc32(data, _crc32(cid))
|
||||
fp.write(o32(crc))
|
||||
|
||||
|
||||
class _idat(object):
|
||||
|
|
@ -634,7 +730,7 @@ class _idat(object):
|
|||
self.chunk(self.fp, b"IDAT", data)
|
||||
|
||||
|
||||
def _save(im, fp, filename, chunk=putchunk, check=0):
|
||||
def _save(im, fp, filename, chunk=putchunk):
|
||||
# save an image to disk (called by the save method)
|
||||
|
||||
mode = im.mode
|
||||
|
|
@ -665,15 +761,10 @@ def _save(im, fp, filename, chunk=putchunk, check=0):
|
|||
mode = "%s;%d" % (mode, bits)
|
||||
|
||||
# encoder options
|
||||
if "dictionary" in im.encoderinfo:
|
||||
dictionary = im.encoderinfo["dictionary"]
|
||||
else:
|
||||
dictionary = b""
|
||||
|
||||
im.encoderconfig = ("optimize" in im.encoderinfo,
|
||||
im.encoderconfig = (im.encoderinfo.get("optimize", False),
|
||||
im.encoderinfo.get("compress_level", -1),
|
||||
im.encoderinfo.get("compress_type", -1),
|
||||
dictionary)
|
||||
im.encoderinfo.get("dictionary", b""))
|
||||
|
||||
# get the corresponding PNG mode
|
||||
try:
|
||||
|
|
@ -681,9 +772,6 @@ def _save(im, fp, filename, chunk=putchunk, check=0):
|
|||
except KeyError:
|
||||
raise IOError("cannot write mode %s as PNG" % mode)
|
||||
|
||||
if check:
|
||||
return check
|
||||
|
||||
#
|
||||
# write minimal PNG file
|
||||
|
||||
|
|
@ -696,6 +784,34 @@ def _save(im, fp, filename, chunk=putchunk, check=0):
|
|||
b'\0', # 11: filter category
|
||||
b'\0') # 12: interlace flag
|
||||
|
||||
chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
|
||||
|
||||
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
|
||||
if icc:
|
||||
# ICC profile
|
||||
# according to PNG spec, the iCCP chunk contains:
|
||||
# Profile name 1-79 bytes (character string)
|
||||
# Null separator 1 byte (null character)
|
||||
# Compression method 1 byte (0)
|
||||
# Compressed profile n bytes (zlib with deflate compression)
|
||||
name = b"ICC Profile"
|
||||
data = name + b"\0\0" + zlib.compress(icc)
|
||||
chunk(fp, b"iCCP", data)
|
||||
|
||||
# You must either have sRGB or iCCP.
|
||||
# Disallow sRGB chunks when an iCCP-chunk has been emitted.
|
||||
chunks.remove(b"sRGB")
|
||||
|
||||
info = im.encoderinfo.get("pnginfo")
|
||||
if info:
|
||||
chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
|
||||
for cid, data in info.chunks:
|
||||
if cid in chunks:
|
||||
chunks.remove(cid)
|
||||
chunk(fp, cid, data)
|
||||
elif cid in chunks_multiple_allowed:
|
||||
chunk(fp, cid, data)
|
||||
|
||||
if im.mode == "P":
|
||||
palette_byte_number = (2 ** bits) * 3
|
||||
palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
|
||||
|
|
@ -742,20 +858,11 @@ def _save(im, fp, filename, chunk=putchunk, check=0):
|
|||
|
||||
info = im.encoderinfo.get("pnginfo")
|
||||
if info:
|
||||
chunks = [b"bKGD", b"hIST"]
|
||||
for cid, data in info.chunks:
|
||||
chunk(fp, cid, data)
|
||||
|
||||
# ICC profile writing support -- 2008-06-06 Florian Hoech
|
||||
if im.info.get("icc_profile"):
|
||||
# ICC profile
|
||||
# according to PNG spec, the iCCP chunk contains:
|
||||
# Profile name 1-79 bytes (character string)
|
||||
# Null separator 1 byte (null character)
|
||||
# Compression method 1 byte (0)
|
||||
# Compressed profile n bytes (zlib with deflate compression)
|
||||
name = b"ICC Profile"
|
||||
data = name + b"\0\0" + zlib.compress(im.info["icc_profile"])
|
||||
chunk(fp, b"iCCP", data)
|
||||
if cid in chunks:
|
||||
chunks.remove(cid)
|
||||
chunk(fp, cid, data)
|
||||
|
||||
ImageFile._save(im, _idat(fp, chunk),
|
||||
[("zip", (0, 0)+im.size, 0, rawmode)])
|
||||
|
|
@ -783,8 +890,7 @@ def getchunks(im, **params):
|
|||
|
||||
def append(fp, cid, *data):
|
||||
data = b"".join(data)
|
||||
hi, lo = Image.core.crc32(data, Image.core.crc32(cid))
|
||||
crc = o16(hi) + o16(lo)
|
||||
crc = o32(_crc32(data, _crc32(cid)))
|
||||
fp.append((cid, data, crc))
|
||||
|
||||
fp = collector()
|
||||
|
|
@ -804,6 +910,6 @@ def getchunks(im, **params):
|
|||
Image.register_open(PngImageFile.format, PngImageFile, _accept)
|
||||
Image.register_save(PngImageFile.format, _save)
|
||||
|
||||
Image.register_extension(PngImageFile.format, ".png")
|
||||
Image.register_extensions(PngImageFile.format, [".png", ".apng"])
|
||||
|
||||
Image.register_mime(PngImageFile.format, "image/png")
|
||||
|
|
|
|||
|
|
@ -15,25 +15,14 @@
|
|||
#
|
||||
|
||||
|
||||
import string
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
b_whitespace = string.whitespace
|
||||
try:
|
||||
import locale
|
||||
locale_lang, locale_enc = locale.getlocale()
|
||||
if locale_enc is None:
|
||||
locale_lang, locale_enc = locale.getdefaultlocale()
|
||||
b_whitespace = b_whitespace.decode(locale_enc)
|
||||
except:
|
||||
pass
|
||||
b_whitespace = b_whitespace.encode('ascii', 'ignore')
|
||||
b_whitespace = b'\x20\x09\x0a\x0b\x0c\x0d'
|
||||
|
||||
MODES = {
|
||||
# standard
|
||||
|
|
@ -94,7 +83,8 @@ class PpmImageFile(ImageFile.ImageFile):
|
|||
if s not in b_whitespace:
|
||||
break
|
||||
if s == b"":
|
||||
raise ValueError("File does not extend beyond magic number")
|
||||
raise ValueError(
|
||||
"File does not extend beyond magic number")
|
||||
if s != b"#":
|
||||
break
|
||||
s = self.fp.readline()
|
||||
|
|
@ -117,17 +107,12 @@ class PpmImageFile(ImageFile.ImageFile):
|
|||
self.mode = 'I'
|
||||
rawmode = 'I;32B'
|
||||
|
||||
self.size = xsize, ysize
|
||||
self._size = xsize, ysize
|
||||
self.tile = [("raw",
|
||||
(0, 0, xsize, ysize),
|
||||
self.fp.tell(),
|
||||
(rawmode, 0, 1))]
|
||||
|
||||
# ALTERNATIVE: load via builtin debug function
|
||||
# self.im = Image.core.open_ppm(self.filename)
|
||||
# self.mode = self.im.mode
|
||||
# self.size = self.im.size
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
|
@ -166,9 +151,8 @@ def _save(im, fp, filename):
|
|||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
||||
Image.register_save(PpmImageFile.format, _save)
|
||||
|
||||
Image.register_extension(PpmImageFile.format, ".pbm")
|
||||
Image.register_extension(PpmImageFile.format, ".pgm")
|
||||
Image.register_extension(PpmImageFile.format, ".ppm")
|
||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"])
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
__version__ = "0.4"
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16be as i16, i32be as i32
|
||||
|
||||
MODES = {
|
||||
# (photoshop mode, bits) -> (pil mode, required channels)
|
||||
|
|
@ -33,13 +34,6 @@ MODES = {
|
|||
(9, 8): ("LAB", 3)
|
||||
}
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16be
|
||||
i32 = _binary.i32be
|
||||
|
||||
|
||||
# --------------------------------------------------------------------.
|
||||
# read PSD images
|
||||
|
|
@ -77,7 +71,7 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
raise IOError("not enough channels")
|
||||
|
||||
self.mode = mode
|
||||
self.size = i32(s[18:]), i32(s[14:])
|
||||
self._size = i32(s[18:]), i32(s[14:])
|
||||
|
||||
#
|
||||
# color mode data
|
||||
|
|
@ -98,7 +92,7 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
# load resources
|
||||
end = self.fp.tell() + size
|
||||
while self.fp.tell() < end:
|
||||
signature = read(4)
|
||||
read(4) # signature
|
||||
id = i16(read(2))
|
||||
name = read(i8(read(1)))
|
||||
if not (len(name) & 1):
|
||||
|
|
@ -130,7 +124,8 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
|
||||
# keep the file open
|
||||
self._fp = self.fp
|
||||
self.frame = 0
|
||||
self.frame = 1
|
||||
self._min_frame = 1
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
|
|
@ -141,12 +136,11 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
return len(self.layers) > 1
|
||||
|
||||
def seek(self, layer):
|
||||
# seek to given layer (1..max)
|
||||
if layer == self.frame:
|
||||
if not self._seek_check(layer):
|
||||
return
|
||||
|
||||
# seek to given layer (1..max)
|
||||
try:
|
||||
if layer <= 0:
|
||||
raise IndexError
|
||||
name, mode, bbox, tile = self.layers[layer-1]
|
||||
self.mode = mode
|
||||
self.tile = tile
|
||||
|
|
@ -213,17 +207,13 @@ def _layerinfo(file):
|
|||
mode = None # unknown
|
||||
|
||||
# skip over blend flags and extra information
|
||||
filler = read(12)
|
||||
read(12) # filler
|
||||
name = ""
|
||||
size = i32(read(4))
|
||||
combined = 0
|
||||
if size:
|
||||
length = i32(read(4))
|
||||
if length:
|
||||
mask_y = i32(read(4))
|
||||
mask_x = i32(read(4))
|
||||
mask_h = i32(read(4)) - mask_y
|
||||
mask_w = i32(read(4)) - mask_x
|
||||
file.seek(length - 16, 1)
|
||||
combined += length + 4
|
||||
|
||||
|
|
@ -307,6 +297,7 @@ def _maketile(file, mode, bbox, channels):
|
|||
# --------------------------------------------------------------------
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(PsdImageFile.format, PsdImageFile, _accept)
|
||||
|
||||
Image.register_extension(PsdImageFile.format, ".psd")
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@
|
|||
# Access.c implementation.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
|
@ -51,8 +49,12 @@ class PyAccess(object):
|
|||
self.image8 = ffi.cast('unsigned char **', vals['image8'])
|
||||
self.image32 = ffi.cast('int **', vals['image32'])
|
||||
self.image = ffi.cast('unsigned char **', vals['image'])
|
||||
self.xsize = vals['xsize']
|
||||
self.ysize = vals['ysize']
|
||||
self.xsize, self.ysize = img.im.size
|
||||
|
||||
# Keep pointer to im object to prevent dereferencing.
|
||||
self._im = img.im
|
||||
if self._im.mode == "P":
|
||||
self._palette = img.palette
|
||||
|
||||
# Debugging is polluting test traces, only useful here
|
||||
# when hacking on PyAccess
|
||||
|
|
@ -68,12 +70,24 @@ class PyAccess(object):
|
|||
numerical value for single band images, and a tuple for
|
||||
multi-band images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param value: The pixel value.
|
||||
:param xy: The pixel coordinate, given as (x, y). See
|
||||
:ref:`coordinate-system`.
|
||||
:param color: The pixel value.
|
||||
"""
|
||||
if self.readonly:
|
||||
raise ValueError('Attempt to putpixel a read only image')
|
||||
(x, y) = self.check_xy(xy)
|
||||
(x, y) = xy
|
||||
if x < 0:
|
||||
x = self.xsize + x
|
||||
if y < 0:
|
||||
y = self.ysize + y
|
||||
(x, y) = self.check_xy((x, y))
|
||||
|
||||
if self._im.mode == "P" and \
|
||||
isinstance(color, (list, tuple)) and len(color) in [3, 4]:
|
||||
# RGB or RGBA value for a P image
|
||||
color = self._palette.getcolor(color)
|
||||
|
||||
return self.set_pixel(x, y, color)
|
||||
|
||||
def __getitem__(self, xy):
|
||||
|
|
@ -82,12 +96,17 @@ class PyAccess(object):
|
|||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param xy: The pixel coordinate, given as (x, y). See
|
||||
:ref:`coordinate-system`.
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
"""
|
||||
|
||||
(x, y) = self.check_xy(xy)
|
||||
(x, y) = xy
|
||||
if x < 0:
|
||||
x = self.xsize + x
|
||||
if y < 0:
|
||||
y = self.ysize + y
|
||||
(x, y) = self.check_xy((x, y))
|
||||
return self.get_pixel(x, y)
|
||||
|
||||
putpixel = __setitem__
|
||||
|
|
@ -132,6 +151,7 @@ class _PyAccess32_3(PyAccess):
|
|||
pixel.r = min(color[0], 255)
|
||||
pixel.g = min(color[1], 255)
|
||||
pixel.b = min(color[2], 255)
|
||||
pixel.a = 255
|
||||
|
||||
|
||||
class _PyAccess32_4(PyAccess):
|
||||
|
|
@ -164,7 +184,7 @@ class _PyAccess8(PyAccess):
|
|||
try:
|
||||
# integer
|
||||
self.pixels[y][x] = min(color, 255)
|
||||
except:
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = min(color[0], 255)
|
||||
|
||||
|
|
@ -181,7 +201,7 @@ class _PyAccessI16_N(PyAccess):
|
|||
try:
|
||||
# integer
|
||||
self.pixels[y][x] = min(color, 65535)
|
||||
except:
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = min(color[0], 65535)
|
||||
|
||||
|
|
@ -202,7 +222,7 @@ class _PyAccessI16_L(PyAccess):
|
|||
except TypeError:
|
||||
color = min(color[0], 65535)
|
||||
|
||||
pixel.l = color & 0xFF
|
||||
pixel.l = color & 0xFF # noqa: E741
|
||||
pixel.r = color >> 8
|
||||
|
||||
|
||||
|
|
@ -219,10 +239,10 @@ class _PyAccessI16_B(PyAccess):
|
|||
pixel = self.pixels[y][x]
|
||||
try:
|
||||
color = min(color, 65535)
|
||||
except:
|
||||
except Exception:
|
||||
color = min(color[0], 65535)
|
||||
|
||||
pixel.l = color >> 8
|
||||
pixel.l = color >> 8 # noqa: E741
|
||||
pixel.r = color & 0xFF
|
||||
|
||||
|
||||
|
|
@ -269,7 +289,7 @@ class _PyAccessF(PyAccess):
|
|||
try:
|
||||
# not a tuple
|
||||
self.pixels[y][x] = color
|
||||
except:
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = color[0]
|
||||
|
||||
|
|
@ -278,6 +298,7 @@ mode_map = {'1': _PyAccess8,
|
|||
'L': _PyAccess8,
|
||||
'P': _PyAccess8,
|
||||
'LA': _PyAccess32_2,
|
||||
'La': _PyAccess32_2,
|
||||
'PA': _PyAccess32_2,
|
||||
'RGB': _PyAccess32_3,
|
||||
'LAB': _PyAccess32_3,
|
||||
|
|
@ -313,5 +334,3 @@ def new(img, readonly=False):
|
|||
logger.debug("PyAccess Not Implemented: %s", img.mode)
|
||||
return None
|
||||
return access_type(img, readonly)
|
||||
|
||||
# End of file
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@
|
|||
# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli.
|
||||
# <ftp://ftp.sgi.com/graphics/SGIIMAGESPEC>
|
||||
#
|
||||
#
|
||||
# History:
|
||||
# 2017-22-07 mb Add RLE decompression
|
||||
# 2016-16-10 mb Add save method without compression
|
||||
# 1995-09-10 fl Created
|
||||
#
|
||||
# Copyright (c) 2016 by Mickael Bonfill.
|
||||
# Copyright (c) 2008 by Karsten Hiddemann.
|
||||
# Copyright (c) 1997 by Secret Labs AB.
|
||||
# Copyright (c) 1995 by Fredrik Lundh.
|
||||
|
|
@ -18,21 +22,34 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8, o8, i16be as i16
|
||||
from ._util import py3
|
||||
import struct
|
||||
import os
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16be
|
||||
__version__ = "0.3"
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 2 and i16(prefix) == 474
|
||||
|
||||
|
||||
MODES = {
|
||||
(1, 1, 1): "L",
|
||||
(1, 2, 1): "L",
|
||||
(2, 1, 1): "L;16B",
|
||||
(2, 2, 1): "L;16B",
|
||||
(1, 3, 3): "RGB",
|
||||
(2, 3, 3): "RGB;16B",
|
||||
(1, 3, 4): "RGBA",
|
||||
(2, 3, 4): "RGBA;16B"
|
||||
}
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for SGI images.
|
||||
|
||||
class SgiImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "SGI"
|
||||
|
|
@ -41,49 +58,171 @@ class SgiImageFile(ImageFile.ImageFile):
|
|||
def _open(self):
|
||||
|
||||
# HEAD
|
||||
s = self.fp.read(512)
|
||||
headlen = 512
|
||||
s = self.fp.read(headlen)
|
||||
|
||||
# magic number : 474
|
||||
if i16(s) != 474:
|
||||
raise ValueError("Not an SGI image file")
|
||||
|
||||
# relevant header entries
|
||||
# compression : verbatim or RLE
|
||||
compression = i8(s[2])
|
||||
|
||||
# bytes, dimension, zsize
|
||||
layout = i8(s[3]), i16(s[4:]), i16(s[10:])
|
||||
# bpc : 1 or 2 bytes (8bits or 16bits)
|
||||
bpc = i8(s[3])
|
||||
|
||||
# determine mode from bytes/zsize
|
||||
if layout == (1, 2, 1) or layout == (1, 1, 1):
|
||||
self.mode = "L"
|
||||
elif layout == (1, 3, 3):
|
||||
self.mode = "RGB"
|
||||
elif layout == (1, 3, 4):
|
||||
self.mode = "RGBA"
|
||||
else:
|
||||
# dimension : 1, 2 or 3 (depending on xsize, ysize and zsize)
|
||||
dimension = i16(s[4:])
|
||||
|
||||
# xsize : width
|
||||
xsize = i16(s[6:])
|
||||
|
||||
# ysize : height
|
||||
ysize = i16(s[8:])
|
||||
|
||||
# zsize : channels count
|
||||
zsize = i16(s[10:])
|
||||
|
||||
# layout
|
||||
layout = bpc, dimension, zsize
|
||||
|
||||
# determine mode from bits/zsize
|
||||
rawmode = ""
|
||||
try:
|
||||
rawmode = MODES[layout]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if rawmode == "":
|
||||
raise ValueError("Unsupported SGI image mode")
|
||||
|
||||
# size
|
||||
self.size = i16(s[6:]), i16(s[8:])
|
||||
self._size = xsize, ysize
|
||||
self.mode = rawmode.split(";")[0]
|
||||
|
||||
# orientation -1 : scanlines begins at the bottom-left corner
|
||||
orientation = -1
|
||||
|
||||
# decoder info
|
||||
if compression == 0:
|
||||
offset = 512
|
||||
pagesize = self.size[0]*self.size[1]*layout[0]
|
||||
self.tile = []
|
||||
for layer in self.mode:
|
||||
self.tile.append(
|
||||
("raw", (0, 0)+self.size, offset, (layer, 0, -1)))
|
||||
offset = offset + pagesize
|
||||
pagesize = xsize * ysize * bpc
|
||||
if bpc == 2:
|
||||
self.tile = [("SGI16", (0, 0) + self.size,
|
||||
headlen, (self.mode, 0, orientation))]
|
||||
else:
|
||||
self.tile = []
|
||||
offset = headlen
|
||||
for layer in self.mode:
|
||||
self.tile.append(
|
||||
("raw", (0, 0) + self.size,
|
||||
offset, (layer, 0, orientation)))
|
||||
offset += pagesize
|
||||
elif compression == 1:
|
||||
raise ValueError("SGI RLE encoding not supported")
|
||||
self.tile = [("sgi_rle", (0, 0) + self.size,
|
||||
headlen, (rawmode, orientation, bpc))]
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
|
||||
raise ValueError("Unsupported SGI image mode")
|
||||
|
||||
# Get the keyword arguments
|
||||
info = im.encoderinfo
|
||||
|
||||
# Byte-per-pixel precision, 1 = 8bits per pixel
|
||||
bpc = info.get("bpc", 1)
|
||||
|
||||
if bpc not in (1, 2):
|
||||
raise ValueError("Unsupported number of bytes per pixel")
|
||||
|
||||
# Flip the image, since the origin of SGI file is the bottom-left corner
|
||||
orientation = -1
|
||||
# Define the file as SGI File Format
|
||||
magicNumber = 474
|
||||
# Run-Length Encoding Compression - Unsupported at this time
|
||||
rle = 0
|
||||
|
||||
# Number of dimensions (x,y,z)
|
||||
dim = 3
|
||||
# X Dimension = width / Y Dimension = height
|
||||
x, y = im.size
|
||||
if im.mode == "L" and y == 1:
|
||||
dim = 1
|
||||
elif im.mode == "L":
|
||||
dim = 2
|
||||
# Z Dimension: Number of channels
|
||||
z = len(im.mode)
|
||||
|
||||
if dim == 1 or dim == 2:
|
||||
z = 1
|
||||
|
||||
# assert we've got the right number of bands.
|
||||
if len(im.getbands()) != z:
|
||||
raise ValueError("incorrect number of bands in SGI write: %s vs %s" %
|
||||
(z, len(im.getbands())))
|
||||
|
||||
# Minimum Byte value
|
||||
pinmin = 0
|
||||
# Maximum Byte value (255 = 8bits per pixel)
|
||||
pinmax = 255
|
||||
# Image name (79 characters max, truncated below in write)
|
||||
imgName = os.path.splitext(os.path.basename(filename))[0]
|
||||
if py3:
|
||||
imgName = imgName.encode('ascii', 'ignore')
|
||||
# Standard representation of pixel in the file
|
||||
colormap = 0
|
||||
fp.write(struct.pack('>h', magicNumber))
|
||||
fp.write(o8(rle))
|
||||
fp.write(o8(bpc))
|
||||
fp.write(struct.pack('>H', dim))
|
||||
fp.write(struct.pack('>H', x))
|
||||
fp.write(struct.pack('>H', y))
|
||||
fp.write(struct.pack('>H', z))
|
||||
fp.write(struct.pack('>l', pinmin))
|
||||
fp.write(struct.pack('>l', pinmax))
|
||||
fp.write(struct.pack('4s', b'')) # dummy
|
||||
fp.write(struct.pack('79s', imgName)) # truncates to 79 chars
|
||||
fp.write(struct.pack('s', b'')) # force null byte after imgname
|
||||
fp.write(struct.pack('>l', colormap))
|
||||
fp.write(struct.pack('404s', b'')) # dummy
|
||||
|
||||
rawmode = 'L'
|
||||
if bpc == 2:
|
||||
rawmode = 'L;16B'
|
||||
|
||||
for channel in im.split():
|
||||
fp.write(channel.tobytes('raw', rawmode, 0, orientation))
|
||||
|
||||
fp.close()
|
||||
|
||||
|
||||
class SGI16Decoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
rawmode, stride, orientation = self.args
|
||||
pagesize = self.state.xsize * self.state.ysize
|
||||
zsize = len(self.mode)
|
||||
self.fd.seek(512)
|
||||
|
||||
for band in range(zsize):
|
||||
channel = Image.new('L', (self.state.xsize, self.state.ysize))
|
||||
channel.frombytes(self.fd.read(2 * pagesize), 'raw',
|
||||
'L;16B', stride, orientation)
|
||||
self.im.putband(channel.im, band)
|
||||
|
||||
return -1, 0
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(SgiImageFile.format, SgiImageFile, _accept)
|
||||
|
||||
Image.register_extension(SgiImageFile.format, ".bw")
|
||||
Image.register_extension(SgiImageFile.format, ".rgb")
|
||||
Image.register_extension(SgiImageFile.format, ".rgba")
|
||||
Image.register_extension(SgiImageFile.format, ".sgi")
|
||||
Image.register_decoder("SGI16", SGI16Decoder)
|
||||
Image.register_open(SgiImageFile.format, SgiImageFile, _accept)
|
||||
Image.register_save(SgiImageFile.format, _save)
|
||||
Image.register_mime(SgiImageFile.format, "image/sgi")
|
||||
Image.register_mime(SgiImageFile.format, "image/rgb")
|
||||
|
||||
Image.register_extensions(SgiImageFile.format,
|
||||
[".bw", ".rgb", ".rgba", ".sgi"])
|
||||
|
||||
# End of file
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@
|
|||
# image data from electron microscopy and tomography.
|
||||
#
|
||||
# Spider home page:
|
||||
# http://spider.wadsworth.org/spider_doc/spider/docs/spider.html
|
||||
# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
|
||||
#
|
||||
# Details about the Spider image format:
|
||||
# http://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
|
||||
# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
|
@ -48,17 +48,16 @@ def isInt(f):
|
|||
return 1
|
||||
else:
|
||||
return 0
|
||||
except ValueError:
|
||||
return 0
|
||||
except OverflowError:
|
||||
except (ValueError, OverflowError):
|
||||
return 0
|
||||
|
||||
|
||||
iforms = [1, 3, -11, -12, -21, -22]
|
||||
|
||||
|
||||
# There is no magic number to identify Spider files, so just check a
|
||||
# series of header locations to see if they have reasonable values.
|
||||
# Returns no.of bytes in the header, if it is a valid Spider header,
|
||||
# Returns no. of bytes in the header, if it is a valid Spider header,
|
||||
# otherwise returns 0
|
||||
|
||||
def isSpiderHeader(t):
|
||||
|
|
@ -75,7 +74,6 @@ def isSpiderHeader(t):
|
|||
labrec = int(h[13]) # no. records in file header
|
||||
labbyt = int(h[22]) # total no. of bytes in header
|
||||
lenbyt = int(h[23]) # record length in bytes
|
||||
# print "labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt)
|
||||
if labbyt != (labrec * lenbyt):
|
||||
return 0
|
||||
# looks like a valid header
|
||||
|
|
@ -83,9 +81,8 @@ def isSpiderHeader(t):
|
|||
|
||||
|
||||
def isSpiderImage(filename):
|
||||
fp = open(filename, 'rb')
|
||||
f = fp.read(92) # read 23 * 4 bytes
|
||||
fp.close()
|
||||
with open(filename, 'rb') as fp:
|
||||
f = fp.read(92) # read 23 * 4 bytes
|
||||
t = struct.unpack('>23f', f) # try big-endian first
|
||||
hdrlen = isSpiderHeader(t)
|
||||
if hdrlen == 0:
|
||||
|
|
@ -98,6 +95,7 @@ class SpiderImageFile(ImageFile.ImageFile):
|
|||
|
||||
format = "SPIDER"
|
||||
format_description = "Spider 2D image"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self):
|
||||
# check header
|
||||
|
|
@ -122,7 +120,7 @@ class SpiderImageFile(ImageFile.ImageFile):
|
|||
if iform != 1:
|
||||
raise SyntaxError("not a Spider 2D image")
|
||||
|
||||
self.size = int(h[12]), int(h[2]) # size in pixels (width, height)
|
||||
self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
|
||||
self.istack = int(h[24])
|
||||
self.imgnumber = int(h[27])
|
||||
|
||||
|
|
@ -173,9 +171,9 @@ class SpiderImageFile(ImageFile.ImageFile):
|
|||
|
||||
def seek(self, frame):
|
||||
if self.istack == 0:
|
||||
raise EOFError("attempt to seek in a non-stack file")
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
if frame >= self._nimages:
|
||||
raise EOFError("attempt to seek past end of file")
|
||||
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self.stkoffset)
|
||||
|
|
@ -195,13 +193,22 @@ class SpiderImageFile(ImageFile.ImageFile):
|
|||
from PIL import ImageTk
|
||||
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image series
|
||||
|
||||
# given a list of filenames, return a list of images
|
||||
def loadImageSeries(filelist=None):
|
||||
" create a list of Image.images for use in montage "
|
||||
"""create a list of Image.images for use in montage"""
|
||||
if filelist is None or len(filelist) < 1:
|
||||
return
|
||||
|
||||
|
|
@ -212,7 +219,7 @@ def loadImageSeries(filelist=None):
|
|||
continue
|
||||
try:
|
||||
im = Image.open(img).convert2byte()
|
||||
except:
|
||||
except Exception:
|
||||
if not isSpiderImage(img):
|
||||
print(img + " is not a Spider image file")
|
||||
continue
|
||||
|
|
@ -267,33 +274,28 @@ def _save(im, fp, filename):
|
|||
raise IOError("Error creating Spider header")
|
||||
|
||||
# write the SPIDER header
|
||||
try:
|
||||
fp = open(filename, 'wb')
|
||||
except:
|
||||
raise IOError("Unable to open %s for writing" % filename)
|
||||
fp.writelines(hdr)
|
||||
|
||||
rawmode = "F;32NF" # 32-bit native floating point
|
||||
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))])
|
||||
|
||||
fp.close()
|
||||
|
||||
|
||||
def _save_spider(im, fp, filename):
|
||||
# get the filename extension and register it with Image
|
||||
ext = os.path.splitext(filename)[1]
|
||||
Image.register_extension("SPIDER", ext)
|
||||
Image.register_extension(SpiderImageFile.format, ext)
|
||||
_save(im, fp, filename)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(SpiderImageFile.format, SpiderImageFile)
|
||||
Image.register_save(SpiderImageFile.format, _save_spider)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if not sys.argv[1:]:
|
||||
print("Syntax: python SpiderImagePlugin.py Spiderimage [outfile]")
|
||||
if len(sys.argv) < 2:
|
||||
print("Syntax: python SpiderImagePlugin.py [infile] [outfile]")
|
||||
sys.exit()
|
||||
|
||||
filename = sys.argv[1]
|
||||
|
|
@ -301,10 +303,6 @@ if __name__ == "__main__":
|
|||
print("input image must be in Spider format")
|
||||
sys.exit()
|
||||
|
||||
outfile = ""
|
||||
if len(sys.argv[1:]) > 1:
|
||||
outfile = sys.argv[2]
|
||||
|
||||
im = Image.open(filename)
|
||||
print("image: " + str(im))
|
||||
print("format: " + str(im.format))
|
||||
|
|
@ -313,10 +311,12 @@ if __name__ == "__main__":
|
|||
print("max, min: ", end=' ')
|
||||
print(im.getextrema())
|
||||
|
||||
if outfile != "":
|
||||
if len(sys.argv) > 2:
|
||||
outfile = sys.argv[2]
|
||||
|
||||
# perform some image operation
|
||||
im = im.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
print(
|
||||
"saving a flipped version of %s as %s " %
|
||||
(os.path.basename(filename), outfile))
|
||||
im.save(outfile, "SPIDER")
|
||||
im.save(outfile, SpiderImageFile.format)
|
||||
|
|
|
|||
|
|
@ -17,12 +17,11 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i32be as i32
|
||||
|
||||
__version__ = "0.3"
|
||||
|
||||
i32 = _binary.i32be
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 4 and i32(prefix) == 0x59a66a95
|
||||
|
|
@ -38,6 +37,21 @@ class SunImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self):
|
||||
|
||||
# The Sun Raster file header is 32 bytes in length
|
||||
# and has the following format:
|
||||
|
||||
# typedef struct _SunRaster
|
||||
# {
|
||||
# DWORD MagicNumber; /* Magic (identification) number */
|
||||
# DWORD Width; /* Width of image in pixels */
|
||||
# DWORD Height; /* Height of image in pixels */
|
||||
# DWORD Depth; /* Number of bits per pixel */
|
||||
# DWORD Length; /* Size of image data in bytes */
|
||||
# DWORD Type; /* Type of raster file */
|
||||
# DWORD ColorMapType; /* Type of color map */
|
||||
# DWORD ColorMapLength; /* Size of the color map in bytes */
|
||||
# } SUNRASTER;
|
||||
|
||||
# HEAD
|
||||
s = self.fp.read(32)
|
||||
if i32(s) != 0x59a66a95:
|
||||
|
|
@ -45,37 +59,79 @@ class SunImageFile(ImageFile.ImageFile):
|
|||
|
||||
offset = 32
|
||||
|
||||
self.size = i32(s[4:8]), i32(s[8:12])
|
||||
self._size = i32(s[4:8]), i32(s[8:12])
|
||||
|
||||
depth = i32(s[12:16])
|
||||
# data_length = i32(s[16:20]) # unreliable, ignore.
|
||||
file_type = i32(s[20:24])
|
||||
palette_type = i32(s[24:28]) # 0: None, 1: RGB, 2: Raw/arbitrary
|
||||
palette_length = i32(s[28:32])
|
||||
|
||||
if depth == 1:
|
||||
self.mode, rawmode = "1", "1;I"
|
||||
elif depth == 4:
|
||||
self.mode, rawmode = "L", "L;4"
|
||||
elif depth == 8:
|
||||
self.mode = rawmode = "L"
|
||||
elif depth == 24:
|
||||
self.mode, rawmode = "RGB", "BGR"
|
||||
if file_type == 3:
|
||||
self.mode, rawmode = "RGB", "RGB"
|
||||
else:
|
||||
self.mode, rawmode = "RGB", "BGR"
|
||||
elif depth == 32:
|
||||
if file_type == 3:
|
||||
self.mode, rawmode = 'RGB', 'RGBX'
|
||||
else:
|
||||
self.mode, rawmode = 'RGB', 'BGRX'
|
||||
else:
|
||||
raise SyntaxError("unsupported mode")
|
||||
raise SyntaxError("Unsupported Mode/Bit Depth")
|
||||
|
||||
compression = i32(s[20:24])
|
||||
if palette_length:
|
||||
if palette_length > 1024:
|
||||
raise SyntaxError("Unsupported Color Palette Length")
|
||||
|
||||
if i32(s[24:28]) != 0:
|
||||
length = i32(s[28:32])
|
||||
offset = offset + length
|
||||
self.palette = ImagePalette.raw("RGB;L", self.fp.read(length))
|
||||
if palette_type != 1:
|
||||
raise SyntaxError("Unsupported Palette Type")
|
||||
|
||||
offset = offset + palette_length
|
||||
self.palette = ImagePalette.raw("RGB;L",
|
||||
self.fp.read(palette_length))
|
||||
if self.mode == "L":
|
||||
self.mode = rawmode = "P"
|
||||
self.mode = "P"
|
||||
rawmode = rawmode.replace('L', 'P')
|
||||
|
||||
stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3)
|
||||
# 16 bit boundaries on stride
|
||||
stride = ((self.size[0] * depth + 15) // 16) * 2
|
||||
|
||||
if compression == 1:
|
||||
# file type: Type is the version (or flavor) of the bitmap
|
||||
# file. The following values are typically found in the Type
|
||||
# field:
|
||||
# 0000h Old
|
||||
# 0001h Standard
|
||||
# 0002h Byte-encoded
|
||||
# 0003h RGB format
|
||||
# 0004h TIFF format
|
||||
# 0005h IFF format
|
||||
# FFFFh Experimental
|
||||
|
||||
# Old and standard are the same, except for the length tag.
|
||||
# byte-encoded is run-length-encoded
|
||||
# RGB looks similar to standard, but RGB byte order
|
||||
# TIFF and IFF mean that they were converted from T/IFF
|
||||
# Experimental means that it's something else.
|
||||
# (https://www.fileformat.info/format/sunraster/egff.htm)
|
||||
|
||||
if file_type in (0, 1, 3, 4, 5):
|
||||
self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))]
|
||||
elif compression == 2:
|
||||
elif file_type == 2:
|
||||
self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)]
|
||||
else:
|
||||
raise SyntaxError('Unsupported Sun Raster file type')
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(SunImageFile.format, SunImageFile, _accept)
|
||||
|
||||
Image.register_extension(SunImageFile.format, ".ras")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import ContainerIO
|
||||
import sys
|
||||
from . import ContainerIO
|
||||
|
||||
|
||||
##
|
||||
|
|
@ -23,19 +24,18 @@ from PIL import ContainerIO
|
|||
|
||||
class TarIO(ContainerIO.ContainerIO):
|
||||
|
||||
##
|
||||
# Create file object.
|
||||
#
|
||||
# @param tarfile Name of TAR file.
|
||||
# @param file Name of member file.
|
||||
|
||||
def __init__(self, tarfile, file):
|
||||
"""
|
||||
Create file object.
|
||||
|
||||
fh = open(tarfile, "rb")
|
||||
:param tarfile: Name of TAR file.
|
||||
:param file: Name of member file.
|
||||
"""
|
||||
self.fh = open(tarfile, "rb")
|
||||
|
||||
while True:
|
||||
|
||||
s = fh.read(512)
|
||||
s = self.fh.read(512)
|
||||
if len(s) != 512:
|
||||
raise IOError("unexpected end of tar file")
|
||||
|
||||
|
|
@ -51,7 +51,21 @@ class TarIO(ContainerIO.ContainerIO):
|
|||
if file == name:
|
||||
break
|
||||
|
||||
fh.seek((size + 511) & (~511), 1)
|
||||
self.fh.seek((size + 511) & (~511), 1)
|
||||
|
||||
# Open region
|
||||
ContainerIO.ContainerIO.__init__(self, fh, fh.tell(), size)
|
||||
ContainerIO.ContainerIO.__init__(self, self.fh, self.fh.tell(), size)
|
||||
|
||||
# Context manager support
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
if sys.version_info.major >= 3:
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
self.fh.close()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@
|
|||
#
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, o8, o16le as o16
|
||||
|
||||
import warnings
|
||||
|
||||
__version__ = "0.3"
|
||||
|
||||
|
|
@ -26,15 +29,13 @@ __version__ = "0.3"
|
|||
# --------------------------------------------------------------------
|
||||
# Read RGA file
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
|
||||
|
||||
MODES = {
|
||||
# map imagetype/depth to rawmode
|
||||
(1, 8): "P",
|
||||
(3, 1): "1",
|
||||
(3, 8): "L",
|
||||
(3, 16): "LA",
|
||||
(2, 16): "BGR;5",
|
||||
(2, 24): "BGR",
|
||||
(2, 32): "BGRA",
|
||||
|
|
@ -54,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
# process header
|
||||
s = self.fp.read(18)
|
||||
|
||||
idlen = i8(s[0])
|
||||
id_len = i8(s[0])
|
||||
|
||||
colormaptype = i8(s[1])
|
||||
imagetype = i8(s[2])
|
||||
|
|
@ -63,7 +64,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
|
||||
flags = i8(s[17])
|
||||
|
||||
self.size = i16(s[12:]), i16(s[14:])
|
||||
self._size = i16(s[12:]), i16(s[14:])
|
||||
|
||||
# validate header fields
|
||||
if colormaptype not in (0, 1) or\
|
||||
|
|
@ -76,6 +77,8 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
self.mode = "L"
|
||||
if depth == 1:
|
||||
self.mode = "1" # ???
|
||||
elif depth == 16:
|
||||
self.mode = "LA"
|
||||
elif imagetype in (1, 9):
|
||||
self.mode = "P"
|
||||
elif imagetype in (2, 10):
|
||||
|
|
@ -99,8 +102,8 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
if imagetype & 8:
|
||||
self.info["compression"] = "tga_rle"
|
||||
|
||||
if idlen:
|
||||
self.info["id_section"] = self.fp.read(idlen)
|
||||
if id_len:
|
||||
self.info["id_section"] = self.fp.read(id_len)
|
||||
|
||||
if colormaptype:
|
||||
# read palette
|
||||
|
|
@ -132,44 +135,57 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
# Write TGA file
|
||||
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16le
|
||||
o32 = _binary.o32le
|
||||
|
||||
SAVE = {
|
||||
"1": ("1", 1, 0, 3),
|
||||
"L": ("L", 8, 0, 3),
|
||||
"LA": ("LA", 16, 0, 3),
|
||||
"P": ("P", 8, 1, 1),
|
||||
"RGB": ("BGR", 24, 0, 2),
|
||||
"RGBA": ("BGRA", 32, 0, 2),
|
||||
}
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
def _save(im, fp, filename):
|
||||
|
||||
try:
|
||||
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise IOError("cannot write mode %s as TGA" % im.mode)
|
||||
|
||||
if check:
|
||||
return check
|
||||
if "rle" in im.encoderinfo:
|
||||
rle = im.encoderinfo["rle"]
|
||||
else:
|
||||
compression = im.encoderinfo.get("compression",
|
||||
im.info.get("compression"))
|
||||
rle = compression == "tga_rle"
|
||||
if rle:
|
||||
imagetype += 8
|
||||
|
||||
id_section = im.encoderinfo.get("id_section",
|
||||
im.info.get("id_section", ""))
|
||||
id_len = len(id_section)
|
||||
if id_len > 255:
|
||||
id_len = 255
|
||||
id_section = id_section[:255]
|
||||
warnings.warn("id_section has been trimmed to 255 characters")
|
||||
|
||||
if colormaptype:
|
||||
colormapfirst, colormaplength, colormapentry = 0, 256, 24
|
||||
else:
|
||||
colormapfirst, colormaplength, colormapentry = 0, 0, 0
|
||||
|
||||
if im.mode == "RGBA":
|
||||
if im.mode in ("LA", "RGBA"):
|
||||
flags = 8
|
||||
else:
|
||||
flags = 0
|
||||
|
||||
orientation = im.info.get("orientation", -1)
|
||||
orientation = im.encoderinfo.get("orientation",
|
||||
im.info.get("orientation", -1))
|
||||
if orientation > 0:
|
||||
flags = flags | 0x20
|
||||
|
||||
fp.write(b"\000" +
|
||||
fp.write(o8(id_len) +
|
||||
o8(colormaptype) +
|
||||
o8(imagetype) +
|
||||
o16(colormapfirst) +
|
||||
|
|
@ -182,16 +198,29 @@ def _save(im, fp, filename, check=0):
|
|||
o8(bits) +
|
||||
o8(flags))
|
||||
|
||||
if id_section:
|
||||
fp.write(id_section)
|
||||
|
||||
if colormaptype:
|
||||
fp.write(im.im.getpalette("RGB", "BGR"))
|
||||
|
||||
ImageFile._save(
|
||||
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
|
||||
if rle:
|
||||
ImageFile._save(
|
||||
im,
|
||||
fp,
|
||||
[("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))])
|
||||
else:
|
||||
ImageFile._save(
|
||||
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
|
||||
|
||||
# write targa version 2 footer
|
||||
fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000")
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
|
||||
Image.register_open(TgaImageFile.format, TgaImageFile)
|
||||
Image.register_save(TgaImageFile.format, _save)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -23,12 +23,17 @@ from collections import namedtuple
|
|||
class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
|
||||
__slots__ = []
|
||||
|
||||
def __new__(cls, value=None, name="unknown", type=None, length=0, enum=None):
|
||||
def __new__(cls, value=None, name="unknown",
|
||||
type=None, length=None, enum=None):
|
||||
return super(TagInfo, cls).__new__(
|
||||
cls, value, name, type, length, enum or {})
|
||||
|
||||
def cvt_enum(self, value):
|
||||
return self.enum.get(value, value)
|
||||
# Using get will call hash(value), which can be expensive
|
||||
# for some types (e.g. Fraction). Since self.enum is rarely
|
||||
# used, it's usually better to test it first.
|
||||
return self.enum.get(value, value) if self.enum else value
|
||||
|
||||
|
||||
def lookup(tag):
|
||||
"""
|
||||
|
|
@ -36,9 +41,9 @@ def lookup(tag):
|
|||
:returns: Taginfo namedtuple, From the TAGS_V2 info if possible,
|
||||
otherwise just populating the value and name from TAGS.
|
||||
If the tag is not recognized, "unknown" is returned for the name
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, 'unknown')))
|
||||
|
||||
|
||||
|
|
@ -47,11 +52,25 @@ def lookup(tag):
|
|||
#
|
||||
# id: (Name, Type, Length, enum_values)
|
||||
#
|
||||
# The length here differs from the length in the tiff spec. For
|
||||
# numbers, the tiff spec is for the number of fields returned. We
|
||||
# agree here. For string-like types, the tiff spec uses the length of
|
||||
# field in bytes. In Pillow, we are using the number of expected
|
||||
# fields, in general 1 for string-like types.
|
||||
|
||||
|
||||
BYTE = 1
|
||||
ASCII = 2
|
||||
SHORT = 3
|
||||
LONG = 4
|
||||
RATIONAL = 5
|
||||
SIGNED_BYTE = 6
|
||||
UNDEFINED = 7
|
||||
SIGNED_SHORT = 8
|
||||
SIGNED_LONG = 9
|
||||
SIGNED_RATIONAL = 10
|
||||
FLOAT = 11
|
||||
DOUBLE = 12
|
||||
|
||||
TAGS_V2 = {
|
||||
|
||||
|
|
@ -61,17 +80,17 @@ TAGS_V2 = {
|
|||
257: ("ImageLength", LONG, 1),
|
||||
258: ("BitsPerSample", SHORT, 0),
|
||||
259: ("Compression", SHORT, 1,
|
||||
{"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4,
|
||||
"LZW": 5, "JPEG": 6, "PackBits": 32773}),
|
||||
{"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3,
|
||||
"Group 4 Fax": 4, "LZW": 5, "JPEG": 6, "PackBits": 32773}),
|
||||
|
||||
262: ("PhotometricInterpretation", SHORT, 1,
|
||||
{"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RBG Palette": 3,
|
||||
{"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RGB Palette": 3,
|
||||
"Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8,
|
||||
"CFA": 32803, # TIFF/EP, Adobe DNG
|
||||
"LinearRaw": 32892}), # Adobe DNG
|
||||
263: ("Thresholding", SHORT, 1),
|
||||
263: ("Threshholding", SHORT, 1),
|
||||
264: ("CellWidth", SHORT, 1),
|
||||
265: ("CellHeight", SHORT, 1),
|
||||
265: ("CellLength", SHORT, 1),
|
||||
266: ("FillOrder", SHORT, 1),
|
||||
269: ("DocumentName", ASCII, 1),
|
||||
|
||||
|
|
@ -88,7 +107,7 @@ TAGS_V2 = {
|
|||
281: ("MaxSampleValue", SHORT, 0),
|
||||
282: ("XResolution", RATIONAL, 1),
|
||||
283: ("YResolution", RATIONAL, 1),
|
||||
284: ("PlanarConfiguration", SHORT, 1, {"Contigous": 1, "Separate": 2}),
|
||||
284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}),
|
||||
285: ("PageName", ASCII, 1),
|
||||
286: ("XPosition", RATIONAL, 1),
|
||||
287: ("YPosition", RATIONAL, 1),
|
||||
|
|
@ -99,7 +118,7 @@ TAGS_V2 = {
|
|||
291: ("GrayResponseCurve", SHORT, 0),
|
||||
292: ("T4Options", LONG, 1),
|
||||
293: ("T6Options", LONG, 1),
|
||||
296: ("ResolutionUnit", SHORT, 1, {"inch": 1, "cm": 2}),
|
||||
296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}),
|
||||
297: ("PageNumber", SHORT, 2),
|
||||
|
||||
301: ("TransferFunction", SHORT, 0),
|
||||
|
|
@ -108,9 +127,9 @@ TAGS_V2 = {
|
|||
|
||||
315: ("Artist", ASCII, 1),
|
||||
316: ("HostComputer", ASCII, 1),
|
||||
317: ("Predictor", SHORT, 1),
|
||||
317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}),
|
||||
318: ("WhitePoint", RATIONAL, 2),
|
||||
319: ("PrimaryChromaticies", SHORT, 6),
|
||||
319: ("PrimaryChromaticities", RATIONAL, 6),
|
||||
|
||||
320: ("ColorMap", SHORT, 0),
|
||||
321: ("HalftoneHints", SHORT, 2),
|
||||
|
|
@ -127,10 +146,12 @@ TAGS_V2 = {
|
|||
338: ("ExtraSamples", SHORT, 0),
|
||||
339: ("SampleFormat", SHORT, 0),
|
||||
|
||||
340: ("SMinSampleValue", 12, 0),
|
||||
341: ("SMaxSampleValue", 12, 0),
|
||||
340: ("SMinSampleValue", DOUBLE, 0),
|
||||
341: ("SMaxSampleValue", DOUBLE, 0),
|
||||
342: ("TransferRange", SHORT, 6),
|
||||
|
||||
347: ("JPEGTables", UNDEFINED, 1),
|
||||
|
||||
# obsolete JPEG tags
|
||||
512: ("JPEGProc", SHORT, 1),
|
||||
513: ("JPEGInterchangeFormat", LONG, 1),
|
||||
|
|
@ -145,40 +166,43 @@ TAGS_V2 = {
|
|||
529: ("YCbCrCoefficients", RATIONAL, 3),
|
||||
530: ("YCbCrSubSampling", SHORT, 2),
|
||||
531: ("YCbCrPositioning", SHORT, 1),
|
||||
532: ("ReferenceBlackWhite", LONG, 0),
|
||||
532: ("ReferenceBlackWhite", RATIONAL, 6),
|
||||
|
||||
700: ('XMP', BYTE, 1),
|
||||
|
||||
33432: ("Copyright", ASCII, 1),
|
||||
34377: ('PhotoshopInfo', BYTE, 1),
|
||||
|
||||
# FIXME add more tags here
|
||||
34665: ("ExifIFD", SHORT, 1),
|
||||
34675: ('ICCProfile', 7, 0),
|
||||
34853: ('GPSInfoIFD', 1, 1),
|
||||
34675: ('ICCProfile', UNDEFINED, 1),
|
||||
34853: ('GPSInfoIFD', BYTE, 1),
|
||||
|
||||
# MPInfo
|
||||
45056: ("MPFVersion", 7, 1),
|
||||
45056: ("MPFVersion", UNDEFINED, 1),
|
||||
45057: ("NumberOfImages", LONG, 1),
|
||||
45058: ("MPEntry", 7, 1),
|
||||
45059: ("ImageUIDList", 7, 0),
|
||||
45058: ("MPEntry", UNDEFINED, 1),
|
||||
45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check
|
||||
45060: ("TotalFrames", LONG, 1),
|
||||
45313: ("MPIndividualNum", LONG, 1),
|
||||
45569: ("PanOrientation", LONG, 1),
|
||||
45570: ("PanOverlap_H", RATIONAL, 1),
|
||||
45571: ("PanOverlap_V", RATIONAL, 1),
|
||||
45572: ("BaseViewpointNum", LONG, 1),
|
||||
45573: ("ConvergenceAngle", 10, 1),
|
||||
45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1),
|
||||
45574: ("BaselineLength", RATIONAL, 1),
|
||||
45575: ("VerticalDivergence", 10, 1),
|
||||
45576: ("AxisDistance_X", 10, 1),
|
||||
45577: ("AxisDistance_Y", 10, 1),
|
||||
45578: ("AxisDistance_Z", 10, 1),
|
||||
45579: ("YawAngle", 10, 1),
|
||||
45580: ("PitchAngle", 10, 1),
|
||||
45581: ("RollAngle", 10, 1),
|
||||
45575: ("VerticalDivergence", SIGNED_RATIONAL, 1),
|
||||
45576: ("AxisDistance_X", SIGNED_RATIONAL, 1),
|
||||
45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1),
|
||||
45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1),
|
||||
45579: ("YawAngle", SIGNED_RATIONAL, 1),
|
||||
45580: ("PitchAngle", SIGNED_RATIONAL, 1),
|
||||
45581: ("RollAngle", SIGNED_RATIONAL, 1),
|
||||
|
||||
50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}),
|
||||
50780: ("BestQualityScale", RATIONAL, 1),
|
||||
50838: ("ImageJMetaDataByteCounts", LONG, 1),
|
||||
50839: ("ImageJMetaData", 7, 1)
|
||||
50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one
|
||||
50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006
|
||||
}
|
||||
|
||||
# Legacy Tags structure
|
||||
|
|
@ -187,10 +211,28 @@ TAGS = {347: 'JPEGTables',
|
|||
700: 'XMP',
|
||||
|
||||
# Additional Exif Info
|
||||
32932: 'Wang Annotation',
|
||||
33434: 'ExposureTime',
|
||||
33437: 'FNumber',
|
||||
33445: 'MD FileTag',
|
||||
33446: 'MD ScalePixel',
|
||||
33447: 'MD ColorTable',
|
||||
33448: 'MD LabName',
|
||||
33449: 'MD SampleInfo',
|
||||
33450: 'MD PrepDate',
|
||||
33451: 'MD PrepTime',
|
||||
33452: 'MD FileUnits',
|
||||
33550: 'ModelPixelScaleTag',
|
||||
33723: 'IptcNaaInfo',
|
||||
33918: 'INGR Packet Data Tag',
|
||||
33919: 'INGR Flag Registers',
|
||||
33920: 'IrasB Transformation Matrix',
|
||||
33922: 'ModelTiepointTag',
|
||||
34264: 'ModelTransformationTag',
|
||||
34377: 'PhotoshopInfo',
|
||||
34735: 'GeoKeyDirectoryTag',
|
||||
34736: 'GeoDoubleParamsTag',
|
||||
34737: 'GeoAsciiParamsTag',
|
||||
34850: 'ExposureProgram',
|
||||
34852: 'SpectralSensitivity',
|
||||
34855: 'ISOSpeedRatings',
|
||||
|
|
@ -201,11 +243,15 @@ TAGS = {347: 'JPEGTables',
|
|||
34867: 'ISOSpeed',
|
||||
34868: 'ISOSpeedLatitudeyyy',
|
||||
34869: 'ISOSpeedLatitudezzz',
|
||||
34908: 'HylaFAX FaxRecvParams',
|
||||
34909: 'HylaFAX FaxSubAddress',
|
||||
34910: 'HylaFAX FaxRecvTime',
|
||||
36864: 'ExifVersion',
|
||||
36867: 'DateTimeOriginal',
|
||||
36868: 'DateTImeDigitized',
|
||||
37121: 'ComponentsConfiguration',
|
||||
37122: 'CompressedBitsPerPixel',
|
||||
37724: 'ImageSourceData',
|
||||
37377: 'ShutterSpeedValue',
|
||||
37378: 'ApertureValue',
|
||||
37379: 'BrightnessValue',
|
||||
|
|
@ -258,7 +304,13 @@ TAGS = {347: 'JPEGTables',
|
|||
42035: 'LensMake',
|
||||
42036: 'LensModel',
|
||||
42037: 'LensSerialNumber',
|
||||
42112: 'GDAL_METADATA',
|
||||
42113: 'GDAL_NODATA',
|
||||
42240: 'Gamma',
|
||||
50215: 'Oce Scanjob Description',
|
||||
50216: 'Oce Application Selector',
|
||||
50217: 'Oce Identification Number',
|
||||
50218: 'Oce ImageLogic Characteristics',
|
||||
|
||||
# Adobe DNG
|
||||
50706: 'DNGVersion',
|
||||
|
|
@ -297,6 +349,7 @@ TAGS = {347: 'JPEGTables',
|
|||
50740: 'DNGPrivateData',
|
||||
50778: 'CalibrationIlluminant1',
|
||||
50779: 'CalibrationIlluminant2',
|
||||
50784: 'Alias Layer Metadata'
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -310,6 +363,7 @@ def _populate():
|
|||
|
||||
TAGS_V2[k] = TagInfo(k, *v)
|
||||
|
||||
|
||||
_populate()
|
||||
##
|
||||
# Map type numbers to type names -- defined in ImageFileDirectory.
|
||||
|
|
@ -336,64 +390,65 @@ TYPES = {}
|
|||
# These tags are handled by default in libtiff, without
|
||||
# adding to the custom dictionary. From tif_dir.c, searching for
|
||||
# case TIFFTAG in the _TIFFVSetField function:
|
||||
# Line: item.
|
||||
# 148: case TIFFTAG_SUBFILETYPE:
|
||||
# 151: case TIFFTAG_IMAGEWIDTH:
|
||||
# 154: case TIFFTAG_IMAGELENGTH:
|
||||
# 157: case TIFFTAG_BITSPERSAMPLE:
|
||||
# 181: case TIFFTAG_COMPRESSION:
|
||||
# 202: case TIFFTAG_PHOTOMETRIC:
|
||||
# 205: case TIFFTAG_THRESHHOLDING:
|
||||
# 208: case TIFFTAG_FILLORDER:
|
||||
# 214: case TIFFTAG_ORIENTATION:
|
||||
# 221: case TIFFTAG_SAMPLESPERPIXEL:
|
||||
# 228: case TIFFTAG_ROWSPERSTRIP:
|
||||
# 238: case TIFFTAG_MINSAMPLEVALUE:
|
||||
# 241: case TIFFTAG_MAXSAMPLEVALUE:
|
||||
# 244: case TIFFTAG_SMINSAMPLEVALUE:
|
||||
# 247: case TIFFTAG_SMAXSAMPLEVALUE:
|
||||
# 250: case TIFFTAG_XRESOLUTION:
|
||||
# 256: case TIFFTAG_YRESOLUTION:
|
||||
# 262: case TIFFTAG_PLANARCONFIG:
|
||||
# 268: case TIFFTAG_XPOSITION:
|
||||
# 271: case TIFFTAG_YPOSITION:
|
||||
# 274: case TIFFTAG_RESOLUTIONUNIT:
|
||||
# 280: case TIFFTAG_PAGENUMBER:
|
||||
# 284: case TIFFTAG_HALFTONEHINTS:
|
||||
# 288: case TIFFTAG_COLORMAP:
|
||||
# 294: case TIFFTAG_EXTRASAMPLES:
|
||||
# 298: case TIFFTAG_MATTEING:
|
||||
# 305: case TIFFTAG_TILEWIDTH:
|
||||
# 316: case TIFFTAG_TILELENGTH:
|
||||
# 327: case TIFFTAG_TILEDEPTH:
|
||||
# 333: case TIFFTAG_DATATYPE:
|
||||
# 344: case TIFFTAG_SAMPLEFORMAT:
|
||||
# 361: case TIFFTAG_IMAGEDEPTH:
|
||||
# 364: case TIFFTAG_SUBIFD:
|
||||
# 376: case TIFFTAG_YCBCRPOSITIONING:
|
||||
# 379: case TIFFTAG_YCBCRSUBSAMPLING:
|
||||
# 383: case TIFFTAG_TRANSFERFUNCTION:
|
||||
# 389: case TIFFTAG_REFERENCEBLACKWHITE:
|
||||
# 393: case TIFFTAG_INKNAMES:
|
||||
# Line: item.
|
||||
# 148: case TIFFTAG_SUBFILETYPE:
|
||||
# 151: case TIFFTAG_IMAGEWIDTH:
|
||||
# 154: case TIFFTAG_IMAGELENGTH:
|
||||
# 157: case TIFFTAG_BITSPERSAMPLE:
|
||||
# 181: case TIFFTAG_COMPRESSION:
|
||||
# 202: case TIFFTAG_PHOTOMETRIC:
|
||||
# 205: case TIFFTAG_THRESHHOLDING:
|
||||
# 208: case TIFFTAG_FILLORDER:
|
||||
# 214: case TIFFTAG_ORIENTATION:
|
||||
# 221: case TIFFTAG_SAMPLESPERPIXEL:
|
||||
# 228: case TIFFTAG_ROWSPERSTRIP:
|
||||
# 238: case TIFFTAG_MINSAMPLEVALUE:
|
||||
# 241: case TIFFTAG_MAXSAMPLEVALUE:
|
||||
# 244: case TIFFTAG_SMINSAMPLEVALUE:
|
||||
# 247: case TIFFTAG_SMAXSAMPLEVALUE:
|
||||
# 250: case TIFFTAG_XRESOLUTION:
|
||||
# 256: case TIFFTAG_YRESOLUTION:
|
||||
# 262: case TIFFTAG_PLANARCONFIG:
|
||||
# 268: case TIFFTAG_XPOSITION:
|
||||
# 271: case TIFFTAG_YPOSITION:
|
||||
# 274: case TIFFTAG_RESOLUTIONUNIT:
|
||||
# 280: case TIFFTAG_PAGENUMBER:
|
||||
# 284: case TIFFTAG_HALFTONEHINTS:
|
||||
# 288: case TIFFTAG_COLORMAP:
|
||||
# 294: case TIFFTAG_EXTRASAMPLES:
|
||||
# 298: case TIFFTAG_MATTEING:
|
||||
# 305: case TIFFTAG_TILEWIDTH:
|
||||
# 316: case TIFFTAG_TILELENGTH:
|
||||
# 327: case TIFFTAG_TILEDEPTH:
|
||||
# 333: case TIFFTAG_DATATYPE:
|
||||
# 344: case TIFFTAG_SAMPLEFORMAT:
|
||||
# 361: case TIFFTAG_IMAGEDEPTH:
|
||||
# 364: case TIFFTAG_SUBIFD:
|
||||
# 376: case TIFFTAG_YCBCRPOSITIONING:
|
||||
# 379: case TIFFTAG_YCBCRSUBSAMPLING:
|
||||
# 383: case TIFFTAG_TRANSFERFUNCTION:
|
||||
# 389: case TIFFTAG_REFERENCEBLACKWHITE:
|
||||
# 393: case TIFFTAG_INKNAMES:
|
||||
|
||||
# some of these are not in our TAGS_V2 dict and were included from tiff.h
|
||||
|
||||
LIBTIFF_CORE = set ([255, 256, 257, 258, 259, 262, 263, 266, 274, 277,
|
||||
278, 280, 281, 340, 341, 282, 283, 284, 286, 287,
|
||||
296, 297, 321, 320, 338, 32995, 322, 323, 32998,
|
||||
32996, 339, 32997, 330, 531, 530, 301, 532, 333,
|
||||
# as above
|
||||
269 # this has been in our tests forever, and works
|
||||
])
|
||||
# This list also exists in encode.c
|
||||
LIBTIFF_CORE = {255, 256, 257, 258, 259, 262, 263, 266, 274, 277,
|
||||
278, 280, 281, 340, 341, 282, 283, 284, 286, 287,
|
||||
296, 297, 321, 320, 338, 32995, 322, 323, 32998,
|
||||
32996, 339, 32997, 330, 531, 530, 301, 532, 333,
|
||||
# as above
|
||||
269 # this has been in our tests forever, and works
|
||||
}
|
||||
|
||||
LIBTIFF_CORE.remove(320) # Array of short, crashes
|
||||
LIBTIFF_CORE.remove(301) # Array of short, crashes
|
||||
LIBTIFF_CORE.remove(532) # Array of long, crashes
|
||||
LIBTIFF_CORE.remove(320) # Array of short, crashes
|
||||
LIBTIFF_CORE.remove(301) # Array of short, crashes
|
||||
LIBTIFF_CORE.remove(532) # Array of long, crashes
|
||||
|
||||
LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
|
||||
LIBTIFF_CORE.remove(322) # We don't have support for tiled images in libtiff
|
||||
LIBTIFF_CORE.remove(323) # Tiled images
|
||||
LIBTIFF_CORE.remove(333) # Ink Names either
|
||||
LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
|
||||
LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff
|
||||
LIBTIFF_CORE.remove(323) # Tiled images
|
||||
LIBTIFF_CORE.remove(333) # Ink Names either
|
||||
|
||||
# Note to advanced users: There may be combinations of these
|
||||
# parameters and values that when added properly, will work and
|
||||
|
|
|
|||
|
|
@ -18,12 +18,11 @@
|
|||
# the WalImageFile.open() function instead.
|
||||
|
||||
# This reader is based on the specification available from:
|
||||
# http://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml
|
||||
# https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml
|
||||
# and has been tested with a few sample files found using google.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image, _binary
|
||||
from . import Image
|
||||
from ._binary import i32le as i32
|
||||
|
||||
try:
|
||||
import builtins
|
||||
|
|
@ -31,48 +30,49 @@ except ImportError:
|
|||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
i32 = _binary.i32le
|
||||
|
||||
|
||||
##
|
||||
# Load texture from a Quake2 WAL texture file.
|
||||
# <p>
|
||||
# By default, a Quake2 standard palette is attached to the texture.
|
||||
# To override the palette, use the <b>putpalette</b> method.
|
||||
#
|
||||
# @param filename WAL file name, or an opened file handle.
|
||||
# @return An image instance.
|
||||
|
||||
def open(filename):
|
||||
"""
|
||||
Load texture from a Quake2 WAL texture file.
|
||||
|
||||
By default, a Quake2 standard palette is attached to the texture.
|
||||
To override the palette, use the <b>putpalette</b> method.
|
||||
|
||||
:param filename: WAL file name, or an opened file handle.
|
||||
:returns: An image instance.
|
||||
"""
|
||||
# FIXME: modify to return a WalImageFile instance instead of
|
||||
# plain Image object ?
|
||||
|
||||
def imopen(fp):
|
||||
# read header fields
|
||||
header = fp.read(32+24+32+12)
|
||||
size = i32(header, 32), i32(header, 36)
|
||||
offset = i32(header, 40)
|
||||
|
||||
# load pixel data
|
||||
fp.seek(offset)
|
||||
|
||||
Image._decompression_bomb_check(size)
|
||||
im = Image.frombytes("P", size, fp.read(size[0] * size[1]))
|
||||
im.putpalette(quake2palette)
|
||||
|
||||
im.format = "WAL"
|
||||
im.format_description = "Quake2 Texture"
|
||||
|
||||
# strings are null-terminated
|
||||
im.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||
next_name = header[56:56+32].split(b"\0", 1)[0]
|
||||
if next_name:
|
||||
im.info["next_name"] = next_name
|
||||
|
||||
return im
|
||||
|
||||
if hasattr(filename, "read"):
|
||||
fp = filename
|
||||
return imopen(filename)
|
||||
else:
|
||||
fp = builtins.open(filename, "rb")
|
||||
|
||||
# read header fields
|
||||
header = fp.read(32+24+32+12)
|
||||
size = i32(header, 32), i32(header, 36)
|
||||
offset = i32(header, 40)
|
||||
|
||||
# load pixel data
|
||||
fp.seek(offset)
|
||||
|
||||
im = Image.frombytes("P", size, fp.read(size[0] * size[1]))
|
||||
im.putpalette(quake2palette)
|
||||
|
||||
im.format = "WAL"
|
||||
im.format_description = "Quake2 Texture"
|
||||
|
||||
# strings are null-terminated
|
||||
im.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||
next_name = header[56:56+32].split(b"\0", 1)[0]
|
||||
if next_name:
|
||||
im.info["next_name"] = next_name
|
||||
|
||||
return im
|
||||
with builtins.open(filename, "rb") as fp:
|
||||
return imopen(fp)
|
||||
|
||||
|
||||
quake2palette = (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
from PIL import Image
|
||||
from PIL import ImageFile
|
||||
from . import Image, ImageFile
|
||||
try:
|
||||
from . import _webp
|
||||
SUPPORTED = True
|
||||
except ImportError:
|
||||
SUPPORTED = False
|
||||
from io import BytesIO
|
||||
from PIL import _webp
|
||||
|
||||
|
||||
_VALID_WEBP_MODES = {
|
||||
"RGBX": True,
|
||||
"RGBA": True,
|
||||
"RGB": True,
|
||||
}
|
||||
|
||||
_VALID_WEBP_LEGACY_MODES = {
|
||||
"RGB": True,
|
||||
"RGBA": True,
|
||||
}
|
||||
|
|
@ -21,7 +30,11 @@ def _accept(prefix):
|
|||
is_webp_file = prefix[8:12] == b"WEBP"
|
||||
is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
|
||||
|
||||
return is_riff_file_format and is_webp_file and is_valid_vp8_mode
|
||||
if is_riff_file_format and is_webp_file and is_valid_vp8_mode:
|
||||
if not SUPPORTED:
|
||||
return "image file could not be identified " \
|
||||
"because WEBP support not installed"
|
||||
return True
|
||||
|
||||
|
||||
class WebPImageFile(ImageFile.ImageFile):
|
||||
|
|
@ -30,32 +43,287 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
format_description = "WebP image"
|
||||
|
||||
def _open(self):
|
||||
data, width, height, self.mode, icc_profile, exif = \
|
||||
_webp.WebPDecode(self.fp.read())
|
||||
if not _webp.HAVE_WEBPANIM:
|
||||
# Legacy mode
|
||||
data, width, height, self.mode, icc_profile, exif = \
|
||||
_webp.WebPDecode(self.fp.read())
|
||||
if icc_profile:
|
||||
self.info["icc_profile"] = icc_profile
|
||||
if exif:
|
||||
self.info["exif"] = exif
|
||||
self._size = width, height
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
self._n_frames = 1
|
||||
return
|
||||
|
||||
# Use the newer AnimDecoder API to parse the (possibly) animated file,
|
||||
# and access muxed chunks like ICC/EXIF/XMP.
|
||||
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
|
||||
|
||||
# Get info from decoder
|
||||
width, height, loop_count, bgcolor, frame_count, mode = \
|
||||
self._decoder.get_info()
|
||||
self._size = width, height
|
||||
self.info["loop"] = loop_count
|
||||
bg_a, bg_r, bg_g, bg_b = \
|
||||
(bgcolor >> 24) & 0xFF, \
|
||||
(bgcolor >> 16) & 0xFF, \
|
||||
(bgcolor >> 8) & 0xFF, \
|
||||
bgcolor & 0xFF
|
||||
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
|
||||
self._n_frames = frame_count
|
||||
self.mode = 'RGB' if mode == 'RGBX' else mode
|
||||
self.rawmode = mode
|
||||
self.tile = []
|
||||
|
||||
# Attempt to read ICC / EXIF / XMP chunks from file
|
||||
icc_profile = self._decoder.get_chunk("ICCP")
|
||||
exif = self._decoder.get_chunk("EXIF")
|
||||
xmp = self._decoder.get_chunk("XMP ")
|
||||
if icc_profile:
|
||||
self.info["icc_profile"] = icc_profile
|
||||
if exif:
|
||||
self.info["exif"] = exif
|
||||
if xmp:
|
||||
self.info["xmp"] = xmp
|
||||
|
||||
self.size = width, height
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
# Initialize seek state
|
||||
self._reset(reset=False)
|
||||
self.seek(0)
|
||||
|
||||
def _getexif(self):
|
||||
from PIL.JpegImagePlugin import _getexif
|
||||
from .JpegImagePlugin import _getexif
|
||||
return _getexif(self)
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
return self._n_frames
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
return self._n_frames > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if not _webp.HAVE_WEBPANIM:
|
||||
return super(WebPImageFile, self).seek(frame)
|
||||
|
||||
# Perform some simple checks first
|
||||
if frame >= self._n_frames:
|
||||
raise EOFError("attempted to seek beyond end of sequence")
|
||||
if frame < 0:
|
||||
raise EOFError("negative frame index is not valid")
|
||||
|
||||
# Set logical frame to requested position
|
||||
self.__logical_frame = frame
|
||||
|
||||
def _reset(self, reset=True):
|
||||
if reset:
|
||||
self._decoder.reset()
|
||||
self.__physical_frame = 0
|
||||
self.__loaded = -1
|
||||
self.__timestamp = 0
|
||||
|
||||
def _get_next(self):
|
||||
# Get next frame
|
||||
ret = self._decoder.get_next()
|
||||
self.__physical_frame += 1
|
||||
|
||||
# Check if an error occurred
|
||||
if ret is None:
|
||||
self._reset() # Reset just to be safe
|
||||
self.seek(0)
|
||||
raise EOFError("failed to decode next frame in WebP file")
|
||||
|
||||
# Compute duration
|
||||
data, timestamp = ret
|
||||
duration = timestamp - self.__timestamp
|
||||
self.__timestamp = timestamp
|
||||
|
||||
# libwebp gives frame end, adjust to start of frame
|
||||
timestamp -= duration
|
||||
return data, timestamp, duration
|
||||
|
||||
def _seek(self, frame):
|
||||
if self.__physical_frame == frame:
|
||||
return # Nothing to do
|
||||
if frame < self.__physical_frame:
|
||||
self._reset() # Rewind to beginning
|
||||
while self.__physical_frame < frame:
|
||||
self._get_next() # Advance to the requested frame
|
||||
|
||||
def load(self):
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
if self.__loaded != self.__logical_frame:
|
||||
self._seek(self.__logical_frame)
|
||||
|
||||
# We need to load the image data for this frame
|
||||
data, timestamp, duration = self._get_next()
|
||||
self.info["timestamp"] = timestamp
|
||||
self.info["duration"] = duration
|
||||
self.__loaded = self.__logical_frame
|
||||
|
||||
# Set tile
|
||||
if self.fp and self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
|
||||
|
||||
return super(WebPImageFile, self).load()
|
||||
|
||||
def tell(self):
|
||||
if not _webp.HAVE_WEBPANIM:
|
||||
return super(WebPImageFile, self).tell()
|
||||
|
||||
return self.__logical_frame
|
||||
|
||||
|
||||
def _save_all(im, fp, filename):
|
||||
encoderinfo = im.encoderinfo.copy()
|
||||
append_images = list(encoderinfo.get("append_images", []))
|
||||
|
||||
# If total frame count is 1, then save using the legacy API, which
|
||||
# will preserve non-alpha modes
|
||||
total = 0
|
||||
for ims in [im]+append_images:
|
||||
total += 1 if not hasattr(ims, "n_frames") else ims.n_frames
|
||||
if total == 1:
|
||||
_save(im, fp, filename)
|
||||
return
|
||||
|
||||
background = (0, 0, 0, 0)
|
||||
if "background" in encoderinfo:
|
||||
background = encoderinfo["background"]
|
||||
elif "background" in im.info:
|
||||
background = im.info["background"]
|
||||
if isinstance(background, int):
|
||||
# GifImagePlugin stores a global color table index in
|
||||
# info["background"]. So it must be converted to an RGBA value
|
||||
palette = im.getpalette()
|
||||
if palette:
|
||||
r, g, b = palette[background*3:(background+1)*3]
|
||||
background = (r, g, b, 0)
|
||||
|
||||
duration = im.encoderinfo.get("duration", 0)
|
||||
loop = im.encoderinfo.get("loop", 0)
|
||||
minimize_size = im.encoderinfo.get("minimize_size", False)
|
||||
kmin = im.encoderinfo.get("kmin", None)
|
||||
kmax = im.encoderinfo.get("kmax", None)
|
||||
allow_mixed = im.encoderinfo.get("allow_mixed", False)
|
||||
verbose = False
|
||||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
method = im.encoderinfo.get("method", 0)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
xmp = im.encoderinfo.get("xmp", "")
|
||||
if allow_mixed:
|
||||
lossless = False
|
||||
|
||||
# Sensible keyframe defaults are from gif2webp.c script
|
||||
if kmin is None:
|
||||
kmin = 9 if lossless else 3
|
||||
if kmax is None:
|
||||
kmax = 17 if lossless else 5
|
||||
|
||||
# Validate background color
|
||||
if (not isinstance(background, (list, tuple)) or len(background) != 4 or
|
||||
not all(v >= 0 and v < 256 for v in background)):
|
||||
raise IOError("Background color is not an RGBA tuple clamped "
|
||||
"to (0-255): %s" % str(background))
|
||||
|
||||
# Convert to packed uint
|
||||
bg_r, bg_g, bg_b, bg_a = background
|
||||
background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0)
|
||||
|
||||
# Setup the WebP animation encoder
|
||||
enc = _webp.WebPAnimEncoder(
|
||||
im.size[0], im.size[1],
|
||||
background,
|
||||
loop,
|
||||
minimize_size,
|
||||
kmin, kmax,
|
||||
allow_mixed,
|
||||
verbose
|
||||
)
|
||||
|
||||
# Add each frame
|
||||
frame_idx = 0
|
||||
timestamp = 0
|
||||
cur_idx = im.tell()
|
||||
try:
|
||||
for ims in [im]+append_images:
|
||||
# Get # of frames in this image
|
||||
if not hasattr(ims, "n_frames"):
|
||||
nfr = 1
|
||||
else:
|
||||
nfr = ims.n_frames
|
||||
|
||||
for idx in range(nfr):
|
||||
ims.seek(idx)
|
||||
ims.load()
|
||||
|
||||
# Make sure image mode is supported
|
||||
frame = ims
|
||||
rawmode = ims.mode
|
||||
if ims.mode not in _VALID_WEBP_MODES:
|
||||
alpha = 'A' in ims.mode or 'a' in ims.mode \
|
||||
or (ims.mode == 'P' and
|
||||
'A' in ims.im.getpalettemode())
|
||||
rawmode = 'RGBA' if alpha else 'RGB'
|
||||
frame = ims.convert(rawmode)
|
||||
|
||||
if rawmode == 'RGB':
|
||||
# For faster conversion, use RGBX
|
||||
rawmode = 'RGBX'
|
||||
|
||||
# Append the frame to the animation encoder
|
||||
enc.add(
|
||||
frame.tobytes('raw', rawmode),
|
||||
timestamp,
|
||||
frame.size[0], frame.size[1],
|
||||
rawmode,
|
||||
lossless,
|
||||
quality,
|
||||
method
|
||||
)
|
||||
|
||||
# Update timestamp and frame index
|
||||
if isinstance(duration, (list, tuple)):
|
||||
timestamp += duration[frame_idx]
|
||||
else:
|
||||
timestamp += duration
|
||||
frame_idx += 1
|
||||
|
||||
finally:
|
||||
im.seek(cur_idx)
|
||||
|
||||
# Force encoder to flush frames
|
||||
enc.add(
|
||||
None,
|
||||
timestamp,
|
||||
0, 0, "", lossless, quality, 0
|
||||
)
|
||||
|
||||
# Get the final output from the encoder
|
||||
data = enc.assemble(icc_profile, exif, xmp)
|
||||
if data is None:
|
||||
raise IOError("cannot write file as WebP (encoder returned None)")
|
||||
|
||||
fp.write(data)
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
image_mode = im.mode
|
||||
if im.mode not in _VALID_WEBP_MODES:
|
||||
raise IOError("cannot write mode %s as WEBP" % image_mode)
|
||||
|
||||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
xmp = im.encoderinfo.get("xmp", "")
|
||||
|
||||
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
||||
alpha = 'A' in im.mode or 'a' in im.mode \
|
||||
or (im.mode == 'P' and 'A' in im.im.getpalettemode())
|
||||
im = im.convert('RGBA' if alpha else 'RGB')
|
||||
|
||||
data = _webp.WebPEncode(
|
||||
im.tobytes(),
|
||||
|
|
@ -65,16 +333,19 @@ def _save(im, fp, filename):
|
|||
float(quality),
|
||||
im.mode,
|
||||
icc_profile,
|
||||
exif
|
||||
exif,
|
||||
xmp
|
||||
)
|
||||
if data is None:
|
||||
raise IOError("cannot write file as WEBP (encoder returned None)")
|
||||
raise IOError("cannot write file as WebP (encoder returned None)")
|
||||
|
||||
fp.write(data)
|
||||
|
||||
|
||||
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
|
||||
Image.register_save(WebPImageFile.format, _save)
|
||||
|
||||
Image.register_extension(WebPImageFile.format, ".webp")
|
||||
Image.register_mime(WebPImageFile.format, "image/webp")
|
||||
if SUPPORTED:
|
||||
Image.register_save(WebPImageFile.format, _save)
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
Image.register_save_all(WebPImageFile.format, _save_all)
|
||||
Image.register_extension(WebPImageFile.format, ".webp")
|
||||
Image.register_mime(WebPImageFile.format, "image/webp")
|
||||
|
|
|
|||
|
|
@ -14,26 +14,37 @@
|
|||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
# WMF/EMF reference documentation:
|
||||
# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf
|
||||
# http://wvware.sourceforge.net/caolan/index.html
|
||||
# http://wvware.sourceforge.net/caolan/ora-wmf.html
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16le as word, si16le as short, \
|
||||
i32le as dword, si32le as _long
|
||||
from ._util import py3
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
_handler = None
|
||||
|
||||
if str != bytes:
|
||||
if py3:
|
||||
long = int
|
||||
|
||||
|
||||
##
|
||||
# Install application-specific WMF image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific WMF image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
||||
if hasattr(Image.core, "drawwmf"):
|
||||
# install default handler (windows only)
|
||||
|
||||
|
|
@ -53,24 +64,11 @@ if hasattr(Image.core, "drawwmf"):
|
|||
|
||||
register_handler(WmfHandler())
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
word = _binary.i16le
|
||||
|
||||
|
||||
def short(c, o=0):
|
||||
v = word(c, o)
|
||||
if v >= 32768:
|
||||
v -= 65536
|
||||
return v
|
||||
|
||||
dword = _binary.i32le
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Read WMF file
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return (
|
||||
prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or
|
||||
|
|
@ -111,8 +109,6 @@ class WmfStubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
self.info["dpi"] = 72
|
||||
|
||||
# print self.mode, self.size, self.info
|
||||
|
||||
# sanity check (standard metafile header)
|
||||
if s[22:26] != b"\x01\x00\t\x00":
|
||||
raise SyntaxError("Unsupported WMF file format")
|
||||
|
|
@ -121,13 +117,13 @@ class WmfStubImageFile(ImageFile.StubImageFile):
|
|||
# enhanced metafile
|
||||
|
||||
# get bounding box
|
||||
x0 = dword(s, 8)
|
||||
y0 = dword(s, 12)
|
||||
x1 = dword(s, 16)
|
||||
y1 = dword(s, 20)
|
||||
x0 = _long(s, 8)
|
||||
y0 = _long(s, 12)
|
||||
x1 = _long(s, 16)
|
||||
y1 = _long(s, 20)
|
||||
|
||||
# get frame (in 0.01 millimeter units)
|
||||
frame = dword(s, 24), dword(s, 28), dword(s, 32), dword(s, 36)
|
||||
frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36)
|
||||
|
||||
# normalize size to 72 dots per inch
|
||||
size = x1 - x0, y1 - y0
|
||||
|
|
@ -147,7 +143,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
|
|||
raise SyntaxError("Unsupported file format")
|
||||
|
||||
self.mode = "RGB"
|
||||
self.size = size
|
||||
self._size = size
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
|
|
@ -158,7 +154,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
|
|||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
if _handler is None or not hasattr(_handler, "save"):
|
||||
raise IOError("WMF save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
|
@ -166,8 +162,8 @@ def _save(im, fp, filename):
|
|||
# --------------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
|
||||
Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept)
|
||||
Image.register_save(WmfStubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(WmfStubImageFile.format, ".wmf")
|
||||
Image.register_extension(WmfStubImageFile.format, ".emf")
|
||||
Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"])
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@
|
|||
# FIXME: make save work (this requires quantization support)
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, o8
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
o8 = _binary.o8
|
||||
_MAGIC = b"P7 332"
|
||||
|
||||
# standard color palette for thumbnails (RGB332)
|
||||
PALETTE = b""
|
||||
|
|
@ -31,6 +32,10 @@ for r in range(8):
|
|||
PALETTE = PALETTE + (o8((r*255)//7)+o8((g*255)//7)+o8((b*255)//3))
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:6] == _MAGIC
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for XV thumbnail images.
|
||||
|
||||
|
|
@ -42,8 +47,7 @@ class XVThumbImageFile(ImageFile.ImageFile):
|
|||
def _open(self):
|
||||
|
||||
# check magic
|
||||
s = self.fp.read(6)
|
||||
if s != b"P7 332":
|
||||
if not _accept(self.fp.read(6)):
|
||||
raise SyntaxError("not an XV thumbnail file")
|
||||
|
||||
# Skip to beginning of next line
|
||||
|
|
@ -54,14 +58,14 @@ class XVThumbImageFile(ImageFile.ImageFile):
|
|||
s = self.fp.readline()
|
||||
if not s:
|
||||
raise SyntaxError("Unexpected EOF reading XV thumbnail file")
|
||||
if s[0] != b'#':
|
||||
if i8(s[0]) != 35: # ie. when not a comment: '#'
|
||||
break
|
||||
|
||||
# parse header line (already read)
|
||||
s = s.strip().split()
|
||||
|
||||
self.mode = "P"
|
||||
self.size = int(s[0:1]), int(s[1:2])
|
||||
self._size = int(s[0]), int(s[1])
|
||||
|
||||
self.palette = ImagePalette.raw("RGB", PALETTE)
|
||||
|
||||
|
|
@ -70,6 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile):
|
|||
self.fp.tell(), (self.mode, 0, 1)
|
||||
)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(XVThumbImageFile.format, XVThumbImageFile)
|
||||
Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept)
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@
|
|||
#
|
||||
|
||||
import re
|
||||
from PIL import Image, ImageFile
|
||||
from . import Image, ImageFile
|
||||
|
||||
__version__ = "0.6"
|
||||
|
||||
# XBM header
|
||||
xbm_head = re.compile(
|
||||
b"\s*#define[ \t]+.*_width[ \t]+(?P<width>[0-9]+)[\r\n]+"
|
||||
br"\s*#define[ \t]+.*_width[ \t]+(?P<width>[0-9]+)[\r\n]+"
|
||||
b"#define[ \t]+.*_height[ \t]+(?P<height>[0-9]+)[\r\n]+"
|
||||
b"(?P<hotspot>"
|
||||
b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+"
|
||||
|
|
@ -63,7 +63,7 @@ class XbmImageFile(ImageFile.ImageFile):
|
|||
)
|
||||
|
||||
self.mode = "1"
|
||||
self.size = xsize, ysize
|
||||
self._size = xsize, ysize
|
||||
|
||||
self.tile = [("xbm", (0, 0)+self.size, m.end(), None)]
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
|
||||
import re
|
||||
from PIL import Image, ImageFile, ImagePalette
|
||||
from PIL._binary import i8, o8
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, o8
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ class XpmImageFile(ImageFile.ImageFile):
|
|||
if m:
|
||||
break
|
||||
|
||||
self.size = int(m.group(1)), int(m.group(2))
|
||||
self._size = int(m.group(1)), int(m.group(2))
|
||||
|
||||
pal = int(m.group(3))
|
||||
bpp = int(m.group(4))
|
||||
|
|
@ -116,13 +116,12 @@ class XpmImageFile(ImageFile.ImageFile):
|
|||
for i in range(ysize):
|
||||
s[i] = self.fp.readline()[1:xsize+1].ljust(xsize)
|
||||
|
||||
self.fp = None
|
||||
|
||||
return b"".join(s)
|
||||
|
||||
#
|
||||
# Registry
|
||||
|
||||
|
||||
Image.register_open(XpmImageFile.format, XpmImageFile, _accept)
|
||||
|
||||
Image.register_extension(XpmImageFile.format, ".xpm")
|
||||
|
|
|
|||
|
|
@ -1,27 +1,41 @@
|
|||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# package placeholder
|
||||
#
|
||||
# Copyright (c) 1999 by Secret Labs AB.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
"""Pillow (Fork of the Python Imaging Library)
|
||||
|
||||
# ;-)
|
||||
Pillow is the friendly PIL fork by Alex Clark and Contributors.
|
||||
https://github.com/python-pillow/Pillow/
|
||||
|
||||
VERSION = '1.1.7' # PIL version
|
||||
PILLOW_VERSION = '3.1.0' # Pillow
|
||||
Pillow is forked from PIL 1.1.7.
|
||||
|
||||
_plugins = ['BmpImagePlugin',
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
|
||||
Copyright (c) 1999 by Secret Labs AB.
|
||||
|
||||
Use PIL.__version__ for this Pillow version.
|
||||
PIL.VERSION is the old PIL version and will be removed in the future.
|
||||
|
||||
;-)
|
||||
"""
|
||||
|
||||
from . import _version
|
||||
|
||||
# VERSION is deprecated and will be removed in Pillow 6.0.0.
|
||||
# PILLOW_VERSION is deprecated and will be removed after that.
|
||||
# Use __version__ instead.
|
||||
VERSION = '1.1.7' # PIL Version
|
||||
PILLOW_VERSION = __version__ = _version.__version__
|
||||
|
||||
del _version
|
||||
|
||||
|
||||
_plugins = ['BlpImagePlugin',
|
||||
'BmpImagePlugin',
|
||||
'BufrStubImagePlugin',
|
||||
'CurImagePlugin',
|
||||
'DcxImagePlugin',
|
||||
'DdsImagePlugin',
|
||||
'EpsImagePlugin',
|
||||
'FitsStubImagePlugin',
|
||||
'FliImagePlugin',
|
||||
'FpxImagePlugin',
|
||||
'FtexImagePlugin',
|
||||
'GbrImagePlugin',
|
||||
'GifImagePlugin',
|
||||
'GribStubImagePlugin',
|
||||
|
|
|
|||
BIN
Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/ExifTags.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/ExifTags.cpython-37.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-37.pyc
Normal file
BIN
Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-37.pyc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue