Module: Acrofill::Metrics

Defined in:
lib/acrofill/metrics.rb

Overview

Glyph widths (1/1000 em) for the standard-14 text fonts, indexed by WinAnsiEncoding code minus 32 (appearance streams are written as Windows-1252 bytes against a /WinAnsiEncoding font, so the byte emitted is the byte measured). Extracted from Adobe AFM metrics.

Only six width tables are stored: the oblique cuts of Helvetica have the same widths as the upright face, and the whole Courier family is monospaced. Vertical metrics (ascender, descender, FontBBox extent) are per cut, since they differ where widths do not.

Defined Under Namespace

Classes: Font

Constant Summary collapse

DEFAULT_WIDTH =
556
COURIER_WIDTH =
600
FIRST_CODE =
32
LAST_CODE =
255
WINDOWS_1252 =
Encoding::Windows_1252
BYTE_ENCODINGS =

Encodings whose bytes are already the codes the font is drawn with.

[Encoding::Windows_1252, Encoding::BINARY].freeze
IDENTITY =
Array.new(256) { |code| code }.freeze
CUTS =

The standard-14 text cuts, indexed by bold + 2 * italic per family.

{
  helvetica: %w[Helvetica Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique].freeze,
  times: %w[Times-Roman Times-Bold Times-Italic Times-BoldItalic].freeze,
  courier: %w[Courier Courier-Bold Courier-Oblique Courier-BoldOblique].freeze
}.freeze
SERIF =

Family and weight hints for BaseFont names outside the standard 14.

/times|roman|serif|georgia|garamond/
SANS =
/sans/
FIXED =

"mono" must not swallow the Monotype foundry prefix: MonotypeCorsiva is a proportional script face, not a typewriter one.

/courier|mono(?!type)/
BOLD =

Likewise "black" is a weight only at the end of a word — Blackadder and Blackoak are display faces, Arial-Black and Roboto-Black are weights.

/bold|black(?![a-z])|heavy/
ITALIC =
/italic|oblique/
TABLES =
{
  'Helvetica' => [
    278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556,
    556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667,
    667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722,
    667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500,
    556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278,
    556, 500, 722, 500, 500, 500, 334, 260, 334, 584, nil, 556, nil, 222, 556, 333, 1000,
    556, 556, 333, 1000, 667, 333, 1000, nil, 611, nil, nil, 222, 222, 333, 333, 350, 556,
    1000, 333, 1000, 500, 333, 944, nil, 500, 667, 278, 333, 556, 556, 556, 556, 260, 556,
    333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556, 537, 278, 333,
    333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667,
    667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722,
    722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556,
    278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556,
    500, 556, 500
  ].freeze,
  'Helvetica-Bold' => [
    278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556,
    556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722,
    722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722,
    667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556,
    611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333,
    611, 556, 778, 556, 556, 500, 389, 280, 389, 584, nil, 556, nil, 278, 556, 500, 1000,
    556, 556, 333, 1000, 667, 333, 1000, nil, 611, nil, nil, 278, 278, 500, 500, 350, 556,
    1000, 333, 1000, 556, 333, 944, nil, 500, 667, 278, 333, 556, 556, 556, 556, 280, 556,
    333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611, 556, 278, 333,
    333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667,
    667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722,
    722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556,
    278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611,
    556, 611, 556
  ].freeze,
  'Times-Roman' => [
    250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278, 500,
    500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722,
    667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667,
    556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444,
    500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500, 500, 500, 333, 389, 278,
    500, 500, 722, 500, 500, 444, 480, 200, 480, 541, nil, 500, nil, 333, 500, 444, 1000,
    500, 500, 333, 1000, 556, 333, 889, nil, 611, nil, nil, 333, 333, 444, 444, 350, 500,
    1000, 333, 980, 389, 333, 722, nil, 444, 722, 250, 333, 500, 500, 500, 500, 200, 500,
    333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300, 300, 333, 500, 453, 250, 333,
    300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722, 889, 667, 611, 611,
    611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722,
    722, 722, 722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444,
    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500,
    500, 500, 500
  ].freeze,
  'Times-Bold' => [
    250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500,
    500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722,
    667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722,
    556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444,
    556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500, 556, 556, 444, 389, 333,
    556, 500, 722, 500, 500, 444, 394, 220, 394, 520, nil, 500, nil, 333, 500, 500, 1000,
    500, 500, 333, 1000, 556, 333, 1000, nil, 667, nil, nil, 333, 333, 500, 500, 350, 500,
    1000, 333, 1000, 389, 333, 722, nil, 444, 722, 250, 333, 500, 500, 500, 500, 220, 500,
    333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300, 300, 333, 556, 540, 250, 333,
    300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667,
    667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722,
    722, 722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556,
    500, 556, 500
  ].freeze,
  'Times-Italic' => [
    250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278, 500,
    500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611,
    611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611,
    500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444,
    500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500, 500, 500, 389, 389, 278,
    500, 444, 667, 444, 444, 389, 400, 275, 400, 541, nil, 500, nil, 333, 500, 556, 889,
    500, 500, 333, 1000, 500, 333, 944, nil, 556, nil, nil, 333, 333, 556, 556, 350, 500,
    889, 333, 980, 389, 333, 667, nil, 389, 556, 250, 389, 500, 500, 500, 500, 275, 500,
    333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300, 300, 333, 500, 523, 250, 333,
    300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611, 889, 667, 611, 611,
    611, 611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722,
    722, 722, 556, 611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444,
    278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500,
    444, 500, 444
  ].freeze,
  'Times-BoldItalic' => [
    250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500,
    500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667,
    667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667,
    556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444,
    500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500, 500, 500, 389, 389, 278,
    556, 444, 667, 500, 444, 389, 348, 220, 348, 570, nil, 500, nil, 333, 500, 500, 1000,
    500, 500, 333, 1000, 556, 333, 944, nil, 611, nil, nil, 333, 333, 500, 500, 350, 500,
    1000, 333, 1000, 389, 333, 722, nil, 389, 611, 250, 389, 500, 500, 500, 500, 220, 500,
    333, 747, 266, 500, 606, 333, 747, 333, 400, 570, 300, 300, 333, 576, 500, 250, 333,
    300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667, 944, 667, 667, 667,
    667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722,
    722, 722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444,
    278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556,
    444, 500, 444
  ].freeze,
  'Courier' => Array.new(LAST_CODE - FIRST_CODE + 1, COURIER_WIDTH).freeze
}.freeze
WIDTHS =

