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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +3 -0
  4. data/Rakefile +1 -1
  5. data/data/hexapdf/fonts/Inter-Bold.ttf +0 -0
  6. data/data/hexapdf/fonts/Inter-BoldItalic.ttf +0 -0
  7. data/data/hexapdf/fonts/Inter-Italic.ttf +0 -0
  8. data/data/hexapdf/fonts/Inter-Regular.ttf +0 -0
  9. data/data/hexapdf/fonts/OFL.txt +92 -0
  10. data/examples/019-acro_form.rb +3 -1
  11. data/examples/030-pdfa.rb +9 -16
  12. data/examples/034-text_shaping.rb +37 -0
  13. data/lib/hexapdf/configuration.rb +13 -1
  14. data/lib/hexapdf/document/annotations.rb +25 -0
  15. data/lib/hexapdf/font/cmap/writer.rb +15 -9
  16. data/lib/hexapdf/font/true_type_wrapper.rb +6 -3
  17. data/lib/hexapdf/font_loader.rb +47 -0
  18. data/lib/hexapdf/layout/container_box.rb +2 -4
  19. data/lib/hexapdf/layout/style.rb +66 -4
  20. data/lib/hexapdf/layout/table_box.rb +96 -13
  21. data/lib/hexapdf/layout/text_fragment.rb +13 -7
  22. data/lib/hexapdf/layout/text_shaper.rb +162 -10
  23. data/lib/hexapdf/type/annotations/appearance_generator.rb +42 -0
  24. data/lib/hexapdf/type/annotations/ink.rb +107 -0
  25. data/lib/hexapdf/type/annotations.rb +1 -0
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/digital_signature/common.rb +5 -5
  28. data/test/hexapdf/digital_signature/test_cms_handler.rb +1 -1
  29. data/test/hexapdf/document/test_annotations.rb +10 -0
  30. data/test/hexapdf/document/test_layout.rb +6 -3
  31. data/test/hexapdf/font/cmap/test_writer.rb +8 -6
  32. data/test/hexapdf/font/test_true_type_wrapper.rb +4 -0
  33. data/test/hexapdf/layout/test_container_box.rb +3 -1
  34. data/test/hexapdf/layout/test_style.rb +4 -0
  35. data/test/hexapdf/layout/test_table_box.rb +117 -1
  36. data/test/hexapdf/layout/test_text_fragment.rb +18 -8
  37. data/test/hexapdf/layout/test_text_shaper.rb +55 -5
  38. data/test/hexapdf/type/annotations/test_appearance_generator.rb +63 -0
  39. data/test/hexapdf/type/annotations/test_ink.rb +31 -0
  40. metadata +25 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e9f612a2b916386baa8a689c47463faf4fff424732101ea75f3bb221c4cf824
4
- data.tar.gz: 60384753e3b5ac2da147631c567933e94342115e8423f4fc718a55aac3bc986c
3
+ metadata.gz: d51b41e4326a58a6c3d51e8ee0ee3ce1ee194199d9967817240dac81205ba573
4
+ data.tar.gz: ac3ca606173c2fad485bf5df8d5fb8947c33aab2fc28b970516adedf804de5eb
5
5
  SHA512:
