Coverage for pyquickhelper/sphinxext/sphinx_image_extension.py: 81%

185 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 Defines a :epkg:`sphinx` extension to add button to share a page 

5""" 

6import os 

7import copy 

8import shutil 

9from html import escape 

10import sphinx 

11from docutils import nodes 

12from docutils.parsers.rst import Directive, directives 

13from sphinx.util.logging import getLogger 

14from sphinx.util import FilenameUniqDict 

15 

16 

17DEFAULT_CONFIG = dict( 

18 default_image_width=None, 

19 default_image_height=None, 

20 cache_path='_images', 

21) 

22 

23 

24class simpleimage_node(nodes.General, nodes.Element): 

25 

26 """ 

27 Defines *image* node. 

28 """ 

29 pass 

30 

31 

32class SimpleImageDirective(Directive): 

33 """ 

34 Adds an image to a page. It can be done by adding:: 

35 

36 .. simpleimage:: filename.png 

37 :width: 400 

38 :height: 600 

39 

40 Available options: 

41 

42 * ``:width:``, ``:height:``, ``:scale:``: resize the image 

43 * ``:target:``: for HTML, clickable image 

44 * ``:alt:``: for HTML 

45 * ``:download:`` if the image is a url, it downloads the image. 

46 * ``:convert:`` convert the image into a new format 

47 """ 

48 required_arguments = True 

49 optional_arguments = 0 

50 final_argument_whitespace = True 

51 option_spec = {'width': directives.unchanged, 

52 'height': directives.unchanged, 

53 'scale': directives.unchanged, 

54 'target': directives.unchanged, 

55 'alt': directives.unchanged, 

56 'download': directives.unchanged, 

57 'convert': directives.unchanged, 

58 } 

59 has_content = True 

60 node_class = simpleimage_node 

61 

62 def run(self): 

63 """ 

64 Runs the directive. 

65 

66 @return a list of nodes 

67 """ 

68 env = self.state.document.settings.env 

69 conf = env.app.config.simpleimages_config 

70 docname = None if env is None else env.docname 

71 if docname is not None: 

72 docname = docname.replace("\\", "/").split("/")[-1] 

73 else: 

74 docname = '' 

75 

76 source = self.state.document.current_source 

77 filename = self.arguments[0] 

78 

79 if '://' in filename: 

80 logger = getLogger("simpleimage") # pragma: no cover 

81 logger.warning( # pragma: no cover 

82 "[simpleimage] url detected %r in docname %r - line %r" 

83 ".", filename, docname, self.lineno) 

84 is_url = True 

85 else: 

86 is_url = False 

87 

88 convert = self.options.get('convert', None) 

89 if convert: 

90 logger = getLogger("simpleimage") # pragma: no cover 

91 logger.warning( # pragma: no cover 

92 "[simpleimage] convert into %r not implemented for %r in " 

93 "docname %r - line %r.", 

94 convert, filename, docname, self.lineno) 

95 

96 download = self.options.get('download', None) 

97 if convert: 

98 logger = getLogger("simpleimage") 

99 logger.warning( # pragma: no cover 

100 "[simpleimage] download not implemented for %r in docname %r - line %r.", 

101 filename, docname, self.lineno) 

102 

103 if not is_url: 

104 env.images_mapping.add_file('', filename) 

105 

106 srcdir = env.srcdir 

107 rstrel = os.path.relpath(source, srcdir) 

108 rstfold = os.path.split(rstrel)[0] 

109 cache = os.path.join(srcdir, conf['cache_path']) 

110 img = os.path.join(cache, filename) 

111 abspath = None 

112 relpath = None 

113 

114 if os.path.exists(img): 

115 abspath = img 

116 relpath = cache 

117 else: 

118 last = rstfold.replace('\\', '/') 

119 img = os.path.join(srcdir, last, filename) 

120 if os.path.exists(img): 

121 relpath = last 

122 abspath = img 

123 

124 if abspath is None: 

125 logger = getLogger("simpleimage") # pragma: no cover 

126 logger.warning( # pragma: no cover 

127 "[simpleimage] Unable to find %r in docname %r - line %r - srcdir=%r.", 

128 filename, docname, self.lineno, srcdir) 

129 else: 

130 abspath = None 

131 relpath = None 

132 

133 width = self.options.get('width', conf['default_image_width']) 

134 height = self.options.get('height', conf['default_image_height']) 

135 scale = self.options.get('scale', None) 

136 alt = self.options.get('alt', None) 

137 target = self.options.get('target', None) 

138 

139 # build node 

140 node = self.__class__.node_class(uri=filename, docname=docname, lineno=self.lineno, 

141 width=width, height=height, abspath=abspath, 

142 relpath=relpath, is_url=is_url, alt=alt, scale=scale, 

143 target=target, convert=convert, download=download) 

144 node['classes'] += ["place-image"] 

145 node['image'] = filename 

146 ns = [node] 

147 return ns 

148 

149 

150def visit_simpleimage_node(self, node): 

151 """ 

