hexapdf 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -2
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/boxes.rb +68 -0
  7. data/examples/graphics.rb +12 -12
  8. data/examples/{text_box_alignment.rb → text_layouter_alignment.rb} +14 -14
  9. data/examples/text_layouter_inline_boxes.rb +66 -0
  10. data/examples/{text_box_line_wrapping.rb → text_layouter_line_wrapping.rb} +9 -10
  11. data/examples/{text_box_shapes.rb → text_layouter_shapes.rb} +58 -54
  12. data/examples/text_layouter_styling.rb +125 -0
  13. data/examples/truetype.rb +5 -7
  14. data/lib/hexapdf/cli/command.rb +1 -0
  15. data/lib/hexapdf/configuration.rb +170 -106
  16. data/lib/hexapdf/content/canvas.rb +41 -36
  17. data/lib/hexapdf/content/graphics_state.rb +15 -0
  18. data/lib/hexapdf/content/operator.rb +1 -1
  19. data/lib/hexapdf/dictionary.rb +20 -8
  20. data/lib/hexapdf/dictionary_fields.rb +8 -6
  21. data/lib/hexapdf/document.rb +25 -26
  22. data/lib/hexapdf/document/fonts.rb +4 -4
  23. data/lib/hexapdf/document/images.rb +2 -2
  24. data/lib/hexapdf/document/pages.rb +16 -16
  25. data/lib/hexapdf/encryption/security_handler.rb +41 -9
  26. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  27. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  28. data/lib/hexapdf/filter/predictor.rb +7 -1
  29. data/lib/hexapdf/font/true_type/font.rb +20 -0
  30. data/lib/hexapdf/font/type1/font.rb +23 -0
  31. data/lib/hexapdf/font_loader.rb +1 -0
  32. data/lib/hexapdf/font_loader/from_configuration.rb +2 -3
  33. data/lib/hexapdf/font_loader/from_file.rb +65 -0
  34. data/lib/hexapdf/image_loader/png.rb +2 -2
  35. data/lib/hexapdf/layout.rb +3 -2
  36. data/lib/hexapdf/layout/box.rb +146 -0
  37. data/lib/hexapdf/layout/inline_box.rb +40 -31
  38. data/lib/hexapdf/layout/{line_fragment.rb → line.rb} +12 -13
  39. data/lib/hexapdf/layout/style.rb +630 -41
  40. data/lib/hexapdf/layout/text_fragment.rb +80 -12
  41. data/lib/hexapdf/layout/{text_box.rb → text_layouter.rb} +164 -109
  42. data/lib/hexapdf/number_tree_node.rb +1 -1
  43. data/lib/hexapdf/parser.rb +4 -1
  44. data/lib/hexapdf/revisions.rb +11 -4
  45. data/lib/hexapdf/stream.rb +8 -9
  46. data/lib/hexapdf/tokenizer.rb +5 -3
  47. data/lib/hexapdf/type.rb +3 -0
  48. data/lib/hexapdf/type/action.rb +56 -0
  49. data/lib/hexapdf/type/actions.rb +52 -0
  50. data/lib/hexapdf/type/actions/go_to.rb +52 -0
  51. data/lib/hexapdf/type/actions/go_to_r.rb +54 -0
  52. data/lib/hexapdf/type/actions/launch.rb +73 -0
  53. data/lib/hexapdf/type/actions/uri.rb +65 -0
  54. data/lib/hexapdf/type/annotation.rb +85 -0
  55. data/lib/hexapdf/type/annotations.rb +51 -0
  56. data/lib/hexapdf/type/annotations/link.rb +70 -0
  57. data/lib/hexapdf/type/annotations/markup_annotation.rb +70 -0
  58. data/lib/hexapdf/type/annotations/text.rb +81 -0
  59. data/lib/hexapdf/type/catalog.rb +3 -1
  60. data/lib/hexapdf/type/embedded_file.rb +6 -11
  61. data/lib/hexapdf/type/file_specification.rb +4 -6
  62. data/lib/hexapdf/type/font.rb +3 -1
  63. data/lib/hexapdf/type/font_descriptor.rb +18 -16
  64. data/lib/hexapdf/type/form.rb +3 -1
  65. data/lib/hexapdf/type/graphics_state_parameter.rb +3 -1
  66. data/lib/hexapdf/type/image.rb +4 -2
  67. data/lib/hexapdf/type/info.rb +2 -5
  68. data/lib/hexapdf/type/names.rb +2 -5
  69. data/lib/hexapdf/type/object_stream.rb +2 -1
  70. data/lib/hexapdf/type/page.rb +14 -1
  71. data/lib/hexapdf/type/page_tree_node.rb +9 -6
  72. data/lib/hexapdf/type/resources.rb +2 -5
  73. data/lib/hexapdf/type/trailer.rb +2 -5
  74. data/lib/hexapdf/type/viewer_preferences.rb +2 -5
  75. data/lib/hexapdf/type/xref_stream.rb +3 -1
  76. data/lib/hexapdf/version.rb +1 -1
  77. data/test/hexapdf/common_tokenizer_tests.rb +3 -1
  78. data/test/hexapdf/content/test_canvas.rb +29 -3
  79. data/test/hexapdf/content/test_graphics_state.rb +11 -0
  80. data/test/hexapdf/content/test_operator.rb +3 -2
  81. data/test/hexapdf/document/test_fonts.rb +8 -8
  82. data/test/hexapdf/document/test_images.rb +4 -12
  83. data/test/hexapdf/document/test_pages.rb +7 -7
  84. data/test/hexapdf/encryption/test_security_handler.rb +1 -5
  85. data/test/hexapdf/filter/test_predictor.rb +40 -12
  86. data/test/hexapdf/font/true_type/test_font.rb +16 -0
  87. data/test/hexapdf/font/type1/test_font.rb +30 -0
  88. data/test/hexapdf/font_loader/test_from_file.rb +29 -0
  89. data/test/hexapdf/font_loader/test_standard14.rb +4 -3
  90. data/test/hexapdf/layout/test_box.rb +104 -0
  91. data/test/hexapdf/layout/test_inline_box.rb +24 -10
  92. data/test/hexapdf/layout/{test_line_fragment.rb → test_line.rb} +9 -9
  93. data/test/hexapdf/layout/test_style.rb +519 -31
  94. data/test/hexapdf/layout/test_text_fragment.rb +136 -15
  95. data/test/hexapdf/layout/{test_text_box.rb → test_text_layouter.rb} +224 -144
  96. data/test/hexapdf/layout/test_text_shaper.rb +1 -1
  97. data/test/hexapdf/test_configuration.rb +12 -6
  98. data/test/hexapdf/test_dictionary.rb +27 -2
  99. data/test/hexapdf/test_dictionary_fields.rb +10 -1
  100. data/test/hexapdf/test_document.rb +14 -13
  101. data/test/hexapdf/test_parser.rb +12 -0
  102. data/test/hexapdf/test_revisions.rb +34 -0
  103. data/test/hexapdf/test_stream.rb +1 -1
  104. data/test/hexapdf/test_type.rb +18 -0
  105. data/test/hexapdf/test_writer.rb +2 -2
  106. data/test/hexapdf/type/actions/test_launch.rb +24 -0
  107. data/test/hexapdf/type/actions/test_uri.rb +23 -0
  108. data/test/hexapdf/type/annotations/test_link.rb +19 -0
  109. data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
  110. data/test/hexapdf/type/annotations/test_text.rb +38 -0
  111. data/test/hexapdf/type/test_annotation.rb +38 -0
  112. data/test/hexapdf/type/test_file_specification.rb +0 -7
  113. data/test/hexapdf/type/test_info.rb +0 -5
  114. data/test/hexapdf/type/test_page.rb +14 -0
  115. data/test/hexapdf/type/test_page_tree_node.rb +4 -1
  116. data/test/hexapdf/type/test_trailer.rb +0 -4
  117. data/test/test_helper.rb +6 -3
  118. metadata +36 -15
  119. data/examples/text_box_inline_boxes.rb +0 -56
  120. data/examples/text_box_styling.rb +0 -72
  121. data/test/hexapdf/type/test_embedded_file.rb +0 -16
  122. data/test/hexapdf/type/test_names.rb +0 -9
