hexapdf 0.34.1 → 0.35.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/examples/009-text_layouter_alignment.rb +7 -7
  4. data/examples/010-text_layouter_inline_boxes.rb +1 -1
  5. data/examples/011-text_layouter_line_wrapping.rb +2 -4
  6. data/examples/013-text_layouter_shapes.rb +9 -11
  7. data/examples/014-text_in_polygon.rb +2 -2
  8. data/examples/016-frame_automatic_box_placement.rb +6 -7
  9. data/examples/017-frame_text_flow.rb +2 -2
  10. data/examples/018-composer.rb +5 -6
  11. data/examples/020-column_box.rb +2 -2
  12. data/examples/021-list_box.rb +1 -1
  13. data/examples/027-composer_optional_content.rb +5 -5
  14. data/examples/028-frame_mask_mode.rb +23 -0
  15. data/examples/029-composer_fallback_fonts.rb +22 -0
  16. data/lib/hexapdf/cli/info.rb +1 -0
  17. data/lib/hexapdf/cli/inspect.rb +55 -2
  18. data/lib/hexapdf/composer.rb +2 -2
  19. data/lib/hexapdf/configuration.rb +61 -1
  20. data/lib/hexapdf/content/canvas.rb +63 -0
  21. data/lib/hexapdf/content/canvas_composer.rb +142 -0
  22. data/lib/hexapdf/content.rb +1 -0
  23. data/lib/hexapdf/dictionary.rb +14 -3
  24. data/lib/hexapdf/document/layout.rb +35 -13
  25. data/lib/hexapdf/encryption/standard_security_handler.rb +15 -0
  26. data/lib/hexapdf/error.rb +2 -1
  27. data/lib/hexapdf/font/invalid_glyph.rb +22 -6
  28. data/lib/hexapdf/font/true_type_wrapper.rb +48 -20
  29. data/lib/hexapdf/font/type1_wrapper.rb +48 -24
  30. data/lib/hexapdf/layout/box.rb +11 -8
  31. data/lib/hexapdf/layout/column_box.rb +5 -3
  32. data/lib/hexapdf/layout/frame.rb +77 -39
  33. data/lib/hexapdf/layout/image_box.rb +3 -3
  34. data/lib/hexapdf/layout/list_box.rb +20 -19
  35. data/lib/hexapdf/layout/style.rb +173 -68
  36. data/lib/hexapdf/layout/table_box.rb +3 -3
  37. data/lib/hexapdf/layout/text_box.rb +5 -5
  38. data/lib/hexapdf/layout/text_fragment.rb +50 -0
  39. data/lib/hexapdf/layout/text_layouter.rb +7 -6
  40. data/lib/hexapdf/object.rb +5 -2
  41. data/lib/hexapdf/pdf_array.rb +5 -0
  42. data/lib/hexapdf/type/acro_form/appearance_generator.rb +16 -11
  43. data/lib/hexapdf/utils/sorted_tree_node.rb +0 -10
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/content/test_canvas.rb +37 -0
  46. data/test/hexapdf/content/test_canvas_composer.rb +112 -0
  47. data/test/hexapdf/document/test_layout.rb +40 -12
  48. data/test/hexapdf/encryption/test_standard_security_handler.rb +43 -0
  49. data/test/hexapdf/font/test_invalid_glyph.rb +13 -1
  50. data/test/hexapdf/font/test_true_type_wrapper.rb +15 -2
  51. data/test/hexapdf/font/test_type1_wrapper.rb +21 -2
  52. data/test/hexapdf/layout/test_column_box.rb +14 -0
  53. data/test/hexapdf/layout/test_frame.rb +181 -95
  54. data/test/hexapdf/layout/test_list_box.rb +7 -7
  55. data/test/hexapdf/layout/test_style.rb +14 -10
  56. data/test/hexapdf/layout/test_table_box.rb +3 -3
  57. data/test/hexapdf/layout/test_text_box.rb +2 -2
  58. data/test/hexapdf/layout/test_text_fragment.rb +37 -0
  59. data/test/hexapdf/layout/test_text_layouter.rb +10 -10
  60. data/test/hexapdf/test_configuration.rb +49 -0
  61. data/test/hexapdf/test_dictionary.rb +1 -1
  62. data/test/hexapdf/test_object.rb +13 -12
  63. data/test/hexapdf/test_pdf_array.rb +9 -0
  64. data/test/hexapdf/test_writer.rb +3 -3
  65. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +41 -13
  66. data/test/hexapdf/utils/test_sorted_tree_node.rb +1 -1
  67. metadata +7 -3
