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