Merge pull request #8 from cervinko/master

Upload PDF Files
pull/11/head
Jan B 9 years ago
commit 2a3680b099

@ -25,99 +25,99 @@ conn.connection.create_function('title_sort', 1, title_sort)
Base = declarative_base() Base = declarative_base()
books_authors_link = Table('books_authors_link', Base.metadata, books_authors_link = Table('books_authors_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'), primary_key=True), Column('book', Integer, ForeignKey('books.id'), primary_key=True),
Column('author', Integer, ForeignKey('authors.id'), primary_key=True) Column('author', Integer, ForeignKey('authors.id'), primary_key=True)
) )
books_tags_link = Table('books_tags_link', Base.metadata, books_tags_link = Table('books_tags_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'), primary_key=True), Column('book', Integer, ForeignKey('books.id'), primary_key=True),
Column('tag', Integer, ForeignKey('tags.id'), primary_key=True) Column('tag', Integer, ForeignKey('tags.id'), primary_key=True)
) )
books_series_link = Table('books_series_link', Base.metadata, books_series_link = Table('books_series_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'), primary_key=True), Column('book', Integer, ForeignKey('books.id'), primary_key=True),
Column('series', Integer, ForeignKey('series.id'), primary_key=True) Column('series', Integer, ForeignKey('series.id'), primary_key=True)
) )
books_ratings_link = Table('books_ratings_link', Base.metadata, books_ratings_link = Table('books_ratings_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'), primary_key=True), Column('book', Integer, ForeignKey('books.id'), primary_key=True),
Column('rating', Integer, ForeignKey('ratings.id'), primary_key=True) Column('rating', Integer, ForeignKey('ratings.id'), primary_key=True)
) )
books_languages_link = Table('books_languages_link', Base.metadata, books_languages_link = Table('books_languages_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'), primary_key=True), Column('book', Integer, ForeignKey('books.id'), primary_key=True),
Column('lang_code', Integer, ForeignKey('languages.id'), primary_key=True) Column('lang_code', Integer, ForeignKey('languages.id'), primary_key=True)
) )
class Comments(Base): class Comments(Base):
__tablename__ = 'comments' __tablename__ = 'comments'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
text = Column(String) text = Column(String)
book = Column(Integer, ForeignKey('books.id')) book = Column(Integer, ForeignKey('books.id'))
def __init__(self, text, book): def __init__(self, text, book):
self.text = text self.text = text
self.book = book self.book = book
def __repr__(self): def __repr__(self):
return u"<Comments({0})>".format(self.text) return u"<Comments({0})>".format(self.text)
class Tags(Base): class Tags(Base):
__tablename__ = 'tags' __tablename__ = 'tags'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String) name = Column(String)
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def __repr__(self): def __repr__(self):
return u"<Tags('{0})>".format(self.name) return u"<Tags('{0})>".format(self.name)
class Authors(Base): class Authors(Base):
__tablename__ = 'authors' __tablename__ = 'authors'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String) name = Column(String)
sort = Column(String) sort = Column(String)
link = Column(String) link = Column(String)
def __init__(self, name, sort, link): def __init__(self, name, sort, link):
self.name = name self.name = name
self.sort = sort self.sort = sort
self.link = link self.link = link
def __repr__(self): def __repr__(self):
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link) return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
class Series(Base): class Series(Base):
__tablename__ = 'series' __tablename__ = 'series'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String) name = Column(String)
sort = Column(String) sort = Column(String)
def __init__(self, name, sort): def __init__(self, name, sort):
self.name = name self.name = name
self.sort = sort self.sort = sort
def __repr__(self): def __repr__(self):
return u"<Series('{0},{1}')>".format(self.name, self.sort) return u"<Series('{0},{1}')>".format(self.name, self.sort)
class Ratings(Base): class Ratings(Base):
__tablename__ = 'ratings' __tablename__ = 'ratings'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
rating = Column(Integer) rating = Column(Integer)
def __init__(self,rating): def __init__(self,rating):
self.rating = rating self.rating = rating
def __repr__(self): def __repr__(self):
return u"<Ratings('{0}')>".format(self.rating) return u"<Ratings('{0}')>".format(self.rating)
class Languages(Base): class Languages(Base):
__tablename__ = 'languages' __tablename__ = 'languages'
@ -132,59 +132,58 @@ class Languages(Base):
return u"<Languages('{0}')>".format(self.lang_code) return u"<Languages('{0}')>".format(self.lang_code)
class Data(Base): class Data(Base):
__tablename__ = 'data' __tablename__ = 'data'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
book = Column(Integer, ForeignKey('books.id')) book = Column(Integer, ForeignKey('books.id'))
format = Column(String) format = Column(String)
uncompressed_size = Column(Integer) uncompressed_size = Column(Integer)
name = Column(String) name = Column(String)
def __init__(self, book, format, uncompressed_size, name): def __init__(self, book, format, uncompressed_size, name):
self.book = book self.book = book
self.format = format self.format = format
self.uncompressed_size = uncompressed_size self.uncompressed_size = uncompressed_size
self.name = name self.name = name
def __repr__(self): def __repr__(self):
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name) return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
class Books(Base): class Books(Base):
__tablename__ = 'books' __tablename__ = 'books'
id = Column(Integer,primary_key=True) id = Column(Integer,primary_key=True)
title = Column(String) title = Column(String)
sort = Column(String) sort = Column(String)
author_sort = Column(String) author_sort = Column(String)
timestamp = Column(String) timestamp = Column(String)
pubdate = Column(String) pubdate = Column(String)
series_index = Column(String) series_index = Column(String)
last_modified = Column(String) last_modified = Column(String)
path = Column(String) path = Column(String)
has_cover = Column(Integer) has_cover = Column(Integer)
authors = relationship('Authors', secondary=books_authors_link, backref='books') authors = relationship('Authors', secondary=books_authors_link, backref='books')
tags = relationship('Tags', secondary=books_tags_link, backref='books') tags = relationship('Tags', secondary=books_tags_link, backref='books')
comments = relationship('Comments', backref='books') comments = relationship('Comments', backref='books')
data = relationship('Data', backref='books') data = relationship('Data', backref='books')
series = relationship('Series', secondary=books_series_link, backref='books') series = relationship('Series', secondary=books_series_link, backref='books')
ratings = relationship('Ratings', secondary=books_ratings_link, backref='books') ratings = relationship('Ratings', secondary=books_ratings_link, backref='books')
languages = relationship('Languages', secondary=books_languages_link, backref='books') languages = relationship('Languages', secondary=books_languages_link, backref='books')
def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, authors, tags): def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, authors, tags):
self.title = title self.title = title
self.sort = sort self.sort = sort
self.author_sort = author_sort self.author_sort = author_sort
self.timestamp = timestamp self.timestamp = timestamp
self.pubdate = pubdate self.pubdate = pubdate
self.series_index = series_index self.series_index = series_index
self.last_modified = last_modified self.last_modified = last_modified
self.path = path self.path = path
self.has_cover = has_cover self.has_cover = has_cover
self.tags = tags
def __repr__(self):
def __repr__(self): return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort, self.timestamp, self.pubdate, self.series_index, self.last_modified ,self.path, self.has_cover)
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort, self.timestamp, self.pubdate, self.series_index, self.last_modified ,self.path, self.has_cover)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session = sessionmaker() Session = sessionmaker()

