Class: Fontisan::Ufo::Glyph

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/ufo/glyph.rb

Overview

A glyph in a UFO source. Holds contours, components, anchors, guidelines, images, unicode codepoints, advance width/height, and a custom-data bag (lib).

The .glif XML format has multiple revisions (1, 2, 3); the parser accepts all of them.

Constant Summary collapse

DEFAULT_UNITS_PER_EM =

OpenType requires GID 0 to be a glyph named .notdef. UFO sources that lack an explicit .notdef (synthetic SVG donors, programmatic sources) need one injected before compile. This constant is the default width used for that injected glyph, and doubles as the fallback unitsPerEm in Head.build when UFO info omits the field. Single source of truth for both call sites.

1000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:) ⇒ Glyph

Returns a new instance of Glyph.



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/fontisan/ufo/glyph.rb', line 42

def initialize(name:)
  @name = name.to_s
  @unicodes = []
  @width = 0.0
  @height = 0.0
  @contours = []
  @components = []
  @anchors = []
  @guidelines = []
  @images = []
  @note = nil
  @lib = Lib.new
end

Instance Attribute Details

#anchorsObject (readonly)

Returns the value of attribute anchors.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def anchors
  @anchors
end

#componentsObject (readonly)

Returns the value of attribute components.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def components
  @components
end

#contoursObject (readonly)

Returns the value of attribute contours.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def contours
  @contours
end

#guidelinesObject (readonly)

Returns the value of attribute guidelines.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def guidelines
  @guidelines
end

#heightObject

Returns the value of attribute height.



38
39
40
# File 'lib/fontisan/ufo/glyph.rb', line 38

def height
  @height
end

#imagesObject (readonly)

Returns the value of attribute images.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def images
  @images
end

#libObject

Returns the value of attribute lib.



38
39
40
# File 'lib/fontisan/ufo/glyph.rb', line 38

def lib
  @lib
end

#nameObject (readonly)

Returns the value of attribute name.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def name
  @name
end

#noteObject

Returns the value of attribute note.



38
39
40
# File 'lib/fontisan/ufo/glyph.rb', line 38

def note
  @note
end

#unicodesObject (readonly)

Returns the value of attribute unicodes.



39
40
41
# File 'lib/fontisan/ufo/glyph.rb', line 39

def unicodes
  @unicodes
end

#widthObject

Returns the value of attribute width.



38
39
40
# File 'lib/fontisan/ufo/glyph.rb', line 38

def width
  @width
end

Class Method Details

.from_glif(xml) ⇒ Glyph

Returns parsed glyph.

Parameters:

  • xml (String)

    the .glif XML body

Returns:

  • (Glyph)

    parsed glyph



112
113
114
115
116
# File 'lib/fontisan/ufo/glyph.rb', line 112

def self.from_glif(xml)
  doc = Nokogiri::XML(xml)
  root = doc.root
  new(name: root["name"]).tap { |g| g.read_from(root) }
end

.notdef(units_per_em: nil) ⇒ Glyph

Synthesize an empty .notdef glyph suitable for GID 0.

OpenType mandates GID 0 be .notdef. UFO sources that lack an explicit .notdef (synthetic SVG donors, programmatic sources) need one injected before compile. The width defaults to the font's units_per_em so the .notdef consumes one em of advance.

Parameters:

  • units_per_em (Integer, nil) (defaults to: nil)

    Advance width for the synthesized glyph. Defaults to DEFAULT_UNITS_PER_EM when nil.

Returns:



32
33
34
35
36
# File 'lib/fontisan/ufo/glyph.rb', line 32

def self.notdef(units_per_em: nil)
  new(name: ".notdef").tap do |g|
    g.width = units_per_em&.to_i || DEFAULT_UNITS_PER_EM
  end
end

Instance Method Details

#add_anchor(anchor) ⇒ Object



70
71
72
73
# File 'lib/fontisan/ufo/glyph.rb', line 70

def add_anchor(anchor)
  @anchors << anchor
  anchor
end

#add_component(component) ⇒ Object



65
66
67
68
# File 'lib/fontisan/ufo/glyph.rb', line 65

def add_component(component)
  @components << component
  component
end

#add_contour(contour) ⇒ Object



60
61
62
63
# File 'lib/fontisan/ufo/glyph.rb', line 60

def add_contour(contour)
  @contours << contour
  contour
end

#add_guideline(guideline) ⇒ Object



75
76
77
78
# File 'lib/fontisan/ufo/glyph.rb', line 75

