summaryrefslogtreecommitdiffstats
path: root/share/extensions/hershey.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/hershey.py')
-rw-r--r--share/extensions/hershey.py2040
1 files changed, 2040 insertions, 0 deletions
diff --git a/share/extensions/hershey.py b/share/extensions/hershey.py
new file mode 100644
index 0000000..fcec22a
--- /dev/null
+++ b/share/extensions/hershey.py
@@ -0,0 +1,2040 @@
+# coding=utf-8
+#
+# Copyright(C) 2021 - Windell H. Oskay, www.evilmadscientist.com
+#
+# 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.
+#
+
+"""
+Hershey Text 3.0.6, 2022-07-21
+
+Copyright 2022, Windell H. Oskay, www.evilmadscientist.com
+
+Major revisions in Hershey Text 3.0:
+
+1. Migrate font format to use SVG fonts.
+ - SVG fonts support unicode, meaning that we can use a full range of
+ characters. We are no longer limited to the ASCII range that the
+ historical Hershey font formats used.
+ - Arbitrary curves are supported within glyphs; we are no longer limited to
+ the straight line segments used in the historical Hershey format.
+ - The set of fonts can now be expanded.
+
+2. Add a mechanism for adding your own SVG fonts, either within the
+ folder containing the default fonts, or from an external file or directory.
+ This is particularly important for installations where one does not
+ have access to edit the contents of the Inkscape extensions directory.
+
+3. Support font mapping: If a given font face is used for a given block of
+ text, check first to see if a matching SVG font is present. If not,
+ substitute with the default (selected) stroke font from the list of
+ included fonts.
+
+4. Instead of entering text (one line at a time) in the extension,
+ this script now converts text (either all text, or all selected text)
+ in the document, replacing it in place. While not every possible
+ method of formatting text is supported, many are.
+
+"""
+
+import os
+import math
+
+from copy import deepcopy
+
+import inkex
+from inkex import Transform, Style, units, AbortExtension
+from inkex.localization import inkex_gettext as _
+
+from inkex import (
+ load_svg,
+ Group,
+ TextElement,
+ FlowPara,
+ SVGfont,
+ FontFace,
+ FlowSpan,
+ Glyph,
+ MissingGlyph,
+ Tspan,
+ FlowRoot,
+ Rectangle,
+ Use,
+ PathElement,
+ Defs,
+)
+
+
+class Hershey(inkex.Effect):
+
+ """
+ An extension for use with Inkscape 1.0
+ """
+
+ def __init__(self):
+ super(Hershey, self).__init__()
+
+ self.arg_parser.add_argument(
+ "--tab",
+ dest="mode",
+ default="render",
+ help="The active tab or mode when Apply was pressed",
+ )
+
+ self.arg_parser.add_argument(
+ "--fontface",
+ dest="fontface",
+ default="HersheySans1",
+ help="The selected font face when Apply was pressed",
+ )
+
+ self.arg_parser.add_argument(
+ "--otherfont",
+ dest="otherfont",
+ default="",
+ help="Optional other font name or path to use",
+ )
+
+ self.arg_parser.add_argument(
+ "--preserve",
+ type=inkex.Boolean,
+ dest="preserve_text",
+ default=False,
+ help="Preserve original text",
+ )
+
+ self.arg_parser.add_argument(
+ "--action",
+ dest="util_mode",
+ default="sample",
+ help="The utility option selected",
+ )
+
+ self.arg_parser.add_argument(
+ "--text",
+ dest="sample_text",
+ default="\nThe Quick Brown Fox Jumps Over a Lazy Dog",
+ help="Text to use for font table",
+ )
+
+ self.font_file_list = dict()
+ self.font_load_fail = False
+
+ self.svg_height = None
+ self.svg_width = None
+
+ self.output_generated = False
+
+ self.warn_unflow = False
+ self.warn_unkern = False
+ self.warn_textpath = (
+ False # For future use: Give warning about text attached to path.
+ )
+ self.font_dict = dict() # Font dictionary - Dictionary of loaded fonts
+
+ self.nodes_to_delete = [] # List of font elements to remove
+
+ self.vb_scale_factor = 0.0104166666
+
+ self.text_string = ""
+ self.text_families = [] # List of font family for characters in the string
+ self.text_heights = [] # List of font heights
+ self.text_spacings = [] # List of vertical line heights
+ self.text_aligns = [] # List of horizontal alignment values
+ self.text_x = [] # List; x-coordinate of text line start
+ self.text_y = [] # List; y-coordinate of text line start
+ self.line_number = 0
+ self.new_line = True
+ self.render_width = 1
+
+ PX_PER_INCH = 96.0
+
+ help_text = """====== Hershey Text Help ======
+
+The Hershey Text extension is designed to replace text in your document (either
+selected text or all text) with specialized "stroke" or "engraving" fonts
+designed for plotters.
+
+Whereas regular "outline" fonts (e.g., TrueType) work by filling in the region
+inside an invisible outline, stroke fonts are composed only of individual lines
+or strokes with finite width; much like human handwriting when using a physical
+pen.
+
+Stroke fonts are most often used for creating text-like paths that computer
+controlled drawing and cutting machines (from pen plotters to CNC routers) can
+efficiently follow.
+
+A full user guide for Hershey Text is available to download from
+ http://wiki.evilmadscientist.com/hershey
+
+
+ ==== Basic operation ====
+
+To use Hershey Text, start with a document that contains text objects. Select
+the "Render" tab of Hershey Text, and choose a font face from the pop-up menu.
+
+When you click Apply, it will render all text elements on your page with the
+selected stroke-based typeface. If you would like to convert only certain text
+elements, click Apply with just those elements selected.
+
+If the "Preserve original text" box is checked, then the original text elements
+on the page will be preserved even when you click Apply. If it is unchecked,
+then the original font elements will be removed once rendered.
+
+You can generate a list of available SVG fonts or a list of all glyphs available
+in a given font by using the tools available on the "Utilities" tab.
+
+
+ ==== How Hershey Text works ====
+
+Hershey Text works by performing font substitution, starting with the text in
+your document and replacing it with paths generated from the characters in the
+selected SVG font.
+
+Hershey Text uses fonts in the SVG font format. While SVG fonts are one of the
+few types that support stroke-based characters, it is important to note that
+converting an outline font to SVG format does not convert it to a stroke based
+font. Indeed, most SVG fonts are actually outline fonts.
+
+This extension *does not* convert outline fonts into stroke fonts, nor does it
+convert other fonts into SVG format. Its sole function is to replace the text
+in your document with paths from the selected SVG font.
+
+
+ ==== Using an external SVG font ====
+
+To use an external SVG font -- one not included with the distribution -- select
+"Other" for the name of the font in the pop-up menu on the "Render" tab. Then,
+do one of the following:
+
+(1) Add your SVG font file (perhaps "example.svg") to the "svg_fonts" directory
+within your Inkscape extensions directory, and enter the name of the font
+("example") in the "Other SVG font name or path" box on the "Render" tab.
+
+or
+
+(2) Place your SVG font file anywhere on your computer, and enter the full path
+to the file in the "Other SVG font name or path" box on the "Render" tab.
+A full path might, for example, look like:
+ /Users/Robin/Documents/AxiDraw/fonts/path_handwriting.svg
+
+
+ ==== Using SVG fonts: Advanced methods ====
+
+In addition to using a single SVG font for substitution, you can also use
+font name mapping to automatically use particular stroke fonts in place of
+specific font faces, to support various automated workflows and to support
+the rapid use of multiple stroke font faces within the same document.
+
+Several SVG fonts are included with this distribution, including both
+single-stroke and multi-stroke fonts. These fonts are included within the
+"svg_fonts" directory within your Inkscape extensions directory.
+
+You can select the font that you would like to use from the pop-up menu on the
+"Render" Tab. You can also make use of your own SVG fonts.
+
+Order of preference for SVG fonts:
+
+(1) If there is an SVG font with name matching that of the font for a given
+piece of text, that font will be used. For example, if the original text is in
+font "FancyScript" and there is a file in svg_fonts with name FancyScript.svg,
+then FancyScript.svg will be used to render the text.
+
+(2) Otherwise (if there is no SVG font available matching the name of the font
+for a given block of text), the face selected from the "Font face" pop-up menu
+will be used as the default font when rendering text with Hershey Text.
+
+(3) You can also enter text in the "Name/Path" box, which can represent one of
+the following: (i) a font name (for a font located in the svg_fonts directory),
+(ii) the path to a font file elsewhere on your computer, or (iii) the path to a
+directory containing (one or more) font files.
+
+(3a) Using a font name:
+If you move a custom SVG font file into your svg_fonts directory, then you can
+enter the name of the SVG font in the "Name/Path" text box and select "Other"
+from the pop-up menu. Then, the named font will be used as the default.
+
+(3b) Using a file path:
+If you enter the path to an SVG font file in the "Name/Path" text box and
+select "Other" from the pop-up menu. Then, that font will be used as the
+default. All SVG fonts located in the same directory as that font file will
+also be available for name-based font substitution. If there are multiple
+font-name matches, files in an external directory take precedence over ones in
+the svg_fonts directory.
+
+(3c) Using a directory path:
+If you enter the path to a directory containing SVG font files in the
+"Name/Path" text box, then all SVG font files files in that directory will be
+available for name-based font substitution. If there are multiple font-name
+matches, files in an external directory take precedence over ones in the
+svg_fonts directory.
+
+
+
+Tips about using these methods with your own custom fonts:
+
+(A) These methods can be used to render different text elements with different
+SVG font faces. You can even rename a font -- either your own custom one or one
+of the bundled ones -- to match the name of a font that you're using. For
+example, if you rename a script font you name a font to "Helvetica.svg",
+then all text in Helvetica will be replaced with that SVG font.
+
+(B) Using a directory path (3c) is a particularly helpful method if you do
+not have access to modify the svg_fonts directory.
+
+
+
+ ==== Limitations ====
+
+This extension renders text into non-editable paths, generated from the
+character geometry of SVG fonts. Once you have rendered the text, the resulting
+paths can be edited with path editing tools, but not text editing tools.
+
+Since this extension works by a process of font substitution, text spanning a
+single line will generally stay that way, whereas text flowed in a box (that
+may span multiple lines) will be re-flowed from scratch. Style information such
+as text size and line spacing can be lost in some cases.
+
+We recommend that you use the live preview option to achieve best results with
+this extension.
+
+
+(c) 2021 Windell H. Oskay
+Evil Mad Scientist Laboratories
+"""
+
+ def getlength_inch(self, name):
+ """
+ Get the <svg> attribute with name "name", and parse it as a length,
+ into a value and associated units. Return value in inches.
+
+ This may cause scaling issues in some circumstances. Note, for
+ example, that Adobe Illustrator uses 72 px per inch, and Inkscape
+ used 90 px per inch prior to version 0.92.
+ """
+ string_to_parse = self.document.getroot().get(name)
+ if string_to_parse:
+ value, unit = units.parse_unit(string_to_parse)
+ if value is None:
+ return None
+ bad_units = {"%", "ex", "em"} # Unsupported units
+ if unit in bad_units:
+ return None
+
+ return units.convert_unit(string_to_parse, "in")
+ return None
+
+ def units_to_userunits(self, input_string):
+ """
+ Custom replacement for the old "unittouu" routine
+
+ Parse the attribute into a value and associated units.
+ Return value in user units (typically "px").
+ Importantly, return None for malformed inputs.
+ """
+
+ value, _ = units.parse_unit(input_string)
+ if value is None:
+ return None
+
+ return units.convert_unit(input_string, "")
+
+ def vb_scale(self, viewbox, p_a_r, doc_width, doc_height):
+ """
+ Parse SVG viewbox and generate scaling parameters.
+ Reference documentation: https://www.w3.org/TR/SVG11/coords.html
+
+ Inputs:
+ viewbox: Contents of SVG viewbox attribute
+ p_a_r: Contents of SVG preserveAspectRatio attribute
+ doc_width: Width of SVG document
+ doc_height: Height of SVG document
+
+ Output: s_x, s_y, o_x, o_y
+ Scale parameters (s_x, s_y) and offset parameters (o_x, o_y)
+
+ """
+ if viewbox is None:
+ return 1, 1, 0, 0 # No viewbox; return default transform
+ vb_array = viewbox.strip().replace(", ", " ").split()
+
+ if len(vb_array) < 4:
+ return 1, 1, 0, 0 # invalid viewbox; return default transform
+
+ min_x = float(vb_array[0]) # Viewbox offset: x
+ min_y = float(vb_array[1]) # Viewbox offset: y
+ width = float(vb_array[2]) # Viewbox width
+ height = float(vb_array[3]) # Viewbox height
+
+ if width <= 0 or height <= 0:
+ return 1, 1, 0, 0 # invalid viewbox; return default transform
+
+ if doc_width is None or doc_height is None:
+ raise AbortExtension(
+ _("Width or height attribute missing on toplevel <svg> tag")
+ )
+
+ d_width = float(doc_width)
+ d_height = float(doc_height)
+
+ if d_width <= 0 or d_height <= 0:
+ return 1, 1, 0, 0 # invalid document size; return default transform
+
+ ar_doc = d_height / d_width # Document aspect ratio
+ ar_vb = height / width # Viewbox aspect ratio
+
+ # Default values of the two preserveAspectRatio parameters:
+ par_align = "xmidymid" # "align" parameter(lowercased)
+ par_mos = "meet" # "meetOrSlice" parameter
+
+ if p_a_r is not None:
+ par_array = p_a_r.strip().replace(", ", " ").lower().split()
+ if len(par_array) > 0:
+ par0 = par_array[0]
+ if par0 == "defer":
+ if len(par_array) > 1:
+ par_align = par_array[1]
+ if len(par_array) > 2:
+ par_mos = par_array[2]
+ else:
+ par_align = par0
+ if len(par_array) > 1:
+ par_mos = par_array[1]
+
+ if par_align == "none":
+ # Scale document to fill page. Do not preserve aspect ratio.
+ # This is not default behavior, nor what happens if par_align
+ # is not given; the "none" value must be _explicitly_ specified.
+
+ s_x = d_width / width
+ s_y = d_height / height
+ o_x = -min_x
+ o_y = -min_y
+ return s_x, s_y, o_x, o_y
+
+ """
+ Other than "none", all situations fall into two classes:
+
+ 1) (ar_doc >= ar_vb AND par_mos == "meet")
+ or (ar_doc < ar_vb AND par_mos == "slice")
+ -> In these cases, scale document up until VB fills doc in X.
+
+ 2) All other cases, i.e.,
+ (ar_doc < ar_vb AND par_mos == "meet")
+ or (ar_doc >= ar_vb AND par_mos == "slice")
+ -> In these cases, scale document up until VB fills doc in Y.
+
+ Note in cases where the scaled viewbox exceeds the document
+ (page) boundaries (all "slice" cases and many "meet" cases where
+ an offset value is given) that this routine does not perform
+ any clipping, but subsequent clipping to the page boundary
+ is appropriate.
+
+ Besides "none", there are 9 possible values of par_align:
+ xminymin xmidymin xmaxymin
+ xminymid xmidymid xmaxymid
+ xminymax xmidymax xmaxymax
+ """
+
+ if ((ar_doc >= ar_vb) and (par_mos == "meet")) or (
+ (ar_doc < ar_vb) and (par_mos == "slice")
+ ):
+ # Case 1: Scale document up until VB fills doc in X.
+
+ s_x = d_width / width
+ s_y = s_x # Uniform aspect ratio
+ o_x = -min_x
+
+ scaled_vb_height = ar_doc * width
+ excess_height = scaled_vb_height - height
+
+ if par_align in {"xminymin", "xmidymin", "xmaxymin"}:
+ # Case: Y-Min: Align viewbox to minimum Y of the viewport.
+ o_y = -min_y
+ # OK: tested with Tall-Meet, Wide-Slice
+
+ elif par_align in {"xminymax", "xmidymax", "xmaxymax"}:
+ # Case: Y-Max: Align viewbox to maximum Y of the viewport.
+ o_y = -min_y + excess_height
+ # OK: tested with Tall-Meet, Wide-Slice
+
+ else: # par_align in {"xminymid", "xmidymid", "xmaxymid"}:
+ # Default case: Y-Mid: Center viewbox on page in Y
+ o_y = -min_y + excess_height / 2
+ # OK: Tested with Tall-Meet, Wide-Slice
+
+ return s_x, s_y, o_x, o_y
+
+ # Case 2: Scale document up until VB fills doc in Y.
+
+ s_y = d_height / height
+ s_x = s_y # Uniform aspect ratio
+ o_y = -min_y
+
+ scaled_vb_width = height / ar_doc
+ excess_width = scaled_vb_width - width
+
+ if par_align in {"xminymin", "xminymid", "xminymax"}:
+ # Case: X-Min: Align viewbox to minimum X of the viewport.
+ o_x = -min_x
+ # OK: Tested with Tall-Slice, Wide-Meet
+
+ elif par_align in {"xmaxymin", "xmaxymid", "xmaxymax"}:
+ # Case: X-Max: Align viewbox to maximum X of the viewport.
+ o_x = -min_x + excess_width
+ # Need test: Tall-Slice, Wide-Meet
+
+ else: # par_align in {"xmidymin", "xmidymid", "xmidymax"}:
+ # Default case: X-Mid: Center viewbox on page in X
+ o_x = -min_x + excess_width / 2
+ # OK: Tested with Tall-Slice, Wide-Meet
+
+ return s_x, s_y, o_x, o_y
+
+ def strip_quotes(self, fontname):
+ """
+ A multi-word font name may have a leading and trailing
+ single or double quotes, depending on the source.
+ If so, remove those quotes.
+ """
+
+ if fontname.startswith("'") and fontname.endswith("'"):
+ return fontname[1:-1]
+ if fontname.startswith('"') and fontname.endswith('"'):
+ return fontname[1:-1]
+ return fontname
+
+ def parse_svg_font(self, node_list):
+ """
+ Parse an input svg, searching for an SVG font. If an
+ SVG font is found, parse it and return a "digest" containing
+ structured information from the font. See below for more
+ about the digest format.
+
+ If the font is not found cannot be parsed, return none.
+
+ Notable limitations:
+
+ (1) This function only parses the first font face found within the
+ tree. We may, in the future, support discovering multiple fonts
+ within an SVG file.
+
+ (2) We are only processing left-to-right and horizontal text,
+ not vertical text nor RTL.
+
+ (3) This function currently performs only certain recursive searches,
+ within the <defs> element. It will not discover fonts nested within
+ groups or other elements. So far as we know, that is not a limitation
+ in practice. (If you have a counterexample please contact Evil Mad
+ Scientist tech support and let us know!)
+
+ (4) Kerning details are not implemented yet.
+ """
+
+ digest = None
+
+ if node_list is None:
+ return None
+
+ for node in node_list:
+ if isinstance(node, Defs):
+ return self.parse_svg_font(node) # Recursive call
+
+ if isinstance(node, SVGfont):
+ """
+ === Internal structure for storing font information ===
+
+ We parse the SVG font file and create a keyed "digest"
+ from it that we can use while rendering text on the page.
+
+ This "digest" will be added to a dictionary that maps
+ each font family name to a single digest.
+
+ The digest itself is a dictionary with the following
+ keys, some of which may have empty values. This format
+ will allow us to add additional keys at a later date,
+ to support additional SVG font features.
+
+ font_id (a string)
+
+ font_family (a string)
+
+ glyphs
+ A dictionary mapping unicode points to a specific
+ dictionary for each point. See below for more about
+ the key format.
+ The dictionary for a given point will include keys:
+ glyph_name(string)
+ horiz_adv_x(numeric)
+ d(string)
+
+ missing_glyph
+ A dictionary for a single code point, with keys:
+ horiz_adv_x(numeric)
+ d(string)
+
+ geometry
+ A dictionary containing geometric data
+ Keys will include:
+ horiz_adv_x(numeric) -- Default value
+ units_per_em(numeric)
+ ascent(numeric)
+ descent(numeric)
+ x_height(numeric)
+ cap_height(numeric)
+ bbox (string)
+ underline_position(numeric)
+ scale
+ A numeric scaling factor computed from the
+ units_per_em value, which gives the overall scale
+ """
+
+ digest = dict()
+ geometry = dict()
+ glyphs = dict()
+ missing_glyph = dict()
+
+ digest["font_id"] = node.get("id")
+
+ horiz_adv_x = node.get("horiz-adv-x")
+
+ if horiz_adv_x is not None:
+ geometry["horiz_adv_x"] = float(horiz_adv_x)
+ # Note: case of no horiz_adv_x value is not handled.
+
+ for element in node:
+
+ if isinstance(element, Glyph):
+ # First, because it is the most common element
+ try:
+ uni_text = element.get("unicode")
+ except:
+ # Can't use this point if no unicode mapping.
+ continue
+
+ if uni_text is None:
+ continue
+
+ if uni_text in glyphs:
+ # Skip if that unicode point is already in the
+ # list of glyphs.(There is not currently support
+ # for alternate glyphs in the font.)
+ continue
+
+ glyph_dict = dict()
+ glyph_dict["glyph_name"] = element.get("glyph-name")
+
+ horiz_adv_x = element.get("horiz-adv-x")
+
+ if horiz_adv_x is not None:
+ glyph_dict["horiz_adv_x"] = float(horiz_adv_x)
+ else:
+ glyph_dict["horiz_adv_x"] = geometry["horiz_adv_x"]
+
+ glyph_dict["d"] = element.get("d") # SVG path data
+ glyphs[uni_text] = glyph_dict
+
+ elif isinstance(element, FontFace):
+ digest["font_family"] = element.get("font-family")
+ units_per_em = element.get("units-per-em")
+
+ if units_per_em is None:
+ # Default: 1000, per SVG specification.
+ geometry["units_per_em"] = 1000.0
+ else:
+ geometry["units_per_em"] = float(units_per_em)
+
+ ascent = element.get("ascent")
+ if ascent is not None:
+ geometry["ascent"] = float(ascent)
+
+ descent = element.get("descent")
+ if descent is not None:
+ geometry["descent"] = float(descent)
+
+ """
+ # Skip these attributes that we are not currently using
+ geometry['x_height'] = element.get('x-height')
+ geometry['cap_height'] = element.get('cap-height')
+ geometry['bbox'] = element.get('bbox')
+ geometry['underline_position'] = element.get('underline-position')
+ """
+
+ elif isinstance(element, MissingGlyph):
+ horiz_adv_x = element.get("horiz-adv-x")
+
+ if horiz_adv_x is not None:
+ missing_glyph["horiz_adv_x"] = float(horiz_adv_x)
+ else:
+ missing_glyph["horiz_adv_x"] = geometry["horiz_adv_x"]
+
+ missing_glyph["d"] = element.get("d") # SVG path data
+ digest["missing_glyph"] = missing_glyph
+
+ # Main scaling factor
+ digest["scale"] = 1.0 / geometry["units_per_em"]
+
+ digest["glyphs"] = glyphs
+ digest["geometry"] = geometry
+
+ return digest
+ return None
+
+ def load_font(self, fontname):
+ """
+ Attempt to load an SVG font from a file in our list
+ of (likely) SVG font files.
+ If we can, add the contents to the font library.
+ Otherwise, add a "None" entry to the font library.
+ """
+
+ if fontname is None:
+ return
+
+ if fontname in self.font_dict:
+ return # Awesome: The font is already loaded.
+
+ if fontname in self.font_file_list:
+ the_path = self.font_file_list[fontname]
+ else:
+ self.font_dict[fontname] = None
+ return # Font not located.
+ try:
+ """
+ Check to see if there is an SVG font file for us to read.
+
+ At present, only one font file will be read per font family;
+ the name of the file must be FONT_NAME.svg, where FONT_NAME
+ is the name of the font family.
+
+ Only the first font found in the font file will be read.
+ Multiple weights and styles within a font family are not
+ presently supported.
+ """
+ font_svg = load_svg(the_path)
+ self.font_dict[fontname] = self.parse_svg_font(font_svg.getroot())
+
+ except IOError:
+ self.font_dict[fontname] = None
+ except:
+ inkex.errormsg(_("Error parsing SVG font at {}").format(str(the_path)))
+ self.font_dict[fontname] = None
+
+ def font_table(self):
+ """
+ Generate display table of all available SVG fonts
+ """
+
+ self.options.preserve_text = False
+
+ # Embed text in group to make manipulation easier:
+ group = self.svg.get_current_layer().add(Group())
+ for fontname in self.font_file_list:
+ self.load_font(fontname)
+
+ font_size = 0.2 # in inches -- will be scaled by viewbox factor.
+ font_size_text = str(font_size / self.vb_scale_factor) + "px"
+
+ labeltext_style = Style(
+ {
+ "stroke": "none",
+ "font-size": font_size_text,
+ "fill": "black",
+ "font-family": "sans-serif",
+ "text-anchor": "end",
+ }
+ )
+
+ x_offset = font_size / self.vb_scale_factor
+ y_offset = 1.5 * x_offset
+ y = y_offset
+
+ for fontname in sorted(self.font_dict):
+ if self.font_dict[fontname] is None:
+ continue # If the SVG file did NOT contain a font, skip it.
+
+ text_attribs = {"x": "0", "y": str(y), "hershey-ignore": "true"}
+ textline = group.add(TextElement(**text_attribs))
+ textline.text = fontname
+ textline.style = labeltext_style
+ text_attribs = {"x": str(x_offset), "y": str(y)}
+
+ sampletext_style = Style(
+ {
+ "stroke": "none",
+ "font-size": font_size_text,
+ "fill": "black",
+ "font-family": fontname,
+ "text-anchor": "start",
+ }
+ )
+ sampleline = group.add(TextElement(**text_attribs))
+
+ try: # python 2
+ sampleline.text = self.options.sample_text.decode("utf-8")
+ except AttributeError: # python 3
+ sampleline.text = self.options.sample_text
+
+ sampleline.style = sampletext_style
+ y += y_offset
+ self.recursively_traverse_svg(group, self.doc_transform)
+
+ def glyph_table(self):
+ """
+ Generate display table of glyphs within the current SVG font. Sorted display of
+ all printable characters in the font _except_ missing glyph.
+ """
+
+ self.options.preserve_text = False
+
+ fontname = self.font_load_wrapper("not_a_font_name") # force load of default
+
+ if self.font_load_fail:
+ inkex.errormsg(_("Font not found; Unable to generate glyph table."))
+ return
+
+ # Embed in group to make manipulation easier:
+ group = self.svg.get_current_layer().add(Group())
+
+ # missing_glyph = self.font_dict[fontname]['missing_glyph']
+
+ glyph_count = 0
+ for glyph in self.font_dict[fontname]["glyphs"]:
+ if self.font_dict[fontname]["glyphs"][glyph]["d"] is not None:
+ glyph_count += 1
+
+ columns = int(math.floor(math.sqrt(glyph_count)))
+
+ font_size = 0.4 # in inches -- will be scaled by viewbox factor.
+ font_size_text = str(font_size / self.vb_scale_factor) + "px"
+
+ glyph_style = Style(
+ {
+ "stroke": "none",
+ "font-size": font_size_text,
+ "fill": "black",
+ "font-family": fontname,
+ "text-anchor": "start",
+ }
+ )
+
+ x_offset = 1.5 * font_size / self.vb_scale_factor
+ y_offset = x_offset
+ x = x_offset
+ y = y_offset
+
+ draw_position = 0
+
+ for glyph in sorted(self.font_dict[fontname]["glyphs"]):
+ if self.font_dict[fontname]["glyphs"][glyph]["d"] is None:
+ continue
+ y_pos, x_pos = divmod(draw_position, columns)
+ x = x_offset * (x_pos + 1)
+ y = y_offset * (y_pos + 1)
+ text_attribs = {"x": str(x), "y": str(y)}
+ sampleline = group.add(TextElement(**text_attribs))
+ sampleline.text = glyph
+ sampleline.style = glyph_style
+ draw_position = draw_position + 1
+
+ self.recursively_traverse_svg(group, self.doc_transform)
+
+ def find_font_files(self):
+ """
+ Create list of "plausible" SVG font files
+
+ List items in primary svg_fonts directory, typically located in the
+ directory where this script is being executed from.
+
+ If there is text given in the "Other name/path" input, that text may
+ represent one of the following:
+
+ (A) The name of a font file, located in the svg_fonts directory.
+ - This may be given with or without the .svg suffix.
+ - If it is a font file, and the font face selected is "other",
+ then use this as the default font face.
+
+ (B) The path to a font file, located elsewhere.
+ - If it is a font file, and the font face selected is "other",
+ then use this as the default font face.
+ - ALSO: Search the directory where that file is located for
+ any other SVG fonts.
+
+ (C) The path to a directory
+ - It may or may not have a trailing separator
+ - Search that directory for SVG fonts.
+
+ This function will create a list of available files that
+ appear to be SVG(SVG font) files. It does not parse the files.
+ We will format it as a dictionary, that maps each file name
+ (without extension) to a path.
+ """
+
+ self.font_file_list = dict()
+
+ # List contents of primary font directory:
+ font_directory_name = "svg_fonts"
+
+ font_dir = os.path.realpath(os.path.join(os.getcwd(), font_directory_name))
+ for dir_item in os.listdir(font_dir):
+ if dir_item.endswith((".svg", ".SVG")):
+ file_path = os.path.join(font_dir, dir_item)
+ if os.path.isfile(file_path): # i.e., if not a directory
+ root, _ = os.path.splitext(dir_item)
+ self.font_file_list[root] = file_path
+
+ # split off file extension(e.g., ".svg")
+ root, _ = os.path.splitext(self.options.otherfont)
+
+ # Check for case "(A)": Input text is the name
+ # of an item in the primary font directory.
+ if root in self.font_file_list:
+ # If we already have that name in our font_file_list,
+ # and "other" is selected, this is now
+ # our default font face.
+ if self.options.fontface == "other":
+ self.options.fontface = root
+ return
+
+ test_path = os.path.realpath(self.options.otherfont)
+
+ # Check for case "(B)": A file, not in primary font directory
+ if os.path.isfile(test_path):
+ directory, file_name = os.path.split(test_path)
+ root, ext = os.path.splitext(file_name)
+ self.font_file_list[root] = test_path
+
+ if self.options.fontface == "other":
+ self.options.fontface = root
+
+ # Also search the directory where that file
+ # was located for other SVG files(which may be fonts)
+
+ for dir_item in os.listdir(directory):
+ if dir_item.endswith((".svg", ".SVG")):
+ file_path = os.path.join(directory, dir_item)
+ if os.path.isfile(file_path): # i.e., if not a directory
+ root, _ = os.path.splitext(dir_item)
+ self.font_file_list[root] = file_path
+ return
+
+ # Check for case "(C)": A directory name
+ if os.path.isdir(test_path):
+ for dir_item in os.listdir(test_path):
+ if dir_item.endswith((".svg", ".SVG")):
+ file_path = os.path.join(test_path, dir_item)
+ if os.path.isfile(file_path): # i.e., if not a directory
+ root, _ = os.path.splitext(dir_item)
+ self.font_file_list[root] = file_path
+
+ def font_load_wrapper(self, fontname):
+ """
+
+ This implements the following logic:
+
+ * Check to see if the font name is in our lookup table of fonts,
+ self.font_dict
+
+ * If the font is not listed in font_dict[]:
+ * Check to see if there is a corresponding SVG font file that
+ can be opened and parsed.
+
+ * If the font can be opened and parsed:
+ * Add that font to font_dict.
+ * Otherwise
+ * Add the font name to font_dict as None.
+
+ * If the font has value None in font_dict:
+ * Try to load fallback font.
+
+ * Fallback font:
+ * If an SVG font matching that in the SVG is not available,
+ check to see if the default font is available. That font
+ is given by self.options.fontface
+
+ * If a font is loaded and available, return the font name.
+ Otherwise, return none.
+
+ """
+
+ self.load_font(fontname) # Load the font if available
+
+ """
+ It *may* be worth building one stroke font (e.g., Hershey Sans 1-stroke) as a
+ variable defined in this file so that it can be used even if no external
+ SVG font files are available.
+ """
+
+ if self.font_dict[fontname] is None:
+
+ # If we were not able to load the requested font::
+ fontname = self.options.fontface # Fallback
+ if fontname not in self.font_dict:
+ self.load_font(fontname)
+ else:
+ pass
+
+ if self.font_dict[fontname] is None:
+ self.font_load_fail = (
+ True # Set a flag so that we only generate one copy of this error.
+ )
+ return None
+ return fontname
+
+ def get_font_char(self, fontname, char):
+ """
+ Given a font face name and a character(unicode point),
+ return an SVG path, horizontal advance value,
+ and scaling factor.
+
+ If the font is not available by name, use the default font.
+ """
+
+ fontname = self.font_load_wrapper(fontname) # Load the font if available
+
+ if fontname is None:
+ return None
+
+ try:
+ scale_factor = self.font_dict[fontname]["scale"]
+ except:
+ scale_factor = 0.001 # Default: 1/1000
+
+ try:
+ if char not in self.font_dict[fontname]["glyphs"]:
+ x_adv = self.font_dict[fontname]["missing_glyph"]["horiz_adv_x"]
+
+ return (
+ self.font_dict[fontname]["missing_glyph"]["d"],
+ x_adv,
+ scale_factor,
+ )
+ x_adv = self.font_dict[fontname]["glyphs"][char]["horiz_adv_x"]
+
+ return self.font_dict[fontname]["glyphs"][char]["d"], x_adv, scale_factor
+ except:
+ return None
+
+ def handle_viewbox(self):
+ """
+ Wrapper function for processing viewbox information
+ """
+
+ self.svg_height = self.getlength_inch("height")
+ self.svg_width = self.getlength_inch("width")
+
+ self.svg = self.document.getroot()
+ viewbox = self.svg.get("viewBox")
+ if viewbox:
+ p_a_r = self.svg.get("preserveAspectRatio")
+ s_x, s_y, o_x, o_y = self.vb_scale(
+ viewbox, p_a_r, self.svg_width, self.svg_height
+ )
+ else:
+ s_x = 1.0 / float(self.PX_PER_INCH) # Handle case of no viewbox
+ s_y = s_x
+ o_x = 0.0
+ o_y = 0.0
+
+ # Initial transform of document is based on viewbox, if present:
+ self.doc_transform = Transform(scale=(s_x, s_y), translate=(o_x, o_y))
+
+ self.vb_scale_factor = (s_x + s_y) / 2.0
+ # In case of non-square aspect ratio, use average value.
+
+ def draw_svg_text(self, chardata, parent):
+ """
+ Render an individual svg glyph
+ """
+ char = chardata["char"]
+ font_family = chardata["font_family"]
+ offset = chardata["offset"]
+ vertoffset = chardata["vertoffset"]
+ font_height = chardata["font_height"]
+ font_scale = 1.0
+
+ # Stroke scale factor, including external transformations:
+ stroke_scale = chardata["stroke_scale"] * self.vb_scale_factor
+
+ try:
+ path_string, adv_x, scale_factor = self.get_font_char(font_family, char)
+ except:
+ adv_x = 0
+ path_string = None
+ scale_factor = 1.0
+
+ if self.font_load_fail:
+ return 0
+
+ font_scale *= scale_factor * font_height
+
+ h_offset = 0
+ v_offset = 0
+
+ # SVG fonts use inverted Y axis; mirror vertically
+ scale_transform = Transform(scale=(font_scale, -font_scale))
+
+ # Combine scales of external transformations with the scaling
+ # applied by this function:
+ _scale = font_scale * stroke_scale
+ if _scale == 0:
+ _scale = 1
+ stroke_width = self.render_width / _scale
+
+ # Stroke-width is a css style element; cannot use scientific notation.
+ # Thus, use variable width for encoding the stroke width factor:
+
+ log_ten = math.log10(stroke_width)
+ if log_ten > 0: # For stroke_width > 1
+ width_string = "{0:.3f}in".format(stroke_width)
+ else:
+ prec = int(math.ceil(-log_ten) + 3)
+ width_string = "{0:.{1}f}in".format(stroke_width, prec)
+
+ p_style = {"stroke-width": width_string}
+
+ the_transform = Transform(translate=(offset + h_offset, vertoffset + v_offset))
+ the_transform @= scale_transform
+
+ if path_string is not None:
+ path_element = parent.add(PathElement())
+ path_element.set_path(path_string)
+ path_element.style = p_style
+ path_element.transform = the_transform
+ self.output_generated = True
+
+ return offset + float(adv_x) * font_scale # new horizontal offset value
+
+ def recursive_get_encl_transform(self, node):
+
+ """
+ Determine the cumulative transform which node inherits from
+ its chain of ancestors.
+ """
+ node = node.getparent()
+ if node is not None:
+ parent_transform = self.recursive_get_encl_transform(node)
+ node_transform = node.get("transform", None)
+ if node_transform is None:
+ return parent_transform
+ trans = Transform(node_transform).matrix
+
+ if parent_transform is None:
+ return trans
+ return Transform(parent_transform) * Transform(trans)
+ return self.doc_transform
+
+ def recursively_parse_flowroot(self, node_list, parent_info):
+ """
+ Parse a flowroot node and its children
+ """
+
+ # By default, inherit these values from parent:
+ font_height_local = parent_info["font_height"]
+ font_family_local = parent_info["font_family"]
+ line_spacing_local = parent_info["line_spacing"]
+ text_align_local = parent_info["align"]
+
+ for node in node_list:
+ node_style = node.style
+
+ font_height = node_style("font-size")
+ try:
+ font_height_local = self.units_to_userunits(font_height)
+ except TypeError:
+ pass
+
+ font_family_local = self.strip_quotes(node_style("font-family"))
+
+ try:
+ line_spacing = node_style("line-height")
+ if "%" in line_spacing: # Handle percentage line spacing(e.g., 125%)
+ line_spacing_local = float(line_spacing.rstrip("%")) / 100.0
+ elif line_spacing == "normal":
+ line_spacing_local = 1.25 # Inkscape default line spacing
+ else:
+ line_spacing_local = self.units_to_userunits(line_spacing)
+ except TypeError:
+ pass
+
+ text_align_local = node_style("text-align") # Use text-anchor in text nodes
+
+ if node.text is not None:
+ self.text_string += node.text
+
+ for _ in node.text:
+ self.text_families.append(font_family_local)
+ self.text_heights.append(font_height_local)
+ self.text_spacings.append(line_spacing_local)
+ self.text_aligns.append(text_align_local)
+
+ if isinstance(node, (FlowPara, FlowSpan)):
+ the_style = dict()
+ the_style["font_height"] = font_height_local
+ the_style["font_family"] = font_family_local
+ the_style["line_spacing"] = line_spacing_local
+ the_style["align"] = text_align_local
+
+ self.recursively_parse_flowroot(node, the_style)
+
+ if node.tail is not None:
+ # By default, inherit these values from parent:
+ font_height_local = parent_info["font_height"]
+ font_family_local = parent_info["font_family"]
+ line_spacing_local = parent_info["line_spacing"]
+
+ text_align_local = parent_info["align"]
+ self.text_string += node.tail
+ for _ in node.tail:
+ self.text_families.append(font_family_local)
+ self.text_heights.append(font_height_local)
+ self.text_spacings.append(line_spacing_local)
+ self.text_aligns.append(text_align_local)
+
+ if isinstance(node, FlowPara):
+ self.text_string += "\n" # Conclude every flowpara with a return
+ self.text_families.append(font_family_local)
+ self.text_heights.append(font_height_local)
+ self.text_spacings.append(line_spacing_local)
+ self.text_aligns.append(text_align_local)
+
+ def recursively_parse_text(self, node, parent_info):
+ """
+ parse a text node and its children
+ """
+
+ # By default, inherit these values from parent:
+ font_height_local = parent_info["font_height"]
+ font_family_local = parent_info["font_family"]
+ anchor_local = parent_info["anchor"]
+ x_local = parent_info["x_pos"]
+ y_local = parent_info["y_pos"]
+ parent_line_spacing = parent_info["line_spacing"]
+
+ node_style = node.style
+
+ font_height = node_style("font-size")
+ try:
+ font_height_local = self.units_to_userunits(font_height)
+ except TypeError:
+ pass
+
+ font_family_local = self.strip_quotes(node_style("font-family"))
+ anchor_local = node_style("text-anchor") # Use text-anchor in text nodes
+
+ try:
+ x_temp = node.get("x")
+ if x_temp is not None:
+ x_local = x_temp
+ except ValueError:
+ pass
+
+ try:
+ y_temp = node.get("y")
+ if y_temp is not None:
+ y_local = y_temp
+ else:
+ # Special case, to handle multi-line text given by tspan
+ # elements that do not have y values
+ if y_local is None:
+ y_local = 0
+ y_local = (
+ float(y_local)
+ + self.line_number * parent_line_spacing * font_height_local
+ )
+ except ValueError:
+ pass
+
+ if node.text is not None:
+ self.text_string += node.text
+
+ for _ in node.text:
+ self.text_families.append(font_family_local)
+ self.text_heights.append(font_height_local)
+ self.text_aligns.append(anchor_local)
+ self.text_x.append(x_local)
+ self.text_y.append(y_local)
+
+ for sub_node in node:
+ # If text is located within a sub_node of this node,
+ # process that sub_node, with this very routine.
+
+ if isinstance(sub_node, Tspan):
+ # Note: There may be additional types of text tags that
+ # we should recursively search as well.
+ node_info = dict()
+ node_info["font_height"] = font_height_local
+ node_info["font_family"] = font_family_local
+ node_info["anchor"] = anchor_local
+ node_info["x_pos"] = x_local
+ node_info["y_pos"] = y_local
+ node_info["line_spacing"] = parent_line_spacing
+
+ adv_line = False
+ role = sub_node.get("sodipodi:role")
+ if role == "line":
+ adv_line = True
+
+ self.recursively_parse_text(sub_node, node_info)
+
+ # Increment line after tspan if it is labeled as a line
+ if adv_line:
+ self.line_number = self.line_number + 1
+
+ if node.tail is not None:
+ _stripped_tail = node.tail.strip()
+ if _stripped_tail is not None:
+ # By default, inherit these values from parent:
+ font_height_local = parent_info["font_height"]
+ font_family_local = parent_info["font_family"]
+ text_align_local = parent_info["anchor"]
+ x_local = parent_info["x_pos"]
+ y_local = parent_info["y_pos"]
+ self.text_string += _stripped_tail
+ for _ in _stripped_tail:
+ self.text_heights.append(font_height_local)
+ self.text_families.append(font_family_local)
+ self.text_aligns.append(text_align_local)
+ self.text_x.append(x_local)
+ self.text_y.append(y_local)
+
+ def recursively_traverse_svg(
+ self,
+ anode_list,
+ mat_current=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
+ parent_visibility="visible",
+ ):
+ """
+ recursively parse the full document and its children,
+ looking for nodes that may contain text
+ """
+
+ for node in anode_list:
+
+ # Ignore invisible nodes
+ vis = node.get("visibility", parent_visibility)
+ if vis == "inherit":
+ vis = parent_visibility
+ if vis in ("hidden", "collapse"):
+ continue
+
+ # First apply the current matrix transform to this node's tranform
+ _matrix = node.transform
+ mat_new = Transform(mat_current) @ Transform(_matrix)
+
+ if isinstance(node, Group):
+
+ recurse_group = True
+ ink_label = node.get("inkscape:label")
+
+ if not ink_label:
+ pass
+ else:
+ if ink_label == "Hershey Text":
+ recurse_group = (
+ False # Do not traverse groups of rendered text.
+ )
+ if recurse_group:
+ self.recursively_traverse_svg(node, mat_new, vis)
+
+ elif isinstance(node, Use):
+ # A <use> element refers to another SVG element via an xlink:href="#blah"
+ # attribute. We will handle the element by doing an XPath search through
+ # the document, looking for the element with the matching id="blah"
+ # attribute. We then recursively process that element after applying
+ # any necessary(x, y) translation.
+ #
+ # Notes:
+ # 1. We ignore the height and width attributes as they do not apply to
+ # path-like elements, and
+ # 2. Even if the use element has visibility="hidden", SVG still calls
+ # for processing the referenced element. The referenced element is
+ # hidden only if its visibility is "inherit" or "hidden".
+
+ refnode = node.href
+ if refnode is None:
+ continue # missing reference
+
+ local_transform = Transform(_matrix)
+ x = float(node.get("x", "0"))
+ y = float(node.get("y", "0"))
+ # Note: the transform has already been applied
+ if (x != 0) or (y != 0):
+ _trans_string = "translate({0:.6E}, {1:.6E})".format(x, y)
+ ref_transform = Transform(_matrix) * Transform(_trans_string)
+ else:
+ ref_transform = local_transform
+
+ try:
+ ref_group = anode_list.add(Group()) # Add a subgroup
+ except AttributeError:
+ inkex.errormsg(
+ _("Unable to process text. Consider unlinking cloned text.")
+ )
+ continue
+
+ # Tests are not using the preset seed for this atm
+ # if 'id' not in ref_group.attrib:
+ # ref_group.set_random_id('')
+
+ ref_group.set("transform", ref_transform)
+
+ ref_group.append(deepcopy(refnode))
+
+ for sub_node in ref_group:
+ # The copied text elements should be removed at the end,
+ # or they will persist if original elements are preserved.
+ self.nodes_to_delete.append(sub_node)
+
+ # Preserve original element?
+ if not self.options.preserve_text:
+ self.nodes_to_delete.append(node)
+
+ elif isinstance(node, (TextElement, FlowRoot)):
+
+ # Flag for when we start a new line of text, for use with indents:
+ self.new_line = True
+
+ start_x = (
+ 0 # Defaults; Fail gracefully in case xy position is not given.
+ )
+ start_y = 0
+
+ # Default line spacing and font height: 125%, 16 px
+ line_spacing = self.units_to_userunits("1.25")
+ font_height = self.units_to_userunits("16px")
+
+ start_x = node.get("x") # XY Position of element
+ start_y = node.get("y")
+
+ bounding_rect = False
+ # rect_height = 100 #default size of bounding rectangle for flowroot object
+ rect_width = (
+ 100 # default size of bounding rectangle for flowroot object
+ )
+ transform = "" # transform(scale, translate, matrix, etc.)
+ text_align = "start"
+
+ try:
+ hershey_ignore = node.get("hershey-ignore")
+ if hershey_ignore is not None:
+ continue # If the attribute is present, skip this node.
+ except ValueError:
+ pass
+
+ node_style = node.style
+
+ try:
+ font_height_temp = node_style.get("font-size", 16)
+ font_height = self.units_to_userunits(font_height_temp)
+ except TypeError:
+ pass
+
+ font_family = self.strip_quotes(node_style("font-family"))
+
+ try:
+ line_spacing_temp = node_style("line-height")
+ if (
+ "%" in line_spacing_temp
+ ): # Handle percentage line spacing(e.g., 125%)
+ line_spacing = float(line_spacing_temp.rstrip("%")) / 100.0
+ elif line_spacing_temp == "normal":
+ line_spacing = 1.25 # Inkscape default line spacing
+ else:
+ line_spacing = self.units_to_userunits(line_spacing_temp)
+ except TypeError:
+ pass
+
+ try:
+ transform = node.transform
+ except ValueError:
+ pass
+
+ if transform is not None:
+ transform2 = Transform(transform).matrix
+
+ """
+ Compute estimate of transformation scale applied to
+ this element, for purposes of calculating the
+ stroke width to apply. When all transforms are applied
+ and our elements are displayed on the page, we want the
+ final visible stroke width to be reasonable.
+ Transformation matrix is [[a c e][b d f]]
+ scale_x = sqrt(a * a + b * b),
+ scale_y = sqrt(c * c + d * d)
+ Take estimated scale as the mean of the two.
+ """
+
+ scale_x = math.sqrt(
+ transform2[0][0] * transform2[0][0]
+ + transform2[1][0] * transform2[1][0]
+ )
+ scale_y = math.sqrt(
+ transform2[0][1] * transform2[0][1]
+ + transform2[1][1] * transform2[1][1]
+ )
+
+ scale_r = (scale_x + scale_y) / 2.0 # Average. ¯\_(ツ)_/¯
+ else:
+ scale_r = 1.0
+
+ the_id = node.get("id")
+
+ # Initialize text attribute lists for each top-level text object:
+ self.text_string = ""
+ self.text_families = (
+ []
+ ) # Lis of font family for characters in the string
+ self.text_heights = [] # List of font heights
+ self.text_spacings = [] # List of vertical line heights
+ self.text_aligns = [] # List of horizontal alignment values
+ self.text_x = [] # List; x-coordinate of text line start
+ self.text_y = [] # List; y-coordinate of text line start
+
+ # Group generated paths together, to make the rendered letters
+ # easier to manipulate in Inkscape once generated:
+ parent = node.getparent()
+
+ group = parent.add(Group())
+ group.label = "Hershey Text"
+
+ style = {
+ "stroke": "#000000",
+ "fill": "none",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ }
+
+ # Apply rounding to ends to improve final engraved text appearance.
+ group.style = style
+ # Some common variables used in both cases A and B:
+ str_pos = 0 # Position through the full string that we are rendering
+ i = 0 # Dummy(index) variable for looping over letters in string
+ w = 0 # Initial spacing offset
+ w_temp = 0 # Temporary variable for horizontal spacing offset
+ width_this_line = (
+ 0 # Estimated width of characters to be stored on this line
+ )
+
+ """
+ CASE A: Handle flowed text nodes
+ """
+
+ if isinstance(node, FlowRoot):
+
+ try:
+ text_align = node_style["text-align"]
+ # Use text-align, not text-anchor, in flowroot
+ except KeyError:
+ pass
+
+ # selects the flowRegion's child(svg:rect) to get @X and @Y
+ flowref = self.svg.getElement(
+ '/svg:svg//*[@id="%s"]/svg:flowRegion[1]' % the_id
+ )[0]
+
+ if isinstance(flowref, Rectangle):
+ start_x = flowref.left
+ start_y = flowref.top
+ rect_height = flowref.height
+ rect_width = flowref.width
+ bounding_rect = True
+
+ elif isinstance(flowref, Use):
+ pass
+
+ # A <use> element refers to another SVG element via an xlink:href="#blah"
+ # attribute. We will handle the element by doing an XPath search through
+ # the document, looking for the element with the matching id="blah"
+ # attribute. We then recursively process that element after applying
+ # any necessary(x, y) translation.
+ #
+ # Notes:
+ # 1. We ignore the height and width attributes as they do not apply to
+ # path-like elements, and
+ # 2. Even if the use element has visibility="hidden", SVG still calls
+ # for processing the referenced element. The referenced element is
+ # hidden only if its visibility is "inherit" or "hidden".
+ # 3. We may be able to unlink clones using the code in pathmodifier.py
+
+ # The following code can render text flowed into a rectangle object.
+ # HOWEVER, it does not handle the various transformations that could occur
+ # be present on those objects, and does not handle more general cases, such
+ # as a rotated rectangle -- for which text *should* flow in a diamond shape.
+ # For the time being, we skip these and issue a warning.
+ #
+ # refid = flowref.get('xlink:href')
+ # if refid is not None:
+ # # [1:] to ignore leading '#' in reference
+ # path = '//*[@id="%s"]' % refid[1:]
+ # refnode = flowref.xpath(path)
+ # if refnode is not None:
+ # refnode = refnode[0]
+ # if isinstance(refnode, Rectangle):
+ # start_x = refnode.get('x")
+ # start_y = refnode.get('y")
+ # rect_height = refnode.get('height")
+ # rect_width = refnode.get('width")
+ # bounding_rect = True
+
+ if not bounding_rect:
+ self.warn_unflow = True
+ continue
+
+ """
+ Recursively loop through content of the flowroot object,
+ looping through text, flowpara, and other things.
+
+ Create multiple lists: One of text content,
+ others of style that should be applied to that content.
+
+ then, loop through those lists, one line at a time,
+ finding how many words fit on a line, etc.
+ """
+
+ the_style = dict()
+ the_style["font_height"] = font_height
+ the_style["font_family"] = font_family
+ the_style["line_spacing"] = line_spacing
+ the_style["align"] = text_align
+
+ self.recursively_parse_flowroot(node, the_style)
+
+ if self.text_string == "":
+ continue # No convertable text in this SVG element.
+
+ if self.text_string.isspace():
+ continue # No convertable text in this SVG element.
+
+ # Initial vertical offset for the flowed text block:
+ v = 0
+
+ # Record that we are on the first line of the paragraph
+ # for setting the v position of the first line.
+ first_line = True
+
+ # Keep track of text height on first line, for moving entire text box:
+ y_offs_overall = 0
+
+ # Split text by lines AND make a list of how long each
+ # line is, including the newline characters.
+ # We need to keep track of this to match up styling
+ # information to the printable characters.
+
+ text_lines = self.text_string.splitlines()
+ extd_text_lines = self.text_string.splitlines(True)
+ str_pos_eol = 0 # str_pos after end of previous text_line.
+
+ nbsp = "\xa0" # Unicode non-breaking space character
+
+ for line_number, text_line in enumerate(text_lines):
+
+ line_length = len(text_line)
+ extd_line_length = len(extd_text_lines[line_number])
+
+ i = 0 # Position within this text_line.
+
+ # A given text_line may take more than one strip
+ # to render, if it overflows our box width.
+
+ line_start = 0 # Value of i when the current strip started.
+
+ if line_length == 0:
+ str_pos_temp = str_pos_eol
+ char_height = float(self.text_heights[str_pos_temp])
+ charline_spacing = float(self.text_spacings[str_pos_temp])
+ char_v_spacing = charline_spacing * char_height
+ v = v + char_v_spacing
+ else:
+ while i < line_length:
+
+ word_start = (
+ i # Value of i at beginning of the current word.
+ )
+
+ while i < line_length: # Step through the line
+ # until we reach the end of the line or word.
+ # (i.e., until we reach whitespace)
+ character = text_line[
+ i
+ ] # character is unicode(not byte string)
+ str_pos_temp = str_pos_eol + i
+
+ char_height = self.text_heights[str_pos_temp]
+ char_family = self.text_families[str_pos_temp]
+
+ try:
+ _, x_adv, scale_factor = self.get_font_char(
+ char_family, character
+ )
+ except:
+ x_adv = 0
+ scale_factor = 1
+
+ w_temp += x_adv * scale_factor * char_height
+
+ i += 1
+ if character.isspace() and not character == nbsp:
+ break # Break at space, except non-breaking
+
+ render_line = False
+ if (
+ w_temp > rect_width
+ ): # If the word will overflow the box
+ if word_start == line_start:
+ # This is the first word in the strip, so this
+ # word(alone) is wider than the box. Render it.
+ render_line = True
+ else: # Not the first word in the strip.
+ # Render the line up UNTIL this word.
+ render_line = True
+ i = word_start
+ elif i >= line_length:
+ # Render at end of text_line, if not overflowing.
+ render_line = True
+
+ if render_line:
+ # Create group for rendering a strip of text:
+ line_group = group.add(Group())
+
+ w_temp = 0
+ w = 0
+
+ self.new_line = True
+ width_this_line = 0
+ line_max_v_spacing = 0
+
+ j = line_start
+
+ while j < i: # Calculate max height for the strip:
+ str_pos_temp = str_pos_eol + j
+ char_height = float(
+ self.text_heights[str_pos_temp]
+ )
+ charline_spacing = float(
+ self.text_spacings[str_pos_temp]
+ )
+ char_v_spacing = charline_spacing * char_height
+ if char_v_spacing > line_max_v_spacing:
+ line_max_v_spacing = char_v_spacing
+ j = j + 1
+
+ v = v + line_max_v_spacing
+
+ char_data = dict()
+ char_data["vertoffset"] = v
+ char_data["stroke_scale"] = scale_r
+
+ j = line_start
+ while j < i: # Render the strip on the page
+ str_pos = str_pos_eol + j
+
+ char_height = self.text_heights[str_pos]
+ char_family = self.text_families[str_pos]
+ text_align = self.text_aligns[str_pos]
+
+ char_data["char"] = text_line[j]
+ char_data["font_height"] = char_height
+ char_data["font_family"] = char_family
+ char_data["offset"] = w
+
+ w = self.draw_svg_text(char_data, line_group)
+
+ width_this_line = w
+
+ j = j + 1
+ str_pos = str_pos + 1
+
+ line_start = i
+
+ # Alignment for the strip:
+
+ the_transform = None
+ if text_align == "center": # when using text-align
+ the_transform = Transform(
+ translate=(
+ (float(rect_width) - width_this_line)
+ / 2
+ )
+ )
+ elif text_align == "end":
+ the_transform = Transform(
+ translate=(
+ float(rect_width) - width_this_line
+ )
+ )
+ if the_transform is not None:
+ line_group.transform = the_transform
+
+ if first_line:
+ y_offs_overall = (
+ line_max_v_spacing / 3
+ ) # Heuristic
+ first_line = False
+
+ str_pos_eol = str_pos_eol + extd_line_length
+ str_pos = str_pos_eol
+
+ the_transform = Transform(
+ translate=(start_x, float(start_y) - y_offs_overall)
+ )
+
+ else: # If this is a text object, rather than a flowroot object:
+ """
+ CASE B: Handle regular(non-flowroot) text nodes
+ """
+
+ # Use text-anchor, not text-align, in text(not flowroot) elements
+ text_align = node_style("text-anchor")
+
+ """
+ Recursively loop through content of the text object,
+ looping through text, tspan, and other things as necessary.
+ (A recursive search since style elements may be nested.)
+
+ Create multiple lists: One of text content, others of the
+ style that should be applied to that content.
+
+ For each line, want to record the plain text, font size
+ per character, text alignment, and x, y start values
+ for that line)
+
+ (We may need to eventually handle additional text types and
+ tags, as when importing from other SVG sources. We should
+ try to eventually support additional formulations
+ of x, y, dx, dy, etc.
+ https://www.w3.org/TR/SVG/text.html#TSpanElement)
+
+ then, loop through those lists, one line at a time,
+ rendering text onto lines. If the x or y values changed,
+ assume we've started a new line.
+
+ Note: A text element creates a single line
+ of text; it does not create multiline text by including
+ line returns within the text itself. Multiple lines of text
+ are created with multiple text or tspan elements.
+ """
+
+ node_info = dict()
+ node_info["font_height"] = font_height
+ node_info["font_family"] = font_family
+ node_info["anchor"] = text_align
+ node_info["x_pos"] = start_x
+ node_info["y_pos"] = start_y
+ node_info["line_spacing"] = line_spacing
+
+ # Keep track of line number. Used in cases where daughter
+ # tspan elements do not have Y positions given.
+ # Reset to zero on each text element.
+ self.line_number = 0
+
+ self.recursively_parse_text(node, node_info)
+ # self.recursively_parse_text(node, font_height, text_align, start_x, start_y)
+
+ if self.text_string == "":
+ continue # No convertable text in this SVG element.
+ if self.text_string.isspace():
+ continue # No convertable text in this SVG element.
+
+ letter_vals = [q for q in self.text_string]
+ str_len = len(letter_vals)
+
+ # Use a group for each line. This starts the first:
+ line_group = group.add(Group())
+
+ i = 0
+ while i < str_len: # Loop through the entire text of the string.
+
+ try:
+ x_start_line = float(self.text_x[i]) # Start new line here.
+ except ValueError:
+ self.warn_unkern = True
+ return
+
+ y_start_line = float(self.text_y[i])
+
+ while i < str_len:
+ # Inner while loop, that we will break out of,
+ # back to the outer while loop.
+
+ q_val = letter_vals[i]
+ charfont_height = self.text_heights[i]
+
+ char_data = dict()
+ char_data["char"] = q_val
+ char_data["font_family"] = self.text_families[i]
+
+ char_data["font_height"] = charfont_height
+ char_data["offset"] = w
+ char_data["vertoffset"] = 0
+ char_data["stroke_scale"] = scale_r
+
+ w = self.draw_svg_text(char_data, line_group)
+ width_this_line = w
+ w_temp = w
+
+ # Set the alignment if(A) this is the last character in the string
+ # or if the next piece of the string is at a different position
+
+ set_alignment = False
+ i_next = i + 1
+ if i_next >= str_len: # End of the string; last character.
+ set_alignment = True
+ elif (float(self.text_x[i_next]) != x_start_line) or (
+ float(self.text_y[i_next]) != y_start_line
+ ):
+ set_alignment = True
+
+ if set_alignment:
+ text_align = self.text_aligns[i]
+ # Not currently supporting text alignment that changes in the span;
+ # Use the text alignment as of the last character.
+
+ # Left(or "start") alignment is default.
+ # if(text_align == "middle"): Center alignment
+ # if(text_align == "end"): Right alignment
+ #
+ # Strategy: Align every row (left, center, or right)
+ # as it is created.
+
+ x_shift = 0
+ if text_align == "middle": # when using text-anchor
+ x_shift = x_start_line - (width_this_line / 2)
+ elif text_align == "end":
+ x_shift = x_start_line - width_this_line
+ else:
+ x_shift = x_start_line
+
+ y_shift = y_start_line
+
+ the_transform = Transform(translate=(x_shift, y_shift))
+
+ line_group.transform = the_transform
+
+ line_group = group.add(
+ Group()
+ ) # Create new group for this line
+
+ self.new_line = True # Used for managing indent defects
+ w = 0
+ i += 1
+ break
+ i += 1 # Only executed when set_alignment is false.
+
+ the_transform = Transform()
+
+ if len(line_group) == 0:
+ parent = line_group.getparent()
+ parent.remove(line_group)
+
+ # End cases A & B. Apply transform to text/flowroot object:
+
+ if transform is not None:
+ result = Transform(transform) @ the_transform
+ else:
+ result = the_transform
+
+ group.transform = result
+
+ if not self.output_generated:
+ parent = group.getparent()
+ parent.remove(group) # remove empty group
+
+ # Preserve original element?
+ if not self.options.preserve_text and self.output_generated:
+ self.nodes_to_delete.append(node)
+
+ def effect(self):
+ """
+ Main entry point; Execute the extension's function.
+ """
+
+ # Input sanitization:
+ self.options.mode = self.options.mode.strip('"')
+ self.options.fontface = self.options.fontface.strip('"')
+ self.options.otherfont = self.options.otherfont.strip('"')
+ self.options.util_mode = self.options.util_mode.strip('"')
+ self.options.sample_text = self.options.sample_text.strip('"')
+
+ self.doc_transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+
+ self.find_font_files()
+
+ self.handle_viewbox()
+
+ # Calculate "ideal" effective width of rendered strokes:
+ # Default: 1/800 of page width or height, whichever is smaller
+
+ _rendered_stroke_scale = 1 / (self.PX_PER_INCH * 800.0)
+
+ if self.svg_width is not None:
+ if self.svg_width < self.svg_height:
+ self.render_width = self.svg_width * _rendered_stroke_scale
+ else:
+ self.render_width = self.svg_height * _rendered_stroke_scale
+
+ if self.options.mode == "help":
+ inkex.errormsg(self.help_text)
+ elif self.options.mode == "utilities":
+
+ if self.options.util_mode == "sample":
+ self.font_table()
+ else:
+ self.glyph_table()
+ else:
+ if self.options.ids:
+ # Traverse selected objects
+ for id_ref in self.options.ids:
+ transform = self.recursive_get_encl_transform(
+ self.svg.selection[id_ref]
+ )
+ self.recursively_traverse_svg(
+ [self.svg.selection[id_ref]], transform
+ )
+ else: # Traverse entire document
+ self.recursively_traverse_svg(
+ self.document.getroot(), self.doc_transform
+ )
+
+ for element_to_remove in self.nodes_to_delete:
+ if element_to_remove is not None:
+ parent = element_to_remove.getparent()
+ if parent is not None:
+ parent.remove(element_to_remove)
+
+ if self.font_load_fail:
+ inkex.errormsg(_("Warning: unable to load SVG stroke fonts."))
+
+ if self.warn_unkern:
+ inkex.errormsg(
+ _(
+ "Warning: unable to render text.\n"
+ + "Please use Text > Remove Manual Kerns to convert it prior to use ."
+ )
+ )
+
+ if self.warn_unflow:
+ inkex.errormsg(
+ _(
+ "Warning: unable to convert text flowed into a frame.\n"
+ + "Please use Text > Unflow to convert it prior to use.\n"
+ + "If you are unable to identify the object in question, "
+ + "please contact technical support for help."
+ )
+ )
+
+
+if __name__ == "__main__":
+ Hershey().run()