@@ -577,7 +577,7 @@ module HexaPDF
577
577
  # equivalent to the property names.
578
578
  #
579
579
  # Example:
580
- # Style.new(font_size: 15, align: :center, valign: center)
580
+ # Style.new(font_size: 15, text_align: :center, text_valign: center)
581
581
  def initialize(**properties)
582
582
  update(**properties)
583
583
  @scaled_item_widths = {}.compare_by_identity
@@ -946,9 +946,9 @@ module HexaPDF
946
946
  # text_rendering_mode: :stroke)
947
947
 
948
948
  ##
949
- # :method: align
949
+ # :method: text_align
950
950
  # :call-seq:
951
- # align(direction = nil)
951
+ # text_align(direction = nil)
952
952
  #
953
953
  # The horizontal alignment of text, defaults to :left.
954
954
  #
@@ -964,15 +964,15 @@ module HexaPDF
964
964
  # #>pdf-composer100
965
965
  # text = "Lorem ipsum dolor sit amet. " * 2
966
966
  # composer.style(:base, border: {width: 1})
967
- # composer.text(text, align: :left)
968
- # composer.text(text, align: :center)
969
- # composer.text(text, align: :right)
970
- # composer.text(text, align: :justify)
967
+ # composer.text(text, text_align: :left)
968
+ # composer.text(text, text_align: :center)
969
+ # composer.text(text, text_align: :right)
970
+ # composer.text(text, text_align: :justify)
971
971
 
972
972
  ##
973
- # :method: valign
973
+ # :method: text_valign
974
974
  # :call-seq:
975
- # valign(direction = nil)
975
+ # text_valign(direction = nil)
976
976
  #
977
977
  # The vertical alignment of items (normally text) inside a text box, defaults to :top.
978
978
  #
@@ -991,9 +991,9 @@ module HexaPDF
991
991
  #
992
992
  # #>pdf-composer100
993
993
  # composer.style(:base, border: {width: 1})
994
- # composer.text("Top aligned", height: 20, valign: :top)
995
- # composer.text("Center aligned", height: 20, valign: :center)
996
- # composer.text("Bottom aligned", valign: :bottom)
994
+ # composer.text("Top aligned", height: 20, text_valign: :top)
995
+ # composer.text("Center aligned", height: 20, text_valign: :center)
996
+ # composer.text("Bottom aligned", text_valign: :bottom)
997
997
 
998
998
  ##
999
999
  # :method: text_indent
@@ -1214,84 +1214,186 @@ module HexaPDF
1214
1214
  # :call-seq:
1215
1215
  # position(value = nil)
1216
1216
  #
1217
- # Specifies how a box should be positioned in a frame. The property #position_hint provides
1218
- # additional, position specific data. Defaults to :default.
1217
+ # Specifies how a box should be positioned in a frame. Defaults to :default.
1218
+ #
1219
+ # The properties #align and #valign provide alignment information while #mask_mode defines how
1220
+ # the to-be-removed region should be constructed.
1219
1221
  #
1220
1222
  # Possible values:
1221
1223
  #
1222
- # :default:: Position the box at the current position. The exact horizontal position is given
1223
- # via the position hint. Space to the left/right of the box can't be used for other
1224
- # boxes.
1224
+ # :default::
1225
+ # Position the box at the current position. The exact horizontal and vertical position
1226
+ # inside the current region is given via the #align and #valign style properties.
1227
+ #
1228
+ # Examples:
1229
+ #
1230
+ # #>pdf-composer100
1231
+ # composer.box(:base, width: 40, height: 20,
1232
+ # style: {align: :right, border: {width: 1}})
1233
+ # composer.box(:base, width: 40, height: 20,
1234
+ # style: {align: :center, valign: :center, border: {width: 1}})
1235
+ #
1236
+ # :float::
1237
+ # This is the same as :default except that the used value for #mask_mode when it is set to
1238
+ # :default is :box instead of :fill_frame_horizontal.
1239
+ #
1240
+ # Examples:
1241
+ #
1242
+ # #>pdf-composer100
1243
+ # composer.box(:base, width: 40, height: 20,
1244
+ # style: {position: :float, border: {width: 1}})
1245
+ # composer.box(:base, width: 40, height: 20,
1246
+ # style: {position: :float, border: {color: "hp-blue", width: 1}})
1247
+ #
1248
+ # :flow::
1249
+ # Flows the content of the box inside the frame around objects.
1250
+ #
1251
+ # A box needs to indicate whether it supports this value by implementing the
1252
+ # #supports_position_flow? method and returning +true+ if it does or +false+ if it
1253
+ # doesn't. If a box doesn't support this value, it is positioned as if the value :default
1254
+ # was set.
1255
+ #
1256
+ # Note that the properties #align and #valign are not used with this value!
1225
1257
  #
