# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2022 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/font/type1/font_metrics' require 'hexapdf/error' module HexaPDF module Font module Type1 # Parses files in the AFM file format. # # Note that this implementation isn't a full AFM parser, only what is needed for parsing the # AFM files for the 14 PDF core fonts is implemented. However, if need be it should be # adaptable to other AFM files. # # For information on the AFM file format have a look at Adobe technical note #5004 - Adobe # Font Metrics File Format Specification Version 4.1, available at the Adobe website. # # == How Parsing Works # # AFM is a line oriented format. Each line consists of one or more values of supported types # (string, name, number, integer, array, boolean) which are separated by whitespace characters # (space, newline, tab) except for the string type which just uses everything until the end of # the line. # # This parser reads in line by line and the type parsing functions parse a value from the # front of the line and then remove the parsed part from the line, including trailing # whitespace characters. class AFMParser # :call-seq: # Parser.parse(filename) -> font_metrics # Parser.parse(io) -> font_metrics # # Parses the IO or file and returns a FontMetrics object. def self.parse(source) if source.respond_to?(:read) new(source).parse else File.open(source) {|file| new(file).parse } end end # Creates a new parse for the given IO stream. def initialize(io) @io = io end # Parses the AFM file and returns a FontMetrics object. def parse @metrics = FontMetrics.new sections = [] each_line do case (command = parse_name) when /\AStart/ sections.push(command) case command when 'StartCharMetrics' then parse_character_metrics when 'StartKernPairs' then parse_kerning_pairs end when /\AEnd/ sections.pop break if sections.empty? && command == 'EndFontMetrics' else if sections.empty? parse_global_font_information(command.to_sym) end end end if @metrics.bounding_box && !@metrics.descender @metrics.descender = @metrics.bounding_box[1] end if @metrics.bounding_box && !@metrics.ascender @metrics.ascender = @metrics.bounding_box[3] end @metrics end private # Parses global font information line for the given +command+ (a symbol). # # It is assumed that the command name has already been parsed from the line. # # Note that writing direction metrics are also processed here since the standard 14 core # fonts' AFM files don't have an extra StartDirection section. def parse_global_font_information(command) case command when :FontName then @metrics.font_name = parse_string when :FullName then @metrics.full_name = parse_string when :FamilyName then @metrics.family_name = parse_string when :CharacterSet then @metrics.character_set = parse_string when :EncodingScheme then @metrics.encoding_scheme = parse_string when :Weight then @metrics.weight = parse_string when :FontBBox @metrics.bounding_box = [parse_number, parse_number, parse_number, parse_number] when :CapHeight then @metrics.cap_height = parse_number when :XHeight then @metrics.x_height = parse_number when :Ascender then @metrics.ascender = parse_number when :Descender then @metrics.descender = parse_number when :StdHW then @metrics.dominant_horizontal_stem_width = parse_number when :StdVW then @metrics.dominant_vertical_stem_width = parse_number when :UnderlinePosition then @metrics.underline_position = parse_number when :UnderlineThickness then @metrics.underline_thickness = parse_number when :ItalicAngle then @metrics.italic_angle = parse_number when :IsFixedPitch then @metrics.is_fixed_pitch = parse_boolean end end # Parses the character metrics in a StartCharMetrics section. # # It is assumed that the StartCharMetrics name has already been parsed from the line. def parse_character_metrics parse_integer.times do read_line char = CharacterMetrics.new if @line =~ /C (\S+) ; WX (\S+) ; N (\S+) ; B (\S+) (\S+) (\S+) (\S+) ;((?: L \S+ \S+ ;)+)?/ char.code = $1.to_i char.width = $2.to_f char.name = $3.to_sym char.bbox = [$4.to_i, $5.to_i, $6.to_i, $7.to_i] if $8 @metrics.ligature_pairs[char.name] = {} $8.scan(/L (\S+) (\S+)/).each do |name, ligature| @metrics.ligature_pairs[char.name][name.to_sym] = ligature.to_sym end end end @metrics.character_metrics[char.name] = char if char.name @metrics.character_metrics[char.code] = char if char.code != -1 end end # Parses the kerning pairs in a StartKernPairs section. # # It is assumed that the StartKernPairs name has already been parsed from the line. def parse_kerning_pairs parse_integer.times do read_line if @line =~ /KPX (\S+) (\S+) (\S+)/ (@metrics.kerning_pairs[$1.to_sym] ||= {})[$2.to_sym] = $3.to_i end end end # Iterates over all the lines in the IO, yielding every time a line has been read into the # internal buffer. def each_line read_line unless parse_name == 'StartFontMetrics' raise HexaPDF::Error, "The AFM file has to start with StartFontMetrics, not #{@line}" end until @io.eof? read_line yield end end # Reads the next line into the current line variable. def read_line @line = @io.readline end # Parses and returns the name at the start of the line, with whitespace stripped. def parse_name result = @line[/\S+\s*/].to_s @line[0, result.size] = '' result.strip! result end # Returns the rest of the line, with whitespace stripped. def parse_string @line.strip! line = @line @line = '' line end # Parses the integer at the start of the line. def parse_integer parse_name.to_i end # Parses the float number at the start of the line. def parse_number parse_name.to_f end # Parses the boolean at the start of the line. def parse_boolean parse_name == 'true' end end end end end