@@ -70,14 +70,38 @@ module HexaPDF
70
70
 
71
71
  # The style to be applied.
72
72
  #
73
- # Only the following properties are used: Style#font, Style#font_size,
74
- # Style#horizontal_scaling, Style#character_spacing, Style#word_spacing and Style#text_rise.
73
+ # Only the following properties are used:
74
+ #
75
+ # * Style#font
76
+ # * Style#font_size
77
+ # * Style#horizontal_scaling
78
+ # * Style#character_spacing
79
+ # * Style#word_spacing
80
+ # * Style#text_rise
81
+ # * Style#text_rendering_mode
82
+ # * Style#subscript
83
+ # * Style#superscript
84
+ # * Style#underline
85
+ # * Style#strikeout
86
+ # * Style#fill_color
87
+ # * Style#fill_alpha
88
+ # * Style#stroke_color
89
+ # * Style#stroke_alpha
90
+ # * Style#stroke_width
91
+ # * Style#stroke_cap_style
92
+ # * Style#stroke_join_style
93
+ # * Style#stroke_miter_limit
94
+ # * Style#stroke_dash_pattern
95
+ # * Style#underlay_callback
96
+ # * Style#overlay_callback
75
97
  attr_reader :style
76
98
 
77
99
  # Creates a new TextFragment object with the given items and style.