1226
- # :float:: Position the box at the current position but let it "float" so that the space to
1227
- # the left/right can still be used. The position hint specifies where the box should
1228
- # float.
1258
+ # Examples:
1229
1259
  #
1230
- # :flow:: Flows the content of the box inside the frame around objects.
1260
+ # #>pdf-composer100
1261
+ # composer.box(:base, width: 40, height: 20,
1262
+ # style: {position: :float, border: {width: 1}})
1263
+ # composer.lorem_ipsum(position: :flow)
1231
1264
  #
1232
- # A box needs to indicate whether it supports this value by implementing the
1233
- # #supports_position_flow? method and returning +true+ if it does or +false+ if it
1234
- # doesn't.
1265
+ # [x, y]::
1266
+ # Position the box with the bottom left corner at the given absolute position relative to
1267
+ # the bottom left corner of the frame.
1235
1268
  #
1236
- # :absolute:: Position the box at an absolute position relative to the frame. The coordinates
1237
- # are given via the position hint.
1269
+ # Examples:
1238
1270
  #
1239
- # See #position_hint for examples
1271
+ # #>pdf-composer100
1272
+ # composer.text('Absolute', position: [50, 50], border: {width: 1})
1273
+ # draw_current_frame_shape("red")
1240
1274
 
1241
1275
  ##
1242
- # :method: position_hint
1276
+ # :method: align
1243
1277
  # :call-seq:
1244
- # position_hint(value = nil)
1278
+ # align(value = nil)
1279
+ #
1280
+ # Specifies the horizontal alignment of a box inside the current region. Defaults to :left.
1281
+ #
1282
+ # Possible values:
1283
+ #
1284
+ # :left:: Align the box to the left side of the current region.
1285
+ # :center:: Horizontally center the box in the current region.
1286
+ # :right:: Align the box to the right side of the current region.
1287
+ #
1288
+ # Examples:
1245
1289
  #
1246
- # Specifies additional information on how a box should be positioned in a frame. The exact
1247
- # meaning depends on the value of the #position property.
1290
+ # #>pdf-composer100
1291
+ # composer.text("Left", border: {width: 1})
1292
+ # draw_current_frame_shape("hp-blue")
1293
+ # composer.text("Center", align: :center, border: {width: 1})
1294
+ # draw_current_frame_shape("hp-orange")
1295
+ # composer.text("Right", align: :right, border: {width: 1})
1296
+ # draw_current_frame_shape("hp-teal")
1297
+
1298
+ ##
1299
+ # :method: valign
1300
+ # :call-seq:
1301
+ # valign(value = nil)
1248
1302
  #
1249
- # Possible values depending on the #position property:
1303
+ # Specifies the vertical alignment of a box inside the current region. Defaults to :top.
1304
+ #
1305
+ # Possible values:
1306
+ #
1307
+ # :top:: Align the box to the top side of the current region.
1308
+ # :center:: Vertically center the box in the current region.
1309
+ # :bottom:: Align the box to the bottom side of the current region.
1310
+ #
1311
+ # Examples:
1312
+ #
1313
+ # #>pdf-composer100
1314
+ # composer.text("Top", mask_mode: :fill_vertical, border: {width: 1})
1315
+ # composer.text("Center", valign: :center, mask_mode: :fill_vertical, border: {width: 1})
1316
+ # composer.text("Bottom", valign: :bottom, border: {width: 1})
1317
+
1318
+ ##
1319
+ # :method: mask_mode
1320
+ # :call-seq:
1321
+ # mask_mode(value = nil)
1322
+ #
1323
+ # Specifies how the mask defining the to-be-removed region should be constructed. Defaults to
1324
+ # :default.
1325
+ #
1326
+ # Possible values:
1250
1327
  #
1251
1328
  # :default::
1329
+ # The actually used value depends on the value of #position:
1252
1330
  #
1253
- # :left:: (default) Align the box to the left side of the available region.
1254
- # :center:: Horizontally center the box in the available region.
1255
- # :right:: Align the box to the right side of the available region.
1331
+ # * For :default the used value is :fill_frame_horizontal.
1332
+ # * For :float the used value is :box.
1333
+ # * For :flow the used value is :fill_frame_horizontal.
1334
+ # * For :absolute the used value is :box.
1256
1335
  #
1257
- # Examples:
1336
+ # :none::
1337
+ # The mask covers nothing (useful for layering boxes over each other).
1258
1338
  #
1259
- # #>pdf-composer100
1260
- # composer.text("Left", border: {width: 1})
1261
- # draw_current_frame_shape("red")
1262
- # composer.text("Center", position_hint: :center, border: {width: 1})
1263
- # draw_current_frame_shape("blue")
1264
- # composer.text("Right", position_hint: :right, border: {width: 1})
1265
- # draw_current_frame_shape("green")
1339
+ # Examples:
1266
1340
  #
1267
- # :float::
1341
+ # #>pdf-composer100
1342
+ # composer.text('Text on bottom', mask_mode: :none)
1343
+ # composer.text('Text on top', fill_color: 'hp-blue')
1344
+ #
1345
+ # :box::
1346
+ # The mask covers the box including the margin around the box.
1347
+ #
1348
+ # Examples:
1349
+ #
1350
+ # #>pdf-composer100
1351
+ # composer.text('Box only mask', mask_mode: :box)
1352
+ # draw_current_frame_shape('hp-blue')
1353
+ # composer.text('Text to the right')
1354
+ #
1355
+ # :fill_horizontal::
1356
+ # The mask covers the box including the margin around the box and the space to the left
1357
+ # and right in the current region.
1268
1358
  #
1269
- # :left:: (default) Float the box to the left side of the available region.
1270
- # :center:: Float the box to the center of the available region.
1271
- # :right:: Float the box to the right side of the available region.
1359
+ # Examples:
1272
1360
  #
1273
- # Examples:
1361
+ # #>pdf-composer100
1362
+ # composer.text('Standard, whole horizontal space')
1363
+ # draw_current_frame_shape('hp-blue')
1364
+ # composer.text('Text underneath')
1274
1365
  #
1275
- # #>pdf-composer100
1276
- # composer.style(:base, position: :float, border: {width: 1})
1277
- # composer.text("Left", position_hint: :left)
1278
- # draw_current_frame_shape("red")
1279
- # composer.text("Center", position_hint: :center)
1280
- # draw_current_frame_shape("blue")
1281
- # composer.text("Right", position_hint: :right)
1282
- # draw_current_frame_shape("green")
1366
+ # :fill_frame_horizontal::
1367
+ # The mask covers the box including the margin around the box and the space to the left
1368
+ # and right in the frame.
1283
1369
  #
1284
- # :absolute::
1370
+ # Examples:
1285
1371
  #
1286
- # An array with the x- and y-coordinates of the bottom left corner of the absolutely
1287
- # positioned box. The coordinates are taken as being relative to the bottom left corner of
1288
- # the frame into which the box is drawn.
1372
+ # #>pdf-composer100
1373
+ # composer.frame.remove_area(Geom2D::Rectangle(100, 50, 10, 50))
1374
+ # composer.text('Mask covers frame horizontally', mask_mode: :fill_frame_horizontal)
1375
+ # draw_current_frame_shape('hp-blue')
1376
+ # composer.text('Text underneath')
1289
1377
  #
1290
- # Examples:
1378
+ # :fill_vertical::
1379
+ # The mask covers the box including the margin around the box and the space to the top
1380
+ # and bottom in the current region.
1291
1381
  #
1292
- # #>pdf-composer100
1293
- # composer.text("Absolute", position: :absolute, position_hint: [30, 40])
1294
- # draw_current_frame_shape("red")
1382
+ # Examples:
1383
+ #
1384
+ # #>pdf-composer100
1385
+ # composer.text('Mask covers vertical space', mask_mode: :fill_vertical)
1386
+ # draw_current_frame_shape('hp-blue')
1387
+ # composer.text('Text to the right')
1388
+ #
1389
+ # :fill::
1390
+ # The mask covers the current region completely.
1391
+ #
1392
+ # Examples:
1393
+ #
1394
+ # #>pdf-composer100
1395
+ # composer.text('Mask covers everything', mask_mode: :fill)
1396
+ # composer.text('On the next page')
1295
1397
 