@ -154,7 +154,7 @@ def get_attachment(file_path):
'permissions?') 'permissions?')
return None return None
def get_valid_filename(value): def get_valid_filename(value, replace_whitespace=True):
""" """
Returns the given string converted to a string that can be used for a clean Returns the given string converted to a string that can be used for a clean
filename. Limits num characters to 128 max. filename. Limits num characters to 128 max.
@ -164,7 +164,9 @@ def get_valid_filename(value):
value = unicodedata.normalize('NFKD', value) value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile('[^\w\s-]', re.UNICODE) re_slugify = re.compile('[^\w\s-]', re.UNICODE)
value = unicode(re_slugify.sub('', value).strip()) value = unicode(re_slugify.sub('', value).strip())
value = re.sub('[\s]+', '_', value, flags=re.U) if replace_whitespace:
value = re.sub('[\s]+', '_', value, flags=re.U)
value = value.replace(u"\u00DF", "ss")
return value return value
def get_normalized_author(value): def get_normalized_author(value):
@ -175,3 +177,25 @@ def get_normalized_author(value):
value = re.sub('[^\w,\s]', '', value, flags=re.U) value = re.sub('[^\w,\s]', '', value, flags=re.U)
value = " ".join(value.split(", ")[::-1]) value = " ".join(value.split(", ")[::-1])
return value return value
def update_dir_stucture(book_id):
db.session.connection().connection.connection.create_function("title_sort",1,db.title_sort)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
path = os.path.join(config.DB_ROOT, book.path)
authordir = book.path.split("/")[0]
new_authordir=get_valid_filename(book.authors[0].name, False)
titledir = book.path.split("/")[1]
new_titledir = get_valid_filename(book.title, False) + " (" + str(book_id) + ")"
if titledir != new_titledir:
new_title_path = os.path.join(os.path.dirname(path), new_titledir)
os.rename(path, new_title_path)
path = new_title_path
book.path = book.path.split("/")[0] + "/" + new_titledir
if authordir != new_authordir:
new_author_path = os.path.join(os.path.join(config.DB_ROOT, new_authordir), os.path.basename(path))
os.renames(path, new_author_path)
book.path = new_authordir + "/" + book.path.split("/")[1]
db.session.commit()

@ -27,4 +27,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
-webkit-box-shadow: 0 5px 8px -6px #777; -webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777; -moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777;
} }
.btn-file {position: relative; overflow: hidden;}
.btn-file input[type=file] {position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

@ -39,7 +39,6 @@
<div class="discover load-more"> <div class="discover load-more">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<div class="row"> <div class="row">
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">

@ -31,6 +31,13 @@
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body> <body>
<script>
$(document).ready(function(){
$("#btn-upload").change(function() {
$("#form-upload").submit();
});
});
</script>
<!-- Static navbar --> <!-- Static navbar -->
<div class="navbar navbar-default navbar-static-top" role="navigation"> <div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid"> <div class="container-fluid">
@ -56,6 +63,15 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right" id="main-nav"> <ul class="nav navbar-nav navbar-right" id="main-nav">
{% if g.user.is_authenticated() %} {% if g.user.is_authenticated() %}
{% if g.user.role %}
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data">
<div class="form-group">
<span class="btn btn-default btn-file">Upload <input id="btn-upload" name="btn-upload" type="file"></span>
</div>
</form>
</li>
{% endif %}
{% if g.user.role %} {% if g.user.role %}
<li><a href="{{url_for('user_list')}}"><span class="glyphicon glyphicon-dashboard"></span> Admin</a></li> <li><a href="{{url_for('user_list')}}"><span class="glyphicon glyphicon-dashboard"></span> Admin</a></li>
{% endif %} {% endif %}

@ -21,6 +21,14 @@ from functools import wraps
import base64 import base64
from sqlalchemy.sql import * from sqlalchemy.sql import *
import json import json
import datetime
from uuid import uuid4
try:
from wand.image import Image
use_generic_pdf_cover = False
except ImportError, e:
use_generic_pdf_cover = True
from shutil import copyfile
app = (Flask(__name__)) app = (Flask(__name__))
@ -235,12 +243,12 @@ def get_opds_download_link(book_id, format):
return response return response
@app.route("/get_authors_json", methods = ['GET', 'POST']) @app.route("/get_authors_json", methods = ['GET', 'POST'])
def get_authors_json(): def get_authors_json():
if request.method == "POST": if request.method == "POST":
form = request.form.to_dict() form = request.form.to_dict()
entries = db.session.execute("select name from authors where name like '%" + form['query'] + "%'") entries = db.session.execute("select name from authors where name like '%" + form['query'] + "%'")
return json.dumps([dict(r) for r in entries]) return json.dumps([dict(r) for r in entries])
@app.route("/", defaults={'page': 1}) @app.route("/", defaults={'page': 1})
@app.route('/page/<int:page>') @app.route('/page/<int:page>')
@ -671,28 +679,34 @@ def edit_book(book_id):
db.session.connection().connection.connection.create_function("title_sort",1,db.title_sort) db.session.connection().connection.connection.create_function("title_sort",1,db.title_sort)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first() book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
if request.method == 'POST': if request.method == 'POST':
edited_books_id = set()
to_save = request.form.to_dict() to_save = request.form.to_dict()
book.title = to_save["book_title"] if book.title != to_save["book_title"]:
book.title = to_save["book_title"]
author_id = book.authors[0].id edited_books_id.add(book.id)
is_author = db.session.query(db.Authors).filter(db.Authors.name == to_save["author_name"].strip()).first() author_id = book.authors[0].id
if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "): if book.authors[0].name != to_save["author_name"].strip():
if is_author: is_author = db.session.query(db.Authors).filter(db.Authors.name == to_save["author_name"].strip()).first()
book.authors.append(is_author) edited_books_id.add(book.id)
book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id)) if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "):
authors_books_count = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).count() if is_author:
if authors_books_count == 0: book.authors.append(is_author)
db.session.query(db.Authors).filter(db.Authors.id == author_id).delete() book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id))
else: authors_books_count = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).count()
book.authors[0].name = to_save["author_name"].strip() if authors_books_count == 0:
else: db.session.query(db.Authors).filter(db.Authors.id == author_id).delete()
if is_author: else:
book.authors.append(is_author) book.authors[0].name = to_save["author_name"].strip()
for linked_book in db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).all():
edited_books_id.add(linked_book.id)
else: else:
book.authors.append(db.Authors(to_save["author_name"].strip(), "", "")) if is_author:
book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id)) book.authors.append(is_author)
else:
book.authors.append(db.Authors(to_save["author_name"].strip(), "", ""))
book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id))
if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg": if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg":
img = requests.get(to_save["cover_url"]) img = requests.get(to_save["cover_url"])
f = open(os.path.join(config.DB_ROOT, book.path, "cover.jpg"), "wb") f = open(os.path.join(config.DB_ROOT, book.path, "cover.jpg"), "wb")
@ -729,9 +743,64 @@ def edit_book(book_id):
new_rating = db.Ratings(rating=int(to_save["rating"].strip())) new_rating = db.Ratings(rating=int(to_save["rating"].strip()))
book.ratings[0] = new_rating book.ratings[0] = new_rating
db.session.commit() db.session.commit()
for b in edited_books_id:
helper.update_dir_stucture(b)
if "detail_view" in to_save: if "detail_view" in to_save:
return redirect(url_for('show_book', id=book.id)) return redirect(url_for('show_book', id=book.id))
else: else:
return render_template('edit_book.html', book=book) return render_template('edit_book.html', book=book)
else: else:
return render_template('edit_book.html', book=book) return render_template('edit_book.html', book=book)
@app.route("/upload", methods = ["GET", "POST"])
@login_required
@admin_required
def upload():
## create the function for sorting...
db.session.connection().connection.connection.create_function("title_sort",1,db.title_sort)
db.session.connection().connection.connection.create_function('uuid4', 0, lambda : str(uuid4()))
if request.method == 'POST' and 'btn-upload' in request.files:
file = request.files['btn-upload']
filename = file.filename
filename_root, fileextension = os.path.splitext(filename)
if fileextension.upper() == ".PDF":
title = filename_root
author = "Unknown"
else:
flash("Upload is only available for PDF files", category="error")
return redirect(url_for('index'))
title_dir = helper.get_valid_filename(title, False)
author_dir = helper.get_valid_filename(author.decode('utf-8'), False)
data_name = title_dir
filepath = config.DB_ROOT + "/" + author_dir + "/" + title_dir
saved_filename = filepath + "/" + data_name + fileextension
if not os.path.exists(filepath):
os.makedirs(filepath)
file.save(saved_filename)
file_size = os.path.getsize(saved_filename)
has_cover = 0
if fileextension.upper() == ".PDF":
if use_generic_pdf_cover:
basedir = os.path.dirname(__file__)
print basedir
copyfile(os.path.join(basedir, "static/generic_cover.jpg"), os.path.join(filepath, "cover.jpg"))
else:
with Image(filename=saved_filename + "[0]", resolution=150) as img:
img.compression_quality = 88
img.save(filename=os.path.join(filepath, "cover.jpg"))
has_cover = 1
is_author = db.session.query(db.Authors).filter(db.Authors.name == author).first()
if is_author:
db_author = is_author
else:
db_author = db.Authors(author, "", "")
db.session.add(db_author)
db_book = db.Books(title, "", "", datetime.datetime.now(), datetime.datetime(101, 01,01), 1, datetime.datetime.now(), author_dir + "/" + title_dir, has_cover, db_author, [])
db_book.authors.append(db_author)
db_data = db.Data(db_book, fileextension.upper()[1:], file_size, data_name)
db_book.data.append(db_data)
db.session.add(db_book)
db.session.commit()
return render_template('edit_book.html', book=db_book)

@ -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;">&nbsp;</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…
Cancel
Save