hexapdf 0.35.1 → 0.37.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.
@@ -0,0 +1,192 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'stringio'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Document::Metadata do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @doc.trailer.info[:Title] = 'Title'
11
+ @metadata = @doc.metadata
12
+ end
13
+
14
+ it "parses the info dictionary on creation" do
15
+ assert_equal('Title', @metadata.title)
16
+ @doc.trailer.info[:Trapped] = :Unknown
17
+ assert_nil(HexaPDF::Document::Metadata.new(@doc).trapped)
18
+ @doc.trailer.info[:Trapped] = :True
19
+ assert_equal(true, HexaPDF::Document::Metadata.new(@doc).trapped)
20
+ @doc.trailer.info[:Trapped] = :False
21
+ assert_equal(false, HexaPDF::Document::Metadata.new(@doc).trapped)
22
+ end
23
+
24
+ describe "default_language" do
25
+ it "use the document's language as default" do
26
+ @doc.catalog[:Lang] = 'de'
27
+ assert_equal("de", HexaPDF::Document::Metadata.new(@doc).default_language)
28
+ end
29
+
30
+ it "falls back to English if the document doesn't have a default language set" do
31
+ assert_equal('en', @metadata.default_language)
32
+ end
33
+
34
+ it "allows changing the default language" do
35
+ @metadata.default_language('de')
36
+ assert_equal('de', @metadata.default_language)
37
+ end
38
+ end
39
+
40
+ it "enables writing the info dict by default" do
41
+ assert(@metadata.write_info_dict?)
42
+ end
43
+
44
+ it "allows setting whether the info dict is written" do
45
+ @metadata.write_info_dict(false)
46
+ refute(@metadata.write_info_dict?)
47
+ end
48
+
49
+ it "enables writing the metadata stream by default" do
50
+ assert(@metadata.write_metadata_stream?)
51
+ end
52
+
53
+ it "allows setting whether the metadata stream is written" do
54
+ @metadata.write_metadata_stream(false)
55
+ refute(@metadata.write_metadata_stream?)
56
+ end
57
+
58
+ it "resolves namespace URI via a prefix" do
59
+ assert_equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#', @metadata.namespace('rdf'))
60
+ end
61
+
62
+ it "allows registering prefixes for namespaces" do
63
+ err = assert_raises(HexaPDF::Error) { @metadata.namespace('hexa') }
64
+ assert_match(/prefix.*hexa.*not registered/, err.message)
65
+ @metadata.register_namespace('hexa', 'hexa:')
66
+ assert_equal('hexa:', @metadata.namespace('hexa'))
67
+ end
68
+
69
+ it "allows registering property types" do
70
+ @metadata.register_property_type('dc', 'title', 'Boolean')
71
+ assert_equal('Boolean', @metadata.instance_variable_get(:@properties)[@metadata.namespace('dc')]['title'])
72
+ end
73
+
74
+ it "allows reading and setting properties" do
75
+ assert_equal('Title', @metadata.property('dc', 'title'))
76
+ @metadata.property('dc', 'title', 'another')
77
+ assert_equal('another', @metadata.property('dc', 'title'))
78
+ @metadata.property('dc', 'title', nil)
79
+ assert_nil(@metadata.property('dc', 'title'))
80
+ refute(@metadata.instance_variable_get(:@metadata)[@metadata.namespace('dc')].key?('title'))
81
+ end
82
+
83
+ it "allows reading and setting all info dictionary properties" do
84
+ [['title', 'dc', 'title'], ['author', 'dc', 'creator'], ['subject', 'dc', 'description'],
85
+ ['keywords', 'pdf', 'Keywords'], ['creator', 'xmp', 'CreatorTool'],
86
+ ['producer', 'pdf', 'Producer'], ['creation_date', 'xmp', 'CreateDate'],
87
+ ['modification_date', 'xmp', 'ModifyDate'], ['trapped', 'pdf', 'Trapped']].each do |name, ns, property|
88
+ @metadata.property(ns, property, 'value')
89
+ assert_equal('value', @metadata.send(name), name)
90
+ @metadata.send(name, 'modified')
91
+ assert_equal('modified', @metadata.property(ns, property), name)
92
+ end
93
+ end
94
+
95
+ describe "metadata writing" do
96
+ before do
97
+ @time = Time.now.floor
98
+ @metadata.title('Title')
99
+ @metadata.author('Author')
100
+ @metadata.subject('Subject')
101
+ @metadata.keywords('Keywords')
102
+ @metadata.creator('Creator')
103
+ @metadata.producer('Producer')
104
+ @metadata.creation_date(@time)
105
+ @metadata.modification_date(@time)
106
+ @metadata.trapped(true)
107
+ end
108
+
109
+ it "writes the info dictionary properties" do
110
+ info = @doc.trailer.info
111
+ @doc.write(StringIO.new, update_fields: false)
112
+ assert_equal('Title', info[:Title])
113
+ assert_equal('Author', info[:Author])
114
+ assert_equal('Subject', info[:Subject])
115
+ assert_equal('Keywords', info[:Keywords])
116
+ assert_equal('Creator', info[:Creator])
117
+ assert_match(/HexaPDF/, info[:Producer])
118
+ assert_same(@time, info[:CreationDate])
119
+ assert_same(@time, info[:ModDate])
120
+ assert_equal(:True, info[:Trapped])
121
+ end
122
+
123
+ it "uses a correctly updated modification date if set so by Document#write" do
124
+ info = @doc.trailer.info
125
+ sleep(0.1)
126
+ @doc.write(StringIO.new)
127
+ assert_same(@time, info[:CreationDate])
128
+ refute_same(@time, info[:ModDate])
129
+ assert(@time < info[:ModDate])
130
+ end
131
+
132
+ it "correctly handles array values for title, author, and subject for info dictionary" do
133
+ @metadata.title(['Title', 'Another'])
134
+ @metadata.author(['Author', 'Author2'])
135
+ @metadata.subject(['Subject', 'Another'])
136
+ @doc.write(StringIO.new)
137
+ info = @doc.trailer.info
138
+ assert_equal('Title', info[:Title])
139
+ assert_equal('Author, Author2', info[:Author])
140
+ assert_equal('Subject', info[:Subject])
141
+ end
142
+
143
+ it "writes the XMP metadata" do
144
+ title = HexaPDF::Document::Metadata::LocalizedString.new('Der Titel')
145
+ title.language = 'de'
146
+ @metadata.title(['Title', title])
147
+ @metadata.author(['Author 1', 'Author 2'])
148
+ @metadata.register_property_type('dc', 'other', 'URI')
149
+ @metadata.property('dc', 'other', 'https://test.org/example')
150
+ @doc.write(StringIO.new, update_fields: false)
151
+ metadata = <<~XMP
152
+ <?xpacket begin="" id=""?>
153
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
154
+ <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
155
+ <dc:title><rdf:Alt>
156
+ <rdf:li xml:lang="en">Title</rdf:li>
157
+ <rdf:li xml:lang="de">Der Titel</rdf:li>
158
+ </rdf:Alt></dc:title>
159
+ <dc:creator><rdf:Seq>
160
+ <rdf:li>Author 1</rdf:li>
161
+ <rdf:li>Author 2</rdf:li>
162
+ </rdf:Seq></dc:creator>
163
+ <dc:description><rdf:Alt>
164
+ <rdf:li xml:lang="en">Subject</rdf:li>
165
+ </rdf:Alt></dc:description>
166
+ <dc:other rdf:resource="https://test.org/example" />
167
+ </rdf:Description>
168
+ <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
169
+ <pdf:Keywords>Keywords</pdf:Keywords>
170
+ <pdf:Producer>Producer</pdf:Producer>
171
+ <pdf:Trapped>True</pdf:Trapped>
172
+ </rdf:Description>
173
+ <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
174
+ <xmp:CreatorTool>Creator</xmp:CreatorTool>
175
+ <xmp:CreateDate>#{@metadata.send(:xmp_date, @time)}</xmp:CreateDate>
176
+ <xmp:ModifyDate>#{@metadata.send(:xmp_date, @time)}</xmp:ModifyDate>
177
+ </rdf:Description>
178
+ </rdf:RDF>
179
+ <?xpacket end="r"?>
180
+ XMP
181
+ assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
182
+ end
183
+
184
+ it "respects the write settings for info dictionary and metadata stream" do
185
+ @metadata.write_info_dict(false)
186
+ @metadata.write_metadata_stream(false)
187
+ @doc.write(StringIO.new)
188
+ assert_nil(@doc.trailer.info[:Author])
189
+ refute(@doc.catalog.key?(:Metadata))
190
+ end
191
+ end
192
+ end
@@ -5,6 +5,12 @@ require 'hexapdf/document'
5
5
  require 'hexapdf/layout/box'
