Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fonts: add support for extracting code points #638

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from

Conversation

robbiet480
Copy link

@robbiet480 robbiet480 commented May 15, 2019

This adds support to the SwiftGen fonts context for extracting code points of a font to make them available to the template. This is useful for generating templates for icon fonts. I am doing this work to modernize Iconic which until this point used a old, modified, Swift 2.3 only version of SwiftGen which meant that recent users have been unable to update their icon fonts. I have adapted the Iconic SwiftGen template but have not submitted it as part of this PR because I was unsure if it was out of scope. If you don't think it will be I will be happy to submit it. I quickly put it up as a Gist if you want to take a look.

Related PR (required to run the linked template, not required for this PR to be merged): SwiftGen/StencilSwiftKit#121

  • I've started my branch from the develop branch (gitflow)
    • And I've selected develop as the "base" branch for this Pull Request I'm about to create
  • I've added an entry in the CHANGELOG.md file to explain my changes and credit myself
    (or added #trivial to the PR title if I think it doesn't warrant a mention in the CHANGELOG)
    • Add the entry in the appropriate section (Bug Fix / New Feature / Breaking Change / Internal Change)
    • Add a period and 2 spaces at the end of your short entry description
    • Add links to your GitHub profile to credit yourself and to the related PR and issue after your description

@robbiet480
Copy link
Author

robbiet480 commented May 15, 2019

Seems Circle is broken on the branch. I did have to remove the opt in rule lower_acl_than_parent from the SwiftLint config to get things building locally. All the ACL issues were in files that I didn't touch though.

@AliSoftware
Copy link
Collaborator

Thanks for the PR (and sorry for the late reply)

Could you update the contexts files with this new code so we can see how the updated YAML contexts look like, and also so that the unit tests are updated accordingly? It's just a matter of using Cmd-U on the "SwiftGenKit - Generate Contexts" scheme in Xcode

I have two main concerns with this change:

  • If I understand the code correctly, it will generate a HUGE list of names for standard fonts. For fonts that are limited to only a couple of glyphs, like "icon fonts" that might be OK. But for a normal font that contains tons of glyphs for almost all possible codepoints in unicode, that is gonna generate a HUGE list. We likely don't want that for most fonts
  • I tried to re-generate the SwiftGenKit contexts (by running the scheme as pointed above) to run the unit tests and it crashes. This is because for regular fonts like the ones we have in our unit tests, like .SFNSDisplay-Regular for example, there are multiple glyphs with the same name (in my tests it crashes with Fatal error: Duplicate values for key: 'space')

So in the end I'm not sure if this makes sense to generate those lists. I understand the use case, but that won't work for regular fonts though. Maybe we could make use of parser options so that the user can specify when they want those list of glyph names generated for which fonts, and default to not generate them? That way users can opt-in for this behaviour only for their icon-fonts and avoid the list of glyphs for regular fonts?

@djbe
Copy link
Member

djbe commented May 17, 2020

Ideally we'd have a mechanism for lazily calculating certain context properties (+ cache, so lazy var?), such as the "code points" in this PR. But that doesn't really work with how our contexts are generated right now (plain dictionaries), and how we serialize them for testing purposes.

@djbe djbe added this to the 6.3.0 milestone May 17, 2020
@SwiftGen-Eve
Copy link

SwiftGen-Eve commented Jun 11, 2020

1 Warning
⚠️ Big PR

Hey 👋 I'm Eve, the friendly bot watching over SwiftGen 🤖

Thanks a lot for your contribution!


Seems like everything is in order 👍 You did a good job here! 🤝

Generated by 🚫 Danger

@robbiet480
Copy link
Author

@AliSoftware I've gone ahead and added a option for extracting code points. Not sure what I need to do in the tests to get it actually testing though.

@robbiet480
Copy link
Author

Never mind, all figured out! Think this is good to go?

@robbiet480
Copy link
Author

robbiet480 commented Jun 11, 2020

Now that I'm actually integrating this, I've realized that this can't yet be merged. For one thing, the option values need to be String so it works with CLI. But now I'm facing this issue and unsure how to get past it. Just pushed the font that has this code point at 7132bca. Suggestions welcome!

let point: UInt32 = 0xF0000
let scal = Unicode.Scalar(point)!
let char = UniChar(scal.value) // Fatal error: Not enough bits to represent the passed value

@djbe
Copy link
Member

djbe commented Jun 11, 2020

@robbiet480 Not sure what you're trying to do there, but that code point 0xF0000 does not fit inside an Int16 (which UniChar is a typealias of).

@robbiet480
Copy link
Author

Yeah its a codepoint that came out of the test icon font I'm using, MaterialDesignIcons. Guess they have so many icons the code points go out of UInt16 space?

@djbe
Copy link
Member

djbe commented Jun 11, 2020

You can write a unicode character in a Swift string like this:

let dollarSign = "\u{24}"        // $,  Unicode scalar U+0024
let blackHeart = "\u{2665}"      // ♥,  Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // 💖, Unicode scalar U+1F496

@AliSoftware
Copy link
Collaborator

AliSoftware commented Jun 11, 2020

Yeah what you probably want is to convert your Unicode.Scalar into a Character, not a UniChar

https://developer.apple.com/documentation/swift/character/2906502-init

in Unicode, code points are not limited to 2^16 so it's not logical to constrain them to UInt16. That's why we use Character in Swift when manipulating strings, and not UniChar which are more a relic of the past from C before full-unicode compliance

@robbiet480
Copy link
Author

@AliSoftware That's hopefully the pointer I needed, thanks!!!

@robbiet480
Copy link
Author

Okay, think it's now all good.

@AliSoftware AliSoftware self-requested a review June 11, 2020 23:25
Copy link
Collaborator

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick review (from phone so might have missed things?) of those latest changes, looking good!

Left a couple of suggestions, and one or two interrogations about things we might want to just conform to be sure, but otherwise LGTM 👍

@@ -53,6 +53,9 @@
[#609](https://github.com/SwiftGen/SwiftGen/issue/609)
[#593](https://github.com/SwiftGen/SwiftGen/pull/593)
[#610](https://github.com/SwiftGen/SwiftGen/pull/610)
* Fonts: extract all codepoints of a font and make available to templates. Useful for icon fonts.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adjusting that CHANGELOG entry to reflect that this is opt-in

Would also rather mention that it extract "glyph names" rather than talking about codepoints, as glyph names are what are more relevant to users here

return descRefs.compactMap { desc -> Fonts.Font? in
let font = CTFontCreateWithFontDescriptorAndOptions(desc, 0.0, nil, [.preventAutoActivation])
let postScriptName = CTFontCopyPostScriptName(font) as String
guard let familyName = CTFontCopyAttribute(font, kCTFontFamilyNameAttribute) as? String,
let style = CTFontCopyAttribute(font, kCTFontStyleNameAttribute) as? String else { return nil }

let relPath = parent.flatMap { file.relative(to: $0) } ?? file

let characterSet = CTFontCopyCharacterSet(font) as CharacterSet
let cgFont = CTFontCopyGraphicsFont(font, nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put these 2 inside the if extractCodePoints?
That might not take that long to run (though maybe that does depending on the font file size?) but better avoid needless calls when the flag is off 🙃

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, not completely sure because maybe the bridging of that API to Swift takes care of that, but worth double-checking: since those two calls are …Copy… don't they need a balancing CTRelease (or CFRelease?) call or similar to avoid leaks?

for unicode in characterSet.codePoints {
guard let unicodeScalar = UnicodeScalar(unicode) else { continue }

let utf16codepoints = Array(Character(unicodeScalar).utf16)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if there isn't a more efficient way to go from codepoint to utf16 than CodePoint>UnicodeScalar>Character>.utf16 🤔
Will have to look at the various String/Character/Unicode APIs to double check, as this is often tricky stuff with edge cases so better be sure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is, and it's more correct: Array(unicodeScalar.utf16)


// Gets the Glyph
guard CTFontGetGlyphsForCharacters(font, utf16codepoints, &glyphs, utf16codepoints.count) == true else {
print("Failed to get glyph for character", unicode, utf16codepoints, unicodeScalar)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a logMessage function to generate warnings and errors during parsing, you should use that instead of print here

}

if glyphs.isEmpty {
print("Glyph set was empty!")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto re:logMessage

static let codePoints = ParserOption(
key: "codePoints",
defaultValue: "false",
help: "If enabled, will extract code points from the font. Only useful for icon fonts."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
help: "If enabled, will extract code points from the font. Only useful for icon fonts."
help: "If enabled, will extract the glyph names from the font. Only useful for icon fonts."

@@ -9,6 +9,14 @@ import Foundation
import PathKit

public enum Fonts {
public enum Option {
static let codePoints = ParserOption(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe name the option glyphNames instead of codePoints?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this parser option needs to be documented in Documentation/Parsers/fonts.md in the "Supported Options" section (you might need to merge develop into your branch first, as we renamed that documentation folder in a recent PR that was open and merged after you opened yours)

@@ -32,7 +41,13 @@ public enum Fonts {
return
}

let fonts = CTFont.parse(file: path, relativeTo: parent)
let getCodePoints: Bool = self.options[Option.codePoints] == "true"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check how we do string-to-boolean for other config flags. I don't think we have many Boolean Parser Options that we can use as reference but we do have some template parameters that are Boolean (e.g. forceNameSpaces for the Assets templates); if there is similar cases, better ensure we do the conversion the same way (== "true" vs != "false", case-insensitivity, etc)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might actually be a nice addition to add to ParserOptionValues by overloading the subscript si that if the return type is inverted to Bool, that ParserOptionValues is the one doing the string-to-Bool conversion. That would allow us to ensure that all future Boolean parser options are treated the same way.

"style": font.style
"style": font.style,
// swiftlint:disable:next force_unwrapping
"icons": font.icons.map { ["name": $0.key, "unicode": $0.value] }.sorted { $0["name"]! < $1["name"]! }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if the force-unwrap is technically never supposed to be nil in that specific case, you could avoid the SwiftLint violation using

($0["name"] ?? "") < ($1["name"] ?? "")

"style": font.style
"style": font.style,
// swiftlint:disable:next force_unwrapping
"icons": font.icons.map { ["name": $0.key, "unicode": $0.value] }.sorted { $0["name"]! < $1["name"]! }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to document that additional key in the documentation for contexts (in Documentation/StencilSwiftKit Contexts/fonts.md)

@AliSoftware
Copy link
Collaborator

AliSoftware commented Jun 12, 2020

Also, you mention in the PR description that you have a template that includes those glyph names but kept it as a gist and didn't include it in the PR, but I think it might be worth it to make some use of it.

I think we should either:

  • create a separate template eg iconfont-swift5.stencil that exposes those icons and glyph names in the generated code
  • or modify the existing templates so that they accept an includeGlyphNames parameter, or even better, maybe we don't need the explicit parameter and can just do something like {% if icons.empty %}// List of glyphs not generated. If this is an icon font and you want the list of glyphs there, use "option: glyphNames" as an option for the parser in your config file{% else %}let glyphNames = …code to generate the list{% endif %}
  • or keep the template as a gist but mention the URL to the gist somewhere in the documentation. But at that point I think we would be better off including the template in the built in templates

So I'd vote for 1 or 2, using solution 2 only if it's a small and localized change to the existing templates that won't make them too convoluted or to hard to maintain. Otherwise or if the code for icon fonts makes more sense to be very specific and different from normal fonts, go with 1 and have a dedicated template for them, which could actually make more sense in practice and would be easier to do given you already have a gist for it.

(Note that we could keep that addition of your template for icon fonts as a separate PR if you prefer to keep things separate and easier to review)

zacwest added a commit to home-assistant/iOS that referenced this pull request Jun 17, 2020
Once SwiftGen/SwiftGen#638 lands, we can update Iconic to use a newer version and everything becomes better. Fixes #663.
zacwest added a commit to home-assistant/iOS that referenced this pull request Jun 17, 2020
Once SwiftGen/SwiftGen#638 lands, we can update Iconic to use a newer version and everything becomes better. Fixes #663.
@AliSoftware
Copy link
Collaborator

So… I tested the code with SF-Text-Pro-Regular.otf (SFSymbols) and I've managed to make it list the glyph names but only the ones for the common textual characters. It won't output any of the SFSymbol names like, say, camera.fill or similar… worth investigating why and if there's a solution for that.

I've actually managed to get the unicode code point of one of the SFSymbols (drag & drop one of the icon from the SF Symbols macOS app into a new file in a text editor, save in utf16, use xxd to show the utf16 hex values) but when I try to call CTFontGetGlyphsForCharacters with that surrogate pair of utf16 code units, it fails (the method returns false). So no idea how we could get a CGGlyph for those characters. And cgFont.numberOfGlyphs returns 7645 which is the number of case that the current code generates, which means that this cgFont.numberOfGlyphs doesn't take the SFSymbol glyphs present in the SFSymbol font into account anyway… 🤔

@djbe djbe modified the milestones: 6.3.0, 6.4.0 Jul 6, 2020
@djbe djbe mentioned this pull request Aug 2, 2020
@djbe djbe changed the title Add support for extracting font code points for use in templates. Fonts: add support for extracting code points Oct 4, 2020
@djbe djbe modified the milestones: 6.5.0, Next minor (new features) Oct 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants