From a33eb16d0efe0d94c6d190e1f32cb2d0c12d8ec2 Mon Sep 17 00:00:00 2001 From: Jany Belluz Date: Wed, 18 Oct 2017 17:21:25 +0100 Subject: [PATCH] WIP Split even more and start implementing the other direction --- Lib/glyphsLib/builder/blue_values.py | 7 +- Lib/glyphsLib/builder/common.py | 10 +- Lib/glyphsLib/builder/constants.py | 2 + Lib/glyphsLib/builder/custom_params.py | 366 +++++++++++++++++++++++++ Lib/glyphsLib/builder/font.py | 308 +++++++-------------- Lib/glyphsLib/builder/names.py | 24 ++ Lib/glyphsLib/builder/user_data.py | 61 +++++ Lib/glyphsLib/classes.py | 4 + 8 files changed, 564 insertions(+), 218 deletions(-) create mode 100644 Lib/glyphsLib/builder/custom_params.py create mode 100644 Lib/glyphsLib/builder/user_data.py diff --git a/Lib/glyphsLib/builder/blue_values.py b/Lib/glyphsLib/builder/blue_values.py index b5cf8ec13..b78d9478f 100644 --- a/Lib/glyphsLib/builder/blue_values.py +++ b/Lib/glyphsLib/builder/blue_values.py @@ -16,9 +16,10 @@ unicode_literals) -def set_blue_values(ufo, alignment_zones): +def to_ufo_blue_values(_context, ufo, master): """Set postscript blue values from Glyphs alignment zones.""" + alignment_zones = master.alignmentZones blue_values = [] other_blues = [] for zone in sorted(alignment_zones): @@ -29,3 +30,7 @@ def set_blue_values(ufo, alignment_zones): ufo.info.postscriptBlueValues = blue_values ufo.info.postscriptOtherBlues = other_blues + + +def to_glyphs_blue_values(context, ufo, master): + pass diff --git a/Lib/glyphsLib/builder/common.py b/Lib/glyphsLib/builder/common.py index 942ecc0d6..6de613779 100644 --- a/Lib/glyphsLib/builder/common.py +++ b/Lib/glyphsLib/builder/common.py @@ -14,8 +14,16 @@ from __future__ import (print_function, division, absolute_import, unicode_literals) +import datetime + +UFO_FORMAT = '%Y/%m/%d %H:%M:%S' def to_ufo_time(datetime_obj): """Format a datetime object as specified for UFOs.""" - return datetime_obj.strftime('%Y/%m/%d %H:%M:%S') + return datetime_obj.strftime(UFO_FORMAT) + + +def from_ufo_time(string): + """Parses a datetime as specified for UFOs into a datetime object.""" + return datetime.strptime(string, UFO_FORMAT) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index 95232ef3e..0b574348a 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -75,3 +75,5 @@ 850: 62, 437: 63, } + +REVERSE_CODEPAGE_RANGES = {value: key for key, value in CODEPAGE_RANGES} diff --git a/Lib/glyphsLib/builder/custom_params.py b/Lib/glyphsLib/builder/custom_params.py new file mode 100644 index 000000000..51a5563b9 --- /dev/null +++ b/Lib/glyphsLib/builder/custom_params.py @@ -0,0 +1,366 @@ +# 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. + +from __future__ import (print_function, division, absolute_import, + unicode_literals) + +import re + +from glyphsLib.util import clear_data, cast_to_number_or_bool, \ + bin_to_int_list, int_list_to_bin +from .filters import parse_glyphs_filter, write_glyphs_filter +from .constants import GLYPHLIB_PREFIX, GLYPHS_PREFIX, PUBLIC_PREFIX, \ + CODEPAGE_RANGES, REVERSE_CODEPAGE_RANGES, UFO2FT_FILTERS_KEY + +"""Set Glyphs custom parameters in UFO info or lib, where appropriate. + +Custom parameter data can be pre-parsed out of Glyphs data and provided via +the `parsed` argument, otherwise `data` should be provided and will be +parsed. The `parsed` option is provided so that custom params can be popped +from Glyphs data once and used several times; in general this is used for +debugging purposes (to detect unused Glyphs data). + +The `non_info` argument can be used to specify potential UFO info attributes +which should not be put in UFO info. +""" + + +def identity(value): + return value + + +class ParamHandler(object): + def __init__(self, glyphs_name, ufo_name=None, + ufo_prefix=GLYPHS_PREFIX, ufo_info=True, + value_to_ufo=identity, value_to_glyphs=identity): + self.glyphs_name = glyphs_name + # By default, they have the same name in both + self.ufo_name = ufo_name or glyphs_name + self.ufo_prefix = ufo_prefix + self.ufo_info = ufo_info + # Value transformation functions + self.value_to_ufo = value_to_ufo + self.value_to_glyphs = value_to_glyphs + + def glyphs_names(self): + # Just in case one handler covers several names + return (self.glyphs_name,) + + def ufo_names(self): + return (self.ufo_name,) + + # By default, the parameter is read from/written to: + # - the Glyphs object's customParameters + # - the UFO's info object if it has a matching attribute, else the lib + def to_glyphs(self, glyphs, ufo): + ufo_value = self._read_from_ufo(ufo) + if ufo_value is None: + return + glyphs_value = self.value_to_glyphs(ufo_value) + self._write_to_glyphs(glyphs, glyphs_value) + + def to_ufo(self, glyphs, ufo): + glyphs_value = self._read_from_glyphs(glyphs) + if glyphs_value is None: + return + ufo_value = self.value_to_ufo(glyphs_value) + self._write_to_ufo(ufo, ufo_value) + + def _read_from_glyphs(self, glyphs): + return glyphs.customParameters[self.glyphs_name] + + def _write_to_glyphs(self, glyphs, value): + glyphs.customParameters[self.glyphs_name] = value + + def _read_from_ufo(self, ufo): + if self.ufo_info and hasattr(ufo.info, self.ufo_name): + return getattr(ufo.info, self.ufo_name) + else: + return ufo.lib[self.ufo_prefix + self.ufo_name] + + def _write_to_ufo(self, ufo, value): + if self.ufo_info and hasattr(ufo.info, self.ufo_name): + # most OpenType table entries go in the info object + setattr(ufo.info, self.ufo_name, value) + else: + # everything else gets dumped in the lib + ufo.lib[self.ufo_prefix + self.ufo_name] = value + + +KNOWN_FAMILY_PARAM_HANDLERS = [] +KNOWN_FAMILY_PARAM_GLYPHS_NAMES = set() +KNOWN_MASTER_PARAM_HANDLERS = [] +KNOWN_MASTER_PARAM_GLYPHS_NAMES = set() +KNOWN_PARAM_UFO_NAMES = set() + + +def register(scope, handler): + if scope == 'family': + KNOWN_FAMILY_PARAM_HANDLERS.append(handler) + KNOWN_FAMILY_PARAM_GLYPHS_NAMES.update(handler.glyphs_names()) + elif scope == 'master': + KNOWN_MASTER_PARAM_HANDLERS.append(handler) + KNOWN_MASTER_PARAM_GLYPHS_NAMES.update(handler.glyphs_names()) + else: + raise RuntimeError + KNOWN_PARAM_UFO_NAMES.update(handler.ufo_names()) + + +# FIXME: (jany) apparently these might only be prefixes, find the list of full names +opentype_attr_prefix_pairs = ( + ('hhea', 'Hhea'), ('description', 'NameDescription'), + ('license', 'NameLicense'), + ('licenseURL', 'NameLicenseURL'), + ('preferredFamilyName', 'NamePreferredFamilyName'), + ('preferredSubfamilyName', 'NamePreferredSubfamilyName'), + ('compatibleFullName', 'NameCompatibleFullName'), + ('sampleText', 'NameSampleText'), + ('WWSFamilyName', 'NameWWSFamilyName'), + ('WWSSubfamilyName', 'NameWWSSubfamilyName'), + ('panose', 'OS2Panose'), + ('typo', 'OS2Typo'), ('unicodeRanges', 'OS2UnicodeRanges'), + ('vendorID', 'OS2VendorID'), + ('versionString', 'NameVersion'), ('fsType', 'OS2Type')) +for glyphs_name, ufo_name in opentype_attr_prefix_pairs: + full_ufo_name = 'openType' + ufo_name + # FIXME: (jany) family or master? + register('family', ParamHandler(glyphs_name, full_ufo_name)) + +# convert code page numbers to OS/2 ulCodePageRange bits +# FIXME: (jany) family or master? +register('family', ParamHandler( + glyphs_name='codePageRanges', + ufo_name='openTypeOS2CodePageRanges', + value_to_ufo=lambda value: [CODEPAGE_RANGES[v] for v in value], + value_to_glyphs=lambda value: [REVERSE_CODEPAGE_RANGES[v] for v in value] +)) + +# enforce that winAscent/Descent are positive, according to UFO spec +for glyphs_name in ('winAscent', 'winDescent'): + ufo_name = 'openTypeOS2W' + glyphs_name[1:] + # FIXME: (jany) family or master? + register('family', ParamHandler( + glyphs_name, ufo_name, + value_to_ufo=lambda value: -abs(value), + value_to_glyphs=abs, + )) + +# The value of these could be a float, and ufoLib/defcon expect an int. +for glyphs_name in ('weightClass', 'widthClass'): + ufo_name = 'openTypeOS2W' + glyphs_name[1:] + # FIXME: (jany) family or master? + register('family', ParamHandler(glyphs_name, ufo_name, value_to_ufo=int)) + + +# convert Glyphs' GASP Table to UFO openTypeGaspRangeRecords +def to_ufo_gasp_table(value): + # XXX maybe the parser should cast the gasp values to int? + value = {int(k): int(v) for k, v in value.items()} + gasp_records = [] + # gasp range records must be sorted in ascending rangeMaxPPEM + for max_ppem, gasp_behavior in sorted(value.items()): + gasp_records.append({ + 'rangeMaxPPEM': max_ppem, + 'rangeGaspBehavior': bin_to_int_list(gasp_behavior)}) + return gasp_records + + +def to_glyphs_gasp_table(value): + return { + record['rangeMaxPPEM']: int_list_to_bin(record['rangeGaspBehavior']) + for record in value + } + +register('family', ParamHandler( + glyphs_name='GASP Table', + ufo_name='openTypeGaspRangeRecords', + value_to_ufo=to_ufo_gasp_table, + value_to_glyphs=to_glyphs_gasp_table, +)) + + +class MiscParamHandler(ParamHandler): + def read_from_glyphs(self, glyphs): + return getattr(glyphs, self.glyphs_name) + + def write_to_glyphs(self, glyphs, value): + setattr(glyphs, self.glyphs_name, value) + + +register('family', MiscParamHandler(glyphs_name='DisplayStrings')) +register('family', MiscParamHandler(glyphs_name='disablesAutomaticAlignment')) + +# deal with any Glyphs naming quirks here +register('family', MiscParamHandler( + glyphs_name='disablesNiceNames', + ufo_name='useNiceNames', + value_to_ufo=lambda value: int(not value), + value_to_glyphs=lambda value: not bool(value) +)) + +for number in ('', '1', '2', '3'): + register('master', MiscParamHandler('customName' + number, ufo_info=False)) + register('master', MiscParamHandler('customValue' + number, + ufo_info=False)) +register('master', MiscParamHandler('weightValue', ufo_info=False)) +register('master', MiscParamHandler('widthValue', ufo_info=False)) + + +class OS2SelectionParamHandler(object): + flags = ( + ('Use Typo Metrics', 7), + ('Has WWS Names', 8), + ) + + def glyphs_names(self): + return [flag[0] for flag in flags] + + def ufo_names(self): + return ('openTypeOS2Selection',) + + def to_glyphs(self, glyphs, ufo): + ufo_flags = ufo.info.openTypeOS2Selection + if ufo_flags is None: + return + for glyphs_name, value in self.flags: + if value in ufo_flags: + glyphs.customParameters[glyphs_name] = True + + def to_ufo(self, glyphs, ufo): + for glyphs_name, value in self.flags: + if glyphs.customParameters[glyphs_name]: + if ufo.info.openTypeOS2Selection is None: + ufo.info.openTypeOS2Selection = [] + ufo.info.openTypeOS2Selection.append(value) + +# FIXME: (jany) master or family? +register('family', OS2SelectionParamHandler()) + +# Postscript attributes +postscript_attrs = ('underlinePosition', 'underlineThickness') +for glyphs_name in postscript_attrs: + ufo_name = 'postscript' + name[0].upper() + name[1:] + # FIXME: (jany) master or family? + register('family', ParamHandler(glyphs_name, ufo_name)) + +# store the public.glyphOrder in lib.plist +# FIXME: (jany) master or family? +register('family', ParamHandler('glyphOrder', ufo_prefix=PUBLIC_PREFIX)) + + +class FilterParamHandler(ParamHandler): + def __init__(self): + super(FilterParamHandler, self).__init__( + glyphs_name='Filter', + ufo_name=UFO2FT_FILTERS_KEY, + ufo_prefix='', + value_to_ufo=parse_glyphs_filter, + value_to_glyphs=write_glyphs_filter, + ) + + # Don't overwrite, append instead + def _write_to_ufo(self, ufo, value): + if self.ufo_name not in ufo.lib.keys(): + ufo.lib[self.ufo_name] = [] + ufo.lib[self.ufo_name].append(value) + +# FIXME: (jany) maybe BOTH master AND family? +register('master', FilterParamHandler) + + +def to_ufo_custom_params(context, ufo, master): + # Handle known parameters + for handler in KNOWN_FAMILY_PARAM_HANDLERS: + handler.to_ufo(context.font, ufo) + for handler in KNOWN_MASTER_PARAM_HANDLERS: + handler.to_ufo(master, ufo) + + # Handle unknown parameters + for name, value in context.font.customParameters: + name = normalize_custom_param_name(name) + if name in KNOWN_FAMILY_PARAM_GLYPHS_NAMES: + continue + ufo.lib[GLYPHS_PREFIX + name] = value + for name, value in master.customParameters: + name = normalize_custom_param_name(name) + if name in KNOWN_MASTER_PARAM_GLYPHS_NAMES: + continue + ufo.lib[GLYPHS_PREFIX + name] = value + + set_default_params(ufo) + + +def to_glyphs_family_custom_params(context, ufo): + # Handle known parameters + for handler in KNOWN_FAMILY_PARAM_HANDLERS: + handler.to_glyphs(context.font, ufo) + + # Handle unknown parameters + _to_glyphs_unknown_parameters(context.font, ufo) + + # FIXME: (jany) do something about default values? + + +def to_glyphs_master_custom_params(context, ufo, master): + # Handle known parameters + for handler in KNOWN_MASTER_PARAM_HANDLERS: + handler.to_glyphs(context.font, ufo) + + # Handle unknown parameters + _to_glyphs_unknown_parameters(master, ufo) + + # FIXME: (jany) do something about default values? + + +def _to_glyphs_unknown_parameters(glyphs, ufo): + for name, value in ufo.info: + name = normalize_custom_param_name(name) + if name not in KNOWN_UFO_INFO_PARAM_NAMES: + # TODO: (jany) + pass + + for name, value in ufo.lib: + name = normalize_custom_param_name(name) + if name not in KNOWN_UFO_LIB_PARAM_NAMES: + # TODO: (jany) + pass + + +def normalize_custom_param_name(name): + """Replace curved quotes with straight quotes in a custom parameter name. + These should be the only keys with problematic (non-ascii) characters, + since they can be user-generated. + """ + + replacements = ( + (u'\u2018', "'"), (u'\u2019', "'"), (u'\u201C', '"'), (u'\u201D', '"')) + for orig, replacement in replacements: + name = name.replace(orig, replacement) + return name + + +def set_default_params(ufo): + """ Set Glyphs.app's default parameters when different from ufo2ft ones. + """ + # ufo2ft defaults to fsType Bit 2 ("Preview & Print embedding"), while + # Glyphs.app defaults to Bit 3 ("Editable embedding") + if ufo.info.openTypeOS2Type is None: + ufo.info.openTypeOS2Type = [3] + + # Reference: + # https://glyphsapp.com/content/1-get-started/2-manuals/1-handbook-glyphs-2-0/Glyphs-Handbook-2.3.pdf#page=200 + if ufo.info.postscriptUnderlineThickness is None: + ufo.info.postscriptUnderlineThickness = 50 + if ufo.info.postscriptUnderlinePosition is None: + ufo.info.postscriptUnderlinePosition = -100 diff --git a/Lib/glyphsLib/builder/font.py b/Lib/glyphsLib/builder/font.py index edcce9acf..9234aa861 100644 --- a/Lib/glyphsLib/builder/font.py +++ b/Lib/glyphsLib/builder/font.py @@ -15,18 +15,18 @@ from __future__ import (print_function, division, absolute_import, unicode_literals) -import re from collections import deque, OrderedDict import logging -from .constants import PUBLIC_PREFIX, GLYPHS_PREFIX, CODEPAGE_RANGES, \ - UFO2FT_FILTERS_KEY -from glyphsLib.util import clear_data, cast_to_number_or_bool, bin_to_int_list -from .guidelines import to_ufo_guidelines -from .common import to_ufo_time -from .filters import parse_glyphs_filter -from .names import build_style_name, build_stylemap_names -from .blue_values import set_blue_values +from .constants import GLYPHS_PREFIX +from .guidelines import to_ufo_guidelines, to_glyphs_guidelines +from .common import to_ufo_time, from_ufo_time +from .names import to_ufo_names, to_glyphs_names +from .blue_values import to_ufo_blue_values, to_glyphs_blue_values +from .user_data import to_ufo_family_user_data, to_ufo_master_user_data, \ + to_glyphs_family_user_data, to_glyphs_master_user_data +from .custom_params import to_ufo_custom_params, \ + to_glyphs_family_custom_params, to_glyphs_master_custom_params logger = logging.getLogger(__name__) @@ -47,16 +47,12 @@ def to_ufo_font_attributes(context, family_name): units_per_em = font.upm version_major = font.versionMajor version_minor = font.versionMinor - user_data = font.userData copyright = font.copyright designer = font.designer designer_url = font.designerURL manufacturer = font.manufacturer manufacturer_url = font.manufacturerURL - misc = ['DisplayStrings', 'disablesAutomaticAlignment', 'disablesNiceNames'] - custom_params = parse_custom_params(font, misc) - for master in font.masters: ufo = context.defcon.Font() @@ -91,222 +87,33 @@ def to_ufo_font_attributes(context, family_name): ufo.info.postscriptStemSnapV = vertical_stems if italic_angle: ufo.info.italicAngle = italic_angle - is_italic = True - else: - is_italic = False width = master.width weight = master.weight - custom = master.customName if weight: ufo.lib[GLYPHS_PREFIX + 'weight'] = weight if width: ufo.lib[GLYPHS_PREFIX + 'width'] = width - if custom: - ufo.lib[GLYPHS_PREFIX + 'custom'] = custom - - styleName = build_style_name( - width if width != 'Regular' else '', - weight, - custom, - is_italic - ) - styleMapFamilyName, styleMapStyleName = build_stylemap_names( - family_name=family_name, - style_name=styleName, - is_bold=(styleName == 'Bold'), - is_italic=is_italic - ) - ufo.info.familyName = family_name - ufo.info.styleName = styleName - ufo.info.styleMapFamilyName = styleMapFamilyName - ufo.info.styleMapStyleName = styleMapStyleName - - set_blue_values(ufo, master.alignmentZones) - set_family_user_data(ufo, user_data) - set_master_user_data(ufo, master.userData) + for number in ('', '1', '2', '3'): + custom_name = getattr(master, 'customName' + number) + if custom_name: + ufo.lib[GLYPHS_PREFIX + 'customName' + number] = custom_name + custom_value = setattr(master, 'customValue' + number) + if custom_value: + ufo.lib[GLYPHS_PREFIX + 'customValue' + number] = custom_value + + to_ufo_names(context, ufo, master, family_name) + to_ufo_blue_values(context, ufo, master) + to_ufo_family_user_data(context, ufo) + to_ufo_master_user_data(context, ufo, master) to_ufo_guidelines(context, ufo, master) - - set_custom_params(ufo, parsed=custom_params) - # the misc attributes double as deprecated info attributes! - # they are Glyphs-related, not OpenType-related, and don't go in info - misc = ('customValue', 'weightValue', 'widthValue') - set_custom_params(ufo, data=master, misc_keys=misc, non_info=misc) - - set_default_params(ufo) + to_ufo_custom_params(context, ufo, master) master_id = master.id ufo.lib[GLYPHS_PREFIX + 'fontMasterID'] = master_id context.ufos[master_id] = ufo -def set_custom_params(ufo, parsed=None, data=None, misc_keys=(), non_info=()): - """Set Glyphs custom parameters in UFO info or lib, where appropriate. - - Custom parameter data can be pre-parsed out of Glyphs data and provided via - the `parsed` argument, otherwise `data` should be provided and will be - parsed. The `parsed` option is provided so that custom params can be popped - from Glyphs data once and used several times; in general this is used for - debugging purposes (to detect unused Glyphs data). - - The `non_info` argument can be used to specify potential UFO info attributes - which should not be put in UFO info. - """ - - if parsed is None: - parsed = parse_custom_params(data or {}, misc_keys) - else: - assert data is None, "Shouldn't provide parsed data and data to parse." - - fsSelection_flags = {'Use Typo Metrics', 'Has WWS Names'} - for name, value in parsed: - name = normalize_custom_param_name(name) - - if name in fsSelection_flags: - if value: - if ufo.info.openTypeOS2Selection is None: - ufo.info.openTypeOS2Selection = [] - if name == 'Use Typo Metrics': - ufo.info.openTypeOS2Selection.append(7) - elif name == 'Has WWS Names': - ufo.info.openTypeOS2Selection.append(8) - continue - - # deal with any Glyphs naming quirks here - if name == 'disablesNiceNames': - name = 'useNiceNames' - value = int(not value) - - # convert code page numbers to OS/2 ulCodePageRange bits - if name == 'codePageRanges': - value = [CODEPAGE_RANGES[v] for v in value] - - # convert Glyphs' GASP Table to UFO openTypeGaspRangeRecords - if name == 'GASP Table': - name = 'openTypeGaspRangeRecords' - # XXX maybe the parser should cast the gasp values to int? - value = {int(k): int(v) for k, v in value.items()} - gasp_records = [] - # gasp range records must be sorted in ascending rangeMaxPPEM - for max_ppem, gasp_behavior in sorted(value.items()): - gasp_records.append({ - 'rangeMaxPPEM': max_ppem, - 'rangeGaspBehavior': bin_to_int_list(gasp_behavior)}) - value = gasp_records - - opentype_attr_prefix_pairs = ( - ('hhea', 'Hhea'), ('description', 'NameDescription'), - ('license', 'NameLicense'), - ('licenseURL', 'NameLicenseURL'), - ('preferredFamilyName', 'NamePreferredFamilyName'), - ('preferredSubfamilyName', 'NamePreferredSubfamilyName'), - ('compatibleFullName', 'NameCompatibleFullName'), - ('sampleText', 'NameSampleText'), - ('WWSFamilyName', 'NameWWSFamilyName'), - ('WWSSubfamilyName', 'NameWWSSubfamilyName'), - ('panose', 'OS2Panose'), - ('typo', 'OS2Typo'), ('unicodeRanges', 'OS2UnicodeRanges'), - ('codePageRanges', 'OS2CodePageRanges'), - ('weightClass', 'OS2WeightClass'), - ('widthClass', 'OS2WidthClass'), - ('win', 'OS2Win'), ('vendorID', 'OS2VendorID'), - ('versionString', 'NameVersion'), ('fsType', 'OS2Type')) - for glyphs_prefix, ufo_prefix in opentype_attr_prefix_pairs: - name = re.sub( - '^' + glyphs_prefix, 'openType' + ufo_prefix, name) - - postscript_attrs = ('underlinePosition', 'underlineThickness') - if name in postscript_attrs: - name = 'postscript' + name[0].upper() + name[1:] - - # enforce that winAscent/Descent are positive, according to UFO spec - if name.startswith('openTypeOS2Win') and value < 0: - value = -value - - # The value of these could be a float, and ufoLib/defcon expect an int. - if name in ('openTypeOS2WeightClass', 'openTypeOS2WidthClass'): - value = int(value) - - if name == 'glyphOrder': - # store the public.glyphOrder in lib.plist - ufo.lib[PUBLIC_PREFIX + name] = value - elif name == 'Filter': - filter_struct = parse_glyphs_filter(value) - if not filter_struct: - continue - if UFO2FT_FILTERS_KEY not in ufo.lib.keys(): - ufo.lib[UFO2FT_FILTERS_KEY] = [] - ufo.lib[UFO2FT_FILTERS_KEY].append(filter_struct) - elif hasattr(ufo.info, name) and name not in non_info: - # most OpenType table entries go in the info object - setattr(ufo.info, name, value) - else: - # everything else gets dumped in the lib - ufo.lib[GLYPHS_PREFIX + name] = value - - -def set_default_params(ufo): - """ Set Glyphs.app's default parameters when different from ufo2ft ones. - """ - # ufo2ft defaults to fsType Bit 2 ("Preview & Print embedding"), while - # Glyphs.app defaults to Bit 3 ("Editable embedding") - if ufo.info.openTypeOS2Type is None: - ufo.info.openTypeOS2Type = [3] - - # Reference: - # https://glyphsapp.com/content/1-get-started/2-manuals/1-handbook-glyphs-2-0/Glyphs-Handbook-2.3.pdf#page=200 - if ufo.info.postscriptUnderlineThickness is None: - ufo.info.postscriptUnderlineThickness = 50 - if ufo.info.postscriptUnderlinePosition is None: - ufo.info.postscriptUnderlinePosition = -100 - - -def normalize_custom_param_name(name): - """Replace curved quotes with straight quotes in a custom parameter name. - These should be the only keys with problematic (non-ascii) characters, - since they can be user-generated. - """ - - replacements = ( - (u'\u2018', "'"), (u'\u2019', "'"), (u'\u201C', '"'), (u'\u201D', '"')) - for orig, replacement in replacements: - name = name.replace(orig, replacement) - return name - - -def parse_custom_params(font, misc_keys): - """Parse customParameters into a list of pairs.""" - - params = [] - for p in font.customParameters: - params.append((p.name, p.value)) - for key in misc_keys: - try: - val = getattr(font, key) - except KeyError: - continue - if val is not None: - params.append((key, val)) - return params - - -def set_family_user_data(ufo, user_data): - """Set family-wide user data as Glyphs does.""" - - for key in user_data.keys(): - ufo.lib[key] = user_data[key] - - -def set_master_user_data(ufo, user_data): - """Set master-specific user data as Glyphs does.""" - - if user_data: - data = {} - for key in user_data.keys(): - data[key] = user_data[key] - ufo.lib[GLYPHS_PREFIX + 'fontMaster.userData'] = data - - def to_glyphs_font_attributes(context, ufo, master, is_initial): """ Copy font attributes from `ufo` either to `context.font` or to `master`. @@ -322,5 +129,74 @@ def to_glyphs_font_attributes(context, ufo, master, is_initial): # what we would be writing, to guard against the info being # modified in only one of the UFOs in a MM. Maybe do this check later, # when the roundtrip without modification works. + if is_initial: + _set_glyphs_font_attributes(context, ufo) + else: + # _compare_and_merge_glyphs_font_attributes(context, ufo) + pass + _set_glyphs_master_attributes(context, ufo, master) + + +def _set_glyphs_font_attributes(context, ufo): + font = context.font + info = ufo.info + + if info.openTypeHeadCreated is not None: + # FIXME: (jany) should wrap in glyphs_datetime? or maybe the GSFont + # should wrap in glyphs_datetime if needed? + font.date = from_ufo_time(info.opentTypeHeadCreated) + font.upm = info.unitsPerEm + font.versionMajor = info.versionMajor + font.versionMinor = info.versionMinor + + if info.copyright is not None: + font.copyright = info.copyright + if info.openTypeNameDesigner is not None: + font.designer = info.openTypeNameDesigner + if info.openTypeNameDesignerURL is not None: + font.designerURL = info.openTypeNameDesignerURL + if info.openTypeNameManufacturer is not None: + font.manufacturer = info.openTypeNameManufacturer + if info.openTypeNameManufacturerURL is not None: + font.manufacturerURL = info.openTypeNameManufacturerURL + + to_glyphs_family_user_data(context, ufo) + to_glyphs_family_custom_params(context, ufo) + + +def _set_glyphs_master_attributes(context, ufo, master): master.id = ufo.lib[GLYPHS_PREFIX + 'fontMasterID'] - # TODO: all the other attributes + + master.ascender = ufo.info.ascender + master.capHeight = ufo.info.capHeight + master.descender = ufo.info.descender + master.xHeight = ufo.info.xHeight + + horizontal_stems = ufo.info.postscriptStemSnapH + vertical_stems = ufo.info.postscriptStemSnapV + italic_angle = -ufo.info.italicAngle + if horizontal_stems: + master.horizontalStems = horizontal_stems + if vertical_stems: + master.verticalStems = vertical_stems + if italic_angle: + master.italicAngle = italic_angle + + width = ufo.lib[GLYPHS_PREFIX + 'width'] + weight = ufo.lib[GLYPHS_PREFIX + 'weight'] + if weight: + master.weight = weight + if width: + master.width = width + for number in ('', '1', '2', '3'): + custom_name = ufo.lib[GLYPHS_PREFIX + 'customName' + number] + if custom_name: + setattr(master, 'customName' + number, custom_name) + custom_value = ufo.lib[GLYPHS_PREFIX + 'customValue' + number] + if custom_value: + setattr(master, 'customValue' + number, custom_value) + + to_glyphs_blue_values(context, ufo, master) + to_glyphs_master_user_data(context, ufo, master) + to_glyphs_guidelines(context, ufo, master) + to_glyphs_master_custom_params(context, ufo, master) diff --git a/Lib/glyphsLib/builder/names.py b/Lib/glyphsLib/builder/names.py index e67a4d19a..02a111eee 100644 --- a/Lib/glyphsLib/builder/names.py +++ b/Lib/glyphsLib/builder/names.py @@ -18,6 +18,30 @@ from collections import deque +def to_ufo_names(context, ufo, master, family_name): + width = master.width + weight = master.weight + custom = master.customName + is_italic = bool(master.italicAngle) + + styleName = build_style_name( + width if width != 'Regular' else '', + weight, + custom, + is_italic + ) + styleMapFamilyName, styleMapStyleName = build_stylemap_names( + family_name=family_name, + style_name=styleName, + is_bold=(styleName == 'Bold'), + is_italic=is_italic + ) + ufo.info.familyName = family_name + ufo.info.styleName = styleName + ufo.info.styleMapFamilyName = styleMapFamilyName + ufo.info.styleMapStyleName = styleMapStyleName + + def build_stylemap_names(family_name, style_name, is_bold=False, is_italic=False, linked_style=None): """Build UFO `styleMapFamilyName` and `styleMapStyleName` based on the diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py new file mode 100644 index 000000000..590aa157a --- /dev/null +++ b/Lib/glyphsLib/builder/user_data.py @@ -0,0 +1,61 @@ +# 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. + +from __future__ import (print_function, division, absolute_import, + unicode_literals) + +from .constants import GLYPHS_PREFIX + +MASTER_USER_DATA_KEY = GLYPHS_PREFIX + 'fontMaster.userData' + + +def to_ufo_family_user_data(context, ufo): + """Set family-wide user data as Glyphs does.""" + user_data = context.font.userData + for key in user_data.keys(): + # FIXME: (jany) Should put a Glyphs prefix? + # FIXME: (jany) At least identify which stuff we have put in lib during + # the Glyphs->UFO so that we don't take it back into userData in + # the other direction. + ufo.lib[key] = user_data[key] + + +def to_ufo_master_user_data(_context, ufo, master): + """Set master-specific user data as Glyphs does.""" + user_data = master.userData + if user_data: + data = {} + for key in user_data.keys(): + data[key] = user_data[key] + ufo.lib[MASTER_USER_DATA_KEY] = data + + +def to_glyphs_family_user_data(context, ufo): + """Set the GSFont userData from the UFO family-wide user data.""" + target_user_data = context.font.userData + for key, value in ufo.lib: + if _user_data_was_originally_there_family_wide(key): + target_user_data[key] = value + + +def to_glyphs_master_user_data(_context, ufo, master): + """Set the GSFontMaster userData from the UFO master-specific user data.""" + user_data = ufo.lib[MASTER_USER_DATA_KEY] + if user_data: + master.userData = user_data + + +def _user_data_was_originally_there_family_wide(key): + # FIXME: (jany) Identify better which keys must be brought back? + return not key.startswith(GLYPHS_PREFIX) diff --git a/Lib/glyphsLib/classes.py b/Lib/glyphsLib/classes.py index 4b9d4a237..77d4108e2 100755 --- a/Lib/glyphsLib/classes.py +++ b/Lib/glyphsLib/classes.py @@ -1170,6 +1170,10 @@ class GSFontMaster(GSBase): _wrapperKeysTranslate = { "guideLines": "guides", "custom": "customName", + # FIXME: (jany) Check in Glyphs + "custom1": "customName1", + "custom2": "customName2", + "custom3": "customName3", } _keyOrder = ( "alignmentZones",