python-djvulibre-0.8.4/0000755000000000000000000000000013441503047015027 5ustar00rootroot00000000000000python-djvulibre-0.8.4/MANIFEST.in0000644000000000000000000000052213347744054016576 0ustar00rootroot00000000000000include MANIFEST.in include COPYING exclude README.rst include doc/COPYING include doc/README include doc/api/*.rst include doc/api/conf.py include doc/changelog include doc/credits include doc/todo include examples/* recursive-include djvu *.py *.pxi *.pxd *.pyx recursive-include tests *.py Makefile *.tex *.djvu include private/* python-djvulibre-0.8.4/PKG-INFO0000644000000000000000000000200413441503047016120 0ustar00rootroot00000000000000Metadata-Version: 1.1 Name: python-djvulibre Version: 0.8.4 Summary: Python support for the DjVu image format Home-page: http://jwilk.net/software/python-djvulibre Author: Jakub Wilk Author-email: jwilk@jwilk.net License: GNU GPL 2 Description: *python-djvulibre* is a set of Python bindings for the `DjVuLibre `_ library, an open source implementation of `DjVu `_. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Cython Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion Classifier: Topic :: Text Processing python-djvulibre-0.8.4/djvu/0000755000000000000000000000000013441503047015777 5ustar00rootroot00000000000000python-djvulibre-0.8.4/djvu/__init__.py0000644000000000000000000000124212726107516020115 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. import sys if sys.version_info < (2, 6): raise RuntimeError('Python >= 2.6 is required') # vim:ts=4 sts=4 sts=4 sw=4 et python-djvulibre-0.8.4/djvu/common.pxi0000644000000000000000000000713513441502050020010 0ustar00rootroot00000000000000# Copyright © 2008-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. include 'config.pxi' # C library: from libc.stdlib cimport free from libc.string cimport strlen # Python memory handling: from cpython.mem cimport PyMem_Malloc as py_malloc from cpython.mem cimport PyMem_Free as py_free # Python numbers: from cpython cimport ( PyInt_Check as is_short_int, PyLong_Check as is_long_int, ) cdef int is_int(object o): return is_short_int(o) or is_long_int(o) from cpython cimport ( PyNumber_Check as is_number, PyFloat_Check as is_float, ) IF PY3K: from cpython cimport PyNumber_Long as int ELSE: from cpython cimport PyNumber_Int as int from cpython cimport PyNumber_Long as long # Python strings: from cpython cimport ( PyUnicode_Check as is_unicode, PyString_Check as is_string, PyBytes_Check as is_bytes, ) from cpython cimport ( PyUnicode_AsUTF8String as encode_utf8, PyUnicode_DecodeUTF8 as decode_utf8_ex, PyBytes_AsStringAndSize as bytes_to_charp, PyBytes_FromStringAndSize as charp_to_bytes, ) IF PY3K: cdef extern from 'Python.h': object charp_to_string 'PyUnicode_FromString'(char *v) ELSE: from cpython cimport PyString_FromString as charp_to_string cdef object decode_utf8(const char *s): return decode_utf8_ex(s, strlen(s), NULL) cdef extern from 'Python.h': int buffer_to_writable_memory 'PyObject_AsWriteBuffer'(object, void **, Py_ssize_t *) # Python booleans: from cpython cimport PyBool_FromLong as bool # Python pointer->integer conversion: from cpython cimport PyLong_FromVoidPtr as voidp_to_int # Python files: from libc.stdio cimport FILE # Python lists: from cpython cimport PyList_Append as list_append # Python rich comparison: from cpython cimport PyObject_RichCompare as richcmp # Python slices: cdef extern from 'Python.h': int is_slice 'PySlice_Check'(object) # Python threads: from cpython cimport ( PyThread_type_lock as Lock, PyThread_allocate_lock as allocate_lock, PyThread_free_lock as free_lock, PyThread_acquire_lock as acquire_lock, PyThread_release_lock as release_lock, WAIT_LOCK, NOWAIT_LOCK, ) # Python type checks: cdef extern from 'object.h': ctypedef struct PyTypeObject: const char *tp_name from cpython cimport PyObject from cpython cimport PyObject_TypeCheck as _typecheck cdef object type(object o): return ((o).ob_type) IF PY3K: cdef object get_type_name(object type): return decode_utf8((type).tp_name) ELSE: cdef const char* get_type_name(object type): return (type).tp_name cdef int typecheck(object o, object type): return _typecheck(o, type) # Python exceptions: cdef void raise_instantiation_error(object cls) except *: raise TypeError('cannot create \'{tp}\' instances'.format(tp=get_type_name(cls))) # Cython before 0.25 didn't support cdef classes deriving from Exception out of # the box: https://github.com/cython/cython/issues/1416 cdef extern from 'pyerrors.h': ctypedef class __builtin__.Exception [object PyBaseExceptionObject]: pass # vim:ts=4 sts=4 sw=4 et ft=pyrex python-djvulibre-0.8.4/djvu/const.py0000644000000000000000000001772512726107516017521 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2008-2015 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. '''DjVuLibre bindings: various constants.''' import djvu.sexpr EMPTY_LIST = djvu.sexpr.Expression([]) EMPTY_OUTLINE = djvu.sexpr.Expression([djvu.sexpr.Symbol('bookmarks')]) METADATA_BIBTEX_KEYS = frozenset(djvu.sexpr.Symbol(x) for x in ''' address annote author booktitle chapter crossref edition editor howpublished institution journal key month note number organization pages publisher school series title type volume year'''.split()) # Retrieved from METADATA_PDFINFO_KEYS = frozenset(djvu.sexpr.Symbol(x) for x in ''' Author CreationDate Creator Keywords ModDate Producer Subject Title Trapped'''.split()) # Retrieved from the PDF specification METADATA_KEYS = METADATA_BIBTEX_KEYS | METADATA_PDFINFO_KEYS class TextZoneType(djvu.sexpr.Symbol): ''' A type of a text zone. You can compare text zone types with the < operator. To create objects of this class, use the get_text_zone_type() function. ''' __cache = {} @classmethod def from_symbol(cls, symbol): return cls.__cache[symbol] def __new__(cls, value, rank): self = djvu.sexpr.Symbol.__new__(cls, value) TextZoneType.__cache[self] = self return self def __init__(self, value, rank): self.__rank = rank def __lt__(self, other): if not isinstance(other, TextZoneType): raise TypeError('cannot compare text zone type with other object') return self.__rank < other.__rank def __le__(self, other): if not isinstance(other, TextZoneType): raise TypeError('cannot compare text zone type with other object') return self.__rank <= other.__rank def __gt__(self, other): if not isinstance(other, TextZoneType): raise TypeError('cannot compare text zone type with other object') return self.__rank > other.__rank def __ge__(self, other): if not isinstance(other, TextZoneType): raise TypeError('cannot compare text zone type with other object') return self.__rank >= other.__rank def __repr__(self): return '<{mod}.{cls}: {name}>'.format( mod=self.__module__, cls=type(self).__name__, name=self ) TEXT_ZONE_PAGE = TextZoneType('page', 7) TEXT_ZONE_COLUMN = TextZoneType('column', 6) TEXT_ZONE_REGION = TextZoneType('region', 5) TEXT_ZONE_PARAGRAPH = TextZoneType('para', 4) TEXT_ZONE_LINE = TextZoneType('line', 3) TEXT_ZONE_WORD = TextZoneType('word', 2) TEXT_ZONE_CHARACTER = TextZoneType('char', 1) def get_text_zone_type(symbol): return TextZoneType.from_symbol(symbol) TEXT_ZONE_SEPARATORS = { TEXT_ZONE_PAGE: '\f', # Form Feed (FF) TEXT_ZONE_COLUMN: '\v', # Vertical tab (VT, LINE TABULATION) TEXT_ZONE_REGION: '\035', # Group Separator (GS, INFORMATION SEPARATOR THREE) TEXT_ZONE_PARAGRAPH: '\037', # Unit Separator (US, INFORMATION SEPARATOR ONE) TEXT_ZONE_LINE: '\n', # Line Feed (LF) TEXT_ZONE_WORD: ' ', # space TEXT_ZONE_CHARACTER: '' } # 8.3.4.2 Maparea (overprinted annotations) ANNOTATION_MAPAREA = djvu.sexpr.Symbol('maparea') # 8.3.4.2 Maparea (overprinted annotations): MAPAREA_SHAPE_RECTANGLE = djvu.sexpr.Symbol('rect') MAPAREA_SHAPE_OVAL = djvu.sexpr.Symbol('oval') MAPAREA_SHAPE_POLYGON = djvu.sexpr.Symbol('poly') MAPAREA_SHAPE_LINE = djvu.sexpr.Symbol('line') MAPAREA_SHAPE_TEXT = djvu.sexpr.Symbol('text') MAPAREA_URI = MAPAREA_URL = djvu.sexpr.Symbol('url') # 8.3.4.2.3.1.1 Border type: MAPAREA_BORDER_NONE = djvu.sexpr.Symbol('none') MAPAREA_BORDER_XOR = djvu.sexpr.Symbol('xor') MAPAREA_BORDER_SOLID_COLOR = djvu.sexpr.Symbol('border') # 8.3.4.2.3.1.1 Border type: MAPAREA_BORDER_SHADOW_IN = djvu.sexpr.Symbol('shadow_in') MAPAREA_BORDER_SHADOW_OUT = djvu.sexpr.Symbol('shadow_out') MAPAREA_BORDER_ETCHED_IN = djvu.sexpr.Symbol('shadow_ein') MAPAREA_BORDER_ETCHED_OUT = djvu.sexpr.Symbol('shadow_eout') MAPAREA_SHADOW_BORDERS = (MAPAREA_BORDER_SHADOW_IN, MAPAREA_BORDER_SHADOW_OUT, MAPAREA_BORDER_ETCHED_IN, MAPAREA_BORDER_ETCHED_OUT) MAPAREA_SHADOW_BORDER_MIN_WIDTH = 1 MAPAREA_SHADOW_BORDER_MAX_WIDTH = 32 # 8.3.4.2.3.1.2 Border always visible MAPAREA_BORDER_ALWAYS_VISIBLE = djvu.sexpr.Symbol('border_avis') # 8.3.4.2.3.1.3 Highlight color and opacity: MAPAREA_HIGHLIGHT_COLOR = djvu.sexpr.Symbol('hilite') MAPAREA_OPACITY = djvu.sexpr.Symbol('opacity') MAPAREA_OPACITY_MIN = 0 MAPAREA_OPACITY_DEFAULT = 50 MAPAREA_OPACITY_MAX = 100 # 8.3.4.2.3.1.4 Line and Text parameters: MAPAREA_ARROW = djvu.sexpr.Symbol('arrow') MAPAREA_LINE_WIDTH = djvu.sexpr.Symbol('width') MAPAREA_LINE_COLOR = djvu.sexpr.Symbol('lineclr') MAPAREA_LINE_MIN_WIDTH = 1 MAPAREA_LINE_COLOR_DEFAULT = '#000000' # 8.3.4.2.3.1.4 Line and Text parameters: MAPAREA_BACKGROUND_COLOR = djvu.sexpr.Symbol('backclr') MAPAREA_TEXT_COLOR = djvu.sexpr.Symbol('textclr') MAPAREA_PUSHPIN = djvu.sexpr.Symbol('pushpin') MAPAREA_TEXT_COLOR_DEFAULT = '#000000' # 8.3.4.1 Initial Document View : ANNOTATION_BACKGROUND = djvu.sexpr.Symbol('background') # 8.3.4.1.1 Background Color ANNOTATION_ZOOM = djvu.sexpr.Symbol('zoom') # 8.3.4.1.2 Initial Zoom ANNOTATION_MODE = djvu.sexpr.Symbol('mode') # 8.3.4.1.3 Initial Display level ANNOTATION_ALIGN = djvu.sexpr.Symbol('align') # 8.3.4.1.4 Alignment # djvuchanges.txt, sections "Metadata Annotations" and "Document Annotations and Metadata": ANNOTATION_METADATA = djvu.sexpr.Symbol('metadata') # 8.3.4.3 Printed headers and footers: ANNOTATION_PRINTED_HEADER = djvu.sexpr.Symbol('phead') ANNOTATION_PRINTED_FOOTER = djvu.sexpr.Symbol('pfoot') PRINTER_HEADER_ALIGN_LEFT = PRINTED_FOOTER_ALIGN_LEFT = djvu.sexpr.Symbol('left') PRINTER_HEADER_ALIGN_CENTER = PRINTED_FOOTER_ALIGN_CENTER = djvu.sexpr.Symbol('center') PRINTER_HEADER_ALIGN_RIGHT = PRINTED_FOOTER_ALIGN_RIGHT = djvu.sexpr.Symbol('right') __all__ = [ 'ANNOTATION_ALIGN', 'ANNOTATION_BACKGROUND', 'ANNOTATION_MAPAREA', 'ANNOTATION_METADATA', 'ANNOTATION_MODE', 'ANNOTATION_PRINTED_FOOTER', 'ANNOTATION_PRINTED_HEADER', 'ANNOTATION_ZOOM', 'EMPTY_LIST', 'EMPTY_OUTLINE', 'MAPAREA_ARROW', 'MAPAREA_BACKGROUND_COLOR', 'MAPAREA_BORDER_ALWAYS_VISIBLE', 'MAPAREA_BORDER_ETCHED_IN', 'MAPAREA_BORDER_ETCHED_OUT', 'MAPAREA_BORDER_NONE', 'MAPAREA_BORDER_SHADOW_IN', 'MAPAREA_BORDER_SHADOW_OUT', 'MAPAREA_BORDER_SOLID_COLOR', 'MAPAREA_BORDER_XOR', 'MAPAREA_HIGHLIGHT_COLOR', 'MAPAREA_LINE_COLOR', 'MAPAREA_LINE_COLOR_DEFAULT', 'MAPAREA_LINE_MIN_WIDTH', 'MAPAREA_LINE_WIDTH', 'MAPAREA_OPACITY', 'MAPAREA_OPACITY_DEFAULT', 'MAPAREA_OPACITY_MAX', 'MAPAREA_OPACITY_MIN', 'MAPAREA_PUSHPIN', 'MAPAREA_SHADOW_BORDERS', 'MAPAREA_SHADOW_BORDER_MAX_WIDTH', 'MAPAREA_SHADOW_BORDER_MIN_WIDTH', 'MAPAREA_SHAPE_LINE', 'MAPAREA_SHAPE_OVAL', 'MAPAREA_SHAPE_POLYGON', 'MAPAREA_SHAPE_RECTANGLE', 'MAPAREA_SHAPE_TEXT', 'MAPAREA_TEXT_COLOR', 'MAPAREA_TEXT_COLOR_DEFAULT', 'MAPAREA_URI', 'MAPAREA_URL', 'METADATA_BIBTEX_KEYS', 'METADATA_KEYS', 'METADATA_PDFINFO_KEYS', 'PRINTED_FOOTER_ALIGN_CENTER', 'PRINTED_FOOTER_ALIGN_LEFT', 'PRINTED_FOOTER_ALIGN_RIGHT', 'PRINTER_HEADER_ALIGN_CENTER', 'PRINTER_HEADER_ALIGN_LEFT', 'PRINTER_HEADER_ALIGN_RIGHT', 'TEXT_ZONE_CHARACTER', 'TEXT_ZONE_COLUMN', 'TEXT_ZONE_LINE', 'TEXT_ZONE_PAGE', 'TEXT_ZONE_PARAGRAPH', 'TEXT_ZONE_REGION', 'TEXT_ZONE_SEPARATORS', 'TEXT_ZONE_WORD', 'TextZoneType', 'get_text_zone_type' ] # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/djvu/decode.pxd0000644000000000000000000002114713347744062017755 0ustar00rootroot00000000000000# Copyright © 2007-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. #cython: language_level=2 cdef extern from 'stdio.h': ctypedef struct FILE from djvu.sexpr cimport cexpr_t, _WrappedCExpr from djvu.sexpr cimport public_c2py as cexpr2py from djvu.sexpr cimport public_py2c as py2cexpr cdef extern from 'libdjvu/ddjvuapi.h': struct ddjvu_context_s union ddjvu_message_s struct ddjvu_job_s struct ddjvu_document_s struct ddjvu_page_s struct ddjvu_format_s struct ddjvu_rect_s struct ddjvu_rectmapper_s ctypedef ddjvu_context_s ddjvu_context_t ctypedef ddjvu_message_s ddjvu_message_t ctypedef ddjvu_job_s ddjvu_job_t ctypedef ddjvu_document_s ddjvu_document_t ctypedef ddjvu_page_s ddjvu_page_t ctypedef ddjvu_format_s ddjvu_format_t ctypedef ddjvu_rect_s ddjvu_rect_t ctypedef ddjvu_rectmapper_s ddjvu_rectmapper_t ctypedef void (*ddjvu_message_callback_t)(ddjvu_context_t* context, void* closure) nogil ctypedef enum ddjvu_status_t: DDJVU_JOB_NOTSTARTED DDJVU_JOB_STARTED DDJVU_JOB_OK DDJVU_JOB_FAILED DDJVU_JOB_STOPPED ctypedef enum ddjvu_message_tag_t: DDJVU_ERROR DDJVU_INFO DDJVU_NEWSTREAM DDJVU_DOCINFO DDJVU_PAGEINFO DDJVU_RELAYOUT DDJVU_REDISPLAY DDJVU_CHUNK DDJVU_THUMBNAIL DDJVU_PROGRESS cdef struct ddjvu_message_any_s: ddjvu_message_tag_t tag ddjvu_context_t* context ddjvu_document_t* document ddjvu_page_t* page ddjvu_job_t* job ctypedef ddjvu_message_any_s ddjvu_message_any_t cdef struct ddjvu_message_error_s: ddjvu_message_any_t any char* message char* function char* filename int lineno cdef struct ddjvu_message_info_s: ddjvu_message_any_t any char* message cdef struct ddjvu_message_newstream_s: ddjvu_message_any_t any int streamid char* name char* url cdef struct ddjvu_message_docinfo_s: ddjvu_message_any_t any ctypedef enum ddjvu_document_type_t: DDJVU_DOCTYPE_UNKNOWN DDJVU_DOCTYPE_SINGLEPAGE DDJVU_DOCTYPE_BUNDLED DDJVU_DOCTYPE_INDIRECT DDJVU_DOCTYPE_OLD_BUNDLED DDJVU_DOCTYPE_OLD_INDEXED cdef struct ddjvu_fileinfo_s: char type int pageno int size char* id char* name char* title ctypedef ddjvu_fileinfo_s ddjvu_fileinfo_t cdef struct ddjvu_pageinfo_s: int width int height int dpi int rotation int version ctypedef ddjvu_pageinfo_s ddjvu_pageinfo_t cdef struct ddjvu_message_pageinfo_s: ddjvu_message_any_t any cdef struct ddjvu_message_relayout_s: ddjvu_message_any_t any cdef struct ddjvu_message_redisplay_s: ddjvu_message_any_t any cdef struct ddjvu_message_chunk_s: ddjvu_message_any_t any char* chunkid ctypedef enum ddjvu_page_type_t: DDJVU_PAGETYPE_UNKNOWN DDJVU_PAGETYPE_BITONAL DDJVU_PAGETYPE_PHOTO DDJVU_PAGETYPE_COMPOUND ctypedef enum ddjvu_page_rotation_t: DDJVU_ROTATE_0 DDJVU_ROTATE_90 DDJVU_ROTATE_180 DDJVU_ROTATE_270 ctypedef enum ddjvu_render_mode_t: DDJVU_RENDER_COLOR DDJVU_RENDER_BLACK DDJVU_RENDER_COLORONLY DDJVU_RENDER_MASKONLY DDJVU_RENDER_BACKGROUND DDJVU_RENDER_FOREGROUND cdef struct ddjvu_rect_s: int x, y unsigned int w, h ctypedef enum ddjvu_format_style_t: DDJVU_FORMAT_BGR24 DDJVU_FORMAT_RGB24 DDJVU_FORMAT_RGBMASK16 DDJVU_FORMAT_RGBMASK32 DDJVU_FORMAT_GREY8 DDJVU_FORMAT_PALETTE8 DDJVU_FORMAT_MSBTOLSB DDJVU_FORMAT_LSBTOMSB cdef struct ddjvu_message_thumbnail_s: ddjvu_message_any_t any int pagenum cdef struct ddjvu_message_progress_s: ddjvu_message_any_t any ddjvu_status_t status int percent cdef union ddjvu_message_s: ddjvu_message_any_s m_any ddjvu_message_error_s m_error ddjvu_message_info_s m_info ddjvu_message_newstream_s m_newstream ddjvu_message_docinfo_s m_docinfo ddjvu_message_pageinfo_s m_pageinfo ddjvu_message_chunk_s m_chunk ddjvu_message_relayout_s m_relayout ddjvu_message_redisplay_s m_redisplay ddjvu_message_thumbnail_s m_thumbnail ddjvu_message_progress_s m_progress cdef class Context cdef class Document cdef class DocumentExtension: cdef Document _document cdef class DocumentPages(DocumentExtension): pass cdef class DocumentFiles(DocumentExtension): cdef object _page_map cdef class Document: cdef ddjvu_document_t* ddjvu_document cdef Context _context cdef DocumentPages _pages cdef DocumentFiles _files cdef object _queue cdef object _condition cdef object __weakref__ cdef object __init(self, Context context, ddjvu_document_t* ddjvu_document) cdef object __clear(self) cdef class _SexprWrapper: cdef object _document_weakref cdef cexpr_t _cexpr cdef class DocumentOutline(DocumentExtension): cdef _SexprWrapper _sexpr cdef object _update_sexpr(self) cdef class Annotations: cdef _SexprWrapper _sexpr cdef object _update_sexpr(self) cdef Document _document cdef class DocumentAnnotations(Annotations): cdef int _compat cdef class Hyperlinks: cdef object _sexpr cdef class Metadata: cdef Annotations _annotations cdef object _keys cdef class File: cdef int _n cdef int _have_info cdef ddjvu_fileinfo_t ddjvu_fileinfo cdef Document _document cdef object _get_info(self) cdef class Page: cdef Document _document cdef ddjvu_pageinfo_t ddjvu_pageinfo cdef int _have_info cdef int _n cdef object _get_info(self) cdef class PageAnnotations(Annotations): cdef Page _page cdef class PageText: cdef Page _page cdef object _details cdef _SexprWrapper _sexpr cdef object _update_sexpr(self) cdef class Context: cdef ddjvu_context_t* ddjvu_context cdef object _queue cdef class PixelFormat: cdef ddjvu_format_t* ddjvu_format cdef int _bpp cdef int _dither_bpp cdef int _row_order cdef int _y_direction cdef double _gamma cdef class PixelFormatRgb(PixelFormat): cdef int _rgb cdef class PixelFormatRgbMask(PixelFormat): cdef unsigned int _params[4] cdef class PixelFormatGrey(PixelFormat): pass cdef class PixelFormatPalette(PixelFormat): cdef unsigned int _palette[216] cdef class PixelFormatPackedBits(PixelFormat): cdef int _little_endian pass cdef class Job: cdef Context _context cdef ddjvu_job_t* ddjvu_job cdef object _queue cdef object _condition cdef object __init(self, Context context, ddjvu_job_t *ddjvu_job) cdef object __clear(self) cdef object __weakref__ cdef class PageJob(Job): pass cdef class SaveJob(Job): cdef object _file cdef class DocumentDecodingJob(Job): cdef object _document cdef object __init_ddj(self, Document document) cdef class AffineTransform: cdef ddjvu_rectmapper_t* ddjvu_rectmapper cdef class Message: cdef ddjvu_message_t* ddjvu_message cdef Context _context cdef Document _document cdef PageJob _page_job cdef Job _job cdef object __init(self) cdef class ErrorMessage(Message): cdef object _message cdef object _location cdef class InfoMessage(Message): cdef object _message cdef class Stream: cdef int _streamid cdef int _open cdef Document _document cdef class NewStreamMessage(Message): cdef object _name cdef object _uri cdef Stream _stream cdef class DocInfoMessage(Message): pass cdef class PageInfoMessage(Message): pass cdef class ChunkMessage(Message): pass cdef class RelayoutMessage(ChunkMessage): pass cdef class RedisplayMessage(ChunkMessage): pass cdef class ThumbnailMessage(Message): cdef int _page_no cdef class ProgressMessage(Message): cdef int _percent cdef ddjvu_status_t _status cdef class Thumbnail: cdef Page _page # vim:ts=4 sts=4 sw=4 et ft=pyrex python-djvulibre-0.8.4/djvu/decode.pyx0000644000000000000000000033015713441502050017766 0ustar00rootroot00000000000000# Copyright © 2007-2019 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. #cython: autotestdict=False #cython: language_level=2 ''' DjVuLibre bindings: module for efficiently decoding and displaying DjVu documents Summary ------- The DDJVU API provides for efficiently decoding and displaying DjVu documents. It provides for displaying images without waiting for the complete DjVu data. Images can be displayed as soon as sufficient data is available. A higher quality image might later be displayed when further data is available. The DjVu library achieves this using a complicated scheme involving multiple threads. The DDJVU API hides this complexity with a familiar event model. ''' include 'common.pxi' cdef object weakref import weakref cdef object thread IF PY3K: import _thread as thread ELSE: import thread cdef object Queue, Empty IF PY3K: from queue import Queue, Empty ELSE: from Queue import Queue, Empty cdef object Condition from threading import Condition cdef object imap, izip IF PY3K: imap = map izip = zip ELSE: from itertools import imap, izip cdef object sys, devnull, format_exc import sys from os import devnull from traceback import format_exc cdef object StringIO IF PY3K: from io import StringIO ELSE: from cStringIO import StringIO cdef object Symbol, SymbolExpression, InvalidExpression from djvu.sexpr import Symbol, SymbolExpression, InvalidExpression cdef object the_sentinel the_sentinel = object() cdef object _context_loft, _document_loft, _document_weak_loft, _job_loft, _job_weak_loft cdef Lock loft_lock _context_loft = {} _document_loft = set() _document_weak_loft = weakref.WeakValueDictionary() _job_loft = set() _job_weak_loft = weakref.WeakValueDictionary() loft_lock = allocate_lock() cdef extern from 'libdjvu/ddjvuapi.h': ddjvu_context_t* ddjvu_context_create(const char *program_name) nogil void ddjvu_context_release(ddjvu_context_t* context) nogil void ddjvu_cache_set_size(ddjvu_context_t* context, unsigned long cachesize) nogil unsigned long ddjvu_cache_get_size(ddjvu_context_t* context) nogil void ddjvu_cache_clear(ddjvu_context_t* context) nogil ddjvu_message_t* ddjvu_message_peek(ddjvu_context_t* context) nogil ddjvu_message_t* ddjvu_message_wait(ddjvu_context_t* context) nogil void ddjvu_message_pop(ddjvu_context_t* context) nogil void ddjvu_message_set_callback(ddjvu_context_t* context, ddjvu_message_callback_t callback, void* closure) nogil ddjvu_status_t ddjvu_job_status(ddjvu_job_t* job) nogil int ddjvu_job_done(ddjvu_job_t* job) nogil int ddjvu_job_error(ddjvu_job_t* job) nogil void ddjvu_job_stop(ddjvu_job_t* job) nogil void ddjvu_job_set_user_data(ddjvu_job_t* job, void* userdata) nogil void* ddjvu_job_get_user_data(ddjvu_job_t* job) nogil void ddjvu_job_release(ddjvu_job_t* job) nogil ddjvu_document_t* ddjvu_document_create(ddjvu_context_t *context, const char *url, int cache) nogil ddjvu_document_t* ddjvu_document_create_by_filename(ddjvu_context_t *context, const char *filename, int cache) nogil ddjvu_job_t* ddjvu_document_job(ddjvu_document_t* document) nogil void ddjvu_document_release(ddjvu_document_t* document) nogil void ddjvu_document_set_user_data(ddjvu_document_t* document, void* userdata) nogil void* ddjvu_document_get_user_data(ddjvu_document_t* document) nogil ddjvu_status_t ddjvu_document_decoding_status(ddjvu_document_t* document) nogil int ddjvu_document_decoding_done(ddjvu_document_t* document) nogil int ddjvu_document_decoding_error(ddjvu_document_t* document) nogil void ddjvu_stream_write(ddjvu_document_t* document, int streamid, const char *data, unsigned long datalen) nogil void ddjvu_stream_close(ddjvu_document_t* document, int streamid, int stop) nogil ddjvu_document_type_t ddjvu_document_get_type(ddjvu_document_t* document) nogil int ddjvu_document_get_pagenum(ddjvu_document_t* document) nogil int ddjvu_document_get_filenum(ddjvu_document_t* document) nogil ddjvu_status_t ddjvu_document_get_fileinfo(ddjvu_document_t* document, int fileno, ddjvu_fileinfo_t* info) nogil int ddjvu_document_check_pagedata(ddjvu_document_t* document, int pageno) nogil ddjvu_status_t ddjvu_document_get_pageinfo(ddjvu_document_t* document, int pageno, ddjvu_pageinfo_t* info) nogil ddjvu_status_t ddjvu_document_get_pageinfo_imp(ddjvu_document_t* document, int pageno, ddjvu_pageinfo_t* info, unsigned int infosz) nogil char* ddjvu_document_get_pagedump(ddjvu_document_t* document, int pageno) nogil char* ddjvu_document_get_filedump(ddjvu_document_t* document, int fileno) nogil ddjvu_page_t* ddjvu_page_create_by_pageno(ddjvu_document_t* document, int pageno) nogil ddjvu_job_t* ddjvu_page_job(ddjvu_page_t* page) nogil void ddjvu_page_release(ddjvu_page_t* page) nogil void ddjvu_page_set_user_data(ddjvu_page_t* page, void* userdata) nogil void* ddjvu_page_get_user_data(ddjvu_page_t* page) nogil ddjvu_status_t ddjvu_page_decoding_status(ddjvu_page_t* page) nogil int ddjvu_page_decoding_done(ddjvu_page_t* page) nogil int ddjvu_page_decoding_error(ddjvu_page_t* page) nogil int ddjvu_page_get_width(ddjvu_page_t* page) nogil int ddjvu_page_get_height(ddjvu_page_t* page) nogil int ddjvu_page_get_resolution(ddjvu_page_t* page) nogil double ddjvu_page_get_gamma(ddjvu_page_t* page) nogil int ddjvu_page_get_version(ddjvu_page_t* page) nogil int ddjvu_code_get_version() nogil ddjvu_page_type_t ddjvu_page_get_type(ddjvu_page_t* page) nogil void ddjvu_page_set_rotation(ddjvu_page_t* page, ddjvu_page_rotation_t rot) nogil ddjvu_page_rotation_t ddjvu_page_get_rotation(ddjvu_page_t* page) nogil ddjvu_page_rotation_t ddjvu_page_get_initial_rotation(ddjvu_page_t* page) nogil int ddjvu_page_render(ddjvu_page_t *page, const ddjvu_render_mode_t mode, const ddjvu_rect_t *pagerect, const ddjvu_rect_t *renderrect, const ddjvu_format_t *pixelformat, unsigned long rowsize, char *imagebuffer) nogil ddjvu_rectmapper_t* ddjvu_rectmapper_create(ddjvu_rect_t* input, ddjvu_rect_t* output) nogil void ddjvu_rectmapper_modify(ddjvu_rectmapper_t* mapper, int rotation, int mirrorx, int mirrory) nogil void ddjvu_rectmapper_release(ddjvu_rectmapper_t* mapper) nogil void ddjvu_map_point(ddjvu_rectmapper_t* mapper, int* x, int* y) nogil void ddjvu_map_rect(ddjvu_rectmapper_t* mapper, ddjvu_rect_t* rect) nogil void ddjvu_unmap_point(ddjvu_rectmapper_t* mapper, int* x, int* y) nogil void ddjvu_unmap_rect(ddjvu_rectmapper_t* mapper, ddjvu_rect_t* rect) nogil ddjvu_format_t* ddjvu_format_create(ddjvu_format_style_t style, int nargs, unsigned int* args) nogil void ddjvu_format_set_row_order(ddjvu_format_t* format, int top_to_bottom) nogil void ddjvu_format_set_y_direction(ddjvu_format_t* format, int top_to_bottom) nogil void ddjvu_format_set_ditherbits(ddjvu_format_t* format, int bits) nogil void ddjvu_format_set_gamma(ddjvu_format_t* format, double gamma) nogil void ddjvu_format_release(ddjvu_format_t* format) nogil ddjvu_status_t ddjvu_thumbnail_status(ddjvu_document_t* document, int pagenum, int start) nogil int ddjvu_thumbnail_render(ddjvu_document_t *document, int pagenum, int *wptr, int *hptr, const ddjvu_format_t *pixelformat, unsigned long rowsize, char *imagebuffer) nogil ddjvu_job_t* ddjvu_document_print(ddjvu_document_t* document, FILE* output, int optc, const char * const *optv) nogil ddjvu_job_t* ddjvu_document_save(ddjvu_document_t* document, FILE* output, int optc, const char * const *optv) nogil void ddjvu_miniexp_release(ddjvu_document_t* document, cexpr_t expr) nogil cexpr_t ddjvu_document_get_outline(ddjvu_document_t* document) nogil cexpr_t ddjvu_document_get_anno(ddjvu_document_t* document, int compat) nogil cexpr_t ddjvu_document_get_pagetext(ddjvu_document_t* document, int pageno, const char *maxdetail) nogil cexpr_t ddjvu_document_get_pageanno(ddjvu_document_t* document, int pageno) nogil const char * ddjvu_anno_get_bgcolor(cexpr_t annotations) nogil const char * ddjvu_anno_get_zoom(cexpr_t annotations) nogil const char * ddjvu_anno_get_mode(cexpr_t annotations) nogil const char * ddjvu_anno_get_horizalign(cexpr_t annotations) nogil const char * ddjvu_anno_get_vertalign(cexpr_t annotations) nogil cexpr_t* ddjvu_anno_get_hyperlinks(cexpr_t annotations) nogil cexpr_t* ddjvu_anno_get_metadata_keys(cexpr_t annotations) nogil const char * ddjvu_anno_get_metadata(cexpr_t annotations, cexpr_t key) nogil # Python files: IF PY3K: from cpython cimport ( PyErr_SetFromErrno as posix_error, PyObject_AsFileDescriptor as file_to_fd, ) cdef int is_file(object o): return not is_number(o) and file_to_fd(o) != -1 ELSE: cdef extern from 'Python.h': FILE* file_to_cfile 'PyFile_AsFile'(object) int is_file 'PyFile_Check'(object) IF WINDOWS: cdef extern from 'io.h' nogil: int dup(int) ELSE: from posix.unistd cimport dup from libc.stdio cimport fclose from libc.stdio cimport fdopen IF HAVE_LANGINFO_H: cdef extern from 'langinfo.h': ctypedef enum nl_item: CODESET char *nl_langinfo(nl_item item) DDJVU_VERSION = ddjvu_code_get_version() FILE_TYPE_PAGE = 'P' FILE_TYPE_THUMBNAILS = 'T' FILE_TYPE_INCLUDE = 'I' DOCUMENT_TYPE_UNKNOWN = DDJVU_DOCTYPE_UNKNOWN DOCUMENT_TYPE_SINGLE_PAGE = DDJVU_DOCTYPE_SINGLEPAGE DOCUMENT_TYPE_BUNDLED = DDJVU_DOCTYPE_BUNDLED DOCUMENT_TYPE_INDIRECT = DDJVU_DOCTYPE_INDIRECT DOCUMENT_TYPE_OLD_BUNDLED = DDJVU_DOCTYPE_OLD_BUNDLED DOCUMENT_TYPE_OLD_INDEXED = DDJVU_DOCTYPE_OLD_INDEXED cdef object check_sentinel(self, kwargs): if kwargs.get('sentinel') is not the_sentinel: raise_instantiation_error(type(self)) cdef object write_unraisable_exception(object cause): try: message = format_exc() except AttributeError: # This mostly happens during interpreter cleanup. # It's worthless to try to recover. raise SystemExit sys.stderr.write( 'Unhandled exception in thread started by {obj!r}\n{msg}\n'.format(obj=cause, msg=message) ) cdef class _FileWrapper: cdef object _file cdef FILE *cfile def __cinit__(self, object file, object mode): self._file = file self.cfile = NULL if not is_file(file): raise TypeError('file must be a real file object') IF PY3K: fd = file_to_fd(file) if fd == -1: posix_error(OSError) fd = dup(fd) if fd == -1: posix_error(OSError) self.cfile = fdopen(fd, mode) if self.cfile == NULL: posix_error(OSError) ELSE: self.cfile = file_to_cfile(file) cdef object close(self): IF PY3K: cdef int rc if self.cfile == NULL: return rc = fclose(self.cfile) self.cfile = NULL if rc != 0: posix_error(OSError) ELSE: if self._file is not None: self._file.flush() self._file = None self.cfile = NULL IF PY3K: def __dealloc__(self): cdef int rc if self.cfile == NULL: return rc = fclose(self.cfile) # XXX It's too late to handle errors. class NotAvailable(Exception): ''' A resource not (yet) available. ''' cdef object _NotAvailable_ _NotAvailable_ = NotAvailable cdef class DocumentExtension: property document: ''' Return the concerned Document. ''' def __get__(self): return self._document cdef class DocumentPages(DocumentExtension): ''' Pages of a document. Use document.pages to obtain instances of this class. Page indexing is zero-based, i.e. pages[0] stands for the very first page. len(pages) might return 1 when called before receiving a DocInfoMessage. ''' def __cinit__(self, Document document not None, **kwargs): check_sentinel(self, kwargs) self._document = document def __len__(self): return ddjvu_document_get_pagenum(self._document.ddjvu_document) def __getitem__(self, key): if is_int(key): if key < 0 or key >= len(self): raise IndexError('page number out of range') return Page(self.document, key) else: raise TypeError('page numbers must be integers') cdef class Page: ''' Page of a document. Use document.pages[N] to obtain instances of this class. ''' def __cinit__(self, Document document not None, int n): self._document = document self._have_info = 0 self._n = n property document: ''' Return the Document which includes the page. ''' def __get__(self): return self._document property file: ''' Return a File associated with the page. ''' def __get__(self): return self._document.files[self] property n: ''' Return the page number. Page indexing is zero-based, i.e. 0 stands for the very first page. ''' def __get__(self): return self._n property thumbnail: ''' Return a Thumbnail for the page. ''' def __get__(self): return Thumbnail(self) cdef object _get_info(self): cdef ddjvu_status_t status if self._have_info: return status = ddjvu_document_get_pageinfo(self._document.ddjvu_document, self._n, &self.ddjvu_pageinfo) ex = JobException_from_c(status) if ex is JobOK: return elif ex is JobStarted: raise _NotAvailable_ else: raise ex def get_info(self, wait=1): ''' P.get_info(wait=True) -> None Attempt to obtain information about the page without decoding the page. If wait is true, wait until the information is available. If the information is not available, raise NotAvailable exception. Then, start fetching the page data, which causes emission of PageInfoMessage messages with empty .page_job. Possible exceptions: NotAvailable, JobFailed. ''' cdef ddjvu_status_t status if self._have_info: return if not wait: return self._get_info() while 1: self._document._condition.acquire() try: status = ddjvu_document_get_pageinfo(self._document.ddjvu_document, self._n, &self.ddjvu_pageinfo) ex = JobException_from_c(status) if ex is JobOK: self._have_info = 1 return elif ex is JobStarted: self._document._condition.wait() else: raise ex finally: self._document._condition.release() property width: ''' Return the page width, in pixels. Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.width property height: ''' Return the page height, in pixels. Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.height property size: ''' page.size == (page.width, page.height) Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.width, self.ddjvu_pageinfo.height property dpi: ''' Return the page resolution, in pixels per inch. Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.dpi property rotation: ''' Return the initial page rotation, in degrees. Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.rotation * 90 property version: ''' Return the page version. Possible exceptions: NotAvailable, JobFailed. See Page.get_info() for details. ''' def __get__(self): self._get_info() return self.ddjvu_pageinfo.version property dump: ''' Return a text describing the contents of the page using the same format as the djvudump command. If the information is not available, raise NotAvailable exception. Then PageInfoMessage messages with empty page_job may be emitted. Possible exceptions: NotAvailable. ''' def __get__(self): cdef char* s s = ddjvu_document_get_pagedump(self._document.ddjvu_document, self._n) if s == NULL: raise _NotAvailable_ try: return decode_utf8(s) finally: free(s) def decode(self, wait=1): ''' P.decode(wait=True) -> a PageJob Initiate data transfer and decoding threads for the page. If wait is true, wait until the job is done. Possible exceptions: - NotAvailable (if called before receiving the DocInfoMessage). - JobFailed (if document decoding failed). ''' cdef PageJob job cdef ddjvu_job_t* ddjvu_job with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: ddjvu_job = ddjvu_page_create_by_pageno(self._document.ddjvu_document, self._n) if ddjvu_job == NULL: raise _NotAvailable_ if ddjvu_document_decoding_error(self._document.ddjvu_document): raise JobException_from_c(ddjvu_document_decoding_status(self._document.ddjvu_document)) job = PageJob(sentinel = the_sentinel) job.__init(self._document._context, ddjvu_job) finally: release_lock(loft_lock) if wait: job.wait() return job property annotations: ''' Return PageAnnotations for the page. ''' def __get__(self): return PageAnnotations(self) property text: ''' Return PageText for the page. ''' def __get__(self): return PageText(self) def __repr__(self): return '{tp}({doc!r}, {n})'.format( tp=get_type_name(Page), doc=self._document, n=self._n, ) cdef class Thumbnail: ''' Thumbnail for a page. Use page.thumbnail to obtain instances of this class. ''' def __cinit__(self, Page page not None): self._page = page property page: ''' Return the page. ''' def __get__(self): return self._page property status: ''' Determine whether the thumbnail is available. Return a JobException subclass indicating the current job status. ''' def __get__(self): return JobException_from_c(ddjvu_thumbnail_status(self._page._document.ddjvu_document, self._page._n, 0)) def calculate(self): ''' T.calculate() -> a JobException Determine whether the thumbnail is available. If it's not, initiate the thumbnail calculating job. Regardless of its success, the completion of the job is signalled by a subsequent ThumbnailMessage. Return a JobException subclass indicating the current job status. ''' return JobException_from_c(ddjvu_thumbnail_status(self._page._document.ddjvu_document, self._page._n, 1)) def render(self, size, PixelFormat pixel_format not None, long row_alignment=1, dry_run=0, buffer=None): ''' T.render((w0, h0), pixel_format, row_alignment=1, dry_run=False, buffer=None) -> ((w1, h1, row_size), data) Render the thumbnail: * not larger than w0 x h0 pixels; * using the pixel_format pixel format; * with each row starting at row_alignment bytes boundary; * into the provided buffer or to a newly created string. Raise NotAvailable when no thumbnail is available. Otherwise, return a ((w1, h1, row_size), data) tuple: * w1 and h1 are actual thumbnail dimensions in pixels (w1 <= w0 and h1 <= h0); * row_size is length of each image row, in bytes; * data is None if dry_run is true; otherwise is contains the actual image data. ''' cdef int iw, ih cdef long w, h, row_size cdef void* memory if row_alignment <= 0: raise ValueError('row_alignment must be a positive integer') w, h = size if w <= 0 or h <= 0: raise ValueError('size width/height must a positive integer') iw, ih = w, h if iw != w or ih != h: raise OverflowError('size width/height is too large') row_size = calculate_row_size(w, row_alignment, pixel_format._bpp) if dry_run: result = None memory = NULL else: result = allocate_image_memory(row_size, h, buffer, &memory) if ddjvu_thumbnail_render(self._page._document.ddjvu_document, self._page._n, &iw, &ih, pixel_format.ddjvu_format, row_size, memory): return (iw, ih, row_size), result else: raise _NotAvailable_ def __repr__(self): return '{tp}({page!r})'.format( tp=get_type_name(Thumbnail), page=self._page, ) cdef class DocumentFiles(DocumentExtension): ''' Component files of a document. Use document.files to obtain instances of this class. File indexing is zero-based, i.e. files[0] stands for the very first file. len(files) might raise NotAvailable when called before receiving a DocInfoMessage. ''' def __cinit__(self, Document document not None, **kwargs): check_sentinel(self, kwargs) self._page_map = None self._document = document def __len__(self): cdef int result result = ddjvu_document_get_filenum(self._document.ddjvu_document) if result is None: raise _NotAvailable_ return result def __getitem__(self, key): cdef int i if is_int(key): if key < 0 or key >= len(self): raise IndexError('file number out of range') return File(self._document, key, sentinel = the_sentinel) elif typecheck(key, Page): if (key)._document is not self._document: raise KeyError(key) if self._page_map is None: self._page_map = {} for i in range(len(self)): file = File(self._document, i, sentinel = the_sentinel) n_page = file.n_page if n_page is not None: self._page_map[n_page] = file try: return self._page_map[(key)._n] except KeyError: raise KeyError(key) else: raise TypeError('DocumentFiles indices must be integers or Page instances') cdef class File: ''' Component file of a document. Use document.files[N] to obtain instances of this class. ''' def __cinit__(self, Document document not None, int n, **kwargs): check_sentinel(self, kwargs) self._document = document self._have_info = 0 self._n = n property document: '''Return the Document which includes the component file.''' def __get__(self): return self._document property n: ''' Return the component file number. File indexing is zero-based, i.e. 0 stands for the very first file. ''' def __get__(self): return self._n cdef object _get_info(self): cdef ddjvu_status_t status if self._have_info: return status = ddjvu_document_get_fileinfo(self._document.ddjvu_document, self._n, &self.ddjvu_fileinfo) ex = JobException_from_c(status) if ex is JobOK: return elif ex is JobStarted: raise _NotAvailable_ else: raise ex def get_info(self, wait=1): ''' F.get_info(wait=True) -> None Attempt to obtain information about the component file. If wait is true, wait until the information is available. Possible exceptions: NotAvailable, JobFailed. ''' cdef ddjvu_status_t status if self._have_info: return if not wait: return self._get_info() while 1: self._document._condition.acquire() try: status = ddjvu_document_get_fileinfo(self._document.ddjvu_document, self._n, &self.ddjvu_fileinfo) ex = JobException_from_c(status) if ex is JobOK: self._have_info = 1 return elif ex is JobStarted: self._document._condition.wait() else: raise ex finally: self._document._condition.release() property type: ''' Return the type of the compound file: * FILE_TYPE_PAGE, * FILE_TYPE_THUMBNAILS, * FILE_TYPE_INCLUDE. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): cdef char buffer[2] self._get_info() buffer[0] = self.ddjvu_fileinfo.type buffer[1] = '\0' return charp_to_string(buffer) property n_page: ''' Return the page number, or None when not applicable. Page indexing is zero-based, i.e. 0 stands for the very first page. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() if self.ddjvu_fileinfo.pageno < 0: return else: return self.ddjvu_fileinfo.pageno property page: ''' Return the page, or None when not applicable. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() if self.ddjvu_fileinfo.pageno < 0: return else: return self._document.pages[self.ddjvu_fileinfo.pageno] property size: ''' Return the compound file size, or None when unknown. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() if self.ddjvu_fileinfo.size < 0: return else: return self.ddjvu_fileinfo.size property id: ''' Return the compound file identifier, or None. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() cdef char* result result = self.ddjvu_fileinfo.id if result == NULL: return else: return decode_utf8(result) property name: ''' Return the compound file name, or None. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() cdef char* result result = self.ddjvu_fileinfo.name if result == NULL: return else: return decode_utf8(result) property title: ''' Return the compound file title, or None. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._get_info() cdef char* result result = self.ddjvu_fileinfo.title if result == NULL: return else: return decode_utf8(result) property dump: ''' Return a text describing the contents of the file using the same format as the djvudump command. If the information is not available, raise NotAvailable exception. Then, PageInfoMessage messages with empty page_job may be emitted. Possible exceptions: NotAvailable. ''' def __get__(self): cdef char* s s = ddjvu_document_get_filedump(self._document.ddjvu_document, self._n) if s == NULL: raise _NotAvailable_ try: return decode_utf8(s) finally: free(s) cdef object pages_to_opt(object pages, int sort_uniq): if sort_uniq: pages = sorted(frozenset(pages)) else: pages = list(pages) for i in range(len(pages)): if not is_int(pages[i]): raise TypeError('page numbers must be integers') if pages[i] < 0: raise ValueError('page number out of range') pages[i] = pages[i] + 1 result = '--pages=' + (','.join(imap(str, pages))) if is_unicode(result): result = encode_utf8(result) return result PRINT_ORIENTATION_AUTO = None PRINT_ORIENTATION_LANDSCAPE = 'landscape' PRINT_ORIENTATION_PORTRAIT = 'portrait' cdef object PRINT_RENDER_MODE_MAP PRINT_RENDER_MODE_MAP = { DDJVU_RENDER_COLOR: None, DDJVU_RENDER_BLACK: 'bw', DDJVU_RENDER_FOREGROUND: 'fore', DDJVU_RENDER_BACKGROUND: 'back' } PRINT_BOOKLET_NO = None PRINT_BOOKLET_YES = 'yes' PRINT_BOOKLET_RECTO = 'recto' PRINT_BOOKLET_VERSO = 'verso' cdef object PRINT_BOOKLET_OPTIONS PRINT_BOOKLET_OPTIONS = (PRINT_BOOKLET_NO, PRINT_BOOKLET_YES, PRINT_BOOKLET_RECTO, PRINT_BOOKLET_VERSO) cdef class SaveJob(Job): ''' Document saving job. Use document.save(...) to obtain instances of this class. ''' def __cinit__(self, **kwargs): self._file = None def wait(self): Job.wait(self) # Ensure that the underlying file is flushed. # FIXME: In Python 3, the file might be never flushed if you don't use wait()! if self._file is not None: (<_FileWrapper> self._file).close() self._file = None cdef class DocumentDecodingJob(Job): ''' Document decoding job. Use document.decoding_job to obtain instances of this class. ''' cdef object __init_ddj(self, Document document): self._context = document._context self._document = document self._condition = document._condition self._queue = document._queue self.ddjvu_job = document.ddjvu_document def __dealloc__(self): self.ddjvu_job = NULL # Don't allow Job.__dealloc__ to release the job. def __repr__(self): return '<{tp} for {doc!r}>'.format( tp=get_type_name(DocumentDecodingJob), doc=self._document, ) cdef class Document: ''' DjVu document. Use context.new_document(...) to obtain instances of this class. ''' def __cinit__(self, **kwargs): self.ddjvu_document = NULL check_sentinel(self, kwargs) self._pages = DocumentPages(self, sentinel = the_sentinel) self._files = DocumentFiles(self, sentinel = the_sentinel) self._context = None self._queue = Queue() self._condition = Condition() cdef object __init(self, Context context, ddjvu_document_t *ddjvu_document): # Assumption: loft_lock is already acquired. assert (context is not None) and ddjvu_document != NULL self.ddjvu_document = ddjvu_document self._context = context _document_loft.add(self) _document_weak_loft[voidp_to_int(ddjvu_document)] = self cdef object __clear(self): with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: _document_loft.discard(self) finally: release_lock(loft_lock) property decoding_status: ''' Return a JobException subclass indicating the decoding job status. ''' def __get__(self): return JobException_from_c(ddjvu_document_decoding_status(self.ddjvu_document)) property decoding_error: ''' Indicate whether the decoding job failed. ''' def __get__(self): return bool(ddjvu_document_decoding_error(self.ddjvu_document)) property decoding_done: ''' Indicate whether the decoding job is done. ''' def __get__(self): return bool(ddjvu_document_decoding_done(self.ddjvu_document)) property decoding_job: ''' Return the DocumentDecodingJob. ''' def __get__(self): cdef DocumentDecodingJob job job = DocumentDecodingJob(sentinel = the_sentinel) job.__init_ddj(self) return job property type: ''' Return the type of the document. The following values are possible: * DOCUMENT_TYPE_UNKNOWN; * DOCUMENT_TYPE_SINGLE_PAGE: single-page document; * DOCUMENT_TYPE_BUNDLED: bundled multi-page document; * DOCUMENT_TYPE_INDIRECT: indirect multi-page document; * (obsolete) DOCUMENT_TYPE_OLD_BUNDLED, * (obsolete) DOCUMENT_TYPE_OLD_INDEXED. Before receiving the DocInfoMessage, DOCUMENT_TYPE_UNKNOWN may be returned. ''' def __get__(self): return ddjvu_document_get_type(self.ddjvu_document) property pages: ''' Return the DocumentPages. ''' def __get__(self): return self._pages property files: ''' Return the DocumentPages. ''' def __get__(self): return self._files property outline: ''' Return the DocumentOutline. ''' def __get__(self): return DocumentOutline(self) property annotations: ''' Return the DocumentAnnotations. ''' def __get__(self): return DocumentAnnotations(self) def __dealloc__(self): if self.ddjvu_document == NULL: return ddjvu_document_release(self.ddjvu_document) def save(self, file=None, indirect=None, pages=None, wait=1): ''' D.save(file=None, indirect=None, pages=, wait=True) -> a SaveJob Save the document as: * a bundled DjVu file or; * an indirect DjVu document with index file name indirect. pages argument specifies a subset of saved pages. If wait is true, wait until the job is done. ''' cdef const char * optv[2] cdef int optc cdef SaveJob job optc = 0 cdef FILE* output cdef Py_ssize_t i cdef _FileWrapper file_wrapper if indirect is None: file_wrapper = _FileWrapper(file, "wb") output = file_wrapper.cfile else: if file is not None: raise TypeError('file must be None if indirect is specified') if not is_string(indirect): raise TypeError('indirect must be a string') file_wrapper = None output = NULL s1 = '--indirect=' + indirect if is_unicode(s1): s1 = encode_utf8(s1) optv[optc] = s1 optc = optc + 1 if pages is not None: s2 = pages_to_opt(pages, 1) optv[optc] = s2 optc = optc + 1 with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: job = SaveJob(sentinel = the_sentinel) job.__init(self._context, ddjvu_document_save(self.ddjvu_document, output, optc, optv)) job._file = file_wrapper finally: release_lock(loft_lock) if wait: job.wait() return job def export_ps(self, file, pages=None, eps=0, level=None, orientation=PRINT_ORIENTATION_AUTO, mode=DDJVU_RENDER_COLOR, zoom=None, color=1, srgb=1, gamma=None, copies=1, frame=0, crop_marks=0, text=0, booklet=PRINT_BOOKLET_NO, booklet_max=0, booklet_align=0, booklet_fold=(18, 200), wait=1): ''' D.export_ps(file, pages=, ..., wait=True) -> a Job Convert the document into PostScript. pages argument specifies a subset of saved pages. If wait is true, wait until the job is done. Additional options ------------------ eps Produce an *Encapsulated* PostScript file. Encapsulated PostScript files are suitable for embedding images into other documents. Encapsulated PostScript file can only contain a single page. Setting this option overrides the options copies, orientation, zoom, crop_marks, and booklet. level Selects the language level of the generated PostScript. Valid language levels are 1, 2, and 3. Level 3 produces the most compact and fast printing PostScript files. Some of these files however require a very modern printer. Level 2 is the default value. The generated PostScript files are almost as compact and work with all but the oldest PostScript printers. Level 1 can be used as a last resort option. orientation Specifies the pages orientation: PRINT_ORIENTATION_AUTO automatic PRINT_ORIENTATION_PORTRAIT portrait PRINT_ORIENTATION_LANDSCAPE landscape mode Specifies how pages should be decoded: RENDER_COLOR render all the layers of the DjVu documents RENDER_BLACK render only the foreground layer mask RENDER_FOREGROUND render only the foreground layer RENDER_BACKGROUND render only the background layer zoom Specifies a zoom factor. The default zoom factor scales the image to fit the page. color Specifies whether to generate a color or a gray scale PostScript file. A gray scale PostScript files are smaller and marginally more portable. srgb The default value, True, generates a PostScript file using device independent colors in compliance with the sRGB specification. Modern printers then produce colors that match the original as well as possible. Specifying a false value generates a PostScript file using device dependent colors. This is sometimes useful with older printers. You can then use the gamma option to tune the output colors. gamma Specifies a gamma correction factor for the device dependent PostScript colors. Argument must be in range 0.3 to 5.0. Gamma correction normally pertains to cathodic screens only. It gets meaningful for printers because several models interpret device dependent RGB colors by emulating the color response of a cathodic tube. copies Specifies the number of copies to print. frame, If true, generate a thin gray border representing the boundaries of the document pages. crop_marks If true, generate crop marks indicating where pages should be cut. text Generate hidden text. This option is deprecated. See also the warning below. booklet * PRINT_BOOKLET_NO Disable booklet mode. This is the default. * PRINT_BOOKLET_YES: Enable recto/verse booklet mode. * PRINT_BOOKLET_RECTO Enable recto booklet mode. * PRINT_BOOKLET_VERSO Enable verso booklet mode. booklet_max Specifies the maximal number of pages per booklet. A single printout might then be composed of several booklets. The argument is rounded up to the next multiple of 4. Specifying 0 sets no maximal number of pages and ensures that the printout will produce a single booklet. This is the default. booklet_align Specifies a positive or negative offset applied to the verso of each sheet. The argument is expressed in points[1]_. This is useful with certain printers to ensure that both recto and verso are properly aligned. The default value is 0. booklet_fold (= (base, increment)) Specifies the extra margin left between both pages on a single sheet. The base value is expressed in points[1]_. This margin is incremented for each outer sheet by value expressed in millipoints. The default value is (18, 200). .. [1] 1 pt = 1/72 in = 0.3528 mm ''' cdef FILE* output cdef SaveJob job cdef _FileWrapper file_wrapper options = [] file_wrapper = _FileWrapper(file, "wb") output = file_wrapper.cfile if pages is not None: list_append(options, pages_to_opt(pages, 0)) if eps: list_append(options, '--format=eps') if level is not None: if not is_int(level): raise TypeError('level must be an integer') list_append(options, '--level={0}'.format(level)) if orientation is not None: if not is_string(orientation): raise TypeError('orientation must be a string or none') list_append(options, '--orientation=' + orientation) if not is_int(mode): raise TypeError('mode must be an integer') try: mode = PRINT_RENDER_MODE_MAP[mode] if mode is not None: list_append(options, '--mode=' + mode) except KeyError: raise ValueError('mode must be equal to RENDER_COLOR, or RENDER_BLACK, or RENDER_FOREGROUND, or RENDER_BACKGROUND') if zoom is not None: if not is_int(zoom): raise TypeError('zoom must be an integer or none') list_append(options, '--zoom={0}'.format(zoom)) if not color: list_append(options, '--color=no') if not srgb: list_append(options, '--srgb=no') if gamma is not None: if not is_int(gamma) and not is_float(gamma): raise TypeError('gamma must be a number or none') list_append(options, '--gamma={0:.16f}'.format(gamma)) if not is_int(copies): raise TypeError('copies must be an integer') if copies != 1: list_append(options, '--options={0}'.format(copies)) if frame: list_append(options, '--frame') if crop_marks: list_append(options, '--cropmarks') if text: list_append(options, '--text') if booklet is not None: if not is_string(booklet): raise TypeError('booklet must be a string or none') if options not in PRINT_BOOKLET_OPTIONS: raise ValueError('booklet must be equal to PRINT_BOOKLET_NO, or PRINT_BOOKLET_YES, or PRINT_BOOKLET_VERSO, or PRINT_BOOKLET_RECTO') list_append(options, '--booklet=' + booklet) if not is_int(booklet_max): raise TypeError('booklet_max must be an integer') if booklet_max: list_append(options, '--bookletmax={0}'.format(booklet_max)) if not is_int(booklet_align): raise TypeError('booklet_align must be an integer') if booklet_align: list_append(options, '--bookletalign={0}'.format(booklet_align)) if is_int(booklet_fold): list_append(options, '--bookletfold={0}'.format(booklet_fold)) else: try: fold_base, fold_incr = booklet_fold if not is_int(fold_base) or not is_int(fold_incr): raise TypeError except TypeError: raise TypeError('booklet_fold must a be an integer or a pair of integers') list_append(options, '--bookletfold={0}+{1}'.format(fold_base, fold_incr)) cdef const char **optv cdef int optc cdef size_t buffer_size buffer_size = len(options) * sizeof (char*) optv = py_malloc(buffer_size) if optv == NULL: raise MemoryError('Unable to allocate {0} bytes for print options'.format(buffer_size)) try: for optc in range(len(options)): option = options[optc] if is_unicode(option): options[optc] = option = encode_utf8(option) optv[optc] = option with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: job = SaveJob(sentinel = the_sentinel) job.__init( self._context, ddjvu_document_print(self.ddjvu_document, output, len(options), optv) ) job._file = file_wrapper finally: release_lock(loft_lock) finally: py_free(optv) if wait: job.wait() return job property message_queue: ''' Return the internal message queue. ''' def __get__(self): return self._queue def get_message(self, wait=1): ''' D.get_message(wait=True) -> a Message or None Get message from the internal document queue. Return None if wait is false and no message is available. ''' try: return self._queue.get(wait) except Empty: return def __iter__(self): return self def __next__(self): return self.get_message() cdef Document Document_from_c(ddjvu_document_t* ddjvu_document): cdef Document result if ddjvu_document == NULL: result = None else: with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: result = _document_weak_loft.get(voidp_to_int(ddjvu_document)) finally: release_lock(loft_lock) return result class FileUri(str): ''' See the Document.new_document() method. ''' FileURI = FileUri cdef object Context_message_distributor def _Context_message_distributor(Context self not None, **kwargs): cdef Message message cdef Document document cdef Job job cdef PageJob page_job cdef ddjvu_message_t* ddjvu_message check_sentinel(self, kwargs) while 1: with nogil: ddjvu_message = ddjvu_message_wait(self.ddjvu_context) try: try: message = Message_from_c(ddjvu_message) finally: ddjvu_message_pop(self.ddjvu_context) if message is None: raise SystemError self.handle_message(message) # XXX Order of branches below is *crucial*. Do not change. if message._job is not None: job = message._job job._condition.acquire() try: job._condition.notifyAll() finally: job._condition.release() if job.is_done: job.__clear() elif message._page_job is not None: raise SystemError # should not happen elif message._document is not None: document = message._document document._condition.acquire() try: document._condition.notifyAll() finally: document._condition.release() if document.decoding_done: document.__clear() except KeyboardInterrupt: return except SystemExit: return except Exception: write_unraisable_exception(self) Context_message_distributor = _Context_message_distributor del _Context_message_distributor cdef class Context: def __cinit__(self, argv0=None): if argv0 is None: argv0 = sys.argv[0] if is_unicode(argv0): argv0 = encode_utf8(argv0) with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: self.ddjvu_context = ddjvu_context_create(argv0) if self.ddjvu_context == NULL: raise MemoryError('Unable to create DjVu context') _context_loft[voidp_to_int(self.ddjvu_context)] = self finally: release_lock(loft_lock) self._queue = Queue() thread.start_new_thread(Context_message_distributor, (self,), {'sentinel': the_sentinel}) property cache_size: def __set__(self, value): if 0 < value < (1 << 31): ddjvu_cache_set_size(self.ddjvu_context, value) else: raise ValueError('0 < cache_size < (2 ** 31) must be satisfied') def __get__(self): return ddjvu_cache_get_size(self.ddjvu_context) def handle_message(self, Message message not None): ''' C.handle_message(message) -> None This method is called, in a separate thread, for every received message, *before* any blocking method finishes. By default do something roughly equivalent to:: if message.job is not None: message.job.message_queue.put(message) elif message.document is not None: message.document.message_queue.put(message) else: message.context.message_queue.put(message) You may want to override this method to change this behaviour. All exceptions raised by this method will be ignored. ''' # XXX Order of branches below is *crucial*. Do not change. if message._job is not None: message._job._queue.put(message) elif message._page_job is not None: raise SystemError # should not happen elif message._document is not None: message._document._queue.put(message) else: message._context._queue.put(message) property message_queue: ''' Return the internal message queue. ''' def __get__(self): return self._queue def get_message(self, wait=1): ''' C.get_message(wait=True) -> a Message or None Get message from the internal context queue. Return None if wait is false and no message is available. ''' try: return self._queue.get(wait) except Empty: return def new_document(self, uri, cache=1): ''' C.new_document(uri, cache=True) -> a Document Creates a decoder for a DjVu document and starts decoding. This method returns immediately. The decoding job then generates messages to request the raw data and to indicate the state of the decoding process. uri specifies an optional URI for the document. The URI follows the usual syntax (protocol://machine/path). It should not end with a slash. It only serves two purposes: - The URI is used as a key for the cache of decoded pages. - The URI is used to document NewStreamMessage messages. Setting argument cache to a true value indicates that decoded pages should be cached when possible. It is important to understand that the URI is not used to access the data. The document generates NewStreamMessage messages to indicate which data is needed. The caller must then provide the raw data using a NewStreamMessage.stream object. To open a local file, provide a FileUri instance as a URI. Localized characters in uri should be in URI-encoded. Possible exceptions: JobFailed. ''' cdef Document document cdef ddjvu_document_t* ddjvu_document with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: if typecheck(uri, FileUri): IF PY3K: uri = encode_utf8(uri) ddjvu_document = ddjvu_document_create_by_filename(self.ddjvu_context, uri, cache) else: IF PY3K: uri = encode_utf8(uri) ddjvu_document = ddjvu_document_create(self.ddjvu_context, uri, cache) if ddjvu_document == NULL: raise JobFailed document = Document(sentinel = the_sentinel) document.__init(self, ddjvu_document) finally: release_lock(loft_lock) return document def __iter__(self): return self def __next__(self): return self.get_message() def clear_cache(self): ''' C.clear_cache() -> None ''' ddjvu_cache_clear(self.ddjvu_context) def __dealloc__(self): ddjvu_context_release(self.ddjvu_context) cdef Context Context_from_c(ddjvu_context_t* ddjvu_context): cdef Context result if ddjvu_context == NULL: result = None else: with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: try: result = _context_loft[voidp_to_int(ddjvu_context)] except KeyError: raise SystemError finally: release_lock(loft_lock) return result RENDER_COLOR = DDJVU_RENDER_COLOR RENDER_BLACK = DDJVU_RENDER_BLACK RENDER_COLOR_ONLY = DDJVU_RENDER_COLORONLY RENDER_MASK_ONLY = DDJVU_RENDER_MASKONLY RENDER_BACKGROUND = DDJVU_RENDER_BACKGROUND RENDER_FOREGROUND = DDJVU_RENDER_FOREGROUND PAGE_TYPE_UNKNOWN = DDJVU_PAGETYPE_UNKNOWN PAGE_TYPE_BITONAL = DDJVU_PAGETYPE_BITONAL PAGE_TYPE_PHOTO = DDJVU_PAGETYPE_PHOTO PAGE_TYPE_COMPOUND = DDJVU_PAGETYPE_COMPOUND cdef class PixelFormat: ''' Abstract pixel format. Don't use this class directly, use one of its subclasses. ''' def __cinit__(self, *args, **kwargs): self._row_order = 0 self._y_direction = 0 self._dither_bpp = 32 self._gamma = 2.2 self.ddjvu_format = NULL for cls in (PixelFormatRgb, PixelFormatRgbMask, PixelFormatGrey, PixelFormatPalette, PixelFormatPackedBits): if typecheck(self, cls): return raise_instantiation_error(type(self)) property rows_top_to_bottom: ''' Flag indicating whether the rows in the pixel buffer are stored starting from the top or the bottom of the image. Default ordering starts from the bottom of the image. This is the opposite of the X11 convention. ''' def __get__(self): return bool(self._row_order) def __set__(self, value): ddjvu_format_set_row_order(self.ddjvu_format, not not value) property y_top_to_bottom: ''' Flag indicating whether the *y* coordinates in the drawing area are oriented from bottom to top, or from top to bottom. The default is bottom to top, similar to PostScript. This is the opposite of the X11 convention. ''' def __get__(self): return bool(self._row_order) def __set__(self, value): ddjvu_format_set_y_direction(self.ddjvu_format, not not value) property bpp: ''' Return the depth of the image, in bits per pixel. ''' def __get__(self): return self._bpp property dither_bpp: ''' The final depth of the image on the screen. This is used to decide which dithering algorithm should be used. The default is usually appropriate. ''' def __get__(self): return self._dither_bpp def __set__(self, int value): if (0 < value < 64): ddjvu_format_set_ditherbits(self.ddjvu_format, value) self._dither_bpp = value else: raise ValueError('0 < value < 64 must be satisfied') property gamma: ''' Gamma of the display for which the pixels are intended. This will be combined with the gamma stored in DjVu documents in order to compute a suitable color correction. The default value is 2.2. ''' def __get__(self): return self._gamma def __set__(self, double value): if (0.5 <= value <= 5.0): ddjvu_format_set_gamma(self.ddjvu_format, value) else: raise ValueError('0.5 <= value <= 5.0 must be satisfied') def __dealloc__(self): if self.ddjvu_format != NULL: ddjvu_format_release(self.ddjvu_format) def __repr__(self): return '{tp}()'.format(tp=get_type_name(type(self))) cdef class PixelFormatRgb(PixelFormat): ''' PixelFormatRgb([byteorder='RGB']) -> a pixel format 24-bit pixel format, with: - RGB (byteorder == 'RGB') or - BGR (byteorder == 'BGR') byte order. ''' def __cinit__(self, byte_order='RGB', unsigned int bpp=24): cdef ddjvu_format_style_t _format if byte_order == 'RGB': self._rgb = 1 _format = DDJVU_FORMAT_RGB24 elif byte_order == 'BGR': self._rgb = 0 _format = DDJVU_FORMAT_BGR24 else: raise ValueError("byte_order must be equal to 'RGB' or 'BGR'") if bpp != 24: raise ValueError('bpp must be equal to 24') self._bpp = 24 self.ddjvu_format = ddjvu_format_create(_format, 0, NULL) property byte_order: ''' Return the byte order: - 'RGB' or - 'BGR'. ''' def __get__(self): if self._rgb: return 'RGB' else: return 'BGR' def __repr__(self): return '{tp}(byte_order = {bo!r}, bpp = {bpp})'.format( tp=get_type_name(PixelFormatRgb), bo=self.byte_order, bpp=self.bpp, ) cdef class PixelFormatRgbMask(PixelFormat): ''' PixelFormatRgbMask(red_mask, green_mask, blue_mask[, xor_value], bpp=16) -> a pixel format PixelFormatRgbMask(red_mask, green_mask, blue_mask[, xor_value], bpp=32) -> a pixel format red_mask, green_mask and blue_mask are bit masks for color components for each pixel. The resulting color is then xored with the xor_value. For example, PixelFormatRgbMask(0xF800, 0x07E0, 0x001F, bpp=16) is a highcolor format with: - 5 (most significant) bits for red, - 6 bits for green, - 5 (least significant) bits for blue. ''' def __cinit__(self, unsigned int red_mask, unsigned int green_mask, unsigned int blue_mask, unsigned int xor_value = 0, unsigned int bpp = 16): cdef ddjvu_format_style_t _format if bpp == 16: _format = DDJVU_FORMAT_RGBMASK16 red_mask = red_mask & 0xFFFF blue_mask = blue_mask & 0xFFFF green_mask = green_mask & 0xFFFF xor_value = xor_value & 0xFFFF elif bpp == 32: _format = DDJVU_FORMAT_RGBMASK32 red_mask = red_mask & 0xFFFFFFFF blue_mask = blue_mask & 0xFFFFFFFF green_mask = green_mask & 0xFFFFFFFF xor_value = xor_value & 0xFFFFFFFF else: raise ValueError('bpp must be equal to 16 or 32') self._bpp = self._dither_bpp = bpp (self._params[0], self._params[1], self._params[2], self._params[3]) = (red_mask, green_mask, blue_mask, xor_value) self.ddjvu_format = ddjvu_format_create(_format, 4, self._params) def __repr__(self): return '{tp}(red_mask = 0x{r:0{w}x}, green_mask = 0x{g:0{w}x}, blue_mask = 0x{b:0{w}x}, xor_value = 0x{x:0{w}x}, bpp = {bpp})'.format( tp=get_type_name(PixelFormatRgbMask), r=self._params[0], g=self._params[1], b=self._params[2], x=self._params[3], w=self.bpp//4, bpp=self.bpp, ) cdef class PixelFormatGrey(PixelFormat): ''' PixelFormatGrey() -> a pixel format 8-bit, grey pixel format. ''' def __cinit__(self, unsigned int bpp = 8): cdef unsigned int params[4] if bpp != 8: raise ValueError('bpp must be equal to 8') self._bpp = self._dither_bpp = bpp self.ddjvu_format = ddjvu_format_create(DDJVU_FORMAT_GREY8, 0, NULL) def __repr__(self): return '{tp}(bpp = {bpp!r})'.format( tp=get_type_name(PixelFormatGrey), bpp=self.bpp, ) cdef class PixelFormatPalette(PixelFormat): ''' PixelFormatPalette(palette) -> a pixel format Palette pixel format. palette must be a dictionary which contains 216 (6 * 6 * 6) entries of a web color cube, such that: - for each key (r, g, b): r in range(0, 6), g in range(0, 6) etc.; - for each value v: v in range(0, 0x100). ''' def __cinit__(self, palette, unsigned int bpp = 8): cdef int i, j, k, n for i in range(6): for j in range(6): for k in range(6): n = palette[(i, j, k)] if not 0 <= n < 0x100: raise ValueError('palette entries must be in range(0, 0x100)') self._palette[i*6*6 + j*6 + k] = n if bpp != 8: raise ValueError('bpp must be equal to 8') self._bpp = self._dither_bpp = bpp self.ddjvu_format = ddjvu_format_create(DDJVU_FORMAT_PALETTE8, 216, self._palette) def __repr__(self): cdef int i, j, k io = StringIO() io.write(get_type_name(PixelFormatPalette) + '({') for i in range(6): for j in range(6): for k in range(6): io.write('({i}, {j}, {k}): 0x{v:02x}'.format(i=i, j=j, k=k, v=self._palette[i * 6 * 6 + j * 6 + k])) if not (i == j == k == 5): io.write(', ') io.write('}}, bpp = {bpp})'.format(bpp=self.bpp)) return io.getvalue() cdef class PixelFormatPackedBits(PixelFormat): ''' PixelFormatPackedBits(endianness) -> a pixel format Bitonal, 1 bit per pixel format with: - most significant bits on the left (endianness=='>') or - least significant bits on the left (endianness=='<'). ''' def __cinit__(self, endianness): cdef ddjvu_format_style_t _format if endianness == '<': self._little_endian = 1 _format = DDJVU_FORMAT_LSBTOMSB elif endianness == '>': self._little_endian = 0 _format = DDJVU_FORMAT_MSBTOLSB else: raise ValueError("endianness must be equal to '<' or '>'") self._bpp = 1 self._dither_bpp = 1 self.ddjvu_format = ddjvu_format_create(_format, 0, NULL) property endianness: ''' The endianness: - '<' (most significant bits on the left) or - '>' (least significant bits on the left). ''' def __get__(self): if self._little_endian: return '<' else: return '>' def __repr__(self): return '{tp}({end!r})'.format( tp=get_type_name(PixelFormatPackedBits), end=self.endianness, ) cdef object calculate_row_size(long width, long row_alignment, int bpp): cdef long result cdef object row_size if bpp == 1: row_size = (width >> 3) + ((width & 7) != 0) elif bpp & 7 == 0: row_size = width row_size = row_size * (bpp >> 3) else: raise SystemError result = ((row_size + (row_alignment - 1)) // row_alignment) * row_alignment return result cdef object allocate_image_memory(long width, long height, object buffer, void **memory): cdef Py_ssize_t c_requested_size cdef Py_ssize_t c_memory_size py_requested_size = int(width) * int(height) try: c_requested_size = py_requested_size except OverflowError: raise MemoryError('Unable to allocate {0} bytes for an image memory'.format(py_requested_size)) if buffer is None: result = charp_to_bytes(NULL, c_requested_size) memory[0] = result else: result = buffer buffer_to_writable_memory(buffer, memory, &c_memory_size) if c_memory_size < c_requested_size: raise ValueError('Image buffer is too small ({0} > {1})'.format(c_requested_size, c_memory_size)) return result cdef class PageJob(Job): ''' A page decoding job. Use page.decode(...) to obtain instances of this class. ''' cdef object __init(self, Context context, ddjvu_job_t *ddjvu_job): Job.__init(self, context, ddjvu_job) property width: ''' Return the page width in pixels. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): cdef int width width = ddjvu_page_get_width( self.ddjvu_job) if width == 0: raise _NotAvailable_ else: return width property height: ''' Return the page height in pixels. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): cdef int height height = ddjvu_page_get_height( self.ddjvu_job) if height == 0: raise _NotAvailable_ else: return height property size: ''' page_job.size == (page_job.width, page_job.height) Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): cdef int width cdef int height width = ddjvu_page_get_width( self.ddjvu_job) height = ddjvu_page_get_height( self.ddjvu_job) if width == 0 or height == 0: raise _NotAvailable_ else: return width, height property dpi: ''' Return the page resolution in pixels per inch. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): cdef int dpi dpi = ddjvu_page_get_resolution( self.ddjvu_job) if dpi == 0: raise _NotAvailable_ else: return dpi property gamma: ''' Return the gamma of the display for which this page was designed. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): return ddjvu_page_get_gamma( self.ddjvu_job) property version: ''' Return the version of the DjVu file format. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): return ddjvu_page_get_version( self.ddjvu_job) property type: ''' Return the type of the page data. Possible values are: * PAGE_TYPE_UNKNOWN, * PAGE_TYPE_BITONAL, * PAGE_TYPE_PHOTO, * PAGE_TYPE_COMPOUND. Possible exceptions: NotAvailable (before receiving a PageInfoMessage). ''' def __get__(self): cdef ddjvu_page_type_t type cdef int is_done is_done = self.is_done type = ddjvu_page_get_type( self.ddjvu_job) if type == DDJVU_PAGETYPE_UNKNOWN and not is_done: # XXX An unavoidable race condition raise _NotAvailable_ return type property initial_rotation: ''' Return the counter-clockwise page rotation angle (in degrees) specified by the orientation flags in the DjVu file. Brain damage warning -------------------- This is useful because maparea coordinates in the annotation chunks are expressed relative to the rotated coordinates whereas text coordinates in the hidden text data are expressed relative to the unrotated coordinates. ''' def __get__(self): return 90 * ddjvu_page_get_initial_rotation( self.ddjvu_job) property rotation: ''' Return the counter-clockwise rotation angle (in degrees) for the page. The rotation is automatically taken into account by render(...) method and width and height properties. ''' def __get__(self): return 90 * ddjvu_page_get_rotation( self.ddjvu_job) def __set__(self, int value): cdef ddjvu_page_rotation_t rotation if value == 0: rotation = DDJVU_ROTATE_0 elif value == 90: rotation = DDJVU_ROTATE_90 elif value == 180: rotation = DDJVU_ROTATE_180 elif value == 270: rotation = DDJVU_ROTATE_180 else: raise ValueError('rotation must be equal to 0, 90, 180, or 270') ddjvu_page_set_rotation( self.ddjvu_job, rotation) def __del__(self): ddjvu_page_set_rotation( self.ddjvu_job, ddjvu_page_get_initial_rotation( self.ddjvu_job)) def render(self, ddjvu_render_mode_t mode, page_rect, render_rect, PixelFormat pixel_format not None, long row_alignment=1, buffer=None): ''' J.render(mode, page_rect, render_rect, pixel_format, row_alignment=1, buffer=None) -> data Render a segment of a page with arbitrary scale. mode indicates which image layers should be rendered: RENDER_COLOR color page or stencil RENDER_BLACK stencil or color page RENDER_COLOR_ONLY color page or fail RENDER_MASK_ONLY stencil or fail RENDER_BACKGROUND color background layer RENDER_FOREGROUND color foreground layer Conceptually this method renders the full page into a rectangle page_rect and copies the pixels specified by rectangle render_rect into a buffer. The actual code is much more efficient than that. pixel_format specifies the expected pixel format. Each row will start at row_alignment bytes boundary. Data will be saved to the provided buffer or to a newly created string. This method makes a best effort to compute an image that reflects the most recently decoded data. Possible exceptions: NotAvailable (to indicate that no image could be computed at this point.) ''' cdef ddjvu_rect_t c_page_rect cdef ddjvu_rect_t c_render_rect cdef Py_ssize_t buffer_size cdef long row_size cdef int bpp cdef long x, y, w, h cdef void *memory if row_alignment <= 0: raise ValueError('row_alignment must be a positive integer') x, y, w, h = page_rect if w <= 0 or h <= 0: raise ValueError('page_rect width/height must be a positive integer') c_page_rect.x, c_page_rect.y, c_page_rect.w, c_page_rect.h = x, y, w, h if c_page_rect.x != x or c_page_rect.y != y or c_page_rect.w != w or c_page_rect.h != h: raise OverflowError('render_rect coordinates are too large') x, y, w, h = render_rect if w <= 0 or h <= 0: raise ValueError('render_rect width/height must be a positive integer') c_render_rect.x, c_render_rect.y, c_render_rect.w, c_render_rect.h = x, y, w, h if c_render_rect.x != x or c_render_rect.y != y or c_render_rect.w != w or c_render_rect.h != h: raise OverflowError('render_rect coordinates are too large') if ( c_page_rect.x > c_render_rect.x or c_page_rect.y > c_render_rect.y or c_page_rect.x + c_page_rect.w < c_render_rect.x + c_render_rect.w or c_page_rect.y + c_page_rect.h < c_render_rect.y + c_render_rect.h ): raise ValueError('render_rect must be inside page_rect') row_size = calculate_row_size(c_render_rect.w, row_alignment, pixel_format._bpp) result = allocate_image_memory(row_size, c_render_rect.h, buffer, &memory) if ddjvu_page_render( self.ddjvu_job, mode, &c_page_rect, &c_render_rect, pixel_format.ddjvu_format, row_size, memory) == 0: raise _NotAvailable_ return result def __dealloc__(self): if self.ddjvu_job == NULL: return ddjvu_page_release( self.ddjvu_job) self.ddjvu_job = NULL cdef PageJob PageJob_from_c(ddjvu_page_t* ddjvu_page): cdef PageJob job job = Job_from_c( ddjvu_page) return job cdef class Job: ''' A job. ''' def __cinit__(self, **kwargs): check_sentinel(self, kwargs) self._context = None self.ddjvu_job = NULL self._condition = Condition() self._queue = Queue() cdef object __init(self, Context context, ddjvu_job_t *ddjvu_job): # Assumption: loft_lock is already acquired. assert (context is not None) and ddjvu_job != NULL self._context = context self.ddjvu_job = ddjvu_job _job_loft.add(self) _job_weak_loft[voidp_to_int(ddjvu_job)] = self cdef object __clear(self): with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: _job_loft.discard(self) finally: release_lock(loft_lock) property status: ''' Return a JobException subclass indicating the job status. ''' def __get__(self): return JobException_from_c(ddjvu_job_status(self.ddjvu_job)) property is_error: ''' Indicate whether the job failed. ''' def __get__(self): return bool(ddjvu_job_error(self.ddjvu_job)) property is_done: ''' Indicate whether the decoding job is done. ''' def __get__(self): return bool(ddjvu_job_done(self.ddjvu_job)) def wait(self): ''' J.wait() -> None Wait until the job is done. ''' while 1: self._condition.acquire() try: if ddjvu_job_done(self.ddjvu_job): break self._condition.wait() finally: self._condition.release() def stop(self): ''' J.stop() -> None Attempt to cancel the job. This is a best effort method. There no guarantee that the job will actually stop. ''' ddjvu_job_stop(self.ddjvu_job) property message_queue: ''' Return the internal message queue. ''' def __get__(self): return self._queue def get_message(self, wait=1): ''' J.get_message(wait=True) -> a Message or None Get message from the internal job queue. Return None if wait is false and no message is available. ''' try: return self._queue.get(wait) except Empty: return def __iter__(self): return self def __next__(self): return self.get_message() def __dealloc__(self): if self.ddjvu_job == NULL: return ddjvu_job_release(self.ddjvu_job) self.ddjvu_job = NULL cdef Job Job_from_c(ddjvu_job_t* ddjvu_job): cdef Job result if ddjvu_job == NULL: result = None else: with nogil: acquire_lock(loft_lock, WAIT_LOCK) try: result = _job_weak_loft.get(voidp_to_int(ddjvu_job)) finally: release_lock(loft_lock) return result cdef class AffineTransform: ''' AffineTransform((x0, y0, w0, h0), (x1, y1, w1, h1)) -> an affine coordinate transformation The object represents an affine coordinate transformation that maps points from rectangle (x0, y0, w0, h0) to rectangle (x1, y1, w1, h1). ''' def __cinit__(self, input, output): cdef ddjvu_rect_t c_input cdef ddjvu_rect_t c_output self.ddjvu_rectmapper = NULL (c_input.x, c_input.y, c_input.w, c_input.h) = input (c_output.x, c_output.y, c_output.w, c_output.h) = output self.ddjvu_rectmapper = ddjvu_rectmapper_create(&c_input, &c_output) def rotate(self, int n): ''' A.rotate(n) -> None Rotate the output rectangle counter-clockwise by n degrees. ''' if n % 90: raise ValueError('n must a multiple of 90') else: ddjvu_rectmapper_modify(self.ddjvu_rectmapper, n // 90, 0, 0) def __call__(self, value): cdef ddjvu_rect_t rect IF PY3K: next = iter(value).__next__ ELSE: next = iter(value).next try: rect.x = next() rect.y = next() except StopIteration: raise ValueError('value must be a pair or a 4-tuple') try: rect.w = next() except StopIteration: ddjvu_map_point(self.ddjvu_rectmapper, &rect.x, &rect.y) return (rect.x, rect.y) try: rect.h = next() except StopIteration: raise ValueError('value must be a pair or a 4-tuple') try: next() except StopIteration: pass else: raise ValueError('value must be a pair or a 4-tuple') ddjvu_map_rect(self.ddjvu_rectmapper, &rect) return (rect.x, rect.y, int(rect.w), int(rect.h)) def apply(self, value): ''' A.apply((x0, y0)) -> (x1, y1) A.apply((x0, y0, w0, h0)) -> (x1, y1, w1, h1) Apply the coordinate transform to a point or a rectangle. ''' return self(value) def inverse(self, value): ''' A.inverse((x0, y0)) -> (x1, y1) A.inverse((x0, y0, w0, h0)) -> (x1, y1, w1, h1) Apply the inverse coordinate transform to a point or a rectangle. ''' cdef ddjvu_rect_t rect IF PY3K: next = iter(value).__next__ ELSE: next = iter(value).next try: rect.x = next() rect.y = next() except StopIteration: raise ValueError('value must be a pair or a 4-tuple') try: rect.w = next() except StopIteration: ddjvu_unmap_point(self.ddjvu_rectmapper, &rect.x, &rect.y) return (rect.x, rect.y) try: rect.h = next() except StopIteration: raise ValueError('value must be a pair or a 4-tuple') try: next() except StopIteration: pass else: raise ValueError('value must be a pair or a 4-tuple') ddjvu_unmap_rect(self.ddjvu_rectmapper, &rect) return (rect.x, rect.y, int(rect.w), int(rect.h)) def mirror_x(self): ''' A.mirror_x() Reverse the X coordinates of the output rectangle. ''' ddjvu_rectmapper_modify(self.ddjvu_rectmapper, 0, 1, 0) def mirror_y(self): ''' A.mirror_y() Reverse the Y coordinates of the output rectangle. ''' ddjvu_rectmapper_modify(self.ddjvu_rectmapper, 0, 0, 1) def __dealloc__(self): if self.ddjvu_rectmapper != NULL: ddjvu_rectmapper_release(self.ddjvu_rectmapper) cdef class Message: ''' An abstract message. ''' def __cinit__(self, **kwargs): check_sentinel(self, kwargs) self.ddjvu_message = NULL cdef object __init(self): if self.ddjvu_message == NULL: raise SystemError self._context = Context_from_c(self.ddjvu_message.m_any.context) self._document = Document_from_c(self.ddjvu_message.m_any.document) self._page_job = PageJob_from_c(self.ddjvu_message.m_any.page) self._job = Job_from_c(self.ddjvu_message.m_any.job) property context: ''' Return the concerned Context. ''' def __get__(self): return self._context property document: ''' Return the concerned Document or None. ''' def __get__(self): return self._document property page_job: ''' Return the concerned PageJob or None. ''' def __get__(self): return self._page_job property job: ''' Return the concerned Job or None. ''' def __get__(self): return self._job cdef class ErrorMessage(Message): ''' An ErrorMessage is generated whenever the decoder or the DDJVU API encounters an error condition. All errors are reported as error messages because they can occur asynchronously. ''' cdef object __init(self): Message.__init(self) IF HAVE_LANGINFO_H: locale_encoding = charp_to_string(nl_langinfo(CODESET)) ELSE: # Presumably a Windows system. import locale locale_encoding = locale.getpreferredencoding() if self.ddjvu_message.m_error.message != NULL: # Things can go awry if user calls setlocale() between the time the # message was created and the time it was received. Let's hope it # never happens, but don't throw an exception if it did anyway. self._message = self.ddjvu_message.m_error.message.decode(locale_encoding, 'replace') else: self._message = None if self.ddjvu_message.m_error.function != NULL: # Should be ASCII-only, so don't care about encoding. function = charp_to_string(self.ddjvu_message.m_error.function) else: function = None if self.ddjvu_message.m_error.filename != NULL: # Should be ASCII-only, so don't care about encoding. filename = charp_to_string(self.ddjvu_message.m_error.filename) else: filename = None self._location = (function, filename, self.ddjvu_message.m_error.lineno) property message: ''' Return the actual error message, as text. ''' def __get__(self): return self._message property location: ''' Return a (function, filename, line_no) tuple indicating where the error was detected. ''' def __get__(self): return self._location IF PY3K: def __str__(self): return self.message ELSE: def __str__(self): IF HAVE_LANGINFO_H: locale_encoding = charp_to_string(nl_langinfo(CODESET)) ELSE: # Presumably a Windows system. import locale locale_encoding = locale.getpreferredencoding() return self.message.encode(locale_encoding, 'replace') def __unicode__(self): return self.message def __repr__(self): return '<{tp}: {msg!r} at {loc!r}>'.format( tp=get_type_name(ErrorMessage), msg=self.message, loc=self.location, ) cdef class InfoMessage(Message): ''' An InfoMessage provides informational text indicating the progress of the decoding process. This might be displayed in the browser status bar. ''' cdef object __init(self): Message.__init(self) self._message = charp_to_string(self.ddjvu_message.m_error.message) property message: ''' Return the actual information message, as text. ''' def __get__(self): return self._message cdef class Stream: ''' Data stream. Use new_stream_message.stream to obtain instances of this class. ''' def __cinit__(self, Document document not None, int streamid, **kwargs): check_sentinel(self, kwargs) self._streamid = streamid self._document = document self._open = 1 def close(self): ''' S.close() -> None Indicate that no more data will be provided on the particular stream. ''' ddjvu_stream_close(self._document.ddjvu_document, self._streamid, 0) self._open = 0 def abort(self): ''' S.abort() -> None Indicate that no more data will be provided on the particular stream, because the user has interrupted the data transfer (for instance by pressing the stop button of a browser) and that the decoding threads should be stopped as soon as feasible. ''' ddjvu_stream_close(self._document.ddjvu_document, self._streamid, 1) self._open = 0 def flush(self): ''' S.flush() -> None Do nothing. (This method is provided solely to implement Python's file-like interface.) ''' def read(self, size=None): ''' S.read([size]) Raise IOError. (This method is provided solely to implement Python's file-like interface.) ''' raise IOError('write-only data stream') def write(self, data): ''' S.write(data) -> None Provide raw data to the DjVu decoder. This method should be called as soon as the data is available, for instance when receiving DjVu data from a network connection. ''' cdef char* raw_data cdef Py_ssize_t length if self._open: bytes_to_charp(data, &raw_data, &length) ddjvu_stream_write(self._document.ddjvu_document, self._streamid, raw_data, length) else: raise IOError('I/O operation on closed file') def __dealloc__(self): if self._document is None: return if self._open: ddjvu_stream_close(self._document.ddjvu_document, self._streamid, 1) cdef class NewStreamMessage(Message): ''' A NewStreamMessage is generated whenever the decoder needs to access raw DjVu data. The caller must then provide the requested data using the .stream file-like object. In the case of indirect documents, a single decoder might simultaneously request several streams of data. ''' cdef object __init(self): Message.__init(self) self._stream = Stream(self.document, self.ddjvu_message.m_newstream.streamid, sentinel = the_sentinel) self._name = charp_to_string(self.ddjvu_message.m_newstream.name) self._uri = charp_to_string(self.ddjvu_message.m_newstream.url) property stream: ''' Return the concerned Stream. ''' def __get__(self): return self._stream property name: ''' The first NewStreamMessage message always has .name set to None. It indicates that the decoder needs to access the data in the main DjVu file. Further NewStreamMessage messages are generated to access the auxiliary files of indirect or indexed DjVu documents. .name then provides the base name of the auxiliary file. ''' def __get__(self): return self._name property uri: ''' Return the requested URI. URI is set according to the uri argument provided to function Context.new_document(). The first NewMessageStream message always contain the URI passed to Context.new_document(). Subsequent NewMessageStream messages contain the URI of the auxiliary files for indirect or indexed DjVu documents. ''' def __get__(self): return self._uri cdef class DocInfoMessage(Message): ''' A DocInfoMessage indicates that basic information about the document has been obtained and decoded. Not much can be done before this happens. Check Document.decoding_status to determine whether the operation was successful. ''' cdef class PageInfoMessage(Message): ''' The page decoding process generates a PageInfoMessage: - when basic page information is available and before any RelayoutMessage or RedisplayMessage, - when the page decoding thread terminates. You can distinguish both cases using PageJob.status. A PageInfoMessage may be also generated as a consequence of reading Page.get_info() or Page.dump. ''' cdef class ChunkMessage(Message): ''' A ChunkMessage indicates that an additional chunk of DjVu data has been decoded. ''' cdef class RelayoutMessage(ChunkMessage): ''' A RelayoutMessage is generated when a DjVu viewer should recompute the layout of the page viewer because the page size and resolution information has been updated. ''' cdef class RedisplayMessage(ChunkMessage): ''' A RedisplayMessage is generated when a DjVu viewer should call PageJob.render() and redisplay the page. This happens, for instance, when newly decoded DjVu data provides a better image. ''' cdef class ThumbnailMessage(Message): ''' A ThumbnailMessage is sent when additional thumbnails are available. ''' cdef object __init(self): Message.__init(self) self._page_no = self.ddjvu_message.m_thumbnail.pagenum property thumbnail: ''' Return the Thumbnail. Raise NotAvailable if the Document has been garbage-collected. ''' def __get__(self): if self._document is None: raise _NotAvailable_ return self._document.pages[self._page_no].thumbnail cdef class ProgressMessage(Message): ''' A ProgressMessage is generated to indicate progress towards the completion of a print or save job. ''' cdef object __init(self): Message.__init(self) self._percent = self.ddjvu_message.m_progress.percent self._status = self.ddjvu_message.m_progress.status property percent: ''' Return the percent of the job done. ''' def __get__(self): return self._percent property status: ''' Return a JobException subclass indicating the current job status. ''' def __get__(self): return JobException_from_c(self._status) cdef object MESSAGE_MAP MESSAGE_MAP = { DDJVU_ERROR: ErrorMessage, DDJVU_INFO: InfoMessage, DDJVU_NEWSTREAM: NewStreamMessage, DDJVU_DOCINFO: DocInfoMessage, DDJVU_PAGEINFO: PageInfoMessage, DDJVU_RELAYOUT: RelayoutMessage, DDJVU_REDISPLAY: RedisplayMessage, DDJVU_CHUNK: ChunkMessage, DDJVU_THUMBNAIL: ThumbnailMessage, DDJVU_PROGRESS: ProgressMessage } cdef Message Message_from_c(ddjvu_message_t* ddjvu_message): cdef Message message if ddjvu_message == NULL: return try: klass = MESSAGE_MAP[ddjvu_message.m_any.tag] except KeyError: raise SystemError message = klass(sentinel = the_sentinel) message.ddjvu_message = ddjvu_message message.__init() return message cdef object JOB_EXCEPTION_MAP cdef object JOB_FAILED_SYMBOL, JOB_STOPPED_SYMBOL JOB_FAILED_SYMBOL = Symbol('failed') JOB_STOPPED_SYMBOL = Symbol('stopped') cdef object JobException_from_sexpr(object sexpr): if typecheck(sexpr, SymbolExpression): if sexpr.value is JOB_FAILED_SYMBOL: return JobFailed elif sexpr.value is JOB_STOPPED_SYMBOL: return JobStopped cdef JobException_from_c(ddjvu_status_t code): try: return JOB_EXCEPTION_MAP[code] except KeyError: raise SystemError class JobException(Exception): ''' Status of a job. Possibly, but not necessarily, exceptional. ''' class JobNotDone(JobException): ''' Operation is not yet done. ''' class JobNotStarted(JobNotDone): ''' Operation was not even started. ''' class JobStarted(JobNotDone): ''' Operation is in progress. ''' class JobDone(JobException): ''' Operation finished. ''' class JobOK(JobDone): ''' Operation finished successfully. ''' class JobFailed(JobDone): ''' Operation failed because of an error. ''' class JobStopped(JobFailed): ''' Operation was interrupted by user. ''' JOB_EXCEPTION_MAP = { DDJVU_JOB_NOTSTARTED: JobNotStarted, DDJVU_JOB_STARTED: JobStarted, DDJVU_JOB_OK: JobOK, DDJVU_JOB_FAILED: JobFailed, DDJVU_JOB_STOPPED: JobStopped } cdef class _SexprWrapper: def __cinit__(self, document, **kwargs): check_sentinel(self, kwargs) self._document_weakref = weakref.ref(document) def __call__(self): return cexpr2py(self._cexpr) def __dealloc__(self): cdef Document document if self._cexpr == NULL: return document = self._document_weakref() if document is None: return ddjvu_miniexp_release(document.ddjvu_document, self._cexpr) cdef _SexprWrapper wrap_sexpr(Document document, cexpr_t cexpr): cdef _SexprWrapper result result = _SexprWrapper(document, sentinel = the_sentinel) result._cexpr = cexpr return result cdef class DocumentOutline(DocumentExtension): ''' DocumentOutline(document) -> a document outline ''' def __cinit__(self, Document document not None): self._document = document self._sexpr = None cdef object _update_sexpr(self): if self._sexpr is not None: return self._sexpr = wrap_sexpr( self._document, ddjvu_document_get_outline(self._document.ddjvu_document) ) def wait(self): ''' O.wait() -> None Wait until the associated S-expression is available. ''' while 1: self._document._condition.acquire() try: try: self.sexpr return except NotAvailable: self._document._condition.wait() finally: self._document._condition.release() property sexpr: ''' Return the associated S-expression. See "Outline/Bookmark syntax" in the djvused manual page. If the S-expression is not available, raise NotAvailable exception. Then, PageInfoMessage messages with empty page_job may be emitted. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._update_sexpr() try: sexpr = self._sexpr() exception = JobException_from_sexpr(sexpr) if exception is not None: raise exception return sexpr except InvalidExpression: self._sexpr = None raise _NotAvailable_ def __repr__(self): return '{tp}({doc!r})'.format( tp=get_type_name(DocumentOutline), doc=self._document, ) cdef class Annotations: ''' Document or page annotation. Don't use this class directly, use one of its subclasses. ''' def __cinit__(self, *args, **kwargs): if typecheck(self, DocumentAnnotations): return if typecheck(self, PageAnnotations): return raise_instantiation_error(type(self)) cdef object _update_sexpr(self): raise NotImplementedError def wait(self): ''' A.wait() -> None Wait until the associated S-expression is available. ''' while 1: self._document._condition.acquire() try: try: self.sexpr return except NotAvailable: self._document._condition.wait() finally: self._document._condition.release() property sexpr: ''' Return the associated S-expression. See "Annotation syntax" in the djvused manual page. If the S-expression is not available, raise NotAvailable exception. Then, PageInfoMessage messages with empty page_job may be emitted. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._update_sexpr() try: sexpr = self._sexpr() exception = JobException_from_sexpr(sexpr) if exception is not None: raise exception return sexpr except InvalidExpression: self._sexpr = None raise _NotAvailable_ property background_color: ''' Parse the annotations and extract the desired background color as a color string '(#FFFFFF)'. See '(background ...)' in the djvused manual page. Return None if this information is not specified. ''' def __get__(self): cdef const char *result result = ddjvu_anno_get_bgcolor(self._sexpr._cexpr) if result == NULL: return return result property zoom: ''' Parse the annotations and extract the desired zoom factor. See '(zoom ...)' in the djvused manual page. Return None if this information is not specified. ''' def __get__(self): cdef const char *result result = ddjvu_anno_get_zoom(self._sexpr._cexpr) if result == NULL: return return result property mode: ''' Parse the annotations and extract the desired display mode. See '(mode ...)' in the djvused manual page. Return zero if this information is not specified. ''' def __get__(self): cdef const char *result result = ddjvu_anno_get_mode(self._sexpr._cexpr) if result == NULL: return return result property horizontal_align: ''' Parse the annotations and extract how the page image should be aligned horizontally. See '(align ...)' in the djvused manual page. Return None if this information is not specified. ''' def __get__(self): cdef const char *result result = ddjvu_anno_get_horizalign(self._sexpr._cexpr) if result == NULL: return return result property vertical_align: ''' Parse the annotations and extract how the page image should be aligned vertically. See '(align ...)' in the djvused manual page. Return None if this information is not specified. ''' def __get__(self): cdef const char *result result = ddjvu_anno_get_vertalign(self._sexpr._cexpr) if result == NULL: return return result property hyperlinks: ''' Return an associated Hyperlinks object. ''' def __get__(self): return Hyperlinks(self) property metadata: ''' Return an associated Metadata object. ''' def __get__(self): return Metadata(self) cdef class DocumentAnnotations(Annotations): ''' DocumentAnnotations(document[, shared=True]) -> document-wide annotations If shared is true and no document-wide annotations are available, shared annotations are considered document-wide. See also "Document annotations and metadata" in the djvuchanges.txt file. ''' def __cinit__(self, Document document not None, shared=1): self._document = document self._compat = shared self._sexpr = None cdef object _update_sexpr(self): if self._sexpr is not None: return self._sexpr = wrap_sexpr( self._document, ddjvu_document_get_anno(self._document.ddjvu_document, self._compat) ) property document: ''' Return the concerned Document. ''' def __get__(self): return self._document cdef class PageAnnotations(Annotations): ''' PageAnnotation(page) -> page annotations ''' def __cinit__(self, Page page not None): self._document = page._document self._page = page self._sexpr = None cdef object _update_sexpr(self): if self._sexpr is not None: return self._sexpr = wrap_sexpr( self._page._document, ddjvu_document_get_pageanno(self._page._document.ddjvu_document, self._page._n) ) property page: ''' Return the concerned page. ''' def __get__(self): return self._page TEXT_DETAILS_PAGE = Symbol('page') TEXT_DETAILS_COLUMN = Symbol('column') TEXT_DETAILS_REGION = Symbol('region') TEXT_DETAILS_PARAGRAPH = Symbol('para') TEXT_DETAILS_LINE = Symbol('line') TEXT_DETAILS_WORD = Symbol('word') TEXT_DETAILS_CHARACTER = Symbol('char') TEXT_DETAILS_ALL = None cdef object TEXT_DETAILS TEXT_DETAILS = { TEXT_DETAILS_PAGE: 7, TEXT_DETAILS_COLUMN: 6, TEXT_DETAILS_REGION: 5, TEXT_DETAILS_PARAGRAPH: 4, TEXT_DETAILS_LINE: 3, TEXT_DETAILS_WORD: 2, TEXT_DETAILS_CHARACTER: 1, } def cmp_text_zone(zonetype1, zonetype2): ''' cmp_text_zone(zonetype1, zonetype2) -> integer Return: - negative if zonetype1 is more concrete than zonetype2; - zero if zonetype1 == zonetype2; - positive if zonetype1 is more general than zonetype2. Possible zone types: - TEXT_ZONE_PAGE, - TEXT_ZONE_COLUMN, - TEXT_ZONE_REGION, - TEXT_ZONE_PARAGRAPH, - TEXT_ZONE_LINE, - TEXT_ZONE_WORD, - TEXT_ZONE_CHARACTER. ''' if not typecheck(zonetype1, Symbol) or not typecheck(zonetype2, Symbol): raise TypeError('zonetype must be a symbol') try: n1 = TEXT_DETAILS[zonetype1] n2 = TEXT_DETAILS[zonetype2] except KeyError: raise ValueError('zonetype must be equal to TEXT_ZONE_PAGE, or TEXT_ZONE_COLUMN, or TEXT_ZONE_REGION, or TEXT_ZONE_PARAGRAPH, or TEXT_ZONE_LINE, or TEXT_ZONE_WORD, or TEXT_ZONE_CHARACTER') if n1 < n2: return -1 elif n1 > n2: return 1 else: return 0 cdef class PageText: ''' PageText(page, details=TEXT_DETAILS_ALL) -> wrapper around page text details controls the level of details in the returned S-expression: - TEXT_DETAILS_PAGE, - TEXT_DETAILS_COLUMN, - TEXT_DETAILS_REGION, - TEXT_DETAILS_PARAGRAPH, - TEXT_DETAILS_LINE, - TEXT_DETAILS_WORD, - TEXT_DETAILS_CHARACTER, - TEXT_DETAILS_ALL. ''' def __cinit__(self, Page page not None, details=TEXT_DETAILS_ALL): if details is None: self._details = charp_to_bytes('', 0) elif not typecheck(details, Symbol): raise TypeError('details must be a symbol or none') elif details not in TEXT_DETAILS: raise ValueError('details must be equal to TEXT_DETAILS_PAGE, or TEXT_DETAILS_COLUMN, or TEXT_DETAILS_REGION, or TEXT_DETAILS_PARAGRAPH, or TEXT_DETAILS_LINE, or TEXT_DETAILS_WORD, or TEXT_DETAILS_CHARACTER or TEXT_DETAILS_ALL') else: self._details = details.bytes self._page = page self._sexpr = None cdef object _update_sexpr(self): if self._sexpr is None: self._sexpr = wrap_sexpr( self._page._document, ddjvu_document_get_pagetext(self._page._document.ddjvu_document, self._page._n, self._details) ) def wait(self): ''' PT.wait() -> None Wait until the associated S-expression is available. ''' while 1: self._page._document._condition.acquire() try: try: self.sexpr return except NotAvailable: self._page._document._condition.wait() finally: self._page._document._condition.release() property page: ''' Return the concerned page. ''' def __get__(self): return self._page property sexpr: ''' Return the associated S-expression. See "Hidden text syntax" in the djvused manual page. If the S-expression is not available, raise NotAvailable exception. Then, PageInfoMessage messages with empty page_job may be emitted. Possible exceptions: NotAvailable, JobFailed. ''' def __get__(self): self._update_sexpr() try: sexpr = self._sexpr() exception = JobException_from_sexpr(sexpr) if exception is not None: raise exception return sexpr except InvalidExpression: self._sexpr = None raise _NotAvailable_ cdef class Hyperlinks: ''' Hyperlinks(annotations) -> sequence of hyperlinks Parse the annotations and return a sequence of '(maparea ...)' S-expressions. See also '(maparea ...)' in the djvused manual page. ''' def __cinit__(self, Annotations annotations not None): cdef cexpr_t* all cdef cexpr_t* current all = ddjvu_anno_get_hyperlinks(annotations._sexpr._cexpr) if all == NULL: raise MemoryError try: current = all self._sexpr = [] while current[0]: list_append(self._sexpr, wrap_sexpr(annotations._document, current[0])) current = current + 1 finally: free(all) def __len__(self): return len(self._sexpr) def __getitem__(self, Py_ssize_t n): return self._sexpr[n]() cdef class Metadata: ''' Metadata(annotations) -> mapping of metadata Parse the annotations and return a mapping of metadata. See also '(metadata ...)' in the djvused manual page. ''' def __cinit__(self, Annotations annotations not None): cdef cexpr_t* all cdef cexpr_t* current self._annotations = annotations all = ddjvu_anno_get_metadata_keys(annotations._sexpr._cexpr) if all == NULL: raise MemoryError try: current = all keys = [] while current[0]: list_append(keys, unicode(wrap_sexpr(annotations._document, current[0])().value)) current = current + 1 self._keys = frozenset(keys) finally: free(all) def __len__(self): return len(self._keys) def __getitem__(self, key): cdef _WrappedCExpr cexpr_key cdef const char *s cexpr_key = py2cexpr(Symbol(key)) s = ddjvu_anno_get_metadata(self._annotations._sexpr._cexpr, cexpr_key.cexpr()) if s == NULL: raise KeyError(key) return decode_utf8(s) def keys(self): ''' M.keys() -> sequence of M's keys ''' return self._keys IF not PY3K: def iterkeys(self): ''' M.iterkeys() -> an iterator over the keys of M ''' return iter(self) def __iter__(self): return iter(self._keys) def values(self): ''' M.values() -> list of M's values ''' return map(self.__getitem__, self._keys) IF not PY3K: def itervalues(self): ''' M.itervalues() -> an iterator over values of M ''' return imap(self.__getitem__, self._keys) def items(self): ''' M.items() -> list of M's (key, value) pairs, as 2-tuples ''' return zip(self._keys, imap(self.__getitem__, self._keys)) IF not PY3K: def iteritems(self): ''' M.iteritems() -> an iterator over the (key, value) items of M ''' return izip(self._keys, imap(self.__getitem__, self._keys)) IF not PY3K: def has_key(self, k): ''' M.has_key(k) -> True if D has a key k, else False ''' return k in self def __contains__(self, k): return k in self._keys __author__ = 'Jakub Wilk ' IF PY3K: __version__ = decode_utf8(PYTHON_DJVULIBRE_VERSION) ELSE: __version__ = str(PYTHON_DJVULIBRE_VERSION) # vim:ts=4 sts=4 sw=4 et ft=pyrex python-djvulibre-0.8.4/djvu/dllpath.py0000644000000000000000000000410413277057165020014 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2011-2017 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. ''' ease finding DjVuLibre DLLs in non-standard locations ''' import ctypes import os if os.name != 'nt': raise ImportError('This module is for Windows only') try: # Python 3.X import winreg unicode = str except ImportError: # Python 2.X import _winreg as winreg def _get(key, subkey): unicode = type(b''.decode()) with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as registry: with winreg.OpenKey(registry, key) as regkey: value, tp = winreg.QueryValueEx(regkey, subkey) del tp if not isinstance(value, unicode): raise TypeError return value _djvulibre_key = r'Software\Microsoft\Windows\CurrentVersion\Uninstall\DjVuLibre+DjView' def guess_dll_path(): try: path = _get(_djvulibre_key, 'UninstallString') except (TypeError, WindowsError): return path = os.path.dirname(path) if os.path.isfile(os.path.join(path, 'libdjvulibre.dll')): return path def _guess_dll_version(): try: version = _get(_djvulibre_key, 'DisplayVersion') except (TypeError, WindowsError): return return version.split('+')[0] def set_dll_search_path(path=None): unicode = type(b''.decode()) if path is None: path = guess_dll_path() if path is None: return if not isinstance(path, unicode): raise TypeError ctypes.windll.kernel32.SetDllDirectoryW(path) return path __all__ = [ 'guess_dll_path', 'set_dll_search_path' ] # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/djvu/sexpr.pxd0000644000000000000000000000202213347744062017662 0ustar00rootroot00000000000000# Copyright © 2007-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. #cython: language_level=2 cdef extern from 'libdjvu/miniexp.h': struct cexpr_s 'miniexp_s' ctypedef cexpr_s* cexpr_t 'miniexp_t' cdef extern struct cvar_s 'minivar_s' ctypedef cvar_s cvar_t 'minivar_t' cdef class _WrappedCExpr: cdef cvar_t* cvar cdef cexpr_t cexpr(self) cdef object print_into(self, object, object, bint) cdef object as_string(self, object, bint) cdef object public_c2py(cexpr_t) cdef _WrappedCExpr public_py2c(object) # vim:ts=4 sts=4 sw=4 et ft=pyrex python-djvulibre-0.8.4/djvu/sexpr.pyx0000644000000000000000000010121013441253560017677 0ustar00rootroot00000000000000# Copyright © 2007-2019 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. #cython: autotestdict=False #cython: language_level=2 ''' DjVuLibre bindings: module for handling Lisp S-expressions ''' cimport cython include 'common.pxi' cdef extern from 'libdjvu/miniexp.h': int cexpr_is_int 'miniexp_numberp'(cexpr_t sexp) nogil int cexpr_to_int 'miniexp_to_int'(cexpr_t sexp) nogil cexpr_t int_to_cexpr 'miniexp_number'(int n) nogil int cexpr_is_symbol 'miniexp_symbolp'(cexpr_t sexp) nogil char* cexpr_to_symbol 'miniexp_to_name'(cexpr_t sexp) nogil cexpr_t symbol_to_cexpr 'miniexp_symbol'(char* name) nogil cexpr_t cexpr_nil 'miniexp_nil' cexpr_t cexpr_dummy 'miniexp_dummy' int cexpr_is_list 'miniexp_listp'(cexpr_t exp) nogil int cexpr_is_nonempty_list 'miniexp_consp'(cexpr_t exp) nogil int cexpr_length 'miniexp_length'(cexpr_t exp) nogil cexpr_t cexpr_head 'miniexp_car'(cexpr_t exp) nogil cexpr_t cexpr_tail 'miniexp_cdr'(cexpr_t exp) nogil cexpr_t cexpr_nth 'miniexp_nth'(int n, cexpr_t exp) nogil cexpr_t pair_to_cexpr 'miniexp_cons'(cexpr_t head, cexpr_t tail) nogil cexpr_t cexpr_replace_head 'miniexp_rplaca'(cexpr_t exp, cexpr_t new_head) nogil cexpr_t cexpr_replace_tail 'miniexp_rplacd'(cexpr_t exp, cexpr_t new_tail) nogil cexpr_t cexpr_reverse_list 'miniexp_reverse'(cexpr_t exp) nogil int cexpr_is_str 'miniexp_stringp'(cexpr_t cexpr) nogil const char * cexpr_to_str 'miniexp_to_str'(cexpr_t cexpr) nogil cexpr_t str_to_cexpr 'miniexp_string'(const char *s) nogil cexpr_t cexpr_substr 'miniexp_substring'(const char *s, int n) nogil cexpr_t cexpr_concat 'miniexp_concat'(cexpr_t cexpr_list) nogil cexpr_t gc_lock 'minilisp_acquire_gc_lock'(cexpr_t cexpr) nogil cexpr_t gc_unlock 'minilisp_release_gc_lock'(cexpr_t cexpr) nogil cvar_t* cvar_new 'minivar_alloc'() nogil void cvar_free 'minivar_free'(cvar_t* v) nogil cexpr_t* cvar_ptr 'minivar_pointer'(cvar_t* v) nogil IF HAVE_MINIEXP_IO_T: ctypedef cexpr_io_s cexpr_io_t 'miniexp_io_t' struct cexpr_io_s 'miniexp_io_s': int (*puts 'fputs')(cexpr_io_t*, char*) int (*getc 'fgetc')(cexpr_io_t*) int (*ungetc)(cexpr_io_t*, int) void *data[4] int *p_flags void cexpr_io_init 'miniexp_io_init'(cexpr_io_t *cio) enum: cexpr_io_print7bits 'miniexp_io_print7bits' cexpr_t cexpr_read 'miniexp_read_r'(cexpr_io_t *cio) cexpr_t cexpr_print 'miniexp_prin_r'(cexpr_io_t *cio, cexpr_t cexpr) cexpr_t cexpr_printw 'miniexp_pprin_r'(cexpr_io_t *cio, cexpr_t cexpr, int width) ELSE: int io_7bit 'minilisp_print_7bits' int (*io_puts 'minilisp_puts')(char *s) int (*io_getc 'minilisp_getc')() int (*io_ungetc 'minilisp_ungetc')(int c) cexpr_t cexpr_read 'miniexp_read'() cexpr_t cexpr_print 'miniexp_prin'(cexpr_t cexpr) cexpr_t cexpr_printw 'miniexp_pprin'(cexpr_t cexpr, int width) cdef extern from 'stdio.h': int EOF cdef object sys import sys cdef object format_exc from traceback import format_exc cdef object StringIO IF PY3K: from io import StringIO ELSE: from cStringIO import StringIO cdef object BytesIO from io import BytesIO cdef object weakref import weakref cdef object symbol_dict symbol_dict = weakref.WeakValueDictionary() cdef object codecs import codecs IF not HAVE_MINIEXP_IO_T: cdef Lock _myio_lock _myio_lock = allocate_lock() cdef class _ExpressionIO: IF HAVE_MINIEXP_IO_T: cdef cexpr_io_t cio cdef int flags ELSE: cdef int (*backup_io_puts)(const char *s) cdef int (*backup_io_getc)() cdef int (*backup_io_ungetc)(int c) cdef int backup_io_7bit cdef object stdin 'stdin_fp' cdef object stdout 'stdout_fp' cdef int stdout_binary cdef object buffer cdef object exc _reentrant = HAVE_MINIEXP_IO_T def __init__(self, object stdin=None, object stdout=None, int escape_unicode=True): IF not HAVE_MINIEXP_IO_T: global io_7bit, io_puts, io_getc, io_ungetc global _myio with nogil: acquire_lock(_myio_lock, WAIT_LOCK) self.backup_io_7bit = io_7bit self.backup_io_puts = io_puts self.backup_io_getc = io_getc self.backup_io_ungetc = io_ungetc self.stdin = stdin self.stdout = stdout IF PY3K: self.stdout_binary = not hasattr(stdout, 'encoding') ELSE: # In Python 2, sys.stdout has the encoding attribute, # even though it accepts byte strings. # Let's only make a special-case for codecs. self.stdout_binary = not isinstance(stdout, codecs.StreamReaderWriter) self.buffer = [] self.exc = None IF HAVE_MINIEXP_IO_T: cexpr_io_init(&self.cio) self.cio.data[0] = self self.cio.getc = _myio_getc self.cio.ungetc = _myio_ungetc self.cio.puts = _myio_puts if escape_unicode: self.flags = cexpr_io_print7bits else: self.flags = 0 self.cio.p_flags = &self.flags ELSE: io_getc = _myio_getc io_ungetc = _myio_ungetc io_puts = _myio_puts io_7bit = escape_unicode _myio = self @cython.final cdef close(self): IF not HAVE_MINIEXP_IO_T: global io_7bit, io_puts, io_getc, io_ungetc global _myio _myio = None self.stdin = None self.stdout = None self.buffer = None IF not HAVE_MINIEXP_IO_T: io_7bit = self.backup_io_7bit io_puts = self.backup_io_puts io_getc = self.backup_io_getc io_ungetc = self.backup_io_ungetc try: if self.exc is not None: raise self.exc[0], self.exc[1], self.exc[2] finally: IF not HAVE_MINIEXP_IO_T: release_lock(_myio_lock) self.exc = None IF HAVE_MINIEXP_IO_T: @cython.final cdef cexpr_t read(self): return cexpr_read(&self.cio) @cython.final cdef cexpr_t print_(self, cexpr_t cexpr): return cexpr_print(&self.cio, cexpr) @cython.final cdef cexpr_t printw(self, cexpr_t cexpr, int width): return cexpr_printw(&self.cio, cexpr, width) ELSE: @cython.final cdef cexpr_t read(self): return cexpr_read() @cython.final cdef cexpr_t print_(self, cexpr_t cexpr): return cexpr_print(cexpr) @cython.final cdef cexpr_t printw(self, cexpr_t cexpr, int width): return cexpr_printw(cexpr, width) IF HAVE_MINIEXP_IO_T: cdef int _myio_puts(cexpr_io_t* cio, const char *s): cdef _ExpressionIO io xio = <_ExpressionIO> cio.data[0] try: if xio.stdout_binary: xio.stdout.write(s) else: xio.stdout.write(decode_utf8(s)) except: xio.exc = sys.exc_info() return EOF cdef int _myio_getc(cexpr_io_t* cio): cdef _ExpressionIO xio cdef int result xio = <_ExpressionIO> cio.data[0] if xio.buffer: return xio.buffer.pop() try: s = xio.stdin.read(1) if not s: return EOF if is_unicode(s): s = encode_utf8(s) IF PY3K: xio.buffer += reversed(s) ELSE: xio.buffer += map(ord, reversed(s)) return xio.buffer.pop() except: xio.exc = sys.exc_info() return EOF cdef int _myio_ungetc(cexpr_io_t* cio, int c): cdef _ExpressionIO io xio = <_ExpressionIO> cio.data[0] list_append(xio.buffer, c) ELSE: cdef _ExpressionIO _myio cdef int _myio_puts(const char *s): try: if _myio.stdout_binary: _myio.stdout.write(s) else: _myio.stdout.write(decode_utf8(s)) except: _myio.exc = sys.exc_info() return EOF cdef int _myio_getc(): cdef int result if _myio.buffer: return _myio.buffer.pop() try: s = _myio.stdin.read(1) if not s: return EOF if is_unicode(s): s = encode_utf8(s) IF PY3K: _myio.buffer += reversed(s) ELSE: _myio.buffer += map(ord, reversed(s)) return _myio.buffer.pop() except: _myio.exc = sys.exc_info() return EOF cdef int _myio_ungetc(int c): list_append(_myio.buffer, c) cdef object the_sentinel the_sentinel = object() cdef class _WrappedCExpr: def __cinit__(self, object sentinel): if sentinel is not the_sentinel: raise_instantiation_error(type(self)) self.cvar = cvar_new() cdef cexpr_t cexpr(self): return cvar_ptr(self.cvar)[0] cdef object print_into(self, object stdout, object width, bint escape_unicode): cdef cexpr_t cexpr cdef _ExpressionIO xio if width is None: pass elif not is_int(width): raise TypeError('width must be an integer') elif width <= 0: raise ValueError('width <= 0') cexpr = self.cexpr() xio = _ExpressionIO(stdout=stdout, escape_unicode=escape_unicode) try: if width is None: xio.print_(cexpr) else: xio.printw(cexpr, width) finally: xio.close() cdef object as_string(self, object width, bint escape_unicode): stdout = StringIO() try: self.print_into(stdout, width, escape_unicode) return stdout.getvalue() finally: stdout.close() def __dealloc__(self): cvar_free(self.cvar) cdef _WrappedCExpr wexpr(cexpr_t cexpr): cdef _WrappedCExpr wexpr wexpr = _WrappedCExpr(sentinel = the_sentinel) cvar_ptr(wexpr.cvar)[0] = cexpr return wexpr cdef class _MissingCExpr(_WrappedCExpr): cdef object print_into(self, object stdout, object width, bint escape_unicode): raise NotImplementedError cdef object as_string(self, object width, bint escape_unicode): raise NotImplementedError cdef _MissingCExpr wexpr_missing(): return _MissingCExpr(the_sentinel) cdef class BaseSymbol: cdef object __weakref__ cdef object _bytes def __cinit__(self, bytes): cdef char *cbytes cbytes = bytes self._bytes = cbytes def __repr__(self): IF PY3K: try: string = self._bytes.decode('UTF-8') except UnicodeDecodeError: string = self._bytes ELSE: string = self._bytes return '{tp}({s!r})'.format(tp=get_type_name(_Symbol_), s=string) def __richcmp__(self, object other, int op): cdef BaseSymbol _self, _other if not typecheck(self, BaseSymbol) or not typecheck(other, BaseSymbol): return NotImplemented _self = self _other = other return richcmp(_self._bytes, _other._bytes, op) def __hash__(self): return hash(self._bytes) property bytes: def __get__(self): return self._bytes IF not PY3K: def __str__(self): return self._bytes IF PY3K: def __str__(self): return self._bytes.decode('UTF-8') ELSE: def __unicode__(self): return self._bytes.decode('UTF-8') def __reduce__(self): return (Symbol, (self._bytes,)) class Symbol(BaseSymbol): @staticmethod def __new__(cls, name): ''' Symbol(name) -> a symbol ''' self = None if is_unicode(name): name = encode_utf8(name) try: if cls is _Symbol_: self = symbol_dict[name] except KeyError: pass if self is None: if not is_bytes(name): name = str(name) IF PY3K: name = encode_utf8(name) self = BaseSymbol.__new__(cls, name) if cls is _Symbol_: symbol_dict[name] = self return self cdef object _Symbol_ _Symbol_ = Symbol def _expression_from_string(s): ''' Expression.from_string(s) -> an expression Read an expression from a string. ''' if is_unicode(s): s = encode_utf8(s) stdin = BytesIO(s) try: return _Expression_.from_stream(stdin) finally: stdin.close() class Expression(BaseExpression): ''' Notes about the textual representation of S-expressions ------------------------------------------------------- Special characters are: * the parenthesis '(' and ')', * the double quote '"', * the vertical bar '|'. Symbols are represented by their name. Vertical bars | can be used to delimit names that contain blanks, special characters, non printable characters, non-ASCII characters, or can be confused as a number. Numbers follow the syntax specified by the C function strtol() with base=0. Strings are delimited by double quotes. All C string escapes are recognized. Non-printable ASCII characters must be escaped. List are represented by an open parenthesis '(' followed by the space separated list elements, followed by a closing parenthesis ')'. When the cdr of the last pair is non zero, the closed parenthesis is preceded by a space, a dot '.', a space, and the textual representation of the cdr. (This is only partially supported by Python bindings.) ''' @staticmethod def __new__(cls, value): ''' Expression(value) -> an expression ''' if typecheck(value, _Expression_) and (not typecheck(value, ListExpression) or not value): return value if is_int(value): return IntExpression(value) elif typecheck(value, _Symbol_): return SymbolExpression(value) elif is_unicode(value): return StringExpression(encode_utf8(value)) elif is_bytes(value): if PY3K: return StringExpression(bytes(value)) else: return StringExpression(str(value)) else: return ListExpression(iter(value)) @staticmethod def from_stream(stdin): ''' Expression.from_stream(stream) -> an expression Read an expression from a stream. ''' cdef _ExpressionIO xio try: xio = _ExpressionIO(stdin=stdin) try: return _c2py(xio.read()) except InvalidExpression: raise ExpressionSyntaxError finally: xio.close() from_string = staticmethod(_expression_from_string) cdef object _Expression_ _Expression_ = Expression cdef object BaseExpression_richcmp(object left, object right, int op): if not typecheck(left, BaseExpression): return NotImplemented elif not typecheck(right, BaseExpression): return NotImplemented return richcmp(left.value, right.value, op) cdef class BaseExpression: ''' Don't use this class directly. Use the Expression class instead. ''' cdef _WrappedCExpr wexpr def __cinit__(self, *args, **kwargs): self.wexpr = wexpr_missing() def print_into(self, stdout, width=None, escape_unicode=True): ''' expr.print_into(file, width=None, escape_unicode=True) -> None Print the expression into the file. ''' self.wexpr.print_into(stdout, width, escape_unicode) def as_string(self, width=None, escape_unicode=True): ''' expr.as_string(width=None, escape_unicode=True) -> a string Return a string representation of the expression. ''' return self.wexpr.as_string(width, escape_unicode) def __str__(self): return self.as_string() IF not PY3K: def __unicode__(self): return self.as_string().decode('UTF-8') property value: ''' The "pythonic" value of the expression. Lisp lists as mapped to Python tuples. ''' def __get__(self): return self._get_value() property lvalue: ''' The "pythonic" value of the expression. Lisp lists as mapped to Python lists. ''' def __get__(self): return self._get_lvalue() def _get_value(self): return self._get_lvalue() def _get_lvalue(self): raise NotImplementedError def __richcmp__(self, other, int op): return BaseExpression_richcmp(self, other, op) def __repr__(self): return '{tp}({expr!r})'.format(tp=get_type_name(_Expression_), expr=self.lvalue) def __copy__(self): # Most of S-expressions are immutable. # Mutable S-expressions should override this method. return self def __deepcopy__(self, memo): # Most of S-expressions are immutable. # Mutable S-expressions should override this method. return self def __reduce__(self): return (_expression_from_string, (self.as_string(),)) class IntExpression(_Expression_): ''' IntExpression can represent any integer in range(-2 ** 29, 2 ** 29). To create objects of this class, use the Expression class constructor. ''' @staticmethod def __new__(cls, value): ''' IntExpression(n) -> an integer expression ''' cdef BaseExpression self self = BaseExpression.__new__(cls) if typecheck(value, _WrappedCExpr): self.wexpr = value elif is_int(value): if -1 << 29 <= value < 1 << 29: self.wexpr = wexpr(int_to_cexpr(value)) else: raise ValueError('value not in range(-2 ** 29, 2 ** 29)') else: raise TypeError('value must be an integer') return self IF PY3K: def __bool__(self): return bool(self.value) ELSE: def __nonzero__(self): return bool(self.value) def __int__(self): return self.value def __long__(self): return long(self.value) def __float__(self): return 0.0 + self.value def _get_lvalue(BaseExpression self not None): return cexpr_to_int(self.wexpr.cexpr()) def __richcmp__(self, other, int op): return BaseExpression_richcmp(self, other, op) def __hash__(self): return hash(self.value) class SymbolExpression(_Expression_): ''' To create objects of this class, use the Expression class constructor. ''' @staticmethod def __new__(cls, value): ''' SymbolExpression(Symbol(s)) -> a symbol expression ''' cdef BaseExpression self cdef BaseSymbol symbol self = BaseExpression.__new__(cls) if typecheck(value, _WrappedCExpr): self.wexpr = value elif typecheck(value, _Symbol_): symbol = value self.wexpr = wexpr(symbol_to_cexpr(symbol._bytes)) else: raise TypeError('value must be a Symbol') return self def _get_lvalue(BaseExpression self not None): return _Symbol_(cexpr_to_symbol(self.wexpr.cexpr())) def __richcmp__(self, other, int op): return BaseExpression_richcmp(self, other, op) def __hash__(self): return hash(self.value) class StringExpression(_Expression_): ''' To create objects of this class, use the Expression class constructor. ''' @staticmethod def __new__(cls, value): ''' SymbolExpression(s) -> a string expression ''' cdef BaseExpression self self = BaseExpression.__new__(cls) if typecheck(value, _WrappedCExpr): self.wexpr = value elif is_bytes(value): gc_lock(NULL) # protect from collecting a just-created object try: self.wexpr = wexpr(str_to_cexpr(value)) finally: gc_unlock(NULL) else: raise TypeError('value must be a byte string') return self @property def bytes(BaseExpression self not None): return cexpr_to_str(self.wexpr.cexpr()) def _get_lvalue(BaseExpression self not None): cdef const char *bytes bytes = cexpr_to_str(self.wexpr.cexpr()) IF PY3K: return decode_utf8(bytes) ELSE: return bytes IF PY3K: def __repr__(BaseExpression self not None): cdef const char *bytes bytes = cexpr_to_str(self.wexpr.cexpr()) try: string = decode_utf8(bytes) except UnicodeDecodeError: string = bytes return '{tp}({s!r})'.format(tp=get_type_name(_Expression_), s=string) def __richcmp__(self, other, int op): return BaseExpression_richcmp(self, other, op) def __hash__(self): return hash(self.value) class InvalidExpression(ValueError): pass class ExpressionSyntaxError(Exception): ''' Invalid expression syntax. ''' pass cdef _WrappedCExpr public_py2c(object o): cdef BaseExpression pyexpr pyexpr = _Expression_(o) if pyexpr is None: raise TypeError return pyexpr.wexpr cdef object public_c2py(cexpr_t cexpr): return _c2py(cexpr) cdef BaseExpression _c2py(cexpr_t cexpr): if cexpr == cexpr_dummy: raise InvalidExpression _wexpr = wexpr(cexpr) if cexpr_is_int(cexpr): result = IntExpression(_wexpr) elif cexpr_is_symbol(cexpr): result = SymbolExpression(_wexpr) elif cexpr_is_list(cexpr): result = ListExpression(_wexpr) elif cexpr_is_str(cexpr): result = StringExpression(_wexpr) else: raise InvalidExpression return result cdef _WrappedCExpr _build_list_cexpr(object items): cdef cexpr_t cexpr cdef BaseExpression citem gc_lock(NULL) # protect from collecting a just-created object try: cexpr = cexpr_nil for item in items: if typecheck(item, BaseExpression): citem = item else: citem = _Expression_(item) if citem is None: raise TypeError cexpr = pair_to_cexpr(citem.wexpr.cexpr(), cexpr) cexpr = cexpr_reverse_list(cexpr) return wexpr(cexpr) finally: gc_unlock(NULL) class ListExpression(_Expression_): ''' To create objects of this class, use the Expression class constructor. ''' @staticmethod def __new__(cls, items): ''' ListExpression(iterable) -> a list expression ''' cdef BaseExpression self self = BaseExpression.__new__(cls) if typecheck(items, _WrappedCExpr): self.wexpr = items else: self.wexpr = _build_list_cexpr(items) return self IF PY3K: def __bool__(BaseExpression self not None): return self.wexpr.cexpr() != cexpr_nil ELSE: def __nonzero__(BaseExpression self not None): return self.wexpr.cexpr() != cexpr_nil def __len__(BaseExpression self not None): cdef cexpr_t cexpr cdef int n cexpr = self.wexpr.cexpr() n = 0 while cexpr != cexpr_nil: cexpr = cexpr_tail(cexpr) n = n + 1 return n def __getitem__(BaseExpression self not None, key): cdef cexpr_t cexpr cdef int n cexpr = self.wexpr.cexpr() if is_int(key): n = key if n < 0: n = n + len(self) if n < 0: raise IndexError('list index of out range') while 1: if cexpr == cexpr_nil: raise IndexError('list index of out range') if n > 0: n = n - 1 cexpr = cexpr_tail(cexpr) else: cexpr = cexpr_head(cexpr) break elif is_slice(key): if (is_int(key.start) or key.start is None) and key.stop is None and key.step is None: n = key.start or 0 if n < 0: n = n + len(self) while n > 0 and cexpr != cexpr_nil: cexpr = cexpr_tail(cexpr) n = n - 1 else: raise NotImplementedError('only [n:] slices are supported') else: raise TypeError('key must be an integer or a slice') return _c2py(cexpr) def __setitem__(BaseExpression self not None, key, value): cdef cexpr_t cexpr cdef cexpr_t prev_cexpr cdef cexpr_t new_cexpr cdef int n cdef BaseExpression pyexpr cexpr = self.wexpr.cexpr() pyexpr = _Expression_(value) new_cexpr = pyexpr.wexpr.cexpr() if is_int(key): n = key if n < 0: n = n + len(self) if n < 0: raise IndexError('list index of out range') while 1: if cexpr == cexpr_nil: raise IndexError('list index of out range') if n > 0: n = n - 1 cexpr = cexpr_tail(cexpr) else: cexpr_replace_head(cexpr, new_cexpr) break elif is_slice(key): if not cexpr_is_list(new_cexpr): raise TypeError('can only assign a list expression') if (is_int(key.start) or key.start is None) and key.stop is None and key.step is None: n = key.start or 0 if n < 0: n = n + len(self) prev_cexpr = cexpr_nil while n > 0 and cexpr != cexpr_nil: prev_cexpr = cexpr cexpr = cexpr_tail(cexpr) n = n - 1 if prev_cexpr == cexpr_nil: self.wexpr = wexpr(new_cexpr) else: cexpr_replace_tail(prev_cexpr, new_cexpr) else: raise NotImplementedError('only [n:] slices are supported') else: raise TypeError('key must be an integer or a slice') def __delitem__(BaseExpression self not None, key): if is_int(key): self.pop(key) elif is_slice(key): self[key] = () else: raise TypeError('key must be an integer or a slice') def extend(self, iterable): iter(iterable) self[len(self):] = iterable def __iadd__(self, iterable): iter(iterable) self[len(self):] = iterable return self def insert(BaseExpression self not None, long index, item): cdef cexpr_t cexpr, new_cexpr cdef BaseExpression citem cexpr = self.wexpr.cexpr() if index < 0: index += len(self) if index < 0: index = 0 if typecheck(item, BaseExpression): citem = item else: citem = _Expression_(item) if citem is None: raise TypeError if index == 0 or cexpr == cexpr_nil: gc_lock(NULL) # protect from collecting a just-created object try: new_cexpr = pair_to_cexpr(citem.wexpr.cexpr(), cexpr) self.wexpr = wexpr(new_cexpr) finally: gc_unlock(NULL) return while 1: assert cexpr != cexpr_nil if index > 1 and cexpr_tail(cexpr) != cexpr_nil: index = index - 1 cexpr = cexpr_tail(cexpr) else: gc_lock(NULL) # protect from collecting a just-created object try: new_cexpr = pair_to_cexpr(citem.wexpr.cexpr(), cexpr_tail(cexpr)) cexpr_replace_tail(cexpr, new_cexpr) finally: gc_unlock(NULL) break def append(BaseExpression self not None, item): return self.insert(len(self), item) def reverse(BaseExpression self not None): cdef cexpr_t cexpr, new_cexpr gc_lock(NULL) # protect from collecting a just-created object try: new_cexpr = cexpr_reverse_list(self.wexpr.cexpr()) self.wexpr = wexpr(new_cexpr) finally: gc_unlock(NULL) def pop(BaseExpression self not None, long index=-1): cdef cexpr_t cexpr, citem cexpr = self.wexpr.cexpr() if cexpr == cexpr_nil: raise IndexError('pop from empty list') if index < 0: index += len(self) if index < 0: raise IndexError('pop index of out range') if index == 0: result = _c2py(cexpr_head(cexpr)) self.wexpr = wexpr(cexpr_tail(cexpr)) return result while cexpr_tail(cexpr) != cexpr_nil: if index > 1: index = index - 1 cexpr = cexpr_tail(cexpr) else: result = _c2py(cexpr_head(cexpr_tail(cexpr))) cexpr_replace_tail(cexpr, cexpr_tail(cexpr_tail(cexpr))) return result raise IndexError('pop index of out range') def remove(BaseExpression self not None, item): cdef cexpr_t cexpr cdef BaseExpression citem cexpr = self.wexpr.cexpr() if cexpr == cexpr_nil: raise IndexError('item not in list') if _c2py(cexpr_head(cexpr)) == item: self.wexpr = wexpr(cexpr_tail(cexpr)) return while 1: assert cexpr != cexpr_nil if cexpr_tail(cexpr) == cexpr_nil: raise IndexError('item not in list') if _c2py(cexpr_head(cexpr_tail(cexpr))) == item: cexpr_replace_tail(cexpr, cexpr_tail(cexpr_tail(cexpr))) return cexpr = cexpr_tail(cexpr) def index(self, value): # TODO: optimize for i, v in enumerate(self): if v == value: return i raise ValueError('value not in list') def count(self, value): # TODO: optimize cdef long counter = 0 for v in self: if v == value: counter += 1 return counter def __iter__(self): return _ListExpressionIterator(self) __hash__ = None def _get_value(BaseExpression self not None): cdef cexpr_t current current = self.wexpr.cexpr() result = [] while current != cexpr_nil: list_append(result, _c2py(cexpr_head(current))._get_value()) current = cexpr_tail(current) return tuple(result) def _get_lvalue(BaseExpression self not None): cdef cexpr_t current current = self.wexpr.cexpr() result = [] while current != cexpr_nil: list_append(result, _c2py(cexpr_head(current))._get_lvalue()) current = cexpr_tail(current) return result def __copy__(self): return _Expression_(self) def __deepcopy__(self, memo): return _Expression_(self._get_value()) if sys.version_info >= (3, 3): import collections.abc as collections_abc else: import collections as collections_abc collections_abc.MutableSequence.register(ListExpression) del collections_abc cdef class _ListExpressionIterator: cdef BaseExpression expression cdef cexpr_t cexpr def __cinit__(self, BaseExpression expression not None): self.expression = expression self.cexpr = expression.wexpr.cexpr() def __next__(self): cdef cexpr_t cexpr cexpr = self.cexpr if cexpr == cexpr_nil: raise StopIteration self.cexpr = cexpr_tail(cexpr) cexpr = cexpr_head(cexpr) return _c2py(cexpr) def __iter__(self): return self __all__ = ('Symbol', 'Expression', 'IntExpression', 'SymbolExpression', 'StringExpression', 'ListExpression', 'InvalidExpression', 'ExpressionSyntaxError') __author__ = 'Jakub Wilk ' IF PY3K: __version__ = decode_utf8(PYTHON_DJVULIBRE_VERSION) ELSE: __version__ = str(PYTHON_DJVULIBRE_VERSION) # vim:ts=4 sts=4 sw=4 et ft=pyrex python-djvulibre-0.8.4/doc/0000755000000000000000000000000013441503047015574 5ustar00rootroot00000000000000python-djvulibre-0.8.4/doc/COPYING0000644000000000000000000004325413277057165016653 0ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. python-djvulibre-0.8.4/doc/README0000644000000000000000000000165413430346736016472 0ustar00rootroot00000000000000Overview ======== **python-djvulibre** is a set of Python bindings for the DjVuLibre_ library, an open source implementation of DjVu_. .. _DjVuLibre: http://djvu.sourceforge.net/ .. _DjVu: http://djvu.org/ Prerequisites ============= The following software is required to build python-djvulibre: * DjVuLibre (≥ 3.5.21) * Python_ (≥ 2.6 or 3.X) * Cython_ (≥ 0.19, or ≥ 0.20 for Python 3) * pkg-config_ (required on POSIX systems) Additionally, the following software is needed to run the tests: * nose_ * subprocess32_ (only for Python 2.X) * DjVuLibre_ command-line tools * Ghostscript_ .. _Python: https://www.python.org/ .. _Cython: http://cython.org/ .. _pkg-config: https://wiki.freedesktop.org/www/Software/pkg-config/ .. _nose: https://nose.readthedocs.io/ .. _subprocess32: https://pypi.org/project/subprocess32/ .. _Ghostscript: https://www.ghostscript.com/ .. vim:ft=rst ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/0000755000000000000000000000000013441503047016345 5ustar00rootroot00000000000000python-djvulibre-0.8.4/doc/api/annotations.rst0000644000000000000000000002056412726107516021451 0ustar00rootroot00000000000000Annotations =========== .. testsetup:: from djvu.decode import * from djvu.const import * .. seealso:: - |djvu3ref|_: 8.3.4 *Annotation Chunk*; - |djvused|_: *Annotation syntax*. .. currentmodule:: djvu.decode .. class:: Annotations Abstract base for document and page annotations. Inheritance diagram: .. inheritance-diagram:: DocumentAnnotations PageAnnotations :parts: 1 .. method:: wait() Wait until the associated S-expression is available. .. attribute:: sexpr :return: the associated S-expression. :rtype: :class:`djvu.sexpr.Expression`. :raise NotAvailable: if the S-expression is not available; then, :class:`PageInfoMessage` messages with empty :attr:`~Message.page_job` may be emitted. :raise JobFailed: on failure. .. currentmodule:: djvu.decode .. class:: DocumentAnnotations(document[, shared=True]) If `shared` is true and no document-wide annotations are available, shared annotations are considered document-wide. .. seealso:: |djvuext|_: *Document annotations and metadata*. .. attribute:: document :return: the concerned document. :rtype: :class:`Document`. .. currentmodule:: djvu.decode .. class:: PageAnnotations(page) .. attribute:: page :return: the concerned page. :rtype: :class:`Page`. Mapareas (overprinted annotations) ---------------------------------- .. seealso:: |djvu3ref|_: 8.3.4.2 *Maparea (overprinted annotations)*. .. class:: Hyperlinks(annotations) A sequence of ``(maparea …)`` S-expressions. .. currentmodule:: djvu.decode The following symbols are pre-defined: .. data:: ANNOTATION_MAPAREA >>> ANNOTATION_MAPAREA Symbol('maparea') .. data:: MAPAREA_SHAPE_RECTANGLE >>> MAPAREA_SHAPE_RECTANGLE Symbol('rect') .. data:: MAPAREA_SHAPE_OVAL >>> MAPAREA_SHAPE_OVAL Symbol('oval') .. data:: MAPAREA_SHAPE_POLYGON >>> MAPAREA_SHAPE_POLYGON Symbol('poly') .. data:: MAPAREA_SHAPE_LINE >>> MAPAREA_SHAPE_LINE Symbol('line') .. data:: MAPAREA_SHAPE_TEXT >>> MAPAREA_SHAPE_TEXT Symbol('text') .. data:: MAPAREA_URI >>> MAPAREA_URI Symbol('url') .. data:: MAPAREA_URL Equivalent to :data:`MAPAREA_URI`. Border style ~~~~~~~~~~~~ .. seealso:: |djvu3ref|_: 8.3.4.2.3.1.1 *Border type*, 8.3.4.2.3.1.2 *Border always visible*. .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: MAPAREA_BORDER_NONE >>> MAPAREA_BORDER_NONE Symbol('none') .. data:: MAPAREA_BORDER_XOR >>> MAPAREA_BORDER_XOR Symbol('xor') .. data:: MAPAREA_BORDER_SOLID_COLOR >>> MAPAREA_BORDER_SOLID_COLOR Symbol('border') .. data:: MAPAREA_BORDER_SHADOW_IN >>> MAPAREA_BORDER_SHADOW_IN Symbol('shadow_in') .. data:: MAPAREA_BORDER_SHADOW_OUT >>> MAPAREA_BORDER_SHADOW_OUT Symbol('shadow_out') .. data:: MAPAREA_BORDER_ETCHED_IN >>> MAPAREA_BORDER_ETCHED_IN Symbol('shadow_ein') .. data:: MAPAREA_BORDER_ETCHED_OUT >>> MAPAREA_BORDER_ETCHED_OUT Symbol('shadow_eout') .. data:: MAPAREA_SHADOW_BORDERS A sequence of all shadow border types. >>> MAPAREA_SHADOW_BORDERS (Symbol('shadow_in'), Symbol('shadow_out'), Symbol('shadow_ein'), Symbol('shadow_eout')) .. data:: MAPAREA_BORDER_ALWAYS_VISIBLE >>> MAPAREA_BORDER_ALWAYS_VISIBLE Symbol('border_avis') The following numeric constant are pre-defined: .. data:: MAPAREA_SHADOW_BORDER_MIN_WIDTH >>> MAPAREA_SHADOW_BORDER_MIN_WIDTH 1 .. data:: MAPAREA_SHADOW_BORDER_MAX_WIDTH >>> MAPAREA_SHADOW_BORDER_MAX_WIDTH 32 Highlight color and opacity ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. seealso:: |djvu3ref|_: 8.3.4.2.3.1.3 *Highlight color and opacity*. .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: MAPAREA_HIGHLIGHT_COLOR >>> MAPAREA_HIGHLIGHT_COLOR Symbol('hilite') .. data:: MAPAREA_OPACITY >>> MAPAREA_OPACITY Symbol('opacity') .. currentmodule:: djvu.const The following numeric constant are pre-defined: .. data:: MAPAREA_OPACITY_MIN >>> MAPAREA_OPACITY_MIN 0 .. data:: MAPAREA_OPACITY_DEFAULT >>> MAPAREA_OPACITY_DEFAULT 50 .. data:: MAPAREA_OPACITY_MAX >>> MAPAREA_OPACITY_MAX 100 Line and text parameters ~~~~~~~~~~~~~~~~~~~~~~~~ .. seealso:: |djvu3ref|_: 8.3.4.2.3.1.4 *Line and Text parameters*. .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: MAPAREA_ARROW >>> MAPAREA_ARROW Symbol('arrow') .. data:: MAPAREA_LINE_WIDTH >>> MAPAREA_LINE_WIDTH Symbol('width') .. data:: MAPAREA_LINE_COLOR >>> MAPAREA_LINE_COLOR Symbol('lineclr') .. data:: MAPAREA_BACKGROUND_COLOR >>> MAPAREA_BACKGROUND_COLOR Symbol('backclr') .. data:: MAPAREA_TEXT_COLOR >>> MAPAREA_TEXT_COLOR Symbol('textclr') .. data:: MAPAREA_PUSHPIN >>> MAPAREA_PUSHPIN Symbol('pushpin') .. currentmodule:: djvu.const The following numeric constants are pre-defined: .. data:: MAPAREA_LINE_MIN_WIDTH >>> MAPAREA_LINE_MIN_WIDTH 1 The following default colors are pre-defined: .. data:: MAPAREA_LINE_COLOR_DEFAULT >>> MAPAREA_LINE_COLOR_DEFAULT '#000000' .. data:: MAPAREA_TEXT_COLOR_DEFAULT >>> MAPAREA_TEXT_COLOR_DEFAULT '#000000' Initial document view --------------------- .. seealso:: |djvu3ref|_: 8.3.4.1 *Initial Document View*. .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: ANNOTATION_BACKGROUND >>> ANNOTATION_BACKGROUND Symbol('background') .. data:: ANNOTATION_ZOOM >>> ANNOTATION_ZOOM Symbol('zoom') .. data:: ANNOTATION_MODE >>> ANNOTATION_MODE Symbol('mode') .. data:: ANNOTATION_ALIGN >>> ANNOTATION_ALIGN Symbol('align') Metadata -------- .. seealso:: |djvuext|_ (*Metadata Annotations*, *Document Annotations and Metadata*). .. _BibTeX: https://www.ctan.org/pkg/bibtex .. currentmodule:: djvu.decode .. class:: Metadata A metadata mapping. .. currentmodule:: djvu.const The following sets contain noteworthy metadata keys: .. data:: METADATA_BIBTEX_KEYS Keys borrowed from the BibTeX_ bibliography system. >>> for key in sorted(METADATA_BIBTEX_KEYS): ... print(repr(key)) ... Symbol('address') Symbol('annote') Symbol('author') Symbol('booktitle') Symbol('chapter') Symbol('crossref') Symbol('edition') Symbol('editor') Symbol('howpublished') Symbol('institution') Symbol('journal') Symbol('key') Symbol('month') Symbol('note') Symbol('number') Symbol('organization') Symbol('pages') Symbol('publisher') Symbol('school') Symbol('series') Symbol('title') Symbol('type') Symbol('volume') Symbol('year') .. data:: METADATA_PDFINFO_KEYS Keys borrowed from the PDF DocInfo. >>> for key in sorted(METADATA_PDFINFO_KEYS): ... print(repr(key)) ... Symbol('Author') Symbol('CreationDate') Symbol('Creator') Symbol('Keywords') Symbol('ModDate') Symbol('Producer') Symbol('Subject') Symbol('Title') Symbol('Trapped') .. data:: METADATA_KEYS Sum of :data:`METADATA_BIBTEX_KEYS` and :data:`METADATA_PDFINFO_KEYS`. .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: ANNOTATION_METADATA >>> ANNOTATION_METADATA Symbol('metadata') Printed headers and footers --------------------------- .. seealso:: |djvu3ref|_ (8.3.4.3 *Printed headers and footers*) .. currentmodule:: djvu.const The following symbols are pre-defined: .. data:: ANNOTATION_PRINTED_HEADER >>> ANNOTATION_PRINTED_HEADER Symbol('phead') .. data:: ANNOTATION_PRINTED_FOOTER >>> ANNOTATION_PRINTED_FOOTER Symbol('pfoot') .. data:: PRINTER_HEADER_ALIGN_LEFT >>> PRINTER_HEADER_ALIGN_LEFT Symbol('left') .. data:: PRINTER_HEADER_ALIGN_CENTER >>> PRINTER_HEADER_ALIGN_CENTER Symbol('center') .. data:: PRINTER_HEADER_ALIGN_RIGHT >>> PRINTER_HEADER_ALIGN_RIGHT Symbol('right') .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/conf.py0000644000000000000000000000335213347744054017661 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2009-2017 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.inheritance_diagram', ] templates_path = ['templates'] source_suffix = '.rst' source_encoding = 'UTF-8' master_doc = 'index' import setup as _setup project = _setup.setup_params['name'] version = release = _setup.py_version pygments_style = 'sphinx' html_theme = 'haiku' html_use_modindex = True html_use_index = False html_show_copyright = False html_show_sphinx = False html_static_path = ['static'] rst_epilog = ''' .. |djvu3ref| replace:: Lizardtech DjVu Reference .. _djvu3ref: http://djvu.org/docs/DjVu3Spec.djvu .. |djvused| replace:: djvused manual .. _djvused: http://djvu.sourceforge.net/doc/man/djvused.html .. |djvuext| replace:: Actual and proposed changes to the DjVu format .. _djvuext: https://sourceforge.net/p/djvu/djvulibre-git/ci/release.3.5.23/tree/doc/djvuchanges.txt ''' # With a bit of our help, docutils is capable of rendering our simple formulas. # Sphinx math extension is not needed. import sphinx.writers.html del sphinx.writers.html.HTMLTranslator.visit_math def setup(app): app.add_stylesheet('docutils-math.css') # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/doc/api/documents.rst0000644000000000000000000001611312726107516021110 0ustar00rootroot00000000000000DjVu documents ============== .. currentmodule:: djvu.decode .. class:: Document DjVu document. Use :meth:`Context.new_document` to obtain instances of this class. .. attribute:: decoding_status :return: a :exc:`JobException` subclass indicating the decoding job status. .. attribute:: decoding_error Indicate whether the decoding job failed. .. attribute:: decoding_done Indicate whether the decoding job is done. .. attribute:: decoding_job :rtype: :exc:`DocumentDecodingJob` .. attribute:: type :return: the type of the document. The following values are possible: :data:`~djvu.decode.DOCUMENT_TYPE_UNKNOWN` :data:`~djvu.decode.DOCUMENT_TYPE_SINGLE_PAGE` single-page document :data:`~djvu.decode.DOCUMENT_TYPE_BUNDLED` bundled multi-page document :data:`~djvu.decode.DOCUMENT_TYPE_INDIRECT` indirect multi-page document :data:`~djvu.decode.DOCUMENT_TYPE_OLD_BUNDLED` (obsolete) :data:`~djvu.decode.DOCUMENT_TYPE_OLD_INDEXED` (obsolete) Before receiving the :class:`DocInfoMessage`, :data:`~djvu.decode.DOCUMENT_TYPE_UNKNOWN` may be returned. .. attribute:: pages :rtype: :class:`DocumentPages`. .. attribute:: files :rtype: :class:`DocumentFiles`. .. attribute:: outline :rtype: :class:`DocumentOutline`. .. attribute:: annotations :rtype: :class:`DocumentAnnotations`. .. method:: save(file[, pages][, wait=True]) save(indirect[, pages][, wait=True]) Save the document as: * a bundled DjVu `file` or; * an indirect DjVu document with index file name `indirect`. `pages` argument specifies a subset of saved pages. If `wait` is true, wait until the job is done. :rtype: :class:`SaveJob`. .. method:: export_ps(file[, …][, wait=True]) Convert the document into PostScript. `pages` argument specifies a subset of saved pages. If `wait` is true, wait until the job is done. Additional options: `eps` Produce an *Encapsulated* PostScript file. Encapsulated PostScript files are suitable for embedding images into other documents. Encapsulated PostScript file can only contain a single page. Setting this option overrides the options `copies`, `orientation`, `zoom`, `crop_marks`, and `booklet`. `level` Selects the language level of the generated PostScript. Valid language levels are 1, 2, and 3. Level 3 produces the most compact and fast printing PostScript files. Some of these files however require a very modern printer. Level 2 is the default value. The generated PostScript files are almost as compact and work with all but the oldest PostScript printers. Level 1 can be used as a last resort option. `orientation` Specifies the pages orientation: :data:`~djvu.decode.PRINT_ORIENTATION_AUTO` automatic :data:`~djvu.decode.PRINT_ORIENTATION_LANDSCAPE` portrait :data:`~djvu.decode.PRINT_ORIENTATION_PORTRAIT` landscape `mode` Specifies how pages should be decoded: :data:`~djvu.decode.RENDER_COLOR` render all the layers of the DjVu documents :data:`~djvu.decode.RENDER_BLACK` render only the foreground layer mask :data:`~djvu.decode.RENDER_FOREGROUND` render only the foreground layer :data:`~djvu.decode.RENDER_BACKGROUND` render only the background layer `zoom` Specifies a zoom factor. The default zoom factor scales the image to fit the page. `color` Specifies whether to generate a color or a gray scale PostScript file. A gray scale PostScript files are smaller and marginally more portable. `srgb` The default value, True, generates a PostScript file using device independent colors in compliance with the sRGB specification. Modern printers then produce colors that match the original as well as possible. Specifying a false value generates a PostScript file using device dependent colors. This is sometimes useful with older printers. You can then use the `gamma` option to tune the output colors. `gamma` Specifies a gamma correction factor for the device dependent PostScript colors. Argument must be in range 0.3 to 5.0. Gamma correction normally pertains to cathodic screens only. It gets meaningful for printers because several models interpret device dependent RGB colors by emulating the color response of a cathodic tube. `copies` Specifies the number of copies to print. `frame`, If true, generate a thin gray border representing the boundaries of the document pages. `crop_marks` If true, generate crop marks indicating where pages should be cut. `text` Generate hidden text. This option is deprecated. See also the warning below. `booklet` :data:`~djvu.decode.PRINT_BOOKLET_NO` Disable booklet mode. This is the default. :data:`~djvu.decode.PRINT_BOOKLET_YES` Enable recto/verse booklet mode. :data:`~djvu.decode.PRINT_BOOKLET_RECTO` Enable recto booklet mode. :data:`~djvu.decode.PRINT_BOOKLET_VERSO` Enable verso booklet mode. `booklet_max` Specifies the maximal number of pages per booklet. A single printout might then be composed of several booklets. The argument is rounded up to the next multiple of 4. Specifying 0 sets no maximal number of pages and ensures that the printout will produce a single booklet. This is the default. `booklet_align` Specifies a positive or negative offset applied to the verso of each sheet. The argument is expressed in points [1]_. This is useful with certain printers to ensure that both recto and verso are properly aligned. The default value is 0. `booklet_fold` (= `(base, increment)`) Specifies the extra margin left between both pages on a single sheet. The base value is expressed in points [1]_. This margin is incremented for each outer sheet by value expressed in millipoints. The default value is (18, 200). .. [1] 1 pt = :math:`\frac1{72}` in = 0.3528 mm .. currentmodule:: djvu.decode .. class:: SaveJob Inheritance diagram: .. inheritance-diagram:: SaveJob :parts: 1 Document saving job. Use :meth:`Document.save` to obtain instances of this class. .. currentmodule:: djvu.decode .. class:: DocumentDecodingJob Inheritance diagram: .. inheritance-diagram:: DocumentDecodingJob :parts: 1 Document decoding job. Use :attr:`Document.decoding_job` to obtain instances of this class. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/event-model.rst0000644000000000000000000000713312726107516021330 0ustar00rootroot00000000000000Event model =========== The DDJVU API provides for efficiently decoding and displaying DjVu documents. It provides for displaying images without waiting for the complete DjVu data. Images can be displayed as soon as sufficient data is available. A higher quality image might later be displayed when further data is available. The DjVu library achieves this using a complicated scheme involving multiple threads. The DDJVU API hides this complexity with a familiar event model. .. currentmodule:: djvu.decode .. data:: DDJVU_VERSION Version of the DDJVU API. .. currentmodule:: djvu.decode .. class:: Context(argv0) .. method:: handle_message(message) This method is called, in a separate thread, for every received message, *before* any blocking method finishes. By default do something roughly equivalent to:: if message.job is not None: message.job.message_queue.put(message) elif message.document is not None: message.document.message_queue.put(message) else: message.context.message_queue.put(message) You may want to override this method to change this behaviour. All exceptions raised by this method will be ignored. .. attribute:: message_queue Return the internal message queue. .. method:: get_message([wait=True]) Get message from the internal context queue. :return: a :class:`Message` instance :return: ``None`` if `wait` is false and no message is available. .. method:: new_document(uri[ ,cache=True]) Creates a decoder for a DjVu document and starts decoding. This method returns immediately. The decoding job then generates messages to request the raw data and to indicate the state of the decoding process. `uri` specifies an optional URI for the document. The URI follows the usual syntax (``protocol://machine/path``). It should not end with a slash. It only serves two purposes: - The URI is used as a key for the cache of decoded pages. - The URI is used to document :class:`NewStreamMessage` messages. Setting argument `cache` to a true value indicates that decoded pages should be cached when possible. It is important to understand that the URI is not used to access the data. The document generates :class:`NewStreamMessage` messages to indicate which data is needed. The caller must then provide the raw data using a :attr:`NewStreamMessage.stream` object. .. class:: djvu.decode.FileUri(filename) To open a local file, provide a :class:`FileUri` instance as an `uri`. Localized characters in `uri` should be in URI-encoded. :rtype: :class:`Document` :raise JobFailed: on failure. .. attribute:: cache_size .. method:: clear_cache() .. currentmodule:: djvu.decode .. class:: Job A job. .. method:: get_message([wait=True]) Get message from the internal job queue. :return: a :class:`Message` instance. :return: ``None`` if `wait` is false and no message is available. .. attribute:: is_done Indicate whether the decoding job is done. .. attribute:: is_error Indicate whether the decoding job is done. .. attribute:: message_queue :return: the internal message queue. .. attribute:: status :return: a :exc:`JobException` subclass indicating the job status. .. method:: stop() Attempt to cancel the job. This is a best effort method. There no guarantee that the job will actually stop. .. method:: wait() Wait until the job is done. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/exceptions.rst0000644000000000000000000000255112726107516021271 0ustar00rootroot00000000000000Exceptions ========== Common exceptions ----------------- .. currentmodule:: djvu.decode .. exception:: NotAvailable A resource not (yet) available. .. currentmodule:: djvu.sexpr .. exception:: ExpressionSyntaxError Syntax error while parsing an S-expression. .. currentmodule:: djvu.sexpr .. exception:: InvalidExpression Invalid S-expression. Job status ---------- .. currentmodule:: djvu.decode .. exception:: JobException Status of a job. Possibly, but not necessarily, exceptional. Inheritance diagram: .. inheritance-diagram:: JobNotDone JobNotStarted JobStarted JobDone JobOK JobFailed JobStopped :parts: 1 .. currentmodule:: djvu.decode .. exception:: JobNotDone Operation is not yet done. .. currentmodule:: djvu.decode .. exception:: JobNotStarted Operation was not even started. .. currentmodule:: djvu.decode .. exception:: JobStarted Operation is in progress. .. currentmodule:: djvu.decode .. exception:: JobDone Operation finished. .. currentmodule:: djvu.decode .. exception:: JobOK Operation finished successfully. .. currentmodule:: djvu.decode .. exception:: JobFailed Operation failed because of an error. .. currentmodule:: djvu.decode .. exception:: JobStopped Operation was interrupted by user. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/expressions.rst0000644000000000000000000000654212726124157021476 0ustar00rootroot00000000000000LISP S-expressions ================== .. testsetup:: from djvu.sexpr import * from djvu.const import * Textual representation ---------------------- Special characters are: * the parenthesis ``(`` and ``)``, * the double quote ``"``, * the vertical bar ``|``. Symbols are represented by their name. Vertical bars ``|`` can be used to delimit names that contain blanks, special characters, non printable characters, non-ASCII characters, or can be confused as a number. Numbers follow the syntax specified by the C function ``strtol()`` with ``base=0``. Strings are delimited by double quotes. All C string escapes are recognized. Non-printable ASCII characters must be escaped. List are represented by an open parenthesis ``(`` followed by the space separated list elements, followed by a closing parenthesis ``)``. When the ``cdr`` of the last pair is non zero, the closed parenthesis is preceded by a space, a dot ``.``, a space, and the textual representation of the ``cdr``. (This is only partially supported by Python bindings.) Symbols ------- .. currentmodule:: djvu.sexpr .. class:: Symbol(str) >>> Symbol('ham') Symbol('ham') S-expressions ------------- .. currentmodule:: djvu.sexpr .. class:: Expression Inheritance diagram: .. inheritance-diagram:: IntExpression ListExpression StringExpression SymbolExpression :parts: 1 .. method:: as_string(width=None, escape_unicode=True) Return a string representation of the expression. .. method:: print_into(file, width=None, escape_unicode=True) Print the expression into the file. .. attribute:: value The “pythonic” value of the expression. Lisp lists as mapped to Python tuples. .. attribute:: lvalue The “pythonic” value of the expression. Lisp lists as mapped to Python lists. .. versionadded:: 0.4 .. currentmodule:: djvu.sexpr .. class:: IntExpression :class:`IntExpression` can represent any integer in range :math:`\left[-2^{29}, 2^{29}\right)`. To create objects of this class, use the :class:`Expression` constructor: >>> x = Expression(42) >>> x Expression(42) >>> type(x) >>> x.as_string() '42' >>> x.value 42 .. currentmodule:: djvu.sexpr .. class:: ListExpression To create objects of this class, use the :class:`Expression` constructor: >>> x = Expression([4, 2]) >>> x Expression([4, 2]) >>> type(x) >>> x.as_string() '(4 2)' >>> x.value (4, 2) >>> x.lvalue [4, 2] .. currentmodule:: djvu.sexpr .. class:: StringExpression To create objects of this class, use the :class:`Expression` constructor: >>> x = Expression('eggs') >>> x Expression('eggs') >>> type(x) >>> x.as_string() '"eggs"' >>> x.value 'eggs' .. currentmodule:: djvu.sexpr .. class:: SymbolExpression To create objects of this class, use the :class:`Expression` constructor: >>> x = Expression(Symbol('ham')) >>> x Expression(Symbol('ham')) >>> type(x) >>> x.as_string() 'ham' >>> x.value Symbol('ham') Varieties --------- .. data:: EMPTY_LIST Empty list S-expression. >>> EMPTY_LIST Expression([]) .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/files.rst0000644000000000000000000000545513277057165020226 0ustar00rootroot00000000000000Document files ============== .. currentmodule:: djvu.decode .. class:: DocumentFiles Component files of a document. Use :attr:`Document.files` to obtain instances of this class. File indexing is zero-based, i.e. :attr:`~Document.files`\ ``[0]`` stands for the first file. ``len(files)`` might raise :exc:`NotAvailable` when called before receiving a :class:`DocInfoMessage`. .. currentmodule:: djvu.decode .. class:: File Component file of a document. Use :attr:`Document.files`\ ``[N]`` to obtain instances of this class. .. attribute:: document :rtype: :class:`Document` .. attribute:: n :return: the component file number. File indexing is zero-based, i.e. 0 stands for the very first file. .. method:: get_info([wait=1]) Attempt to obtain information about the component file. If `wait` is true, wait until the information is available. :raise NotAvailable: if the information is not available. :raise JobFailed: on failure. .. attribute:: type :return: the type of the compound file. The following types are possible: .. data:: FILE_TYPE_PAGE .. data:: FILE_TYPE_THUMBNAILS .. data:: FILE_TYPE_INCLUDE :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: n_page :return: the page number, or ``None`` when not applicable. Page indexing is zero-based, i.e. 0 stands for the very first page. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: page :return: the page, or ``None`` when not applicable. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: size :return: the compound file size, or ``None`` when unknown. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: id :return: the compound file identifier, or ``None``. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: name :return: the compound file name, or ``None``. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: title :return: the compound file title, or ``None``. :raise NotAvailable: see :meth:`~File.get_info`. :raise JobFailed: on failure. .. attribute:: dump :return: a text describing the contents of the file using the same format as the ``djvudump`` command. If the information is not available, raise :exc:`NotAvailable` exception. Then, :exc:`PageInfoMessage` messages with empty :attr:`~Message.page_job` may be emitted. :raise NotAvailable: see above. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/geometry.rst0000644000000000000000000001047413277057165020754 0ustar00rootroot00000000000000Geometry and colors =================== Geometry -------- Transformations ~~~~~~~~~~~~~~~ .. currentmodule:: djvu.decode .. class:: AffineTransform((x0, y0, w0, h0), (x1, y1, w1, h1)) The object represents an affine coordinate transformation that maps points from rectangle (`x0`, `y0`, `w0`, `h0`) to rectangle (`x1`, `y1`, `w1`, `h1`). .. method:: rotate(n) Rotate the output rectangle counter-clockwise by `n` degrees. .. method:: apply((x, y)) apply((x, y, w, h)) Apply the coordinate transform to a point or a rectangle. .. method:: inverse((x, y)) inverse((x, y, w, h)) Apply the inverse coordinate transform to a point or a rectangle. .. method:: mirror_x() Reverse the X coordinates of the output rectangle. .. method:: mirror_y() Reverse the Y coordinates of the output rectangle. Pixel formats ------------- .. currentmodule:: djvu.decode .. class:: PixelFormat Abstract base for all pixel formats. Inheritance diagram: .. inheritance-diagram:: PixelFormatRgb PixelFormatRgbMask PixelFormatGrey PixelFormatPalette PixelFormatPackedBits :parts: 1 .. attribute:: rows_top_to_bottom Flag indicating whether the rows in the pixel buffer are stored starting from the top or the bottom of the image. Default ordering starts from the bottom of the image. This is the opposite of the X11 convention. .. attribute:: y_top_to_bottom Flag indicating whether the *y* coordinates in the drawing area are oriented from bottom to top, or from top to bottom. The default is bottom to top, similar to PostScript. This is the opposite of the X11 convention. .. attribute:: bpp Return the depth of the image, in bits per pixel. .. attribute:: dither_bpp The final depth of the image on the screen. This is used to decide which dithering algorithm should be used. The default is usually appropriate. .. attribute:: gamma Gamma of the display for which the pixels are intended. This will be combined with the gamma stored in DjVu documents in order to compute a suitable color correction. The default value is 2.2. .. currentmodule:: djvu.decode .. class:: PixelFormatRgb([byteorder='RGB']) 24-bit pixel format, with: - RGB (`byteorder` = ``'RGB'``) or - BGR (`byteorder` = ``'BGR'``) byte order. .. currentmodule:: djvu.decode .. class:: PixelFormatRgbMask(red_mask, green_mask, blue_mask[, xor_value], bpp=16) PixelFormatRgbMask(red_mask, green_mask, blue_mask[, xor_value], bpp=32) `red_mask`, `green_mask` and `blue_mask` are bit masks for color components for each pixel. The resulting color is then xored with the `xor_value`. For example, ``PixelFormatRgbMask(0xF800, 0x07E0, 0x001F, bpp=16)`` is a highcolor format with: - 5 (most significant) bits for red, - 6 bits for green, - 5 (least significant) bits for blue. .. currentmodule:: djvu.decode .. class:: PixelFormatGrey() 8-bit, grey pixel format. .. currentmodule:: djvu.decode .. class:: PixelFormatPalette(palette) Palette pixel format. `palette` must be a dictionary which contains 216 (6 × 6 × 6) entries of a web color cube, such that: - for each key ``(r, g, b)``: ``r in range(0, 6)``, ``g in range(0, 6)`` etc.; - for each value ``v``: ``v in range(0, 0x100)``. .. currentmodule:: djvu.decode .. class:: PixelFormatPackedBits(endianness) Bitonal, 1 bit per pixel format with: - most significant bits on the left (*endianness* = ``'>'``) or - least significant bits on the left (*endianness* = ``'<'``). Render modes ------------ .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_COLOR Render color page or stencil. .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_BLACK Render stencil or color page. .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_COLOR_ONLY Render color page or fail. .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_MASK_ONLY Render stencil or fail. .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_BACKGROUND Render color background layer. .. currentmodule:: djvu.decode .. data:: djvu.decode.RENDER_FOREGROUND Render color foreground layer. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/index.rst0000644000000000000000000000035112726107516020213 0ustar00rootroot00000000000000API === Contents: .. toctree:: :maxdepth: 3 expressions event-model documents pages geometry files outline annotations text-zones messages exceptions * :ref:`search` .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/messages.rst0000644000000000000000000001323613277057165020727 0ustar00rootroot00000000000000Messages ======== .. currentmodule:: djvu.decode .. class:: Message An abstract base for all messages. .. attribute:: context :return: the concerned context. :rtype: :class:`Context`. .. attribute:: document :return: the concerned document or ``None``. :type: :class:`Document` .. attribute:: page_job :return: the concerned page job or ``None``. :rtype: :class:`PageJob` .. attribute:: job :return: the concerned job or ``None``. :rtype: :class:`Job` Inheritance diagram: .. inheritance-diagram:: ErrorMessage InfoMessage NewStreamMessage DocInfoMessage PageInfoMessage ChunkMessage RelayoutMessage RedisplayMessage ThumbnailMessage ProgressMessage :parts: 1 .. class:: ErrorMessage An :class:`ErrorMessage` is generated whenever the decoder or the DDJVU API encounters an error condition. All errors are reported as error messages because they can occur asynchronously. .. attribute:: message :return: the actual error message, as text. .. attribute:: location :return: a (`function`, `filename`, `line_no`) tuple indicating where the error was detected. .. class:: InfoMessage A :class:`InfoMessage` provides informational text indicating the progress of the decoding process. This might be displayed in the browser status bar. .. attribute:: message :return: the actual error message, as text. .. class:: NewStreamMessage A :class:`NewStreamMessage` is generated whenever the decoder needs to access raw DjVu data. The caller must then provide the requested data using the :attr:`stream` file-like object. In the case of indirect documents, a single decoder might simultaneously request several streams of data. .. attribute:: name The first :class:`NewStreamMessage` message always has :attr:`name` set to ``None``. It indicates that the decoder needs to access the data in the main DjVu file. Further :class:`NewStreamMessage` messages are generated to access the auxiliary files of indirect or indexed DjVu documents. :attr:`name` then provides the base name of the auxiliary file. .. attribute:: uri :return: the requested URI. URI is set according to the `uri` argument provided to function :meth:`Context.new_document`. The first :class:`NewStreamMessage` message always contain the URI passed to :meth:`Context.new_document`. Subsequent :class:`NewStreamMessage` messages contain the URI of the auxiliary files for indirect or indexed DjVu documents. .. attribute:: stream :return: a data stream. .. class:: djvu.decode.Stream .. method:: close() Indicate that no more data will be provided on the particular stream. .. method:: abort() Indicate that no more data will be provided on the particular stream, because the user has interrupted the data transfer (for instance by pressing the stop button of a browser) and that the decoding threads should be stopped as soon as feasible. .. method:: flush() Do nothing. (This method is provided solely to implement Python's file-like interface.) .. method:: read([size]) :raise exceptions.IOError: always. (This method is provided solely to implement Python's file-like interface.) .. method:: write(data) Provide raw data to the DjVu decoder. This method should be called as soon as the data is available, for instance when receiving DjVu data from a network connection. .. class:: DocInfoMessage A :class:`DocInfoMessage` indicates that basic information about the document has been obtained and decoded. Not much can be done before this happens. Check the document's :attr:`~Document.decoding_status` to determine whether the operation was successful. .. class:: PageInfoMessage The page decoding process generates a :class:`PageInfoMessage`: - when basic page information is available and before any :class:`RelayoutMessage` or :class:`RedisplayMessage`, - when the page decoding thread terminates. You can distinguish both cases using the page job's :attr:`~Job.status`. A :class:`PageInfoMessage` may be also generated as a consequence of reading :attr:`Page.get_info()` or :attr:`Page.dump`. .. class:: ChunkMessage A :class:`ChunkMessage` indicates that an additional chunk of DjVu data has been decoded. .. class:: RelayoutMessage A :class:`RelayoutMessage` is generated when a DjVu viewer should recompute the layout of the page viewer because the page size and resolution information has been updated. .. class:: RedisplayMessage A :class:`RedisplayMessage` is generated when a DjVu viewer should call :meth:`PageJob.render` and redisplay the page. This happens, for instance, when newly decoded DjVu data provides a better image. .. class:: ThumbnailMessage A :class:`ThumbnailMessage` is sent when additional thumbnails are available. .. attribute:: thumbnail :rtype: :class:`Thumbnail` :raise NotAvailable: if the :class:`Document` has been garbage-collected. .. class:: ProgressMessage A :class:`ProgressMessage` is generated to indicate progress towards the completion of a print or save job. .. attribute:: percent :return: the percent of the job done. .. attribute:: status :return: a :class:`JobException` subclass indicating the current job status. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/outline.rst0000644000000000000000000000164012726107516020565 0ustar00rootroot00000000000000Document outline ================ .. testsetup:: from djvu.const import * from djvu.decode import * .. seealso:: |djvu3ref|_ (8.3.3 *Document Outline Chunk*). Representing outlines as S-expressions is DjVuLibre-specific; see *Outline/Bookmark syntax* in |djvused|_ for reference. .. currentmodule:: djvu.decode .. class:: DocumentOutline .. method:: wait() Wait until the associated S-expression is available. .. attribute:: sexpr :return: the associated S-expression. If the S-expression is not available, raise :exc:`NotAvailable` exception. Then, :class:`PageInfoMessage` messages with empty :attr:`~Message.page_job` may be emitted. :raise NotAvailable: see above. :raise JobFailed: on failure. .. data:: djvu.const.EMPTY_OUTLINE Empty outline S-expression. >>> EMPTY_OUTLINE Expression([Symbol('bookmarks')]) .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/pages.rst0000644000000000000000000001750313277057165020220 0ustar00rootroot00000000000000Document pages ============== .. currentmodule:: djvu.decode .. class:: DocumentPages Pages of a document. Use :attr:`Document.pages` to obtain instances of this class. Page indexing is zero-based, i.e. :attr:`~Document.pages`\ ``[0]`` stands for the first page. ``len(pages)`` might return 1 when called before receiving a :class:`DocInfoMessage`. .. currentmodule:: djvu.decode .. class:: Page Page of a document. Use :attr:`Document.pages`\ ``[N]`` to obtain instances of this class. .. attribute:: document :rtype: :class:`Document` .. attribute:: file :return: a file associated with the page. :rtype: :class:`File`. .. attribute:: n :return: the page number. Page indexing is zero-based, i.e. 0 stands for the very first page. .. attribute:: thumbnail :return: a thumbnail for the page. :rtype: :class:`Thumbnail`. .. method:: get_info([wait=1]) Attempt to obtain information about the page without decoding the page. If `wait` is true, wait until the information is available. If the information is not available, raise :exc:`NotAvailable` exception. Then, start fetching the page data, which causes emission of :class:`PageInfoMessage` messages with empty :attr:`~Message.page_job`. :raise NotAvailable: see above. :raise JobFailed: on failure. .. attribute:: width :return: the page width, in pixels. :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: height :return: the page height, in pixels. :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: size :return: ``(page.width, page.height)`` :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: dpi :return: the page resolution, in pixels per inch. :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: rotation :return: the initial page rotation, in degrees. :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: version :return: the page version. :raise NotAvailable: see :meth:`get_info`. :raise JobFailed: on failure. .. attribute:: dump :return: a text describing the contents of the page using the same format as the ``djvudump`` command. If the information is not available, raise :exc:`NotAvailable` exception. Then :class:`PageInfoMessage` messages with empty :attr:`~Message.page_job` may be emitted. :raise NotAvailable: see above. .. method:: decode([wait=1]) Initiate data transfer and decoding threads for the page. If `wait` is true, wait until the job is done. :rtype: :class:`PageJob`. :raise NotAvailable: if called before receiving the :class:`DocInfoMessage`. :raise JobFailed: if document decoding failed. .. attribute:: annotations :rtype: :class:`PageAnnotations` .. attribute:: text :rtype: :class:`PageText` .. class:: PageJob Inheritance diagram: .. inheritance-diagram:: PageJob :parts: 1 A page decoding job. Use :meth:`Page.decode` to obtain instances of this class. .. attribute:: width :return: the page width in pixels. :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: height :return: the page height in pixels. :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: size :return: ``(page_job.width, page_job.height)`` :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: dpi :return: the page resolution in pixels per inch. :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: gamma :return: the gamma of the display for which this page was designed. :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: version :return: the version of the DjVu file format. :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: type :return: the type of the page data. Possible values are: .. data:: PAGE_TYPE_UNKNOWN .. data:: PAGE_TYPE_BITONAL .. data:: PAGE_TYPE_PHOTO .. data:: PAGE_TYPE_COMPOUND :raise NotAvailable: before receiving a :class:`PageInfoMessage`. .. attribute:: initial_rotation :return: the counter-clockwise page rotation angle (in degrees) specified by the orientation flags in the DjVu file. .. warning:: This is useful because ``maparea`` coordinates in the annotation chunks are expressed relative to the rotated coordinates whereas text coordinates in the hidden text data are expressed relative to the unrotated coordinates. .. attribute:: rotation :return: the counter-clockwise rotation angle (in degrees) for the page. The rotation is automatically taken into account by :meth:`render` method and :attr:`width` and :attr:`height` properties. .. method:: render(self, mode, page_rect, render_rect, pixel_format[, row_alignment=1][, buffer=None]) Render a segment of a page with arbitrary scale. `mode` indicates which image layers should be rendered: * :data:`~djvu.decode.RENDER_COLOR`, or * :data:`~djvu.decode.RENDER_BLACK`, or * :data:`~djvu.decode.RENDER_COLOR_ONLY`, or * :data:`~djvu.decode.RENDER_MASK_ONLY`, or * :data:`~djvu.decode.RENDER_BACKGROUND`, or * :data:`~djvu.decode.RENDER_FOREGROUND`. Conceptually this method renders the full page into a rectangle `page_rect` and copies the pixels specified by rectangle `render_rect` into a buffer. The actual code is much more efficient than that. `pixel_format` (a :class:`~djvu.decode.PixelFormat` instance) specifies the expected pixel format. Each row will start at `row_alignment` bytes boundary. Data will be saved to the provided buffer or to a newly created string. This method makes a best effort to compute an image that reflects the most recently decoded data. :raise NotAvailable: to indicate that no image could be computed at this point. .. currentmodule:: djvu.decode .. class:: Thumbnail Thumbnail for a page. Use :attr:`Page.thumbnail` to obtain instances of this class. .. attribute:: page :return: the page. .. attribute:: status Determine whether the thumbnail is available. :return: a :exc:`JobException` subclass indicating the current job status. .. method:: calculate() Determine whether the thumbnail is available. If it's not, initiate the thumbnail calculating job. Regardless of its success, the completion of the job is signalled by a subsequent :class:`ThumbnailMessage`. Return a :exc:`JobException` subclass indicating the current job status. .. method:: render((w0, h0)[, pixel_format][, row_alignment=1][, dry_run=False][, buffer=None]) Render the thumbnail: * not larger than `w0` × `h0` pixels; * using the `pixel_format` (a :class:`~djvu.decode.PixelFormat` instance) pixel format; * with each row starting at `row_alignment` bytes boundary; * into the provided buffer or to a newly created string. :return: a ((`w1`, `h1`, `row_size`), `data`) tuple. * `w1` and `h1` are actual thumbnail dimensions in pixels (`w1` ≤ `w0` and `h1` ≤ `h0`); * `row_size` is length of each image row, in bytes; * `data` is ``None`` if `dry_run` is true; otherwise is contains the actual image data. :raise NotAvailable: when no thumbnail is available. .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/api/text-zones.rst0000644000000000000000000000650512726107516021233 0ustar00rootroot00000000000000Text zones ========== .. testsetup:: from djvu.const import * from djvu.decode import * from djvu.sexpr import * from pprint import pprint .. seealso:: |djvu3ref|_ (8.3.5 *Text Chunk*). Representing text zones as S-expressions is DjVuLibre-specific; see |djvused|_ for reference. .. currentmodule:: djvu.decode .. class:: PageText(page[, details=TEXT_DETAILS_ALL]) A wrapper around page text. `details` controls the level of details in the returned S-expression: * :data:`~TEXT_DETAILS_PAGE`, or * :data:`~TEXT_DETAILS_COLUMN`, or * :data:`~TEXT_DETAILS_REGION`, or * :data:`~TEXT_DETAILS_PARAGRAPH`, or * :data:`~TEXT_DETAILS_LINE`, or * :data:`~TEXT_DETAILS_WORD`, or * :data:`~TEXT_DETAILS_CHARACTER`, or * :data:`~TEXT_DETAILS_ALL`. .. method:: wait() Wait until the associated S-expression is available. .. attribute:: page :rtype: :class:`Page` .. attribute:: sexpr :rtype: :class:`djvu.sexpr.Expression` :raise NotAvailable: if the S-expression is not available; then, :class:`PageInfoMessage` messages with empty :attr:`~Message.page_job` may be emitted. :raise JobFailed: on failure. .. currentmodule:: djvu.const .. class:: TextZoneType A type of a text zone. To create objects of this class, use the :func:`get_text_zone_type()` function. .. currentmodule:: djvu.const .. function:: get_text_zone_type(symbol) Return one of the following text zone types: .. data:: TEXT_ZONE_PAGE >>> get_text_zone_type(Symbol('page')) is TEXT_ZONE_PAGE True .. data:: TEXT_ZONE_COLUMN >>> get_text_zone_type(Symbol('column')) is TEXT_ZONE_COLUMN True .. data:: TEXT_ZONE_REGION >>> get_text_zone_type(Symbol('region')) is TEXT_ZONE_REGION True .. data:: TEXT_ZONE_PARAGRAPH >>> get_text_zone_type(Symbol('para')) is TEXT_ZONE_PARAGRAPH True .. data:: TEXT_ZONE_LINE >>> get_text_zone_type(Symbol('line')) is TEXT_ZONE_LINE True .. data:: TEXT_ZONE_WORD >>> get_text_zone_type(Symbol('word')) is TEXT_ZONE_WORD True .. data:: TEXT_ZONE_CHARACTER >>> get_text_zone_type(Symbol('char')) is TEXT_ZONE_CHARACTER True You can compare text zone types using the ``>`` operator: >>> TEXT_ZONE_PAGE > TEXT_ZONE_COLUMN > TEXT_ZONE_REGION > TEXT_ZONE_PARAGRAPH True >>> TEXT_ZONE_PARAGRAPH > TEXT_ZONE_LINE > TEXT_ZONE_WORD > TEXT_ZONE_CHARACTER True .. currentmodule:: djvu.decode .. function:: cmp_text_zone(zonetype1, zonetype2) :return: a negative integer if `zonetype1` is more concrete than `zonetype2`. :return: a negative integer if `zonetype1` is the same as `zonetype2`. :return: a positive integer if `zonetype1` is the general than `zonetype2`. .. currentmodule:: djvu.const .. data:: TEXT_ZONE_SEPARATORS Dictionary that maps text types to their separators. >>> pprint(TEXT_ZONE_SEPARATORS) {: '', : ' ', : '\n', : '\x1f', : '\x1d', : '\x0b', : '\x0c'} .. vim:ts=3 sts=3 sw=3 et python-djvulibre-0.8.4/doc/changelog0000644000000000000000000003704013441502135017447 0ustar00rootroot00000000000000python-djvulibre (0.8.4) unstable; urgency=low * Fix compatibility with Python 3.8. * Fix error handling when getting S-expressions for outlines, annotations or page texts. -- Jakub Wilk Mon, 11 Mar 2019 16:52:59 +0100 python-djvulibre (0.8.3) unstable; urgency=low * Require subprocess32 for test suite with Python 2.X. https://github.com/jwilk/python-djvulibre/issues/8 -- Jakub Wilk Mon, 11 Feb 2019 20:53:17 +0100 python-djvulibre (0.8.2) unstable; urgency=low * Explicitly set Cython's Python language level to 2 in pxd files too. This might fix build failures with future versions of Cython. * Improve documentation: + Update the credits file to make it clear that the project is no longer being funded. + Remove .txt extensions from misc. files. * Improve the test suite: + Treat the “POSIX” locale the same way as the “C” locale. (POSIX says they are equivalent.) + Add test for version consistency. + Add check against BSD make. (Only GNU make is supported.) -- Jakub Wilk Mon, 17 Sep 2018 17:29:12 +0200 python-djvulibre (0.8.1) unstable; urgency=low * Improve documentation: + Add README, and move information about build requirements there. + Apply minor wording and typographic fixes to API documentation. + Don't include Sphinx version in HTML documentation. + Don't include copyright information in HTML documentation. * Improve examples: + Make djvu-dump-text and djvu-crop-text compatible with Python 3. + Make djvu2png render all layers by default. + Unify command-line argument parsing. * Explicitly set Cython's Python language level to 2. This might fix build failures with future versions of Cython. * Improve the build system: + Declare build-dependencies (as per PEP-518). + Add the bdist_wheel command. + Use distutils644 to normalize tarball permissions etc. + Fix Python version check. * Improve the test suite. + Be more tolerant about ps2ascii output. Fixes some failures with Ghostscript 9.22. https://github.com/jwilk/python-djvulibre/issues/6 -- Jakub Wilk Wed, 30 May 2018 18:48:50 +0200 python-djvulibre (0.8) unstable; urgency=low * Fix Windows support. It is now possible to build the package using the default compiler, Microsoft Visual C++ Compiler for Python. https://github.com/jwilk/python-djvulibre/issues/1 + Remove support for cross-compiling using MinGW. * Fix test failures on non-Linux systems. * Improve documentation: + Fix doctests' compatibility with Python 3.6. + Don't require TeX distribution to render math formulas. + Simplify some module docstrings. + Fix typos in docstrings. + Update bug tracker URLs. The project repo has moved to GitHub. * Improve the setup script: + Check Cython version. + Use “python -m cython” instead of the “cython” script. + Report build-time requirements to pip. + Don't use setuptools, except on Windows. + Remove poorly standardized “platforms” metadata key. (The information about supported platform is available in the “Operating System” classifiers.) + Add the “Programming Language :: Cython” classifier. * Don't hardcode the Python interpreter path in script shebangs; use “#!/usr/bin/env python” instead. -- Jakub Wilk Fri, 05 Aug 2016 22:52:25 +0200 python-djvulibre (0.7.2) unstable; urgency=low * Fix compatibility with Cython 0.24. * Fix typo in the documentation. -- Jakub Wilk Thu, 12 May 2016 14:07:13 +0200 python-djvulibre (0.7.1) unstable; urgency=low * Update URL for “actual and proposed changes to the DjVu format”. * Use HTTPS for sourceforge.net URLs. * Fix a few typos in the documentation. * Improve the test suite: + Normalize whitespace in ps2ascii output. -- Jakub Wilk Fri, 11 Mar 2016 22:09:00 +0100 python-djvulibre (0.7) unstable; urgency=low * Make Expression.from_string() accept both Unicode and byte strings. * Implement __unicode__() for expressions. * Implement __float__() for integer expressions. * Be more liberal about object types that can be passed to escape_unicode. * Fix the long() return type with Cython 0.23 and Python 2.6. https://mail.python.org/pipermail/cython-devel/2015-August/004485.html * Improve setup.py: + Put files that vary with Python version (*.c, config.pxi) into a version-specific temporary directory. With this change, it's possible to build against multiple Python versions in parallel. * Update BibTeX URL in the documentation. * Improve the test suite. -- Jakub Wilk Tue, 25 Aug 2015 23:46:20 +0200 python-djvulibre (0.6.1) unstable; urgency=low * Fix the DjVuLibre version check. * Add “const” to variable declarations and function prototypes. * Optimize S-expression input/output. * Require Cython >= 0.19, or Cython >= 0.20 for Python 3. + Remove work-arounds for older versions. * Don't pass -Wno-uninitialized to the compiler. (Modern versions of Cython don't generate code that would trigger this warning.) * Require pkg-config for building on POSIX systems. -- Jakub Wilk Fri, 14 Aug 2015 16:22:44 +0200 python-djvulibre (0.6) unstable; urgency=low * Fix build failure with Cython 0.23. * Document which software if required to build the package. * Require DjVuLibre >= 3.5.21: + Add version check to the setup script. + Remove work-arounds for older versions. + Remove documentation warnings about older versions. * Remove the DjVuLibreBug class. * Improve the test suite. -- Jakub Wilk Wed, 12 Aug 2015 19:12:01 +0200 python-djvulibre (0.5) unstable; urgency=low * Make print_into() raise exceptions properly instead of just printing them on stderr. * Make it possible to print S-expressions into file-like objects created by codecs.open(). * Make djvu.sexpr.__version__ a byte string in Python 2.X. * Don't include DjVu file format version in djvu.decode.__version__. This information is more conveniently available in djvu.decode.DDJVU_VERSION. * Make S-expression input/output methods reentrant. (DjVuLibre >= 3.5.26 is required for this feature.) * Improve error handling. * Improve the test suite. -- Jakub Wilk Sun, 02 Aug 2015 17:45:08 +0200 python-djvulibre (0.4.1) unstable; urgency=low * Exclude djvu/config.pxi from the source tarball. Thanks to Daniel Stender for the bug report. https://github.com/jwilk/python-djvulibre/issues/4 -- Jakub Wilk Mon, 27 Jul 2015 10:28:21 +0200 python-djvulibre (0.4) unstable; urgency=low * Drop support for Python 2.5. * Add the lvalue attribute to expressions. This is the same as value, except that Lisp lists are mapped to Python lists, rather than Python tuples. * Use the lvalue in __repr__(). * Make Symbol objects comparable using “<” and other inequality operators. * Make it possible to disable octal-escaping of non-ASCII Unicode characters when printing expressions into a file. * Stop using sphinx.ext.intersphinx. Making it work offline and consistently across different Python versions is too much of a hassle, and it was used only to cross-reference one well-known exception (IOError). * Make djvu.decode.__version__ a byte string in Python 2.X. * Improve the test suite. -- Jakub Wilk Wed, 22 Jul 2015 21:05:11 +0200 python-djvulibre (0.3.10) unstable; urgency=low * Improve error handling in examples. * Improve the test suite: + When running external programs, don't reset environment completely, but only set LC_ALL=C. + Skip more tests (instead of making them fail) when they are run in an unsuitable environment. + Correctly detect ASCII locale encoding on FreeBSD (and possibly other non-GNU systems). + Don't hard-code libc error messages. + Fix test_bad_io failures with Cython ≥ 0.21. * Improve setup.py: + Make it possible to build the package natively on Windows. https://github.com/jwilk/python-djvulibre/issues/1 * Use HTTPS URLs when they are available, in documentation and code. -- Jakub Wilk Tue, 04 Nov 2014 11:44:09 +0100 python-djvulibre (0.3.9) unstable; urgency=low * Ensure that all S-expression output is 7-bit. This is work-around for . -- Jakub Wilk Mon, 19 Mar 2012 20:41:53 +0100 python-djvulibre (0.3.8) unstable; urgency=low * Ensure that S-expression input/output functions are always initialized. This is work-around for . -- Jakub Wilk Sun, 18 Mar 2012 23:44:55 +0100 python-djvulibre (0.3.7) unstable; urgency=low * Use floor division operator instead of relying on “classic” division semantics. -- Jakub Wilk Wed, 14 Mar 2012 08:32:35 +0100 python-djvulibre (0.3.6) unstable; urgency=low * Improve test suite: + Fix compatibility with Python 3.X (broken in 0.3.5). + Test expression parsing again. + Capture stderr output for tests which produce unhandled exceptions. * Fix compatibility with Python 2.5 (broken since 0.3.0). -- Jakub Wilk Tue, 06 Mar 2012 00:14:57 +0100 python-djvulibre (0.3.5) unstable; urgency=low * Improve setup.py: + Fix error message when pkg-config is not found (a regression introduced in 0.3.4). * Improve test suite: + Verify that LANGUAGE environment variable is unset before running tests sensitive to locale settings. Thanks to Daniel Stender for the bug report. + Skip some tests (instead of making them fail) when they are run in an unsuitable environment. Note that it's still recommended to use --no-skip when running the test suite. -- Jakub Wilk Sat, 18 Feb 2012 21:05:41 +0100 python-djvulibre (0.3.4) unstable; urgency=low * Optimize DocumentPages.__len__() and DocumentFiles.__len__(). * Fix compatibility with Python 3.X (broken in 0.3.3). * Update various external documentation URLs. https://bugs.debian.org/627290 * Improve test suite: + Normalize whitespace in ps2ascii output. https://bugs.debian.org/646177 * Improve setup.py: + Print a more meaningful error message if pkg-config fails. + build_sphinx is now available even without setuptools. + build_sphinx can now import extension modules from the build directory. -- Jakub Wilk Sat, 22 Oct 2011 01:36:38 +0200 python-djvulibre (0.3.3) unstable; urgency=low * Improve setup.py: + Add “Operating System :: Microsoft :: Windows” to trove classifiers. * Add Windows-specific module djvu.dllpath, which is aimed to ease finding DjVuLibre DLLs in non-standard locations. * Make expression and symbol objects picklable. * Add work-around for . * Fix test suite compatibility with Python 3. -- Jakub Wilk Mon, 04 Apr 2011 21:06:01 +0200 python-djvulibre (0.3.2) unstable; urgency=low * Python ≥ 2.6: make djvu.sexpr.ListExpression a “virtual subclass” of collections.MutableSequence. * Add append, count, extend, index, insert, pop, remove and reverse methods for list expressions. * Add ‘+=’ and ‘del’ operators for list expressions. * Fix compatibility with Cython 0.12. * Fix compatibility with some non-POSIX operating systems. * Add work-around for . * Improve setup.py: + Add work-around for . + Make ‘clean --all’ remove temporary *.pxi and *.c files. + Don't import Cython modules; calls the ‘cython’ binary instead. + Allow cross-compilation using MinGW cross compiler. -- Jakub Wilk Wed, 15 Dec 2010 21:17:39 +0100 python-djvulibre (0.3.1) unstable; urgency=low * Add another example program. * Fix encoding issues with djvu.decode.ErrorMessage. Thanks to Kyrill Detinov for the bug report. -- Jakub Wilk Fri, 19 Nov 2010 17:44:51 +0100 python-djvulibre (0.3.0) unstable; urgency=low * Add support for Python 3. * Fix tests on 64-bit architectures. * Fix compatibility with Cython 0.13. * Message.message are now always Unicode strings. -- Jakub Wilk Fri, 29 Oct 2010 01:51:33 +0200 python-djvulibre (0.1.18) unstable; urgency=low * Fix handling of non-ASCII metadata. -- Jakub Wilk Fri, 25 Jun 2010 23:32:05 +0200 python-djvulibre (0.1.17) unstable; urgency=low * Allow rendering images directly into a writable buffer (e.g. an array), rather than to a newly created string. That should ease integration of python-djvulibre with e.g. numpy or cairo. * Add two simple example programs. -- Jakub Wilk Thu, 11 Feb 2010 17:48:36 +0100 python-djvulibre (0.1.16) unstable; urgency=low * Make reading S-expression from streams more efficient. * Fix exception handling while reading/writing S-expression from/to streams. -- Jakub Wilk Sun, 03 Jan 2010 12:52:12 +0100 python-djvulibre (0.1.15) unstable; urgency=low * Don't let text zones compare equal to unrelated objects. -- Jakub Wilk Wed, 04 Nov 2009 20:02:07 +0100 python-djvulibre (0.1.14) unstable; urgency=low * Fix infinite recursion when comparing a text zone with a string. Thanks to Rogério Brito for the bug report. -- Jakub Wilk Thu, 03 Sep 2009 23:36:47 +0200 python-djvulibre (0.1.13) unstable; urgency=low * Fix major breakage introduced in 0.1.12. Thanks to Piotr Ożarowski for reporting that. * Get rid of some spurious warnings. -- Jakub Wilk Sun, 16 Aug 2009 10:49:42 +0200 python-djvulibre (0.1.12) unstable; urgency=low * Use Cython (rather than Pyrex) to compile sources. -- Jakub Wilk Thu, 13 Aug 2009 13:07:16 +0200 python-djvulibre (0.1.11) unstable; urgency=low * Provide a bit more comprehensive documentation. -- Jakub Wilk Tue, 14 Jul 2009 16:33:13 +0200 python-djvulibre (0.1.10) unstable; urgency=low * Remove the `PageInfo` class (it served no purpose). * Work around a Pyrex compilation bug. -- Jakub Wilk Tue, 07 Jul 2009 23:12:27 +0200 python-djvulibre (0.1.9) unstable; urgency=low * Fill up the package metadata. -- Jakub Wilk Sat, 16 May 2009 11:05:45 +0200 python-djvulibre (0.1.8) unstable; urgency=low * Enable compilation with recent Pyrex. -- Jakub Wilk Sat, 12 Jul 2008 12:10:08 +0200 python-djvulibre (0.1.7) unstable; urgency=low * Protect from collecting just created S-expressions. -- Jakub Wilk Fri, 20 Jun 2008 12:39:24 +0200 python-djvulibre (0.1.6) unstable; urgency=low * Call `Context.handle_message()` *before* any blocking method finishes. * Better error handling in `Page.decode()`. -- Jakub Wilk Wed, 14 May 2008 11:19:52 +0200 python-djvulibre (0.1.5) unstable; urgency=low * Fix a race condition in `DocumentDecodingJob.wait()`. * `DocumentPages` and `DocumentFiles` checks for actual length and raises `IndexError` exceptions. * Add `Page.size` and `PageJob.size` properties. * Remove spurious overflow checks. -- Jakub Wilk Wed, 07 May 2008 18:36:51 +0200 python-djvulibre (0.1.4) unstable; urgency=low * Initial release. -- Jakub Wilk Wed, 07 May 2008 00:17:17 +0200 python-djvulibre-0.8.4/doc/credits0000644000000000000000000000025513347744054017170 0ustar00rootroot00000000000000python-djvulibre development was supported by the Polish Ministry of Science and Higher Education's grant no. N N519 384036 (2009–2012, https://bitbucket.org/jsbien/ndt). python-djvulibre-0.8.4/doc/todo0000644000000000000000000000056713347744054016506 0ustar00rootroot00000000000000Verify that byte and Unicode strings are used consistently. Document how they are used. The ``FileUri`` class is ugly. Implement a nicer interface. Add an example for using streams. Improve the test suite. Track test coverage. Cython ≥ 0.23 provides the plugin ``Cython.Coverage`` that can be added to ``.coveragerc``. .. vim:ft=rst ts=3 sts=3 sw=3 et tw=72 python-djvulibre-0.8.4/examples/0000755000000000000000000000000013441503047016645 5ustar00rootroot00000000000000python-djvulibre-0.8.4/examples/djvu-crop-text0000755000000000000000000000736013303343211021464 0ustar00rootroot00000000000000#!/usr/bin/env python # encoding=UTF-8 # Copyright © 2008-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. from __future__ import print_function import argparse import os import sys import djvu.const import djvu.decode import djvu.sexpr EMPTY_TEXT_SEXPR = djvu.sexpr.Expression([djvu.const.TEXT_ZONE_PAGE, 0, 0, 0, 0, '']) class ArgumentParser(argparse.ArgumentParser): def __init__(self): argparse.ArgumentParser.__init__(self) self.add_argument('-p', '--pages', dest='pages', action='store', help='pages to process') self.add_argument('path', metavar='DJVU-FILE', action='store', help='DjVu file to process') def parse_args(self): options = argparse.ArgumentParser.parse_args(self) try: if options.pages is not None: pages = [] for rng in options.pages.split(','): if '-' in rng: x, y = map(int, options.pages.split('-', 1)) pages += range(x, y + 1) else: pages += [int(rng)] options.pages = pages except (TypeError, ValueError): self.error('Unable to parse page numbers') return options def crop_text(sexpr, width, height): if isinstance(sexpr, djvu.sexpr.ListExpression) and len(sexpr) >= 5: tp = sexpr[0] x0, y0, x1, y1 = (sexpr[i].value for i in range(1, 5)) if x1 < 0 or y1 < 0 or x0 >= width or y0 >= height: return x0 = max(0, x0) y0 = max(0, y0) x1 = min(x1, width) y1 = min(y1, height) children = (crop_text(child, width, height) for child in sexpr[5:]) children = [child for child in children if child is not None] if not children: return return djvu.sexpr.Expression([tp, x0, y0, x1, y1] + children) else: return sexpr class Context(djvu.decode.Context): def handle_message(self, message): if isinstance(message, djvu.decode.ErrorMessage): print(message, file=sys.stderr) # Exceptions in handle_message() are ignored, so sys.exit() # wouldn't work here. os._exit(1) def process_page(self, page): print('- Page #{0}'.format(page.n + 1), file=sys.stderr) page.get_info() text = crop_text(page.text.sexpr, page.width, page.height) if not text: text = EMPTY_TEXT_SEXPR return text def process(self, path, pages=None): print('Processing {path!r}:'.format(path=path), file=sys.stderr) document = self.new_document(djvu.decode.FileURI(path)) document.decoding_job.wait() sed_file = sys.stdout if pages is None: pages = iter(document.pages) else: pages = (document.pages[i - 1] for i in pages) sed_file.write('remove-txt\n') for page in pages: sed_file.write('select {0}\n'.format(page.n + 1)) sed_file.write('set-txt\n') self.process_page(page).print_into(sed_file) sed_file.write('\n.\n\n') def main(): parser = ArgumentParser() options = parser.parse_args() context = Context() context.process(options.path, options.pages) if __name__ == '__main__': main() # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/examples/djvu-dump-text0000755000000000000000000000346013303343211021463 0ustar00rootroot00000000000000#!/usr/bin/env python # encoding=UTF-8 # Copyright © 2008-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. from __future__ import print_function import argparse import os import sys import djvu.decode def print_text(sexpr, level=0): if level > 0: print(' ' * (2 * level - 1), end=' ') if isinstance(sexpr, djvu.sexpr.ListExpression): if len(sexpr) == 0: return print(str(sexpr[0].value), [sexpr[i].value for i in range(1, 5)]) for child in sexpr[5:]: print_text(child, level + 1) else: print(sexpr) class Context(djvu.decode.Context): def handle_message(self, message): if isinstance(message, djvu.decode.ErrorMessage): print(message, file=sys.stderr) # Exceptions in handle_message() are ignored, so sys.exit() # wouldn't work here. os._exit(1) def process(self, path): document = self.new_document(djvu.decode.FileURI(path)) document.decoding_job.wait() for page in document.pages: page.get_info() print_text(page.text.sexpr) def main(): ap = argparse.ArgumentParser() ap.add_argument('path', metavar='DJVU-FILE') options = ap.parse_args() context = Context() context.process(options.path) if __name__ == '__main__': main() # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/examples/djvu2png0000755000000000000000000000627313303343211020332 0ustar00rootroot00000000000000#!/usr/bin/env python # encoding=UTF-8 # Copyright © 2010-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. from __future__ import print_function import argparse import os import sys import cairo import djvu.decode import numpy cairo_pixel_format = cairo.FORMAT_ARGB32 djvu_pixel_format = djvu.decode.PixelFormatRgbMask(0xFF0000, 0xFF00, 0xFF, bpp=32) djvu_pixel_format.rows_top_to_bottom = 1 djvu_pixel_format.y_top_to_bottom = 0 class Context(djvu.decode.Context): def handle_message(self, message): if isinstance(message, djvu.decode.ErrorMessage): print(message, file=sys.stderr) # Exceptions in handle_message() are ignored, so sys.exit() # wouldn't work here. os._exit(1) def process(self, djvu_path, png_path, mode): document = self.new_document(djvu.decode.FileURI(djvu_path)) document.decoding_job.wait() for page in document.pages: page_job = page.decode(wait=True) width, height = page_job.size rect = (0, 0, width, height) bytes_per_line = cairo.ImageSurface.format_stride_for_width(cairo_pixel_format, width) color_buffer = numpy.zeros((height, bytes_per_line), dtype=numpy.uint32) page_job.render(mode, rect, rect, djvu_pixel_format, row_alignment=bytes_per_line, buffer=color_buffer) mask_buffer = numpy.zeros((height, bytes_per_line), dtype=numpy.uint32) if mode == djvu.decode.RENDER_FOREGROUND: page_job.render(djvu.decode.RENDER_MASK_ONLY, rect, rect, djvu_pixel_format, row_alignment=bytes_per_line, buffer=mask_buffer) mask_buffer <<= 24 color_buffer |= mask_buffer color_buffer ^= 0xFF000000 surface = cairo.ImageSurface.create_for_data(color_buffer, cairo_pixel_format, width, height) surface.write_to_png(png_path) # Multi-page documents are not yet supported: break def main(): parser = argparse.ArgumentParser() parser.set_defaults(mode=djvu.decode.RENDER_COLOR) group = parser.add_mutually_exclusive_group() group.add_argument('--foreground', dest='mode', action='store_const', const=djvu.decode.RENDER_FOREGROUND) group.add_argument('--background', dest='mode', action='store_const', const=djvu.decode.RENDER_BACKGROUND) group.add_argument('--mask', dest='mode', action='store_const', const=djvu.decode.RENDER_MASK_ONLY) parser.add_argument('djvu_path', metavar='DJVU-FILE') parser.add_argument('png_path', metavar='PNG-FILE') options = parser.parse_args(sys.argv[1:]) context = Context() context.process(options.djvu_path, options.png_path, options.mode) if __name__ == '__main__': main() # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/private/0000755000000000000000000000000013441503047016501 5ustar00rootroot00000000000000python-djvulibre-0.8.4/private/apt-install-build-reqs0000755000000000000000000000264213430350473022731 0ustar00rootroot00000000000000#!/bin/sh # Copyright © 2015-2019 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. pkgs_base=' build-essential libdjvulibre-dev pkg-config python-dev cython ' pkgs_tests=' python-nose python-subprocess32 djvulibre-bin ghostscript ' pkgs="$pkgs_base" usage() { printf 'Usage: %s [--tests]\n' "$0" } set -e -u if ! args=$(getopt -n "$0" -o 'h' --long 'help,tests,py3' -- "$@") then usage >&2 exit 1 fi opt_tests= opt_py3= eval set -- "$args" while true do case "$1" in -h|--help) usage; exit 0;; --tests) opt_tests=y; shift;; --py3) opt_py3=y; shift;; --) shift; break;; *) printf '%s: internal error (%s)\n' "$0" "$1" >&2; exit 1;; esac done [ "$opt_tests" ] && pkgs="$pkgs $pkgs_tests" if [ "$opt_py3" ] then pkgs=$(printf '%s' "$pkgs" | sed -e '/^python-subprocess32$/d' -e 's/^python-/python3-/') fi PS4='# ' ( set -x # shellcheck disable=SC2086 apt-get install $pkgs ) # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/private/build-and-test0000755000000000000000000000256513355366111021256 0ustar00rootroot00000000000000#!/bin/sh # Copyright © 2015-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. usage() { printf 'Usage: %s [--no-build] [pythonX.Y...]\n' "$0" } set -e -u if ! args=$(getopt -n "$0" -o 'hj:' --long 'help,jobs:,no-build' -- "$@") then usage >&2 exit 1 fi eval set -- "$args" opt_jobs=$(nproc) || opt_jobs=1 opt_build=y while true do case "$1" in -h|--help) usage; exit 0;; -j|--jobs) opt_jobs=$2; shift 2;; --no-build) opt_build=; shift;; --) shift; break;; *) printf '%s: internal error (%s)\n' "$0" "$1" >&2; exit 1;; esac done [ $# = 0 ] && set -- python [ -z $opt_build ] || printf '%s\n' "$@" \ | xargs -P"$opt_jobs" -t -I'{python}' env '{python}' setup.py build --build-lib 'build/{python}' cd tests printf '%s\n' "$@" \ | xargs -t -I'{python}' env PYTHONPATH="$PWD/../build/{python}" '{python}' -c 'import nose; nose.main()' --verbose # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/private/check-rst0000755000000000000000000000230013347744054020317 0ustar00rootroot00000000000000#!/bin/sh # Copyright © 2016-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. set -e -u here=${0%/*} here=${here#./} root="$here/../" root=${root#private/../} rst2xml=$(command -v rst2xml) \ || rst2xml=$(command -v rst2xml.py) \ || { printf 'rst2xml not found\n' >&2; exit 1; } rst2xml=${rst2xml##*/} options='--input-encoding=UTF-8 --strict' if [ $# -eq 0 ] then print_desc='python setup.py --long-description' echo "(cd ${root:-.} && $print_desc) | $rst2xml $options - /dev/null" >&2 (cd "${root:-.}" && $print_desc) | "$rst2xml" $options - /dev/null || exit 1 fi if [ $# -eq 0 ] then grep -r -w -l ft=rst "${root}doc/" else printf '%s\n' "$@" fi | xargs -L1 -t -I{} "$rst2xml" $options {} /dev/null # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/setup.cfg0000644000000000000000000000015213277057165016662 0ustar00rootroot00000000000000[pycodestyle] filename = *.py,*.pyx,*.px[di] ignore = E12,E131,E2,E3,E4,E501,E722,W504 show-source = true python-djvulibre-0.8.4/setup.py0000644000000000000000000002514113277057165016560 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2007-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. ''' *python-djvulibre* is a set of Python bindings for the `DjVuLibre `_ library, an open source implementation of `DjVu `_. ''' import glob import io import os import re import subprocess as ipc import sys need_setuptools = False if os.name == 'nt': import djvu.dllpath need_setuptools = True if need_setuptools: import setuptools.extension assert setuptools.extension.have_pyrex import distutils.core import distutils.ccompiler import distutils.command.build_ext import distutils.command.sdist import distutils.dep_util import distutils.dir_util import distutils.version try: import sphinx.setup_command as sphinx_setup_command except ImportError: sphinx_setup_command = None try: from wheel.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None try: import distutils644 except ImportError: pass else: distutils644.install() b'' # Python >= 2.6 is required def ext_modules(): for pyx_file in glob.iglob(os.path.join('djvu', '*.pyx')): module, _ = os.path.splitext(os.path.basename(pyx_file)) yield module ext_modules = list(ext_modules()) def get_version(): path = os.path.join(os.path.dirname(__file__), 'doc', 'changelog') with io.open(path, encoding='UTF-8') as file: line = file.readline() return line.split()[1].strip('()') def run_pkgconfig(*cmdline): cmdline = ['pkg-config'] + list(cmdline) try: pkgconfig = ipc.Popen( cmdline, stdout=ipc.PIPE, stderr=ipc.PIPE ) except EnvironmentError as exc: msg = 'cannot execute pkg-config: {exc.strerror}'.format(exc=exc) distutils.log.warn(msg) return stdout, stderr = pkgconfig.communicate() stdout = stdout.decode('ASCII') stderr = stderr.decode('ASCII', 'replace') if pkgconfig.returncode != 0: msg = 'pkg-config failed: {msg}'.format(msg=stderr.strip()) distutils.log.warn(msg) return return stdout def pkgconfig_build_flags(*packages, **kwargs): flag_map = { '-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries', } fallback = dict( libraries=['djvulibre'], ) if os.name == 'nt': dll_path = djvu.dllpath.guess_dll_path() if dll_path is not None: fallback.update( include_dirs=[os.path.join(dll_path, 'include')], library_dirs=[os.path.join(dll_path)], libraries=['libdjvulibre'], ) stdout = run_pkgconfig('--libs', '--cflags', *packages) if stdout is None: return fallback kwargs.setdefault('extra_link_args', []) kwargs.setdefault('extra_compile_args', []) for argument in stdout.split(): key = argument[:2] try: value = argument[2:] kwargs.setdefault(flag_map[key], []).append(value) except KeyError: kwargs['extra_link_args'].append(argument) kwargs['extra_compile_args'].append(argument) return kwargs def pkgconfig_version(package): stdout = run_pkgconfig('--modversion', package) if stdout is None: return return stdout.strip() def get_djvulibre_version(): version = pkgconfig_version('ddjvuapi') if version is None: if os.name == 'posix': raise RuntimeError('cannot determine DjVuLibre version') elif os.name == 'nt': version = djvu.dllpath._guess_dll_version() version = version or '0' return distutils.version.LooseVersion(version) def get_cython_version(): cmdline = ['python', '-m', 'cython', '--version'] cmd = ipc.Popen(cmdline, stdout=ipc.PIPE, stderr=ipc.STDOUT) stdout, stderr = cmd.communicate() if not isinstance(stdout, str): stdout = stdout.decode('ASCII') match = re.match(r'\ACython version (\d\S+)', stdout) if match: ver = match.group(1) else: ver = '0' return distutils.version.LooseVersion(ver) py_version = get_version() cython_version = get_cython_version() if str is bytes: # Python 2.X req_cython_version = '0.19' else: # Python 3.X req_cython_version = '0.20' # Work-around for : os.environ.pop('CFLAGS', None) class build_ext(distutils.command.build_ext.build_ext): def run(self): djvulibre_version = get_djvulibre_version() if djvulibre_version != '0' and djvulibre_version < '3.5.21': raise RuntimeError('DjVuLibre >= 3.5.21 is required') new_config = [ 'DEF PY3K = {0}'.format(sys.version_info >= (3, 0)), 'DEF PYTHON_DJVULIBRE_VERSION = b"{0}"'.format(py_version), 'DEF HAVE_MINIEXP_IO_T = {0}'.format(djvulibre_version >= '3.5.26'), 'DEF HAVE_LANGINFO_H = {0}'.format(os.name == 'posix'), 'DEF WINDOWS = {0}'.format(os.name == 'nt'), ] self.src_dir = src_dir = os.path.join(self.build_temp, 'src') distutils.dir_util.mkpath(src_dir) self.config_path = os.path.join(src_dir, 'config.pxi') try: with open(self.config_path, 'rt') as fp: old_config = fp.read() except IOError: old_config = '' if '\n'.join(new_config).strip() != old_config.strip(): distutils.log.info('creating {conf!r}'.format(conf=self.config_path)) distutils.file_util.write_file(self.config_path, new_config) distutils.command.build_ext.build_ext.run(self) def build_extensions(self): self.check_extensions_list(self.extensions) for ext in self.extensions: ext.sources = list(self.cython_sources(ext)) self.build_extension(ext) def cython_sources(self, ext): for source in ext.sources: source_base = os.path.basename(source) assert source_base.endswith('.pyx') target = os.path.join( self.src_dir, '{mod}.c'.format(mod=source_base[:-4]) ) yield target depends = [source, self.config_path] + ext.depends if not (self.force or distutils.dep_util.newer_group(depends, target)): distutils.log.debug('not cythoning {ext.name!r} (up-to-date)'.format(ext=ext)) continue distutils.log.info('cythoning {ext.name!r} extension'.format(ext=ext)) def build_c(source, target): if cython_version < req_cython_version: raise RuntimeError('Cython >= {ver} is required'.format(ver=req_cython_version)) distutils.spawn.spawn([ 'python', '-m', 'cython', '-I', os.path.dirname(self.config_path), '-o', target, source, ]) self.make_file(depends, target, build_c, [source, target]) if sphinx_setup_command: class build_sphinx(sphinx_setup_command.BuildDoc): def run(self): # Make sure that djvu module is imported from the correct # directory. # # The current directory (which is normally in sys.path[0]) is # typically a wrong choice: it contains djvu/__init__.py but not # the extension modules. Prepend the directory that build_ext would # use instead. build_ext = self.get_finalized_command('build_ext') sys.path[:0] = [build_ext.build_lib] for ext in ext_modules: __import__('djvu.' + ext) del sys.path[0] sphinx_setup_command.BuildDoc.run(self) else: build_sphinx = None class sdist(distutils.command.sdist.sdist): def maybe_move_file(self, base_dir, src, dst): src = os.path.join(base_dir, src) dst = os.path.join(base_dir, dst) if os.path.exists(src): self.move_file(src, dst) def make_release_tree(self, base_dir, files): distutils.command.sdist.sdist.make_release_tree(self, base_dir, files) self.maybe_move_file(base_dir, 'COPYING', 'doc/COPYING') compiler_flags = pkgconfig_build_flags('ddjvuapi') classifiers = ''' Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: GNU General Public License (GPL) Operating System :: POSIX Operating System :: Microsoft :: Windows Programming Language :: Cython Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 3 Topic :: Multimedia :: Graphics Topic :: Multimedia :: Graphics :: Graphics Conversion Topic :: Text Processing '''.strip().splitlines() meta = dict( name='python-djvulibre', version=py_version, author='Jakub Wilk', author_email='jwilk@jwilk.net', license='GNU GPL 2', description='Python support for the DjVu image format', long_description=__doc__.strip(), classifiers=classifiers, url='http://jwilk.net/software/python-djvulibre', ) setup_params = dict( packages=['djvu'], ext_modules=[ distutils.command.build_ext.Extension( 'djvu.{mod}'.format(mod=name), ['djvu/{mod}.pyx'.format(mod=name)], depends=(['djvu/common.pxi'] + glob.glob('djvu/*.pxd')), **compiler_flags ) for name in ext_modules ], py_modules=['djvu.const'], cmdclass=dict( (cmd.__name__, cmd) for cmd in (build_ext, build_sphinx, sdist, bdist_wheel) if cmd is not None ), **meta ) if __name__ == '__main__': egg_info_for_pip = ('setuptools' in sys.modules) and (sys.argv[1] == 'egg_info') if (cython_version < req_cython_version) and egg_info_for_pip: # This shouldn't happen with pip >= 10, thanks to PEP-518 support. # For older versions, we use this hack to trick it into installing Cython: distutils.core.setup( install_requires=['Cython>={ver}'.format(ver=req_cython_version)], # Conceptually, “setup_requires” would make more sense than # “install_requires”, but the former is not supported by pip. **meta ) else: distutils.core.setup(**setup_params) # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/tests/0000755000000000000000000000000013441503047016171 5ustar00rootroot00000000000000python-djvulibre-0.8.4/tests/images/0000755000000000000000000000000013441503047017436 5ustar00rootroot00000000000000python-djvulibre-0.8.4/tests/images/Makefile0000644000000000000000000000157613347744054021121 0ustar00rootroot00000000000000# Copyright © 2010-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. PDF2DJVU = pdf2djvu tex_files = $(wildcard *.tex) pdf_files = $(tex_files:.tex=.pdf) djvu_files = $(pdf_files:.pdf=.djvu) .PHONY: all all: $(djvu_files) %.pdf: %.tex pdflatex $(<) pdflatex $(<) %.djvu: %.pdf $(PDF2DJVU) $(<) -o $(@) .PHONY: clean clean: rm -f $(pdf_files) *.log *.out *.aux .error = GNU make is required # vim:ts=4 sts=4 sw=4 noet python-djvulibre-0.8.4/tests/images/test0.djvu0000644000000000000000000000756312726107516021410 0ustar00rootroot00000000000000AT&TFORMgDJVMDIRM?J @̿մs ᪷zG2޳T+ &xNNAVM@&niG]VSZ3<@DXjY9-[NFORMDJVIANTz`K'8܇C[qyӫbVsi4A(<)ߺvPϣ_Cq=sx})"#q6W ,CϿb0]~52!1k,`.7{(f[ӭGN7LhZlvv.,Ƴ?s=gϥx\7E8FdYtf%D]u[$gJ:(sH?s*ATLoi"}JT"{UJ>^PQ'逸Ns"s60YVĥ/kH~06H"`W޲)1K#yo97>}j*}'4cVoo zjoڸ~^eg$jΦUjt_M]x}nO3pH„f1s"!M#9R,3Jn4u4sgM7_ԺjM+[!%ONcH 촣Y0 %ѓV?É'>Yu ǵJςFKBZG{>2:<}THk,uP-l|N;1d(-RaJi]9azae1=(F$OQomV(wLx " ^~ ZZ5]ê}純$wAjrHgD[[}o` Jπ,X@Bζ.U"@~zlE c),je;OUԚ+p,5RhM5W'D[Ԉ7mԒ3 1>ۆ :W%X{qaS@1fYᄔ;;$7O'3pg#-,TcK7Dg񃈵ݜSUwHϑO @)sǴMb2tbF6„?!l vHD9b Mj@n>^]v$6[lÕ6\iCSL `a"ܲxv-©}!2OL;[$BDU^*6v *?gEx3u>8,rXehX+u3!X(r&BF-|!jikJJcVS3>Ȭ9)I< jhF~mrެ45TF5T&Ζ%e&_QT3FJ[ F%Qҥe)}9lߙ롚i=YL{"1vԵ;0;|g&uΏQ2Wzk7'+lǻ{ܞu8ElA{TS||Wg;>鸐 PY KZJjϟmѹ4Q=3%xՕUZ5<ڸv5W^n1v}% )FG~X_K\ |I1V٘]TXTzZGh9|eߺ׏Y9{_"&spDW Yce7  #$c\LH;!T# -4ti`Z[Ahf%c+k) !ݤzqsr|17|S'g)NMu+oQĘB@ԃ YH$%dӺdh>\ CǀW=W: !N+TO.12_O2fV:xZy/]έ>S|;U& m=q7 D*fdϠ/OOຉ,A]sw 0Ŀ{H\k*/Wys;eE>RzX1rOFiGrL"E aEpV,gE!s;մŅëު`wfϪjԕ޺;o`Y8j6FJQ FORM+DJVUINFO ,INCLshared_anno.iffSjbzՀv%y{E&&<՛6fdy5@d͑yIgiZob ֜]hG+6h^Zx\?U_峴x-$A!;۬=bRL 8sA)9Ǽ'YN(ZkYM="h͑ yE^_,;bq)MDw/4cCD[֩F +3s`8ɮ.'";A}Y-&+2{r8S(q+]bSYj14$@a![-A:_1qdI,#6*rjc3rYԶ#7@.GjwXgLŦS4"f"U`jjd,J_ 7@M$.33ڧe഻4ɻ#*܏I׆ Olr((t|=7b ~>UefS%Kc>#TXwh9ACA աyʂd&))KE+24t#qàdgwo/j,A*EdhWâល&(cQi DcRe,S+&VD|.ذw*cb9FiʉՈ&^>p$PPRhI[q?px{䭍 a?ѵV;Lݬ(Txq+rQߔsPku͸&6PJwDg |I,⺪cQ>/ww|S_NNl&SAo7q9ttFa$ңb+ZY\AȝA0v+(W*}jdHgifSqgSfVT]8iΦx:J^Qnw:;/EYbJrXϛe\r(?1,"FKRYeANTzft| ^Bz`4[\?ηPy{Gbꕚvۜ Ez/膇|b^p[kjo(7FMTXTzZEoQ` % % This file is part of python-djvulibre. % % python-djvulibre is free software; you can redistribute it and/or modify it % under the terms of the GNU General Public License version 2 as published by % the Free Software Foundation. % % python-djvulibre is distributed in the hope that it will be useful, but % WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY % or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for % more details. \documentclass[12pt]{article} \usepackage[pdftex,unicode,pdfauthor={Jakub Wilk},bookmarks,plainpages=false]{hyperref} \usepackage[utf8]{inputenc} \usepackage{times} \setlength{\parindent}{0mm} \begin{document} \section{Lorem ipsum} \label{lorem-ipsum} Optio reprehenderit molestias amet aliquam, similique doloremque fuga labore voluptatum voluptatem, commodi culpa voluptas, officia tenetur expedita quidem hic repellat molestiae quis accusamus dolores repudiandae, quidem in ad voluptas eligendi maiores placeat ex consectetur at tenetur amet. \newpage \section{Hyperlinks} \subsection{local} $\to$ \ref{lorem-ipsum} \subsection{remote} \url{http://jwilk.net/} \end{document} python-djvulibre-0.8.4/tests/images/test1.djvu0000644000000000000000000000013712726107516021377 0ustar00rootroot00000000000000AT&TFORMSDJVUINFO @0,Sjbz5kf#S0+sr;v-UKåeM>#nZq6C}7python-djvulibre-0.8.4/tests/sexpr-gc.py0000644000000000000000000000243113036714410020271 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2007-2015 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. from __future__ import division if __name__ != '__main__': raise ImportError('This module is not intended for import') import djvu.sexpr import os proc_status = '/proc/{pid}/status'.format(pid=os.getpid()) scale = dict(kB=1024) def mem_info(key='VmSize'): try: file = open(proc_status) for line in file: if line.startswith('{key}:'.format(key=key)): _, value, unit = line.split(None, 3) return int(value) * scale[unit] finally: file.close() try: range = xrange except NameError: pass step = 1 << 17 while True: mb = mem_info() / (1 << 20) print('{mb:.2f}M'.format(mb=mb)) [djvu.sexpr.Expression(4) for i in range(step)] # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/tests/test_const.py0000644000000000000000000001360312726107516020741 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2008-2015 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. from djvu.const import ( TEXT_ZONE_CHARACTER, TEXT_ZONE_COLUMN, TEXT_ZONE_LINE, TEXT_ZONE_PAGE, TEXT_ZONE_PARAGRAPH, TEXT_ZONE_REGION, TEXT_ZONE_WORD, TextZoneType, get_text_zone_type, ) from djvu.sexpr import ( Symbol, ) from tools import ( assert_equal, assert_is, assert_is_instance, assert_list_equal, assert_not_equal, assert_raises_str, assert_repr, wildcard_import, # Python 2/3 compat: cmp, ) class test_text_zones(): zones = [ TEXT_ZONE_PAGE, TEXT_ZONE_COLUMN, TEXT_ZONE_REGION, TEXT_ZONE_PARAGRAPH, TEXT_ZONE_LINE, TEXT_ZONE_WORD, TEXT_ZONE_CHARACTER ] def test_type(self): for zone in self.zones: assert_equal(type(zone), TextZoneType) assert_is_instance(zone, Symbol) def test_repr(self): assert_repr(TEXT_ZONE_PAGE, '') assert_repr(TEXT_ZONE_COLUMN, '') assert_repr(TEXT_ZONE_REGION, '') assert_repr(TEXT_ZONE_PARAGRAPH, '') assert_repr(TEXT_ZONE_LINE, '') assert_repr(TEXT_ZONE_WORD, '') assert_repr(TEXT_ZONE_CHARACTER, '') def test_identity(self): assert_is(TEXT_ZONE_PAGE, get_text_zone_type(Symbol('page'))) assert_is(TEXT_ZONE_COLUMN, get_text_zone_type(Symbol('column'))) assert_is(TEXT_ZONE_REGION, get_text_zone_type(Symbol('region'))) assert_is(TEXT_ZONE_PARAGRAPH, get_text_zone_type(Symbol('para'))) assert_is(TEXT_ZONE_LINE, get_text_zone_type(Symbol('line'))) assert_is(TEXT_ZONE_WORD, get_text_zone_type(Symbol('word'))) assert_is(TEXT_ZONE_CHARACTER, get_text_zone_type(Symbol('char'))) def test_comparison1(self): assert_not_equal(TEXT_ZONE_PAGE, '') assert_not_equal(TEXT_ZONE_PAGE, 42) with assert_raises_str(TypeError, 'cannot compare text zone type with other object'): TEXT_ZONE_PAGE < 42 with assert_raises_str(TypeError, 'cannot compare text zone type with other object'): TEXT_ZONE_PAGE <= 42 with assert_raises_str(TypeError, 'cannot compare text zone type with other object'): TEXT_ZONE_PAGE > 42 with assert_raises_str(TypeError, 'cannot compare text zone type with other object'): TEXT_ZONE_PAGE >= 42 def test_comparison2(self): assert_equal(self.zones, sorted(self.zones, reverse=True)) assert_equal( [[cmp(z1, z2) for z1 in self.zones] for z2 in self.zones], [ [0, -1, -1, -1, -1, -1, -1], [+1, 0, -1, -1, -1, -1, -1], [+1, +1, 0, -1, -1, -1, -1], [+1, +1, +1, 0, -1, -1, -1], [+1, +1, +1, +1, 0, -1, -1], [+1, +1, +1, +1, +1, 0, -1], [+1, +1, +1, +1, +1, +1, 0], ] ) def test_wildcard_import(): ns = wildcard_import('djvu.const') assert_list_equal( sorted(ns.keys()), [ 'ANNOTATION_ALIGN', 'ANNOTATION_BACKGROUND', 'ANNOTATION_MAPAREA', 'ANNOTATION_METADATA', 'ANNOTATION_MODE', 'ANNOTATION_PRINTED_FOOTER', 'ANNOTATION_PRINTED_HEADER', 'ANNOTATION_ZOOM', 'EMPTY_LIST', 'EMPTY_OUTLINE', 'MAPAREA_ARROW', 'MAPAREA_BACKGROUND_COLOR', 'MAPAREA_BORDER_ALWAYS_VISIBLE', 'MAPAREA_BORDER_ETCHED_IN', 'MAPAREA_BORDER_ETCHED_OUT', 'MAPAREA_BORDER_NONE', 'MAPAREA_BORDER_SHADOW_IN', 'MAPAREA_BORDER_SHADOW_OUT', 'MAPAREA_BORDER_SOLID_COLOR', 'MAPAREA_BORDER_XOR', 'MAPAREA_HIGHLIGHT_COLOR', 'MAPAREA_LINE_COLOR', 'MAPAREA_LINE_COLOR_DEFAULT', 'MAPAREA_LINE_MIN_WIDTH', 'MAPAREA_LINE_WIDTH', 'MAPAREA_OPACITY', 'MAPAREA_OPACITY_DEFAULT', 'MAPAREA_OPACITY_MAX', 'MAPAREA_OPACITY_MIN', 'MAPAREA_PUSHPIN', 'MAPAREA_SHADOW_BORDERS', 'MAPAREA_SHADOW_BORDER_MAX_WIDTH', 'MAPAREA_SHADOW_BORDER_MIN_WIDTH', 'MAPAREA_SHAPE_LINE', 'MAPAREA_SHAPE_OVAL', 'MAPAREA_SHAPE_POLYGON', 'MAPAREA_SHAPE_RECTANGLE', 'MAPAREA_SHAPE_TEXT', 'MAPAREA_TEXT_COLOR', 'MAPAREA_TEXT_COLOR_DEFAULT', 'MAPAREA_URI', 'MAPAREA_URL', 'METADATA_BIBTEX_KEYS', 'METADATA_KEYS', 'METADATA_PDFINFO_KEYS', 'PRINTED_FOOTER_ALIGN_CENTER', 'PRINTED_FOOTER_ALIGN_LEFT', 'PRINTED_FOOTER_ALIGN_RIGHT', 'PRINTER_HEADER_ALIGN_CENTER', 'PRINTER_HEADER_ALIGN_LEFT', 'PRINTER_HEADER_ALIGN_RIGHT', 'TEXT_ZONE_CHARACTER', 'TEXT_ZONE_COLUMN', 'TEXT_ZONE_LINE', 'TEXT_ZONE_PAGE', 'TEXT_ZONE_PARAGRAPH', 'TEXT_ZONE_REGION', 'TEXT_ZONE_SEPARATORS', 'TEXT_ZONE_WORD', 'TextZoneType', 'get_text_zone_type' ] ) # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/tests/test_decode.py0000644000000000000000000011254113430350473021032 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2007-2019 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. import array import errno import os import re import shutil import sys import tempfile import warnings if sys.version_info >= (3, 2): import subprocess else: try: import subprocess32 as subprocess except ImportError as exc: import subprocess msg = str(exc) warnings.warn(msg, category=RuntimeWarning) # subprocess is not thread-safe in Python < 3.2 del msg, exc from djvu.decode import ( AffineTransform, Context, DDJVU_VERSION, DOCUMENT_TYPE_BUNDLED, DOCUMENT_TYPE_SINGLE_PAGE, DocInfoMessage, Document, DocumentAnnotations, DocumentDecodingJob, DocumentOutline, ErrorMessage, File, FileUri, Hyperlinks, Job, JobFailed, JobOK, Message, Metadata, NewStreamMessage, NotAvailable, PAGE_TYPE_BITONAL, Page, PageAnnotations, PageJob, PageText, PixelFormat, PixelFormatGrey, PixelFormatPackedBits, PixelFormatPalette, PixelFormatRgb, PixelFormatRgbMask, RENDER_COLOR, SaveJob, Stream, TEXT_DETAILS_ALL, TEXT_DETAILS_CHARACTER, TEXT_DETAILS_COLUMN, TEXT_DETAILS_LINE, TEXT_DETAILS_PAGE, TEXT_DETAILS_PARAGRAPH, TEXT_DETAILS_REGION, TEXT_DETAILS_WORD, ThumbnailMessage, __version__, ) from djvu.sexpr import ( Expression, Symbol, ) from tools import ( assert_equal, assert_false, assert_is, assert_is_instance, assert_list_equal, assert_multi_line_equal, assert_raises, assert_raises_regex, assert_raises_str, assert_repr, assert_true, SkipTest, skip_unless_c_messages, skip_unless_command_exists, skip_unless_translation_exists, get_changelog_version, interim_locale, locale_encoding, wildcard_import, # Python 2/3 compat: b, py3k, u, unicode, ) images = os.path.join(os.path.dirname(__file__), 'images', '') if sys.version_info >= (3, 2): array_tobytes = array.array.tobytes else: array_tobytes = array.array.tostring def run(*cmd, **kwargs): stdin = kwargs.pop('stdin', None) env = dict(os.environ) for key, value in kwargs.items(): if key.isupper(): env[key] = value continue raise TypeError('{key!r} is an invalid keyword argument for this function'.format(key=key)) kwargs = dict( stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, ) if stdin is not None: kwargs.update(stdin=subprocess.PIPE) child = subprocess.Popen(list(cmd), **kwargs) (stdout, stderr) = child.communicate(stdin) if child.returncode != 0: raise subprocess.CalledProcessError(child.returncode, cmd[0]) return (stdout, stderr) def create_djvu(commands='', sexpr=''): skip_unless_command_exists('djvused') if sexpr: commands += '\nset-ant\n{sexpr}\n.\n'.format(sexpr=sexpr) file = tempfile.NamedTemporaryFile(prefix='test', suffix='djvu') file.seek(0) file.write( b'\x41\x54\x26\x54\x46\x4F\x52\x4D\x00\x00\x00\x22\x44\x4A\x56\x55' b'\x49\x4E\x46\x4F\x00\x00\x00\x0A\x00\x01\x00\x01\x18\x00\x2C\x01' b'\x16\x01\x53\x6A\x62\x7A\x00\x00\x00\x04\xBC\x73\x1B\xD7' ) file.flush() (stdout, stderr) = run('djvused', '-s', file.name, stdin=commands.encode(locale_encoding)) assert_equal(stdout, ''.encode(locale_encoding)) assert_equal(stderr, ''.encode(locale_encoding)) return file def test_context_cache(): context = Context() assert_equal(context.cache_size, 10 << 20) for n in -100, 0, 1 << 31: with assert_raises_str(ValueError, '0 < cache_size < (2 ** 31) must be satisfied'): context.cache_size = n with assert_raises_str(ValueError, '0 < cache_size < (2 ** 31) must be satisfied'): context.cache_size = 0 n = 1 while n < (1 << 31): context.cache_size = n assert_equal(context.cache_size, n) n = (n + 1) * 2 - 1 context.clear_cache() class test_documents: def test_bad_new(self): with assert_raises_str(TypeError, "cannot create 'djvu.decode.Document' instances"): Document() def test_nonexistent(self): path = '__nonexistent__' try: os.stat(path) except OSError as ex: c_message = ex.args[1] else: raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path) c_message.encode('ASCII') skip_unless_c_messages() context = Context() with assert_raises(JobFailed): context.new_document(FileUri(path)) message = context.get_message() assert_equal(type(message), ErrorMessage) assert_equal(type(message.message), unicode) assert_equal( message.message, "[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message) ) assert_equal(str(message), message.message) assert_equal(unicode(message), message.message) def test_nonexistent_ja(self): skip_unless_c_messages() skip_unless_translation_exists('ja_JP.UTF-8') path = '__nonexistent__' context = Context() try: with interim_locale(LC_ALL='ja_JP.UTF-8'): os.stat(path) except OSError as ex: c_message = ex.args[1] else: raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path) try: c_message.encode('ASCII') except UnicodeError: pass else: raise AssertionError( 'ja_JP error message is ASCII-only: {msg!r}'.format(msg=c_message) ) with interim_locale(LC_ALL='ja_JP.UTF-8'): with assert_raises(JobFailed): context.new_document(FileUri(path)) message = context.get_message() assert_equal(type(message), ErrorMessage) assert_equal(type(message.message), unicode) assert_equal( message.message, u("[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message)) ) assert_equal( str(message), "[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message) ) assert_equal(unicode(message), message.message) def test_new_document(self): context = Context() document = context.new_document(FileUri(images + 'test1.djvu')) assert_equal(type(document), Document) message = document.get_message() assert_equal(type(message), DocInfoMessage) assert_true(document.decoding_done) assert_false(document.decoding_error) assert_equal(document.decoding_status, JobOK) assert_equal(document.type, DOCUMENT_TYPE_SINGLE_PAGE) assert_equal(len(document.pages), 1) assert_equal(len(document.files), 1) decoding_job = document.decoding_job assert_true(decoding_job.is_done) assert_false(decoding_job.is_error) assert_equal(decoding_job.status, JobOK) file = document.files[0] assert_is(type(file), File) assert_is(file.document, document) assert_is(file.get_info(), None) assert_equal(file.type, 'P') assert_equal(file.n_page, 0) page = file.page assert_equal(type(page), Page) assert_is(page.document, document) assert_equal(page.n, 0) assert_is(file.size, None) assert_equal(file.id, u('test1.djvu')) assert_equal(type(file.id), unicode) assert_equal(file.name, u('test1.djvu')) assert_equal(type(file.name), unicode) assert_equal(file.title, u('test1.djvu')) assert_equal(type(file.title), unicode) dump = document.files[0].dump assert_equal(type(dump), unicode) assert_equal( [line for line in dump.splitlines()], [ u(' FORM:DJVU [83] '), u(' INFO [10] DjVu 64x48, v24, 300 dpi, gamma=2.2'), u(' Sjbz [53] JB2 bilevel data'), ] ) page = document.pages[0] assert_equal(type(page), Page) assert_is(page.document, document) assert_is(page.get_info(), None) assert_equal(page.width, 64) assert_equal(page.height, 48) assert_equal(page.size, (64, 48)) assert_equal(page.dpi, 300) assert_equal(page.rotation, 0) assert_equal(page.version, 24) file = page.file assert_equal(type(file), File) assert_equal(file.id, u('test1.djvu')) assert_equal(type(file.id), unicode) dump = document.files[0].dump assert_equal(type(dump), unicode) assert_equal( [line for line in dump.splitlines()], [ u(' FORM:DJVU [83] '), u(' INFO [10] DjVu 64x48, v24, 300 dpi, gamma=2.2'), u(' Sjbz [53] JB2 bilevel data'), ] ) assert_is(document.get_message(wait=False), None) assert_is(context.get_message(wait=False), None) with assert_raises_str(IndexError, 'file number out of range'): document.files[-1].get_info() assert_is(document.get_message(wait=False), None) assert_is(context.get_message(wait=False), None) with assert_raises_str(IndexError, 'page number out of range'): document.pages[-1] with assert_raises_str(IndexError, 'page number out of range'): document.pages[1] assert_is(document.get_message(wait=False), None) assert_is(context.get_message(wait=False), None) def test_save(self): skip_unless_command_exists('djvudump') context = Context() original_filename = images + 'test0.djvu' document = context.new_document(FileUri(original_filename)) message = document.get_message() assert_equal(type(message), DocInfoMessage) assert_true(document.decoding_done) assert_false(document.decoding_error) assert_equal(document.decoding_status, JobOK) assert_equal(document.type, DOCUMENT_TYPE_BUNDLED) assert_equal(len(document.pages), 2) assert_equal(len(document.files), 3) (stdout0, stderr0) = run('djvudump', original_filename, LC_ALL='C') assert_equal(stderr0, b'') stdout0 = stdout0.replace(b'\r\n', b'\n') tmpdir = tempfile.mkdtemp() try: tmp = open(os.path.join(tmpdir, 'tmp.djvu'), 'wb') job = document.save(tmp) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) tmp.close() (stdout, stderr) = run('djvudump', tmp.name, LC_ALL='C') assert_equal(stderr, b'') stdout = stdout.replace(b'\r\n', b'\n') assert_equal(stdout, stdout0) finally: shutil.rmtree(tmpdir) tmp = None tmpdir = tempfile.mkdtemp() try: tmp = open(os.path.join(tmpdir, 'tmp.djvu'), 'wb') job = document.save(tmp, pages=(0,)) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) tmp.close() stdout, stderr = run('djvudump', tmp.name, LC_ALL='C') assert_equal(stderr, b'') stdout = stdout.replace(b'\r\n', b'\n') stdout0 = stdout0.split(b'\n') stdout = stdout.split(b'\n') stdout[4] = stdout[4].replace(b' (1)', b'') assert_equal(len(stdout), 10) assert_equal(stdout[3:-1], stdout0[4:10]) assert_equal(stdout[-1], b'') finally: shutil.rmtree(tmpdir) tmp = None tmpdir = tempfile.mkdtemp() try: tmpfname = os.path.join(tmpdir, 'index.djvu') job = document.save(indirect=tmpfname) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) (stdout, stderr) = run('djvudump', tmpfname, LC_ALL='C') assert_equal(stderr, b'') stdout = stdout.replace(b'\r\n', b'\n') stdout = stdout.split(b'\n') stdout0 = ( [b' shared_anno.iff -> shared_anno.iff'] + [b(' p{n:04}.djvu -> p{n:04}.djvu'.format(n=n)) for n in range(1, 3)] ) assert_equal(len(stdout), 7) assert_equal(stdout[2:-2], stdout0) assert_equal(stdout[-1], b'') finally: shutil.rmtree(tmpdir) tmpdir = tempfile.mkdtemp() try: tmpfname = os.path.join(tmpdir, 'index.djvu') job = document.save(indirect=tmpfname, pages=(0,)) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) (stdout, stderr) = run('djvudump', tmpfname, LC_ALL='C') stdout = stdout.replace(b'\r\n', b'\n') assert_equal(stderr, b'') stdout = stdout.split(b'\n') assert_equal(len(stdout), 5) assert_equal(stdout[2], b' shared_anno.iff -> shared_anno.iff') assert_equal(stdout[3], b' p0001.djvu -> p0001.djvu') assert_equal(stdout[-1], b'') finally: shutil.rmtree(tmpdir) def test_export_ps(self): skip_unless_command_exists('ps2ascii') context = Context() document = context.new_document(FileUri(images + 'test0.djvu')) message = document.get_message() assert_equal(type(message), DocInfoMessage) assert_true(document.decoding_done) assert_false(document.decoding_error) assert_equal(document.decoding_status, JobOK) assert_equal(document.type, DOCUMENT_TYPE_BUNDLED) assert_equal(len(document.pages), 2) assert_equal(len(document.files), 3) with tempfile.NamedTemporaryFile() as tmp: job = document.export_ps(tmp.file) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) stdout, stderr = run('ps2ascii', tmp.name, LC_ALL='C') assert_equal(stderr, b'') stdout = re.sub(br'[\x00\s]+', b' ', stdout) assert_equal(stdout, b' ') with tempfile.NamedTemporaryFile() as tmp: job = document.export_ps(tmp.file, pages=(0,), text=True) assert_equal(type(job), SaveJob) assert_true(job.is_done) assert_false(job.is_error) stdout, stderr = run('ps2ascii', tmp.name, LC_ALL='C') assert_equal(stderr, b'') stdout = stdout.decode('ASCII') stdout = re.sub(r'[\x00\s]+', ' ', stdout) stdout = ' '.join(stdout.split()[:3]) expected = '1 Lorem ipsum' assert_multi_line_equal(stdout, expected) class test_pixel_formats(): def test_bad_new(self): with assert_raises_str(TypeError, "cannot create 'djvu.decode.PixelFormat' instances"): PixelFormat() def test_rgb(self): pf = PixelFormatRgb() assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'RGB', bpp = 24)") pf = PixelFormatRgb('RGB') assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'RGB', bpp = 24)") pf = PixelFormatRgb('BGR') assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'BGR', bpp = 24)") def test_rgb_mask(self): pf = PixelFormatRgbMask(0xFF, 0xF00, 0x1F000, 0, 16) assert_repr(pf, "djvu.decode.PixelFormatRgbMask(red_mask = 0x00ff, green_mask = 0x0f00, blue_mask = 0xf000, xor_value = 0x0000, bpp = 16)") pf = PixelFormatRgbMask(0xFF000000, 0xFF0000, 0xFF00, 0xFF, 32) assert_repr(pf, "djvu.decode.PixelFormatRgbMask(red_mask = 0xff000000, green_mask = 0x00ff0000, blue_mask = 0x0000ff00, xor_value = 0x000000ff, bpp = 32)") def test_grey(self): pf = PixelFormatGrey() assert_repr(pf, "djvu.decode.PixelFormatGrey(bpp = 8)") def test_palette(self): with assert_raises(KeyError) as ecm: pf = PixelFormatPalette({}) assert_equal( ecm.exception.args, ((0, 0, 0),) ) data = dict(((i, j, k), i + 7 * j + 37 + k) for i in range(6) for j in range(6) for k in range(6)) pf = PixelFormatPalette(data) data_repr = ', '.join( '{k!r}: 0x{v:02x}'.format(k=k, v=v) for k, v in sorted(data.items()) ) assert_equal( repr(pf), 'djvu.decode.PixelFormatPalette({{{data}}}, bpp = 8)'.format(data=data_repr) ) def test_packed_bits(self): pf = PixelFormatPackedBits('<') assert_repr(pf, "djvu.decode.PixelFormatPackedBits('<')") assert_equal(pf.bpp, 1) pf = PixelFormatPackedBits('>') assert_repr(pf, "djvu.decode.PixelFormatPackedBits('>')") assert_equal(pf.bpp, 1) class test_page_jobs(): def test_bad_new(self): with assert_raises_str(TypeError, "cannot create 'djvu.decode.PageJob' instances"): PageJob() def test_decode(self): context = Context() document = context.new_document(FileUri(images + 'test1.djvu')) message = document.get_message() assert_equal(type(message), DocInfoMessage) page_job = document.pages[0].decode() assert_true(page_job.is_done) assert_equal(type(page_job), PageJob) assert_true(page_job.is_done) assert_false(page_job.is_error) assert_equal(page_job.status, JobOK) assert_equal(page_job.width, 64) assert_equal(page_job.height, 48) assert_equal(page_job.size, (64, 48)) assert_equal(page_job.dpi, 300) assert_equal(page_job.gamma, 2.2) assert_equal(page_job.version, 24) assert_equal(page_job.type, PAGE_TYPE_BITONAL) assert_equal((page_job.rotation, page_job.initial_rotation), (0, 0)) with assert_raises_str(ValueError, 'rotation must be equal to 0, 90, 180, or 270'): page_job.rotation = 100 page_job.rotation = 180 assert_equal((page_job.rotation, page_job.initial_rotation), (180, 0)) del page_job.rotation assert_equal((page_job.rotation, page_job.initial_rotation), (0, 0)) with assert_raises_str(ValueError, 'page_rect width/height must be a positive integer'): page_job.render(RENDER_COLOR, (0, 0, -1, -1), (0, 0, 10, 10), PixelFormatRgb()) with assert_raises_str(ValueError, 'render_rect width/height must be a positive integer'): page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, -1, -1), PixelFormatRgb()) with assert_raises_str(ValueError, 'render_rect must be inside page_rect'): page_job.render(RENDER_COLOR, (0, 0, 10, 10), (2, 2, 10, 10), PixelFormatRgb()) with assert_raises_str(ValueError, 'row_alignment must be a positive integer'): page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 10, 10), PixelFormatRgb(), -1) with assert_raises_regex(MemoryError, r'\AUnable to allocate [0-9]+ bytes for an image memory\Z'): x = int((sys.maxsize // 2) ** 0.5) page_job.render(RENDER_COLOR, (0, 0, x, x), (0, 0, x, x), PixelFormatRgb(), 8) s = page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1) assert_equal(s, b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEF\xFF\xFF\xFF\xA4\xFF\xFF\xFF\xB8') buffer = array.array('B', b'\0') with assert_raises_str(ValueError, 'Image buffer is too small (16 > 1)'): page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1, buffer) buffer = array.array('B', b'\0' * 16) assert_is(page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1, buffer), buffer) s = array_tobytes(buffer) assert_equal(s, b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEF\xFF\xFF\xFF\xA4\xFF\xFF\xFF\xB8') class test_thumbnails: def test(self): context = Context() document = context.new_document(FileUri(images + 'test1.djvu')) message = document.get_message() assert_equal(type(message), DocInfoMessage) thumbnail = document.pages[0].thumbnail assert_equal(thumbnail.status, JobOK) assert_equal(thumbnail.calculate(), JobOK) message = document.get_message() assert_equal(type(message), ThumbnailMessage) assert_equal(message.thumbnail.page.n, 0) (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), dry_run=True) assert_equal((w, h, r), (5, 3, 5)) assert_is(pixels, None) (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey()) assert_equal((w, h, r), (5, 3, 5)) assert_equal(pixels[:15], b'\xFF\xEB\xA7\xF2\xFF\xFF\xBF\x86\xBE\xFF\xFF\xE7\xD6\xE7\xFF') buffer = array.array('B', b'\0') with assert_raises_str(ValueError, 'Image buffer is too small (25 > 1)'): (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), buffer=buffer) buffer = array.array('B', b'\0' * 25) (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), buffer=buffer) assert_is(pixels, buffer) s = array_tobytes(buffer[:15]) assert_equal(s, b'\xFF\xEB\xA7\xF2\xFF\xFF\xBF\x86\xBE\xFF\xFF\xE7\xD6\xE7\xFF') def test_jobs(): with assert_raises_str(TypeError, "cannot create 'djvu.decode.Job' instances"): Job() with assert_raises_str(TypeError, "cannot create 'djvu.decode.DocumentDecodingJob' instances"): DocumentDecodingJob() class test_affine_transforms(): def test_bad_args(self): with assert_raises_str(ValueError, 'need more than 2 values to unpack'): AffineTransform((1, 2), (3, 4, 5)) def test1(self): af = AffineTransform((0, 0, 10, 10), (17, 42, 42, 100)) assert_equal(type(af), AffineTransform) assert_equal(af((0, 0)), (17, 42)) assert_equal(af((0, 10)), (17, 142)) assert_equal(af((10, 0)), (59, 42)) assert_equal(af((10, 10)), (59, 142)) assert_equal(af((0, 0, 10, 10)), (17, 42, 42, 100)) assert_equal(af(x for x in (0, 0, 10, 10)), (17, 42, 42, 100)) assert_equal(af.apply((123, 456)), af((123, 456))) assert_equal(af.apply((12, 34, 56, 78)), af((12, 34, 56, 78))) assert_equal(af.inverse((17, 42)), (0, 0)) assert_equal(af.inverse((17, 142)), (0, 10)) assert_equal(af.inverse((59, 42)), (10, 0)) assert_equal(af.inverse((59, 142)), (10, 10)) assert_equal(af.inverse((17, 42, 42, 100)), (0, 0, 10, 10)) assert_equal(af.inverse(x for x in (17, 42, 42, 100)), (0, 0, 10, 10)) assert_equal(af.inverse(af((234, 567))), (234, 567)) assert_equal(af.inverse(af((23, 45, 67, 78))), (23, 45, 67, 78)) class test_messages(): def test_bad_new(self): with assert_raises_str(TypeError, "cannot create 'djvu.decode.Message' instances"): Message() class test_streams: def test_bad_new(self): with assert_raises_str(TypeError, "Argument 'document' has incorrect type (expected djvu.decode.Document, got NoneType)"): Stream(None, 42) def test(self): context = Context() document = context.new_document('dummy://dummy.djvu') message = document.get_message() assert_equal(type(message), NewStreamMessage) assert_equal(message.name, 'dummy.djvu') assert_equal(message.uri, 'dummy://dummy.djvu') assert_equal(type(message.stream), Stream) with assert_raises(NotAvailable): document.outline.sexpr with assert_raises(NotAvailable): document.annotations.sexpr with assert_raises(NotAvailable): document.pages[0].text.sexpr with assert_raises(NotAvailable): document.pages[0].annotations.sexpr try: with open(images + 'test1.djvu', 'rb') as fp: message.stream.write(fp.read()) finally: message.stream.close() with assert_raises_str(IOError, 'I/O operation on closed file'): message.stream.write(b'eggs') message = document.get_message() assert_equal(type(message), DocInfoMessage) outline = document.outline outline.wait() x = outline.sexpr assert_equal(x, Expression([])) anno = document.annotations anno.wait() x = anno.sexpr assert_equal(x, Expression([])) text = document.pages[0].text text.wait() x = text.sexpr assert_equal(x, Expression([])) anno = document.pages[0].annotations anno.wait() x = anno.sexpr assert_equal(x, Expression([])) def test_metadata(): model_metadata = { 'English': 'eggs', u('Русский'): u('яйца'), } meta = '\n'.join(u('|{k}| {v}').format(k=k, v=v) for k, v in model_metadata.items()) test_script = u('set-meta\n{meta}\n.\n').format(meta=meta) try: test_file = create_djvu(test_script) except UnicodeEncodeError: raise SkipTest('you need to run this test with LC_CTYPE=C or LC_CTYPE=.UTF-8') try: context = Context() document = context.new_document(FileUri(test_file.name)) message = document.get_message() assert_equal(type(message), DocInfoMessage) annotations = document.annotations assert_equal(type(annotations), DocumentAnnotations) annotations.wait() metadata = annotations.metadata assert_equal(type(metadata), Metadata) assert_equal(len(metadata), len(model_metadata)) assert_equal(sorted(metadata), sorted(model_metadata)) if not py3k: assert_equal(sorted(metadata.iterkeys()), sorted(model_metadata.iterkeys())) assert_equal(sorted(metadata.keys()), sorted(model_metadata.keys())) if not py3k: assert_equal(sorted(metadata.itervalues()), sorted(model_metadata.itervalues())) assert_equal(sorted(metadata.values()), sorted(model_metadata.values())) if not py3k: assert_equal(sorted(metadata.iteritems()), sorted(model_metadata.iteritems())) assert_equal(sorted(metadata.items()), sorted(model_metadata.items())) for k in metadata: assert_equal(type(k), unicode) assert_equal(type(metadata[k]), unicode) for k in None, 42, '+'.join(model_metadata): with assert_raises(KeyError) as ecm: metadata[k] assert_equal(ecm.exception.args, (k,)) finally: test_file.close() class test_sexpr: def test(self): context = Context() document = context.new_document(FileUri(images + 'test0.djvu')) assert_equal(type(document), Document) message = document.get_message() assert_equal(type(message), DocInfoMessage) anno = DocumentAnnotations(document, shared=False) assert_equal(type(anno), DocumentAnnotations) anno.wait() x = anno.sexpr assert_equal(x, Expression([])) anno = document.annotations assert_equal(type(anno), DocumentAnnotations) anno.wait() assert_is(anno.background_color, None) assert_is(anno.horizontal_align, None) assert_is(anno.vertical_align, None) assert_is(anno.mode, None) assert_is(anno.zoom, None) expected_metadata = [ Symbol('metadata'), [Symbol('ModDate'), '2015-08-17 19:54:57+02:00'], [Symbol('CreationDate'), '2015-08-17 19:54:57+02:00'], [Symbol('Producer'), 'pdfTeX-1.40.16'], [Symbol('Creator'), 'LaTeX with hyperref package'], [Symbol('Author'), 'Jakub Wilk'] ] expected_xmp = [ Symbol('xmp'), '' '' '' 'Jakub Wilk' 'image/vnd.djvu' 'pdfTeX-1.40.16' 'LaTeX with hyperref package' '2015-08-17T19:54:57+02:00' '2015-08-17T19:54:57+02:00' '2015-08-17T17:54:58+00:00' '' '\n' ] assert_equal( anno.sexpr, Expression([expected_metadata, expected_xmp]) ) metadata = anno.metadata assert_equal(type(metadata), Metadata) hyperlinks = anno.hyperlinks assert_equal(type(hyperlinks), Hyperlinks) assert_equal(len(hyperlinks), 0) assert_equal(list(hyperlinks), []) outline = document.outline assert_equal(type(outline), DocumentOutline) outline.wait() assert_equal(outline.sexpr, Expression( [Symbol('bookmarks'), ['Lorem ipsum', '#p0001.djvu'], ['Hyperlinks', '#p0002.djvu', ['local', '#p0002.djvu'], ['remote', '#p0002.djvu'] ] ] )) page = document.pages[1] anno = page.annotations assert_equal(type(anno), PageAnnotations) anno.wait() assert_is(anno.background_color, None) assert_is(anno.horizontal_align, None) assert_is(anno.vertical_align, None) assert_is(anno.mode, None) assert_is(anno.zoom, None) expected_hyperlinks = [ [Symbol('maparea'), '#p0001.djvu', '', [Symbol('rect'), 520, 2502, 33, 42], [Symbol('border'), Symbol('#ff0000')]], [Symbol('maparea'), 'http://jwilk.net/', '', [Symbol('rect'), 458, 2253, 516, 49], [Symbol('border'), Symbol('#00ffff')]] ] assert_equal( anno.sexpr, Expression([expected_metadata, expected_xmp] + expected_hyperlinks) ) page_metadata = anno.metadata assert_equal(type(page_metadata), Metadata) assert_equal(page_metadata.keys(), metadata.keys()) assert_equal([page_metadata[k] == metadata[k] for k in metadata], [True, True, True, True, True]) hyperlinks = anno.hyperlinks assert_equal(type(hyperlinks), Hyperlinks) assert_equal(len(hyperlinks), 2) assert_equal( list(hyperlinks), [Expression(h) for h in expected_hyperlinks] ) text = page.text assert_equal(type(text), PageText) text.wait() text_s = text.sexpr text_s_detail = [PageText(page, details).sexpr for details in (TEXT_DETAILS_PAGE, TEXT_DETAILS_COLUMN, TEXT_DETAILS_REGION, TEXT_DETAILS_PARAGRAPH, TEXT_DETAILS_LINE, TEXT_DETAILS_WORD, TEXT_DETAILS_CHARACTER, TEXT_DETAILS_ALL)] assert_equal(text_s_detail[0], text_s_detail[1]) assert_equal(text_s_detail[1], text_s_detail[2]) assert_equal(text_s_detail[2], text_s_detail[3]) assert_equal( text_s_detail[0], Expression( [Symbol('page'), 0, 0, 2550, 3300, '2 Hyperlinks \n' '2.1 local \n' + u('→1 \n') + '2.2 remote \nhttp://jwilk.net/ \n' '2 \n' ] ) ) assert_equal( text_s_detail[4], Expression( [Symbol('page'), 0, 0, 2550, 3300, [Symbol('line'), 462, 2712, 910, 2777, '2 Hyperlinks '], [Symbol('line'), 462, 2599, 714, 2641, '2.1 local '], [Symbol('line'), 464, 2505, 544, 2540, u('→1 ')], [Symbol('line'), 462, 2358, 772, 2400, '2.2 remote '], [Symbol('line'), 463, 2256, 964, 2298, 'http://jwilk.net/ '], [Symbol('line'), 1260, 375, 1282, 409, '2 '] ] ) ) assert_equal(text_s_detail[5], text_s) assert_equal(text_s_detail[6], text_s) assert_equal(text_s_detail[7], text_s) assert_equal( text_s, Expression( [Symbol('page'), 0, 0, 2550, 3300, [Symbol('line'), 462, 2712, 910, 2777, [Symbol('word'), 462, 2727, 495, 2776, '2'], [Symbol('word'), 571, 2712, 910, 2777, 'Hyperlinks']], [Symbol('line'), 462, 2599, 714, 2641, [Symbol('word'), 462, 2599, 532, 2641, '2.1'], [Symbol('word'), 597, 2599, 714, 2640, 'local']], [Symbol('line'), 464, 2505, 544, 2540, [Symbol('word'), 464, 2505, 544, 2540, u('→1')]], [Symbol('line'), 462, 2358, 772, 2400, [Symbol('word'), 462, 2358, 535, 2400, '2.2'], [Symbol('word'), 598, 2358, 772, 2397, 'remote']], [Symbol('line'), 463, 2256, 964, 2298, [Symbol('word'), 463, 2256, 964, 2298, 'http://jwilk.net/']], [Symbol('line'), 1260, 375, 1282, 409, [Symbol('word'), 1260, 375, 1282, 409, '2']] ] ) ) with assert_raises_str(TypeError, 'details must be a symbol or none'): PageText(page, 'eggs') with assert_raises_str(ValueError, 'details must be equal to TEXT_DETAILS_PAGE, or TEXT_DETAILS_COLUMN, or TEXT_DETAILS_REGION, or TEXT_DETAILS_PARAGRAPH, or TEXT_DETAILS_LINE, or TEXT_DETAILS_WORD, or TEXT_DETAILS_CHARACTER or TEXT_DETAILS_ALL'): PageText(page, Symbol('eggs')) def test_version(): assert_is_instance(__version__, str) assert_equal(__version__, get_changelog_version()) assert_is_instance(DDJVU_VERSION, int) def test_wildcard_import(): ns = wildcard_import('djvu.decode') assert_list_equal( sorted(ns.keys()), [ 'AffineTransform', 'Annotations', 'ChunkMessage', 'Context', 'DDJVU_VERSION', 'DOCUMENT_TYPE_BUNDLED', 'DOCUMENT_TYPE_INDIRECT', 'DOCUMENT_TYPE_OLD_BUNDLED', 'DOCUMENT_TYPE_OLD_INDEXED', 'DOCUMENT_TYPE_SINGLE_PAGE', 'DOCUMENT_TYPE_UNKNOWN', 'DocInfoMessage', 'Document', 'DocumentAnnotations', 'DocumentDecodingJob', 'DocumentExtension', 'DocumentFiles', 'DocumentOutline', 'DocumentPages', 'ErrorMessage', 'FILE_TYPE_INCLUDE', 'FILE_TYPE_PAGE', 'FILE_TYPE_THUMBNAILS', 'File', 'FileURI', 'FileUri', 'Hyperlinks', 'InfoMessage', 'Job', 'JobDone', 'JobException', 'JobFailed', 'JobNotDone', 'JobNotStarted', 'JobOK', 'JobStarted', 'JobStopped', 'Message', 'Metadata', 'NewStreamMessage', 'NotAvailable', 'PAGE_TYPE_BITONAL', 'PAGE_TYPE_COMPOUND', 'PAGE_TYPE_PHOTO', 'PAGE_TYPE_UNKNOWN', 'PRINT_BOOKLET_NO', 'PRINT_BOOKLET_RECTO', 'PRINT_BOOKLET_VERSO', 'PRINT_BOOKLET_YES', 'PRINT_ORIENTATION_AUTO', 'PRINT_ORIENTATION_LANDSCAPE', 'PRINT_ORIENTATION_PORTRAIT', 'Page', 'PageAnnotations', 'PageInfoMessage', 'PageJob', 'PageText', 'PixelFormat', 'PixelFormatGrey', 'PixelFormatPackedBits', 'PixelFormatPalette', 'PixelFormatRgb', 'PixelFormatRgbMask', 'ProgressMessage', 'RENDER_BACKGROUND', 'RENDER_BLACK', 'RENDER_COLOR', 'RENDER_COLOR_ONLY', 'RENDER_FOREGROUND', 'RENDER_MASK_ONLY', 'RedisplayMessage', 'RelayoutMessage', 'SaveJob', 'Stream', 'TEXT_DETAILS_ALL', 'TEXT_DETAILS_CHARACTER', 'TEXT_DETAILS_COLUMN', 'TEXT_DETAILS_LINE', 'TEXT_DETAILS_PAGE', 'TEXT_DETAILS_PARAGRAPH', 'TEXT_DETAILS_REGION', 'TEXT_DETAILS_WORD', 'Thumbnail', 'ThumbnailMessage', 'cmp_text_zone' ] ) # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/tests/test_sexpr.py0000644000000000000000000006037013441253560020753 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2007-2019 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. import codecs import copy import errno import io import os import shutil import sys import tempfile if sys.version_info >= (3, 3): import collections.abc as collections_abc else: import collections as collections_abc import pickle try: import cPickle as cpickle except ImportError: cpickle = None from djvu.sexpr import ( Expression, ExpressionSyntaxError, Symbol, _ExpressionIO, __version__, ) from tools import ( SkipTest, assert_equal, assert_false, assert_in, assert_is, assert_is_instance, assert_less, assert_list_equal, assert_not_equal, assert_not_in, assert_raises, assert_raises_str, assert_repr, get_changelog_version, wildcard_import, # Python 2/3 compat: StringIO, b, long, py3k, u, unicode, ) def assert_pickle_equal(obj): for pickle_module in pickle, cpickle: if pickle_module is None: continue for protocol in range(pickle.HIGHEST_PROTOCOL + 1): pickled_obj = pickle_module.dumps(obj, protocol=protocol) repickled_obj = pickle_module.loads(pickled_obj) assert_equal(obj, repickled_obj) class test_int_expressions(): def t(self, n, x=None): if x is None: x = Expression(n) assert_is(x, Expression(x)) # __repr__(): assert_repr(x, 'Expression({n})'.format(n=int(n))) # value: v = x.value assert_equal(type(v), int) assert_equal(v, n) # lvalue: v = x.lvalue assert_equal(type(v), int) assert_equal(v, n) # __int__(): i = int(x) assert_equal(type(i), int) assert_equal(i, n) # __long__(): i = long(x) assert_equal(type(i), long) assert_equal(i, n) # __float__(): i = float(x) assert_equal(type(i), float) assert_equal(i, n) # __str__(): s = str(x) assert_equal(s, str(n)) # __unicode__(): s = unicode(x) assert_equal(s, str(n)) # __eq__(), __ne__(): assert_equal(x, Expression(n)) assert_not_equal(x, n) assert_not_equal(x, Expression(n + 37)) # __hash__(): assert_equal(hash(x), n) # __bool__() / __nonzero__(): obj = object() if n: assert_is(x and obj, obj) assert_is(x or obj, x) else: assert_is(x and obj, x) assert_is(x or obj, obj) # pickle: assert_pickle_equal(x) def test_int(self): self.t(42) def test_parse(self): self.t(42, Expression.from_string('42')) def test_unpickle(self): # pickle as generated by python-djvulibre 0.3.3: p = b"cdjvu.sexpr\n_expression_from_string\np0\n(S'42'\np1\ntp2\nRp3\n." x = pickle.loads(p) self.t(42, x) def test_0(self): self.t(0) def test_long(self): self.t(long(42)) def test_limits(self): assert_equal(Expression((1 << 29) - 1).value, (1 << 29) - 1) assert_equal(Expression(-1 << 29).value, -1 << 29) with assert_raises_str(ValueError, 'value not in range(-2 ** 29, 2 ** 29)'): Expression(1 << 29) with assert_raises_str(ValueError, 'value not in range(-2 ** 29, 2 ** 29)'): Expression((-1 << 29) - 1) class test_float_expressions(): # TODO: float expressions are not implemented yet def test_parse(self): with assert_raises(ExpressionSyntaxError): x = Expression.from_string('3.14') if isinstance(x.value, Symbol): raise ExpressionSyntaxError class test_symbols(): def t(self, name, sname=None): if sname is None: sname = name if py3k: [uname, bname] = [sname, sname.encode('UTF-8')] else: [uname, bname] = [sname.decode('UTF-8'), sname] symbol = Symbol(name) assert_equal(type(symbol), Symbol) assert_equal(symbol, Symbol(name)) assert_is(symbol, Symbol(name)) assert_equal(str(symbol), sname) assert_equal(unicode(symbol), uname) assert_not_equal(symbol, bname) assert_not_equal(symbol, uname) assert_equal(hash(symbol), hash(bname)) assert_pickle_equal(symbol) return symbol def test_ascii(self): self.t('eggs') def test_nonascii(self): x = self.t(b('ветчина'), 'ветчина') y = self.t(u('ветчина'), 'ветчина') assert x is y def test_inequality(self): assert_less( Symbol('eggs'), Symbol('ham'), ) class test_symbol_expressions(): def t(self, name, sname): if sname is None: sname = name if py3k: [uname, bname] = [sname, sname.encode('UTF-8')] else: [uname, bname] = [sname.decode('UTF-8'), sname] sym = Symbol(name) x = Expression(sym) assert_is(x, Expression(x)) # __repr__(x) assert_repr(x, 'Expression({sym!r})'.format(sym=sym)) # value: v = x.value assert_equal(type(v), Symbol) assert_equal(v, sym) # lvalue: v = x.lvalue assert_equal(type(v), Symbol) assert_equal(v, sym) # __str__(): assert_equal(str(x), sname) assert_repr(x, repr(Expression.from_string(sname))) # __unicode__(): assert_equal(unicode(x), uname) assert_repr(x, repr(Expression.from_string(uname))) # __eq__(), __ne__(): assert_equal(x, Expression(sym)) assert_not_equal(x, Expression(name)) assert_not_equal(x, sym) # __hash__(): assert_equal( hash(x), hash(bname.strip(b'|')) ) # pickle: assert_pickle_equal(x) return x def test_ascii(self): self.t('eggs', 'eggs') def test_nonascii(self): x = self.t(b('ветчина'), '|ветчина|') y = self.t(u('ветчина'), '|ветчина|') assert_equal(x, y) assert_equal(hash(x), hash(y)) def test_string_expressions(): x = Expression('eggs') assert_repr(x, "Expression('eggs')") assert_is(x, Expression(x)) assert_equal(x.value, 'eggs') assert_equal(x.lvalue, 'eggs') assert_equal(str(x), '"eggs"') assert_repr(x, repr(Expression.from_string(str(x)))) assert_equal(x, Expression('eggs')) assert_not_equal(x, Expression(Symbol('eggs'))) assert_not_equal(x, 'eggs') assert_equal(hash(x), hash('eggs')) assert_pickle_equal(x) class test_unicode_expressions(): def test1(self): x = Expression(u('eggs')) assert_repr(x, "Expression('eggs')") assert_is(x, Expression(x)) def test2(self): x = Expression(u('żółw')) if py3k: assert_repr(x, "Expression('żółw')") else: assert_repr(x, r"Expression('\xc5\xbc\xc3\xb3\xc5\x82w')") class test_list_expressions(): def test1(self): x = Expression(()) assert_repr(x, "Expression([])") y = Expression(x) assert_is(x, y) assert_equal(x.value, ()) assert_equal(x.lvalue, []) assert_equal(len(x), 0) assert_equal(bool(x), False) assert_equal(list(x), []) def test2(self): x = Expression([[1, 2], 3, [4, 5, Symbol('baz')], ['quux']]) assert_repr(x, "Expression([[1, 2], 3, [4, 5, Symbol('baz')], ['quux']])") y = Expression(x) assert_repr(y, repr(x)) assert_false(x is y) assert_equal(x.value, ((1, 2), 3, (4, 5, Symbol('baz')), ('quux',))) assert_equal(x.lvalue, [[1, 2], 3, [4, 5, Symbol('baz')], ['quux']]) assert_equal(str(x), '((1 2) 3 (4 5 baz) ("quux"))') assert_repr(x, repr(Expression.from_string(str(x)))) assert_equal(len(x), 4) assert_equal(bool(x), True) assert_equal(tuple(x), (Expression((1, 2)), Expression(3), Expression((4, 5, Symbol('baz'))), Expression(('quux',)))) with assert_raises_str(TypeError, 'key must be an integer or a slice'): x[object()] assert_equal(x[1], Expression(3)) assert_equal(x[-1][0], Expression('quux')) with assert_raises_str(IndexError, 'list index of out range'): x[6] with assert_raises_str(IndexError, 'list index of out range'): x[-6] assert_equal(x[:].value, x.value) assert_equal(x[:].lvalue, x.lvalue) assert_repr(x[1:], "Expression([3, [4, 5, Symbol('baz')], ['quux']])") assert_repr(x[-2:], "Expression([[4, 5, Symbol('baz')], ['quux']])") x[-2:] = 4, 5, 6 assert_repr(x, 'Expression([[1, 2], 3, 4, 5, 6])') x[0] = 2 assert_repr(x, 'Expression([2, 3, 4, 5, 6])') x[:] = (1, 3, 5) assert_repr(x, 'Expression([1, 3, 5])') x[3:] = 7, assert_repr(x, 'Expression([1, 3, 5, 7])') with assert_raises_str(NotImplementedError, 'only [n:] slices are supported'): x[object():] with assert_raises_str(NotImplementedError, 'only [n:] slices are supported'): x[:2] with assert_raises_str(NotImplementedError, 'only [n:] slices are supported'): x[object():] = [] with assert_raises_str(NotImplementedError, 'only [n:] slices are supported'): x[:2] = [] with assert_raises_str(TypeError, 'can only assign a list expression'): x[:] = 0 assert_equal(x, Expression((1, 3, 5, 7))) assert_not_equal(x, Expression((2, 4, 6))) assert_not_equal(x, (1, 3, 5, 7)) with assert_raises_str(TypeError, "unhashable type: 'ListExpression'"): hash(x) def test_insert(self): lst = [] expr = Expression(()) for pos in [-8, 4, 6, -5, -7, 5, 7, 2, -3, 8, 10, -2, 1, -9, -10, -4, -6, 0, 9, 3, -1]: lst.insert(pos, pos) assert_is(expr.insert(pos, pos), None) assert_equal(expr, Expression(lst)) assert_equal(expr.lvalue, lst) def test_append(self): expr = Expression(()) for i in range(10): assert_is(expr.append(i), None) assert_equal(expr, Expression(range(i + 1))) assert_equal(expr.lvalue, list(range(i + 1))) def test_extend(self): lst = [] expr = Expression(()) for ext in [1], [], [2, 3]: lst.extend(ext) expr.extend(ext) assert_equal(expr, Expression(lst)) assert_equal(expr.lvalue, lst) with assert_raises_str(TypeError, "'int' object is not iterable"): expr.extend(0) def test_inplace_add(self): lst = [] expr0 = expr = Expression(()) for ext in [], [1], [], [2, 3]: lst += ext expr += ext assert_equal(expr, Expression(lst)) assert_equal(expr.lvalue, lst) assert_is(expr, expr0) with assert_raises_str(TypeError, "'int' object is not iterable"): expr += 0 def test_pop(self): expr = Expression([0, 1, 2, 3, 4, 5, 6]) assert_equal(expr.pop(0), Expression(0)) assert_equal(expr, Expression([1, 2, 3, 4, 5, 6])) with assert_raises_str(IndexError, 'pop index of out range'): expr.pop(6) assert_equal(expr.pop(5), Expression(6)) assert_equal(expr, Expression([1, 2, 3, 4, 5])) assert_equal(expr.pop(-1), Expression(5)) assert_equal(expr, Expression([1, 2, 3, 4])) assert_equal(expr.pop(-2), Expression(3)) assert_equal(expr, Expression([1, 2, 4])) assert_equal(expr.pop(1), Expression(2)) assert_equal(expr, Expression([1, 4])) expr.pop() expr.pop() with assert_raises_str(IndexError, 'pop from empty list'): expr.pop() for i in range(-2, 3): with assert_raises_str(IndexError, 'pop from empty list'): expr.pop(i) def test_delitem(self): expr = Expression([0, 1, 2, 3, 4, 5, 6]) del expr[0] assert_equal(expr, Expression([1, 2, 3, 4, 5, 6])) with assert_raises_str(IndexError, 'pop index of out range'): expr.pop(6) del expr[5] assert_equal(expr, Expression([1, 2, 3, 4, 5])) del expr[-1] assert_equal(expr, Expression([1, 2, 3, 4])) del expr[-2] assert_equal(expr, Expression([1, 2, 4])) del expr[1] assert_equal(expr, Expression([1, 4])) del expr[1:] assert_equal(expr, Expression([1])) del expr[:] assert_equal(expr, Expression([])) for i in range(-2, 3): with assert_raises_str(IndexError, 'pop from empty list'): del expr[i] def test_remove(self): expr = Expression([0, 1, 2, 3, 4, 5, 6]) expr.remove(Expression(0)) assert_equal(expr, Expression([1, 2, 3, 4, 5, 6])) with assert_raises_str(IndexError, 'item not in list'): expr.remove(Expression(0)) expr.remove(Expression(6)) assert_equal(expr, Expression([1, 2, 3, 4, 5])) expr.remove(Expression(5)) assert_equal(expr, Expression([1, 2, 3, 4])) expr.remove(Expression(3)) assert_equal(expr, Expression([1, 2, 4])) expr.remove(Expression(2)) assert_equal(expr, Expression([1, 4])) expr.remove(Expression(4)) expr.remove(Expression(1)) with assert_raises_str(IndexError, 'item not in list'): expr.remove(Expression(-1)) def test_contains(self): expr = Expression(()) assert_not_in(Expression(42), expr) lst = (1, 2, 3) expr = Expression(lst) for x in lst: assert_not_in(x, expr) assert_in(Expression(x), expr) assert_not_in(Expression(max(lst) + 1), expr) def test_index(self): expr = Expression(()) with assert_raises_str(ValueError, 'value not in list'): expr.index(Expression(42)) lst = [1, 2, 3] expr = Expression(lst) for x in lst: i = lst.index(x) j = expr.index(Expression(x)) assert_equal(i, j) with assert_raises_str(ValueError, 'value not in list'): expr.index(Expression(max(lst) + 1)) def test_count(self): lst = [1, 2, 2, 3, 2] expr = Expression(lst) for x in lst + [max(lst) + 1]: i = lst.count(x) j = expr.count(Expression(x)) assert_equal(i, j) def test_reverse(self): for lst in (), (1, 2, 3): expr = Expression(lst) assert_equal( Expression(reversed(expr)), Expression(reversed(lst)) ) assert_equal( Expression(reversed(expr)).value, tuple(reversed(lst)) ) assert_is(expr.reverse(), None) assert_equal( expr, Expression(reversed(lst)) ) assert_equal( expr.value, tuple(reversed(lst)) ) def test_copy1(self): x = Expression([1, [2], 3]) y = Expression(x) x[1][0] = 0 assert_repr(x, 'Expression([1, [0], 3])') assert_repr(y, 'Expression([1, [0], 3])') x[1] = 0 assert_repr(x, 'Expression([1, 0, 3])') assert_repr(y, 'Expression([1, [0], 3])') def test_copy2(self): x = Expression([1, [2], 3]) y = copy.copy(x) x[1][0] = 0 assert_repr(x, 'Expression([1, [0], 3])') assert_repr(y, 'Expression([1, [0], 3])') x[1] = 0 assert_repr(x, 'Expression([1, 0, 3])') assert_repr(y, 'Expression([1, [0], 3])') def test_copy3(self): x = Expression([1, [2], 3]) y = copy.deepcopy(x) x[1][0] = 0 assert_repr(x, 'Expression([1, [0], 3])') assert_repr(y, 'Expression([1, [2], 3])') x[1] = 0 assert_repr(x, 'Expression([1, 0, 3])') assert_repr(y, 'Expression([1, [2], 3])') def test_abc(self): x = Expression(()) assert_is_instance(x, collections_abc.MutableSequence) assert_is_instance(iter(x), collections_abc.Iterator) def test_pickle(self): for lst in (), (1, 2, 3), (1, (2, 3)): x = Expression(lst) assert_pickle_equal(x) class test_expression_parser(): def test_badstring(self): with assert_raises(ExpressionSyntaxError): Expression.from_string('(1') def test_attr_from_file(self): assert_is(getattr(Expression, 'from_file', None), None) def test_bad_io(self): with assert_raises_str(AttributeError, "'int' object has no attribute 'read'"): Expression.from_stream(42) def test_bad_file_io(self): if os.name == 'nt': raise SkipTest('not implemented on Windows') path = '/proc/self/mem' try: os.stat(path) except OSError as exc: raise SkipTest('{exc.filename}: {exc.strerror}'.format(exc=exc)) with open('/proc/self/mem') as fp: with assert_raises(IOError) as ecm: Expression.from_stream(fp) assert_in( ecm.exception.errno, (errno.EIO, errno.EFAULT) ) if py3k: def test_bad_unicode_io(self): fp = StringIO(chr(0xD800)) with assert_raises(UnicodeEncodeError): Expression.from_stream(fp) class test_expression_parser_ascii(): expr = '(eggs) (ham)' repr = ["Expression([Symbol('eggs')])", "Expression([Symbol('ham')])"] def t(self, fp): def read(): return Expression.from_stream(fp) x = read() assert_repr(x, self.repr[0]) x = read() assert_repr(x, self.repr[1]) with assert_raises(ExpressionSyntaxError): x = read() def test_stringio(self): fp = StringIO(self.expr) self.t(fp) def test_bytesio(self): fp = io.BytesIO(b(self.expr)) self.t(fp) def test_file_io_text(self): if py3k: fp = tempfile.TemporaryFile(mode='w+t', encoding='UTF-16-LE') else: fp = tempfile.TemporaryFile(mode='w+t') with fp: fp.write(self.expr) fp.flush() fp.seek(0) self.t(fp) def test_codecs_io(self): tmpdir = tempfile.mkdtemp() try: path = os.path.join(tmpdir, 'tmp') with codecs.open(path, mode='w+', encoding='UTF-16-LE') as fp: fp.write(u(self.expr)) fp.seek(0) self.t(fp) finally: shutil.rmtree(tmpdir) def test_file_io_binary(self): with tempfile.TemporaryFile(mode='w+b') as fp: fp.write(b(self.expr)) fp.flush() fp.seek(0) self.t(fp) class test_expression_parser_nonascii(test_expression_parser_ascii): expr = '"jeż" "żółw"' repr = [r"Expression('jeż')", r"Expression('żółw')"] if not py3k: repr = [s.decode('ISO-8859-1').encode('ASCII', 'backslashreplace') for s in repr] class test_expression_writer(): def test_bad_io(self): expr = Expression(23) with assert_raises_str(AttributeError, "'int' object has no attribute 'write'"): expr.print_into(42) def test_bad_file_io(self): ecm = None path = '/dev/full' try: os.stat(path) except OSError as exc: raise SkipTest('{exc.filename}: {exc.strerror}'.format(exc=exc)) fp = open(path, 'w', buffering=2) expr = Expression(23) try: with assert_raises(IOError) as ecm: for i in range(10000): expr.print_into(fp) finally: try: fp.close() except IOError: if ecm is None: raise assert_equal(ecm.exception.errno, errno.ENOSPC) def test_reentrant(self): if not _ExpressionIO._reentrant: raise SkipTest('this test requires DjVuLibre >= 3.5.26') class File(object): def write(self, s): expr.as_string() expr = Expression(23) fp = File() expr.print_into(fp) def test_escape_unicode_type(self): expr = Expression(23) fp = StringIO() for v in True, False, 1, 0, 'yes', '': expr.print_into(fp, escape_unicode=v) expr.as_string(escape_unicode=v) class test_expression_writer_ascii(): expr = Expression([Symbol('eggs'), Symbol('ham')]) repr = urepr = '(eggs ham)' def test_stringio_7(self): fp = StringIO() self.expr.print_into(fp) assert_equal(fp.getvalue(), self.repr) def test_stringio_8(self): fp = StringIO() self.expr.print_into(fp, escape_unicode=False) assert_equal(fp.getvalue(), self.urepr) def test_bytesio_7(self): fp = io.BytesIO() self.expr.print_into(fp) assert_equal(fp.getvalue(), b(self.repr)) def test_bytesio_8(self): fp = io.BytesIO() self.expr.print_into(fp, escape_unicode=False) assert_equal(fp.getvalue(), b(self.urepr)) def test_file_io_text_7(self): with tempfile.TemporaryFile(mode='w+t') as fp: self.expr.print_into(fp) fp.seek(0) assert_equal(fp.read(), self.repr) def test_file_io_text_8(self): if py3k: fp = tempfile.TemporaryFile(mode='w+t', encoding='UTF-16-LE') else: fp = tempfile.TemporaryFile(mode='w+t') with fp: self.expr.print_into(fp, escape_unicode=False) fp.seek(0) assert_equal(fp.read(), self.urepr) def test_codecs_io_text_7(self): tmpdir = tempfile.mkdtemp() try: path = os.path.join(tmpdir, 'tmp') with codecs.open(path, mode='w+', encoding='UTF-16-LE') as fp: self.expr.print_into(fp) fp.seek(0) assert_equal(fp.read(), self.repr) finally: shutil.rmtree(tmpdir) def test_codecs_io_text_8(self): tmpdir = tempfile.mkdtemp() try: path = os.path.join(tmpdir, 'tmp') with codecs.open(path, mode='w+', encoding='UTF-16-LE') as fp: self.expr.print_into(fp, escape_unicode=False) fp.seek(0) assert_equal(fp.read(), u(self.urepr)) finally: shutil.rmtree(tmpdir) def test_file_io_binary_7(self): with tempfile.TemporaryFile(mode='w+b') as fp: self.expr.print_into(fp) fp.seek(0) assert_equal(fp.read(), b(self.repr)) def test_file_io_binary_8(self): with tempfile.TemporaryFile(mode='w+b') as fp: self.expr.print_into(fp, escape_unicode=False) fp.seek(0) assert_equal(fp.read(), b(self.urepr)) def test_as_string_7(self): s = self.expr.as_string() assert_equal(s, self.repr) def test_as_string_8(self): s = self.expr.as_string(escape_unicode=False) assert_equal(s, self.urepr) class test_expression_writer_nonascii(test_expression_writer_ascii): expr = Expression(u('żółw')) repr = r'"\305\274\303\263\305\202w"' urepr = r'"żółw"' def test_version(): assert_is_instance(__version__, str) assert_equal(__version__, get_changelog_version()) def test_wildcard_import(): ns = wildcard_import('djvu.sexpr') assert_list_equal( sorted(ns.keys()), [ 'Expression', 'ExpressionSyntaxError', 'IntExpression', 'InvalidExpression', 'ListExpression', 'StringExpression', 'Symbol', 'SymbolExpression' ] ) # vim:ts=4 sts=4 sw=4 et python-djvulibre-0.8.4/tests/tools.py0000644000000000000000000001604213347744054017720 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2010-2018 Jakub Wilk # # This file is part of python-djvulibre. # # python-djvulibre is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by # the Free Software Foundation. # # python-djvulibre is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. import codecs import contextlib import distutils.spawn import io import locale import os import re import sys from nose import SkipTest import nose.tools from nose.tools import ( assert_true, assert_false, assert_equal, assert_not_equal, ) def get_changelog_version(): here = os.path.dirname(__file__) path = os.path.join(here, '../doc/changelog') with io.open(path, encoding='UTF-8') as file: line = file.readline() return line.split()[1].strip('()') def noseimport(vmaj, vmin, name=None): def wrapper(f): if f.__module__ == 'unittest.case': return f if sys.version_info >= (vmaj, vmin): return getattr(nose.tools, name or f.__name__) return f return wrapper @noseimport(2, 7) def assert_in(x, y): assert_true( x in y, msg='{0!r} not found in {1!r}'.format(x, y) ) @noseimport(2, 7) def assert_is(x, y): assert_true( x is y, msg='{0!r} is not {1!r}'.format(x, y) ) @noseimport(2, 7) def assert_is_instance(obj, cls): assert_true( isinstance(obj, cls), msg='{0!r} is not an instance of {1!r}'.format(obj, cls) ) @noseimport(2, 7) def assert_less(x, y): assert_true( x < y, msg='{0!r} not less than {1!r}'.format(x, y) ) @noseimport(2, 7) def assert_list_equal(x, y): assert_is_instance(x, list) assert_is_instance(y, list) return assert_equal(x, y) @noseimport(2, 7) def assert_multi_line_equal(x, y): return assert_equal(x, y) if sys.version_info >= (2, 7): type(assert_multi_line_equal.__self__).maxDiff = None @noseimport(2, 7) def assert_not_in(x, y): assert_true( x not in y, msg='{0!r} unexpectedly found in {1!r}'.format(x, y) ) @noseimport(2, 7) class assert_raises(object): def __init__(self, exc_type): self._exc_type = exc_type self.exception = None def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): if exc_type is None: assert_true(False, '{0} not raised'.format(self._exc_type.__name__)) if not issubclass(exc_type, self._exc_type): return False if isinstance(exc_value, exc_type): pass # This branch is not always taken in Python 2.6: # https://bugs.python.org/issue7853 elif isinstance(exc_value, tuple): exc_value = exc_type(*exc_value) else: exc_value = exc_type(exc_value) self.exception = exc_value return True @noseimport(2, 7, 'assert_raises_regexp') @noseimport(3, 2) @contextlib.contextmanager def assert_raises_regex(exc_type, regex): with assert_raises(exc_type) as ecm: yield assert_regex(str(ecm.exception), regex) @noseimport(2, 7, 'assert_regexp_matches') @noseimport(3, 2) def assert_regex(text, regex): if isinstance(regex, (bytes, str, unicode)): regex = re.compile(regex) if not regex.search(text): message = "Regex didn't match: {0!r} not found in {1!r}".format(regex.pattern, text) assert_true(False, msg=message) @contextlib.contextmanager def assert_raises_str(exc_type, s): with assert_raises(exc_type) as ecm: yield assert_equal(str(ecm.exception), s) try: locale.LC_MESSAGES except AttributeError: # A non-POSIX system. locale.LC_MESSAGES = locale.LC_ALL locale_encoding = locale.getpreferredencoding() if codecs.lookup(locale_encoding) == codecs.lookup('US-ASCII'): locale_encoding = 'UTF-8' py3k = sys.version_info >= (3, 0) if py3k: u = str else: def u(s): return s.decode('UTF-8') if py3k: def b(s): return s.encode('UTF-8') else: b = bytes long = type(1 << 999) if py3k: def cmp(x, y): if x == y: return 0 if x < y: return -1 if x > y: return 1 assert False else: cmp = cmp if py3k: from io import StringIO else: from io import BytesIO as StringIO unicode = type(u('')) @contextlib.contextmanager def interim(obj, **override): copy = dict((key, getattr(obj, key)) for key in override) for key, value in override.items(): setattr(obj, key, value) try: yield finally: for key, value in copy.items(): setattr(obj, key, value) @contextlib.contextmanager def interim_locale(**kwargs): old_locale = locale.setlocale(locale.LC_ALL) try: for category, value in kwargs.items(): category = getattr(locale, category) try: locale.setlocale(category, value) except locale.Error as exc: raise SkipTest(exc) yield finally: locale.setlocale(locale.LC_ALL, old_locale) def assert_repr(self, expected): return assert_equal(repr(self), expected) def skip_unless_c_messages(): if locale.setlocale(locale.LC_MESSAGES) not in ('C', 'POSIX'): raise SkipTest('you need to run this test with LC_MESSAGES=C') if os.getenv('LANGUAGE', '') != '': raise SkipTest('you need to run this test with LANGUAGE unset') def skip_unless_translation_exists(lang): messages = {} langs = ['C', lang] for lang in langs: with interim_locale(LC_ALL=lang): try: open(__file__ + '/') except EnvironmentError as exc: messages[lang] = str(exc) messages = set(messages.values()) assert 1 <= len(messages) <= 2 if len(messages) == 1: raise SkipTest('libc translation not found: ' + lang) def skip_unless_command_exists(command): if distutils.spawn.find_executable(command): return raise SkipTest('command not found: ' + command) def wildcard_import(mod): ns = {} exec('from {mod} import *'.format(mod=mod), {}, ns) return ns __all__ = [ # Python 2/3 compat: 'StringIO', 'b', 'cmp', 'long', 'py3k', 'u', 'unicode', # nose 'SkipTest', 'assert_equal', 'assert_false', 'assert_in', 'assert_is', 'assert_is_instance', 'assert_less', 'assert_list_equal', 'assert_multi_line_equal', 'assert_not_equal', 'assert_not_in', 'assert_raises', 'assert_raises_regex', 'assert_regex', 'assert_true', # misc 'assert_raises_str', 'assert_repr', 'get_changelog_version', 'interim', 'interim_locale', 'locale_encoding', 'skip_unless_c_messages', 'skip_unless_command_exists', 'skip_unless_translation_exists', 'wildcard_import', ] # vim:ts=4 sts=4 sw=4 et