6
6
 
7
7
  describe HexaPDF::Layout::Box do
8
+ before do
9
+ @frame = Object.new
10
+ def @frame.x; 0; end
11
+ def @frame.y; 100; end
12
+ end
13
+
8
14
  def create_box(**args, &block)
9
15
  HexaPDF::Layout::Box.new(**args, &block)
10
16
  end
@@ -70,15 +76,13 @@ describe HexaPDF::Layout::Box do
70
76
  end
71
77
 
72
78
  describe "fit" do
73
- before do
74
- @frame = Object.new
75
- end
76
-
77
79
  it "fits a fixed sized box" do
78
- box = create_box(width: 50, height: 50)
80
+ box = create_box(width: 50, height: 50, style: {padding: 5})
79
81
  assert(box.fit(100, 100, @frame))
80
82
  assert_equal(50, box.width)
81
83
  assert_equal(50, box.height)
84
+ assert_equal(5, box.instance_variable_get(:@fit_x))
85
+ assert_equal(55, box.instance_variable_get(:@fit_y))
82
86
  end
83
87
 
84
88
  it "uses the maximum available width" do
@@ -106,49 +110,71 @@ describe HexaPDF::Layout::Box do
106
110
  box = create_box(width: 101)
107
111
  refute(box.fit(100, 100, @frame))