Cuts that share a width table with another cut.

TABLES.merge(
  'Helvetica-Oblique' => TABLES['Helvetica'],
  'Helvetica-BoldOblique' => TABLES['Helvetica-Bold'],
  'Courier-Bold' => TABLES['Courier'],
  'Courier-Oblique' => TABLES['Courier'],
  'Courier-BoldOblique' => TABLES['Courier']
).freeze
VERTICAL =

[ascender, descender, FontBBox top, FontBBox bottom] in 1/1000 em, from the same Adobe AFM data as the widths. pdftk places baselines from the ascender and spaces multiline rows by the FontBBox extent, so these drive vertical geometry.

{
  'Helvetica' => [718, -207, 931, -225].freeze,
  'Helvetica-Bold' => [718, -207, 962, -228].freeze,
  'Helvetica-Oblique' => [718, -207, 931, -225].freeze,
  'Helvetica-BoldOblique' => [718, -207, 962, -228].freeze,
  'Times-Roman' => [683, -217, 898, -218].freeze,
  'Times-Bold' => [683, -217, 935, -218].freeze,
  'Times-Italic' => [683, -217, 883, -217].freeze,
  'Times-BoldItalic' => [683, -217, 921, -218].freeze,
  'Courier' => [629, -157, 805, -250].freeze,
  'Courier-Bold' => [629, -157, 801, -250].freeze,
  'Courier-Oblique' => [629, -157, 805, -250].freeze,
  'Courier-BoldOblique' => [629, -157, 801, -250].freeze
}.freeze
WIN_ANSI_GLYPHS =

WinAnsiEncoding glyph names, index = code - 32. Used to re-encode a value for a font whose /Encoding remaps codes: the value is turned into glyph names, then into the codes this font draws them at.

[
  :space, :exclam, :quotedbl, :numbersign, :dollar, :percent, :ampersand, :quotesingle,
  :parenleft, :parenright, :asterisk, :plus, :comma, :hyphen, :period, :slash, :zero,
  :one, :two, :three, :four, :five, :six, :seven, :eight, :nine, :colon, :semicolon,
  :less, :equal, :greater, :question, :at, :A, :B, :C, :D, :E, :F, :G, :H, :I, :J, :K,
  :L, :M, :N, :O, :P, :Q, :R, :S, :T, :U, :V, :W, :X, :Y, :Z, :bracketleft, :backslash,
  :bracketright, :asciicircum, :underscore, :grave, :a, :b, :c, :d, :e, :f, :g, :h, :i,
  :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z, :braceleft, :bar,
  :braceright, :asciitilde, :controlDEL, :Euro, nil, :quotesinglbase, :florin,
  :quotedblbase, :ellipsis, :dagger, :daggerdbl, :circumflex, :perthousand, :Scaron,
  :guilsinglleft, :OE, nil, :Zcaron, nil, nil, :quoteleft, :quoteright, :quotedblleft,
  :quotedblright, :bullet, :endash, :emdash, :tilde, :trademark, :scaron,
  :guilsinglright, :oe, nil, :zcaron, :Ydieresis, :space, :exclamdown, :cent, :sterling,
  :currency, :yen, :brokenbar, :section, :dieresis, :copyright, :ordfeminine,
  :guillemotleft, :logicalnot, :hyphen, :registered, :macron, :degree, :plusminus,
  :twosuperior, :threesuperior, :acute, :mu, :paragraph, :periodcentered, :cedilla,
  :onesuperior, :ordmasculine, :guillemotright, :onequarter, :onehalf, :threequarters,
  :questiondown, :Agrave, :Aacute, :Acircumflex, :Atilde, :Adieresis, :Aring, :AE,
  :Ccedilla, :Egrave, :Eacute, :Ecircumflex, :Edieresis, :Igrave, :Iacute, :Icircumflex,
  :Idieresis, :Eth, :Ntilde, :Ograve, :Oacute, :Ocircumflex, :Otilde, :Odieresis,
  :multiply, :Oslash, :Ugrave, :Uacute, :Ucircumflex, :Udieresis, :Yacute, :Thorn,
  :germandbls, :agrave, :aacute, :acircumflex, :atilde, :adieresis, :aring, :ae,
  :ccedilla, :egrave, :eacute, :ecircumflex, :edieresis, :igrave, :iacute, :icircumflex,
  :idieresis, :eth, :ntilde, :ograve, :oacute, :ocircumflex, :otilde, :odieresis,
  :divide, :oslash, :ugrave, :uacute, :ucircumflex, :udieresis, :yacute, :thorn,
  :ydieresis
].freeze
FONTS =
VERTICAL.to_h do |name, (asc, desc, top, bottom)|
  [name, build_font(WIDTHS[name], asc, desc, top, bottom)]
