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,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Text
5
+ module Formatted
6
+ # A run of text sharing one format on one line, with its measurements and, once drawn, its position.
7
+ class Fragment
8
+ attr_reader :format_state, :text
9
+ attr_writer :width
10
+ attr_accessor :line_height, :descender, :ascender, :word_spacing, :left, :baseline
11
+
12
+ def initialize(text, format_state, document)
13
+ @format_state = format_state
14
+ @document = document
15
+ @word_spacing = 0
16
+ @original_text = text
17
+ @text = process_text(@original_text)
18
+ end
19
+
20
+ def width
21
+ @word_spacing.zero? ? @width : @width + (@word_spacing * space_count)
22
+ end
23
+
24
+ def height
25
+ top - bottom
26
+ end
27
+
28
+ def subscript?
29
+ styles.include?(:subscript)
30
+ end
31
+
32
+ def superscript?
33
+ styles.include?(:superscript)
34
+ end
35
+
36
+ def y_offset
37
+ if subscript? then -descender
38
+ elsif superscript? then 0.85 * ascender
39
+ else 0
40
+ end
41
+ end
42
+
43
+ def bounding_box
44
+ [left, bottom, right, top]
45
+ end
46
+
47
+ def absolute_bounding_box
48
+ left, bottom, right, top = bounding_box
49
+ bounds = @document.bounds
50
+ [left + bounds.absolute_left, bottom + bounds.absolute_bottom, right + bounds.absolute_left, top + bounds.absolute_bottom]
51
+ end
52
+
53
+ def underline_points
54
+ y = baseline - 1.25
55
+ [[left, y], [right, y]]
56
+ end
57
+
58
+ def strikethrough_points
59
+ y = baseline + (ascender * 0.3)
60
+ [[left, y], [right, y]]
61
+ end
62
+
63
+ def styles
64
+ @format_state[:styles] || []
65
+ end
66
+
67
+ def link
68
+ @format_state[:link]
69
+ end
70
+
71
+ def anchor
72
+ @format_state[:anchor]
73
+ end
74
+
75
+ def local
76
+ @format_state[:local]
77
+ end
78
+
79
+ def color
80
+ @format_state[:color]
81
+ end
82
+
83
+ def font
84
+ @format_state[:font]
85
+ end
86
+
87
+ def size
88
+ @format_state[:size]
89
+ end
90
+
91
+ def character_spacing
92
+ @format_state[:character_spacing] || @document.character_spacing
93
+ end
94
+
95
+ def direction
96
+ @format_state[:direction]
97
+ end
98
+
99
+ def default_direction=(direction)
100
+ return if @format_state[:direction]
101
+
102
+ @format_state[:direction] = direction
103
+ @text = process_text(@original_text)
104
+ end
105
+
106
+ def include_trailing_white_space!
107
+ @format_state.delete(:exclude_trailing_white_space)
108
+ @text = process_text(@original_text)
109
+ end
110
+
111
+ def space_count
112
+ @text.count(" ")
113
+ end
114
+
115
+ def callback_objects
116
+ callback = @format_state[:callback]
117
+ callback.nil? ? [] : Array(callback)
118
+ end
119
+
120
+ def right
121
+ left + width
122
+ end
123
+
124
+ def top
125
+ baseline + ascender
126
+ end
127
+
128
+ def bottom
129
+ baseline - descender
130
+ end
131
+
132
+ def top_left
133
+ [left, top]
134
+ end
135
+
136
+ def top_right
137
+ [right, top]
138
+ end
139
+
140
+ def bottom_right
141
+ [right, bottom]
142
+ end
143
+
144
+ def bottom_left
145
+ [left, bottom]
146
+ end
147
+
148
+ def absolute_left
149
+ absolute_bounding_box[0]
150
+ end
151
+
152
+ def absolute_right
153
+ absolute_bounding_box[2]
154
+ end
155
+
156
+ def absolute_top
157
+ absolute_bounding_box[3]
158
+ end
159
+
160
+ def absolute_bottom
161
+ absolute_bounding_box[1]
162
+ end
163
+
164
+ def absolute_top_left
165
+ [absolute_left, absolute_top]
166
+ end
167
+
168
+ def absolute_top_right
169
+ [absolute_right, absolute_top]
170
+ end
171
+
172
+ def absolute_bottom_left
173
+ [absolute_left, absolute_bottom]
174
+ end
175
+
176
+ def absolute_bottom_right
177
+ [absolute_right, absolute_bottom]
178
+ end
179
+
180
+ private
181
+
182
+ def process_text(text)
183
+ string = (text.encoding == ::Encoding::UTF_8) ? text.gsub(ZWSP, "") : text
184
+ if @format_state[:exclude_trailing_white_space]
185
+ string = string.rstrip
186
+ string = process_soft_hyphens(string[0..-2]) + string[-1..] if soft_hyphens_need_processing?(string)
187
+ elsif soft_hyphens_need_processing?(string)
188
+ string = process_soft_hyphens(string)
189
+ end
190
+ (direction == :rtl) ? string.reverse : string
191
+ end
192
+
193
+ def soft_hyphens_need_processing?(string)
194
+ !string.empty? && @format_state[:normalized_soft_hyphen]
195
+ end
196
+
197
+ def process_soft_hyphens(string)
198
+ soft_hyphen = @format_state[:normalized_soft_hyphen]
199
+ string = string.dup.force_encoding(soft_hyphen.encoding) if string.encoding != soft_hyphen.encoding
200
+ string.gsub(soft_hyphen, "")
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Prawn
6
+ module Text
7
+ module Formatted
8
+ # Fills one line with as many words of the arranger's fragments as fit, breaking after spaces, hyphens and
9
+ # soft hyphens, and inside a word only when the line holds a single one.
10
+ class LineWrap
11
+ # Characters and patterns of one encoding, computed once: Prawn rebuilt them for every line.
12
+ Breaks = Data.define(:soft_hyphen, :zero_width_space, :scan_pattern, :word_division_pattern, :ends_with_break_pattern,
13
+ :last_word_pattern, :begins_with_break_pattern)
14
+
15
+ BREAKS_BY_ENCODING = Hash.new do |cache, encoding|
16
+ cache[encoding] = breaks_for(encoding)
17
+ end
18
+
19
+ def self.encode_or_nil(character, encoding)
20
+ character.encode(encoding)
21
+ rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
22
+ nil
23
+ end
24
+
25
+ def self.breaks_for(encoding)
26
+ soft_hyphen = encode_or_nil(SHY, encoding)
27
+ zero_width_space = encode_or_nil(ZWSP, encoding)
28
+ whitespace = " \t#{zero_width_space}".encode(encoding)
29
+ break_characters = [whitespace, soft_hyphen, "-"].join
30
+ patterns = ["[^#{break_characters}]+#{soft_hyphen}", "[^#{break_characters}]+-+", "[^#{break_characters}]+", "[#{whitespace}]+",
31
+ "-+[^#{break_characters}]*", soft_hyphen.to_s]
32
+ Breaks.new(
33
+ soft_hyphen: soft_hyphen&.freeze,
34
+ zero_width_space: zero_width_space&.freeze,
35
+ scan_pattern: Regexp.new(patterns.map { _1.encode(encoding) }.join("|")),
36
+ word_division_pattern: Regexp.union([*["\t", "\n", "\v", "\r", " "].map { _1.encode(encoding) }, zero_width_space, soft_hyphen, "-"].compact),
37
+ ends_with_break_pattern: Regexp.new("[#{break_characters}]$"),
38
+ last_word_pattern: Regexp.new("[^#{break_characters}]*$"),
39
+ begins_with_break_pattern: Regexp.new("^[#{break_characters}]")
40
+ )
41
+ end
42
+
43
+ attr_reader :space_count
44
+
45
+ def width
46
+ @accumulated_width || 0
47
+ end
48
+
49
+ def paragraph_finished?
50
+ @newline_encountered || next_string_newline? || @arranger.finished?
51
+ end
52
+
53
+ def tokenize(fragment)
54
+ fragment.scan(BREAKS_BY_ENCODING[fragment.encoding].scan_pattern)
55
+ end
56
+
57
+ def wrap_line(options)
58
+ initialize_line(options)
59
+ while (fragment = @arranger.next_string)
60
+ @fragment_output = +""
61
+ fragment.lstrip! if first_fragment_on_this_line?(fragment)
62
+ next if empty_line?(fragment)
63
+ break unless apply_font_settings_and_add_fragment_to_line(fragment)
64
+ end
65
+ @arranger.finalize_line
66
+ @accumulated_width = @arranger.line_width
67
+ @space_count = @arranger.space_count
68
+ @arranger.line
69
+ end
70
+
71
+ private
72
+
73
+ def first_fragment_on_this_line?(fragment)
74
+ line_empty? && fragment != "\n"
75
+ end
76
+
77
+ def empty_line?(fragment)
78
+ empty = line_empty? && fragment.empty? && next_string_newline?
79
+ @arranger.update_last_string("", "", BREAKS_BY_ENCODING[fragment.encoding].soft_hyphen) if empty
80
+ empty
81
+ end
82
+
83
+ def next_string_newline?
84
+ @arranger.preview_next_string == "\n"
85
+ end
86
+
87
+ def apply_font_settings_and_add_fragment_to_line(fragment)
88
+ added = nil
89
+ @arranger.apply_font_settings { added = add_fragment_to_line(fragment) }
90
+ added
91
+ end
92
+
93
+ def add_fragment_to_line(fragment)
94
+ case fragment
95
+ when "" then return true
96
+ when "\n"
97
+ @newline_encountered = true
98
+ return false
99
+ end
100
+
101
+ breaks = BREAKS_BY_ENCODING[fragment.encoding]
102
+ # Words are read one at a time: a line stops long before the end of its paragraph.
103
+ scanner = StringScanner.new(fragment)
104
+ while (segment = scanner.scan(breaks.scan_pattern))
105
+ segment_width = (segment == breaks.zero_width_space) ? 0 : @document.width_of(segment, @width_options)
106
+ if @accumulated_width + segment_width <= @width
107
+ @accumulated_width += segment_width
108
+ @accumulated_width -= @document.width_of(breaks.soft_hyphen, @width_options) if breaks.soft_hyphen && segment.end_with?(breaks.soft_hyphen)
109
+ @fragment_output << segment
110
+ else
111
+ @line_contains_more_than_one_word = false if @accumulated_width.zero? && @line_contains_more_than_one_word
112
+ end_of_the_line_reached(segment)
113
+ fragment_finished(fragment)
114
+ return false
115
+ end
116
+ end
117
+ fragment_finished(fragment)
118
+ true
119
+ end
120
+
121
+ def line_empty?
122
+ @line_empty && @accumulated_width.zero?
123
+ end
124
+
125
+ def initialize_line(options)
126
+ @document = options[:document]
127
+ # Shared by every word measured on the line: passing kerning: to each call built a hash per word.
128
+ @width_options = {kerning: options[:kerning]}
129
+ @width = options[:width]
130
+ @disable_wrap_by_char = options[:disable_wrap_by_char]
131
+ @accumulated_width = 0
132
+ @line_empty = true
133
+ @line_contains_more_than_one_word = false
134
+ @arranger = options[:arranger]
135
+ @arranger.initialize_line
136
+ @newline_encountered = false
137
+ @line_full = false
138
+ end
139
+
140
+ def fragment_finished(fragment)
141
+ if fragment == "\n"
142
+ @newline_encountered = true
143
+ @line_empty = false
144
+ else
145
+ update_output_based_on_last_fragment(fragment, BREAKS_BY_ENCODING[fragment.encoding].soft_hyphen)
146
+ update_line_status_based_on_last_output
147
+ pull_preceding_fragment_to_join_this_one?(fragment)
148
+ end
149
+ remember_this_fragment_for_backward_looking_ops
150
+ end
151
+
152
+ def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
153
+ remaining_text = fragment.slice(@fragment_output.length..fragment.length)
154
+ raise Errors::CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
155
+
156
+ @arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
157
+ end
158
+
159
+ def pull_preceding_fragment_to_join_this_one?(current_fragment)
160
+ return unless @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
161
+ return if @previous_fragment_ended_with_breakable || fragment_begins_with_breakable?(current_fragment)
162
+
163
+ @fragment_output = @previous_fragment_output_without_last_word
164
+ update_output_based_on_last_fragment(@previous_fragment)
165
+ end
166
+
167
+ def remember_this_fragment_for_backward_looking_ops
168
+ @previous_fragment = @fragment_output.dup
169
+ breaks = BREAKS_BY_ENCODING[@previous_fragment.encoding]
170
+ @previous_fragment_ended_with_breakable = @previous_fragment.match?(breaks.ends_with_break_pattern)
171
+ last_word = @previous_fragment.slice(breaks.last_word_pattern)
172
+ @previous_fragment_output_without_last_word = @previous_fragment.slice(0, @previous_fragment.length - (last_word&.length || 0))
173
+ end
174
+
175
+ def fragment_begins_with_breakable?(fragment)
176
+ fragment.match?(BREAKS_BY_ENCODING[fragment.encoding].begins_with_break_pattern)
177
+ end
178
+
179
+ def line_finished?
180
+ @line_full || paragraph_finished?
181
+ end
182
+
183
+ def update_line_status_based_on_last_output
184
+ @line_contains_more_than_one_word = true if @fragment_output&.match?(BREAKS_BY_ENCODING[@fragment_output.encoding].word_division_pattern)
185
+ end
186
+
187
+ def end_of_the_line_reached(segment)
188
+ update_line_status_based_on_last_output
189
+ wrap_by_char(segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
190
+ @line_full = true
191
+ end
192
+
193
+ def wrap_by_char(segment)
194
+ segment.each_char do |character|
195
+ character_width = @document.width_of(character)
196
+ break unless @accumulated_width + character_width <= @width
197
+
198
+ @accumulated_width += character_width
199
+ @fragment_output << character
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Text
5
+ module Formatted
6
+ # Turns inline markup (<b>, <i>, <u>, <strikethrough>, <sub>, <sup>, <link>, <a>, <color>, <font>, <br>)
7
+ # into formatted text arrays, and back.
8
+ class Parser
9
+ PARSER_REGEX = Regexp.new(
10
+ "\n|<b>|</b>|<i>|</i>|<u>|</u>|<strikethrough>|</strikethrough>|<sub>|</sub>|<sup>|</sup>|<link[^>]*>|</link>|" \
11
+ "<color[^>]*>|</color>|<font[^>]*>|</font>|<strong>|</strong>|<em>|</em>|<a[^>]*>|</a>|[^<\n]+",
12
+ Regexp::MULTILINE
13
+ )
14
+ ESCAPE_CHARS = {"&" => "&amp;", ">" => "&gt;", "<" => "&lt;"}.freeze
15
+ UNESCAPE_CHARS = ESCAPE_CHARS.invert.freeze
16
+ CMYK_ATTRIBUTES = %w[c m y k].freeze
17
+ ESCAPE_PATTERN = Regexp.union(ESCAPE_CHARS.keys)
18
+ UNESCAPE_PATTERN = Regexp.union(UNESCAPE_CHARS.keys)
19
+ STYLE_BY_OPENING_TAG = {
20
+ "<b>" => :bold, "<strong>" => :bold, "<i>" => :italic, "<em>" => :italic, "<u>" => :underline,
21
+ "<strikethrough>" => :strikethrough, "<sub>" => :subscript, "<sup>" => :superscript
22
+ }.freeze
23
+ STYLE_BY_CLOSING_TAG = STYLE_BY_OPENING_TAG.transform_keys { _1.sub("<", "</") }.freeze
24
+ PREFIX_BY_STYLE = {bold: "<b>", italic: "<i>", underline: "<u>", strikethrough: "<strikethrough>", subscript: "<sub>", superscript: "<sup>"}.freeze
25
+ SUFFIX_BY_STYLE = PREFIX_BY_STYLE.transform_values { _1.sub("<", "</") }.freeze
26
+
27
+ def self.format(string, *_arguments)
28
+ array_from_tokens(string.gsub(%r{<br\s*/?>}, "\n").scan(PARSER_REGEX))
29
+ end
30
+
31
+ def self.to_string(array)
32
+ array.map do |hash|
33
+ prefix = +""
34
+ suffix = +""
35
+ hash[:styles]&.each do |style|
36
+ prefix << PREFIX_BY_STYLE[style]
37
+ suffix = SUFFIX_BY_STYLE[style] + suffix
38
+ end
39
+ font = (" name='#{hash[:font]}'" if hash[:font])
40
+ size = (" size='#{hash[:size]}'" if hash[:size])
41
+ character_spacing = (" character_spacing='#{hash[:character_spacing]}'" if hash[:character_spacing])
42
+ if font || size || character_spacing
43
+ prefix << "<font#{font}#{size}#{character_spacing}>"
44
+ suffix = "</font>"
45
+ end
46
+ link = (" href='#{hash[:link]}'" if hash[:link])
47
+ anchor = (" anchor='#{hash[:anchor]}'" if hash[:anchor])
48
+ if link || anchor
49
+ prefix << "<link#{link}#{anchor}>"
50
+ suffix = "</link>"
51
+ end
52
+ if hash[:color]
53
+ prefix <<
54
+ if hash[:color].is_a?(Array)
55
+ c, m, y, k = hash[:color]
56
+ "<color c='#{c}' m='#{m}' y='#{y}' k='#{k}'>"
57
+ else
58
+ "<color rgb='#{hash[:color]}'>"
59
+ end
60
+ suffix = "</color>"
61
+ end
62
+ prefix + escape(hash[:text]) + suffix
63
+ end.join
64
+ end
65
+
66
+ def self.array_paragraphs(array)
67
+ paragraphs = []
68
+ paragraph = []
69
+ previous_string = "\n"
70
+ array.each do |hash|
71
+ hash[:text].scan(/[^\n]+|\n/).each do |string|
72
+ if string == "\n"
73
+ paragraph << hash.merge(text: "\n") if previous_string == "\n"
74
+ paragraphs << paragraph unless paragraph.empty?
75
+ paragraph = []
76
+ else
77
+ paragraph << hash.merge(text: string)
78
+ end
79
+ previous_string = string
80
+ end
81
+ end
82
+ paragraphs << paragraph unless paragraph.empty?
83
+ paragraphs
84
+ end
85
+
86
+ def self.array_from_tokens(tokens)
87
+ array = []
88
+ styles = []
89
+ colors = []
90
+ link = nil
91
+ anchor = nil
92
+ local = nil
93
+ fonts = []
94
+ sizes = []
95
+ character_spacings = []
96
+ tokens.each do |token|
97
+ if (style = STYLE_BY_OPENING_TAG[token])
98
+ styles << style
99
+ elsif (style = STYLE_BY_CLOSING_TAG[token])
100
+ styles.delete(style)
101
+ else
102
+ case token
103
+ when "</link>", "</a>"
104
+ link = anchor = local = nil
105
+ when "</color>"
106
+ colors.pop
107
+ when "</font>"
108
+ fonts.pop
109
+ sizes.pop
110
+ character_spacings.pop
111
+ when /^<link[^>]*>$/, /^<a[^>]*>$/
112
+ link = attribute(token, "href") || link
113
+ anchor = attribute(token, "anchor") || anchor
114
+ local = attribute(token, "local") || local
115
+ when /^<color[^>]*>$/
116
+ rgb = attribute(token, "rgb", hash_prefixed: true)
117
+ colors << rgb if rgb
118
+ cmyk = CMYK_ATTRIBUTES.map { |name| attribute(token, name, hash_prefixed: true)&.then { |value| Integer(value, 10) } }
119
+ colors << cmyk if cmyk.all?
120
+ when /^<font[^>]*>$/
121
+ attribute(token, "name")&.then { fonts << _1 }
122
+ attribute(token, "size")&.then { sizes << Float(_1) }
123
+ attribute(token, "character_spacing")&.then { character_spacings << Float(_1) }
124
+ else
125
+ array << {
126
+ text: unescape(token), styles: styles.dup, color: colors.last, local:, link:, anchor:,
127
+ font: fonts.last, size: sizes.last, character_spacing: character_spacings.last
128
+ }
129
+ end
130
+ end
131
+ end
132
+ array
133
+ end
134
+
135
+ def self.attribute(token, name, hash_prefixed: false)
136
+ prefix = hash_prefixed ? "#?" : ""
137
+ token[/\b#{name}="#{prefix}([^"]*)"/, 1] || token[/\b#{name}='#{prefix}([^']*)'/, 1]
138
+ end
139
+
140
+ def self.escape(text)
141
+ text.gsub(ESCAPE_PATTERN, ESCAPE_CHARS)
142
+ end
143
+
144
+ def self.unescape(text)
145
+ text.gsub(UNESCAPE_PATTERN, UNESCAPE_CHARS)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Text
5
+ module Formatted
6
+ # Lays formatted text out line by line inside a box's width and height.
7
+ module Wrap
8
+ def initialize(_array, options)
9
+ @line_wrap = LineWrap.new
10
+ @arranger = Arranger.new(@document, kerning: options[:kerning])
11
+ @disable_wrap_by_char = options[:disable_wrap_by_char]
12
+ end
13
+
14
+ def wrap(array)
15
+ initialize_wrap(array)
16
+ loop do
17
+ @line_wrap.wrap_line(document: @document, kerning: @kerning, width: available_width, arranger: @arranger,
18
+ disable_wrap_by_char: @disable_wrap_by_char)
19
+ break unless enough_height_for_this_line?
20
+
21
+ move_baseline_down
22
+ print_line
23
+ break if @single_line || @arranger.finished?
24
+ end
25
+ @text = @printed_lines.join("\n")
26
+ @everything_printed = @arranger.finished?
27
+ @arranger.unconsumed
28
+ end
29
+
30
+ private
31
+
32
+ def print_line
33
+ @nothing_printed = false
34
+ printed_fragments = []
35
+ fragments_this_line = []
36
+ word_spacing = word_spacing_for_this_line
37
+ @arranger.fragments.each do |fragment|
38
+ fragment.word_spacing = word_spacing
39
+ if fragment.text == "\n"
40
+ printed_fragments << "\n" if @printed_lines.last == ""
41
+ break
42
+ end
43
+ printed_fragments << fragment.text
44
+ fragments_this_line << fragment
45
+ end
46
+ @arranger.fragments.replace([])
47
+
48
+ accumulated_width = 0
49
+ drawn_fragments = []
50
+ fragments_this_line.reverse! if @direction == :rtl
51
+ fragments_this_line.each do |fragment|
52
+ fragment.default_direction = @direction
53
+ drawn_fragment = [fragment, accumulated_width, @line_wrap.width, word_spacing]
54
+ format_and_draw_fragment(*drawn_fragment)
55
+ drawn_fragments << drawn_fragment if @laid_out_lines
56
+ accumulated_width += fragment.width
57
+ end
58
+ @laid_out_lines << [@baseline_y, drawn_fragments] if @laid_out_lines
59
+ @printed_lines << printed_fragments.map { _1.dup.force_encoding(::Encoding::UTF_8) }.join
60
+ end
61
+
62
+ def word_spacing_for_this_line
63
+ return 0 unless @align == :justify && @line_wrap.space_count.positive? && !@line_wrap.paragraph_finished?
64
+
65
+ (available_width - @line_wrap.width) / @line_wrap.space_count
66
+ end
67
+
68
+ # Slack in points, so floating point sums do not drop a line that fits exactly.
69
+ HEIGHT_TOLERANCE = 0.0001
70
+
71
+ def enough_height_for_this_line?
72
+ @line_height = @arranger.max_line_height
73
+ @descender = @arranger.max_descender
74
+ @ascender = @arranger.max_ascender
75
+ line_extent = @baseline_y.zero? ? @ascender + @descender : @descender + @line_height + @leading
76
+ return true if @baseline_y.abs + line_extent <= @height + HEIGHT_TOLERANCE
77
+
78
+ @arranger.repack_unretrieved
79
+ false
80
+ end
81
+
82
+ def initialize_wrap(array)
83
+ @text = nil
84
+ @arranger.format_array = array
85
+ @line_height = 0
86
+ @descender = 0
87
+ @ascender = 0
88
+ @baseline_y = 0
89
+ @printed_lines = []
90
+ @laid_out_lines&.clear
91
+ @nothing_printed = true
92
+ @everything_printed = false
93
+ end
94
+
95
+ def format_and_draw_fragment(fragment, accumulated_width, line_width, word_spacing)
96
+ @arranger.apply_color_and_font_settings(fragment) do
97
+ draw_fragment(fragment, accumulated_width, line_width, word_spacing)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end