6
- metadata.gz: c9dff3546d3a3636ea4d9a0f9a3d9e649c20c642b6bebb53b8c98adf49a46f6cce11a8cf2d8a0761766ad98fec360c54a52333ecad027ea0694941599266c648
7
- data.tar.gz: bbc5c0d5af26c6369009a0ddf8d652b1a562c0d89c64e007f2e294af4f34032fa4524814e1dc19f20213148453f88f79de8def53121bb1fda4e44093458987c7
6
+ metadata.gz: dea3e27895918e159dedb6b8ff44b5db055e886656d1439d8d927fab57e9b1039489d9234392109b52a881ae67a02716c48a12dba9534e4d7894ab5e4c93251f
7
+ data.tar.gz: bef1d2e5217562c4772e1dc16a1a38e8342c741a1bcc404cabe203ed07268e8e25c2f5476749a8712362cece9dada4ed91e6e989a88872f7c4d08c3e24298823
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## 1.9.0 - 2026-06-07
2
+
3
+ ### Added
4
+
5
+ * Support for HarfBuzz based text shaping
6
+ * [HexaPDF::Layout::Style#shaping_engine] for specifying the used shaping engine
7
+ * [HexaPDF::Layout::Style#font_script] for specifying the script of text
8
+ * [HexaPDF::Layout::Style#language] for specifying the language of text
9
+ * [HexaPDF::Layout::Style#direction] for specifying the direction of text
10
+ * [HexaPDF::Font::TrueTypeWrapper#filename] for retrieving the filename from
11
+ which the wrapped font was created
12
+ * Inter as bundled font in variants regular, bold, italic and bold italic
13
+ * [HexaPDF::Type::Annotations::Ink] for ink annotations as well as
14
+ [HexaPDF::Document::Annotations#create_scribble]
15
+
16
+ ### Changed
17
+
18
+ * [HexaPDF::Layout::TableBox] to support splitting cells
19
+ * [HexaPDF::Font::CMap::Writer] to support ToUnicode CMaps with mappings from
20
+ one input code to multiple codepoints
21
+
22
+ ### Fixed
23
+
24
+ * [HexaPDF::Layout::ContainerBox] to correctly set width and height when
25
+ splitting
26
+
27
+
1
28
  ## 1.8.0 - 2026-05-14
2
29
 
3
30
  ### Added
data/README.md CHANGED
@@ -177,6 +177,9 @@ Some included files have a different license:
177
177
 
178
178
  * The file `test/data/fonts/Ubuntu-Title.ttf` is licensed under the SIL Open Font License.
179
179
 
180
+ * The files in `data/hexapdf/fonts` are licensed under SIL Open Font License, Version 1.1, see the
181
+ file `data/hexapdf/fonts/OFL.txt`.
182
+
180
183
  * The AES test vector files in `test/data/aes-test-vectors` have been created using the test vector
181
184
  file available from <http://csrc.nist.gov/groups/STM/cavp/block-ciphers.html#test-vectors>.
182
185
 
data/Rakefile CHANGED
@@ -47,7 +47,7 @@ namespace :dev do
47
47
  end
48
48
 
49
49
  task :test_all do
50
- versions = `rbenv versions --bare | grep -i ^3.`.split("\n")
50
+ versions = `rbenv versions --bare | grep -i '^3.[2-9]\\|^4.'`.split("\n")
51
51
  versions.each do |version|
52
52
  sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
53
53
  end
Binary file
Binary file
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -4,7 +4,8 @@
4
4
  # fields. HexaPDF supports the creation and processing of these forms.
5
5
  #
6
6
  # This example show-cases how to create the various form field types and their
7
- # possible standard appearances.
7
+ # possible standard appearances (e.g. setting the background color and border
8
+ # style).
8
9
  #
9
10
  # The [HexaPDF::Type::AcroForm::Form] and [HexaPDF::Type::AcroForm::Field]
10
11
  # classes provide a plethora of convenience methods for working with forms, like
@@ -52,6 +53,7 @@ canvas.text("Text fields", at: [50, 480])
52
53
  canvas.text("Single line", at: [70, 450])
53
54
  tx = form.create_text_field("Single Line", font_size: 16)
54
55
  widget = tx.create_widget(page, Rect: [200, 445, 500, 465])
56
+ widget.background_color("lightyellow").border_style(color: "black", style: [2])
55
57
  tx.field_value = "A sample test string!"
56
58
 
57
59
  canvas.text("Multiline", at: [70, 420])
data/examples/030-pdfa.rb CHANGED
@@ -13,14 +13,6 @@ require 'hexapdf'
13
13
 
14
14
  HexaPDF::Composer.create('pdfa.pdf') do |composer|
15
15
  composer.document.task(:pdfa)
16
- composer.document.config['font.map'] = {
17
- 'Lato' => {
18
- none: '/usr/share/fonts/truetype/lato/Lato-Regular.ttf',
19
- bold: '/usr/share/fonts/truetype/lato/Lato-Bold.ttf',
20
- italic: '/usr/share/fonts/truetype/lato/Lato-Italic.ttf',
21
- bold_italic: '/usr/share/fonts/truetype/lato/Lato-BoldItalic.ttf',
22
- },
23
- }
24
16
 
25
17
  company = {
26
18
  name: 'Sample Corp Limited',
@@ -29,21 +21,21 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
29
21
 
30
22
  # Define all styles
31
23
  composer.styles(
32
- base: {font: 'Lato', font_size: 10, line_spacing: 1.3},
24
+ base: {font: 'Inter', font_size: 10, line_spacing: 1.3},
33
25
  top: {font_size: 8},
34
26
  top_box: {padding: [100, 0, 0], margin: [0, 0, 10], border: {width: [0, 0, 1]}},
35
- header: {font: 'Lato bold', font_size: 20, margin: [50, 0, 20]},
27
+ header: {font: 'Inter bold', font_size: 20, margin: [50, 0, 20]},
36
28
  line_items: {border: {width: 1, color: "eee"}, margin: [20, 0]},
37
29
  line_item_cell: {font_size: 8},
38
30
  footer: {border: {width: [1, 0, 0], color: "darkgrey"}, padding: [5, 0, 0],
39
31
  valign: :bottom},
40
- footer_heading: {font: 'Lato bold', font_size: 8, padding: [0, 0, 8]},
32
+ footer_heading: {font: 'Inter bold', font_size: 8, padding: [0, 0, 8]},
41
33
  footer_text: {font_size: 8, fill_color: "darkgrey"},
42
34
  )
43
35
 
44
36
  # Top part
45
37
  composer.box(:container, style: :top_box) do |container|
46
- container.formatted_text([{text: company[:name], font: 'Lato bold'},
38
+ container.formatted_text([{text: company[:name], font: 'Inter bold'},
47
39
  " - " + company[:address].join(' - ')], style: :top)
48
40
  end
49
41
  composer.text("Mega Client\nSmall Lane 5\n67890 Noonestown", mask_mode: :box)
@@ -52,7 +44,7 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
52
44
  ["Service date:", "2024-02-01"]]
53
45
  composer.table(cells, column_widths: [150, 80], style: {align: :right}) do |args|
54
46
  args[] = {cell: {border: {width: 0}, padding: 2}, text_align: :right}
55
- args[0..-1, 0] = {font: 'Lato bold'}
47
+ args[0..-1, 0] = {font: 'Inter bold'}
56
48
  end
57
49
 
58
50
  # Middle part
@@ -67,10 +59,11 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
67
59
  cells << [nil, nil, nil, "€ #{250 * max * (max + 1) / 2},00"]
68
60
  composer.table(cells, column_widths: [250, 80], style: :line_items) do |args|
69
61
  args[] = {cell: {border: {width: 0}, padding: 8}, style: :line_item_cell}
70
- args[0] = {cell: {background_color: "eee"}, font: "Lato bold"}
62
+ args[0] = {cell: {background_color: "eee"}, font: "Inter bold"}
71
63
  args[-1] = {cell: {background_color: "eee", border: {width: [2, 0, 0]}},
72
- font: "Lato bold"}
73
- args[0..-1, 1..-1] = {text_align: :right}
64
+ font: "Inter bold"}
65
+ args[0..-1, 1..-1] = {text_align: :right, shaping_engine: :harfbuzz,
66
+ font_features: {tnum: true}}
74
67
  end
75
68
 
76
69
  composer.text("Please transfer the total amount via SEPA transfer to the bank " \
@@ -0,0 +1,37 @@
1
+ # # Text Shaping
2
+ #
3
+ # The built-in text shaping functionality is very limited. However, it is
4
+ # possible to switch to a HarfBuzz based implementation (using the
5
+ # `harfbuzz-ruby` gem) that supports many languages and scripts as well as
6
+ # OpenType features. Note that this only works with TrueType fonts!
7
+ #
8
+ # In general it is advised to use TrueType font files and enable HarfBuzz as the
9
+ # result is more correct and visually better. The disadvantage is that the
10
+ # additional processing requirements have a slight performance impact.
11
+ #
12
+ # By using the 'font_features' style property it is possible to selectively
13
+ # enable/disable OpenType features like discretionary ligatures. Note that fonts
14
+ # determine which features are on by default (like 'calt' for the Inter font
15
+ # used in this example).
16
+ #
17
+ # Usage:
18
+ # : `ruby text_shaping.rb`
19
+ #
20
+
21
+ require 'hexapdf'
22
+
23
+ sample_text = <<EOT.tr("\n", " ")
24
+ This sample shows some OpenType features, like contextual alternatives (2*3 and
25
+ -->), discretionary ligatures (difficult?!), fractions (15/157) or compositions
26
+ (5⃝ ×⃞ 3⃝ =⃞ 1⃝5⃝).
27
+ EOT
28
+
29
+ HexaPDF::Composer.create('text_shaping.pdf') do |composer|
30
+ composer.style(:base, font: 'Inter')
31
+ composer.style(:header, font_bold: true, margin: [10, 0, 0])
32
+ composer.text('Without HarfBuzz', style: :header)
33
+ composer.text(sample_text)
34
+ composer.text('With HarfBuzz', style: :header)
35
+ composer.text(sample_text, shaping_engine: :harfbuzz,
36
+ font_features: {dlig: true, frac: true})
37
+ end
@@ -36,6 +36,7 @@
36
36
 
37
37
  require 'hexapdf/font/invalid_glyph'
38
38
  require 'hexapdf/error'
39
+ require 'hexapdf/data_dir'
39
40
 
40
41
  module HexaPDF
41
42
 
@@ -333,6 +334,8 @@ module HexaPDF
333
334
  # [italic] For the italic or oblique variant of the font
334
335
  # [bold_italic] For the bold and italic/oblique variant of the font
335
336
  #
337
+ # The default value registers the bundled Inter font (see the files in data/hexapdf/fonts).
338
+ #
336
339
  # font.on_invalid_glyph::
337
340
  # Callback hook when a character cannot be mapped to a glyph and one or more glyphs from a
338
341
  # different font should be used. Only applies when using high-level text creation facilities.
@@ -563,7 +566,14 @@ module HexaPDF
563
566
  },
564
567
  'font.default' => 'Times',
565
568
  'font.fallback' => ['ZapfDingbats', 'Symbol'],
566
- 'font.map' => {},
569
+ 'font.map' => {
570
+ 'Inter' => {
571
+ none: File.join(HexaPDF.data_dir, 'fonts', 'Inter-Regular.ttf'),
572
+ bold: File.join(HexaPDF.data_dir, 'fonts', 'Inter-Bold.ttf'),
573
+ italic: File.join(HexaPDF.data_dir, 'fonts', 'Inter-Italic.ttf'),
574
+ bold_italic: File.join(HexaPDF.data_dir, 'fonts', 'Inter-BoldItalic.ttf'),
575
+ },
576
+ },
567
577
  'font.on_invalid_glyph' => method(:font_on_invalid_glyph),
568
578
  'font.on_missing_glyph' => proc do |char, font_wrapper|
569
579
  HexaPDF::Font::InvalidGlyph.new(font_wrapper, char)
@@ -786,6 +796,7 @@ module HexaPDF
786
796
  Circle: 'HexaPDF::Type::Annotations::Circle',
787
797
  Polygon: 'HexaPDF::Type::Annotations::Polygon',
788
798
  PolyLine: 'HexaPDF::Type::Annotations::Polyline',
799
+ Ink: 'HexaPDF::Type::Annotations::Ink',
789
800
  XML: 'HexaPDF::Type::Metadata',
790
801
  GTS_PDFX: 'HexaPDF::Type::OutputIntent',
791
802
  GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
@@ -819,6 +830,7 @@ module HexaPDF
819
830
  Circle: 'HexaPDF::Type::Annotations::Circle',
820
831
  Polygon: 'HexaPDF::Type::Annotations::Polygon',
821
832
  PolyLine: 'HexaPDF::Type::Annotations::Polyline',
833
+ Ink: 'HexaPDF::Type::Annotations::Ink',
822
834
  },
823
835
  XXAcroFormField: {
824
836
  Tx: 'HexaPDF::Type::AcroForm::TextField',
@@ -205,6 +205,31 @@ module HexaPDF
205
205
  border_style(color: 0, width: 1)
206
206
  end
207
207
 
208
+ # :call-seq:
209
+ # annotations.create_scribble(page, *points) -> annotation
210
+ #
211
+ # Creates an ink annotation on the given page and returns it.
212
+ #
213
+ # If +points+ (alternating horizontal and vertical coordinates) are given, the path created
214
+ # from them is added to the annotation.
215
+ #
216
+ # The ink annotation uses a black color and a width of 1pt for the stroke style. It can be
217
+ # further styled using the convenience methods on the returned annotation object.
218
+ #
219
+ # Example:
220
+ #
221
+ # #>pdf-small
222
+ # doc.annotations.create_scribble(doc.pages[0], 20, 20, 30, 70, 80, 60, 40, 30).
223
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
224
+ # regenerate_appearance
225
+ #
226
+ # See: Type::Annotations::Ink
227
+ def create_scribble(page, *points)
228
+ annot = create_and_add_to_page(:Ink, page)
229
+ annot.add_path(*points) unless points.empty?
230
+ annot.border_style(color: 0, width: 1)
231
+ end
232
+
208
233
  private
209
234
 
210
235
  # Returns the root of the destinations name tree.
@@ -46,8 +46,8 @@ module HexaPDF
46
46
  # Maximum number of entries in one section.
47
47
  MAX_ENTRIES_IN_SECTION = 100
48
48
 
49
- # Returns a ToUnicode CMap for the given input code to Unicode codepoint mapping which needs
50
- # to be sorted by input codes.
49
+ # Returns a ToUnicode CMap for the given mapping of input codes to Unicode codepoints and/or
50
+ # Strings. The mapping needs to be sorted by input codes.
51
51
  #
52
52
  # Note that the returned CMap always uses a 16-bit input code space!
53
53
  def create_to_unicode_cmap(mapping)
@@ -57,9 +57,13 @@ module HexaPDF
57
57
 
58
58
  result = create_sections("bfchar", chars.size / 2) do |index|
59
59
  index *= 2
60
- sprintf("<%04X>", chars[index]) << "<" <<
61
- ((+'').force_encoding(::Encoding::UTF_16BE) << chars[index + 1]).unpack1('H*') <<
62
- ">\n"
60
+ value = chars[index + 1]
61
+ value = if value.kind_of?(Integer)
62
+ (+'').force_encoding(::Encoding::UTF_16BE) << value
63
+ else
64
+ value.encode(::Encoding::UTF_16BE)
65
+ end
66
+ sprintf("<%04X>", chars[index]) << "<#{value.unpack1('H*')}>\n"
63
67
  end
64
68
 
65
69
  result << create_sections("bfrange", ranges.size / 3) do |index|
@@ -113,7 +117,9 @@ module HexaPDF
113
117
  last_code, last_value = *mapping[0]
114
118
  is_range = false
115
119
  mapping.slice(1..-1).each do |code, value|
116
- if last_code + 1 == code && last_value + 1 == value && code % 256 != 0
120
+ if last_value.kind_of?(String)
121
+ chars << last_code << last_value
122
+ elsif last_code + 1 == code && last_value + 1 == value && code % 256 != 0
117
123
  ranges << last_code << nil << last_value unless is_range
118
124
  is_range = true
119
125
  elsif is_range
@@ -127,10 +133,10 @@ module HexaPDF
127
133
  end
128
134
 
129
135
  # Handle last remaining mapping
130
- if is_range
131
- ranges[-2] = last_code
132
- else
136
+ if last_value.kind_of?(String) || !is_range
133
137
  chars << last_code << last_value
138
+ else
139
+ ranges[-2] = last_code
134
140
  end
135
141
 
136
142
  [chars, ranges]
@@ -184,6 +184,11 @@ module HexaPDF
184
184
  !@subsetter.nil?
185
185
  end
186
186
 
187
+ # The filename of the wrapped TrueType font object if it was loaded from a file.
188
+ def filename
189
+ wrapped_font.io.path
190
+ end
191
+
187
192
  # Returns a Glyph object for the given glyph ID and +str+ pair.
188
193
  #
189
194
  # The optional argument +str+ should be the string representation of the glyph. It is possible
@@ -358,9 +363,7 @@ module HexaPDF
358
363
  def create_to_unicode_cmap(dict, document)
359
364
  stream = HexaPDF::StreamData.new do
360
365
  mapping = @encoded_glyphs.map do |glyph, (_, char_code)|
361
- # Using 0xFFFD as mentioned in Adobe #5411, last line before section 1.5
362
- # TODO: glyph.str assumed to consist of single char, No support for multiple chars
363
- [char_code, glyph.str.ord || 0xFFFD]
366
+ [char_code, glyph.str.length == 1 ? glyph.str.ord : glyph.str]
364
367
  end.sort_by!(&:first)
365
368
  HexaPDF::Font::CMap.create_to_unicode_cmap(mapping)
366
369
  end
@@ -45,6 +45,53 @@ module HexaPDF
45
45
  # needs to be usable by the PDF canvas. See below for details.
46
46
  #
47
47
  #
48
+ # == Available Font Loaders
49
+ #
50
+ # The following font loaders are available:
51
+ #
52
+ # HexaPDF::FontLoader::Standard14::
53
+ # This one makes the standard 14 PDF fonts (Helvetica in variants none, italic, bold and bold
54
+ # italic; Times in variants none, italic, bold and bold italic; Symbol; and ZapfDingbats)
55
+ # available.
56
+ #
57
+ # Usage:
58
+ #
59
+ # canvas.font('Times', variant: :bold)
60
+ #
61
+ # HexaPDF::FontLoader::FromFile::
62
+ # Interprets the font name as filename and tries to load the font from there. In this case a
63
+ # +:variant+ argument cannot be used as the font file is directly specified.
64
+ #
65
+ # Usage:
66
+ #
67
+ # canvas.font('/usr/share/fonts/truetype/hack/Hack-Regular.ttf')
68
+ #
69
+ # HexaPDF::FontLoader::VariantFromName:
70
+ # This one doesn't really load a font itself but makes it possible to append the variant name
71
+ # to the font name. So it resolves e.g. 'Times bold' to the font 'Times' in variant :bold.
72
+ #
73
+ # Usage:
74
+ #
75
+ # canvas.font('Times bold')
76
+ #
77
+ # HexaPDF::FontLoader::FromConfiguration
78
+ # This font loader defers to FromFile when loading the actual fonts. It allows defining font
79
+ # mappings using the configuration option 'font.map' where a font name is mapped to a hash
80
+ # that maps variant names to font file names.
81
+ #
82
+ # Usage:
83
+ #
84
+ # doc.config['font.map'] = {
85
+ # 'Hack' => {
86
+ # none: '/usr/share/fonts/ttf/Hack-Regular.ttf',
87
+ # bold: '/usr/share/fonts/ttf/Hack-Bold.ttf',
88
+ # italic: '/usr/share/fonts/ttf/Hack-Italic.ttf',
89
+ # bold_italic: '/usr/share/fonts/ttf/Hack-BoldItalic.ttf',
90
+ # },
91
+ # }
92
+ # canvas.font('Hack', variant: :italic)
93
+ #
94
+ #
48
95
  # == Implementation of a Font Loader
49
96
  #
50
97
  # Each font loader is a (stateless) object (normally a module) that has to be callable, i.e. it
@@ -163,15 +163,13 @@ module HexaPDF
163
163
  @box_fitter = BoxFitter.new([my_frame])
164
164
  children.each {|box| @box_fitter.fit(box) }
165
165
 
166
- if @box_fitter.success?
166
+ if @box_fitter.success? || (!@box_fitter.fit_results.empty? && @splitable)
167
167
  update_content_width do
168
168
  result = @box_fitter.fit_results.max_by {|r| r.mask.x + r.mask.width }
169
169
  children.empty? ? 0 : result.mask.x + result.mask.width - my_frame.left
170
170
  end
171
171
  update_content_height { @box_fitter.content_heights.max }
172
- fit_result.success!
173
- elsif !@box_fitter.fit_results.empty? && @splitable
174
- fit_result.overflow!
172
+ @box_fitter.success? ? fit_result.success! : fit_result.overflow!
175
173
  end
176
174
  end
177
175
 
@@ -811,18 +811,76 @@ module HexaPDF
811
811
  # font_features(features = nil)
812
812
  #
813
813
  # The font features (e.g. kerning, ligatures, ...) that should be applied by the shaping
814
- # engine, defaults to {} (i.e. no font features are applied).
814
+ # engine, defaults to the empty hash {} (i.e. no font features are applied).
815
815
  #
816
- # Each feature to be applied is indicated by a key with a truthy value.
816
+ # Each feature can either be activated using `true` as value, deactivated using `false` as
817
+ # value or set to a specific mode using an integer. What is actually supported depends on the
818
+ # used shaping engine.
817
819
  #
818
- # See: HexaPDF::Layout::TextShaper#shape_text for available features.
820
+ # See: #shaping_engine
819
821
  #
820
822
  # Examples:
821
823
  #
822
824
  # #>pdf-composer100
823
825
  # composer.style(:base, font: ["Times", custom_encoding: true], font_size: 30)
824
826
  # composer.text("Test flight")
825
- # composer.text("Test flight", font_features: {kern: true, liga: true})
827
+ # composer.text("Test flight", font_features: {kern: true, liga: false})
828
+
829
+ ##
830
+ # :method: font_script
831
+ # :call-seq:
832
+ # font_script(script = nil)
833
+ #
834
+ # The script in which the text is written, defaults to +nil+.
835
+ #
836
+ # This is used by the shaping engine to select the correct shaping implementation. If not set,
837
+ # the script is guessed from the text.
838
+ #
839
+ # See: #shaping_engine
840
+
841
+ ##
842
+ # :method: language
843
+ # :call-seq:
844
+ # language(lang = nil)
845
+ #
846
+ # The language in which the text is written, defaults to +nil+.
847
+ #
848
+ # This is used, for example, by the shaping engine to correctly shape the text.
849
+ #
850
+ # See: #shaping_engine
851
+
852
+ ##
853
+ # :method: direction
854
+ # :call-seq:
855
+ # direction(dir = nil)
856
+ #
857
+ # The direction of text, defaults to +:ltr+ (possible values are +:ltr+ and +:rtl+).
858
+ #
859
+ # This is used by the shaping engine to correctly shape the text.
860
+ #
861
+ # See: #shaping_engine
862
+
863
+ ##
864
+ # :method: shaping_engine
865
+ # :call-seq:
866
+ # shaping_engine(engine = nil)
867
+ #
868
+ # The shaping engine that should be used, defaults to +:internal+. The other possible value is
869
+ # +:harfbuzz+.
870
+ #
871
+ # When set to +:harfbuzz+, the Rubygem +harfbuzz-ruby+ needs to be installed. If it is not
872
+ # available, HexaPDF will raise an error.
873
+ #
874
+ # See: HexaPDF::Layout::TextShaper for details.
875
+ #
876
+ # Examples:
877
+ #
878
+ # #>pdf-composer100
879
+ # composer.style(:base, font: 'Inter')
880
+ # composer.text("Incoming WAVE!")
881
+ # composer.formatted_text(["Incoming WAVE!",
882
+ # {text: " Take Cover!", font_features: {ss06: true}}],
883
+ # shaping_engine: :harfbuzz)
826
884
 
827
885
  ##
828
886
  # :method: text_rendering_mode
@@ -1541,6 +1599,10 @@ module HexaPDF
1541
1599
  [:horizontal_scaling, 100],
1542
1600
  [:text_rise, 0],
1543
1601
  [:font_features, {}],
1602
+ [:font_script, nil],
1603
+ [:language, nil],
1604
+ [:direction, :ltr, {valid_values: [:ltr, :rtl]}],
1605
+ [:shaping_engine, :internal, {valid_values: [:internal, :harfbuzz]}],
1544
1606
  [:text_rendering_mode, "Content::TextRenderingMode::FILL",
1545
1607
  {setter: "Content::TextRenderingMode.normalize(value)"}],
1546
1608
  [:subscript, false,