2017-03-06 16:09:27 +03:00
# -*- coding: utf-8; mode: python -*-
# pylint: disable=C0103, R0903, R0912, R0915
u """
scalable figure and image handling
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Sphinx extension which implements scalable image handling .
: copyright : Copyright ( C ) 2016 Markus Heiser
: license : GPL Version 2 , June 1991 see Linux / COPYING for details .
The build for image formats depend on image ' s source format and output ' s
destination format . This extension implement methods to simplify image
handling from the author ' s POV. Directives like ``kernel-figure`` implement
methods * to * always get the best output - format even if some tools are not
installed . For more details take a look at ` ` convert_image ( . . . ) ` ` which is
the core of all conversions .
* ` ` . . kernel - image ` ` : for image handling / a ` ` . . image : : ` ` replacement
* ` ` . . kernel - figure ` ` : for figure handling / a ` ` . . figure : : ` ` replacement
* ` ` . . kernel - render ` ` : for render markup / a concept to embed * render *
markups ( or languages ) . Supported markups ( see ` ` RENDER_MARKUP_EXT ` ` )
- ` ` DOT ` ` : render embedded Graphviz ' s **DOC**
- ` ` SVG ` ` : render embedded Scalable Vector Graphics ( * * SVG * * )
- . . . * developable *
Used tools :
* ` ` dot ( 1 ) ` ` : Graphviz ( http : / / www . graphviz . org ) . If Graphviz is not
available , the DOT language is inserted as literal - block .
* SVG to PDF : To generate PDF , you need at least one of this tools :
- ` ` convert ( 1 ) ` ` : ImageMagick ( https : / / www . imagemagick . org )
List of customizations :
* generate PDF from SVG / used by PDF ( LaTeX ) builder
* generate SVG ( html - builder ) and PDF ( latex - builder ) from DOT files .
DOT : see http : / / www . graphviz . org / content / dot - language
"""
import os
from os import path
import subprocess
from hashlib import sha1
import sys
from docutils import nodes
from docutils . statemachine import ViewList
from docutils . parsers . rst import directives
from docutils . parsers . rst . directives import images
import sphinx
from sphinx . util . nodes import clean_astext
from six import iteritems
2019-05-21 23:23:43 +03:00
import kernellog
2017-03-06 16:09:27 +03:00
PY3 = sys . version_info [ 0 ] == 3
if PY3 :
_unicode = str
else :
_unicode = unicode
# Get Sphinx version
major , minor , patch = sphinx . version_info [ : 3 ]
if major == 1 and minor > 3 :
# patches.Figure only landed in Sphinx 1.4
from sphinx . directives . patches import Figure # pylint: disable=C0413
else :
Figure = images . Figure
__version__ = ' 1.0.0 '
# simple helper
# -------------
def which ( cmd ) :
2018-01-11 14:00:28 +03:00
""" Searches the ``cmd`` in the ``PATH`` environment.
2017-03-06 16:09:27 +03:00
This * which * searches the PATH for executable ` ` cmd ` ` . First match is
returned , if nothing is found , ` ` None ` is returned .
"""
envpath = os . environ . get ( ' PATH ' , None ) or os . defpath
for folder in envpath . split ( os . pathsep ) :
fname = folder + os . sep + cmd
if path . isfile ( fname ) :
return fname
def mkdir ( folder , mode = 0o775 ) :
if not path . isdir ( folder ) :
os . makedirs ( folder , mode )
def file2literal ( fname ) :
with open ( fname , " r " ) as src :
data = src . read ( )
node = nodes . literal_block ( data , data )
return node
def isNewer ( path1 , path2 ) :
""" Returns True if ``path1`` is newer than ``path2``
If ` ` path1 ` ` exists and is newer than ` ` path2 ` ` the function returns
` ` True ` ` is returned otherwise ` ` False ` `
"""
return ( path . exists ( path1 )
and os . stat ( path1 ) . st_ctime > os . stat ( path2 ) . st_ctime )
def pass_handle ( self , node ) : # pylint: disable=W0613
pass
# setup conversion tools and sphinx extension
# -------------------------------------------
# Graphviz's dot(1) support
dot_cmd = None
# ImageMagick' convert(1) support
convert_cmd = None
def setup ( app ) :
# check toolchain first
app . connect ( ' builder-inited ' , setupTools )
# image handling
app . add_directive ( " kernel-image " , KernelImage )
app . add_node ( kernel_image ,
html = ( visit_kernel_image , pass_handle ) ,
latex = ( visit_kernel_image , pass_handle ) ,
texinfo = ( visit_kernel_image , pass_handle ) ,
text = ( visit_kernel_image , pass_handle ) ,
man = ( visit_kernel_image , pass_handle ) , )
# figure handling
app . add_directive ( " kernel-figure " , KernelFigure )
app . add_node ( kernel_figure ,
html = ( visit_kernel_figure , pass_handle ) ,
latex = ( visit_kernel_figure , pass_handle ) ,
texinfo = ( visit_kernel_figure , pass_handle ) ,
text = ( visit_kernel_figure , pass_handle ) ,
man = ( visit_kernel_figure , pass_handle ) , )
# render handling
app . add_directive ( ' kernel-render ' , KernelRender )
app . add_node ( kernel_render ,
html = ( visit_kernel_render , pass_handle ) ,
latex = ( visit_kernel_render , pass_handle ) ,
texinfo = ( visit_kernel_render , pass_handle ) ,
text = ( visit_kernel_render , pass_handle ) ,
man = ( visit_kernel_render , pass_handle ) , )
app . connect ( ' doctree-read ' , add_kernel_figure_to_std_domain )
return dict (
version = __version__ ,
parallel_read_safe = True ,
parallel_write_safe = True
)
def setupTools ( app ) :
u """
Check available build tools and log some * verbose * messages .
This function is called once , when the builder is initiated .
"""
global dot_cmd , convert_cmd # pylint: disable=W0603
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , " kfigure: check installed tools ... " )
2017-03-06 16:09:27 +03:00
dot_cmd = which ( ' dot ' )
convert_cmd = which ( ' convert ' )
if dot_cmd :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , " use dot(1) from: " + dot_cmd )
2017-03-06 16:09:27 +03:00
else :
2019-05-21 23:23:43 +03:00
kernellog . warn ( app , " dot(1) not found, for better output quality install "
" graphviz from http://www.graphviz.org " )
2017-03-06 16:09:27 +03:00
if convert_cmd :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , " use convert(1) from: " + convert_cmd )
2017-03-06 16:09:27 +03:00
else :
2019-05-21 23:23:43 +03:00
kernellog . warn ( app ,
2017-03-06 16:09:27 +03:00
" convert(1) not found, for SVG to PDF conversion install "
" ImageMagick (https://www.imagemagick.org) " )
# integrate conversion tools
# --------------------------
RENDER_MARKUP_EXT = {
# The '.ext' must be handled by convert_image(..) function's *in_ext* input.
# <name> : <.ext>
' DOT ' : ' .dot ' ,
' SVG ' : ' .svg '
}
def convert_image ( img_node , translator , src_fname = None ) :
""" Convert a image node for the builder.
Different builder prefer different image formats , e . g . * latex * builder
prefer PDF while * html * builder prefer SVG format for images .
This function handles output image formats in dependence of source the
format ( of the image ) and the translator ' s output format.
"""
app = translator . builder . app
fname , in_ext = path . splitext ( path . basename ( img_node [ ' uri ' ] ) )
if src_fname is None :
src_fname = path . join ( translator . builder . srcdir , img_node [ ' uri ' ] )
if not path . exists ( src_fname ) :
src_fname = path . join ( translator . builder . outdir , img_node [ ' uri ' ] )
dst_fname = None
# in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , ' assert best format for: ' + img_node [ ' uri ' ] )
2017-03-06 16:09:27 +03:00
if in_ext == ' .dot ' :
if not dot_cmd :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app ,
" dot from graphviz not available / include DOT raw. " )
2017-03-06 16:09:27 +03:00
img_node . replace_self ( file2literal ( src_fname ) )
elif translator . builder . format == ' latex ' :
dst_fname = path . join ( translator . builder . outdir , fname + ' .pdf ' )
img_node [ ' uri ' ] = fname + ' .pdf '
img_node [ ' candidates ' ] = { ' * ' : fname + ' .pdf ' }
elif translator . builder . format == ' html ' :
dst_fname = path . join (
translator . builder . outdir ,
translator . builder . imagedir ,
fname + ' .svg ' )
img_node [ ' uri ' ] = path . join (
translator . builder . imgpath , fname + ' .svg ' )
img_node [ ' candidates ' ] = {
' * ' : path . join ( translator . builder . imgpath , fname + ' .svg ' ) }
else :
# all other builder formats will include DOT as raw
img_node . replace_self ( file2literal ( src_fname ) )
elif in_ext == ' .svg ' :
if translator . builder . format == ' latex ' :
if convert_cmd is None :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app ,
" no SVG to PDF conversion available / include SVG raw. " )
2017-03-06 16:09:27 +03:00
img_node . replace_self ( file2literal ( src_fname ) )
else :
dst_fname = path . join ( translator . builder . outdir , fname + ' .pdf ' )
img_node [ ' uri ' ] = fname + ' .pdf '
img_node [ ' candidates ' ] = { ' * ' : fname + ' .pdf ' }
if dst_fname :
# the builder needs not to copy one more time, so pop it if exists.
translator . builder . images . pop ( img_node [ ' uri ' ] , None )
_name = dst_fname [ len ( translator . builder . outdir ) + 1 : ]
if isNewer ( dst_fname , src_fname ) :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app ,
" convert: {out} / %s already exists and is newer " % _name )
2017-03-06 16:09:27 +03:00
else :
ok = False
mkdir ( path . dirname ( dst_fname ) )
if in_ext == ' .dot ' :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , ' convert DOT to: {out} / ' + _name )
2017-03-06 16:09:27 +03:00
ok = dot2format ( app , src_fname , dst_fname )
elif in_ext == ' .svg ' :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , ' convert SVG to: {out} / ' + _name )
2017-03-06 16:09:27 +03:00
ok = svg2pdf ( app , src_fname , dst_fname )
if not ok :
img_node . replace_self ( file2literal ( src_fname ) )
def dot2format ( app , dot_fname , out_fname ) :
""" Converts DOT file to ``out_fname`` using ``dot(1)``.
* ` ` dot_fname ` ` pathname of the input DOT file , including extension ` ` . dot ` `
* ` ` out_fname ` ` pathname of the output file , including format extension
The * format extension * depends on the ` ` dot ` ` command ( see ` ` man dot ` `
option ` ` - Txxx ` ` ) . Normally you will use one of the following extensions :
- ` ` . ps ` ` for PostScript ,
- ` ` . svg ` ` or ` ` svgz ` ` for Structured Vector Graphics ,
- ` ` . fig ` ` for XFIG graphics and
- ` ` . png ` ` or ` ` gif ` ` for common bitmap graphics .
"""
out_format = path . splitext ( out_fname ) [ 1 ] [ 1 : ]
cmd = [ dot_cmd , ' -T %s ' % out_format , dot_fname ]
exit_code = 42
with open ( out_fname , " w " ) as out :
exit_code = subprocess . call ( cmd , stdout = out )
if exit_code != 0 :
2019-05-21 23:23:43 +03:00
kernellog . warn ( app ,
" Error # %d when calling: %s " % ( exit_code , " " . join ( cmd ) ) )
2017-03-06 16:09:27 +03:00
return bool ( exit_code == 0 )
def svg2pdf ( app , svg_fname , pdf_fname ) :
""" Converts SVG to PDF with ``convert(1)`` command.
Uses ` ` convert ( 1 ) ` ` from ImageMagick ( https : / / www . imagemagick . org ) for
conversion . Returns ` ` True ` ` on success and ` ` False ` ` if an error occurred .
* ` ` svg_fname ` ` pathname of the input SVG file with extension ( ` ` . svg ` ` )
* ` ` pdf_name ` ` pathname of the output PDF file with extension ( ` ` . pdf ` ` )
"""
cmd = [ convert_cmd , svg_fname , pdf_fname ]
# use stdout and stderr from parent
exit_code = subprocess . call ( cmd )
if exit_code != 0 :
2019-05-21 23:23:43 +03:00
kernellog . warn ( app , " Error # %d when calling: %s " % ( exit_code , " " . join ( cmd ) ) )
2017-03-06 16:09:27 +03:00
return bool ( exit_code == 0 )
# image handling
# ---------------------
def visit_kernel_image ( self , node ) : # pylint: disable=W0613
""" Visitor of the ``kernel_image`` Node.
Handles the ` ` image ` ` child - node with the ` ` convert_image ( . . . ) ` ` .
"""
img_node = node [ 0 ]
convert_image ( img_node , self )
class kernel_image ( nodes . image ) :
""" Node for ``kernel-image`` directive. """
pass
class KernelImage ( images . Image ) :
u """ KernelImage directive
Earns everything from ` ` . . image : : ` ` directive , except * remote URI * and
* glob * pattern . The KernelImage wraps a image node into a
kernel_image node . See ` ` visit_kernel_image ` ` .
"""
def run ( self ) :
uri = self . arguments [ 0 ]
if uri . endswith ( ' .* ' ) or uri . find ( ' :// ' ) != - 1 :
raise self . severe (
' Error in " %s : %s " : glob pattern and remote images are not allowed '
% ( self . name , uri ) )
result = images . Image . run ( self )
if len ( result ) == 2 or isinstance ( result [ 0 ] , nodes . system_message ) :
return result
( image_node , ) = result
# wrap image node into a kernel_image node / see visitors
node = kernel_image ( ' ' , image_node )
return [ node ]
# figure handling
# ---------------------
def visit_kernel_figure ( self , node ) : # pylint: disable=W0613
""" Visitor of the ``kernel_figure`` Node.
Handles the ` ` image ` ` child - node with the ` ` convert_image ( . . . ) ` ` .
"""
img_node = node [ 0 ] [ 0 ]
convert_image ( img_node , self )
class kernel_figure ( nodes . figure ) :
""" Node for ``kernel-figure`` directive. """
class KernelFigure ( Figure ) :
u """ KernelImage directive
Earns everything from ` ` . . figure : : ` ` directive , except * remote URI * and
* glob * pattern . The KernelFigure wraps a figure node into a kernel_figure
node . See ` ` visit_kernel_figure ` ` .
"""
def run ( self ) :
uri = self . arguments [ 0 ]
if uri . endswith ( ' .* ' ) or uri . find ( ' :// ' ) != - 1 :
raise self . severe (
' Error in " %s : %s " : '
' glob pattern and remote images are not allowed '
% ( self . name , uri ) )
result = Figure . run ( self )
if len ( result ) == 2 or isinstance ( result [ 0 ] , nodes . system_message ) :
return result
( figure_node , ) = result
# wrap figure node into a kernel_figure node / see visitors
node = kernel_figure ( ' ' , figure_node )
return [ node ]
# render handling
# ---------------------
def visit_kernel_render ( self , node ) :
""" Visitor of the ``kernel_render`` Node.
If rendering tools available , save the markup of the ` ` literal_block ` ` child
node into a file and replace the ` ` literal_block ` ` node with a new created
` ` image ` ` node , pointing to the saved markup file . Afterwards , handle the
image child - node with the ` ` convert_image ( . . . ) ` ` .
"""
app = self . builder . app
srclang = node . get ( ' srclang ' )
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , ' visit kernel-render node lang: " %s " ' % ( srclang ) )
2017-03-06 16:09:27 +03:00
tmp_ext = RENDER_MARKUP_EXT . get ( srclang , None )
if tmp_ext is None :
2019-05-21 23:23:43 +03:00
kernellog . warn ( app , ' kernel-render: " %s " unknown / include raw. ' % ( srclang ) )
2017-03-06 16:09:27 +03:00
return
if not dot_cmd and tmp_ext == ' .dot ' :
2019-05-21 23:23:43 +03:00
kernellog . verbose ( app , " dot from graphviz not available / include raw. " )
2017-03-06 16:09:27 +03:00
return
literal_block = node [ 0 ]
code = literal_block . astext ( )
hashobj = code . encode ( ' utf-8 ' ) # str(node.attributes)
fname = path . join ( ' %s - %s ' % ( srclang , sha1 ( hashobj ) . hexdigest ( ) ) )
tmp_fname = path . join (
self . builder . outdir , self . builder . imagedir , fname + tmp_ext )
if not path . isfile ( tmp_fname ) :
mkdir ( path . dirname ( tmp_fname ) )
with open ( tmp_fname , " w " ) as out :
out . write ( code )
img_node = nodes . image ( node . rawsource , * * node . attributes )
img_node [ ' uri ' ] = path . join ( self . builder . imgpath , fname + tmp_ext )
img_node [ ' candidates ' ] = {
' * ' : path . join ( self . builder . imgpath , fname + tmp_ext ) }
literal_block . replace_self ( img_node )
convert_image ( img_node , self , tmp_fname )
class kernel_render ( nodes . General , nodes . Inline , nodes . Element ) :
""" Node for ``kernel-render`` directive. """
pass
class KernelRender ( Figure ) :
u """ KernelRender directive
Render content by external tool . Has all the options known from the
* figure * directive , plus option ` ` caption ` ` . If ` ` caption ` ` has a
value , a figure node with the * caption * is inserted . If not , a image node is
inserted .
The KernelRender directive wraps the text of the directive into a
literal_block node and wraps it into a kernel_render node . See
` ` visit_kernel_render ` ` .
"""
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
# earn options from 'figure'
option_spec = Figure . option_spec . copy ( )
option_spec [ ' caption ' ] = directives . unchanged
def run ( self ) :
return [ self . build_node ( ) ]
def build_node ( self ) :
srclang = self . arguments [ 0 ] . strip ( )
if srclang not in RENDER_MARKUP_EXT . keys ( ) :
return [ self . state_machine . reporter . warning (
2018-01-11 14:00:28 +03:00
' Unknown source language " %s " , use one of: %s . ' % (
2017-03-06 16:09:27 +03:00
srclang , " , " . join ( RENDER_MARKUP_EXT . keys ( ) ) ) ,
line = self . lineno ) ]
code = ' \n ' . join ( self . content )
if not code . strip ( ) :
return [ self . state_machine . reporter . warning (
' Ignoring " %s " directive without content. ' % (
self . name ) ,
line = self . lineno ) ]
node = kernel_render ( )
node [ ' alt ' ] = self . options . get ( ' alt ' , ' ' )
node [ ' srclang ' ] = srclang
literal_node = nodes . literal_block ( code , code )
node + = literal_node
caption = self . options . get ( ' caption ' )
if caption :
# parse caption's content
parsed = nodes . Element ( )
self . state . nested_parse (
ViewList ( [ caption ] , source = ' ' ) , self . content_offset , parsed )
caption_node = nodes . caption (
parsed [ 0 ] . rawsource , ' ' , * parsed [ 0 ] . children )
caption_node . source = parsed [ 0 ] . source
caption_node . line = parsed [ 0 ] . line
figure_node = nodes . figure ( ' ' , node )
for k , v in self . options . items ( ) :
figure_node [ k ] = v
figure_node + = caption_node
node = figure_node
return node
def add_kernel_figure_to_std_domain ( app , doctree ) :
""" Add kernel-figure anchors to ' std ' domain.
The ` ` StandardDomain . process_doc ( . . ) ` ` method does not know how to resolve
the caption ( label ) of ` ` kernel - figure ` ` directive ( it only knows about
standard nodes , e . g . table , figure etc . ) . Without any additional handling
this will result in a ' undefined label ' for kernel - figures .
This handle adds labels of kernel - figure to the ' std ' domain labels .
"""
std = app . env . domains [ " std " ]
docname = app . env . docname
labels = std . data [ " labels " ]
for name , explicit in iteritems ( doctree . nametypes ) :
if not explicit :
continue
labelid = doctree . nameids [ name ]
if labelid is None :
continue
node = doctree . ids [ labelid ]
if node . tagname == ' kernel_figure ' :
for n in node . next_node ( ) :
if n . tagname == ' caption ' :
sectname = clean_astext ( n )
# add label to std domain
labels [ name ] = docname , labelid , sectname
break