1949 lines
No EOL
77 KiB
Python
1949 lines
No EOL
77 KiB
Python
#!/usr/bin/env python
|
|
# coding=utf-8
|
|
#
|
|
# Copyright (c) 2023 David Burghoff <burghoff@utexas.edu>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
# pylint: disable=import-error
|
|
"""
|
|
AutoExporter for Inkscape
|
|
|
|
This module provides an extension for Inkscape to automate the export of SVG files
|
|
into multiple formats. It supports watching directories, exporting immediately,
|
|
and offers various configuration options for exporting to PDF, PNG, EMF, EPS,
|
|
and plain SVG. The module handles preprocessing steps to ensure compatibility
|
|
and quality of exported files, such as embedding linked images, fixing markers,
|
|
scaling text, and more.
|
|
|
|
Main Features:
|
|
- Watches specified directories for changes and exports files automatically.
|
|
- Supports exporting to multiple formats with various options.
|
|
- Handles preprocessing of SVG files to fix common issues and improve compatibility.
|
|
- Provides postprocessing to clean up and adjust SVGs for better rendering in
|
|
different environments.
|
|
- Includes utility functions for handling subprocesses, file manipulations,
|
|
and SVG operations.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import re
|
|
import copy
|
|
import subprocess
|
|
import warnings
|
|
import pickle
|
|
import math
|
|
import shutil
|
|
import tempfile
|
|
import hashlib
|
|
import lxml
|
|
import random
|
|
import threading
|
|
|
|
import dhelpers as dh
|
|
import inkex
|
|
from inkex import TextElement, Transform, Vector2d
|
|
from inkex.text.utils import default_style_atts, unique
|
|
from inkex.text.cache import BaseElementCache
|
|
from inkex.text.parser import ParsedText, xyset
|
|
|
|
import numpy as np
|
|
import image_helpers as ih
|
|
|
|
otp_support_tags = BaseElementCache.otp_support_tags
|
|
peltag = inkex.PathElement.ctag
|
|
|
|
USE_TERMINAL = False
|
|
DEBUGGING = False
|
|
DISPPROFILE = False
|
|
|
|
|
|
MAXATTEMPTS = 2
|
|
MAX_THREADS = 10;
|
|
sema1 = threading.Semaphore(MAX_THREADS)
|
|
sema2 = threading.Semaphore(MAX_THREADS)
|
|
|
|
class AutoExporter(inkex.EffectExtension):
|
|
"""Automates exporting of SVG files in multiple formats."""
|
|
|
|
def add_arguments(self, pars): # pylint: disable=no-self-use
|
|
"""Add arguments to the parser."""
|
|
pars.add_argument("--tab", help="The selected UI-tab when OK was pressed")
|
|
pars.add_argument("--watchdir", help="Watch directory")
|
|
pars.add_argument("--writedir", help="Write directory")
|
|
pars.add_argument(
|
|
"--usepdf", type=inkex.Boolean, default=False, help="Export PDF?"
|
|
)
|
|
pars.add_argument(
|
|
"--usepng", type=inkex.Boolean, default=False, help="Export PNG?"
|
|
)
|
|
pars.add_argument(
|
|
"--useemf", type=inkex.Boolean, default=False, help="Export EMF?"
|
|
)
|
|
pars.add_argument(
|
|
"--useeps", type=inkex.Boolean, default=False, help="Export EPS?"
|
|
)
|
|
pars.add_argument(
|
|
"--usepsvg", type=inkex.Boolean, default=False, help="Export plain SVG?"
|
|
)
|
|
pars.add_argument("--dpi", default=600, help="Rasterization DPI")
|
|
# pars.add_argument("--dpi_im", default=300, help="Resampling DPI")
|
|
pars.add_argument(
|
|
"--imagemode2",
|
|
type=inkex.Boolean,
|
|
default=True,
|
|
help="Embedded image handling",
|
|
)
|
|
pars.add_argument(
|
|
"--thinline",
|
|
type=inkex.Boolean,
|
|
default=True,
|
|
help="Prevent thin line enhancement",
|
|
)
|
|
pars.add_argument(
|
|
"--texttopath", type=inkex.Boolean, default=False, help="Text to paths?"
|
|
)
|
|
pars.add_argument(
|
|
"--backingrect",
|
|
type=inkex.Boolean,
|
|
default=True,
|
|
help="Add backing rectangle?",
|
|
)
|
|
pars.add_argument(
|
|
"--stroketopath", type=inkex.Boolean, default=False, help="Stroke to paths?"
|
|
)
|
|
pars.add_argument(
|
|
"--latexpdf", type=inkex.Boolean, default=False, help="Make LaTeX PDF?"
|
|
)
|
|
pars.add_argument(
|
|
"--testmode", type=inkex.Boolean, default=False, help="Test mode?"
|
|
)
|
|
pars.add_argument("--testpage", type=int, default=1, help="Test mode page")
|
|
pars.add_argument("--v", type=str, default="1.2", help="Version for debugging")
|
|
pars.add_argument(
|
|
"--rasterizermode", type=int, default=1, help="Mark for rasterization"
|
|
)
|
|
pars.add_argument(
|
|
"--margin", type=float, default=0.5, help="Document margin (mm)"
|
|
)
|
|
pars.add_argument(
|
|
"--exportwhat", type=int, default=1, help="Export what?"
|
|
)
|
|
|
|
def effect(self):
|
|
"""Start the Autoexporter or initiate export now."""
|
|
if self.options.tab == "rasterizer":
|
|
sel = [self.svg.selection[i] for i in range(len(self.svg.selection))]
|
|
for elem in sel:
|
|
if self.options.rasterizermode == 1:
|
|
elem.set("autoexporter_rasterize", "png")
|
|
elif self.options.rasterizermode == 2:
|
|
elem.set("autoexporter_rasterize", "jpg")
|
|
elif self.options.rasterizermode == 3:
|
|
elem.set("autoexporter_rasterize", "topath")
|
|
else:
|
|
elem.set("autoexporter_rasterize", None)
|
|
return
|
|
self.options.exportnow = self.options.exportwhat==3
|
|
self.options.watchhere = self.options.exportwhat==2
|
|
|
|
# self.options.testmode = True;
|
|
if self.options.testmode:
|
|
self.options.usepsvg = True
|
|
self.options.thinline = True
|
|
self.options.imagemode2 = True
|
|
self.options.texttopath = True
|
|
self.options.stroketopath = True
|
|
self.options.exportnow = True
|
|
self.options.margin = 0.5
|
|
self.options.latexpdf = False
|
|
|
|
if DISPPROFILE:
|
|
# pylint: disable=import-outside-toplevel
|
|
import cProfile
|
|
import pstats
|
|
import io
|
|
from pstats import SortKey
|
|
# pylint: enable=import-outside-toplevel
|
|
|
|
prf = cProfile.Profile()
|
|
prf.enable()
|
|
|
|
formats = [
|
|
self.options.usepdf,
|
|
self.options.usepng,
|
|
self.options.useemf,
|
|
self.options.useeps,
|
|
self.options.usepsvg,
|
|
]
|
|
formats = [
|
|
["pdf", "png", "emf", "eps", "psvg"][i]
|
|
for i in range(len(formats))
|
|
if formats[i]
|
|
]
|
|
|
|
# Make an options copy we can pass to the external program
|
|
optcopy = copy.copy(self.options)
|
|
delattr(optcopy, "output")
|
|
delattr(optcopy, "input_file")
|
|
optcopy.reduce_images = self.options.imagemode2
|
|
|
|
bfn = inkex.inkscape_system_info.binary_location
|
|
pyloc, pybin = os.path.split(sys.executable)
|
|
|
|
if not (self.options.exportnow):
|
|
aepy = os.path.abspath(
|
|
os.path.join(dh.get_script_path(), "autoexporter_script.py")
|
|
)
|
|
|
|
if self.options.watchhere:
|
|
pth = dh.Get_Current_File(self, "To watch this document's location, ")
|
|
optcopy.watchdir = os.path.dirname(pth)
|
|
optcopy.writedir = os.path.dirname(pth)
|
|
|
|
if not os.path.exists(optcopy.watchdir):
|
|
dh.idebug(
|
|
"Watch directory could not be found. Please be sure the "
|
|
"selected location is valid.\n"
|
|
)
|
|
dh.idebug(
|
|
"Note: Linux AppImage and Snap installations of Inkscape "
|
|
"cannot see locations outside the installation directory."
|
|
)
|
|
sys.exit()
|
|
|
|
# Pass settings using a config file. Include the current path so
|
|
# Inkex can be called if needed.
|
|
optcopy.inkscape_bfn = bfn
|
|
optcopy.formats = formats
|
|
optcopy.syspath = sys.path
|
|
|
|
try:
|
|
with warnings.catch_warnings():
|
|
# Ignore ImportWarning for Gtk/Pango
|
|
warnings.simplefilter("ignore")
|
|
# Prevent Gtk-Message: Failed to load module "xapp-gtk3-module"
|
|
os.environ["GTK_MODULES"] = ""
|
|
# pylint: disable=import-outside-toplevel, unused-import
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk # noqa
|
|
# pylint: enable=import-outside-toplevel, unused-import
|
|
guitype = "gtk"
|
|
except ImportError:
|
|
guitype = "terminal"
|
|
if USE_TERMINAL:
|
|
guitype = "terminal"
|
|
optcopy.guitype = guitype
|
|
|
|
aes = os.path.join(
|
|
os.path.abspath(tempfile.gettempdir()), "si_ae_settings.p"
|
|
)
|
|
with open(aes, "wb") as file:
|
|
pickle.dump(optcopy, file)
|
|
warnings.simplefilter("ignore", ResourceWarning)
|
|
# prevent warning that process is open
|
|
|
|
if guitype == "gtk":
|
|
AutoExporter._gtk_call(pybin, aepy)
|
|
else:
|
|
AutoExporter._terminal_call(pybin, aepy, pyloc)
|
|
|
|
else:
|
|
if not (self.options.testmode):
|
|
pth = dh.Get_Current_File(self, "To do a direct export, ")
|
|
else:
|
|
pth = self.options.input_file
|
|
optcopy.original_file = pth
|
|
optcopy.debug = DEBUGGING
|
|
optcopy.prints = False
|
|
optcopy.linked_locations = ih.get_linked_locations(self)
|
|
# needed to find linked images in relative directories
|
|
optcopy.formats = formats
|
|
optcopy.outtemplate = pth
|
|
optcopy.bfn = bfn
|
|
|
|
Exporter(self.options.input_file, optcopy).export_all()
|
|
|
|
if DISPPROFILE:
|
|
prf.disable()
|
|
sio = io.StringIO()
|
|
sortby = SortKey.CUMULATIVE
|
|
pst = pstats.Stats(prf, stream=sio).sort_stats(sortby)
|
|
pst.print_stats()
|
|
dh.debug(sio.getvalue())
|
|
|
|
if self.options.testmode:
|
|
nfn = os.path.abspath(pth[0:-4] + "_plain.svg")
|
|
stream = self.options.output
|
|
|
|
if isinstance(stream, str):
|
|
# Copy the new file
|
|
shutil.copyfile(nfn, self.options.output)
|
|
else:
|
|
# Write to the output stream
|
|
svg2 = get_svg(nfn)
|
|
newdoc = lxml.etree.tostring(svg2, pretty_print=True)
|
|
try:
|
|
stream.write(newdoc)
|
|
except TypeError:
|
|
# we hope that this happens only when document needs to be encoded
|
|
stream.write(newdoc.encode("utf-8")) # type: ignore
|
|
self.options.output = None
|
|
|
|
os.remove(nfn)
|
|
|
|
# Runs a Python script using a Python binary in a working directory
|
|
# It detaches from Inkscape, allowing it to continue running after the
|
|
# extension has finished
|
|
@staticmethod
|
|
def _gtk_call(python_bin, python_script):
|
|
"""Run a Python script using GTK terminal."""
|
|
devnull_location = dh.shared_temp(filename="si_ae_output.txt")
|
|
with open(devnull_location, "w") as devnull:
|
|
subprocess.Popen(
|
|
[python_bin, python_script], stdout=devnull, stderr=devnull
|
|
)
|
|
|
|
@staticmethod
|
|
def _terminal_call(python_bin, python_script, python_wd):
|
|
"""Run a Python script using a terminal."""
|
|
|
|
def escp(x):
|
|
return x.replace(" ", "\\\\ ")
|
|
|
|
if sys.platform == "darwin":
|
|
# https://stackoverflow.com/questions/39840632/launch-python-script-in-new-terminal
|
|
os.system(
|
|
'osascript -e \'tell application "Terminal" to do script "'
|
|
+ escp(sys.executable)
|
|
+ " "
|
|
+ escp(python_script)
|
|
+ "\"' >/dev/null"
|
|
)
|
|
elif sys.platform == "win32":
|
|
if "pythonw.exe" in python_bin:
|
|
python_bin = python_bin.replace("pythonw.exe", "python.exe")
|
|
subprocess.Popen([python_bin, python_script], shell=False, cwd=python_wd)
|
|
|
|
# if 'pythonw.exe' in python_bin:
|
|
# python_bin = python_bin.replace('pythonw.exe', 'python.exe')
|
|
# DETACHED_PROCESS = 0x08000000
|
|
# subprocess.Popen([python_bin, python_script, 'standalone'],
|
|
# creationflags=DETACHED_PROCESS)
|
|
else:
|
|
if sys.executable[0:4] == "/tmp":
|
|
inkex.utils.errormsg(
|
|
"This appears to be an AppImage of Inkscape, which the "
|
|
"Autoexporter cannot support since AppImages are sandboxed."
|
|
)
|
|
return
|
|
if sys.executable[0:5] == "/snap":
|
|
inkex.utils.errormsg(
|
|
"This appears to be an Snap installation of Inkscape, which "
|
|
"the Autoexporter cannot support since Snap installations "
|
|
"are sandboxed."
|
|
)
|
|
return
|
|
terminals = [
|
|
"x-terminal-emulator",
|
|
"mate-terminal",
|
|
"gnome-terminal",
|
|
"terminator",
|
|
"xfce4-terminal",
|
|
"urxvt",
|
|
"rxvt",
|
|
"termit",
|
|
"Eterm",
|
|
"aterm",
|
|
"uxterm",
|
|
"xterm",
|
|
"roxterm",
|
|
"termite",
|
|
"lxterminal",
|
|
"terminology",
|
|
"st",
|
|
"qterminal",
|
|
"lilyterm",
|
|
"tilix",
|
|
"terminix",
|
|
"konsole",
|
|
"kitty",
|
|
"guake",
|
|
"tilda",
|
|
"alacritty",
|
|
"hyper",
|
|
"terminal",
|
|
"iTerm",
|
|
"mintty",
|
|
"xiterm",
|
|
"terminal.app",
|
|
"Terminal.app",
|
|
"terminal-w",
|
|
"terminal.js",
|
|
"Terminal.js",
|
|
"conemu",
|
|
"cmder",
|
|
"powercmd",
|
|
"terminus",
|
|
"termina",
|
|
"terminal-plus",
|
|
"iterm2",
|
|
"terminus-terminal",
|
|
"terminal-tabs",
|
|
]
|
|
terms = []
|
|
for terminal in terminals:
|
|
result = subprocess.run(
|
|
["which", terminal],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if result.returncode == 0:
|
|
terms.append(terminal)
|
|
|
|
for t in reversed(terms):
|
|
if t == "x-terminal-emulator":
|
|
linux_terminal_call = "x-terminal-emulator -e bash -c '%CMD'"
|
|
elif t == "gnome-terminal":
|
|
linux_terminal_call = 'gnome-terminal -- bash -c "%CMD; exec bash"'
|
|
elif t == "konsole":
|
|
linux_terminal_call = "konsole -e bash -c '%CMD'"
|
|
os.system(
|
|
linux_terminal_call.replace(
|
|
"%CMD",
|
|
escp(sys.executable) + " " + escp(python_script) + " >/dev/null",
|
|
)
|
|
)
|
|
|
|
class Exporter():
|
|
def __init__(self,fin,opts):
|
|
self.filein = fin
|
|
self.__dict__.update(vars(opts))
|
|
|
|
sema_temp = threading.Semaphore(1)
|
|
def export_all(self):
|
|
"""Export all files in specified formats."""
|
|
# Make a temp directory
|
|
self.tempdir, self.temphead = dh.shared_temp('ae')
|
|
self.tempbase = joinmod(self.tempdir,self.temphead)
|
|
|
|
|
|
if self.debug:
|
|
if self.prints:
|
|
self.prints("\n " + joinmod(self.tempdir, ""))
|
|
|
|
# Make sure output directory exists
|
|
outdir = os.path.dirname(self.outtemplate)
|
|
if not os.path.exists(outdir):
|
|
os.makedirs(outdir)
|
|
|
|
# Add a document margin
|
|
cfile = self.filein # current file we're working on
|
|
if self.margin != 0:
|
|
svg = get_svg(cfile)
|
|
tmp = self.tempbase + "_marg.svg"
|
|
Exporter.add_margin(svg, self.margin, self.testmode)
|
|
dh.overwrite_svg(svg, tmp)
|
|
cfile = copy.copy(tmp)
|
|
|
|
# Do png before any preprocessing
|
|
if "png" in self.formats:
|
|
finished, myo = self.export_file(cfile, "png")
|
|
|
|
# Do preprocessing
|
|
if any(fmt in self.formats for fmt in ["pdf", "emf", "eps", "psvg"]):
|
|
cfile = self.preprocessing(cfile)
|
|
|
|
# Do vector outputs
|
|
newfiles = []
|
|
for fmt in self.formats:
|
|
if fmt != "png":
|
|
finished, myo = self.export_file(cfile, fmt)
|
|
if finished:
|
|
newfiles.append(myo)
|
|
|
|
# Remove temporary outputs and directory
|
|
failed_to_delete = None
|
|
if not (self.debug):
|
|
self.clear_temp()
|
|
else:
|
|
warnings.simplefilter("ignore", ResourceWarning)
|
|
# prevent warning that process is open
|
|
subprocess.Popen(f'explorer "{os.path.realpath(self.tempdir)}"')
|
|
return failed_to_delete
|
|
|
|
def clear_temp(self):
|
|
""" Clear any temporary files """
|
|
if not (self.debug):
|
|
tmps = []
|
|
for t in os.listdir(self.tempdir):
|
|
tmp = joinmod(self.tempdir, t)
|
|
try:
|
|
one_day_ago = time.time() - 24 * 60 * 60
|
|
if os.path.getmtime(tmp) < one_day_ago:
|
|
tmps.append(joinmod(self.tempdir, t))
|
|
except FileNotFoundError: # already deleted
|
|
pass
|
|
if tmp.startswith(self.tempbase):
|
|
tmps.append(joinmod(self.tempdir, t))
|
|
for tmp in tmps:
|
|
if os.path.exists(tmp):
|
|
deleted = False
|
|
nattempts = 0
|
|
while not (deleted) and nattempts < MAXATTEMPTS:
|
|
try:
|
|
os.remove(tmp)
|
|
deleted = True
|
|
except PermissionError:
|
|
time.sleep(1)
|
|
nattempts += 1
|
|
|
|
def preprocessing(self, fin):
|
|
"""Modifications that are done prior to conversion to any vector output"""
|
|
if self.prints:
|
|
fname = os.path.split(self.filein)[1]
|
|
try:
|
|
offset = round(os.get_terminal_size().columns / 2)
|
|
except OSError:
|
|
offset = 40
|
|
fname = fname + " " * max(0, offset - len(fname))
|
|
self.prints(fname + ": Preprocessing vector output", flush=True)
|
|
timestart = time.time()
|
|
|
|
tempdir = self.tempdir
|
|
temphead = self.temphead
|
|
|
|
# SVG modifications that should be done prior to any binary calls
|
|
cfile = fin
|
|
svg = get_svg(cfile)
|
|
|
|
# Prune hidden items and remove language switching
|
|
stag = inkex.addNS("switch", "svg")
|
|
todelete, todelang, switches = [], [], []
|
|
for elem in dh.visible_descendants(svg):
|
|
if elem.cspecified_style.get("display") == "none":
|
|
todelete.append(elem)
|
|
if elem.get("systemLanguage") is not None:
|
|
lang = inkex.inkscape_system_info.language
|
|
if elem.get("systemLanguage") == lang:
|
|
todelang.append(elem)
|
|
# Remove other languages from switches
|
|
if elem.getparent().tag == stag:
|
|
todelete.extend(
|
|
[
|
|
k
|
|
for k in elem.getparent()
|
|
if k.get("systemLanguage") != lang
|
|
]
|
|
)
|
|
else:
|
|
# Remove non-matching languages
|
|
todelete.append(elem)
|
|
if elem.tag == stag:
|
|
switches.append(elem)
|
|
for elem in unique(todelete):
|
|
elem.delete()
|
|
for elem in todelang:
|
|
elem.set("systemLanguage", None)
|
|
for elem in switches:
|
|
if len(elem)==1:
|
|
dh.ungroup(elem)
|
|
|
|
# Embed linked images into the SVG. This should be done prior to clone unlinking
|
|
# since some images may be cloned
|
|
if self.exportnow:
|
|
lls = self.linked_locations
|
|
else:
|
|
lls = ih.get_linked_locations_file(cfile, svg)
|
|
for k in lls:
|
|
elem = svg.getElementById(k)
|
|
ih.embed_external_image(elem, lls[k])
|
|
|
|
vds = dh.visible_descendants(svg)
|
|
raster_ids, image_ids, jpgs = [], [], []
|
|
for elem in vds:
|
|
# Unlink any clones for the PDF image and marker fixes
|
|
if isinstance(elem, (inkex.Use)) and not (isinstance(elem, (inkex.Symbol))):
|
|
newel = dh.unlink2(elem)
|
|
myi = vds.index(elem)
|
|
vds[myi] = newel
|
|
for ddv in newel.descendants2()[1:]:
|
|
vds.append(ddv)
|
|
|
|
# Remove trivial groups inside masks/transparent objects or clipped groups
|
|
if isinstance(elem, (inkex.Group)):
|
|
ancs = elem.ancestors2(includeme=True)
|
|
masked = any(
|
|
anc.get_link("mask", svg) is not None
|
|
or (
|
|
elem.ccascaded_style.get("opacity") is not None
|
|
and float(elem.ccascaded_style.get("opacity")) < 1
|
|
)
|
|
for anc in ancs
|
|
)
|
|
clipped = any(
|
|
anc.get_link("clip-path", svg) is not None for anc in ancs
|
|
)
|
|
if (
|
|
len(list(elem)) == 1
|
|
and (masked or clipped)
|
|
and elem.get("autoexporter_rasterize") not in ["png", "jpg", "True"]
|
|
):
|
|
dh.ungroup(elem)
|
|
|
|
# Remove groups inside clips
|
|
cpth = elem.get_link("clip-path", svg)
|
|
if cpth is not None:
|
|
while any(isinstance(v, inkex.Group) for v in list(cpth)):
|
|
for grp in list(cpth):
|
|
if isinstance(grp, inkex.Group):
|
|
dh.ungroup(grp)
|
|
|
|
stps = []
|
|
ttps = []
|
|
for elem in vds:
|
|
elid = elem.get_id()
|
|
|
|
# Fix marker export bug for PDFs
|
|
mkrs = Exporter.marker_fix(elem)
|
|
# Convert markers to path for Office
|
|
if len(mkrs) > 0 and self.usepsvg:
|
|
stps.append(elid)
|
|
|
|
# Fix opacity bug for Office PDF saving
|
|
if len(mkrs) == 0: # works poorly with markers
|
|
Exporter.opacity_fix(elem)
|
|
|
|
# Disable connectors
|
|
elem.set("inkscape:connector-type", None)
|
|
|
|
# Find out which objects need to be rasterized
|
|
sty = elem.cstyle
|
|
# only want the object with the filter
|
|
if sty.get_link("filter", svg) is not None:
|
|
raster_ids.append(elid) # filtered objects (always rasterized at PDF)
|
|
if (sty.get("fill") is not None and "url" in sty.get("fill")) or (
|
|
sty.get("stroke") is not None and "url" in sty.get("stroke")
|
|
):
|
|
raster_ids.append(elid) # gradient objects
|
|
if elem.get_link("mask") is not None:
|
|
raster_ids.append(elid) # masked objects (always rasterized at PDF)
|
|
if elem.get("autoexporter_rasterize") in ["png", "jpg", "True"]:
|
|
raster_ids.append(elid)
|
|
# rasterizer-marked
|
|
if elem.get("autoexporter_rasterize") == "jpg":
|
|
jpgs.append(elid)
|
|
if elem.get("autoexporter_rasterize")=='topath':
|
|
ttps.append(elid)
|
|
if isinstance(elem, (inkex.Image)):
|
|
image_ids.append(elid)
|
|
# Inkscape inappropriately clips non-'optimizeQuality' images
|
|
# when generating PDFs and calculating bounding boxes
|
|
elem.cstyle["image-rendering"] = "optimizeQuality"
|
|
# Set image x,y,width,height to standard values to prevent Office
|
|
# from mishandling clips
|
|
Exporter.standardize_image(elem)
|
|
|
|
# Remove style attributes with invalid URLs
|
|
# (Office doesn't display the element while Inkscape ignores it)
|
|
for satt in list(sty.keys()):
|
|
if (
|
|
satt in dh.urlatts
|
|
and sty.get(satt).startswith("url")
|
|
and sty.get_link(satt, svg) is None
|
|
):
|
|
elem.cstyle[satt] = None
|
|
|
|
# Prune single-point paths, which Inkscape doesn't show
|
|
if elem.tag in otp_support_tags:
|
|
pth = elem.cpath
|
|
firstpt = None
|
|
trivial = True
|
|
for pnt in pth.end_points:
|
|
if firstpt is None:
|
|
firstpt = (pnt.x, pnt.y)
|
|
elif (pnt.x, pnt.y) != firstpt:
|
|
trivial = False
|
|
break
|
|
if trivial:
|
|
elem.delete(deleteup=True)
|
|
|
|
# Fix Avenir/Whitney
|
|
tetag = inkex.TextElement.ctag
|
|
frtag = inkex.FlowRoot.ctag
|
|
tels = [elem for elem in vds if elem.tag in {tetag, frtag}]
|
|
|
|
dh.character_fixer(tels)
|
|
|
|
# Strip all sodipodi:role lines from document
|
|
# Conversion to plain SVG does this automatically but poorly
|
|
excludetxtids = []
|
|
self.duplicatelabels = dict()
|
|
# if self.usepsvg:
|
|
if len(tels) > 0:
|
|
svg.make_char_table()
|
|
self.ctable = svg.char_table
|
|
# store for later
|
|
|
|
nels = []
|
|
for elem in reversed(tels):
|
|
if elem.parsed_text.isflow:
|
|
nels += elem.parsed_text.flow_to_text()
|
|
tels.remove(elem)
|
|
tels += nels
|
|
|
|
for elem in tels:
|
|
elem.parsed_text.strip_text_baseline_shift()
|
|
elem.parsed_text.strip_sodipodi_role_line()
|
|
elem.parsed_text.fuse_fonts()
|
|
Exporter.subsuper_fix(elem)
|
|
|
|
# Preserve duplicate of text to be converted to paths
|
|
if self.texttopath and elem.get("display") != "none" and elem.croot is not None:
|
|
dup = elem.duplicate()
|
|
excludetxtids.append(dup.get_id())
|
|
grp = dh.group([dup])
|
|
grp.set("display", "none")
|
|
|
|
# tel = svg.new_element(inkex.TextElement, svg)
|
|
tel = inkex.TextElement()
|
|
grp.append(tel) # Remember the original ID
|
|
tel.text = "{0}: {1}".format(DUP_KEY, elem.get_id())
|
|
tel.set("display", "none")
|
|
excludetxtids.append(tel.get_id())
|
|
self.duplicatelabels[tel.get_id()] = elem.get_id()
|
|
|
|
# Make sure nested tspans have fill specified (STP bug)
|
|
for dsd in elem.descendants2()[1:]:
|
|
if (
|
|
"fill" in dsd.cspecified_style
|
|
and dsd.cspecified_style["fill"] != "#000000"
|
|
):
|
|
dsd.cstyle["fill"] = dsd.cspecified_style["fill"]
|
|
|
|
tmp = self.tempbase + "_mod.svg"
|
|
dh.overwrite_svg(svg, tmp)
|
|
cfile = tmp
|
|
self.excludetxtids = excludetxtids
|
|
|
|
do_rasterizations = len(raster_ids + image_ids) > 0
|
|
do_stroketopaths = self.texttopath or self.stroketopath or len(stps) > 0 or len(ttps)>0
|
|
|
|
dpi = self.dpi
|
|
class Act():
|
|
'''
|
|
Represents a single binary call Action.
|
|
type == 'stp' creates an action that stroke-to-path's a group of objects
|
|
type == 'imgt' exports a PNG copy of an image with a transparent background
|
|
type == 'imgo' exports a PNG copy of an image with an opaque background, and objects above hidden
|
|
'''
|
|
def __init__(self,typ,els,fname=None,overlaps=None):
|
|
self.type = typ
|
|
if isinstance(els,list):
|
|
self.els = els
|
|
else:
|
|
self.els = [els]
|
|
if fname is None:
|
|
elid = self.els[0].get_id()
|
|
if typ=='imgt':
|
|
fname = temphead + "_im_" + elid + "." + imgtype
|
|
else:
|
|
fname = temphead + "_imbg_" + elid + "." + imgtype
|
|
self.fname = fname;
|
|
self.overlaps = overlaps
|
|
|
|
if (
|
|
(self.stroketopath or len(stps) > 0)
|
|
and inkex.installed_ivp[0] >= 1
|
|
and inkex.installed_ivp[1] > 0
|
|
):
|
|
stpact = "object-stroke-to-path"
|
|
else:
|
|
stpact = "object-to-path"
|
|
|
|
def __str__(self):
|
|
if self.type == 'stp':
|
|
return "select:{0}; {1}; export-filename:{2}; export-do; unselect:{0}; ".format(
|
|
",".join(self.els), Act.stpact, self.fname
|
|
)
|
|
elif self.type == 'imgt':
|
|
fmt1 = (
|
|
"export-id:{0}; export-id-only; export-dpi:{1}; "
|
|
"export-filename:{2}; export-background-opacity:0.0; "
|
|
"export-do; "
|
|
)
|
|
return fmt1.format(self.els[0].get_id(), int(dpi), self.fname)
|
|
elif self.type == 'imgo':
|
|
el = self.els[0]
|
|
fmt2 = (
|
|
"export-id:{0}; export-dpi:{1}; "
|
|
"export-filename:{2}; export-background-opacity:1.0; "
|
|
"export-do; "
|
|
)
|
|
actv = fmt2.format(el.get_id(), int(dpi), self.fname)
|
|
|
|
# For export all, hide objects on top
|
|
displays = {el: el.cstyle.get('display') for el in overlaps[el]}
|
|
hides = ['select:{0}; object-set-property:display,none; unselect:{0}; '.format(el.get_id()) for el in overlaps[el]]
|
|
unhides = ['select:{0}; object-set-property:display,{1}; unselect:{0}; '.format(el.get_id(), \
|
|
displays[el] if displays[el] is not None else '') for el in overlaps[el]]
|
|
return ''.join(hides) + actv + ''.join(unhides)
|
|
|
|
def split(self,intermediate_fn):
|
|
''' Splits a STP act into two sub-acts '''
|
|
spl = math.ceil(len(self.els) / 2)
|
|
act1 = Act('stp',self.els[:spl],intermediate_fn)
|
|
act2 = Act('stp',self.els[spl:],self.fname)
|
|
return act1, act2
|
|
|
|
|
|
allacts = []
|
|
if do_stroketopaths:
|
|
svg = get_svg(cfile)
|
|
vdd = dh.visible_descendants(svg)
|
|
|
|
updatefile = False
|
|
tels = []
|
|
if self.texttopath or len(ttps)>0:
|
|
for elem in vdd:
|
|
if (elem.get_id() in ttps or self.texttopath) and (elem.tag == tetag and elem.get_id() not in excludetxtids):
|
|
tels.append(elem.get_id())
|
|
for dsd in elem.descendants2():
|
|
# Fuse fill and stroke to account for STP bugs
|
|
if "fill" in dsd.cspecified_style:
|
|
dsd.cstyle["fill"] = dsd.cspecified_style["fill"]
|
|
updatefile = True
|
|
if "stroke" in dsd.cspecified_style:
|
|
dsd.cstyle["stroke"] = dsd.cspecified_style["stroke"]
|
|
updatefile = True
|
|
|
|
pels, dgroups = [], []
|
|
if self.stroketopath or len(stps) > 0:
|
|
# Stroke to Path has a number of bugs, try to fix them
|
|
if self.stroketopath:
|
|
stpels = vdd
|
|
elif len(stps) > 0:
|
|
stpels = [el for el in vdd if el.get_id() in stps]
|
|
stpels = [el for el in stpels if el.get_id() not in raster_ids]
|
|
pels, dgroups = Exporter.stroke_to_path_fixes(stpels)
|
|
updatefile = True
|
|
|
|
if updatefile:
|
|
dh.overwrite_svg(svg, cfile)
|
|
|
|
tmpstp = self.tempbase + "_stp.svg"
|
|
allacts += [Act('stp',tels + pels,tmpstp)]
|
|
|
|
|
|
# Rasterizations
|
|
actts, actos = [], []
|
|
if do_rasterizations:
|
|
svg = get_svg(cfile)
|
|
|
|
vds = dh.visible_descendants(svg)
|
|
els = [el for el in vds if el.get_id() in list(set(raster_ids + image_ids))]
|
|
if len(els) > 0:
|
|
imgtype = "png"
|
|
|
|
overlaps = dh.overlapping_els(svg,els)
|
|
for elem in els:
|
|
elid = elem.get_id()
|
|
actts.append(Act('imgt',elem))
|
|
actos.append(Act('imgo',elem,overlaps=overlaps))
|
|
|
|
allacts += (actos + actts) if ih.hasPIL else actts
|
|
# export-id-onlys need to go last
|
|
|
|
imgs_trnp = {act.fname:act.els[0].get_id() for act in actts}
|
|
imgs_opqe = {act.fname:act.els[0].get_id() for act in actos}
|
|
|
|
# To reduce the number of binary calls, we collect the stroke-to-path and
|
|
# rasterization actions into a single call that also gets the Bounding Boxes.
|
|
# Skip binary call during testing
|
|
if len(allacts) > 0 and not self.testmode:
|
|
# use relative paths to reduce arg length
|
|
|
|
bbs = self.split_acts(fnm=cfile, acts=allacts)
|
|
|
|
imgs = imgs_opqe | imgs_trnp
|
|
missing_images = [t for t in imgs if not os.path.exists(os.path.join(tempdir, t)) and imgs[t] in bbs]
|
|
if missing_images:
|
|
raise TimeoutError(
|
|
"\nThe Inkscape binary could not generate the temporary images "
|
|
+ ", ".join(missing_images) + ' in ' + tempdir + '.\n\n'
|
|
+ "This may be a temporary issue; try running the extension again."
|
|
)
|
|
if do_stroketopaths:
|
|
cfile = tmpstp
|
|
|
|
if do_rasterizations:
|
|
svg = get_svg(cfile)
|
|
vds = dh.visible_descendants(svg)
|
|
els = [el for el in vds if el.get_id() in list(set(raster_ids + image_ids))]
|
|
if len(els) > 0:
|
|
jimgs_trnp = [os.path.join(tempdir, t) for t in imgs_trnp]
|
|
jimgs_opqe = [os.path.join(tempdir, t) for t in imgs_opqe]
|
|
|
|
for i, elem in enumerate(els):
|
|
img_trnp = jimgs_trnp[i]
|
|
img_opqe = jimgs_opqe[i]
|
|
|
|
if os.path.exists(img_trnp):
|
|
anyalpha0 = False
|
|
if ih.hasPIL:
|
|
bbox = ih.crop_images([img_trnp, img_opqe])
|
|
anyalpha0 = ih.Set_Alpha0_RGB(img_trnp, img_opqe)
|
|
if elem.get_id() in jpgs:
|
|
tmpjpg = img_trnp.replace(".png", ".jpg")
|
|
ih.to_jpeg(img_opqe, tmpjpg)
|
|
img_trnp = copy.copy(tmpjpg)
|
|
else:
|
|
bbox = None
|
|
|
|
# Compare size of old and new images
|
|
osz = ih.embedded_size(elem)
|
|
if osz is None:
|
|
osz = float("inf")
|
|
nsz = os.path.getsize(img_trnp)
|
|
hasmaskclip = (
|
|
elem.get_link("mask") is not None
|
|
or elem.get_link("clip-path") is not None
|
|
) # clipping and masking
|
|
embedimg = (nsz < osz) or (anyalpha0 or hasmaskclip)
|
|
if embedimg:
|
|
Exporter.replace_with_raster(
|
|
elem, img_trnp, bbs[elem.get_id()], bbox
|
|
)
|
|
|
|
tmp = self.tempbase + "_eimg.svg"
|
|
dh.overwrite_svg(svg, tmp)
|
|
cfile = tmp
|
|
|
|
if do_stroketopaths:
|
|
svg = get_svg(cfile)
|
|
# Remove temporary groups
|
|
if self.stroketopath:
|
|
for elid in dgroups:
|
|
elem = svg.getElementById(elid)
|
|
if elem is not None:
|
|
dh.ungroup(elem)
|
|
|
|
tmp = self.tempbase + "_poststp.svg"
|
|
dh.overwrite_svg(svg, tmp)
|
|
cfile = tmp
|
|
|
|
if self.prints:
|
|
self.prints(
|
|
fname
|
|
+ ": Preprocessing done ("
|
|
+ str(round(1000 * (time.time() - timestart)) / 1000)
|
|
+ " s)"
|
|
)
|
|
return cfile
|
|
|
|
def check(self,func, *args, **kwargs):
|
|
"""Wraps a binary call with a check if thread has been stopped; if so, clear the temp file and exit"""
|
|
with sema1 if func==dh.wrapped_binary else sema2:
|
|
if hasattr(self, "aeThread") and self.aeThread.stopped is True:
|
|
self.clear_temp()
|
|
sys.exit()
|
|
ret = func(*args, **kwargs)
|
|
if hasattr(self, "aeThread") and self.aeThread.stopped is True:
|
|
self.clear_temp()
|
|
sys.exit()
|
|
return ret
|
|
|
|
def split_acts(self, fnm, acts, reserved=None, get_bbs=True):
|
|
"""Split actions and run."""
|
|
if reserved is None:
|
|
reserved = set()
|
|
|
|
eargs = ["--actions", "".join([str(a) for a in acts])]
|
|
bbs = None
|
|
try:
|
|
bbs = self.check(dh.wrapped_binary,filename=fnm, inkscape_binary=self.bfn, extra_args=eargs, get_bbs = get_bbs, cwd=self.tempdir)
|
|
fnf_err = False
|
|
except FileNotFoundError:
|
|
fnf_err = True
|
|
|
|
|
|
missing_exports = []
|
|
missing_fns = []
|
|
found_fns = []
|
|
for i, act in enumerate(acts):
|
|
actfile = act.fname
|
|
if actfile is not None and not os.path.exists(joinmod(self.tempdir,actfile)):
|
|
missing_exports.append(act)
|
|
missing_fns.append(actfile)
|
|
if os.path.exists(joinmod(self.tempdir,actfile)):
|
|
found_fns.append(actfile)
|
|
|
|
# dh.idebug('Found '+str(found_fns))
|
|
# dh.idebug('FNF: '+str(fnf_err))
|
|
# dh.idebug('Missing ' + str(missing_fns) +'\n')
|
|
|
|
if fnf_err:
|
|
if len(acts)==1:
|
|
if missing_exports[0].type=='stp':
|
|
bbs = self.split_stp(
|
|
fnm, missing_exports, 0, reserved=reserved, get_bbs = get_bbs
|
|
)
|
|
else:
|
|
# Already simplified call as much as we can...
|
|
raise Exception('FileNotFoundError and cannot split:\n'+str(acts))
|
|
elif len(missing_exports)>0:
|
|
acts1 = missing_exports[: math.ceil(len(missing_exports) / 2)]
|
|
acts2 = missing_exports[math.ceil(len(missing_exports) / 2) :]
|
|
bbs = self.split_acts(
|
|
fnm=fnm, acts=acts1, reserved=reserved, get_bbs = get_bbs
|
|
)
|
|
if len(acts2)>0:
|
|
self.split_acts(
|
|
fnm=fnm, acts=acts2, reserved=reserved, get_bbs = False
|
|
)
|
|
elif any(act.type=='stp' for act in missing_exports):
|
|
stp_idx = [i for i, act in enumerate(missing_exports) if act.type=='stp'][0]
|
|
bbs = self.split_stp(
|
|
fnm, missing_exports, stp_idx, reserved=reserved, get_bbs = get_bbs
|
|
)
|
|
return bbs
|
|
|
|
|
|
def split_stp(self,fnm, acts, selii, reserved, get_bbs=True, cwd=None):
|
|
"""
|
|
Windows cannot handle arguments that are too long.
|
|
If needed, this can split a stroke-to-path operation into two.
|
|
"""
|
|
act = acts[selii]
|
|
actfn = act.fname
|
|
|
|
reserved.update({fnm, actfn})
|
|
isreserved = True
|
|
cnt = 0
|
|
while isreserved:
|
|
actfna = actfn.strip(".svg") + str(cnt) + ".svg"
|
|
isreserved = actfna in reserved
|
|
cnt += 1
|
|
reserved.update({actfna})
|
|
|
|
if len(act.els) == 1:
|
|
# cannot split, fact that we failed means there is a STP crash
|
|
shutil.copy(fnm, actfn)
|
|
return self.split_acts(
|
|
fnm=fnm,
|
|
acts=acts[:selii] + acts[selii + 1 :],
|
|
reserved=reserved
|
|
)
|
|
act1, act2 = act.split(actfna)
|
|
acts1 = acts[:selii] + [act1] if len(act1.els) > 0 else acts[:selii]
|
|
acts2 = (
|
|
[act2] + acts[selii + 1 :] if len(act2.els) > 0 else acts[selii + 1 :]
|
|
)
|
|
|
|
bbs = self.split_acts(
|
|
fnm=fnm, acts=acts1, reserved=reserved, get_bbs=get_bbs
|
|
)
|
|
bbs = self.split_acts(
|
|
fnm=actfna, acts=acts2, reserved=reserved, get_bbs=get_bbs
|
|
)
|
|
return bbs
|
|
|
|
def export_file(self, fin, fformat):
|
|
"""Use the Inkscape binary to export the file"""
|
|
myoutput = self.outtemplate[0:-4] + "." + fformat
|
|
if self.prints:
|
|
fname = os.path.split(self.filein)[1]
|
|
try:
|
|
offset = round(os.get_terminal_size().columns / 2)
|
|
except OSError:
|
|
offset = 40
|
|
fname = fname + " " * max(0, offset - len(fname))
|
|
self.prints(fname + ": Converting to " + fformat, flush=True)
|
|
timestart = time.time()
|
|
|
|
ispsvg = fformat == "psvg"
|
|
notpng = not (fformat == "png")
|
|
|
|
cfile = fin
|
|
if self.thinline and notpng:
|
|
svg = get_svg(cfile)
|
|
if fformat in ["pdf", "eps"]:
|
|
Exporter.thinline_dehancement(svg, "bezier")
|
|
else:
|
|
Exporter.thinline_dehancement(svg, "split")
|
|
tmp = self.tempbase + "_tld" + fformat[0] + ".svg"
|
|
dh.overwrite_svg(svg, tmp)
|
|
cfile = copy.copy(tmp)
|
|
|
|
if fformat == "psvg":
|
|
myoutput = myoutput.replace(".psvg", "_plain.svg")
|
|
|
|
def overwrite_output(filein, fileout):
|
|
if os.path.exists(fileout):
|
|
os.remove(fileout)
|
|
args = [
|
|
self.bfn,
|
|
"--export-background",
|
|
"#ffffff",
|
|
"--export-background-opacity",
|
|
"1.0",
|
|
"--export-dpi",
|
|
str(self.dpi),
|
|
"--export-filename",
|
|
fileout,
|
|
filein,
|
|
]
|
|
if fileout.endswith(".pdf") and self.latexpdf:
|
|
if os.path.exists(fileout + "_tex"):
|
|
os.remove(fileout + "_tex")
|
|
args = args[0:5] + ["--export-latex"] + args[5:]
|
|
if fileout.endswith(".svg"):
|
|
args = (
|
|
[args[0]]
|
|
+ ["--vacuum-defs"]
|
|
+ args[1:5]
|
|
+ ["--export-plain-svg"]
|
|
+ args[5:]
|
|
)
|
|
self.check(dh.subprocess_repeat,args)
|
|
|
|
def make_output(filein, fileout):
|
|
if fileout.endswith(".svg"):
|
|
if not (self.testmode):
|
|
overwrite_output(filein, fileout)
|
|
else:
|
|
shutil.copy(filein, fileout) # skip conversion
|
|
self.made_outputs = [fileout]
|
|
|
|
osvg = get_svg(filein)
|
|
|
|
pgs = osvg.cdocsize.pgs
|
|
haspgs = inkex.installed_haspages
|
|
if (haspgs or self.testmode) and len(pgs) > 0:
|
|
# bbs = dh.BB2(type("DummyClass", (), {"svg": osvg}))
|
|
bbs = dh.BB2(osvg)
|
|
dlbl = self.duplicatelabels
|
|
outputs = []
|
|
pgiis = (
|
|
range(len(pgs)) if not (self.testmode) else [self.testpage - 1]
|
|
)
|
|
for i in pgiis:
|
|
# match the viewbox to each page and delete them
|
|
psvg = get_svg(fileout)
|
|
# plain SVG has no pages
|
|
pgs2 = psvg.cdocsize.pgs
|
|
|
|
Exporter.change_viewbox_to_page(psvg, pgs[i])
|
|
# Only need to delete other Pages in testmode since
|
|
# plain SVGs have none
|
|
if self.testmode:
|
|
for j in reversed(range(len(pgs2))):
|
|
pgs2[j].delete()
|
|
|
|
# Delete content not on current page
|
|
pgbb = dh.bbox(psvg.cdocsize.effvb)
|
|
for k, bbx in bbs.items():
|
|
removeme = not (dh.bbox(bbx).intersect(pgbb))
|
|
if k in dlbl:
|
|
removeme = not (
|
|
dlbl[k] in bbs
|
|
and dh.bbox(bbs[dlbl[k]]).intersect(pgbb)
|
|
)
|
|
if removeme and psvg.getElementById(k) is not None:
|
|
psvg.getElementById(k).delete()
|
|
|
|
pname = pgs[i].get("inkscape:label")
|
|
pname = str(i + 1) if pname is None else pname
|
|
addendum = (
|
|
"_page_" + pname
|
|
if not (self.testmode or len(pgs) == 1)
|
|
else ""
|
|
)
|
|
outparts = fileout.split(".")
|
|
pgout = ".".join(outparts[:-1]) + addendum + "." + outparts[-1]
|
|
dh.overwrite_svg(psvg, pgout)
|
|
outputs.append(pgout)
|
|
self.made_outputs = outputs
|
|
else:
|
|
svg = get_svg(filein)
|
|
pgs = svg.cdocsize.pgs
|
|
|
|
haspgs = inkex.installed_haspages
|
|
if (haspgs or self.testmode) and len(pgs) > 1:
|
|
outputs = []
|
|
pgiis = (
|
|
range(len(pgs)) if not (self.testmode) else [self.testpage - 1]
|
|
)
|
|
for i in pgiis:
|
|
# match the viewbox to each page and delete them
|
|
svgpg = get_svg(filein)
|
|
pgs2 = svgpg.cdocsize.pgs
|
|
Exporter.change_viewbox_to_page(svgpg, pgs[i])
|
|
for j in reversed(range(len(pgs2))):
|
|
pgs2[j].delete()
|
|
|
|
pname = pgs[i].get("inkscape:label")
|
|
pname = str(i + 1) if pname is None else pname
|
|
addendum = "_page_" + pname if not (self.testmode) else ""
|
|
svgpgfn = self.tempbase + addendum + ".svg"
|
|
dh.overwrite_svg(svgpg, svgpgfn)
|
|
|
|
outparts = fileout.split(".")
|
|
pgout = ".".join(outparts[:-1]) + addendum + "." + outparts[-1]
|
|
overwrite_output(svgpgfn, pgout)
|
|
outputs.append(pgout)
|
|
self.made_outputs = outputs
|
|
else:
|
|
overwrite_output(filein, fileout)
|
|
self.made_outputs = [fileout]
|
|
|
|
if not (ispsvg):
|
|
make_output(cfile, myoutput)
|
|
finalnames = self.made_outputs
|
|
else:
|
|
tmp = self.tempbase + "_tmp_small.svg"
|
|
make_output(cfile, tmp)
|
|
|
|
moutputs = self.made_outputs
|
|
finalnames = []
|
|
for mout in moutputs:
|
|
svg = get_svg(mout)
|
|
self.postprocessing(svg)
|
|
finalname = myoutput
|
|
if len(moutputs) > 1:
|
|
pnum, _ = os.path.splitext(mout.split("_page_")[-1])
|
|
finalname = myoutput.replace(
|
|
"_plain.svg", "_page_" + pnum + "_plain.svg"
|
|
)
|
|
dh.overwrite_svg(svg, finalname)
|
|
finalnames.append(finalname)
|
|
|
|
# Remove any previous outputs that we did not just make
|
|
directory, file_name = os.path.split(myoutput)
|
|
base_name, extension = os.path.splitext(file_name)
|
|
if extension == ".svg" and base_name.endswith("_plain"):
|
|
base_name = base_name[:-6] # Remove "_plain" from the base name
|
|
pattern = re.compile(
|
|
rf"{re.escape(base_name)}(_page_.*)?_plain{re.escape(extension)}$"
|
|
)
|
|
else:
|
|
pattern = re.compile(
|
|
rf"{re.escape(base_name)}(_page_.*)?{re.escape(extension)}$"
|
|
)
|
|
matching_files = []
|
|
for file in os.listdir(directory):
|
|
if pattern.match(file):
|
|
matching_files.append(os.path.join(directory, file))
|
|
for file in matching_files:
|
|
if file not in finalnames:
|
|
try:
|
|
os.remove(file)
|
|
except PermissionError:
|
|
pass
|
|
|
|
if self.prints:
|
|
toc = time.time() - timestart
|
|
self.prints(
|
|
fname
|
|
+ ": Conversion to "
|
|
+ fformat
|
|
+ " done ("
|
|
+ str(round(1000 * toc) / 1000)
|
|
+ " s)"
|
|
)
|
|
return True, myoutput
|
|
|
|
def postprocessing(self, svg):
|
|
"""Postprocessing of SVGs, mainly for overcoming bugs in Office products"""
|
|
vds = dh.visible_descendants(svg)
|
|
|
|
# Shift viewbox corner to (0,0) by translating top-level elements
|
|
evb = svg.cdocsize.effvb
|
|
if not (evb[0] == 0 and evb[1] == 0):
|
|
newtr = Transform("translate(" + str(-evb[0]) + ", " + str(-evb[1]) + ")")
|
|
svg.set_viewbox([0, 0, evb[2], evb[3]])
|
|
ndefs = [el for el in list(svg) if not (el.tag in dh.unungroupable)]
|
|
for elem in ndefs:
|
|
elem.ctransform = newtr @ elem.ctransform
|
|
|
|
for dsd in vds:
|
|
if (
|
|
isinstance(dsd, (TextElement))
|
|
and dsd.get_id() not in self.excludetxtids
|
|
):
|
|
scaleto = 100 if not self.testmode else 10
|
|
# Make 10 px in test mode so that errors are not unnecessarily large
|
|
Exporter.scale_text(dsd, scaleto)
|
|
elif isinstance(dsd, (inkex.Image)):
|
|
if ih.hasPIL:
|
|
Exporter.merge_mask(dsd)
|
|
# while dsd.getparent()!=dsd.croot and dsd.getparent() is not None
|
|
# and dsd.croot is not None:
|
|
# # iteratively remove groups containing images
|
|
# # I don't think it's necessary?
|
|
# dh.ungroup(dsd.getparent());
|
|
elif isinstance(dsd, (inkex.Group)):
|
|
if len(dsd) == 0:
|
|
dsd.delete(deleteup=True)
|
|
|
|
if self.backingrect:
|
|
r = inkex.Rectangle()
|
|
tlvl = [el for el in list(svg) if not (el.tag in dh.unungroupable)]
|
|
if len(tlvl) > 0:
|
|
svg.insert(svg.index(tlvl[0]), r)
|
|
else:
|
|
svg.append(r)
|
|
r.cstyle["fill"] = "#ffffff"
|
|
r.cstyle["fill-opacity"] = ".00001"
|
|
r.cstyle["stroke"] = "none"
|
|
vbx = svg.cdocsize.effvb
|
|
r.set("x", vbx[0])
|
|
r.set("y", vbx[1])
|
|
r.set("width", vbx[2])
|
|
r.set("height", vbx[3])
|
|
|
|
# if self.thinline:
|
|
# for dsd in vds:
|
|
# self.Bezier_to_Split(dsd) # moved to deprecated
|
|
|
|
# dh.idebug(self.svgtopdf_vbs)
|
|
# embed_original = False
|
|
# if embed_original:
|
|
# def main_contents(svg):
|
|
# ret = list(svg)
|
|
# for k in reversed(ret):
|
|
# if k.tag in [inkex.addNS('metadata','svg'),
|
|
# inkex.addNS('namedview','sodipodi')]:
|
|
# ret.remove(k);
|
|
# return ret
|
|
|
|
# grp = dh.group(main_contents(svg))
|
|
# svgo = get_svg(original_file)
|
|
# go = dh.group(main_contents(svgo))
|
|
# svg.append(go)
|
|
# go.set('style','display:none');
|
|
# go.set('inkscape:label','SI original');
|
|
# go.set('inkscape:groupmode','layer');
|
|
|
|
# # Conversion to PDF changes the viewbox to pixels. Convert back to the
|
|
# # original viewbox by applying a transform
|
|
# dds = self.svgtopdf_dss[i]
|
|
# tfmt = 'matrix({0},{1},{2},{3},{4},{5})';
|
|
# T =inkex.Transform(tfmt.format(dds.uuw,0,0,dds.uuh,
|
|
# -dds.effvb[0]*dds.uuw,
|
|
# -dds.effvb[1]*dds.uuh))
|
|
# grp.set('transform',str(-T))
|
|
# svg.set_viewbox(dds.effvb)
|
|
|
|
tel = None
|
|
for elem in list(svg):
|
|
if (
|
|
isinstance(elem, (inkex.TextElement,))
|
|
and elem.text is not None
|
|
and ORIG_KEY in elem.text
|
|
):
|
|
tel = elem
|
|
if tel is None:
|
|
# tel = svg.new_element(inkex.TextElement, svg)
|
|
tel = inkex.TextElement()
|
|
svg.append(tel)
|
|
tel.text = ORIG_KEY + ": {0}, hash: {1}".format(
|
|
self.original_file, hash_file(self.original_file)
|
|
)
|
|
tel.set("style", "display:none")
|
|
dh.clean_up_document(svg) # Clean up
|
|
|
|
PTH_COMMANDS = list("MLHVCSQTAZmlhvcsqtaz")
|
|
|
|
@staticmethod
|
|
def thinline_dehancement(svg, mode="split"):
|
|
"""
|
|
Prevents thin-line enhancement in certain bad PDF renderers
|
|
'bezier' mode converts h,v,l path commands to trivial Beziers
|
|
'split' mode splits h,v,l path commands into two path commands
|
|
The Office PDF renderer removes trivial Beziers, as does conversion to EMF
|
|
The Inkscape PDF/EPS renderer removes split commands
|
|
Split is more general, so apply it to everything but PDFs/EPS
|
|
"""
|
|
|
|
command_chars = {"h", "v", "l", "H", "V", "L"}
|
|
split = mode == "split"
|
|
for elem in svg.descendants2():
|
|
if elem.tag in otp_support_tags and not elem.tag == peltag:
|
|
elem.object_to_path()
|
|
|
|
pthd = elem.get("d")
|
|
if pthd and any(char in pthd for char in command_chars):
|
|
if any(char in pthd for char in {"H", "V", "L"}):
|
|
pthd = str(inkex.Path(pthd).to_relative())
|
|
dds = [v for v in pthd.replace(",", " ").split(" ") if v]
|
|
current_command = None
|
|
i = 0
|
|
while i < len(dds):
|
|
token = dds[i]
|
|
if token in {"v", "h", "l"}:
|
|
current_command = token
|
|
dds[i] = ""
|
|
elif token in Exporter.PTH_COMMANDS:
|
|
current_command = None
|
|
else:
|
|
if current_command == "h":
|
|
hval = float(token)
|
|
dds[i] = (
|
|
f"h {hval / 2} {hval / 2}"
|
|
if split
|
|
else f"c {hval},0 {hval},0 {hval},0"
|
|
)
|
|
elif current_command == "v":
|
|
vval = float(token)
|
|
dds[i] = (
|
|
f"v {vval / 2} {vval / 2}"
|
|
if split
|
|
else f"c 0,{vval} 0,{vval} 0,{vval}"
|
|
)
|
|
elif current_command == "l":
|
|
lxv = float(token)
|
|
lyv = float(dds[i + 1])
|
|
dds[i] = (
|
|
f"l {lxv / 2},{lyv / 2} {lxv / 2},{lyv / 2}"
|
|
if split
|
|
else f"c {lxv},{lyv} {lxv},{lyv} {lxv},{lyv}"
|
|
)
|
|
dds[i + 1] = ""
|
|
i += 1
|
|
i += 1
|
|
|
|
newd = " ".join(v for v in dds if v).replace(" ", " ")
|
|
elem.set("d", newd)
|
|
|
|
@staticmethod
|
|
def marker_fix(elem):
|
|
"""Fixes the marker bug that occurs with context-stroke and context-fill"""
|
|
mkrs = Exporter.get_markers(elem)
|
|
sty = elem.cspecified_style
|
|
for mtyp, mkrel in mkrs.items():
|
|
dh.get_strokefill(elem)
|
|
mkrds = mkrel.descendants2()
|
|
anycontext = any(
|
|
a in ("stroke", "fill") and "context" in v
|
|
for d in mkrds
|
|
for a, v in d.cspecified_style.items()
|
|
)
|
|
if anycontext:
|
|
handled = True
|
|
dup = mkrel.duplicate()
|
|
dupds = dup.descendants2()
|
|
for dsd in dupds:
|
|
dsty = dsd.cspecified_style
|
|
for att, val in dsty.items():
|
|
if att in ("stroke", "fill") and "context" in val:
|
|
if val == "context-stroke":
|
|
dsd.cstyle[att] = sty.get("stroke", "none")
|
|
elif val == "context-fill":
|
|
dsd.cstyle[att] = sty.get("fill", "none")
|
|
else: # I don't know what this is
|
|
handled = False
|
|
if handled:
|
|
elem.cstyle[mtyp] = dup.get_id(as_url=2)
|
|
return mkrs
|
|
|
|
@staticmethod
|
|
def opacity_fix(elem):
|
|
"""
|
|
Fuse opacity onto fill and stroke for path-like elements
|
|
Helps prevent rasterization-at-PDF for Office products
|
|
"""
|
|
sty = elem.cspecified_style
|
|
if (
|
|
sty.get("opacity") is not None
|
|
and float(sty.get("opacity", 1.0)) != 1.0
|
|
and elem.tag in otp_support_tags
|
|
):
|
|
strf = dh.get_strokefill(elem) # fuses opacity and
|
|
# stroke-opacity/fill-opacity
|
|
if strf.stroke is not None and strf.fill is None:
|
|
elem.cstyle["stroke-opacity"] = strf.stroke.alpha
|
|
elem.cstyle["opacity"] = 1
|
|
elif strf.fill is not None and strf.stroke is None:
|
|
elem.cstyle["fill-opacity"] = strf.fill.alpha
|
|
elem.cstyle["opacity"] = 1
|
|
|
|
@staticmethod
|
|
def subsuper_fix(elem):
|
|
"""
|
|
Replace super and sub with numerical values
|
|
Collapse Tspans with 0% baseline-shift, which Office displays incorrectly
|
|
"""
|
|
for dsd in elem.descendants2():
|
|
bsh = dsd.ccascaded_style.get("baseline-shift")
|
|
if bsh in ["super", "sub"]:
|
|
sty = dsd.ccascaded_style
|
|
sty["baseline-shift"] = "40%" if bsh == "super" else "-20%"
|
|
dsd.cstyle = sty
|
|
|
|
for dsd in reversed(elem.descendants2()): # all Tspans
|
|
bsh = dsd.ccascaded_style.get("baseline-shift")
|
|
if bsh is not None and bsh.replace(" ", "") == "0%":
|
|
# see if any ancestors with non-zero shift
|
|
anysubsuper = False
|
|
for anc in dsd.ancestors2(stopafter=elem):
|
|
bsa = anc.ccascaded_style.get("baseline-shift")
|
|
if bsa is not None and "%" in bsa:
|
|
anysubsuper = float(bsa.replace(" ", "").strip("%")) != 0
|
|
|
|
# split parent
|
|
myp = dsd.getparent()
|
|
if anysubsuper and myp is not None:
|
|
dds = dh.split_text(myp)
|
|
for dsd2 in reversed(dds):
|
|
if (
|
|
len(list(dsd2)) == 1
|
|
and list(dsd2)[0].ccascaded_style.get("baseline-shift")
|
|
== "0%"
|
|
):
|
|
sel = list(dsd2)[0]
|
|
mys = sel.ccascaded_style
|
|
if mys.get("baseline-shift") == "0%":
|
|
mys["baseline-shift"] = dsd2.ccascaded_style.get(
|
|
"baseline-shift"
|
|
)
|
|
fsz, scf, _ = dh.composed_width(sel, "font-size")
|
|
mys["font-size"] = str(fsz / scf)
|
|
dsd2.addprevious(sel)
|
|
sel.cstyle = mys
|
|
sel.tail = dsd2.tail
|
|
dsd2.delete()
|
|
Exporter.subsuper_fix(
|
|
elem
|
|
) # parent is now gone, so start over
|
|
return
|
|
|
|
@staticmethod
|
|
def scale_text(elem, scaleto):
|
|
"""
|
|
Sets all font-sizes to 100 px by moving size into transform
|
|
Office rounds every fonts to the nearest px and then transforms it,
|
|
so this makes text sizes more accurate
|
|
"""
|
|
svg = elem.croot
|
|
szs = []
|
|
for dsd in elem.descendants2(): # all Tspan sizes
|
|
fsz = dsd.ccascaded_style.get("font-size")
|
|
fsz = {"small": "10px", "medium": "12px", "large": "14px"}.get(fsz, fsz)
|
|
if fsz is not None and "%" not in fsz:
|
|
szs.append(dh.ipx(fsz))
|
|
if len(szs) == 0:
|
|
maxsz = default_style_atts["font-size"]
|
|
maxsz = {"small": 10, "medium": 12, "large": 14}.get(maxsz, maxsz)
|
|
else:
|
|
maxsz = max(szs)
|
|
|
|
scv = 1 / maxsz * scaleto
|
|
|
|
# Make a dummy group so we can properly compose the transform
|
|
grp = dh.group([elem], moveTCM=True)
|
|
|
|
for dsd in reversed(elem.descendants2()):
|
|
x = ParsedText.get_xy(dsd, "x")
|
|
y = ParsedText.get_xy(dsd, "y")
|
|
dxv = ParsedText.get_xy(dsd, "dx")
|
|
dyv = ParsedText.get_xy(dsd, "dy")
|
|
|
|
if x[0] is not None:
|
|
xyset(dsd, "x", [v * scv for v in x])
|
|
if y[0] is not None:
|
|
xyset(dsd, "y", [v * scv for v in y])
|
|
if dxv[0] is not None:
|
|
xyset(dsd, "dx", [v * scv for v in dxv])
|
|
if dyv[0] is not None:
|
|
xyset(dsd, "dy", [v * scv for v in dyv])
|
|
|
|
fsz, scf, _ = dh.composed_width(dsd, "font-size")
|
|
if scf==0:
|
|
continue
|
|
dsd.cstyle["font-size"] = "{:.3f}".format(fsz / scf * scv)
|
|
|
|
otherpx = [
|
|
"letter-spacing",
|
|
"inline-size",
|
|
"stroke-width",
|
|
"stroke-dasharray",
|
|
]
|
|
for oth in otherpx:
|
|
othv = dsd.ccascaded_style.get(oth)
|
|
if (
|
|
othv is None
|
|
and oth == "stroke-width"
|
|
and "stroke" in dsd.ccascaded_style
|
|
):
|
|
othv = default_style_atts[oth]
|
|
if othv is not None:
|
|
if "," not in othv:
|
|
if 'em' not in othv: # scaling not needed for em sizes
|
|
dsd.cstyle[oth] = str((dh.ipx(othv) or 0) * scv)
|
|
else:
|
|
dsd.cstyle[oth] = ",".join(
|
|
[str((dh.ipx(v) or 0) * scv) for v in othv.split(",")]
|
|
)
|
|
|
|
shape = dsd.ccascaded_style.get_link("shape-inside", svg)
|
|
if shape is not None:
|
|
dup = shape.duplicate()
|
|
svg.cdefs.append(dup)
|
|
dup.ctransform = (
|
|
inkex.Transform((scv, 0, 0, scv, 0, 0)) @ dup.ctransform
|
|
)
|
|
dsd.cstyle["shape-inside"] = dup.get_id(as_url=2)
|
|
|
|
elem.ctransform = inkex.Transform((1 / scv, 0, 0, 1 / scv, 0, 0))
|
|
dh.ungroup(grp)
|
|
|
|
@staticmethod
|
|
def merge_mask(elem):
|
|
"""
|
|
Office will poorly rasterize masked elements at PDF-time
|
|
Revert alpha masks made by PDFication back to alpha
|
|
"""
|
|
mymask = elem.get_link("mask")
|
|
if mymask is not None and len(list(mymask)) == 1:
|
|
# only if one object in mask
|
|
mask = list(mymask)[0]
|
|
if isinstance(mask, inkex.Image):
|
|
if mask.get("height") == "1" and mask.get("width") == "1":
|
|
im1 = ih.str_to_ImagePIL(elem.get("xlink:href")).convert("RGBA")
|
|
im2 = ih.str_to_ImagePIL(mask.get("xlink:href")).convert("RGBA")
|
|
# only if mask the same size
|
|
if im1.size == im2.size:
|
|
d1a = np.asarray(im1)
|
|
d2a = np.asarray(im2)
|
|
# only if image is opaque
|
|
if np.where(d1a[:, :, 3] == 255, True, False).all():
|
|
nda = np.stack(
|
|
(
|
|
d1a[:, :, 0],
|
|
d1a[:, :, 1],
|
|
d1a[:, :, 2],
|
|
d2a[:, :, 0],
|
|
),
|
|
2,
|
|
)
|
|
# pylint: disable=import-outside-toplevel
|
|
from PIL import Image as ImagePIL
|
|
# pylint: enable=import-outside-toplevel
|
|
|
|
newstr = ih.ImagePIL_to_str(ImagePIL.fromarray(nda))
|
|
elem.set("xlink:href", newstr)
|
|
mask.delete()
|
|
elem.set("mask", None)
|
|
|
|
@staticmethod
|
|
def standardize_image(elem):
|
|
"""Office has difficulty with clipped images with a nonzero x or y."""
|
|
xv = dh.ipx(elem.get('x','0'))
|
|
yv = dh.ipx(elem.get('y','0'))
|
|
if xv!=0 or yv!=0:
|
|
grp = dh.group([elem], moveTCM=True)
|
|
elem.set('x','0')
|
|
elem.set('y','0')
|
|
elem.ctransform = inkex.Transform(f'translate({xv},{yv})')
|
|
dh.ungroup(grp)
|
|
|
|
@staticmethod
|
|
def replace_with_raster(elem, imgloc, bbx, imgbbox):
|
|
"""Replace vector elements with raster images."""
|
|
svg = elem.croot
|
|
if svg is None: # in case we were already rasterized within ancestor
|
|
return
|
|
ih.embed_external_image(elem, imgloc)
|
|
|
|
# The exported image has a different size and shape than the original
|
|
# Correct by putting transform/clip/mask on a new parent group, then
|
|
# fix location, then ungroup
|
|
grp = dh.group([elem], moveTCM=True)
|
|
grp.set("clip-path", None)
|
|
# conversion to bitmap already includes clips
|
|
grp.set("mask", None) # conversion to bitmap already includes masks
|
|
|
|
# Calculate what transform is needed to preserve the image's location
|
|
ctf = elem.ccomposed_transform
|
|
pbb = [
|
|
Vector2d(bbx[0], bbx[1]),
|
|
Vector2d(bbx[0] + bbx[2], bbx[1]),
|
|
Vector2d(bbx[0] + bbx[2], bbx[1] + bbx[3]),
|
|
Vector2d(bbx[0], bbx[1] + bbx[3]),
|
|
]
|
|
# top-left,tr,br,bl
|
|
put = [
|
|
(-ctf).apply_to_point(p) for p in pbb
|
|
] # untransformed bbox (where the image needs to go)
|
|
|
|
newel = inkex.Image()
|
|
dh.replace_element(elem, newel)
|
|
newel.set("x", 0)
|
|
myx = 0
|
|
newel.set("y", 0)
|
|
myy = 0
|
|
newel.set("width", 1)
|
|
myw = dh.ipx(newel.get("width"))
|
|
newel.set("height", 1)
|
|
myh = dh.ipx(newel.get("height"))
|
|
newel.set("xlink:href", elem.get("xlink:href"))
|
|
|
|
# Inkscape inappropriately clips non-'optimizeQuality' images
|
|
# when generating PDFs
|
|
sty = "image-rendering:optimizeQuality"
|
|
elem = newel
|
|
|
|
pgo = [
|
|
Vector2d(myx, myy),
|
|
Vector2d(myx + myw, myy),
|
|
Vector2d(myx + myw, myy + myh),
|
|
Vector2d(myx, myy + myh),
|
|
] # where the image is
|
|
elem.set("preserveAspectRatio", "none") # prevents aspect ratio snapping
|
|
elem.set("style", sty) # override existing styles
|
|
|
|
# pylint: disable=invalid-name
|
|
a = np.array(
|
|
[
|
|
[pgo[0].x, 0, pgo[0].y, 0, 1, 0],
|
|
[0, pgo[0].x, 0, pgo[0].y, 0, 1],
|
|
[pgo[1].x, 0, pgo[1].y, 0, 1, 0],
|
|
[0, pgo[1].x, 0, pgo[1].y, 0, 1],
|
|
[pgo[2].x, 0, pgo[2].y, 0, 1, 0],
|
|
[0, pgo[2].x, 0, pgo[2].y, 0, 1],
|
|
]
|
|
)
|
|
b = np.array(
|
|
[
|
|
[
|
|
put[0].x,
|
|
put[0].y,
|
|
put[1].x,
|
|
put[1].y,
|
|
put[2].x,
|
|
put[2].y,
|
|
]
|
|
]
|
|
).T
|
|
T = np.linalg.solve(a, b)
|
|
T = "matrix(" + ",".join([str(v[0]) for v in T]) + ")"
|
|
elem.set("transform", T)
|
|
# pylint: enable=invalid-name
|
|
|
|
# If we cropped, need to modify location according to bbox
|
|
if ih.hasPIL and imgbbox is not None:
|
|
elem.set("x", str(myx + imgbbox[0] * myw))
|
|
elem.set("y", str(myy + imgbbox[1] * myh))
|
|
elem.set("width", str((imgbbox[2] - imgbbox[0]) * myw))
|
|
elem.set("height", str((imgbbox[3] - imgbbox[1]) * myh))
|
|
dh.ungroup(grp)
|
|
|
|
@staticmethod
|
|
def add_margin(svg, amt_mm, testmode):
|
|
"""Add margin to the document."""
|
|
mrgn = inkex.units.convert_unit(str(amt_mm) + "mm", "px")
|
|
uuw, uuh = svg.cdocsize.uuw, svg.cdocsize.uuh
|
|
# pgs = self.Get_Pages(svg)
|
|
|
|
haspgs = inkex.installed_haspages or testmode
|
|
if haspgs and len(svg.cdocsize.pgs) > 0:
|
|
# Has Pages
|
|
for page in svg.cdocsize.pgs:
|
|
newbbuu = svg.cdocsize.pxtouupgs(
|
|
[
|
|
page.bbpx[0] - mrgn,
|
|
page.bbpx[1] - mrgn,
|
|
page.bbpx[2] + 2 * mrgn,
|
|
page.bbpx[3] + 2 * mrgn,
|
|
]
|
|
)
|
|
page.set("x", str(newbbuu[0]))
|
|
page.set("y", str(newbbuu[1]))
|
|
page.set("width", str(newbbuu[2]))
|
|
page.set("height", str(newbbuu[3]))
|
|
svg.cdocsize = None
|
|
|
|
else:
|
|
# If an old version of Inkscape or has no Pages, defined by viewbox
|
|
vbx = svg.cdocsize.effvb
|
|
nvb = [
|
|
vbx[0] - mrgn / uuw,
|
|
vbx[1] - mrgn / uuh,
|
|
vbx[2] + 2 * mrgn / uuw,
|
|
vbx[3] + 2 * mrgn / uuh,
|
|
]
|
|
svg.set_viewbox(nvb)
|
|
|
|
@staticmethod
|
|
def change_viewbox_to_page(svg, page):
|
|
"""Change viewbox to match specified page."""
|
|
newvb = page.croot.cdocsize.pxtouu(page.bbpx)
|
|
svg.set_viewbox(newvb)
|
|
|
|
@staticmethod
|
|
def get_markers(elem):
|
|
"""Returns valid marker keys and corresponding elements"""
|
|
mkrd = dict()
|
|
for mtyp in ["marker", "marker-start", "marker-mid", "marker-end"]:
|
|
mkrel = elem.cstyle.get_link(mtyp, elem.croot)
|
|
if mkrel is not None:
|
|
mkrd[mtyp] = mkrel
|
|
return mkrd
|
|
|
|
@staticmethod
|
|
def stroke_to_path_fixes(els):
|
|
"""
|
|
Stroke to Path has tons of bugs. Try to preempt them.
|
|
1. STP does not properly handle clips, so move clips and
|
|
masks to a temp parent group.
|
|
2. Densely-spaced nodes can be converted incorrectly, so scale paths up
|
|
to be size 1000 and position corner at (0,0)
|
|
3. Starting markers can be flipped.
|
|
4. Markers on groups cause crashes
|
|
5. Markers are given the wrong size
|
|
"""
|
|
dummy_groups = []
|
|
path_els = []
|
|
stroked_els = []
|
|
if len(els) > 0:
|
|
svg = els[0].croot
|
|
for elem in els:
|
|
mkrs = Exporter.get_markers(elem)
|
|
if isinstance(elem, inkex.Group) and len(mkrs) > 0:
|
|
dh.ungroup(elem)
|
|
elif elem.tag in otp_support_tags:
|
|
sty = elem.cspecified_style
|
|
if "stroke" in sty and sty["stroke"] != "none":
|
|
stroked_els.append((elem, sty, mkrs))
|
|
|
|
for elem, sty, mkrs in stroked_els:
|
|
swd = sty.get("stroke-width")
|
|
if dh.ipx(swd) == 0:
|
|
# Fix bug on zero-stroke paths
|
|
sty["stroke"] = "none"
|
|
elem.cstyle = sty
|
|
elif swd is None:
|
|
sty["stroke-width"] = default_style_atts['stroke-width']
|
|
else:
|
|
# Clip and mask issues solved by moving to a dummy parent group
|
|
gndp = dh.group([elem], moveTCM=True)
|
|
# Do it again so we can add transform later without
|
|
# messing up clip
|
|
grp = dh.group([elem], moveTCM=True)
|
|
|
|
path_els.append(elem.get_id())
|
|
dummy_groups.extend([gndp.get_id(), grp.get_id()])
|
|
|
|
# Scale up most paths to be size 1000 and position corner
|
|
# at (0,0)
|
|
if inkex.addNS("type", "sodipodi") not in elem.attrib:
|
|
# Object to path doesn't currently support
|
|
# Inkscape-specific objects
|
|
elem.object_to_path()
|
|
bbx = elem.bounding_box2(
|
|
dotransform=False, includestroke=False, roughpath=False
|
|
)
|
|
if len(mkrs) == 0:
|
|
# scaleby = 1000
|
|
maxsz = max(bbx.w, bbx.h)
|
|
scaleby = 1000 / maxsz if maxsz > 0 else 1000
|
|
else:
|
|
# For paths with markers, scale to make stroke-width=1
|
|
# Prevents incorrect marker size
|
|
# https://gitlab.com/inkscape/inbox/-/issues/10506#note_1931910230
|
|
scaleby = 1 / dh.ipx(swd) if swd is not None else 1
|
|
|
|
tfm = Transform("scale({0})".format(scaleby)) @ Transform(
|
|
"translate({0},{1})".format(-bbx.x1, -bbx.y1)
|
|
)
|
|
# relative paths seem to have some bugs
|
|
pth2 = str(elem.cpath.to_absolute().transform(tfm))
|
|
elem.set("d", pth2)
|
|
|
|
# Put transform on parent group since STP cannot convert
|
|
# transformed paths correctly if they have dashes
|
|
# See https://gitlab.com/inkscape/inbox/-/issues/7844
|
|
grp.ctransform = -tfm
|
|
|
|
csty = elem.cspecified_style
|
|
if "stroke-width" in csty:
|
|
swd = dh.ipx(csty["stroke-width"])
|
|
elem.cstyle["stroke-width"] = str(swd * scaleby)
|
|
if "stroke-dasharray" in csty:
|
|
sda = dh.listsplit(csty["stroke-dasharray"])
|
|
elem.cstyle["stroke-dasharray"] = (
|
|
str([(sdv or 0) * scaleby for sdv in sda])
|
|
.strip("[")
|
|
.strip("]")
|
|
)
|
|
|
|
# Fix bug on start markers where auto-start-reverse
|
|
# oriented markers are inverted by STP
|
|
sty = elem.cspecified_style
|
|
mstrt = sty.get_link("marker-start", svg)
|
|
if mstrt is not None:
|
|
if mstrt.get("orient") == "auto-start-reverse":
|
|
dup = mstrt.duplicate()
|
|
dup.set("orient", "auto")
|
|
for dkid in list(dup):
|
|
dkid.ctransform = Transform("scale(-1)") @ dkid.ctransform
|
|
sty["marker-start"] = dup.get_id(as_url=2)
|
|
elem.cstyle = sty
|
|
return path_els, dummy_groups
|
|
|
|
|
|
# Convenience functions
|
|
def joinmod(dirc, fname):
|
|
"""Join directory and file name with absolute path."""
|
|
return os.path.join(os.path.abspath(dirc), fname)
|
|
|
|
def get_svg(fin):
|
|
"""Load an SVG file and return the root svg element."""
|
|
try:
|
|
svg = inkex.load_svg(fin).getroot()
|
|
except lxml.etree.XMLSyntaxError:
|
|
# Try removing problematic bytes
|
|
with open(fin, "rb") as file:
|
|
bytes_content = file.read()
|
|
cleaned_content = bytes_content.decode("utf-8", errors="ignore")
|
|
nfin = dh.shared_temp(filename="cleaned.svg")
|
|
with open(nfin, "w", encoding="utf-8") as file:
|
|
file.write(cleaned_content)
|
|
svg = inkex.load_svg(nfin).getroot()
|
|
return svg
|
|
|
|
|
|
ORIG_KEY = "si_ae_original_filename"
|
|
DUP_KEY = "si_ae_original_duplicate"
|
|
|
|
|
|
def hash_file(filename):
|
|
"""Calculate hash of a file."""
|
|
hashv = hashlib.sha256()
|
|
with open(filename, "rb") as file:
|
|
chunk = 0
|
|
while chunk != b"":
|
|
chunk = file.read(1024)
|
|
hashv.update(chunk)
|
|
return hashv.hexdigest()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
dh.Run_SI_Extension(AutoExporter(), "Autoexporter") |