100
+ #
101
+ # The argument +style+ can either be a Style object or a hash of style options.
78
102
  def initialize(items:, style: Style.new)
79
103
  @items = items || []
80
- @style = style
104
+ @style = (style.kind_of?(Style) ? style : Style.new(style))
81
105
  end
82
106
 
83
107
  # Draws the text onto the canvas at the given position.
@@ -85,13 +109,57 @@ module HexaPDF
85
109
  # Before the text is drawn using HexaPDF::Content;:Canvas#show_glyphs, the text properties
86
110
  # mentioned in the description of #style are set.
87
111
  def draw(canvas, x, y)
112
+ return if items.empty?
113
+
114
+ style.underlays.draw(canvas, x, y + y_min, self)
115
+
116
+ # Set general font related graphics state
88
117
  canvas.move_text_cursor(offset: [x, y])
89
- canvas.font(style.font, size: style.font_size).
118
+ canvas.font(style.font, size: style.calculated_font_size).
90
119
  horizontal_scaling(style.horizontal_scaling).
91
120
  character_spacing(style.character_spacing).
92
121
  word_spacing(style.word_spacing).
93
- text_rise(style.text_rise)
122
+ text_rise(style.calculated_text_rise).
123
+ text_rendering_mode(style.text_rendering_mode)
124
+
125
+ # Set fill and/or stroke related graphics state
126
+ canvas.opacity(fill_alpha: style.fill_alpha, stroke_alpha: style.stroke_alpha)
127
+ trm = canvas.text_rendering_mode
128
+ if trm.value.even? # text is filled
129
+ canvas.fill_color(style.fill_color)
130
+ end
131
+ if trm == :stroke || trm == :fill_stroke || trm == :stroke_clip || trm == :fill_stroke_clip
132
+ canvas.stroke_color(style.stroke_color).
133
+ line_width(style.stroke_width).
134
+ line_cap_style(style.stroke_cap_style).
135
+ line_join_style(style.stroke_join_style).
136
+ miter_limit(style.stroke_miter_limit).
137
+ line_dash_pattern(style.stroke_dash_pattern)
138
+ end
139
+
94
140
  canvas.show_glyphs_only(items)
141
+
142
+ if style.underline
143
+ y_offset = style.calculated_underline_position
144
+ canvas.stroke_color(style.fill_color).
145
+ line_width(style.calculated_underline_thickness).
146
+ line_cap_style(:butt).
147
+ line_dash_pattern(0).
148
+ line(x, y + y_offset, x + width, y + y_offset).
149
+ stroke
150
+ end
151
+
152
+ if style.strikeout
153
+ y_offset = style.calculated_strikeout_position
154
+ canvas.stroke_color(style.fill_color).
155
+ line_width(style.calculated_strikeout_thickness).
156
+ line_cap_style(:butt).
157
+ line_dash_pattern(0).
158
+ line(x, y + y_offset, x + width, y + y_offset).
159
+ stroke
160
+ end
161
+
162
+ style.overlays.draw(canvas, x, y + y_min, self)
95
163
  end
96
164
 
97
165
  # The minimum x-coordinate of the first glyph.
@@ -106,24 +174,24 @@ module HexaPDF
106
174
 
107
175
  # The minimum y-coordinate, calculated using the scaled descender of the font.
108
176
  def y_min
109
- @y_min ||= style.scaled_font_descender + style.text_rise
177
+ @y_min ||= style.scaled_font_descender + style.calculated_text_rise
110
178
  end
111
179
 
112
180
  # The maximum y-coordinate, calculated using the scaled ascender of the font.
113
181
  def y_max
114
- @y_max ||= style.scaled_font_ascender + style.text_rise
182
+ @y_max ||= style.scaled_font_ascender + style.calculated_text_rise
115
183
  end
116
184
 
117
185
  # The minimum y-coordinate of any item.
118
186
  def exact_y_min
119
- @exact_y_min ||= (@items.min_by(&:y_min)&.y_min || 0) * style.font_size / 1000.0 +
120
- style.text_rise
187
+ @exact_y_min ||= (@items.min_by(&:y_min)&.y_min || 0) *
188
+ style.calculated_font_size / 1000.0 + style.calculated_text_rise
121
189
  end
122
190
 
123
191
  # The maximum y-coordinate of any item.
124
192
  def exact_y_max
125
- @exact_y_max ||= (@items.max_by(&:y_max)&.y_max || 0) * style.font_size / 1000.0 +
126
- style.text_rise
193
+ @exact_y_max ||= (@items.max_by(&:y_max)&.y_max || 0) *
194
+ style.calculated_font_size / 1000.0 + style.calculated_text_rise
127
195
  end
