didjvu-0.2.7/0000755000000000000000000000000012120174731012741 5ustar rootroot00000000000000didjvu-0.2.7/MANIFEST.in0000644000000000000000000000031511754247214014507 0ustar rootroot00000000000000include COPYING include MANIFEST.in include didjvu include didjvu.py include doc/*.1 include doc/*.txt include doc/*.xml include doc/changelog recursive-include lib *.py recursive-include tests *.py *.xmp didjvu-0.2.7/doc/0000755000000000000000000000000012120174731013506 5ustar rootroot00000000000000didjvu-0.2.7/doc/didjvu.10000644000000000000000000002546112120174731015065 0ustar rootroot00000000000000.\" [created by setup.py sdist] '\" t .\" Title: didjvu .\" Author: Jakub Wilk .\" Generator: DocBook XSL Stylesheets v1.76.1 .\" Date: 03/13/2013 .\" Manual: didjvu manual .\" Source: didjvu 0.2.7 .\" Language: English .\" .TH "DIDJVU" "1" 2013-03-13 "didjvu 0\&.2\&.7" "didjvu manual" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" didjvu \- DjVu encoder with foreground/background separation .SH "SYNOPSIS" .HP \w'\fBdidjvu\ separate\fR\ 'u \fBdidjvu separate\fR [{\fB\-o\fR\ |\ \fB\-\-output\fR}\ \fIoutput\-mask\fR] [\fIoption\fR...] \fIinput\-image\fR .HP \w'\fBdidjvu\ separate\fR\ 'u \fBdidjvu separate\fR \fB\-\-output\-template\fR\ \fIname\-template\fR [\fIoption\fR...] \fIinput\-image\fR... .HP \w'\fBdidjvu\ encode\fR\ 'u \fBdidjvu encode\fR [{\fB\-o\fR\ |\ \fB\-\-output\fR}\ \fIoutput\-djvu\fR] [\fIoption\fR...] \fIinput\-image\fR .HP \w'\fBdidjvu\ encode\fR\ 'u \fBdidjvu encode\fR \fB\-\-output\-template\fR\ \fIname\-template\fR [\fIoption\fR...] \fIinput\-image\fR... .HP \w'\fBdidjvu\ bundle\fR\ 'u \fBdidjvu bundle\fR [{\fB\-o\fR\ |\ \fB\-\-output\fR}\ \fIoutput\-djvu\fR] [\fIoption\fR...] \fIinput\-image\fR... .HP \w'\fBdidjvu\fR\ 'u \fBdidjvu\fR {\fB\-\-version\fR | \fB\-\-help\fR | \fB\-h\fR} .SH "DESCRIPTION" .PP \fBdidjvu\fR uses the \m[blue]\fIGamera\fR\m[]\&\s-2\u[1]\d\s+2 framework to separate foreground/background layers, which can be then encoded into a DjVu file\&. .PP \fBdidjvu separate\fR generates images mask(s) for the supplied input image(s)\&. Masks are saved in the PNG format\&. .PP \fBdidjvu encode\fR converts the supplied input image(s) to single\-page DjVu documents(s)\&. .PP \fBdidjvu bundle\fR converts the supplied input image(s) to a bundled multi\-page DjVu document\&. .SH "OPTIONS" .PP \fBdidjvu\fR accepts the following options: .SS "Input, output" .PP \fB\-o\fR, \fB\-\-output=\fR\fB\fIoutput\-djvu\fR\fR .RS 4 Generate a bundled multi\-page document\&. Write the file into \fIoutput\-djvu\fR file instead of standard output\&. .sp For \fBseparate\fR and \fBencode\fR commands this option is allowed only if there is exactly one input file (i\&.e\&. exactly one output file)\&. .RE .PP \fB\-\-output\-template=\fR\fB\fItemplate\fR\fR .RS 4 Specifies the naming scheme for output files for the \fBseparate\fR and \fBencode\fR commands\&. Please consult the \(lqTEMPLATE LANGUAGE\(rq section for the template language description\&. .sp This option is mandatory if there is more than one input file (i\&.e\&. more than one output file)\&. .RE .PP \fB\-\-pageid\-template=\fR\fB\fItemplate\fR\fR .RS 4 Specifies the naming scheme for page identifiers for the \fBbundle\fR command\&. Please consult the \(lqTEMPLATE LANGUAGE\(rq section for the template language description\&. The default template is \(lq{base\-ext}\&.djvu\(rq\&. .sp For portability reasons, page identifiers: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} must consist only of lowercase ASCII letters, digits, _, +, \- and dot, .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} cannot start with a +, \- or a dot, .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} cannot contain two consecutive dots, .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} must end with the \&.djvu extension\&. .RE .sp .RE .SS "Masks" .PP \fB\-\-loss\-level=\fR\fB\fIn\fR\fR .RS 4 Specifies the aggressiveness of the lossy compression for the \fBseparate\fR and \fBencode\fR commands\&. The default is 0 (lossless)\&. Valid values are integers between 0 and 200, inclusive\&. .sp This option is similar to the \fB\-losslevel\fR option of \fBcjb2\fR; please consult the \fBcjb2\fR(1) manual page for details\&. .RE .PP \fB\-\-lossless\fR .RS 4 Synonym for \fB\-\-loss\-level=0\fR\&. .RE .PP \fB\-\-clean\fR .RS 4 Synonym for \fB\-\-loss\-level=1\fR\&. .RE .PP \fB\-\-lossy\fR .RS 4 Synonym for \fB\-\-loss\-level=100\fR\&. .RE .PP \fB\-m\fR, \fB\-\-method=\fR\fB\fImethod\fR\fR .RS 4 Use the selected method to generate image mask (i\&.e\&. separate foreground from background)\&. The following methods should be available: .PP abutaleb .RS 4 Abutaleb locally\-adaptive thresholding algorithm .RE .PP bernsen .RS 4 Bernsen thresholding algorithm .RE .PP brink .RS 4 Brink and Pendock\*(Aqs minimum\-cross entropy method .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br this method requires Gamera \(>= 3\&.4\&.0 .sp .5v .RE .RE .PP djvu .RS 4 DjVu thresholding algorithm .RE .PP niblack .RS 4 Niblack adaptive thresholding algorithm .RE .PP otsu .RS 4 Otsu thresholding algorithm .RE .PP sauvola .RS 4 Sauvola adaptive thresholding algorithm .RE .PP shading\-subtraction .RS 4 thresholds an image after subtracting a possibly shaded background .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br this method requires Gamera \(>= 3\&.3\&.1 .sp .5v .RE .RE .PP tsai .RS 4 splitting along a threshold value determined using the Tsai Moment Preserving Threshold algorithm .RE .PP white\-rohrer .RS 4 White and Rohrer dynamic thresholding algorithm .RE .sp The default is \(oqdjvu\(cq\&. .sp Please consult the \m[blue]\fIGamera documentation\fR\m[]\&\s-2\u[2]\d\s+2 for details\&. .RE .PP \fB\-p\fR, \fB\-\-pages\-per\-dict=\fR\fB\fIn\fR\fR .RS 4 For \fBbundle\fR command: if \fIn\fR > 1, compress mask using \fBminidjvu\fR using \fIn\fR pages in one pass\&. The default is 1\&. .RE .PP \fB\-\-masks\fR, \fB\-\-mask=\fR\fB\fIinput\-mask\fR\fR .RS 4 Use the pre\-generated image masks for the \fBencode\fR and \fBbundle\fR commands\&. .RE .SS "Foreground/background quality, resolution" .PP (These options apply to \fBencode\fR and \fBbundle\fR commands only\&.) .PP \fB\-d\fR, \fB\-\-dpi=\fR\fB\fIresolution\fR\fR .RS 4 Specifies the desired resolution to \fIresolution\fR dots per inch\&. The default is 300 dpi\&. The allowed range is: 72 \(<= \fIresolution\fR \(<= 6000\&. .RE .PP \fB\-\-fg\-slices=\fR\fB\fIn\fR\fR .RS 4 Specifies the encoding quality of the IW44 foreground layer\&. The default is 100\&. .sp This option is similar to the \fB\-slice\fR option of \fBc44\fR; please consult the \fBc44\fR(1) manual page for details\&. .RE .PP \fB\-\-fg\-crcb=normal\fR .RS 4 Select normal chrominance encoding of the foreground layer\&. .RE .PP \fB\-\-fg\-crcb=half\fR .RS 4 Select half resolution chrominance encoding of the foreground layer\&. .RE .PP \fB\-\-fg\-crcb=full\fR .RS 4 Select the highest possible quality for encoding the chrominance information of the foreground layer\&. This is the default\&. .RE .PP \fB\-\-fg\-crcb=none\fR .RS 4 Disable the encoding of the chrominance of the foreground layer\&. .RE .PP \fB\-\-fg\-subsample=\fR\fB\fIn\fR\fR .RS 4 Specifies the foreground/background subsampling ratio\&. The default is 6\&. Valid values are integers between 1 and 12, inclusive\&. .RE .PP \fB\-\-bg\-slices=\fR\fB\fIn\fR\fR\fB+\fR\fB\fI\&...\fR\fR\fB+\fR\fB\fIn\fR\fR, \fB\-\-bg\-slices=\fR\fB\fIn\fR\fR\fB,\fR\fB\fI\&...\fR\fR\fB,\fR\fB\fIn\fR\fR .RS 4 Specifies the encoding quality of the IW44 background layer\&. The default is 74+10+6+7\&. .sp This option is similar to the \fB\-slice\fR option of \fBc44\fR; please consult the \fBc44\fR(1) manual page for details\&. .RE .PP \fB\-\-bg\-crcb=normal\fR .RS 4 Select normal chrominance encoding of the background layer\&. This is the default\&. .RE .PP \fB\-\-bg\-crcb=half\fR .RS 4 Select half resolution chrominance encoding of the background layer\&. .RE .PP \fB\-\-bg\-crcb=full\fR .RS 4 Select the highest possible quality for encoding the chrominance information of the background layer\&. .RE .PP \fB\-\-bg\-crcb=none\fR .RS 4 Disable the encoding of the chrominance of the background layer\&. .RE .PP \fB\-\-bg\-subsample=\fR\fB\fIn\fR\fR .RS 4 Specifies the foreground/background subsampling ratio\&. The default is 3\&. Valid values are integers between 1 and 12, inclusive\&. .RE .SS "XMP support" .PP (These options apply to \fBencode\fR and \fBbundle\fR commands only\&.) .PP \fB\-\-xmp\fR .RS 4 Create sidecar \m[blue]\fIXMP\fR\m[]\&\s-2\u[3]\d\s+2 metadata\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br This option is experimental! .sp .5v .RE .RE .SS "Verbosity, help" .PP \fB\-v\fR, \fB\-\-verbose\fR .RS 4 Display more informational messages while converting the file\&. .RE .PP \fB\-q\fR, \fB\-\-quiet\fR .RS 4 Don\*(Aqt display informational messages while converting the file\&. .RE .PP \fB\-\-version\fR .RS 4 Output version information and exit\&. .RE .PP \fB\-h\fR, \fB\-\-help\fR .RS 4 Display help and exit\&. .RE .SH "ENVIRONMENT" .PP The following environment variables affects \fBdidjvu\fR: .PP \fITMPDIR\fR .RS 4 \fBdidjvu\fR makes havy use of temporary files\&. It will store them in a directory specified by this variable\&. The default is /tmp\&. .RE .SH "TEMPLATE LANGUAGE" .SS "Template syntax" .PP The template language uses the \m[blue]\fIPython string formatting syntax\fR\m[]\&\s-2\u[4]\d\s+2\&. .SS "Available field names" .PP \fIname\fR .RS 4 Input file path\&. .RE .PP \fIname\-ext\fR .RS 4 Input file path without file extension\&. .RE .PP \fIbase\fR .RS 4 Input file name without directory components\&. .RE .PP \fIbase\-ext\fR .RS 4 Input file name without directory components and without file extension\&. .RE .PP \fIpage\fR, \fIpage+\fR\fI\fIN\fR\fR, \fIpage\-\fR\fI\fIN\fR\fR .RS 4 Page number, optionally offseted by a number \fIN\fR\&. .RE .SH "SEE ALSO" .PP \fBdjvu\fR(1), \fBdjvumake\fR(1), \fBc44\fR(1), \fBcjb2\fR(1), \fBminidjvu\fR(1), .SH "NOTES" .IP " 1." 4 Gamera .RS 4 \m[blue]\fI\%http://gamera.informatik.hsnr.de/\fR\m[] .RE .IP " 2." 4 Gamera documentation .RS 4 \m[blue]\fI\%http://gamera.sourceforge.net/doc/html/binarization.html\fR\m[] .RE .IP " 3." 4 XMP .RS 4 \m[blue]\fI\%http://www.adobe.com/devnet/xmp.html\fR\m[] .RE .IP " 4." 4 Python string formatting syntax .RS 4 \m[blue]\fI\%http://docs.python.org/library/string.html#format-string-syntax\fR\m[] .RE didjvu-0.2.7/doc/didjvu.xml0000644000000000000000000006123012120153065015514 0ustar rootroot00000000000000 ]> &p; manual &p; Jakub Wilk jwilk@jwilk.net &p; 1 &version; &p; DjVu encoder with foreground/background separation &p; separate output-mask option input-image &p; separate name-template option input-image &p; encode output-djvu option input-image &p; encode name-template option input-image &p; bundle output-djvu option input-image &p; Description &p; uses the Gamera framework to separate foreground/background layers, which can be then encoded into a DjVu file. &p; separate generates images mask(s) for the supplied input image(s). Masks are saved in the PNG format. &p; encode converts the supplied input image(s) to single-page DjVu documents(s). &p; bundle converts the supplied input image(s) to a bundled multi-page DjVu document. Options &p; accepts the following options: Input, output Generate a bundled multi-page document. Write the file into output-djvu file instead of standard output. For separate and encode commands this option is allowed only if there is exactly one input file (i.e. exactly one output file). Specifies the naming scheme for output files for the separate and encode commands. Please consult the section for the template language description. This option is mandatory if there is more than one input file (i.e. more than one output file). Specifies the naming scheme for page identifiers for the bundle command. Please consult the section for the template language description. The default template is {base-ext}.djvu. For portability reasons, page identifiers: must consist only of lowercase ASCII letters, digits, _, +, - and dot, cannot start with a +, - or a dot, cannot contain two consecutive dots, must end with the .djvu extension. Masks Specifies the aggressiveness of the lossy compression for the separate and encode commands. The default is 0 (lossless). Valid values are integers between 0 and 200, inclusive. This option is similar to the option of cjb2; please consult the cjb2 1 manual page for details. Synonym for . Synonym for . Synonym for . Use the selected method to generate image mask (i.e. separate foreground from background). The following methods should be available: abutaleb Abutaleb locally-adaptive thresholding algorithm bernsen Bernsen thresholding algorithm brink Brink and Pendock's minimum-cross entropy method this method requires Gamera ≥ 3.4.0 djvu DjVu thresholding algorithm niblack Niblack adaptive thresholding algorithm otsu Otsu thresholding algorithm sauvola Sauvola adaptive thresholding algorithm shading-subtraction thresholds an image after subtracting a possibly shaded background this method requires Gamera ≥ 3.3.1 tsai splitting along a threshold value determined using the Tsai Moment Preserving Threshold algorithm white-rohrer White and Rohrer dynamic thresholding algorithm The default is ‘djvu’. Please consult the Gamera documentation for details. For bundle command: if n > 1, compress mask using minidjvu using n pages in one pass. The default is 1. Use the pre-generated image masks for the encode and bundle commands. Foreground/background quality, resolution (These options apply to encode and bundle commands only.) Specifies the desired resolution to resolution dots per inch. The default is 300 dpi. The allowed range is: 72 ≤ resolution ≤ 6000. Specifies the encoding quality of the IW44 foreground layer. The default is 100. This option is similar to the option of c44; please consult the c44 1 manual page for details. Select normal chrominance encoding of the foreground layer. Select half resolution chrominance encoding of the foreground layer. Select the highest possible quality for encoding the chrominance information of the foreground layer. This is the default. Disable the encoding of the chrominance of the foreground layer. Specifies the foreground/background subsampling ratio. The default is 6. Valid values are integers between 1 and 12, inclusive. Specifies the encoding quality of the IW44 background layer. The default is 74+10+6+7. This option is similar to the option of c44; please consult the c44 1 manual page for details. Select normal chrominance encoding of the background layer. This is the default. Select half resolution chrominance encoding of the background layer. Select the highest possible quality for encoding the chrominance information of the background layer. Disable the encoding of the chrominance of the background layer. Specifies the foreground/background subsampling ratio. The default is 3. Valid values are integers between 1 and 12, inclusive. XMP support (These options apply to encode and bundle commands only.) Create sidecar XMP metadata. This option is experimental! Verbosity, help Display more informational messages while converting the file. Don't display informational messages while converting the file. Output version information and exit. Display help and exit. Environment The following environment variables affects &p;: TMPDIR &p; makes havy use of temporary files. It will store them in a directory specified by this variable. The default is /tmp. Template language Template syntax The template language uses the Python string formatting syntax. Available field names name Input file path. name-ext Input file path without file extension. base Input file name without directory components. base-ext Input file name without directory components and without file extension. page page+N page-N Page number, optionally offseted by a number N. See also djvu 1 , djvumake 1 , c44 1 , cjb2 1 , minidjvu 1 , didjvu-0.2.7/doc/credits.txt0000644000000000000000000000025411754247211015713 0ustar rootroot00000000000000This software has been developed for the Polish Ministry of Science and Higher Education's grant no. N N519 384036 (May 2009 - May 2012, https://bitbucket.org/jsbien/ndt). didjvu-0.2.7/doc/todo.txt0000644000000000000000000000177711754247211015236 0ustar rootroot00000000000000Things to do ============ * Make sure that very small images are subsampled correctly. See the DjVuLibre bug report for details: http://sf.net/tracker/?func=detail&aid=1972089&group_id=32953&atid=406583 for details. * Allow running *didjvu* in parallel (``-j``/``--jobs``). * Allow specifying page titles (``--page-title-template``). * Allow customizing gamma value (``--gamma``). * Allow using other than PIL imaging libraries (e.g. GraphicsMagick, ExactImage). Or not to use any (apart from Gamera) library at all. * Optimize the ``subsample_fg()`` function, which is called if non-standard image quality options are used. * Allow running ``didjvu encode`` and ``didjvu separate`` on a single-page DjVu files. * Allow running ``didjvu bundle`` on DjVu files. * Make sure that ``--pageid-template`` never provokes page-id conflicts. * Make ``didjvu bundle`` display correct “bits-per-pixel” information. * Add support for ``--fg-slices 0``. .. vim:ft=rst ts=3 sw=3 et tw=72 didjvu-0.2.7/doc/dependencies.txt0000644000000000000000000000202011754247211016675 0ustar rootroot00000000000000Dependencies ============ * Python_ ≥ 2.6 * Gamera_ ≥ 3.2.3 — toolkit for building document image recognition systems * Python Imaging Library (PIL_) * DjVuLibre_ ≥ 3.5.21 — library for the DjVu_ file format * minidjvu_ ≥ 0.8 (needed for the ``--pages-per-dict``/``-p`` option) — bitonal DjVu_ encoder/decoder * argparse_ — Python command line parser For XMP support one of the following libraries is needed: * python-xmp-toolkit_ - Python XMP library * pyexiv2_ ≥ 0.3 - Python bindings to Exiv2_ .. _Python: http://python.org/ .. _Gamera: http://gamera.informatik.hsnr.de/ .. _PIL: http://www.pythonware.com/products/pil/ .. _DjVuLibre: http://djvu.sourceforge.net/ .. _minidjvu: http://minidjvu.sourceforge.net/ .. _DjVu: http://djvu.org/ .. _argparse: http://code.google.com/p/argparse/ .. _python-xmp-toolkit: http://code.google.com/p/python-xmp-toolkit/ .. _pyexiv2: http://tilloy.net/dev/pyexiv2/ .. _Exiv2: http://www.exiv2.org/ .. vim:ft=rst ts=3 sw=3 et tw=72 didjvu-0.2.7/doc/changelog0000644000000000000000000000734312120174461015367 0ustar rootroot00000000000000didjvu (0.2.7) unstable; urgency=low * Improve the manual page, as per man-pages(7) recommendations: - Remove the “AUTHOR” sections. * Make “setup.py clean -a” remove compiled manual page (unless it was built by “setup.py sdist”). * Check Python version at runtime, not only in setup.py. * Add support for the Brink and Pendock's minimum-cross entropy method. * Fix the ‘bundle’ command when used without the -o/--output option. -- Jakub Wilk Wed, 13 Mar 2013 23:09:52 +0100 didjvu (0.2.6) unstable; urgency=low * Fix compatibility with PIL ≥ 1.2. * Fix a copy&paste error in the manual page. Thanks to Janusz S. Bień for the bug report. * Fix the ‘separate’ subcommand. Thanks to Janusz S. Bień for the bug report. -- Jakub Wilk Tue, 15 May 2012 11:57:24 +0200 didjvu (0.2.5) unstable; urgency=low * Use RFC 3339 date format in the manual page. Don't call external programs to build it. -- Jakub Wilk Thu, 12 Apr 2012 12:24:50 +0200 didjvu (0.2.4) unstable; urgency=low * Make ‘didjvu --help’ print defaults. * Improve XMP support: + Store more information about conversion options (loss-level, C44 encoding options). + Rename ‘ncc’ to ‘n-connected-components’ for clarity. + Don't convert timestamps to UTC, as recommended by the XMP specification. + Use pyexiv2 as XMP backend if python-xmp-toolkit is not available. + Add XMP support to the ‘bundle’ command. -- Jakub Wilk Tue, 13 Mar 2012 21:12:22 +0100 didjvu (0.2.3) unstable; urgency=low * Produce more helpful error message when an external command is not found or a third-party module cannot be imported. * Add experimental support for XMP metadata. -- Jakub Wilk Wed, 01 Feb 2012 20:16:18 +0100 didjvu (0.2.2) unstable; urgency=low * Make ‘didjvu --help’ print also Gamera version number. * Try loading images with Gamera, and only fall back to Python Imaging Library. This should be both faster and allow to load wider variety of TIFF images. * Check Python version in setup.py. * Let the setup.py script build the manual page. * Disable the Gatos et. al thresholding algorithm for the time being, as it didn't work anyway. * Optimize Gamera → PIL conversions. * Prevent Numpy from being imported by Gamera (loading it takes noticeable amount of time, even though it's never needed). * Remove LANGUAGE variable from environment when calling external processes. * Correct error message about writing binary data to a terminal. * Add support for the “shading subtraction” algorithm. * Allow (and prefer) hyphens instead of underscores in method names. -- Jakub Wilk Mon, 23 Jan 2012 00:10:37 +0100 didjvu (0.2.1) unstable; urgency=low * Add the manual page. * Allow basic functionality of didjvu even with Python 2.5. * Implement a work-around for memory leak in the Gamera library. * Add support for reading palette-based images. Thanks to Janusz S. Bień for the bug report. * Make -m/--method work again. * Fix -p/--pages-per-dict. -- Jakub Wilk Fri, 29 Oct 2010 01:05:03 +0200 didjvu (0.2) unstable; urgency=low * Python ≥ 2.6 in now required. * Implement a new command-line interface. Three subcommands are available: + separate — generate masks for images; + encode — convert images to single-page DjVu documents; + bundle — convert images to bundled multi-page DjVu document. -- Jakub Wilk Wed, 08 Sep 2010 00:10:17 +0200 didjvu (0.1) unstable; urgency=low * Initial release. -- Jakub Wilk Fri, 18 Dec 2009 22:25:14 +0100 didjvu-0.2.7/didjvu0000755000000000000000000000027511512314047014160 0ustar rootroot00000000000000#!/usr/bin/python # encoding=UTF-8 if __name__ != '__main__': raise ImportError('This module is not intended for import') from didjvu import didjvu didjvu.main() # vim:ts=4 sw=4 et didjvu-0.2.7/lib/0000755000000000000000000000000012120174731013507 5ustar rootroot00000000000000didjvu-0.2.7/lib/templates.py0000644000000000000000000001101411754247204016064 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2010, 2011, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''filename templates''' import re import os import sys try: from string import Formatter except ImportError: # This is very rough, incomplete backport of string.Formatter class. # It's provided here to allow basic functionality of didjvu even with # Python 2.5. import warnings warnings.warn(RuntimeWarning('Python %d.%d is not supported, please use Python >= 2.6' % sys.version_info[:2]), stacklevel=999) class Formatter(): _split = re.compile(''' ( [{][{] | [}][}] | [{][^}]*[}] ) ''', re.VERBOSE).split @staticmethod def _not_implemented(): raise NotImplementedError('Please use Python >= 2.6') def format(self, format_string, *args, **kwargs): return self.vformat(format_string, args, kwargs) def vformat(self, format_string, args, kwargs): result = [] for literal_text, field_name, format_spec, conversion in self.parse(format_string): if conversion is not None: self._not_implemented() if literal_text: result += [literal_text] if field_name is not None: obj, _ = self.get_field(field_name, args, kwargs) obj = self.convert_field(obj, conversion) result += [self.format_field(obj, format_spec)] return ''.join(result) def convert_field(self, value, conversion): if conversion is None: return value else: self._not_implemented() def get_field(self, field_name, args, kwargs): return self.get_value(field_name, args, kwargs), field_name def format_field(self, value, format_spec): if format_spec is not None: self._not_implemented() return str(value) def parse(self, format_string): for token in self._split(format_string): if token in ('{{', '}}'): yield token[0], None, None, None elif token[:1] + token[-1:] == '{}': if (':' in token) or ('!' in token) or ('.' in token): self._not_implemented() yield '', token[1:-1], None, None elif token: yield token, None, None, None def get_value(self, key, args, kwargs): if isinstance(key, (int, long)): return args[key] else: return kwargs[key] formatter = Formatter() del Formatter def expand(template, name, page): ''' >>> path = '/path/to/eggs.png' >>> expand('{name}', path, 0) '/path/to/eggs.png' >>> expand('{base}', path, 0) 'eggs.png' >>> expand('{name-ext}.djvu', path, 0) '/path/to/eggs.djvu' >>> expand('{base-ext}.djvu', path, 0) 'eggs.djvu' >>> expand('{page}', path, 0) '1' >>> expand('{page:04}', path, 0) '0001' >>> expand('{page}', path, 42) '43' >>> expand('{page+26}', path, 42) '69' >>> expand('{page-26}', path, 42) '17' ''' base = os.path.basename(name) name_ext, _ = os.path.splitext(name) base_ext, _ = os.path.splitext(base) d = { 'name': name, 'name-ext': name_ext, 'base': base, 'base-ext': base_ext, 'page': page + 1, } for _, var, _, _ in formatter.parse(template): if var is None: continue if '+' in var: sign = +1 base_var, offset = var.split('+') elif '-' in var: sign = -1 base_var, offset = var.split('-') else: continue try: offset = sign * int(offset, 10) except ValueError: continue try: base_value = d[base_var] except LookupError: continue if not isinstance(base_value, int): continue d[var] = d[base_var] + offset return formatter.vformat(template, (), d) # vim:ts=4 sw=4 et didjvu-0.2.7/lib/temporary.py0000644000000000000000000000214611754247204016116 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2009, 2010, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''temporary files and directories''' import contextlib import functools import shutil import tempfile file = functools.partial(tempfile.NamedTemporaryFile, prefix='didjvu.') name = functools.partial(tempfile.mktemp, prefix='didjvu.') wrapper = tempfile._TemporaryFileWrapper @contextlib.contextmanager def directory(*args, **kwargs): kwargs = dict(kwargs) kwargs.setdefault('prefix', 'didjvu.') tmpdir = tempfile.mkdtemp(*args, **kwargs) try: yield tmpdir finally: shutil.rmtree(tmpdir) __all__ = ['file', 'directory', 'name', 'wrapper'] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/utils.py0000644000000000000000000000165512120173774015236 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2011, 2012, 2013 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''various helper functions''' import os debian = os.path.exists('/etc/debian_version') def enhance_import_error(exception, package, debian_package, homepage): message = str(exception) format = '%(message)s; please install the %(package)s package' if debian: package = debian_package else: format += ' <%(homepage)s>' exception.args = [format % locals()] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/version.py0000644000000000000000000000152211767717721015570 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2011, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. __version__ = '0.2.7' def get_software_agent(): try: import gamera except ImportError: gamera = None result = 'didjvu ' + __version__ try: result += ' (Gamera %s)' % gamera.__version__ except (AttributeError, TypeError, ValueError): pass return result # vim:ts=4 sw=4 et didjvu-0.2.7/lib/filetype.py0000644000000000000000000000207411712266211015706 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2010 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. from __future__ import with_statement class generic(object): def __new__(self, *args, **kwargs): raise NotImplementedError @classmethod def like(cls, other): return issubclass(cls, other) class djvu(generic): pass class djvu_single(djvu): pass def check(filename): cls = generic with open(filename, 'rb') as file: header = file.read(16) if header.startswith('AT&TFORM'): cls = djvu if header.endswith('DJVU'): cls = djvu_single return cls # vim:ts=4 sw=4 et didjvu-0.2.7/lib/cli.py0000644000000000000000000002431111754252752014645 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2009, 2010, 2011, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''command line interface for *didjvu*''' from . import utils try: import argparse except ImportError, ex: utils.enhance_import_error(ex, 'argparse', 'python-argparse', 'http://code.google.com/p/argparse/') raise from . import djvu_extra as djvu from . import version as version_module try: from . import xmp except ImportError, xmp_import_error: xmp = None def range_int(x, y, typename): class rint(int): def __new__(cls, n): n = int(n) if not (x <= n <= y): raise ValueError return n return type(typename, (rint,), {}) dpi_type = range_int(djvu.DPI_MIN, djvu.DPI_MAX, 'dpi') losslevel_type = range_int(djvu.LOSS_LEVEL_MIN, djvu.LOSS_LEVEL_MAX, 'loss level') subsample_type = range_int(djvu.SUBSAMPLE_MIN, djvu.SUBSAMPLE_MAX, 'subsample') def slice_type(max_slices=99): def slices(value): if ',' in value: result = map(int, value.split(',')) elif '+' in value: result = [] accum = 0 for slice in value.split('+'): accum += int(slice) result += accum, else: result = [int(value)] if not result: raise ValueError('invalid slice specification') if len(result) > max_slices: raise ValueError('too many slices') return result return slices def get_slice_repr(lst): def fold(lst, obj): return lst + [obj - sum(lst)] plus_lst = reduce(fold, lst[1:], lst[:1]) return '+'.join(map(str, plus_lst)) class intact(object): def __init__(self, x): self.x = x def __call__(self): return self.x def replace_underscores(s): return s.replace('_', '-') class ArgumentParser(argparse.ArgumentParser): class defaults: pageid_template = '{base-ext}.djvu' pages_per_dict = 1 dpi = djvu.DPI_DEFAULT fg_slices = [100] fg_crcb = djvu.CRCB.full fg_subsample = 6 bg_slices = [74, 84, 90, 97] bg_crcb = djvu.CRCB.normal bg_subsample = 3 def __init__(self, methods, default_method): argparse.ArgumentParser.__init__(self, formatter_class=argparse.RawDescriptionHelpFormatter) version = version_module.get_software_agent() self.add_argument('--version', action='version', version=version, help='show version information and exit') p_separate = self.add_subparser('separate', help='generate masks for images') p_encode = self.add_subparser('encode', help='convert images to single-page DjVu documents') p_bundle = self.add_subparser('bundle', help='convert images to bundled multi-page DjVu document') epilog = [] default = self.defaults for p in p_separate, p_encode, p_bundle: epilog += ['%s --help' % p.prog] p.add_argument('-o', '--output', metavar='FILE', help='output filename') if p is p_bundle: p.add_argument( '--pageid-template', metavar='TEMPLATE', default=default.pageid_template, help='naming scheme for page identifiers (default: "%s")' % default.pageid_template ) else: p.add_argument( '--output-template', metavar='TEMPLATE', help='naming scheme for output file (e.g. "%s")' % default.pageid_template ) p.add_argument('--losslevel', dest='loss_level', type=losslevel_type, help=argparse.SUPPRESS) p.add_argument('--loss-level', dest='loss_level', type=losslevel_type, metavar='N', help='aggressiveness of lossy compression') p.add_argument('--lossless', dest='loss_level', action='store_const', const=djvu.LOSS_LEVEL_MIN, help='lossless compression (default)') p.add_argument('--clean', dest='loss_level', action='store_const', const=djvu.LOSS_LEVEL_CLEAN, help='lossy compression: remove flyspecks') p.add_argument('--lossy', dest='loss_level', action='store_const', const=djvu.LOSS_LEVEL_LOSSY, help='lossy compression: substitute patterns with small variations') if p is not p_separate: p.add_argument('--masks', nargs='+', metavar='MASK', help='use pre-generated masks') p.add_argument('--mask', action='append', dest='masks', metavar='MASK', help='use a pre-generated mask') for layer, layer_name in ('fg', 'foreground'), ('bg', 'background'): if layer == 'fg': p.add_argument( '--fg-slices', type=slice_type(1), metavar='N', help='number of slices for background (default: %s)' % get_slice_repr(default.fg_slices) ) else: p.add_argument( '--bg-slices', type=slice_type(), metavar='N+...+N', help='number of slices in each forgeground chunk (default: %s)' % get_slice_repr(default.bg_slices) ) default_crcb = getattr(default, '%s_crcb' % layer) p.add_argument( '--%s-crcb' % layer, choices=map(str, djvu.CRCB.values), help='chrominance encoding for %s (default: %s)' % (layer_name, default_crcb) ) default_subsample = getattr(default, '%s_subsample' % layer) p.add_argument( '--%s-subsample' % layer, type=subsample_type, metavar='N', help='subsample ratio for %s (default: %d)' % (layer_name, default_subsample) ) p.add_argument('--fg-bg-defaults', help=argparse.SUPPRESS, action='store_const', const=1) if p is not p_separate: p.add_argument( '-d', '--dpi', type=dpi_type, metavar='N', help='image resolution (default: %d)' % default.dpi ) if p is p_bundle: p.add_argument( '-p', '--pages-per-dict', type=int, metavar='N', help='how many pages to compress in one pass (default: %d)' % default.pages_per_dict ) p.add_argument( '-m', '--method', choices=methods, type=replace_underscores, default=default_method, help='binarization method (default: %s)' % default_method ) if p is p_encode or p is p_bundle: p.add_argument('--xmp', action='store_true', help='create sidecar XMP metadata (experimental!)') p.add_argument('-v', '--verbose', dest='verbosity', action='append_const', const=None, help='more informational messages') p.add_argument('-q', '--quiet', dest='verbosity', action='store_const', const=[], help='no informational messages') p.add_argument('input', metavar='', nargs='+') p.set_defaults( masks=[], fg_bg_defaults=None, loss_level=djvu.LOSS_LEVEL_MIN, pages_per_dict=default.pages_per_dict, dpi=default.dpi, fg_slices=intact(default.fg_slices), fg_crcb=intact(default.fg_crcb), fg_subsample=intact(default.fg_subsample), bg_slices=intact(default.bg_slices), bg_crcb=intact(default.bg_crcb), bg_subsample=intact(default.bg_subsample), verbosity=[None], xmp=False, ) self.epilog = 'more help:\n ' + '\n '.join(epilog) self.__methods = methods def add_subparser(self, name, **kwargs): try: self.__subparsers except AttributeError: self.__subparsers = self.add_subparsers(parser_class=argparse.ArgumentParser) p = self.__subparsers.add_parser(name, **kwargs) p.set_defaults(_action_=name) return p def parse_args(self, actions): o = argparse.ArgumentParser.parse_args(self) if o.fg_bg_defaults is None: for layer in 'fg', 'bg': namespace = argparse.Namespace() setattr(o, '%s_options' % layer, namespace) for facet in 'slices', 'crcb', 'subsample': attrname = '%s_%s' % (layer, facet) value = getattr(o, attrname) if isinstance(value, intact): value = value() else: o.fg_bg_defaults = False setattr(namespace, facet, value) delattr(o, attrname) if isinstance(namespace.crcb, str): namespace.crcb = getattr(djvu.CRCB, namespace.crcb) if o.fg_bg_defaults is not False: o.fg_bg_defaults = True o.verbosity = len(o.verbosity) if o.pages_per_dict <= 1: o.pages_per_dict = 1 action = getattr(actions, vars(o).pop('_action_')) o.method = self.__methods[o.method] try: if not xmp and o.xmp: raise xmp_import_error except AttributeError: pass return action(o) def dump_options(o, multipage=False): yield ('method', o.method.didjvu_name) if multipage: yield ('pages-per-dict', o.pages_per_dict) yield ('loss-level', o.loss_level) if o.fg_bg_defaults: yield ('fg-bg-defaults', True) else: for layer_name in 'fg', 'bg': layer = getattr(o, layer_name + '_options') yield (layer_name + '-crcb', str(layer.crcb)) yield (layer_name + '-slices', get_slice_repr(layer.slices)) yield (layer_name + '-subsample', layer.subsample) __all__ = ['ArgumentParser', 'dump_options'] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/ipc.py0000644000000000000000000001077711754247204014660 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2008, 2009, 2010, 2011, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''interprocess communication''' import logging import os import re import signal import subprocess # CalledProcessError, CalledProcessInterrupted # ============================================ # Protect from scanadf[0] and possibly other brain-dead software that set # SIGCHLD to SIG_IGN. # [0] http://bugs.debian.org/596232 if os.name == 'posix': signal.signal(signal.SIGCHLD, signal.SIG_DFL) def get_signal_names(): _signame_pattern = re.compile('^SIG[A-Z0-9]*$') data = dict( (name, getattr(signal, name)) for name in dir(signal) if _signame_pattern.match(name) ) try: if data['SIGABRT'] == data['SIGIOT']: del data['SIGIOT'] except KeyError: pass try: if data['SIGCHLD'] == data['SIGCLD']: del data['SIGCLD'] except KeyError: pass return dict((no, name) for name, no in data.iteritems()) CalledProcessError = subprocess.CalledProcessError class CalledProcessInterrupted(CalledProcessError): _signal_names = get_signal_names() def __init__(self, signal_id, command): Exception.__init__(self, command, signal_id) def __str__(self): signal_name = self._signal_names.get(self.args[1], self.args[1]) return 'Command %r was interrupted by signal %s' % (self.args[0], signal_name) del get_signal_names # Subprocess # ========== def shell_escape(s, safe=re.compile('^[a-zA-Z0-9_+/=.,:%-]+$').match): if safe(s): return s return "'%s'" % s.replace("'", r"'\''") def shell_escape_list(lst): return ' '.join(map(shell_escape, lst)) class Subprocess(subprocess.Popen): @classmethod def override_env(cls, override): env = os.environ # We'd like to: # - preserve LC_CTYPE (which is required by some DjVuLibre tools), # - but reset all other locale settings (which tend to break things). lc_ctype = env.get('LC_ALL') or env.get('LC_CTYPE') or env.get('LANG') env = dict( (k, v) for k, v in env.iteritems() if not (k.startswith('LC_') or k in ('LANG', 'LANGUAGE')) ) if lc_ctype: env['LC_CTYPE'] = lc_ctype if override: env.update(override) return env def __init__(self, *args, **kwargs): kwargs['env'] = self.override_env(kwargs.get('env')) if os.name == 'posix': kwargs.update(close_fds=True) try: commandline = kwargs['args'] except KeyError: commandline = args[0] if logger.isEnabledFor(logging.DEBUG): logger.debug(shell_escape_list(commandline)) self.__command = commandline[0] try: subprocess.Popen.__init__(self, *args, **kwargs) except EnvironmentError, ex: ex.filename = self.__command raise def wait(self): return_code = subprocess.Popen.wait(self) if return_code > 0: raise CalledProcessError(return_code, self.__command) if return_code < 0: raise CalledProcessInterrupted(-return_code, self.__command) # PIPE # ==== PIPE = subprocess.PIPE # Proxy # ===== class Proxy(object): def __init__(self, object, wait_fn, temporaries): self._object = object self._wait_fn = wait_fn self._temporaries = temporaries def __getattribute__(self, name): if name.startswith('_'): return object.__getattribute__(self, name) self._wait_fn() self._wait_fn = int return getattr(self._object, name) def __setattr__(self, name, value): if name.startswith('_'): return object.__setattr__(self, name, value) self._wait_fn() self._wait_fn = int setattr(self._object, name, value) # logging support # =============== logger = logging.getLogger('didjvu.ipc') # __all__ # ======= __all__ = [ 'CalledProcessError', 'CalledProcessInterrupted', 'Subprocess', 'PIPE', 'Proxy', ] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/didjvu.py0000644000000000000000000004354712120174044015360 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2009, 2010, 2011, 2012, 2013 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. from __future__ import with_statement import itertools import os import re import logging import sys from . import cli from . import djvu_extra as djvu from . import filetype from . import gamera_extra as gamera from . import ipc from . import templates from . import temporary try: from . import xmp except ImportError: xmp = None logger = None def setup_logging(): global logger logger = logging.getLogger('didjvu.main') ipc_logger = logging.getLogger('didjvu.ipc') logging.NOSY = (logging.INFO + logging.DEBUG) // 2 def nosy(msg, *args, **kwargs): logger.log(logging.NOSY, msg, *args, **kwargs) logger.nosy = nosy # Main handler: handler = logging.StreamHandler() formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) logger.addHandler(handler) # IPC handler: handler = logging.StreamHandler() formatter = logging.Formatter('+ %(message)s') handler.setFormatter(formatter) ipc_logger.addHandler(handler) def parallel_for(o, f, *iterables): for args in zip(*iterables): f(o, *args) def check_tty(): if sys.stdout.isatty(): print >>sys.stderr, 'I won\'t write binary data to a terminal.' sys.exit(1) def copy_file(input_file, output_file): length = 0 while 1: block = input_file.read(0x1000) if not block: break length += len(block) output_file.write(block) return length def get_subsampled_dim(image, subsample): width = (image.ncols + subsample - 1) // subsample height = (image.nrows + subsample - 1) // subsample return gamera.Dim(width, height) def subsample_fg(image, mask, options): # TODO: Optimize, perhaps using Cython. ratio = options.subsample subsampled_size = get_subsampled_dim(mask, ratio) mask = mask.to_greyscale() mask = mask.threshold(254) mask = mask.erode() subsampled_image = gamera.Image((0, 0), subsampled_size, pixel_type=gamera.RGB) subsampled_mask = gamera.Image((0, 0), subsampled_size, pixel_type=gamera.ONEBIT) y0 = 0 width, height = image.ncols, image.nrows image = image.to_rgb() image_get = image.get mask_get = mask.get subsampled_image_set = subsampled_image.set subsampled_mask_set = subsampled_mask.set for sy in xrange(0, subsampled_image.nrows): x0 = 0 for sx in xrange(0, subsampled_image.ncols): n = r = g = b = 0 y = y0 for dy in xrange(ratio): if y >= height: break x = x0 for dx in xrange(ratio): if x >= width: break pt = (x, y) if mask_get(pt): n += 1 pixel = image_get(pt) r += pixel.red g += pixel.green b += pixel.blue x += 1 y += 1 if n > 0: r = (r + n // 2) // n g = (g + n // 2) // n b = (b + n // 2) // n subsampled_image_set((sx, sy), gamera.RGBPixel(r, g , b)) else: subsampled_mask_set((sx, sy), 1) x0 += ratio y0 += ratio return subsampled_image, subsampled_mask def subsample_bg(image, mask, options): dim = get_subsampled_dim(mask, options.subsample) mask = mask.to_greyscale() mask = mask.resize(dim, 0) mask = mask.dilate().dilate() mask = mask.threshold(254) image = image.resize(dim, 1) return image, mask def make_layer(image, mask, subsampler, options): image, mask = subsampler(image, mask, options) return djvu.photo_to_djvu( image=gamera.to_pil_rgb(image), mask_image=gamera.to_pil_1bpp(mask), slices=options.slices, crcb=options.crcb ) def image_to_djvu(width, height, image, mask, options): dpi = options.dpi loss_level = options.loss_level if options.pages_per_dict > 1: # XXX This should probably go the other way round: we run minidjvu # first, and then reuse its masks. loss_level = 0 sjbz = djvu.bitonal_to_djvu(gamera.to_pil_1bpp(mask), loss_level=loss_level) if options.fg_bg_defaults: image = gamera.to_pil_rgb(image) return djvu.Multichunk(width, height, dpi, image=image, sjbz=sjbz) else: fg_djvu = make_layer(image, mask, subsample_fg, options.fg_options) bg_djvu = make_layer(image, mask, subsample_bg, options.bg_options) fg44, bg44 = map(djvu.djvu_to_iw44, [fg_djvu, bg_djvu]) return djvu.Multichunk(width, height, dpi, fg44=fg44, bg44=bg44, sjbz=sjbz) def generate_mask(filename, image, method): ''' Generate mask using the provided method (if filename is not None); or simply load it from file (if filename is not None). ''' if filename is None: return method(image) else: return gamera.load_image(filename) _pageid_chars = re.compile('^[A-Za-z0-9_+.-]+$').match def check_pageid_sanity(pageid): if not _pageid_chars(pageid): raise ValueError('Pageid must consist only of lowercase ASCII letters, digits, _, +, - and dot.') if pageid[:1] in ('.', '+', '-'): raise ValueError('Pageid cannot start with +, - or a dot.') if '..' in pageid: raise ValueError('Pageid cannot contain two consecutive dots.') assert pageid == os.path.basename(pageid) if pageid.endswith('.djvu'): return pageid else: raise ValueError('Pageid must end with the .djvu extension.') def replace_ext(filename, ext): return '%s.%s' % (os.path.splitext(filename)[0], ext) class namespace(): pass class main(): compression_info_template = \ '%(bits_per_pixel).3f bits/pixel; ' \ '%(ratio).3f:1, %(percent_saved).2f%% saved, ' \ '%(bytes_in)d bytes in, %(bytes_out)d bytes out' def __init__(self): setup_logging() parser = cli.ArgumentParser(gamera.methods, default_method='djvu') parser.parse_args(actions=self) def check_common(self, o): if len(o.masks) == 0: o.masks = [None for x in o.input] elif len(o.masks) != len(o.input): raise ValueError('%d input images != %d masks' % (len(o.input), len(o.masks))) ipc_logger = logging.getLogger('didjvu.ipc') assert logger is not None log_level = { 0: logging.WARNING, 1: logging.INFO, 2: logging.NOSY, }.get(o.verbosity, logging.DEBUG) logger.setLevel(log_level) ipc_logger.setLevel(log_level) gamera.init() def check_multi_output(self, o): self.check_common(o) if o.output is None: if o.output_template is not None: o.output = [templates.expand(o.output_template, f, n) for n, f in enumerate(o.input)] o.xmp_output = [s + '.xmp' if o.xmp else None for s in o.output] elif len(o.input) == 1: o.output = [sys.stdout] o.xmp_output = [None] check_tty() else: raise ValueError("cannot output multiple files to stdout") else: if len(o.input) == 1: o.xmp_output = [o.output + '.xmp'] if o.xmp else [None] o.output = [o.output] else: raise ValueError("cannot output multiple files to a single file") assert len(o.masks) == len(o.output) == len(o.input) o.output = ( open(f, 'wb') if isinstance(f, basestring) else f for f in o.output ) o.xmp_output = ( open(f, 'wb') if isinstance(f, basestring) else f for f in o.xmp_output ) def check_single_output(self, o): self.check_common(o) if o.output is None: o.output = [sys.stdout] o.xmp_output = [None] check_tty() else: filename = o.output o.output = [file(filename, 'wb')] o.xmp_output = [file(filename + '.xmp', 'wb')] if o.xmp else [None] assert len(o.output) == len(o.xmp_output) == 1 def encode(self, o): self.check_multi_output(o) parallel_for(o, self.encode_one, o.input, o.masks, o.output, o.xmp_output) def encode_one(self, o, image_filename, mask_filename, output, xmp_output): bytes_in = os.path.getsize(image_filename) logger.info('%s:' % image_filename) ftype = filetype.check(image_filename) if ftype.like(filetype.djvu): if ftype.like(filetype.djvu_single): logger.info('- copying DjVu as is') with open(image_filename, 'rb') as djvu_file: copy_file(djvu_file, output) else: # TODO: Figure out how many pages the multi-page document # consist of. If it's only one, continue. raise NotImplementedError("I don't know what to do with this file") return logger.info('- reading image') image = gamera.load_image(image_filename) width, height = image.ncols, image.nrows logger.nosy('- image size: %d x %d', width, height) mask = generate_mask(mask_filename, image, o.method) if xmp_output: n_connected_components = len(mask.cc_analysis()) logger.info('- converting to DjVu') djvu_doc = image_to_djvu(width, height, image, mask, options=o) djvu_file = djvu_doc.save() try: bytes_out = copy_file(djvu_file, output) finally: djvu_file.close() bits_per_pixel = 8.0 * bytes_out / (width * height) ratio = 1.0 * bytes_in / bytes_out percent_saved = (1.0 * bytes_in - bytes_out) * 100 / bytes_in logger.info('- %s', self.compression_info_template % locals()) if xmp_output: logger.info('- saving XMP metadata') metadata = xmp.metadata() metadata.import_(image_filename) internal_properties = list(cli.dump_options(o)) + [ ('n-connected-components', str(n_connected_components)) ] metadata.update( media_type='image/vnd.djvu', internal_properties=internal_properties, ) metadata.write(xmp_output) def separate_one(self, o, image_filename, output): logger.info('%s:', image_filename) ftype = filetype.check(image_filename) if ftype.like(filetype.djvu): # TODO: Figure out how many pages the document consist of. # If it's only one, extract the existing mask. raise NotImplementedError("I don't know what to do with this file") logger.info('- reading image') image = gamera.load_image(image_filename) width, height = image.ncols, image.nrows logger.nosy('- image size: %d x %d' % (width, height)) logger.info('- thresholding') mask = generate_mask(None, image, o.method) logger.info('- saving') if output is not sys.stdout: # A real file mask.save_PNG(output.name) else: tmp_output = temporary.file(suffix='.png') try: mask.save_PNG(tmp_output.name) copy_file(tmp_output, output) finally: tmp_output.close() def separate(self, o): self.check_multi_output(o) for mask in o.masks: assert mask is None parallel_for(o, self.separate_one, o.input, o.output) def bundle(self, o): self.check_single_output(o) if o.pages_per_dict <= 1: self.bundle_simple(o) else: self.bundle_complex(o) [xmp_output] = o.xmp_output if xmp_output: logger.info('saving XMP metadata') metadata = xmp.metadata() internal_properties = list(cli.dump_options(o, multipage=True)) metadata.update( media_type='image/vnd.djvu', internal_properties=internal_properties, ) metadata.write(xmp_output) def _bundle_simple_page(self, o, input, mask, component_name): with open(component_name, 'wb') as component: self.encode_one(o, input, mask, component, None) def bundle_simple(self, o): [output] = o.output with temporary.directory() as tmpdir: bytes_in = 0 component_filenames = [] for page, (input, mask) in enumerate(zip(o.input, o.masks)): bytes_in += os.path.getsize(input) pageid = templates.expand(o.pageid_template, input, page) # TODO: Check for filename conflicts. check_pageid_sanity(pageid) component_filenames += os.path.join(tmpdir, pageid), parallel_for(o, self._bundle_simple_page, o.input, o.masks, component_filenames) logger.info('bundling') djvu_file = djvu.bundle_djvu(*component_filenames) try: bytes_out = copy_file(djvu_file, output) finally: djvu_file.close() bits_per_pixel = float('nan') # FIXME! ratio = 1.0 * bytes_in / bytes_out percent_saved = (1.0 * bytes_in - bytes_out) * 100 / bytes_in logger.nosy(self.compression_info_template % locals()) def _bundle_complex_page(self, o, page, minidjvu_in_dir, image_filename, mask_filename, pixels): logger.info('%s:', image_filename) ftype = filetype.check(image_filename) if ftype.like(filetype.djvu): # TODO: Allow to merge existing documents (even multi-page ones). raise NotImplementedError("I don't know what to do with this file") logger.info('- reading image') image = gamera.load_image(image_filename) width, height = image.ncols, image.nrows pixels[0] += width * height logger.nosy('- image size: %d x %d', width, height) mask = generate_mask(mask_filename, image, o.method) logger.info('- converting to DjVu') page.djvu = image_to_djvu(width, height, image, mask, options=o) image = mask = None page.sjbz = djvu.Multichunk(width, height, o.dpi, sjbz=page.djvu['sjbz']) page.sjbz_symlink = os.path.join(minidjvu_in_dir, page.pageid) os.symlink(page.sjbz.save().name, page.sjbz_symlink) def bundle_complex(self, o): [output] = o.output with temporary.directory() as minidjvu_in_dir: bytes_in = 0 pixels = [0] page_info = [] for pageno, (image_filename, mask_filename) in enumerate(zip(o.input, o.masks)): page = namespace() page_info += page, bytes_in += os.path.getsize(image_filename) page.pageid = templates.expand(o.pageid_template, image_filename, pageno) check_pageid_sanity(page.pageid) # TODO: Check for filename conflicts. parallel_for(o, self._bundle_complex_page, page_info, itertools.repeat(minidjvu_in_dir), o.input, o.masks, itertools.repeat(pixels) ) [pixels] = pixels with temporary.directory() as minidjvu_out_dir: logger.info('creating shared dictionaries') def chdir(): os.chdir(minidjvu_out_dir) arguments = ['minidjvu', '--indirect', '--aggression', str(o.loss_level), '--pages-per-dict', str(o.pages_per_dict), ] arguments += [page.sjbz_symlink for page in page_info] index_filename = temporary.name(prefix='__index__.', suffix='.djvu', dir=minidjvu_out_dir) index_filename = os.path.basename(index_filename) # FIXME: Name conflicts are still possible! arguments += [index_filename] ipc.Subprocess(arguments, preexec_fn=chdir).wait() os.remove(os.path.join(minidjvu_out_dir, index_filename)) component_filenames = [] for pageno, page in enumerate(page_info): if pageno % o.pages_per_dict == 0: iff_name = replace_ext(page_info[pageno].pageid, 'iff') iff_name = os.path.join(minidjvu_out_dir, iff_name) component_filenames += [iff_name] sjbz_name = os.path.join(minidjvu_out_dir, page.pageid) component_filenames += [sjbz_name] page.djvu['sjbz'] = sjbz_name page.djvu['incl'] = iff_name page.djvu = page.djvu.save() page.djvu_symlink = os.path.join(minidjvu_out_dir, page.pageid) os.unlink(page.djvu_symlink) os.symlink(page.djvu.name, page.djvu_symlink) logger.info('bundling') djvu_file = djvu.bundle_djvu(*component_filenames) try: bytes_out = copy_file(djvu_file, output) finally: djvu_file.close() bits_per_pixel = 8.0 * bytes_out / pixels ratio = 1.0 * bytes_in / bytes_out percent_saved = (1.0 * bytes_in - bytes_out) * 100 / bytes_in logger.nosy('%s', self.compression_info_template % locals()) # vim:ts=4 sw=4 et didjvu-0.2.7/lib/timestamp.py0000644000000000000000000000305211754247211016072 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. import time import datetime class Timestamp(object): def __init__(self, unixtime): self._localtime = time.localtime(unixtime) def _str(self): return time.strftime('%Y-%m-%dT%H:%M:%S', self._localtime) def _str_tz(self): offset = time.timezone if not self._localtime.tm_isdst else time.altzone hours, minutes = divmod(abs(offset) // 60, 60) return '%s%02d:%02d' % ('+' if offset < 0 else '-', hours, minutes) def __str__(self): '''Format the timestamp object in accordance with RFC 3339.''' return self._str() + self._str_tz() def as_datetime(self, cls=datetime.datetime): offset = time.timezone if not self._localtime.tm_isdst else time.altzone class tz(datetime.tzinfo): def utcoffset(self, dt): return datetime.timedelta(seconds=-offset) def dst(self, dt): return datetime.timedelta(0) return cls(*self._localtime[:6], tzinfo=tz()) def now(): return Timestamp(time.time()) # vim:ts=4 sw=4 et didjvu-0.2.7/lib/__init__.py0000644000000000000000000000051212110522346015614 0ustar rootroot00000000000000# encoding=UTF-8 import sys if sys.version_info < (2, 5): # Only Python ≥ 2.6 is officially supported, but the software is not completely # unusable with Python 2.5: raise RuntimeError('Python >= 2.6 is required') if sys.version_info >= (3, 0): raise RuntimeError('Python 2.X is required') # vim:ts=4 sw=4 et didjvu-0.2.7/lib/gamera_extra.py0000644000000000000000000001306512120173747016533 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2009, 2010, 2011, 2012, 2013 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''bridge to the Gamera framework''' import ctypes import re import sys import warnings from . import utils try: from PIL import Image as PIL except ImportError, ex: utils.enhance_import_error(ex, 'Python Imaging Library', 'python-imaging', 'http://www.pythonware.com/products/pil/') raise else: # Gamera (<< 3.4.0) still expects that PIL can be imported as Image sys.modules['Image'] = PIL try: import gamera except ImportError, ex: utils.enhance_import_error(ex, 'Gamera', 'python-gamera', 'http://gamera.sourceforge.net/') raise from gamera import __version__ as version from gamera.core import load_image as _load_image from gamera.core import init_gamera as _init from gamera.core import Image, RGB, GREYSCALE, ONEBIT, Dim, RGBPixel from gamera.plugins.pil_io import from_pil as _from_pil def has_version(*req_version): return tuple(map(int, version.split('.'))) >= req_version def load_image(filename): try: # Natively, Gamera supports only TIFF and PNG formats. However, it # supports wider variety of TIFFs than PIL. return _load_image(filename) except IOError: pil_image = PIL.open(filename) # Gamera supports importing only 8-bit and RGB from PIL: if pil_image.mode == '1': pil_image = pil_image.convert('L') elif pil_image.mode not in ('RGB', 'L'): pil_image = pil_image.convert('RGB') assert pil_image.mode in ('RGB', 'L') return _from_pil(pil_image) def colorspace_wrapper(plugin): pixel_types = plugin.self_type.pixel_types def new_plugin(image, method=[None]): if image.data.pixel_type not in pixel_types: if RGB in pixel_types: image = image.to_rgb() elif GREYSCALE in pixel_types: image = image.to_greyscale() if method[0] is None: method[0] = plugin() return method[0](image) new_plugin.__name__ = plugin.__name__ return new_plugin def _load_methods(): replace_suffix = re.compile('_threshold$').sub class _methods: from gamera.plugins.threshold import abutaleb_threshold from gamera.plugins.threshold import bernsen_threshold from gamera.plugins.threshold import djvu_threshold from gamera.plugins.threshold import otsu_threshold from gamera.plugins.threshold import tsai_moment_preserving_threshold as tsai # TODO: from gamera.plugins.binarization import gatos_threshold from gamera.plugins.binarization import niblack_threshold from gamera.plugins.binarization import sauvola_threshold from gamera.plugins.binarization import white_rohrer_threshold if has_version(3, 3, 1): from gamera.plugins.binarization import shading_subtraction if has_version(3, 4, 0): from gamera.plugins.binarization import brink_threshold methods = {} for name, plugin in vars(_methods).items(): if name.startswith('_'): continue name = replace_suffix('', name) name = name.replace('_', '-') method = colorspace_wrapper(plugin) method.didjvu_name = name methods[name] = method return methods methods = _load_methods() def to_pil_rgb(image): # About 20% faster than the standard .to_pil() method of Gamera 3.2.6. buffer = ctypes.create_string_buffer(3 * image.ncols * image.nrows) image.to_buffer(buffer) return PIL.frombuffer('RGB', (image.ncols, image.nrows), buffer, 'raw', 'RGB', 0, 1) def to_pil_1bpp(image): return image.to_greyscale().to_pil() def _decref(o): ''' Forcibly decrease refcount of the object by 1. ''' libc = ctypes.cdll.LoadLibrary(None) memmove = libc.memmove memmove.argtypes = [ctypes.py_object, ctypes.py_object, ctypes.c_size_t] memmove.restype = ctypes.py_object return memmove(o, None, 0) def _monkeypatch_to_raw_string(): ''' Monkey-patch to _to_raw_string plugin function to return objects with correct refcounts. ''' from gamera.plugins import _string_io old_to_raw_string = _string_io._to_raw_string def fixed_to_raw_string(*args, **kwargs): o = old_to_raw_string(*args, **kwargs) assert sys.getrefcount(o) == 3 o = _decref(o) assert sys.getrefcount(o) == 2 return o _string_io._to_raw_string = fixed_to_raw_string Image._to_raw_string = fixed_to_raw_string def init(): sys.modules['numpy'] = None result = _init() test_image = Image((0, 0), (5, 5), RGB) test_string = test_image._to_raw_string() refcount = sys.getrefcount(test_string) if refcount == 3: # See: http://tech.groups.yahoo.com/group/gamera-devel/message/2068 warnings.warn(RuntimeWarning('Working around memory leak in the Gamera library'), stacklevel=2) _monkeypatch_to_raw_string() fixed_test_string = test_image._to_raw_string() assert sys.getrefcount(fixed_test_string) == 2 assert test_string == fixed_test_string else: assert refcount == 2 return result # vim:ts=4 sw=4 et didjvu-0.2.7/lib/xmp/0000755000000000000000000000000012120174731014313 5ustar rootroot00000000000000didjvu-0.2.7/lib/xmp/pyexiv2_backend.py0000644000000000000000000001313211754247211017750 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''XMP support (pyexiv2 backend)''' import datetime import itertools import xml.etree.cElementTree as etree import pyexiv2.xmp from .. import temporary from .. import timestamp from .. import version from . import namespaces as ns def xmp_register_namespace(prefix, uri): # work-around for class fool_pyexiv2(str): def endswith(self, suffix, *args, **kwargs): return True pyexiv2.xmp.register_namespace(fool_pyexiv2(uri), prefix) xmp_register_namespace('didjvu', ns.didjvu) try: etree.register_namespace except AttributeError: def et_register_namespace(prefix, uri): import xml.etree.ElementTree as etree etree._namespace_map[uri] = prefix etree.register_namespace = et_register_namespace del et_register_namespace etree.register_namespace('x', 'adobe:ns:meta/') class XmpError(RuntimeError): pass class Event(object): def __init__(self, action=None, software_agent=None, parameters=None, instance_id=None, changed=None, when=None, ): if software_agent is None: software_agent = version.get_software_agent() self._items = [ ('action', action), ('softwareAgent', software_agent), ('parameters', parameters), ('instanceID', instance_id), ('changed', changed), ('when', str(when)), ] @property def items(self): return iter(self._items) class datetime_for_pyexiv2(datetime.datetime): __almost_zero = 1.0 while __almost_zero / 2 > 0: __almost_zero /= 2 def __init__(self, year, month, day, hour, minute, second, microsecond=0, tzinfo=None): datetime.datetime.__init__(self) self.__second = second @property def second(self): # pyexiv2 uses HH:MM format (instead of HH:MM:SS) if .seconds is 0. # Let's fool it into thinking it's always non-zero. return self.__second or self.__almost_zero class MetadataBase(object): def _reload(self): fp = self._fp fp.flush() fp.seek(0) self._meta = pyexiv2.ImageMetadata(fp.name) self._meta.read() def _add_history(self): try: self['xmpMM.History'] except LookupError: pass else: return self._meta.write() fp = self._fp fp.seek(0) xmp = etree.parse(fp) description = None try: xmp_find = xmp.iterfind except AttributeError: # Python 2.6 xmp_find = xmp.findall for description in xmp_find('.//{%s}Description' % ns.rdf): pass if description is None: raise XmpError('Cannot add xmpMM:History') e_description = etree.SubElement(description, '{%s}History' % ns.xmpmm) etree.SubElement(e_description, '{%s}Seq' % ns.rdf) fp.seek(0) fp.truncate() xmp.write(fp, encoding='UTF-8') fp.flush() fp.seek(0) self._reload() try: self['xmpMM.History'] except LookupError: raise XmpError('Cannot add xmpMM:History') def __init__(self): self._fp = fp = temporary.file(suffix='.xmp') fp.write('' '' % ns.rdf ) self._reload() self._original_meta = self._meta def __del__(self): try: fp = self._fp except AttributeError: pass else: fp.close() def get(self, key, fallback=None): try: return self[key] except LookupError: return fallback def __getitem__(self, key): tag = self._meta['Xmp.' + key] if tag.type == 'MIMEType': value = tag.raw_value else: value = tag.value return value def __setitem__(self, key, value): if isinstance(value, timestamp.Timestamp): value = value.as_datetime(cls=datetime_for_pyexiv2) elif key.startswith('didjvu.'): value = str(value) elif key == 'dc.format' and isinstance(value, basestring): value = tuple(value.split('/', 1)) self._meta['Xmp.' + key] = value def add_to_history(self, event, index): for key, value in event.items: if value is None: continue self['xmpMM.History[%d]/stEvt:%s' % (index, key)] = value def append_to_history(self, event): self._add_history() keys = self._meta.xmp_keys for i in itertools.count(1): key = 'Xmp.xmpMM.History[%d]' % i if key not in keys: break return self.add_to_history(event, i) def serialize(self): self._meta.write() fp = self._fp fp.seek(0) return fp.read() def read(self, file): data = file.read() fp = self._fp fp.seek(0) fp.truncate() fp.write(data) self._reload() __all__ = ['MetadataBase'] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/xmp/libxmp_backend.py0000644000000000000000000000626311754247211017644 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''XMP support (python-xmp-toolkit backend)''' import libxmp from .. import timestamp from . import namespaces as ns class XmpError(RuntimeError): pass class MetadataBase(object): def __init__(self): backend = self._backend = libxmp.XMPMeta() prefix = backend.register_namespace(ns.didjvu, 'didjvu') if prefix is None: raise XmpError('Cannot register namespace for didjvu internal properties') @classmethod def _expand_key(cls, key): namespace, key = key.split('.', 1) namespace = getattr(ns, namespace.lower()) return namespace, key def get(self, key, fallback=None): namespace, key = self._expand_key(key) backend = self._backend result = backend.get_property(namespace, key) if result is None: result = fallback return result def __getitem__(self, key): result = self.get(key) if result is None: raise KeyError(key) return result def __setitem__(self, key, value): namespace, key = self._expand_key(key) backend = self._backend if isinstance(value, bool): rc = backend.set_property_bool(namespace, key, value) elif isinstance(value, int): rc = backend.set_property_int(namespace, key, value) elif isinstance(value, list) and len(value) == 0: rc = backend.set_property(namespace, key, '', prop_value_is_array=True, prop_array_is_ordered=True ) else: if isinstance(value, timestamp.Timestamp): value = str(value) rc = backend.set_property(namespace, key, value) if rc is None: raise XmpError('Cannot set property') def add_to_history(self, event, index): for key, value in event.items: if value is None: continue self['xmpMM.History[%d]/stEvt:%s' % (index, key)] = value def append_to_history(self, event): backend = self._backend def count_history(): return backend.count_array_items(ns.xmpmm, 'History') count = count_history() if count == 0: self['xmpMM.History'] = [] assert count_history() == 0 result = self.add_to_history(event, count + 1) assert count_history() == count + 1 return result def serialize(self): backend = self._backend return backend.serialize_and_format(omit_packet_wrapper=True, tabchr=' ') def read(self, file): backend = self._backend xmp = file.read() backend.parse_from_str(xmp) __all__ = ['MetadataBase'] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/xmp/__init__.py0000644000000000000000000000612211754247211016433 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''XMP support''' import errno import uuid from .. import timestamp from .. import utils from .. import version try: from . import libxmp_backend as default_backend except ImportError, exc: try: from . import pyexiv2_backend as default_backend except ImportError: utils.enhance_import_error(exc, 'python-xmp-toolkit', 'python-libxmp', 'http://code.google.com/p/python-xmp-toolkit/') raise exc def gen_uuid(): return 'uuid:' + str(uuid.uuid4()).replace('-', '') class Event(object): def __init__(self, action=None, software_agent=None, parameters=None, instance_id=None, changed=None, when=None, ): if software_agent is None: software_agent = version.get_software_agent() self._items = [ ('action', action), ('softwareAgent', software_agent), ('parameters', parameters), ('instanceID', instance_id), ('changed', changed), ('when', str(when)), ] @property def items(self): return iter(self._items) def metadata(backend=default_backend): class Metadata(backend.MetadataBase): def update(self, media_type, internal_properties={}): instance_id = gen_uuid() now = timestamp.now() original_media_type = self.get('dc.format') # TODO: try to guess original media type self['dc.format'] = media_type if original_media_type is not None: event_params = 'from %s to %s' % (original_media_type, media_type) else: event_params = 'to %s' % (media_type,) self['xmp.ModifyDate'] = now self['xmp.MetadataDate'] = now self['xmpMM.InstanceID'] = instance_id event = Event( action='converted', parameters=event_params, instance_id=instance_id, when=now, ) self.append_to_history(event) for k, v in internal_properties: self['didjvu.' + k] = v def import_(self, image_filename): try: file = open(image_filename + '.xmp', 'rb') except (OSError, IOError), ex: if ex.errno == errno.ENOENT: return raise try: self.read(file) finally: file.close() def write(self, file): file.write(self.serialize()) return Metadata() __all__ = ['metadata'] # vim:ts=4 sw=4 et didjvu-0.2.7/lib/xmp/namespaces.py0000644000000000000000000000044611754247211017016 0ustar rootroot00000000000000# encoding=UTF-8 didjvu = 'http://jwilk.net/software/didjvu#' dc = 'http://purl.org/dc/elements/1.1/' rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' tiff = 'http://ns.adobe.com/tiff/1.0/' xmp = 'http://ns.adobe.com/xap/1.0/' xmpmm = 'http://ns.adobe.com/xap/1.0/mm/' # vim:ts=4 sw=4 et didjvu-0.2.7/lib/djvu_extra.py0000644000000000000000000002456111754247211016252 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2009, 2010 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. '''wrappers for the DjVuLibre utilities''' from __future__ import with_statement import os import re import struct from . import ipc from . import temporary DPI_MIN = 72 DPI_DEFAULT = 300 DPI_MAX = 6000 LOSS_LEVEL_MIN = 0 LOSS_LEVEL_CLEAN = 1 LOSS_LEVEL_LOSSY = 100 LOSS_LEVEL_MAX = 200 SUBSAMPLE_MIN = 1 SUBSAMPLE_DEFAULT = 3 SUBSAMPLE_MAX = 12 IW44_SLICES_DEFAULT = (74, 89, 99) class Crcb(object): def __init__(self, sort_key, name): self._sort_key = sort_key self._name = name def __cmp__(self, other): if not isinstance(other, Crcb): return NotImplemented return cmp(self._sort_key, other._sort_key) def __str__(self): return self._name def __repr__(self): return '%s.CRCB.%s' % (type(self).__module__, self._name) class CRCB: values = [ Crcb(3, 'full'), Crcb(2, 'normal'), Crcb(1, 'half'), Crcb(0, 'none'), ] for _value in CRCB.values: setattr(CRCB, str(_value), _value) del _value def bitonal_to_djvu(image, dpi=300, loss_level=0): pbm_file = temporary.file(suffix='.pbm') image.save(pbm_file.name) djvu_file = temporary.file(suffix='.djvu', mode='r+b') args = ['cjb2', '-losslevel', str(loss_level), pbm_file.name, djvu_file.name] return ipc.Proxy(djvu_file, ipc.Subprocess(args).wait, [pbm_file]) def photo_to_djvu(image, dpi=100, slices=IW44_SLICES_DEFAULT, gamma=2.2, mask_image=None, crcb=CRCB.normal): ppm_file = temporary.file(suffix='.ppm') temporaries = [ppm_file] image.save(ppm_file.name) djvu_file = temporary.file(suffix='.djvu', mode='r+b') if not isinstance(crcb, Crcb): raise TypeError args = [ 'c44', '-dpi', str(dpi), '-slice', ','.join(map(str, slices)), '-gamma', '%.1f' % gamma, '-crcb%s' % crcb, ] if mask_image is not None: pbm_file = temporary.file(suffix='.pbm') mask_image.save(pbm_file.name) args += ['-mask', pbm_file.name] temporaries += [pbm_file] args += [ppm_file.name, djvu_file.name] return ipc.Proxy(djvu_file, ipc.Subprocess(args).wait, temporaries) def djvu_to_iw44(djvu_file): # TODO: Use Multichunk. iw44_file = temporary.file(suffix='.iw44') args = ['djvuextract', djvu_file.name, 'BG44=%s' % iw44_file.name] return ipc.Proxy(iw44_file, ipc.Subprocess(args).wait, [djvu_file]) def _int_or_none(x): if x is None: return if isinstance(x, int): return x raise ValueError def _chunk_order(key): # INCL must go before Sjbz. if key[0] == 'incl': return -2 # djvuextract expects Sjbz before PPM. if key[0] == 'sjbz': return -1 return 0 class Multichunk(object): _chunk_names = 'Sjbz Smmr BG44 BGjp BG2k FGbz FG44 FGjp FG2k INCL Djbz' _chunk_names = dict((x.lower(), x) for x in _chunk_names.split()) _info_re = re.compile(' ([0-9]+)x([0-9]+),.* ([0-9]+) dpi,').search def __init__(self, width=None, height=None, dpi=None, **chunks): self.width = _int_or_none(width) self.height = _int_or_none(height) self.dpi = _int_or_none(dpi) self._chunks = {} self._dirty = set() # Chunks that need to be re-read from the file. self._pristine = False # Should save() be a no-op? self._file = None for (k, v) in chunks.iteritems(): self[k] = v def _load_file(self): args = ['djvudump', self._file.name] dump = ipc.Subprocess(args, stdout=ipc.PIPE) self.width = self.height = self.dpi = None keys = set() try: header = dump.stdout.readline() if not header.startswith(' FORM:DJVU '): raise ValueError for line in dump.stdout: if line[:4] == ' ' and line[8:9] == ' ': key = line[4:8] if key == 'INFO': m = self._info_re(line[8:]) self.width, self.height, self.dpi = m.groups() else: keys.add(key.lower()) else: ValueError finally: dump.wait() self._chunks = dict((key, None) for key in keys) self._dirty.add(self._chunks) self._pristine = True @classmethod def from_file(cls, djvu_file): self = cls() self._file = djvu_file self._load_file() return self def __contains__(self, key): key = key.lower() if key in self._chunks: return True def __setitem__(self, key, value): if key == 'image': ppm_file = temporary.file(suffix='.ppm') value.save(ppm_file.name) key = 'PPM' value = ppm_file else: key = key.lower() if key not in self._chunk_names: raise ValueError self._chunks[key] = value self._dirty.discard(key) self._pristine = False def __getitem__(self, key): key = key.lower() value = self._chunks[key] if key in self._dirty: self.save() self._load_file() self._update_chunks() value = self._chunks[key] assert value is not None return value def _update_chunks(self): assert self._file is not None args = ['djvuextract', self._file.name] chunk_files = {} for key in self._dirty: chunk_file = temporary.file(suffix='.%s-chunk' % key) args += ['%s=%s' % (self._chunk_names[key], chunk_file.name)] chunk_files[key] = chunk_file djvuextract = ipc.Subprocess(args, stderr=open(os.devnull, 'w')) for key in self._chunks: self._chunks[key] = ipc.Proxy(chunk_files[key], djvuextract.wait, [self._file]) self._dirty.discard(key) assert not self._dirty # The file reference is not needed anymore. self._file = None def save(self): if (self._file is not None) and self._pristine: return self._file if self.width is None: raise ValueError if self.height is None: raise ValueError if self.dpi is None: raise ValueError if len(self._chunks) == 0: raise ValueError args = ['djvumake', None, 'INFO=%d,%d,%d' % (self.width, self.height, self.dpi)] for key, value in sorted(self._chunks.iteritems(), key=_chunk_order): try: key = self._chunk_names[key] except KeyError: pass if not isinstance(value, basestring): value = value.name if key == 'BG44': value += ':99' args += ['%s=%s' % (key, value)] with temporary.directory() as tmpdir: djvu_filename = args[1] = os.path.join(tmpdir, 'result.djvu') ipc.Subprocess(args).wait() self._chunks.pop('PPM', None) assert 'PPM' not in self._dirty djvu_new_filename = temporary.name(suffix='.djvu') os.link(djvu_filename, djvu_new_filename) self._file = temporary.wrapper(file(djvu_new_filename, mode='r+b'), djvu_new_filename) self._pristine = True return self._file _djvu_header = 'AT&TFORM\0\0\0\0DJVMDIRM\0\0\0\0\1' def bundle_djvu_via_indirect(*component_filenames): with temporary.directory() as tmpdir: pageids = [] page_sizes = [] for filename in component_filenames: pageid = os.path.basename(filename) os.symlink(filename, os.path.join(tmpdir, pageid)) pageids += [pageid] page_size = os.path.getsize(filename) if page_size >= 1 << 24: # Would overflow; but 0 is fine, too. page_size = 0 page_sizes += [page_size] with temporary.file(dir=tmpdir, suffix='djvu') as index_file: index_file.write(_djvu_header) index_file.write(struct.pack('>H', len(pageids))) index_file.flush() bzz = ipc.Subprocess(['bzz', '-e', '-', '-'], stdin=ipc.PIPE, stdout=index_file) try: for page_size in page_sizes: bzz.stdin.write(struct.pack('>I', page_size)[1:]) for pageid in pageids: bzz.stdin.write(struct.pack('B', not pageid.endswith('.iff'))) for pageid in pageids: bzz.stdin.write(pageid) bzz.stdin.write('\0') finally: bzz.stdin.close() bzz.wait() index_file_size = index_file.tell() i = 0 while True: i = _djvu_header.find('\0' * 4, i) if i < 0: break index_file.seek(i) index_file.write(struct.pack('>I', index_file_size - i - 4)) i += 4 index_file.flush() djvu_file = temporary.file(suffix='.djvu') ipc.Subprocess(['djvmcvt', '-b', index_file.name, djvu_file.name]).wait() return djvu_file def bundle_djvu(*component_filenames): assert len(component_filenames) > 0 if any(c.endswith('.iff') for c in component_filenames): # We can't use ``djvm -c``. return bundle_djvu_via_indirect(*component_filenames) raise NotImplementedError else: djvu_file = temporary.file(suffix='.djvu') args = ['djvm', '-c', djvu_file.name] args += component_filenames return ipc.Proxy(djvu_file, ipc.Subprocess(args).wait, None) __all__ = [ 'bitonal_to_djvu', 'photo_to_djvu', 'djvu_to_iw44', 'Multichunk', 'DPI_MIN', 'DPI_DEFAULT', 'DPI_MAX', 'LOSS_LEVEL_MIN', 'LOSS_LEVEL_CLEAN', 'LOSS_LEVEL_LOSSY', 'LOSS_LEVEL_MAX', 'SUBSAMPLE_MIN', 'SUBSAMPLE_DEFAULT', 'SUBSAMPLE_MAX', 'IW44_SLICES_DEFAULT', 'CRCB', ] # vim:ts=4 sw=4 et didjvu-0.2.7/tests/0000755000000000000000000000000012120174731014103 5ustar rootroot00000000000000didjvu-0.2.7/tests/test_utils.py0000644000000000000000000000341212107143546016661 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2010, 2011 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. from __future__ import with_statement import sys from . common import ( interim, exception, ) from lib import utils class test_enhance_import(): @classmethod def setup_class(cls): sys.modules['nonexistent'] = None def test_debian(self): with interim(utils, debian=True): with exception(ImportError, 'No module named nonexistent; please install the python-nonexistent package'): try: import nonexistent except ImportError, ex: utils.enhance_import_error(ex, 'PyNonexistent', 'python-nonexistent', 'http://pynonexistent.example.net/') raise nonexistent.f() # quieten pyflakes def test_nondebian(self): with interim(utils, debian=False): with exception(ImportError, 'No module named nonexistent; please install the PyNonexistent package '): try: import nonexistent except ImportError, ex: utils.enhance_import_error(ex, 'PyNonexistent', 'python-nonexistent', 'http://pynonexistent.example.net/') raise nonexistent.f() # quieten pyflakes # vim:ts=4 sw=4 et didjvu-0.2.7/tests/example.png.xmp0000644000000000000000000000247411754247211017064 0ustar rootroot00000000000000 scanhelper 0.2.4 2012-02-01T16:28:00+01:00 2012-02-29T20:30:21+01:00 image/png 69 42 uuid:a2686c01b50e4b6aab2cccdef40f6286 created scanhelper 0.2.4 uuid:a2686c01b50e4b6aab2cccdef40f6286 2012-02-01T16:28:00+01:00 didjvu-0.2.7/tests/common.py0000644000000000000000000000412311754247211015753 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2010, 2011, 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. import contextlib import re import sys from nose import SkipTest from nose.tools import ( assert_equal, assert_not_equal, assert_true, ) def assert_regexp_matches(regexp, text): if isinstance(regexp, basestring): regexp = re.compile(regexp) if not regexp.search(text): message = '''Regexp didn't match: %r not found in %r''' % (regexp.pattern, text) raise AssertionError(message) def assert_rfc3339_timestamp(timestamp): return assert_regexp_matches( '^[0-9]{4}(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){2}([+-][0-9]{2}:[0-9]{2}|Z)$', timestamp ) @contextlib.contextmanager def exception(exc_type, string=None, regex=None, callback=None): if sum(x is not None for x in (string, regex, callback)) != 1: raise ValueError('exactly one of: string, regex, callback must be provided') if string is not None: def callback(exc): assert_equal(str(exc), string) elif regex is not None: def callback(exc): assert_regexp_matches(regex, str(exc)) try: yield None except exc_type: _, exc, _ = sys.exc_info() callback(exc) else: message = '%s was not raised' % exc_type.__name__ raise AssertionError(message) @contextlib.contextmanager def interim(obj, **override): copy = dict((key, getattr(obj, key)) for key in override) for key, value in override.iteritems(): setattr(obj, key, value) try: yield finally: for key, value in copy.iteritems(): setattr(obj, key, value) # vim:ts=4 sw=4 et didjvu-0.2.7/tests/__init__.py0000644000000000000000000000000011754247211016210 0ustar rootroot00000000000000didjvu-0.2.7/tests/test_xmp.py0000644000000000000000000004063711754247211016340 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. import os from . common import ( SkipTest, assert_equal, assert_not_equal, assert_regexp_matches, assert_rfc3339_timestamp, assert_true, ) xmp_backends = [] from lib import ipc from lib import temporary from lib import xmp from lib.xmp import namespaces as ns try: from lib.xmp import libxmp_backend except ImportError, libxmp_backend_import_error: class libxmp_backend: # dummy replacement class MetadataBase(object): def __init__(self): raise SkipTest(libxmp_backend_import_error) try: from lib.xmp import pyexiv2_backend except ImportError, pyexiv2_backend_import_error: class pyexiv2_backend: # dummy replacement class MetadataBase(object): def __init__(self): raise SkipTest(pyexiv2_backend_import_error) xmp_backends = [libxmp_backend, pyexiv2_backend] try: import libxmp import libxmp.consts except ImportError, libxmp_import_error: libxmp = None def test_uuid(): uuid1 = xmp.gen_uuid() assert_correct_uuid(uuid1) uuid2 = xmp.gen_uuid() assert_correct_uuid(uuid2) assert_not_equal(uuid1, uuid2) def tag_backend(backend): class tag(object): def __repr__(self): return backend.__name__.split('.')[-1] return tag() class tag_exiv2(object): def __repr__(self): return 'exiv2_checker' tag_exiv2 = tag_exiv2() class tag_libxmp(object): def __repr__(self): return 'libxmp_checker' tag_libxmp = tag_libxmp() def run_exiv2(filename, fail_ok=False): try: child = ipc.Subprocess( ['exiv2', 'print', '-P', 'Xkt', filename], stdout=ipc.PIPE ) except OSError, ex: raise SkipTest(ex) for line in sorted(child.stdout): yield line try: child.wait() except ipc.CalledProcessError: if not fail_ok: raise def assert_correct_uuid(uuid): return assert_regexp_matches( '^uuid:[0-9a-f]{32}$', uuid ) def assert_correct_software_agent(software_agent): return assert_regexp_matches( '^didjvu [0-9.]+( [(]Gamera [0-9.]+[)])?', software_agent ) def test_namespaces(): if libxmp is None: raise SkipTest(libxmp_import_error) assert_equal(libxmp.consts.XMP_NS_DC, ns.dc) assert_equal(libxmp.consts.XMP_NS_RDF, ns.rdf) assert_equal(libxmp.consts.XMP_NS_TIFF, ns.tiff) assert_equal(libxmp.consts.XMP_NS_XMP, ns.xmp) assert_equal(libxmp.consts.XMP_NS_XMP_MM, ns.xmpmm) class test_metadata(): def test_empty(self): for backend in xmp_backends: with temporary.file() as xmp_file: exc = None try: meta = xmp.metadata(backend=backend) meta.write(xmp_file) xmp_file.flush() xmp_file.seek(0) except Exception, exc: pass yield self._test_empty_exiv2(xmp_file, exception=exc), tag_backend(backend), tag_exiv2 yield self._test_empty_libxmp(xmp_file, exception=exc), tag_backend(backend), tag_libxmp def _test_empty_exiv2(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception for line in run_exiv2(xmp_file.name, fail_ok=True): assert_equal(line, '') return test def _test_empty_libxmp(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception if libxmp is None: raise SkipTest(libxmp_import_error) import xml.etree.cElementTree as etree import cStringIO as io meta = libxmp.XMPMeta() meta.parse_from_str(xmp_file.read()) xml_meta = meta.serialize_to_str(omit_all_formatting=True, omit_packet_wrapper=True) xml_meta = io.StringIO(xml_meta) iterator = etree.iterparse(xml_meta, events=('start', 'end')) iterator = iter(iterator) # odd, but needed for Python 2.6 pop = lambda: next(iterator) event, element = pop() assert_equal(event, 'start') assert_equal(element.tag, '{adobe:ns:meta/}xmpmeta') event, element = pop() assert_equal(event, 'start') assert_equal(element.tag, '{%s}RDF' % ns.rdf) event, element = pop() assert_equal(event, 'start') assert_equal(element.tag, '{%s}Description' % ns.rdf) assert_equal(element.attrib['{%s}about' % ns.rdf], '') event, element = pop() assert_equal(event, 'end') event, element = pop() assert_equal(event, 'end') event, element = pop() assert_equal(event, 'end') try: event, element = pop() except StopIteration: event, element = None, None assert_true(event is None) return test def test_new(self): for backend in xmp_backends: with temporary.file() as xmp_file: exc = None try: meta = xmp.metadata(backend=backend) meta.update( media_type='image/x-test', internal_properties=[ ('test_int', 42), ('test_str', 'eggs'), ('test_bool', True), ] ) meta.write(xmp_file) xmp_file.flush() xmp_file.seek(0) except Exception, exc: pass yield self._test_new_exiv2(xmp_file, exception=exc), tag_backend(backend), tag_exiv2 yield self._test_new_libxmp(xmp_file, exception=exc), tag_backend(backend), tag_libxmp def _test_new_exiv2(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception output = run_exiv2(xmp_file.name) def pop(): return tuple(next(output).rstrip('\n').split(None, 1)) # Dublin Core: assert_equal(pop(), ('Xmp.dc.format', 'image/x-test')) # internal properties: assert_equal(pop(), ('Xmp.didjvu.test_bool', 'True')) assert_equal(pop(), ('Xmp.didjvu.test_int', '42')) assert_equal(pop(), ('Xmp.didjvu.test_str', 'eggs')) # XMP: key, metadata_date = pop() assert_rfc3339_timestamp(metadata_date) assert_equal(key, 'Xmp.xmp.MetadataDate') key, modify_date = pop() assert_equal(key, 'Xmp.xmp.ModifyDate') assert_rfc3339_timestamp(modify_date) assert_equal(metadata_date, modify_date) # XMP Media Management: assert_equal(pop(), ('Xmp.xmpMM.History', 'type="Seq"')) # - History[1]: assert_equal(pop(), ('Xmp.xmpMM.History[1]', 'type="Struct"')) assert_equal(pop(), ('Xmp.xmpMM.History[1]/stEvt:action', 'converted')) key, evt_uuid = pop() assert_correct_uuid(evt_uuid) assert_equal(key, 'Xmp.xmpMM.History[1]/stEvt:instanceID') assert_equal(pop(), ('Xmp.xmpMM.History[1]/stEvt:parameters', 'to image/x-test')) key, software_agent = pop() assert_equal(key, 'Xmp.xmpMM.History[1]/stEvt:softwareAgent') assert_correct_software_agent(software_agent) key, evt_date = pop() assert_equal((key, evt_date), ('Xmp.xmpMM.History[1]/stEvt:when', modify_date)) # - InstanceID: key, uuid = pop() assert_equal(key, 'Xmp.xmpMM.InstanceID') assert_correct_uuid(uuid) assert_equal(uuid, evt_uuid) try: line = pop() except StopIteration: line = None assert_true(line is None) return test def _test_new_libxmp(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception if libxmp is None: raise SkipTest(libxmp_import_error) meta = libxmp.XMPMeta() def get(namespace, key): return meta.get_property(namespace, key) meta.parse_from_str(xmp_file.read()) assert_equal(get(ns.dc, 'format'), 'image/x-test') mod_date = get(ns.xmp, 'ModifyDate') metadata_date = get(ns.xmp, 'MetadataDate') assert_equal(mod_date, metadata_date) uuid = get(ns.xmpmm, 'InstanceID') assert_correct_uuid(uuid) assert_equal(get(ns.xmpmm, 'History[1]/stEvt:action'), 'converted') software_agent = get(ns.xmpmm, 'History[1]/stEvt:softwareAgent') assert_correct_software_agent(software_agent) assert_equal(get(ns.xmpmm, 'History[1]/stEvt:parameters'), 'to image/x-test') assert_equal(get(ns.xmpmm, 'History[1]/stEvt:instanceID'), uuid) assert_equal(get(ns.xmpmm, 'History[1]/stEvt:when'), str(mod_date)) assert_equal(get(ns.didjvu, 'test_int'), '42') assert_equal(get(ns.didjvu, 'test_str'), 'eggs') assert_equal(get(ns.didjvu, 'test_bool'), 'True') return test def test_updated(self): image_path = os.path.join(os.path.dirname(__file__), 'example.png') for backend in xmp_backends: with temporary.file() as xmp_file: exc = None try: meta = xmp.metadata(backend=backend) meta.import_(image_path) meta.update( media_type='image/x-test', internal_properties=[ ('test_int', 42), ('test_str', 'eggs'), ('test_bool', True), ] ) meta.write(xmp_file) xmp_file.flush() xmp_file.seek(0) except Exception, exc: pass yield self._test_updated_exiv2(xmp_file, exception=exc), tag_backend(backend), tag_exiv2 yield self._test_updated_libxmp(xmp_file, exception=exc), tag_backend(backend), tag_libxmp _original_software_agent = 'scanhelper 0.2.4' _original_create_date = '2012-02-01T16:28:00+01:00' _original_uuid = 'uuid:a2686c01b50e4b6aab2cccdef40f6286' def _test_updated_exiv2(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception output = run_exiv2(xmp_file.name) def pop(): return tuple(next(output).rstrip('\n').split(None, 1)) # Dublin Core: assert_equal(pop(), ('Xmp.dc.format', 'image/x-test')) # internal properties: assert_equal(pop(), ('Xmp.didjvu.test_bool', 'True')) assert_equal(pop(), ('Xmp.didjvu.test_int', '42')) assert_equal(pop(), ('Xmp.didjvu.test_str', 'eggs')) # TIFF: assert_equal(pop(), ('Xmp.tiff.ImageHeight', '42')) assert_equal(pop(), ('Xmp.tiff.ImageWidth', '69')) # XMP: key, create_date = pop() assert_equal((key, create_date), ('Xmp.xmp.CreateDate', self._original_create_date)) assert_equal(pop(), ('Xmp.xmp.CreatorTool', self._original_software_agent)) key, metadata_date = pop() assert_equal(key, 'Xmp.xmp.MetadataDate') assert_rfc3339_timestamp(metadata_date) key, modify_date = pop() assert_equal(key, 'Xmp.xmp.ModifyDate') assert_rfc3339_timestamp(modify_date) assert_equal(metadata_date, modify_date) # XMP Media Management: assert_equal(pop(), ('Xmp.xmpMM.History', 'type="Seq"')) # - History[1]: assert_equal(pop(), ('Xmp.xmpMM.History[1]', 'type="Struct"')) assert_equal(pop(), ('Xmp.xmpMM.History[1]/stEvt:action', 'created')) key, original_uuid = pop() assert_equal(key, 'Xmp.xmpMM.History[1]/stEvt:instanceID') assert_correct_uuid(original_uuid) assert_equal(original_uuid, self._original_uuid) assert_equal(pop(), ('Xmp.xmpMM.History[1]/stEvt:softwareAgent', self._original_software_agent)) assert_equal(pop(), ('Xmp.xmpMM.History[1]/stEvt:when', create_date)) # - History[2] assert_equal(pop(), ('Xmp.xmpMM.History[2]', 'type="Struct"')) assert_equal(pop(), ('Xmp.xmpMM.History[2]/stEvt:action', 'converted')) key, evt_uuid = pop() assert_equal(key, 'Xmp.xmpMM.History[2]/stEvt:instanceID') assert_correct_uuid(evt_uuid) assert_equal(pop(), ('Xmp.xmpMM.History[2]/stEvt:parameters', 'from image/png to image/x-test')) key, software_agent = pop() assert_equal(key, 'Xmp.xmpMM.History[2]/stEvt:softwareAgent') assert_correct_software_agent(software_agent) assert_equal(pop(), ('Xmp.xmpMM.History[2]/stEvt:when', metadata_date)) # - InstanceID: key, uuid = pop() assert_equal(key, 'Xmp.xmpMM.InstanceID') assert_correct_uuid(evt_uuid) assert_equal(uuid, evt_uuid) assert_not_equal(uuid, original_uuid) try: line = pop() except StopIteration: line = None assert_true(line is None) return test def _test_updated_libxmp(self, xmp_file, exception=None): def test(*dummy): if exception is not None: raise exception if libxmp is None: raise SkipTest(libxmp_import_error) meta = libxmp.XMPMeta() def get(namespace, key): return meta.get_property(namespace, key) meta.parse_from_str(xmp_file.read()) assert_equal(get(ns.dc, 'format'), 'image/x-test') assert_equal(get(ns.tiff, 'ImageWidth'), '69') assert_equal(get(ns.tiff, 'ImageHeight'), '42') assert_equal(get(ns.xmp, 'CreatorTool'), self._original_software_agent) create_date = get(ns.xmp, 'CreateDate') assert_equal(create_date, self._original_create_date) mod_date = get(ns.xmp, 'ModifyDate') assert_true(mod_date > create_date) metadata_date = get(ns.xmp, 'MetadataDate') assert_equal(mod_date, metadata_date) uuid = get(ns.xmpmm, 'InstanceID') assert_correct_uuid(uuid) # History[1] assert_equal(get(ns.xmpmm, 'History[1]/stEvt:action'), 'created') assert_equal(get(ns.xmpmm, 'History[1]/stEvt:softwareAgent'), self._original_software_agent) original_uuid = get(ns.xmpmm, 'History[1]/stEvt:instanceID') assert_correct_uuid(original_uuid) assert_equal(original_uuid, self._original_uuid) assert_not_equal(uuid, original_uuid) assert_equal(get(ns.xmpmm, 'History[1]/stEvt:when'), create_date) # History[2] assert_equal(get(ns.xmpmm, 'History[2]/stEvt:action'), 'converted') software_agent = get(ns.xmpmm, 'History[2]/stEvt:softwareAgent') assert_correct_software_agent(software_agent) assert_equal(get(ns.xmpmm, 'History[2]/stEvt:parameters'), 'from image/png to image/x-test') assert_equal(get(ns.xmpmm, 'History[2]/stEvt:instanceID'), uuid) assert_equal(get(ns.xmpmm, 'History[2]/stEvt:when'), mod_date) # internal properties assert_equal(get(ns.didjvu, 'test_int'), '42') assert_equal(get(ns.didjvu, 'test_str'), 'eggs') assert_equal(get(ns.didjvu, 'test_bool'), 'True') return test # vim:ts=4 sw=4 et didjvu-0.2.7/tests/test_timestamp.py0000644000000000000000000000145311754247211017530 0ustar rootroot00000000000000# encoding=UTF-8 # Copyright © 2012 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. from . common import assert_rfc3339_timestamp from lib import timestamp def test_now(): result = timestamp.now() assert_rfc3339_timestamp(str(result)) def test_explicit_construct(): result = timestamp.Timestamp(100000) assert_rfc3339_timestamp(str(result)) # vim:ts=4 sw=4 et didjvu-0.2.7/setup.py0000644000000000000000000001121112120174431014444 0ustar rootroot00000000000000#!/usr/bin/python # encoding=UTF-8 # Copyright © 2009, 2010, 2011, 2012, 2013 Jakub Wilk # # 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; version 2 dated June, 1991. # # 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. ''' *didjvu* uses the `Gamera `_ framework to separate foreground/background layers, which it can then encode into a `DjVu `_ file. ''' from __future__ import with_statement classifiers = ''' Development Status :: 4 - Beta Environment :: Console Intended Audience :: End Users/Desktop License :: OSI Approved :: GNU General Public License (GPL) Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 2 Topic :: Text Processing Topic :: Multimedia :: Graphics '''.strip().splitlines() import glob import os import re import distutils.core from distutils.command.build import build as distutils_build from distutils.command.clean import clean as distutils_clean from distutils.command.sdist import sdist as distutils_sdist from lib.version import __version__ class build_doc(distutils_build): description = 'build documentation' _url_regex = re.compile( r'^(\\%http://.*)', re.MULTILINE ) _date_regex = re.compile( '"(?P[0-9]{2})/(?P[0-9]{2})/(?P[0-9]{4})"' ) def build_man(self, manname, commandline): self.spawn(commandline) with open(manname, 'r+') as file: contents = file.read() # Format URLs consistently: contents = self._url_regex.sub( lambda m: r'\m[blue]\fI%s\fR\m[]' % m.groups(), contents, ) # Use RFC 3339 date format: contents = self._date_regex.sub( lambda m: '%(year)s-%(month)s-%(day)s' % m.groupdict(), contents ) file.seek(0) file.truncate() file.write(contents) def run(self): for xmlname in glob.glob(os.path.join('doc', '*.xml')): manname = os.path.splitext(xmlname)[0] + '.1' command = [ 'xsltproc', '--nonet', '--param', 'man.authors.section.enabled', '0', '--param', 'man.charmap.use.subset', '0', '--param', 'man.font.links', '"I"', '--output', 'doc/', 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl', xmlname, ] self.make_file([xmlname], manname, self.build_man, [manname, command]) distutils_build.sub_commands[:0] = [('build_doc', None)] class clean(distutils_clean): def run(self): distutils_clean.run(self) if not self.all: return for manname in glob.glob(os.path.join('doc', '*.1')): with open(manname, 'r') as file: stamp = file.readline() if stamp != sdist.manpage_stamp: self.execute(os.unlink, [manname], 'removing %s' % manname) class sdist(distutils_sdist): manpage_stamp = '''.\\" [created by setup.py sdist]\n''' def run(self): self.run_command('build_doc') return distutils_sdist.run(self) def _rewrite_manpage(self, manname): with open(manname, 'r') as file: contents = file.read() os.unlink(manname) with open(manname, 'w') as file: file.write(self.manpage_stamp) file.write(contents) def make_release_tree(self, base_dir, files): distutils_sdist.make_release_tree(self, base_dir, files) for manname in glob.glob(os.path.join(base_dir, 'doc', '*.1')): self.execute(self._rewrite_manpage, [manname], 'rewriting %s' % manname) distutils.core.setup( name = 'didjvu', version = __version__, license = 'GNU GPL 2', description = 'DjVu encoder with foreground/background separation', long_description = __doc__.strip(), classifiers = classifiers, url = 'http://jwilk.net/software/didjvu', author = 'Jakub Wilk', author_email = 'jwilk@jwilk.net', packages = ['didjvu'], package_dir = dict(didjvu='lib'), scripts = ['didjvu'], data_files = [('share/man/man1', glob.glob('doc/*.1'))], cmdclass = dict( build_doc=build_doc, clean=clean, sdist=sdist, ), ) # vim:ts=4 sw=4 et didjvu-0.2.7/didjvu.py0000644000000000000000000000025611772523267014621 0ustar rootroot00000000000000# encoding=UTF-8 ''' helper module that allows using the command-line tool without installing it ''' import sys import lib sys.modules['didjvu'] = lib # vim:ts=4 sw=4 et didjvu-0.2.7/COPYING0000644000000000000000000004325412010167027014002 0ustar rootroot00000000000000 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. didjvu-0.2.7/PKG-INFO0000644000000000000000000000154612120174731014044 0ustar rootroot00000000000000Metadata-Version: 1.1 Name: didjvu Version: 0.2.7 Summary: DjVu encoder with foreground/background separation Home-page: http://jwilk.net/software/didjvu Author: Jakub Wilk Author-email: jwilk@jwilk.net License: GNU GPL 2 Description: *didjvu* uses the `Gamera `_ framework to separate foreground/background layers, which it can then encode into a `DjVu `_ file. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Topic :: Text Processing Classifier: Topic :: Multimedia :: Graphics