def normalize_flag_aspect(svg, viewbox_size, width, height, right_margin, top_margin): apply_viewbox_preserve_aspect_ratio(svg) current_box = _bbox(tuple(s.bounding_box() for s in svg.shapes())) # Try to keep overall proportions for the flags that are considerably # narrower or wider than the standard aspect ratio. aspect = current_box.w / current_box.h aspect /= STD_ASPECT aspect = sqrt(aspect) # Discount the effect if 0.9 <= aspect <= 1.1: aspect = 1.0 else: print("Non-standard aspect ratio:", aspect) xmin = _x_aspect(right_margin, aspect, viewbox_size) ymin = _y_aspect(top_margin, aspect, viewbox_size) xmax = _x_aspect(right_margin + width, aspect, viewbox_size) ymax = _y_aspect(top_margin + height, aspect, viewbox_size) new_box = Rect(xmin, ymin, xmax - xmin, ymax - ymin) affine = Affine2D.rect_to_rect(current_box, new_box) _picosvg_transform(svg, affine) square_viewbox = Rect(0, 0, viewbox_size, viewbox_size) svg.svg_root.attrib["viewBox"] = " ".join(ntos(v) for v in square_viewbox) for attr_name in ("width", "height"): if attr_name in svg.svg_root.attrib: del svg.svg_root.attrib[attr_name]
def rect_to_rect(cls, src: Rect, dst: Rect) -> "Affine2D": """ Return Affine2D set to scale and translate src Rect to dst Rect. The mapping completely fills dst, it does not preserve aspect ratio. """ if src.empty(): return cls.identity() if dst.empty(): return cls(0, 0, 0, 0, 0, 0) sx = dst.w / src.w sy = dst.h / src.h tx = dst.x - src.x * sx ty = dst.y - src.y * sy return cls(sx, 0, 0, sy, tx, ty)
def apply_viewbox_preserve_aspect_ratio(svg): """If viewport != viewBox apply the resulting transform and remove viewBox. Takes 'preserveAspectRatio' into account. E.g. The Qatar flag (QA.svg) needs this treatment. """ svg_root = svg.svg_root width = svg_root.attrib.get("width") height = svg_root.attrib.get("height") if width is not None and height is not None and "viewBox" in svg_root.attrib: # ignore absolute length units; we're only interested in the relative size # of viewport vs viewbox here width = SVG_UNITS_RE.sub("", width) height = SVG_UNITS_RE.sub("", height) viewport = Rect(0, 0, float(width), float(height)) viewbox = svg.view_box() if viewport != viewbox: transform = Affine2D.rect_to_rect( viewbox, viewport, svg_root.attrib.get("preserveAspectRatio", "xMidYMid"), ) _picosvg_transform(svg, transform) del svg_root.attrib["viewBox"] if "preserveAspectRatio" in svg_root.attrib: del svg_root.attrib["preserveAspectRatio"]
def rect_to_rect( cls, src: Rect, dst: Rect, preserveAspectRatio: str = "none", ) -> "Affine2D": """Return Affine2D set to scale and translate src Rect to dst Rect. By default the mapping completely fills dst, it does not preserve aspect ratio, unless the 'preserveAspectRatio' argument is used. See https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute for the list of values supported. """ if src.empty(): return cls.identity() if dst.empty(): return cls(0, 0, 0, 0, 0, 0) # We follow the same process described in the SVG spec for computing the # equivalent scale + translation which maps from viewBox (src) to viewport (dst) # coordinates given the value of preserveAspectRatio. # https://www.w3.org/TR/SVG/coords.html#ComputingAViewportsTransform sx = dst.w / src.w sy = dst.h / src.h align, _, meetOrSlice = preserveAspectRatio.lower().strip().partition( " ") if (align not in cls._ALIGN_VALUES or meetOrSlice and meetOrSlice not in cls._MEET_OR_SLICE): raise ValueError( f"Invalid preserveAspectRatio: {preserveAspectRatio!r}") if align != "none": sx = sy = max(sx, sy) if "slice" in meetOrSlice else min(sx, sy) tx = dst.x - src.x * sx ty = dst.y - src.y * sy if "xmid" in align: tx += (dst.w - src.w * sx) / 2 elif "xmax" in align: tx += dst.w - src.w * sx if "ymid" in align: ty += (dst.h - src.h * sy) / 2 elif "ymax" in align: ty += dst.h - src.h * sy return cls(sx, 0, 0, sy, tx, ty)
def update_symbol(symbol, ttfont, icon_name, symbol_wght_name): glyph_name = icon_font.resolve_ligature(ttfont, icon_name) upem = ttfont["head"].unitsPerEm # For Icon fonts, the Glyphs are Y shifted by upem and the Y axis is flipped. symbol.write_icon( symbol_wght_name, ttfont.getGlyphSet()[glyph_name], SVGPathPen(ttfont.getGlyphSet()), Rect(0, upem, upem, -upem), )
def _bbox(boxes): min_corner = (9999, 9999) max_corner = (-9999, -9999) for box in boxes: min_corner = tuple( min(v1, v2) for v1, v2 in zip(min_corner, (box.x, box.y))) max_corner = tuple( max(v1, v2) for v1, v2 in zip(max_corner, (box.x + box.w, box.y + box.h))) return Rect(*min_corner, *(maxv - minv for minv, maxv in zip(min_corner, max_corner)))
def _get_gradient_transform(grad_el, shape_bbox, view_box, upem) -> Affine2D: transform = map_viewbox_to_font_emsquare(view_box, upem) gradient_units = grad_el.attrib.get("gradientUnits", "objectBoundingBox") if gradient_units == "objectBoundingBox": bbox_space = Rect(0, 0, 1, 1) bbox_transform = Affine2D.rect_to_rect(bbox_space, shape_bbox) transform = Affine2D.product(bbox_transform, transform) if "gradientTransform" in grad_el.attrib: gradient_transform = Affine2D.fromstring( grad_el.attrib["gradientTransform"]) transform = Affine2D.product(gradient_transform, transform) return transform
def main(argv): icon_name = os.path.splitext(os.path.basename(FLAGS.out))[0] dest_region = Rect(0, 0, 120, 120) symbol = _new_symbol() for font_filename in argv[1:]: ttfont = ttLib.TTFont(font_filename) svg_path = _draw_svg_path(ttfont, icon_name, dest_region) ttfont.close() symbol_wght_name = font_filename.split(".")[-2] _write_icon(symbol, symbol_wght_name, svg_path) _drop_empty_icons(symbol) with open(FLAGS.out, "w") as f: f.write(symbol.tostring())
def main(argv): if len(argv) > 2: sys.exit("Expected Only 1 non-flag Argument.") symbol = Symbol() pico = SVG.parse(argv[1]).topicosvg() main_svg = pico.xpath_one("//svg:svg") symbol.write_icon( _REQUIRED_SYMBOL, svgLib.SVGPath.fromstring(pico.tostring()), SVGPathPen(None), Rect( 0, 0, parse_float(main_svg.get("width")), parse_float(main_svg.get("height")), ), ) symbol.drop_empty_icons() symbol.write_to(FLAGS.out)
def _get_gradient_transform( config: FontConfig, grad_el: etree.Element, shape_bbox: Rect, view_box: Rect, glyph_width: int, ) -> Affine2D: transform = map_viewbox_to_font_space( view_box, config.ascender, config.descender, glyph_width, config.transform ) gradient_units = grad_el.attrib.get("gradientUnits", "objectBoundingBox") if gradient_units == "objectBoundingBox": bbox_space = Rect(0, 0, 1, 1) bbox_transform = Affine2D.rect_to_rect(bbox_space, shape_bbox) transform = Affine2D.compose_ltr((bbox_transform, transform)) if "gradientTransform" in grad_el.attrib: gradient_transform = Affine2D.fromstring(grad_el.attrib["gradientTransform"]) transform = Affine2D.compose_ltr((gradient_transform, transform)) return transform
def _map_font_to_viewbox(upem: int, view_box: Rect): if view_box != Rect(0, 0, view_box.w, view_box.w): raise ValueError("We simply must have a SQUARE from 0,0") affine = color_glyph.map_viewbox_to_font_emsquare(Rect(0, 0, upem, upem), view_box.w) return Transform(*affine)
parser.add_argument("destdir") parser.add_argument( "--round", dest="rounding_ndigits", metavar="NDIGITS", type=int, default=ROUNDING_NDIGITS, help="default: %(default)s", ) parser.add_argument( "--viewbox-size", metavar="SIZE", type=int, default=VIEW_BOX_SIZE, help="default: %(default)s", ) options = parser.parse_args(sys.argv[1:]) font = TTFont(options.fontfile) viewbox = Rect(0, 0, options.viewbox_size, options.viewbox_size) os.makedirs(options.destdir, exist_ok=True) for glyph_name, svg in colr_to_svg( viewbox, font, rounding_ndigits=options.rounding_ndigits).items(): output_file = os.path.join(options.destdir, f"{glyph_name}.svg") with open(output_file, "w") as f: f.write(svg.tostring(pretty_print=True)) print(f"{output_file}")
def parse_view_box(s: str) -> Rect: box = tuple(float(v) for v in re.split(r",|\s+", s)) if len(box) != 4: raise ValueError(f"Unable to parse viewBox: {s!r}") return Rect(*box)
def bounding_box(self) -> Rect: x1, y1, x2, y2 = svg_pathops.bounding_box(self.as_cmd_seq()) return Rect(x1, y1, x2 - x1, y2 - y1)
"""Convert COLRv1 font to a set of SVG files, one per base color glyph.""" import os import sys # extend PYTHONPATH to include ../tests dir where colr_to_svg module is located sys.path.insert( 0, os.path.join(os.path.dirname(os.path.dirname(__file__)), "tests")) from colr_to_svg import colr_to_svg from fontTools.ttLib import TTFont from picosvg.geometric_types import Rect from lxml import etree VIEW_BOX_SIZE = 128 try: fontfile, destdir = sys.argv[1:] except ValueError: sys.exit("usage: ./colr2svg.py FONTFILE SVG_OUTPUT_DIR") font = TTFont(fontfile) viewbox = Rect(0, 0, VIEW_BOX_SIZE, VIEW_BOX_SIZE) os.makedirs(destdir, exist_ok=True) for glyph_name, svg in colr_to_svg(viewbox, font).items(): output_file = os.path.join(destdir, f"{glyph_name}.svg") with open(output_file, "wb") as f: f.write(etree.tostring(svg.svg_root, pretty_print=True)) print(f"{output_file}")
def test_empty_rect(): assert Rect(x=1, y=2, w=3, h=0).empty() assert Rect(x=1, y=2, w=0, h=3).empty()
def _build_transformation(self, rect): return Affine2D.rect_to_rect(rect, Rect(0, 0, _SYMBOL_SIZE, _SYMBOL_SIZE)).translate( 0, _SYMBOL_DY_MULTIPLE * rect.h)