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

65 statements  

1# SPDX-FileCopyrightText: 2022 James R. Barlow 

2# SPDX-License-Identifier: MPL-2.0 

3 

4"""Integrate JBIG2 image decoding. 

5 

6Requires third-party JBIG2 decoder in the form of an external program, like 

7jbig2dec. 

8""" 

9 

10from __future__ import annotations 

11 

12import os 

13import sys 

14from abc import ABC, abstractmethod 

15from pathlib import Path 

16from subprocess import DEVNULL, PIPE, CalledProcessError, run 

17from tempfile import TemporaryDirectory 

18 

19from packaging.version import InvalidVersion, Version 

20from PIL import Image 

21 

22from pikepdf._exceptions import DependencyError 

23 

24if sys.platform == 'win32': 

25 from subprocess import CREATE_NO_WINDOW 

26 

27 CREATION_FLAGS: int = CREATE_NO_WINDOW 

28else: 

29 CREATION_FLAGS = 0 

30 

31 

32class JBIG2DecoderInterface(ABC): 

33 """pikepdf's C++ expects this Python interface to be available for JBIG2.""" 

34 

35 @abstractmethod 

36 def check_available(self) -> None: 

37 """Check if decoder is available. Throws DependencyError if not.""" 

38 

39 @abstractmethod 

40 def decode_jbig2(self, jbig2: bytes, jbig2_globals: bytes) -> bytes: 

41 """Decode JBIG2 from jbig2 and globals, returning decoded bytes.""" 

42 

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 

51 

52 

53class JBIG2Decoder(JBIG2DecoderInterface): 

54 """JBIG2 decoder implementation.""" 

55 

56 def __init__(self, *, subprocess_run=run, creationflags=CREATION_FLAGS): 

57 """Initialize the decoder.""" 

58 self._run = subprocess_run 

59 self._creationflags = creationflags 

60 

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)") 

66 

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" 

73 

74 args = [ 

75 "jbig2dec", 

76 "--embedded", 

77 "--format", 

78 "png", 

79 "--output", 

80 os.fspath(output_path), 

81 ] 

82 

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) 

88 

89 if len(jbig2_globals) > 0: 

90 global_path.write_bytes(jbig2_globals) 

91 args.append(os.fspath(global_path)) 

92 

93 args.append(os.fspath(image_path)) 

94 

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() 

100 

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 

121 

122 

123_jbig2_decoder: JBIG2DecoderInterface = JBIG2Decoder() 

124 

125 

126def get_decoder() -> JBIG2DecoderInterface: 

127 """Return an instance of a JBIG2 decoder.""" 

128 return _jbig2_decoder 

129 

130 

131def set_decoder(jbig2_decoder: JBIG2DecoderInterface) -> None: 

132 """Set the JBIG2 decoder to use.""" 

133 global _jbig2_decoder 

134 _jbig2_decoder = jbig2_decoder