128
196
 
129
197
  # The width of the text fragment.
@@ -148,7 +216,7 @@ module HexaPDF
148
216
 
149
217
  # Returns the vertical alignment inside a line which is always :text for text fragments.
150
218
  #
151
- # See LineFragment for details.
219
+ # See Line for details.
152
220
  def valign
153
221
  :text
154
222
  end
@@ -34,7 +34,7 @@
34
34
  require 'hexapdf/error'
35
35
  require 'hexapdf/layout/text_fragment'
36
36
  require 'hexapdf/layout/inline_box'
37
- require 'hexapdf/layout/line_fragment'
37
+ require 'hexapdf/layout/line'
38
38
  require 'hexapdf/layout/numeric_refinements'
39
39
 
40
40
  module HexaPDF
@@ -54,14 +54,14 @@ module HexaPDF
54
54
  #
55
55
  # Laying out text consists of two phases:
56
56
  #
57
- # 1. The items of the text box are broken into pieces which are wrapped into Box, Glue or
58
- # Penalty objects. Additional Penalty objects marking line breaking opportunities are
59
- # inserted where needed. This step is done by the SimpleTextSegmentation module.
57
+ # 1. The items are broken into pieces which are wrapped into Box, Glue or Penalty objects.
58
+ # Additional Penalty objects marking line breaking opportunities are inserted where needed.
59
+ # This step is done by the SimpleTextSegmentation module.
60
60
  #
61
61
  # 2. The pieces are arranged into lines using a very simple algorithm that just puts the maximum
62
62
  # number of consecutive pieces into each line. This step is done by the SimpleLineWrapping
63
63
  # module.
64
- class TextBox
64
+ class TextLayouter
65
65
 
66
66
  using NumericRefinements
67
67
 
@@ -81,6 +81,11 @@ module HexaPDF
81
81
  @item.width
82
82
  end
83
83
 
84
+ # The height of the item.
85
+ def height
86
+ @item.height
87
+ end
88
+
84
89
  # Returns :box.
85
90
  def type
86
91
  :box
@@ -127,6 +132,12 @@ module HexaPDF
127
132
  # All numbers greater than this one are deemed infinite.
128
133
  INFINITY = 1000
129
134
 
135
+ # The penalty value for a mandatory paragraph break.
136
+ PARAGRAPH_BREAK = -INFINITY - 1_000_000
137
+
138
+ # The penalty value for a mandatory line break.
139
+ LINE_BREAK = -INFINITY - 1_000_001
140
+
130
141
  # The penalty for breaking at this point.
131
142
  attr_reader :penalty
132
143
 
@@ -148,8 +159,11 @@ module HexaPDF
148
159
  :penalty
149
160
  end
150
161
 
151
- # Singleton object describing a Penalty for a mandatory break.
152
- MandatoryBreak = new(-Penalty::INFINITY)
162
+ # Singleton object describing a Penalty for a mandatory paragraph break.
163
+ MandatoryParagraphBreak = new(PARAGRAPH_BREAK)
164
+
165
+ # Singleton object describing a Penalty for a mandatory line break.
166
+ MandatoryLineBreak = new(LINE_BREAK)
153
167
 
154
168
  # Singleton object describing a Penalty for a prohibited break.
155
169
  ProhibitedBreak = new(Penalty::INFINITY)
@@ -217,11 +231,13 @@ module HexaPDF
217
231
  glues[item.style] ||=
218
232
  Glue.new(TextFragment.new(items: [glyph].freeze, style: item.style))
219
233
  result << glues[item.style]
220
- when "\n", "\v", "\f", "\u{85}", "\u{2028}", "\u{2029}"
221
- result << Penalty::MandatoryBreak
234
+ when "\n", "\v", "\f", "\u{85}", "\u{2029}"
235
+ result << Penalty::MandatoryParagraphBreak
236
+ when "\u{2028}"
237
+ result << Penalty::MandatoryLineBreak
222
238
  when "\r"
223
239
  if item.items[i + 1]&.kind_of?(Numeric) || item.items[i + 1].str != "\n"
224
- result << Penalty::MandatoryBreak
240
+ result << Penalty::MandatoryParagraphBreak
225
241
  end
226
242
  when '-'
227
243
  result << Penalty::Standard
@@ -255,32 +271,35 @@ module HexaPDF
255
271
  class SimpleLineWrapping
256
272
 
257
273
  # :call-seq:
258
- # SimpleLineWrapping.call(items, available_width) {|line, item| block } -> rest
274
+ # SimpleLineWrapping.call(items, width_block) {|line, item| block } -> rest
259
275
  #
260
276
  # Arranges the items into lines.
261
277
  #