1296
1398
  [
1297
1399
  [:font, "raise HexaPDF::Error, 'No font set'"],
@@ -1324,8 +1426,8 @@ module HexaPDF
1324
1426
  [:stroke_miter_limit, 10.0],
1325
1427
  [:stroke_dash_pattern, "Content::LineDashPattern.new",
1326
1428
  {setter: "Content::LineDashPattern.normalize(value, phase)", extra_args: ", phase = 0"}],
1327
- [:align, :left, {valid_values: [:left, :center, :right, :justify]}],
1328
- [:valign, :top, {valid_values: [:top, :center, :bottom]}],
1429
+ [:text_align, :left, {valid_values: [:left, :center, :right, :justify]}],
1430
+ [:text_valign, :top, {valid_values: [:top, :center, :bottom]}],
1329
1431
  [:text_indent, 0],
1330
1432
  [:line_spacing, "LineSpacing.new(type: :single)",
1331
1433
  {setter: "LineSpacing.new(**(value.kind_of?(Symbol) || value.kind_of?(Numeric) ? " \
@@ -1340,8 +1442,11 @@ module HexaPDF
1340
1442
  [:border, "Border.new", {setter: "Border.new(**value)"}],
1341
1443
  [:overlays, "Layers.new", {setter: "Layers.new(value)"}],
1342
1444
  [:underlays, "Layers.new", {setter: "Layers.new(value)"}],
1343
- [:position, :default, {valid_values: [:default, :float, :flow, :absolute]}],
1344
- [:position_hint, nil],
1445
+ [:position, :default],
1446
+ [:align, :left, {valid_values: [:left, :center, :right]}],
1447
+ [:valign, :top, {valid_values: [:top, :center, :bottom]}],
1448
+ [:mask_mode, :default, {valid_values: [:default, :none, :box, :fill_horizontal,
1449
+ :fill_frame_horizontal, :fill_vertical, :fill]}],
1345
1450
  ].each do |name, default, options = {}|
1346
1451
  default = default.inspect unless default.kind_of?(String)
1347
1452
  setter = options.delete(:setter) || "value"
@@ -112,8 +112,8 @@ module HexaPDF
112
112
  # Each table can have header rows and footer rows which are shown for all split parts:
113
113
  #
114
114
  # #>pdf-composer
115
- # header = lambda {|tb| [[{content: layout.text('Header', align: :center), col_span: 2}]] }
116
- # footer = lambda {|tb| [[layout.text('left'), layout.text('right', align: :right)]] }
115
+ # header = lambda {|tb| [[{content: layout.text('Header', text_align: :center), col_span: 2}]] }
116
+ # footer = lambda {|tb| [[layout.text('left'), layout.text('right', text_align: :right)]] }
117
117
  # cells = [[layout.text('A'), layout.text('B')],
118
118
  # [layout.text('C'), layout.text('D')],
119
119
  # [layout.text('E'), layout.text('F')]]
@@ -588,7 +588,7 @@ module HexaPDF
588
588
  super && (!@last_fitted_row_index || @last_fitted_row_index < 0)
589
589
  end
590
590
 
591
- # Fits the table into the available space.
591
+ # Fits the table into the current region of the frame.
592
592
  def fit(available_width, available_height, frame)
593
593
  return false if (@initial_width > 0 && @initial_width > available_width) ||
594
594
  (@initial_height > 0 && @initial_height > available_height)
@@ -68,9 +68,9 @@ module HexaPDF
68
68
 
69
69
  # Fits the text box into the Frame.
70
70
  #
71
- # Depending on the 'position' style property, the text is either fit into the rectangular area
72
- # given by +available_width+ and +available_height+, or fit to the outline of the frame
73
- # starting from the top (when 'position' is set to :flow).
71
+ # Depending on the 'position' style property, the text is either fit into the current region
72
+ # of the frame using +available_width+ and +available_height+, or fit to the shape of the
73
+ # frame starting from the top (when 'position' is set to :flow).
74
74
  #
75
75
  # The spacing after the last line can be controlled via the style property +last_line_gap+.
76
76
  #
@@ -90,12 +90,12 @@ module HexaPDF
90
90
  height = (@initial_height > 0 ? @initial_height : available_height) - @height
91
91
  @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
92
92
  end
93
- @width += if @initial_width > 0 || style.align == :center || style.align == :right
93
+ @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
94
94
  width
95
95
  else
96
96
  @result.lines.max_by(&:width)&.width || 0
97
97
  end
98
- @height += if @initial_height > 0 || style.valign == :center || style.valign == :bottom
98
+ @height += if @initial_height > 0 || style.text_valign == :center || style.text_valign == :bottom
99
99
  height
100
100
  else
101
101
  @result.height
@@ -70,6 +70,56 @@ module HexaPDF
70
70
  TextShaper.new.shape_text(fragment)
71
71
  end
72
72
 
73
+ # :call-seq:
74
+ # TextFragment.create_with_fallback_glyphs(text, style) -> [frag]
75
+ # TextFragment.create_with_fallback_glyphs(text, style) {|codepoint| block } -> [frag1, frag2, ...]
76
+ #
77
+ # Creates one or more TextFragment objects for the given text - possibly using glyphs from
78
+ # fallback fonts -, shapes them and returns them.
79
+ #
80
+ # If no block is given, the method works like #create but returns the text fragment inside an
81
+ # array.
82
+ #
83
+ # If a block is given, the text is split on codepoints for which there is no glyph in the
84
+ # style's font. For the parts with valid glyphs TextFragment objects are created like with
85
+ # #create. Each codepoint without a valid glyph is yielded to the given block together with
86
+ # the associated HexaPDF::Font::InvalidGlyph object as arguments. The block needs to return an
87
+ # array of either HexaPDF::Font::Type1Wrapper::Glyph or HexaPDF::Font::TrueTypeWrapper::Glyph
88
+ # objects. This array is then used for creating a TextFragment object.
89
+ #
90
+ # The needed style of the text fragments is specified by the +style+ argument (see
91
+ # Style::create for details). Note that the resulting style object needs at least the font
92
+ # set.
93
+ def self.create_with_fallback_glyphs(text, style)
94
+ return [create(text, style)] if !block_given? || text.empty?
95
+
96
+ style = Style.create(style)
97
+ styles = Hash.new {|h, k| h[k] = style.dup.font(k) }
98
+ styles[style.font] = style
99
+
100
+ result = []
101
+ items = []
102
+ shaper = TextShaper.new
103
+ font = style.font
104
+ text.each_codepoint do |codepoint|
105
+ glyph = font.decode_codepoint(codepoint)
106
+ if glyph.valid? || glyph.control_char?
107
+ items << glyph
108
+ else
109
+ unless items.empty?
110
+ result << shaper.shape_text(new(items, style))
111
+ items = []
112
+ end
113
+ fallback = yield(codepoint, glyph)
114
+ unless fallback.empty?
115
+ result << shaper.shape_text(new(fallback, styles[fallback.first.font_wrapper]))
116
+ end
117
+ end
118
+ end
119
+ result << shaper.shape_text(new(items, style)) unless items.empty?
120
+ result
121
+ end
122
+
73
123
  # The items (glyphs and kerning values) of the text fragment.
74
124
  attr_accessor :items
75
125
 
@@ -662,8 +662,9 @@ module HexaPDF
662
662
 
663
663
  # The style to be applied.
664
664
  #
665
- # Only the following properties are used: Style#text_indent, Style#align, Style#valign,
666
- # Style#line_spacing, Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
665
+ # Only the following properties are used: Style#text_indent, Style#text_align,
666
+ # Style#text_valign, Style#line_spacing, Style#fill_horizontal,
667
+ # Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
667
668
  attr_reader :style
668
669
 
669
670
  # Creates a new TextLayouter object with the given style.
@@ -910,9 +911,9 @@ module HexaPDF
910
911
  end
911
912
  end
912
913
 
913
- # Returns the initial baseline offset from the top, based on the valign style option.
914
+ # Returns the initial baseline offset from the top, based on the text_valign style property.
914
915
  def initial_baseline_offset(lines, height, actual_height)
915
- case style.valign
916
+ case style.text_valign
916
917
  when :top
917
918
  lines.first.y_max
918
919
  when :center
@@ -922,9 +923,9 @@ module HexaPDF
922
923
  end
923
924
  end
924
925
 
925
- # Returns the horizontal offset from the left side, based on the align style option.
926
+ # Returns the horizontal offset from the left side, based on the text_align style property.
926
927
  def horizontal_alignment_offset(line, available_width)
927
- case style.align
928
+ case style.text_align
928
929
  when :left then 0
929
930
  when :center then (available_width - line.width) / 2
930
931
  when :right then available_width - line.width
@@ -303,6 +303,11 @@ module HexaPDF
303
303
  return false unless auto_correct
304
304
  end
305
305
  result
306
+ rescue HexaPDF::Error
307
+ raise
308
+ rescue
309
+ yield("Error: Unexpected value encountered", false, self) if block_given?
310
+ false
306
311
  end
307
312
 
308
313
  # Makes a deep copy of the source PDF object and resets the object identifier.
@@ -424,8 +429,6 @@ module HexaPDF
424
429
  yield("Object must be an indirect object", true)
425
430
  document.add(self)
426
431
  end
427
-
428
- validate_nested(value, &block)
429
432
  end
430
433
 
431
434
  # Validates all nested values of the object, i.e. values inside collection objects.
@@ -212,6 +212,11 @@ module HexaPDF
212
212
  data
213
213
  end
214
214
 
215
+ def perform_validation(&block) # :nodoc:
216
+ super
217
+ each {|element| validate_nested(element, &block) }
218
+ end
219
+
215
220
  end
216
221
 
217
222
  end
@@ -361,12 +361,16 @@ module HexaPDF
361
361
  value, text_color = apply_javascript_formatting(@field.field_value)
362
362
  style.fill_color = text_color if text_color
363
363
  calculate_and_apply_font_size(value, style, width, height, padding)
364
- fragment = HexaPDF::Layout::TextFragment.create(value, style)
364
+ line = HexaPDF::Layout::Line.new(@document.layout.text_fragments(value, style: style))
365
365
 
366
- if @field.concrete_field_type == :comb_text_field
366
+ if @field.concrete_field_type == :comb_text_field && !value.empty?
367
367
  unless @field.key?(:MaxLen)
368
368
  raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
369
369
  end
370
+ unless line.items.size == 1
371
+ raise HexaPDF::Error, "Fallback glyphs are not yet supported with comb text fields"
372
+ end
373
+ fragment = line.items[0]
370
374
  new_items = []
371
375
  cell_width = width.to_f / @field[:MaxLen]
372
376
  scaled_cell_width = cell_width / style.scaled_font_size.to_f
@@ -376,6 +380,7 @@ module HexaPDF
376
380
  new_items << fragment.items.last
377
381
  fragment.items.replace(new_items)
378
382
  fragment.clear_cache
383
+ line.clear_cache
379
384
  # Adobe always seems to add 1 to the first offset...
380
385
  x_offset = 1 + (cell_width - style.scaled_item_width(fragment.items[0])) / 2.0
381
386
  x = case @field.text_alignment
@@ -387,8 +392,8 @@ module HexaPDF
387
392
  # Adobe seems to be left/right-aligning based on twice the border width
388
393
  x = case @field.text_alignment
389
394
  when :left then 2 * padding
390
- when :right then [width - 2 * padding - fragment.width, 2 * padding].max
391
- when :center then [(width - fragment.width) / 2.0, 2 * padding].max
395
+ when :right then [width - 2 * padding - line.width, 2 * padding].max
396
+ when :center then [(width - line.width) / 2.0, 2 * padding].max
392
397
  end
393
398
  end
394
399
 
@@ -400,14 +405,14 @@ module HexaPDF
400
405
  style.font_size
401
406
  y = padding + (height - 2 * padding - cap_height) / 2.0
402
407
  y = padding - style.scaled_font_descender if y < 0
403
- fragment.draw(canvas, x, y)
408
+ line.each {|fragment, fx, _| fragment.draw(canvas, x + fx, y) }
404
409
  end
405
410
 
406
411
  # Draws multiple lines of text inside the widget's rectangle.
407
412
  def draw_multiline_text(canvas, width, height, style, padding)
408
- items = [Layout::TextFragment.create(@field.field_value, style)]
413
+ items = @document.layout.text_fragments(@field.field_value, style: style)
409
414
  layouter = Layout::TextLayouter.new(style)
410
- layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
415
+ layouter.style.text_align(@field.text_alignment).line_spacing(:proportional, 1.25)
411
416
 
412
417
  result = nil
413
418
  if style.font_size == 0 # need to auto-size text
@@ -437,12 +442,12 @@ module HexaPDF
437
442
 
438
443
  option_items = @field.option_items
439
444
  top_index = @field.list_box_top_index
440
- items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
445
+ items = @document.layout.text_fragments(option_items[top_index..-1].join("\n"), style: style)
441
446
  # Should use /I but if it differs from /V, we need to use /V; so just use /V...
442
447
  indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
443
448
 
444
449
  layouter = Layout::TextLayouter.new(style)
445
- layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
450
+ layouter.style.text_align(@field.text_alignment).line_spacing(:proportional, 1.25)
446
451
  result = layouter.fit(items, width - 4 * padding, height)
447
452
 
448
453
  unless result.lines.empty?
@@ -490,8 +495,8 @@ module HexaPDF
490
495
  font.scaling_factor / 1000.0
491
496
  # The constant factor was found empirically by checking what Adobe Reader etc. do
492
497
  style.font_size = (height - 2 * padding) / unit_font_size * 0.85
493
- fragment = HexaPDF::Layout::TextFragment.create(value, style)
494
- style.font_size = [style.font_size, style.font_size * (width - 4 * padding) / fragment.width].min
498
+ calc_width = @document.layout.text_fragments(value, style: style).sum(&:width)
499
+ style.font_size = [style.font_size, style.font_size * (width - 4 * padding) / calc_width].min
495
500
  style.clear_cache
496
501
  end
497
502
 
@@ -307,16 +307,6 @@ module HexaPDF
307
307
  super
308
308
  container_name = leaf_node_container_name
309
309
 
310
- # All kids entries must be indirect objects
311
- if key?(:Kids)
312
- self[:Kids].each_with_index do |kid, index|
313
- unless kid.kind_of?(HexaPDF::Object) && kid.indirect?
314
- yield("Child entries of sorted tree nodes must be indirect objects", true)
315
- value[:Kids][index] = document.add(kid)
316
- end
317
- end
318
- end
319
-
320
310
  # All keys of the container must be lexically ordered strings and the container must be
321
311
  # correctly formatted
322
312
  if key?(container_name)
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.34.1'
40
+ VERSION = '0.35.0'
41
41
 
42
42
  end
@@ -756,6 +756,28 @@ describe HexaPDF::Content::Canvas do
756
756
  end
757
757
  end
758
758
 
759
+ describe "form" do
760
+ it "uses the context dimensions if none are given" do
761
+ form = @canvas.form
762
+ assert_equal(@canvas.context.box.value, form.box.value)
763
+ end
764
+
765
+ it "uses the provided dimensions" do
766
+ form = @canvas.form(300, 200)
767
+ assert_equal([0, 0, 300, 200], form.box.value)
768
+ end
769
+
770
+ it "yields the canvas for defining the form's content" do
771
+ yielded_canvas = nil
772
+ form = @canvas.form {|canvas| yielded_canvas = canvas }
773
+ assert_equal(form.canvas, yielded_canvas)
774
+ end
775
+
776
+ it "raises an ArgumentError if only one of width/height is provided" do
777
+ assert_raises(ArgumentError) { @canvas.form(20) }
778
+ end
779
+ end
780
+
759
781
  describe "graphic_object" do
760
782
  it "returns a new graphic object given a name" do
761
783
  arc = @canvas.graphic_object(:arc)
@@ -1329,6 +1351,21 @@ describe HexaPDF::Content::Canvas do
1329
1351
  end
1330
1352
  end
1331
1353
 
1354
+ describe "composer" do
1355
+ it "creates a CanvasComposer, yields it and returns it" do
1356
+ comp1 = nil
1357
+ comp2 = @canvas.composer {|composer| comp1 = composer }
1358
+ assert_kind_of(HexaPDF::Content::CanvasComposer, comp1)
1359
+ assert_same(comp1, comp2)
1360
+ assert_same(@canvas, comp1.canvas)
1361
+ end
1362
+
1363
+ it "passes on the margin argument" do
1364
+ comp = @canvas.composer(margin: 20)
1365
+ assert_equal(20, comp.frame.x)
1366
+ end
1367
+ end
1368
+
1332
1369
  describe "color_from_specification "do
1333
1370
  it "accepts a color string" do
1334
1371
  assert_equal([1, 0, 0], @canvas.color_from_specification("red").components)