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

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 

20 

21from pikepdf._exceptions import DependencyError 

22 

23if sys.platform == 'win32': 

24 from subprocess import CREATE_NO_WINDOW 

25 

26 CREATION_FLAGS: int = CREATE_NO_WINDOW 

27else: 

28 CREATION_FLAGS = 0 

29 

30 

31class JBIG2DecoderInterface(ABC): 

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

33 

34 @abstractmethod 

35 def check_available(self) -> None: 

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

37 

38 @abstractmethod 

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

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

41 

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 

50 

51 

52class JBIG2Decoder(JBIG2DecoderInterface): 

53 """JBIG2 decoder implementation.""" 

54 

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

56 """Initialize the decoder.""" 

57 self._run = subprocess_run 

58 self._creationflags = creationflags 

59 

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

65 

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" 

72 

73 args = [ 

74 "jbig2dec", 

75 "--embedded", 

76 "--format", 

77 "png", 

78 "--output", 

79 os.fspath(output_path), 

80 ] 

81 

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) 

87 

88 if len(jbig2_globals) > 0: 

89 global_path.write_bytes(jbig2_globals) 

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

91 

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

93 

94 self._run( 

95 args, stdout=DEVNULL, check=True, creationflags=self._creationflags 

96 ) 

97 from PIL import Image 

98 

99 with Image.open(output_path) as im: 

100 return im.tobytes() 

101 

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 

122 

123 

124_jbig2_decoder: JBIG2DecoderInterface = JBIG2Decoder() 

125 

126 

127def get_decoder() -> JBIG2DecoderInterface: 

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

129 return _jbig2_decoder 

130 

131 

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

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

134 global _jbig2_decoder 

135 _jbig2_decoder = jbig2_decoder