262
- # The +available_width+ argument can either be a simple number or a callable object:
278
+ # The +width_block+ argument has to be a callable object that returns the width of the line:
263
279
  #
264
- # * If all lines should have the same width, the +available_width+ argument should be a
265
- # number. This is the general case.
280
+ # * If the line width doesn't depend on the height or the vertical position of the line
281
+ # (i.e. fixed line width), the +width_block+ should have an arity of zero. However, this
282
+ # doesn't mean that the block is called only once; it is actually called before each new
283
+ # line (e.g. for varying line widths that don't depend on the line height; one common case
284
+ # is the indentation of the first line). This is the general case.
266
285
  #
267
- # * However, if lines should have varying lengths (e.g. for flowing text around shapes), the
268
- # +available_width+ argument should be an object responding to #call(line_height) where
286
+ # * However, if lines should have varying widths (e.g. for flowing text around shapes), the
287
+ # +width_block+ argument should be an object responding to #call(line_height) where
269
288
  # +line_height+ is the height of the currently layed out line. The caller is responsible
270
- # for tracking the height of the already layed out lines. The result of the method call
271
- # should be the available width.
289
+ # for tracking the height of the already layed out lines. This method involves more work
290
+ # and is therefore slower.
272
291
  #
273
292
  # Regardless of whether varying line widths are used or not, each time a line is finished,
274
- # it is yielded to the caller. The second argument +item+ is the TextFragment or InlineBox
275
- # that doesn't fit anymore, or +nil+ in case of mandatory line breaks or when the line break
276
- # occured at a glue item. If the yielded line is empty and the yielded item is not +nil+,
277
- # this single item doesn't fit into the available width; the caller has to handle this
278
- # situation, e.g. by stopping.
293
+ # it is yielded to the caller. The second argument +item+ is the item that caused the line
294
+ # break (e.g. a Box, Glue or Penalty). The return value should be truthy if line wrapping
295
+ # should continue, or falsy if it should stop. If the yielded line is empty and the yielded
296
+ # item is a box item, this single item didn't fit into the available width; the caller has
297
+ # to handle this situation, e.g. by stopping.
279
298
  #
280
299
  # After the algorithm is finished, it returns the unused items.
281
- def self.call(items, available_width, &block)
282
- obj = new(items, available_width)
283
- if available_width.respond_to?(:call)
300
+ def self.call(items, width_block, &block)
301
+ obj = new(items, width_block)
302
+ if width_block.arity == 1
284
303
  obj.variable_width_wrapping(&block)
285
304
  else
286
305
  obj.fixed_width_wrapping(&block)
@@ -291,9 +310,10 @@ module HexaPDF
291
310
 
292
311
  # Creates a new line wrapping object that arranges the +items+ on lines with the given
293
312
  # width.
294
- def initialize(items, available_width)
313
+ def initialize(items, width_block)
295
314
  @items = items
296
- @available_width = @width_block = available_width
315
+ @width_block = width_block
316
+ @available_width = @width_block.call(0)
297
317
  @line_items = []
298
318
  @width = 0
299
319
  @glue_items = []
@@ -302,11 +322,11 @@ module HexaPDF
302
322
  @last_breakpoint_line_items_index = 0
303
323
  @break_prohibited_state = false
304
324
 
305
- @height_calc = LineFragment::HeightCalculator.new
325
+ @height_calc = Line::HeightCalculator.new
306
326
  @line_height = 0
307
327
  end
308
328
 
309
- # Peforms the line wrapping with a fixed width.
329
+ # Peforms line wrapping with a fixed width per line, with line height playing no role.
310
330
  def fixed_width_wrapping
311
331
  index = 0
312
332
 
@@ -318,18 +338,18 @@ module HexaPDF
318
338
  index = reset_line_to_last_breakpoint_state
319
339
  item = @items[index]
320
340
  end
321
- break unless yield(create_line, item.item)
341
+ break unless yield(create_line, item)
322
342
  reset_after_line_break(index)
323
343
  redo
324
344
  end
325
345
  when :glue
326
346
  unless add_glue_item(item.item, index)
327
- break unless yield(create_line, nil)
347
+ break unless yield(create_line, item)
328
348
  reset_after_line_break(index + 1)
329
349
  end
330
350
  when :penalty
331
351
  if item.penalty <= -Penalty::INFINITY
332
- break unless yield(create_unjustified_line, nil)
352
+ break unless yield(create_unjustified_line, item)
333
353
  reset_after_line_break(index + 1)
334
354
  elsif item.penalty >= Penalty::INFINITY
335
355
  @break_prohibited_state = true
@@ -365,7 +385,6 @@ module HexaPDF
365
385
  # Performs the line wrapping with variable widths.
366
386
  def variable_width_wrapping