108
112
  end
113
+
114
+ it "can use the #content_width/#content_height helper methods" do
115
+ box = create_box
116
+ box.define_singleton_method(:fit_content) do |aw, ah, frame|
117
+ update_content_width { 10 }
118
+ update_content_height { 20 }
119
+ true
120
+ end
121
+ assert(box.fit(100, 100, @frame))
122
+ assert_equal(10, box.width)
123
+ assert_equal(20, box.height)
124
+
125
+ box = create_box(width: 30, height: 50)
126
+ box.define_singleton_method(:fit_content) do |aw, ah, frame|
127
+ update_content_width { 10 }
128
+ update_content_height { 20 }
129
+ true
130
+ end
131
+ assert(box.fit(100, 100, @frame))
132
+ assert_equal(30, box.width)
133
+ assert_equal(50, box.height)
134
+ end
109
135
  end
110
136
 
111
137
  describe "split" do
112
138
  before do
113
139
  @box = create_box(width: 100, height: 100)
114
- @box.fit(100, 100, nil)
140
+ @box.fit(100, 100, @frame)
115
141
  end
116
142
 
117
143
  it "doesn't need to be split if it completely fits" do
118
- assert_equal([@box, nil], @box.split(100, 100, nil))
144
+ assert_equal([@box, nil], @box.split(100, 100, @frame))
119
145
  end
120
146
 
121
147
  it "can't be split if it doesn't (completely) fit and its width is greater than the available width" do
122
148
  @box.fit(90, 100, nil)
123
- assert_equal([nil, @box], @box.split(50, 150, nil))
149
+ assert_equal([nil, @box], @box.split(50, 150, @frame))
124
150
  end
125
151
 
126
152
  it "can't be split if it doesn't (completely) fit and its height is greater than the available height" do
127
153
  @box.fit(90, 100, nil)
128
- assert_equal([nil, @box], @box.split(150, 50, nil))
154
+ assert_equal([nil, @box], @box.split(150, 50, @frame))
129
155
  end
130
156
 
131
157
  it "can't be split if it doesn't (completely) fit and its content width is zero" do
132
158
  box = create_box(width: 0, height: 100)
133
- assert_equal([nil, box], box.split(150, 150, nil))
159
+ assert_equal([nil, box], box.split(150, 150, @frame))
134
160
  end
135
161
 
136
162
  it "can't be split if it doesn't (completely) fit and its content height is zero" do
137
163
  box = create_box(width: 100, height: 0)
138
- assert_equal([nil, box], box.split(150, 150, nil))
164
+ assert_equal([nil, box], box.split(150, 150, @frame))
139
165
  end
140
166
 
141
167
  it "can't be split if it doesn't (completely) fit as the default implementation " \
142
168
  "knows nothing about the content" do
143
169
  @box.style.position = :flow # make sure we would generally be splitable
144
170
  @box.fit(90, 100, nil)
145
- assert_equal([nil, @box], @box.split(150, 150, nil))
171
+ assert_equal([nil, @box], @box.split(150, 150, @frame))
146
172
  end
147
173
  end
148
174
 
149
175
  it "can create a cloned box for splitting" do
150
176
  box = create_box
151
- box.fit(100, 100, nil)
177
+ box.fit(100, 100, @frame)
152
178
  cloned_box = box.send(:create_split_box)
153
179
  assert(cloned_box.split_box?)
154
180
  refute(cloned_box.instance_variable_get(:@fit_successful))
