| # Copyright 2015 Google, Inc. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| # Google Author(s): Doug Felt |
| |
| import math |
| import random |
| import re |
| import string |
| |
| import svg_cleaner |
| |
| class SvgBuilder(object): |
| """Modifies a font to add SVG glyphs from a document or string. Once built you |
| can call add_from_filename or add_from_doc multiple times to add SVG |
| documents, which should contain a single root svg element representing the glyph. |
| This element must have width and height attributes (in px), these are used to |
| determine how to scale the glyph. The svg should be designed to fit inside |
| this bounds and have its origin at the top left. Adding the svg generates a |
| transform to scale and position the glyph, so the svg element should not have |
| a transform attribute since it will be overwritten. Any id attribute on the |
| glyph is also overwritten. |
| |
| Adding a glyph can generate additional default glyphs for components of a |
| ligature that are not already present. |
| |
| It is possible to add SVG images to a font that already has corresponding |
| glyphs. If a glyph exists already, then its hmtx advance is assumed valid. |
| Otherwise we will generate an advance based on the image's width and scale |
| factor. Callers should ensure that glyphs for components of ligatures are |
| added before the ligatures themselves, otherwise glyphs generated for missing |
| ligature components will be assigned zero metrics metrics that will not be |
| overridden later.""" |
| |
| def __init__(self, font_builder): |
| font_builder.init_svg() |
| |
| self.font_builder = font_builder |
| self.cleaner = svg_cleaner.SvgCleaner() |
| |
| font = font_builder.font |
| self.font_ascent = font['hhea'].ascent |
| self.font_height = self.font_ascent - font['hhea'].descent |
| self.font_upem = font['head'].unitsPerEm |
| |
| def add_from_filename(self, ustr, filename): |
| with open(filename, "r") as fp: |
| return self.add_from_doc(ustr, fp.read(), filename=filename) |
| |
| def _strip_px(self, val): |
| return float(val[:-2] if val.endswith('px') else val) |
| |
| def add_from_doc(self, ustr, svgdoc, filename=None): |
| """Cleans the svg doc, tweaks the root svg element's |
| attributes, then updates the font. ustr is the character or ligature |
| string, svgdoc is the svg document xml. The doc must have a single |
| svg root element.""" |
| |
| # The svg element must have an id attribute of the form 'glyphNNN' where NNN |
| # is the glyph id. We capture the index of the glyph we're adding and write |
| # it into the svg. |
| # |
| # We generate a transform that places the origin at the top left of the |
| # ascent and uniformly scales it to fit both the font height (ascent - |
| # descent) and glyph advance if it is already present. The initial viewport |
| # is 1000x1000. When present, viewBox scales to fit this and uses default |
| # values for preserveAspectRatio that center the viewBox in this viewport |
| # ('xMidyMid meet'), and ignores the width and height. If viewBox is not |
| # present, width and height cause a (possibly non-uniform) scale to be |
| # applied that map the extent to the viewport. This is unfortunate for us, |
| # since we want to preserve the aspect ratio, and the image is likely |
| # designed for a viewport with the width and height it requested. |
| # |
| # If we have an advance, we want to replicate the behavior of viewBox, |
| # except using a 'viewport' of advance, ascent+descent. If we don't have |
| # an advance, we scale the height and compute the advance from the scaled |
| # width. |
| # |
| # Lengths using percentage units map 100% to the width/height/diagonal |
| # of the viewBox, or if it is not defined, the viewport. Since we can't |
| # define the viewport, we must always have a viewBox. |
| |
| cleaner = self.cleaner |
| fbuilder = self.font_builder |
| |
| tree = cleaner.tree_from_text(svgdoc) |
| |
| name, index, exists = fbuilder.add_components_and_ligature(ustr) |
| |
| advance = 0 |
| if exists: |
| advance = fbuilder.hmtx[name][0] |
| |
| vb = tree.attrs.get('viewBox') |
| if vb: |
| x, y, w, h = map(self._strip_px, re.split('\s*,\s*|\s+', vb)) |
| else: |
| wid = tree.attrs.get('width') |
| ht = tree.attrs.get('height') |
| if not (wid and ht): |
| raise ValueError( |
| 'missing viewBox and width or height attrs (%s)' % filename) |
| x, y, w, h = 0, 0, self._strip_px(wid), self._strip_px(ht) |
| |
| # We're going to assume default values for preserveAspectRatio for now, |
| # this preserves aspect ratio and centers in the viewport. |
| # |
| # The viewport is 0,0 1000x1000. First compute the scaled extent and |
| # translations that center the image rect in the viewport, then scale and |
| # translate the result to fit our true 'viewport', which has an origin at |
| # 0,-ascent and an extent of advance (if defined) x font_height. We won't |
| # try to optimize this, it's clearer what we're doing this way. |
| |
| # Since the viewport is square, we can just compare w and h to determine |
| # which to fit to the viewport extent. Get our position and extent in |
| # the viewport. |
| if w > h: |
| scale_to_viewport = 1000.0 / w |
| h_in_viewport = scale_to_viewport * h |
| y_in_viewport = (1000 - h_in_viewport) / 2 |
| w_in_viewport = 1000.0 |
| x_in_viewport = 0.0 |
| else: |
| scale_to_viewport = 1000.0 / h |
| h_in_viewport = 1000.0 |
| y_in_viewport = 0.0 |
| w_in_viewport = scale_to_viewport * w |
| x_in_viewport = (1000 - w_in_viewport) / 2 |
| |
| # Now, compute the scale and translations that fit this rectangle to our |
| # true 'viewport'. The true viewport is not square so we need to choose the |
| # smaller of the scales that fit its height or width. We start with height, |
| # if there's no advance then we're done, otherwise we might have to fit the |
| # advance. |
| scale = self.font_height / h_in_viewport |
| fit_height = True |
| if advance and scale * w_in_viewport > advance: |
| scale = advance / w_in_viewport |
| fit_height = False |
| |
| # Compute transforms that put the top left of the image where we want it. |
| ty = -self.font_ascent - scale * y_in_viewport |
| tx = -scale * x_in_viewport |
| |
| # Adjust them to center the image horizontally if we fit the full height, |
| # vertically otherwise. |
| if fit_height and advance: |
| tx += (advance - scale * w_in_viewport) / 2 |
| else: |
| ty += (self.font_height - scale * h_in_viewport) / 2 |
| |
| cleaner.clean_tree(tree) |
| |
| tree.attrs['id'] = 'glyph%s' % index |
| |
| transform = 'translate(%g, %g) scale(%g)' % (tx, ty, scale) |
| tree.attrs['transform'] = transform |
| |
| tree.attrs['viewBox'] = '%g %g %g %g' % (x, y, w, h) |
| |
| # In order to clip, we need to create a path and reference it. You'd think |
| # establishing a rectangular clip would be simpler... Aaaaand... as it |
| # turns out, in FF the clip on the outer svg element is only relative to the |
| # initial viewport, and is not affected by the viewBox or transform on the |
| # svg element. Unlike chrome. So either we apply an inverse transform, or |
| # insert a group with the clip between the svg and its children. The latter |
| # seems cleaner, ultimately. |
| clip_id = 'clip_' + ''.join( |
| random.choice(string.ascii_lowercase) for i in range(8)) |
| clip_text = ('<g clip-path="url(#%s)"><clipPath id="%s">' |
| '<path d="M%g %gh%gv%gh%gz"/></clipPath></g>' % ( |
| clip_id, clip_id, x, y, w, h, -w)) |
| clip_tree = cleaner.tree_from_text(clip_text) |
| clip_tree.contents.extend(tree.contents) |
| tree.contents = [clip_tree] |
| |
| svgdoc = cleaner.tree_to_text(tree) |
| |
| hmetrics = None |
| if not exists: |
| # There was no advance to fit, so no horizontal centering. The image advance is |
| # all there is. |
| # hmetrics is horiz advance and lsb |
| advance = scale * w_in_viewport |
| hmetrics = [int(round(advance)), 0] |
| |
| fbuilder.add_svg(svgdoc, hmetrics, name, index) |