367
387
  index = 0
368
- @available_width = @width_block.call(@line_height)
369
388
 
370
389
  while (item = @items[index])
371
390
  case item.type
@@ -382,18 +401,18 @@ module HexaPDF
382
401
  index = reset_line_to_last_breakpoint_state
383
402
  item = @items[index]
384
403
  end
385
- break unless yield(create_line, item.item)
404
+ break unless yield(create_line, item)
386
405
  reset_after_line_break(index)
387
406
  redo
388
407
  end
389
408
  when :glue
390
409
  unless add_glue_item(item.item, index)
391
- break unless yield(create_line, nil)
410
+ break unless yield(create_line, item)
392
411
  reset_after_line_break(index + 1)
393
412
  end
394
413
  when :penalty
395
414
  if item.penalty <= -Penalty::INFINITY
396
- break unless yield(create_unjustified_line, nil)
415
+ break unless yield(create_unjustified_line, item)
397
416
  reset_after_line_break(index + 1)
398
417
  elsif item.penalty >= Penalty::INFINITY
399
418
  @break_prohibited_state = true
@@ -474,12 +493,12 @@ module HexaPDF
474
493
  @width + item.width <= @available_width
475
494
  end
476
495
 
477
- # Creates a LineFragment object from the current line items.
496
+ # Creates a Line object from the current line items.
478
497
  def create_line
479
- LineFragment.new(@line_items)
498
+ Line.new(@line_items)
480
499
  end
481
500
 
482
- # Creates a LineFragment object from the current line items that ignores line justification.
501
+ # Creates a Line object from the current line items that ignores line justification.
483
502
  def create_unjustified_line
484
503
  create_line.tap(&:ignore_justification!)
485
504
  end
@@ -494,6 +513,7 @@ module HexaPDF
494
513
  @last_breakpoint_index = index
495
514
  @last_breakpoint_line_items_index = 0
496
515
  @break_prohibited_state = false
516
+ @available_width = @width_block.call(0)
497
517
 
498
518
  @line_height = 0
499
519
  @height_calc.reset
@@ -502,12 +522,12 @@ module HexaPDF
502
522
  end
503
523
 
504
524
 
505
- # Creates a new TextBox object for the given text and returns it.
525
+ # Creates a new TextLayouter object for the given text and returns it.
506
526
  #
507
527
  # See ::new for information on +height+.
508
528
  #
509
- # The style of the text box can be specified using additional options, of which font is
510
- # mandatory.
529
+ # The style that gets applied to the text and the layout itself can be specified using
530
+ # additional options, of which font is mandatory.
511
531
  def self.create(text, width:, height: nil, x_offsets: nil, **options)
512
532
  frag = TextFragment.create(text, **options)
513
533
  new(items: [frag], width: width, height: height, x_offsets: x_offsets, style: frag.style)
@@ -519,19 +539,19 @@ module HexaPDF
519
539
  # Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
520
540
  attr_reader :style
521
541
 
522
- # The items (TextFragment and InlineBox objects) of the text box that should be layed out.
542
+ # The items (TextFragment and InlineBox objects) that should be layed out.
523
543
  attr_reader :items
524
544
 
525
- # Array of LineFragment objects describing the lines of the text box.
545
+ # Array of Line objects describing the layed out lines.
526
546
  #
527
547
  # The array is only valid after #fit was called.
528
548
  attr_reader :lines
529
549
 
530
- # The actual height of the text box. Can be +nil+ if the items have not been layed out yet,
531
- # i.e. if #fit has not been called.
550
+ # The actual height of the layed out text. Can be +nil+ if the items have not been layed out
551
+ # yet, i.e. if #fit has not been called.
532
552
  attr_reader :actual_height
533
553
 
534
- # Creates a new TextBox object with the given width containing the given items.
554
+ # Creates a new TextLayouter object with the given width containing the given items.
535
555
  #
536
556
  # The width can either be a simple number specifying a fixed width, or an object that responds
537
557
  # to #call(height, line_height) where +height+ is the bottom of last line and +line_height+ is
@@ -539,12 +559,15 @@ module HexaPDF
539
559
  # these height restrictions.
540
560
  #
541
561
  # The optional +x_offsets+ argument works like +width+ but can be used to specify (varying)
542
- # offsets from the left of the box (e.g. when the left side of the text should follow a
543
- # certain shape).
562
+ # offsets from the left side (e.g. when the left side of the text should follow a certain
563
+ # shape).
564
+ #
565
+ # The height is optional and if not specified means that the text layout has infinite height.
544
566
  #
545
- # The height is optional and if not specified means that the text box has infinite height.
567
+ # The +style+ argument can either be a Style object or a hash of style options. See #style for
568
+ # the properties that are used by the layouter.
546
569
  def initialize(items: [], width:, height: nil, x_offsets: nil, style: Style.new)