@@ -0,0 +1,84 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/layout/container_box'
6
+
7
+ describe HexaPDF::Layout::ContainerBox do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
11
+ end
12
+
13
+ def create_box(children, **kwargs)
14
+ HexaPDF::Layout::ContainerBox.new(children: Array(children), **kwargs)
15
+ end
16
+
17
+ def check_box(box, width, height, fit_pos = nil)
18
+ assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box didn't fit")
19
+ assert_equal(width, box.width, "box width")
20
+ assert_equal(height, box.height, "box height")
21
+ if fit_pos
22
+ box_fitter = box.instance_variable_get(:@box_fitter)
23
+ assert_equal(fit_pos.size, box_fitter.fit_results.size)
24
+ fit_pos.each_with_index do |(x, y), index|
25
+ assert_equal(x, box_fitter.fit_results[index].x, "result[#{index}].x")
26
+ assert_equal(y, box_fitter.fit_results[index].y, "result[#{index}].y")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "empty?" do
32
+ it "is empty if nothing is fit yet" do
33
+ assert(create_box([]).empty?)
34
+ end
35
+
36
+ it "is empty if no box fits" do
37
+ box = create_box(@doc.layout.box(width: 110))
38
+ box.fit(@frame.available_width, @frame.available_height, @frame)
39
+ assert(box.empty?)
40
+ end
41
+
42
+ it "is not empty if at least one box fits" do
43
+ box = create_box(@doc.layout.box(width: 50, height: 20))
44
+ check_box(box, 100, 20)
45
+ refute(box.empty?)
46
+ end
47
+ end
48
+
49
+ describe "fit_content" do
50
+ it "fits the children according to their mask_mode, valign, and align style properties" do
51
+ box = create_box([@doc.layout.box(height: 20),
52
+ @doc.layout.box(height: 20, style: {valign: :bottom, mask_mode: :fill_horizontal}),
53
+ @doc.layout.box(width: 20, style: {align: :right, mask_mode: :fill_vertical})])
54
+ check_box(box, 100, 100, [[0, 80], [0, 0], [80, 20]])
55
+ end
56
+
57
+ it "respects the initially set width/height as well as border/padding" do
58
+ box = create_box(@doc.layout.box(height: 20), height: 50, width: 40,
59
+ style: {padding: 2, border: {width: 3}})
60
+ check_box(box, 40, 50, [[5, 75]])
61
+ end
62
+ end
63
+
64
+ describe "draw_content" do
65
+ before do
66
+ @canvas = @doc.pages.add.canvas
67
+ end
68
+
69
+ it "draws the result onto the canvas" do
70
+ child_box = @doc.layout.box(height: 20) do |canvas, b|
71
+ canvas.line(0, 0, b.content_width, b.content_height).end_path
72
+ end
73
+ box = create_box(child_box)
74
+ box.fit(100, 100, @frame)
75
+ box.draw(@canvas, 0, 50)
76
+ assert_operators(@canvas.contents, [[:save_graphics_state],
77
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 50]],
78
+ [:move_to, [0, 0]],
79
+ [:line_to, [100, 20]],
80
+ [:end_path],
81
+ [:restore_graphics_state]])
82
+ end
83
+ end
84
+ end
@@ -13,10 +13,11 @@ describe HexaPDF::Layout::Frame::FitResult do
13
13
  result = HexaPDF::Layout::Frame::FitResult.new(box)
14
14
  result.mask = Geom2D::Rectangle(0, 0, 20, 20)
15
15
  result.x = result.y = 0
16
- result.draw(canvas)
16
+ result.draw(canvas, dx: 10, dy: 15)
17
17
  assert_equal(<<~CONTENTS, canvas.contents)
18
18
  /OC /P1 BDC
19
19
  q
20
+ 1 0 0 1 10 15 cm
20
21
  0.0 0.501961 0.0 rg
21
22
  0.0 0.392157 0.0 RG
22
23
  /GS1 gs
@@ -25,7 +26,7 @@ describe HexaPDF::Layout::Frame::FitResult do
25
26
  Q
26
27
  EMC
27
28
  q
28
- 1 0 0 1 0 0 cm
29
+ 1 0 0 1 10 15 cm
29
30
  Q
30
31
  CONTENTS
31
32
  ocg = doc.optional_content.ocgs.first
@@ -18,6 +18,12 @@ describe HexaPDF::Filter do
18
18
  assert_equal(@str, collector(fib))
19
19
  assert_equal('', collector(fib))
20
20
  end
