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
« 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
18class CommonSphinxWriterHelpers:
19 """
20 Common functions used in @see cl RstTranslator
21 and @see cl MdTranslator.
22 """
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
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`.
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']
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]
66 atts['src'] = uri
67 atts['alt'] = node.get('alt', uri)
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']]
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
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']
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]
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
125 ext = os.path.splitext(full)[-1]
126 name = self.hash_md5_readfile(full) + ext
127 remote = False
129 if fold is not None and not os.path.exists(fold):
130 os.makedirs(fold)
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))
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
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__))
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))
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]};')
227 if style:
228 atts['style'] = ' '.join(style)
230 if 'align' in node:
231 atts['class'] = f"align-{node['align']}"
233 return atts