sphinxcontrib/actdiag.py
# -*- coding: utf-8 -*-
"""
actdiag.sphinx_ext
~~~~~~~~~~~~~~~~~~~~
Allow actdiag-formatted diagrams to be included in Sphinx-generated
documents inline.
:copyright: Copyright 2010 by Takeshi Komiya.
:license: BSDL.
"""
from __future__ import absolute_import
import os
import re
import posixpath
import traceback
import pkg_resources
from collections import namedtuple
from docutils import nodes
from sphinx import addnodes
from sphinx.util import logging
from sphinx.util.osutil import ensuredir
import actdiag.utils.rst.nodes
import actdiag.utils.rst.directives
from blockdiag.utils.bootstrap import detectfont, Application
from blockdiag.utils.fontmap import FontMap
from blockdiag.utils.rst.directives import with_blockdiag
# fontconfig; it will be initialized on `builder-inited` event.
fontmap = None
logger = logging.getLogger(__name__)
class actdiag_node(actdiag.utils.rst.nodes.actdiag):
def to_drawer(self, image_format, builder, **kwargs):
if 'filename' in kwargs:
filename = kwargs.pop('filename')
else:
filename = self.get_abspath(image_format, builder)
antialias = builder.config.actdiag_antialias
transparency = builder.config.actdiag_transparency
image = super(actdiag_node, self).to_drawer(image_format, filename, fontmap,
antialias=antialias, transparency=transparency,
**kwargs)
for node in image.diagram.traverse_nodes():
node.href = resolve_reference(builder, node.href)
return image
def get_relpath(self, image_format, builder):
options = dict(antialias=builder.config.actdiag_antialias,
fontpath=builder.config.actdiag_fontpath,
fontmap=builder.config.actdiag_fontmap,
format=image_format,
transparency=builder.config.actdiag_transparency)
if hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) or HTML writer
outputdir = builder.imgpath
else:
outputdir = ''
return posixpath.join(outputdir, self.get_path(**options))
def get_abspath(self, image_format, builder):
options = dict(antialias=builder.config.actdiag_antialias,
fontpath=builder.config.actdiag_fontpath,
fontmap=builder.config.actdiag_fontmap,
format=image_format,
transparency=builder.config.actdiag_transparency)
if hasattr(builder, 'imagedir'): # Sphinx (>= 1.3.x)
outputdir = os.path.join(builder.outdir, builder.imagedir)
elif hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) and HTML writer
outputdir = os.path.join(builder.outdir, '_images')
else:
outputdir = builder.outdir
path = os.path.join(outputdir, self.get_path(**options))
ensuredir(os.path.dirname(path))
return path
class Actdiag(actdiag.utils.rst.directives.ActdiagDirective):
node_class = actdiag_node
def node2image(self, node, diagram):
return node
def resolve_reference(builder, href):
if href is None:
return None
pattern = re.compile("^:ref:`(.+?)`", re.UNICODE)
matched = pattern.search(href)
if matched is None:
return href
elif not hasattr(builder, 'current_docname'): # ex. latex builder
return matched.group(1)
else:
refid = matched.group(1)
domain = builder.env.domains['std']
node = addnodes.pending_xref(refexplicit=False)
xref = domain.resolve_xref(builder.env, builder.current_docname, builder,
'ref', refid, node, node)
if xref:
if 'refid' in xref:
return "#" + xref['refid']
else:
return xref['refuri']
else:
logger.warning('undefined label: %s', refid)
return None
def html_render_svg(self, node):
image = node.to_drawer('SVG', self.builder, filename=None, nodoctype=True)
image.draw()
# align
align = node['options'].get('align', 'default')
self.body.append('<div class="align-%s">' % align)
self.context.append('</div>\n')
# reftarget
for node_id in node['ids']:
self.body.append('<span id="%s"></span>' % node_id)
# resize image
size = image.pagesize().resize(**node['options'])
self.body.append(image.save(size))
self.context.append('')
def html_render_clickablemap(self, image, width_ratio, height_ratio):
href_nodes = [node for node in image.nodes if node.href]
if not href_nodes:
return
self.body.append('<map name="map_%d">' % id(image))
for node in href_nodes:
x1, y1, x2, y2 = image.metrics.cell(node)
x1 *= width_ratio
x2 *= width_ratio
y1 *= height_ratio
y2 *= height_ratio
areatag = '<area shape="rect" coords="%s,%s,%s,%s" href="%s">' % (x1, y1, x2, y2, node.href)
self.body.append(areatag)
self.body.append('</map>')
def html_render_png(self, node):
image = node.to_drawer('PNG', self.builder)
if not os.path.isfile(image.filename):
image.draw()
image.save()
# align
align = node['options'].get('align', 'default')
self.body.append('<div class="align-%s">' % align)
self.context.append('</div>\n')
# link to original image
relpath = node.get_relpath('PNG', self.builder)
if 'width' in node['options'] or 'height' in node['options'] or 'scale' in node['options']:
self.body.append('<a class="reference internal image-reference" href="%s">' % relpath)
self.context.append('</a>')
else:
self.context.append('')
# <img> tag
original_size = image.pagesize()
resized = original_size.resize(**node['options'])
img_attr = dict(src=relpath,
width=resized.width,
height=resized.height)
if any(node.href for node in image.nodes):
img_attr['usemap'] = "#map_%d" % id(image)
width_ratio = float(resized.width) / original_size.width
height_ratio = float(resized.height) / original_size.height
html_render_clickablemap(self, image, width_ratio, height_ratio)
if 'alt' in node['options']:
img_attr['alt'] = node['options']['alt']
self.body.append(self.starttag(node, 'img', '', empty=True, **img_attr))
@with_blockdiag
def html_visit_actdiag(self, node):
try:
image_format = get_image_format_for(self.builder)
if image_format.upper() == 'SVG':
html_render_svg(self, node)
else:
html_render_png(self, node)
except UnicodeEncodeError:
if self.builder.config.actdiag_debug:
traceback.print_exc()
msg = ("actdiag error: UnicodeEncodeError caught "
"(check your font settings)")
logger.warning(msg)
raise nodes.SkipNode
except Exception as exc:
if self.builder.config.actdiag_debug:
traceback.print_exc()
logger.warning('dot code %r: %s', node['code'], exc)
raise nodes.SkipNode
def html_depart_actdiag(self, node):
self.body.append(self.context.pop())
self.body.append(self.context.pop())
def get_image_format_for(builder):
if builder.format in ('html', 'slides'):
image_format = builder.config.actdiag_html_image_format.upper()
elif builder.format == 'latex':
if builder.config.actdiag_tex_image_format:
image_format = builder.config.actdiag_tex_image_format.upper()
else:
image_format = builder.config.actdiag_latex_image_format.upper()
else:
image_format = 'PNG'
if image_format.upper() not in ('PNG', 'PDF', 'SVG'):
raise ValueError('unknown format: %s' % image_format)
if image_format.upper() == 'PDF':
try:
import reportlab # NOQA: importing test
except ImportError:
raise ImportError('Could not output PDF format. Install reportlab.')
return image_format
def on_builder_inited(self):
# show deprecated message
if self.builder.config.actdiag_tex_image_format:
logger.warning('actdiag_tex_image_format is deprecated. Use actdiag_latex_image_format.')
# initialize fontmap
global fontmap
try:
fontmappath = self.builder.config.actdiag_fontmap
fontmap = FontMap(fontmappath)
except Exception:
fontmap = FontMap(None)
try:
fontpath = self.builder.config.actdiag_fontpath
if isinstance(fontpath, str):
fontpath = [fontpath]
if fontpath:
config = namedtuple('Config', 'font')(fontpath)
fontpath = detectfont(config)
fontmap.set_default_font(fontpath)
except Exception:
pass
def on_doctree_resolved(self, doctree, docname):
if self.builder.format in ('html', 'slides'):
return
try:
image_format = get_image_format_for(self.builder)
except Exception as exc:
if self.builder.config.actdiag_debug:
traceback.print_exc()
logger.warning('actdiag error: %s', exc)
for node in doctree.traverse(actdiag_node):
node.parent.remove(node)
return
for node in doctree.traverse(actdiag_node):
try:
with Application():
relfn = node.get_relpath(image_format, self.builder)
image = node.to_drawer(image_format, self.builder)
if not os.path.isfile(image.filename):
image.draw()
image.save()
image = nodes.image(uri=relfn, candidates={'*': relfn}, **node['options'])
node.parent.replace(node, image)
except Exception as exc:
if self.builder.config.actdiag_debug:
traceback.print_exc()
logger.warning('dot code %r: %s', node['code'], exc)
node.parent.remove(node)
def setup(app):
app.add_node(actdiag_node,
html=(html_visit_actdiag, html_depart_actdiag))
app.add_directive('actdiag', Actdiag)
app.add_config_value('actdiag_fontpath', None, 'html')
app.add_config_value('actdiag_fontmap', None, 'html')
app.add_config_value('actdiag_antialias', False, 'html')
app.add_config_value('actdiag_transparency', True, 'html')
app.add_config_value('actdiag_debug', False, 'html')
app.add_config_value('actdiag_html_image_format', 'PNG', 'html')
app.add_config_value('actdiag_tex_image_format', None, 'html') # backward compatibility for 0.6.1
app.add_config_value('actdiag_latex_image_format', 'PNG', 'html')
app.connect("builder-inited", on_builder_inited)
app.connect("doctree-resolved", on_doctree_resolved)
return {
'version': pkg_resources.require('actdiag')[0].version,
'parallel_read_safe': True,
'parallel_write_safe': True,
}