21
+
22
+ it "returns the correct length of the fiber" do
23
+ str = "\u{FEFF}Öl"
24
+ fib = @obj.source_from_proc { str }
25
+ assert_equal(6, fib.length)
26
+ end
21
27
  end
22
28
 
23
29
  describe "source_from_string" do
@@ -30,6 +36,12 @@ describe HexaPDF::Filter do
30
36
  it "returns the whole string" do
31
37
  assert_equal(@str, collector(@obj.source_from_string(@str)))
32
38
  end
39
+
40
+ it "returns the correct size of the fiber" do
41
+ str = "\u{FEFF}Öl"
42
+ fib = @obj.source_from_string(str)
43
+ assert_equal(6, fib.length)
44
+ end
33
45
  end
34
46
 
35
47
  describe "source_from_io" do
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.35.1)>>
43
+ <</Producer(HexaPDF version 0.37.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.35.1)>>
75
+ <</Producer(HexaPDF version 0.37.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -116,9 +116,17 @@ describe HexaPDF::Type::FontSimple do
116
116
  assert_equal(" ", @font.to_utf8(32))
117
117
  end
118
118
 
119
+ it "swallows errors during retrieving the font's encoding" do
120
+ @font.delete(:ToUnicode)
121
+ @font.delete(:Encoding)
122
+ err = assert_raises(HexaPDF::Error) { @font.to_utf8(32) }
123
+ assert_match(/No Unicode mapping/, err.message)
124
+ end
125
+
119
126
  it "calls the configured proc if no correct mapping could be found" do
120
127
  @font.delete(:ToUnicode)
121
- assert_raises(HexaPDF::Error) { @font.to_utf8(0) }
128
+ err = assert_raises(HexaPDF::Error) { @font.to_utf8(0) }
129
+ assert_match(/No Unicode mapping/, err.message)
122
130
  end
123
131
  end
124
132
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.1
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -358,6 +358,7 @@ files:
358
358
  - lib/hexapdf/document/fonts.rb
359
359
  - lib/hexapdf/document/images.rb
360
360
  - lib/hexapdf/document/layout.rb
361
+ - lib/hexapdf/document/metadata.rb
361
362
  - lib/hexapdf/document/pages.rb
362
363
  - lib/hexapdf/encryption.rb
363
364
  - lib/hexapdf/encryption/aes.rb
@@ -434,6 +435,7 @@ files:
434
435
  - lib/hexapdf/layout/box.rb
435
436
  - lib/hexapdf/layout/box_fitter.rb
436
437
  - lib/hexapdf/layout/column_box.rb
438
+ - lib/hexapdf/layout/container_box.rb
437
439
  - lib/hexapdf/layout/frame.rb
438
440
  - lib/hexapdf/layout/image_box.rb
439
441
  - lib/hexapdf/layout/inline_box.rb
@@ -504,6 +506,7 @@ files:
504
506
  - lib/hexapdf/type/image.rb
505
507
  - lib/hexapdf/type/info.rb
506
508
  - lib/hexapdf/type/mark_information.rb
509
+ - lib/hexapdf/type/metadata.rb
507
510
  - lib/hexapdf/type/names.rb
508
511
  - lib/hexapdf/type/object_stream.rb
509
512
  - lib/hexapdf/type/optional_content_configuration.rb
@@ -630,6 +633,7 @@ files:
630
633
  - test/hexapdf/document/test_fonts.rb
631
634
  - test/hexapdf/document/test_images.rb
632
635
  - test/hexapdf/document/test_layout.rb
636
+ - test/hexapdf/document/test_metadata.rb
633
637
  - test/hexapdf/document/test_pages.rb
634
638
  - test/hexapdf/encryption/common.rb
635
639
  - test/hexapdf/encryption/test_aes.rb
@@ -695,6 +699,7 @@ files:
695
699
  - test/hexapdf/layout/test_box.rb
696
700
  - test/hexapdf/layout/test_box_fitter.rb
697
701
  - test/hexapdf/layout/test_column_box.rb
702
+ - test/hexapdf/layout/test_container_box.rb
698
703
  - test/hexapdf/layout/test_frame.rb
699
704
  - test/hexapdf/layout/test_image_box.rb
700
705
  - test/hexapdf/layout/test_inline_box.rb
@@ -794,7 +799,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
794
799
  requirements:
795
800
  - - ">="
796
801
  - !ruby/object:Gem::Version
797
- version: '2.6'
802
+ version: '2.7'
798
803
  required_rubygems_version: !ruby/object:Gem::Requirement
799
804
  requirements:
800
805
  - - ">="