152 Visits a image node. 

153 Copies the image. 

154 """ 

155 if node['abspath'] is not None: 

156 outdir = self.builder.outdir 

157 relpath = os.path.join(outdir, node['relpath']) 

158 dname = os.path.split(node['uri'])[0] 

159 if dname: 

160 relpath = os.path.join(relpath, dname) 

161 if not os.path.exists(relpath): 

162 os.makedirs(relpath) 

163 if os.path.dirname(node['abspath']) != relpath: 

164 shutil.copy(node['abspath'], relpath) 

165 logger = getLogger("image") # pragma: no cover 

166 logger.info("[image] copy '{0}' to '{1}'".format( # pragma: no cover 

167 node['uri'], relpath)) 

168 

169 

170def _clean_value(val): 

171 if isinstance(val, tuple): 

172 return val[0] 

173 return val 

174 

175 

176def depart_simpleimage_node_html(self, node): 

177 """ 

178 What to do when leaving a node *image* 

179 the function should have different behaviour, 

180 depending on the format, or the setup should 

181 specify a different function for each. 

182 """ 

183 if node.hasattr("uri"): 

184 filename = node["uri"] 

185 width = _clean_value(node["width"]) 

186 height = _clean_value(node["height"]) 

187 scale = node["scale"] 

188 alt = node["alt"] 

189 target = node["target"] 

190 found = node["abspath"] is not None or node["is_url"] 

191 if not found: # pragma: no cover 

192 body = f"<b>unable to find '{filename}'</b>" 

193 self.body.append(body) 

194 else: 

195 body = '<img src="{0}" {1} {2}/>' 

196 width = f' width="{width}"' if width else "" 

197 height = f' height="{height}"' if height else "" 

198 if width or height: 

199 style = f"{width}{height}" 

200 elif scale: 

201 style = f" width={scale}%" 

202 alt = f' alt="{escape(alt)}"' if alt else "" 

203 body = body.format(filename, style, alt) 

204 if target: 

205 body = f'<a href="{escape(target)}">{body}</a>' 

206 self.body.append(body) 

207 

208 

209def depart_simpleimage_node_text(self, node): 

210 """ 

211 What to do when leaving a node *image* 

212 the function should have different behaviour, 

213 depending on the format, or the setup should 

214 specify a different function for each. 

215 """ 

216 if 'rst' in (self.builder.name, self.builder.format): 

217 depart_simpleimage_node_rst(self, node) 

218 elif 'md' in (self.builder.name, self.builder.format): 

219 depart_simpleimage_node_md(self, node) 

220 elif 'latex' in (self.builder.name, self.builder.format): 

221 depart_simpleimage_node_latex(self, node) 

222 elif node.hasattr("uri"): 

223 filename = node["uri"] 

224 width = _clean_value(node["width"]) 

225 height = _clean_value(node["height"]) 

226 scale = node["scale"] 

227 alt = node["alt"] 

228 target = node["target"] 

229 found = node["abspath"] is not None or node["is_url"] 

230 if not found: # pragma: no cover 

231 body = f"unable to find '{filename}'" 

232 self.body.append(body) 

233 else: 

234 body = '\nimage {0}{1}{2}: {3}{4}\n' 

235 width = f' width="{width}"' if width else "" 

236 height = f' height="{height}"' if height else "" 

237 scale = f' scale="{scale}"' if scale else "" 

238 alt = ' alt="{0}"'.format(alt.replace('"', '\\"')) if alt else "" 

239 target = ' target="{0}"'.format( 

240 target.replace('"', '\\"')) if target else "" 

241 body = body.format(width, height, scale, filename, alt, target) 

242 self.add_text(body) 

243 

244 

245def depart_simpleimage_node_latex(self, node): 

246 """ 

247 What to do when leaving a node *image* 

248 the function should have different behaviour, 

249 depending on the format, or the setup should 

250 specify a different function for each. 

