Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pikepdf/jbig2.py: 43%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SPDX-FileCopyrightText: 2022 James R. Barlow
2# SPDX-License-Identifier: MPL-2.0
4"""Integrate JBIG2 image decoding.
6Requires third-party JBIG2 decoder in the form of an external program, like
7jbig2dec.
8"""
10from __future__ import annotations
12import os
13import sys
14from abc import ABC, abstractmethod
15from pathlib import Path
16from subprocess import DEVNULL, PIPE, CalledProcessError, run
17from tempfile import TemporaryDirectory
19from packaging.version import InvalidVersion, Version
21from pikepdf._exceptions import DependencyError
23if sys.platform == 'win32':
24 from subprocess import CREATE_NO_WINDOW
26 CREATION_FLAGS: int = CREATE_NO_WINDOW
27else:
28 CREATION_FLAGS = 0
31class JBIG2DecoderInterface(ABC):
32 """pikepdf's C++ expects this Python interface to be available for JBIG2."""
34 @abstractmethod
35 def check_available(self) -> None:
36 """Check if decoder is available. Throws DependencyError if not."""
38 @abstractmethod
39 def decode_jbig2(self, jbig2: bytes, jbig2_globals: bytes) -> bytes:
40 """Decode JBIG2 from jbig2 and globals, returning decoded bytes."""
42 def available(self) -> bool:
43 """Return True if decoder is available."""
44 try:
45 self.check_available()
46 except DependencyError:
47 return False
48 else:
49 return True
52class JBIG2Decoder(JBIG2DecoderInterface):
53 """JBIG2 decoder implementation."""
55 def __init__(self, *, subprocess_run=run, creationflags=CREATION_FLAGS):
56 """Initialize the decoder."""
57 self._run = subprocess_run
58 self._creationflags = creationflags
60 def check_available(self) -> None:
61 """Check if jbig2dec is installed and usable."""
62 version = self._version()
63 if version is not None and version < Version('0.15'):
64 raise DependencyError("jbig2dec is too old (older than version 0.15)")
66 def decode_jbig2(self, jbig2: bytes, jbig2_globals: bytes) -> bytes:
67 """Decode JBIG2 from binary data, returning decode bytes."""
68 with TemporaryDirectory(prefix='pikepdf-', suffix='.jbig2') as tmpdir:
69 image_path = Path(tmpdir) / "image"
70 global_path = Path(tmpdir) / "global"
71 output_path = Path(tmpdir) / "outfile"
73 args = [
74 "jbig2dec",
75 "--embedded",
76 "--format",
77 "png",
78 "--output",
79 os.fspath(output_path),
80 ]
82 # Get the raw stream, because we can't decode im_obj
83 # (that is why we're here).
84 # (Strictly speaking we should remove any non-JBIG2 filters if double
85 # encoded).
86 image_path.write_bytes(jbig2)
88 if len(jbig2_globals) > 0:
89 global_path.write_bytes(jbig2_globals)
90 args.append(os.fspath(global_path))
92 args.append(os.fspath(image_path))
94 self._run(
95 args, stdout=DEVNULL, check=True, creationflags=self._creationflags
96 )
97 from PIL import Image
99 with Image.open(output_path) as im:
100 return im.tobytes()
102 def _version(self) -> Version | None:
103 try:
104 proc = self._run(
105 ['jbig2dec', '--version'],
106 stdout=PIPE,
107 check=True,
108 encoding='ascii',
109 creationflags=self._creationflags,
110 )
111 except (CalledProcessError, FileNotFoundError) as e:
112 raise DependencyError("jbig2dec - not installed or not found") from e
113 else:
114 result = proc.stdout
115 version_str = result.replace(
116 'jbig2dec', ''
117 ).strip() # returns "jbig2dec 0.xx"
118 try:
119 return Version(version_str)
120 except InvalidVersion:
121 return None
124_jbig2_decoder: JBIG2DecoderInterface = JBIG2Decoder()
127def get_decoder() -> JBIG2DecoderInterface:
128 """Return an instance of a JBIG2 decoder."""
129 return _jbig2_decoder
132def set_decoder(jbig2_decoder: JBIG2DecoderInterface) -> None:
133 """Set the JBIG2 decoder to use."""
134 global _jbig2_decoder
135 _jbig2_decoder = jbig2_decoder