1 # -*- coding: utf-8 -*-
6 Allow blockdiag-formatted diagrams to be included in Sphinx-generated
9 :copyright: Copyright 2010 by Takeshi Komiya.
13 from __future__ import absolute_import
20 from collections import namedtuple
21 from docutils import nodes
22 from sphinx import addnodes
23 from sphinx.util.osutil import ensuredir
25 import blockdiag.utils.rst.nodes
26 import blockdiag.utils.rst.directives
27 from blockdiag.utils.bootstrap import detectfont, Application
28 from blockdiag.utils.compat import u, string_types
29 from blockdiag.utils.fontmap import FontMap
30 from blockdiag.utils.rst.directives import with_blockdiag
32 # fontconfig; it will be initialized on `builder-inited` event.
36 class blockdiag_node(blockdiag.utils.rst.nodes.blockdiag):
37 def to_drawer(self, image_format, builder, **kwargs):
38 if 'filename' in kwargs:
39 filename = kwargs.pop('filename')
41 filename = self.get_abspath(image_format, builder)
43 antialias = builder.config.blockdiag_antialias
44 transparency = builder.config.blockdiag_transparency
45 image = super(blockdiag_node, self).to_drawer(image_format, filename, fontmap,
46 antialias=antialias, transparency=transparency,
48 for node in image.diagram.traverse_nodes():
49 node.href = resolve_reference(builder, node.href)
53 def get_relpath(self, image_format, builder):
54 options = dict(antialias=builder.config.blockdiag_antialias,
55 fontpath=builder.config.blockdiag_fontpath,
56 fontmap=builder.config.blockdiag_fontmap,
58 transparency=builder.config.blockdiag_transparency)
59 if hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) or HTML writer
60 outputdir = builder.imgpath
63 return posixpath.join(outputdir, self.get_path(**options))
65 def get_abspath(self, image_format, builder):
66 options = dict(antialias=builder.config.blockdiag_antialias,
67 fontpath=builder.config.blockdiag_fontpath,
68 fontmap=builder.config.blockdiag_fontmap,
70 transparency=builder.config.blockdiag_transparency)
72 if hasattr(builder, 'imagedir'): # Sphinx (>= 1.3.x)
73 outputdir = os.path.join(builder.outdir, builder.imagedir)
74 elif hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) and HTML writer
75 outputdir = os.path.join(builder.outdir, '_images')
77 outputdir = builder.outdir
78 path = os.path.join(outputdir, self.get_path(**options))
79 ensuredir(os.path.dirname(path))
84 class Blockdiag(blockdiag.utils.rst.directives.BlockdiagDirective):
85 node_class = blockdiag_node
87 def node2image(self, node, diagram):
91 def resolve_reference(builder, href):
95 pattern = re.compile(u("^:ref:`(.+?)`"), re.UNICODE)
96 matched = pattern.search(href)
99 elif not hasattr(builder, 'current_docname'): # ex. latex builder
100 return matched.group(1)
102 refid = matched.group(1)
103 domain = builder.env.domains['std']
104 node = addnodes.pending_xref(refexplicit=False)
105 xref = domain.resolve_xref(builder.env, builder.current_docname, builder,
106 'ref', refid, node, node)
109 return "#" + xref['refid']
111 return xref['refuri']
113 builder.warn('undefined label: %s' % refid)
117 def html_render_svg(self, node):
118 image = node.to_drawer('SVG', self.builder, filename=None, nodoctype=True)
121 if 'align' in node['options']:
122 align = node['options']['align']
123 self.body.append('<div align="%s" class="align-%s">' % (align, align))
124 self.context.append('</div>\n')
126 self.body.append('<div>')
127 self.context.append('</div>\n')
130 for node_id in node['ids']:
131 self.body.append('<span id="%s"></span>' % node_id)
134 size = image.pagesize().resize(**node['options'])
135 self.body.append(image.save(size))
136 self.context.append('')
139 def html_render_clickablemap(self, image, width_ratio, height_ratio):
140 href_nodes = [node for node in image.nodes if node.href]
144 self.body.append('<map name="map_%d">' % id(image))
145 for node in href_nodes:
146 x1, y1, x2, y2 = image.metrics.cell(node)
152 areatag = '<area shape="rect" coords="%s,%s,%s,%s" href="%s">' % (x1, y1, x2, y2, node.href)
153 self.body.append(areatag)
155 self.body.append('</map>')
158 def html_render_png(self, node):
159 image = node.to_drawer('PNG', self.builder)
160 if not os.path.isfile(image.filename):
165 if 'align' in node['options']:
166 align = node['options']['align']
167 self.body.append('<div align="%s" class="align-%s">' % (align, align))
168 self.context.append('</div>\n')
170 self.body.append('<div>')
171 self.context.append('</div>')
173 # link to original image
174 relpath = node.get_relpath('PNG', self.builder)
175 if 'width' in node['options'] or 'height' in node['options'] or 'scale' in node['options']:
176 self.body.append('<a class="reference internal image-reference" href="%s">' % relpath)
177 self.context.append('</a>')
179 self.context.append('')
182 original_size = image.pagesize()
183 resized = original_size.resize(**node['options'])
184 img_attr = dict(src=relpath,
186 height=resized.height)
188 if any(node.href for node in image.nodes):
189 img_attr['usemap'] = "#map_%d" % id(image)
191 width_ratio = float(resized.width) / original_size.width
192 height_ratio = float(resized.height) / original_size.height
193 html_render_clickablemap(self, image, width_ratio, height_ratio)
195 if 'alt' in node['options']:
196 img_attr['alt'] = node['options']['alt']
198 self.body.append(self.starttag(node, 'img', '', empty=True, **img_attr))
202 def html_visit_blockdiag(self, node):
204 image_format = get_image_format_for(self.builder)
205 if image_format.upper() == 'SVG':
206 html_render_svg(self, node)
208 html_render_png(self, node)
209 except UnicodeEncodeError:
210 if self.builder.config.blockdiag_debug:
211 traceback.print_exc()
213 msg = ("blockdiag error: UnicodeEncodeError caught "
214 "(check your font settings)")
215 self.builder.warn(msg)
217 except Exception as exc:
218 if self.builder.config.blockdiag_debug:
219 traceback.print_exc()
221 self.builder.warn('dot code %r: %s' % (node['code'], str(exc)))
225 def html_depart_blockdiag(self, node):
226 self.body.append(self.context.pop())
227 self.body.append(self.context.pop())
230 def get_image_format_for(builder):
231 if builder.format in ('html', 'slides'):
232 image_format = builder.config.blockdiag_html_image_format.upper()
233 elif builder.format == 'latex':
234 if builder.config.blockdiag_tex_image_format:
235 image_format = builder.config.blockdiag_tex_image_format.upper()
237 image_format = builder.config.blockdiag_latex_image_format.upper()
241 if image_format.upper() not in ('PNG', 'PDF', 'SVG'):
242 raise ValueError('unknown format: %s' % image_format)
244 if image_format.upper() == 'PDF':
246 import reportlab # NOQA: importing test
248 raise ImportError('Could not output PDF format. Install reportlab.')
253 def on_builder_inited(self):
254 # show deprecated message
255 if self.builder.config.blockdiag_tex_image_format:
256 self.builder.warn('blockdiag_tex_image_format is deprecated. Use blockdiag_latex_image_format.')
262 fontmappath = self.builder.config.blockdiag_fontmap
263 fontmap = FontMap(fontmappath)
265 fontmap = FontMap(None)
268 fontpath = self.builder.config.blockdiag_fontpath
269 if isinstance(fontpath, string_types):
270 fontpath = [fontpath]
273 config = namedtuple('Config', 'font')(fontpath)
274 fontpath = detectfont(config)
275 fontmap.set_default_font(fontpath)
280 def on_doctree_resolved(self, doctree, docname):
281 if self.builder.format in ('html', 'slides'):
285 image_format = get_image_format_for(self.builder)
286 except Exception as exc:
287 if self.builder.config.blockdiag_debug:
288 traceback.print_exc()
290 self.builder.warn('blockdiag error: %s' % exc)
291 for node in doctree.traverse(blockdiag_node):
292 node.parent.remove(node)
296 for node in doctree.traverse(blockdiag_node):
299 relfn = node.get_relpath(image_format, self.builder)
300 image = node.to_drawer(image_format, self.builder)
301 if not os.path.isfile(image.filename):
305 image = nodes.image(uri=relfn, candidates={'*': relfn}, **node['options'])
306 node.parent.replace(node, image)
307 except Exception as exc:
308 if self.builder.config.blockdiag_debug:
309 traceback.print_exc()
311 self.builder.warn('dot code %r: %s' % (node['code'], str(exc)))
312 node.parent.remove(node)
316 app.add_node(blockdiag_node,
317 html=(html_visit_blockdiag, html_depart_blockdiag))
318 app.add_directive('blockdiag', Blockdiag)
319 app.add_config_value('blockdiag_fontpath', None, 'html')
320 app.add_config_value('blockdiag_fontmap', None, 'html')
321 app.add_config_value('blockdiag_antialias', False, 'html')
322 app.add_config_value('blockdiag_transparency', True, 'html')
323 app.add_config_value('blockdiag_debug', False, 'html')
324 app.add_config_value('blockdiag_html_image_format', 'PNG', 'html')
325 app.add_config_value('blockdiag_tex_image_format', None, 'html') # backward compatibility for 1.3.1
326 app.add_config_value('blockdiag_latex_image_format', 'PNG', 'html')
327 app.connect("builder-inited", on_builder_inited)
328 app.connect("doctree-resolved", on_doctree_resolved)
331 'version': pkg_resources.require('blockdiag')[0].version,
332 'parallel_read_safe': True,
333 'parallel_write_safe': True,