Hide keyboard shortcuts

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 

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 "Unable to find any file matching pattern " 

114 "'{}'.".format(full)) 

115 full = files[0] 

116 

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 

125 

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

127 name = self.hash_md5_readfile(full) + ext 

128 remote = False 

129 

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

131 os.makedirs(fold) 

132 

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

139 

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 

181 

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

189 

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

219 

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

227 

228 if style: 

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

230 

231 if 'align' in node: 

232 atts['class'] = 'align-%s' % node['align'] 

233 

234 return atts