hexapdf 1.8.0 → 1.9.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 +27 -0
- data/README.md +3 -0
- data/Rakefile +1 -1
- data/data/hexapdf/fonts/Inter-Bold.ttf +0 -0
- data/data/hexapdf/fonts/Inter-BoldItalic.ttf +0 -0
- data/data/hexapdf/fonts/Inter-Italic.ttf +0 -0
- data/data/hexapdf/fonts/Inter-Regular.ttf +0 -0
- data/data/hexapdf/fonts/OFL.txt +92 -0
- data/examples/019-acro_form.rb +3 -1
- data/examples/030-pdfa.rb +9 -16
- data/examples/034-text_shaping.rb +37 -0
- data/lib/hexapdf/configuration.rb +13 -1
- data/lib/hexapdf/document/annotations.rb +25 -0
- data/lib/hexapdf/font/cmap/writer.rb +15 -9
- data/lib/hexapdf/font/true_type_wrapper.rb +6 -3
- data/lib/hexapdf/font_loader.rb +47 -0
- data/lib/hexapdf/layout/container_box.rb +2 -4
- data/lib/hexapdf/layout/style.rb +66 -4
- data/lib/hexapdf/layout/table_box.rb +96 -13
- data/lib/hexapdf/layout/text_fragment.rb +13 -7
- data/lib/hexapdf/layout/text_shaper.rb +162 -10
- data/lib/hexapdf/type/annotations/appearance_generator.rb +42 -0
- data/lib/hexapdf/type/annotations/ink.rb +107 -0
- data/lib/hexapdf/type/annotations.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/digital_signature/common.rb +5 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +1 -1
- data/test/hexapdf/document/test_annotations.rb +10 -0
- data/test/hexapdf/document/test_layout.rb +6 -3
- data/test/hexapdf/font/cmap/test_writer.rb +8 -6
- data/test/hexapdf/font/test_true_type_wrapper.rb +4 -0
- data/test/hexapdf/layout/test_container_box.rb +3 -1
- data/test/hexapdf/layout/test_style.rb +4 -0
- data/test/hexapdf/layout/test_table_box.rb +117 -1
- data/test/hexapdf/layout/test_text_fragment.rb +18 -8
- data/test/hexapdf/layout/test_text_shaper.rb +55 -5
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +63 -0
- data/test/hexapdf/type/annotations/test_ink.rb +31 -0
- metadata +25 -3
|
@@ -108,6 +108,17 @@ module HexaPDF
|
|
|
108
108
|
# [layout.text('E'), layout.text('F')]]
|
|
109
109
|
# composer.column(height: 50) {|col| col.table(cells) }
|
|
110
110
|
#
|
|
111
|
+
# If a cell doesn't completely fit, it is split. Non-split cells in the same continuation row
|
|
112
|
+
# retain their style but are empty:
|
|
113
|
+
#
|
|
114
|
+
# #>pdf-composer
|
|
115
|
+
# cells = [[layout.text('A'), layout.text('B')],
|
|
116
|
+
# [layout.text('C'), layout.text("D1\nD2")],
|
|
117
|
+
# [layout.text('E'), layout.text('F')]]
|
|
118
|
+
# composer.column(height: 50) {|col| col.table(cells) }
|
|
119
|
+
#
|
|
120
|
+
# Note that the above is only true for cells in rows that are not part of a row-span.
|
|
121
|
+
#
|
|
111
122
|
# It is also possible to use row and column spans:
|
|
112
123
|
#
|
|
113
124
|
# #>pdf-composer
|
|
@@ -237,6 +248,7 @@ module HexaPDF
|
|
|
237
248
|
|
|
238
249
|
# Fits the children of the table cell into the given rectangular area.
|
|
239
250
|
def fit_content(available_width, available_height, frame)
|
|
251
|
+
@remaining_boxes = nil
|
|
240
252
|
width = available_width - reserved_width
|
|
241
253
|
height = @used_height = available_height - reserved_height
|
|
242
254
|
return if width <= 0 || height <= 0
|
|
@@ -245,21 +257,27 @@ module HexaPDF
|
|
|
245
257
|
case children
|
|
246
258
|
when Box
|
|
247
259
|
child_result = frame.fit(children)
|
|
248
|
-
if child_result.
|
|
260
|
+
box, @remaining_boxes = children.split if child_result.overflow?
|
|
261
|
+
if child_result.success? || box
|
|
249
262
|
@preferred_width = child_result.x + child_result.box.width + reserved_width
|
|
250
263
|
@height = @preferred_height = child_result.box.height + reserved_height
|
|
251
264
|
@fit_results = [child_result]
|
|
252
|
-
fit_result.success!
|
|
265
|
+
box ? fit_result.overflow! : fit_result.success!
|
|
253
266
|
end
|
|
254
267
|
when Array
|
|
255
268
|
box_fitter = BoxFitter.new([frame])
|
|
256
269
|
children.each {|box| box_fitter.fit(box) }
|
|
257
|
-
if box_fitter.success?
|
|
270
|
+
if box_fitter.success? || !box_fitter.fit_results.empty?
|
|
258
271
|
max_x_result = box_fitter.fit_results.max_by {|result| result.x + result.box.width }
|
|
259
272
|
@preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
|
|
260
273
|
@height = @preferred_height = box_fitter.content_heights[0] + reserved_height
|
|
261
274
|
@fit_results = box_fitter.fit_results
|
|
262
|
-
|
|
275
|
+
if box_fitter.success?
|
|
276
|
+
fit_result.success!
|
|
277
|
+
else
|
|
278
|
+
@remaining_boxes = box_fitter.remaining_boxes
|
|
279
|
+
fit_result.overflow!
|
|
280
|
+
end
|
|
263
281
|
end
|
|
264
282
|
else
|
|
265
283
|
@preferred_width = reserved_width
|
|
@@ -274,6 +292,14 @@ module HexaPDF
|
|
|
274
292
|
end
|
|
275
293
|
end
|
|
276
294
|
|
|
295
|
+
# Splits the content of the cell.
|
|
296
|
+
def split_content
|
|
297
|
+
box = create_split_box
|
|
298
|
+
box.instance_variable_set(:@children, @remaining_boxes)
|
|
299
|
+
box.instance_variable_set(:@remaining_boxes, nil)
|
|
300
|
+
[self, box]
|
|
301
|
+
end
|
|
302
|
+
|
|
277
303
|
# Draws the content of the cell.
|
|
278
304
|
def draw_content(canvas, x, y)
|
|
279
305
|
return if @fit_results.empty?
|
|
@@ -363,7 +389,7 @@ module HexaPDF
|
|
|
363
389
|
# Note that the same cell instance may be returned for different (row, column) arguments if
|
|
364
390
|
# the cell spans more than one row and/or column.
|
|
365
391
|
def [](row, column)
|
|
366
|
-
@cells[row]&.[](column)
|
|
392
|
+
row == @overridden_row_index ? @overridden_row_cells[column] : @cells[row]&.[](column)
|
|
367
393
|
end
|
|
368
394
|
|
|
369
395
|
# Returns the number of rows.
|
|
@@ -378,7 +404,15 @@ module HexaPDF
|
|
|
378
404
|
|
|
379
405
|
# Iterates over each row.
|
|
380
406
|
def each_row(&block)
|
|
381
|
-
|
|
407
|
+
return to_enum(__method__) unless block_given?
|
|
408
|
+
|
|
409
|
+
if @overridden_row_index
|
|
410
|
+
@cells[0...@overridden_row_index].each(&block)
|
|
411
|
+
block&.call(@overridden_row_cells)
|
|
412
|
+
@cells[(@overridden_row_index + 1)..-1].each(&block)
|
|
413
|
+
else
|
|
414
|
+
@cells.each(&block)
|
|
415
|
+
end
|
|
382
416
|
end
|
|
383
417
|
|
|
384
418
|
# Applies the given style properties to all cells and optionally yields all cells for more
|
|
@@ -409,12 +443,19 @@ module HexaPDF
|
|
|
409
443
|
row_heights = {}
|
|
410
444
|
zero_height_rows = {}
|
|
411
445
|
row_spans = []
|
|
446
|
+
split_row_cells = nil
|
|
412
447
|
|
|
413
448
|
@cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
|
|
449
|
+
columns = @overridden_row_cells if row_index == @overridden_row_index
|
|
450
|
+
|
|
451
|
+
# Rows containing row-spanning cells aren't supported for cell-splitting
|
|
452
|
+
row_has_row_spans = columns.any? {|cell| cell.row != row_index || cell.row_span > 1 }
|
|
453
|
+
|
|
414
454
|
# 1. Fit all columns of the row and record the max height of all non-row-span cells. If
|
|
415
455
|
# a row has zero height (usually because it only has row-span cells), record that
|
|
416
456
|
# information. Additionally store all cells with row-spans.
|
|
417
457
|
row_fit = true
|
|
458
|
+
row_has_split_cells = false
|
|
418
459
|
row_height = 0
|
|
419
460
|
columns.each_with_index do |cell, col_index|
|
|
420
461
|
next if cell.row != row_index || cell.column != col_index
|
|
@@ -423,9 +464,14 @@ module HexaPDF
|
|
|
423
464
|
else
|
|
424
465
|
column_info[cell.column].last
|
|
425
466
|
end
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
467
|
+
cell_fit_result = cell.fit(available_cell_width, available_height, frame)
|
|
468
|
+
unless cell_fit_result.success?
|
|
469
|
+
if !row_has_row_spans && cell_fit_result.overflow?
|
|
470
|
+
row_has_split_cells = true
|
|
471
|
+
else
|
|
472
|
+
row_fit = false
|
|
473
|
+
break
|
|
474
|
+
end
|
|
429
475
|
end
|
|
430
476
|
if row_height < cell.preferred_height && cell.row_span == 1
|
|
431
477
|
row_height = cell.preferred_height
|
|
@@ -462,6 +508,11 @@ module HexaPDF
|
|
|
462
508
|
available_height -= cell.preferred_height - row_span_height
|
|
463
509
|
end
|
|
464
510
|
end
|
|
511
|
+
|
|
512
|
+
if row_has_split_cells
|
|
513
|
+
split_row_cells = create_split_row_cells(columns)
|
|
514
|
+
break
|
|
515
|
+
end
|
|
465
516
|
else
|
|
466
517
|
last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
|
|
467
518
|
break
|
|
@@ -473,6 +524,7 @@ module HexaPDF
|
|
|
473
524
|
# final height and top-left corner of each cell needs to be set.
|
|
474
525
|
running_height = 0
|
|
475
526
|
@cells[start_row..last_fitted_row_index].each.with_index(start_row) do |columns, row_index|
|
|
527
|
+
columns = @overridden_row_cells if row_index == @overridden_row_index
|
|
476
528
|
columns.each_with_index do |cell, col_index|
|
|
477
529
|
next if cell.row != row_index || cell.column != col_index
|
|
478
530
|
cell.left = column_info[cell.column].first
|
|
@@ -488,13 +540,15 @@ module HexaPDF
|
|
|
488
540
|
end
|
|
489
541
|
end
|
|
490
542
|
|
|
491
|
-
[height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index
|
|
543
|
+
[height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index,
|
|
544
|
+
split_row_cells]
|
|
492
545
|
end
|
|
493
546
|
|
|
494
547
|
# Draws the rows from +start_row+ to +end_row+ on the given +canvas+, with the top-left
|
|
495
548
|
# corner of the resulting table being at (+x+, +y+).
|
|
496
549
|
def draw_rows(start_row, end_row, canvas, x, y)
|
|
497
550
|
@cells[start_row..end_row].each.with_index(start_row) do |columns, row_index|
|
|
551
|
+
columns = @overridden_row_cells if row_index == @overridden_row_index
|
|
498
552
|
columns.each_with_index do |cell, col_index|
|
|
499
553
|
next if cell.row != row_index || cell.column != col_index
|
|
500
554
|
cell.draw(canvas, x + cell.left, y - cell.top - cell.height)
|
|
@@ -557,6 +611,22 @@ module HexaPDF
|
|
|
557
611
|
end
|
|
558
612
|
end
|
|
559
613
|
|
|
614
|
+
# Splits all cells in +columns+ and returns the new array.
|
|
615
|
+
def create_split_row_cells(columns)
|
|
616
|
+
columns.map.with_index do |cell, col_index|
|
|
617
|
+
next cell unless cell.column == col_index
|
|
618
|
+
_, split_box = cell.split
|
|
619
|
+
if split_box
|
|
620
|
+
split_box
|
|
621
|
+
else
|
|
622
|
+
# Create an empty clone of the fully fit cell
|
|
623
|
+
empty_cell = cell.send(:create_split_box)
|
|
624
|
+
empty_cell.instance_variable_set(:@children, nil)
|
|
625
|
+
empty_cell
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
560
630
|
end
|
|
561
631
|
|
|
562
632
|
# The Cells instance containing the data of the table.
|
|
@@ -682,6 +752,7 @@ module HexaPDF
|
|
|
682
752
|
@special_cells_fit_not_successful = false
|
|
683
753
|
[@header_cells, @footer_cells].each do |special_cells|
|
|
684
754
|
next unless special_cells
|
|
755
|
+
# Ignore split-cells (3rd result element) as not fully-fit headers/footers lead to failure
|
|
685
756
|
special_used_height, last_fitted_row_index = special_cells.fit_rows(0, height, columns, frame)
|
|
686
757
|
height -= special_used_height
|
|
687
758
|
used_height += special_used_height
|
|
@@ -689,13 +760,14 @@ module HexaPDF
|
|
|
689
760
|
return nil if @special_cells_fit_not_successful
|
|
690
761
|
end
|
|
691
762
|
|
|
692
|
-
main_used_height, @last_fitted_row_index
|
|
763
|
+
main_used_height, @last_fitted_row_index, @split_row_cells =
|
|
764
|
+
@cells.fit_rows(@start_row_index, height, columns, frame)
|
|
693
765
|
used_height += main_used_height
|
|
694
766
|
|
|
695
767
|
update_content_width { columns[-1].sum + rw }
|
|
696
768
|
update_content_height { used_height + rh }
|
|
697
769
|
|
|
698
|
-
if @last_fitted_row_index == @cells.number_of_rows - 1
|
|
770
|
+
if @last_fitted_row_index == @cells.number_of_rows - 1 && !@split_row_cells
|
|
699
771
|
fit_result.success!
|
|
700
772
|
elsif @last_fitted_row_index >= 0
|
|
701
773
|
fit_result.overflow!
|
|
@@ -724,8 +796,19 @@ module HexaPDF
|
|
|
724
796
|
# Splits the content of the table box. This method is called from Box#split.
|
|
725
797
|
def split_content
|
|
726
798
|
box = create_split_box
|
|
727
|
-
|
|
799
|
+
|
|
800
|
+
if @split_row_cells
|
|
801
|
+
cells = @cells.clone
|
|
802
|
+
cells.instance_variable_set(:@overridden_row_cells, @split_row_cells)
|
|
803
|
+
cells.instance_variable_set(:@overridden_row_index, @last_fitted_row_index)
|
|
804
|
+
box.instance_variable_set(:@cells, cells)
|
|
805
|
+
box.instance_variable_set(:@start_row_index, @last_fitted_row_index)
|
|
806
|
+
else
|
|
807
|
+
box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
|
|
808
|
+
end
|
|
809
|
+
|
|
728
810
|
box.instance_variable_set(:@last_fitted_row_index, -1)
|
|
811
|
+
box.instance_variable_set(:@split_row_cells, nil)
|
|
729
812
|
box.instance_variable_set(:@special_cells_fit_not_successful, nil)
|
|
730
813
|
header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
|
|
731
814
|
box.instance_variable_set(:@header_cells, header_cells)
|
|
@@ -111,20 +111,24 @@ module HexaPDF
|
|
|
111
111
|
font = style.font
|
|
112
112
|
text.each_codepoint do |codepoint|
|
|
113
113
|
glyph = font.decode_codepoint(codepoint)
|
|
114
|
-
if glyph.valid?
|
|
114
|
+
if glyph.valid?
|
|
115
115
|
items << glyph
|
|
116
116
|
else
|
|
117
117
|
unless items.empty?
|
|
118
|
-
result
|
|
118
|
+
result.append(*shaper.shape_text(new(items, style)))
|
|
119
119
|
items = []
|
|
120
120
|
end
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
if glyph.control_char?
|
|
122
|
+
result.append(new([glyph], style))
|
|
123
|
+
else
|
|
124
|
+
fallback = yield(codepoint, glyph)
|
|
125
|
+
unless fallback.empty?
|
|
126
|
+
result.append(*shaper.shape_text(new(fallback, styles[fallback.first.font_wrapper])))
|
|
127
|
+
end
|
|
124
128
|
end
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
|
-
result
|
|
131
|
+
result.append(*shaper.shape_text(new(items, style))) unless items.empty?
|
|
128
132
|
result
|
|
129
133
|
end
|
|
130
134
|
|
|
@@ -251,7 +255,9 @@ module HexaPDF
|
|
|
251
255
|
tx = x - tlm.e
|
|
252
256
|
ty = y - tlm.f
|
|
253
257
|
if tx.abs < PRECISION
|
|
254
|
-
if
|
|
258
|
+
if ty.abs < PRECISION
|
|
259
|
+
# do nothing
|
|
260
|
+
elsif (ty + canvas.graphics_state.leading).abs < PRECISION
|
|
255
261
|
canvas.move_text_cursor
|
|
256
262
|
else
|
|
257
263
|
canvas.move_text_cursor(offset: [0, ty], absolute: false)
|
|
@@ -34,8 +34,71 @@
|
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
35
|
#++
|
|
36
36
|
|
|
37
|
+
require 'hexapdf/error'
|
|
37
38
|
require 'hexapdf/layout/numeric_refinements'
|
|
38
39
|
|
|
40
|
+
HARFBUZZ_AVAILABLE = begin
|
|
41
|
+
require 'harfbuzz'
|
|
42
|
+
true
|
|
43
|
+
rescue LoadError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if HARFBUZZ_AVAILABLE
|
|
47
|
+
class HarfBuzz::Buffer #:nodoc:
|
|
48
|
+
|
|
49
|
+
GLYPH_INFO_SIZE = HarfBuzz::C::HbGlyphInfoT.size
|
|
50
|
+
GLYPH_INFO_CODEPOINT_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:codepoint)
|
|
51
|
+
GLYPH_INFO_CLUSTER_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:cluster)
|
|
52
|
+
GLYPH_POS_SIZE = HarfBuzz::C::HbGlyphPositionT.size
|
|
53
|
+
GLYPH_POS_XADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_advance)
|
|
54
|
+
GLYPH_POS_YADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_advance)
|
|
55
|
+
GLYPH_POS_XOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_offset)
|
|
56
|
+
GLYPH_POS_YOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_offset)
|
|
57
|
+
|
|
58
|
+
# Iterates efficiently over the shaping result without creating intermediary objects.
|
|
59
|
+
def each_result
|
|
60
|
+
return enum_for(__method__) unless block_given?
|
|
61
|
+
|
|
62
|
+
length_ptr = FFI::MemoryPointer.new(:uint)
|
|
63
|
+
infos_ptr = HarfBuzz::C.hb_buffer_get_glyph_infos(@ptr, length_ptr)
|
|
64
|
+
length_ptr = FFI::MemoryPointer.new(:uint)
|
|
65
|
+
positions_ptr = HarfBuzz::C.hb_buffer_get_glyph_positions(@ptr, length_ptr)
|
|
66
|
+
length = length_ptr.read_uint
|
|
67
|
+
|
|
68
|
+
return if infos_ptr.null? || positions_ptr.null? || length.zero?
|
|
69
|
+
|
|
70
|
+
last_info_cluster_offset = (length - 1) * GLYPH_INFO_SIZE + GLYPH_INFO_CLUSTER_OFFSET
|
|
71
|
+
i = 0
|
|
72
|
+
while i < length
|
|
73
|
+
info_offset = i * GLYPH_INFO_SIZE
|
|
74
|
+
pos_offset = i * GLYPH_POS_SIZE
|
|
75
|
+
|
|
76
|
+
glyph_id = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CODEPOINT_OFFSET)
|
|
77
|
+
cluster = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CLUSTER_OFFSET)
|
|
78
|
+
|
|
79
|
+
next_cluster = nil
|
|
80
|
+
tmp_offset = info_offset + GLYPH_INFO_CLUSTER_OFFSET + GLYPH_INFO_SIZE
|
|
81
|
+
while tmp_offset <= last_info_cluster_offset &&
|
|
82
|
+
(next_cluster = infos_ptr.get_uint32(tmp_offset)) == cluster
|
|
83
|
+
tmp_offset += GLYPH_INFO_SIZE
|
|
84
|
+
next_cluster = nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
x_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_XADVANCE_OFFSET)
|
|
88
|
+
y_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_YADVANCE_OFFSET)
|
|
89
|
+
x_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_XOFFSET_OFFSET)
|
|
90
|
+
y_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_YOFFSET_OFFSET)
|
|
91
|
+
|
|
92
|
+
yield(glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset)
|
|
93
|
+
|
|
94
|
+
i += 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
39
102
|
module HexaPDF
|
|
40
103
|
module Layout
|
|
41
104
|
|
|
@@ -44,23 +107,33 @@ module HexaPDF
|
|
|
44
107
|
# This class is used to perform text shaping, i.e. changing the position of glyphs (e.g. for
|
|
45
108
|
# kerning) or substituting one or more glyphs for other glyphs (e.g. for ligatures).
|
|
46
109
|
#
|
|
47
|
-
#
|
|
110
|
+
# The class contains two shaping engines: A very limited custom one and one based on
|
|
111
|
+
# HarfBuzz. Which one is used for shaping can be selected via the Style#shaping_engine property.
|
|
48
112
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
113
|
+
# The custom implementation is always used for Type1 fonts and supports kerning and ligature
|
|
114
|
+
# substitution. It also supports the 'kern' table for TrueType fonts if HarfBuzz is not used.
|
|
51
115
|
#
|
|
52
|
-
#
|
|
116
|
+
# For complex scripts or the need of special font features it is recommended to use the shaping
|
|
117
|
+
# engine based on HarfBuzz, even though it is slightly slower. For it to work the
|
|
118
|
+
# +harfbuzz-ruby+ gem needs to be installed. OpenType features can be activated and deactivated
|
|
119
|
+
# using Style#font_features.
|
|
53
120
|
class TextShaper
|
|
54
121
|
|
|
55
|
-
# Shapes the given text fragment in-place
|
|
122
|
+
# Shapes the given text fragment. Returns either the in-place modified fragment or, for
|
|
123
|
+
# complex shaping, an array of fragments.
|
|
56
124
|
#
|
|
57
|
-
# The
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# :kern:: Pair-wise kerning.
|
|
61
|
-
# :liga:: Ligature substitution.
|
|
125
|
+
# The style properties Style#shaping_engine, Style#font_features, Style#font_script,
|
|
126
|
+
# Style#language and Style#direction are used for shaping.
|
|
62
127
|
def shape_text(text_fragment)
|
|
63
128
|
font = text_fragment.style.font
|
|
129
|
+
if text_fragment.style.shaping_engine == :harfbuzz && font.font_type == :TrueType
|
|
130
|
+
unless HARFBUZZ_AVAILABLE
|
|
131
|
+
raise HexaPDF::Error, "Shaping engine harfbuzz required but the needed Rubygem " \
|
|
132
|
+
"harfbuzz-ruby is not available"
|
|
133
|
+
end
|
|
134
|
+
return harfbuzz_shape_text(text_fragment)
|
|
135
|
+
end
|
|
136
|
+
|
|
64
137
|
if text_fragment.style.font_features[:liga] && font.wrapped_font.features.include?(:liga)
|
|
65
138
|
if font.font_type == :Type1
|
|
66
139
|
process_type1_ligatures(text_fragment)
|
|
@@ -76,11 +149,90 @@ module HexaPDF
|
|
|
76
149
|
end
|
|
77
150
|
text_fragment.clear_cache
|
|
78
151
|
end
|
|
152
|
+
|
|
79
153
|
text_fragment
|
|
80
154
|
end
|
|
81
155
|
|
|
82
156
|
private
|
|
83
157
|
|
|
158
|
+
# Shapes the text fragment with HarfBuzz.
|
|
159
|
+
def harfbuzz_shape_text(text_fragment, text = nil)
|
|
160
|
+
text ||= text_fragment.items.map(&:str).join
|
|
161
|
+
style = text_fragment.style
|
|
162
|
+
|
|
163
|
+
# Cache the used main Harfbuzz font objects
|
|
164
|
+
hb_font = style.font.pdf_object.document.cache('harfbuzz', style.font.filename) do
|
|
165
|
+
blob = HarfBuzz::Blob.from_file!(style.font.filename)
|
|
166
|
+
face = HarfBuzz::Face.new(blob, 0)
|
|
167
|
+
HarfBuzz::Font.new(face)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Prepare the buffer and then shape the text. We are using cluster level 1 as this is the
|
|
171
|
+
# recommended level.
|
|
172
|
+
buffer = HarfBuzz::Buffer.new
|
|
173
|
+
buffer.add_utf8(text)
|
|
174
|
+
buffer.cluster_level = 1
|
|
175
|
+
buffer.direction = style.direction
|
|
176
|
+
buffer.script = style.font_script if style.font_script?
|
|
177
|
+
buffer.language = style.language if style.language?
|
|
178
|
+
buffer.guess_segment_properties
|
|
179
|
+
HarfBuzz.shape(hb_font, buffer, HarfBuzz::Feature.from_hash(style.font_features))
|
|
180
|
+
|
|
181
|
+
# Prepare the iteration over the shaping result. The final output will either be
|
|
182
|
+
# +text_fragment+ (no non-zero y_offsets) or +result+ containing at least two TextFragment
|
|
183
|
+
# instances.
|
|
184
|
+
result = nil
|
|
185
|
+
font = style.font
|
|
186
|
+
fragment = text_fragment
|
|
187
|
+
fragment.clear_cache
|
|
188
|
+
items = text_fragment.items.clear
|
|
189
|
+
last_cluster = nil
|
|
190
|
+
last_y_offset = 0
|
|
191
|
+
buffer.each_result do |glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset|
|
|
192
|
+
advance = (x_advance - x_offset) * font.scaling_factor
|
|
193
|
+
|
|
194
|
+
# 1. Determine the source characters for each glyph via their cluster numbers. If two or
|
|
195
|
+
# more glyphs have the same cluster number, the first gets the resulting string while the
|
|
196
|
+
# rest map to an empty string. Otherwise copying from the PDF would result in multiple
|
|
197
|
+
# copies of the resulting string.
|
|
198
|
+
str = (cluster == last_cluster ? '' : text.byteslice(cluster...(next_cluster || text.bytesize)))
|
|
199
|
+
|
|
200
|
+
# 2. Handle invalid glyphs with id=0 by mapping them to an InvalidGlyph instance
|
|
201
|
+
if glyph_id.zero?
|
|
202
|
+
glyph = font.decode_codepoint(str.ord)
|
|
203
|
+
advance = glyph.width
|
|
204
|
+
else
|
|
205
|
+
glyph = font.glyph(glyph_id, str)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 3. Handle differing y_offsets by creating TextFragment instances with appropriate text
|
|
209
|
+
# rise properties.
|
|
210
|
+
if y_offset != last_y_offset
|
|
211
|
+
(result ||= []) << fragment
|
|
212
|
+
items = []
|
|
213
|
+
if y_offset.zero?
|
|
214
|
+
fragment = text_fragment.dup_attributes(items)
|
|
215
|
+
else
|
|
216
|
+
fragment = TextFragment.new(items, style.dup, properties: text_fragment.properties)
|
|
217
|
+
fragment.style.text_rise += y_offset * font.scaling_factor * fragment.style.font_size *
|
|
218
|
+
fragment.style.font.pdf_object.glyph_scaling_factor
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# 4. Handle the correct x-positioning using x_offset. Also addjust the horizontal advance
|
|
223
|
+
# based on the glyph's fixed advance width as well as x_advance and x_offset (via
|
|
224
|
+
# +advance+).
|
|
225
|
+
items << -x_offset * font.scaling_factor unless x_offset.zero?
|
|
226
|
+
items << glyph
|
|
227
|
+
items << glyph.width - advance if glyph.width - advance != 0
|
|
228
|
+
|
|
229
|
+
last_cluster = cluster
|
|
230
|
+
last_y_offset = y_offset
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
result ? result.append(fragment) : text_fragment
|
|
234
|
+
end
|
|
235
|
+
|
|
84
236
|
# Processes the text fragment and substitutes ligatures.
|
|
85
237
|
def process_type1_ligatures(text_fragment)
|
|
86
238
|
items = text_fragment.items
|
|
@@ -77,6 +77,7 @@ module HexaPDF
|
|
|
77
77
|
when :Circle then create_square_circle_appearance(:circle)
|
|
78
78
|
when :Polygon then create_polygon_polyline_appearance(:polygon)
|
|
79
79
|
when :PolyLine then create_polygon_polyline_appearance(:polyline)
|
|
80
|
+
when :Ink then create_ink_appearance
|
|
80
81
|
else
|
|
81
82
|
raise HexaPDF::Error, "Appearance regeneration for #{@annot[:Subtype]} not yet supported"
|
|
82
83
|
end
|
|
@@ -362,6 +363,47 @@ module HexaPDF
|
|
|
362
363
|
|
|
363
364
|
end
|
|
364
365
|
|
|
366
|
+
# Creates the appropriate appearance for an ink annotation.
|
|
367
|
+
#
|
|
368
|
+
# See: HexaPDF::Type::Annotations::Ink
|
|
369
|
+
def create_ink_appearance
|
|
370
|
+
# Prepare the annotation
|
|
371
|
+
form = (@annot[:AP] ||= {})[:N] ||=
|
|
372
|
+
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
|
|
373
|
+
form.contents = ""
|
|
374
|
+
@annot.flag(:print)
|
|
375
|
+
@annot.unflag(:hidden)
|
|
376
|
+
|
|
377
|
+
# Get all needed values from the annotation
|
|
378
|
+
paths = @annot.paths
|
|
379
|
+
border_style = @annot.border_style
|
|
380
|
+
opacity = @annot.opacity
|
|
381
|
+
|
|
382
|
+
# Calculate the annotation's rectangle as well as the form bounding box
|
|
383
|
+
x_coords = []
|
|
384
|
+
y_coords = []
|
|
385
|
+
paths.each do |path|
|
|
386
|
+
path.each_with_index {|coord, index| (index.even? ? x_coords : y_coords) << coord }
|
|
387
|
+
end
|
|
388
|
+
min_x, max_x = x_coords.minmax
|
|
389
|
+
min_y, max_y = y_coords.minmax
|
|
390
|
+
padding = 4 * border_style.width
|
|
391
|
+
rect = [min_x - padding, min_y - padding, max_x + padding, max_y + padding]
|
|
392
|
+
@annot[:Rect] = rect
|
|
393
|
+
form[:BBox] = rect.dup
|
|
394
|
+
|
|
395
|
+
# Set the appropriate graphics state
|
|
396
|
+
canvas = form.canvas(translate: false)
|
|
397
|
+
canvas.opacity(**opacity.to_h)
|
|
398
|
+
canvas.stroke_color(border_style.color) if border_style.color
|
|
399
|
+
canvas.line_width(border_style.width)
|
|
400
|
+
canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
|
|
401
|
+
|
|
402
|
+
# Draw the polylines
|
|
403
|
+
paths.each {|path| canvas.polyline(*path) }
|
|
404
|
+
border_style.color ? canvas.stroke : canvas.end_path
|
|
405
|
+
end
|
|
406
|
+
|
|
365
407
|
# Calculates the padding needed around the line endings based on the line ending +style+ and
|
|
366
408
|
# the +border_width+.
|
|
367
409
|
def calculate_line_ending_padding(style, border_width)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2026 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'hexapdf/type/annotation'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module Type
|
|
41
|
+
module Annotations
|
|
42
|
+
|
|
43
|
+
# An ink annotation is a markup annotation that displays a freehand "scribble" composed of one
|
|
44
|
+
# or more disjoint paths.
|
|
45
|
+
#
|
|
46
|
+
# The convenience method #add_path can be used to add paths to the annotation.
|
|
47
|
+
#
|
|
48
|
+
# The style of the scribble can be customized using the convenience methods Annotation#opacity
|
|
49
|
+
# and BorderStyling#border_style (note that only a simple line dash pattern is supported).
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
#
|
|
53
|
+
# #>pdf-small
|
|
54
|
+
# doc.annotations.create_scribble(doc.pages[0]).
|
|
55
|
+
# add_path(10, 10, 50, 40, 40, 80, 80, 60).
|
|
56
|
+
# add_path(5, 90, 20, 90, 15, 50, 80, 5).
|
|
57
|
+
# border_style(color: "hp-blue", width: 2, style: [3, 1]).
|
|
58
|
+
# regenerate_appearance
|
|
59
|
+
#
|
|
60
|
+
# See: PDF2.0 s12.5.6.13, HexaPDF::Type::MarkupAnnotation
|
|
61
|
+
class Ink < MarkupAnnotation
|
|
62
|
+
|
|
63
|
+
include BorderStyling
|
|
64
|
+
|
|
65
|
+
define_field :Subtype, type: Symbol, required: true, default: :Ink
|
|
66
|
+
define_field :InkList, type: PDFArray, required: true
|
|
67
|
+
define_field :BS, type: :Border
|
|
68
|
+
define_field :Path, type: PDFArray, version: '2.0'
|
|
69
|
+
|
|
70
|
+
# :call-seq:
|
|
71
|
+
# annot.paths => [[x00, y00, x01, y01, ...], [x10, y10, x11, y11, ...], ...]
|
|
72
|
+
#
|
|
73
|
+
# Returns an array with all the paths of this annotation.
|
|
74
|
+
def paths
|
|
75
|
+
self[:InkList].to_a
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# :call-seq:
|
|
79
|
+
# annot.add_path => [x0, y0, x1, y1, ...]
|
|
80
|
+
# annot.add_path(*points) => annot
|
|
81
|
+
#
|
|
82
|
+
# Adds the path consisting of the given +points+ to the list of paths and returns self.
|
|
83
|
+
#
|
|
84
|
+
# Adding at least one path is required. Note, however, that without setting the appearance
|
|
85
|
+
# style through convenience methods like #border_style nothing will be shown.
|
|
86
|
+
#
|
|
87
|
+
# Example:
|
|
88
|
+
#
|
|
89
|
+
# #>pdf-small
|
|
90
|
+
# doc.annotations.create_scribble(doc.pages[0]).
|
|
91
|
+
# add_path(10, 10, 40, 60, 90, 90).
|
|
92
|
+
# regenerate_appearance
|
|
93
|
+
def add_path(*points)
|
|
94
|
+
if points.length % 2 != 0
|
|
95
|
+
raise ArgumentError, "An even number of arguments must be provided"
|
|
96
|
+
else
|
|
97
|
+
self[:InkList] ||= []
|
|
98
|
+
self[:InkList] << points
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -60,6 +60,7 @@ module HexaPDF
|
|
|
60
60
|
autoload(:PolygonPolyline, 'hexapdf/type/annotations/polygon_polyline')
|
|
61
61
|
autoload(:Polygon, 'hexapdf/type/annotations/polygon')
|
|
62
62
|
autoload(:Polyline, 'hexapdf/type/annotations/polyline')
|
|
63
|
+
autoload(:Ink, 'hexapdf/type/annotations/ink')
|
|
63
64
|
|
|
64
65
|
end
|
|
65
66
|
|