251 """ 

252 if node.hasattr("uri"): 

253 width = _clean_value(node["width"]) 

254 height = _clean_value(node["height"]) 

255 scale = node["scale"] 

256 alt = node["alt"] 

257 full = os.path.join(node["relpath"], node['uri']) 

258 found = node['abspath'] is not None or node["is_url"] 

259 if not found: # pragma: no cover 

260 body = f"\\textbf{{unable to find '{full}'}}" 

261 self.body.append(body) 

262 else: 

263 body = '\\includegraphics{0}{{{1}}}\n' 

264 width = f"width={width}" if width else "" 

265 height = f"height={height}" if height else "" 

266 scale = f"scale={scale}" if scale else "" 

267 if width or height or scale: 

268 dims = [_ for _ in [width, height, scale] if _] 

269 style = f"[{','.join(dims)}]" 

270 else: 

271 style = "" 

272 alt = ' alt="{0}"'.format(alt.replace('"', '\\"')) if alt else "" 

273 full = full.replace('\\', '/').replace('_', '\\_') 

274 body = body.format(style, full) 

275 self.body.append(body) 

276 

277 

278def depart_simpleimage_node_rst(self, node): 

279 """ 

280 What to do when leaving a node *image* 

281 the function should have different behaviour, 

282 depending on the format, or the setup should 

283 specify a different function for each. 

284 """ 

285 if node.hasattr("uri"): 

286 filename = node["uri"] 

287 found = node["abspath"] is not None or node["is_url"] 

288 if not found: # pragma: no cover 

289 body = f".. simpleimage:: {filename} [not found]" 

290 self.add_text(body + self.nl) 

291 else: 

292 options = SimpleImageDirective.option_spec 

293 body = f".. simpleimage:: {filename}" 

294 self.new_state(0) 

295 self.add_text(body + self.nl) 

296 for opt in options: 

297 v = node.get(opt, None) 

298 if v: 

299 self.add_text(f' :{opt}: {v}' + self.nl) 

300 self.end_state(wrap=False) 

301 

302 

303def depart_simpleimage_node_md(self, node): 

304 """ 

305 What to do when leaving a node *image* 

306 the function should have different behaviour, 

307 depending on the format, or the setup should 

308 specify a different function for each. 

309 """ 

310 if node.hasattr("uri"): 

311 filename = node["uri"] 

312 found = node["abspath"] is not None or node["is_url"] 

313 if not found: # pragma: no cover 

314 body = f"[{filename}](not found)" 

315 self.add_text(body + self.nl) 

316 else: 

317 alt = node.get("alt", "") 

318 uri = filename 

319 width = node.get('width', '').replace('px', '') 

320 height = node.get('height', '').replace('px', '') 

321 style = f" ={width}x{height}" 

322 if style == " =x": 

323 style = "" 

324 text = f"![{alt}]({uri}{style})" 

325 self.add_text(text) 

326 

327 

328def initialize_simpleimages_directive(app): 

329 """ 

330 Initializes the image directives. 

331 """ 

332 global DEFAULT_CONFIG 

333 

334 config = copy.deepcopy(DEFAULT_CONFIG) 

335 config.update(app.config.simpleimages_config) 

336 app.config.simpleimages_config = config 

337 app.env.images_mapping = FilenameUniqDict() 

338 

339 

340def setup(app): 

341 """ 

342 setup for ``image`` (sphinx) 

343 """ 

344 global DEFAULT_CONFIG 

345 if hasattr(app, "add_mapping"): 

346 app.add_mapping('simpleimages_mapping', simpleimage_node) 

347 app.add_config_value('simpleimages_config', DEFAULT_CONFIG, 'env') 

348 app.connect('builder-inited', initialize_simpleimages_directive) 

349 app.add_node(simpleimage_node, 

350 html=(visit_simpleimage_node, depart_simpleimage_node_html), 

351 epub=(visit_simpleimage_node, depart_simpleimage_node_html), 

352 elatex=(visit_simpleimage_node, 

353 depart_simpleimage_node_latex), 

354 latex=(visit_simpleimage_node, depart_simpleimage_node_latex), 

355 rst=(visit_simpleimage_node, depart_simpleimage_node_rst), 

356 md=(visit_simpleimage_node, depart_simpleimage_node_md), 

357 text=(visit_simpleimage_node, depart_simpleimage_node_text)) 

358 

359 app.add_directive('simpleimage', SimpleImageDirective) 

360 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}