commit
2a3680b099
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
@ -0,0 +1,6 @@
|
|||||||
|
""":mod:`wand` --- Simple `MagickWand API`_ binding for Python
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. _MagickWand API: http://www.imagemagick.org/script/magick-wand.php
|
||||||
|
|
||||||
|
"""
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,307 @@
|
|||||||
|
""":mod:`wand.color` --- Colors
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 0.1.2
|
||||||
|
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
from .api import MagickPixelPacket, library
|
||||||
|
from .compat import binary, text
|
||||||
|
from .resource import Resource
|
||||||
|
from .version import QUANTUM_DEPTH
|
||||||
|
|
||||||
|
__all__ = 'Color', 'scale_quantum_to_int8'
|
||||||
|
|
||||||
|
|
||||||
|
class Color(Resource):
|
||||||
|
"""Color value.
|
||||||
|
|
||||||
|
Unlike any other objects in Wand, its resource management can be
|
||||||
|
implicit when it used outside of :keyword:`with` block. In these case,
|
||||||
|
its resource are allocated for every operation which requires a resource
|
||||||
|
and destroyed immediately. Of course it is inefficient when the
|
||||||
|
operations are much, so to avoid it, you should use color objects
|
||||||
|
inside of :keyword:`with` block explicitly e.g.::
|
||||||
|
|
||||||
|
red_count = 0
|
||||||
|
with Color('#f00') as red:
|
||||||
|
with Image(filename='image.png') as img:
|
||||||
|
for row in img:
|
||||||
|
for col in row:
|
||||||
|
if col == red:
|
||||||
|
red_count += 1
|
||||||
|
|
||||||
|
:param string: a color namel string e.g. ``'rgb(255, 255, 255)'``,
|
||||||
|
``'#fff'``, ``'white'``. see `ImageMagick Color Names`_
|
||||||
|
doc also
|
||||||
|
:type string: :class:`basestring`
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3.0
|
||||||
|
:class:`Color` objects become hashable.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
`ImageMagick Color Names`_
|
||||||
|
The color can then be given as a color name (there is a limited
|
||||||
|
but large set of these; see below) or it can be given as a set
|
||||||
|
of numbers (in decimal or hexadecimal), each corresponding to
|
||||||
|
a channel in an RGB or RGBA color model. HSL, HSLA, HSB, HSBA,
|
||||||
|
CMYK, or CMYKA color models may also be specified. These topics
|
||||||
|
are briefly described in the sections below.
|
||||||
|
|
||||||
|
.. _ImageMagick Color Names: http://www.imagemagick.org/script/color.php
|
||||||
|
|
||||||
|
.. describe:: == (other)
|
||||||
|
|
||||||
|
Equality operator.
|
||||||
|
|
||||||
|
:param other: a color another one
|
||||||
|
:type color: :class:`Color`
|
||||||
|
:returns: ``True`` only if two images equal.
|
||||||
|
:rtype: :class:`bool`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
c_is_resource = library.IsPixelWand
|
||||||
|
c_destroy_resource = library.DestroyPixelWand
|
||||||
|
c_get_exception = library.PixelGetException
|
||||||
|
c_clear_exception = library.PixelClearException
|
||||||
|
|
||||||
|
__slots__ = 'raw', 'c_resource', 'allocated'
|
||||||
|
|
||||||
|
def __init__(self, string=None, raw=None):
|
||||||
|
if (string is None and raw is None or
|
||||||
|
string is not None and raw is not None):
|
||||||
|
raise TypeError('expected one argument')
|
||||||
|
|
||||||
|
self.allocated = 0
|
||||||
|
if raw is None:
|
||||||
|
self.raw = ctypes.create_string_buffer(
|
||||||
|
ctypes.sizeof(MagickPixelPacket)
|
||||||
|
)
|
||||||
|
with self:
|
||||||
|
library.PixelSetColor(self.resource, binary(string))
|
||||||
|
library.PixelGetMagickColor(self.resource, self.raw)
|
||||||
|
else:
|
||||||
|
self.raw = raw
|
||||||
|
|
||||||
|
def __getinitargs__(self):
|
||||||
|
return self.string, None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
if not self.allocated:
|
||||||
|
with self.allocate():
|
||||||
|
self.resource = library.NewPixelWand()
|
||||||
|
library.PixelSetMagickColor(self.resource, self.raw)
|
||||||
|
self.allocated += 1
|
||||||
|
return Resource.__enter__(self)
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.allocated -= 1
|
||||||
|
if not self.allocated:
|
||||||
|
Resource.__exit__(self, type, value, traceback)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def string(self):
|
||||||
|
"""(:class:`basestring`) The string representation of the color."""
|
||||||
|
with self:
|
||||||
|
color_string = library.PixelGetColorAsString(self.resource)
|
||||||
|
return text(color_string.value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def normalized_string(self):
|
||||||
|
"""(:class:`basestring`) The normalized string representation of
|
||||||
|
the color. The same color is always represented to the same
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self:
|
||||||
|
string = library.PixelGetColorAsNormalizedString(self.resource)
|
||||||
|
return text(string.value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def c_equals(a, b):
|
||||||
|
"""Raw level version of equality test function for two pixels.
|
||||||
|
|
||||||
|
:param a: a pointer to PixelWand to compare
|
||||||
|
:type a: :class:`ctypes.c_void_p`
|
||||||
|
:param b: a pointer to PixelWand to compare
|
||||||
|
:type b: :class:`ctypes.c_void_p`
|
||||||
|
:returns: ``True`` only if two pixels equal
|
||||||
|
:rtype: :class:`bool`
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
It's only for internal use. Don't use it directly.
|
||||||
|
Use ``==`` operator of :class:`Color` instead.
|
||||||
|
|
||||||
|
"""
|
||||||
|
alpha = library.PixelGetAlpha
|
||||||
|
return bool(library.IsPixelWandSimilar(a, b, 0) and
|
||||||
|
alpha(a) == alpha(b))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Color):
|
||||||
|
return False
|
||||||
|
with self as this:
|
||||||
|
with other:
|
||||||
|
return self.c_equals(this.resource, other.resource)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
if self.alpha:
|
||||||
|
return hash(self.normalized_string)
|
||||||
|
return hash(None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def red(self):
|
||||||
|
"""(:class:`numbers.Real`) Red, from 0.0 to 1.0."""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetRed(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def green(self):
|
||||||
|
"""(:class:`numbers.Real`) Green, from 0.0 to 1.0."""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetGreen(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blue(self):
|
||||||
|
"""(:class:`numbers.Real`) Blue, from 0.0 to 1.0."""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetBlue(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alpha(self):
|
||||||
|
"""(:class:`numbers.Real`) Alpha value, from 0.0 to 1.0."""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetAlpha(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def red_quantum(self):
|
||||||
|
"""(:class:`numbers.Integral`) Red.
|
||||||
|
Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetRedQuantum(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def green_quantum(self):
|
||||||
|
"""(:class:`numbers.Integral`) Green.
|
||||||
|
Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetGreenQuantum(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blue_quantum(self):
|
||||||
|
"""(:class:`numbers.Integral`) Blue.
|
||||||
|
Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetBlueQuantum(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alpha_quantum(self):
|
||||||
|
"""(:class:`numbers.Integral`) Alpha value.
|
||||||
|
Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self:
|
||||||
|
return library.PixelGetAlphaQuantum(self.resource)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def red_int8(self):
|
||||||
|
"""(:class:`numbers.Integral`) Red as 8bit integer which is a common
|
||||||
|
style. From 0 to 255.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return scale_quantum_to_int8(self.red_quantum)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def green_int8(self):
|
||||||
|
"""(:class:`numbers.Integral`) Green as 8bit integer which is
|
||||||
|
a common style. From 0 to 255.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return scale_quantum_to_int8(self.green_quantum)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blue_int8(self):
|
||||||
|
"""(:class:`numbers.Integral`) Blue as 8bit integer which is
|
||||||
|
a common style. From 0 to 255.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return scale_quantum_to_int8(self.blue_quantum)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alpha_int8(self):
|
||||||
|
"""(:class:`numbers.Integral`) Alpha value as 8bit integer which is
|
||||||
|
a common style. From 0 to 255.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return scale_quantum_to_int8(self.alpha_quantum)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.string
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
c = type(self)
|
||||||
|
return '{0}.{1}({2!r})'.format(c.__module__, c.__name__, self.string)
|
||||||
|
|
||||||
|
def _repr_html_(self):
|
||||||
|
html = """
|
||||||
|
<span style="background-color:#{red:02X}{green:02X}{blue:02X};
|
||||||
|
display:inline-block;
|
||||||
|
line-height:1em;
|
||||||
|
width:1em;"> </span>
|
||||||
|
<strong>#{red:02X}{green:02X}{blue:02X}</strong>
|
||||||
|
"""
|
||||||
|
return html.format(red=self.red_int8,
|
||||||
|
green=self.green_int8,
|
||||||
|
blue=self.blue_int8)
|
||||||
|
|
||||||
|
|
||||||
|
def scale_quantum_to_int8(quantum):
|
||||||
|
"""Straightforward port of :c:func:`ScaleQuantumToChar()` inline
|
||||||
|
function.
|
||||||
|
|
||||||
|
:param quantum: quantum value
|
||||||
|
:type quantum: :class:`numbers.Integral`
|
||||||
|
:returns: 8bit integer of the given ``quantum`` value
|
||||||
|
:rtype: :class:`numbers.Integral`
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
if quantum <= 0:
|
||||||
|
return 0
|
||||||
|
table = {8: 1, 16: 257.0, 32: 16843009.0, 64: 72340172838076673.0}
|
||||||
|
v = quantum / table[QUANTUM_DEPTH]
|
||||||
|
if v >= 255:
|
||||||
|
return 255
|
||||||
|
return int(v + 0.5)
|
Binary file not shown.
@ -0,0 +1,119 @@
|
|||||||
|
""":mod:`wand.compat` --- Compatibility layer
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module provides several subtle things to support
|
||||||
|
multiple Python versions (2.6, 2.7, 3.2--3.5) and VM implementations
|
||||||
|
(CPython, PyPy).
|
||||||
|
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
__all__ = ('PY3', 'binary', 'binary_type', 'encode_filename', 'file_types',
|
||||||
|
'nested', 'string_type', 'text', 'text_type', 'xrange')
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`bool`) Whether it is Python 3.x or not.
|
||||||
|
PY3 = sys.version_info >= (3,)
|
||||||
|
|
||||||
|
#: (:class:`type`) Type for representing binary data. :class:`str` in Python 2
|
||||||
|
#: and :class:`bytes` in Python 3.
|
||||||
|
binary_type = bytes if PY3 else str
|
||||||
|
|
||||||
|
#: (:class:`type`) Type for text data. :class:`basestring` in Python 2
|
||||||
|
#: and :class:`str` in Python 3.
|
||||||
|
string_type = str if PY3 else basestring # noqa
|
||||||
|
|
||||||
|
#: (:class:`type`) Type for representing Unicode textual data.
|
||||||
|
#: :class:`unicode` in Python 2 and :class:`str` in Python 3.
|
||||||
|
text_type = str if PY3 else unicode # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def binary(string, var=None):
|
||||||
|
"""Makes ``string`` to :class:`str` in Python 2.
|
||||||
|
Makes ``string`` to :class:`bytes` in Python 3.
|
||||||
|
|
||||||
|
:param string: a string to cast it to :data:`binary_type`
|
||||||
|
:type string: :class:`bytes`, :class:`str`, :class:`unicode`
|
||||||
|
:param var: an optional variable name to be used for error message
|
||||||
|
:type var: :class:`str`
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(string, text_type):
|
||||||
|
return string.encode()
|
||||||
|
elif isinstance(string, binary_type):
|
||||||
|
return string
|
||||||
|
if var:
|
||||||
|
raise TypeError('{0} must be a string, not {1!r}'.format(var, string))
|
||||||
|
raise TypeError('expected a string, not ' + repr(string))
|
||||||
|
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
def text(string):
|
||||||
|
if isinstance(string, bytes):
|
||||||
|
return string.decode('utf-8')
|
||||||
|
return string
|
||||||
|
else:
|
||||||
|
def text(string):
|
||||||
|
"""Makes ``string`` to :class:`str` in Python 3.
|
||||||
|
Does nothing in Python 2.
|
||||||
|
|
||||||
|
:param string: a string to cast it to :data:`text_type`
|
||||||
|
:type string: :class:`bytes`, :class:`str`, :class:`unicode`
|
||||||
|
|
||||||
|
"""
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
#: The :func:`xrange()` function. Alias for :func:`range()` in Python 3.
|
||||||
|
xrange = range if PY3 else xrange # noqa
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`type`, :class:`tuple`) Types for file objects that have
|
||||||
|
#: ``fileno()``.
|
||||||
|
file_types = io.RawIOBase if PY3 else (io.RawIOBase, types.FileType)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_filename(filename):
|
||||||
|
"""If ``filename`` is a :data:`text_type`, encode it to
|
||||||
|
:data:`binary_type` according to filesystem's default encoding.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(filename, text_type):
|
||||||
|
return filename.encode(sys.getfilesystemencoding())
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
nested = contextlib.nested
|
||||||
|
except AttributeError:
|
||||||
|
# http://hg.python.org/cpython/file/v2.7.6/Lib/contextlib.py#l88
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def nested(*managers):
|
||||||
|
exits = []
|
||||||
|
vars = []
|
||||||
|
exc = (None, None, None)
|
||||||
|
try:
|
||||||
|
for mgr in managers:
|
||||||
|
exit = mgr.__exit__
|
||||||
|
enter = mgr.__enter__
|
||||||
|
vars.append(enter())
|
||||||
|
exits.append(exit)
|
||||||
|
yield vars
|
||||||
|
except:
|
||||||
|
exc = sys.exc_info()
|
||||||
|
finally:
|
||||||
|
while exits:
|
||||||
|
exit = exits.pop()
|
||||||
|
try:
|
||||||
|
if exit(*exc):
|
||||||
|
exc = (None, None, None)
|
||||||
|
except:
|
||||||
|
exc = sys.exc_info()
|
||||||
|
if exc != (None, None, None):
|
||||||
|
# PEP 3109
|
||||||
|
e = exc[0](exc[1])
|
||||||
|
e.__traceback__ = e[2]
|
||||||
|
raise e
|
Binary file not shown.
@ -0,0 +1,78 @@
|
|||||||
|
""":mod:`wand.display` --- Displaying images
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The :func:`display()` functions shows you the image. It is useful for
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
If you are in Mac, the image will be opened by your default image application
|
||||||
|
(:program:`Preview.app` usually).
|
||||||
|
|
||||||
|
If you are in Windows, the image will be opened by :program:`imdisplay.exe`,
|
||||||
|
or your default image application (:program:`Windows Photo Viewer` usually)
|
||||||
|
if :program:`imdisplay.exe` is unavailable.
|
||||||
|
|
||||||
|
You can use it from CLI also. Execute :mod:`wand.display` module through
|
||||||
|
:option:`python -m` option:
|
||||||
|
|
||||||
|
.. sourcecode:: console
|
||||||
|
|
||||||
|
$ python -m wand.display wandtests/assets/mona-lisa.jpg
|
||||||
|
|
||||||
|
.. versionadded:: 0.1.9
|
||||||
|
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from .image import Image
|
||||||
|
from .api import library
|
||||||
|
from .exceptions import BlobError, DelegateError
|
||||||
|
|
||||||
|
__all__ = 'display',
|
||||||
|
|
||||||
|
|
||||||
|
def display(image, server_name=':0'):
|
||||||
|
"""Displays the passed ``image``.
|
||||||
|
|
||||||
|
:param image: an image to display
|
||||||
|
:type image: :class:`~wand.image.Image`
|
||||||
|
:param server_name: X11 server name to use. it is ignored and not used
|
||||||
|
for Mac. default is ``':0'``
|
||||||
|
:type server_name: :class:`str`
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(image, Image):
|
||||||
|
raise TypeError('image must be a wand.image.Image instance, not ' +
|
||||||
|
repr(image))
|
||||||
|
system = platform.system()
|
||||||
|
if system == 'Windows':
|
||||||
|
try:
|
||||||
|
image.save(filename='win:.')
|
||||||
|
except DelegateError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if system in ('Windows', 'Darwin'):
|
||||||
|
ext = '.' + image.format.lower()
|
||||||
|
path = tempfile.mktemp(suffix=ext)
|
||||||
|
image.save(filename=path)
|
||||||
|
os.system(('start ' if system == 'Windows' else 'open ') + path)
|
||||||
|
else:
|
||||||
|
library.MagickDisplayImage.argtypes = [ctypes.c_void_p,
|
||||||
|
ctypes.c_char_p]
|
||||||
|
library.MagickDisplayImage(image.wand, str(server_name).encode())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print>>sys.stderr, 'usage: python -m wand.display FILE'
|
||||||
|
raise SystemExit
|
||||||
|
path = sys.argv[1]
|
||||||
|
try:
|
||||||
|
with Image(filename=path) as image:
|
||||||
|
display(image)
|
||||||
|
except BlobError:
|
||||||
|
print>>sys.stderr, 'cannot read the file', path
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,111 @@
|
|||||||
|
""":mod:`wand.exceptions` --- Errors and warnings
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module maps MagickWand API's errors and warnings to Python's native
|
||||||
|
exceptions and warnings. You can catch all MagickWand errors using Python's
|
||||||
|
natural way to catch errors.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
`ImageMagick Exceptions <http://www.imagemagick.org/script/exception.php>`_
|
||||||
|
|
||||||
|
.. versionadded:: 0.1.1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class WandException(Exception):
|
||||||
|
"""All Wand-related exceptions are derived from this class."""
|
||||||
|
|
||||||
|
|
||||||
|
class WandWarning(WandException, Warning):
|
||||||
|
"""Base class for Wand-related warnings."""
|
||||||
|
|
||||||
|
|
||||||
|
class WandError(WandException):
|
||||||
|
"""Base class for Wand-related errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class WandFatalError(WandException):
|
||||||
|
"""Base class for Wand-related fatal errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class WandLibraryVersionError(WandException):
|
||||||
|
"""Base class for Wand-related ImageMagick version errors.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.2
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`list`) A list of error/warning domains, these descriptions and
|
||||||
|
#: codes. The form of elements is like: (domain name, description, codes).
|
||||||
|
DOMAIN_MAP = [
|
||||||
|
('ResourceLimit',
|
||||||
|
'A program resource is exhausted e.g. not enough memory.',
|
||||||
|
(MemoryError,),
|
||||||
|
[300, 400, 700]),
|
||||||
|
('Type', 'A font is unavailable; a substitution may have occurred.', (),
|
||||||
|
[305, 405, 705]),
|
||||||
|
('Option', 'A command-line option was malformed.', (), [310, 410, 710]),
|
||||||
|
('Delegate', 'An ImageMagick delegate failed to complete.', (),
|
||||||
|
[315, 415, 715]),
|
||||||
|
('MissingDelegate',
|
||||||
|
'The image type can not be read or written because the appropriate; '
|
||||||
|
'delegate is missing.',
|
||||||
|
(ImportError,),
|
||||||
|
[320, 420, 720]),
|
||||||
|
('CorruptImage', 'The image file may be corrupt.',
|
||||||
|
(ValueError,), [325, 425, 725]),
|
||||||
|
('FileOpen', 'The image file could not be opened for reading or writing.',
|
||||||
|
(IOError,), [330, 430, 730]),
|
||||||
|
('Blob', 'A binary large object could not be allocated, read, or written.',
|
||||||
|
(IOError,), [335, 435, 735]),
|
||||||
|
('Stream', 'There was a problem reading or writing from a stream.',
|
||||||
|
(IOError,), [340, 440, 740]),
|
||||||
|
('Cache', 'Pixels could not be read or written to the pixel cache.',
|
||||||
|
(), [345, 445, 745]),
|
||||||
|
('Coder', 'There was a problem with an image coder.', (), [350, 450, 750]),
|
||||||
|
('Module', 'There was a problem with an image module.', (),
|
||||||
|
[355, 455, 755]),
|
||||||
|
('Draw', 'A drawing operation failed.', (), [360, 460, 760]),
|
||||||
|
('Image', 'The operation could not complete due to an incompatible image.',
|
||||||
|
(), [365, 465, 765]),
|
||||||
|
('Wand', 'There was a problem specific to the MagickWand API.', (),
|
||||||
|
[370, 470, 770]),
|
||||||
|
('Random', 'There is a problem generating a true or pseudo-random number.',
|
||||||
|
(), [375, 475, 775]),
|
||||||
|
('XServer', 'An X resource is unavailable.', (), [380, 480, 780]),
|
||||||
|
('Monitor', 'There was a problem activating the progress monitor.', (),
|
||||||
|
[385, 485, 785]),
|
||||||
|
('Registry', 'There was a problem getting or setting the registry.', (),
|
||||||
|
[390, 490, 790]),
|
||||||
|
('Configure', 'There was a problem getting a configuration file.', (),
|
||||||
|
[395, 495, 795]),
|
||||||
|
('Policy',
|
||||||
|
'A policy denies access to a delegate, coder, filter, path, or resource.',
|
||||||
|
(), [399, 499, 799])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`list`) The list of (base_class, suffix) pairs (for each code).
|
||||||
|
#: It would be zipped with :const:`DOMAIN_MAP` pairs' last element.
|
||||||
|
CODE_MAP = [
|
||||||
|
(WandWarning, 'Warning'),
|
||||||
|
(WandError, 'Error'),
|
||||||
|
(WandFatalError, 'FatalError')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`dict`) The dictionary of (code, exc_type).
|
||||||
|
TYPE_MAP = {}
|
||||||
|
|
||||||
|
|
||||||
|
for domain, description, bases, codes in DOMAIN_MAP:
|
||||||
|
for code, (base, suffix) in zip(codes, CODE_MAP):
|
||||||
|
name = domain + suffix
|
||||||
|
locals()[name] = TYPE_MAP[code] = type(name, (base,) + bases, {
|
||||||
|
'__doc__': description,
|
||||||
|
'wand_error_code': code
|
||||||
|
})
|
||||||
|
del name, base, suffix
|
Binary file not shown.
@ -0,0 +1,103 @@
|
|||||||
|
""":mod:`wand.font` --- Fonts
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
:class:`Font` is an object which takes the :attr:`~Font.path` of font file,
|
||||||
|
:attr:`~Font.size`, :attr:`~Font.color`, and whether to use
|
||||||
|
:attr:`~Font.antialias`\ ing. If you want to use font by its name rather
|
||||||
|
than the file path, use TTFQuery_ package. The font path resolution by its
|
||||||
|
name is a very complicated problem to achieve.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
TTFQuery_ --- Find and Extract Information from TTF Files
|
||||||
|
TTFQuery builds on the `FontTools-TTX`_ package to allow the Python
|
||||||
|
programmer to accomplish a number of tasks:
|
||||||
|
|
||||||
|
- query the system to find installed fonts
|
||||||
|
|
||||||
|
- retrieve metadata about any TTF font file
|
||||||
|
|
||||||
|
- this includes the glyph outlines (shape) of individual code-points,
|
||||||
|
which allows for rendering the glyphs in 3D (such as is done in
|
||||||
|
OpenGLContext)
|
||||||
|
|
||||||
|
- lookup/find fonts by:
|
||||||
|
|
||||||
|
- abstract family type
|
||||||
|
- proper font name
|
||||||
|
|
||||||
|
- build simple metadata registries for run-time font matching
|
||||||
|
|
||||||
|
.. _TTFQuery: http://ttfquery.sourceforge.net/
|
||||||
|
.. _FontTools-TTX: http://sourceforge.net/projects/fonttools/
|
||||||
|
|
||||||
|
"""
|
||||||
|
import numbers
|
||||||
|
|
||||||
|
from .color import Color
|
||||||
|
from .compat import string_type, text
|
||||||
|
|
||||||
|
__all__ = 'Font',
|
||||||
|
|
||||||
|
|
||||||
|
class Font(tuple):
|
||||||
|
"""Font struct which is a subtype of :class:`tuple`.
|
||||||
|
|
||||||
|
:param path: the path of the font file
|
||||||
|
:type path: :class:`str`, :class:`basestring`
|
||||||
|
:param size: the size of typeface. 0 by default which means *autosized*
|
||||||
|
:type size: :class:`numbers.Real`
|
||||||
|
:param color: the color of typeface. black by default
|
||||||
|
:type color: :class:`~wand.color.Color`
|
||||||
|
:param antialias: whether to use antialiasing. :const:`True` by default
|
||||||
|
:type antialias: :class:`bool`
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3.9
|
||||||
|
The ``size`` parameter becomes optional. Its default value is
|
||||||
|
0, which means *autosized*.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, path, size=0, color=None, antialias=True):
|
||||||
|
if not isinstance(path, string_type):
|
||||||
|
raise TypeError('path must be a string, not ' + repr(path))
|
||||||
|
if not isinstance(size, numbers.Real):
|
||||||
|
raise TypeError('size must be a real number, not ' + repr(size))
|
||||||
|
if color is None:
|
||||||
|
color = Color('black')
|
||||||
|
elif not isinstance(color, Color):
|
||||||
|
raise TypeError('color must be an instance of wand.color.Color, '
|
||||||
|
'not ' + repr(color))
|
||||||
|
path = text(path)
|
||||||
|
return tuple.__new__(cls, (path, size, color, bool(antialias)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""(:class:`basestring`) The path of font file."""
|
||||||
|
return self[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
"""(:class:`numbers.Real`) The font size in pixels."""
|
||||||
|
return self[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
"""(:class:`wand.color.Color`) The font color."""
|
||||||
|
return self[2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def antialias(self):
|
||||||
|
"""(:class:`bool`) Whether to apply antialiasing (``True``)
|
||||||
|
or not (``False``).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self[3]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '{0.__module__}.{0.__name__}({1})'.format(
|
||||||
|
type(self),
|
||||||
|
tuple.__repr__(self)
|
||||||
|
)
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,244 @@
|
|||||||
|
""":mod:`wand.resource` --- Global resource management
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
There is the global resource to manage in MagickWand API. This module
|
||||||
|
implements automatic global resource management through reference counting.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import ctypes
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .api import library
|
||||||
|
from .compat import string_type
|
||||||
|
from .exceptions import TYPE_MAP, WandException
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('genesis', 'terminus', 'increment_refcount', 'decrement_refcount',
|
||||||
|
'Resource', 'DestroyedResourceError')
|
||||||
|
|
||||||
|
|
||||||
|
def genesis():
|
||||||
|
"""Instantiates the MagickWand API.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Don't call this function directly. Use :func:`increment_refcount()` and
|
||||||
|
:func:`decrement_refcount()` functions instead.
|
||||||
|
|
||||||
|
"""
|
||||||
|
library.MagickWandGenesis()
|
||||||
|
|
||||||
|
|
||||||
|
def terminus():
|
||||||
|
"""Cleans up the MagickWand API.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Don't call this function directly. Use :func:`increment_refcount()` and
|
||||||
|
:func:`decrement_refcount()` functions instead.
|
||||||
|
|
||||||
|
"""
|
||||||
|
library.MagickWandTerminus()
|
||||||
|
|
||||||
|
|
||||||
|
#: (:class:`numbers.Integral`) The internal integer value that maintains
|
||||||
|
#: the number of referenced objects.
|
||||||
|
#:
|
||||||
|
#: .. warning::
|
||||||
|
#:
|
||||||
|
#: Don't touch this global variable. Use :func:`increment_refcount()` and
|
||||||
|
#: :func:`decrement_refcount()` functions instead.
|
||||||
|
#:
|
||||||
|
reference_count = 0
|
||||||
|
|
||||||
|
|
||||||
|
def increment_refcount():
|
||||||
|
"""Increments the :data:`reference_count` and instantiates the MagickWand
|
||||||
|
API if it is the first use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global reference_count
|
||||||
|
if reference_count:
|
||||||
|
reference_count += 1
|
||||||
|
else:
|
||||||
|
genesis()
|
||||||
|
reference_count = 1
|
||||||
|
|
||||||
|
|
||||||
|
def decrement_refcount():
|
||||||
|
"""Decrements the :data:`reference_count` and cleans up the MagickWand
|
||||||
|
API if it will be no more used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global reference_count
|
||||||
|
if not reference_count:
|
||||||
|
raise RuntimeError('wand.resource.reference_count is already zero')
|
||||||
|
reference_count -= 1
|
||||||
|
if not reference_count:
|
||||||
|
terminus()
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""Abstract base class for MagickWand object that requires resource
|
||||||
|
management. Its all subclasses manage the resource semiautomatically
|
||||||
|
and support :keyword:`with` statement as well::
|
||||||
|
|
||||||
|
with Resource() as resource:
|
||||||
|
# use the resource...
|
||||||
|
pass
|
||||||
|
|
||||||
|
It doesn't implement constructor by itself, so subclasses should
|
||||||
|
implement it. Every constructor should assign the pointer of its
|
||||||
|
resource data into :attr:`resource` attribute inside of :keyword:`with`
|
||||||
|
:meth:`allocate()` context. For example::
|
||||||
|
|
||||||
|
class Pizza(Resource):
|
||||||
|
'''My pizza yummy.'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
with self.allocate():
|
||||||
|
self.resource = library.NewPizza()
|
||||||
|
|
||||||
|
.. versionadded:: 0.1.2
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` predicate function
|
||||||
|
#: that returns whether the given pointer (that contains a resource data
|
||||||
|
#: usuaully) is a valid resource.
|
||||||
|
#:
|
||||||
|
#: .. note::
|
||||||
|
#:
|
||||||
|
#: It is an abstract attribute that has to be implemented
|
||||||
|
#: in the subclass.
|
||||||
|
c_is_resource = NotImplemented
|
||||||
|
|
||||||
|
#: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that destroys
|
||||||
|
#: the :attr:`resource`.
|
||||||
|
#:
|
||||||
|
#: .. note::
|
||||||
|
#:
|
||||||
|
#: It is an abstract attribute that has to be implemented
|
||||||
|
#: in the subclass.
|
||||||
|
c_destroy_resource = NotImplemented
|
||||||
|
|
||||||
|
#: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that gets
|
||||||
|
#: an exception from the :attr:`resource`.
|
||||||
|
#:
|
||||||
|
#: .. note::
|
||||||
|
#:
|
||||||
|
#: It is an abstract attribute that has to be implemented
|
||||||
|
#: in the subclass.
|
||||||
|
c_get_exception = NotImplemented
|
||||||
|
|
||||||
|
#: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that clears
|
||||||
|
#: an exception of the :attr:`resource`.
|
||||||
|
#:
|
||||||
|
#: .. note::
|
||||||
|
#:
|
||||||
|
#: It is an abstract attribute that has to be implemented
|
||||||
|
#: in the subclass.
|
||||||
|
c_clear_exception = NotImplemented
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resource(self):
|
||||||
|
"""Internal pointer to the resource instance. It may raise
|
||||||
|
:exc:`DestroyedResourceError` when the resource has destroyed already.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if getattr(self, 'c_resource', None) is None:
|
||||||
|
raise DestroyedResourceError(repr(self) + ' is destroyed already')
|
||||||
|
return self.c_resource
|
||||||
|
|
||||||
|
@resource.setter
|
||||||
|
def resource(self, resource):
|
||||||
|
# Delete the existing resource if there is one
|
||||||
|
if getattr(self, 'c_resource', None):
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
if self.c_is_resource(resource):
|
||||||
|
self.c_resource = resource
|
||||||
|
else:
|
||||||
|
raise TypeError(repr(resource) + ' is an invalid resource')
|
||||||
|
increment_refcount()
|
||||||
|
|
||||||
|
@resource.deleter
|
||||||
|
def resource(self):
|
||||||
|
self.c_destroy_resource(self.resource)
|
||||||
|
self.c_resource = None
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def allocate(self):
|
||||||
|
"""Allocates the memory for the resource explicitly. Its subclasses
|
||||||
|
should assign the created resource into :attr:`resource` attribute
|
||||||
|
inside of this context. For example::
|
||||||
|
|
||||||
|
with resource.allocate():
|
||||||
|
resource.resource = library.NewResource()
|
||||||
|
|
||||||
|
"""
|
||||||
|
increment_refcount()
|
||||||
|
try:
|
||||||
|
yield self
|
||||||
|
except:
|
||||||
|
decrement_refcount()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
"""Cleans up the resource explicitly. If you use the resource in
|
||||||
|
:keyword:`with` statement, it was called implicitly so have not to
|
||||||
|
call it.
|
||||||
|
|
||||||
|
"""
|
||||||
|
del self.resource
|
||||||
|
decrement_refcount()
|
||||||
|
|
||||||
|
def get_exception(self):
|
||||||
|
"""Gets a current exception instance.
|
||||||
|
|
||||||
|
:returns: a current exception. it can be ``None`` as well if any
|
||||||
|
errors aren't occurred
|
||||||
|
:rtype: :class:`wand.exceptions.WandException`
|
||||||
|
|
||||||
|
"""
|
||||||
|
severity = ctypes.c_int()
|
||||||
|
desc = self.c_get_exception(self.resource, ctypes.byref(severity))
|
||||||
|
if severity.value == 0:
|
||||||
|
return
|
||||||
|
self.c_clear_exception(self.wand)
|
||||||
|
exc_cls = TYPE_MAP[severity.value]
|
||||||
|
message = desc.value
|
||||||
|
if not isinstance(message, string_type):
|
||||||
|
message = message.decode(errors='replace')
|
||||||
|
return exc_cls(message)
|
||||||
|
|
||||||
|
def raise_exception(self, stacklevel=1):
|
||||||
|
"""Raises an exception or warning if it has occurred."""
|
||||||
|
e = self.get_exception()
|
||||||
|
if isinstance(e, Warning):
|
||||||
|
warnings.warn(e, stacklevel=stacklevel + 1)
|
||||||
|
elif isinstance(e, Exception):
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
try:
|
||||||
|
self.destroy()
|
||||||
|
except DestroyedResourceError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DestroyedResourceError(WandException, ReferenceError, AttributeError):
|
||||||
|
"""An error that rises when some code tries access to an already
|
||||||
|
destroyed resource.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3.0
|
||||||
|
It becomes a subtype of :exc:`wand.exceptions.WandException`.
|
||||||
|
|
||||||
|
"""
|
Binary file not shown.
@ -0,0 +1,345 @@
|
|||||||
|
""":mod:`wand.sequence` --- Sequences
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import ctypes
|
||||||
|
import numbers
|
||||||
|
|
||||||
|
from .api import libmagick, library
|
||||||
|
from .compat import binary, xrange
|
||||||
|
from .image import BaseImage, ImageProperty
|
||||||
|
from .version import MAGICK_VERSION_INFO
|
||||||
|
|
||||||
|
__all__ = 'Sequence', 'SingleImage'
|
||||||
|
|
||||||
|
|
||||||
|
class Sequence(ImageProperty, collections.MutableSequence):
|
||||||
|
"""The list-like object that contains every :class:`SingleImage`
|
||||||
|
in the :class:`~wand.image.Image` container. It implements
|
||||||
|
:class:`collections.Sequence` prototocol.
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, image):
|
||||||
|
super(Sequence, self).__init__(image)
|
||||||
|
self.instances = []
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
for instance in self.instances:
|
||||||
|
if instance is not None:
|
||||||
|
instance.c_resource = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_index(self):
|
||||||
|
"""(:class:`numbers.Integral`) The current index of
|
||||||
|
its internal iterator.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
It's only for internal use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return library.MagickGetIteratorIndex(self.image.wand)
|
||||||
|
|
||||||
|
@current_index.setter
|
||||||
|
def current_index(self, index):
|
||||||
|
library.MagickSetIteratorIndex(self.image.wand, index)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def index_context(self, index):
|
||||||
|
"""Scoped setter of :attr:`current_index`. Should be
|
||||||
|
used for :keyword:`with` statement e.g.::
|
||||||
|
|
||||||
|
with image.sequence.index_context(3):
|
||||||
|
print(image.size)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
It's only for internal use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
index = self.validate_position(index)
|
||||||
|
tmp_idx = self.current_index
|
||||||
|
self.current_index = index
|
||||||
|
yield index
|
||||||
|
self.current_index = tmp_idx
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return library.MagickGetNumberImages(self.image.wand)
|
||||||
|
|
||||||
|
def validate_position(self, index):
|
||||||
|
if not isinstance(index, numbers.Integral):
|
||||||
|
raise TypeError('index must be integer, not ' + repr(index))
|
||||||
|
length = len(self)
|
||||||
|
if index >= length or index < -length:
|
||||||
|
raise IndexError(
|
||||||
|
'out of index: {0} (total: {1})'.format(index, length)
|
||||||
|
)
|
||||||
|
if index < 0:
|
||||||
|
index += length
|
||||||
|
return index
|
||||||
|
|
||||||
|
def validate_slice(self, slice_, as_range=False):
|
||||||
|
if not (slice_.step is None or slice_.step == 1):
|
||||||
|
raise ValueError('slicing with step is unsupported')
|
||||||
|
length = len(self)
|
||||||
|
if slice_.start is None:
|
||||||
|
start = 0
|
||||||
|
elif slice_.start < 0:
|
||||||
|
start = length + slice_.start
|
||||||
|
else:
|
||||||
|
start = slice_.start
|
||||||
|
start = min(length, start)
|
||||||
|
if slice_.stop is None:
|
||||||
|
stop = 0
|
||||||
|
elif slice_.stop < 0:
|
||||||
|
stop = length + slice_.stop
|
||||||
|
else:
|
||||||
|
stop = slice_.stop
|
||||||
|
stop = min(length, stop or length)
|
||||||
|
return xrange(start, stop) if as_range else slice(start, stop, None)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
if isinstance(index, slice):
|
||||||
|
slice_ = self.validate_slice(index)
|
||||||
|
return [self[i] for i in xrange(slice_.start, slice_.stop)]
|
||||||
|
index = self.validate_position(index)
|
||||||
|
instances = self.instances
|
||||||
|
instances_length = len(instances)
|
||||||
|
if index < instances_length:
|
||||||
|
instance = instances[index]
|
||||||
|
if (instance is not None and
|
||||||
|
getattr(instance, 'c_resource', None) is not None):
|
||||||
|
return instance
|
||||||
|
else:
|
||||||
|
number_to_extend = index - instances_length + 1
|
||||||
|
instances.extend(None for _ in xrange(number_to_extend))
|
||||||
|
wand = self.image.wand
|
||||||
|
tmp_idx = library.MagickGetIteratorIndex(wand)
|
||||||
|
library.MagickSetIteratorIndex(wand, index)
|
||||||
|
image = library.GetImageFromMagickWand(wand)
|
||||||
|
exc = libmagick.AcquireExceptionInfo()
|
||||||
|
single_image = libmagick.CloneImages(image, binary(str(index)), exc)
|
||||||
|
libmagick.DestroyExceptionInfo(exc)
|
||||||
|
single_wand = library.NewMagickWandFromImage(single_image)
|
||||||
|
single_image = libmagick.DestroyImage(single_image)
|
||||||
|
library.MagickSetIteratorIndex(wand, tmp_idx)
|
||||||
|
instance = SingleImage(single_wand, self.image, image)
|
||||||
|
self.instances[index] = instance
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def __setitem__(self, index, image):
|
||||||
|
if isinstance(index, slice):
|
||||||
|
tmp_idx = self.current_index
|
||||||
|
slice_ = self.validate_slice(index)
|
||||||
|
del self[slice_]
|
||||||
|
self.extend(image, offset=slice_.start)
|
||||||
|
self.current_index = tmp_idx
|
||||||
|
else:
|
||||||
|
if not isinstance(image, BaseImage):
|
||||||
|
raise TypeError('image must be an instance of wand.image.'
|
||||||
|
'BaseImage, not ' + repr(image))
|
||||||
|
with self.index_context(index) as index:
|
||||||
|
library.MagickRemoveImage(self.image.wand)
|
||||||
|
library.MagickAddImage(self.image.wand, image.wand)
|
||||||
|
|
||||||
|
def __delitem__(self, index):
|
||||||
|
if isinstance(index, slice):
|
||||||
|
range_ = self.validate_slice(index, as_range=True)
|
||||||
|
for i in reversed(range_):
|
||||||
|
del self[i]
|
||||||
|
else:
|
||||||
|
with self.index_context(index) as index:
|
||||||
|
library.MagickRemoveImage(self.image.wand)
|
||||||
|
if index < len(self.instances):
|
||||||
|
del self.instances[index]
|
||||||
|
|
||||||
|
def insert(self, index, image):
|
||||||
|
try:
|
||||||
|
index = self.validate_position(index)
|
||||||
|
except IndexError:
|
||||||
|
index = len(self)
|
||||||
|
if not isinstance(image, BaseImage):
|
||||||
|
raise TypeError('image must be an instance of wand.image.'
|
||||||
|
'BaseImage, not ' + repr(image))
|
||||||
|
if not self:
|
||||||
|
library.MagickAddImage(self.image.wand, image.wand)
|
||||||
|
elif index == 0:
|
||||||
|
tmp_idx = self.current_index
|
||||||
|
self_wand = self.image.wand
|
||||||
|
wand = image.sequence[0].wand
|
||||||
|
try:
|
||||||
|
# Prepending image into the list using MagickSetFirstIterator()
|
||||||
|
# and MagickAddImage() had not worked properly, but was fixed
|
||||||
|
# since 6.7.6-0 (rev7106).
|
||||||
|
if MAGICK_VERSION_INFO >= (6, 7, 6, 0):
|
||||||
|
library.MagickSetFirstIterator(self_wand)
|
||||||
|
library.MagickAddImage(self_wand, wand)
|
||||||
|
else:
|
||||||
|
self.current_index = 0
|
||||||
|
library.MagickAddImage(self_wand,
|
||||||
|
self.image.sequence[0].wand)
|
||||||
|
self.current_index = 0
|
||||||
|
library.MagickAddImage(self_wand, wand)
|
||||||
|
self.current_index = 0
|
||||||
|
library.MagickRemoveImage(self_wand)
|
||||||
|
finally:
|
||||||
|
self.current_index = tmp_idx
|
||||||
|
else:
|
||||||
|
with self.index_context(index - 1):
|
||||||
|
library.MagickAddImage(self.image.wand, image.sequence[0].wand)
|
||||||
|
self.instances.insert(index, None)
|
||||||
|
|
||||||
|
def append(self, image):
|
||||||
|
if not isinstance(image, BaseImage):
|
||||||
|
raise TypeError('image must be an instance of wand.image.'
|
||||||
|
'BaseImage, not ' + repr(image))
|
||||||
|
wand = self.image.wand
|
||||||
|
tmp_idx = self.current_index
|
||||||
|
try:
|
||||||
|
library.MagickSetLastIterator(wand)
|
||||||
|
library.MagickAddImage(wand, image.sequence[0].wand)
|
||||||
|
finally:
|
||||||
|
self.current_index = tmp_idx
|
||||||
|
self.instances.append(None)
|
||||||
|
|
||||||
|
def extend(self, images, offset=None):
|
||||||
|
tmp_idx = self.current_index
|
||||||
|
wand = self.image.wand
|
||||||
|
length = 0
|
||||||
|
try:
|
||||||
|
if offset is None:
|
||||||
|
library.MagickSetLastIterator(self.image.wand)
|
||||||
|
else:
|
||||||
|
if offset == 0:
|
||||||
|
images = iter(images)
|
||||||
|
self.insert(0, next(images))
|
||||||
|
offset += 1
|
||||||
|
self.current_index = offset - 1
|
||||||
|
if isinstance(images, type(self)):
|
||||||
|
library.MagickAddImage(wand, images.image.wand)
|
||||||
|
length = len(images)
|
||||||
|
else:
|
||||||
|
delta = 1 if MAGICK_VERSION_INFO >= (6, 7, 6, 0) else 2
|
||||||
|
for image in images:
|
||||||
|
if not isinstance(image, BaseImage):
|
||||||
|
raise TypeError(
|
||||||
|
'images must consist of only instances of '
|
||||||
|
'wand.image.BaseImage, not ' + repr(image)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
library.MagickAddImage(wand, image.sequence[0].wand)
|
||||||
|
self.instances = []
|
||||||
|
if offset is None:
|
||||||
|
library.MagickSetLastIterator(self.image.wand)
|
||||||
|
else:
|
||||||
|
self.current_index += delta
|
||||||
|
length += 1
|
||||||
|
finally:
|
||||||
|
self.current_index = tmp_idx
|
||||||
|
null_list = [None] * length
|
||||||
|
if offset is None:
|
||||||
|
self.instances[offset:] = null_list
|
||||||
|
else:
|
||||||
|
self.instances[offset:offset] = null_list
|
||||||
|
|
||||||
|
def _repr_png_(self):
|
||||||
|
library.MagickResetIterator(self.image.wand)
|
||||||
|
repr_wand = library.MagickAppendImages(self.image.wand, 1)
|
||||||
|
length = ctypes.c_size_t()
|
||||||
|
blob_p = library.MagickGetImagesBlob(repr_wand,
|
||||||
|
ctypes.byref(length))
|
||||||
|
if blob_p and length.value:
|
||||||
|
blob = ctypes.string_at(blob_p, length.value)
|
||||||
|
library.MagickRelinquishMemory(blob_p)
|
||||||
|
return blob
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SingleImage(BaseImage):
|
||||||
|
"""Each single image in :class:`~wand.image.Image` container.
|
||||||
|
For example, it can be a frame of GIF animation.
|
||||||
|
|
||||||
|
Note that all changes on single images are invisible to their
|
||||||
|
containers until they are :meth:`~wand.image.BaseImage.close`\ d
|
||||||
|
(:meth:`~wand.resource.Resource.destroy`\ ed).
|
||||||
|
|
||||||
|
.. versionadded:: 0.3.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: (:class:`wand.image.Image`) The container image.
|
||||||
|
container = None
|
||||||
|
|
||||||
|
def __init__(self, wand, container, c_original_resource):
|
||||||
|
super(SingleImage, self).__init__(wand)
|
||||||
|
self.container = container
|
||||||
|
self.c_original_resource = c_original_resource
|
||||||
|
self._delay = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sequence(self):
|
||||||
|
return self,
|
||||||
|
|
||||||
|
@property
|
||||||
|
def index(self):
|
||||||
|
"""(:class:`numbers.Integral`) The index of the single image in
|
||||||
|
the :attr:`container` image.
|
||||||
|
|
||||||
|
"""
|
||||||
|
wand = self.container.wand
|
||||||
|
library.MagickResetIterator(wand)
|
||||||
|
image = library.GetImageFromMagickWand(wand)
|
||||||
|
i = 0
|
||||||
|
while self.c_original_resource != image and image:
|
||||||
|
image = libmagick.GetNextImageInList(image)
|
||||||
|
i += 1
|
||||||
|
assert image
|
||||||
|
assert self.c_original_resource == image
|
||||||
|
return i
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delay(self):
|
||||||
|
"""(:class:`numbers.Integral`) The delay to pause before display
|
||||||
|
the next image (in the :attr:`~wand.image.BaseImage.sequence` of
|
||||||
|
its :attr:`container`). It's hundredths of a second.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._delay is None:
|
||||||
|
container = self.container
|
||||||
|
with container.sequence.index_context(self.index):
|
||||||
|
self._delay = library.MagickGetImageDelay(container.wand)
|
||||||
|
return self._delay
|
||||||
|
|
||||||
|
@delay.setter
|
||||||
|
def delay(self, delay):
|
||||||
|
if not isinstance(delay, numbers.Integral):
|
||||||
|
raise TypeError('delay must be an integer, not ' + repr(delay))
|
||||||
|
elif delay < 0:
|
||||||
|
raise ValueError('delay cannot be less than zero')
|
||||||
|
self._delay = delay
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
if self.dirty:
|
||||||
|
self.container.sequence[self.index] = self
|
||||||
|
if self._delay is not None:
|
||||||
|
container = self.container
|
||||||
|
with container.sequence.index_context(self.index):
|
||||||
|
library.MagickSetImageDelay(container.wand, self._delay)
|
||||||
|
super(SingleImage, self).destroy()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
cls = type(self)
|
||||||
|
if getattr(self, 'c_resource', None) is None:
|
||||||
|
return '<{0}.{1}: (closed)>'.format(cls.__module__, cls.__name__)
|
||||||
|
return '<{0}.{1}: {2} ({3}x{4})>'.format(
|
||||||
|
cls.__module__, cls.__name__,
|
||||||
|
self.signature[:7], self.width, self.height
|
||||||
|
)
|
Binary file not shown.
@ -0,0 +1,251 @@
|
|||||||
|
""":mod:`wand.version` --- Version data
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can find the current version in the command line interface:
|
||||||
|
|
||||||
|
.. sourcecode:: console
|
||||||
|
|
||||||
|
$ python -m wand.version
|
||||||
|
0.0.0
|
||||||
|
$ python -m wand.version --verbose
|
||||||
|
Wand 0.0.0
|
||||||
|
ImageMagick 6.7.7-6 2012-06-03 Q16 http://www.imagemagick.org
|
||||||
|
$ python -m wand.version --config | grep CC | cut -d : -f 2
|
||||||
|
gcc -std=gnu99 -std=gnu99
|
||||||
|
$ python -m wand.version --fonts | grep Helvetica
|
||||||
|
Helvetica
|
||||||
|
Helvetica-Bold
|
||||||
|
Helvetica-Light
|
||||||
|
Helvetica-Narrow
|
||||||
|
Helvetica-Oblique
|
||||||
|
$ python -m wand.version --formats | grep CMYK
|
||||||
|
CMYK
|
||||||
|
CMYKA
|
||||||
|
|
||||||
|
.. versionadded:: 0.2.0
|
||||||
|
The command line interface.
|
||||||
|
|
||||||
|
.. versionadded:: 0.2.2
|
||||||
|
The ``--verbose``/``-v`` option which also prints ImageMagick library
|
||||||
|
version for CLI.
|
||||||
|
|
||||||
|
.. versionadded:: 0.4.1
|
||||||
|
The ``--fonts``, ``--formats``, & ``--config`` option allows printing
|
||||||
|
additional information about ImageMagick library.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .api import libmagick, library
|
||||||
|
except ImportError:
|
||||||
|
libmagick = None
|
||||||
|
from .compat import binary, string_type, text
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('VERSION', 'VERSION_INFO', 'MAGICK_VERSION',
|
||||||
|
'MAGICK_VERSION_INFO', 'MAGICK_VERSION_NUMBER',
|
||||||
|
'MAGICK_RELEASE_DATE', 'MAGICK_RELEASE_DATE_STRING',
|
||||||
|
'QUANTUM_DEPTH', 'configure_options', 'fonts', 'formats')
|
||||||
|
|
||||||
|
#: (:class:`tuple`) The version tuple e.g. ``(0, 1, 2)``.
|
||||||
|
#:
|
||||||
|
#: .. versionchanged:: 0.1.9
|
||||||
|
#: Becomes :class:`tuple`. (It was string before.)
|
||||||
|
VERSION_INFO = (0, 4, 2)
|
||||||
|
|
||||||
|
#: (:class:`basestring`) The version string e.g. ``'0.1.2'``.
|
||||||
|
#:
|
||||||
|
#: .. versionchanged:: 0.1.9
|
||||||
|
#: Becomes string. (It was :class:`tuple` before.)
|
||||||
|
VERSION = '{0}.{1}.{2}'.format(*VERSION_INFO)
|
||||||
|
|
||||||
|
if libmagick:
|
||||||
|
c_magick_version = ctypes.c_size_t()
|
||||||
|
#: (:class:`basestring`) The version string of the linked ImageMagick
|
||||||
|
#: library. The exactly same string to the result of
|
||||||
|
#: :c:func:`GetMagickVersion` function.
|
||||||
|
#:
|
||||||
|
#: Example::
|
||||||
|
#:
|
||||||
|
#: 'ImageMagick 6.7.7-6 2012-06-03 Q16 http://www.imagemagick.org'
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.2.1
|
||||||
|
MAGICK_VERSION = text(
|
||||||
|
libmagick.GetMagickVersion(ctypes.byref(c_magick_version))
|
||||||
|
)
|
||||||
|
|
||||||
|
#: (:class:`numbers.Integral`) The version number of the linked
|
||||||
|
#: ImageMagick library.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.2.1
|
||||||
|
MAGICK_VERSION_NUMBER = c_magick_version.value
|
||||||
|
|
||||||
|
_match = re.match(r'^ImageMagick\s+(\d+)\.(\d+)\.(\d+)(?:-(\d+))?',
|
||||||
|
MAGICK_VERSION)
|
||||||
|
#: (:class:`tuple`) The version tuple e.g. ``(6, 7, 7, 6)`` of
|
||||||
|
#: :const:`MAGICK_VERSION`.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.2.1
|
||||||
|
MAGICK_VERSION_INFO = tuple(int(v or 0) for v in _match.groups())
|
||||||
|
|
||||||
|
#: (:class:`datetime.date`) The release date of the linked ImageMagick
|
||||||
|
#: library. The same to the result of :c:func:`GetMagickReleaseDate`
|
||||||
|
#: function.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.2.1
|
||||||
|
MAGICK_RELEASE_DATE_STRING = text(libmagick.GetMagickReleaseDate())
|
||||||
|
|
||||||
|
#: (:class:`basestring`) The date string e.g. ``'2012-06-03'`` of
|
||||||
|
#: :const:`MAGICK_RELEASE_DATE_STRING`. This value is the exactly same
|
||||||
|
#: string to the result of :c:func:`GetMagickReleaseDate` function.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.2.1
|
||||||
|
MAGICK_RELEASE_DATE = datetime.date(
|
||||||
|
*map(int, MAGICK_RELEASE_DATE_STRING.split('-')))
|
||||||
|
|
||||||
|
c_quantum_depth = ctypes.c_size_t()
|
||||||
|
libmagick.GetMagickQuantumDepth(ctypes.byref(c_quantum_depth))
|
||||||
|
#: (:class:`numbers.Integral`) The quantum depth configuration of
|
||||||
|
#: the linked ImageMagick library. One of 8, 16, 32, or 64.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.3.0
|
||||||
|
QUANTUM_DEPTH = c_quantum_depth.value
|
||||||
|
|
||||||
|
del c_magick_version, _match, c_quantum_depth
|
||||||
|
|
||||||
|
|
||||||
|
def configure_options(pattern='*'):
|
||||||
|
"""
|
||||||
|
Queries ImageMagick library for configurations options given at
|
||||||
|
compile-time.
|
||||||
|
|
||||||
|
Example: Find where the ImageMagick documents are installed::
|
||||||
|
|
||||||
|
>>> from wand.version import configure_options
|
||||||
|
>>> configure_options('DOC*')
|
||||||
|
{'DOCUMENTATION_PATH': '/usr/local/share/doc/ImageMagick-6'}
|
||||||
|
|
||||||
|
:param pattern: A term to filter queries against. Supports wildcard '*'
|
||||||
|
characters. Default patterns '*' for all options.
|
||||||
|
:type pattern: :class:`basestring`
|
||||||
|
:returns: Directory of configuration options matching given pattern
|
||||||
|
:rtype: :class:`collections.defaultdict`
|
||||||
|
"""
|
||||||
|
if not isinstance(pattern, string_type):
|
||||||
|
raise TypeError('pattern must be a string, not ' + repr(pattern))
|
||||||
|
pattern_p = ctypes.create_string_buffer(binary(pattern))
|
||||||
|
config_count = ctypes.c_size_t(0)
|
||||||
|
configs = {}
|
||||||
|
configs_p = library.MagickQueryConfigureOptions(pattern_p,
|
||||||
|
ctypes.byref(config_count))
|
||||||
|
cursor = 0
|
||||||
|
while cursor < config_count.value:
|
||||||
|
config = configs_p[cursor].value
|
||||||
|
value = library.MagickQueryConfigureOption(config)
|
||||||
|
configs[text(config)] = text(value.value)
|
||||||
|
cursor += 1
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
|
def fonts(pattern='*'):
|
||||||
|
"""
|
||||||
|
Queries ImageMagick library for available fonts.
|
||||||
|
|
||||||
|
Available fonts can be configured by defining `types.xml`,
|
||||||
|
`type-ghostscript.xml`, or `type-windows.xml`.
|
||||||
|
Use :func:`wand.version.configure_options` to locate system search path,
|
||||||
|
and `resources <http://www.imagemagick.org/script/resources.php>`_
|
||||||
|
article for defining xml file.
|
||||||
|
|
||||||
|
Example: List all bold Helvetica fonts::
|
||||||
|
|
||||||
|
>>> from wand.version import fonts
|
||||||
|
>>> fonts('*Helvetica*Bold*')
|
||||||
|
['Helvetica-Bold', 'Helvetica-Bold-Oblique', 'Helvetica-BoldOblique',
|
||||||
|
'Helvetica-Narrow-Bold', 'Helvetica-Narrow-BoldOblique']
|
||||||
|
|
||||||
|
|
||||||
|
:param pattern: A term to filter queries against. Supports wildcard '*'
|
||||||
|
characters. Default patterns '*' for all options.
|
||||||
|
:type pattern: :class:`basestring`
|
||||||
|
:returns: Sequence of matching fonts
|
||||||
|
:rtype: :class:`collections.Sequence`
|
||||||
|
"""
|
||||||
|
if not isinstance(pattern, string_type):
|
||||||
|
raise TypeError('pattern must be a string, not ' + repr(pattern))
|
||||||
|
pattern_p = ctypes.create_string_buffer(binary(pattern))
|
||||||
|
number_fonts = ctypes.c_size_t(0)
|
||||||
|
fonts = []
|
||||||
|
fonts_p = library.MagickQueryFonts(pattern_p,
|
||||||
|
ctypes.byref(number_fonts))
|
||||||
|
cursor = 0
|
||||||
|
while cursor < number_fonts.value:
|
||||||
|
font = fonts_p[cursor].value
|
||||||
|
fonts.append(text(font))
|
||||||
|
cursor += 1
|
||||||
|
return fonts
|
||||||
|
|
||||||
|
|
||||||
|
def formats(pattern='*'):
|
||||||
|
"""
|
||||||
|
Queries ImageMagick library for supported formats.
|
||||||
|
|
||||||
|
Example: List supported PNG formats::
|
||||||
|
|
||||||
|
>>> from wand.version import formats
|
||||||
|
>>> formats('PNG*')
|
||||||
|
['PNG', 'PNG00', 'PNG8', 'PNG24', 'PNG32', 'PNG48', 'PNG64']
|
||||||
|
|
||||||
|
|
||||||
|
:param pattern: A term to filter formats against. Supports wildcards '*'
|
||||||
|
characters. Default pattern '*' for all formats.
|
||||||
|
:type pattern: :class:`basestring`
|
||||||
|
:returns: Sequence of matching formats
|
||||||
|
:rtype: :class:`collections.Sequence`
|
||||||
|
"""
|
||||||
|
if not isinstance(pattern, string_type):
|
||||||
|
raise TypeError('pattern must be a string, not ' + repr(pattern))
|
||||||
|
pattern_p = ctypes.create_string_buffer(binary(pattern))
|
||||||
|
number_formats = ctypes.c_size_t(0)
|
||||||
|
formats = []
|
||||||
|
formats_p = library.MagickQueryFormats(pattern_p,
|
||||||
|
ctypes.byref(number_formats))
|
||||||
|
cursor = 0
|
||||||
|
while cursor < number_formats.value:
|
||||||
|
value = formats_p[cursor].value
|
||||||
|
formats.append(text(value))
|
||||||
|
cursor += 1
|
||||||
|
return formats
|
||||||
|
|
||||||
|
if __doc__ is not None:
|
||||||
|
__doc__ = __doc__.replace('0.0.0', VERSION)
|
||||||
|
|
||||||
|
del libmagick
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
options = frozenset(sys.argv[1:])
|
||||||
|
if '-v' in options or '--verbose' in options:
|
||||||
|
print('Wand', VERSION)
|
||||||
|
try:
|
||||||
|
print(MAGICK_VERSION)
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
elif '--fonts' in options:
|
||||||
|
for font in fonts():
|
||||||
|
print(font)
|
||||||
|
elif '--formats' in options:
|
||||||
|
for supported_format in formats():
|
||||||
|
print(supported_format)
|
||||||
|
elif '--config' in options:
|
||||||
|
config_options = configure_options()
|
||||||
|
for key in config_options:
|
||||||
|
print('{:24s}: {}'.format(key, config_options[key]))
|
||||||
|
else:
|
||||||
|
print(VERSION)
|
Binary file not shown.
Loading…
Reference in New Issue