diff --git a/build/.idea/build.iml b/build/.idea/build.iml index 4eda48f..14585d1 100644 --- a/build/.idea/build.iml +++ b/build/.idea/build.iml @@ -4,7 +4,7 @@ - + diff --git a/server/www/packages/packages-windows/x86/PIL/BdfFontFile.py b/server/www/packages/packages-windows/x86/PIL/BdfFontFile.py index e6cc22f..c8bc604 100644 --- a/server/www/packages/packages-windows/x86/PIL/BdfFontFile.py +++ b/server/www/packages/packages-windows/x86/PIL/BdfFontFile.py @@ -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 # -------------------------------------------------------------------- @@ -119,9 +120,9 @@ class BdfFontFile(FontFile.FontFile): # fontname = ";".join(font[1:]) - # print "#", fontname + # print("#", fontname) # for i in comments: - # print "#", i + # print("#", i) while True: c = bdf_char(fp) diff --git a/server/www/packages/packages-windows/x86/PIL/BlpImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/BlpImagePlugin.py new file mode 100644 index 0000000..9b1a99a --- /dev/null +++ b/server/www/packages/packages-windows/x86/PIL/BlpImagePlugin.py @@ -0,0 +1,435 @@ +""" +Blizzard Mipmap Format (.blp) +Jerome Leclanche + +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("> 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(">= 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("> 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(" 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:]) + # 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") diff --git a/server/www/packages/packages-windows/x86/PIL/DcxImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/DcxImagePlugin.py index f9034d1..2045927 100644 --- a/server/www/packages/packages-windows/x86/PIL/DcxImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/DcxImagePlugin.py @@ -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]) diff --git a/server/www/packages/packages-windows/x86/PIL/DdsImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/DdsImagePlugin.py index 2ebfdf0..e755f94 100644 --- a/server/www/packages/packages-windows/x86/PIL/DdsImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/DdsImagePlugin.py @@ -3,7 +3,7 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: - http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + 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: @@ -12,7 +12,7 @@ Full text of the CC0 license: import struct from io import BytesIO -from PIL import Image, ImageFile +from . import Image, ImageFile # Magic ("DDS ") @@ -93,118 +93,11 @@ DXT3_FOURCC = 0x33545844 DXT5_FOURCC = 0x35545844 -def _decode565(bits): - a = ((bits >> 11) & 0x1f) << 3 - b = ((bits >> 5) & 0x3f) << 2 - c = (bits & 0x1f) << 3 - return a, b, c +# dxgiformat.h - -def _c2a(a, b): - return (2 * a + b) // 3 - - -def _c2b(a, b): - return (a + b) // 2 - - -def _c3(a, b): - return (2 * b + a) // 3 - - -def _dxt1(data, width, height): - # TODO implement this function as pixel format in decode.c - ret = bytearray(4 * width * height) - - for y in range(0, height, 4): - for x in range(0, width, 4): - color0, color1, bits = struct.unpack("> 2 - 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, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) - else: - r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1) - elif control == 3: - if color0 > color1: - r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) - else: - r, g, b = 0, 0, 0 - - idx = 4 * ((y + j) * width + (x + i)) - ret[idx:idx+4] = struct.pack('4B', r, g, b, 255) - - return bytes(ret) - - -def _dxtc_alpha(a0, a1, ac0, ac1, ai): - if ai <= 12: - ac = (ac0 >> ai) & 7 - elif ai == 15: - ac = (ac0 >> 15) | ((ac1 << 1) & 6) - else: - ac = (ac1 >> (ai - 16)) & 7 - - if ac == 0: - alpha = a0 - elif ac == 1: - alpha = a1 - elif a0 > a1: - alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7 - elif ac == 6: - alpha = 0 - elif ac == 7: - alpha = 0xff - else: - alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 - - return alpha - - -def _dxt5(data, width, height): - # TODO implement this function as pixel format in decode.c - ret = bytearray(4 * width * height) - - for y in range(0, height, 4): - for x in range(0, width, 4): - a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", - data.read(16)) - - r0, g0, b0 = _decode565(c0) - r1, g1, b1 = _decode565(c1) - - for j in range(4): - for i in range(4): - ai = 3 * (4 * j + i) - alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai) - - cc = (code >> 2 * (4 * j + i)) & 3 - if cc == 0: - r, g, b = r0, g0, b0 - elif cc == 1: - r, g, b = r1, g1, b1 - elif cc == 2: - r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) - elif cc == 3: - r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) - - idx = 4 * ((y + j) * width + (x + i)) - ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha) - - return bytes(ret) +DXGI_FORMAT_BC7_TYPELESS = 97 +DXGI_FORMAT_BC7_UNORM = 98 +DXGI_FORMAT_BC7_UNORM_SRGB = 99 class DdsImageFile(ImageFile.ImageFile): @@ -233,28 +126,39 @@ class DdsImageFile(ImageFile.ImageFile): bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I", header.read(20)) - self.tile = [ - ("raw", (0, 0) + self.size, 0, (self.mode, 0, 1)) - ] - + data_start = header_size + 4 + n = 0 if fourcc == b"DXT1": self.pixel_format = "DXT1" - codec = _dxt1 + n = 1 + elif fourcc == b"DXT3": + self.pixel_format = "DXT3" + n = 2 elif fourcc == b"DXT5": self.pixel_format = "DXT5" - codec = _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(" 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 as v: + raise SyntaxError("not an EPS file") - if m: - k, v = m.group(1, 2) - self.info[k] = v - if k == "BoundingBox": - try: - # Note: The DSC spec says that BoundingBox - # fields should be integers, but some drivers - # put floating point values there anyway. - box = [int(float(i)) for i in v.split()] - self.size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, - (length, box))] - except: - pass - - else: - m = field.match(s) if m: - k = m.group(1) + 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 - 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 # @@ -322,7 +323,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: @@ -379,7 +380,7 @@ def _save(im, fp, filename, eps=1): base_fp = fp 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') if eps: @@ -418,11 +419,11 @@ def _save(im, fp, filename, eps=1): # # -------------------------------------------------------------------- + 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") diff --git a/server/www/packages/packages-windows/x86/PIL/FitsStubImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/FitsStubImagePlugin.py index 7aefff2..be926ca 100644 --- a/server/www/packages/packages-windows/x86/PIL/FitsStubImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/FitsStubImagePlugin.py @@ -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 @@ -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"]) diff --git a/server/www/packages/packages-windows/x86/PIL/FliImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/FliImagePlugin.py index a07dc29..2c190b6 100644 --- a/server/www/packages/packages-windows/x86/PIL/FliImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/FliImagePlugin.py @@ -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,6 +49,9 @@ 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]) @@ -59,7 +59,7 @@ class FliImageFile(ImageFile.ImageFile): # 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,43 +111,20 @@ 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: @@ -179,10 +154,10 @@ class FliImageFile(ImageFile.ImageFile): def tell(self): return self.__frame + # # 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"]) diff --git a/server/www/packages/packages-windows/x86/PIL/FontFile.py b/server/www/packages/packages-windows/x86/PIL/FontFile.py index db8e6be..46e49bc 100644 --- a/server/www/packages/packages-windows/x86/PIL/FontFile.py +++ b/server/www/packages/packages-windows/x86/PIL/FontFile.py @@ -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,7 @@ 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 + # print(chr(i), dst, s) self.metrics[i] = d, dst, s def save(self, filename): @@ -100,16 +102,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]) diff --git a/server/www/packages/packages-windows/x86/PIL/FpxImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/FpxImagePlugin.py index aefc574..d7bba42 100644 --- a/server/www/packages/packages-windows/x86/PIL/FpxImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/FpxImagePlugin.py @@ -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") @@ -112,7 +114,7 @@ class FpxImageFile(ImageFile.ImageFile): if id in prop: self.jpeg[i] = prop[id] - # print len(self.jpeg), "tables loaded" + # print(len(self.jpeg), "tables loaded") self._open_subimage(1, self.maxid) @@ -141,7 +143,7 @@ class FpxImageFile(ImageFile.ImageFile): offset = i32(s, 28) length = i32(s, 32) - # print size, self.mode, self.rawmode + # print(size, self.mode, self.rawmode) if size != self.size: raise IOError("subimage mismatch") @@ -216,11 +218,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") diff --git a/server/www/packages/packages-windows/x86/PIL/FtexImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/FtexImagePlugin.py index f3a2d7f..9b98090 100644 --- a/server/www/packages/packages-windows/x86/PIL/FtexImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/FtexImagePlugin.py @@ -13,7 +13,7 @@ packed custom format called FTEX. This file format uses file extensions FTC and * 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, called. This +The FTC and FTU texture files both use the same format. This has the following structure: {header} {format_directory} @@ -42,8 +42,7 @@ Note: All data is stored in little-Endian (Intel) byte order. import struct from io import BytesIO -from PIL import Image, ImageFile -from PIL.DdsImagePlugin import _dxt1 +from . import Image, ImageFile MAGIC = b"FTEX" @@ -73,8 +72,8 @@ class FtexImageFile(ImageFile.ImageFile): data = self.fp.read(mipmap_size) if format == FORMAT_DXT1: - data = _dxt1(BytesIO(data), self.width, self.height) - self.tile = [("raw", (0, 0) + self.size, 0, ('RGBX', 0, 1))] + 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: @@ -92,5 +91,4 @@ def _validate(prefix): Image.register_open(FtexImageFile.format, FtexImageFile, _validate) -Image.register_extension(FtexImageFile.format, ".ftc") -Image.register_extension(FtexImageFile.format, ".ftu") +Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/server/www/packages/packages-windows/x86/PIL/GbrImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/GbrImagePlugin.py index d62981c..bd4c12f 100644 --- a/server/www/packages/packages-windows/x86/PIL/GbrImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/GbrImagePlugin.py @@ -24,9 +24,8 @@ # 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): @@ -90,5 +89,6 @@ class GbrImageFile(ImageFile.ImageFile): # # registry + Image.register_open(GbrImageFile.format, GbrImageFile, _accept) Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/server/www/packages/packages-windows/x86/PIL/GdImageFile.py b/server/www/packages/packages-windows/x86/PIL/GdImageFile.py index ae3500f..2ca1e82 100644 --- a/server/www/packages/packages-windows/x86/PIL/GdImageFile.py +++ b/server/www/packages/packages-windows/x86/PIL/GdImageFile.py @@ -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,41 @@ 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") diff --git a/server/www/packages/packages-windows/x86/PIL/GifImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/GifImagePlugin.py index 6bca4dd..1bfbb5f 100644 --- a/server/www/packages/packages-windows/x86/PIL/GifImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/GifImagePlugin.py @@ -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): @@ -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) @@ -262,7 +260,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 @@ -285,7 +283,7 @@ 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 @@ -294,52 +292,173 @@ class GifImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # 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) + im_out = _normalize_palette(im_out, palette, im.encoderinfo) + + for s in _get_global_header(im_out, im.encoderinfo): + fp.write(s) + + # local image header + flags = 0 + if get_interlace(im): + flags = flags | 64 + _write_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, + RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data + + +def _write_multiple_frames(im, fp, palette): + + duration = im.encoderinfo.get("duration", None) + disposal = im.encoderinfo.get('disposal', None) + + im_frames = [] + frame_count = 0 + for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): + for im_frame in ImageSequence.Iterator(imSequence): + # a copy is required here since seek can still mutate the image + im_frame = _normalize_mode(im_frame.copy()) + im_frame = _normalize_palette(im_frame, palette, im.encoderinfo) + + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo['duration'] = duration[frame_count] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + frame_count += 1 + + if im_frames: + # delta frame + previous = im_frames[-1] + if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']): + delta = ImageChops.subtract_modulo(im_frame, + previous['im']) + else: + delta = ImageChops.subtract_modulo(im_frame.convert('RGB'), + previous['im'].convert('RGB')) + bbox = delta.getbbox() + if not bbox: + # This frame is identical to the previous frame + if duration: + previous['encoderinfo']['duration'] += encoderinfo['duration'] + continue + else: + bbox = None + im_frames.append({ + 'im': im_frame, + 'bbox': bbox, + 'encoderinfo': encoderinfo + }) + + if len(im_frames) > 1: + for frame_data in im_frames: + im_frame = frame_data['im'] + if not frame_data['bbox']: + # global header + for s in _get_global_header(im_frame, + frame_data['encoderinfo']): + fp.write(s) + offset = (0, 0) + else: + # compress difference + frame_data['encoderinfo']['include_color_table'] = True + + im_frame = im_frame.crop(frame_data['bbox']) + offset = frame_data['bbox'][:2] + _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo']) + return True + + def _save_all(im, fp, filename): _save(im, fp, filename, save_all=True) def _save(im, fp, filename, save_all=False): - - 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) - + for k, v in im.info.items(): + im.encoderinfo.setdefault(k, v) # header try: palette = im.encoderinfo["palette"] @@ -347,58 +466,8 @@ def _save(im, fp, filename, save_all=False): 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 @@ -407,10 +476,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: @@ -419,7 +485,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"] @@ -430,32 +496,31 @@ 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)) @@ -476,17 +541,30 @@ 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 = im.encoderinfo.get("palette", None) + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + if color_table_size: + flags = flags | 128 # local color table flag + flags = flags | color_table_size + fp.write(b"," + o16(offset[0]) + # offset o16(offset[1]) + o16(im.size[0]) + # size o16(im.size[1]) + - o8(flags) + # flags - 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 @@ -494,25 +572,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() @@ -531,27 +605,84 @@ 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. + + :param im: Image object + :param info: encoderinfo + :returns: list of indexes of palette entries in use, or None + """ + if im.mode in ("P", "L") and info and info.get("optimize", 0): + # Potentially expensive operation. + + # The palette saves 3 bytes per color not used, but palette + # lengths are restricted to 3*(2**N) bytes. Max saving would + # be 768 -> 6 bytes if we went all the way down to 2 colors. + # * If we're over 128 colors, we can't save any space. + # * If there aren't any holes, it's not worth collapsing. + # * If we have a 'large' image, the palette is in the noise. + + # create the new palette if not every color is used + optimise = _FORCE_OPTIMIZE or im.mode == 'L' + if optimise or im.width * im.height < 512 * 512: + # check which colors are used + used_palette_colors = [] + for i, count in enumerate(im.histogram()): + if count: + used_palette_colors.append(i) + + if optimise or (len(used_palette_colors) <= 128 and + max(used_palette_colors) > len(used_palette_colors)): + return used_palette_colors -def _get_used_palette_colors(im): - used_palette_colors = [] - - # 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 _get_color_table_size(palette_bytes): + # calculate the palette size for the header + import math + color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1 + if color_table_size < 0: + color_table_size = 0 + return color_table_size -def getheader(im, palette=None, info=None): +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< 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 @@ -561,101 +692,97 @@ def getheader(im, palette=None, info=None): 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))): + (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): continue version = b"89a" break else: - if im.info.get("version") == "89a": + if im.info.get("version") == b"89a": version = b"89a" - header = [ - b"GIF"+version + # signature + version - o16(im.size[0]) + # canvas width - o16(im.size[1]) # canvas height + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + + background = info["background"] if "background" in info else 0 + + return [ + b"GIF"+version + # signature + version + o16(im.size[0]) + # canvas width + o16(im.size[1]), # canvas height + + # Logical Screen Descriptor + # size of global color table + global color table flag + o8(color_table_size + 128), # packed fields + # background + reserved/aspect + o8(background) + o8(0), + + # Global Color Table + _get_header_palette(palette_bytes) ] - 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 +def _write_frame_data(fp, im_frame, offset, params): + try: + im_frame.encoderinfo = params - if _get_optimize(im, info): - used_palette_colors = _get_used_palette_colors(im) + # local image header + _write_local_header(fp, im_frame, offset, 0) - # create the new palette if not every color is used - if len(used_palette_colors) < 256: - palette_bytes = b"" - new_positions = {} + ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0, + RAWMODE[im_frame.mode])]) - 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 + fp.write(b"\0") # end of image data + finally: + del im_frame.encoderinfo - # 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)) +# -------------------------------------------------------------------- +# Legacy GIF utilities - if not palette_bytes: - palette_bytes = source_palette - # Logical Screen Descriptor - # 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 +def getheader(im, palette=None, info=None): + """ + Legacy Method to get Gif data from image. - # add the missing amount of bytes - # the palette has to be 2< 0: - palette_bytes += o8(0) * 3 * actual_target_size_diff + 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 = [] @@ -666,18 +793,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 diff --git a/server/www/packages/packages-windows/x86/PIL/GimpGradientFile.py b/server/www/packages/packages-windows/x86/PIL/GimpGradientFile.py index 45af573..10593da 100644 --- a/server/www/packages/packages-windows/x86/PIL/GimpGradientFile.py +++ b/server/www/packages/packages-windows/x86/PIL/GimpGradientFile.py @@ -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] diff --git a/server/www/packages/packages-windows/x86/PIL/GimpPaletteFile.py b/server/www/packages/packages-windows/x86/PIL/GimpPaletteFile.py index 4bf3ca3..6eef6a2 100644 --- a/server/www/packages/packages-windows/x86/PIL/GimpPaletteFile.py +++ b/server/www/packages/packages-windows/x86/PIL/GimpPaletteFile.py @@ -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") diff --git a/server/www/packages/packages-windows/x86/PIL/GribStubImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/GribStubImagePlugin.py index 8ffad81..33c8291 100644 --- a/server/www/packages/packages-windows/x86/PIL/GribStubImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/GribStubImagePlugin.py @@ -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): diff --git a/server/www/packages/packages-windows/x86/PIL/Hdf5StubImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/Hdf5StubImagePlugin.py index f7945be..de4d5bb 100644 --- a/server/www/packages/packages-windows/x86/PIL/Hdf5StubImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/Hdf5StubImagePlugin.py @@ -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 @@ -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"]) diff --git a/server/www/packages/packages-windows/x86/PIL/IcnsImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/IcnsImagePlugin.py index a4366e9..dc93f6a 100644 --- a/server/www/packages/packages-windows/x86/PIL/IcnsImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/IcnsImagePlugin.py @@ -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 @@ -302,36 +301,39 @@ 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 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 +345,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 +357,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") diff --git a/server/www/packages/packages-windows/x86/PIL/IcoImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/IcoImagePlugin.py index a01aed3..428fdd4 100644 --- a/server/www/packages/packages-windows/x86/PIL/IcoImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/IcoImagePlugin.py @@ -25,7 +25,8 @@ 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(" 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,13 @@ class ImImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))] def tell(self): - return self.frame # # -------------------------------------------------------------------- # Save IM files + SAVE = { # mode: (im type, raw mode) "1": ("0 1", "1"), @@ -318,20 +315,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 +340,7 @@ def _save(im, fp, filename, check=0): # -------------------------------------------------------------------- # Registry + Image.register_open(ImImageFile.format, ImImageFile) Image.register_save(ImImageFile.format, _save) diff --git a/server/www/packages/packages-windows/x86/PIL/Image.py b/server/www/packages/packages-windows/x86/PIL/Image.py index 64f4613..c589526 100644 --- a/server/www/packages/packages-windows/x86/PIL/Image.py +++ b/server/www/packages/packages-windows/x86/PIL/Image.py @@ -24,9 +24,11 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - -from PIL import VERSION, PILLOW_VERSION, _plugins +# 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. +from . import VERSION, PILLOW_VERSION, __version__, _plugins +from ._util import py3 import logging import warnings @@ -39,6 +41,10 @@ class DecompressionBombWarning(RuntimeWarning): pass +class DecompressionBombError(Exception): + pass + + class _imaging_not_installed(object): # module placeholder def __getattr__(self, id): @@ -46,17 +52,8 @@ class _imaging_not_installed(object): # Limit to around a quarter gigabyte for a 24 bit (3 bpp) image -MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 / 4 / 3) +MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass try: # If the _imaging C module is not present, Pillow will not load. @@ -64,10 +61,14 @@ try: # import Image and use the Image.core variable instead. # Also note that Image.core is not a publicly documented interface, # and should be considered private and subject to change. - from PIL import _imaging as core - if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): + from . import _imaging as core + if __version__ != getattr(core, 'PILLOW_VERSION', None): raise ImportError("The _imaging extension was built for another " - " version of Pillow or PIL") + "version of Pillow or PIL:\n" + "Core version: %s\n" + "Pillow version: %s" % + (getattr(core, 'PILLOW_VERSION', None), + __version__)) except ImportError as v: core = _imaging_not_installed() @@ -109,20 +110,25 @@ except ImportError: import __builtin__ builtins = __builtin__ -from PIL import ImageMode -from PIL._binary import i8 -from PIL._util import isPath -from PIL._util import isStringType -from PIL._util import deferred_error +from . import ImageMode +from ._binary import i8 +from ._util import isPath, isStringType, deferred_error import os import sys import io import struct +import atexit # type stuff -import collections import numbers +try: + # Python 3 + from collections.abc import Callable +except ImportError: + # Python 2.7 + from collections import Callable + # works everywhere, win for pypy, not cpython USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') @@ -132,6 +138,16 @@ try: except ImportError: HAS_CFFI = False +try: + from pathlib import Path + HAS_PATHLIB = True +except ImportError: + try: + from pathlib2 import Path + HAS_PATHLIB = True + except ImportError: + HAS_PATHLIB = False + def isImageType(t): """ @@ -146,6 +162,7 @@ def isImageType(t): """ return hasattr(t, "im") + # # Constants (also defined in _imagingmodule.c!) @@ -158,6 +175,7 @@ ROTATE_90 = 2 ROTATE_180 = 3 ROTATE_270 = 4 TRANSPOSE = 5 +TRANSVERSE = 6 # transforms AFFINE = 0 @@ -168,13 +186,14 @@ MESH = 4 # resampling filters NEAREST = NONE = 0 -LANCZOS = ANTIALIAS = 1 +BOX = 4 BILINEAR = LINEAR = 2 +HAMMING = 5 BICUBIC = CUBIC = 3 +LANCZOS = ANTIALIAS = 1 # dithers -NONE = 0 -NEAREST = 0 +NEAREST = NONE = 0 ORDERED = 1 # Not yet implemented RASTERIZE = 2 # Not yet implemented FLOYDSTEINBERG = 3 # default @@ -210,6 +229,8 @@ MIME = {} SAVE = {} SAVE_ALL = {} EXTENSION = {} +DECODERS = {} +ENCODERS = {} # -------------------------------------------------------------------- # Modes supported by this version @@ -245,7 +266,7 @@ else: _MODE_CONV = { # official modes - "1": ('|b1', None), # broken + "1": ('|b1', None), # Bits need to be extended to bytes "L": ('|u1', None), "LA": ('|u1', 2), "I": (_ENDIAN + 'i4', None), @@ -275,15 +296,14 @@ _MODE_CONV = { def _conv_type_shape(im): - shape = im.size[1], im.size[0] typ, extra = _MODE_CONV[im.mode] if extra is None: - return shape, typ + return (im.size[1], im.size[0]), typ else: - return shape+(extra,), typ + return (im.size[1], im.size[0], extra), typ -MODES = sorted(_MODEINFO.keys()) +MODES = sorted(_MODEINFO) # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! @@ -340,6 +360,7 @@ def getmodebands(mode): """ return len(ImageMode.getmode(mode).bands) + # -------------------------------------------------------------------- # Helpers @@ -347,30 +368,30 @@ _initialized = 0 def preinit(): - "Explicitly load standard file format drivers." + """Explicitly load standard file format drivers.""" global _initialized if _initialized >= 1: return try: - from PIL import BmpImagePlugin + from . import BmpImagePlugin except ImportError: pass try: - from PIL import GifImagePlugin + from . import GifImagePlugin except ImportError: pass try: - from PIL import JpegImagePlugin + from . import JpegImagePlugin except ImportError: pass try: - from PIL import PpmImagePlugin + from . import PpmImagePlugin except ImportError: pass try: - from PIL import PngImagePlugin + from . import PngImagePlugin except ImportError: pass # try: @@ -414,6 +435,11 @@ def _getdecoder(mode, decoder_name, args, extra=()): elif not isinstance(args, tuple): args = (args,) + try: + decoder = DECODERS[decoder_name] + return decoder(mode, *args + extra) + except KeyError: + pass try: # get decoder decoder = getattr(core, decoder_name + "_decoder") @@ -431,6 +457,11 @@ def _getencoder(mode, encoder_name, args, extra=()): elif not isinstance(args, tuple): args = (args,) + try: + encoder = ENCODERS[encoder_name] + return encoder(mode, *args + extra) + except KeyError: + pass try: # get encoder encoder = getattr(core, encoder_name + "_encoder") @@ -495,6 +526,7 @@ class Image(object): """ format = None format_description = None + _close_exclusive_fp_after_loading = True def __init__(self): # FIXME: take "new" parameters / other image? @@ -521,16 +553,15 @@ class Image(object): new.im = im new.mode = im.mode new.size = im.size - if self.palette: - new.palette = self.palette.copy() - if im.mode == "P" and not new.palette: - from PIL import ImagePalette - new.palette = ImagePalette.ImagePalette() + if im.mode in ('P', 'PA'): + if self.palette: + new.palette = self.palette.copy() + else: + from . import ImagePalette + new.palette = ImagePalette.ImagePalette() new.info = self.info.copy() return new - _makeself = _new # compatibility - # Context Manager Support def __enter__(self): return self @@ -547,44 +578,69 @@ class Image(object): This function is only required to close images that have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. + :py:meth:`~PIL.Image.Image.load` method. See + :ref:`file-handling` for more information. """ try: self.fp.close() + self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) + if getattr(self, 'map', None): + self.map = None + # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image # object is gone. self.im = deferred_error(ValueError("Operation on closed image")) + if sys.version_info.major >= 3: + def __del__(self): + if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp') + and self.fp and self._exclusive_fp): + self.fp.close() + self.fp = None + def _copy(self): self.load() self.im = self.im.copy() self.pyaccess = None self.readonly = 0 - def _dump(self, file=None, format=None): + def _ensure_mutable(self): + if self.readonly: + self._copy() + else: + self.load() + + def _dump(self, file=None, format=None, **options): import tempfile + suffix = '' if format: suffix = '.'+format + if not file: - f, file = tempfile.mkstemp(suffix) + f, filename = tempfile.mkstemp(suffix) os.close(f) + else: + filename = file + if not filename.endswith(suffix): + filename = filename + suffix self.load() + if not format or format == "PPM": - self.im.save_ppm(file) + self.im.save_ppm(filename) else: - if not file.endswith(format): - file = file + "." + format - self.save(file, format) - return file + self.save(filename, format, **options) + + return filename def __eq__(self, other): - return (self.__class__.__name__ == other.__class__.__name__ and + return (isinstance(other, Image) and + self.__class__.__name__ == other.__class__.__name__ and self.mode == other.mode and self.size == other.size and self.info == other.info and @@ -614,17 +670,21 @@ class Image(object): self.save(b, 'PNG') return b.getvalue() - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr + @property + def __array_interface__(self): + # numpy array interface support + new = {} + shape, typestr = _conv_type_shape(self) + new['shape'] = shape + new['typestr'] = typestr + new['version'] = 3 + if self.mode == '1': + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new['data'] = self.tobytes('raw', 'L') + else: new['data'] = self.tobytes() - new['version'] = 3 - return new - raise AttributeError(name) + return new def __getstate__(self): return [ @@ -690,8 +750,8 @@ class Image(object): return b"".join(data) def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " + - "Please call tobytes() instead.") + raise NotImplementedError("tostring() has been removed. " + "Please call tobytes() instead.") def tobitmap(self, name="image"): """ @@ -741,16 +801,18 @@ class Image(object): raise ValueError("cannot decode image data") def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") + raise NotImplementedError("fromstring() has been removed. " + "Please call frombytes() instead.") def load(self): """ Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the Image class automatically loads an opened image when it is - accessed for the first time. This method will close the file - associated with the image. + accessed for the first time. + + This method will close the file associated with the image. See + :ref:`file-handling` for more information. :returns: An image access object. :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` @@ -772,7 +834,7 @@ class Image(object): if HAS_CFFI and USE_CFFI_ACCESS: if self.pyaccess: return self.pyaccess - from PIL import PyAccess + from . import PyAccess self.pyaccess = PyAccess.new(self, self.readonly) if self.pyaccess: return self.pyaccess @@ -827,19 +889,17 @@ class Image(object): :returns: An :py:class:`~PIL.Image.Image` object. """ - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - self.load() + if not mode and self.mode == "P": + # determine default mode + if self.palette: + mode = self.palette.mode + else: + mode = "RGB" + if not mode or (mode == self.mode and not matrix): + return self.copy() + if matrix: # matrix conversion if mode not in ("L", "RGB"): @@ -858,8 +918,10 @@ class Image(object): if self.mode in ('L', 'RGB') and mode == 'RGBA': # Use transparent conversion to promote from transparent # color to an alpha channel. - return self._new(self.im.convert_transparent( + new_im = self._new(self.im.convert_transparent( mode, self.info['transparency'])) + del(new_im.info['transparency']) + return new_im elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'): t = self.info['transparency'] if isinstance(t, bytes): @@ -874,11 +936,11 @@ class Image(object): trns_im = Image()._new(core.new(self.mode, (1, 1))) if self.mode == 'P': trns_im.putpalette(self.palette) - if type(t) == tuple: + if isinstance(t, tuple): try: t = trns_im.palette.getcolor(t) except: - raise ValueError("Couldn't allocate a palette "+ + raise ValueError("Couldn't allocate a palette " "color for transparency") trns_im.putpixel((0, 0), t) @@ -905,7 +967,7 @@ class Image(object): if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) - from PIL import ImagePalette + from . import ImagePalette new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. @@ -963,7 +1025,7 @@ class Image(object): 2 = fast octree 3 = libimagequant :param kmeans: Integer - :param palette: Quantize to the :py:class:`PIL.ImagingPalette` palette. + :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. :returns: A new image """ @@ -992,7 +1054,7 @@ class Image(object): "only RGB or L mode images can be quantized to a palette" ) im = self.im.convert("P", 1, palette.im) - return self._makeself(im) + return self._new(im) return self._new(self.im.quantize(colors, method, kmeans)) @@ -1013,24 +1075,43 @@ class Image(object): """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel - coordinate. + coordinate. See :ref:`coordinate-system`. - This is a lazy operation. Changes to the source image may or - may not be reflected in the cropped image. To break the - connection, call the :py:meth:`~PIL.Image.Image.load` method on - the cropped copy. + Note: Prior to Pillow 3.4.0, this was a lazy operation. :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. :rtype: :py:class:`~PIL.Image.Image` :returns: An :py:class:`~PIL.Image.Image` object. """ - self.load() if box is None: return self.copy() - # lazy operation - return _ImageCrop(self, box) + self.load() + return self._new(self._crop(self.im, box)) + + def _crop(self, im, box): + """ + Returns a rectangular region from the core image object im. + + This is equivalent to calling im.crop((x0, y0, x1, y1)), but + includes additional sanity checks. + + :param im: a core image object + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :returns: A core image object. + """ + + x0, y0, x1, y1 = map(int, map(round, box)) + + if x1 < x0: + x1 = x0 + if y1 < y0: + y1 = y0 + + _decompression_bomb_check((x1, y1)) + + return im.crop((x0, y0, x1, y1)) def draft(self, mode, size): """ @@ -1044,6 +1125,9 @@ class Image(object): in place. If the image has already been loaded, this method has no effect. + Note: This method is not implemented for most images. It is + currently implemented only for JPEG and PCD images. + :param mode: The requested mode. :param size: The requested size. """ @@ -1063,17 +1147,20 @@ class Image(object): :param filter: Filter kernel. :returns: An :py:class:`~PIL.Image.Image` object. """ + from . import ImageFilter + self.load() - if isinstance(filter, collections.Callable): + if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): raise TypeError("filter argument should be ImageFilter.Filter " + "instance or class") - if self.im.bands == 1: + multiband = isinstance(filter, ImageFilter.MultibandFilter) + if self.im.bands == 1 or multiband: return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't + ims = [] for c in range(self.im.bands): ims.append(self._new(filter.filter(self.im.getband(c)))) @@ -1095,8 +1182,9 @@ class Image(object): image. :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. If the image - is completely empty, this method returns None. + left, upper, right, and lower pixel coordinate. See + :ref:`coordinate-system`. If the image is completely empty, this + method returns None. """ @@ -1186,10 +1274,10 @@ class Image(object): self.load() try: - if bytes is str: - return [i8(c) for c in self.im.getpalette()] - else: + if py3: return list(self.im.getpalette()) + else: + return [i8(c) for c in self.im.getpalette()] except ValueError: return None # no palette @@ -1197,7 +1285,8 @@ class Image(object): """ Returns the pixel value at a given position. - :param xy: The coordinate, given as (x, y). + :param xy: The coordinate, given as (x, y). See + :ref:`coordinate-system`. :returns: The pixel value. If the image is a multi-layer image, this method returns a tuple. """ @@ -1249,16 +1338,16 @@ class Image(object): return self.im.histogram() def offset(self, xoffset, yoffset=None): - raise NotImplementedError("offset() has been removed. " + - "Please call ImageChops.offset() instead.") + raise NotImplementedError("offset() has been removed. " + "Please call ImageChops.offset() instead.") def paste(self, im, box=None, mask=None): """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). If a 4-tuple is given, the size of the pasted image - must match the size of the region. + (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size + of the pasted image must match the size of the region. If the modes don't match, the pasted image is converted to the mode of this image (see the :py:meth:`~PIL.Image.Image.convert` method for @@ -1298,8 +1387,7 @@ class Image(object): box = None if box is None: - # cover all of self - box = (0, 0) + self.size + box = (0, 0) if len(box) == 2: # upper left corner given; get size from image or mask @@ -1312,10 +1400,10 @@ class Image(object): raise ValueError( "cannot determine region size; use 4-item box" ) - box = box + (box[0]+size[0], box[1]+size[1]) + box += (box[0]+size[0], box[1]+size[1]) if isStringType(im): - from PIL import ImageColor + from . import ImageColor im = ImageColor.getcolor(im, self.mode) elif isImageType(im): @@ -1326,9 +1414,7 @@ class Image(object): im = im.convert(self.mode) im = im.im - self.load() - if self.readonly: - self._copy() + self._ensure_mutable() if mask: mask.load() @@ -1336,11 +1422,59 @@ class Image(object): else: self.im.paste(im, box) + def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): + """ 'In-place' analog of Image.alpha_composite. Composites an image + onto this image. + + :param im: image to composite over this one + :param dest: Optional 2 tuple (left, top) specifying the upper + left corner in this (destination) image. + :param source: Optional 2 (left, top) tuple for the upper left + corner in the overlay source image, or 4 tuple (left, top, right, + bottom) for the bounds of the source rectangle + + Performance Note: Not currently implemented in-place in the core layer. + """ + + if not isinstance(source, (list, tuple)): + raise ValueError("Source must be a tuple") + if not isinstance(dest, (list, tuple)): + raise ValueError("Destination must be a tuple") + if not len(source) in (2, 4): + raise ValueError("Source must be a 2 or 4-tuple") + if not len(dest) == 2: + raise ValueError("Destination must be a 2-tuple") + if min(source) < 0: + raise ValueError("Source must be non-negative") + if min(dest) < 0: + raise ValueError("Destination must be non-negative") + + if len(source) == 2: + source = source + im.size + + # over image, crop if it's not the whole thing. + if source == (0, 0) + im.size: + overlay = im + else: + overlay = im.crop(source) + + # target for the paste + box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) + + # destination image. don't copy if we're using the whole image. + if box == (0, 0) + self.size: + background = self + else: + background = self.crop(box) + + result = alpha_composite(background, overlay) + self.paste(result, box) + def point(self, lut, mode=None): """ Maps this image through a lookup table or function. - :param lut: A lookup table, containing 256 (or 65336 if + :param lut: A lookup table, containing 256 (or 65536 if self.mode=="I" and mode == "L") values per band in the image. A function can be used instead, it should take a single argument. The function is called once for each @@ -1386,9 +1520,7 @@ class Image(object): other color value. """ - self.load() - if self.readonly: - self._copy() + self._ensure_mutable() if self.mode not in ("LA", "RGBA"): # attempt to promote self to a matching alpha mode @@ -1396,14 +1528,13 @@ class Image(object): mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) - self.pyaccess = None except (AttributeError, ValueError): # do things the hard way im = self.im.convert(mode) if im.mode not in ("LA", "RGBA"): raise ValueError # sanity check self.im = im - self.pyaccess = None + self.pyaccess = None self.mode = self.im.mode except (KeyError, ValueError): raise ValueError("illegal image mode") @@ -1445,9 +1576,7 @@ class Image(object): :param offset: An optional offset value. The default is 0.0. """ - self.load() - if self.readonly: - self._copy() + self._ensure_mutable() self.im.putdata(data, scale, offset) @@ -1461,8 +1590,9 @@ class Image(object): string. :param data: A palette sequence (either a list or a string). + :param rawmode: The raw mode of the palette. """ - from PIL import ImagePalette + from . import ImagePalette if self.mode not in ("L", "P"): raise ValueError("illegal image mode") @@ -1471,10 +1601,10 @@ class Image(object): palette = ImagePalette.raw(data.rawmode, data.palette) else: if not isinstance(data, bytes): - if bytes is str: - data = "".join(chr(x) for x in data) - else: + if py3: data = bytes(data) + else: + data = "".join(chr(x) for x in data) palette = ImagePalette.raw(rawmode, data) self.mode = "P" self.palette = palette @@ -1497,57 +1627,145 @@ class Image(object): * :py:meth:`~PIL.Image.Image.putdata` * :py:mod:`~PIL.ImageDraw` - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :param value: The pixel value. """ - self.load() if self.readonly: self._copy() - self.pyaccess = None - self.load() + self.load() if self.pyaccess: return self.pyaccess.putpixel(xy, value) return self.im.putpixel(xy, value) - def resize(self, size, resample=NEAREST): + def remap_palette(self, dest_map, source_palette=None): + """ + Rewrites the image to reorder the palette. + + :param dest_map: A list of indexes into the original palette. + e.g. [1,0] would swap a two item palette, and list(range(255)) + is the identity transform. + :param source_palette: Bytes or None. + :returns: An :py:class:`~PIL.Image.Image` object. + + """ + from . import ImagePalette + + if self.mode not in ("L", "P"): + raise ValueError("illegal image mode") + + if source_palette is None: + if self.mode == "P": + real_source_palette = self.im.getpalette("RGB")[:768] + else: # L-mode + real_source_palette = bytearray(i//3 for i in range(768)) + else: + real_source_palette = source_palette + + palette_bytes = b"" + new_positions = [0]*256 + + # pick only the used colors from the palette + for i, oldPosition in enumerate(dest_map): + palette_bytes += real_source_palette[oldPosition*3:oldPosition*3+3] + new_positions[oldPosition] = i + + # replace the palette color id of all pixel with the new id + + # Palette images are [0..255], mapped through a 1 or 3 + # byte/color map. We need to remap the whole image + # from palette 1 to palette 2. New_positions is + # an array of indexes into palette 1. Palette 2 is + # palette 1 with any holes removed. + + # We're going to leverage the convert mechanism to use the + # C code to remap the image from palette 1 to palette 2, + # by forcing the source image into 'L' mode and adding a + # mapping 'L' mode palette, then converting back to 'L' + # sans palette thus converting the image bytes, then + # assigning the optimized RGB palette. + + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. + + mapping_palette = bytearray(new_positions) + + m_im = self.copy() + m_im.mode = 'P' + + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=mapping_palette*3, + size=768) + # possibly set palette dirty, then + # m_im.putpalette(mapping_palette, 'L') # converts to 'P' + # or just force it. + # UNDONE -- this is part of the general issue with palettes + m_im.im.putpalette(*m_im.palette.getdata()) + + m_im = m_im.convert('L') + + # Internally, we require 768 bytes for a palette. + new_palette_bytes = (palette_bytes + + (768 - len(palette_bytes)) * b'\x00') + m_im.putpalette(new_palette_bytes) + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=palette_bytes, + size=len(palette_bytes)) + + return m_im + + def resize(self, size, resample=NEAREST, box=None): """ Returns a resized copy of this image. :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation), - :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation), or - :py:attr:`PIL.Image.LANCZOS` (a high-quality downsampling filter). + one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`, + :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`, + :py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`. If omitted, or if the image has mode "1" or "P", it is set :py:attr:`PIL.Image.NEAREST`. + See: :ref:`concept-filters`. + :param box: An optional 4-tuple of floats giving the region + of the source image which should be scaled. + The values should be within (0, 0, width, height) rectangle. + If omitted or None, the entire source is used. :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS): + if resample not in ( + NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING, + ): raise ValueError("unknown resampling filter") - self.load() - size = tuple(size) - if self.size == size: - return self._new(self.im) + + if box is None: + box = (0, 0) + self.size + else: + box = tuple(box) + + if self.size == size and box == (0, 0) + self.size: + return self.copy() if self.mode in ("1", "P"): resample = NEAREST if self.mode == 'LA': - return self.convert('La').resize(size, resample).convert('LA') + return self.convert('La').resize(size, resample, box).convert('LA') if self.mode == 'RGBA': - return self.convert('RGBa').resize(size, resample).convert('RGBA') + return self.convert('RGBa').resize(size, resample, box).convert('RGBA') - return self._new(self.im.resize(size, resample)) + self.load() - def rotate(self, angle, resample=NEAREST, expand=0): + return self._new(self.im.resize(size, resample, box)) + + def rotate(self, angle, resample=NEAREST, expand=0, center=None, + translate=None, fillcolor=None): """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -1560,54 +1778,97 @@ class Image(object): environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. + set :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the - input image. + input image. Note that the expand flag assumes rotation around + the center and no translation. + :param center: Optional center of rotation (a 2-tuple). Origin is + the upper left corner. Default is the center of the image. + :param translate: An optional post-rotate translation (a 2-tuple). + :param fillcolor: An optional color for area outside the rotated image. :returns: An :py:class:`~PIL.Image.Image` object. """ angle = angle % 360.0 - # Fast paths regardless of filter - if angle == 0: - return self._new(self.im) - if angle == 180: - return self.transpose(ROTATE_180) - if angle == 90 and expand: - return self.transpose(ROTATE_90) - if angle == 270 and expand: - return self.transpose(ROTATE_270) + # Fast paths regardless of filter, as long as we're not + # translating or changing the center. + if not (center or translate): + if angle == 0: + return self.copy() + if angle == 180: + return self.transpose(ROTATE_180) + if angle == 90 and expand: + return self.transpose(ROTATE_90) + if angle == 270 and expand: + return self.transpose(ROTATE_270) + + # Calculate the affine matrix. Note that this is the reverse + # transformation (from destination image to source) because we + # want to interpolate the (discrete) destination pixel from + # the local area around the (floating) source pixel. + + # The matrix we actually want (note that it operates from the right): + # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) + # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) + # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) + + # The reverse matrix is thus: + # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) + # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) + # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) + + # In any case, the final translation may be updated at the end to + # compensate for the expand flag. + + w, h = self.size + + if translate is None: + post_trans = (0, 0) + else: + post_trans = translate + if center is None: + rotn_center = (w / 2.0, h / 2.0) # FIXME These should be rounded to ints? + else: + rotn_center = center angle = - math.radians(angle) matrix = [ round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0 - ] + ] - def transform(x, y, matrix=matrix): + def transform(x, y, matrix): (a, b, c, d, e, f) = matrix return a*x + b*y + c, d*x + e*y + f - w, h = self.size + matrix[2], matrix[5] = transform(-rotn_center[0] - post_trans[0], + -rotn_center[1] - post_trans[1], matrix) + matrix[2] += rotn_center[0] + matrix[5] += rotn_center[1] + if expand: # calculate output size xx = [] yy = [] for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) + x, y = transform(x, y, matrix) xx.append(x) yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) + nw = int(math.ceil(max(xx)) - math.floor(min(xx))) + nh = int(math.ceil(max(yy)) - math.floor(min(yy))) - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y + # We multiply a translation matrix from the right. Because of its + # special form, this is the same as taking the image of the + # translation vector as new translation vector. + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, + -(nh - h) / 2.0, + matrix) + w, h = nw, nh - return self.transform((w, h), AFFINE, matrix, resample) + return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor) def save(self, fp, format=None, **params): """ @@ -1631,7 +1892,7 @@ class Image(object): format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used. - :param options: Extra parameters to the image writer. + :param params: Extra parameters to the image writer. :returns: None :exception KeyError: If the output format could not be determined from the file name. Use the format option to solve this. @@ -1644,21 +1905,17 @@ class Image(object): if isPath(fp): filename = fp open_fp = True - elif sys.version_info >= (3, 4): - from pathlib import Path - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif hasattr(fp, "name") and isPath(fp.name): + elif HAS_PATHLIB and isinstance(fp, Path): + filename = str(fp) + open_fp = True + if not filename and hasattr(fp, "name") and isPath(fp.name): # only set the name for metadata purposes filename = fp.name # may mutate self! self.load() - save_all = False - if 'save_all' in params: - save_all = params.pop('save_all') + save_all = params.pop('save_all', False) self.encoderinfo = params self.encoderconfig = () @@ -1669,7 +1926,10 @@ class Image(object): if not format: if ext not in EXTENSION: init() - format = EXTENSION[ext] + try: + format = EXTENSION[ext] + except KeyError: + raise ValueError('unknown file extension: {}'.format(ext)) if format.upper() not in SAVE: init() @@ -1679,7 +1939,12 @@ class Image(object): save_handler = SAVE[format.upper()] if open_fp: - fp = builtins.open(filename, "wb") + if params.get('append', False): + fp = builtins.open(filename, "r+b") + else: + # Open also for reading ("+"), because TIFF save_all + # writer needs to go back and edit the written data. + fp = builtins.open(filename, "w+b") try: save_handler(self, fp, filename) @@ -1718,8 +1983,8 @@ class Image(object): PPM file, and calls either the **xv** utility or the **display** utility, depending on which one can be found. - On OS X, this method saves the image to a temporary BMP file, and opens - it with the native Preview application. + On macOS, this method saves the image to a temporary BMP file, and + opens it with the native Preview application. On Windows, it saves the image to a temporary BMP file, and uses the standard BMP display utility to show it (usually Paint). @@ -1739,6 +2004,9 @@ class Image(object): containing a copy of one of the original bands (red, green, blue). + If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` + method can be more convenient and faster. + :returns: A tuple containing bands. """ @@ -1746,11 +2014,31 @@ class Image(object): if self.im.bands == 1: ims = [self.copy()] else: - ims = [] - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) + ims = map(self._new, self.im.split()) return tuple(ims) + def getchannel(self, channel): + """ + Returns an image containing a single channel of the source image. + + :param channel: What channel to return. Could be index + (0 for "R" channel of "RGB") or channel name + ("A" for alpha channel of "RGBA"). + :returns: An image in "L" mode. + + .. versionadded:: 4.3.0 + """ + self.load() + + if isStringType(channel): + try: + channel = self.getbands().index(channel) + except ValueError: + raise ValueError( + 'The image has no channel "{}"'.format(channel)) + + return self._new(self.im.getband(channel)) + def tell(self): """ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. @@ -1808,7 +2096,8 @@ class Image(object): # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. - def transform(self, size, method, data=None, resample=NEAREST, fill=1): + def transform(self, size, method, data=None, resample=NEAREST, + fill=1, fillcolor=None): """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data @@ -1822,6 +2111,20 @@ class Image(object): :py:attr:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or :py:attr:`PIL.Image.MESH` (map a number of source quadrilaterals in one operation). + + It may also be an :py:class:`~PIL.Image.ImageTransformHandler` + object:: + class Example(Image.ImageTransformHandler): + def transform(size, method, data, resample, fill=1): + # Return result + + It may also be an object with a :py:meth:`~method.getdata` method + that returns a tuple supplying new **method** and **data** values:: + class Example(object): + def getdata(self): + method = Image.EXTENT + data = (0, 0, 100, 100) + return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), @@ -1829,16 +2132,21 @@ class Image(object): environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is set to :py:attr:`PIL.Image.NEAREST`. + :param fill: If **method** is an + :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of + the arguments passed to it. Otherwise, it is unused. + :param fillcolor: Optional fill color for the area outside the transform + in the output image. :returns: An :py:class:`~PIL.Image.Image` object. """ if self.mode == 'LA': return self.convert('La').transform( - size, method, data, resample, fill).convert('LA') + size, method, data, resample, fill, fillcolor).convert('LA') if self.mode == 'RGBA': return self.convert('RGBa').transform( - size, method, data, resample, fill).convert('RGBA') + size, method, data, resample, fill, fillcolor).convert('RGBA') if isinstance(method, ImageTransformHandler): return method.transform(size, self, resample=resample, fill=fill) @@ -1850,13 +2158,15 @@ class Image(object): if data is None: raise ValueError("missing method data") - im = new(self.mode, size, None) + im = new(self.mode, size, fillcolor) if method == MESH: # list of quads for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) + im.__transformer(box, self, QUAD, quad, resample, + fillcolor is None) else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) + im.__transformer((0, 0)+size, self, method, data, + resample, fillcolor is None) return im @@ -1874,7 +2184,7 @@ class Image(object): xs = float(x1 - x0) / w ys = float(y1 - y0) / h method = AFFINE - data = (xs, 0, x0 + xs/2, 0, ys, y0 + ys/2) + data = (xs, 0, x0, 0, ys, y0) elif method == PERSPECTIVE: data = data[0:8] @@ -1915,8 +2225,8 @@ class Image(object): :param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`, - :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270` or - :py:attr:`PIL.Image.TRANSPOSE`. + :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270`, + :py:attr:`PIL.Image.TRANSPOSE` or :py:attr:`PIL.Image.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ @@ -1934,57 +2244,19 @@ class Image(object): def toqimage(self): """Returns a QImage copy of this image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqimage(self) def toqpixmap(self): """Returns a QPixmap copy of this image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqpixmap(self) -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - # Round to nearest integer, runs int(round(x)) when unpacking - x0, y0, x1, y1 = map(int, map(round, box)) - - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - if self.im: - return self.im.pixel_access(self.readonly) - - # FIXME: future versions should optimize crop/paste - # sequences! - - # -------------------------------------------------------------------- # Abstract handlers. @@ -2005,11 +2277,29 @@ class ImageTransformHandler(object): # Debugging def _wedge(): - "Create greyscale wedge (for debugging only)" + """Create greyscale wedge (for debugging only)""" return Image()._new(core.wedge("L")) +def _check_size(size): + """ + Common check to enforce type and sanity check on size tuples + + :param size: Should be a 2 tuple of (width, height) + :returns: True, or raises a ValueError + """ + + if not isinstance(size, (list, tuple)): + raise ValueError("Size must be a tuple") + if len(size) != 2: + raise ValueError("Size must be a tuple of length 2") + if size[0] < 0 or size[1] < 0: + raise ValueError("Width and height must be >= 0") + + return True + + def new(mode, size, color=0): """ Creates a new image with the given mode and size. @@ -2026,6 +2316,8 @@ def new(mode, size, color=0): :returns: An :py:class:`~PIL.Image.Image` object. """ + _check_size(size) + if color is None: # don't initialize return Image()._new(core.new(mode, size)) @@ -2033,7 +2325,7 @@ def new(mode, size, color=0): if isStringType(color): # css3-style specifier - from PIL import ImageColor + from . import ImageColor color = ImageColor.getcolor(color, mode) return Image()._new(core.fill(mode, size, color)) @@ -2063,6 +2355,8 @@ def frombytes(mode, size, data, decoder_name="raw", *args): :returns: An :py:class:`~PIL.Image.Image` object. """ + _check_size(size) + # may pass tuple instead of argument list if len(args) == 1 and isinstance(args[0], tuple): args = args[0] @@ -2077,7 +2371,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args): def fromstring(*args, **kw): raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") + "Please call frombytes() instead.") def frombuffer(mode, size, data, decoder_name="raw", *args): @@ -2115,6 +2409,8 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): .. versionadded:: 1.1.4 """ + _check_size(size) + # may pass tuple instead of argument list if len(args) == 1 and isinstance(args[0], tuple): args = args[0] @@ -2157,16 +2453,13 @@ def fromarray(obj, mode=None): arr = obj.__array_interface__ shape = arr['shape'] ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None + strides = arr.get('strides', None) if mode is None: try: typekey = (1, 1) + shape[2:], arr['typestr'] mode, rawmode = _fromarray_typemap[typekey] except KeyError: - # print typekey + # print(typekey) raise TypeError("Cannot handle this data type") else: rawmode = mode @@ -2191,7 +2484,7 @@ def fromarray(obj, mode=None): def fromqimage(im): """Creates an image instance from a QImage image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqimage(im) @@ -2199,15 +2492,16 @@ def fromqimage(im): def fromqpixmap(im): """Creates an image instance from a QPixmap image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqpixmap(im) + _fromarray_typemap = { # (shape, typestr) => mode, rawmode # first two members of shape are set to one - # ((1, 1), "|b1"): ("1", "1"), # broken + ((1, 1), "|b1"): ("1", "1;8"), ((1, 1), "|u1"): ("L", "L"), ((1, 1), "|i1"): ("I", "I;8"), ((1, 1), " 2 * MAX_IMAGE_PIXELS: + raise DecompressionBombError( + "Image size (%d pixels) exceeds limit of %d pixels, " + "could be decompression bomb DOS attack." % + (pixels, 2 * MAX_IMAGE_PIXELS)) + if pixels > MAX_IMAGE_PIXELS: warnings.warn( "Image size (%d pixels) exceeds limit of %d pixels, " @@ -2254,7 +2554,7 @@ def open(fp, mode="r"): the file remains open and the actual image data is not read from the file until you try to process the data (or call the :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. + :py:func:`~PIL.Image.new`. See :ref:`file-handling`. :param fp: A filename (string), pathlib.Path object or a file object. The file object must implement :py:meth:`~file.read`, @@ -2269,20 +2569,22 @@ def open(fp, mode="r"): if mode != "r": raise ValueError("bad mode %r" % mode) + exclusive_fp = False filename = "" if isPath(fp): filename = fp - elif sys.version_info >= (3, 4): - from pathlib import Path - if isinstance(fp, Path): - filename = str(fp.resolve()) + elif HAS_PATHLIB and isinstance(fp, Path): + filename = str(fp.resolve()) + if filename: fp = builtins.open(filename, "rb") + exclusive_fp = True try: fp.seek(0) except (AttributeError, io.UnsupportedOperation): fp = io.BytesIO(fp.read()) + exclusive_fp = True prefix = fp.read(16) @@ -2311,8 +2613,11 @@ def open(fp, mode="r"): im = _open_core(fp, filename, prefix) if im: + im._exclusive_fp = exclusive_fp return im + if exclusive_fp: + fp.close() raise IOError("cannot identify image file %r" % (filename if filename else fp)) @@ -2405,16 +2710,14 @@ def merge(mode, bands): if getmodebands(mode) != len(bands) or "*" in mode: raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): + for band in bands[1:]: + if band.mode != getmodetype(mode): raise ValueError("mode mismatch") - if im.size != bands[0].size: + if band.size != bands[0].size: raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) + for band in bands: + band.load() + return bands[0]._new(core.merge(mode, *[b.im for b in bands])) # -------------------------------------------------------------------- @@ -2480,6 +2783,56 @@ def register_extension(id, extension): EXTENSION[extension.lower()] = id.upper() +def register_extensions(id, extensions): + """ + Registers image extensions. This function should not be + used in application code. + + :param id: An image format identifier. + :param extensions: A list of extensions used for this format. + """ + for extension in extensions: + register_extension(id, extension) + + +def registered_extensions(): + """ + Returns a dictionary containing all file extensions belonging + to registered plugins + """ + if not EXTENSION: + init() + return EXTENSION + + +def register_decoder(name, decoder): + """ + Registers an image decoder. This function should not be + used in application code. + + :param name: The name of the decoder + :param decoder: A callable(mode, args) that returns an + ImageFile.PyDecoder object + + .. versionadded:: 4.1.0 + """ + DECODERS[name] = decoder + + +def register_encoder(name, encoder): + """ + Registers an image encoder. This function should not be + used in application code. + + :param name: The name of the encoder + :param encoder: A callable(mode, args) that returns an + ImageFile.PyEncoder object + + .. versionadded:: 4.1.0 + """ + ENCODERS[name] = encoder + + # -------------------------------------------------------------------- # Simple display support. User code may override this. @@ -2489,7 +2842,7 @@ def _show(image, **options): def _showxv(image, title=None, **options): - from PIL import ImageShow + from . import ImageShow ImageShow.show(image, title, **options) @@ -2519,4 +2872,59 @@ def effect_noise(size, sigma): """ return Image()._new(core.effect_noise(size, sigma)) -# End of file + +def linear_gradient(mode): + """ + Generate 256x256 linear gradient from black to white, top to bottom. + + :param mode: Input mode. + """ + return Image()._new(core.linear_gradient(mode)) + + +def radial_gradient(mode): + """ + Generate 256x256 radial gradient from black to white, centre to edge. + + :param mode: Input mode. + """ + return Image()._new(core.radial_gradient(mode)) + + +# -------------------------------------------------------------------- +# Resources + +def _apply_env_variables(env=None): + if env is None: + env = os.environ + + for var_name, setter in [ + ('PILLOW_ALIGNMENT', core.set_alignment), + ('PILLOW_BLOCK_SIZE', core.set_block_size), + ('PILLOW_BLOCKS_MAX', core.set_blocks_max), + ]: + if var_name not in env: + continue + + var = env[var_name].lower() + + units = 1 + for postfix, mul in [('k', 1024), ('m', 1024*1024)]: + if var.endswith(postfix): + units = mul + var = var[:-len(postfix)] + + try: + var = int(var) * units + except ValueError: + warnings.warn("{0} is not int".format(var_name)) + continue + + try: + setter(var) + except ValueError as e: + warnings.warn("{0}: {1}".format(var_name, e)) + + +_apply_env_variables() +atexit.register(core.clear_cache) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageChops.py b/server/www/packages/packages-windows/x86/PIL/ImageChops.py index ba5350e..8901673 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageChops.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageChops.py @@ -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): diff --git a/server/www/packages/packages-windows/x86/PIL/ImageCms.py b/server/www/packages/packages-windows/x86/PIL/ImageCms.py index 6d5801a..d82e30e 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageCms.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageCms.py @@ -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 @@ -361,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 @@ -552,6 +554,7 @@ def buildProofTransform( except (IOError, TypeError, ValueError) as v: raise PyCMSError(v) + buildTransformFromOpenProfiles = buildTransform buildProofTransformFromOpenProfiles = buildProofTransform @@ -950,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 diff --git a/server/www/packages/packages-windows/x86/PIL/ImageColor.py b/server/www/packages/packages-windows/x86/PIL/ImageColor.py index 56c38e4..08c00fd 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageColor.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageColor.py @@ -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,63 @@ 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 +100,22 @@ 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,6 +153,7 @@ def getcolor(color, mode): return color + (alpha,) return color + colormap = { # X11 colour table from https://drafts.csswg.org/css-color-4/, with # gray/grey spelling issues fixed. This is a superset of HTML 4.0 diff --git a/server/www/packages/packages-windows/x86/PIL/ImageDraw.py b/server/www/packages/packages-windows/x86/PIL/ImageDraw.py index a3e5270..5bc8902 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageDraw.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageDraw.py @@ -31,10 +31,9 @@ # 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. @@ -50,8 +49,8 @@ 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 + :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 @@ -87,25 +86,14 @@ class ImageDraw(object): self.fill = 0 self.font = None - def setink(self, ink): - raise NotImplementedError("setink() has been removed. " + - "Please use keyword arguments instead.") - - def setfill(self, onoff): - raise NotImplementedError("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 - def getfont(self): - """Get the current default font.""" + """ + 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 @@ -208,12 +196,12 @@ class ImageDraw(object): def _multiline_check(self, text): """Draw text.""" - split_character = "\n" if isinstance(text, type("")) else b"\n" + 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) @@ -222,7 +210,6 @@ class ImageDraw(object): if self._multiline_check(text): return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) - ink, fill = self._getink(fill) if font is None: font = self.getfont() @@ -230,17 +217,17 @@ 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) @@ -259,35 +246,40 @@ class ImageDraw(object): left += (max_width - widths[idx]) else: assert False, 'align must be "left", "center" or "right"' - self.text((left, top), line, fill, font, anchor) + self.text((left, top), line, fill, font, anchor, + direction=direction, features=features) top += line_spacing left = xy[0] - def textsize(self, text, font=None, *args, **kwargs): + 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, *args, **kwargs) + 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 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 + :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 @@ -298,6 +290,7 @@ def Draw(im, mode=None): except AttributeError: return ImageDraw(im, mode) + # experimental access to the outline API try: Outline = Image.core.outline @@ -310,46 +303,51 @@ 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. - @return A (drawing context, drawing resource factory) tuple. + :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 -def floodfill(image, xy, value, border=None): +def floodfill(image, xy, value, border=None, thresh=0): """ (experimental) Fills a bounded region with a given color. - @param image Target image. - @param xy Seed position (a 2-item coordinate tuple). - @param value Fill color. - @param border Optional border value. If given, the region consists of + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. + :param value: Fill color. + :param border: Optional border value. If given, the region consists of pixels with a color different from the border color. If not given, the region consists of pixels having the same color as the seed pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of non- + homogeneous, but similar, colors. """ # based on an implementation by Eric S. Raymond pixel = image.load() x, y = xy try: background = pixel[x, y] - if 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: @@ -362,7 +360,7 @@ def floodfill(image, xy, value, border=None): except IndexError: pass else: - if p == background: + if _color_diff(p, background) <= thresh: pixel[s, t] = value newedge.append((s, t)) edge = newedge @@ -381,4 +379,9 @@ def floodfill(image, xy, value, border=None): newedge.append((s, t)) edge = newedge -# End of file + +def _color_diff(rgb1, rgb2): + """ + Uses 1-norm distance to calculate difference between two rgb values. + """ + return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageDraw2.py b/server/www/packages/packages-windows/x86/PIL/ImageDraw2.py index 62ee116..f7902b0 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageDraw2.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageDraw2.py @@ -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) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageEnhance.py b/server/www/packages/packages-windows/x86/PIL/ImageEnhance.py index 56b5c01..11c9c3a 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageEnhance.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageEnhance.py @@ -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): @@ -67,7 +67,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 +82,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 +97,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')) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageFile.py b/server/www/packages/packages-windows/x86/PIL/ImageFile.py index b21e9e3..681dee5 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageFile.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageFile.py @@ -27,8 +27,8 @@ # 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 @@ -78,6 +78,8 @@ class ImageFile(Image.Image): def __init__(self, fp=None, filename=None): Image.Image.__init__(self) + self._min_frame = 0 + self.tile = None self.readonly = 1 # until we know better @@ -88,10 +90,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 +105,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 +118,18 @@ class ImageFile(Image.Image): pass + def get_format_mimetype(self): + if self.format is None: + return + return Image.MIME.get(self.format.upper()) + def verify(self): "Check file integrity" # raise exception if something's wrong. must be called # directly after open, and closes file when finished. + if self._exclusive_fp: + self.fp.close() self.fp = None def load(self): @@ -150,31 +165,34 @@ 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) - self.map = mmap.mmap(fp.fileno(), size, access=mmap.ACCESS_READ) + 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) @@ -187,66 +205,54 @@ class ImageFile(Image.Image): for decoder_name, extents, offset, args in self.tile: decoder = Image._getdecoder(self.mode, decoder_name, - args, self.decoderconfig) - seek(offset) + args, self.decoderconfig) try: + seek(offset) decoder.setimage(self.im, extents) - except ValueError: - continue - 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: + 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: # truncated jpeg + if LOAD_TRUNCATED_IMAGES: + break + else: + self.tile = [] + raise IOError("image file is truncated " + "(%d bytes not processed)" % len(b)) + + b = b + s + n, err_code = decoder.decode(b) + if n < 0: break - else: - raise IOError("image file is truncated") - - if not s and not decoder.handles_eof: # truncated jpeg - self.tile = [] - - # JpegDecode needs to clean things up here either way - # If we don't destroy the decompressor, - # we have a memory leak. - decoder.cleanup() - - if LOAD_TRUNCATED_IMAGES: - break - else: - raise IOError("image file is truncated " - "(%d bytes not processed)" % len(b)) - - b = b + s - n, err_code = decoder.decode(b) - if n < 0: - break - b = b[n:] - - # Need to cleanup here to prevent leaks in PyPy - decoder.cleanup() + b = b[n:] + finally: + # Need to cleanup here to prevent leaks + decoder.cleanup() self.tile = [] self.readonly = readonly - self.fp = None # might be shared + 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) - # 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() - return Image.Image.load(self) def load_prepare(self): @@ -270,6 +276,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): """ @@ -305,8 +321,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 @@ -377,11 +391,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 @@ -408,6 +419,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. @@ -429,12 +446,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 @@ -473,7 +489,7 @@ def _save(im, fp, tile, bufsize=0): e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) - l,s = e.encode_to_pyfd() + l, s = e.encode_to_pyfd() else: while True: l, s, d = e.encode(bufsize) @@ -492,7 +508,7 @@ def _save(im, fp, tile, bufsize=0): e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) - l,s = e.encode_to_pyfd() + l, s = e.encode_to_pyfd() else: s = e.encode_to_file(fh, bufsize) if s < 0: @@ -524,3 +540,128 @@ 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` + """ + + _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") diff --git a/server/www/packages/packages-windows/x86/PIL/ImageFilter.py b/server/www/packages/packages-windows/x86/PIL/ImageFilter.py index baa168a..e77349d 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageFilter.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageFilter.py @@ -15,14 +15,25 @@ # 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 Kernel(MultibandFilter): """ Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. @@ -39,6 +50,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: @@ -126,7 +138,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 +153,7 @@ class ModeFilter(Filter): return image.modefilter(self.size) -class GaussianBlur(Filter): +class GaussianBlur(MultibandFilter): """Gaussian blur filter. :param radius: Blur radius. @@ -156,7 +167,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 @@ -246,6 +277,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 +306,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) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageFont.py b/server/www/packages/packages-windows/x86/PIL/ImageFont.py index af1166d..3ac29e8 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageFont.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageFont.py @@ -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: + 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,25 @@ 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 +157,34 @@ 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. @@ -166,34 +197,35 @@ class 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. + encoding=self.encoding if encoding is None else encoding, + layout_engine=self.layout_engine if layout_engine is None else 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 +245,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 +265,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 +303,16 @@ 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) + 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) + 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 +328,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 +351,7 @@ def load_default(): f = ImageFont() f._load_pilfont_data( # courB08 - BytesIO(base64.decodestring(b''' + BytesIO(base64.b64decode(b''' UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -407,7 +443,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 +469,3 @@ Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR w7IkEbzhVQAAAABJRU5ErkJggg== ''')))) return f - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageGrab.py b/server/www/packages/packages-windows/x86/PIL/ImageGrab.py index 85bc474..712b02c 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageGrab.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageGrab.py @@ -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 @@ -41,7 +41,7 @@ def grab(bbox=None): 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: @@ -72,10 +72,9 @@ def grabclipboard(): os.unlink(filepath) return im else: - debug = 0 # temporary interface - data = Image.core.grabclipboard(debug) + data = Image.core.grabclipboard() if isinstance(data, bytes): - from PIL import BmpImagePlugin + from . import BmpImagePlugin import io return BmpImagePlugin.DibImageFile(io.BytesIO(data)) return data diff --git a/server/www/packages/packages-windows/x86/PIL/ImageMath.py b/server/www/packages/packages-windows/x86/PIL/ImageMath.py index c0f3820..d985877 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageMath.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageMath.py @@ -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 @@ -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_": @@ -268,5 +269,3 @@ def eval(expression, _dict={}, **kw): return out.im except AttributeError: return out - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageMode.py b/server/www/packages/packages-windows/x86/PIL/ImageMode.py index 583fd7e..b227f21 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageMode.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageMode.py @@ -14,7 +14,7 @@ # # mode descriptor cache -_modes = {} +_modes = None class ModeDescriptor(object): @@ -32,21 +32,24 @@ class ModeDescriptor(object): 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["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") + 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] - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageMorph.py b/server/www/packages/packages-windows/x86/PIL/ImageMorph.py index 902ed8d..579ee4e 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageMorph.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageMorph.py @@ -5,8 +5,9 @@ # # Copyright (c) 2014 Dov Grobgeld -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 @@ -151,15 +152,15 @@ class LutBuilder(object): patterns += self._pattern_permute(pattern, options, result) # # Debugging -# for p,r in patterns: -# print p,r -# print '--' +# 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 +211,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 +223,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 +234,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 +248,3 @@ class MorphOp(object): def set_lut(self, lut): """Set the lut from an external source""" self.lut = lut - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageOps.py b/server/www/packages/packages-windows/x86/PIL/ImageOps.py index f317645..25d491a 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageOps.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageOps.py @@ -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 @@ -178,6 +179,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 +208,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,6 +272,7 @@ 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 @@ -415,6 +440,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 +454,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 +489,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 +517,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)) diff --git a/server/www/packages/packages-windows/x86/PIL/ImagePalette.py b/server/www/packages/packages-windows/x86/PIL/ImagePalette.py index 3b60068..cecc645 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImagePalette.py +++ b/server/www/packages/packages-windows/x86/PIL/ImagePalette.py @@ -17,10 +17,7 @@ # import array -from PIL import ImageColor -from PIL import GimpPaletteFile -from PIL import GimpGradientFile -from PIL import PaletteFile +from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile class ImagePalette(object): @@ -197,23 +194,23 @@ def load(filename): # FIXME: supports GIMP gradients only - fp = open(filename, "rb") + with open(filename, "rb") as fp: - 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") + 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 diff --git a/server/www/packages/packages-windows/x86/PIL/ImagePath.py b/server/www/packages/packages-windows/x86/PIL/ImagePath.py index b308749..8cbfec0 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImagePath.py +++ b/server/www/packages/packages-windows/x86/PIL/ImagePath.py @@ -14,49 +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 - - def compact(self, distance=2): - """ - Compacts the path, by removing points that are close to each other. - This method modifies the path in place. - """ - pass - - def getbbox(self): - """Gets the bounding box.""" - pass - - def map(self, function): - """Maps the path through a function.""" - pass - - def tolist(self, flat=0): - """ - 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. - """ - pass - - def transform(self, matrix): - """Transforms the path.""" - pass - - -# override with C implementation Path = Image.core.path - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageQt.py b/server/www/packages/packages-windows/x86/PIL/ImageQt.py index 4eb3654..c9dc363 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageQt.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageQt.py @@ -16,28 +16,36 @@ # 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, RuntimeError): +qt_versions = [ + ['5', 'PyQt5'], + ['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, RuntimeError): - try: + if qt_module == 'PyQt5': + from PyQt5.QtGui import QImage, qRgba, QPixmap + from PyQt5.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 +55,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 @@ -122,10 +131,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") if isPath(im): im = Image.open(im) @@ -156,26 +165,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']) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageShow.py b/server/www/packages/packages-windows/x86/PIL/ImageShow.py index c18ff22..b50d613 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageShow.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageShow.py @@ -18,7 +18,7 @@ from PIL import Image import os import sys -if sys.version_info >= (3, 3): +if sys.version_info.major >= 3: from shlex import quote else: from pipes import quote @@ -39,13 +39,13 @@ def register(viewer, order=1): 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. - @return True if a suitable viewer was found, false otherwise. + :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): @@ -69,7 +69,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) @@ -77,6 +77,7 @@ class Viewer(object): # hook methods format = None + options = {} def get_format(self, image): """Return format name, or None to save as PGM/PPM""" @@ -87,7 +88,7 @@ class Viewer(object): def save_image(self, image): """Save to temporary file, and return filename""" - return image._dump(format=self.get_format(image)) + return image._dump(format=self.get_format(image), **self.options) def show_image(self, image, **options): """Display given image""" @@ -100,6 +101,7 @@ class Viewer(object): # -------------------------------------------------------------------- + if sys.platform == "win32": class WindowsViewer(Viewer): @@ -115,7 +117,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 @@ -142,6 +145,9 @@ else: return None class UnixViewer(Viewer): + format = "PNG" + options = {'compress_level': 1} + def show_file(self, file, **options): command, executable = self.get_command_ex(file, **options) command = "(%s %s; rm -f %s)&" % (command, quote(file), @@ -159,6 +165,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 @@ -172,7 +186,9 @@ else: register(XVViewer) if __name__ == "__main__": - # usage: python ImageShow.py imagefile [title] - print(show(Image.open(sys.argv[1]), *sys.argv[2:])) -# End of file + if len(sys.argv) < 2: + print("Syntax: python ImageShow.py imagefile [title]") + sys.exit() + + print(show(Image.open(sys.argv[1]), *sys.argv[2:])) diff --git a/server/www/packages/packages-windows/x86/PIL/ImageStat.py b/server/www/packages/packages-windows/x86/PIL/ImageStat.py index f3c138b..cd58fc8 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageStat.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageStat.py @@ -144,4 +144,5 @@ class Stat(object): v.append(math.sqrt(self.var[i])) return v + Global = Stat # compatibility diff --git a/server/www/packages/packages-windows/x86/PIL/ImageTk.py b/server/www/packages/packages-windows/x86/PIL/ImageTk.py index 6d47130..b5ad53d 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageTk.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageTk.py @@ -25,14 +25,21 @@ # See the README file for information on usage and redistribution. # -try: - import tkinter -except ImportError: - import Tkinter - tkinter = Tkinter - del Tkinter +import sys -from PIL import Image +if sys.version_info.major > 2: + import tkinter +else: + import Tkinter as tkinter + +# required for pypy, which always has cffi installed +try: + from cffi import FFI + ffi = FFI() +except ImportError: + pass + +from . import Image from io import BytesIO @@ -162,8 +169,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 @@ -182,9 +189,15 @@ 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'): + # Pypy is using a ffi cdata element + # (Pdb) self.tk.interp + # + _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) @@ -264,6 +277,8 @@ 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) @@ -286,5 +301,3 @@ def _show(image, title): if title: top.title(title) UI(top, image).pack() - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageTransform.py b/server/www/packages/packages-windows/x86/PIL/ImageTransform.py index 9f48833..c3f6af8 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageTransform.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageTransform.py @@ -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): @@ -41,10 +41,10 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - @def AffineTransform(matrix) - @param matrix A 6-tuple (a, b, c, d, e, f) containing the first two rows + 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. - @see Image#Image.transform """ method = Image.AFFINE @@ -62,10 +62,10 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - @def ExtentTransform(bbox) - @param bbox A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. - @see Image#Image.transform + 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 @@ -77,11 +77,11 @@ class QuadTransform(Transform): 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 (x0, y0, x1, y1, x2, y2, y3, y3) which contain the + 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. - @see Image#Image.transform """ method = Image.QUAD @@ -91,10 +91,8 @@ class MeshTransform(Transform): Define a 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 + See :py:meth:`~PIL.Image.Image.transform` + + :param data: A list of (bbox, quad) tuples. """ method = Image.MESH - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImageWin.py b/server/www/packages/packages-windows/x86/PIL/ImageWin.py index 1e408da..9b86270 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImageWin.py +++ b/server/www/packages/packages-windows/x86/PIL/ImageWin.py @@ -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,14 +183,6 @@ class Dib(object): """ return self.image.tobytes() - def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please use frombytes() instead.") - - def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " + - "Please use tobytes() instead.") - class Window(object): """Create a Window with the given title size.""" @@ -233,5 +226,3 @@ class ImageWindow(Window): def ui_handle_repair(self, dc, x0, y0, x1, y1): self.image.draw(dc, (x0, y0, x1, y1)) - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/ImtImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/ImtImagePlugin.py index 63e8924..05e8cd3 100644 --- a/server/www/packages/packages-windows/x86/PIL/ImtImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/ImtImagePlugin.py @@ -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) diff --git a/server/www/packages/packages-windows/x86/PIL/IptcImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/IptcImagePlugin.py index 56d1de4..f5a8de1 100644 --- a/server/www/packages/packages-windows/x86/PIL/IptcImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/IptcImagePlugin.py @@ -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,7 +103,7 @@ class IptcImageFile(ImageFile.ImageFile): else: self.info[tag] = tagdata - # print tag, self.info[tag] + # print(tag, self.info[tag]) # mode layers = i8(self.info[(3, 60)][0]) @@ -168,14 +164,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 +179,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 diff --git a/server/www/packages/packages-windows/x86/PIL/Jpeg2KImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/Jpeg2KImagePlugin.py index 02b1e53..25fbefb 100644 --- a/server/www/packages/packages-windows/x86/PIL/Jpeg2KImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/Jpeg2KImagePlugin.py @@ -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: @@ -85,7 +85,7 @@ def _parse_jp2_header(fp): mode = None bpc = None nc = None - + hio = io.BytesIO(header) while True: lbox, tbox = struct.unpack('>I4s', hio.read(8)) @@ -114,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' @@ -144,7 +144,7 @@ def _parse_jp2_header(fp): if size is None or mode is None: raise SyntaxError("Malformed jp2 header") - + return (size, mode) ## @@ -192,7 +192,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): length = -1 self.tile = [('jpeg2k', (0, 0) + self.size, 0, - (self.codec, self.reduce, self.layers, fd, length, self.fp))] + (self.codec, self.reduce, self.layers, fd, length))] def load(self): if self.reduce: @@ -207,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): @@ -266,15 +266,11 @@ 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') diff --git a/server/www/packages/packages-windows/x86/PIL/JpegImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/JpegImagePlugin.py index 9d4eaab..97ef834 100644 --- a/server/www/packages/packages-windows/x86/PIL/JpegImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/JpegImagePlugin.py @@ -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" @@ -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 @@ -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): # @@ -316,7 +334,7 @@ class JpegImageFile(ImageFile.ImageFile): if i in MARKER: name, description, handler = MARKER[i] - # print hex(i), name, description + # print(hex(i), name, description) if handler is not None: handler(self, i) if i == 0xFFDA: # start of scan @@ -336,11 +354,30 @@ class JpegImageFile(ImageFile.ImageFile): 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 @@ -349,7 +386,7 @@ 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 @@ -377,7 +414,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) @@ -403,10 +442,11 @@ def _fixup_dict(src_dict): try: if len(value) == 1 and not isinstance(value, dict): return value[0] - except: pass + except: + 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): @@ -485,8 +525,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)) @@ -534,7 +574,6 @@ RAWMODE = { "1": "L", "L": "L", "RGB": "RGB", - "RGBA": "RGB", "RGBX": "RGB", "CMYK": "CMYK;I", # assume adobe conventions "YCbCr": "YCbCr", @@ -585,7 +624,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) @@ -612,7 +651,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": @@ -641,7 +684,7 @@ def _save(im, fp, filename): for idx, table in enumerate(qtables): try: if len(table) != 64: - raise + raise TypeError table = array.array('B', table) except TypeError: raise ValueError("Invalid quantization table") @@ -674,15 +717,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, @@ -692,20 +740,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) @@ -747,9 +799,6 @@ def jpeg_factory(fp=None, filename=None): 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") diff --git a/server/www/packages/packages-windows/x86/PIL/JpegPresets.py b/server/www/packages/packages-windows/x86/PIL/JpegPresets.py index ece33bb..5f01f0d 100644 --- a/server/www/packages/packages-windows/x86/PIL/JpegPresets.py +++ b/server/www/packages/packages-windows/x86/PIL/JpegPresets.py @@ -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,12 @@ 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://web.archive.org/web/20120328125543/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" + '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 +86,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 +162,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 +181,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, diff --git a/server/www/packages/packages-windows/x86/PIL/McIdasImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/McIdasImagePlugin.py index b753603..06da33f 100644 --- a/server/www/packages/packages-windows/x86/PIL/McIdasImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/McIdasImagePlugin.py @@ -17,7 +17,7 @@ # import struct -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.2" @@ -66,6 +66,7 @@ class McIdasImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] + # -------------------------------------------------------------------- # registry diff --git a/server/www/packages/packages-windows/x86/PIL/MicImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/MicImagePlugin.py index 3c91244..1dbb6a5 100644 --- a/server/www/packages/packages-windows/x86/PIL/MicImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/MicImagePlugin.py @@ -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: @@ -95,6 +98,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): return self.frame + # # -------------------------------------------------------------------- diff --git a/server/www/packages/packages-windows/x86/PIL/MpegImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/MpegImagePlugin.py index 6671b86..fca7f9d 100644 --- a/server/www/packages/packages-windows/x86/PIL/MpegImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/MpegImagePlugin.py @@ -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" @@ -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") diff --git a/server/www/packages/packages-windows/x86/PIL/MpoImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/MpoImagePlugin.py index 1d26021..460ccec 100644 --- a/server/www/packages/packages-windows/x86/PIL/MpoImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/MpoImagePlugin.py @@ -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,14 +72,13 @@ 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): diff --git a/server/www/packages/packages-windows/x86/PIL/MspImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/MspImagePlugin.py index 85f8e76..9692d11 100644 --- a/server/www/packages/packages-windows/x86/PIL/MspImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/MspImagePlugin.py @@ -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"] @@ -61,13 +68,93 @@ class MspImageFile(ImageFile.ImageFile): 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 +182,7 @@ def _save(im, fp, filename): # image body ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))]) + # # registry diff --git a/server/www/packages/packages-windows/x86/PIL/OleFileIO-README.md b/server/www/packages/packages-windows/x86/PIL/OleFileIO-README.md deleted file mode 100644 index eb6c9bc..0000000 --- a/server/www/packages/packages-windows/x86/PIL/OleFileIO-README.md +++ /dev/null @@ -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: - -- **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-pillow.org), 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 © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 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. diff --git a/server/www/packages/packages-windows/x86/PIL/OleFileIO.py b/server/www/packages/packages-windows/x86/PIL/OleFileIO.py index 1998e3c..b3caa10 100644 --- a/server/www/packages/packages-windows/x86/PIL/OleFileIO.py +++ b/server/www/packages/packages-windows/x86/PIL/OleFileIO.py @@ -1,2305 +1,4 @@ -#!/usr/bin/env python - -# olefile (formerly OleFileIO_PL) version 0.42 2015-01-25 -# -# Module to read/write Microsoft OLE2 files (also called Structured Storage or -# Microsoft Compound Document File Format), such as Microsoft Office 97-2003 -# documents, Image Composer and FlashPix files, Outlook messages, ... -# This version is compatible with Python 2.6+ and 3.x -# -# Project website: http://www.decalage.info/olefile -# -# olefile is copyright (c) 2005-2015 Philippe Lagadec (http://www.decalage.info) -# -# olefile is based on the OleFileIO module from the PIL library v1.1.6 -# See: http://www.pythonware.com/products/pil/index.htm -# -# The Python Imaging Library (PIL) is -# Copyright (c) 1997-2005 by Secret Labs AB -# Copyright (c) 1995-2005 by Fredrik Lundh -# -# See source code and LICENSE.txt for information on usage and redistribution. - - -# Since OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported -# This import enables print() as a function rather than a keyword -# (main requirement to be compatible with Python 3.x) -# The comment on the line below should be printed on Python 2.5 or older: -from __future__ import print_function # This version of olefile requires Python 2.6+ or 3.x. - - -__author__ = "Philippe Lagadec" -__date__ = "2015-01-25" -__version__ = '0.42b' - -#--- LICENSE ------------------------------------------------------------------ - -# olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec -# (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. - -# ---------- -# PIL License: -# -# 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(s) not be used -# in advertising or publicity pertaining to distribution of the software -# without specific, written prior permission. -# -# SECRET LABS AB AND THE AUTHORS 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 AUTHORS 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. - -#----------------------------------------------------------------------------- -# CHANGELOG: (only olefile/OleFileIO_PL changes compared to PIL 1.1.6) -# 2005-05-11 v0.10 PL: - a few fixes for Python 2.4 compatibility -# (all changes flagged with [PL]) -# 2006-02-22 v0.11 PL: - a few fixes for some Office 2003 documents which raise -# exceptions in _OleStream.__init__() -# 2006-06-09 v0.12 PL: - fixes for files above 6.8MB (DIFAT in loadfat) -# - added some constants -# - added header values checks -# - added some docstrings -# - getsect: bugfix in case sectors >512 bytes -# - getsect: added conformity checks -# - DEBUG_MODE constant to activate debug display -# 2007-09-04 v0.13 PL: - improved/translated (lots of) comments -# - updated license -# - converted tabs to 4 spaces -# 2007-11-19 v0.14 PL: - added OleFileIO._raise_defect() to adapt sensitivity -# - improved _unicode() to use Python 2.x unicode support -# - fixed bug in _OleDirectoryEntry -# 2007-11-25 v0.15 PL: - added safety checks to detect FAT loops -# - fixed _OleStream which didn't check stream size -# - added/improved many docstrings and comments -# - moved helper functions _unicode and _clsid out of -# OleFileIO class -# - improved OleFileIO._find() to add Unix path syntax -# - OleFileIO._find() is now case-insensitive -# - added get_type() and get_rootentry_name() -# - rewritten loaddirectory and _OleDirectoryEntry -# 2007-11-27 v0.16 PL: - added _OleDirectoryEntry.kids_dict -# - added detection of duplicate filenames in storages -# - added detection of duplicate references to streams -# - added get_size() and exists() to _OleDirectoryEntry -# - added isOleFile to check header before parsing -# - added __all__ list to control public keywords in pydoc -# 2007-12-04 v0.17 PL: - added _load_direntry to fix a bug in loaddirectory -# - improved _unicode(), added workarounds for Python <2.3 -# - added set_debug_mode and -d option to set debug mode -# - fixed bugs in OleFileIO.open and _OleDirectoryEntry -# - added safety check in main for large or binary -# properties -# - allow size>0 for storages for some implementations -# 2007-12-05 v0.18 PL: - fixed several bugs in handling of FAT, MiniFAT and -# streams -# - added option '-c' in main to check all streams -# 2009-12-10 v0.19 PL: - bugfix for 32 bit arrays on 64 bits platforms -# (thanks to Ben G. and Martijn for reporting the bug) -# 2009-12-11 v0.20 PL: - bugfix in OleFileIO.open when filename is not plain str -# 2010-01-22 v0.21 PL: - added support for big-endian CPUs such as PowerPC Macs -# 2012-02-16 v0.22 PL: - fixed bug in getproperties, patch by chuckleberryfinn -# (https://bitbucket.org/decalage/olefileio_pl/issue/7) -# - added close method to OleFileIO (fixed issue #2) -# 2012-07-25 v0.23 PL: - added support for file-like objects (patch by mete0r_kr) -# 2013-05-05 v0.24 PL: - getproperties: added conversion from filetime to python -# datetime -# - main: displays properties with date format -# - new class OleMetadata to parse standard properties -# - added get_metadata method -# 2013-05-07 v0.24 PL: - a few improvements in OleMetadata -# 2013-05-24 v0.25 PL: - getproperties: option to not convert some timestamps -# - OleMetaData: total_edit_time is now a number of seconds, -# not a timestamp -# - getproperties: added support for VT_BOOL, VT_INT, V_UINT -# - getproperties: filter out null chars from strings -# - getproperties: raise non-fatal defects instead of -# exceptions when properties cannot be parsed properly -# 2013-05-27 PL: - getproperties: improved exception handling -# - _raise_defect: added option to set exception type -# - all non-fatal issues are now recorded, and displayed -# when run as a script -# 2013-07-11 v0.26 PL: - added methods to get modification and creation times -# of a directory entry or a storage/stream -# - fixed parsing of direntry timestamps -# 2013-07-24 PL: - new options in listdir to list storages and/or streams -# 2014-02-04 v0.30 PL: - upgraded code to support Python 3.x by Martin Panter -# - several fixes for Python 2.6 (xrange, MAGIC) -# - reused i32 from Pillow's _binary -# 2014-07-18 v0.31 - preliminary support for 4K sectors -# 2014-07-27 v0.31 PL: - a few improvements in OleFileIO.open (header parsing) -# - Fixed loadfat for large files with 4K sectors (issue #3) -# 2014-07-30 v0.32 PL: - added write_sect to write sectors to disk -# - added write_mode option to OleFileIO.__init__ and open -# 2014-07-31 PL: - fixed padding in write_sect for Python 3, added checks -# - added write_stream to write a stream to disk -# 2014-09-26 v0.40 PL: - renamed OleFileIO_PL to olefile -# 2014-11-09 NE: - added support for Jython (Niko Ehrenfeuchter) -# 2014-11-13 v0.41 PL: - improved isOleFile and OleFileIO.open to support OLE -# data in a string buffer and file-like objects. -# 2014-11-21 PL: - updated comments according to Pillow's commits -# 2015-01-24 v0.42 PL: - changed the default path name encoding from Latin-1 -# to UTF-8 on Python 2.x (Unicode on Python 3.x) -# - added path_encoding option to override the default -# - fixed a bug in _list when a storage is empty - -#----------------------------------------------------------------------------- -# TODO (for version 1.0): -# + get rid of print statements, to simplify Python 2.x and 3.x support -# + add is_stream and is_storage -# + remove leading and trailing slashes where a path is used -# + add functions path_list2str and path_str2list -# + fix how all the methods handle unicode str and/or bytes as arguments -# + add path attrib to _OleDirEntry, set it once and for all in init or -# append_kids (then listdir/_list can be simplified) -# - TESTS with Linux, MacOSX, Python 1.5.2, various files, PIL, ... -# - add underscore to each private method, to avoid their display in -# pydoc/epydoc documentation - Remove it for classes to be documented -# - replace all raised exceptions with _raise_defect (at least in OleFileIO) -# - merge code from _OleStream and OleFileIO.getsect to read sectors -# (maybe add a class for FAT and MiniFAT ?) -# - add method to check all streams (follow sectors chains without storing all -# stream in memory, and report anomalies) -# - use _OleDirectoryEntry.kids_dict to improve _find and _list ? -# - fix Unicode names handling (find some way to stay compatible with Py1.5.2) -# => if possible avoid converting names to Latin-1 -# - review DIFAT code: fix handling of DIFSECT blocks in FAT (not stop) -# - rewrite OleFileIO.getproperties -# - improve docstrings to show more sample uses -# - see also original notes and FIXME below -# - remove all obsolete FIXMEs -# - OleMetadata: fix version attrib according to -# http://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx - -# IDEAS: -# - in OleFileIO._open and _OleStream, use size=None instead of 0x7FFFFFFF for -# streams with unknown size -# - use arrays of int instead of long integers for FAT/MiniFAT, to improve -# performance and reduce memory usage ? (possible issue with values >2^31) -# - provide tests with unittest (may need write support to create samples) -# - move all debug code (and maybe dump methods) to a separate module, with -# a class which inherits OleFileIO ? -# - fix docstrings to follow epydoc format -# - add support for big endian byte order ? -# - create a simple OLE explorer with wxPython - -# FUTURE EVOLUTIONS to add write support: -# see issue #6 on Bitbucket: -# https://bitbucket.org/decalage/olefileio_pl/issue/6/improve-olefileio_pl-to-write-ole-files - -#----------------------------------------------------------------------------- -# NOTES from PIL 1.1.6: - -# History: -# 1997-01-20 fl Created -# 1997-01-22 fl Fixed 64-bit portability quirk -# 2003-09-09 fl Fixed typo in OleFileIO.loadfat (noted by Daniel Haertle) -# 2004-02-29 fl Changed long hex constants to signed integers -# -# Notes: -# FIXME: sort out sign problem (eliminate long hex constants) -# FIXME: change filename to use "a/b/c" instead of ["a", "b", "c"] -# FIXME: provide a glob mechanism function (using fnmatchcase) -# -# Literature: -# -# "FlashPix Format Specification, Appendix A", Kodak and Microsoft, -# September 1996. -# -# Quotes: -# -# "If this document and functionality of the Software conflict, -# the actual functionality of the Software represents the correct -# functionality" -- Microsoft, in the OLE format specification - -#------------------------------------------------------------------------------ - - -import io -import sys -import struct -import array -import os.path -import datetime - -#=== COMPATIBILITY WORKAROUNDS ================================================ - -# [PL] Define explicitly the public API to avoid private objects in pydoc: -#TODO: add more -# __all__ = ['OleFileIO', 'isOleFile', 'MAGIC'] - -# For Python 3.x, need to redefine long as int: -if str is not bytes: - long = int - -# Need to make sure we use xrange both on Python 2 and 3.x: -try: - # on Python 2 we need xrange: - iterrange = xrange -except: - # no xrange, for Python 3 it was renamed as range: - iterrange = range - -# [PL] workaround to fix an issue with array item size on 64 bits systems: -if array.array('L').itemsize == 4: - # on 32 bits platforms, long integers in an array are 32 bits: - UINT32 = 'L' -elif array.array('I').itemsize == 4: - # on 64 bits platforms, integers in an array are 32 bits: - UINT32 = 'I' -elif array.array('i').itemsize == 4: - # On 64 bit Jython, signed integers ('i') are the only way to store our 32 - # bit values in an array in a *somewhat* reasonable way, as the otherwise - # perfectly suited 'H' (unsigned int, 32 bits) results in a completely - # unusable behaviour. This is most likely caused by the fact that Java - # doesn't have unsigned values, and thus Jython's "array" implementation, - # which is based on "jarray", doesn't have them either. - # NOTE: to trick Jython into converting the values it would normally - # interpret as "signed" into "unsigned", a binary-and operation with - # 0xFFFFFFFF can be used. This way it is possible to use the same comparing - # operations on all platforms / implementations. The corresponding code - # lines are flagged with a 'JYTHON-WORKAROUND' tag below. - UINT32 = 'i' -else: - raise ValueError('Need to fix a bug with 32 bit arrays, please contact author...') - - -# [PL] These workarounds were inspired from the Path module -# (see http://www.jorendorff.com/articles/python/path/) -try: - basestring -except NameError: - basestring = str - -# [PL] Experimental setting: if True, OLE filenames will be kept in Unicode -# if False (default PIL behaviour), all filenames are converted to Latin-1. -KEEP_UNICODE_NAMES = True - -if sys.version_info[0] < 3: - # On Python 2.x, the default encoding for path names is UTF-8: - DEFAULT_PATH_ENCODING = 'utf-8' -else: - # On Python 3.x, the default encoding for path names is Unicode (None): - DEFAULT_PATH_ENCODING = None - - -#=== DEBUGGING =============================================================== - -#TODO: replace this by proper logging - -# [PL] DEBUG display mode: False by default, use set_debug_mode() or "-d" on -# command line to change it. -DEBUG_MODE = False - - -def debug_print(msg): - print(msg) - - -def debug_pass(msg): - pass - - -debug = debug_pass - - -def set_debug_mode(debug_mode): - """ - Set debug mode on or off, to control display of debugging messages. - :param mode: True or False - """ - global DEBUG_MODE, debug - DEBUG_MODE = debug_mode - if debug_mode: - debug = debug_print - else: - debug = debug_pass - - -#=== CONSTANTS =============================================================== - -# magic bytes that should be at the beginning of every OLE file: -MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' - -# [PL]: added constants for Sector IDs (from AAF specifications) -MAXREGSECT = 0xFFFFFFFA # (-6) maximum SECT -DIFSECT = 0xFFFFFFFC # (-4) denotes a DIFAT sector in a FAT -FATSECT = 0xFFFFFFFD # (-3) denotes a FAT sector in a FAT -ENDOFCHAIN = 0xFFFFFFFE # (-2) end of a virtual stream chain -FREESECT = 0xFFFFFFFF # (-1) unallocated sector - -# [PL]: added constants for Directory Entry IDs (from AAF specifications) -MAXREGSID = 0xFFFFFFFA # (-6) maximum directory entry ID -NOSTREAM = 0xFFFFFFFF # (-1) unallocated directory entry - -# [PL] object types in storage (from AAF specifications) -STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc) -STGTY_STORAGE = 1 # element is a storage object -STGTY_STREAM = 2 # element is a stream object -STGTY_LOCKBYTES = 3 # element is an ILockBytes object -STGTY_PROPERTY = 4 # element is an IPropertyStorage object -STGTY_ROOT = 5 # element is a root storage - - -# -# -------------------------------------------------------------------- -# property types - -VT_EMPTY = 0; VT_NULL = 1; VT_I2 = 2; VT_I4 = 3; VT_R4 = 4; VT_R8 = 5; VT_CY = 6; -VT_DATE = 7; VT_BSTR = 8; VT_DISPATCH = 9; VT_ERROR = 10; VT_BOOL = 11; -VT_VARIANT = 12; VT_UNKNOWN = 13; VT_DECIMAL = 14; VT_I1 = 16; VT_UI1 = 17; -VT_UI2 = 18; VT_UI4 = 19; VT_I8 = 20; VT_UI8 = 21; VT_INT = 22; VT_UINT = 23; -VT_VOID = 24; VT_HRESULT = 25; VT_PTR = 26; VT_SAFEARRAY = 27; VT_CARRAY = 28; -VT_USERDEFINED = 29; VT_LPSTR = 30; VT_LPWSTR = 31; VT_FILETIME = 64; -VT_BLOB = 65; VT_STREAM = 66; VT_STORAGE = 67; VT_STREAMED_OBJECT = 68; -VT_STORED_OBJECT = 69; VT_BLOB_OBJECT = 70; VT_CF = 71; VT_CLSID = 72; -VT_VECTOR = 0x1000; - -# map property id to name (for debugging purposes) - -VT = {} -for keyword, var in list(vars().items()): - if keyword[:3] == "VT_": - VT[var] = keyword - -# -# -------------------------------------------------------------------- -# Some common document types (root.clsid fields) - -WORD_CLSID = "00020900-0000-0000-C000-000000000046" -#TODO: check Excel, PPT, ... - -# [PL]: Defect levels to classify parsing errors - see OleFileIO._raise_defect() -DEFECT_UNSURE = 10 # a case which looks weird, but not sure it's a defect -DEFECT_POTENTIAL = 20 # a potential defect -DEFECT_INCORRECT = 30 # an error according to specifications, but parsing - # can go on -DEFECT_FATAL = 40 # an error which cannot be ignored, parsing is - # impossible - -# Minimal size of an empty OLE file, with 512-bytes sectors = 1536 bytes -# (this is used in isOleFile and OleFile.open) -MINIMAL_OLEFILE_SIZE = 1536 - -# [PL] add useful constants to __all__: -# for key in list(vars().keys()): -# if key.startswith('STGTY_') or key.startswith('DEFECT_'): -# __all__.append(key) - - -#=== FUNCTIONS =============================================================== - -def isOleFile(filename): - """ - Test if a file is an OLE container (according to the magic bytes in its header). - - :param filename: string-like or file-like object, OLE file to parse - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read and seek methods), - it is parsed as-is. - - :returns: True if OLE, False otherwise. - """ - # check if filename is a string-like or file-like object: - if hasattr(filename, 'read'): - # file-like object: use it directly - header = filename.read(len(MAGIC)) - # just in case, seek back to start of file: - filename.seek(0) - elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: - # filename is a bytes string containing the OLE file to be parsed: - header = filename[:len(MAGIC)] - else: - # string-like object: filename of file on disk - header = open(filename, 'rb').read(len(MAGIC)) - if header == MAGIC: - return True - else: - return False - - -if bytes is str: - # version for Python 2.x - def i8(c): - return ord(c) -else: - # version for Python 3.x - def i8(c): - return c if c.__class__ is int else c[0] - - -#TODO: replace i16 and i32 with more readable struct.unpack equivalent? - -def i16(c, o = 0): - """ - Converts a 2-bytes (16 bits) string to an integer. - - c: string containing bytes to convert - o: offset of bytes to convert in string - """ - return struct.unpack(" len(fat): - raise IOError('malformed OLE document, stream too large') - # optimization(?): data is first a list of strings, and join() is called - # at the end to concatenate all in one string. - # (this may not be really useful with recent Python versions) - data = [] - # if size is zero, then first sector index should be ENDOFCHAIN: - if size == 0 and sect != ENDOFCHAIN: - debug('size == 0 and sect != ENDOFCHAIN:') - raise IOError('incorrect OLE sector index for empty stream') - # [PL] A fixed-length for loop is used instead of an undefined while - # loop to avoid DoS attacks: - for i in range(nb_sectors): - # Sector index may be ENDOFCHAIN, but only if size was unknown - if sect == ENDOFCHAIN: - if unknown_size: - break - else: - # else this means that the stream is smaller than declared: - debug('sect=ENDOFCHAIN before expected size') - raise IOError('incomplete OLE stream') - # sector index should be within FAT: - if sect < 0 or sect >= len(fat): - debug('sect=%d (%X) / len(fat)=%d' % (sect, sect, len(fat))) - debug('i=%d / nb_sectors=%d' % (i, nb_sectors)) -## tmp_data = b"".join(data) -## f = open('test_debug.bin', 'wb') -## f.write(tmp_data) -## f.close() -## debug('data read so far: %d bytes' % len(tmp_data)) - raise IOError('incorrect OLE FAT, sector index out of range') - #TODO: merge this code with OleFileIO.getsect() ? - #TODO: check if this works with 4K sectors: - try: - fp.seek(offset + sectorsize * sect) - except: - debug('sect=%d, seek=%d, filesize=%d' % - (sect, offset+sectorsize*sect, filesize)) - raise IOError('OLE sector index out of range') - sector_data = fp.read(sectorsize) - # [PL] check if there was enough data: - # Note: if sector is the last of the file, sometimes it is not a - # complete sector (of 512 or 4K), so we may read less than - # sectorsize. - if len(sector_data) != sectorsize and sect != (len(fat)-1): - debug('sect=%d / len(fat)=%d, seek=%d / filesize=%d, len read=%d' % - (sect, len(fat), offset+sectorsize*sect, filesize, len(sector_data))) - debug('seek+len(read)=%d' % (offset+sectorsize*sect+len(sector_data))) - raise IOError('incomplete OLE sector') - data.append(sector_data) - # jump to next sector in the FAT: - try: - sect = fat[sect] & 0xFFFFFFFF # JYTHON-WORKAROUND - except IndexError: - # [PL] if pointer is out of the FAT an exception is raised - raise IOError('incorrect OLE FAT, sector index out of range') - # [PL] Last sector should be a "end of chain" marker: - if sect != ENDOFCHAIN: - raise IOError('incorrect last sector index in OLE stream') - data = b"".join(data) - # Data is truncated to the actual stream size: - if len(data) >= size: - data = data[:size] - # actual stream size is stored for future use: - self.size = size - elif unknown_size: - # actual stream size was not known, now we know the size of read - # data: - self.size = len(data) - else: - # read data is less than expected: - debug('len(data)=%d, size=%d' % (len(data), size)) - raise IOError('OLE stream size is less than declared') - # when all data is read in memory, BytesIO constructor is called - io.BytesIO.__init__(self, data) - # Then the _OleStream object can be used as a read-only file object. - - -#--- _OleDirectoryEntry ------------------------------------------------------- - -class _OleDirectoryEntry(object): - - """ - OLE2 Directory Entry - """ - # [PL] parsing code moved from OleFileIO.loaddirectory - - # struct to parse directory entries: - # <: little-endian byte order, standard sizes - # (note: this should guarantee that Q returns a 64 bits int) - # 64s: string containing entry name in unicode (max 31 chars) + null char - # H: uint16, number of bytes used in name buffer, including null = (len+1)*2 - # B: uint8, dir entry type (between 0 and 5) - # B: uint8, color: 0=black, 1=red - # I: uint32, index of left child node in the red-black tree, NOSTREAM if none - # I: uint32, index of right child node in the red-black tree, NOSTREAM if none - # I: uint32, index of child root node if it is a storage, else NOSTREAM - # 16s: CLSID, unique identifier (only used if it is a storage) - # I: uint32, user flags - # Q (was 8s): uint64, creation timestamp or zero - # Q (was 8s): uint64, modification timestamp or zero - # I: uint32, SID of first sector if stream or ministream, SID of 1st sector - # of stream containing ministreams if root entry, 0 otherwise - # I: uint32, total stream size in bytes if stream (low 32 bits), 0 otherwise - # I: uint32, total stream size in bytes if stream (high 32 bits), 0 otherwise - STRUCT_DIRENTRY = '<64sHBBIII16sIQQIII' - # size of a directory entry: 128 bytes - DIRENTRY_SIZE = 128 - assert struct.calcsize(STRUCT_DIRENTRY) == DIRENTRY_SIZE - - def __init__(self, entry, sid, olefile): - """ - Constructor for an _OleDirectoryEntry object. - Parses a 128-bytes entry from the OLE Directory stream. - - :param entry : string (must be 128 bytes long) - :param sid : index of this directory entry in the OLE file directory - :param olefile: OleFileIO containing this directory entry - """ - self.sid = sid - # ref to olefile is stored for future use - self.olefile = olefile - # kids is a list of children entries, if this entry is a storage: - # (list of _OleDirectoryEntry objects) - self.kids = [] - # kids_dict is a dictionary of children entries, indexed by their - # name in lowercase: used to quickly find an entry, and to detect - # duplicates - self.kids_dict = {} - # flag used to detect if the entry is referenced more than once in - # directory: - self.used = False - # decode DirEntry - ( - name, - namelength, - self.entry_type, - self.color, - self.sid_left, - self.sid_right, - self.sid_child, - clsid, - self.dwUserFlags, - self.createTime, - self.modifyTime, - self.isectStart, - sizeLow, - sizeHigh - ) = struct.unpack(_OleDirectoryEntry.STRUCT_DIRENTRY, entry) - if self.entry_type not in [STGTY_ROOT, STGTY_STORAGE, STGTY_STREAM, STGTY_EMPTY]: - olefile.raise_defect(DEFECT_INCORRECT, 'unhandled OLE storage type') - # only first directory entry can (and should) be root: - if self.entry_type == STGTY_ROOT and sid != 0: - olefile.raise_defect(DEFECT_INCORRECT, 'duplicate OLE root entry') - if sid == 0 and self.entry_type != STGTY_ROOT: - olefile.raise_defect(DEFECT_INCORRECT, 'incorrect OLE root entry') - #debug (struct.unpack(fmt_entry, entry[:len_entry])) - # name should be at most 31 unicode characters + null character, - # so 64 bytes in total (31*2 + 2): - if namelength > 64: - olefile.raise_defect(DEFECT_INCORRECT, 'incorrect DirEntry name length') - # if exception not raised, namelength is set to the maximum value: - namelength = 64 - # only characters without ending null char are kept: - name = name[:(namelength-2)] - #TODO: check if the name is actually followed by a null unicode character ([MS-CFB] 2.6.1) - #TODO: check if the name does not contain forbidden characters: - # [MS-CFB] 2.6.1: "The following characters are illegal and MUST NOT be part of the name: '/', '\', ':', '!'." - # name is converted from UTF-16LE to the path encoding specified in the OleFileIO: - self.name = olefile._decode_utf16_str(name) - - debug('DirEntry SID=%d: %s' % (self.sid, repr(self.name))) - debug(' - type: %d' % self.entry_type) - debug(' - sect: %d' % self.isectStart) - debug(' - SID left: %d, right: %d, child: %d' % (self.sid_left, - self.sid_right, self.sid_child)) - - # sizeHigh is only used for 4K sectors, it should be zero for 512 bytes - # sectors, BUT apparently some implementations set it as 0xFFFFFFFF, 1 - # or some other value so it cannot be raised as a defect in general: - if olefile.sectorsize == 512: - if sizeHigh != 0 and sizeHigh != 0xFFFFFFFF: - debug('sectorsize=%d, sizeLow=%d, sizeHigh=%d (%X)' % - (olefile.sectorsize, sizeLow, sizeHigh, sizeHigh)) - olefile.raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size') - self.size = sizeLow - else: - self.size = sizeLow + (long(sizeHigh) << 32) - debug(' - size: %d (sizeLow=%d, sizeHigh=%d)' % (self.size, sizeLow, sizeHigh)) - - self.clsid = _clsid(clsid) - # a storage should have a null size, BUT some implementations such as - # Word 8 for Mac seem to allow non-null values => Potential defect: - if self.entry_type == STGTY_STORAGE and self.size != 0: - olefile.raise_defect(DEFECT_POTENTIAL, 'OLE storage with size>0') - # check if stream is not already referenced elsewhere: - if self.entry_type in (STGTY_ROOT, STGTY_STREAM) and self.size > 0: - if self.size < olefile.minisectorcutoff \ - and self.entry_type == STGTY_STREAM: # only streams can be in MiniFAT - # ministream object - minifat = True - else: - minifat = False - olefile._check_duplicate_stream(self.isectStart, minifat) - - def build_storage_tree(self): - """ - Read and build the red-black tree attached to this _OleDirectoryEntry - object, if it is a storage. - Note that this method builds a tree of all subentries, so it should - only be called for the root object once. - """ - debug('build_storage_tree: SID=%d - %s - sid_child=%d' - % (self.sid, repr(self.name), self.sid_child)) - if self.sid_child != NOSTREAM: - # if child SID is not NOSTREAM, then this entry is a storage. - # Let's walk through the tree of children to fill the kids list: - self.append_kids(self.sid_child) - - # Note from OpenOffice documentation: the safest way is to - # recreate the tree because some implementations may store broken - # red-black trees... - - # in the OLE file, entries are sorted on (length, name). - # for convenience, we sort them on name instead: - # (see rich comparison methods in this class) - self.kids.sort() - - def append_kids(self, child_sid): - """ - Walk through red-black tree of children of this directory entry to add - all of them to the kids list. (recursive method) - - :param child_sid : index of child directory entry to use, or None when called - first time for the root. (only used during recursion) - """ - # [PL] this method was added to use simple recursion instead of a complex - # algorithm. - # if this is not a storage or a leaf of the tree, nothing to do: - if child_sid == NOSTREAM: - return - # check if child SID is in the proper range: - if child_sid < 0 or child_sid >= len(self.olefile.direntries): - self.olefile.raise_defect(DEFECT_FATAL, 'OLE DirEntry index out of range') - # get child direntry: - child = self.olefile._load_direntry(child_sid) #direntries[child_sid] - debug('append_kids: child_sid=%d - %s - sid_left=%d, sid_right=%d, sid_child=%d' - % (child.sid, repr(child.name), child.sid_left, child.sid_right, child.sid_child)) - # the directory entries are organized as a red-black tree. - # (cf. Wikipedia for details) - # First walk through left side of the tree: - self.append_kids(child.sid_left) - # Check if its name is not already used (case-insensitive): - name_lower = child.name.lower() - if name_lower in self.kids_dict: - self.olefile.raise_defect(DEFECT_INCORRECT, - "Duplicate filename in OLE storage") - # Then the child_sid _OleDirectoryEntry object is appended to the - # kids list and dictionary: - self.kids.append(child) - self.kids_dict[name_lower] = child - # Check if kid was not already referenced in a storage: - if child.used: - self.olefile.raise_defect(DEFECT_INCORRECT, - 'OLE Entry referenced more than once') - child.used = True - # Finally walk through right side of the tree: - self.append_kids(child.sid_right) - # Afterwards build kid's own tree if it's also a storage: - child.build_storage_tree() - - def __eq__(self, other): - "Compare entries by name" - return self.name == other.name - - def __lt__(self, other): - "Compare entries by name" - return self.name < other.name - - def __ne__(self, other): - return not self.__eq__(other) - - def __le__(self, other): - return self.__eq__(other) or self.__lt__(other) - - # Reflected __lt__() and __le__() will be used for __gt__() and __ge__() - - #TODO: replace by the same function as MS implementation ? - # (order by name length first, then case-insensitive order) - - def dump(self, tab = 0): - "Dump this entry, and all its subentries (for debug purposes only)" - TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)", - "(property)", "(root)"] - print(" "*tab + repr(self.name), TYPES[self.entry_type], end=' ') - if self.entry_type in (STGTY_STREAM, STGTY_ROOT): - print(self.size, "bytes", end=' ') - print() - if self.entry_type in (STGTY_STORAGE, STGTY_ROOT) and self.clsid: - print(" "*tab + "{%s}" % self.clsid) - - for kid in self.kids: - kid.dump(tab + 2) - - def getmtime(self): - """ - Return modification time of a directory entry. - - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - if self.modifyTime == 0: - return None - return filetime2datetime(self.modifyTime) - - def getctime(self): - """ - Return creation time of a directory entry. - - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - if self.createTime == 0: - return None - return filetime2datetime(self.createTime) - - -#--- OleFileIO ---------------------------------------------------------------- - -class OleFileIO(object): - """ - OLE container object - - This class encapsulates the interface to an OLE 2 structured - storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and - :py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to - access the contents of this file. - - Object names are given as a list of strings, one for each subentry - level. The root entry should be omitted. For example, the following - code extracts all image streams from a Microsoft Image Composer file:: - - ole = OleFileIO("fan.mic") - - for entry in ole.listdir(): - if entry[1:2] == "Image": - fin = ole.openstream(entry) - fout = open(entry[0:1], "wb") - while True: - s = fin.read(8192) - if not s: - break - fout.write(s) - - You can use the viewer application provided with the Python Imaging - Library to view the resulting files (which happens to be standard - TIFF files). - """ - - def __init__(self, filename=None, raise_defects=DEFECT_FATAL, - write_mode=False, debug=False, path_encoding=DEFAULT_PATH_ENCODING): - """ - Constructor for the OleFileIO class. - - :param filename: file to open. - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read, seek and tell methods), - it is parsed as-is. - - :param raise_defects: minimal level for defects to be raised as exceptions. - (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a - security-oriented application, see source code for details) - - :param write_mode: bool, if True the file is opened in read/write mode instead - of read-only by default. - - :param debug: bool, set debug mode - - :param path_encoding: None or str, name of the codec to use for path - names (streams and storages), or None for Unicode. - Unicode by default on Python 3+, UTF-8 on Python 2.x. - (new in olefile 0.42, was hardcoded to Latin-1 until olefile v0.41) - """ - set_debug_mode(debug) - # minimal level for defects to be raised as exceptions: - self._raise_defects_level = raise_defects - # list of defects/issues not raised as exceptions: - # tuples of (exception type, message) - self.parsing_issues = [] - self.write_mode = write_mode - self.path_encoding = path_encoding - self._filesize = None - self.fp = None - if filename: - self.open(filename, write_mode=write_mode) - - def raise_defect(self, defect_level, message, exception_type=IOError): - """ - This method should be called for any defect found during file parsing. - It may raise an IOError exception according to the minimal level chosen - for the OleFileIO object. - - :param defect_level: defect level, possible values are: - - - DEFECT_UNSURE : a case which looks weird, but not sure it's a defect - - DEFECT_POTENTIAL : a potential defect - - DEFECT_INCORRECT : an error according to specifications, but parsing can go on - - DEFECT_FATAL : an error which cannot be ignored, parsing is impossible - - :param message: string describing the defect, used with raised exception. - :param exception_type: exception class to be raised, IOError by default - """ - # added by [PL] - if defect_level >= self._raise_defects_level: - raise exception_type(message) - else: - # just record the issue, no exception raised: - self.parsing_issues.append((exception_type, message)) - - def _decode_utf16_str(self, utf16_str, errors='replace'): - """ - Decode a string encoded in UTF-16 LE format, as found in the OLE - directory or in property streams. Return a string encoded - according to the path_encoding specified for the OleFileIO object. - - :param utf16_str: bytes string encoded in UTF-16 LE format - :param errors: str, see python documentation for str.decode() - :return: str, encoded according to path_encoding - """ - unicode_str = utf16_str.decode('UTF-16LE', errors) - if self.path_encoding: - # an encoding has been specified for path names: - return unicode_str.encode(self.path_encoding, errors) - else: - # path_encoding=None, return the Unicode string as-is: - return unicode_str - - def open(self, filename, write_mode=False): - """ - Open an OLE2 file in read-only or read/write mode. - Read and parse the header, FAT and directory. - - :param filename: string-like or file-like object, OLE file to parse - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read, seek and tell methods), - it is parsed as-is. - - :param write_mode: bool, if True the file is opened in read/write mode instead - of read-only by default. (ignored if filename is not a path) - """ - self.write_mode = write_mode - # [PL] check if filename is a string-like or file-like object: - # (it is better to check for a read() method) - if hasattr(filename, 'read'): - #TODO: also check seek and tell methods? - # file-like object: use it directly - self.fp = filename - elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: - # filename is a bytes string containing the OLE file to be parsed: - # convert it to BytesIO - self.fp = io.BytesIO(filename) - else: - # string-like object: filename of file on disk - if self.write_mode: - # open file in mode 'read with update, binary' - # According to https://docs.python.org/2/library/functions.html#open - # 'w' would truncate the file, 'a' may only append on some Unixes - mode = 'r+b' - else: - # read-only mode by default - mode = 'rb' - self.fp = open(filename, mode) - # obtain the filesize by using seek and tell, which should work on most - # file-like objects: - #TODO: do it above, using getsize with filename when possible? - #TODO: fix code to fail with clear exception when filesize cannot be obtained - filesize = 0 - self.fp.seek(0, os.SEEK_END) - try: - filesize = self.fp.tell() - finally: - self.fp.seek(0) - self._filesize = filesize - - # lists of streams in FAT and MiniFAT, to detect duplicate references - # (list of indexes of first sectors of each stream) - self._used_streams_fat = [] - self._used_streams_minifat = [] - - header = self.fp.read(512) - - if len(header) != 512 or header[:8] != MAGIC: - self.raise_defect(DEFECT_FATAL, "not an OLE2 structured storage file") - - # [PL] header structure according to AAF specifications: - ##Header - ##struct StructuredStorageHeader { // [offset from start (bytes), length (bytes)] - ##BYTE _abSig[8]; // [00H,08] {0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, - ## // 0x1a, 0xe1} for current version - ##CLSID _clsid; // [08H,16] reserved must be zero (WriteClassStg/ - ## // GetClassFile uses root directory class id) - ##USHORT _uMinorVersion; // [18H,02] minor version of the format: 33 is - ## // written by reference implementation - ##USHORT _uDllVersion; // [1AH,02] major version of the dll/format: 3 for - ## // 512-byte sectors, 4 for 4 KB sectors - ##USHORT _uByteOrder; // [1CH,02] 0xFFFE: indicates Intel byte-ordering - ##USHORT _uSectorShift; // [1EH,02] size of sectors in power-of-two; - ## // typically 9 indicating 512-byte sectors - ##USHORT _uMiniSectorShift; // [20H,02] size of mini-sectors in power-of-two; - ## // typically 6 indicating 64-byte mini-sectors - ##USHORT _usReserved; // [22H,02] reserved, must be zero - ##ULONG _ulReserved1; // [24H,04] reserved, must be zero - ##FSINDEX _csectDir; // [28H,04] must be zero for 512-byte sectors, - ## // number of SECTs in directory chain for 4 KB - ## // sectors - ##FSINDEX _csectFat; // [2CH,04] number of SECTs in the FAT chain - ##SECT _sectDirStart; // [30H,04] first SECT in the directory chain - ##DFSIGNATURE _signature; // [34H,04] signature used for transactions; must - ## // be zero. The reference implementation - ## // does not support transactions - ##ULONG _ulMiniSectorCutoff; // [38H,04] maximum size for a mini stream; - ## // typically 4096 bytes - ##SECT _sectMiniFatStart; // [3CH,04] first SECT in the MiniFAT chain - ##FSINDEX _csectMiniFat; // [40H,04] number of SECTs in the MiniFAT chain - ##SECT _sectDifStart; // [44H,04] first SECT in the DIFAT chain - ##FSINDEX _csectDif; // [48H,04] number of SECTs in the DIFAT chain - ##SECT _sectFat[109]; // [4CH,436] the SECTs of first 109 FAT sectors - ##}; - - # [PL] header decoding: - # '<' indicates little-endian byte ordering for Intel (cf. struct module help) - fmt_header = '<8s16sHHHHHHLLLLLLLLLL' - header_size = struct.calcsize(fmt_header) - debug("fmt_header size = %d, +FAT = %d" % (header_size, header_size + 109*4)) - header1 = header[:header_size] - ( - self.Sig, - self.clsid, - self.MinorVersion, - self.DllVersion, - self.ByteOrder, - self.SectorShift, - self.MiniSectorShift, - self.Reserved, self.Reserved1, - self.csectDir, - self.csectFat, - self.sectDirStart, - self.signature, - self.MiniSectorCutoff, - self.MiniFatStart, - self.csectMiniFat, - self.sectDifStart, - self.csectDif - ) = struct.unpack(fmt_header, header1) - debug(struct.unpack(fmt_header, header1)) - - if self.Sig != MAGIC: - # OLE signature should always be present - self.raise_defect(DEFECT_FATAL, "incorrect OLE signature") - if self.clsid != bytearray(16): - # according to AAF specs, CLSID should always be zero - self.raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header") - debug("MinorVersion = %d" % self.MinorVersion) - debug("DllVersion = %d" % self.DllVersion) - if self.DllVersion not in [3, 4]: - # version 3: usual format, 512 bytes per sector - # version 4: large format, 4K per sector - self.raise_defect(DEFECT_INCORRECT, "incorrect DllVersion in OLE header") - debug("ByteOrder = %X" % self.ByteOrder) - if self.ByteOrder != 0xFFFE: - # For now only common little-endian documents are handled correctly - self.raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header") - # TODO: add big-endian support for documents created on Mac ? - # But according to [MS-CFB] ? v20140502, ByteOrder MUST be 0xFFFE. - self.SectorSize = 2**self.SectorShift - debug("SectorSize = %d" % self.SectorSize) - if self.SectorSize not in [512, 4096]: - self.raise_defect(DEFECT_INCORRECT, "incorrect SectorSize in OLE header") - if (self.DllVersion == 3 and self.SectorSize != 512) \ - or (self.DllVersion == 4 and self.SectorSize != 4096): - self.raise_defect(DEFECT_INCORRECT, "SectorSize does not match DllVersion in OLE header") - self.MiniSectorSize = 2**self.MiniSectorShift - debug("MiniSectorSize = %d" % self.MiniSectorSize) - if self.MiniSectorSize not in [64]: - self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorSize in OLE header") - if self.Reserved != 0 or self.Reserved1 != 0: - self.raise_defect(DEFECT_INCORRECT, "incorrect OLE header (non-null reserved bytes)") - debug("csectDir = %d" % self.csectDir) - # Number of directory sectors (only allowed if DllVersion != 3) - if self.SectorSize == 512 and self.csectDir != 0: - self.raise_defect(DEFECT_INCORRECT, "incorrect csectDir in OLE header") - debug("csectFat = %d" % self.csectFat) - # csectFat = number of FAT sectors in the file - debug("sectDirStart = %X" % self.sectDirStart) - # sectDirStart = 1st sector containing the directory - debug("signature = %d" % self.signature) - # Signature should be zero, BUT some implementations do not follow this - # rule => only a potential defect: - # (according to MS-CFB, may be != 0 for applications supporting file - # transactions) - if self.signature != 0: - self.raise_defect(DEFECT_POTENTIAL, "incorrect OLE header (signature>0)") - debug("MiniSectorCutoff = %d" % self.MiniSectorCutoff) - # MS-CFB: This integer field MUST be set to 0x00001000. This field - # specifies the maximum size of a user-defined data stream allocated - # from the mini FAT and mini stream, and that cutoff is 4096 bytes. - # Any user-defined data stream larger than or equal to this cutoff size - # must be allocated as normal sectors from the FAT. - if self.MiniSectorCutoff != 0x1000: - self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorCutoff in OLE header") - debug("MiniFatStart = %X" % self.MiniFatStart) - debug("csectMiniFat = %d" % self.csectMiniFat) - debug("sectDifStart = %X" % self.sectDifStart) - debug("csectDif = %d" % self.csectDif) - - # calculate the number of sectors in the file - # (-1 because header doesn't count) - self.nb_sect = ((filesize + self.SectorSize-1) // self.SectorSize) - 1 - debug("Number of sectors in the file: %d" % self.nb_sect) - #TODO: change this test, because an OLE file MAY contain other data - # after the last sector. - - # file clsid - self.clsid = _clsid(header[8:24]) - - #TODO: remove redundant attributes, and fix the code which uses them? - self.sectorsize = self.SectorSize #1 << i16(header, 30) - self.minisectorsize = self.MiniSectorSize #1 << i16(header, 32) - self.minisectorcutoff = self.MiniSectorCutoff # i32(header, 56) - - # check known streams for duplicate references (these are always in FAT, - # never in MiniFAT): - self._check_duplicate_stream(self.sectDirStart) - # check MiniFAT only if it is not empty: - if self.csectMiniFat: - self._check_duplicate_stream(self.MiniFatStart) - # check DIFAT only if it is not empty: - if self.csectDif: - self._check_duplicate_stream(self.sectDifStart) - - # Load file allocation tables - self.loadfat(header) - # Load directory. This sets both the direntries list (ordered by sid) - # and the root (ordered by hierarchy) members. - self.loaddirectory(self.sectDirStart)#i32(header, 48)) - self.ministream = None - self.minifatsect = self.MiniFatStart #i32(header, 60) - - def close(self): - """ - close the OLE file, to release the file object - """ - self.fp.close() - - def _check_duplicate_stream(self, first_sect, minifat=False): - """ - Checks if a stream has not been already referenced elsewhere. - This method should only be called once for each known stream, and only - if stream size is not null. - - :param first_sect: int, index of first sector of the stream in FAT - :param minifat: bool, if True, stream is located in the MiniFAT, else in the FAT - """ - if minifat: - debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect) - used_streams = self._used_streams_minifat - else: - debug('_check_duplicate_stream: sect=%d in FAT' % first_sect) - # some values can be safely ignored (not a real stream): - if first_sect in (DIFSECT, FATSECT, ENDOFCHAIN, FREESECT): - return - used_streams = self._used_streams_fat - #TODO: would it be more efficient using a dict or hash values, instead - # of a list of long ? - if first_sect in used_streams: - self.raise_defect(DEFECT_INCORRECT, 'Stream referenced twice') - else: - used_streams.append(first_sect) - - def dumpfat(self, fat, firstindex=0): - "Displays a part of FAT in human-readable form for debugging purpose" - # [PL] added only for debug - if not DEBUG_MODE: - return - # dictionary to convert special FAT values in human-readable strings - VPL = 8 # values per line (8+1 * 8+1 = 81) - fatnames = { - FREESECT: "..free..", - ENDOFCHAIN: "[ END. ]", - FATSECT: "FATSECT ", - DIFSECT: "DIFSECT " - } - nbsect = len(fat) - nlines = (nbsect+VPL-1)//VPL - print("index", end=" ") - for i in range(VPL): - print("%8X" % i, end=" ") - print() - for l in range(nlines): - index = l*VPL - print("%8X:" % (firstindex+index), end=" ") - for i in range(index, index+VPL): - if i >= nbsect: - break - sect = fat[i] - aux = sect & 0xFFFFFFFF # JYTHON-WORKAROUND - if aux in fatnames: - name = fatnames[aux] - else: - if sect == i+1: - name = " --->" - else: - name = "%8X" % sect - print(name, end=" ") - print() - - def dumpsect(self, sector, firstindex=0): - "Displays a sector in a human-readable form, for debugging purpose." - if not DEBUG_MODE: - return - VPL = 8 # number of values per line (8+1 * 8+1 = 81) - tab = array.array(UINT32, sector) - if sys.byteorder == 'big': - tab.byteswap() - nbsect = len(tab) - nlines = (nbsect+VPL-1)//VPL - print("index", end=" ") - for i in range(VPL): - print("%8X" % i, end=" ") - print() - for l in range(nlines): - index = l*VPL - print("%8X:" % (firstindex+index), end=" ") - for i in range(index, index+VPL): - if i >= nbsect: - break - sect = tab[i] - name = "%8X" % sect - print(name, end=" ") - print() - - def sect2array(self, sect): - """ - convert a sector to an array of 32 bits unsigned integers, - swapping bytes on big endian CPUs such as PowerPC (old Macs) - """ - a = array.array(UINT32, sect) - # if CPU is big endian, swap bytes: - if sys.byteorder == 'big': - a.byteswap() - return a - - def loadfat_sect(self, sect): - """ - Adds the indexes of the given sector to the FAT - - :param sect: string containing the first FAT sector, or array of long integers - :returns: index of last FAT sector. - """ - # a FAT sector is an array of ulong integers. - if isinstance(sect, array.array): - # if sect is already an array it is directly used - fat1 = sect - else: - # if it's a raw sector, it is parsed in an array - fat1 = self.sect2array(sect) - self.dumpsect(sect) - # The FAT is a sector chain starting at the first index of itself. - for isect in fat1: - isect = isect & 0xFFFFFFFF # JYTHON-WORKAROUND - debug("isect = %X" % isect) - if isect == ENDOFCHAIN or isect == FREESECT: - # the end of the sector chain has been reached - debug("found end of sector chain") - break - # read the FAT sector - s = self.getsect(isect) - # parse it as an array of 32 bits integers, and add it to the - # global FAT array - nextfat = self.sect2array(s) - self.fat = self.fat + nextfat - return isect - - def loadfat(self, header): - """ - Load the FAT table. - """ - # The 1st sector of the file contains sector numbers for the first 109 - # FAT sectors, right after the header which is 76 bytes long. - # (always 109, whatever the sector size: 512 bytes = 76+4*109) - # Additional sectors are described by DIF blocks - - sect = header[76:512] - debug("len(sect)=%d, so %d integers" % (len(sect), len(sect)//4)) - #fat = [] - # [PL] FAT is an array of 32 bits unsigned ints, it's more effective - # to use an array than a list in Python. - # It's initialized as empty first: - self.fat = array.array(UINT32) - self.loadfat_sect(sect) - #self.dumpfat(self.fat) -## for i in range(0, len(sect), 4): -## ix = i32(sect, i) -## # [PL] if ix == -2 or ix == -1: # ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: -## if ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: -## break -## s = self.getsect(ix) -## #fat = fat + [i32(s, i) for i in range(0, len(s), 4)] -## fat = fat + array.array(UINT32, s) - if self.csectDif != 0: - # [PL] There's a DIFAT because file is larger than 6.8MB - # some checks just in case: - if self.csectFat <= 109: - # there must be at least 109 blocks in header and the rest in - # DIFAT, so number of sectors must be >109. - self.raise_defect(DEFECT_INCORRECT, 'incorrect DIFAT, not enough sectors') - if self.sectDifStart >= self.nb_sect: - # initial DIFAT block index must be valid - self.raise_defect(DEFECT_FATAL, 'incorrect DIFAT, first index out of range') - debug("DIFAT analysis...") - # We compute the necessary number of DIFAT sectors : - # Number of pointers per DIFAT sector = (sectorsize/4)-1 - # (-1 because the last pointer is the next DIFAT sector number) - nb_difat_sectors = (self.sectorsize//4)-1 - # (if 512 bytes: each DIFAT sector = 127 pointers + 1 towards next DIFAT sector) - nb_difat = (self.csectFat-109 + nb_difat_sectors-1)//nb_difat_sectors - debug("nb_difat = %d" % nb_difat) - if self.csectDif != nb_difat: - raise IOError('incorrect DIFAT') - isect_difat = self.sectDifStart - for i in iterrange(nb_difat): - debug("DIFAT block %d, sector %X" % (i, isect_difat)) - #TODO: check if corresponding FAT SID = DIFSECT - sector_difat = self.getsect(isect_difat) - difat = self.sect2array(sector_difat) - self.dumpsect(sector_difat) - self.loadfat_sect(difat[:nb_difat_sectors]) - # last DIFAT pointer is next DIFAT sector: - isect_difat = difat[nb_difat_sectors] - debug("next DIFAT sector: %X" % isect_difat) - # checks: - if isect_difat not in [ENDOFCHAIN, FREESECT]: - # last DIFAT pointer value must be ENDOFCHAIN or FREESECT - raise IOError('incorrect end of DIFAT') -## if len(self.fat) != self.csectFat: -## # FAT should contain csectFat blocks -## print("FAT length: %d instead of %d" % (len(self.fat), self.csectFat)) -## raise IOError('incorrect DIFAT') - # since FAT is read from fixed-size sectors, it may contain more values - # than the actual number of sectors in the file. - # Keep only the relevant sector indexes: - if len(self.fat) > self.nb_sect: - debug('len(fat)=%d, shrunk to nb_sect=%d' % (len(self.fat), self.nb_sect)) - self.fat = self.fat[:self.nb_sect] - debug('\nFAT:') - self.dumpfat(self.fat) - - def loadminifat(self): - """ - Load the MiniFAT table. - """ - # MiniFAT is stored in a standard sub-stream, pointed to by a header - # field. - # NOTE: there are two sizes to take into account for this stream: - # 1) Stream size is calculated according to the number of sectors - # declared in the OLE header. This allocated stream may be more than - # needed to store the actual sector indexes. - # (self.csectMiniFat is the number of sectors of size self.SectorSize) - stream_size = self.csectMiniFat * self.SectorSize - # 2) Actually used size is calculated by dividing the MiniStream size - # (given by root entry size) by the size of mini sectors, *4 for - # 32 bits indexes: - nb_minisectors = (self.root.size + self.MiniSectorSize-1) // self.MiniSectorSize - used_size = nb_minisectors * 4 - debug('loadminifat(): minifatsect=%d, nb FAT sectors=%d, used_size=%d, stream_size=%d, nb MiniSectors=%d' % - (self.minifatsect, self.csectMiniFat, used_size, stream_size, nb_minisectors)) - if used_size > stream_size: - # This is not really a problem, but may indicate a wrong implementation: - self.raise_defect(DEFECT_INCORRECT, 'OLE MiniStream is larger than MiniFAT') - # In any case, first read stream_size: - s = self._open(self.minifatsect, stream_size, force_FAT=True).read() - # [PL] Old code replaced by an array: - # self.minifat = [i32(s, i) for i in range(0, len(s), 4)] - self.minifat = self.sect2array(s) - # Then shrink the array to used size, to avoid indexes out of MiniStream: - debug('MiniFAT shrunk from %d to %d sectors' % (len(self.minifat), nb_minisectors)) - self.minifat = self.minifat[:nb_minisectors] - debug('loadminifat(): len=%d' % len(self.minifat)) - debug('\nMiniFAT:') - self.dumpfat(self.minifat) - - def getsect(self, sect): - """ - Read given sector from file on disk. - - :param sect: int, sector index - :returns: a string containing the sector data. - """ - # From [MS-CFB]: A sector number can be converted into a byte offset - # into the file by using the following formula: - # (sector number + 1) x Sector Size. - # This implies that sector #0 of the file begins at byte offset Sector - # Size, not at 0. - - # [PL] the original code in PIL was wrong when sectors are 4KB instead of - # 512 bytes: - # self.fp.seek(512 + self.sectorsize * sect) - # [PL]: added safety checks: - # print("getsect(%X)" % sect) - try: - self.fp.seek(self.sectorsize * (sect+1)) - except: - debug('getsect(): sect=%X, seek=%d, filesize=%d' % - (sect, self.sectorsize*(sect+1), self._filesize)) - self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') - sector = self.fp.read(self.sectorsize) - if len(sector) != self.sectorsize: - debug('getsect(): sect=%X, read=%d, sectorsize=%d' % - (sect, len(sector), self.sectorsize)) - self.raise_defect(DEFECT_FATAL, 'incomplete OLE sector') - return sector - - def write_sect(self, sect, data, padding=b'\x00'): - """ - Write given sector to file on disk. - - :param sect: int, sector index - :param data: bytes, sector data - :param padding: single byte, padding character if data < sector size - """ - if not isinstance(data, bytes): - raise TypeError("write_sect: data must be a bytes string") - if not isinstance(padding, bytes) or len(padding) != 1: - raise TypeError("write_sect: padding must be a bytes string of 1 char") - #TODO: we could allow padding=None for no padding at all - try: - self.fp.seek(self.sectorsize * (sect+1)) - except: - debug('write_sect(): sect=%X, seek=%d, filesize=%d' % - (sect, self.sectorsize*(sect+1), self._filesize)) - self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') - if len(data) < self.sectorsize: - # add padding - data += padding * (self.sectorsize - len(data)) - elif len(data) < self.sectorsize: - raise ValueError("Data is larger than sector size") - self.fp.write(data) - - def loaddirectory(self, sect): - """ - Load the directory. - - :param sect: sector index of directory stream. - """ - # The directory is stored in a standard - # substream, independent of its size. - - # open directory stream as a read-only file: - # (stream size is not known in advance) - self.directory_fp = self._open(sect) - - # [PL] to detect malformed documents and avoid DoS attacks, the maximum - # number of directory entries can be calculated: - max_entries = self.directory_fp.size // 128 - debug('loaddirectory: size=%d, max_entries=%d' % - (self.directory_fp.size, max_entries)) - - # Create list of directory entries - # self.direntries = [] - # We start with a list of "None" object - self.direntries = [None] * max_entries -## for sid in iterrange(max_entries): -## entry = fp.read(128) -## if not entry: -## break -## self.direntries.append(_OleDirectoryEntry(entry, sid, self)) - # load root entry: - root_entry = self._load_direntry(0) - # Root entry is the first entry: - self.root = self.direntries[0] - # read and build all storage trees, starting from the root: - self.root.build_storage_tree() - - def _load_direntry(self, sid): - """ - Load a directory entry from the directory. - This method should only be called once for each storage/stream when - loading the directory. - - :param sid: index of storage/stream in the directory. - :returns: a _OleDirectoryEntry object - - :exception IOError: if the entry has always been referenced. - """ - # check if SID is OK: - if sid < 0 or sid >= len(self.direntries): - self.raise_defect(DEFECT_FATAL, "OLE directory index out of range") - # check if entry was already referenced: - if self.direntries[sid] is not None: - self.raise_defect(DEFECT_INCORRECT, - "double reference for OLE stream/storage") - # if exception not raised, return the object - return self.direntries[sid] - self.directory_fp.seek(sid * 128) - entry = self.directory_fp.read(128) - self.direntries[sid] = _OleDirectoryEntry(entry, sid, self) - return self.direntries[sid] - - def dumpdirectory(self): - """ - Dump directory (for debugging only) - """ - self.root.dump() - - def _open(self, start, size = 0x7FFFFFFF, force_FAT=False): - """ - Open a stream, either in FAT or MiniFAT according to its size. - (openstream helper) - - :param start: index of first sector - :param size: size of stream (or nothing if size is unknown) - :param force_FAT: if False (default), stream will be opened in FAT or MiniFAT - according to size. If True, it will always be opened in FAT. - """ - debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' % - (start, size, str(force_FAT))) - # stream size is compared to the MiniSectorCutoff threshold: - if size < self.minisectorcutoff and not force_FAT: - # ministream object - if not self.ministream: - # load MiniFAT if it wasn't already done: - self.loadminifat() - # The first sector index of the miniFAT stream is stored in the - # root directory entry: - size_ministream = self.root.size - debug('Opening MiniStream: sect=%d, size=%d' % - (self.root.isectStart, size_ministream)) - self.ministream = self._open(self.root.isectStart, - size_ministream, force_FAT=True) - return _OleStream(fp=self.ministream, sect=start, size=size, - offset=0, sectorsize=self.minisectorsize, - fat=self.minifat, filesize=self.ministream.size) - else: - # standard stream - return _OleStream(fp=self.fp, sect=start, size=size, - offset=self.sectorsize, - sectorsize=self.sectorsize, fat=self.fat, - filesize=self._filesize) - - def _list(self, files, prefix, node, streams=True, storages=False): - """ - listdir helper - - :param files: list of files to fill in - :param prefix: current location in storage tree (list of names) - :param node: current node (_OleDirectoryEntry object) - :param streams: bool, include streams if True (True by default) - new in v0.26 - :param storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) - """ - prefix = prefix + [node.name] - for entry in node.kids: - if entry.entry_type == STGTY_STORAGE: - # this is a storage - if storages: - # add it to the list - files.append(prefix[1:] + [entry.name]) - # check its kids - self._list(files, prefix, entry, streams, storages) - elif entry.entry_type == STGTY_STREAM: - # this is a stream - if streams: - # add it to the list - files.append(prefix[1:] + [entry.name]) - else: - self.raise_defect(DEFECT_INCORRECT, 'The directory tree contains an entry which is not a stream nor a storage.') - - def listdir(self, streams=True, storages=False): - """ - Return a list of streams and/or storages stored in this file - - :param streams: bool, include streams if True (True by default) - new in v0.26 - :param storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) - :returns: list of stream and/or storage paths - """ - files = [] - self._list(files, [], self.root, streams, storages) - return files - - def _find(self, filename): - """ - Returns directory entry of given filename. (openstream helper) - Note: this method is case-insensitive. - - :param filename: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :returns: sid of requested filename - :exception IOError: if file not found - """ - - # if filename is a string instead of a list, split it on slashes to - # convert to a list: - if isinstance(filename, basestring): - filename = filename.split('/') - # walk across storage tree, following given path: - node = self.root - for name in filename: - for kid in node.kids: - if kid.name.lower() == name.lower(): - break - else: - raise IOError("file not found") - node = kid - return node.sid - - def openstream(self, filename): - """ - Open a stream as a read-only file object (BytesIO). - Note: filename is case-insensitive. - - :param filename: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :returns: file object (read-only) - :exception IOError: if filename not found, or if this is not a stream. - """ - sid = self._find(filename) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - raise IOError("this file is not a stream") - return self._open(entry.isectStart, entry.size) - - def write_stream(self, stream_name, data): - """ - Write a stream to disk. For now, it is only possible to replace an - existing stream by data of the same size. - - :param stream_name: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :param data: bytes, data to be written, must be the same size as the original - stream. - """ - if not isinstance(data, bytes): - raise TypeError("write_stream: data must be a bytes string") - sid = self._find(stream_name) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - raise IOError("this is not a stream") - size = entry.size - if size != len(data): - raise ValueError("write_stream: data must be the same size as the existing stream") - if size < self.minisectorcutoff: - raise NotImplementedError("Writing a stream in MiniFAT is not implemented yet") - sect = entry.isectStart - # number of sectors to write - nb_sectors = (size + (self.sectorsize-1)) // self.sectorsize - debug('nb_sectors = %d' % nb_sectors) - for i in range(nb_sectors): - # try: - # self.fp.seek(offset + self.sectorsize * sect) - # except: - # debug('sect=%d, seek=%d' % - # (sect, offset+self.sectorsize*sect)) - # raise IOError('OLE sector index out of range') - # extract one sector from data, the last one being smaller: - if i < (nb_sectors-1): - data_sector = data[i*self.sectorsize:(i+1)*self.sectorsize] - #TODO: comment this if it works - assert(len(data_sector) == self.sectorsize) - else: - data_sector = data[i*self.sectorsize:] - # TODO: comment this if it works - debug('write_stream: size=%d sectorsize=%d data_sector=%d size%%sectorsize=%d' - % (size, self.sectorsize, len(data_sector), size % self.sectorsize)) - assert(len(data_sector) % self.sectorsize == size % self.sectorsize) - self.write_sect(sect, data_sector) -# self.fp.write(data_sector) - # jump to next sector in the FAT: - try: - sect = self.fat[sect] - except IndexError: - # [PL] if pointer is out of the FAT an exception is raised - raise IOError('incorrect OLE FAT, sector index out of range') - # [PL] Last sector should be a "end of chain" marker: - if sect != ENDOFCHAIN: - raise IOError('incorrect last sector index in OLE stream') - - def get_type(self, filename): - """ - Test if given filename exists as a stream or a storage in the OLE - container, and return its type. - - :param filename: path of stream in storage tree. (see openstream for syntax) - :returns: False if object does not exist, its entry type (>0) otherwise: - - - STGTY_STREAM: a stream - - STGTY_STORAGE: a storage - - STGTY_ROOT: the root entry - """ - try: - sid = self._find(filename) - entry = self.direntries[sid] - return entry.entry_type - except: - return False - - def getmtime(self, filename): - """ - Return modification time of a stream/storage. - - :param filename: path of stream/storage in storage tree. (see openstream for - syntax) - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - sid = self._find(filename) - entry = self.direntries[sid] - return entry.getmtime() - - def getctime(self, filename): - """ - Return creation time of a stream/storage. - - :param filename: path of stream/storage in storage tree. (see openstream for - syntax) - :returns: None if creation time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - sid = self._find(filename) - entry = self.direntries[sid] - return entry.getctime() - - def exists(self, filename): - """ - Test if given filename exists as a stream or a storage in the OLE - container. - Note: filename is case-insensitive. - - :param filename: path of stream in storage tree. (see openstream for syntax) - :returns: True if object exist, else False. - """ - try: - sid = self._find(filename) - return True - except: - return False - - def get_size(self, filename): - """ - Return size of a stream in the OLE container, in bytes. - - :param filename: path of stream in storage tree (see openstream for syntax) - :returns: size in bytes (long integer) - :exception IOError: if file not found - :exception TypeError: if this is not a stream. - """ - sid = self._find(filename) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - #TODO: Should it return zero instead of raising an exception ? - raise TypeError('object is not an OLE stream') - return entry.size - - def get_rootentry_name(self): - """ - Return root entry name. Should usually be 'Root Entry' or 'R' in most - implementations. - """ - return self.root.name - - def getproperties(self, filename, convert_time=False, no_conversion=None): - """ - Return properties described in substream. - - :param filename: path of stream in storage tree (see openstream for syntax) - :param convert_time: bool, if True timestamps will be converted to Python datetime - :param no_conversion: None or list of int, timestamps not to be converted - (for example total editing time is not a real timestamp) - - :returns: a dictionary of values indexed by id (integer) - """ - # REFERENCE: [MS-OLEPS] https://msdn.microsoft.com/en-us/library/dd942421.aspx - # make sure no_conversion is a list, just to simplify code below: - if no_conversion is None: - no_conversion = [] - # stream path as a string to report exceptions: - streampath = filename - if not isinstance(streampath, str): - streampath = '/'.join(streampath) - - fp = self.openstream(filename) - - data = {} - - try: - # header - s = fp.read(28) - clsid = _clsid(s[8:24]) - - # format id - s = fp.read(20) - fmtid = _clsid(s[:16]) - fp.seek(i32(s, 16)) - - # get section - s = b"****" + fp.read(i32(fp.read(4))-4) - # number of properties: - num_props = i32(s, 4) - except BaseException as exc: - # catch exception while parsing property header, and only raise - # a DEFECT_INCORRECT then return an empty dict, because this is not - # a fatal error when parsing the whole file - msg = 'Error while parsing properties header in stream %s: %s' % ( - repr(streampath), exc) - self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) - return data - - for i in range(num_props): - try: - id = 0 # just in case of an exception - id = i32(s, 8+i*8) - offset = i32(s, 12+i*8) - type = i32(s, offset) - - debug('property id=%d: type=%d offset=%X' % (id, type, offset)) - - # test for common types first (should perhaps use - # a dictionary instead?) - - if type == VT_I2: # 16-bit signed integer - value = i16(s, offset+4) - if value >= 32768: - value = value - 65536 - elif type == VT_UI2: # 2-byte unsigned integer - value = i16(s, offset+4) - elif type in (VT_I4, VT_INT, VT_ERROR): - # VT_I4: 32-bit signed integer - # VT_ERROR: HRESULT, similar to 32-bit signed integer, - # see http://msdn.microsoft.com/en-us/library/cc230330.aspx - value = i32(s, offset+4) - elif type in (VT_UI4, VT_UINT): # 4-byte unsigned integer - value = i32(s, offset+4) # FIXME - elif type in (VT_BSTR, VT_LPSTR): - # CodePageString, see http://msdn.microsoft.com/en-us/library/dd942354.aspx - # size is a 32 bits integer, including the null terminator, and - # possibly trailing or embedded null chars - #TODO: if codepage is unicode, the string should be converted as such - count = i32(s, offset+4) - value = s[offset+8:offset+8+count-1] - # remove all null chars: - value = value.replace(b'\x00', b'') - elif type == VT_BLOB: - # binary large object (BLOB) - # see http://msdn.microsoft.com/en-us/library/dd942282.aspx - count = i32(s, offset+4) - value = s[offset+8:offset+8+count] - elif type == VT_LPWSTR: - # UnicodeString - # see http://msdn.microsoft.com/en-us/library/dd942313.aspx - # "the string should NOT contain embedded or additional trailing - # null characters." - count = i32(s, offset+4) - value = self._decode_utf16_str(s[offset+8:offset+8+count*2]) - elif type == VT_FILETIME: - value = long(i32(s, offset+4)) + (long(i32(s, offset+8)) << 32) - # FILETIME is a 64-bit int: "number of 100ns periods - # since Jan 1,1601". - if convert_time and id not in no_conversion: - debug('Converting property #%d to python datetime, value=%d=%fs' - % (id, value, float(value) / 10000000)) - # convert FILETIME to Python datetime.datetime - # inspired from http://code.activestate.com/recipes/511425-filetime-to-datetime/ - _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0) - debug('timedelta days=%d' % (value//(10*1000000*3600*24))) - value = _FILETIME_null_date + datetime.timedelta(microseconds=value//10) - else: - # legacy code kept for backward compatibility: returns a - # number of seconds since Jan 1,1601 - value = value // 10000000 # seconds - elif type == VT_UI1: # 1-byte unsigned integer - value = i8(s[offset+4]) - elif type == VT_CLSID: - value = _clsid(s[offset+4:offset+20]) - elif type == VT_CF: - # PropertyIdentifier or ClipboardData?? - # see http://msdn.microsoft.com/en-us/library/dd941945.aspx - count = i32(s, offset+4) - value = s[offset+8:offset+8+count] - elif type == VT_BOOL: - # VARIANT_BOOL, 16 bits bool, 0x0000=Fals, 0xFFFF=True - # see http://msdn.microsoft.com/en-us/library/cc237864.aspx - value = bool(i16(s, offset+4)) - else: - value = None # everything else yields "None" - debug('property id=%d: type=%d not implemented in parser yet' % (id, type)) - - # missing: VT_EMPTY, VT_NULL, VT_R4, VT_R8, VT_CY, VT_DATE, - # VT_DECIMAL, VT_I1, VT_I8, VT_UI8, - # see http://msdn.microsoft.com/en-us/library/dd942033.aspx - - # FIXME: add support for VT_VECTOR - # VT_VECTOR is a 32 uint giving the number of items, followed by - # the items in sequence. The VT_VECTOR value is combined with the - # type of items, e.g. VT_VECTOR|VT_BSTR - # see http://msdn.microsoft.com/en-us/library/dd942011.aspx - - # print("%08x" % id, repr(value), end=" ") - # print("(%s)" % VT[i32(s, offset) & 0xFFF]) - - data[id] = value - except BaseException as exc: - # catch exception while parsing each property, and only raise - # a DEFECT_INCORRECT, because parsing can go on - msg = 'Error while parsing property id %d in stream %s: %s' % ( - id, repr(streampath), exc) - self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) - - return data - - def get_metadata(self): - """ - Parse standard properties streams, return an OleMetadata object - containing all the available metadata. - (also stored in the metadata attribute of the OleFileIO object) - - new in version 0.25 - """ - self.metadata = OleMetadata() - self.metadata.parse_properties(self) - return self.metadata - -# -# -------------------------------------------------------------------- -# This script can be used to dump the directory of any OLE2 structured -# storage file. - -if __name__ == "__main__": - - # [PL] display quick usage info if launched from command-line - if len(sys.argv) <= 1: - print('olefile version %s %s - %s' % (__version__, __date__, __author__)) - print( -""" -Launched from the command line, this script parses OLE files and prints info. - -Usage: olefile.py [-d] [-c] [file2 ...] - -Options: --d : debug mode (displays a lot of debug information, for developers only) --c : check all streams (for debugging purposes) - -For more information, see http://www.decalage.info/olefile -""") - sys.exit() - - check_streams = False - for filename in sys.argv[1:]: - # try: - # OPTIONS: - if filename == '-d': - # option to switch debug mode on: - set_debug_mode(True) - continue - if filename == '-c': - # option to switch check streams mode on: - check_streams = True - continue - - ole = OleFileIO(filename)#, raise_defects=DEFECT_INCORRECT) - print("-" * 68) - print(filename) - print("-" * 68) - ole.dumpdirectory() - for streamname in ole.listdir(): - if streamname[-1][0] == "\005": - print(streamname, ": properties") - props = ole.getproperties(streamname, convert_time=True) - props = sorted(props.items()) - for k, v in props: - # [PL]: avoid to display too large or binary values: - if isinstance(v, (basestring, bytes)): - if len(v) > 50: - v = v[:50] - if isinstance(v, bytes): - # quick and dirty binary check: - for c in (1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31): - if c in bytearray(v): - v = '(binary data)' - break - print(" ", k, v) - - if check_streams: - # Read all streams to check if there are errors: - print('\nChecking streams...') - for streamname in ole.listdir(): - # print name using repr() to convert binary chars to \xNN: - print('-', repr('/'.join(streamname)), '-', end=' ') - st_type = ole.get_type(streamname) - if st_type == STGTY_STREAM: - print('size %d' % ole.get_size(streamname)) - # just try to read stream in memory: - ole.openstream(streamname) - else: - print('NOT a stream : type=%d' % st_type) - print() - -# for streamname in ole.listdir(): -# # print name using repr() to convert binary chars to \xNN: -# print('-', repr('/'.join(streamname)),'-', end=' ') -# print(ole.getmtime(streamname)) -# print() - - print('Modification/Creation times of all directory entries:') - for entry in ole.direntries: - if entry is not None: - print('- %s: mtime=%s ctime=%s' % (entry.name, - entry.getmtime(), entry.getctime())) - print() - - # parse and display metadata: - meta = ole.get_metadata() - meta.dump() - print() - # [PL] Test a few new methods: - root = ole.get_rootentry_name() - print('Root entry name: "%s"' % root) - if ole.exists('worddocument'): - print("This is a Word document.") - print("type of stream 'WordDocument':", ole.get_type('worddocument')) - print("size :", ole.get_size('worddocument')) - if ole.exists('macros/vba'): - print("This document may contain VBA macros.") - - # print parsing issues: - print('\nNon-fatal issues raised during parsing:') - if ole.parsing_issues: - for exctype, msg in ole.parsing_issues: - print('- %s: %s' % (exctype.__name__, msg)) - else: - print('None') -## except IOError as v: -## print("***", "cannot read", file, "-", v) - -# this code was developed while listening to The Wedding Present "Sea Monsters" +raise ImportError( + 'PIL.OleFileIO is deprecated. Use the olefile Python package ' + 'instead. This module will be removed in a future version.' +) diff --git a/server/www/packages/packages-windows/x86/PIL/PSDraw.py b/server/www/packages/packages-windows/x86/PIL/PSDraw.py index d4e7b18..d2ded6f 100644 --- a/server/www/packages/packages-windows/x86/PIL/PSDraw.py +++ b/server/www/packages/packages-windows/x86/PIL/PSDraw.py @@ -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 diff --git a/server/www/packages/packages-windows/x86/PIL/PaletteFile.py b/server/www/packages/packages-windows/x86/PIL/PaletteFile.py index ef50fee..9ed69d6 100644 --- a/server/www/packages/packages-windows/x86/PIL/PaletteFile.py +++ b/server/www/packages/packages-windows/x86/PIL/PaletteFile.py @@ -13,7 +13,7 @@ # See the README file for information on usage and redistribution. # -from PIL._binary import o8 +from ._binary import o8 ## diff --git a/server/www/packages/packages-windows/x86/PIL/PalmImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PalmImagePlugin.py index 4f415ff..7d7b165 100644 --- a/server/www/packages/packages-windows/x86/PIL/PalmImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PalmImagePlugin.py @@ -7,7 +7,8 @@ # 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" @@ -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() diff --git a/server/www/packages/packages-windows/x86/PIL/PcdImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PcdImagePlugin.py index b53635a..fa95b50 100644 --- a/server/www/packages/packages-windows/x86/PIL/PcdImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PcdImagePlugin.py @@ -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,8 +41,9 @@ 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 @@ -51,6 +51,13 @@ class PcdImageFile(ImageFile.ImageFile): 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 diff --git a/server/www/packages/packages-windows/x86/PIL/PcfFontFile.py b/server/www/packages/packages-windows/x86/PIL/PcfFontFile.py index c200690..eba85fe 100644 --- a/server/www/packages/packages-windows/x86/PIL/PcfFontFile.py +++ b/server/www/packages/packages-windows/x86/PIL/PcfFontFile.py @@ -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)] diff --git a/server/www/packages/packages-windows/x86/PIL/PcxImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PcxImagePlugin.py index 9440d53..564713a 100644 --- a/server/www/packages/packages-windows/x86/PIL/PcxImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PcxImagePlugin.py @@ -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" @@ -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) diff --git a/server/www/packages/packages-windows/x86/PIL/PdfImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PdfImagePlugin.py index 7decf0e..8538bcd 100644 --- a/server/www/packages/packages-windows/x86/PIL/PdfImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PdfImagePlugin.py @@ -20,11 +20,10 @@ # 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 -__version__ = "0.4" +__version__ = "0.5" # @@ -37,19 +36,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) @@ -60,196 +46,179 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, save_all=False): resolution = im.encoderinfo.get("resolution", 72.0) + is_appending = im.encoderinfo.get("append", False) + title = im.encoderinfo.get("title", None) + author = im.encoderinfo.get("author", None) + subject = im.encoderinfo.get("subject", None) + keywords = im.encoderinfo.get("keywords", None) + creator = im.encoderinfo.get("creator", None) + producer = im.encoderinfo.get("producer", None) + + 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") + + if title: + existing_pdf.info.Title = title + if author: + existing_pdf.info.Author = author + if subject: + existing_pdf.info.Subject = subject + if keywords: + existing_pdf.info.Keywords = keywords + if creator: + existing_pdf.info.Creator = creator + if producer: + existing_pdf.info.Producer = producer # # 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) diff --git a/server/www/packages/packages-windows/x86/PIL/PdfParser.py b/server/www/packages/packages-windows/x86/PIL/PdfParser.py new file mode 100644 index 0000000..c0635ef --- /dev/null +++ b/server/www/packages/packages-windows/x86/PIL/PdfParser.py @@ -0,0 +1,844 @@ +import codecs +import collections +import mmap +import os +import re +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): + return decode_text(value) + else: + 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, 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)): + return pdf_repr(encode_text(x)) + elif isinstance(x, bytes): + return b"(" + x.replace(b"\\", b"\\\\").replace(b"(", b"\\(").replace(b")", b"\\)") + b")" # XXX escape more chars? handle binary garbage + 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: + return float(m.group(1)), m.end() # XXX Decimal instead of float??? + m = cls.re_string_hex.match(data, offset) + if m: + hex_string = bytearray([b for b in m.group(1) if b in b"0123456789abcdefABCDEF"]) # filter out whitespace + if len(hex_string) % 2 == 1: + hex_string.append(ord(b"0")) # append a 0 if the length is not even - yes, at the end + 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 diff --git a/server/www/packages/packages-windows/x86/PIL/PixarImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PixarImagePlugin.py index fd002d9..220577c 100644 --- a/server/www/packages/packages-windows/x86/PIL/PixarImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PixarImagePlugin.py @@ -19,16 +19,15 @@ # 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" @@ -63,6 +62,7 @@ class PixarImageFile(ImageFile.ImageFile): # create tile descriptor (assuming "dumped") self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))] + # # -------------------------------------------------------------------- diff --git a/server/www/packages/packages-windows/x86/PIL/PngImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PngImagePlugin.py index 18deec5..8260619 100644 --- a/server/www/packages/packages-windows/x86/PIL/PngImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PngImagePlugin.py @@ -31,24 +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" @@ -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,9 +101,6 @@ 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 @@ -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,7 +136,7 @@ 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): @@ -145,13 +149,13 @@ class ChunkStream(object): return try: - crc1 = Image.core.crc32(data, Image.core.crc32(cid)) - crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) + crc1 = _crc32(data, _crc32(cid)) + crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError("broken PNG file (bad header checksum in %s)" + raise SyntaxError("broken PNG file (bad header checksum in %r)" % cid) except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %s)" + raise SyntaxError("broken PNG file (incomplete checksum in %r)" % cid) def crc_skip(self, cid, data): @@ -189,7 +193,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 """ @@ -246,7 +251,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 @@ -256,14 +261,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') @@ -309,7 +314,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: @@ -384,12 +389,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 @@ -414,7 +438,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') @@ -450,7 +474,7 @@ class PngStream(ChunkStream): v = b"" if k: - if bytes is not str: + if py3: k = k.decode('latin-1', 'strict') v = v.decode('latin-1', 'replace') @@ -487,7 +511,7 @@ class PngStream(ChunkStream): 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") @@ -539,7 +563,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) @@ -621,10 +645,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'), @@ -644,14 +664,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): @@ -665,7 +685,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 @@ -696,15 +716,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: @@ -712,9 +727,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 @@ -727,6 +739,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] @@ -773,20 +813,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 = 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) + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0)+im.size, 0, rawmode)]) @@ -814,8 +845,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() diff --git a/server/www/packages/packages-windows/x86/PIL/PpmImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PpmImagePlugin.py index 68073ca..c599ba8 100644 --- a/server/www/packages/packages-windows/x86/PIL/PpmImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PpmImagePlugin.py @@ -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 @@ -123,11 +112,6 @@ class PpmImageFile(ImageFile.ImageFile): 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 +150,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"]) diff --git a/server/www/packages/packages-windows/x86/PIL/PsdImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/PsdImagePlugin.py index d06e320..f6e04f7 100644 --- a/server/www/packages/packages-windows/x86/PIL/PsdImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/PsdImagePlugin.py @@ -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 @@ -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 @@ -307,6 +301,7 @@ def _maketile(file, mode, bbox, channels): # -------------------------------------------------------------------- # registry + Image.register_open(PsdImageFile.format, PsdImageFile, _accept) Image.register_extension(PsdImageFile.format, ".psd") diff --git a/server/www/packages/packages-windows/x86/PIL/PyAccess.py b/server/www/packages/packages-windows/x86/PIL/PyAccess.py index c9cbd70..cce2de2 100644 --- a/server/www/packages/packages-windows/x86/PIL/PyAccess.py +++ b/server/www/packages/packages-windows/x86/PIL/PyAccess.py @@ -20,8 +20,6 @@ # Access.c implementation. # -from __future__ import print_function - import logging import sys @@ -51,8 +49,10 @@ 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 # Debugging is polluting test traces, only useful here # when hacking on PyAccess @@ -68,8 +68,9 @@ 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') @@ -82,7 +83,8 @@ 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. """ @@ -132,6 +134,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 +167,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 +184,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) @@ -269,7 +272,7 @@ class _PyAccessF(PyAccess): try: # not a tuple self.pixels[y][x] = color - except: + except TypeError: # tuple self.pixels[y][x] = color[0] @@ -314,5 +317,3 @@ def new(img, readonly=False): logger.debug("PyAccess Not Implemented: %s", img.mode) return None return access_type(img, readonly) - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/SgiImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/SgiImagePlugin.py index f890c7e..ef0f40e 100644 --- a/server/www/packages/packages-windows/x86/PIL/SgiImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/SgiImagePlugin.py @@ -7,9 +7,13 @@ # See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. # # +# # 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,170 @@ 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 diff --git a/server/www/packages/packages-windows/x86/PIL/SpiderImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/SpiderImagePlugin.py index 07f623c..d502779 100644 --- a/server/www/packages/packages-windows/x86/PIL/SpiderImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/SpiderImagePlugin.py @@ -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,7 @@ 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) + # print("labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt)) if labbyt != (labrec * lenbyt): return 0 # looks like a valid header @@ -83,9 +82,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 +96,7 @@ class SpiderImageFile(ImageFile.ImageFile): format = "SPIDER" format_description = "Spider 2D image" + _close_exclusive_fp_after_loading = False def _open(self): # check header @@ -174,8 +173,8 @@ class SpiderImageFile(ImageFile.ImageFile): def seek(self, frame): if self.istack == 0: raise EOFError("attempt to seek in a non-stack file") - if frame >= self._nimages: - raise EOFError("attempt to seek past end of file") + if not self._seek_check(frame): + return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.fp = self.__fp self.fp.seek(self.stkoffset) @@ -201,7 +200,7 @@ class SpiderImageFile(ImageFile.ImageFile): # 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 @@ -267,17 +266,11 @@ 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 @@ -287,12 +280,13 @@ def _save_spider(im, fp, filename): # -------------------------------------------------------------------- + Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if not sys.argv[1:]: + if len(sys.argv) < 2: print("Syntax: python SpiderImagePlugin.py [infile] [outfile]") sys.exit() @@ -301,10 +295,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,7 +303,9 @@ 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( diff --git a/server/www/packages/packages-windows/x86/PIL/SunImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/SunImagePlugin.py index af63144..fd5e827 100644 --- a/server/www/packages/packages-windows/x86/PIL/SunImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/SunImagePlugin.py @@ -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: @@ -48,34 +62,75 @@ class SunImageFile(ImageFile.ImageFile): 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") diff --git a/server/www/packages/packages-windows/x86/PIL/TarIO.py b/server/www/packages/packages-windows/x86/PIL/TarIO.py index 4e5115b..0e949ff 100644 --- a/server/www/packages/packages-windows/x86/PIL/TarIO.py +++ b/server/www/packages/packages-windows/x86/PIL/TarIO.py @@ -14,7 +14,7 @@ # See the README file for information on usage and redistribution. # -from PIL import ContainerIO +from . import ContainerIO ## @@ -23,14 +23,13 @@ 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. + :param tarfile: Name of TAR file. + :param file: Name of member file. + """ fh = open(tarfile, "rb") while True: diff --git a/server/www/packages/packages-windows/x86/PIL/TgaImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/TgaImagePlugin.py index a75ce29..57b6ae2 100644 --- a/server/www/packages/packages-windows/x86/PIL/TgaImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/TgaImagePlugin.py @@ -17,7 +17,8 @@ # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, o8, o16le as o16 __version__ = "0.3" @@ -26,15 +27,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", @@ -76,6 +75,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): @@ -132,35 +133,35 @@ 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 + rle = im.encoderinfo.get("rle", False) + + if rle: + imagetype += 8 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 @@ -185,13 +186,23 @@ def _save(im, fp, filename, check=0): 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) diff --git a/server/www/packages/packages-windows/x86/PIL/TiffImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/TiffImagePlugin.py index 524d42a..6f032f4 100644 --- a/server/www/packages/packages-windows/x86/PIL/TiffImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/TiffImagePlugin.py @@ -41,10 +41,9 @@ from __future__ import division, print_function -from PIL import Image, ImageFile -from PIL import ImagePalette -from PIL import _binary -from PIL import TiffTags +from . import Image, ImageFile, ImagePalette, TiffTags +from ._binary import i8, o8 +from ._util import py3 import collections from fractions import Fraction @@ -59,6 +58,13 @@ import warnings from .TiffTags import TYPES +try: + # Python 3 + from collections.abc import MutableMapping +except ImportError: + # Python 2.7 + from collections import MutableMapping + __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -71,9 +77,6 @@ IFD_LEGACY_API = True II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) -i8 = _binary.i8 -o8 = _binary.o8 - # # -------------------------------------------------------------------- # Read TIFF files @@ -132,7 +135,7 @@ COMPRESSION_INFO = { 34677: "tiff_sgilog24", } -COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) +COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, @@ -177,14 +180,14 @@ OPEN_INFO = { (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), - (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), - (MM, 1, (2,), 1, (16,), ()): ("I;16BS", "I;16BS"), + (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), - (MM, 1, (2,), 1, (32,), ()): ("I;32BS", "I;32BS"), + (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), @@ -199,6 +202,10 @@ OPEN_INFO = { (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), @@ -206,6 +213,17 @@ OPEN_INFO = { (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), + (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), @@ -227,15 +245,30 @@ OPEN_INFO = { (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (II, 6, (1,), 1, (8, 8, 8, 8), (0,)): ("YCbCr", "YCbCrX"), + (MM, 6, (1,), 1, (8, 8, 8, 8), (0,)): ("YCbCr", "YCbCrX"), + (II, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXXX"), + (MM, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXXX"), + (II, 6, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("YCbCr", "YCbCrXXX"), + (MM, 6, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("YCbCr", "YCbCrXXX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } -PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] +PREFIXES = [ + b"MM\x00\x2A", # Valid TIFF header with big-endian byte order + b"II\x2A\x00", # Valid TIFF header with little-endian byte order + b"MM\x2A\x00", # Invalid TIFF header, assume big-endian + b"II\x00\x2A", # Invalid TIFF header, assume little-endian +] def _accept(prefix): @@ -247,6 +280,7 @@ def _limit_rational(val, max_val): n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d + ## # Wrapper for TIFF IFDs. @@ -278,12 +312,12 @@ class IFDRational(Rational): self._numerator = value self._val = float(1) - if type(value) == Fraction: + if isinstance(value, Fraction): self._numerator = value.numerator self._denominator = value.denominator self._val = value - if type(value) == IFDRational: + if isinstance(value, IFDRational): self._denominator = value.denominator self._numerator = value.numerator self._val = value._val @@ -294,11 +328,7 @@ class IFDRational(Rational): return elif denominator == 1: - if sys.hexversion < 0x2070000 and type(value) == float: - # python 2.6 is different. - self._val = Fraction.from_float(value) - else: - self._val = Fraction(value) + self._val = Fraction(value) else: self._val = Fraction(value, denominator) @@ -342,7 +372,7 @@ class IFDRational(Rational): 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', 'ceil', 'floor', 'round'] - print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) + print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ __add__ = _delegate('__add__') @@ -375,7 +405,7 @@ class IFDRational(Rational): __round__ = _delegate('__round__') -class ImageFileDirectory_v2(collections.MutableMapping): +class ImageFileDirectory_v2(MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -471,15 +501,6 @@ class ImageFileDirectory_v2(collections.MutableMapping): def __str__(self): return str(dict(self)) - def as_dict(self): - """Return a dictionary of the image's tags. - - .. deprecated:: 3.0.0 - """ - warnings.warn("as_dict() is deprecated. " + - "Please use dict(ifd) instead.", DeprecationWarning) - return dict(self) - def named(self): """ :returns: dict of name|key: value @@ -506,7 +527,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata - if bytes is str: + if not py3: def has_key(self, tag): return tag in self @@ -515,7 +536,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - if bytes is str: + if not py3: basetypes += unicode, info = TiffTags.lookup(tag) @@ -536,25 +557,43 @@ class ImageFileDirectory_v2(collections.MutableMapping): elif all(isinstance(v, float) for v in values): self.tagtype[tag] = 12 else: - if bytes is str: - # Never treat data as binary by default on Python 2. - self.tagtype[tag] = 2 - else: + if py3: if all(isinstance(v, str) for v in values): self.tagtype[tag] = 2 + else: + # Never treat data as binary by default on Python 2. + self.tagtype[tag] = 2 - if self.tagtype[tag] == 7 and bytes is not str: - values = [value.encode("ascii", 'replace') if isinstance(value, str) else value] + if self.tagtype[tag] == 7 and py3: + values = [value.encode("ascii", 'replace') if isinstance( + value, str) else value] values = tuple(info.cvt_enum(value) for value in values) dest = self._tags_v1 if legacy_api else self._tags_v2 - if info.length == 1: - if legacy_api and self.tagtype[tag] in [5, 10]: + # Three branches: + # Spec'd length == 1, Actual length 1, store as element + # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. + # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. + # Don't mess with the legacy api, since it's frozen. + if ((info.length == 1) or + (info.length is None and len(values) == 1 and not legacy_api)): + # Don't mess with the legacy api, since it's frozen. + if legacy_api and self.tagtype[tag] in [5, 10]: # rationals values = values, - dest[tag], = values + try: + dest[tag], = values + except ValueError: + # We've got a builtin tag with 1 expected entry + warnings.warn( + "Metadata Warning, tag %s had too many entries: %s, expected 1" % ( + tag, len(values))) + dest[tag] = values[0] + else: + # Spec'd length > 1 or undefined + # Unspec'd, and length > 1 dest[tag] = values def __delitem__(self, tag): @@ -573,7 +612,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def _register_loader(idx, size): def decorator(func): - from PIL.TiffTags import TYPES + from .TiffTags import TYPES if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") _load_dispatch[idx] = size, func @@ -587,19 +626,23 @@ class ImageFileDirectory_v2(collections.MutableMapping): return decorator def _register_basic(idx_fmt_name): - from PIL.TiffTags import TYPES + from .TiffTags import TYPES idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( - self._unpack("{0}{1}".format(len(data) // size, fmt), data)) + self._unpack("{}{}".format(len(data) // size, fmt), data)) _write_dispatch[idx] = lambda self, *values: ( b"".join(self._pack(fmt, value) for value in values)) list(map(_register_basic, - [(3, "H", "short"), (4, "L", "long"), - (6, "b", "signed byte"), (8, "h", "signed short"), - (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + [(3, "H", "short"), + (4, "L", "long"), + (6, "b", "signed byte"), + (8, "h", "signed short"), + (9, "l", "signed long"), + (11, "f", "float"), + (12, "d", "double")])) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): @@ -618,14 +661,15 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if sys.version_info[0] == 2: + if sys.version_info.major == 2: value = value.decode('ascii', 'replace') return b"" + value.encode('ascii', 'replace') + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): - vals = self._unpack("{0}L".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + vals = self._unpack("{}L".format(len(data) // 4), data) + + def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -644,8 +688,9 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack("{0}l".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + vals = self._unpack("{}l".format(len(data) // 4), data) + + def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -669,7 +714,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): try: for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): - tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) + tag, typ, count, data = self._unpack("HHL4s", + self._ensure_read(fp, 12)) if DEBUG: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") @@ -697,8 +743,11 @@ class ImageFileDirectory_v2(collections.MutableMapping): if len(data) != size: warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d. " - "Skipping tag %s" % (size, len(data), tag)) + "Expecting to read %d bytes but only got %d." + " Skipping tag %s" % (size, len(data), tag)) + continue + + if not data: continue self._tagdata[tag] = data @@ -754,7 +803,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), data)) + entries.append((tag, typ, count, self._pack("L", offset), + data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data @@ -783,6 +833,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return offset + ImageFileDirectory_v2._load_dispatch = _load_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch for idx, name in TYPES.items(): @@ -801,7 +852,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' ifd.tagtype[key] = 2 - print ifd[key] + print(ifd[key]) ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, @@ -890,6 +941,7 @@ class TiffImageFile(ImageFile.ImageFile): format = "TIFF" format_description = "Adobe TIFF" + _close_exclusive_fp_after_loading = False def _open(self): "Open the first image in a TIFF file" @@ -934,20 +986,25 @@ class TiffImageFile(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): "Select a given frame as current image" - self._seek(max(frame, 0)) # Questionable backwards compatibility. + if not self._seek_check(frame): + return + self._seek(frame) # Create a new core image object on second and # subsequent frames in the image. Image may be # different size/mode. @@ -975,6 +1032,7 @@ class TiffImageFile(ImageFile.ImageFile): self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) + self.__next = self.tag_v2.next # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame @@ -993,22 +1051,8 @@ class TiffImageFile(ImageFile.ImageFile): compression = self._compression if compression == "raw": args = (rawmode, 0, 1) - elif compression == "jpeg": - args = rawmode, "" - if JPEGTABLES in self.tag_v2: - # Hack to handle abbreviated JPEG headers - # FIXME This will fail with more than one value - self.tile_prefix, = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode - elif compression == "tiff_lzw": - args = rawmode - if PREDICTOR in self.tag_v2: - # Section 14: Differencing Predictor - self.decoderconfig = (self.tag_v2[PREDICTOR],) - - if ICCPROFILE in self.tag_v2: - self.info['icc_profile'] = self.tag_v2[ICCPROFILE] return args @@ -1017,6 +1061,12 @@ class TiffImageFile(ImageFile.ImageFile): return self._load_libtiff() return super(TiffImageFile, self).load() + def load_end(self): + # allow closing if we're on the first frame, there's no next + # This is the ImageFile.load path only, libtiff specific below. + if self.__frame == 0 and not self.__next: + self._close_exclusive_fp_after_loading = True + def _load_libtiff(self): """ Overload method triggered when we detect a compressed tiff Calls out to libtiff """ @@ -1036,8 +1086,28 @@ class TiffImageFile(ImageFile.ImageFile): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = self.tile[0][3] + (self.tag_v2.offset,) - decoder = Image._getdecoder(self.mode, 'libtiff', args, + args = list(self.tile[0][3]) + [self.tag_v2.offset] + + # To be nice on memory footprint, if there's a + # file descriptor, use that instead of reading + # into a string in python. + # libtiff closes the file descriptor, so pass in a dup. + try: + fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + # flush the file descriptor, prevents error on pypy 2.4+ + # should also eliminate the need for fp.tell for py3 + # in _seek + if hasattr(self.fp, "flush"): + self.fp.flush() + except IOError: + # io.BytesIO have a fileno, but returns an IOError if + # it doesn't use a file descriptor. + fp = False + + if fp: + args[2] = fp + + decoder = Image._getdecoder(self.mode, 'libtiff', tuple(args), self.decoderconfig) try: decoder.setimage(self.im, extents) @@ -1074,16 +1144,14 @@ class TiffImageFile(ImageFile.ImageFile): self.tile = [] self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible - if hasattr(self.fp, 'close'): - if not self.__next: + if self._exclusive_fp: + if self.__frame == 0 and not self.__next: self.fp.close() - self.fp = None # might be shared + self.fp = None # might be shared if err < 0: raise IOError(err) - self.load_end() - return Image.Image.load(self) def _setup(self): @@ -1119,7 +1187,7 @@ class TiffImageFile(ImageFile.ImageFile): sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) if (len(sampleFormat) > 1 - and max(sampleFormat) == min(sampleFormat) == 1): + and max(sampleFormat) == min(sampleFormat) == 1): # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just @@ -1127,12 +1195,23 @@ class TiffImageFile(ImageFile.ImageFile): # for more exotic images. sampleFormat = (1,) + bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) + extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) + if photo in (2, 6, 8): # RGB, YCbCr, LAB + bps_count = 3 + elif photo == 5: # CMYK + bps_count = 4 + else: + bps_count = 1 + bps_count += len(extra_tuple) + # Some files have only one value in bps_tuple, + # while should have more. Fix it + if bps_count > len(bps_tuple) and len(bps_tuple) == 1: + bps_tuple = bps_tuple * bps_count + # mode: check photometric interpretation and bits per pixel - key = ( - self.tag_v2.prefix, photo, sampleFormat, fillorder, - self.tag_v2.get(BITSPERSAMPLE, (1,)), - self.tag_v2.get(EXTRASAMPLES, ()) - ) + key = (self.tag_v2.prefix, photo, sampleFormat, fillorder, + bps_tuple, extra_tuple) if DEBUG: print("format key:", key) try: @@ -1152,11 +1231,16 @@ class TiffImageFile(ImageFile.ImageFile): yres = self.tag_v2.get(Y_RESOLUTION, 1) if xres and yres: - resunit = self.tag_v2.get(RESOLUTION_UNIT, 1) + resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch self.info["dpi"] = xres, yres elif resunit == 3: # dots per centimeter. convert to dpi self.info["dpi"] = xres * 2.54, yres * 2.54 + elif resunit is None: # used to default to 1, but now 2) + self.info["dpi"] = xres, yres + # For backward compatibility, + # we also preserve the old behavior + self.info["resolution"] = xres, yres else: # No absolute unit of measurement self.info["resolution"] = xres, yres @@ -1169,16 +1253,9 @@ class TiffImageFile(ImageFile.ImageFile): offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] - if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", - "group4", "tiff_jpeg", - "tiff_adobe_deflate", - "tiff_thunderscan", - "tiff_deflate", - "tiff_sgilog", - "tiff_sgilog24", - "tiff_raw_16"]: + if READ_LIBTIFF or self._compression != 'raw': # if DEBUG: - # print "Activating g4 compression for whole file" + # print("Activating g4 compression for whole file") # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) @@ -1190,24 +1267,6 @@ class TiffImageFile(ImageFile.ImageFile): self.use_load_libtiff = True - # To be nice on memory footprint, if there's a - # file descriptor, use that instead of reading - # into a string in python. - - # libtiff closes the file descriptor, so pass in a dup. - try: - fp = hasattr(self.fp, "fileno") and \ - os.dup(self.fp.fileno()) - # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell for py3 - # in _seek - if hasattr(self.fp, "flush"): - self.fp.flush() - except IOError: - # io.BytesIO have a fileno, but returns an IOError if - # it doesn't use a file descriptor. - fp = False - # libtiff handles the fillmode for us, so 1;IR should # actually be 1;I. Including the R double reverses the # bits, so stripes of the image are reversed. See @@ -1228,12 +1287,16 @@ class TiffImageFile(ImageFile.ImageFile): # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if self.mode in ('I;16B', 'I;16') and 'I;16' in rawmode: + if rawmode == 'I;16': rawmode = 'I;16N' + if ';16B' in rawmode: + rawmode = rawmode.replace(';16B', ';16N') + if ';16L' in rawmode: + rawmode = rawmode.replace(';16L', ';16N') # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds - a = (rawmode, self._compression, fp) + a = (rawmode, self._compression, False) self.tile.append( (self._compression, (0, 0, w, ysize), @@ -1241,12 +1304,12 @@ class TiffImageFile(ImageFile.ImageFile): a = None else: - for i in range(len(offsets)): + for i, offset in enumerate(offsets): a = self._decoder(rawmode, l, i) self.tile.append( (self._compression, (0, min(y, ysize), w, min(y+h, ysize)), - offsets[i], a)) + offset, a)) if DEBUG: print("tiles: ", self.tile) y = y + h @@ -1280,11 +1343,17 @@ class TiffImageFile(ImageFile.ImageFile): print("- unsupported data organization") raise SyntaxError("unknown data organization") + # Fix up info. + if ICCPROFILE in self.tag_v2: + self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + # fixup palette descriptor if self.mode == "P": palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + + # # -------------------------------------------------------------------- # Write TIFF files @@ -1359,12 +1428,12 @@ def _save(im, fp, filename): IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): if key in im.tag_v2: ifd[key] = im.tag_v2[key] - ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None) + ifd.tagtype[key] = im.tag_v2.tagtype[key] - # preserve ICC profile (should also work when saving other formats - # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - if "icc_profile" in im.info: - ifd[ICCPROFILE] = im.info["icc_profile"] + # preserve ICC profile (should also work when saving other formats + # which support profiles as TIFF) -- 2008-06-06 Florian Hoech + if "icc_profile" in im.info: + ifd[ICCPROFILE] = im.info["icc_profile"] for key, name in [(IMAGEDESCRIPTION, "description"), (X_RESOLUTION, "resolution"), @@ -1376,11 +1445,6 @@ def _save(im, fp, filename): (DATE_TIME, "date_time"), (ARTIST, "artist"), (COPYRIGHT, "copyright")]: - name_with_spaces = name.replace("_", " ") - if "_" in name and name_with_spaces in im.encoderinfo: - warnings.warn("%r is deprecated; use %r instead" % - (name_with_spaces, name), DeprecationWarning) - ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] @@ -1404,7 +1468,6 @@ def _save(im, fp, filename): if im.mode == "P": lut = im.im.getpalette("RGB", "RGB;L") ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) - # data orientation stride = len(bits) * ((im.size[0]*bits[0]+7)//8) ifd[ROWSPERSTRIP] = im.size[1] @@ -1448,7 +1511,7 @@ def _save(im, fp, filename): if tag not in TiffTags.LIBTIFF_CORE: continue if tag not in atts and tag not in blocklist: - if isinstance(value, unicode if bytes is str else str): + if isinstance(value, str if py3 else unicode): atts[tag] = value.encode('ascii', 'replace') + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) @@ -1491,14 +1554,285 @@ def _save(im, fp, filename): # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd + +class AppendingTiffWriter: + fieldSizes = [ + 0, # None + 1, # byte + 1, # ascii + 2, # short + 4, # long + 8, # rational + 1, # sbyte + 1, # undefined + 2, # sshort + 4, # slong + 8, # srational + 4, # float + 8, # double + ] + + # StripOffsets = 273 + # FreeOffsets = 288 + # TileOffsets = 324 + # JPEGQTables = 519 + # JPEGDCTables = 520 + # JPEGACTables = 521 + Tags = {273, 288, 324, 519, 520, 521} + + def __init__(self, fn, new=False): + if hasattr(fn, 'read'): + self.f = fn + self.close_fp = False + else: + self.name = fn + self.close_fp = True + try: + self.f = io.open(fn, "w+b" if new else "r+b") + except IOError: + self.f = io.open(fn, "w+b") + self.beginning = self.f.tell() + self.setup() + + def setup(self): + # Reset everything. + self.f.seek(self.beginning, os.SEEK_SET) + + self.whereToWriteNewIFDOffset = None + self.offsetOfNewPage = 0 + + self.IIMM = IIMM = self.f.read(4) + if not IIMM: + # empty file - first page + self.isFirst = True + return + + self.isFirst = False + if IIMM == b"II\x2a\x00": + self.setEndian("<") + elif IIMM == b"MM\x00\x2a": + self.setEndian(">") + else: + raise RuntimeError("Invalid TIFF file header") + + self.skipIFDs() + self.goToEnd() + + def finalize(self): + if self.isFirst: + return + + # fix offsets + self.f.seek(self.offsetOfNewPage) + + IIMM = self.f.read(4) + if not IIMM: + # raise RuntimeError("nothing written into new page") + # Make it easy to finish a frame without committing to a new one. + return + + if IIMM != self.IIMM: + raise RuntimeError("IIMM of new page doesn't match IIMM of " + "first page") + + IFDoffset = self.readLong() + IFDoffset += self.offsetOfNewPage + self.f.seek(self.whereToWriteNewIFDOffset) + self.writeLong(IFDoffset) + self.f.seek(IFDoffset) + self.fixIFD() + + def newFrame(self): + # Call this to finish a frame. + self.finalize() + self.setup() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.close_fp: + self.close() + return False + + def tell(self): + return self.f.tell() - self.offsetOfNewPage + + def seek(self, offset, whence): + if whence == os.SEEK_SET: + offset += self.offsetOfNewPage + + self.f.seek(offset, whence) + return self.tell() + + def goToEnd(self): + self.f.seek(0, os.SEEK_END) + pos = self.f.tell() + + # pad to 16 byte boundary + padBytes = 16 - pos % 16 + if 0 < padBytes < 16: + self.f.write(bytes(bytearray(padBytes))) + self.offsetOfNewPage = self.f.tell() + + def setEndian(self, endian): + self.endian = endian + self.longFmt = self.endian + "L" + self.shortFmt = self.endian + "H" + self.tagFormat = self.endian + "HHL" + + def skipIFDs(self): + while True: + IFDoffset = self.readLong() + if IFDoffset == 0: + self.whereToWriteNewIFDOffset = self.f.tell() - 4 + break + + self.f.seek(IFDoffset) + numTags = self.readShort() + self.f.seek(numTags * 12, os.SEEK_CUR) + + def write(self, data): + return self.f.write(data) + + def readShort(self): + value, = struct.unpack(self.shortFmt, self.f.read(2)) + return value + + def readLong(self): + value, = struct.unpack(self.longFmt, self.f.read(4)) + return value + + def rewriteLastShortToLong(self, value): + self.f.seek(-2, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def rewriteLastShort(self, value): + self.f.seek(-2, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) + if bytesWritten is not None and bytesWritten != 2: + raise RuntimeError("wrote only %u bytes but wanted 2" % + bytesWritten) + + def rewriteLastLong(self, value): + self.f.seek(-4, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def writeShort(self, value): + bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) + if bytesWritten is not None and bytesWritten != 2: + raise RuntimeError("wrote only %u bytes but wanted 2" % + bytesWritten) + + def writeLong(self, value): + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def close(self): + self.finalize() + self.f.close() + + def fixIFD(self): + numTags = self.readShort() + + for i in range(numTags): + tag, fieldType, count = struct.unpack(self.tagFormat, + self.f.read(8)) + + fieldSize = self.fieldSizes[fieldType] + totalSize = fieldSize * count + isLocal = (totalSize <= 4) + if not isLocal: + offset = self.readLong() + offset += self.offsetOfNewPage + self.rewriteLastLong(offset) + + if tag in self.Tags: + curPos = self.f.tell() + + if isLocal: + self.fixOffsets(count, isShort=(fieldSize == 2), + isLong=(fieldSize == 4)) + self.f.seek(curPos + 4) + else: + self.f.seek(offset) + self.fixOffsets(count, isShort=(fieldSize == 2), + isLong=(fieldSize == 4)) + self.f.seek(curPos) + + offset = curPos = None + + elif isLocal: + # skip the locally stored value that is not an offset + self.f.seek(4, os.SEEK_CUR) + + def fixOffsets(self, count, isShort=False, isLong=False): + if not isShort and not isLong: + raise RuntimeError("offset is neither short nor long") + + for i in range(count): + offset = self.readShort() if isShort else self.readLong() + offset += self.offsetOfNewPage + if isShort and offset >= 65536: + # offset is now too large - we must convert shorts to longs + if count != 1: + raise RuntimeError("not implemented") # XXX TODO + + # simple case - the offset is just one and therefore it is + # local (not referenced with another offset) + self.rewriteLastShortToLong(offset) + self.f.seek(-10, os.SEEK_CUR) + self.writeShort(4) # rewrite the type to LONG + self.f.seek(8, os.SEEK_CUR) + elif isShort: + self.rewriteLastShort(offset) + else: + self.rewriteLastLong(offset) + + +def _save_all(im, fp, filename): + encoderinfo = im.encoderinfo.copy() + encoderconfig = im.encoderconfig + append_images = list(encoderinfo.get("append_images", [])) + if not hasattr(im, "n_frames") and not append_images: + return _save(im, fp, filename) + + cur_idx = im.tell() + try: + with AppendingTiffWriter(fp) as tf: + for ims in [im]+append_images: + ims.encoderinfo = encoderinfo + ims.encoderconfig = encoderconfig + if not hasattr(ims, "n_frames"): + nfr = 1 + else: + nfr = ims.n_frames + + for idx in range(nfr): + ims.seek(idx) + ims.load() + _save(ims, tf, filename) + tf.newFrame() + finally: + im.seek(cur_idx) + + # # -------------------------------------------------------------------- # Register Image.register_open(TiffImageFile.format, TiffImageFile, _accept) Image.register_save(TiffImageFile.format, _save) +Image.register_save_all(TiffImageFile.format, _save_all) -Image.register_extension(TiffImageFile.format, ".tif") -Image.register_extension(TiffImageFile.format, ".tiff") +Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/server/www/packages/packages-windows/x86/PIL/TiffTags.py b/server/www/packages/packages-windows/x86/PIL/TiffTags.py index ecc63ba..427f3a4 100644 --- a/server/www/packages/packages-windows/x86/PIL/TiffTags.py +++ b/server/www/packages/packages-windows/x86/PIL/TiffTags.py @@ -23,7 +23,7 @@ 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 {}) @@ -142,6 +142,8 @@ TAGS_V2 = { 341: ("SMaxSampleValue", DOUBLE, 0), 342: ("TransferRange", SHORT, 6), + 347: ("JPEGTables", UNDEFINED, 1), + # obsolete JPEG tags 512: ("JPEGProc", SHORT, 1), 513: ("JPEGInterchangeFormat", LONG, 1), @@ -158,7 +160,10 @@ TAGS_V2 = { 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", LONG, 0), + 700: ('XMP', BYTE, 1), + 33432: ("Copyright", ASCII, 1), + 34377: ('PhotoshopInfo', BYTE, 1), # FIXME add more tags here 34665: ("ExifIFD", SHORT, 1), @@ -169,7 +174,7 @@ TAGS_V2 = { 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), 45058: ("MPEntry", UNDEFINED, 1), - 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check + 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check 45060: ("TotalFrames", LONG, 1), 45313: ("MPIndividualNum", LONG, 1), 45569: ("PanOrientation", LONG, 1), @@ -188,8 +193,8 @@ TAGS_V2 = { 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 1), - 50839: ("ImageJMetaData", UNDEFINED, 1) + 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one + 50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006 } # Legacy Tags structure @@ -350,6 +355,7 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) + _populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. @@ -377,54 +383,54 @@ TYPES = {} # 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: +# 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 - ]) +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 diff --git a/server/www/packages/packages-windows/x86/PIL/WalImageFile.py b/server/www/packages/packages-windows/x86/PIL/WalImageFile.py index 0cbd1ca..6602cc8 100644 --- a/server/www/packages/packages-windows/x86/PIL/WalImageFile.py +++ b/server/www/packages/packages-windows/x86/PIL/WalImageFile.py @@ -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. -#

