require 'set' # Font metadata extraction require 'ttfunk' require 'distorted/modular_technology/pango' require 'distorted/modular_technology/ttfunk' require 'distorted/modular_technology/vips_save' require 'distorted/checking_you_out' require 'distorted/injection_of_love' module Cooltrainer module DistorteD module Font # TODO: Test OTF, OTB, and others. # NOTE: Traditional bitmap fonts won't be supported due to Pango 1.44 # and later switching to Harfbuzz from Freetype: # https://gitlab.gnome.org/GNOME/pango/-/issues/386 # https://blogs.gnome.org/mclasen/2019/05/25/pango-future-directions/ LOWER_WORLD = CHECKING::YOU::IN(/^font\/ttf/) OUTER_LIMITS = CHECKING::YOU::IN(/^font\/ttf/) ATTRIBUTES = Set[ :alt, ] ATTRIBUTES_VALUES = { } ATTRIBUTES_DEFAULT = { } # Maybe T0DO: Process output with TTFunk instead of only using it # to generate images and metadata. self::OUTER_LIMITS.each { |t| define_method(t.distorted_method) { |*a, **k, &b| copy_file(*a, **k, &b) } } include Cooltrainer::DistorteD::Technology::TTFunk include Cooltrainer::DistorteD::Technology::Pango include Cooltrainer::DistorteD::Technology::VipsSave include Cooltrainer::DistorteD::InjectionOfLove # irb(main):089:0> chars.take(5) # => [[1, 255], [2, 1], [3, 2], [4, 3], [5, 4]] # irb(main):090:0> chars.values.take(5) # => [255, 1, 2, 3, 4] # irb(main):091:0> chars.values.map(&:chr).take(5) # => ["\xFF", "\x01", "\x02", "\x03", "\x04"] def to_pango output = '' << cr << '' << cr output << " #{font_name}" << cr << cr output << " #{font_description}" << cr output << " #{font_copyright}" << cr output << " #{font_version}" << cr << cr # Print a preview String in using the loaded font. Or don't. if abstract(:title) output << cr << cr << " #{g_markup_escape_text(abstract(:title))}" << cr << cr << cr end # /!\ MANDATORY READING /!\ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html # # "The 'cmap' table maps character codes to glyph indices. # The choice of encoding for a particular font is dependent upon the conventions # used by the intended platform. A font intended to run on multiple platforms # with different encoding conventions will require multiple encoding tables. # As a result, the 'cmap' table may contain multiple subtables, # one for each supported encoding scheme." # # Cmap#unicode is a convenient shortcut to sorting the subtables # and removing any unusable ones: # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap.rb # # irb(main):174:0> font_meta.cmap.tables.count # => 3 # irb(main):175:0> font_meta.cmap.unicode.count # => 2 to_ttfunk.cmap.tables.each do |table| next if !table.unicode? # Each subtable's `code_map` is a Hash map of character codes (the Hash keys) # to the glyph IDs from the original font (the Hash's values). # # Subtable::encode takes: # - a Hash mapping character codes to original font glyph IDs. # - the desired output encoding — Set[:mac_roman, :unicode, :unicode_ucs4] # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb # …and returns a Hash with keys: # - :charmap — Hash mapping the characters in the input charmap # to a another hash containing both the `:old` # and `:new` glyph ids for each character code. # - :subtable — String encoded subtable for the given encoding. encoded = TTFunk::Table::Cmap::Subtable::encode(table&.code_map, :unicode).dig(:charmap) output << "" i = 0 encoded.each_pair { |c, (old, new)| begin if glyph = to_ttfunk.glyph_outlines.for(c) # Add a space on either side of the character so they aren't # all smooshed up against each other and unreadable. output << ' ' << g_markup_escape_char(c) << ' ' if i >= 15 output << cr i = 0 else i = i + 1 end else end rescue NoMethodError => nme # TTFunk's `glyph_outlines.for()` will raise this if we call it # for a codepoint that does not exist in the font, which we will # not do because we are enumerating the codepoints in the font, # but we should still handle the possibility. # irb(main):060:0> font.glyph_outlines.for(555555) # # Traceback (most recent call last): # 6: from /usr/bin/irb:23:in `
' # 5: from /usr/bin/irb:23:in `load' # 4: from /home/okeeblow/.gems/gems/irb-1.2.4/exe/irb:11:in `' # 3: from (irb):60 # 2: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/glyf.rb:35:in `for' # 1: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/loca.rb:35:in `size_of' # NoMethodError (undefined method `-' for nil:NilClass) end } output << '' << cr end output << '' output end # Return the `src` as the font_path since we aren't using # any of the built-in fonts. def font_path path end def to_vips_image # https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text Vips::Image.text( # This string must be well-escaped Pango Markup: # https://developer.gnome.org/pango/stable/pango-Markup.html # However the official function for escaping text is # not implemented in Ruby GLib, so we have to do it ourselves. to_pango, **{ # String absolute path to TTF :fontfile => font_path, # It's not enough to just specify the TTF path; # we must also specify a font family, subfamily, and size. :font => "#{font_name}", # Space between lines (in Points). :spacing => to_ttfunk.line_gap, # Requires libvips 8.8 :justify => false, :dpi => 144, }, ) end end # Font end # DistorteD end # Cooltrainer # Notes on file-format specifics and software-library-specifics # # # TTF (via TTFunk) # # ## Cmap # # Each TTFunk::Table::Cmap::Format class responds to `:supported?` # with its own internal boolean telling us if that Format is usable in TTFunk. # This has nothing to do with any font file itself, just the library code. # irb(main)> font.cmap.tables.map{|t| t.supported?} # => [true, true, true] # # Any subclass of TTFunk::Table::Cmap::Subtable responds to `:unicode?` # with a boolean calculated from the instance `@platform_id` and `@encoding_id`, # and those numeric IDs are assigned to the symbolic (e.g. `:macroman`) names in: # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb # irb(main)> font.cmap.tables.map{|t| t.unicode?} # => [true, false, true] #