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,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Text
5
+ module Formatted
6
+ # Hands the line wrapper one string at a time out of formatted text, takes back what did not fit, and
7
+ # measures the fragments of each finished line.
8
+ class Arranger
9
+ class NotFinalized < StandardError
10
+ DEFAULT_MESSAGE = "Lines must be finalized"
11
+
12
+ def initialize(message = DEFAULT_MESSAGE, method: nil)
13
+ super((method && message == DEFAULT_MESSAGE) ? "Lines must be finalized before calling ##{method}" : message)
14
+ end
15
+ end
16
+
17
+ class BadFontFamily < StandardError
18
+ def initialize(message = "Bad font family")
19
+ super
20
+ end
21
+ end
22
+
23
+ # Relative size of subscript and superscript text.
24
+ SCRIPT_SIZE_RATIO = 0.583
25
+
26
+ attr_reader :max_line_height, :max_descender, :max_ascender, :finalized, :unconsumed, :fragments, :current_format_state
27
+ attr_accessor :consumed
28
+
29
+ def initialize(document, options = {})
30
+ @document = document
31
+ @fragments = []
32
+ @unconsumed = []
33
+ @kerning = options[:kerning]
34
+ end
35
+
36
+ def space_count
37
+ raise NotFinalized.new(method: "space_count") unless finalized
38
+
39
+ @fragments.sum(&:space_count)
40
+ end
41
+
42
+ def line_width
43
+ raise NotFinalized.new(method: "line_width") unless finalized
44
+
45
+ @fragments.inject(0) { |sum, fragment| sum + fragment.width }
46
+ end
47
+
48
+ def line
49
+ raise NotFinalized.new(method: "line") unless finalized
50
+
51
+ @fragments.map do |fragment|
52
+ fragment.text.encode(::Encoding::UTF_8)
53
+ rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
54
+ fragment.text.dup.force_encoding(::Encoding::UTF_8)
55
+ end.join
56
+ end
57
+
58
+ def finalize_line
59
+ @finalized = true
60
+ omit_trailing_whitespace_from_line_width
61
+ @fragments = []
62
+ @consumed.each do |hash|
63
+ fragment = Fragment.new(hash[:text], hash.except(:text), @document)
64
+ @fragments << fragment
65
+ measure(fragment)
66
+ @max_line_height = [@max_line_height, fragment.line_height].compact.max
67
+ @max_descender = [@max_descender, fragment.descender].compact.max
68
+ @max_ascender = [@max_ascender, fragment.ascender].compact.max
69
+ end
70
+ end
71
+
72
+ def format_array=(array)
73
+ initialize_line
74
+ @unconsumed = []
75
+ array.each do |hash|
76
+ hash[:text].scan(/[^\n]+|\n/) { |line| @unconsumed << hash.merge(text: line) }
77
+ end
78
+ end
79
+
80
+ def initialize_line
81
+ @finalized = false
82
+ @max_line_height = 0
83
+ @max_descender = 0
84
+ @max_ascender = 0
85
+ @consumed = []
86
+ @fragments = []
87
+ end
88
+
89
+ def finished?
90
+ @unconsumed.empty?
91
+ end
92
+
93
+ def next_string
94
+ raise NotFinalized.new(method: "next_string") if finalized
95
+
96
+ next_unconsumed_hash = @unconsumed.shift
97
+ return unless next_unconsumed_hash
98
+
99
+ @consumed << next_unconsumed_hash.dup
100
+ @current_format_state = next_unconsumed_hash.except(:text)
101
+ next_unconsumed_hash[:text]
102
+ end
103
+
104
+ def preview_next_string
105
+ @unconsumed.first&.fetch(:text)
106
+ end
107
+
108
+ def apply_color_and_font_settings(fragment, &)
109
+ return apply_font_settings(fragment, &) unless fragment.color
110
+
111
+ original_fill_color = @document.fill_color
112
+ original_stroke_color = @document.stroke_color
113
+ @document.fill_color(*fragment.color)
114
+ @document.stroke_color(*fragment.color)
115
+ apply_font_settings(fragment, &)
116
+ @document.stroke_color = original_stroke_color
117
+ @document.fill_color = original_fill_color
118
+ end
119
+
120
+ def apply_font_settings(fragment = nil, &)
121
+ if fragment
122
+ font = fragment.font
123
+ size = fragment.size
124
+ character_spacing = fragment.character_spacing
125
+ styles = fragment.styles
126
+ else
127
+ font = current_format_state[:font]
128
+ size = current_format_state[:size]
129
+ character_spacing = current_format_state[:character_spacing] || @document.character_spacing
130
+ styles = current_format_state[:styles]
131
+ end
132
+ font_style = font_style(styles)
133
+
134
+ @document.character_spacing(character_spacing) do
135
+ if font || font_style != :normal
136
+ raise BadFontFamily unless @document.font.family
137
+
138
+ @document.font(font || @document.font.family, style: font_style) { apply_font_size(size, styles, &) }
139
+ else
140
+ apply_font_size(size, styles, &)
141
+ end
142
+ end
143
+ end
144
+
145
+ def update_last_string(printed, unprinted, normalized_soft_hyphen = nil)
146
+ return if printed.nil?
147
+
148
+ if printed.empty?
149
+ @consumed.pop
150
+ else
151
+ @consumed.last[:text] = printed
152
+ @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen if normalized_soft_hyphen
153
+ end
154
+ @unconsumed.unshift(@current_format_state.merge(text: unprinted)) unless unprinted.empty?
155
+ load_previous_format_state if printed.empty?
156
+ end
157
+
158
+ def retrieve_fragment
159
+ raise NotFinalized, "Lines must be finalized before fragments can be retrieved" unless finalized
160
+
161
+ @fragments.shift
162
+ end
163
+
164
+ def repack_unretrieved
165
+ new_unconsumed = []
166
+ while (fragment = retrieve_fragment)
167
+ fragment.include_trailing_white_space!
168
+ new_unconsumed << fragment.format_state.merge(text: fragment.text)
169
+ end
170
+ @unconsumed = new_unconsumed.concat(@unconsumed)
171
+ end
172
+
173
+ def font_style(styles)
174
+ styles = Array(styles)
175
+ bold = styles.include?(:bold)
176
+ italic = styles.include?(:italic)
177
+ if bold && italic then :bold_italic
178
+ elsif bold then :bold
179
+ elsif italic then :italic
180
+ else :normal
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def load_previous_format_state
187
+ @current_format_state = @consumed.empty? ? {} : @consumed.last.except(:text)
188
+ end
189
+
190
+ def apply_font_size(size, styles, &)
191
+ if styles&.include?(:subscript) || styles&.include?(:superscript)
192
+ size = (size || @document.font_size) * SCRIPT_SIZE_RATIO
193
+ end
194
+ size.nil? ? yield : @document.font_size(size, &)
195
+ end
196
+
197
+ def omit_trailing_whitespace_from_line_width
198
+ @consumed.reverse_each do |hash|
199
+ break if hash[:text] == "\n"
200
+
201
+ hash[:exclude_trailing_white_space] = true
202
+ break unless hash[:text].strip.empty? && @consumed.length > 1
203
+ end
204
+ end
205
+
206
+ def measure(fragment)
207
+ apply_font_settings(fragment) do
208
+ fragment.width = @document.width_of(fragment.text, kerning: @kerning)
209
+ fragment.line_height = @document.font.height
210
+ fragment.descender = @document.font.descender
211
+ fragment.ascender = @document.font.ascender
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Text
5
+ module Formatted
6
+ # Formatted text laid out in a rectangle: wrapped, aligned, shrunk or truncated to fit, possibly rotated.
7
+ class Box
8
+ include Wrap
9
+
10
+ VALID_OPTIONS = (Text::VALID_OPTIONS + %i[
11
+ at height width align valign rotate rotate_around overflow min_font_size disable_wrap_by_char leading
12
+ character_spacing mode single_line document direction fallback_fonts draw_text_callback
13
+ ]).freeze
14
+ DEFAULT_MINIMUM_FONT_SIZE = 5
15
+ SHRINK_STEP = 0.5
16
+
17
+ attr_reader :text, :at, :line_height, :ascender, :descender, :leading
18
+
19
+ # Modules every new box of this class extends itself with: a subclass starts with its parent's.
20
+ def self.extensions
21
+ @extensions ||= []
22
+ end
23
+
24
+ def self.inherited(subclass)
25
+ super
26
+ subclass.extensions.concat(extensions)
27
+ end
28
+
29
+ def initialize(formatted_text, options = {})
30
+ @inked = false
31
+ self.class.extensions.reverse_each { extend(_1) }
32
+ Prawn.verify_options(valid_options, options)
33
+ options = options.dup
34
+ @overflow = options[:overflow] || :truncate
35
+ @disable_wrap_by_char = options[:disable_wrap_by_char]
36
+ @original_array = formatted_text
37
+ @text = nil
38
+ @document = options[:document]
39
+ @direction = options[:direction] || @document.text_direction
40
+ @fallback_fonts = options[:fallback_fonts] || @document.fallback_fonts
41
+ @at = (options[:at] || [@document.bounds.left, @document.bounds.top]).dup
42
+ @width = options[:width] || (@document.bounds.right - @at[0])
43
+ @height = options[:height] || default_height
44
+ @align = options[:align] || ((@direction == :rtl) ? :right : :left)
45
+ @vertical_align = options[:valign] || :top
46
+ @leading = options[:leading] || @document.default_leading
47
+ @character_spacing = options[:character_spacing] || @document.character_spacing
48
+ @mode = options[:mode] || @document.text_rendering_mode
49
+ @mode = :fill if @mode == :unknown
50
+ @rotate = options[:rotate] || 0
51
+ @rotate_around = options[:rotate_around] || :upper_left
52
+ @single_line = options[:single_line]
53
+ @draw_text_callback = options[:draw_text_callback]
54
+ if @overflow == :expand
55
+ @height = default_height
56
+ @overflow = :truncate
57
+ end
58
+ @min_font_size = options[:min_font_size] || DEFAULT_MINIMUM_FONT_SIZE
59
+ options[:kerning] = @document.default_kerning? if options[:kerning].nil?
60
+ @options = {kerning: options[:kerning], size: options[:size], style: options[:style]}
61
+ super
62
+ end
63
+
64
+ def nothing_printed?
65
+ @nothing_printed
66
+ end
67
+
68
+ def everything_printed?
69
+ @everything_printed
70
+ end
71
+
72
+ def line_gap
73
+ line_height - (ascender + descender)
74
+ end
75
+
76
+ def render(flags = {})
77
+ unprinted_text = []
78
+ @laid_out_lines = flags[:keep_layout] ? [] : nil
79
+ with_text_settings do
80
+ text = normalized_text(flags)
81
+ shrink_to_fit(text) if @overflow == :shrink_to_fit
82
+ process_vertical_alignment(text)
83
+ @inked = true unless flags[:dry_run]
84
+ unprinted_text = (@rotate != 0 && @inked) ? render_rotated(text) : wrap(text)
85
+ @inked = false
86
+ end
87
+ unprinted_text.map { |fragment| fragment.merge(text: @document.font.to_utf8(fragment[:text])) }
88
+ end
89
+
90
+ # Whether the lines of a `render(dry_run: true, keep_layout: true)` can be drawn again as they are.
91
+ def layout_reusable?
92
+ !@laid_out_lines.nil? && @everything_printed && @rotate.zero? && @vertical_align == :top &&
93
+ @overflow != :shrink_to_fit && self.class.extensions.empty? && @original_array.none? { _1[:color] }
94
+ end
95
+
96
+ # Draws, with its top left corner at `at`, the lines the last kept layout wrapped, without wrapping them again.
97
+ def render_laid_out(at)
98
+ @at = at.dup
99
+ with_text_settings do
100
+ @inked = true
101
+ @laid_out_lines.each do |baseline_y, drawn_fragments|
102
+ @baseline_y = baseline_y
103
+ drawn_fragments.each { format_and_draw_fragment(*_1) }
104
+ end
105
+ @inked = false
106
+ end
107
+ end
108
+
109
+ def available_width
110
+ @width
111
+ end
112
+
113
+ def height
114
+ return 0 if @baseline_y.nil? || @descender.nil?
115
+
116
+ (@baseline_y - @descender).abs
117
+ end
118
+
119
+ def draw_fragment(fragment, accumulated_width = 0, line_width = 0, word_spacing = 0)
120
+ x =
121
+ case @align
122
+ when :left then @at[0]
123
+ when :center then @at[0] + (@width * 0.5) - (line_width * 0.5)
124
+ when :right then @at[0] + @width - line_width
125
+ when :justify then (@direction == :ltr) ? @at[0] : @at[0] + @width - line_width
126
+ else raise ArgumentError, "align must be one of :left, :right, :center or :justify symbols"
127
+ end
128
+ x += accumulated_width
129
+ y = @at[1] + @baseline_y + fragment.y_offset
130
+ fragment.left = x
131
+ fragment.baseline = y
132
+ return unless @inked
133
+
134
+ fragment.callback_objects.each { |callback| callback.render_behind(fragment) if callback.respond_to?(:render_behind) }
135
+ @document.word_spacing(word_spacing) do
136
+ if @draw_text_callback
137
+ @draw_text_callback.call(fragment.text, at: [x, y], kerning: @kerning)
138
+ else
139
+ @document.draw_text!(fragment.text, at: [x, y], kerning: @kerning)
140
+ end
141
+ end
142
+ draw_fragment_overlays(fragment)
143
+ end
144
+
145
+ def valid_options
146
+ VALID_OPTIONS
147
+ end
148
+
149
+ private
150
+
151
+ def with_text_settings(&)
152
+ @document.save_font do
153
+ @document.character_spacing(@character_spacing) do
154
+ @document.text_rendering_mode(@mode) do
155
+ process_options
156
+ @document.font_size(@font_size, &)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def normalized_text(flags)
163
+ text = normalize_encoding
164
+ text.each { _1.delete(:color) } if flags[:dry_run]
165
+ text
166
+ end
167
+
168
+ def original_text
169
+ @original_array.map(&:dup)
170
+ end
171
+
172
+ def normalize_encoding
173
+ formatted_text = original_text
174
+ formatted_text = formatted_text.flat_map { fragments_by_fallback_font(_1) } unless @fallback_fonts.empty?
175
+ formatted_text.each do |hash|
176
+ if hash[:font]
177
+ @document.font(hash[:font]) { hash[:text] = @document.font.normalize_encoding(hash[:text]) }
178
+ else
179
+ hash[:text] = @document.font.normalize_encoding(hash[:text])
180
+ end
181
+ end
182
+ formatted_text
183
+ end
184
+
185
+ def fragments_by_fallback_font(hash)
186
+ original_font = @document.font.family
187
+ fragment_font = hash[:font] || original_font
188
+ fragment_font_style = @arranger.font_style(hash[:styles])
189
+ fragment_font_options = (fragment_font_style == :normal) ? {} : {style: fragment_font_style}
190
+ fallback_fonts = [*@fallback_fonts, fragment_font]
191
+
192
+ font_glyph_pairs = []
193
+ @document.save_font do
194
+ hash[:text].each_char do |character|
195
+ font_glyph_pairs << [font_for_glyph(character, fragment_font, fallback_fonts.dup, fragment_font_options), character]
196
+ end
197
+ end
198
+ font_glyph_pairs.each { |pair| pair[0] = nil if pair[0] == original_font } if hash[:font].nil?
199
+
200
+ fragments = []
201
+ current_font = nil
202
+ font_glyph_pairs.each do |font, character|
203
+ if font != current_font || fragments.empty?
204
+ current_font = font
205
+ fragment = hash.merge(text: character)
206
+ fragment[:font] = font unless font.nil?
207
+ fragments << fragment
208
+ else
209
+ fragments.last[:text] += character
210
+ end
211
+ end
212
+ fragments
213
+ end
214
+
215
+ def font_for_glyph(character, current_font, fallback_fonts, current_font_options = {})
216
+ loop do
217
+ @document.font(current_font, current_font_options)
218
+ return current_font if fallback_fonts.empty? || @document.font.glyph_present?(character)
219
+
220
+ current_font = fallback_fonts.shift
221
+ end
222
+ end
223
+
224
+ def move_baseline_down
225
+ if @baseline_y.zero?
226
+ @baseline_y = -@ascender
227
+ else
228
+ @baseline_y -= @line_height + @leading
229
+ end
230
+ end
231
+
232
+ def default_height
233
+ frame = @document.bounds
234
+ frame = frame.parent while frame.stretchy? && frame.parent
235
+ @at[1] + @document.bounds.absolute_bottom - frame.absolute_bottom
236
+ end
237
+
238
+ def process_vertical_alignment(text)
239
+ return if @vertical_alignment_processed
240
+
241
+ @vertical_alignment_processed = true
242
+ return if @vertical_align == :top
243
+
244
+ wrap(text)
245
+ case @vertical_align
246
+ when :center then @at[1] -= (@height - height + @descender) * 0.5
247
+ when :bottom then @at[1] -= @height - height
248
+ else raise ArgumentError, "valign must be one of :left, :right or :center symbols"
249
+ end
250
+ @height = height
251
+ end
252
+
253
+ def shrink_to_fit(text)
254
+ loop do
255
+ if @disable_wrap_by_char && @font_size > @min_font_size
256
+ begin
257
+ wrap(text)
258
+ rescue Errors::CannotFit
259
+ # Too big for a word to fit whole: the next, smaller size may make it fit.
260
+ end
261
+ else
262
+ wrap(text)
263
+ end
264
+ break if @everything_printed || @font_size <= @min_font_size
265
+
266
+ @font_size = [@font_size - SHRINK_STEP, @min_font_size].max
267
+ @document.font_size = @font_size
268
+ end
269
+ end
270
+
271
+ def process_options
272
+ @document.process_text_options(@options)
273
+ @font_size = @options[:size]
274
+ @kerning = @options[:kerning]
275
+ end
276
+
277
+ def render_rotated(text)
278
+ x, y =
279
+ case @rotate_around
280
+ when :center then [@at[0] + (@width * 0.5), @at[1] - (@height * 0.5)]
281
+ when :upper_right then [@at[0] + @width, @at[1]]
282
+ when :lower_right then [@at[0] + @width, @at[1] - @height]
283
+ when :lower_left then [@at[0], @at[1] - @height]
284
+ else [@at[0], @at[1]]
285
+ end
286
+ unprinted_text = ""
287
+ @document.rotate(@rotate, origin: [x, y]) { unprinted_text = wrap(text) }
288
+ unprinted_text
289
+ end
290
+
291
+ def draw_fragment_overlays(fragment)
292
+ @document.stroke_line(fragment.underline_points) if fragment.styles.include?(:underline)
293
+ @document.stroke_line(fragment.strikethrough_points) if fragment.styles.include?(:strikethrough)
294
+ draw_link_annotations(fragment)
295
+ fragment.callback_objects.each { |callback| callback.render_in_front(fragment) if callback.respond_to?(:render_in_front) }
296
+ end
297
+
298
+ def draw_link_annotations(fragment)
299
+ if fragment.link
300
+ @document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0],
301
+ A: {Type: :Action, S: :URI, URI: PDF::Core::LiteralString.new(fragment.link)})
302
+ end
303
+ @document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0], Dest: fragment.anchor) if fragment.anchor
304
+ return unless fragment.local
305
+
306
+ @document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0],
307
+ A: {Type: :Action, S: :Launch, F: PDF::Core::LiteralString.new(fragment.local), NewWindow: true})
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end