Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 "Unable to find any file matching pattern "
114 "'{}'.".format(full))
115 full = files[0]
117 if not os.path.exists(full):
118 this = os.path.abspath(os.path.dirname(__file__))
119 repl = os.path.join(
120 this, "sphinximages", "sphinxtrib", "missing.png")
121 logger = logging.getLogger("image")
122 logger.warning(
123 "[image] unable to find image '{0}', replaced by '{1}'".format(full, repl))
124 full = repl
126 ext = os.path.splitext(full)[-1]
127 name = self.hash_md5_readfile(full) + ext
128 remote = False
130 if fold is not None and not os.path.exists(fold):
131 os.makedirs(fold)
133 dest = os.path.join(fold, name) if fold else None
134 if dest is not None and '*' in dest:
135 raise RuntimeError( # pragma: no cover
136 "Wrong destination '{} // {}' image_dest='{}' atts['src']='{}' "
137 "srcdir='{}' full='{}'.".format(
138 fold, name, image_dest, atts['src'], srcdir, full))
140 if dest is not None:
141 if not os.path.exists(dest):
142 if remote:
143 if atts.get('download', False):
144 # Downloads the image
145 try:
146 get_url_content_timeout(
147 atts['src'], output=dest, encoding=None, timeout=20)
148 full = atts['src']
149 except InternetException as e: # pragma: no cover
150 logger = logging.getLogger("image")
151 logger.warning(
152 "[image] unable to get content for url '{0}' due to '{1}'"
153 "".format(atts['src'], e))
154 this = os.path.abspath(
155 os.path.dirname(__file__))
156 full = os.path.join(
157 this, "sphinximages", "sphinxtrib", "missing.png")
158 shutil.copy(full, dest)
159 else:
160 name = atts['src']
161 full = name
162 dest = name
163 else:
164 if ':' in dest and len(dest) > 2:
165 dest = dest[:2] + dest[2:].replace(':', '_')
166 ext = os.path.splitext(dest)[-1]
167 if ext not in ('.png', '.jpg'):
168 dest += '.png'
169 try:
170 shutil.copy(full, dest)
171 except (FileNotFoundError, OSError) as e:
172 raise FileNotFoundError( # pragma: no cover
173 "Unable to copy from '{0}' to '{1}'.".format(full, dest)) from e
174 full = dest
175 else:
176 full = dest
177 else:
178 name = atts['src']
179 full = name
180 dest = name
182 atts['src'] = name
183 atts['full'] = full
184 atts['dest'] = dest
185 else:
186 raise ValueError( # pragma: no cover
187 "No image was found in node (class='{1}')\n{0}".format(
188 node, self.__class__.__name__))
190 # image size
191 if 'width' in node:
192 atts['width'] = node['width']
193 if 'height' in node:
194 atts['height'] = node['height']
195 if 'download' in node:
196 atts['download'] = node['download']
197 if 'scale' in node:
198 import PIL
199 if 'width' not in node or 'height' not in node:
200 imagepath = urllib.request.url2pathname(uri)
201 try:
202 img = PIL.Image.open(
203 imagepath.encode(sys.getfilesystemencoding()))
204 except (IOError, UnicodeEncodeError): # pragma: no cover
205 pass # TODO: warn?
206 else:
207 self.settings.record_dependencies.add( # pylint: disable=E1101
208 imagepath.replace('\\', '/'))
209 if 'width' not in atts:
210 atts['width'] = '%dpx' % img.size[0]
211 if 'height' not in atts:
212 atts['height'] = '%dpx' % img.size[1]
213 for att_name in 'width', 'height':
214 if att_name in atts:
215 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
216 atts[att_name] = '%s%s' % (
217 float(match.group(1)) * (float(node['scale']) / 100),
218 match.group(2))
220 style = []
221 for att_name in 'width', 'height':
222 if att_name in atts:
223 if re.match(r'^[0-9.]+$', atts[att_name]):
224 # Interpret unitless values as pixels.
225 atts[att_name] += 'px'
226 style.append('%s: %s;' % (att_name, atts[att_name]))
228 if style:
229 atts['style'] = ' '.join(style)
231 if 'align' in node:
232 atts['class'] = 'align-%s' % node['align']
234 return atts