Coverage for pyquickhelper/sphinxext/_sphinx_common_builder.py: 64%

135 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Common functions for :epkg:`Sphinx` writers. 

5""" 

6import hashlib 

7import os 

8import re 

9import glob 

10import urllib.request 

11import shutil 

12import sys 

13import logging 

14from .sphinximages.sphinxtrib.images import get_image_extension 

15from ..filehelper import get_url_content_timeout, InternetException 

16 

17 

18class CommonSphinxWriterHelpers: 

19 """ 

20 Common functions used in @see cl RstTranslator 

21 and @see cl MdTranslator. 

22 """ 

23 

24 def hash_md5_readfile(self, filename): 

25 """ 

26 Computes a hash of a file. 

27 @param filename filename 

28 @return string 

29 """ 

30 with open(filename, 'rb') as f: 

31 m = hashlib.md5() 

32 readBytes = 1024 ** 2 # read 1024 bytes per time 

33 totalBytes = 0 

34 while readBytes: 

35 readString = f.read(readBytes) 

36 m.update(readString) 

37 readBytes = len(readString) 

38 totalBytes += readBytes 

39 res = m.hexdigest() 

40 if len(res) > 20: 

41 res = res[:20] 

42 return res 

43 

44 def base_visit_image(self, node, image_dest=None): 

45 """ 

46 Processes an image. By default, it writes the image on disk. 

47 Inspired from 

48 `visit_image <https://github.com/docutils-mirror/docutils/blob/master/docutils/writers/html4css1/__init__.py#L1019>`_ 

49 implemented in :epkg:`docutils`. 

50 

51 @param node image node 

52 @param image_dest image destination (location where they will be copied) 

53 @return attributes 

