summaryrefslogtreecommitdiff
path: root/lib/extensions/lettering_svg_font_to_layers.py
blob: 17e62d54f0c29cf47f2a20f93ea11b00413455c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python3
# coding=utf-8
#
# Copyright (C) 2011 Felipe Correa da Silva Sanches <juca@members.fsf.org>
#
# 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.
#
#
# Adapted for the inkstitch lettering module to allow glyph annotations for characters
# in specific positions or settings, also allows scaling.Changes: see git history
"""Extension for converting svg fonts to layers"""

from inkex import Layer, PathElement, errormsg

from .base import InkstitchExtension

from ..svg import PIXELS_PER_MM


class LetteringSvgFontToLayers(InkstitchExtension):
    """
    An Ink/Stitch extension for converting SVG font glyphs into separate layers.
    This extension processes an SVG font embedded in an SVG document, scaling and converting each glyph into its own layer for
    further manipulation or embroidery digitization. It provides options to control the number of glyphs processed,
    the reference character for scaling, and the desired reference height in millimeters.
    Attributes:
        None explicitly defined.
    Methods:
        add_arguments(pars):
            Adds command-line arguments for glyph count, reference character, and reference height.
        scale_hkerning(scale_by):
        flip_cordinate_system(elem, emsize, baseline, scale_by):
            Scales and translates the element's path to match the desired coordinate system.
        reference_size(font, reference):
        set_view_port(font, scale_by):
            Sets the SVG viewport size and viewBox based on the scaled em size, return the width (= height) of the viewport
        set_guide_lines(font, scale_by):
            Adds guide lines for baseline, ascender, caps, x-height, and descender to the SVG. Return the baseline position
        convert_glyph_to_layer(glyph, emsize, baseline, scale_by, hide_layer):
            Converts a single glyph into a new SVG layer, applying scaling and coordinate transformation.
        effect():
            Main entry point for the extension. Processes the SVG font, scales glyphs, creates layers, and applies kerning scaling.

    Usage:
        This extension is intended to be used within the Ink/Stitch environment as a command-line or GUI extension for converting
        SVG font glyphs into layers suitable for embroidery design.
    """

    def add_arguments(self, pars) -> None:
        """ Adds command-line arguments for glyph count, reference character, and reference height.
        """
        pars.add_argument(
            "--reference",
            type=str,
            default="M",
            help="Reference character for size"
        )
        pars.add_argument(
            "--height",
            type=float,
            default=20.0,
            help="Reference character height in mm"
        )

    def scale_hkerning(self, scale_by) -> None:
        """
         Scales all horizontal kerning (hkern) values in the font by the given factor.
        """
        font = self.svg.defs.findone("svg:font")
        for hkern in font.findall("svg:hkern"):
            k = hkern.get("k", None)
            if k is not None:
                k = round(float(k) * scale_by, 2)
                hkern.set(("k"), str(k))

    def flip_cordinate_system(self, elem, emsize, baseline, scale_by):
        """
        Scale and translate the element's path, returns the path object
        """
        path = elem.path
        path.scale(scale_by, -scale_by, inplace=True)
        path.translate(0, float(emsize) - float(baseline), inplace=True)
        return path

    def reference_size(self, font, reference):
        """
        Returns the height of the reference glyph used for scaling.
        """
        for glyph in font.findall("svg:glyph"):
            if glyph.get("glyph-name") == str(reference):
                path = glyph.path
                return path.bounding_box().height
                break

    def set_view_port(self, font, scale_by) -> float:
        """
        Sets the SVG viewport size and viewBox based on the scaled em size, return the width (= height) of the viewport
        """
        fontface = font.findone("svg:font-face")

        emsize = fontface.get("units-per-em")
        emsize = round(float(emsize) * scale_by, 2)
        fontface.set("units-per-em", str(emsize))

        self.svg.set("width", str(emsize))
        self.svg.set("height", str(emsize))
        self.svg.set("viewBox", "0 0 " + str(emsize) + " " + str(emsize))
        return emsize

    def set_guide_lines(self, font, scale_by) -> float:
        """
        Adds guide lines for baseline, ascender, caps, x-height, and descender to the SVG. Return the baseline position_
        """

        baseline = font.get("horiz-origin-y")
        horiz_adv_x = round(float(font.get("horiz-adv-x", 0)) * scale_by, 2)
        font.set("horiz-adv-x", str(horiz_adv_x))

        fontface = font.findone("svg:font-face")
        if baseline is None:
            baseline = 0
        else:
            baseline = float(baseline) * scale_by

        guidebase = (self.svg.viewbox_height) - float(baseline)

        caps = round(float(fontface.get("cap-height", 0)) * scale_by, 2)
        xheight = round(float(fontface.get("x-height", 0)) * scale_by, 2)
        ascender = round(float(fontface.get("ascent", 0)) * scale_by, 2)
        descender = round(float(fontface.get("descent", 0)) * scale_by, 2)

        fontface.set("cap-height", str(caps))
        fontface.set("x-height", str(xheight))
        fontface.set("ascent", str(ascender))
        fontface.set("descent", str(descender))

        self.svg.namedview.add_guide(guidebase, True, "baseline")
        self.svg.namedview.add_guide(guidebase - ascender, True, "ascender")
        self.svg.namedview.add_guide(guidebase - caps, True, "caps")
        self.svg.namedview.add_guide(guidebase - xheight, True, "xheight")
        self.svg.namedview.add_guide(guidebase - descender, True, "decender")

        return baseline

    def convert_glyph_to_layer(self, glyph, emsize, baseline, scale_by, hide_layer):  # noqa C901
        """
        Converts a single glyph into a new SVG layer, applying scaling and coordinate transformation.
        """
        unicode_char = glyph.get("unicode")

        glyph_name = glyph.get("glyph-name").split('.')
        # if we are in the PUA zone, then we need to find the information in the glyph_name
        if unicode_char is None or ord(unicode_char[0]) >= 57344:
            if len(glyph_name) == 2:
                unicode_char = glyph_name[0]
            else:
                return

        typographic_feature = ''
        if len(glyph_name) == 2:
            typographic_feature = glyph_name[1]
        else:
            arabic_form = glyph.get('arabic-form', None)
            if arabic_form is not None and len(arabic_form) > 4:
                typographic_feature = arabic_form[:4]
        if typographic_feature:
            typographic_feature = f".{typographic_feature}"

        layer = self.svg.add(Layer.new(f"GlyphLayer-{unicode_char}{typographic_feature}"))

        # glyph layers (except the first one) are initially hidden
        if hide_layer:
            layer.style["display"] = "none"

        # Using curve description in d attribute of svg:glyph
        path = layer.add(PathElement())
        path.path = self.flip_cordinate_system(glyph, emsize, baseline, scale_by)

        unicode_char = glyph.get("unicode")
        if unicode_char is None:

            glyph_name = glyph.get("glyph-name").split('.')
            if unicode_char is None:
                if len(glyph_name) == 2:
                    unicode_char = glyph_name[0]
                else:
                    return

            typographic_feature = ''
            if len(glyph_name) == 2:
                typographic_feature = glyph_name[1]
            else:
                arabic_form = glyph.get('arabic-form', None)
                if arabic_form is not None and len(arabic_form) > 4:
                    typographic_feature = arabic_form[:4]
            if typographic_feature:
                typographic_feature = f".{typographic_feature}"

            layer = self.svg.add(Layer.new(f"GlyphLayer-{unicode_char}{typographic_feature}"))

            # glyph layers (except the first one) are initially hidden
            if hide_layer:
                layer.style["display"] = "none"

            # Using curve description in d attribute of svg:glyph
            path = layer.add(PathElement())
            path.path = self.flip_cordinate_system(glyph, emsize, baseline, scale_by)

    def effect(self) -> None:
        # Current code only reads the first svgfont instance
        font = self.svg.defs.findone("svg:font")
        if font is None:
            return errormsg("There are no svg fonts")

        reference_size = self.reference_size(font, self.options.reference)
        if reference_size is None:
            return errormsg("Reference glyph not found in the font")

        scale_by = self.options.height * PIXELS_PER_MM / reference_size
        emsize = self.set_view_port(font, scale_by)
        baseline = self.set_guide_lines(font, scale_by)

        count = 0
        for glyph in font.findall("svg:glyph"):
            hide_layer = count != 0
            hax = glyph.get("horiz-adv-x", None)
            if hax is not None:
                hax = round(float(hax) * scale_by, 2)
                glyph.set(("horiz-adv-x"), str(hax))
            self.convert_glyph_to_layer(glyph, emsize, baseline, scale_by, hide_layer=hide_layer)
            count += 1
        self.scale_hkerning(scale_by)


if __name__ == "__main__":
    LetteringSvgFontToLayers().run()