end.freeze

Class Method Summary collapse

Class Method Details

.build_font(widths, ascender, descender, top, bottom, remap = nil) ⇒ Object

A frozen Font, with its code-space width table derived once.



228
229
230
231
# File 'lib/acrofill/metrics.rb', line 228

def self.build_font(widths, ascender, descender, top, bottom, remap = nil)
  Font.new(widths, ascender, descender, top, bottom, remap,
           code_width_table(widths, remap)).freeze
end

.canonical_name(base_font) ⇒ Object

Maps any BaseFont name onto one of the twelve standard-14 text cuts.



320
321
322
323
324
325
326
327
328
329
330
# File 'lib/acrofill/metrics.rb', line 320

def self.canonical_name(base_font)
  name = base_font.to_s
  return name if FONTS.key?(name)

  lower = name.downcase
  family = if FIXED.match?(lower) then :courier
           elsif serif?(lower) then :times
           else :helvetica
           end
  CUTS[family][weight_index(lower)]
end

.code_width_table(widths, remap) ⇒ Object

Width of the glyph this font draws at each of the 256 byte codes. widths is indexed by WinAnsi code, but /Differences can move a glyph onto any code — including one below 32, which no WinAnsi-indexed table can express — and the appearance stream emits the moved code. Measuring in the font's own code space is what keeps rendering and metrics in step; a code the font draws nothing at is worth nothing, not the average glyph width the old WinAnsi lookup fell back to.



240
241
242
243
244
245
246
247
# File 'lib/acrofill/metrics.rb', line 240

def self.code_width_table(widths, remap)
  table = Array.new(256, 0)
  (FIRST_CODE..LAST_CODE).each do |code|
    width = widths[code - FIRST_CODE] || DEFAULT_WIDTH
    table[remap ? remap[code] : code] = width
  end
  table.freeze
end

.font_for(base_font) ⇒ Object

Metrics for a BaseFont name. Standard-14 names resolve directly; anything else (ArialMT, TimesNewRomanPS-BoldMT, a template's embedded face) is classified by family and weight, which is far closer than treating everything as Helvetica.



304
305
306
# File 'lib/acrofill/metrics.rb', line 304

def self.font_for(base_font)
  FONTS.fetch(canonical_name(base_font))
end

.remap_for(differences) ⇒ Object

A 256-entry code translation table for a font whose /Encoding moves glyphs off their WinAnsi codes, or nil when nothing moves. differences is => glyph name, as /Differences declares it.



265
266
267
268
269
270
271
272
273
274
# File 'lib/acrofill/metrics.rb', line 265

def self.remap_for(differences)
  return nil if differences.empty?

  at = drawn_at(differences)
  table = IDENTITY.dup
  WIN_ANSI_GLYPHS.each_with_index do |glyph, index|
    table[index + FIRST_CODE] = at[glyph] if at[glyph]
  end
  table == IDENTITY ? nil : table.freeze
end

.standard_font(base_font) ⇒ Object

Metrics for a name that is one of the standard 14, or nil. Unlike #font_for this does not classify: a template's own face only inherits standard-14 vertical metrics when it actually names one.



315
316
317
# File 'lib/acrofill/metrics.rb', line 315

def self.standard_font(base_font)
  FONTS[base_font.to_s]
end

.string_width(str, widths, size) ⇒ Object

Width of str at size points. widths is a 256-entry code-space table from #code_width_table, or a BaseFont name (resolved here, at the cost of the lookup).



256
257
258
259
260
# File 'lib/acrofill/metrics.rb', line 256

def self.string_width(str, widths, size)
  widths = font_for(widths).code_widths unless widths.is_a?(Array)
  units = to_win_ansi(str).each_byte.sum { |code| widths[code] }
  units * size / 1000.0
end

.widths_for(base_font) ⇒ Object



308
309
310
# File 'lib/acrofill/metrics.rb', line 308

def self.widths_for(base_font)
  font_for(base_font).widths
end