prawnx 0.1.1

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 (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
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", "#{PrawnX::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(it) }
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 { it.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 { File.exist?("#{it}/#{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 { it.is_a?(String) ? it.force_encoding(::Encoding::Windows_1252) : it }
123
+ end
124
+
125
+ def unscaled_width_of(string)
126
+ string.each_byte.sum { @width_by_byte[it] }
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(it, 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 PrawnX
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 { it.is_a?(Numeric) ? it : glyph_string(it) }
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}, 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 { [it, [Integer(@ttf.advance_widths[it] * scale_factor)]] }
41
+ descendant = @document.ref!(
42
+ Type: :Font, Subtype: :CIDFontType0, BaseFont: font_name, FontDescriptor: descriptor, W: widths,
43
+ CIDSystemInfo: {Registry: PDF::LiteralString.new("Adobe"), Ordering: PDF::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,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module PrawnX
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 = TrueType::Subset.new(@ttf)
51
+ @cid_by_cluster = {}
52
+ @simple_subsets = TrueType::SimpleSubsets.new(->(codes) { cmap[Array(codes).first] })
53
+ @layout = 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(it * 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 { [cmap[it], it] }
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 { it.zero? ? @ascender : it }
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 StandardError
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 glyph_present?(character)
179
+ cmap[character.codepoints.first].positive?
180
+ end
181
+
182
+ # Glyphs, which character spacing applies to: fewer than characters once ligatures join some.
183
+ def character_count(string)
184
+ @layout ? @layout.shape(string.codepoints).size : string.length
185
+ end
186
+
187
+ private
188
+
189
+ def read_ttf_file
190
+ TrueType::File.open(@name)
191
+ end
192
+
193
+ def cmap
194
+ @ttf.glyph_by_code
195
+ end
196
+
197
+ def scale_factor
198
+ @scale_factor ||= 1000.0 / @ttf.header.units_per_em
199
+ end
200
+
201
+ def character_width_by_code(code)
202
+ return 0.0 if code == LINE_FEED
203
+
204
+ @width_by_code[code] ||= Integer(@ttf.advance_widths[cmap[code]] * scale_factor)
205
+ end
206
+
207
+ # Runs of code points, separated by the kerning offset between two glyphs, in thousandths of the font size.
208
+ def kern(string)
209
+ runs = []
210
+ string.each_codepoint do |code|
211
+ if runs.empty?
212
+ runs << [code]
213
+ elsif (offset = @ttf.kerning_offset(cmap[runs.last.last], cmap[code]))
214
+ runs << -(offset * scale_factor) << [code]
215
+ else
216
+ runs.last << code
217
+ end
218
+ end
219
+ runs
220
+ end
221
+
222
+ def glyph_string(codes)
223
+ cluster_string(codes.map { [cmap[it], it] })
224
+ end
225
+
226
+ # One character identifier per cluster of characters, in order of first use (0 stays .notdef): characters
227
+ # sharing a glyph keep their own Unicode value for text extraction, a ligature all of its characters.
228
+ def cluster_string(clusters)
229
+ clusters.map do |glyph, codes|
230
+ @cid_by_cluster[codes] ||= begin
231
+ (@glyph_by_cid ||= [0]) << glyph
232
+ @glyph_by_cid.size - 1
233
+ end
234
+ end.pack("n*")
235
+ end
236
+
237
+ # PDF word spacing (Tw) only reaches byte 32 of single-byte encodings (ISO 32000-1 §9.3.3): with two-byte
238
+ # codes, each space gets the same shift as a TJ offset instead.
239
+ def space_out_words(runs, word_spacing, font_size)
240
+ space = @layout ? cluster_string([[cmap[32], [32]]]) : glyph_string([32])
241
+ # From the spacing as Tw would write it, rounded to five decimals, so spaces land where Prawn's do.
242
+ offset = -(Float(PDF.real(word_spacing)) * 1000.0 / font_size)
243
+ runs.flat_map do |run|
244
+ next [run] unless run.is_a?(String)
245
+
246
+ run.b.scan(/../mn).slice_after(space).flat_map { |codes| codes.last == space ? [codes.join, offset] : [codes.join] }
247
+ end
248
+ end
249
+
250
+ def shaped_width_of(string, options)
251
+ width = 0
252
+ previous_glyph = nil
253
+ @layout.shape(string.codepoints).each do |glyph, codes|
254
+ if options[:kerning] && previous_glyph && (offset = kerning_between(previous_glyph, glyph))
255
+ width += offset * scale_factor
256
+ end
257
+ width += codes == [LINE_FEED] ? 0.0 : Integer(@ttf.advance_widths[glyph] * scale_factor)
258
+ previous_glyph = glyph
259
+ end
260
+ width * ((options[:size] || size) / 1000.0)
261
+ end
262
+
263
+ def shaped_runs(text, kerning)
264
+ clusters = @layout.shape(text.codepoints)
265
+ return cluster_string(clusters) unless kerning
266
+
267
+ runs = []
268
+ run = []
269
+ clusters.each do |cluster|
270
+ if run.any? && (offset = kerning_between(run.last[0], cluster[0]))
271
+ runs << cluster_string(run) << -(offset * scale_factor)
272
+ run = []
273
+ end
274
+ run << cluster
275
+ end
276
+ runs << cluster_string(run)
277
+ end
278
+
279
+ def kerning_between(left, right)
280
+ return @ttf.kerning_offset(left, right) unless @layout
281
+
282
+ @layout.kerning(left, right) || (@ttf.kerning_offset(left, right) unless options[:features].include?(:kern))
283
+ end
284
+
285
+ def encode_kerned_codepoints(codepoints)
286
+ encoded = []
287
+ run_start = 0
288
+ previous_glyph = nil
289
+ codepoints.each_with_index do |code, index|
290
+ glyph = cmap[code]
291
+ if previous_glyph && (offset = @ttf.kerning_offset(previous_glyph, glyph))
292
+ merge_encoded_parts(encoded, @simple_subsets.encode_codepoints(codepoints[run_start...index], cmap))
293
+ encoded.last[1] = Array(encoded.last[1]) << -(offset * scale_factor)
294
+ run_start = index
295
+ end
296
+ previous_glyph = glyph
297
+ end
298
+ merge_encoded_parts(encoded, @simple_subsets.encode_codepoints(codepoints[run_start..], cmap))
299
+ end
300
+
301
+ # Runs of one subset merge into the previous part when it is of the same subset, as in Prawn's TJ arrays.
302
+ def append_encoded_run(encoded, run)
303
+ merge_encoded_parts(encoded, @simple_subsets.encode(run))
304
+ end
305
+
306
+ def merge_encoded_parts(encoded, parts)
307
+ return encoded if parts.empty?
308
+
309
+ if encoded.any? && parts.first[0] == encoded.last[0]
310
+ encoded.last[1] << parts.shift[1]
311
+ end
312
+ encoded.concat(parts)
313
+ end
314
+
315
+ def register(subset)
316
+ reference = @document.ref!(Type: :Font, BaseFont: basename.delete("\0").to_sym)
317
+ @document.before_render { embed(reference, subset) }
318
+ reference
319
+ end
320
+
321
+ # A simple TrueType font of single-byte codes, as Prawn embeds them: a font program with a Mac Roman cmap
322
+ # from those codes to its glyphs, widths per code, and a ToUnicode map.
323
+ def embed(reference, subset_index)
324
+ subset = @simple_subsets[subset_index]
325
+ program = TrueType::Subset.new(@ttf)
326
+ # Glyphs numbered as TTFunk numbers them, by character code then components: TrueType hinting can depend
327
+ # on glyph indices, and a different order rasterizes differently in MuPDF.
328
+ subset.glyph_by_code.sort.each { |_code, glyph| program.glyph_for(glyph) unless glyph.zero? }
329
+ program.add_components(subset.glyph_by_code.values.uniq.sort)
330
+ glyph_by_code = subset.glyph_by_code.transform_values { program.glyph_for(it) }
331
+ mac_roman_cmap = [0, 1, 1, 0, 12, 0, 262, 0].pack("n4 N n3") + Array.new(256) { glyph_by_code[it] || 0 }.pack("C*")
332
+ font_name = :"#{subset_tag_for(program.original_glyphs)}+#{basename.delete("\0")[0, 33]}"
333
+ font_bytes = program.bytes("cmap" => mac_roman_cmap)
334
+ font_file = @document.ref!({Length1: font_bytes.bytesize}, font_bytes)
335
+ descriptor = @document.ref!(
336
+ Type: :FontDescriptor, FontName: font_name, FontBBox: bbox, Flags: pdf_flags, StemV: stem_v, ItalicAngle: italic_angle,
337
+ Ascent: @ascender, Descent: @descender, CapHeight: cap_height, XHeight: x_height, FontFile2: font_file,
338
+ )
339
+ first_code, last_code = subset.glyph_by_code.keys.minmax
340
+ widths = (first_code..last_code).map do |code|
341
+ glyph = subset.glyph_by_code[code]
342
+ glyph ? Integer(@ttf.advance_widths[glyph] * scale_factor) : 0
343
+ end
344
+ codes_by_code = subset.code_by_cluster.each_with_object({}) { |(codes, code), map| map[code] ||= Array(codes) }
345
+ reference.data.update(Subtype: :TrueType, BaseFont: font_name, FontDescriptor: descriptor, FirstChar: first_code, LastChar: last_code,
346
+ Widths: @document.ref!(widths), ToUnicode: @document.ref!({}, to_unicode_cmap(codes_by_code, code_bytes: 1)))
347
+ end
348
+
349
+ # Six capital letters derived from the glyphs kept, as ISO 32000-1 §9.6.4 asks of a subset's name.
350
+ def subset_tag
351
+ subset_tag_for(@subset.original_glyphs)
352
+ end
353
+
354
+ def subset_tag_for(glyphs)
355
+ Digest::MD5.digest(glyphs.pack("n*")).bytes.first(6).map { ("A".ord + (it % 26)).chr }.join
356
+ end
357
+
358
+ def to_unicode_cmap(codes_by_cid, code_bytes: 2)
359
+ code_format = "%0#{code_bytes * 2}X"
360
+ entries = codes_by_cid.map { |cid, codes| format("<#{code_format}> <%s>", cid, Array(codes).pack("U*").encode(::Encoding::UTF_16BE).unpack1("H*").upcase) }
361
+ blocks = entries.each_slice(TO_UNICODE_ENTRIES_PER_BLOCK).map { "#{it.size} beginbfchar\n#{it.join("\n")}\nendbfchar" }
362
+ <<~CMAP
363
+ /CIDInit /ProcSet findresource begin
364
+ 12 dict begin
365
+ begincmap
366
+ /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
367
+ /CMapName /Adobe-Identity-UCS def
368
+ /CMapType 2 def
369
+ 1 begincodespacerange
370
+ #{code_bytes == 1 ? "<00> <FF>" : "<0000> <FFFF>"}
371
+ endcodespacerange
372
+ #{blocks.join("\n")}
373
+ endcmap
374
+ CMapName currentdict /CMap defineresource pop
375
+ end
376
+ end
377
+ CMAP
378
+ end
379
+ end
380
+
381
+ class TTC < TTF
382
+ def self.font_names(file)
383
+ collection = ::File.binread(file)
384
+ Array.new(collection.unpack1("@8 N")) { TrueType::File.new(collection, index: it).font_name }
385
+ end
386
+
387
+ private
388
+
389
+ def read_ttf_file
390
+ index = @options[:font].is_a?(Numeric) ? @options[:font] : self.class.font_names(@name).index(@options[:font]) || 0
391
+ TrueType::File.open(@name, index:)
392
+ end
393
+ end
394
+
395
+ class DFont < TTF
396
+ private
397
+
398
+ def read_ttf_file
399
+ TrueType::File.open(@name, index: @options[:font] || 0, resource: true)
400
+ end
401
+ end
402
+ end
403
+
404
+ class Font
405
+ AFM = Fonts::AFM
406
+ TTF = Fonts::TTF
407
+ TTC = Fonts::TTC
408
+ DFont = Fonts::DFont
409
+ end
410
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ # What a PDF `q` saves and `Q` restores, as far as PrawnX writes it.
5
+ class GraphicState
6
+ UNDASHED = {dash: nil, space: nil, phase: 0}.freeze
7
+
8
+ attr_accessor :color_space, :dash, :cap_style, :join_style, :line_width, :fill_color, :stroke_color
9
+
10
+ def initialize(previous_state = nil)
11
+ if previous_state
12
+ @color_space = previous_state.color_space.dup
13
+ @fill_color = previous_state.fill_color.dup
14
+ @stroke_color = previous_state.stroke_color.dup
15
+ @dash = previous_state.dash.dup
16
+ @cap_style = previous_state.cap_style
17
+ @join_style = previous_state.join_style
18
+ @line_width = previous_state.line_width
19
+ else
20
+ @color_space = {}
21
+ @fill_color = "000000"
22
+ @stroke_color = "000000"
23
+ @dash = UNDASHED.dup
24
+ @cap_style = :butt
25
+ @join_style = :miter
26
+ @line_width = 1
27
+ end
28
+ end
29
+
30
+ def dup
31
+ GraphicState.new(self)
32
+ end
33
+
34
+ def dash_setting
35
+ return "[] 0 d" unless dash[:dash]
36
+
37
+ lengths = dash[:dash].is_a?(Array) ? dash[:dash] : [dash[:dash], dash[:space]]
38
+ "[#{PDF.real_params(lengths)}] #{PDF.real(dash[:phase])} d"
39
+ end
40
+ end
41
+ end