# -*- 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-2020 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'
require 'hexapdf/font/encoding'
require 'hexapdf/error'
module HexaPDF
module Font
# This class wraps a generic Type1 font object and provides the methods needed for working with
# the font in a PDF context.
class Type1Wrapper
# Represents a single glyph of the wrapped font.
class Glyph
# The name of the glyph.
attr_reader :name
alias id name
# The string representation of the glyph.
attr_reader :str
# Creates a new Glyph object.
def initialize(font, name, str)
@font = font
@name = name
@str = str
end
# Returns the glyph's minimum x coordinate.
def x_min
@font.metrics.character_metrics[name].bbox[0]
end
# Returns the glyph's maximum x coordinate.
def x_max
@font.metrics.character_metrics[name].bbox[2]
end
# Returns the glyph's minimum y coordinate.
def y_min
@font.metrics.character_metrics[name].bbox[1]
end
# Returns the glyph's maximum y coordinate.
def y_max
@font.metrics.character_metrics[name].bbox[3]
end
# Returns the width of the glyph.
def width
@width ||= @font.width(name)
end
# Returns +true+ if the word spacing parameter needs to be applied for the glyph.
def apply_word_spacing?
@name == :space
end
#:nodoc:
def inspect
"#<#{self.class.name} font=#{@font.full_name.inspect} id=#{name.inspect} #{str.inspect}>"
end
end
private_constant :Glyph
# Returns the wrapped Type1 font object.
attr_reader :wrapped_font
# Returns the PDF object associated with the wrapper.
attr_reader :pdf_object
# Creates a new Type1Wrapper object wrapping the Type1 font.
#
# The optional argument +pdf_object+ can be used to set the PDF font object that this wrapper
# should be associated with. If no object is set, a suitable one is automatically created.
#
# If +pdf_object+ is provided, the PDF object's encoding is used. Otherwise, the
# WinAnsiEncoding or, for 'Special' fonts, the font's internal encoding is used. The optional
# argument +custom_encoding+ can be set to +true+ so that a custom encoding is used (only
# respected if +pdf_object+ is not provided).
def initialize(document, font, pdf_object: nil, custom_encoding: false)
@wrapped_font = font
@pdf_object = pdf_object || create_pdf_object(document)
@missing_glyph_callable = document.config['font.on_missing_glyph']
if pdf_object
@encoding = pdf_object.encoding
@max_code = 255 # Encoding is not modified
elsif custom_encoding
@encoding = Encoding::Base.new
@encoding.code_to_name[32] = :space
@max_code = 32 # 32 = space
elsif @wrapped_font.metrics.character_set == 'Special'
@encoding = @wrapped_font.encoding
@max_code = 255 # Encoding is not modified
else
@encoding = Encoding.for_name(:WinAnsiEncoding)
@max_code = 255 # Encoding is not modified
end
@zapf_dingbats_opt = {zapf_dingbats: (@wrapped_font.font_name == 'ZapfDingbats')}
@name_to_glyph = {}
@codepoint_to_glyph = {}
@encoded_glyphs = {}
end
# Returns the type of the font, i.e. :Type1.
def font_type
:Type1
end
# Returns 1 since all Type1 fonts use 1000 units for the em-square.
def scaling_factor
1
end
# Returns a Glyph object for the given glyph name.
def glyph(name)
@name_to_glyph[name] ||=
begin
str = Encoding::GlyphList.name_to_unicode(name, **@zapf_dingbats_opt)
if @wrapped_font.metrics.character_metrics.key?(name)
Glyph.new(@wrapped_font, name, str)
else
@missing_glyph_callable.call(str, font_type, @wrapped_font)
end
end
end
# Returns an array of glyph objects representing the characters in the UTF-8 encoded string.
#
# If a Unicode codepoint is not available as glyph object, it is tried to map the codepoint
# using the font's internal encoding. This is useful, for example, for the ZapfDingbats font
# to use ASCII characters for accessing the glyphs.
def decode_utf8(str)
str.codepoints.map! do |c|
@codepoint_to_glyph[c] ||=
begin
name = Encoding::GlyphList.unicode_to_name(+'' << c, **@zapf_dingbats_opt)
if @wrapped_font.metrics.character_set == 'Special' &&
(name == :'.notdef' || !@wrapped_font.metrics.character_metrics.key?(name))
name = @encoding.name(c)
end
name = +"u" << c.to_s(16).rjust(6, '0') if name == :'.notdef'
glyph(name)
end
end
end
# Encodes the glyph and returns the code string.
def encode(glyph)
@encoded_glyphs[glyph.name] ||=
begin
if glyph.name == @wrapped_font.missing_glyph_id
raise HexaPDF::Error, "Glyph for #{glyph.str.inspect} missing"
end
code = @encoding.code(glyph.name)
if code
code.chr.freeze
elsif @max_code < 255
@max_code += 1
@encoding.code_to_name[@max_code] = glyph.name
@max_code.chr.freeze
else
raise HexaPDF::Error, "Type1 encoding has no codepoint for #{glyph.name}"
end
end
end
private
# Array of valid encoding names in PDF
VALID_ENCODING_NAMES = [:WinAnsiEncoding, :MacRomanEncoding, :MacExpertEncoding].freeze
# Creates a PDF object representing the wrapped font for the given PDF document.
def create_pdf_object(document)
fd = document.wrap({Type: :FontDescriptor,
FontName: @wrapped_font.font_name.intern,
FontWeight: @wrapped_font.weight_class,
FontBBox: @wrapped_font.bounding_box,
ItalicAngle: @wrapped_font.italic_angle || 0,
Ascent: @wrapped_font.ascender || 0,
Descent: @wrapped_font.descender || 0,
CapHeight: @wrapped_font.cap_height,
XHeight: @wrapped_font.x_height,
StemH: @wrapped_font.dominant_horizontal_stem_width,
StemV: @wrapped_font.dominant_vertical_stem_width || 0})
fd.flag(:fixed_pitch) if @wrapped_font.metrics.is_fixed_pitch
fd.flag(@wrapped_font.metrics.character_set == 'Special' ? :symbolic : :nonsymbolic)
fd.must_be_indirect = true
dict = document.wrap({Type: :Font, Subtype: :Type1,
BaseFont: @wrapped_font.font_name.intern,
FontDescriptor: fd})
dict.font_wrapper = self
document.register_listener(:complete_objects) do
min, max = @encoding.code_to_name.keys.minmax
dict[:FirstChar] = min
dict[:LastChar] = max
dict[:Widths] = (min..max).map {|code| glyph(@encoding.name(code)).width }
if VALID_ENCODING_NAMES.include?(@encoding.encoding_name)
dict[:Encoding] = @encoding.encoding_name
elsif @encoding != @wrapped_font.encoding
differences = [min]
(min..max).each {|code| differences << @encoding.name(code) }
dict[:Encoding] = {Differences: differences}
end
end
dict
end
end
end
end