Added support for SVG to `pcb_print`
- And now is much faster because all the processing is done using SVGs and we generate PDFs only during the last step.
This commit is contained in:
parent
db3a6c05cc
commit
4e659c3ddd
|
|
@ -1536,6 +1536,7 @@ Next time you need this list just use an alias, like this:
|
|||
- `dnf_filter`: [string|list(string)='_none'] Name of the filter to mark components as not fitted.
|
||||
A short-cut to use for simple cases where a variant is an overkill.
|
||||
- `drill_marks`: [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale).
|
||||
- `format`: [string='PDF'] [PDF,SVG] Format for the output file/s.
|
||||
- `hide_excluded`: [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant.
|
||||
- `output`: [string='%f-%i%I%v.%x'] Filename for the output PDF (%i=assembly, %x=pdf). Affected by global options.
|
||||
- *output_name*: Alias for output.
|
||||
|
|
@ -3066,6 +3067,7 @@ Additionally we support:
|
|||
- **Python macros**: Juha Jeronen (@Technologicat)
|
||||
- **Board2Pdf**: Albin Dennevi
|
||||
- **PyPDF2**: Mathieu Fenniak
|
||||
- **svgutils**: Bartosz Telenczuk (@btel)
|
||||
- **Contributors**:
|
||||
- **Error filters ideas**: Leandro Heck (@leoheck)
|
||||
- **GitHub Actions Integration/SVG output**: @nerdyscout
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Package: kibot
|
|||
Architecture: all
|
||||
Multi-Arch: foreign
|
||||
Depends: ${misc:Depends}, ${python3:Depends}, python3-distutils, python3-yaml, kicad (>= 5.1.6), python3-wxgtk4.0
|
||||
Recommends: kibom.inti-cmnb (>= 1.8.0), interactivehtmlbom.inti-cmnb, pcbdraw, imagemagick, librsvg2-bin, python3-xlsxwriter, rar, poppler-utils
|
||||
Recommends: kibom.inti-cmnb (>= 1.8.0), interactivehtmlbom.inti-cmnb, pcbdraw, imagemagick, librsvg2-bin, python3-xlsxwriter, rar, poppler-utils, python3-lxml
|
||||
Suggests: pandoc, texlive-latex-base, texlive-latex-recommended, git
|
||||
Description: KiCad Bot
|
||||
KiBot is a program which helps you to automate the generation of KiCad
|
||||
|
|
|
|||
|
|
@ -1444,6 +1444,7 @@ Additionally we support:
|
|||
- **Python macros**: Juha Jeronen (@Technologicat)
|
||||
- **Board2Pdf**: Albin Dennevi
|
||||
- **PyPDF2**: Mathieu Fenniak
|
||||
- **svgutils**: Bartosz Telenczuk (@btel)
|
||||
- **Contributors**:
|
||||
- **Error filters ideas**: Leandro Heck (@leoheck)
|
||||
- **GitHub Actions Integration/SVG output**: @nerdyscout
|
||||
|
|
|
|||
|
|
@ -966,6 +966,8 @@ outputs:
|
|||
dnf_filter: '_none'
|
||||
# [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale)
|
||||
drill_marks: 'full'
|
||||
# [string='PDF'] [PDF,SVG] Format for the output file/s
|
||||
format: 'PDF'
|
||||
# [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant
|
||||
hide_excluded: false
|
||||
# [string='%f-%i%I%v.%x'] Filename for the output PDF (%i=assembly, %x=pdf). Affected by global options
|
||||
|
|
|
|||
|
|
@ -8,16 +8,17 @@
|
|||
# Note: Original code released as Public Domain
|
||||
import os
|
||||
import subprocess
|
||||
from pcbnew import PLOT_CONTROLLER, PLOT_FORMAT_PDF, FromMM
|
||||
from shutil import rmtree
|
||||
from pcbnew import PLOT_CONTROLLER, FromMM, PLOT_FORMAT_SVG
|
||||
from shutil import rmtree, which
|
||||
from tempfile import mkdtemp
|
||||
from .svgutils.transform import fromstring
|
||||
from .error import KiPlotConfigurationError
|
||||
from .gs import GS
|
||||
from .optionable import Optionable
|
||||
from .out_base import VariantOptions
|
||||
from .kicad.color_theme import load_color_theme
|
||||
from .kicad.patch_svg import patch_svg_file
|
||||
from .misc import CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT
|
||||
from .misc import CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT, MISSING_TOOL
|
||||
from .kiplot import check_script, exec_with_retry, add_extra_options
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from .layer import Layer, get_priority
|
||||
|
|
@ -25,9 +26,7 @@ from . import PyPDF2
|
|||
from . import log
|
||||
|
||||
logger = log.get_logger()
|
||||
|
||||
# TODO:
|
||||
# - SVG?
|
||||
SVG2PDF = 'rsvg-convert'
|
||||
|
||||
|
||||
def _run_command(cmd):
|
||||
|
|
@ -56,83 +55,42 @@ def to_gray(color):
|
|||
return (avg, avg, avg)
|
||||
|
||||
|
||||
def colorize_pdf(folder, in_file, out_file, color, black_holes):
|
||||
er = None
|
||||
pdf_color = [PyPDF2.generic.FloatObject(color[0]), PyPDF2.generic.FloatObject(color[1]),
|
||||
PyPDF2.generic.FloatObject(color[2])]
|
||||
black_color = [PyPDF2.generic.FloatObject(0), PyPDF2.generic.FloatObject(0), PyPDF2.generic.FloatObject(0)]
|
||||
try:
|
||||
with open(os.path.join(folder, in_file), "rb") as f:
|
||||
source = PyPDF2.PdfFileReader(f, "rb")
|
||||
output = PyPDF2.PdfFileWriter()
|
||||
for page in range(source.getNumPages()):
|
||||
page = source.getPage(page)
|
||||
content_object = page["/Contents"].getObject()
|
||||
content = PyPDF2.pdf.ContentStream(content_object, source)
|
||||
for i, (operands, operator) in enumerate(content.operations):
|
||||
if operator == b"rg" or operator == b"RG":
|
||||
if operands == [0, 0, 0]:
|
||||
# Replace black by the selected color
|
||||
content.operations[i] = (pdf_color, operator)
|
||||
elif black_holes and operands == [1, 1, 1]:
|
||||
# Replace white by black
|
||||
content.operations[i] = (black_color, operator)
|
||||
page.__setitem__(PyPDF2.generic.NameObject('/Contents'), content)
|
||||
output.addPage(page)
|
||||
try:
|
||||
with open(os.path.join(folder, out_file), "wb") as outputStream:
|
||||
output.write(outputStream)
|
||||
except (IOError, ValueError, EOFError) as e:
|
||||
er = str(e)
|
||||
if er:
|
||||
raise KiPlotConfigurationError('Error creating `{}` ({})'.format(out_file, er))
|
||||
except (IOError, ValueError, EOFError) as e:
|
||||
er = str(e)
|
||||
if er:
|
||||
raise KiPlotConfigurationError('Error reading `{}` ({})'.format(in_file, er))
|
||||
def to_gray_hex(color):
|
||||
rgb, alpha = hex_to_rgb(color)
|
||||
avg = (rgb[0]+rgb[1]+rgb[2])/3
|
||||
avg_str = '%02X' % int(avg*255)
|
||||
return '#'+avg_str+avg_str+avg_str
|
||||
|
||||
|
||||
def merge_pdf(input_folder, input_files, output_folder, output_file):
|
||||
def load_svg(file, color, black_holes, monochrome):
|
||||
with open(file, 'rt') as f:
|
||||
content = f.read()
|
||||
color = color[:7]
|
||||
if monochrome:
|
||||
color = to_gray_hex(color)
|
||||
if black_holes:
|
||||
content = content.replace('#FFFFFF', '**black_hole**')
|
||||
if color != '#000000':
|
||||
content = content.replace('#000000', color)
|
||||
if black_holes:
|
||||
content = content.replace('**black_hole**', '#000000')
|
||||
return content
|
||||
|
||||
|
||||
def merge_svg(input_folder, input_files, output_folder, output_file, black_holes, monochrome):
|
||||
""" Merge all pages into one """
|
||||
output = PyPDF2.PdfFileWriter()
|
||||
# Collect all pages, as a merged one
|
||||
i = 0
|
||||
er = None
|
||||
open_files = []
|
||||
extra_debug = GS.debug_level >= 3
|
||||
for filename in input_files:
|
||||
if extra_debug:
|
||||
logger.debug(" - {}".format(filename))
|
||||
try:
|
||||
file = open(os.path.join(input_folder, filename), 'rb')
|
||||
open_files.append(file)
|
||||
pdf_reader = PyPDF2.PdfFileReader(file)
|
||||
page_obj = pdf_reader.getPage(0)
|
||||
if(i == 0):
|
||||
merged_page = page_obj
|
||||
else:
|
||||
merged_page.mergePage(page_obj)
|
||||
i = i+1
|
||||
except (IOError, ValueError, EOFError) as e:
|
||||
er = str(e)
|
||||
if er:
|
||||
raise KiPlotConfigurationError('Error reading `{}` ({})'.format(filename, er))
|
||||
output.addPage(merged_page)
|
||||
# Write the result to a file
|
||||
pdf_output = None
|
||||
try:
|
||||
pdf_output = open(os.path.join(output_folder, output_file), 'wb')
|
||||
output.write(pdf_output)
|
||||
except (IOError, ValueError, EOFError) as e:
|
||||
er = str(e)
|
||||
finally:
|
||||
if pdf_output:
|
||||
pdf_output.close()
|
||||
if er:
|
||||
raise KiPlotConfigurationError('Error creating `{}` ({})'.format(output_file, er))
|
||||
# Close the input files
|
||||
for f in open_files:
|
||||
f.close()
|
||||
first = True
|
||||
for (file, color) in input_files:
|
||||
file = os.path.join(input_folder, file)
|
||||
new_layer = fromstring(load_svg(file, color, black_holes, monochrome))
|
||||
if first:
|
||||
svg_out = new_layer
|
||||
first = False
|
||||
else:
|
||||
root = new_layer.getroot()
|
||||
root.moveto(1, 1)
|
||||
svg_out.append([root])
|
||||
svg_out.save(os.path.join(output_folder, output_file))
|
||||
|
||||
|
||||
def create_pdf_from_pages(input_folder, input_files, output_fn):
|
||||
|
|
@ -169,17 +127,20 @@ def create_pdf_from_pages(input_folder, input_files, output_fn):
|
|||
f.close()
|
||||
|
||||
|
||||
def colorize_layer(suffix, color, monochrome, filelist, temp_dir, black_holes=False):
|
||||
in_file = GS.pcb_basename+"-"+suffix+".pdf"
|
||||
if color != "#000000":
|
||||
out_file = GS.pcb_basename+"-"+suffix+"-colored.pdf"
|
||||
logger.debug('- Giving color to {} -> {} ({})'.format(in_file, out_file, color))
|
||||
rgb, alpha = hex_to_rgb(color)
|
||||
color = rgb if not monochrome else to_gray(rgb)
|
||||
colorize_pdf(temp_dir, in_file, out_file, color, black_holes)
|
||||
filelist.append(out_file)
|
||||
else:
|
||||
filelist.append(in_file)
|
||||
def svg_to_pdf(input_folder, svg_file, pdf_file):
|
||||
# Note: rsvg-convert uses 90 dpi but KiCad (and the docs I found) says SVG pt is 72 dpi
|
||||
cmd = [SVG2PDF, '-d', '72', '-p', '72', '-f', 'pdf', '-o', os.path.join(input_folder, pdf_file),
|
||||
os.path.join(input_folder, svg_file)]
|
||||
_run_command(cmd)
|
||||
|
||||
|
||||
def create_pdf_from_svg_pages(input_folder, input_files, output_fn):
|
||||
svg_files = []
|
||||
for svg_file in input_files:
|
||||
pdf_file = svg_file.replace('.svg', '.pdf')
|
||||
svg_to_pdf(input_folder, svg_file, pdf_file)
|
||||
svg_files.append(pdf_file)
|
||||
create_pdf_from_pages(input_folder, svg_files, output_fn)
|
||||
|
||||
|
||||
class LayerOptions(Layer):
|
||||
|
|
@ -275,8 +236,9 @@ class PCB_PrintOptions(VariantOptions):
|
|||
self.title = ''
|
||||
""" Text used to replace the sheet title. %VALUE expansions are allowed.
|
||||
If it starts with `+` the text is concatenated """
|
||||
self.format = 'PDF'
|
||||
""" [PDF,SVG] Format for the output file/s """
|
||||
super().__init__()
|
||||
self._expand_ext = 'pdf'
|
||||
self._expand_id = 'assembly'
|
||||
|
||||
@property
|
||||
|
|
@ -306,6 +268,7 @@ class PCB_PrintOptions(VariantOptions):
|
|||
else:
|
||||
la.color = "#000000"
|
||||
self._drill_marks = PCB_PrintOptions._drill_marks_map[self._drill_marks]
|
||||
self._expand_ext = self.format.lower()
|
||||
|
||||
def filter_components(self):
|
||||
if not self._comps:
|
||||
|
|
@ -355,7 +318,7 @@ class PCB_PrintOptions(VariantOptions):
|
|||
po.SetScale(1.0)
|
||||
po.SetNegative(False)
|
||||
pc.SetLayer(self.edge_layer)
|
||||
pc.OpenPlotfile('frame', PLOT_FORMAT_PDF, p.sheet)
|
||||
pc.OpenPlotfile('frame', PLOT_FORMAT_SVG, p.sheet)
|
||||
pc.PlotLayer()
|
||||
self.restore_edge_cuts()
|
||||
|
||||
|
|
@ -389,11 +352,13 @@ class PCB_PrintOptions(VariantOptions):
|
|||
if os.path.isfile(video_name):
|
||||
os.remove(video_name)
|
||||
patch_svg_file(output, remove_bkg=True)
|
||||
# Note: rsvg-convert uses 90 dpi but KiCad (and the docs I found) says SVG pt is 72 dpi
|
||||
cmd = ['rsvg-convert', '-d', '72', '-p', '72', '-f', 'pdf', '-o', output.replace('.svg', '.pdf'), output]
|
||||
_run_command(cmd)
|
||||
|
||||
def generate_output(self, output):
|
||||
if which(SVG2PDF) is None:
|
||||
logger.error('`{}` not installed and needed for PDF output'.format(SVG2PDF))
|
||||
logger.error('Install `librsvg2-bin` or equivalent')
|
||||
exit(MISSING_TOOL)
|
||||
output_dir = os.path.dirname(output)
|
||||
temp_dir = mkdtemp(prefix='tmp-kibot-pcb_print-')
|
||||
logger.debug('- Temporal dir: {}'.format(temp_dir))
|
||||
# Plot options
|
||||
|
|
@ -418,6 +383,7 @@ class PCB_PrintOptions(VariantOptions):
|
|||
if GS.ki5():
|
||||
po.SetLineWidth(FromMM(p.line_width))
|
||||
po.SetPlotPadsOnSilkLayer(not p.exclude_pads_from_silkscreen)
|
||||
filelist = []
|
||||
for la in p.layers:
|
||||
id = la._id
|
||||
logger.debug('- Plotting layer {} ({})'.format(la.layer, id))
|
||||
|
|
@ -425,8 +391,9 @@ class PCB_PrintOptions(VariantOptions):
|
|||
po.SetPlotValue(la.plot_footprint_values)
|
||||
po.SetPlotInvisibleText(la.force_plot_invisible_refs_vals)
|
||||
pc.SetLayer(id)
|
||||
pc.OpenPlotfile(la.suffix, PLOT_FORMAT_PDF, p.sheet)
|
||||
pc.OpenPlotfile(la.suffix, PLOT_FORMAT_SVG, p.sheet)
|
||||
pc.PlotLayer()
|
||||
filelist.append((GS.pcb_basename+"-"+la.suffix+".svg", la.color))
|
||||
# 2) Plot the frame using an empty layer and 1.0 scale
|
||||
if self.plot_sheet_reference:
|
||||
logger.debug('- Plotting the frame')
|
||||
|
|
@ -434,24 +401,23 @@ class PCB_PrintOptions(VariantOptions):
|
|||
self.plot_frame_ki6(pc, po, p)
|
||||
else:
|
||||
self.plot_frame_ki5(temp_dir)
|
||||
pc.ClosePlot()
|
||||
# 3) Apply the colors to the layer PDFs
|
||||
filelist = []
|
||||
for la in p.layers:
|
||||
colorize_layer(la.suffix, la.color, p.monochrome, filelist, temp_dir, p.black_holes)
|
||||
# 4) Apply color to the frame
|
||||
if self.plot_sheet_reference:
|
||||
color = p.sheet_reference_color if p.sheet_reference_color else self._color_theme.pcb_frame
|
||||
colorize_layer('frame', color, p.monochrome, filelist, temp_dir)
|
||||
# 5) Stack all layers in one file
|
||||
assembly_file = GS.pcb_basename+"-"+str(n)+".pdf"
|
||||
filelist.append((GS.pcb_basename+"-frame.svg", color))
|
||||
pc.ClosePlot()
|
||||
# 3) Stack all layers in one file
|
||||
if self.format == 'PDF':
|
||||
assembly_file = GS.pcb_basename+"-"+str(n+1)+".svg"
|
||||
else:
|
||||
id = self._expand_id+('_page_%02d' % (n+1))
|
||||
assembly_file = self.expand_filename(output_dir, self.output, id, self._expand_ext)
|
||||
logger.debug('- Merging layers to {}'.format(assembly_file))
|
||||
merge_pdf(temp_dir, filelist, temp_dir, assembly_file)
|
||||
merge_svg(temp_dir, filelist, temp_dir, assembly_file, p.black_holes, p.monochrome)
|
||||
pages.append(assembly_file)
|
||||
self.restore_title()
|
||||
# Join all pages in one file
|
||||
logger.debug('- Creating output file {}'.format(output))
|
||||
create_pdf_from_pages(temp_dir, pages, output)
|
||||
if self.format == 'PDF':
|
||||
logger.debug('- Creating output file {}'.format(output))
|
||||
create_pdf_from_svg_pages(temp_dir, pages, output)
|
||||
# Remove the temporal files
|
||||
rmtree(temp_dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import transform, compose # noqa: F401
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
# coding=utf-8
|
||||
"""SVG definitions designed for easy SVG composing
|
||||
|
||||
Features:
|
||||
* allow for wildcard import
|
||||
* defines a mini language for SVG composing
|
||||
* short but readable names
|
||||
* easy nesting
|
||||
* method chaining
|
||||
* no boilerplate code (reading files, extracting objects from svg,
|
||||
transversing XML tree)
|
||||
* universal methods applicable to all element types
|
||||
* don't have to learn python
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import transform
|
||||
|
||||
CONFIG = {
|
||||
"svg.file_path": ".",
|
||||
"figure.save_path": ".",
|
||||
"image.file_path": ".",
|
||||
"text.position": (0, 0),
|
||||
"text.size": 8,
|
||||
"text.weight": "normal",
|
||||
"text.font": "Verdana",
|
||||
}
|
||||
|
||||
|
||||
class Element(transform.FigureElement):
|
||||
"""Base class for new SVG elements."""
|
||||
|
||||
def rotate(self, angle, x=0, y=0):
|
||||
"""Rotate element by given angle around given pivot.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
angle : float
|
||||
rotation angle in degrees
|
||||
x, y : float
|
||||
pivot coordinates in user coordinate system (defaults to top-left
|
||||
corner of the figure)
|
||||
"""
|
||||
super(Element, self).rotate(angle, x, y)
|
||||
return self
|
||||
|
||||
def move(self, x, y):
|
||||
"""Move the element by x, y.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x,y : int, str
|
||||
amount of horizontal and vertical shift
|
||||
|
||||
Notes
|
||||
-----
|
||||
The x, y can be given with a unit (for example, "3px", "5cm"). If no
|
||||
unit is given the user unit is assumed ("px"). In SVG all units are
|
||||
defined in relation to the user unit [1]_.
|
||||
|
||||
.. [1] W3C SVG specification:
|
||||
https://www.w3.org/TR/SVG/coords.html#Units
|
||||
"""
|
||||
self.moveto(x, y)
|
||||
return self
|
||||
|
||||
def find_id(self, element_id):
|
||||
"""Find a single element with the given ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_id : str
|
||||
ID of the element to find
|
||||
|
||||
Returns
|
||||
-------
|
||||
found element
|
||||
"""
|
||||
element = transform.FigureElement.find_id(self, element_id)
|
||||
return Element(element.root)
|
||||
|
||||
def find_ids(self, element_ids):
|
||||
"""Find elements with given IDs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_ids : list of strings
|
||||
list of IDs to find
|
||||
|
||||
Returns
|
||||
-------
|
||||
a new `Panel` object which contains all the found elements.
|
||||
"""
|
||||
elements = [transform.FigureElement.find_id(self, eid) for eid in element_ids]
|
||||
return Panel(*elements)
|
||||
|
||||
|
||||
class SVG(Element):
|
||||
"""SVG from file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fname : str
|
||||
full path to the file
|
||||
fix_mpl : bool
|
||||
replace pt units with px units to fix files created with matplotlib
|
||||
"""
|
||||
|
||||
def __init__(self, fname=None, fix_mpl=False):
|
||||
if fname:
|
||||
fname = os.path.join(CONFIG["svg.file_path"], fname)
|
||||
svg = transform.fromfile(fname)
|
||||
if fix_mpl:
|
||||
w, h = svg.get_size()
|
||||
svg.set_size((w.replace("pt", ""), h.replace("pt", "")))
|
||||
super(SVG, self).__init__(svg.getroot().root)
|
||||
|
||||
# if height/width is in % units, we can't store the absolute values
|
||||
if svg.width.endswith("%"):
|
||||
self._width = None
|
||||
else:
|
||||
self._width = Unit(svg.width).to("px")
|
||||
if svg.height.endswith("%"):
|
||||
self._height = None
|
||||
else:
|
||||
self._height = Unit(svg.height).to("px")
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Get width of the svg file in px"""
|
||||
if self._width:
|
||||
return self._width.value
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""Get height of the svg file in px"""
|
||||
if self._height:
|
||||
return self._height.value
|
||||
|
||||
|
||||
class MplFigure(SVG):
|
||||
"""Matplotlib figure
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fig : matplotlib Figure instance
|
||||
instance of Figure to be converted
|
||||
kws :
|
||||
keyword arguments passed to matplotlib's savefig method
|
||||
"""
|
||||
|
||||
def __init__(self, fig, **kws):
|
||||
svg = transform.from_mpl(fig, savefig_kw=kws)
|
||||
self.root = svg.getroot().root
|
||||
|
||||
|
||||
class Image(Element):
|
||||
"""Raster or vector image
|
||||
|
||||
Parameters
|
||||
----------
|
||||
width : float
|
||||
height : float
|
||||
image dimensions
|
||||
fname : str
|
||||
full path to the file
|
||||
"""
|
||||
|
||||
def __init__(self, width, height, fname):
|
||||
fname = os.path.join(CONFIG["image.file_path"], fname)
|
||||
_, fmt = os.path.splitext(fname)
|
||||
fmt = fmt.lower()[1:]
|
||||
with open(fname, "rb") as fid:
|
||||
img = transform.ImageElement(fid, width, height, fmt)
|
||||
self.root = img.root
|
||||
|
||||
|
||||
class Text(Element):
|
||||
"""Text element.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
content
|
||||
x, y : float or str
|
||||
Text position. If unit is not given it will assume user units (px).
|
||||
size : float, optional
|
||||
Font size.
|
||||
weight : str, optional
|
||||
Font weight. It can be one of: normal, bold, bolder or lighter.
|
||||
font : str, optional
|
||||
Font family.
|
||||
"""
|
||||
|
||||
def __init__(self, text, x=None, y=None, **kwargs):
|
||||
params = {
|
||||
"size": CONFIG["text.size"],
|
||||
"weight": CONFIG["text.weight"],
|
||||
"font": CONFIG["text.font"],
|
||||
}
|
||||
if x is None or y is None:
|
||||
x, y = CONFIG["text.position"]
|
||||
params.update(kwargs)
|
||||
element = transform.TextElement(x, y, text, **params)
|
||||
Element.__init__(self, element.root)
|
||||
|
||||
|
||||
class Panel(Element):
|
||||
"""Figure panel.
|
||||
|
||||
Panel is a group of elements that can be transformed together. Usually
|
||||
it relates to a labeled figure panel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
svgelements : objects deriving from Element class
|
||||
one or more elements that compose the panel
|
||||
|
||||
Notes
|
||||
-----
|
||||
The grouped elements need to be properly arranged in scale and position.
|
||||
"""
|
||||
|
||||
def __init__(self, *svgelements):
|
||||
element = transform.GroupElement(svgelements)
|
||||
Element.__init__(self, element.root)
|
||||
|
||||
def __iter__(self):
|
||||
elements = self.root.getchildren()
|
||||
return (Element(el) for el in elements)
|
||||
|
||||
|
||||
class Line(Element):
|
||||
"""Line element connecting given points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points : sequence of tuples
|
||||
List of point x,y coordinates.
|
||||
width : float, optional
|
||||
Line width.
|
||||
color : str, optional
|
||||
Line color. Any of the HTML/CSS color definitions are allowed.
|
||||
"""
|
||||
|
||||
def __init__(self, points, width=1, color="black"):
|
||||
element = transform.LineElement(points, width=width, color=color)
|
||||
Element.__init__(self, element.root)
|
||||
|
||||
|
||||
class Grid(Element):
|
||||
"""Line grid with coordinate labels to facilitate placement of new
|
||||
elements.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dx : float
|
||||
Spacing between the vertical lines.
|
||||
dy : float
|
||||
Spacing between horizontal lines.
|
||||
size : float or str
|
||||
Font size of the labels.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This element is mainly useful for manual placement of the elements.
|
||||
"""
|
||||
|
||||
def __init__(self, dx, dy, size=8):
|
||||
self.size = size
|
||||
lines = self._gen_grid(dx, dy)
|
||||
element = transform.GroupElement(lines)
|
||||
Element.__init__(self, element.root)
|
||||
|
||||
def _gen_grid(self, dx, dy, width=0.5):
|
||||
xmax, ymax = 1000, 1000
|
||||
x, y = 0, 0
|
||||
lines = []
|
||||
txt = []
|
||||
while x < xmax:
|
||||
lines.append(transform.LineElement([(x, 0), (x, ymax)], width=width))
|
||||
txt.append(transform.TextElement(x, dy / 2, str(x), size=self.size))
|
||||
x += dx
|
||||
while y < ymax:
|
||||
lines.append(transform.LineElement([(0, y), (xmax, y)], width=width))
|
||||
txt.append(transform.TextElement(0, y, str(y), size=self.size))
|
||||
y += dy
|
||||
return lines + txt
|
||||
|
||||
|
||||
class Figure(Panel):
|
||||
"""Main figure class.
|
||||
|
||||
This should be always the top class of all the generated SVG figures.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
width, height : float or str
|
||||
Figure size. If unit is not given, user units (px) are assumed.
|
||||
"""
|
||||
|
||||
def __init__(self, width, height, *svgelements):
|
||||
Panel.__init__(self, *svgelements)
|
||||
self.width = Unit(width)
|
||||
self.height = Unit(height)
|
||||
|
||||
def save(self, fname):
|
||||
"""Save figure to SVG file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fname : str
|
||||
Full path to file.
|
||||
"""
|
||||
element = transform.SVGFigure(self.width, self.height)
|
||||
element.append(self)
|
||||
element.save(os.path.join(CONFIG["figure.save_path"], fname))
|
||||
|
||||
def tostr(self):
|
||||
"""Export SVG as a string"""
|
||||
element = transform.SVGFigure(self.width, self.height)
|
||||
element.append(self)
|
||||
svgstr = element.to_str()
|
||||
return svgstr
|
||||
|
||||
def _repr_svg_(self):
|
||||
return self.tostr().decode("ascii")
|
||||
|
||||
def tile(self, ncols, nrows):
|
||||
"""Automatically tile the panels of the figure.
|
||||
|
||||
This will re-arranged all elements of the figure (first in the
|
||||
hierarchy) so that they will uniformly cover the figure area.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ncols, nrows : type
|
||||
The number of columns and rows to arrange the elements into.
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
ncols * nrows must be larger or equal to number of
|
||||
elements, otherwise some elements will go outside the figure borders.
|
||||
"""
|
||||
dx = (self.width / ncols).to("px").value
|
||||
dy = (self.height / nrows).to("px").value
|
||||
ix, iy = 0, 0
|
||||
for el in self:
|
||||
el.move(dx * ix, dy * iy)
|
||||
ix += 1
|
||||
if ix >= ncols:
|
||||
ix = 0
|
||||
iy += 1
|
||||
if iy > nrows:
|
||||
break
|
||||
return self
|
||||
|
||||
|
||||
class Unit:
|
||||
"""Implementation of SVG units and conversions between them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
measure : str
|
||||
value with unit (for example, '2cm')
|
||||
"""
|
||||
|
||||
per_inch = {"px": 90, "cm": 2.54, "mm": 25.4, "pt": 72.0}
|
||||
|
||||
def __init__(self, measure):
|
||||
try:
|
||||
self.value = float(measure)
|
||||
self.unit = "px"
|
||||
except ValueError:
|
||||
m = re.match(r"([0-9]+\.?[0-9]*)([a-z]+)", measure)
|
||||
value, unit = m.groups()
|
||||
self.value = float(value)
|
||||
self.unit = unit
|
||||
|
||||
def to(self, unit):
|
||||
"""Convert to a given unit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
unit : str
|
||||
Name of the unit to convert to.
|
||||
|
||||
Returns
|
||||
-------
|
||||
u : Unit
|
||||
new Unit object with the requested unit and computed value.
|
||||
"""
|
||||
u = Unit("0cm")
|
||||
u.value = self.value / self.per_inch[self.unit] * self.per_inch[unit]
|
||||
u.unit = unit
|
||||
return u
|
||||
|
||||
def __str__(self):
|
||||
return "{}{}".format(self.value, self.unit)
|
||||
|
||||
def __repr__(self):
|
||||
return "Unit({})".format(str(self))
|
||||
|
||||
def __mul__(self, number):
|
||||
u = Unit("0cm")
|
||||
u.value = self.value * number
|
||||
u.unit = self.unit
|
||||
return u
|
||||
|
||||
def __truediv__(self, number):
|
||||
return self * (1.0 / number)
|
||||
|
||||
def __div__(self, number):
|
||||
return self * (1.0 / number)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# coding=utf-8
|
||||
|
||||
from svgutils.transform import SVGFigure, GroupElement
|
||||
|
||||
|
||||
class BaseTemplate(SVGFigure):
|
||||
def __init__(self):
|
||||
SVGFigure.__init__(self)
|
||||
self.figures = []
|
||||
|
||||
def add_figure(self, fig):
|
||||
w, h = fig.get_size()
|
||||
root = fig.getroot()
|
||||
self.figures.append({"root": root, "width": w, "height": h})
|
||||
|
||||
def _transform(self):
|
||||
pass
|
||||
|
||||
def save(self, fname):
|
||||
self._generate_layout()
|
||||
SVGFigure.save(self, fname)
|
||||
|
||||
def _generate_layout(self):
|
||||
|
||||
for i, f in enumerate(self.figures):
|
||||
new_element = self._transform(f["root"], self.figures[:i])
|
||||
self.append(new_element)
|
||||
|
||||
|
||||
class VerticalLayout(BaseTemplate):
|
||||
def _transform(self, element, transform_list):
|
||||
for t in transform_list:
|
||||
element = GroupElement([element])
|
||||
element.moveto(0, t["height"])
|
||||
return element
|
||||
|
||||
|
||||
class ColumnLayout(BaseTemplate):
|
||||
def __init__(self, nrows, row_height=None, col_width=None):
|
||||
"""Multiple column layout with nrows and required number of
|
||||
columns. col_width
|
||||
determines the width of the column (defaults to width of the
|
||||
first added element)"""
|
||||
|
||||
self.nrows = nrows
|
||||
self.col_width = col_width
|
||||
self.row_height = row_height
|
||||
|
||||
BaseTemplate.__init__(self)
|
||||
|
||||
def _transform(self, element, transform_list):
|
||||
|
||||
rows = 0
|
||||
|
||||
if not transform_list:
|
||||
return element
|
||||
|
||||
n_elements = len(transform_list)
|
||||
rows = n_elements % self.nrows
|
||||
cols = int(n_elements / self.nrows)
|
||||
|
||||
if self.col_width is None:
|
||||
self.col_width = transform_list[0]["width"]
|
||||
if self.row_height is None:
|
||||
self.row_height = transform_list[0]["height"]
|
||||
|
||||
for _ in range(rows):
|
||||
element = GroupElement([element])
|
||||
element.moveto(0, self.row_height)
|
||||
|
||||
for _ in range(cols):
|
||||
element = GroupElement([element])
|
||||
element.moveto(self.col_width, 0)
|
||||
|
||||
return element
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
from lxml import etree
|
||||
from copy import deepcopy
|
||||
import codecs
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
||||
XLINK_NAMESPACE = "http://www.w3.org/1999/xlink"
|
||||
SVG = "{%s}" % SVG_NAMESPACE
|
||||
XLINK = "{%s}" % XLINK_NAMESPACE
|
||||
NSMAP = {None: SVG_NAMESPACE, "xlink": XLINK_NAMESPACE}
|
||||
|
||||
|
||||
class FigureElement(object):
|
||||
"""Base class representing single figure element"""
|
||||
|
||||
def __init__(self, xml_element, defs=None):
|
||||
|
||||
self.root = xml_element
|
||||
|
||||
def moveto(self, x, y, scale_x=1, scale_y=None):
|
||||
"""Move and scale element.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, y : float
|
||||
displacement in x and y coordinates in user units ('px').
|
||||
scale_x : float
|
||||
x-direction scaling factor. To scale down scale_x < 1, scale up scale_x > 1.
|
||||
scale_y : (optional) float
|
||||
y-direction scaling factor. To scale down scale_y < 1, scale up scale_y > 1.
|
||||
If set to default (None), then scale_y=scale_x.
|
||||
"""
|
||||
if scale_y is None:
|
||||
scale_y = scale_x
|
||||
self.root.set(
|
||||
"transform",
|
||||
"translate(%s, %s) scale(%s %s) %s"
|
||||
% (x, y, scale_x, scale_y, self.root.get("transform") or ""),
|
||||
)
|
||||
|
||||
def rotate(self, angle, x=0, y=0):
|
||||
"""Rotate element by given angle around given pivot.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
angle : float
|
||||
rotation angle in degrees
|
||||
x, y : float
|
||||
pivot coordinates in user coordinate system (defaults to top-left
|
||||
corner of the figure)
|
||||
"""
|
||||
self.root.set(
|
||||
"transform",
|
||||
"%s rotate(%f %f %f)" % (self.root.get("transform") or "", angle, x, y),
|
||||
)
|
||||
|
||||
def skew(self, x=0, y=0):
|
||||
"""Skew the element by x and y degrees
|
||||
Convenience function which calls skew_x and skew_y
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x,y : float, float
|
||||
skew angle in degrees (default 0)
|
||||
|
||||
If an x/y angle is given as zero degrees, that transformation is omitted.
|
||||
"""
|
||||
if x != 0:
|
||||
self.skew_x(x)
|
||||
if y != 0:
|
||||
self.skew_y(y)
|
||||
|
||||
return self
|
||||
|
||||
def skew_x(self, x):
|
||||
"""Skew element along the x-axis by the given angle.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : float
|
||||
x-axis skew angle in degrees
|
||||
"""
|
||||
self.root.set(
|
||||
"transform", "%s skewX(%f)" % (self.root.get("transform") or "", x)
|
||||
)
|
||||
return self
|
||||
|
||||
def skew_y(self, y):
|
||||
"""Skew element along the y-axis by the given angle.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y : float
|
||||
y-axis skew angle in degrees
|
||||
"""
|
||||
self.root.set(
|
||||
"transform", "%s skewY(%f)" % (self.root.get("transform") or "", y)
|
||||
)
|
||||
return self
|
||||
|
||||
def scale(self, x=0, y=None):
|
||||
"""Scale element separately across the two axes x and y.
|
||||
If y is not provided, it is assumed equal to x (according to the
|
||||
W3 specification).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : float
|
||||
x-axis scaling factor. To scale down x < 1, scale up x > 1.
|
||||
y : (optional) float
|
||||
y-axis scaling factor. To scale down y < 1, scale up y > 1.
|
||||
|
||||
"""
|
||||
self.moveto(0, 0, x, y)
|
||||
return self
|
||||
|
||||
def __getitem__(self, i):
|
||||
return FigureElement(self.root.getchildren()[i])
|
||||
|
||||
def copy(self):
|
||||
"""Make a copy of the element"""
|
||||
return deepcopy(self.root)
|
||||
|
||||
def tostr(self):
|
||||
"""String representation of the element"""
|
||||
return etree.tostring(self.root, pretty_print=True)
|
||||
|
||||
def find_id(self, element_id):
|
||||
"""Find element by its id.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_id : str
|
||||
ID of the element to find
|
||||
|
||||
Returns
|
||||
-------
|
||||
FigureElement
|
||||
one of the children element with the given ID."""
|
||||
find = etree.XPath("//*[@id=$id]")
|
||||
return FigureElement(find(self.root, id=element_id)[0])
|
||||
|
||||
|
||||
class TextElement(FigureElement):
|
||||
"""Text element.
|
||||
|
||||
Corresponds to SVG ``<text>`` tag."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
size=8,
|
||||
font="Verdana",
|
||||
weight="normal",
|
||||
letterspacing=0,
|
||||
anchor="start",
|
||||
color="black",
|
||||
):
|
||||
txt = etree.Element(
|
||||
SVG + "text",
|
||||
{
|
||||
"x": str(x),
|
||||
"y": str(y),
|
||||
"font-size": str(size),
|
||||
"font-family": font,
|
||||
"font-weight": weight,
|
||||
"letter-spacing": str(letterspacing),
|
||||
"text-anchor": str(anchor),
|
||||
"fill": str(color),
|
||||
},
|
||||
)
|
||||
txt.text = text
|
||||
FigureElement.__init__(self, txt)
|
||||
|
||||
|
||||
class ImageElement(FigureElement):
|
||||
"""Inline image element.
|
||||
|
||||
Correspoonds to SVG ``<image>`` tag. Image data encoded as base64 string.
|
||||
"""
|
||||
|
||||
def __init__(self, stream, width, height, format="png"):
|
||||
base64str = codecs.encode(stream.read(), "base64").rstrip()
|
||||
uri = "data:image/{};base64,{}".format(format, base64str.decode("ascii"))
|
||||
attrs = {"width": str(width), "height": str(height), XLINK + "href": uri}
|
||||
img = etree.Element(SVG + "image", attrs)
|
||||
FigureElement.__init__(self, img)
|
||||
|
||||
|
||||
class LineElement(FigureElement):
|
||||
"""Line element.
|
||||
|
||||
Corresponds to SVG ``<path>`` tag. It handles only piecewise
|
||||
straight segments
|
||||
"""
|
||||
|
||||
def __init__(self, points, width=1, color="black"):
|
||||
linedata = "M{} {} ".format(*points[0])
|
||||
linedata += " ".join(map(lambda x: "L{} {}".format(*x), points[1:]))
|
||||
line = etree.Element(
|
||||
SVG + "path", {"d": linedata, "stroke-width": str(width), "stroke": color}
|
||||
)
|
||||
FigureElement.__init__(self, line)
|
||||
|
||||
|
||||
class GroupElement(FigureElement):
|
||||
"""Group element.
|
||||
|
||||
Container for other elements. Corresponds to SVG ``<g>`` tag.
|
||||
"""
|
||||
|
||||
def __init__(self, element_list, attrib=None):
|
||||
new_group = etree.Element(SVG + "g", attrib=attrib)
|
||||
for e in element_list:
|
||||
if isinstance(e, FigureElement):
|
||||
new_group.append(e.root)
|
||||
else:
|
||||
new_group.append(e)
|
||||
self.root = new_group
|
||||
|
||||
|
||||
class SVGFigure(object):
|
||||
"""SVG Figure.
|
||||
|
||||
It setups standalone SVG tree. It corresponds to SVG ``<svg>`` tag.
|
||||
"""
|
||||
|
||||
def __init__(self, width=None, height=None):
|
||||
self.root = etree.Element(SVG + "svg", nsmap=NSMAP)
|
||||
self.root.set("version", "1.1")
|
||||
|
||||
self._width = 0
|
||||
self._height = 0
|
||||
|
||||
if width:
|
||||
try:
|
||||
self.width = width # this goes to @width.setter a few lines down
|
||||
except AttributeError:
|
||||
# int or str
|
||||
self._width = width
|
||||
|
||||
if height:
|
||||
try:
|
||||
self.height = height # this goes to @height.setter a few lines down
|
||||
except AttributeError:
|
||||
self._height = height
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Figure width"""
|
||||
return self.root.get("width")
|
||||
|
||||
@width.setter
|
||||
def width(self, value):
|
||||
self._width = value.value
|
||||
self.root.set("width", str(value))
|
||||
self.root.set("viewBox", "0 0 %s %s" % (self._width, self._height))
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""Figure height"""
|
||||
return self.root.get("height")
|
||||
|
||||
@height.setter
|
||||
def height(self, value):
|
||||
self._height = value.value
|
||||
self.root.set("height", str(value))
|
||||
self.root.set("viewBox", "0 0 %s %s" % (self._width, self._height))
|
||||
|
||||
def append(self, element):
|
||||
"""Append new element to the SVG figure"""
|
||||
try:
|
||||
self.root.append(element.root)
|
||||
except AttributeError:
|
||||
self.root.append(GroupElement(element).root)
|
||||
|
||||
def getroot(self):
|
||||
"""Return the root element of the figure.
|
||||
|
||||
The root element is a group of elements after stripping the toplevel
|
||||
``<svg>`` tag.
|
||||
|
||||
Returns
|
||||
-------
|
||||
GroupElement
|
||||
All elements of the figure without the ``<svg>`` tag.
|
||||
"""
|
||||
if "class" in self.root.attrib:
|
||||
attrib = {"class": self.root.attrib["class"]}
|
||||
else:
|
||||
attrib = None
|
||||
return GroupElement(self.root.getchildren(), attrib=attrib)
|
||||
|
||||
def to_str(self):
|
||||
"""
|
||||
Returns a string of the SVG figure.
|
||||
"""
|
||||
return etree.tostring(
|
||||
self.root, xml_declaration=True, standalone=True, pretty_print=True
|
||||
)
|
||||
|
||||
def save(self, fname, encoding=None):
|
||||
"""
|
||||
Save figure to a file
|
||||
Default encoding is "ASCII" when None is specified, as dictated by lxml .
|
||||
"""
|
||||
out = etree.tostring(
|
||||
self.root,
|
||||
xml_declaration=True,
|
||||
standalone=True,
|
||||
pretty_print=True,
|
||||
encoding=encoding,
|
||||
)
|
||||
with open(fname, "wb") as fid:
|
||||
fid.write(out)
|
||||
|
||||
def find_id(self, element_id):
|
||||
"""Find elements with the given ID"""
|
||||
find = etree.XPath("//*[@id=$id]")
|
||||
return FigureElement(find(self.root, id=element_id)[0])
|
||||
|
||||
def get_size(self):
|
||||
"""Get figure size"""
|
||||
return self.root.get("width"), self.root.get("height")
|
||||
|
||||
def set_size(self, size):
|
||||
"""Set figure size"""
|
||||
w, h = size
|
||||
self.root.set("width", w)
|
||||
self.root.set("height", h)
|
||||
|
||||
|
||||
def fromfile(fname):
|
||||
"""Open SVG figure from file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fname : str
|
||||
name of the SVG file
|
||||
|
||||
Returns
|
||||
-------
|
||||
SVGFigure
|
||||
newly created :py:class:`SVGFigure` initialised with the file content
|
||||
"""
|
||||
fig = SVGFigure()
|
||||
with open(fname) as fid:
|
||||
svg_file = etree.parse(fid, parser=etree.XMLParser(huge_tree=True))
|
||||
|
||||
fig.root = svg_file.getroot()
|
||||
return fig
|
||||
|
||||
|
||||
def fromstring(text):
|
||||
"""Create a SVG figure from a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
string representing the SVG content. Must be valid SVG.
|
||||
|
||||
Returns
|
||||
-------
|
||||
SVGFigure
|
||||
newly created :py:class:`SVGFigure` initialised with the string
|
||||
content.
|
||||
"""
|
||||
fig = SVGFigure()
|
||||
svg = etree.fromstring(text.encode(), parser=etree.XMLParser(huge_tree=True))
|
||||
|
||||
fig.root = svg
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def from_mpl(fig, savefig_kw=None):
|
||||
"""Create a SVG figure from a ``matplotlib`` figure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fig : matplotlib.Figure instance
|
||||
|
||||
savefig_kw : dict
|
||||
keyword arguments to be passed to matplotlib's
|
||||
`savefig`
|
||||
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
SVGFigure
|
||||
newly created :py:class:`SVGFigure` initialised with the string
|
||||
content.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
If you want to overlay the figure on another SVG, you may want to pass
|
||||
the `transparent` option:
|
||||
|
||||
>>> from svgutils import transform
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> fig = plt.figure()
|
||||
>>> line, = plt.plot([1,2])
|
||||
>>> svgfig = transform.from_mpl(fig,
|
||||
... savefig_kw=dict(transparent=True))
|
||||
>>> svgfig.getroot()
|
||||
<svgutils.transform.GroupElement object at ...>
|
||||
|
||||
|
||||
"""
|
||||
|
||||
fid = StringIO()
|
||||
if savefig_kw is None:
|
||||
savefig_kw = {}
|
||||
|
||||
try:
|
||||
fig.savefig(fid, format="svg", **savefig_kw)
|
||||
except ValueError:
|
||||
raise (ValueError, "No matplotlib SVG backend")
|
||||
fid.seek(0)
|
||||
fig = fromstring(fid.read())
|
||||
|
||||
# workaround mpl units bug
|
||||
w, h = fig.get_size()
|
||||
fig.set_size((w.replace("pt", ""), h.replace("pt", "")))
|
||||
|
||||
return fig
|
||||
|
|
@ -13,6 +13,7 @@ outputs:
|
|||
# drill_marks: small
|
||||
title: Chau
|
||||
# plot_sheet_reference: false
|
||||
format: 'SVG'
|
||||
pages:
|
||||
- # monochrome: true
|
||||
scaling: 2.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue