mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-28 19:10:31 +00:00
commit
2a3680b099
203
cps/db.py
203
cps/db.py
@ -25,99 +25,99 @@ conn.connection.create_function('title_sort', 1, title_sort)
|
||||
Base = declarative_base()
|
||||
|
||||
books_authors_link = Table('books_authors_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('author', Integer, ForeignKey('authors.id'), primary_key=True)
|
||||
)
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('author', Integer, ForeignKey('authors.id'), primary_key=True)
|
||||
)
|
||||
|
||||
books_tags_link = Table('books_tags_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('tag', Integer, ForeignKey('tags.id'), primary_key=True)
|
||||
)
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('tag', Integer, ForeignKey('tags.id'), primary_key=True)
|
||||
)
|
||||
|
||||
books_series_link = Table('books_series_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('series', Integer, ForeignKey('series.id'), primary_key=True)
|
||||
)
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('series', Integer, ForeignKey('series.id'), primary_key=True)
|
||||
)
|
||||
|
||||
books_ratings_link = Table('books_ratings_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('rating', Integer, ForeignKey('ratings.id'), primary_key=True)
|
||||
)
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('rating', Integer, ForeignKey('ratings.id'), primary_key=True)
|
||||
)
|
||||
|
||||
books_languages_link = Table('books_languages_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('lang_code', Integer, ForeignKey('languages.id'), primary_key=True)
|
||||
)
|
||||
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
|
||||
Column('lang_code', Integer, ForeignKey('languages.id'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class Comments(Base):
|
||||
__tablename__ = 'comments'
|
||||
__tablename__ = 'comments'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
text = Column(String)
|
||||
book = Column(Integer, ForeignKey('books.id'))
|
||||
id = Column(Integer, primary_key=True)
|
||||
text = Column(String)
|
||||
book = Column(Integer, ForeignKey('books.id'))
|
||||
|
||||
def __init__(self, text, book):
|
||||
self.text = text
|
||||
self.book = book
|
||||
def __init__(self, text, book):
|
||||
self.text = text
|
||||
self.book = book
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Comments({0})>".format(self.text)
|
||||
def __repr__(self):
|
||||
return u"<Comments({0})>".format(self.text)
|
||||
|
||||
|
||||
class Tags(Base):
|
||||
__tablename__ = 'tags'
|
||||
__tablename__ = 'tags'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String)
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String)
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Tags('{0})>".format(self.name)
|
||||
def __repr__(self):
|
||||
return u"<Tags('{0})>".format(self.name)
|
||||
|
||||
class Authors(Base):
|
||||
__tablename__ = 'authors'
|
||||
__tablename__ = 'authors'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
sort = Column(String)
|
||||
link = Column(String)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
sort = Column(String)
|
||||
link = Column(String)
|
||||
|
||||
def __init__(self, name, sort, link):
|
||||
self.name = name
|
||||
self.sort = sort
|
||||
self.link = link
|
||||
def __init__(self, name, sort, link):
|
||||
self.name = name
|
||||
self.sort = sort
|
||||
self.link = link
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
||||
def __repr__(self):
|
||||
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
||||
|
||||
class Series(Base):
|
||||
__tablename__ = 'series'
|
||||
__tablename__ = 'series'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
sort = Column(String)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
sort = Column(String)
|
||||
|
||||
def __init__(self, name, sort):
|
||||
self.name = name
|
||||
self.sort = sort
|
||||
def __init__(self, name, sort):
|
||||
self.name = name
|
||||
self.sort = sort
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Series('{0},{1}')>".format(self.name, self.sort)
|
||||
def __repr__(self):
|
||||
return u"<Series('{0},{1}')>".format(self.name, self.sort)
|
||||
|
||||
class Ratings(Base):
|
||||
__tablename__ = 'ratings'
|
||||
__tablename__ = 'ratings'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
rating = Column(Integer)
|
||||
id = Column(Integer, primary_key=True)
|
||||
rating = Column(Integer)
|
||||
|
||||
def __init__(self,rating):
|
||||
self.rating = rating
|
||||
def __init__(self,rating):
|
||||
self.rating = rating
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Ratings('{0}')>".format(self.rating)
|
||||
def __repr__(self):
|
||||
return u"<Ratings('{0}')>".format(self.rating)
|
||||
|
||||
class Languages(Base):
|
||||
__tablename__ = 'languages'
|
||||
@ -132,59 +132,58 @@ class Languages(Base):
|
||||
return u"<Languages('{0}')>".format(self.lang_code)
|
||||
|
||||
class Data(Base):
|
||||
__tablename__ = 'data'
|
||||
__tablename__ = 'data'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
book = Column(Integer, ForeignKey('books.id'))
|
||||
format = Column(String)
|
||||
uncompressed_size = Column(Integer)
|
||||
name = Column(String)
|
||||
id = Column(Integer, primary_key=True)
|
||||
book = Column(Integer, ForeignKey('books.id'))
|
||||
format = Column(String)
|
||||
uncompressed_size = Column(Integer)
|
||||
name = Column(String)
|
||||
|
||||
def __init__(self, book, format, uncompressed_size, name):
|
||||
self.book = book
|
||||
self.format = format
|
||||
self.uncompressed_size = uncompressed_size
|
||||
self.name = name
|
||||
def __init__(self, book, format, uncompressed_size, name):
|
||||
self.book = book
|
||||
self.format = format
|
||||
self.uncompressed_size = uncompressed_size
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
||||
def __repr__(self):
|
||||
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
||||
|
||||
class Books(Base):
|
||||
__tablename__ = 'books'
|
||||
__tablename__ = 'books'
|
||||
|
||||
id = Column(Integer,primary_key=True)
|
||||
title = Column(String)
|
||||
sort = Column(String)
|
||||
author_sort = Column(String)
|
||||
timestamp = Column(String)
|
||||
pubdate = Column(String)
|
||||
series_index = Column(String)
|
||||
last_modified = Column(String)
|
||||
path = Column(String)
|
||||
has_cover = Column(Integer)
|
||||
id = Column(Integer,primary_key=True)
|
||||
title = Column(String)
|
||||
sort = Column(String)
|
||||
author_sort = Column(String)
|
||||
timestamp = Column(String)
|
||||
pubdate = Column(String)
|
||||
series_index = Column(String)
|
||||
last_modified = Column(String)
|
||||
path = Column(String)
|
||||
has_cover = Column(Integer)
|
||||
|
||||
authors = relationship('Authors', secondary=books_authors_link, backref='books')
|
||||
tags = relationship('Tags', secondary=books_tags_link, backref='books')
|
||||
comments = relationship('Comments', backref='books')
|
||||
data = relationship('Data', backref='books')
|
||||
series = relationship('Series', secondary=books_series_link, backref='books')
|
||||
ratings = relationship('Ratings', secondary=books_ratings_link, backref='books')
|
||||
languages = relationship('Languages', secondary=books_languages_link, backref='books')
|
||||
authors = relationship('Authors', secondary=books_authors_link, backref='books')
|
||||
tags = relationship('Tags', secondary=books_tags_link, backref='books')
|
||||
comments = relationship('Comments', backref='books')
|
||||
data = relationship('Data', backref='books')
|
||||
series = relationship('Series', secondary=books_series_link, backref='books')
|
||||
ratings = relationship('Ratings', secondary=books_ratings_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):
|
||||
self.title = title
|
||||
self.sort = sort
|
||||
self.author_sort = author_sort
|
||||
self.timestamp = timestamp
|
||||
self.pubdate = pubdate
|
||||
self.series_index = series_index
|
||||
self.last_modified = last_modified
|
||||
self.path = path
|
||||
self.has_cover = has_cover
|
||||
self.tags = tags
|
||||
def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, authors, tags):
|
||||
self.title = title
|
||||
self.sort = sort
|
||||
self.author_sort = author_sort
|
||||
self.timestamp = timestamp
|
||||
self.pubdate = pubdate
|
||||
self.series_index = series_index
|
||||
self.last_modified = last_modified
|
||||
self.path = path
|
||||
self.has_cover = has_cover
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker()
|
||||
|
@ -154,7 +154,7 @@ def get_attachment(file_path):
|
||||
'permissions?')
|
||||
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
|
||||
filename. Limits num characters to 128 max.
|
||||
@ -164,7 +164,9 @@ def get_valid_filename(value):
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
re_slugify = re.compile('[^\w\s-]', re.UNICODE)
|
||||
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
|
||||
|
||||
def get_normalized_author(value):
|
||||
@ -175,3 +177,25 @@ def get_normalized_author(value):
|
||||
value = re.sub('[^\w,\s]', '', value, flags=re.U)
|
||||
value = " ".join(value.split(", ")[::-1])
|
||||
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;
|
||||
-moz-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;}
|
||||
|
BIN
cps/static/generic_cover.jpg
Normal file
BIN
cps/static/generic_cover.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
@ -39,7 +39,6 @@
|
||||
<div class="discover load-more">
|
||||
<h2>{{title}}</h2>
|
||||
<div class="row">
|
||||
|
||||
{% for entry in entries %}
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
|
@ -31,6 +31,13 @@
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$("#btn-upload").change(function() {
|
||||
$("#form-upload").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- Static navbar -->
|
||||
<div class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
@ -56,6 +63,15 @@
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right" id="main-nav">
|
||||
{% 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 %}
|
||||
<li><a href="{{url_for('user_list')}}"><span class="glyphicon glyphicon-dashboard"></span> Admin</a></li>
|
||||
{% endif %}
|
||||
|
121
cps/web.py
121
cps/web.py
@ -21,6 +21,14 @@ from functools import wraps
|
||||
import base64
|
||||
from sqlalchemy.sql import *
|
||||
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__))
|
||||
|
||||
@ -235,12 +243,12 @@ def get_opds_download_link(book_id, format):
|
||||
return response
|
||||
|
||||
@app.route("/get_authors_json", methods = ['GET', 'POST'])
|
||||
def get_authors_json():
|
||||
if request.method == "POST":
|
||||
form = request.form.to_dict()
|
||||
entries = db.session.execute("select name from authors where name like '%" + form['query'] + "%'")
|
||||
return json.dumps([dict(r) for r in entries])
|
||||
|
||||
def get_authors_json():
|
||||
if request.method == "POST":
|
||||
form = request.form.to_dict()
|
||||
entries = db.session.execute("select name from authors where name like '%" + form['query'] + "%'")
|
||||
return json.dumps([dict(r) for r in entries])
|
||||
|
||||
|
||||
@app.route("/", defaults={'page': 1})
|
||||
@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)
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
if request.method == 'POST':
|
||||
edited_books_id = set()
|
||||
to_save = request.form.to_dict()
|
||||
book.title = to_save["book_title"]
|
||||
|
||||
author_id = book.authors[0].id
|
||||
|
||||
is_author = db.session.query(db.Authors).filter(db.Authors.name == to_save["author_name"].strip()).first()
|
||||
if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "):
|
||||
if is_author:
|
||||
book.authors.append(is_author)
|
||||
if book.title != to_save["book_title"]:
|
||||
book.title = to_save["book_title"]
|
||||
edited_books_id.add(book.id)
|
||||
|
||||
author_id = book.authors[0].id
|
||||
if book.authors[0].name != to_save["author_name"].strip():
|
||||
is_author = db.session.query(db.Authors).filter(db.Authors.name == to_save["author_name"].strip()).first()
|
||||
edited_books_id.add(book.id)
|
||||
if book.authors[0].name not in ("Unknown", "Unbekannt", "", " "):
|
||||
if is_author:
|
||||
book.authors.append(is_author)
|
||||
book.authors.remove(db.session.query(db.Authors).get(book.authors[0].id))
|
||||
authors_books_count = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).count()
|
||||
if authors_books_count == 0:
|
||||
db.session.query(db.Authors).filter(db.Authors.id == author_id).delete()
|
||||
else:
|
||||
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:
|
||||
if is_author:
|
||||
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))
|
||||
authors_books_count = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id.is_(author_id))).count()
|
||||
if authors_books_count == 0:
|
||||
db.session.query(db.Authors).filter(db.Authors.id == author_id).delete()
|
||||
else:
|
||||
book.authors[0].name = to_save["author_name"].strip()
|
||||
else:
|
||||
if is_author:
|
||||
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":
|
||||
img = requests.get(to_save["cover_url"])
|
||||
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()))
|
||||
book.ratings[0] = new_rating
|
||||
db.session.commit()
|
||||
for b in edited_books_id:
|
||||
helper.update_dir_stucture(b)
|
||||
if "detail_view" in to_save:
|
||||
return redirect(url_for('show_book', id=book.id))
|
||||
else:
|
||||
return render_template('edit_book.html', book=book)
|
||||
else:
|
||||
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)
|
||||
|
6
lib/wand/__init__.py
Normal file
6
lib/wand/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
""":mod:`wand` --- Simple `MagickWand API`_ binding for Python
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _MagickWand API: http://www.imagemagick.org/script/magick-wand.php
|
||||
|
||||
"""
|
BIN
lib/wand/__init__.pyc
Normal file
BIN
lib/wand/__init__.pyc
Normal file
Binary file not shown.
1399
lib/wand/api.py
Normal file
1399
lib/wand/api.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/wand/api.pyc
Normal file
BIN
lib/wand/api.pyc
Normal file
Binary file not shown.
307
lib/wand/color.py
Normal file
307
lib/wand/color.py
Normal file
@ -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)
|
BIN
lib/wand/color.pyc
Normal file
BIN
lib/wand/color.pyc
Normal file
Binary file not shown.
119
lib/wand/compat.py
Normal file
119
lib/wand/compat.py
Normal file
@ -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
|
BIN
lib/wand/compat.pyc
Normal file
BIN
lib/wand/compat.pyc
Normal file
Binary file not shown.
78
lib/wand/display.py
Normal file
78
lib/wand/display.py
Normal file
@ -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
|
1988
lib/wand/drawing.py
Normal file
1988
lib/wand/drawing.py
Normal file
File diff suppressed because it is too large
Load Diff
111
lib/wand/exceptions.py
Normal file
111
lib/wand/exceptions.py
Normal file
@ -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
|
BIN
lib/wand/exceptions.pyc
Normal file
BIN
lib/wand/exceptions.pyc
Normal file
Binary file not shown.
103
lib/wand/font.py
Normal file
103
lib/wand/font.py
Normal file
@ -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)
|
||||
)
|
BIN
lib/wand/font.pyc
Normal file
BIN
lib/wand/font.pyc
Normal file
Binary file not shown.
3498
lib/wand/image.py
Normal file
3498
lib/wand/image.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/wand/image.pyc
Normal file
BIN
lib/wand/image.pyc
Normal file
Binary file not shown.
244
lib/wand/resource.py
Normal file
244
lib/wand/resource.py
Normal file
@ -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`.
|
||||
|
||||
"""
|
BIN
lib/wand/resource.pyc
Normal file
BIN
lib/wand/resource.pyc
Normal file
Binary file not shown.
345
lib/wand/sequence.py
Normal file
345
lib/wand/sequence.py
Normal file
@ -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
|
||||
)
|
BIN
lib/wand/sequence.pyc
Normal file
BIN
lib/wand/sequence.pyc
Normal file
Binary file not shown.
251
lib/wand/version.py
Normal file
251
lib/wand/version.py
Normal file
@ -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)
|
BIN
lib/wand/version.pyc
Normal file
BIN
lib/wand/version.pyc
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user