547
- @style = style
570
+ @style = (style.kind_of?(Style) ? style : Style.new(style))
548
571
  @lines = []
549
572
  self.items = items
550
573
  @width = width
@@ -552,7 +575,7 @@ module HexaPDF
552
575
  @x_offsets = x_offsets && (x_offsets.respond_to?(:call) ? x_offsets : proc { x_offsets })
553
576
  end
554
577
 
555
- # Sets the items to be arranged by the text box, clearing the internal state.
578
+ # Sets the items to be arranged by the text layouter, clearing the internal state.
556
579
  #
557
580
  # If the items array contains items before text segmentation, the text segmentation algorithm
558
581
  # is automatically applied.
@@ -566,80 +589,105 @@ module HexaPDF
566
589
  end
567
590
 
568
591
  # :call-seq:
569
- # text_box.fit -> [remaining_items, actual_height]
592
+ # text_layouter.fit -> [remaining_items, reason]
570
593
  #
571
- # Fits the items into the text box and returns the remaining items as well as the actual
572
- # height needed.
594
+ # Fits the items into the set area and returns the remaining items as well as the reason why
595
+ # there are remaining items.
573
596
  #
574
- # Note: If the text box height has not been set and variable line widths are used, no search
575
- # for a possible vertical offset is done in case a single item doesn't fit.
597
+ # The reason can be +:success+ if there are no remaining items, +:box+ if a single text or
598
+ # inline box item is too wide to fit alone on a line, or +:height+ if there was not enough
599
+ # height for all items.
600
+ #
601
+ # The fitted lines can be retrieved via #lines and the total height via #actual_height.
602
+ #
603
+ # Note: If no height has been set and variable line widths are used, no search for a possible
604
+ # vertical offset is done in case a single item doesn't fit.
576
605
  #
577
606
  # This method is automatically called as part of the drawing routine but it can also be used
578
- # by itself to determine the actual height of the text box.
607
+ # by itself to determine the actual height of the layed out text.
579
608
  def fit
580
609
  @lines.clear
581
610
  @actual_height = 0
582
- y_offset = 0
583
-
584
- items = @items
585
- if style.text_indent != 0
586
- items = [Box.new(InlineBox.new(style.text_indent, 0) { })].concat(items)
587
- end
588
-
589
- if @width.respond_to?(:call)
590
- width_arg = proc {|h| @width.call(@actual_height, h)}
591
- width_block = @width
592
- else
593
- width_arg = @width
594
- width_block = proc { @width }
595
- end
596
611
 
597
- rest = style.text_line_wrapping_algorithm.call(items, width_arg) do |line, item|
598
- line << TextFragment.new(items: [], style: style) if item.nil? && line.items.empty?
599
- new_height = @actual_height + line.height +
600
- (@lines.empty? ? 0 : style.line_spacing.gap(@lines.last, line))
601
-
602
- if new_height <= @height && !line.items.empty?
603
- # valid line found, use it
604
- cur_width = width_block.call(@actual_height, line.height)
605
- line.x_offset = horizontal_alignment_offset(line, cur_width)
606
- line.x_offset += @x_offsets.call(@actual_height, line.height) if @x_offsets
607
- line.y_offset = if y_offset
608
- y_offset + (@lines.last ? -@lines.last.y_min + line.y_max : 0)
609
- else
610
- style.line_spacing.baseline_distance(@lines.last, line)
611
- end
612
- @actual_height = new_height
613
- @lines << line
614
- y_offset = nil
615
- true
616
- elsif new_height <= @height && @height != Float::INFINITY
617
- # some height left but item didn't fit on the line, search downwards for usable space
618
- new_height = @actual_height
619
- while item.width > width_block.call(new_height, item.height) && new_height <= @height
620
- new_height += item.height / 3
621
- end
622
- if new_height + item.height <= @height
623
- y_offset = new_height - @actual_height
612
+ rest = @items
613
+ y_offset = 0
614
+ indent = (style.text_indent != 0 ? style.text_indent : 0)
615
+ width_block = if @width.respond_to?(:call)
616
+ proc {|h| @width.call(@actual_height, h) - indent }
617
+ else
618
+ proc { @width - indent }
619
+ end
620
+
621
+ while true
622
+ too_wide_box = nil
623
+
624
+ rest = style.text_line_wrapping_algorithm.call(rest, width_block) do |line, item|
625
+ line << TextFragment.new(items: [], style: style) if item&.type != :box && line.items.empty?
626
+ new_height = @actual_height + line.height +
627
+ (@lines.empty? ? 0 : style.line_spacing.gap(@lines.last, line))
628
+
629
+ if new_height > @height
630
+ nil
631
+ elsif !line.items.empty?
632
+ # valid line found, use it
633
+ cur_width = width_block.call(line.height)
634
+ line.x_offset = indent + horizontal_alignment_offset(line, cur_width)
635
+ line.x_offset += @x_offsets.call(@actual_height, line.height) if @x_offsets
636
+ line.y_offset = if y_offset
637
+ y_offset + (@lines.last ? -@lines.last.y_min + line.y_max : 0)
638
+ else
639
+ style.line_spacing.baseline_distance(@lines.last, line)
640
+ end
624
641
  @actual_height = new_height
