gamba 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +339 -0
  5. data/GPLv3 +674 -0
  6. data/LICENSE +65 -0
  7. data/LICENSE-prawn-svg +21 -0
  8. data/README.md +148 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/lib/gamba/annotations.rb +21 -0
  25. data/lib/gamba/combine.rb +307 -0
  26. data/lib/gamba/document/bounding_box.rb +193 -0
  27. data/lib/gamba/document/column_box.rb +91 -0
  28. data/lib/gamba/document/internals.rb +264 -0
  29. data/lib/gamba/document/span.rb +25 -0
  30. data/lib/gamba/document.rb +268 -0
  31. data/lib/gamba/encoding.rb +13 -0
  32. data/lib/gamba/errors.rb +39 -0
  33. data/lib/gamba/font.rb +228 -0
  34. data/lib/gamba/fonts/afm.rb +171 -0
  35. data/lib/gamba/fonts/otf.rb +50 -0
  36. data/lib/gamba/fonts/ttf.rb +413 -0
  37. data/lib/gamba/graphic_state.rb +84 -0
  38. data/lib/gamba/graphics/color.rb +174 -0
  39. data/lib/gamba/graphics/patterns.rb +100 -0
  40. data/lib/gamba/graphics/style.rb +159 -0
  41. data/lib/gamba/graphics.rb +212 -0
  42. data/lib/gamba/grid.rb +216 -0
  43. data/lib/gamba/images/image.rb +271 -0
  44. data/lib/gamba/images/png_decoder.rb +65 -0
  45. data/lib/gamba/images.rb +115 -0
  46. data/lib/gamba/measurement_extensions.rb +40 -0
  47. data/lib/gamba/measurements.rb +62 -0
  48. data/lib/gamba/outline.rb +148 -0
  49. data/lib/gamba/page.rb +125 -0
  50. data/lib/gamba/page_geometry.rb +27 -0
  51. data/lib/gamba/pdf/decryption.rb +165 -0
  52. data/lib/gamba/pdf/outline_node.rb +53 -0
  53. data/lib/gamba/pdf/reader.rb +616 -0
  54. data/lib/gamba/pdf/reference.rb +31 -0
  55. data/lib/gamba/pdf/writer.rb +105 -0
  56. data/lib/gamba/pdf.rb +87 -0
  57. data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
  58. data/lib/gamba/pdf_a.rb +132 -0
  59. data/lib/gamba/repeater.rb +79 -0
  60. data/lib/gamba/security.rb +111 -0
  61. data/lib/gamba/stamp.rb +76 -0
  62. data/lib/gamba/table/cell/kinds.rb +246 -0
  63. data/lib/gamba/table/cell.rb +331 -0
  64. data/lib/gamba/table/cells.rb +208 -0
  65. data/lib/gamba/table.rb +298 -0
  66. data/lib/gamba/text/box.rb +16 -0
  67. data/lib/gamba/text/formatted/arranger.rb +217 -0
  68. data/lib/gamba/text/formatted/box.rb +312 -0
  69. data/lib/gamba/text/formatted/fragment.rb +205 -0
  70. data/lib/gamba/text/formatted/line_wrap.rb +205 -0
  71. data/lib/gamba/text/formatted/parser.rb +150 -0
  72. data/lib/gamba/text/formatted/wrap.rb +103 -0
  73. data/lib/gamba/text.rb +310 -0
  74. data/lib/gamba/transformation_stack.rb +43 -0
  75. data/lib/gamba/true_type/cff_subset.rb +157 -0
  76. data/lib/gamba/true_type/file.rb +372 -0
  77. data/lib/gamba/true_type/open_type_layout.rb +189 -0
  78. data/lib/gamba/true_type/simple_subsets.rb +108 -0
  79. data/lib/gamba/true_type/subset.rb +206 -0
  80. data/lib/gamba/version.rb +10 -0
  81. data/lib/gamba/view.rb +28 -0
  82. data/lib/gamba.rb +124 -0
  83. data/lib/pdf/core.rb +4 -0
  84. data/lib/prawn/measurement_extensions.rb +4 -0
  85. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  86. data/lib/prawn/svg/attributes/mask.rb +16 -0
  87. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  88. data/lib/prawn/svg/attributes/space.rb +10 -0
  89. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  90. data/lib/prawn/svg/attributes/transform.rb +10 -0
  91. data/lib/prawn/svg/attributes.rb +6 -0
  92. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  93. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  94. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  95. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  96. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  97. data/lib/prawn/svg/color.rb +240 -0
  98. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  99. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  100. data/lib/prawn/svg/css/font_parser.rb +46 -0
  101. data/lib/prawn/svg/css/selector_parser.rb +180 -0
  102. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  103. data/lib/prawn/svg/css/values_parser.rb +68 -0
  104. data/lib/prawn/svg/document.rb +169 -0
  105. data/lib/prawn/svg/elements/anchor.rb +10 -0
  106. data/lib/prawn/svg/elements/base.rb +315 -0
  107. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  108. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  109. data/lib/prawn/svg/elements/circle.rb +19 -0
  110. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  111. data/lib/prawn/svg/elements/container.rb +10 -0
  112. data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
  113. data/lib/prawn/svg/elements/ellipse.rb +20 -0
  114. data/lib/prawn/svg/elements/gradient.rb +183 -0
  115. data/lib/prawn/svg/elements/ignored.rb +5 -0
  116. data/lib/prawn/svg/elements/image.rb +178 -0
  117. data/lib/prawn/svg/elements/line.rb +27 -0
  118. data/lib/prawn/svg/elements/marker.rb +72 -0
  119. data/lib/prawn/svg/elements/mask.rb +65 -0
  120. data/lib/prawn/svg/elements/path.rb +294 -0
  121. data/lib/prawn/svg/elements/pattern.rb +150 -0
  122. data/lib/prawn/svg/elements/polygon.rb +25 -0
  123. data/lib/prawn/svg/elements/polyline.rb +23 -0
  124. data/lib/prawn/svg/elements/rect.rb +64 -0
  125. data/lib/prawn/svg/elements/root.rb +22 -0
  126. data/lib/prawn/svg/elements/switch.rb +92 -0
  127. data/lib/prawn/svg/elements/text.rb +99 -0
  128. data/lib/prawn/svg/elements/text_component.rb +352 -0
  129. data/lib/prawn/svg/elements/text_node.rb +308 -0
  130. data/lib/prawn/svg/elements/text_path.rb +153 -0
  131. data/lib/prawn/svg/elements/use.rb +142 -0
  132. data/lib/prawn/svg/elements/viewport.rb +29 -0
  133. data/lib/prawn/svg/elements.rb +46 -0
  134. data/lib/prawn/svg/extension.rb +25 -0
  135. data/lib/prawn/svg/font.rb +20 -0
  136. data/lib/prawn/svg/font_metrics.rb +144 -0
  137. data/lib/prawn/svg/font_registry.rb +315 -0
  138. data/lib/prawn/svg/funciri.rb +14 -0
  139. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  140. data/lib/prawn/svg/gradients.rb +53 -0
  141. data/lib/prawn/svg/interface.rb +72 -0
  142. data/lib/prawn/svg/length.rb +43 -0
  143. data/lib/prawn/svg/link_renderer.rb +40 -0
  144. data/lib/prawn/svg/loaders/data.rb +17 -0
  145. data/lib/prawn/svg/loaders/file.rb +111 -0
  146. data/lib/prawn/svg/loaders/web.rb +24 -0
  147. data/lib/prawn/svg/paint.rb +67 -0
  148. data/lib/prawn/svg/pathable.rb +153 -0
  149. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  150. data/lib/prawn/svg/pdfmatrix.rb +39 -0
  151. data/lib/prawn/svg/percentage.rb +24 -0
  152. data/lib/prawn/svg/properties.rb +329 -0
  153. data/lib/prawn/svg/renderer.rb +249 -0
  154. data/lib/prawn/svg/state.rb +24 -0
  155. data/lib/prawn/svg/transform_parser.rb +78 -0
  156. data/lib/prawn/svg/ttc.rb +35 -0
  157. data/lib/prawn/svg/ttf.rb +95 -0
  158. data/lib/prawn/svg/url_loader.rb +46 -0
  159. data/lib/prawn/svg/version.rb +5 -0
  160. data/lib/prawn/table.rb +4 -0
  161. data/lib/prawn-svg.rb +52 -0
  162. data/lib/prawn.rb +3 -0
  163. metadata +263 -0
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Fonts
5
+ # The 14 standard PDF fonts, measured from Adobe's AFM files and written in WinAnsiEncoding.
6
+ class AFM < Font
7
+ BUILT_INS = %w[
8
+ Courier Helvetica Times-Roman Symbol ZapfDingbats Courier-Bold Courier-Oblique Courier-BoldOblique Times-Bold
9
+ Times-Italic Times-BoldItalic Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique
10
+ ].freeze
11
+ METRICS_BY_PATH = {}
12
+ METRICS_LOCK = Mutex.new
13
+
14
+ Metrics = Data.define(:width_by_byte, :kerning_by_byte_pair, :attributes)
15
+
16
+ class << self
17
+ attr_accessor :hide_m17n_warning
18
+ end
19
+ self.hide_m17n_warning = false
20
+
21
+ def self.metrics_path
22
+ @metrics_path ||= ENV["METRICS"]&.split(":") ||
23
+ [".", "/usr/lib/afm", "/usr/local/lib/afm", "/usr/openwin/lib/fonts/afm", "#{Prawn::DATADIR}/fonts"]
24
+ end
25
+
26
+ attr_reader :attributes
27
+
28
+ def initialize(document, name, options = {})
29
+ name ||= options[:family]
30
+ raise Errors::UnknownFont, "#{name} (#{options[:style] || "normal"}) is not a known font." unless BUILT_INS.include?(name)
31
+ if document.pdf_a?
32
+ raise Errors::PdfAViolation, "PDF/A embeds every font, which #{name}, a standard PDF font, cannot be: use a TrueType or OpenType font"
33
+ end
34
+
35
+ super
36
+ file_name = @name.end_with?(".afm") ? @name : "#{@name}.afm"
37
+ file_name = find_font(file_name) unless file_name.start_with?("/")
38
+ metrics = METRICS_LOCK.synchronize { METRICS_BY_PATH[file_name] ||= parse_afm(file_name) }
39
+ @width_by_byte = metrics.width_by_byte
40
+ @kerning_by_byte_pair = metrics.kerning_by_byte_pair
41
+ @attributes = metrics.attributes
42
+ @ascender = Integer(@attributes.fetch("ascender", "0"), 10)
43
+ @descender = Integer(@attributes.fetch("descender", "0"), 10)
44
+ @line_gap = Float(bbox[3] - bbox[1]) - (@ascender - @descender)
45
+ end
46
+
47
+ def unicode?
48
+ false
49
+ end
50
+
51
+ def bbox
52
+ @bbox ||= @attributes["fontbbox"].split(/\s+/).map { Integer(_1) }
53
+ end
54
+
55
+ def compute_width_of(string, options = {})
56
+ scale = (options[:size] || size) / 1000.0
57
+ return unscaled_width_of(string) * scale unless options[:kerning]
58
+
59
+ strings, offsets = kern(string).partition { _1.is_a?(String) }
60
+ (unscaled_width_of(strings.join) - offsets.sum) * scale
61
+ end
62
+
63
+ def has_kerning_data?
64
+ !@kerning_by_byte_pair.empty?
65
+ end
66
+
67
+ def normalize_encoding(text)
68
+ text.encode(::Encoding::Windows_1252)
69
+ rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
70
+ raise Errors::IncompatibleStringEncoding,
71
+ "Your document includes text that's not compatible with the Windows-1252 character set.\n" \
72
+ "If you need full UTF-8 support, use external fonts instead of PDF's built-in fonts.\n"
73
+ end
74
+
75
+ def to_utf8(text)
76
+ text.encode(::Encoding::UTF_8)
77
+ end
78
+
79
+ def character_count(string)
80
+ string.length
81
+ end
82
+
83
+ def encode_text(text, options = {})
84
+ [[0, options[:kerning] ? kern(text) : text]]
85
+ end
86
+
87
+ def glyph_present?(character)
88
+ !normalize_encoding(character).nil?
89
+ rescue Errors::IncompatibleStringEncoding
90
+ false
91
+ end
92
+
93
+ private
94
+
95
+ def register(_subset)
96
+ dictionary = {Type: :Font, Subtype: :Type1, BaseFont: name.to_sym}
97
+ dictionary[:Encoding] = :WinAnsiEncoding unless symbolic?
98
+ @document.ref!(dictionary)
99
+ end
100
+
101
+ def symbolic?
102
+ attributes["characterset"] == "Special"
103
+ end
104
+
105
+ def find_font(file)
106
+ directory = self.class.metrics_path.find { |path| File.exist?("#{path}/#{file}") }
107
+ raise Errors::UnknownFont, "Couldn't find the font: #{file} in any of:\n#{self.class.metrics_path.join("\n")}" unless directory
108
+
109
+ "#{directory}/#{file}"
110
+ end
111
+
112
+ # Runs of bytes, separated by the kerning offset between two of them, in thousandths of the font size.
113
+ def kern(string)
114
+ runs = [String.new(encoding: ::Encoding::BINARY)]
115
+ last_byte = nil
116
+ string.each_byte do |byte|
117
+ offset = last_byte && @kerning_by_byte_pair[(last_byte << 8) | byte]
118
+ runs << -offset << String.new(encoding: ::Encoding::BINARY) if offset
119
+ runs.last << byte
120
+ last_byte = byte
121
+ end
122
+ runs.map { |run| run.is_a?(String) ? run.force_encoding(::Encoding::Windows_1252) : run }
123
+ end
124
+
125
+ def unscaled_width_of(string)
126
+ string.each_byte.sum { @width_by_byte[_1] }
127
+ end
128
+
129
+ def parse_afm(file_name)
130
+ width_by_name = {}
131
+ kerning_by_name_pair = {}
132
+ attributes = {}
133
+ sections = []
134
+ File.foreach(file_name) do |line|
135
+ if (section = line[/^Start(\w+)/, 1])
136
+ sections.push(section)
137
+ elsif line.start_with?("End")
138
+ sections.pop
139
+ else
140
+ case sections
141
+ when %w[FontMetrics CharMetrics]
142
+ next unless /^CH?\s/.match?(line)
143
+
144
+ width_by_name[line[/\bN\s+(\.?\w+)\s*;/, 1]] = Integer(line[/\bWX\s+(\d+)\s*;/, 1], 10)
145
+ when %w[FontMetrics KernData KernPairs]
146
+ next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/
147
+
148
+ kerning_by_name_pair[[$1, $2]] = Integer($3, 10)
149
+ when %w[FontMetrics KernData TrackKern], %w[FontMetrics Composites]
150
+ next
151
+ else
152
+ next unless line =~ /(^\w+)\s+(.*)/
153
+
154
+ key = $1.downcase
155
+ attributes[key] = attributes[key] ? [*attributes[key], $2] : $2
156
+ end
157
+ end
158
+ end
159
+
160
+ byte_by_name = Encoding::WinAnsi::CHARACTERS.each_with_index.to_h
161
+ kerning_by_byte_pair = kerning_by_name_pair.each_with_object({}) do |((left, right), offset), table|
162
+ left_byte = byte_by_name[left]
163
+ right_byte = byte_by_name[right]
164
+ table[(left_byte << 8) | right_byte] = offset if left_byte && right_byte
165
+ end
166
+ width_by_byte = Encoding::WinAnsi::CHARACTERS.map { width_by_name.fetch(_1, 0) }
167
+ Metrics.new(width_by_byte: width_by_byte.freeze, kerning_by_byte_pair: kerning_by_byte_pair.freeze, attributes: attributes.freeze)
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Fonts
5
+ # An OpenType font with CFF outlines, embedded as a Type0 font over a CIDFontType0: its character identifiers
6
+ # are glyph indices, as a CFF program that is not CID-keyed requires (ISO 32000-1 §9.7.4.2).
7
+ class OTF < TTF
8
+ def encode_text(text, options = {})
9
+ text = text.chomp
10
+ runs =
11
+ if @layout
12
+ Array(shaped_runs(text, options[:kerning]))
13
+ elsif options[:kerning]
14
+ kern(text).map { |run| run.is_a?(Numeric) ? run : glyph_string(run) }
15
+ else
16
+ [glyph_string(text.codepoints)]
17
+ end
18
+ runs = space_out_words(runs, options[:word_spacing], options[:font_size]) if options[:word_spacing].to_f.nonzero?
19
+ [[0, (runs.size == 1) ? runs.first : runs]]
20
+ end
21
+
22
+ private
23
+
24
+ def cluster_string(clusters)
25
+ clusters.map do |glyph, codes|
26
+ @code_by_glyph[glyph] ||= codes
27
+ glyph
28
+ end.pack("n*")
29
+ end
30
+
31
+ def embed(reference, _subset)
32
+ @code_by_glyph ||= {}
33
+ glyphs = [0, *@code_by_glyph.keys].uniq
34
+ font_name = :"#{subset_tag_for(glyphs)}+#{basename.delete("\0")[0, 33]}"
35
+ font_file = @document.ref!({Subtype: :CIDFontType0C}, Gamba::TrueType::CffSubset.new(@ttf.table("CFF ")).bytes(glyphs))
36
+ descriptor = @document.ref!(
37
+ Type: :FontDescriptor, FontName: font_name, FontBBox: bbox, Flags: pdf_flags, StemV: stem_v, ItalicAngle: italic_angle,
38
+ Ascent: @ascender, Descent: @descender, CapHeight: cap_height, XHeight: x_height, FontFile3: font_file
39
+ )
40
+ widths = glyphs.sort.flat_map { |glyph| [glyph, [Integer(@ttf.advance_widths[glyph] * scale_factor)]] }
41
+ descendant = @document.ref!(
42
+ Type: :Font, Subtype: :CIDFontType0, BaseFont: font_name, FontDescriptor: descriptor, W: widths,
43
+ CIDSystemInfo: {Registry: PDF::Core::LiteralString.new("Adobe"), Ordering: PDF::Core::LiteralString.new("Identity"), Supplement: 0}
44
+ )
45
+ reference.data.update(Subtype: :Type0, BaseFont: font_name, Encoding: :"Identity-H", DescendantFonts: [descendant],
46
+ ToUnicode: @document.ref!({}, to_unicode_cmap(@code_by_glyph)))
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,413 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module Prawn
6
+ module Fonts
7
+ # A TrueType font, measured like Prawn measures it, embedded as one composite font per document: a Type0 font
8
+ # with Identity-H encoding over a subset of the glyphs used.
9
+ class TTF < Font
10
+ class Error < StandardError
11
+ DEFAULT_MESSAGE = "TTF font error"
12
+
13
+ def initialize(message = DEFAULT_MESSAGE, font: nil)
14
+ super((font && message == DEFAULT_MESSAGE) ? "TTF font error in font #{font}" : message)
15
+ end
16
+ end
17
+
18
+ class NoUnicodeCMap < Error
19
+ def initialize(message = "No unicode cmap found in font", font: nil)
20
+ super(font ? "No unicode cmap found in font #{font}" : message)
21
+ end
22
+ end
23
+
24
+ class NoPostscriptName < Error
25
+ def initialize(message = "Can not detect a postscript name", font: nil)
26
+ super(font ? "Can not detect a postscript name in font #{font}" : message)
27
+ end
28
+ end
29
+
30
+ LINE_FEED = 10
31
+ # FontDescriptor flags, ISO 32000-1 table 123.
32
+ FIXED_PITCH_FLAG = 0x0001
33
+ SERIF_FLAG = 0x0002
34
+ SYMBOLIC_FLAG = 0x0004
35
+ SCRIPT_FLAG = 0x0008
36
+ ITALIC_FLAG = 0x0040
37
+ # OS/2 sFamilyClass high bytes of serif and script families.
38
+ SERIF_FAMILY_CLASSES = [1, 2, 3, 4, 5, 7].freeze
39
+ SCRIPT_FAMILY_CLASS = 10
40
+ TO_UNICODE_ENTRIES_PER_BLOCK = 100
41
+
42
+ attr_reader :ttf
43
+
44
+ def initialize(document, name, options = {})
45
+ super
46
+ @ttf = read_ttf_file
47
+ raise NoUnicodeCMap.new(font: name) unless @ttf.unicode_cmap?
48
+ @code_by_glyph = {}
49
+
50
+ @subset = Gamba::TrueType::Subset.new(@ttf)
51
+ @cid_by_cluster = {}
52
+ @simple_subsets = Gamba::TrueType::SimpleSubsets.new(->(codes) { cmap[Array(codes).first] })
53
+ @layout = Gamba::TrueType::OpenTypeLayout.new(@ttf, options[:features]) if options[:features]&.any?
54
+ @width_by_code = {}
55
+ @ascender = Integer(@ttf.ascent * scale_factor)
56
+ @descender = Integer(@ttf.descent * scale_factor)
57
+ @line_gap = Integer(@ttf.line_gap * scale_factor)
58
+ end
59
+
60
+ def unicode?
61
+ true
62
+ end
63
+
64
+ # One pass over the characters, adding each kerning offset before the character it precedes: the same
65
+ # sums, in the same order, as measuring the runs of #kern.
66
+ def compute_width_of(string, options = {})
67
+ return shaped_width_of(string, options) if @layout
68
+
69
+ scale = (options[:size] || size) / 1000.0
70
+ kerning = options[:kerning]
71
+ width = 0
72
+ previous_glyph = nil
73
+ string.each_codepoint do |code|
74
+ glyph = cmap[code]
75
+ if kerning && previous_glyph && (offset = @ttf.kerning_offset(previous_glyph, glyph))
76
+ width -= -(offset * scale_factor)
77
+ end
78
+ width += character_width_by_code(code)
79
+ previous_glyph = glyph
80
+ end
81
+ width * scale
82
+ end
83
+
84
+ def bbox
85
+ @bbox ||= @ttf.header.to_h.values_at(:x_min, :y_min, :x_max, :y_max).map! { Integer(_1 * scale_factor) }
86
+ end
87
+
88
+ def has_kerning_data?
89
+ @ttf.kerning?
90
+ end
91
+
92
+ def encode_text(text, options = {})
93
+ text = text.chomp
94
+ unless @layout
95
+ return options[:kerning] ? encode_kerned_codepoints(text.codepoints) : @simple_subsets.encode_codepoints(text.codepoints, cmap)
96
+ end
97
+
98
+ clusters = @layout ? @layout.shape(text.codepoints) : text.codepoints.map { |code| [cmap[code], code] }
99
+ return @simple_subsets.encode(clusters) unless options[:kerning]
100
+
101
+ encoded = []
102
+ run = []
103
+ clusters.each do |cluster|
104
+ if run.any? && (offset = kerning_between(run.last[0], cluster[0]))
105
+ append_encoded_run(encoded, run)
106
+ encoded.last[1] = Array(encoded.last[1]) << -(offset * scale_factor)
107
+ run = []
108
+ end
109
+ run << cluster
110
+ end
111
+ append_encoded_run(encoded, run)
112
+ end
113
+
114
+ def basename
115
+ @basename ||= @ttf.postscript_name
116
+ end
117
+
118
+ def stem_v
119
+ 0
120
+ end
121
+
122
+ # The post table's Fixed italic angle, read the way Prawn reads it.
123
+ def italic_angle
124
+ @italic_angle ||= begin
125
+ raw = @ttf.italic_angle_fixed
126
+ if raw
127
+ high = raw >> 16
128
+ low = raw & 0xFF
129
+ high = -((high ^ 0xFFFF) + 1) if high & 0x8000 != 0
130
+ Float("#{high}.#{low}")
131
+ else
132
+ 0
133
+ end
134
+ end
135
+ end
136
+
137
+ def cap_height
138
+ @cap_height ||= @ttf.os2[:cap_height].to_i.then { |cap_height| cap_height.zero? ? @ascender : cap_height }
139
+ end
140
+
141
+ def x_height
142
+ @ttf.os2[:x_height] || 0
143
+ end
144
+
145
+ def family_class
146
+ @family_class ||= @ttf.os2[:family_class].to_i >> 8
147
+ end
148
+
149
+ def serif?
150
+ SERIF_FAMILY_CLASSES.include?(family_class)
151
+ end
152
+
153
+ def script?
154
+ family_class == SCRIPT_FAMILY_CLASS
155
+ end
156
+
157
+ def pdf_flags
158
+ flags = SYMBOLIC_FLAG
159
+ flags |= FIXED_PITCH_FLAG if @ttf.fixed_pitch?
160
+ flags |= SERIF_FLAG if serif?
161
+ flags |= SCRIPT_FLAG if script?
162
+ flags |= ITALIC_FLAG if italic_angle != 0
163
+ flags
164
+ end
165
+
166
+ def normalize_encoding(text)
167
+ text.encode(::Encoding::UTF_8)
168
+ rescue
169
+ raise Errors::IncompatibleStringEncoding,
170
+ "Encoding #{text.encoding} can not be transparently converted to UTF-8. " \
171
+ "Please ensure the encoding of the string you are attempting to use is set correctly"
172
+ end
173
+
174
+ def to_utf8(text)
175
+ text.encode(::Encoding::UTF_8)
176
+ end
177
+
178
+ def measured_string(string)
179
+ (string.encoding == ::Encoding::UTF_8) ? string : normalize_encoding(string)
180
+ end
181
+
182
+ def glyph_present?(character)
183
+ cmap[character.codepoints.first].positive?
184
+ end
185
+
186
+ # Glyphs, which character spacing applies to: fewer than characters once ligatures join some.
187
+ def character_count(string)
188
+ @layout ? @layout.shape(string.codepoints).size : string.length
189
+ end
190
+
191
+ private
192
+
193
+ def read_ttf_file
194
+ Gamba::TrueType::File.open(@name)
195
+ end
196
+
197
+ def cmap
198
+ @ttf.glyph_by_code
199
+ end
200
+
201
+ def scale_factor
202
+ @scale_factor ||= 1000.0 / @ttf.header.units_per_em
203
+ end
204
+
205
+ def character_width_by_code(code)
206
+ return 0.0 if code == LINE_FEED
207
+
208
+ @width_by_code[code] ||= Integer(@ttf.advance_widths[cmap[code]] * scale_factor)
209
+ end
210
+
211
+ # Runs of code points, separated by the kerning offset between two glyphs, in thousandths of the font size.
212
+ def kern(string)
213
+ runs = []
214
+ string.each_codepoint do |code|
215
+ if runs.empty?
216
+ runs << [code]
217
+ elsif (offset = @ttf.kerning_offset(cmap[runs.last.last], cmap[code]))
218
+ runs << -(offset * scale_factor) << [code]
219
+ else
220
+ runs.last << code
221
+ end
222
+ end
223
+ runs
224
+ end
225
+
226
+ def glyph_string(codes)
227
+ cluster_string(codes.map { |code| [cmap[code], code] })
228
+ end
229
+
230
+ # One character identifier per cluster of characters, in order of first use (0 stays .notdef): characters
231
+ # sharing a glyph keep their own Unicode value for text extraction, a ligature all of its characters.
232
+ def cluster_string(clusters)
233
+ clusters.map do |glyph, codes|
234
+ @cid_by_cluster[codes] ||= begin
235
+ (@glyph_by_cid ||= [0]) << glyph
236
+ @glyph_by_cid.size - 1
237
+ end
238
+ end.pack("n*")
239
+ end
240
+
241
+ # PDF word spacing (Tw) only reaches byte 32 of single-byte encodings (ISO 32000-1 §9.3.3): with two-byte
242
+ # codes, each space gets the same shift as a TJ offset instead.
243
+ def space_out_words(runs, word_spacing, font_size)
244
+ space = @layout ? cluster_string([[cmap[32], [32]]]) : glyph_string([32])
245
+ # From the spacing as Tw would write it, rounded to five decimals, so spaces land where Prawn's do.
246
+ offset = -(Float(PDF::Core.real(word_spacing)) * 1000.0 / font_size)
247
+ runs.flat_map do |run|
248
+ next [run] unless run.is_a?(String)
249
+
250
+ run.b.scan(/../mn).slice_after(space).flat_map { |codes| (codes.last == space) ? [codes.join, offset] : [codes.join] }
251
+ end
252
+ end
253
+
254
+ def shaped_width_of(string, options)
255
+ width = 0
256
+ previous_glyph = nil
257
+ @layout.shape(string.codepoints).each do |glyph, codes|
258
+ if options[:kerning] && previous_glyph && (offset = kerning_between(previous_glyph, glyph))
259
+ width += offset * scale_factor
260
+ end
261
+ width += (codes == [LINE_FEED]) ? 0.0 : Integer(@ttf.advance_widths[glyph] * scale_factor)
262
+ previous_glyph = glyph
263
+ end
264
+ width * ((options[:size] || size) / 1000.0)
265
+ end
266
+
267
+ def shaped_runs(text, kerning)
268
+ clusters = @layout.shape(text.codepoints)
269
+ return cluster_string(clusters) unless kerning
270
+
271
+ runs = []
272
+ run = []
273
+ clusters.each do |cluster|
274
+ if run.any? && (offset = kerning_between(run.last[0], cluster[0]))
275
+ runs << cluster_string(run) << -(offset * scale_factor)
276
+ run = []
277
+ end
278
+ run << cluster
279
+ end
280
+ runs << cluster_string(run)
281
+ end
282
+
283
+ def kerning_between(left, right)
284
+ return @ttf.kerning_offset(left, right) unless @layout
285
+
286
+ @layout.kerning(left, right) || (@ttf.kerning_offset(left, right) unless options[:features].include?(:kern))
287
+ end
288
+
289
+ def encode_kerned_codepoints(codepoints)
290
+ encoded = []
291
+ run_start = 0
292
+ previous_glyph = nil
293
+ codepoints.each_with_index do |code, index|
294
+ glyph = cmap[code]
295
+ if previous_glyph && (offset = @ttf.kerning_offset(previous_glyph, glyph))
296
+ merge_encoded_parts(encoded, @simple_subsets.encode_codepoints(codepoints[run_start...index], cmap))
297
+ encoded.last[1] = Array(encoded.last[1]) << -(offset * scale_factor)
298
+ run_start = index
299
+ end
300
+ previous_glyph = glyph
301
+ end
302
+ merge_encoded_parts(encoded, @simple_subsets.encode_codepoints(codepoints[run_start..], cmap))
303
+ end
304
+
305
+ # Runs of one subset merge into the previous part when it is of the same subset, as in Prawn's TJ arrays.
306
+ def append_encoded_run(encoded, run)
307
+ merge_encoded_parts(encoded, @simple_subsets.encode(run))
308
+ end
309
+
310
+ def merge_encoded_parts(encoded, parts)
311
+ return encoded if parts.empty?
312
+
313
+ if encoded.any? && parts.first[0] == encoded.last[0]
314
+ encoded.last[1] << parts.shift[1]
315
+ end
316
+ encoded.concat(parts)
317
+ end
318
+
319
+ def register(subset)
320
+ reference = @document.ref!(Type: :Font, BaseFont: basename.delete("\0").to_sym)
321
+ @document.before_render { embed(reference, subset) }
322
+ reference
323
+ end
324
+
325
+ # A simple TrueType font of single-byte codes, as Prawn embeds them: a font program with a Mac Roman cmap
326
+ # from those codes to its glyphs, widths per code, and a ToUnicode map.
327
+ def embed(reference, subset_index)
328
+ subset = @simple_subsets[subset_index]
329
+ program = Gamba::TrueType::Subset.new(@ttf)
330
+ # Glyphs numbered as TTFunk numbers them, by character code then components: TrueType hinting can depend
331
+ # on glyph indices, and a different order rasterizes differently in MuPDF.
332
+ subset.glyph_by_code.sort.each { |_code, glyph| program.glyph_for(glyph) unless glyph.zero? }
333
+ program.add_components(subset.glyph_by_code.values.uniq.sort!)
334
+ glyph_by_code = subset.glyph_by_code.transform_values { program.glyph_for(_1) }
335
+ mac_roman_cmap = [0, 1, 1, 0, 12, 0, 262, 0].pack("n4 N n3") + Array.new(256) { |code| glyph_by_code[code] || 0 }.pack("C*")
336
+ font_name = :"#{subset_tag_for(program.original_glyphs)}+#{basename.delete("\0")[0, 33]}"
337
+ font_bytes = program.bytes("cmap" => mac_roman_cmap)
338
+ font_file = @document.ref!({Length1: font_bytes.bytesize}, font_bytes)
339
+ descriptor = @document.ref!(
340
+ Type: :FontDescriptor, FontName: font_name, FontBBox: bbox, Flags: pdf_flags, StemV: stem_v, ItalicAngle: italic_angle,
341
+ Ascent: @ascender, Descent: @descender, CapHeight: cap_height, XHeight: x_height, FontFile2: font_file
342
+ )
343
+ first_code, last_code = subset.glyph_by_code.keys.minmax
344
+ widths = (first_code..last_code).map do |code|
345
+ glyph = subset.glyph_by_code[code]
346
+ glyph ? Integer(@ttf.advance_widths[glyph] * scale_factor) : 0
347
+ end
348
+ codes_by_code = subset.code_by_cluster.each_with_object({}) { |(codes, code), map| map[code] ||= Array(codes) }
349
+ reference.data.update(Subtype: :TrueType, BaseFont: font_name, FontDescriptor: descriptor, FirstChar: first_code, LastChar: last_code,
350
+ Widths: @document.ref!(widths), ToUnicode: @document.ref!({}, to_unicode_cmap(codes_by_code, code_bytes: 1)))
351
+ end
352
+
353
+ # Six capital letters derived from the glyphs kept, as ISO 32000-1 §9.6.4 asks of a subset's name.
354
+ def subset_tag
355
+ subset_tag_for(@subset.original_glyphs)
356
+ end
357
+
358
+ def subset_tag_for(glyphs)
359
+ Digest::MD5.digest(glyphs.pack("n*")).bytes.first(6).map! { |byte| ("A".ord + (byte % 26)).chr }.join
360
+ end
361
+
362
+ def to_unicode_cmap(codes_by_cid, code_bytes: 2)
363
+ entries = codes_by_cid.map { |cid, codes| format("<%0*X> <%s>", code_bytes * 2, cid, Array(codes).pack("U*").encode(::Encoding::UTF_16BE).unpack1("H*").upcase) }
364
+ blocks = entries.each_slice(TO_UNICODE_ENTRIES_PER_BLOCK).map { |block_entries| "#{block_entries.size} beginbfchar\n#{block_entries.join("\n")}\nendbfchar" }
365
+ <<~CMAP
366
+ /CIDInit /ProcSet findresource begin
367
+ 12 dict begin
368
+ begincmap
369
+ /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
370
+ /CMapName /Adobe-Identity-UCS def
371
+ /CMapType 2 def
372
+ 1 begincodespacerange
373
+ #{(code_bytes == 1) ? "<00> <FF>" : "<0000> <FFFF>"}
374
+ endcodespacerange
375
+ #{blocks.join("\n")}
376
+ endcmap
377
+ CMapName currentdict /CMap defineresource pop
378
+ end
379
+ end
380
+ CMAP
381
+ end
382
+ end
383
+
384
+ class TTC < TTF
385
+ def self.font_names(file)
386
+ collection = ::File.binread(file)
387
+ Array.new(collection.unpack1("@8 N")) { |index| Gamba::TrueType::File.new(collection, index:).font_name }
388
+ end
389
+
390
+ private
391
+
392
+ def read_ttf_file
393
+ index = @options[:font].is_a?(Numeric) ? @options[:font] : self.class.font_names(@name).index(@options[:font]) || 0
394
+ Gamba::TrueType::File.open(@name, index:)
395
+ end
396
+ end
397
+
398
+ class DFont < TTF
399
+ private
400
+
401
+ def read_ttf_file
402
+ Gamba::TrueType::File.open(@name, index: @options[:font] || 0, resource: true)
403
+ end
404
+ end
405
+ end
406
+
407
+ class Font
408
+ AFM = Fonts::AFM
409
+ TTF = Fonts::TTF
410
+ TTC = Fonts::TTC
411
+ DFont = Fonts::DFont
412
+ end
413
+ end