54 """ 

55 atts = {} 

56 uri = node['uri'] 

57 

58 # place SVG and SWF images in an <object> element 

59 types = {'.svg': 'image/svg+xml', 

60 '.swf': 'application/x-shockwave-flash'} 

61 ext = os.path.splitext(uri)[1].lower() 

62 if ext in ('.svg', '.swf'): 

63 atts['data'] = uri 

64 atts['type'] = types[ext] 

65 

66 atts['src'] = uri 

67 atts['alt'] = node.get('alt', uri) 

68 

69 env = self.builder.env # pylint: disable=E1101 

70 if hasattr(env, 'remote_images') and atts['src'] in env.remote_images: 

71 atts['src'] = env.remote_images[atts['src']] 

72 

73 # Makes a local copy of the image 

74 if 'src' in atts: 

75 builder = self.builder # pylint: disable=E1101 

76 srcdir = builder.srcdir 

77 if srcdir == "IMPOSSIBLE:TOFIND": 

78 srcdir = None 

79 if image_dest is None: 

80 outdir = builder.outdir 

81 if builder.current_docname and builder.current_docname != "<<string>>": 

82 if srcdir is None: 

83 current = os.path.dirname(builder.current_docname) 

84 else: 

85 current = os.path.dirname(os.path.join( 

86 srcdir, builder.current_docname)) 

87 if current is None or not os.path.exists(current): 

88 raise FileNotFoundError( # pragma: no cover 

89 "Unable to find document '{0}' current_docname='{1}'" 

90 "".format(current, builder.current_docname)) 

91 dest = os.path.dirname(os.path.join( 

92 outdir, builder.current_docname)) 

93 fold = outdir 

94 else: 

95 # current_docname is None which means 

96 # no file should be created 

97 fold = None 

98 else: 

99 fold = image_dest 

100 

101 if atts['src'].startswith('http:') or atts['src'].startswith('https:'): 

102 name = hashlib.sha1(atts['src'].encode()).hexdigest() 

103 ext = get_image_extension(atts['src']) 

104 remote = True 

105 else: 

106 full = os.path.join( 

107 srcdir, atts['src']) if srcdir else atts['src'] 

108 

109 if '*' in full: 

110 files = glob.glob(full) 

111 if len(files) == 0: 

112 raise FileNotFoundError( # pragma: no cover 

113 f"Unable to find any file matching pattern '{full}'.") 

114 full = files[0] 

115 

116 if not os.path.exists(full): 

117 this = os.path.abspath(os.path.dirname(__file__)) 

118 repl = os.path.join( 

119 this, "sphinximages", "sphinxtrib", "missing.png") 

120 logger = logging.getLogger("image") 

121 logger.warning("[image] unable to find image %r, replaced by %r.", 

122 full, repl) 

123 full = repl 

124 

125 ext = os.path.splitext(full)[-1] 

126 name = self.hash_md5_readfile(full) + ext 

127 remote = False 

128 

129 if fold is not None and not os.path.exists(fold): 

130 os.makedirs(fold) 

131 

132 dest = os.path.join(fold, name) if fold else None 

133 if dest is not None and '*' in dest: 

134 raise RuntimeError( # pragma: no cover 

135 "Wrong destination '{} // {}' image_dest='{}' atts['src']='{}' " 

136 "srcdir='{}' full='{}'.".format( 

137 fold, name, image_dest, atts['src'], srcdir, full)) 

138 

139 if dest is not None: 

140 if not os.path.exists(dest): 

141 if remote: 

142 if atts.get('download', False): 

143 # Downloads the image 

144 try: 

145 get_url_content_timeout( 

146 atts['src'], output=dest, encoding=None, timeout=20) 

147 full = atts['src'] 

148 except InternetException as e: # pragma: no cover 

149 logger = logging.getLogger("image") 

150 logger.warning( 

151 "[image] unable to get content for url %r due to %r", 

152 atts['src'], e) 

153 this = os.path.abspath( 

154 os.path.dirname(__file__)) 

155 full = os.path.join( 

156 this, "sphinximages", "sphinxtrib", "missing.png") 

157 shutil.copy(full, dest) 

158 else: 

159 name = atts['src'] 

160 full = name 

161 dest = name 

162 else: 

163 if ':' in dest and len(dest) > 2: 

164 dest = dest[:2] + dest[2:].replace(':', '_') 

165 ext = os.path.splitext(dest)[-1] 

166 if ext not in ('.png', '.jpg'): 

167 dest += '.png' 

168 try: 

169 shutil.copy(full, dest) 

170 except (FileNotFoundError, OSError) as e: 

171 raise FileNotFoundError( # pragma: no cover 

172 f"Unable to copy from '{full}' to '{dest}'.") from e 

173 full = dest 

174 else: 

175 full = dest 

176 else: 

177 name = atts['src'] 

178 full = name 

179 dest = name 

180 

181 atts['src'] = name 

182 atts['full'] = full 

183 atts['dest'] = dest 

184 else: 

185 raise ValueError( # pragma: no cover 

186 "No image was found in node (class='{1}')\n{0}".format( 

187 node, self.__class__.__name__)) 

188 

189 # image size 

190 if 'width' in node: 

191 atts['width'] = node['width'] 

192 if 'height' in node: 

193 atts['height'] = node['height'] 

194 if 'download' in node: 

195 atts['download'] = node['download'] 

196 if 'scale' in node: 

197 import PIL 

198 if 'width' not in node or 'height' not in node: 

199 imagepath = urllib.request.url2pathname(uri) 

200 try: 

201 img = PIL.Image.open( 

202 imagepath.encode(sys.getfilesystemencoding())) 

203 except (IOError, UnicodeEncodeError): # pragma: no cover 

204 pass # TODO: warn? 

205 else: 

206 self.settings.record_dependencies.add( # pylint: disable=E1101 

207 imagepath.replace('\\', '/')) 

208 if 'width' not in atts: 

209 atts['width'] = '%dpx' % img.size[0] 

210 if 'height' not in atts: 

211 atts['height'] = '%dpx' % img.size[1] 

212 for att_name in 'width', 'height': 

213 if att_name in atts: 

214 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name]) 

215 atts[att_name] = '%s%s' % ( 

216 float(match.group(1)) * (float(node['scale']) / 100), 

217 match.group(2)) 

218 

219 style = [] 

220 for att_name in 'width', 'height': 

221 if att_name in atts: 

222 if re.match(r'^[0-9.]+$', atts[att_name]): 

223 # Interpret unitless values as pixels. 

224 atts[att_name] += 'px' 

225 style.append(f'{att_name}: {atts[att_name]};') 

226 

227 if style: 

228 atts['style'] = ' '.join(style) 

229 

230 if 'align' in node: 

231 atts['class'] = f"align-{node['align']}" 

232 

233 return atts