mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-06 21:40:09 +03:00
Merge from master
This commit is contained in:
commit
e891c63096
|
@ -16,7 +16,7 @@ python:
|
|||
install:
|
||||
- "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake"
|
||||
- "pip install cffi"
|
||||
- "pip install coveralls nose nose-cov"
|
||||
- "pip install coveralls nose nose-cov pyroma"
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi
|
||||
|
||||
# webp
|
||||
|
|
15
CHANGES.rst
15
CHANGES.rst
|
@ -4,6 +4,21 @@ Changelog (Pillow)
|
|||
2.5.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Support for Resolution in BMP files #734
|
||||
[gcq]
|
||||
|
||||
- Fix error in setup.py for Python 3
|
||||
[matthew-brett]
|
||||
|
||||
- Pyroma fix and add Python 3.4 to setup metadata #742
|
||||
[wirefool]
|
||||
|
||||
- Top level flake8 fixes #741
|
||||
[aclark]
|
||||
|
||||
- Remove obsolete Animated Raster Graphics (ARG) support
|
||||
[hugovk]
|
||||
|
||||
- Fix test_imagedraw failures #727
|
||||
[cgohlke]
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ include *.c
|
|||
include *.h
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.txt
|
||||
include .coveragerc
|
||||
include .gitattributes
|
||||
include .travis.yml
|
||||
|
@ -54,6 +55,7 @@ recursive-include Tests *.png
|
|||
recursive-include Tests *.ppm
|
||||
recursive-include Tests *.psd
|
||||
recursive-include Tests *.py
|
||||
recursive-include Tests *.rst
|
||||
recursive-include Tests *.spider
|
||||
recursive-include Tests *.tar
|
||||
recursive-include Tests *.tif
|
||||
|
@ -66,6 +68,7 @@ recursive-include Tk *.c
|
|||
recursive-include Tk *.txt
|
||||
recursive-include Tk *.rst
|
||||
recursive-include depends *.sh
|
||||
recursive-include depends *.rst
|
||||
recursive-include docs *.bat
|
||||
recursive-include docs *.gitignore
|
||||
recursive-include docs *.html
|
||||
|
|
5
Makefile
5
Makefile
|
@ -1,8 +1,13 @@
|
|||
|
||||
|
||||
pre:
|
||||
virtualenv .
|
||||
bin/pip install -r requirements.txt
|
||||
bin/python setup.py develop
|
||||
bin/python selftest.py
|
||||
bin/nosetests Tests/test_*.py
|
||||
bin/python setup.py install
|
||||
bin/python test-installed.py
|
||||
check-manifest
|
||||
pyroma .
|
||||
viewdoc
|
||||
|
|
|
@ -1,506 +0,0 @@
|
|||
#
|
||||
# THIS IS WORK IN PROGRESS
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# ARG animation support code
|
||||
#
|
||||
# history:
|
||||
# 1996-12-30 fl Created
|
||||
# 1996-01-06 fl Added safe scripting environment
|
||||
# 1996-01-10 fl Added JHDR, UHDR and sYNC support
|
||||
# 2005-03-02 fl Removed AAPP and ARUN support
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1996-97.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
__version__ = "0.4"
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette
|
||||
|
||||
from PIL.PngImagePlugin import i8, i16, i32, ChunkStream, _MODES
|
||||
|
||||
MAGIC = b"\212ARG\r\n\032\n"
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# ARG parser
|
||||
|
||||
class ArgStream(ChunkStream):
|
||||
"Parser callbacks for ARG data"
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
ChunkStream.__init__(self, fp)
|
||||
|
||||
self.eof = 0
|
||||
|
||||
self.im = None
|
||||
self.palette = None
|
||||
|
||||
self.__reset()
|
||||
|
||||
def __reset(self):
|
||||
|
||||
# reset decoder state (called on init and sync)
|
||||
|
||||
self.count = 0
|
||||
self.id = None
|
||||
self.action = ("NONE",)
|
||||
|
||||
self.images = {}
|
||||
self.names = {}
|
||||
|
||||
|
||||
def chunk_AHDR(self, offset, bytes):
|
||||
"AHDR -- animation header"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced AHDR chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
self.size = i32(s), i32(s[4:])
|
||||
try:
|
||||
self.mode, self.rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
||||
except:
|
||||
raise SyntaxError("unknown ARG mode")
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AHDR size", self.size)
|
||||
print("AHDR mode", self.mode, self.rawmode)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_AFRM(self, offset, bytes):
|
||||
"AFRM -- next frame follows"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced AFRM chunk")
|
||||
|
||||
self.show = 1
|
||||
self.id = 0
|
||||
self.count = 1
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if len(s) >= 2:
|
||||
self.id = i16(s)
|
||||
if len(s) >= 4:
|
||||
self.count = i16(s[2:4])
|
||||
if len(s) >= 6:
|
||||
self.repair = i16(s[4:6])
|
||||
else:
|
||||
self.repair = None
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AFRM", self.id, self.count)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_ADEF(self, offset, bytes):
|
||||
"ADEF -- store image"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced ADEF chunk")
|
||||
|
||||
self.show = 0
|
||||
self.id = 0
|
||||
self.count = 1
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if len(s) >= 2:
|
||||
self.id = i16(s)
|
||||
if len(s) >= 4:
|
||||
self.count = i16(s[2:4])
|
||||
|
||||
if Image.DEBUG:
|
||||
print("ADEF", self.id, self.count)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_NAME(self, offset, bytes):
|
||||
"NAME -- name the current image"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced NAME chunk")
|
||||
|
||||
name = self.fp.read(bytes)
|
||||
self.names[self.id] = name
|
||||
|
||||
return name
|
||||
|
||||
def chunk_AEND(self, offset, bytes):
|
||||
"AEND -- end of animation"
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AEND")
|
||||
|
||||
self.eof = 1
|
||||
|
||||
raise EOFError("end of ARG file")
|
||||
|
||||
def __getmodesize(self, s, full=1):
|
||||
|
||||
size = i32(s), i32(s[4:])
|
||||
|
||||
try:
|
||||
mode, rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
||||
except:
|
||||
raise SyntaxError("unknown image mode")
|
||||
|
||||
if full:
|
||||
if i8(s[12]):
|
||||
pass # interlace not yet supported
|
||||
if i8(s[11]):
|
||||
raise SyntaxError("unknown filter category")
|
||||
|
||||
return size, mode, rawmode
|
||||
|
||||
def chunk_PAST(self, offset, bytes):
|
||||
"PAST -- paste one image into another"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced PAST chunk")
|
||||
|
||||
if self.repair is not None:
|
||||
# we must repair the target image before we
|
||||
# start pasting
|
||||
|
||||
# brute force; a better solution would be to
|
||||
# update only the dirty rectangles in images[id].
|
||||
# note that if images[id] doesn't exist, it must
|
||||
# be created
|
||||
|
||||
self.images[self.id] = self.images[self.repair].copy()
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
im = self.images[i16(s)]
|
||||
x, y = i32(s[2:6]), i32(s[6:10])
|
||||
bbox = x, y, im.size[0]+x, im.size[1]+y
|
||||
|
||||
if im.mode in ["RGBA"]:
|
||||
# paste with transparency
|
||||
# FIXME: should handle P+transparency as well
|
||||
self.images[self.id].paste(im, bbox, im)
|
||||
else:
|
||||
# paste without transparency
|
||||
self.images[self.id].paste(im, bbox)
|
||||
|
||||
self.action = ("PAST",)
|
||||
self.__store()
|
||||
|
||||
return s
|
||||
|
||||
def chunk_BLNK(self, offset, bytes):
|
||||
"BLNK -- create blank image"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced BLNK chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# store image (FIXME: handle colour)
|
||||
self.action = ("BLNK",)
|
||||
self.im = Image.core.fill(mode, size, 0)
|
||||
self.__store()
|
||||
|
||||
return s
|
||||
|
||||
def chunk_IHDR(self, offset, bytes):
|
||||
"IHDR -- full image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced IHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("IHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.zip_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_DHDR(self, offset, bytes):
|
||||
"DHDR -- delta image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced DHDR chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
|
||||
size, mode, rawmode = self.__getmodesize(s)
|
||||
|
||||
# delta header
|
||||
diff = i8(s[13])
|
||||
offs = i32(s[14:18]), i32(s[18:22])
|
||||
|
||||
bbox = offs + (offs[0]+size[0], offs[1]+size[1])
|
||||
|
||||
if Image.DEBUG:
|
||||
print("DHDR", diff, bbox)
|
||||
|
||||
# FIXME: decode and apply image
|
||||
self.action = ("DHDR", diff, bbox)
|
||||
|
||||
# setup decoder
|
||||
self.im = Image.core.new(mode, size)
|
||||
|
||||
self.decoder = Image.core.zip_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_JHDR(self, offset, bytes):
|
||||
"JHDR -- JPEG image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced JHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("JHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.jpeg_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_UHDR(self, offset, bytes):
|
||||
"UHDR -- uncompressed image data follows (EXPERIMENTAL)"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced UHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("UHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.raw_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_IDAT(self, offset, bytes):
|
||||
"IDAT -- image data block"
|
||||
|
||||
# pass compressed chunks through the decoder
|
||||
s = self.fp.read(bytes)
|
||||
self.data = self.data + s
|
||||
n, e = self.decoder.decode(self.data)
|
||||
if n < 0:
|
||||
# end of image
|
||||
if e < 0:
|
||||
raise IOError("decoder error %d" % e)
|
||||
else:
|
||||
self.data = self.data[n:]
|
||||
|
||||
return s
|
||||
|
||||
def chunk_DEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_JEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_UEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_IEND(self, offset, bytes):
|
||||
"IEND -- end of image"
|
||||
|
||||
# we now have a new image. carry out the operation
|
||||
# defined by the image header.
|
||||
|
||||
# won't need these anymore
|
||||
del self.decoder
|
||||
del self.data
|
||||
|
||||
self.__store()
|
||||
|
||||
return self.fp.read(bytes)
|
||||
|
||||
def __store(self):
|
||||
|
||||
# apply operation
|
||||
cid = self.action[0]
|
||||
|
||||
if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]:
|
||||
# store
|
||||
self.images[self.id] = self.im
|
||||
|
||||
elif cid == "DHDR":
|
||||
# paste
|
||||
cid, mode, bbox = self.action
|
||||
im0 = self.images[self.id]
|
||||
im1 = self.im
|
||||
if mode == 0:
|
||||
im1 = im1.chop_add_modulo(im0.crop(bbox))
|
||||
im0.paste(im1, bbox)
|
||||
|
||||
self.count -= 1
|
||||
|
||||
if self.count == 0 and self.show:
|
||||
self.im = self.images[self.id]
|
||||
raise EOFError # end of this frame
|
||||
|
||||
def chunk_PLTE(self, offset, bytes):
|
||||
"PLTE -- palette data"
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if self.mode == "P":
|
||||
self.palette = ImagePalette.raw("RGB", s)
|
||||
return s
|
||||
|
||||
def chunk_sYNC(self, offset, bytes):
|
||||
"SYNC -- reset decoder"
|
||||
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced sYNC chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
self.__reset()
|
||||
return s
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# ARG reader
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == MAGIC
|
||||
|
||||
##
|
||||
# Image plugin for the experimental Animated Raster Graphics format.
|
||||
|
||||
class ArgImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "ARG"
|
||||
format_description = "Animated raster graphics"
|
||||
|
||||
def _open(self):
|
||||
|
||||
if Image.warnings:
|
||||
Image.warnings.warn(
|
||||
"The ArgImagePlugin driver is obsolete, and will be removed "
|
||||
"from a future release of PIL. If you rely on this module, "
|
||||
"please contact the PIL authors.",
|
||||
RuntimeWarning
|
||||
)
|
||||
|
||||
if self.fp.read(8) != MAGIC:
|
||||
raise SyntaxError("not an ARG file")
|
||||
|
||||
self.arg = ArgStream(self.fp)
|
||||
|
||||
# read and process the first chunk (AHDR)
|
||||
|
||||
cid, offset, bytes = self.arg.read()
|
||||
|
||||
if cid != "AHDR":
|
||||
raise SyntaxError("expected an AHDR chunk")
|
||||
|
||||
s = self.arg.call(cid, offset, bytes)
|
||||
|
||||
self.arg.crc(cid, s)
|
||||
|
||||
# image characteristics
|
||||
self.mode = self.arg.mode
|
||||
self.size = self.arg.size
|
||||
|
||||
def load(self):
|
||||
|
||||
if self.arg.im is None:
|
||||
self.seek(0)
|
||||
|
||||
# image data
|
||||
self.im = self.arg.im
|
||||
self.palette = self.arg.palette
|
||||
|
||||
# set things up for further processing
|
||||
Image.Image.load(self)
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
if self.arg.eof:
|
||||
raise EOFError("end of animation")
|
||||
|
||||
self.fp = self.arg.fp
|
||||
|
||||
while True:
|
||||
|
||||
#
|
||||
# process chunks
|
||||
|
||||
cid, offset, bytes = self.arg.read()
|
||||
|
||||
if self.arg.eof:
|
||||
raise EOFError("end of animation")
|
||||
|
||||
try:
|
||||
s = self.arg.call(cid, offset, bytes)
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
except "glurk": # AttributeError
|
||||
if Image.DEBUG:
|
||||
print(cid, bytes, "(unknown)")
|
||||
s = self.fp.read(bytes)
|
||||
|
||||
self.arg.crc(cid, s)
|
||||
|
||||
self.fp.read(4) # ship extra CRC
|
||||
|
||||
def tell(self):
|
||||
return 0
|
||||
|
||||
def verify(self):
|
||||
"Verify ARG file"
|
||||
|
||||
# back up to first chunk
|
||||
self.fp.seek(8)
|
||||
|
||||
self.arg.verify(self)
|
||||
self.arg.close()
|
||||
|
||||
self.fp = None
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open("ARG", ArgImageFile, _accept)
|
||||
|
||||
Image.register_extension("ARG", ".arg")
|
||||
|
||||
Image.register_mime("ARG", "video/x-arg")
|
|
@ -28,6 +28,7 @@ __version__ = "0.7"
|
|||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
import math
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
|
@ -88,6 +89,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
bits = i16(s[14:])
|
||||
self.size = i32(s[4:]), i32(s[8:])
|
||||
compression = i32(s[16:])
|
||||
pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter
|
||||
lutsize = 4
|
||||
colors = i32(s[32:])
|
||||
direction = -1
|
||||
|
@ -95,6 +97,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
# upside-down storage
|
||||
self.size = self.size[0], 2**32 - self.size[1]
|
||||
direction = 0
|
||||
|
||||
self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), pxperm))
|
||||
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" % len(s))
|
||||
|
@ -203,30 +207,37 @@ def _save(im, fp, filename, check=0):
|
|||
if check:
|
||||
return check
|
||||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (96, 96))
|
||||
|
||||
# 1 meter == 39.3701 inches
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
|
||||
|
||||
stride = ((im.size[0]*bits+7)//8+3)&(~3)
|
||||
header = 40 # or 64 for OS/2 version 2
|
||||
offset = 14 + header + colors * 4
|
||||
image = stride * im.size[1]
|
||||
|
||||
# bitmap header
|
||||
fp.write(b"BM" + # file type (magic)
|
||||
o32(offset+image) + # file size
|
||||
o32(0) + # reserved
|
||||
o32(offset)) # image data offset
|
||||
fp.write(b"BM" + # file type (magic)
|
||||
o32(offset+image) + # file size
|
||||
o32(0) + # reserved
|
||||
o32(offset)) # image data offset
|
||||
|
||||
# bitmap info header
|
||||
fp.write(o32(header) + # info header size
|
||||
o32(im.size[0]) + # width
|
||||
o32(im.size[1]) + # height
|
||||
o16(1) + # planes
|
||||
o16(bits) + # depth
|
||||
o32(0) + # compression (0=uncompressed)
|
||||
o32(image) + # size of bitmap
|
||||
o32(1) + o32(1) + # resolution
|
||||
o32(colors) + # colors used
|
||||
o32(colors)) # colors important
|
||||
fp.write(o32(header) + # info header size
|
||||
o32(im.size[0]) + # width
|
||||
o32(im.size[1]) + # height
|
||||
o16(1) + # planes
|
||||
o16(bits) + # depth
|
||||
o32(0) + # compression (0=uncompressed)
|
||||
o32(image) + # size of bitmap
|
||||
o32(ppm[0]) + o32(ppm[1]) + # resolution
|
||||
o32(colors) + # colors used
|
||||
o32(colors)) # colors important
|
||||
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
|
||||
if im.mode == "1":
|
||||
for i in (0, 255):
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
VERSION = '1.1.7' # PIL version
|
||||
PILLOW_VERSION = '2.4.0' # Pillow
|
||||
|
||||
_plugins = ['ArgImagePlugin',
|
||||
'BmpImagePlugin',
|
||||
_plugins = ['BmpImagePlugin',
|
||||
'BufrStubImagePlugin',
|
||||
'CurImagePlugin',
|
||||
'DcxImagePlugin',
|
||||
|
|
|
@ -18,25 +18,6 @@ import sys
|
|||
Image.DEBUG = 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# experimental: support ARG animation scripts
|
||||
|
||||
import ArgImagePlugin
|
||||
|
||||
def applet_hook(animation, images):
|
||||
app = animation(animation_display, images)
|
||||
app.run()
|
||||
|
||||
ArgImagePlugin.APPLET_HOOK = applet_hook
|
||||
|
||||
class AppletDisplay:
|
||||
def __init__(self, ui):
|
||||
self.__ui = ui
|
||||
def paste(self, im, bbox):
|
||||
self.__ui.image.paste(im, bbox)
|
||||
def update(self):
|
||||
self.__ui.update_idletasks()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# an image animation player
|
||||
|
||||
|
@ -56,10 +37,6 @@ class UI(Label):
|
|||
else:
|
||||
self.image = ImageTk.PhotoImage(im)
|
||||
|
||||
# APPLET SUPPORT (very crude, and not 100% safe)
|
||||
global animation_display
|
||||
animation_display = AppletDisplay(self)
|
||||
|
||||
Label.__init__(self, master, image=self.image, bg="black", bd=0)
|
||||
|
||||
self.update()
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
Pillow test files.
|
||||
|
||||
Test scripts are named `test_xxx.py` and use the `unittest` module. A base class and helper functions can be found in `helper.py`.
|
||||
|
||||
Run the tests from the root of the Pillow source distribution:
|
||||
|
||||
python selftest.py
|
||||
nosetests Tests/test_*.py
|
||||
|
||||
Or with coverage:
|
||||
|
||||
coverage run --append --include=PIL/* selftest.py
|
||||
coverage run --append --include=PIL/* -m nose Tests/test_*.py
|
||||
coverage report
|
||||
coverage html
|
||||
open htmlcov/index.html
|
||||
|
||||
To run an individual test:
|
||||
|
||||
python Tests/test_image.py
|
24
Tests/README.rst
Normal file
24
Tests/README.rst
Normal file
|
@ -0,0 +1,24 @@
|
|||
Pillow Tests
|
||||
============
|
||||
|
||||
Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``.
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
||||
Run the tests from the root of the Pillow source distribution::
|
||||
|
||||
python selftest.py
|
||||
nosetests Tests/test_*.py
|
||||
|
||||
Or with coverage::
|
||||
|
||||
coverage run --append --include=PIL/* selftest.py
|
||||
coverage run --append --include=PIL/* -m nose Tests/test_*.py
|
||||
coverage report
|
||||
coverage html
|
||||
open htmlcov/index.html
|
||||
|
||||
To run an individual test::
|
||||
|
||||
python Tests/test_image.py
|
|
@ -1,222 +0,0 @@
|
|||
# PyCMSTests.py
|
||||
# Examples of how to use pyCMS, as well as tests to verify it works properly
|
||||
# By Kevin Cazabon (kevin@cazabon.com)
|
||||
|
||||
# Imports
|
||||
import os
|
||||
from PIL import Image
|
||||
from PIL import ImageCms
|
||||
|
||||
# import PyCMSError separately so we can catch it
|
||||
PyCMSError = ImageCms.PyCMSError
|
||||
|
||||
#######################################################################
|
||||
# Configuration:
|
||||
#######################################################################
|
||||
# set this to the image you want to test with
|
||||
IMAGE = "c:\\temp\\test.tif"
|
||||
|
||||
# set this to where you want to save the output images
|
||||
OUTPUTDIR = "c:\\temp\\"
|
||||
|
||||
# set these to two different ICC profiles, one for input, one for output
|
||||
# set the corresponding mode to the proper PIL mode for that profile
|
||||
INPUT_PROFILE = "c:\\temp\\profiles\\sRGB.icm"
|
||||
INMODE = "RGB"
|
||||
|
||||
OUTPUT_PROFILE = "c:\\temp\\profiles\\genericRGB.icm"
|
||||
OUTMODE = "RGB"
|
||||
|
||||
PROOF_PROFILE = "c:\\temp\\profiles\\monitor.icm"
|
||||
|
||||
# set to True to show() images, False to save them into OUTPUT_DIRECTORY
|
||||
SHOW = False
|
||||
|
||||
# Tests you can enable/disable
|
||||
TEST_error_catching = True
|
||||
TEST_profileToProfile = True
|
||||
TEST_profileToProfile_inPlace = True
|
||||
TEST_buildTransform = True
|
||||
TEST_buildTransformFromOpenProfiles = True
|
||||
TEST_buildProofTransform = True
|
||||
TEST_getProfileInfo = True
|
||||
TEST_misc = False
|
||||
|
||||
#######################################################################
|
||||
# helper functions
|
||||
#######################################################################
|
||||
def outputImage(im, funcName = None):
|
||||
# save or display the image, depending on value of SHOW_IMAGES
|
||||
if SHOW:
|
||||
im.show()
|
||||
else:
|
||||
im.save(os.path.join(OUTPUTDIR, "%s.tif" %funcName))
|
||||
|
||||
|
||||
#######################################################################
|
||||
# The tests themselves
|
||||
#######################################################################
|
||||
|
||||
if TEST_error_catching:
|
||||
im = Image.open(IMAGE)
|
||||
try:
|
||||
#neither of these proifles exists (unless you make them), so we should
|
||||
# get an error
|
||||
imOut = ImageCms.profileToProfile(im, "missingProfile.icm", "cmyk.icm")
|
||||
|
||||
except PyCMSError as reason:
|
||||
print("We caught a PyCMSError: %s\n\n" %reason)
|
||||
|
||||
print("error catching test completed successfully (if you see the message \
|
||||
above that we caught the error).")
|
||||
|
||||
if TEST_profileToProfile:
|
||||
# open the image file using the standard PIL function Image.open()
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# send the image, input/output profiles, and rendering intent to
|
||||
# ImageCms.profileToProfile()
|
||||
imOut = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
outputMode = OUTMODE)
|
||||
|
||||
# now that the image is converted, save or display it
|
||||
outputImage(imOut, "profileToProfile")
|
||||
|
||||
print("profileToProfile test completed successfully.")
|
||||
|
||||
if TEST_profileToProfile_inPlace:
|
||||
# we'll do the same test as profileToProfile, but modify im in place
|
||||
# instead of getting a new image returned to us
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# send the image to ImageCms.profileToProfile(), specifying inPlace = True
|
||||
result = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
outputMode = OUTMODE, inPlace = True)
|
||||
|
||||
# now that the image is converted, save or display it
|
||||
if result is None:
|
||||
# this is the normal result when modifying in-place
|
||||
outputImage(im, "profileToProfile_inPlace")
|
||||
else:
|
||||
# something failed...
|
||||
print("profileToProfile in-place failed: %s" %result)
|
||||
|
||||
print("profileToProfile in-place test completed successfully.")
|
||||
|
||||
if TEST_buildTransform:
|
||||
# make a transform using the input and output profile path strings
|
||||
transform = ImageCms.buildTransform(INPUT_PROFILE, OUTPUT_PROFILE, INMODE, \
|
||||
OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildTransform")
|
||||
|
||||
# then transform it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildTransform_inPlace")
|
||||
|
||||
print("buildTransform test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the transform structure.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(transform)
|
||||
|
||||
if TEST_buildTransformFromOpenProfiles:
|
||||
# we'll actually test a couple profile open/creation functions here too
|
||||
|
||||
# first, get a handle to an input profile, in this case we'll create
|
||||
# an sRGB profile on the fly:
|
||||
inputProfile = ImageCms.createProfile("sRGB")
|
||||
|
||||
# then, get a handle to the output profile
|
||||
outputProfile = ImageCms.getOpenProfile(OUTPUT_PROFILE)
|
||||
|
||||
# make a transform from these
|
||||
transform = ImageCms.buildTransformFromOpenProfiles(inputProfile, \
|
||||
outputProfile, INMODE, OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildTransformFromOpenProfiles")
|
||||
|
||||
# then do it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildTransformFromOpenProfiles_inPlace")
|
||||
|
||||
print("buildTransformFromOpenProfiles test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the each item.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(inputProfile)
|
||||
del(outputProfile)
|
||||
del(transform)
|
||||
|
||||
if TEST_buildProofTransform:
|
||||
# make a transform using the input and output and proof profile path
|
||||
# strings
|
||||
# images converted with this transform will simulate the appearance
|
||||
# of the output device while actually being displayed/proofed on the
|
||||
# proof device. This usually means a monitor, but can also mean
|
||||
# other proof-printers like dye-sub, etc.
|
||||
transform = ImageCms.buildProofTransform(INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
PROOF_PROFILE, INMODE, OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildProofTransform")
|
||||
|
||||
# then transform it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildProofTransform_inPlace")
|
||||
|
||||
print("buildProofTransform test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the transform structure.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(transform)
|
||||
|
||||
if TEST_getProfileInfo:
|
||||
# get a profile handle
|
||||
profile = ImageCms.getOpenProfile(INPUT_PROFILE)
|
||||
|
||||
# lets print some info about our input profile:
|
||||
print("Profile name (retrieved from profile string path name): %s" %ImageCms.getProfileName(INPUT_PROFILE))
|
||||
|
||||
# or, you could do the same thing using a profile handle as the arg
|
||||
print("Profile name (retrieved from profile handle): %s" %ImageCms.getProfileName(profile))
|
||||
|
||||
# now lets get the embedded "info" tag contents
|
||||
# once again, you can use a path to a profile, or a profile handle
|
||||
print("Profile info (retrieved from profile handle): %s" %ImageCms.getProfileInfo(profile))
|
||||
|
||||
# and what's the default intent of this profile?
|
||||
print("The default intent is (this will be an integer): %d" %(ImageCms.getDefaultIntent(profile)))
|
||||
|
||||
# Hmmmm... but does this profile support INTENT_ABSOLUTE_COLORIMETRIC?
|
||||
print("Does it support INTENT_ABSOLUTE_COLORIMETRIC?: (1 is yes, -1 is no): %s" \
|
||||
%ImageCms.isIntentSupported(profile, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, \
|
||||
ImageCms.DIRECTION_INPUT))
|
||||
|
||||
print("getProfileInfo test completed successfully.")
|
||||
|
||||
if TEST_misc:
|
||||
# test the versions, about, and copyright functions
|
||||
print("Versions: %s" %str(ImageCms.versions()))
|
||||
print("About:\n\n%s" %ImageCms.about())
|
||||
print("Copyright:\n\n%s" %ImageCms.copyright())
|
||||
|
||||
print("misc test completed successfully.")
|
||||
|
|
@ -37,6 +37,18 @@ class TestFileBmp(PillowTestCase):
|
|||
self.assertEqual(im.size, reloaded.size)
|
||||
self.assertEqual(reloaded.format, "BMP")
|
||||
|
||||
def test_dpi(self):
|
||||
dpi = (72, 72)
|
||||
|
||||
output = io.BytesIO()
|
||||
im = lena()
|
||||
im.save(output, "BMP", dpi=dpi)
|
||||
|
||||
output.seek(0)
|
||||
reloaded = Image.open(output)
|
||||
|
||||
self.assertEqual(reloaded.info["dpi"], dpi)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
33
Tests/test_pyroma.py
Normal file
33
Tests/test_pyroma.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import unittest
|
||||
|
||||
try:
|
||||
import pyroma
|
||||
except ImportError:
|
||||
# Skip via setUp()
|
||||
pass
|
||||
|
||||
|
||||
class TestPyroma(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
try:
|
||||
import pyroma
|
||||
except ImportError:
|
||||
self.skipTest("ImportError")
|
||||
|
||||
def test_pyroma(self):
|
||||
# Arrange
|
||||
data = pyroma.projectdata.get_data(".")
|
||||
|
||||
# Act
|
||||
rating = pyroma.ratings.rate(data)
|
||||
|
||||
# Assert
|
||||
# Should have a perfect score
|
||||
self.assertEqual(rating, (10, []))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
# End of file
|
4
depends/README.rst
Normal file
4
depends/README.rst
Normal file
|
@ -0,0 +1,4 @@
|
|||
Depends
|
||||
=======
|
||||
|
||||
Scripts in this directory can be used to download, build & install non-packaged dependencies; useful for testing with Travis CI.
|
|
@ -1,14 +1,6 @@
|
|||
Plugin reference
|
||||
================
|
||||
|
||||
:mod:`ArgImagePlugin` Module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: PIL.ArgImagePlugin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`BmpImagePlugin` Module
|
||||
----------------------------
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ Functions
|
|||
|
||||
.. autofunction:: open
|
||||
|
||||
.. warning:: > To protect against potential DOS attacks caused by "[decompression bombs](https://en.wikipedia.org/wiki/Zip_bomb)" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain limit. If desired, the warning can be turned into an error with `warnings.simplefilter('error', Image.DecompressionBombWarning)` or suppressed entirely with `warnings.simplefilter('ignore', Image.DecompressionBombWarning)`. See also [the logging documentation](https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module) to have warnings output to the logging facility instead of stderr.
|
||||
|
||||
Image processing
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from multiprocessing import Pool, cpu_count
|
|||
from distutils.ccompiler import CCompiler
|
||||
import os
|
||||
|
||||
|
||||
# hideous monkeypatching. but. but. but.
|
||||
def _mp_compile_one(tp):
|
||||
(self, obj, build, cc_args, extra_postargs, pp_opts) = tp
|
||||
|
@ -15,21 +16,20 @@ def _mp_compile_one(tp):
|
|||
self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
|
||||
return
|
||||
|
||||
|
||||
def _mp_compile(self, sources, output_dir=None, macros=None,
|
||||
include_dirs=None, debug=0, extra_preargs=None,
|
||||
extra_postargs=None, depends=None):
|
||||
"""Compile one or more source files.
|
||||
|
||||
|
||||
see distutils.ccompiler.CCompiler.compile for comments.
|
||||
"""
|
||||
# A concrete compiler class can either override this method
|
||||
# entirely or implement _compile().
|
||||
|
||||
macros, objects, extra_postargs, pp_opts, build = \
|
||||
self._setup_compile(output_dir, macros, include_dirs, sources,
|
||||
depends, extra_postargs)
|
||||
cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
|
||||
|
||||
macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
|
||||
output_dir, macros, include_dirs, sources, depends, extra_postargs)
|
||||
cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
|
||||
|
||||
try:
|
||||
max_procs = int(os.environ.get('MAX_CONCURRENCY', cpu_count()))
|
||||
|
@ -38,10 +38,12 @@ def _mp_compile(self, sources, output_dir=None, macros=None,
|
|||
pool = Pool(max_procs)
|
||||
try:
|
||||
print ("Building using %d processes" % pool._processes)
|
||||
except: pass
|
||||
arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) for obj in objects]
|
||||
results = pool.map_async(_mp_compile_one,arr)
|
||||
|
||||
except:
|
||||
pass
|
||||
arr = [
|
||||
(self, obj, build, cc_args, extra_postargs, pp_opts) for obj in objects
|
||||
]
|
||||
pool.map_async(_mp_compile_one, arr)
|
||||
pool.close()
|
||||
pool.join()
|
||||
# Return *all* object filenames, not just the ones we just built.
|
||||
|
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Testing reqs
|
||||
-e .
|
||||
nose
|
86
setup.py
86
setup.py
|
@ -14,8 +14,6 @@ import re
|
|||
import struct
|
||||
import sys
|
||||
|
||||
import mp_compile
|
||||
|
||||
from distutils.command.build_ext import build_ext
|
||||
from distutils import sysconfig
|
||||
from setuptools import Extension, setup, find_packages
|
||||
|
@ -212,7 +210,9 @@ class pil_build_ext(build_ext):
|
|||
# if Homebrew is installed, use its lib and include directories
|
||||
import subprocess
|
||||
try:
|
||||
prefix = subprocess.check_output(['brew', '--prefix']).strip()
|
||||
prefix = subprocess.check_output(
|
||||
['brew', '--prefix']
|
||||
).strip().decode('latin1')
|
||||
except:
|
||||
# Homebrew not installed
|
||||
prefix = None
|
||||
|
@ -405,7 +405,12 @@ class pil_build_ext(build_ext):
|
|||
|
||||
# Find the best version
|
||||
for directory in self.compiler.include_dirs:
|
||||
for name in os.listdir(directory):
|
||||
try:
|
||||
listdir = os.listdir(directory)
|
||||
except Exception:
|
||||
# WindowsError, FileNotFoundError
|
||||
continue
|
||||
for name in listdir:
|
||||
if name.startswith('openjpeg-') and \
|
||||
os.path.isfile(os.path.join(directory, name,
|
||||
'openjpeg.h')):
|
||||
|
@ -707,40 +712,41 @@ class pil_build_ext(build_ext):
|
|||
finally:
|
||||
os.unlink(tmpfile)
|
||||
|
||||
if __name__=='__main__':
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
description='Python Imaging Library (Fork)',
|
||||
long_description=(
|
||||
_read('README.rst') + b'\n' +
|
||||
_read('CHANGES.rst')).decode('utf-8'),
|
||||
author='Alex Clark (fork author)',
|
||||
author_email='aclark@aclark.net',
|
||||
url='http://python-pillow.github.io/',
|
||||
classifiers=[
|
||||
"Development Status :: 6 - Mature",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Scanners",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
|
||||
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
"Programming Language :: Python :: 3.3", ],
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
ext_modules=[Extension("PIL._imaging", ["_imaging.c"])],
|
||||
include_package_data=True,
|
||||
packages=find_packages(),
|
||||
scripts=glob.glob("Scripts/pil*.py"),
|
||||
test_suite='PIL.tests',
|
||||
keywords=["Imaging", ],
|
||||
license='Standard PIL License',
|
||||
zip_safe=True,
|
||||
)
|
||||
|
||||
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
description='Python Imaging Library (Fork)',
|
||||
long_description=(
|
||||
_read('README.rst') + b'\n' +
|
||||
_read('CHANGES.rst')).decode('utf-8'),
|
||||
author='Alex Clark (fork author)',
|
||||
author_email='aclark@aclark.net',
|
||||
url='http://python-pillow.github.io/',
|
||||
classifiers=[
|
||||
"Development Status :: 6 - Mature",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Scanners",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
|
||||
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
],
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
ext_modules=[Extension("PIL._imaging", ["_imaging.c"])],
|
||||
include_package_data=True,
|
||||
packages=find_packages(),
|
||||
scripts=glob.glob("Scripts/pil*.py"),
|
||||
test_suite='PIL.tests',
|
||||
keywords=["Imaging", ],
|
||||
license='Standard PIL License',
|
||||
zip_safe=True,
|
||||
)
|
||||
# End of file
|
||||
|
|
|
@ -4,8 +4,8 @@ import os
|
|||
import sys
|
||||
import glob
|
||||
|
||||
# monkey with the path, removing the local directory but adding the Tests/ directory
|
||||
# for helper.py and the other local imports there.
|
||||
# monkey with the path, removing the local directory but adding the Tests/
|
||||
# directory for helper.py and the other local imports there.
|
||||
|
||||
del(sys.path[0])
|
||||
sys.path.insert(0, os.path.abspath('./Tests'))
|
||||
|
@ -16,7 +16,7 @@ sys.path.insert(0, os.path.abspath('./Tests'))
|
|||
if len(sys.argv) == 1:
|
||||
sys.argv.extend(glob.glob('Tests/test*.py'))
|
||||
|
||||
# Make sure that nose doesn't muck with our paths.
|
||||
# Make sure that nose doesn't muck with our paths.
|
||||
if ('--no-path-adjustment' not in sys.argv) and ('-P' not in sys.argv):
|
||||
sys.argv.insert(1, '--no-path-adjustment')
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user