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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/examples/009-text_layouter_alignment.rb +7 -7
- data/examples/010-text_layouter_inline_boxes.rb +1 -1
- data/examples/011-text_layouter_line_wrapping.rb +2 -4
- data/examples/013-text_layouter_shapes.rb +9 -11
- data/examples/014-text_in_polygon.rb +2 -2
- data/examples/016-frame_automatic_box_placement.rb +6 -7
- data/examples/017-frame_text_flow.rb +2 -2
- data/examples/018-composer.rb +5 -6
- data/examples/020-column_box.rb +2 -2
- data/examples/021-list_box.rb +1 -1
- data/examples/027-composer_optional_content.rb +5 -5
- data/examples/028-frame_mask_mode.rb +23 -0
- data/examples/029-composer_fallback_fonts.rb +22 -0
- data/lib/hexapdf/cli/info.rb +1 -0
- data/lib/hexapdf/cli/inspect.rb +55 -2
- data/lib/hexapdf/composer.rb +2 -2
- data/lib/hexapdf/configuration.rb +61 -1
- data/lib/hexapdf/content/canvas.rb +63 -0
- data/lib/hexapdf/content/canvas_composer.rb +142 -0
- data/lib/hexapdf/content.rb +1 -0
- data/lib/hexapdf/dictionary.rb +14 -3
- data/lib/hexapdf/document/layout.rb +35 -13
- data/lib/hexapdf/encryption/standard_security_handler.rb +15 -0
- data/lib/hexapdf/error.rb +2 -1
- data/lib/hexapdf/font/invalid_glyph.rb +22 -6
- data/lib/hexapdf/font/true_type_wrapper.rb +48 -20
- data/lib/hexapdf/font/type1_wrapper.rb +48 -24
- data/lib/hexapdf/layout/box.rb +11 -8
- data/lib/hexapdf/layout/column_box.rb +5 -3
- data/lib/hexapdf/layout/frame.rb +77 -39
- data/lib/hexapdf/layout/image_box.rb +3 -3
- data/lib/hexapdf/layout/list_box.rb +20 -19
- data/lib/hexapdf/layout/style.rb +173 -68
- data/lib/hexapdf/layout/table_box.rb +3 -3
- data/lib/hexapdf/layout/text_box.rb +5 -5
- data/lib/hexapdf/layout/text_fragment.rb +50 -0
- data/lib/hexapdf/layout/text_layouter.rb +7 -6
- data/lib/hexapdf/object.rb +5 -2
- data/lib/hexapdf/pdf_array.rb +5 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +16 -11
- data/lib/hexapdf/utils/sorted_tree_node.rb +0 -10
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +37 -0
- data/test/hexapdf/content/test_canvas_composer.rb +112 -0
- data/test/hexapdf/document/test_layout.rb +40 -12
- data/test/hexapdf/encryption/test_standard_security_handler.rb +43 -0
- data/test/hexapdf/font/test_invalid_glyph.rb +13 -1
- data/test/hexapdf/font/test_true_type_wrapper.rb +15 -2
- data/test/hexapdf/font/test_type1_wrapper.rb +21 -2
- data/test/hexapdf/layout/test_column_box.rb +14 -0
- data/test/hexapdf/layout/test_frame.rb +181 -95
- data/test/hexapdf/layout/test_list_box.rb +7 -7
- data/test/hexapdf/layout/test_style.rb +14 -10
- data/test/hexapdf/layout/test_table_box.rb +3 -3
- data/test/hexapdf/layout/test_text_box.rb +2 -2
- data/test/hexapdf/layout/test_text_fragment.rb +37 -0
- data/test/hexapdf/layout/test_text_layouter.rb +10 -10
- data/test/hexapdf/test_configuration.rb +49 -0
- data/test/hexapdf/test_dictionary.rb +1 -1
- data/test/hexapdf/test_object.rb +13 -12
- data/test/hexapdf/test_pdf_array.rb +9 -0
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +41 -13
- data/test/hexapdf/utils/test_sorted_tree_node.rb +1 -1
- metadata +7 -3
data/lib/hexapdf/layout/style.rb
CHANGED
|
@@ -577,7 +577,7 @@ module HexaPDF
|
|
|
577
577
|
# equivalent to the property names.
|
|
578
578
|
#
|
|
579
579
|
# Example:
|
|
580
|
-
# Style.new(font_size: 15,
|
|
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:
|
|
949
|
+
# :method: text_align
|
|
950
950
|
# :call-seq:
|
|
951
|
-
#
|
|
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,
|
|
968
|
-
# composer.text(text,
|
|
969
|
-
# composer.text(text,
|
|
970
|
-
# composer.text(text,
|
|
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:
|
|
973
|
+
# :method: text_valign
|
|
974
974
|
# :call-seq:
|
|
975
|
-
#
|
|
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,
|
|
995
|
-
# composer.text("Center aligned", height: 20,
|
|
996
|
-
# composer.text("Bottom aligned",
|
|
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.
|
|
1218
|
-
#
|
|
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::
|
|
1223
|
-
#
|
|
1224
|
-
#
|
|
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
|
-
#
|
|
1227
|
-
# the left/right can still be used. The position hint specifies where the box should
|
|
1228
|
-
# float.
|
|
1258
|
+
# Examples:
|
|
1229
1259
|
#
|
|
1230
|
-
#
|
|
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
|
-
#
|
|
1233
|
-
#
|
|
1234
|
-
#
|
|
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
|
-
#
|
|
1237
|
-
# are given via the position hint.
|
|
1269
|
+
# Examples:
|
|
1238
1270
|
#
|
|
1239
|
-
#
|
|
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:
|
|
1276
|
+
# :method: align
|
|
1243
1277
|
# :call-seq:
|
|
1244
|
-
#
|
|
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
|
-
#
|
|
1247
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
1254
|
-
#
|
|
1255
|
-
#
|
|
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
|
-
#
|
|
1336
|
+
# :none::
|
|
1337
|
+
# The mask covers nothing (useful for layering boxes over each other).
|
|
1258
1338
|
#
|
|
1259
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
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
|
-
#
|
|
1370
|
+
# Examples:
|
|
1285
1371
|
#
|
|
1286
|
-
#
|
|
1287
|
-
#
|
|
1288
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
1293
|
-
#
|
|
1294
|
-
#
|
|
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
|
-
[:
|
|
1328
|
-
[:
|
|
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
|
|
1344
|
-
[:
|
|
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',
|
|
116
|
-
# footer = lambda {|tb| [[layout.text('left'), layout.text('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
|
|
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
|
|
72
|
-
#
|
|
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.
|
|
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.
|
|
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#
|
|
666
|
-
# Style#
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
data/lib/hexapdf/object.rb
CHANGED
|
@@ -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.
|
data/lib/hexapdf/pdf_array.rb
CHANGED
|
@@ -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
|
-
|
|
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 -
|
|
391
|
-
when :center then [(width -
|
|
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 =
|
|
413
|
+
items = @document.layout.text_fragments(@field.field_value, style: style)
|
|
409
414
|
layouter = Layout::TextLayouter.new(style)
|
|
410
|
-
layouter.style.
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
494
|
-
style.font_size = [style.font_size, style.font_size * (width - 4 * padding) /
|
|
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)
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -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)
|