def add_guideline(guideline)
  @guidelines << guideline
  guideline
end

#add_image(image) ⇒ Object



80
81
82
83
# File 'lib/fontisan/ufo/glyph.rb', line 80

def add_image(image)
  @images << image
  image
end

#add_unicode(codepoint) ⇒ Object



56
57
58
# File 'lib/fontisan/ufo/glyph.rb', line 56

def add_unicode(codepoint)
  @unicodes << codepoint.to_i
end

#bboxBoundingBox?

Returns axis-aligned bbox of contours.

Returns:

  • (BoundingBox, nil)

    axis-aligned bbox of contours.



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fontisan/ufo/glyph.rb', line 96

def bbox
  return nil if @contours.empty?

  points = @contours.flat_map(&:points)
  return nil if points.empty?

  BoundingBox.new(
    x_min: points.map(&:x).min,
    y_min: points.map(&:y).min,
    x_max: points.map(&:x).max,
    y_max: points.map(&:y).max,
  )
end

#composite?Boolean

Composite glyphs reference other glyphs via components.

Returns:

  • (Boolean)


86
87
88
# File 'lib/fontisan/ufo/glyph.rb', line 86

def composite?
  !@components.empty?
end

#point_countObject

Total number of points across all contours.



91
92
93
# File 'lib/fontisan/ufo/glyph.rb', line 91

def point_count
  @contours.sum(&:point_count)
end

#read_from(root) ⇒ Object

Populate this glyph from a Nokogiri node.



134
135
136
137
138
139
140
141
142
143
# File 'lib/fontisan/ufo/glyph.rb', line 134

def read_from(root)
  read_advance(root)
  read_unicodes(root)
  read_outline(root)
  read_anchors(root)
  read_guidelines(root)
  read_image(root)
  read_lib(root)
  read_note(root)
end

#to_glifString

Render this glyph back to .glif XML.

Returns:

  • (String)

    XML text



120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/fontisan/ufo/glyph.rb', line 120

def to_glif
  builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
    xml.glyph(name: @name, format: "2") do
      emit_advance(xml)
      emit_unicodes(xml)
      emit_outline(xml)
      emit_lib(xml)
      emit_note(xml)
    end
  end
  builder.to_xml
end

#to_outlineFontisan::Models::Outline

Convert this glyph to a fontisan Models::Outline for use by the CFF charstring builder.

UFO contours use cubic Bezier curves ("curve" type with two off-curve controls). Single off-curves are interpreted as quadratic and degree-elevated to cubic.



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
# File 'lib/fontisan/ufo/glyph.rb', line 153

def to_outline
  commands = []
  bbox_hash = { x_min: 0, y_min: 0, x_max: 0, y_max: 0 }

  @contours.each do |contour|
    next if contour.points.empty?

    points = contour.points
    commands << { type: :move_to, x: points.first.x, y: points.first.y }

    i = 1
    while i < points.size
      pt = points[i]
      if pt.on_curve?
        commands << { type: :line_to, x: pt.x, y: pt.y }
        i += 1
      elsif i + 2 < points.size &&
          !points[i + 1].on_curve? && points[i + 2].on_curve?
        commands << {
          type: :curve_to,
          cx1: points[i].x, cy1: points[i].y,
          cx2: points[i + 1].x, cy2: points[i + 1].y,
          x: points[i + 2].x, y: points[i + 2].y
        }
        i += 3
      elsif i + 1 < points.size && points[i + 1].on_curve?
        prev = points[i - 1]
        nxt = points[i + 1]
        cx1 = prev.x + (2.0 / 3.0) * (pt.x - prev.x)
        cy1 = prev.y + (2.0 / 3.0) * (pt.y - prev.y)
        cx2 = nxt.x + (2.0 / 3.0) * (pt.x - nxt.x)
        cy2 = nxt.y + (2.0 / 3.0) * (pt.y - nxt.y)
        commands << {
          type: :curve_to,
          cx1: cx1, cy1: cy1, cx2: cx2, cy2: cy2,
          x: nxt.x, y: nxt.y
        }
        i += 2
      else
        i += 1
      end
    end

    commands << { type: :close_path }
  end

  bb = bbox
  if bb
    bbox_hash = {
      x_min: bb.x_min.to_i, y_min: bb.y_min.to_i,
      x_max: bb.x_max.to_i, y_max: bb.y_max.to_i
    }
  end

  Fontisan::Models::Outline.new(
    glyph_id: 0,
    commands: commands,
    bbox: bbox_hash,
    width: @width.to_i,
  )
end