Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pikepdf/jbig2.py: 45%
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
20from PIL import Image
22from pikepdf._exceptions import DependencyError
24if sys.platform == 'win32':
25 from subprocess import CREATE_NO_WINDOW
27 CREATION_FLAGS: int = CREATE_NO_WINDOW
28else:
29 CREATION_FLAGS = 0
32class JBIG2DecoderInterface(ABC):
33 """pikepdf's C++ expects this Python interface to be available for JBIG2."""
35 @abstractmethod
36 def check_available(self) -> None:
37 """Check if decoder is available. Throws DependencyError if not."""
39 @abstractmethod
40 def decode_jbig2(self, jbig2: bytes, jbig2_globals: bytes) -> bytes:
41 """Decode JBIG2 from jbig2 and globals, returning decoded bytes."""
43 def available(self) -> bool:
44 """Return True if decoder is available."""
45 try:
46 self.check_available()
47 except DependencyError:
48 return False
49 else:
50 return True
53class JBIG2Decoder(JBIG2DecoderInterface):
54 """JBIG2 decoder implementation."""
56 def __init__(self, *, subprocess_run=run, creationflags=CREATION_FLAGS):
57 """Initialize the decoder."""
58 self._run = subprocess_run
59 self._creationflags = creationflags
61 def check_available(self) -> None:
62 """Check if jbig2dec is installed and usable."""
63 version = self._version()
64 if version is not None and version < Version('0.15'):
65 raise DependencyError("jbig2dec is too old (older than version 0.15)")
67 def decode_jbig2(self, jbig2: bytes, jbig2_globals: bytes) -> bytes:
68 """Decode JBIG2 from binary data, returning decode bytes."""
69 with TemporaryDirectory(prefix='pikepdf-', suffix='.jbig2') as tmpdir:
70 image_path = Path(tmpdir) / "image"
71 global_path = Path(tmpdir) / "global"
72 output_path = Path(tmpdir) / "outfile"
74 args = [
75 "jbig2dec",
76 "--embedded",
77 "--format",
78 "png",
79 "--output",
80 os.fspath(output_path),
81 ]
83 # Get the raw stream, because we can't decode im_obj
84 # (that is why we're here).
85 # (Strictly speaking we should remove any non-JBIG2 filters if double
86 # encoded).
87 image_path.write_bytes(jbig2)
89 if len(jbig2_globals) > 0:
90 global_path.write_bytes(jbig2_globals)
91 args.append(os.fspath(global_path))
93 args.append(os.fspath(image_path))
95 self._run(
96 args, stdout=DEVNULL, check=True, creationflags=self._creationflags
97 )
98 with Image.open(output_path) as im:
99 return im.tobytes()
101 def _version(self) -> Version | None:
102 try:
103 proc = self._run(
104 ['jbig2dec', '--version'],
105 stdout=PIPE,
106 check=True,
107 encoding='ascii',
108 creationflags=self._creationflags,
109 )
110 except (CalledProcessError, FileNotFoundError) as e:
111 raise DependencyError("jbig2dec - not installed or not found") from e
112 else:
113 result = proc.stdout
114 version_str = result.replace(
115 'jbig2dec', ''
116 ).strip() # returns "jbig2dec 0.xx"
117 try:
118 return Version(version_str)
119 except InvalidVersion:
120 return None
123_jbig2_decoder: JBIG2DecoderInterface = JBIG2Decoder()
126def get_decoder() -> JBIG2DecoderInterface:
127 """Return an instance of a JBIG2 decoder."""
128 return _jbig2_decoder
131def set_decoder(jbig2_decoder: JBIG2DecoderInterface) -> None:
132 """Set the JBIG2 decoder to use."""
133 global _jbig2_decoder
134 _jbig2_decoder = jbig2_decoder