require 'prawn/encoding' module Prawn class Font class AFM < Font BUILT_INS = %w[ Courier Helvetica Times-Roman Symbol ZapfDingbats Courier-Bold Courier-Oblique Courier-BoldOblique Times-Bold Times-Italic Times-BoldItalic Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique ] def self.metrics_path if m = ENV['METRICS'] @metrics_path ||= m.split(':') else @metrics_path ||= [ ".", "/usr/lib/afm", "/usr/local/lib/afm", "/usr/openwin/lib/fonts/afm/", Prawn::BASEDIR+'/data/fonts/'] end end attr_reader :attributes def initialize(document, name, options={}) unless BUILT_INS.include?(name) raise Prawn::Errors::UnknownFont, "#{name} is not a known font." end super @attributes = {} @glyph_widths = {} @bounding_boxes = {} @kern_pairs = {} file_name = @name.dup file_name << ".afm" unless file_name =~ /\.afm$/ file_name = file_name[0] == ?/ ? file_name : find_font(file_name) parse_afm(file_name) @ascender = @attributes["ascender"].to_i @descender = @attributes["descender"].to_i @line_gap = Float(bbox[3] - bbox[1]) - (@ascender - @descender) end def bbox @bbox ||= @attributes['fontbbox'].split(/\s+/).map { |e| Integer(e) } end # calculates the width of the supplied string. # # String *must* be encoded as WinAnsi # def width_of(string, options={}) scale = (options[:size] || size) / 1000.0 if options[:kerning] no_kern_opts = options.merge(:kerning => false) kern(string).inject(0) do |s,r| if r.is_a? String s + width_of(r, no_kern_opts) else s - (r * scale) end end else string.unpack("C*").inject(0) do |s,r| s + latin_glyphs_table[r] end * scale end end def has_kerning_data? @kern_pairs.any? end # built-in fonts only work with winansi encoding, so translate the # string. Changes the encoding in-place, so the argument itself # is replaced with a string in WinAnsi encoding. def normalize_encoding(text) enc = Prawn::Encoding::WinAnsi.new text.replace text.unpack("U*").collect { |i| enc[i] }.pack("C*") end # Perform any changes to the string that need to happen # before it is rendered to the canvas. Returns an array of # subset "chunks", where each chunk is an array of two elements. # The first element is the font subset number, and the second # is either a string or an array (for kerned text). # # For Adobe fonts, there is only ever a single subset, so # the first element of the array is "0", and the second is # the string itself (or an array, if kerning is performed). # # The +text+ parameter must be in WinAnsi encoding (cp1252). def encode_text(text, options={}) [[0, options[:kerning] ? kern(text) : text]] end private def register(subset) @document.ref(:Type => :Font, :Subtype => :Type1, :BaseFont => name.to_sym, :Encoding => :WinAnsiEncoding) end def find_font(file) self.class.metrics_path.find { |f| File.exist? "#{f}/#{file}" } + "/#{file}" rescue NoMethodError raise Prawn::Errors::UnknownFont, "Couldn't find the font: #{file} in any of:\n" + self.class.metrics_path.join("\n") end def parse_afm(file_name) section = [] File.foreach(file_name) do |line| case line when /^Start(\w+)/ section.push $1 next when /^End(\w+)/ section.pop next end case section when ["FontMetrics", "CharMetrics"] next unless line =~ /^CH?\s/ name = line[/\bN\s+(\.?\w+)\s*;/, 1] @glyph_widths[name] = line[/\bWX\s+(\d+)\s*;/, 1].to_i @bounding_boxes[name] = line[/\bB\s+([^;]+);/, 1].to_s.rstrip when ["FontMetrics", "KernData", "KernPairs"] next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/ @kern_pairs[[$1, $2]] = $3.to_i when ["FontMetrics", "KernData", "TrackKern"], ["FontMetrics", "Composites"] next else parse_generic_afm_attribute(line) end end end def parse_generic_afm_attribute(line) line =~ /(^\w+)\s+(.*)/ key, value = $1.to_s.downcase, $2 @attributes[key] = @attributes[key] ? Array(@attributes[key]) << value : value end # converts a string into an array with spacing offsets # bewteen characters that need to be kerned # # String *must* be encoded as WinAnsi # def kern(string) kerned = string.unpack("C*").inject([]) do |a,r| if a.last.is_a? Array if k = latin_kern_pairs_table[[a.last.last, r]] a << k << [r] else a.last << r end else a << [r] end a end kerned.map { |r| i = r.is_a?(Array) ? r.pack("C*") : r i.force_encoding("Windows-1252") if i.respond_to?(:force_encoding) i.is_a?(Numeric) ? -i : i } end def latin_kern_pairs_table @kern_pairs_table ||= @kern_pairs.inject({}) do |h,p| h[p[0].map { |n| Encoding::WinAnsi::CHARACTERS.index(n) }] = p[1] h end end def latin_glyphs_table @glyphs_table ||= (0..255).map do |i| @glyph_widths[Encoding::WinAnsi::CHARACTERS[i]].to_i end end end end end