-# By default, a Quake2 standard palette is attached to the texture. -# To override the palette, use the putpalette 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 putpalette 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 = ( diff --git a/server/www/packages/packages-windows/x86/PIL/WebPImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/WebPImagePlugin.py index 6837b53..39a8f2e 100644 --- a/server/www/packages/packages-windows/x86/PIL/WebPImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/WebPImagePlugin.py @@ -1,10 +1,13 @@ -from PIL import Image -from PIL import ImageFile +from . import Image, ImageFile, _webp from io import BytesIO -from PIL import _webp _VALID_WEBP_MODES = { + "RGBX": True, + "RGBA": True, + } + +_VALID_WEBP_LEGACY_MODES = { "RGB": True, "RGBA": True, } @@ -30,32 +33,263 @@ 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 = 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 + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] + + 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 = encoderinfo.get("background", (0, 0, 0, 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 + if ims.mode not in _VALID_WEBP_MODES: + alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode() + frame = ims.convert('RGBA' if alpha else 'RGBX') + + # Append the frame to the animation encoder + enc.add( + frame.tobytes(), + timestamp, + frame.size[0], frame.size[1], + frame.mode, + 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 = im.mode == 'P' and 'A' in im.im.getpalettemode() + im = im.convert('RGBA' if alpha else 'RGB') data = _webp.WebPEncode( im.tobytes(), @@ -65,16 +299,18 @@ 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) - +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") diff --git a/server/www/packages/packages-windows/x86/PIL/WmfImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/WmfImagePlugin.py index 3163210..2135844 100644 --- a/server/www/packages/packages-windows/x86/PIL/WmfImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/WmfImagePlugin.py @@ -14,26 +14,36 @@ # # 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 +63,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,7 +108,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = 72 - # print self.mode, self.size, self.info + # print(self.mode, self.size, self.info) # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": @@ -121,13 +118,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 @@ -158,7 +155,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 +163,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"]) diff --git a/server/www/packages/packages-windows/x86/PIL/XVThumbImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/XVThumbImagePlugin.py index 9fe9ca1..a7d39ed 100644 --- a/server/www/packages/packages-windows/x86/PIL/XVThumbImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/XVThumbImagePlugin.py @@ -17,12 +17,11 @@ # 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) @@ -32,6 +31,7 @@ for r in range(8): for b in range(4): PALETTE = PALETTE + (o8((r*255)//7)+o8((g*255)//7)+o8((b*255)//3)) + def _accept(prefix): return prefix[:6] == _MAGIC @@ -47,7 +47,7 @@ class XVThumbImageFile(ImageFile.ImageFile): def _open(self): # check magic - if self.fp.read(6) != _MAGIC: + if not _accept(self.fp.read(6)): raise SyntaxError("not an XV thumbnail file") # Skip to beginning of next line @@ -58,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) @@ -74,6 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.fp.tell(), (self.mode, 0, 1) )] + # -------------------------------------------------------------------- Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/server/www/packages/packages-windows/x86/PIL/XbmImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/XbmImagePlugin.py index bca8828..b43fbef 100644 --- a/server/www/packages/packages-windows/x86/PIL/XbmImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/XbmImagePlugin.py @@ -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[0-9]+)[\r\n]+" + br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" diff --git a/server/www/packages/packages-windows/x86/PIL/XpmImagePlugin.py b/server/www/packages/packages-windows/x86/PIL/XpmImagePlugin.py index 556adb8..a5cca0e 100644 --- a/server/www/packages/packages-windows/x86/PIL/XpmImagePlugin.py +++ b/server/www/packages/packages-windows/x86/PIL/XpmImagePlugin.py @@ -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" @@ -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") diff --git a/server/www/packages/packages-windows/x86/PIL/__init__.py b/server/www/packages/packages-windows/x86/PIL/__init__.py index e5dcf43..a07280e 100644 --- a/server/www/packages/packages-windows/x86/PIL/__init__.py +++ b/server/www/packages/packages-windows/x86/PIL/__init__.py @@ -1,20 +1,34 @@ -# -# 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.3.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 + +__doc__ = __doc__.format(__version__) # include version in docstring + + +_plugins = ['BlpImagePlugin', + 'BmpImagePlugin', 'BufrStubImagePlugin', 'CurImagePlugin', 'DcxImagePlugin', diff --git a/server/www/packages/packages-windows/x86/PIL/_binary.py b/server/www/packages/packages-windows/x86/PIL/_binary.py index 2f5e8ff..767c13b 100644 --- a/server/www/packages/packages-windows/x86/PIL/_binary.py +++ b/server/www/packages/packages-windows/x86/PIL/_binary.py @@ -11,50 +11,70 @@ # See the README file for information on usage and redistribution. # -from struct import unpack, pack +from struct import unpack_from, pack +from ._util import py3 -if bytes is str: - def i8(c): - return ord(c) - - def o8(i): - return chr(i & 255) -else: +if py3: def i8(c): return c if c.__class__ is int else c[0] def o8(i): return bytes((i & 255,)) +else: + def i8(c): + return ord(c) + + def o8(i): + return chr(i & 255) # Input, le = little endian, be = big endian -# TODO: replace with more readable struct.unpack equivalent def i16le(c, o=0): """ - Converts a 2-bytes (16 bits) string to an integer. + Converts a 2-bytes (16 bits) string to an unsigned integer. c: string containing bytes to convert o: offset of bytes to convert in string """ - return unpack("H", c[o:o+2])[0] + return unpack_from(">H", c, o)[0] def i32be(c, o=0): - return unpack(">I", c[o:o+4])[0] + return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian @@ -72,5 +92,3 @@ def o16be(i): def o32be(i): return pack(">I", i) - -# End of file diff --git a/server/www/packages/packages-windows/x86/PIL/_imaging.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imaging.cp37-win32.pyd new file mode 100644 index 0000000..e4d5937 Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imaging.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imaging.pyd b/server/www/packages/packages-windows/x86/PIL/_imaging.pyd deleted file mode 100644 index cecd80d..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imaging.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingcms.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingcms.cp37-win32.pyd new file mode 100644 index 0000000..2678c32 Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imagingcms.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingcms.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingcms.pyd deleted file mode 100644 index 6ed1456..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imagingcms.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingft.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingft.cp37-win32.pyd new file mode 100644 index 0000000..41634bb Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imagingft.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingft.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingft.pyd deleted file mode 100644 index 3617f51..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imagingft.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingmath.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingmath.cp37-win32.pyd new file mode 100644 index 0000000..8e031db Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imagingmath.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingmath.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingmath.pyd deleted file mode 100644 index 845a52f..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imagingmath.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingmorph.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingmorph.cp37-win32.pyd new file mode 100644 index 0000000..faa96ef Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imagingmorph.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingmorph.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingmorph.pyd deleted file mode 100644 index f15966b..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imagingmorph.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingtk.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingtk.cp37-win32.pyd new file mode 100644 index 0000000..fed76bf Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_imagingtk.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_imagingtk.pyd b/server/www/packages/packages-windows/x86/PIL/_imagingtk.pyd deleted file mode 100644 index 7f02fe0..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_imagingtk.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/_tkinter_finder.py b/server/www/packages/packages-windows/x86/PIL/_tkinter_finder.py index df41591..987d962 100644 --- a/server/www/packages/packages-windows/x86/PIL/_tkinter_finder.py +++ b/server/www/packages/packages-windows/x86/PIL/_tkinter_finder.py @@ -2,7 +2,7 @@ """ import sys -if sys.version_info[0] > 2: +if sys.version_info.major > 2: from tkinter import _tkinter as tk else: from Tkinter import tkinter as tk @@ -11,7 +11,7 @@ if hasattr(sys, 'pypy_find_executable'): # Tested with packages at https://bitbucket.org/pypy/pypy/downloads. # PyPies 1.6, 2.0 do not have tkinter built in. PyPy3-2.3.1 gives an # OSError trying to import tkinter. Otherwise: - try: # PyPy 5.1, 4.0.0, 2.6.1, 2.6.0 + try: # PyPy 5.1, 4.0.0, 2.6.1, 2.6.0 TKINTER_LIB = tk.tklib_cffi.__file__ except AttributeError: # PyPy3 2.4, 2.1-beta1; PyPy 2.5.1, 2.5.0, 2.4.0, 2.3, 2.2, 2.1 diff --git a/server/www/packages/packages-windows/x86/PIL/_util.py b/server/www/packages/packages-windows/x86/PIL/_util.py index 51c6f68..6618c62 100644 --- a/server/www/packages/packages-windows/x86/PIL/_util.py +++ b/server/www/packages/packages-windows/x86/PIL/_util.py @@ -1,17 +1,19 @@ -import os +import os, sys -if bytes is str: - def isStringType(t): - return isinstance(t, basestring) +py3 = sys.version_info.major >= 3 - def isPath(f): - return isinstance(f, basestring) -else: +if py3: def isStringType(t): return isinstance(t, str) def isPath(f): return isinstance(f, (bytes, str)) +else: + def isStringType(t): + return isinstance(t, basestring) + + def isPath(f): + return isinstance(f, basestring) # Checks if an object is a string, and that it points to a directory. diff --git a/server/www/packages/packages-windows/x86/PIL/_version.py b/server/www/packages/packages-windows/x86/PIL/_version.py new file mode 100644 index 0000000..b42628d --- /dev/null +++ b/server/www/packages/packages-windows/x86/PIL/_version.py @@ -0,0 +1,2 @@ +# Master version for Pillow +__version__ = '5.2.0' diff --git a/server/www/packages/packages-windows/x86/PIL/_webp.cp37-win32.pyd b/server/www/packages/packages-windows/x86/PIL/_webp.cp37-win32.pyd new file mode 100644 index 0000000..5086304 Binary files /dev/null and b/server/www/packages/packages-windows/x86/PIL/_webp.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/PIL/_webp.pyd b/server/www/packages/packages-windows/x86/PIL/_webp.pyd deleted file mode 100644 index cbaab26..0000000 Binary files a/server/www/packages/packages-windows/x86/PIL/_webp.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/PIL/features.py b/server/www/packages/packages-windows/x86/PIL/features.py index fd87f09..9926445 100644 --- a/server/www/packages/packages-windows/x86/PIL/features.py +++ b/server/www/packages/packages-windows/x86/PIL/features.py @@ -1,46 +1,30 @@ -from PIL import Image +from . import Image modules = { "pil": "PIL._imaging", - "tkinter": "PIL._imagingtk", + "tkinter": "PIL._tkinter_finder", "freetype2": "PIL._imagingft", "littlecms2": "PIL._imagingcms", "webp": "PIL._webp", - "transp_webp": ("WEBP", "WebPDecoderBuggyAlpha") } def check_module(feature): - if feature not in modules: + if not (feature in modules): raise ValueError("Unknown module %s" % feature) module = modules[feature] - method_to_call = None - if type(module) is tuple: - module, method_to_call = module - try: - imported_module = __import__(module) - except ImportError: - # If a method is being checked, None means that - # rather than the method failing, the module required for the method - # failed to be imported first - return None if method_to_call else False - - if method_to_call: - method = getattr(imported_module, method_to_call) - return method() is True - else: + __import__(module) return True + except ImportError: + return False def get_supported_modules(): - supported_modules = [] - for feature in modules: - if check_module(feature): - supported_modules.append(feature) - return supported_modules + return [f for f in modules if check_module(f)] + codecs = { "jpg": "jpeg", @@ -60,8 +44,42 @@ def check_codec(feature): def get_supported_codecs(): - supported_codecs = [] - for feature in codecs: - if check_codec(feature): - supported_codecs.append(feature) - return supported_codecs + return [f for f in codecs if check_codec(f)] + + +features = { + "webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'), + "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), + "raqm": ("PIL._imagingft", "HAVE_RAQM") +} + + +def check_feature(feature): + if feature not in features: + raise ValueError("Unknown feature %s" % feature) + + module, flag = features[feature] + + try: + imported_module = __import__(module, fromlist=['PIL']) + return getattr(imported_module, flag) + except ImportError: + return None + + +def get_supported_features(): + return [f for f in features if check_feature(f)] + + +def check(feature): + return (feature in modules and check_module(feature) or + feature in codecs and check_codec(feature) or + feature in features and check_feature(feature)) + + +def get_supported(): + ret = get_supported_modules() + ret.extend(get_supported_features()) + ret.extend(get_supported_codecs()) + return ret diff --git a/server/www/packages/packages-windows/x86/psutil/__init__.py b/server/www/packages/packages-windows/x86/psutil/__init__.py index d6a66de..e129965 100644 --- a/server/www/packages/packages-windows/x86/psutil/__init__.py +++ b/server/www/packages/packages-windows/x86/psutil/__init__.py @@ -10,7 +10,7 @@ sensors) in Python. Supported platforms: - Linux - Windows - - OSX + - macOS - FreeBSD - OpenBSD - NetBSD @@ -43,19 +43,19 @@ from ._common import deprecated_method from ._common import memoize from ._common import memoize_when_activated from ._common import wrap_numbers as _wrap_numbers -from ._compat import callable from ._compat import long from ._compat import PY3 as _PY3 from ._common import STATUS_DEAD from ._common import STATUS_DISK_SLEEP -from ._common import STATUS_IDLE # bsd +from ._common import STATUS_IDLE from ._common import STATUS_LOCKED +from ._common import STATUS_PARKED from ._common import STATUS_RUNNING from ._common import STATUS_SLEEPING from ._common import STATUS_STOPPED from ._common import STATUS_TRACING_STOP -from ._common import STATUS_WAITING # bsd +from ._common import STATUS_WAITING from ._common import STATUS_WAKING from ._common import STATUS_ZOMBIE @@ -79,9 +79,10 @@ from ._common import AIX from ._common import BSD from ._common import FREEBSD # NOQA from ._common import LINUX +from ._common import MACOS from ._common import NETBSD # NOQA from ._common import OPENBSD # NOQA -from ._common import OSX +from ._common import OSX # deprecated alias from ._common import POSIX # NOQA from ._common import SUNOS from ._common import WINDOWS @@ -152,7 +153,7 @@ elif WINDOWS: from ._psutil_windows import REALTIME_PRIORITY_CLASS # NOQA from ._pswindows import CONN_DELETE_TCB # NOQA -elif OSX: +elif MACOS: from . import _psosx as _psplatform elif BSD: @@ -189,6 +190,7 @@ __all__ = [ "STATUS_RUNNING", "STATUS_IDLE", "STATUS_SLEEPING", "STATUS_DISK_SLEEP", "STATUS_STOPPED", "STATUS_TRACING_STOP", "STATUS_ZOMBIE", "STATUS_DEAD", "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_LOCKED", + "STATUS_PARKED", "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", @@ -200,8 +202,8 @@ __all__ = [ "POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED", - "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "OSX", "POSIX", "SUNOS", - "WINDOWS", "AIX", + "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "MACOS", "OSX", "POSIX", + "SUNOS", "WINDOWS", "AIX", # classes "Process", "Popen", @@ -219,7 +221,7 @@ __all__ = [ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.2" +__version__ = "5.4.7" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED @@ -266,13 +268,9 @@ else: ret = {} for pid in pids(): try: - proc = _psplatform.Process(pid) - ppid = proc.ppid() - except (NoSuchProcess, AccessDenied): - # Note: AccessDenied is unlikely to happen. + ret[pid] = _psplatform.Process(pid).ppid() + except (NoSuchProcess, ZombieProcess): pass - else: - ret[pid] = ppid return ret @@ -828,7 +826,7 @@ class Process(object): """ return self._proc.cpu_num() - # Linux, OSX and Windows only + # Linux, macOS and Windows only if hasattr(_psplatform.Process, "environ"): def environ(self): @@ -1032,7 +1030,7 @@ class Process(object): namedtuple representing the accumulated process time, in seconds. This is similar to os.times() but per-process. - On OSX and Windows children_user and children_system are + On macOS and Windows children_user and children_system are always set to 0. """ return self._proc.cpu_times() @@ -1054,7 +1052,7 @@ class Process(object): def memory_full_info(self): """This method returns the same information as memory_info(), - plus, on some platform (Linux, OSX, Windows), also provides + plus, on some platform (Linux, macOS, Windows), also provides additional metrics (USS, PSS and swap). The additional metrics provide a better representation of actual process memory usage. @@ -1656,6 +1654,27 @@ def _cpu_busy_time(times): return busy +def _cpu_times_deltas(t1, t2): + assert t1._fields == t2._fields, (t1, t2) + field_deltas = [] + for field in _psplatform.scputimes._fields: + field_delta = getattr(t2, field) - getattr(t1, field) + # CPU times are always supposed to increase over time + # or at least remain the same and that's because time + # cannot go backwards. + # Surprisingly sometimes this might not be the case (at + # least on Windows and Linux), see: + # https://github.com/giampaolo/psutil/issues/392 + # https://github.com/giampaolo/psutil/issues/645 + # https://github.com/giampaolo/psutil/issues/1210 + # Trim negative deltas to zero to ignore decreasing fields. + # top does the same. Reference: + # https://gitlab.com/procps-ng/procps/blob/v3.3.12/top/top.c#L5063 + field_delta = max(0, field_delta) + field_deltas.append(field_delta) + return _psplatform.scputimes(*field_deltas) + + def cpu_percent(interval=None, percpu=False): """Return a float representing the current system-wide CPU utilization as a percentage. @@ -1698,18 +1717,11 @@ def cpu_percent(interval=None, percpu=False): raise ValueError("interval is not positive (got %r)" % interval) def calculate(t1, t2): - t1_all = _cpu_tot_time(t1) - t1_busy = _cpu_busy_time(t1) + times_delta = _cpu_times_deltas(t1, t2) - t2_all = _cpu_tot_time(t2) - t2_busy = _cpu_busy_time(t2) + all_delta = _cpu_tot_time(times_delta) + busy_delta = _cpu_busy_time(times_delta) - # this usually indicates a float precision issue - if t2_busy <= t1_busy: - return 0.0 - - busy_delta = t2_busy - t1_busy - all_delta = t2_all - t1_all try: busy_perc = (busy_delta / all_delta) * 100 except ZeroDivisionError: @@ -1778,28 +1790,18 @@ def cpu_times_percent(interval=None, percpu=False): def calculate(t1, t2): nums = [] - all_delta = _cpu_tot_time(t2) - _cpu_tot_time(t1) - for field in t1._fields: - field_delta = getattr(t2, field) - getattr(t1, field) - try: - field_perc = (100 * field_delta) / all_delta - except ZeroDivisionError: - field_perc = 0.0 + times_delta = _cpu_times_deltas(t1, t2) + all_delta = _cpu_tot_time(times_delta) + # "scale" is the value to multiply each delta with to get percentages. + # We use "max" to avoid division by zero (if all_delta is 0, then all + # fields are 0 so percentages will be 0 too. all_delta cannot be a + # fraction because cpu times are integers) + scale = 100.0 / max(1, all_delta) + for field_delta in times_delta: + field_perc = field_delta * scale field_perc = round(field_perc, 1) - # CPU times are always supposed to increase over time - # or at least remain the same and that's because time - # cannot go backwards. - # Surprisingly sometimes this might not be the case (at - # least on Windows and Linux), see: - # https://github.com/giampaolo/psutil/issues/392 - # https://github.com/giampaolo/psutil/issues/645 - # I really don't know what to do about that except - # forcing the value to 0 or 100. - if field_perc > 100.0: - field_perc = 100.0 - # `<=` because `-0.0 == 0.0` evaluates to True - elif field_perc <= 0.0: - field_perc = 0.0 + # make sure we don't return negative values or values over 100% + field_perc = min(max(0.0, field_perc), 100.0) nums.append(field_perc) return _psplatform.scputimes(*nums) @@ -1899,9 +1901,9 @@ def virtual_memory(): - used: memory used, calculated differently depending on the platform and designed for informational purposes only: - OSX: active + inactive + wired + macOS: active + inactive + wired BSD: active + wired + cached - LINUX: total - free + Linux: total - free - free: memory not being used at all (zeroed) that is readily available; @@ -1919,10 +1921,10 @@ def virtual_memory(): - buffers (BSD, Linux): cache for things like file system metadata. - - cached (BSD, OSX): + - cached (BSD, macOS): cache for various things. - - wired (OSX, BSD): + - wired (macOS, BSD): memory that is marked to always stay in RAM. It is never moved to disk. - shared (BSD): @@ -2011,7 +2013,8 @@ def disk_io_counters(perdisk=False, nowrap=True): On recent Windows versions 'diskperf -y' command may need to be executed first otherwise this function won't find any disk. """ - rawdict = _psplatform.disk_io_counters() + kwargs = dict(perdisk=perdisk) if LINUX else {} + rawdict = _psplatform.disk_io_counters(**kwargs) if not rawdict: return {} if perdisk else None if nowrap: @@ -2047,7 +2050,7 @@ def net_io_counters(pernic=False, nowrap=True): - errout: total number of errors while sending - dropin: total number of incoming packets which were dropped - dropout: total number of outgoing packets which were dropped - (always 0 on OSX and BSD) + (always 0 on macOS and BSD) If *pernic* is True return the same information for every network interface installed on the system as a dictionary @@ -2103,7 +2106,7 @@ def net_connections(kind='inet'): | all | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ - On OSX this function requires root privileges. + On macOS this function requires root privileges. """ return _psplatform.net_connections(kind) @@ -2152,7 +2155,7 @@ def net_if_addrs(): separator = ":" if POSIX else "-" while addr.count(separator) < 5: addr += "%s00" % separator - ret[name].append(_common.snic(fam, addr, mask, broadcast, ptp)) + ret[name].append(_common.snicaddr(fam, addr, mask, broadcast, ptp)) return dict(ret) @@ -2176,7 +2179,7 @@ def net_if_stats(): # ===================================================================== -# Linux +# Linux, macOS if hasattr(_psplatform, "sensors_temperatures"): def sensors_temperatures(fahrenheit=False): @@ -2214,7 +2217,7 @@ if hasattr(_psplatform, "sensors_temperatures"): __all__.append("sensors_temperatures") -# Linux +# Linux, macOS if hasattr(_psplatform, "sensors_fans"): def sensors_fans(): @@ -2227,7 +2230,7 @@ if hasattr(_psplatform, "sensors_fans"): __all__.append("sensors_fans") -# Linux, Windows, FreeBSD, OSX +# Linux, Windows, FreeBSD, macOS if hasattr(_psplatform, "sensors_battery"): def sensors_battery(): @@ -2270,13 +2273,6 @@ def users(): return _psplatform.users() -def set_procfs_path(path): - """Set an alternative path for /proc filesystem on Linux, Solaris - and AIX. This superseds PROCFS_PATH variable which is deprecated. - """ - _psplatform.PROCFS_PATH = path - - # ===================================================================== # --- Windows services # ===================================================================== diff --git a/server/www/packages/packages-windows/x86/psutil/_common.py b/server/www/packages/packages-windows/x86/psutil/_common.py index 870971e..2cc3939 100644 --- a/server/www/packages/packages-windows/x86/psutil/_common.py +++ b/server/www/packages/packages-windows/x86/psutil/_common.py @@ -42,8 +42,8 @@ PY3 = sys.version_info[0] == 3 __all__ = [ # constants - 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'OSX', 'POSIX', 'SUNOS', - 'WINDOWS', + 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', + 'SUNOS', 'WINDOWS', 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', # connection constants 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED', @@ -55,11 +55,11 @@ __all__ = [ 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL', - 'STATUS_WAKING', 'STATUS_ZOMBIE', + 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED', # named tuples 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', - 'sdiskusage', 'snetio', 'snic', 'snicstats', 'sswap', 'suser', + 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser', # utility functions 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', 'parse_environ_block', 'path_exists_strict', 'usage_percent', @@ -75,12 +75,13 @@ __all__ = [ POSIX = os.name == "posix" WINDOWS = os.name == "nt" LINUX = sys.platform.startswith("linux") -OSX = sys.platform.startswith("darwin") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias FREEBSD = sys.platform.startswith("freebsd") OPENBSD = sys.platform.startswith("openbsd") NETBSD = sys.platform.startswith("netbsd") BSD = FREEBSD or OPENBSD or NETBSD -SUNOS = sys.platform.startswith("sunos") or sys.platform.startswith("solaris") +SUNOS = sys.platform.startswith(("sunos", "solaris")) AIX = sys.platform.startswith("aix") @@ -99,10 +100,11 @@ STATUS_ZOMBIE = "zombie" STATUS_DEAD = "dead" STATUS_WAKE_KILL = "wake-kill" STATUS_WAKING = "waking" -STATUS_IDLE = "idle" # FreeBSD, OSX +STATUS_IDLE = "idle" # Linux, macOS, FreeBSD STATUS_LOCKED = "locked" # FreeBSD STATUS_WAITING = "waiting" # FreeBSD STATUS_SUSPENDED = "suspended" # NetBSD +STATUS_PARKED = "parked" # Linux # Process.connections() and psutil.net_connections() CONN_ESTABLISHED = "ESTABLISHED" @@ -182,7 +184,8 @@ suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid']) sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status', 'pid']) # psutil.net_if_addrs() -snic = namedtuple('snic', ['family', 'address', 'netmask', 'broadcast', 'ptp']) +snicaddr = namedtuple('snicaddr', + ['family', 'address', 'netmask', 'broadcast', 'ptp']) # psutil.net_if_stats() snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu']) # psutil.cpu_stats() @@ -195,7 +198,7 @@ shwtemp = namedtuple( 'shwtemp', ['label', 'current', 'high', 'critical']) # psutil.sensors_battery() sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged']) -# psutil.sensors_battery() +# psutil.sensors_fans() sfan = namedtuple('sfan', ['label', 'current']) # --- for Process methods @@ -261,14 +264,14 @@ del AF_INET, AF_UNIX, SOCK_STREAM, SOCK_DGRAM # =================================================================== -def usage_percent(used, total, _round=None): +def usage_percent(used, total, round_=None): """Calculate percentage usage of 'used' against 'total'.""" try: ret = (used / total) * 100 except ZeroDivisionError: ret = 0.0 if isinstance(used, float) or isinstance(total, float) else 0 - if _round is not None: - return round(ret, _round) + if round_ is not None: + return round(ret, round_) else: return ret diff --git a/server/www/packages/packages-windows/x86/psutil/_compat.py b/server/www/packages/packages-windows/x86/psutil/_compat.py index de91638..08aefe4 100644 --- a/server/www/packages/packages-windows/x86/psutil/_compat.py +++ b/server/www/packages/packages-windows/x86/psutil/_compat.py @@ -10,7 +10,7 @@ import os import sys __all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "callable", "lru_cache", "which"] + "lru_cache", "which"] PY3 = sys.version_info[0] == 3 @@ -38,14 +38,6 @@ else: return s -# removed in 3.0, reintroduced in 3.2 -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - # --- stdlib additions diff --git a/server/www/packages/packages-windows/x86/psutil/_exceptions.py b/server/www/packages/packages-windows/x86/psutil/_exceptions.py index c08e6d8..6dbbd28 100644 --- a/server/www/packages/packages-windows/x86/psutil/_exceptions.py +++ b/server/www/packages/packages-windows/x86/psutil/_exceptions.py @@ -39,7 +39,7 @@ class NoSuchProcess(Error): class ZombieProcess(NoSuchProcess): """Exception raised when querying a zombie process. This is - raised on OSX, BSD and Solaris only, and not always: depending + raised on macOS, BSD and Solaris only, and not always: depending on the query the OS may be able to succeed anyway. On Linux all zombie processes are querable (hence this is never raised). Windows doesn't have zombie processes. diff --git a/server/www/packages/packages-windows/x86/psutil/_psaix.py b/server/www/packages/packages-windows/x86/psutil/_psaix.py index 9abc8d1..7ba212d 100644 --- a/server/www/packages/packages-windows/x86/psutil/_psaix.py +++ b/server/www/packages/packages-windows/x86/psutil/_psaix.py @@ -117,7 +117,7 @@ def get_procfs_path(): def virtual_memory(): total, avail, free, pinned, inuse = cext.virtual_mem() - percent = usage_percent((total - avail), total, _round=1) + percent = usage_percent((total - avail), total, round_=1) return svmem(total, avail, percent, inuse, free) @@ -125,7 +125,7 @@ def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" total, free, sin, sout = cext.swap_mem() used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, sin, sout) @@ -269,7 +269,8 @@ def net_if_stats(): stdout, stderr = [x.decode(sys.stdout.encoding) for x in (stdout, stderr)] if p.returncode == 0: - re_result = re.search("Running: (\d+) Mbps.*?(\w+) Duplex", stdout) + re_result = re.search( + r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout) if re_result is not None: speed = int(re_result.group(1)) duplex = re_result.group(2) @@ -534,7 +535,7 @@ class Process(object): for x in (stdout, stderr)] if "no such process" in stderr.lower(): raise NoSuchProcess(self.pid, self._name) - procfiles = re.findall("(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) + procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) retlist = [] for fd, path in procfiles: path = path.strip() diff --git a/server/www/packages/packages-windows/x86/psutil/_psbsd.py b/server/www/packages/packages-windows/x86/psutil/_psbsd.py index 0553401..7f4bcb6 100644 --- a/server/www/packages/packages-windows/x86/psutil/_psbsd.py +++ b/server/www/packages/packages-windows/x86/psutil/_psbsd.py @@ -188,7 +188,7 @@ def virtual_memory(): shared = int(line.split()[1]) * 1024 avail = inactive + cached + free used = active + wired + cached - percent = usage_percent((total - avail), total, _round=1) + percent = usage_percent((total - avail), total, round_=1) return svmem(total, avail, percent, used, free, active, inactive, buffers, cached, shared, wired) @@ -196,7 +196,7 @@ def virtual_memory(): def swap_memory(): """System swap memory as (total, used, free, sin, sout) namedtuple.""" total, used, free, sin, sout = cext.swap_mem() - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, sin, sout) @@ -345,12 +345,18 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) return ret diff --git a/server/www/packages/packages-windows/x86/psutil/_pslinux.py b/server/www/packages/packages-windows/x86/psutil/_pslinux.py index b9b4334..df624de 100644 --- a/server/www/packages/packages-windows/x86/psutil/_pslinux.py +++ b/server/www/packages/packages-windows/x86/psutil/_pslinux.py @@ -68,7 +68,6 @@ __extra__all__ = [ # ===================================================================== -PROCFS_PATH = None POWER_SUPPLY_PATH = "/sys/class/power_supply" HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) HAS_PRLIMIT = hasattr(cext, "linux_prlimit") @@ -89,7 +88,20 @@ BOOT_TIME = None # set later # speedup, see: https://github.com/giampaolo/psutil/issues/708 BIGFILE_BUFFERING = -1 if PY3 else 8192 LITTLE_ENDIAN = sys.byteorder == 'little' -SECTOR_SIZE_FALLBACK = 512 + +# "man iostat" states that sectors are equivalent with blocks and have +# a size of 512 bytes. Despite this value can be queried at runtime +# via /sys/block/{DISK}/queue/hw_sector_size and results may vary +# between 1k, 2k, or 4k... 512 appears to be a magic constant used +# throughout Linux source code: +# * https://stackoverflow.com/a/38136179/376587 +# * https://lists.gt.net/linux/kernel/2241060 +# * https://github.com/giampaolo/psutil/issues/1305 +# * https://github.com/torvalds/linux/blob/ +# 4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99 +# * https://lkml.org/lkml/2015/8/17/234 +DISK_SECTOR_SIZE = 512 + if enum is None: AF_LINK = socket.AF_PACKET else: @@ -112,7 +124,10 @@ else: globals().update(IOPriority.__members__) -# taken from /fs/proc/array.c +# See: +# https://github.com/torvalds/linux/blame/master/fs/proc/array.c +# ...and (TASK_* constants): +# https://github.com/torvalds/linux/blob/master/include/linux/sched.h PROC_STATUSES = { "R": _common.STATUS_RUNNING, "S": _common.STATUS_SLEEPING, @@ -123,7 +138,9 @@ PROC_STATUSES = { "X": _common.STATUS_DEAD, "x": _common.STATUS_DEAD, "K": _common.STATUS_WAKE_KILL, - "W": _common.STATUS_WAKING + "W": _common.STATUS_WAKING, + "I": _common.STATUS_IDLE, + "P": _common.STATUS_PARKED, } # https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h @@ -150,7 +167,7 @@ TCP_STATUSES = { # psutil.virtual_memory() svmem = namedtuple( 'svmem', ['total', 'available', 'percent', 'used', 'free', - 'active', 'inactive', 'buffers', 'cached', 'shared']) + 'active', 'inactive', 'buffers', 'cached', 'shared', 'slab']) # psutil.disk_io_counters() sdiskio = namedtuple( 'sdiskio', ['read_count', 'write_count', @@ -211,36 +228,8 @@ else: def get_procfs_path(): - """Return updated PROCFS_PATH constant. - Return value is cached after 10 calls. - """ - global PROCFS_PATH - - if PROCFS_PATH is not None: - return PROCFS_PATH - - path = sys.modules['psutil'].PROCFS_PATH - if path != "/proc": - msg = \ - "you used `psutil.PROCFS_PATH = %s` somewhere in your code; " \ - "that is deprecated and will be ignored in the future; replace " \ - "it with `set_procfs_path(%r)`" % (path, path) - warnings.warn(msg, category=FutureWarning, stacklevel=2) - PROCFS_PATH = path - - # Cache the value if path remained the same after 10 calls. - # This means that from now on any change to psutil.PROCFS_PATH - # will be ignored. - # This is based on the assumption that it's likely that the user - # does "psutil.PROCFS_PATH" at import time, not later. - get_procfs_path.ncalls += 1 - if get_procfs_path.ncalls >= 10: - PROCFS_PATH = path - - return path - - -get_procfs_path.ncalls = 0 + """Return updated psutil.PROCFS_PATH constant.""" + return sys.modules['psutil'].PROCFS_PATH def readlink(path): @@ -276,17 +265,23 @@ def file_flags_to_mode(flags): return mode -def get_sector_size(partition): - """Return the sector size of a partition. - Used by disk_io_counters(). +def is_storage_device(name): + """Return True if the given name refers to a root device (e.g. + "sda", "nvme0n1") as opposed to a logical partition (e.g. "sda1", + "nvme0n1p1"). If name is a virtual device (e.g. "loop1", "ram") + return True. """ - try: - with open("/sys/block/%s/queue/hw_sector_size" % partition, "rt") as f: - return int(f.read()) - except (IOError, ValueError): - # man iostat states that sectors are equivalent with blocks and - # have a size of 512 bytes since 2.4 kernels. - return SECTOR_SIZE_FALLBACK + # Readapted from iostat source code, see: + # https://github.com/sysstat/sysstat/blob/ + # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208 + # Some devices may have a slash in their name (e.g. cciss/c0d0...). + name = name.replace('/', '!') + including_virtual = True + if including_virtual: + path = "/sys/block/%s" % name + else: + path = "/sys/block/%s/device" % name + return os.access(path, os.F_OK) @memoize @@ -323,7 +318,7 @@ def cat(fname, fallback=_DEFAULT, binary=True): try: with open_binary(fname) if binary else open_text(fname) as f: return f.read().strip() - except IOError: + except (IOError, OSError): if fallback is not _DEFAULT: return fallback else: @@ -470,6 +465,11 @@ def virtual_memory(): inactive = 0 missing_fields.append('inactive') + try: + slab = mems[b"Slab:"] + except KeyError: + slab = 0 + used = total - free - cached - buffers if used < 0: # May be symptomatic of running within a LCX container where such @@ -500,7 +500,7 @@ def virtual_memory(): if avail > total: avail = free - percent = usage_percent((total - avail), total, _round=1) + percent = usage_percent((total - avail), total, round_=1) # Warn about missing metrics which are set to 0. if missing_fields: @@ -510,7 +510,7 @@ def virtual_memory(): warnings.warn(msg, RuntimeWarning) return svmem(total, avail, percent, used, free, - active, inactive, buffers, cached, shared) + active, inactive, buffers, cached, shared, slab) def swap_memory(): @@ -533,7 +533,7 @@ def swap_memory(): free *= unit_multiplier used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) # get pgin/pgouts try: f = open_binary("%s/vmstat" % get_procfs_path()) @@ -1026,10 +1026,16 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext.net_if_duplex_speed(name) - ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) return ret @@ -1041,35 +1047,11 @@ def net_if_stats(): disk_usage = _psposix.disk_usage -def disk_io_counters(): +def disk_io_counters(perdisk=False): """Return disk I/O statistics for every disk installed on the system as a dict of raw tuples. """ - # determine partitions we want to look for - def get_partitions(): - partitions = [] - with open_text("%s/partitions" % get_procfs_path()) as f: - lines = f.readlines()[2:] - for line in reversed(lines): - _, _, _, name = line.split() - if name[-1].isdigit(): - # we're dealing with a partition (e.g. 'sda1'); 'sda' will - # also be around but we want to omit it - partitions.append(name) - else: - if not partitions or not partitions[-1].startswith(name): - # we're dealing with a disk entity for which no - # partitions have been defined (e.g. 'sda' but - # 'sda1' was not around), see: - # https://github.com/giampaolo/psutil/issues/338 - partitions.append(name) - return partitions - - retdict = {} - partitions = get_partitions() - with open_text("%s/diskstats" % get_procfs_path()) as f: - lines = f.readlines() - for line in lines: + def read_procfs(): # OK, this is a bit confusing. The format of /proc/diskstats can # have 3 variations. # On Linux 2.4 each line has always 15 fields, e.g.: @@ -1083,33 +1065,77 @@ def disk_io_counters(): # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats - fields = line.split() - fields_len = len(fields) - if fields_len == 15: - # Linux 2.4 - name = fields[3] - reads = int(fields[2]) - (reads_merged, rbytes, rtime, writes, writes_merged, - wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) - elif fields_len == 14: - # Linux 2.6+, line referring to a disk - name = fields[2] - (reads, reads_merged, rbytes, rtime, writes, writes_merged, - wbytes, wtime, _, busy_time, _) = map(int, fields[3:14]) - elif fields_len == 7: - # Linux 2.6+, line referring to a partition - name = fields[2] - reads, rbytes, writes, wbytes = map(int, fields[3:]) - rtime = wtime = reads_merged = writes_merged = busy_time = 0 - else: - raise ValueError("not sure how to interpret line %r" % line) + with open_text("%s/diskstats" % get_procfs_path()) as f: + lines = f.readlines() + for line in lines: + fields = line.split() + flen = len(fields) + if flen == 15: + # Linux 2.4 + name = fields[3] + reads = int(fields[2]) + (reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) + elif flen == 14: + # Linux 2.6+, line referring to a disk + name = fields[2] + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[3:14]) + elif flen == 7: + # Linux 2.6+, line referring to a partition + name = fields[2] + reads, rbytes, writes, wbytes = map(int, fields[3:]) + rtime = wtime = reads_merged = writes_merged = busy_time = 0 + else: + raise ValueError("not sure how to interpret line %r" % line) + yield (name, reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + + def read_sysfs(): + for block in os.listdir('/sys/block'): + for root, _, files in os.walk(os.path.join('/sys/block', block)): + if 'stat' not in files: + continue + with open_text(os.path.join(root, 'stat')) as f: + fields = f.read().strip().split() + name = os.path.basename(root) + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields) + yield (name, reads, writes, rbytes, wbytes, rtime, + wtime, reads_merged, writes_merged, busy_time) + + if os.path.exists('%s/diskstats' % get_procfs_path()): + gen = read_procfs() + elif os.path.exists('/sys/block'): + gen = read_sysfs() + else: + raise NotImplementedError( + "%s/diskstats nor /sys/block filesystem are available on this " + "system" % get_procfs_path()) + + retdict = {} + for entry in gen: + (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, + writes_merged, busy_time) = entry + if not perdisk and not is_storage_device(name): + # perdisk=False means we want to calculate totals so we skip + # partitions (e.g. 'sda1', 'nvme0n1p1') and only include + # base disk devices (e.g. 'sda', 'nvme0n1'). Base disks + # include a total of all their partitions + some extra size + # of their own: + # $ cat /proc/diskstats + # 259 0 sda 10485760 ... + # 259 1 sda1 5186039 ... + # 259 1 sda2 5082039 ... + # See: + # https://github.com/giampaolo/psutil/pull/1313 + continue + + rbytes *= DISK_SECTOR_SIZE + wbytes *= DISK_SECTOR_SIZE + retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) - if name in partitions: - ssize = get_sector_size(name) - rbytes *= ssize - wbytes *= ssize - retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, - reads_merged, writes_merged, busy_time) return retdict @@ -1169,26 +1195,37 @@ def sensors_temperatures(): for base in basenames: try: - current = float(cat(base + '_input')) / 1000.0 - except (IOError, OSError) as err: + path = base + '_input' + current = float(cat(path)) / 1000.0 + path = os.path.join(os.path.dirname(base), 'name') + unit_name = cat(path, binary=False) + except (IOError, OSError, ValueError) as err: # A lot of things can go wrong here, so let's just skip the - # whole entry. + # whole entry. Sure thing is Linux's /sys/class/hwmon really + # is a stinky broken mess. # https://github.com/giampaolo/psutil/issues/1009 # https://github.com/giampaolo/psutil/issues/1101 # https://github.com/giampaolo/psutil/issues/1129 - warnings.warn("ignoring %r" % err, RuntimeWarning) + # https://github.com/giampaolo/psutil/issues/1245 + # https://github.com/giampaolo/psutil/issues/1323 + warnings.warn("ignoring %r for file %r" % (err, path), + RuntimeWarning) continue - unit_name = cat(os.path.join(os.path.dirname(base), 'name'), - binary=False) high = cat(base + '_max', fallback=None) critical = cat(base + '_crit', fallback=None) label = cat(base + '_label', fallback='', binary=False) if high is not None: - high = float(high) / 1000.0 + try: + high = float(high) / 1000.0 + except ValueError: + high = None if critical is not None: - critical = float(critical) / 1000.0 + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None ret[unit_name].append((label, current, high, critical)) @@ -1246,9 +1283,13 @@ def sensors_battery(): return int(ret) if ret.isdigit() else ret return None - root = os.path.join(POWER_SUPPLY_PATH, "BAT0") - if not os.path.exists(root): + bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT')] + if not bats: return None + # Get the first available battery. Usually this is "BAT0", except + # some rare exceptions: + # https://github.com/giampaolo/psutil/issues/1238 + root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) # Base metrics. energy_now = multi_cat( @@ -1397,9 +1438,8 @@ def ppid_map(): data = f.read() except EnvironmentError as err: # Note: we should be able to access /stat for all processes - # so we won't bump into EPERM, which is good. - if err.errno not in (errno.ENOENT, errno.ESRCH, - errno.EPERM, errno.EACCES): + # aka it's unlikely we'll bump into EPERM, which is good. + if err.errno not in (errno.ENOENT, errno.ESRCH): raise else: rpar = data.rfind(b')') @@ -1632,9 +1672,10 @@ class Process(object): @wrap_exceptions def memory_full_info( self, - _private_re=re.compile(br"Private.*:\s+(\d+)"), - _pss_re=re.compile(br"Pss.*:\s+(\d+)"), - _swap_re=re.compile(br"Swap.*:\s+(\d+)")): + # Gets Private_Clean, Private_Dirty, Private_Hugetlb. + _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), + _pss_re=re.compile(br"\nPss\:\s+(\d+)"), + _swap_re=re.compile(br"\nSwap\:\s+(\d+)")): basic_mem = self.memory_info() # Note: using 3 regexes is faster than reading the file # line by line. diff --git a/server/www/packages/packages-windows/x86/psutil/_psosx.py b/server/www/packages/packages-windows/x86/psutil/_psosx.py index 4c97af7..fbfedf3 100644 --- a/server/www/packages/packages-windows/x86/psutil/_psosx.py +++ b/server/www/packages/packages-windows/x86/psutil/_psosx.py @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""OSX platform implementation.""" +"""macOS platform implementation.""" import contextlib import errno @@ -122,7 +122,7 @@ def virtual_memory(): total, active, inactive, wired, free = cext.virtual_mem() avail = inactive + free used = active + inactive + wired - percent = usage_percent((total - avail), total, _round=1) + percent = usage_percent((total - avail), total, round_=1) return svmem(total, avail, percent, used, free, active, inactive, wired) @@ -130,7 +130,7 @@ def virtual_memory(): def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" total, used, free, sin, sout = cext.swap_mem() - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, sin, sout) @@ -174,7 +174,7 @@ def cpu_stats(): def cpu_freq(): """Return CPU frequency. - On OSX per-cpu frequency is not supported. + On macOS per-cpu frequency is not supported. Also, the returned frequency never changes, see: https://arstechnica.com/civis/viewtopic.php?f=19&t=465002 """ @@ -213,8 +213,7 @@ def disk_partitions(all=False): def sensors_battery(): - """Return battery information. - """ + """Return battery information.""" try: percent, minsleft, power_plugged = cext.sensors_battery() except NotImplementedError: @@ -241,7 +240,7 @@ net_if_addrs = cext_posix.net_if_addrs def net_connections(kind='inet'): """System-wide network connections.""" - # Note: on OSX this will fail with AccessDenied unless + # Note: on macOS this will fail with AccessDenied unless # the process is owned by root. ret = [] for pid in pids(): @@ -262,12 +261,18 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) return ret @@ -304,17 +309,16 @@ def users(): def pids(): ls = cext.pids() if 0 not in ls: - # On certain OSX versions pids() C doesn't return PID 0 but + # On certain macOS versions pids() C doesn't return PID 0 but # "ps" does and the process is querable via sysctl(): # https://travis-ci.org/giampaolo/psutil/jobs/309619941 try: Process(0).create_time() + ls.insert(0, 0) except NoSuchProcess: - return False + pass except AccessDenied: - ls.append(0) - else: - ls.append(0) + ls.insert(0, 0) return ls @@ -335,6 +339,8 @@ def wrap_exceptions(fun): if err.errno in (errno.EPERM, errno.EACCES): raise AccessDenied(self.pid, self._name) raise + except cext.ZombieProcessError: + raise ZombieProcess(self.pid, self._name, self._ppid) return wrapper @@ -558,8 +564,7 @@ class Process(object): @wrap_exceptions def threads(self): - with catch_zombie(self): - rawlist = cext.proc_threads(self.pid) + rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: ntuple = _common.pthread(thread_id, utime, stime) @@ -568,5 +573,4 @@ class Process(object): @wrap_exceptions def memory_maps(self): - with catch_zombie(self): - return cext.proc_memory_maps(self.pid) + return cext.proc_memory_maps(self.pid) diff --git a/server/www/packages/packages-windows/x86/psutil/_psposix.py b/server/www/packages/packages-windows/x86/psutil/_psposix.py index 6bb8444..9c3fac2 100644 --- a/server/www/packages/packages-windows/x86/psutil/_psposix.py +++ b/server/www/packages/packages-windows/x86/psutil/_psposix.py @@ -156,7 +156,7 @@ def disk_usage(path): # User usage percent compared to the total amount of space # the user can use. This number would be higher if compared # to root's because the user has less space (usually -5%). - usage_percent_user = usage_percent(used, total_user, _round=1) + usage_percent_user = usage_percent(used, total_user, round_=1) # NB: the percentage is -5% than what shown by df due to # reserved blocks that we are currently not considering: diff --git a/server/www/packages/packages-windows/x86/psutil/_pssunos.py b/server/www/packages/packages-windows/x86/psutil/_pssunos.py index 5471d5a..e2f33a3 100644 --- a/server/www/packages/packages-windows/x86/psutil/_pssunos.py +++ b/server/www/packages/packages-windows/x86/psutil/_pssunos.py @@ -79,7 +79,11 @@ proc_info_map = dict( nice=4, num_threads=5, status=6, - ttynr=7) + ttynr=7, + uid=8, + euid=9, + gid=10, + egid=11) # ===================================================================== @@ -127,7 +131,7 @@ def virtual_memory(): # note: there's no difference on Solaris free = avail = os.sysconf('SC_AVPHYS_PAGES') * PAGE_SIZE used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return svmem(total, avail, percent, used, free) @@ -159,7 +163,7 @@ def swap_memory(): total += int(int(t) * 512) free += int(int(f) * 512) used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, sin * PAGE_SIZE, sout * PAGE_SIZE) @@ -394,7 +398,10 @@ class Process(object): @memoize_when_activated def _proc_cred(self): - return cext.proc_cred(self.pid, self._procfs_path) + @wrap_exceptions + def proc_cred(self): + return cext.proc_cred(self.pid, self._procfs_path) + return proc_cred(self) @wrap_exceptions def name(self): @@ -432,27 +439,10 @@ class Process(object): @wrap_exceptions def nice_get(self): - # Note #1: for some reason getpriority(3) return ESRCH (no such - # process) for certain low-pid processes, no matter what (even - # as root). - # The process actually exists though, as it has a name, - # creation time, etc. - # The best thing we can do here appears to be raising AD. - # Note: tested on Solaris 11; on Open Solaris 5 everything is - # fine. - # - # Note #2: we also can get niceness from /proc/pid/psinfo - # but it's wrong, see: - # https://github.com/giampaolo/psutil/issues/1082 - try: - return cext_posix.getpriority(self.pid) - except EnvironmentError as err: - # 48 is 'operation not supported' but errno does not expose - # it. It occurs for low system pids. - if err.errno in (errno.ENOENT, errno.ESRCH, 48): - if pid_exists(self.pid): - raise AccessDenied(self.pid, self._name) - raise + # Note #1: getpriority(3) doesn't work for realtime processes. + # Psinfo is what ps uses, see: + # https://github.com/giampaolo/psutil/issues/1194 + return self._proc_basic_info()[proc_info_map['nice']] @wrap_exceptions def nice_set(self, value): @@ -471,12 +461,22 @@ class Process(object): @wrap_exceptions def uids(self): - real, effective, saved, _, _, _ = self._proc_cred() + try: + real, effective, saved, _, _, _ = self._proc_cred() + except AccessDenied: + real = self._proc_basic_info()[proc_info_map['uid']] + effective = self._proc_basic_info()[proc_info_map['euid']] + saved = None return _common.puids(real, effective, saved) @wrap_exceptions def gids(self): - _, _, _, real, effective, saved = self._proc_cred() + try: + _, _, _, real, effective, saved = self._proc_cred() + except AccessDenied: + real = self._proc_basic_info()[proc_info_map['gid']] + effective = self._proc_basic_info()[proc_info_map['egid']] + saved = None return _common.puids(real, effective, saved) @wrap_exceptions diff --git a/server/www/packages/packages-windows/x86/psutil/_psutil_windows.cp37-win32.pyd b/server/www/packages/packages-windows/x86/psutil/_psutil_windows.cp37-win32.pyd new file mode 100644 index 0000000..feb16eb Binary files /dev/null and b/server/www/packages/packages-windows/x86/psutil/_psutil_windows.cp37-win32.pyd differ diff --git a/server/www/packages/packages-windows/x86/psutil/_psutil_windows.pyd b/server/www/packages/packages-windows/x86/psutil/_psutil_windows.pyd deleted file mode 100644 index be0bfa9..0000000 Binary files a/server/www/packages/packages-windows/x86/psutil/_psutil_windows.pyd and /dev/null differ diff --git a/server/www/packages/packages-windows/x86/psutil/_pswindows.py b/server/www/packages/packages-windows/x86/psutil/_pswindows.py index b6c58c9..18651d6 100644 --- a/server/www/packages/packages-windows/x86/psutil/_pswindows.py +++ b/server/www/packages/packages-windows/x86/psutil/_pswindows.py @@ -9,6 +9,7 @@ import errno import functools import os import sys +import time from collections import namedtuple from . import _common @@ -78,7 +79,6 @@ __extra__all__ = [ # ===================================================================== CONN_DELETE_TCB = "DELETE_TCB" -WAIT_TIMEOUT = 0x00000102 # 258 in decimal ACCESS_DENIED_ERRSET = frozenset([errno.EPERM, errno.EACCES, cext.ERROR_ACCESS_DENIED]) NO_SUCH_SERVICE_ERRSET = frozenset([cext.ERROR_INVALID_NAME, @@ -200,7 +200,7 @@ def py2_strencode(s): if isinstance(s, str): return s else: - return s.encode(ENCODING, errors=ENCODING_ERRS) + return s.encode(ENCODING, ENCODING_ERRS) # ===================================================================== @@ -217,7 +217,7 @@ def virtual_memory(): avail = availphys free = availphys used = total - avail - percent = usage_percent((total - avail), total, _round=1) + percent = usage_percent((total - avail), total, round_=1) return svmem(total, avail, percent, used, free) @@ -227,7 +227,7 @@ def swap_memory(): total = mem[2] free = mem[3] used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, 0, 0) @@ -247,7 +247,7 @@ def disk_usage(path): path = path.decode(ENCODING, errors="strict") total, free = cext.disk_usage(path) used = total - free - percent = usage_percent(used, total, _round=1) + percent = usage_percent(used, total, round_=1) return _common.sdiskusage(total, used, free, percent) @@ -288,7 +288,7 @@ def cpu_count_logical(): def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" + """Return the number of physical CPU cores in the system.""" return cext.cpu_count_phys() @@ -792,18 +792,43 @@ class Process(object): if timeout is None: cext_timeout = cext.INFINITE else: - # WaitForSingleObject() expects time in milliseconds + # WaitForSingleObject() expects time in milliseconds. cext_timeout = int(timeout * 1000) + + timer = getattr(time, 'monotonic', time.time) + stop_at = timer() + timeout if timeout is not None else None + + try: + # Exit code is supposed to come from GetExitCodeProcess(). + # May also be None if OpenProcess() failed with + # ERROR_INVALID_PARAMETER, meaning PID is already gone. + exit_code = cext.proc_wait(self.pid, cext_timeout) + except cext.TimeoutExpired: + # WaitForSingleObject() returned WAIT_TIMEOUT. Just raise. + raise TimeoutExpired(timeout, self.pid, self._name) + except cext.TimeoutAbandoned: + # WaitForSingleObject() returned WAIT_ABANDONED, see: + # https://github.com/giampaolo/psutil/issues/1224 + # We'll just rely on the internal polling and return None + # when the PID disappears. Subprocess module does the same + # (return None): + # https://github.com/python/cpython/blob/ + # be50a7b627d0aa37e08fa8e2d5568891f19903ce/ + # Lib/subprocess.py#L1193-L1194 + exit_code = None + + # At this point WaitForSingleObject() returned WAIT_OBJECT_0, + # meaning the process is gone. Stupidly there are cases where + # its PID may still stick around so we do a further internal + # polling. + delay = 0.0001 while True: - ret = cext.proc_wait(self.pid, cext_timeout) - if ret == WAIT_TIMEOUT: - raise TimeoutExpired(timeout, self.pid, self._name) - if pid_exists(self.pid): - if timeout is None: - continue - else: - raise TimeoutExpired(timeout, self.pid, self._name) - return ret + if not pid_exists(self.pid): + return exit_code + if stop_at and timer() >= stop_at: + raise TimeoutExpired(timeout, pid=self.pid, name=self._name) + time.sleep(delay) + delay = min(delay * 2, 0.04) # incremental delay @wrap_exceptions def username(self): diff --git a/server/www/teleport/.idea/teleport.iml b/server/www/teleport/.idea/teleport.iml index d58057b..0059564 100644 --- a/server/www/teleport/.idea/teleport.iml +++ b/server/www/teleport/.idea/teleport.iml @@ -7,7 +7,7 @@ - +