# -*- 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-2023 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/true_type/table'
module HexaPDF
module Font
module TrueType
class Table
# The 'name' table contains the human-readable names for features, font names, style names,
# copyright notices and so on.
#
# See: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
class Name < Table
# Table for mapping symbolic names to name_id codes.
NAME_MAP = {
copyright: 0,
font_family: 1,
font_subfamily: 2,
unique_subfamily: 3,
font_name: 4,
version: 5,
postscript_name: 6,
trademark: 7,
manufacturer: 8,
designer: 9,
description: 10,
vendor_url: 11,
designer_url: 12,
license: 13,
license_url: 14,
preferred_family: 16,
preferred_subfamily: 17,
compatible_full: 18,
sample_text: 19,
postscript_cid_name: 20,
wws_family: 21,
wws_subfamily: 22,
}.freeze
# Contains the information for a Name Record.
#
# The string value is converted to UTF-8 if possible, otherwise it stays in BINARY.
class Record < String
# Indicates Unicode version.
PLATFORM_UNICODE = 0
# QuickDraw Script Manager code for Macintosh.
PLATFORM_MACINTOSH = 1
# Microsoft encoding.
PLATFORM_MICROSOFT = 3
# The platform identifier code.
attr_reader :platform_id
# The platform specific encoding identified.
attr_reader :encoding_id
# The language identified.
attr_reader :language_id
# Create a new name record.
def initialize(text, pid, eid, lid)
@platform_id = pid
@encoding_id = eid
@language_id = lid
if platform?(:unicode) ||
(platform?(:microsoft) && encoding_id == 1 || encoding_id == 10)
text.encode!(::Encoding::UTF_8, ::Encoding::UTF_16BE)
elsif platform?(:macintosh) && encoding_id == 0
text.encode!(::Encoding::UTF_8, ::Encoding::MACROMAN)
end
super(text)
end
# Returns +true+ if this record has the given platform identifier which can either be
# :unicode, :macintosh or :microsoft.
def platform?(identifier)
platform_id == case identifier
when :unicode then PLATFORM_UNICODE
when :macintosh then PLATFORM_MACINTOSH
when :microsoft then PLATFORM_MICROSOFT
else
raise ArgumentError, "Unknown platform identifier: #{identifier}"
end
end
# Returns +true+ if this record is a "preferred" one.
#
# The label "preferred" is set on a name if it represents the US English version of the
# name in a decodable encoding:
# * platform_id :macintosh, encoding_id 0 (Roman) and language_id 0 (English); or
# * platform_id :microsoft, encoding_id 1 (Unicode) and language_id 1033 (US English).
def preferred?
(platform_id == PLATFORM_MACINTOSH && encoding_id == 0 && language_id == 0) ||
(platform_id == PLATFORM_MICROSOFT && encoding_id == 1 && language_id == 1033)
end
end
# Holds records for the same name type (e.g. :font_name, :postscript_name, ...).
class Records < Array
# Returns the preferred record in this collection.
#
# This is either the first record where Record#preferred? is true or else just the first
# record in the collection.
def preferred_record
find(&:preferred?) || self[0]
end
end
# The format of the table.
attr_accessor :format
# The name records.
attr_accessor :records
# The mapping of language IDs starting from 0x8000 to language tags conforming to IETF BCP
# 47.
attr_accessor :language_tags
# Returns an array with all available entries for the given name identifier (either a
# symbol or an ID).
#
# See: NAME_MAP
def [](name_or_id)
@records[name_or_id.kind_of?(Symbol) ? NAME_MAP[name_or_id] : name_or_id]
end
private
def parse_table #:nodoc:
@format, count, string_offset = read_formatted(6, 'n3')
string_offset += directory_entry.offset
@records = Hash.new {|h, k| h[k] = Records.new }
@language_tags = {}
record_rows = Array.new(count) { read_formatted(12, 'n6') }
if @format == 1
count = read_formatted(2, 'n').first
language_rows = Array.new(count) { read_formatted(4, 'n2') }
end
record_rows.each do |pid, eid, lid, nid, length, offset|
io.pos = string_offset + offset
@records[nid] << Record.new(io.read(length), pid, eid, lid)
end
if @format == 1
language_rows.each_with_index do |(length, offset), index|
io.pos = string_offset + offset
@language_tags[0x8000 + index] =
io.read(length).encode!(::Encoding::UTF_8, ::Encoding::UTF_16BE)
end
end
end
end
end
end
end
end