Commit 2a3680b0 authored by Jan B's avatar Jan B

Merge pull request #8 from cervinko/master

Upload PDF Files
parents 7342837a 8973e81a
...@@ -25,99 +25,99 @@ conn.connection.create_function('title_sort', 1, title_sort) ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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;
} }
\ No newline at end of file
.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;}
...@@ -39,7 +39,6 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
""":mod:`wand` --- Simple `MagickWand API`_ binding for Python
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. _MagickWand API: http://www.imagemagick.org/script/magick-wand.php
"""
This diff is collapsed.
""":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)
""":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
""":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
This diff is collapsed.
""":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
""":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)
)
This diff is collapsed.
""":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`.
"""
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment