1# encoding=UTF-8
2
3# Copyright © 2007-2021 Jakub Wilk <jwilk@jwilk.net>
4#
5# This file is part of python-djvulibre.
6#
7# python-djvulibre is free software; you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 2 as published by
9# the Free Software Foundation.
10#
11# python-djvulibre is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14# more details.
15
16import array
17import errno
18import os
19import re
20import shutil
21import sys
22import tempfile
23import warnings
24
25if sys.version_info >= (3, 2):
26    import subprocess
27else:
28    try:
29        import subprocess32 as subprocess
30    except ImportError as exc:
31        import subprocess
32        msg = str(exc)
33        warnings.warn(msg, category=RuntimeWarning)  # subprocess is not thread-safe in Python < 3.2
34        del msg, exc
35
36from djvu.decode import (
37    AffineTransform,
38    Context,
39    DDJVU_VERSION,
40    DOCUMENT_TYPE_BUNDLED,
41    DOCUMENT_TYPE_SINGLE_PAGE,
42    DocInfoMessage,
43    Document,
44    DocumentAnnotations,
45    DocumentDecodingJob,
46    DocumentOutline,
47    ErrorMessage,
48    File,
49    FileUri,
50    Hyperlinks,
51    Job,
52    JobFailed,
53    JobOK,
54    Message,
55    Metadata,
56    NewStreamMessage,
57    NotAvailable,
58    PAGE_TYPE_BITONAL,
59    Page,
60    PageAnnotations,
61    PageJob,
62    PageText,
63    PixelFormat,
64    PixelFormatGrey,
65    PixelFormatPackedBits,
66    PixelFormatPalette,
67    PixelFormatRgb,
68    PixelFormatRgbMask,
69    RENDER_COLOR,
70    SaveJob,
71    Stream,
72    TEXT_DETAILS_ALL,
73    TEXT_DETAILS_CHARACTER,
74    TEXT_DETAILS_COLUMN,
75    TEXT_DETAILS_LINE,
76    TEXT_DETAILS_PAGE,
77    TEXT_DETAILS_PARAGRAPH,
78    TEXT_DETAILS_REGION,
79    TEXT_DETAILS_WORD,
80    ThumbnailMessage,
81    __version__,
82)
83from djvu.sexpr import (
84    Expression,
85    Symbol,
86)
87
88from tools import (
89    TestCase,
90    testcase,
91    assert_equal,
92    assert_false,
93    assert_is,
94    assert_is_instance,
95    assert_list_equal,
96    assert_multi_line_equal,
97    assert_raises,
98    assert_raises_regex,
99    assert_raises_str,
100    assert_repr,
101    assert_true,
102    SkipTest,
103    skip_unless_c_messages,
104    skip_unless_command_exists,
105    skip_unless_translation_exists,
106    get_changelog_version,
107    interim_locale,
108    locale_encoding,
109    wildcard_import,
110    # Python 2/3 compat:
111    b,
112    py3k,
113    u,
114    unicode,
115)
116
117images = os.path.join(os.path.dirname(__file__), 'images', '')
118
119if sys.version_info >= (3, 2):
120    array_tobytes = array.array.tobytes
121else:
122    array_tobytes = array.array.tostring
123
124if sys.version_info < (2, 7):
125    memoryview = None  # make pyflakes happy
126
127def run(*cmd, **kwargs):
128    stdin = kwargs.pop('stdin', None)
129    env = dict(os.environ)
130    for key, value in kwargs.items():
131        if key.isupper():
132            env[key] = value
133            continue
134        raise TypeError('{key!r} is an invalid keyword argument for this function'.format(key=key))
135    kwargs = dict(
136        stdout=subprocess.PIPE,
137        stderr=subprocess.PIPE,
138        env=env,
139    )
140    if stdin is not None:
141        kwargs.update(stdin=subprocess.PIPE)
142    child = subprocess.Popen(list(cmd), **kwargs)
143    (stdout, stderr) = child.communicate(stdin)
144    if child.returncode != 0:
145        raise subprocess.CalledProcessError(child.returncode, cmd[0])
146    return (stdout, stderr)
147
148def create_djvu(commands='', sexpr=''):
149    skip_unless_command_exists('djvused')
150    if sexpr:
151        commands += '\nset-ant\n{sexpr}\n.\n'.format(sexpr=sexpr)
152    file = tempfile.NamedTemporaryFile(prefix='test', suffix='djvu')
153    file.seek(0)
154    file.write(
155        b'\x41\x54\x26\x54\x46\x4F\x52\x4D\x00\x00\x00\x22\x44\x4A\x56\x55'
156        b'\x49\x4E\x46\x4F\x00\x00\x00\x0A\x00\x01\x00\x01\x18\x00\x2C\x01'
157        b'\x16\x01\x53\x6A\x62\x7A\x00\x00\x00\x04\xBC\x73\x1B\xD7'
158    )
159    file.flush()
160    (stdout, stderr) = run('djvused', '-s', file.name, stdin=commands.encode(locale_encoding))
161    assert_equal(stdout, ''.encode(locale_encoding))
162    assert_equal(stderr, ''.encode(locale_encoding))
163    return file
164
165@testcase
166def test_context_cache():
167    context = Context()
168    assert_equal(context.cache_size, 10 << 20)
169    for n in -100, 0, 1 << 31:
170        with assert_raises_str(ValueError, '0 < cache_size < (2 ** 31) must be satisfied'):
171            context.cache_size = n
172    with assert_raises_str(ValueError, '0 < cache_size < (2 ** 31) must be satisfied'):
173        context.cache_size = 0
174    n = 1
175    while n < (1 << 31):
176        context.cache_size = n
177        assert_equal(context.cache_size, n)
178        n = (n + 1) * 2 - 1
179    context.clear_cache()
180
181class test_documents(TestCase):
182
183    def test_bad_new(self):
184        with assert_raises_str(TypeError, "cannot create 'djvu.decode.Document' instances"):
185            Document()
186
187    def test_nonexistent(self):
188        path = '__nonexistent__'
189        try:
190            os.stat(path)
191        except OSError as ex:
192            c_message = ex.args[1]
193        else:
194            raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path)
195        c_message.encode('ASCII')
196        skip_unless_c_messages()
197        context = Context()
198        with assert_raises(JobFailed):
199            context.new_document(FileUri(path))
200        message = context.get_message()
201        assert_equal(type(message), ErrorMessage)
202        assert_equal(type(message.message), unicode)
203        assert_equal(
204            message.message,
205            "[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message)
206        )
207        assert_equal(str(message), message.message)
208        assert_equal(unicode(message), message.message)
209
210    def test_nonexistent_ja(self):
211        skip_unless_c_messages()
212        skip_unless_translation_exists('ja_JP.UTF-8')
213        path = '__nonexistent__'
214        context = Context()
215        try:
216            with interim_locale(LC_ALL='ja_JP.UTF-8'):
217                os.stat(path)
218        except OSError as ex:
219            c_message = ex.args[1]
220        else:
221            raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path)
222        try:
223            c_message.encode('ASCII')
224        except UnicodeError:
225            pass
226        else:
227            raise AssertionError(
228                'ja_JP error message is ASCII-only: {msg!r}'.format(msg=c_message)
229            )
230        with interim_locale(LC_ALL='ja_JP.UTF-8'):
231            with assert_raises(JobFailed):
232                context.new_document(FileUri(path))
233            message = context.get_message()
234            assert_equal(type(message), ErrorMessage)
235            assert_equal(type(message.message), unicode)
236            assert_equal(
237                message.message,
238                u("[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message))
239            )
240            assert_equal(
241                str(message),
242                "[1-11711] Failed to open '{path}': {msg}.".format(path=path, msg=c_message)
243            )
244            assert_equal(unicode(message), message.message)
245
246    def test_new_document(self):
247        context = Context()
248        document = context.new_document(FileUri(images + 'test1.djvu'))
249        assert_equal(type(document), Document)
250        message = document.get_message()
251        assert_equal(type(message), DocInfoMessage)
252        assert_true(document.decoding_done)
253        assert_false(document.decoding_error)
254        assert_equal(document.decoding_status, JobOK)
255        assert_equal(document.type, DOCUMENT_TYPE_SINGLE_PAGE)
256        assert_equal(len(document.pages), 1)
257        assert_equal(len(document.files), 1)
258        decoding_job = document.decoding_job
259        assert_true(decoding_job.is_done)
260        assert_false(decoding_job.is_error)
261        assert_equal(decoding_job.status, JobOK)
262        file = document.files[0]
263        assert_is(type(file), File)
264        assert_is(file.document, document)
265        assert_is(file.get_info(), None)
266        assert_equal(file.type, 'P')
267        assert_equal(file.n_page, 0)
268        page = file.page
269        assert_equal(type(page), Page)
270        assert_is(page.document, document)
271        assert_equal(page.n, 0)
272        assert_is(file.size, None)
273        assert_equal(file.id, u('test1.djvu'))
274        assert_equal(type(file.id), unicode)
275        assert_equal(file.name, u('test1.djvu'))
276        assert_equal(type(file.name), unicode)
277        assert_equal(file.title, u('test1.djvu'))
278        assert_equal(type(file.title), unicode)
279        dump = document.files[0].dump
280        assert_equal(type(dump), unicode)
281        assert_equal(
282            [line for line in dump.splitlines()], [
283                u('  FORM:DJVU [83] '),
284                u('    INFO [10]         DjVu 64x48, v24, 300 dpi, gamma=2.2'),
285                u('    Sjbz [53]         JB2 bilevel data'),
286            ]
287        )
288        page = document.pages[0]
289        assert_equal(type(page), Page)
290        assert_is(page.document, document)
291        assert_is(page.get_info(), None)
292        assert_equal(page.width, 64)
293        assert_equal(page.height, 48)
294        assert_equal(page.size, (64, 48))
295        assert_equal(page.dpi, 300)
296        assert_equal(page.rotation, 0)
297        assert_equal(page.version, 24)
298        file = page.file
299        assert_equal(type(file), File)
300        assert_equal(file.id, u('test1.djvu'))
301        assert_equal(type(file.id), unicode)
302        dump = document.files[0].dump
303        assert_equal(type(dump), unicode)
304        assert_equal(
305            [line for line in dump.splitlines()], [
306                u('  FORM:DJVU [83] '),
307                u('    INFO [10]         DjVu 64x48, v24, 300 dpi, gamma=2.2'),
308                u('    Sjbz [53]         JB2 bilevel data'),
309            ]
310        )
311        assert_is(document.get_message(wait=False), None)
312        assert_is(context.get_message(wait=False), None)
313        with assert_raises_str(IndexError, 'file number out of range'):
314            document.files[-1].get_info()
315        assert_is(document.get_message(wait=False), None)
316        assert_is(context.get_message(wait=False), None)
317        with assert_raises_str(IndexError, 'page number out of range'):
318            document.pages[-1]
319        with assert_raises_str(IndexError, 'page number out of range'):
320            document.pages[1]
321        assert_is(document.get_message(wait=False), None)
322        assert_is(context.get_message(wait=False), None)
323
324    def test_save(self):
325        skip_unless_command_exists('djvudump')
326        context = Context()
327        original_filename = images + 'test0.djvu'
328        document = context.new_document(FileUri(original_filename))
329        message = document.get_message()
330        assert_equal(type(message), DocInfoMessage)
331        assert_true(document.decoding_done)
332        assert_false(document.decoding_error)
333        assert_equal(document.decoding_status, JobOK)
334        assert_equal(document.type, DOCUMENT_TYPE_BUNDLED)
335        assert_equal(len(document.pages), 2)
336        assert_equal(len(document.files), 3)
337        (stdout0, stderr0) = run('djvudump', original_filename, LC_ALL='C')
338        assert_equal(stderr0, b'')
339        stdout0 = stdout0.replace(b'\r\n', b'\n')
340        tmpdir = tempfile.mkdtemp()
341        try:
342            tmp = open(os.path.join(tmpdir, 'tmp.djvu'), 'wb')
343            job = document.save(tmp)
344            assert_equal(type(job), SaveJob)
345            assert_true(job.is_done)
346            assert_false(job.is_error)
347            tmp.close()
348            (stdout, stderr) = run('djvudump', tmp.name, LC_ALL='C')
349            assert_equal(stderr, b'')
350            stdout = stdout.replace(b'\r\n', b'\n')
351            assert_equal(stdout, stdout0)
352        finally:
353            shutil.rmtree(tmpdir)
354            tmp = None
355        tmpdir = tempfile.mkdtemp()
356        try:
357            tmp = open(os.path.join(tmpdir, 'tmp.djvu'), 'wb')
358            job = document.save(tmp, pages=(0,))
359            assert_equal(type(job), SaveJob)
360            assert_true(job.is_done)
361            assert_false(job.is_error)
362            tmp.close()
363            stdout, stderr = run('djvudump', tmp.name, LC_ALL='C')
364            assert_equal(stderr, b'')
365            stdout = stdout.replace(b'\r\n', b'\n')
366            stdout0 = stdout0.split(b'\n')
367            stdout = stdout.split(b'\n')
368            stdout[4] = stdout[4].replace(b' (1)', b'')
369            assert_equal(len(stdout), 10)
370            assert_equal(stdout[3:-1], stdout0[4:10])
371            assert_equal(stdout[-1], b'')
372        finally:
373            shutil.rmtree(tmpdir)
374            tmp = None
375        tmpdir = tempfile.mkdtemp()
376        try:
377            tmpfname = os.path.join(tmpdir, 'index.djvu')
378            job = document.save(indirect=tmpfname)
379            assert_equal(type(job), SaveJob)
380            assert_true(job.is_done)
381            assert_false(job.is_error)
382            (stdout, stderr) = run('djvudump', tmpfname, LC_ALL='C')
383            assert_equal(stderr, b'')
384            stdout = stdout.replace(b'\r\n', b'\n')
385            stdout = stdout.split(b'\n')
386            stdout0 = (
387                [b'      shared_anno.iff -> shared_anno.iff'] +
388                [b('      p{n:04}.djvu -> p{n:04}.djvu'.format(n=n)) for n in range(1, 3)]
389            )
390            assert_equal(len(stdout), 7)
391            assert_equal(stdout[2:-2], stdout0)
392            assert_equal(stdout[-1], b'')
393        finally:
394            shutil.rmtree(tmpdir)
395        tmpdir = tempfile.mkdtemp()
396        try:
397            tmpfname = os.path.join(tmpdir, 'index.djvu')
398            job = document.save(indirect=tmpfname, pages=(0,))
399            assert_equal(type(job), SaveJob)
400            assert_true(job.is_done)
401            assert_false(job.is_error)
402            (stdout, stderr) = run('djvudump', tmpfname, LC_ALL='C')
403            stdout = stdout.replace(b'\r\n', b'\n')
404            assert_equal(stderr, b'')
405            stdout = stdout.split(b'\n')
406            assert_equal(len(stdout), 5)
407            assert_equal(stdout[2], b'      shared_anno.iff -> shared_anno.iff')
408            assert_equal(stdout[3], b'      p0001.djvu -> p0001.djvu')
409            assert_equal(stdout[-1], b'')
410        finally:
411            shutil.rmtree(tmpdir)
412
413    def test_export_ps(self):
414        skip_unless_command_exists('ps2ascii')
415        context = Context()
416        document = context.new_document(FileUri(images + 'test0.djvu'))
417        message = document.get_message()
418        assert_equal(type(message), DocInfoMessage)
419        assert_true(document.decoding_done)
420        assert_false(document.decoding_error)
421        assert_equal(document.decoding_status, JobOK)
422        assert_equal(document.type, DOCUMENT_TYPE_BUNDLED)
423        assert_equal(len(document.pages), 2)
424        assert_equal(len(document.files), 3)
425        with tempfile.NamedTemporaryFile() as tmp:
426            job = document.export_ps(tmp.file)
427            assert_equal(type(job), SaveJob)
428            assert_true(job.is_done)
429            assert_false(job.is_error)
430            stdout, stderr = run('ps2ascii', tmp.name, LC_ALL='C')
431            assert_equal(stderr, b'')
432            stdout = re.sub(br'[\x00\s]+', b' ', stdout)
433            assert_equal(stdout, b' ')
434        with tempfile.NamedTemporaryFile() as tmp:
435            job = document.export_ps(tmp.file, pages=(0,), text=True)
436            assert_equal(type(job), SaveJob)
437            assert_true(job.is_done)
438            assert_false(job.is_error)
439            stdout, stderr = run('ps2ascii', tmp.name, LC_ALL='C')
440            assert_equal(stderr, b'')
441            stdout = stdout.decode('ASCII')
442            stdout = re.sub(r'[\x00\s]+', ' ', stdout)
443            stdout = ' '.join(stdout.split()[:3])
444            expected = '1 Lorem ipsum'
445            assert_multi_line_equal(stdout, expected)
446
447class test_pixel_formats(TestCase):
448
449    def test_bad_new(self):
450        with assert_raises_str(TypeError, "cannot create 'djvu.decode.PixelFormat' instances"):
451            PixelFormat()
452
453    def test_rgb(self):
454        pf = PixelFormatRgb()
455        assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'RGB', bpp = 24)")
456        pf = PixelFormatRgb('RGB')
457        assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'RGB', bpp = 24)")
458        pf = PixelFormatRgb('BGR')
459        assert_repr(pf, "djvu.decode.PixelFormatRgb(byte_order = 'BGR', bpp = 24)")
460
461    def test_rgb_mask(self):
462        pf = PixelFormatRgbMask(0xFF, 0xF00, 0x1F000, 0, 16)
463        assert_repr(pf, "djvu.decode.PixelFormatRgbMask(red_mask = 0x00ff, green_mask = 0x0f00, blue_mask = 0xf000, xor_value = 0x0000, bpp = 16)")
464        pf = PixelFormatRgbMask(0xFF000000, 0xFF0000, 0xFF00, 0xFF, 32)
465        assert_repr(pf, "djvu.decode.PixelFormatRgbMask(red_mask = 0xff000000, green_mask = 0x00ff0000, blue_mask = 0x0000ff00, xor_value = 0x000000ff, bpp = 32)")
466
467    def test_grey(self):
468        pf = PixelFormatGrey()
469        assert_repr(pf, "djvu.decode.PixelFormatGrey(bpp = 8)")
470
471    def test_palette(self):
472        with assert_raises(KeyError) as ecm:
473            pf = PixelFormatPalette({})
474        assert_equal(
475            ecm.exception.args,
476            ((0, 0, 0),)
477        )
478        data = dict(((i, j, k), i + 7 * j + 37 + k) for i in range(6) for j in range(6) for k in range(6))
479        pf = PixelFormatPalette(data)
480        data_repr = ', '.join(
481            '{k!r}: 0x{v:02x}'.format(k=k, v=v) for k, v in sorted(data.items())
482        )
483        assert_equal(
484            repr(pf),
485            'djvu.decode.PixelFormatPalette({{{data}}}, bpp = 8)'.format(data=data_repr)
486        )
487
488    def test_packed_bits(self):
489        pf = PixelFormatPackedBits('<')
490        assert_repr(pf, "djvu.decode.PixelFormatPackedBits('<')")
491        assert_equal(pf.bpp, 1)
492        pf = PixelFormatPackedBits('>')
493        assert_repr(pf, "djvu.decode.PixelFormatPackedBits('>')")
494        assert_equal(pf.bpp, 1)
495
496class test_page_jobs(TestCase):
497
498    def test_bad_new(self):
499        with assert_raises_str(TypeError, "cannot create 'djvu.decode.PageJob' instances"):
500            PageJob()
501
502    def test_decode(self):
503        context = Context()
504        document = context.new_document(FileUri(images + 'test1.djvu'))
505        message = document.get_message()
506        assert_equal(type(message), DocInfoMessage)
507        page_job = document.pages[0].decode()
508        assert_true(page_job.is_done)
509        assert_equal(type(page_job), PageJob)
510        assert_true(page_job.is_done)
511        assert_false(page_job.is_error)
512        assert_equal(page_job.status, JobOK)
513        assert_equal(page_job.width, 64)
514        assert_equal(page_job.height, 48)
515        assert_equal(page_job.size, (64, 48))
516        assert_equal(page_job.dpi, 300)
517        assert_equal(page_job.gamma, 2.2)
518        assert_equal(page_job.version, 24)
519        assert_equal(page_job.type, PAGE_TYPE_BITONAL)
520        assert_equal((page_job.rotation, page_job.initial_rotation), (0, 0))
521        with assert_raises_str(ValueError, 'rotation must be equal to 0, 90, 180, or 270'):
522            page_job.rotation = 100
523        page_job.rotation = 180
524        assert_equal((page_job.rotation, page_job.initial_rotation), (180, 0))
525        del page_job.rotation
526        assert_equal((page_job.rotation, page_job.initial_rotation), (0, 0))
527
528        with assert_raises_str(ValueError, 'page_rect width/height must be a positive integer'):
529            page_job.render(RENDER_COLOR, (0, 0, -1, -1), (0, 0, 10, 10), PixelFormatRgb())
530
531        with assert_raises_str(ValueError, 'render_rect width/height must be a positive integer'):
532            page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, -1, -1), PixelFormatRgb())
533
534        with assert_raises_str(ValueError, 'render_rect must be inside page_rect'):
535            page_job.render(RENDER_COLOR, (0, 0, 10, 10), (2, 2, 10, 10), PixelFormatRgb())
536
537        with assert_raises_str(ValueError, 'row_alignment must be a positive integer'):
538            page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 10, 10), PixelFormatRgb(), -1)
539
540        with assert_raises_regex(MemoryError, r'\AUnable to allocate [0-9]+ bytes for an image memory\Z'):
541            x = int((sys.maxsize // 2) ** 0.5)
542            page_job.render(RENDER_COLOR, (0, 0, x, x), (0, 0, x, x), PixelFormatRgb(), 8)
543
544        s = page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1)
545        assert_equal(s, b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEF\xFF\xFF\xFF\xA4\xFF\xFF\xFF\xB8')
546
547        buffer = array.array('B', b'\0')
548        with assert_raises_str(ValueError, 'Image buffer is too small (16 > 1)'):
549            page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1, buffer)
550
551        buffer = array.array('B', b'\0' * 16)
552        assert_is(page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 4, 4), PixelFormatGrey(), 1, buffer), buffer)
553        s = array_tobytes(buffer)
554        assert_equal(s, b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEF\xFF\xFF\xFF\xA4\xFF\xFF\xFF\xB8')
555
556        buffer = array.array('I', [0] * 4)
557        pixel_format = PixelFormatRgbMask(0xFF0000, 0xFF00, 0xFF, bpp=32)
558        assert_is(page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 2, 2), pixel_format, 1, buffer), buffer)
559        s = array_tobytes(buffer)
560        assert_equal(s, b'\xFF\xFF\xFF\x00' * 4)
561
562        if sys.version_info >= (3, 3):
563            buffer = bytearray(16)
564            memview = memoryview(buffer).cast('I', shape=(2, 2))
565            assert_is(page_job.render(RENDER_COLOR, (0, 0, 10, 10), (0, 0, 2, 2), pixel_format, 1, memview), memview)
566            s = bytes(buffer)
567            assert_equal(s, b'\xFF\xFF\xFF\x00' * 4)
568
569class test_thumbnails(TestCase):
570
571    def test(self):
572        context = Context()
573        document = context.new_document(FileUri(images + 'test1.djvu'))
574        message = document.get_message()
575        assert_equal(type(message), DocInfoMessage)
576        thumbnail = document.pages[0].thumbnail
577        assert_equal(thumbnail.status, JobOK)
578        assert_equal(thumbnail.calculate(), JobOK)
579        message = document.get_message()
580        assert_equal(type(message), ThumbnailMessage)
581        assert_equal(message.thumbnail.page.n, 0)
582        (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), dry_run=True)
583        assert_equal((w, h, r), (5, 3, 5))
584        assert_is(pixels, None)
585        (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey())
586        assert_equal((w, h, r), (5, 3, 5))
587        assert_equal(pixels[:15], b'\xFF\xEB\xA7\xF2\xFF\xFF\xBF\x86\xBE\xFF\xFF\xE7\xD6\xE7\xFF')
588        buffer = array.array('B', b'\0')
589        with assert_raises_str(ValueError, 'Image buffer is too small (25 > 1)'):
590            (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), buffer=buffer)
591        buffer = array.array('B', b'\0' * 25)
592        (w, h, r), pixels = thumbnail.render((5, 5), PixelFormatGrey(), buffer=buffer)
593        assert_is(pixels, buffer)
594        s = array_tobytes(buffer[:15])
595        assert_equal(s, b'\xFF\xEB\xA7\xF2\xFF\xFF\xBF\x86\xBE\xFF\xFF\xE7\xD6\xE7\xFF')
596
597@testcase
598def test_jobs():
599
600    with assert_raises_str(TypeError, "cannot create 'djvu.decode.Job' instances"):
601        Job()
602
603    with assert_raises_str(TypeError, "cannot create 'djvu.decode.DocumentDecodingJob' instances"):
604        DocumentDecodingJob()
605
606class test_affine_transforms(TestCase):
607
608    def test_bad_args(self):
609        with assert_raises_str(ValueError, 'need more than 2 values to unpack'):
610            AffineTransform((1, 2), (3, 4, 5))
611
612    def test1(self):
613        af = AffineTransform((0, 0, 10, 10), (17, 42, 42, 100))
614        assert_equal(type(af), AffineTransform)
615        assert_equal(af((0, 0)), (17, 42))
616        assert_equal(af((0, 10)), (17, 142))
617        assert_equal(af((10, 0)), (59, 42))
618        assert_equal(af((10, 10)), (59, 142))
619        assert_equal(af((0, 0, 10, 10)), (17, 42, 42, 100))
620        assert_equal(af(x for x in (0, 0, 10, 10)), (17, 42, 42, 100))
621        assert_equal(af.apply((123, 456)), af((123, 456)))
622        assert_equal(af.apply((12, 34, 56, 78)), af((12, 34, 56, 78)))
623        assert_equal(af.inverse((17, 42)), (0, 0))
624        assert_equal(af.inverse((17, 142)), (0, 10))
625        assert_equal(af.inverse((59, 42)), (10, 0))
626        assert_equal(af.inverse((59, 142)), (10, 10))
627        assert_equal(af.inverse((17, 42, 42, 100)), (0, 0, 10, 10))
628        assert_equal(af.inverse(x for x in (17, 42, 42, 100)), (0, 0, 10, 10))
629        assert_equal(af.inverse(af((234, 567))), (234, 567))
630        assert_equal(af.inverse(af((23, 45, 67, 78))), (23, 45, 67, 78))
631
632class test_messages(TestCase):
633
634    def test_bad_new(self):
635        with assert_raises_str(TypeError, "cannot create 'djvu.decode.Message' instances"):
636            Message()
637
638class test_streams(TestCase):
639
640    def test_bad_new(self):
641        with assert_raises_str(TypeError, "Argument 'document' has incorrect type (expected djvu.decode.Document, got NoneType)"):
642            Stream(None, 42)
643
644    def test(self):
645        context = Context()
646        document = context.new_document('dummy://dummy.djvu')
647        message = document.get_message()
648        assert_equal(type(message), NewStreamMessage)
649        assert_equal(message.name, 'dummy.djvu')
650        assert_equal(message.uri, 'dummy://dummy.djvu')
651        assert_equal(type(message.stream), Stream)
652        with assert_raises(NotAvailable):
653            document.outline.sexpr
654        with assert_raises(NotAvailable):
655            document.annotations.sexpr
656        with assert_raises(NotAvailable):
657            document.pages[0].text.sexpr
658        with assert_raises(NotAvailable):
659            document.pages[0].annotations.sexpr
660        try:
661            with open(images + 'test1.djvu', 'rb') as fp:
662                message.stream.write(fp.read())
663        finally:
664            message.stream.close()
665        with assert_raises_str(IOError, 'I/O operation on closed file'):
666            message.stream.write(b'eggs')
667        message = document.get_message()
668        assert_equal(type(message), DocInfoMessage)
669        outline = document.outline
670        outline.wait()
671        x = outline.sexpr
672        assert_equal(x, Expression([]))
673        anno = document.annotations
674        anno.wait()
675        x = anno.sexpr
676        assert_equal(x, Expression([]))
677        text = document.pages[0].text
678        text.wait()
679        x = text.sexpr
680        assert_equal(x, Expression([]))
681        anno = document.pages[0].annotations
682        anno.wait()
683        x = anno.sexpr
684        assert_equal(x, Expression([]))
685
686@testcase
687def test_metadata():
688
689    model_metadata = {
690        'English': 'eggs',
691        u('Русский'): u('яйца'),
692    }
693    meta = '\n'.join(u('|{k}| {v}').format(k=k, v=v) for k, v in model_metadata.items())
694    test_script = u('set-meta\n{meta}\n.\n').format(meta=meta)
695    try:
696        test_file = create_djvu(test_script)
697    except UnicodeEncodeError:
698        raise SkipTest('you need to run this test with LC_CTYPE=C or LC_CTYPE=<lang>.UTF-8')
699    try:
700        context = Context()
701        document = context.new_document(FileUri(test_file.name))
702        message = document.get_message()
703        assert_equal(type(message), DocInfoMessage)
704        annotations = document.annotations
705        assert_equal(type(annotations), DocumentAnnotations)
706        annotations.wait()
707        metadata = annotations.metadata
708        assert_equal(type(metadata), Metadata)
709        assert_equal(len(metadata), len(model_metadata))
710        assert_equal(sorted(metadata), sorted(model_metadata))
711        if not py3k:
712            assert_equal(sorted(metadata.iterkeys()), sorted(model_metadata.iterkeys()))
713        assert_equal(sorted(metadata.keys()), sorted(model_metadata.keys()))
714        if not py3k:
715            assert_equal(sorted(metadata.itervalues()), sorted(model_metadata.itervalues()))
716        assert_equal(sorted(metadata.values()), sorted(model_metadata.values()))
717        if not py3k:
718            assert_equal(sorted(metadata.iteritems()), sorted(model_metadata.iteritems()))
719        assert_equal(sorted(metadata.items()), sorted(model_metadata.items()))
720        for k in metadata:
721            assert_equal(type(k), unicode)
722            assert_equal(type(metadata[k]), unicode)
723        for k in None, 42, '+'.join(model_metadata):
724            with assert_raises(KeyError) as ecm:
725                metadata[k]
726            assert_equal(ecm.exception.args, (k,))
727    finally:
728        test_file.close()
729
730class test_sexpr(TestCase):
731
732    def test(self):
733        context = Context()
734        document = context.new_document(FileUri(images + 'test0.djvu'))
735        assert_equal(type(document), Document)
736        message = document.get_message()
737        assert_equal(type(message), DocInfoMessage)
738        anno = DocumentAnnotations(document, shared=False)
739        assert_equal(type(anno), DocumentAnnotations)
740        anno.wait()
741        x = anno.sexpr
742        assert_equal(x, Expression([]))
743        anno = document.annotations
744        assert_equal(type(anno), DocumentAnnotations)
745        anno.wait()
746        assert_is(anno.background_color, None)
747        assert_is(anno.horizontal_align, None)
748        assert_is(anno.vertical_align, None)
749        assert_is(anno.mode, None)
750        assert_is(anno.zoom, None)
751        expected_metadata = [
752            Symbol('metadata'),
753            [Symbol('ModDate'), '2015-08-17 19:54:57+02:00'],
754            [Symbol('CreationDate'), '2015-08-17 19:54:57+02:00'],
755            [Symbol('Producer'), 'pdfTeX-1.40.16'],
756            [Symbol('Creator'), 'LaTeX with hyperref package'],
757            [Symbol('Author'), 'Jakub Wilk']
758        ]
759        expected_xmp = [
760            Symbol('xmp'),
761            '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
762            '<rdf:Description rdf:about="">'
763                '<xmpMM:History xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"><rdf:Seq><rdf:li xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#" stEvt:action="converted" stEvt:parameters="from application/pdf to image/vnd.djvu" stEvt:softwareAgent="pdf2djvu 0.8.1 (DjVuLibre 3.5.27, Poppler 0.26.5, GraphicsMagick++ 1.3.21, GNOME XSLT 1.1.28, GNOME XML 2.9.2, PStreams 0.8.0)" stEvt:when="2015-08-17T17:54:58+00:00"/></rdf:Seq></xmpMM:History>'
764                '<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Jakub Wilk</dc:creator>'
765                '<dc:format xmlns:dc="http://purl.org/dc/elements/1.1/">image/vnd.djvu</dc:format>'
766                '<pdf:Producer xmlns:pdf="http://ns.adobe.com/pdf/1.3/">pdfTeX-1.40.16</pdf:Producer>'
767                '<xmp:CreatorTool xmlns:xmp="http://ns.adobe.com/xap/1.0/">LaTeX with hyperref package</xmp:CreatorTool>'
768                '<xmp:CreateDate xmlns:xmp="http://ns.adobe.com/xap/1.0/">2015-08-17T19:54:57+02:00</xmp:CreateDate>'
769                '<xmp:ModifyDate xmlns:xmp="http://ns.adobe.com/xap/1.0/">2015-08-17T19:54:57+02:00</xmp:ModifyDate>'
770                '<xmp:MetadataDate xmlns:xmp="http://ns.adobe.com/xap/1.0/">2015-08-17T17:54:58+00:00</xmp:MetadataDate>'
771            '</rdf:Description>'
772            '</rdf:RDF>\n'
773        ]
774        assert_equal(
775            anno.sexpr,
776            Expression([expected_metadata, expected_xmp])
777        )
778        metadata = anno.metadata
779        assert_equal(type(metadata), Metadata)
780        hyperlinks = anno.hyperlinks
781        assert_equal(type(hyperlinks), Hyperlinks)
782        assert_equal(len(hyperlinks), 0)
783        assert_equal(list(hyperlinks), [])
784        outline = document.outline
785        assert_equal(type(outline), DocumentOutline)
786        outline.wait()
787        assert_equal(outline.sexpr, Expression(
788            [Symbol('bookmarks'),
789                ['Lorem ipsum', '#p0001.djvu'],
790                ['Hyperlinks', '#p0002.djvu',
791                    ['local', '#p0002.djvu'],
792                    ['remote', '#p0002.djvu']
793                ]
794            ]
795        ))
796        page = document.pages[1]
797        anno = page.annotations
798        assert_equal(type(anno), PageAnnotations)
799        anno.wait()
800        assert_is(anno.background_color, None)
801        assert_is(anno.horizontal_align, None)
802        assert_is(anno.vertical_align, None)
803        assert_is(anno.mode, None)
804        assert_is(anno.zoom, None)
805        expected_hyperlinks = [
806            [Symbol('maparea'), '#p0001.djvu', '', [Symbol('rect'), 520, 2502, 33, 42], [Symbol('border'), Symbol('#ff0000')]],
807            [Symbol('maparea'), 'http://jwilk.net/', '', [Symbol('rect'), 458, 2253, 516, 49], [Symbol('border'), Symbol('#00ffff')]]
808        ]
809        assert_equal(
810            anno.sexpr,
811            Expression([expected_metadata, expected_xmp] + expected_hyperlinks)
812        )
813        page_metadata = anno.metadata
814        assert_equal(type(page_metadata), Metadata)
815        assert_equal(page_metadata.keys(), metadata.keys())
816        assert_equal([page_metadata[k] == metadata[k] for k in metadata], [True, True, True, True, True])
817        hyperlinks = anno.hyperlinks
818        assert_equal(type(hyperlinks), Hyperlinks)
819        assert_equal(len(hyperlinks), 2)
820        assert_equal(
821            list(hyperlinks),
822            [Expression(h) for h in expected_hyperlinks]
823        )
824        text = page.text
825        assert_equal(type(text), PageText)
826        text.wait()
827        text_s = text.sexpr
828        text_s_detail = [PageText(page, details).sexpr for details in (TEXT_DETAILS_PAGE, TEXT_DETAILS_COLUMN, TEXT_DETAILS_REGION, TEXT_DETAILS_PARAGRAPH, TEXT_DETAILS_LINE, TEXT_DETAILS_WORD, TEXT_DETAILS_CHARACTER, TEXT_DETAILS_ALL)]
829        assert_equal(text_s_detail[0], text_s_detail[1])
830        assert_equal(text_s_detail[1], text_s_detail[2])
831        assert_equal(text_s_detail[2], text_s_detail[3])
832        assert_equal(
833            text_s_detail[0],
834            Expression(
835                [Symbol('page'), 0, 0, 2550, 3300,
836                    '2 Hyperlinks \n'
837                    '2.1 local \n' +
838                    u('→1 \n') +
839                    '2.2 remote \nhttp://jwilk.net/ \n'
840                    '2 \n'
841                ]
842            )
843        )
844        assert_equal(
845            text_s_detail[4],
846            Expression(
847                [Symbol('page'), 0, 0, 2550, 3300,
848                    [Symbol('line'), 462, 2712, 910, 2777, '2 Hyperlinks '],
849                    [Symbol('line'), 462, 2599, 714, 2641, '2.1 local '],
850                    [Symbol('line'), 464, 2505, 544, 2540, u('→1 ')],
851                    [Symbol('line'), 462, 2358, 772, 2400, '2.2 remote '],
852                    [Symbol('line'), 463, 2256, 964, 2298, 'http://jwilk.net/ '],
853                    [Symbol('line'), 1260, 375, 1282, 409, '2 ']
854                ]
855            )
856        )
857        assert_equal(text_s_detail[5], text_s)
858        assert_equal(text_s_detail[6], text_s)
859        assert_equal(text_s_detail[7], text_s)
860        assert_equal(
861            text_s,
862            Expression(
863                [Symbol('page'), 0, 0, 2550, 3300,
864                    [Symbol('line'), 462, 2712, 910, 2777, [Symbol('word'), 462, 2727, 495, 2776, '2'], [Symbol('word'), 571, 2712, 910, 2777, 'Hyperlinks']],
865                    [Symbol('line'), 462, 2599, 714, 2641, [Symbol('word'), 462, 2599, 532, 2641, '2.1'], [Symbol('word'), 597, 2599, 714, 2640, 'local']],
866                    [Symbol('line'), 464, 2505, 544, 2540, [Symbol('word'), 464, 2505, 544, 2540, u('→1')]],
867                    [Symbol('line'), 462, 2358, 772, 2400, [Symbol('word'), 462, 2358, 535, 2400, '2.2'], [Symbol('word'), 598, 2358, 772, 2397, 'remote']],
868                    [Symbol('line'), 463, 2256, 964, 2298, [Symbol('word'), 463, 2256, 964, 2298, 'http://jwilk.net/']],
869                    [Symbol('line'), 1260, 375, 1282, 409, [Symbol('word'), 1260, 375, 1282, 409, '2']]
870                ]
871            )
872        )
873        with assert_raises_str(TypeError, 'details must be a symbol or none'):
874            PageText(page, 'eggs')
875        with assert_raises_str(ValueError, 'details must be equal to TEXT_DETAILS_PAGE, or TEXT_DETAILS_COLUMN, or TEXT_DETAILS_REGION, or TEXT_DETAILS_PARAGRAPH, or TEXT_DETAILS_LINE, or TEXT_DETAILS_WORD, or TEXT_DETAILS_CHARACTER or TEXT_DETAILS_ALL'):
876            PageText(page, Symbol('eggs'))
877
878@testcase
879def test_version():
880    assert_is_instance(__version__, str)
881    assert_equal(__version__, get_changelog_version())
882    assert_is_instance(DDJVU_VERSION, int)
883
884@testcase
885def test_wildcard_import():
886    ns = wildcard_import('djvu.decode')
887    assert_list_equal(
888        sorted(ns.keys()), [
889            'AffineTransform',
890            'Annotations',
891            'ChunkMessage',
892            'Context',
893            'DDJVU_VERSION',
894            'DOCUMENT_TYPE_BUNDLED',
895            'DOCUMENT_TYPE_INDIRECT',
896            'DOCUMENT_TYPE_OLD_BUNDLED',
897            'DOCUMENT_TYPE_OLD_INDEXED',
898            'DOCUMENT_TYPE_SINGLE_PAGE',
899            'DOCUMENT_TYPE_UNKNOWN',
900            'DocInfoMessage',
901            'Document',
902            'DocumentAnnotations',
903            'DocumentDecodingJob',
904            'DocumentExtension',
905            'DocumentFiles',
906            'DocumentOutline',
907            'DocumentPages',
908            'ErrorMessage',
909            'FILE_TYPE_INCLUDE',
910            'FILE_TYPE_PAGE',
911            'FILE_TYPE_THUMBNAILS',
912            'File',
913            'FileURI',
914            'FileUri',
915            'Hyperlinks',
916            'InfoMessage',
917            'Job',
918            'JobDone',
919            'JobException',
920            'JobFailed',
921            'JobNotDone',
922            'JobNotStarted',
923            'JobOK',
924            'JobStarted',
925            'JobStopped',
926            'Message',
927            'Metadata',
928            'NewStreamMessage',
929            'NotAvailable',
930            'PAGE_TYPE_BITONAL',
931            'PAGE_TYPE_COMPOUND',
932            'PAGE_TYPE_PHOTO',
933            'PAGE_TYPE_UNKNOWN',
934            'PRINT_BOOKLET_NO',
935            'PRINT_BOOKLET_RECTO',
936            'PRINT_BOOKLET_VERSO',
937            'PRINT_BOOKLET_YES',
938            'PRINT_ORIENTATION_AUTO',
939            'PRINT_ORIENTATION_LANDSCAPE',
940            'PRINT_ORIENTATION_PORTRAIT',
941            'Page',
942            'PageAnnotations',
943            'PageInfoMessage',
944            'PageJob',
945            'PageText',
946            'PixelFormat',
947            'PixelFormatGrey',
948            'PixelFormatPackedBits',
949            'PixelFormatPalette',
950            'PixelFormatRgb',
951            'PixelFormatRgbMask',
952            'ProgressMessage',
953            'RENDER_BACKGROUND',
954            'RENDER_BLACK',
955            'RENDER_COLOR',
956            'RENDER_COLOR_ONLY',
957            'RENDER_FOREGROUND',
958            'RENDER_MASK_ONLY',
959            'RedisplayMessage',
960            'RelayoutMessage',
961            'SaveJob',
962            'Stream',
963            'TEXT_DETAILS_ALL',
964            'TEXT_DETAILS_CHARACTER',
965            'TEXT_DETAILS_COLUMN',
966            'TEXT_DETAILS_LINE',
967            'TEXT_DETAILS_PAGE',
968            'TEXT_DETAILS_PARAGRAPH',
969            'TEXT_DETAILS_REGION',
970            'TEXT_DETAILS_WORD',
971            'Thumbnail',
972            'ThumbnailMessage',
973            'cmp_text_zone'
974        ]
975    )
976
977del testcase
978
979# vim:ts=4 sts=4 sw=4 et
980