642
+ @lines << line
643
+ y_offset = nil
644
+ indent = if item&.type == :penalty && item.penalty == Penalty::PARAGRAPH_BREAK
645
+ style.text_indent
646
+ else
647
+ 0
648
+ end
625
649
  true
650
+ elsif @height != Float::INFINITY
651
+ # some height left but item didn't fit on the line, search downwards for usable space
652
+ old_height = @actual_height
653
+ while item.width > width_block.call(item.height) && @actual_height <= @height
654
+ @actual_height += item.height / 3
655
+ end
656
+ if @actual_height + item.height <= @height
657
+ y_offset = @actual_height - old_height
658
+ true
659
+ else
660
+ @actual_height = old_height
661
+ too_wide_box = item
662
+ nil
663
+ end
626
664
  else
665
+ too_wide_box = item
627
666
  nil
628
667
  end
668
+ end
669
+
670
+ if too_wide_box && too_wide_box.item.kind_of?(TextFragment) &&
671
+ too_wide_box.item.items.size > 1
672
+ rest[0..rest.index(too_wide_box)] = too_wide_box.item.items.map do |item|
673
+ Box.new(TextFragment.new(items: [item], style: too_wide_box.item.style))
674
+ end
675
+ too_wide_box = nil
629
676
  else
630
- nil
677
+ reason = (too_wide_box ? :box : (rest.empty? ? :success : :height))
678
+ break
631
679
  end
632
680
  end
633
681
 
634
- [rest, @actual_height]
682
+ [rest, reason]
635
683
  end
636
684
 
637
- # Draws the text box onto the canvas with the top-left corner being at [x, y].
685
+ # Draws the layed out text onto the canvas with the top-left corner being at [x, y].
638
686
  #
639
687
  # Depending on the value of +fit+ the text may also be fitted:
640
688
  #
641
689
  # * If +true+, then #fit is always called.
642
- # * If +:if_needed+, then #fit is only called if it has been called before.
690
+ # * If +:if_needed+, then #fit is only called if it has not been called before.
643
691
  # * If +false+, then #fit is never called.
644
692
  def draw(canvas, x, y, fit: :if_needed)
645
693
  self.fit if fit == true || (!@actual_height && fit == :if_needed)
@@ -649,7 +697,15 @@ module HexaPDF
649
697
  y -= initial_baseline_offset + @lines.first.y_offset
650
698
  @lines.each_with_index do |line, index|
651
699
  line_x = x + line.x_offset
652
- line.each {|item, item_x, item_y| item.draw(canvas, line_x + item_x, y + item_y) }
700
+ line.each do |item, item_x, item_y|
701
+ if item.kind_of?(TextFragment)
702
+ item.draw(canvas, line_x + item_x, y + item_y)
703
+ elsif !item.empty?
704
+ canvas.restore_graphics_state
705
+ item.draw(canvas, line_x + item_x, y + item_y)
706
+ canvas.save_graphics_state
707
+ end
708
+ end
653
709
  y -= @lines[index + 1].y_offset if @lines[index + 1]
654
710
  end
655
711
  end
@@ -657,20 +713,19 @@ module HexaPDF
657
713
 
658
714
  private
659
715
 
660
- # Returns the initial baseline offset from the top of the text box, based on the valign style
661
- # option.
716
+ # Returns the initial baseline offset from the top, based on the valign style option.
662
717
  def initial_baseline_offset
663
718
  case style.valign
664
719
  when :top
665
720
  @lines.first.y_max
666
721
  when :center
667
722
  if @height == Float::INFINITY
668
- raise HexaPDF::Error, "Can't vertically align a text box with unlimited height"
723
+ raise HexaPDF::Error, "Can't vertically align when using unlimited height"
669
724
  end
670
725
  (@height - @actual_height) / 2.0 + @lines.first.y_max
671
726
  when :bottom
672
727
  if @height == Float::INFINITY
673
- raise HexaPDF::Error, "Can't vertically align a text box with unlimited height"
728
+ raise HexaPDF::Error, "Can't vertically align when using unlimited height"
674
729
  end
675
730
  (@height - @actual_height) + @lines.first.y_max
676
731
  end