hexapdf 0.33.0 → 0.34.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/examples/026-optional_content.rb +55 -0
  4. data/examples/027-composer_optional_content.rb +83 -0
  5. data/lib/hexapdf/cli/command.rb +7 -1
  6. data/lib/hexapdf/cli/fonts.rb +1 -1
  7. data/lib/hexapdf/cli/inspect.rb +2 -4
  8. data/lib/hexapdf/composer.rb +3 -2
  9. data/lib/hexapdf/configuration.rb +21 -1
  10. data/lib/hexapdf/content/canvas.rb +52 -0
  11. data/lib/hexapdf/content/operator.rb +3 -1
  12. data/lib/hexapdf/dictionary.rb +1 -0
  13. data/lib/hexapdf/dictionary_fields.rb +1 -2
  14. data/lib/hexapdf/digital_signature/verification_result.rb +1 -2
  15. data/lib/hexapdf/document/layout.rb +3 -0
  16. data/lib/hexapdf/document/pages.rb +1 -1
  17. data/lib/hexapdf/document.rb +7 -0
  18. data/lib/hexapdf/encryption/ruby_aes.rb +10 -20
  19. data/lib/hexapdf/layout/box.rb +23 -3
  20. data/lib/hexapdf/layout/column_box.rb +2 -1
  21. data/lib/hexapdf/layout/frame.rb +23 -6
  22. data/lib/hexapdf/layout/inline_box.rb +20 -9
  23. data/lib/hexapdf/layout/list_box.rb +34 -20
  24. data/lib/hexapdf/layout/page_style.rb +2 -1
  25. data/lib/hexapdf/layout/style.rb +46 -6
  26. data/lib/hexapdf/layout/table_box.rb +9 -7
  27. data/lib/hexapdf/layout/text_box.rb +9 -2
  28. data/lib/hexapdf/layout/text_fragment.rb +28 -2
  29. data/lib/hexapdf/layout/text_layouter.rb +21 -5
  30. data/lib/hexapdf/stream.rb +1 -2
  31. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  32. data/lib/hexapdf/type/actions.rb +1 -0
  33. data/lib/hexapdf/type/annotations/text.rb +1 -2
  34. data/lib/hexapdf/type/catalog.rb +10 -1
  35. data/lib/hexapdf/type/cid_font.rb +15 -1
  36. data/lib/hexapdf/type/form.rb +75 -5
  37. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  38. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  39. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  40. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  41. data/lib/hexapdf/type/page.rb +40 -16
  42. data/lib/hexapdf/type/page_label.rb +4 -8
  43. data/lib/hexapdf/type.rb +4 -0
  44. data/lib/hexapdf/utils/pdf_doc_encoding.rb +0 -1
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/test/hexapdf/content/test_canvas.rb +49 -0
  47. data/test/hexapdf/content/test_operator.rb +3 -1
  48. data/test/hexapdf/document/test_layout.rb +7 -2
  49. data/test/hexapdf/document/test_pages.rb +6 -6
  50. data/test/hexapdf/layout/test_box.rb +13 -4
  51. data/test/hexapdf/layout/test_frame.rb +13 -1
  52. data/test/hexapdf/layout/test_inline_box.rb +17 -8
  53. data/test/hexapdf/layout/test_list_box.rb +48 -31
  54. data/test/hexapdf/layout/test_style.rb +10 -0
  55. data/test/hexapdf/layout/test_table_box.rb +32 -26
  56. data/test/hexapdf/layout/test_text_box.rb +8 -0
  57. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  58. data/test/hexapdf/layout/test_text_layouter.rb +32 -5
  59. data/test/hexapdf/test_composer.rb +30 -0
  60. data/test/hexapdf/test_dictionary.rb +10 -0
  61. data/test/hexapdf/test_document.rb +4 -0
  62. data/test/hexapdf/test_writer.rb +3 -3
  63. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  64. data/test/hexapdf/type/test_catalog.rb +11 -0
  65. data/test/hexapdf/type/test_form.rb +119 -0
  66. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  67. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  68. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  69. data/test/hexapdf/type/test_page.rb +7 -5
  70. metadata +14 -3
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/actions/set_ocg_state'
6
+
7
+ describe HexaPDF::Type::Actions::SetOCGState do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @action = HexaPDF::Type::Actions::SetOCGState.new({}, document: @doc)
11
+ @ocg = @doc.optional_content.add_ocg('Test')
12
+ end
13
+
14
+ describe "add_state_change" do
15
+ it "allows using Ruby-esque and PDF type names for the state change type" do
16
+ @action.add_state_change(:on, @ocg)
17
+ @action.add_state_change(:ON, @ocg)
18
+ @action.add_state_change(:off, @ocg)
19
+ @action.add_state_change(:OFF, @ocg)
20
+ @action.add_state_change(:toggle, @ocg)
21
+ @action.add_state_change(:Toggle, @ocg)
22
+ assert_equal([:ON, @ocg, :ON, @ocg, :OFF, @ocg, :OFF, @ocg, :Toggle, @ocg, :Toggle, @ocg],
23
+ @action[:State].value)
24
+ end
25
+
26
+ it "allows specifying more than one OCG" do
27
+ @action.add_state_change(:on, [@ocg, @doc.optional_content.add_ocg('Test2')])
28
+ assert_equal([:ON, @ocg, @doc.optional_content.ocg('Test2')], @action[:State].value)
29
+ end
30
+
31
+ it "raises an error if the provide state change type is invalid" do
32
+ assert_raises(ArgumentError) { @action.add_state_change(:unknown, nil) }
33
+ end
34
+
35
+ it "raises an error if an OCG specified via a string does not exist" do
36
+ error = assert_raises(HexaPDF::Error) { @action.add_state_change(:on, "Unknown") }
37
+ assert_match(/Invalid OCG.*Unknown.*specified/, error.message)
38
+ end
39
+ end
40
+ end
@@ -39,6 +39,17 @@ describe HexaPDF::Type::Catalog do
39
39
  assert_same(outline, @catalog.outline)
40
40
  end
41
41
 
42
+ it "uses or creates the optional content properties dictionary on access" do
43
+ @catalog[:OCProperties] = hash = {}
44
+ assert_equal(:XXOCProperties, @catalog.optional_content.type)
45
+ assert_same(hash, @catalog.optional_content.value)
46
+
47
+ @catalog.delete(:OCProperties)
48
+ oc = @catalog.optional_content
49
+ assert_equal([], oc[:OCGs])
50
+ assert_equal(:XXOCConfiguration, oc[:D].type)
51
+ end
52
+
42
53
  describe "acro_form" do
43
54
  it "returns an existing form object" do
44
55
  @catalog[:AcroForm] = :test
@@ -1,6 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
+ require 'stringio'
5
+ require 'tempfile'
4
6
  require 'hexapdf/document'
5
7
  require 'hexapdf/type/form'
6
8
 
@@ -85,6 +87,18 @@ describe HexaPDF::Type::Form do
85
87
  @form.process_contents(processor, original_resources: resources)
86
88
  assert_same(resources, processor.resources)
87
89
  end
90
+
91
+ it "uses the referenced content in case of a Reference XObject" do
92
+ @form[:Ref] = @doc.add({F: {}})
93
+ io = StringIO.new
94
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
95
+ @form[:Ref][:F].embed(io, name: 'test')
96
+ @form[:Ref][:Page] = 0
97
+
98
+ processor = HexaPDF::TestUtils::OperatorRecorder.new
99
+ @form.process_contents(processor)
100
+ assert_equal([[:set_line_width, [5]]], processor.recorded_ops)
101
+ end
88
102
  end
89
103
 
90
104
  describe "canvas" do
@@ -116,4 +130,109 @@ describe HexaPDF::Type::Form do
116
130
  assert_raises(HexaPDF::Error) { @form.canvas }
117
131
  end
118
132
  end
133
+
134
+ describe "reference_xobject?" do
135
+ it "returns true if the form is a reference XObject" do
136
+ refute(@form.reference_xobject?)
137
+ @form[:Ref] = {}
138
+ assert(@form.reference_xobject?)
139
+ end
140
+ end
141
+
142
+ describe "referenced_content" do
143
+ before do
144
+ @form[:BBox] = [10, 10, 110, 60]
145
+ @form[:Matrix] = [1, 0, 0, 1, 10, 20]
146
+ @form[:Ref] = @doc.add({F: {}})
147
+ @ref = @form[:Ref]
148
+ end
149
+
150
+ it "returns a Form XObject with the imported page from an embedded file" do
151
+ io = StringIO.new
152
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
153
+ @ref[:F].embed(io, name: 'test.pdf')
154
+ @ref[:Page] = 0
155
+
156
+ ref_form = @form.referenced_content
157
+ refute_nil(ref_form)
158
+ assert_equal([10, 10, 110, 60], ref_form[:BBox].value)
159
+ assert_equal([1, 0, 0, 1, 10, 20], ref_form[:Matrix].value)
160
+ assert_equal("5 w\n", ref_form.contents)
161
+ end
162
+
163
+ it "returns a Form XObject with the imported page from an external file" do
164
+ file = Tempfile.new('hexapdf')
165
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(file.path)
166
+ @ref[:F].path = file.path
167
+ @ref[:Page] = 0
168
+ assert_equal("5 w\n", @form.referenced_content.contents)
169
+ end
170
+
171
+ it "also works with a page label" do
172
+ file = Tempfile.new('hexapdf')
173
+ HexaPDF::Document.new.tap do |d|
174
+ d.pages.add
175
+ d.pages.add
176
+ d.pages.add.canvas.line_width(5)
177
+ d.pages.add
178
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test', start_number: 4)
179
+ end.write(file.path)
180
+ @ref[:F].path = file.path
181
+ @ref[:Page] = 'Test5'
182
+ assert_equal("5 w\n", @form.referenced_content.contents)
183
+ end
184
+
185
+ it "flattens printable annotations into the page's content stream" do
186
+ io = StringIO.new
187
+ HexaPDF::Document.new.tap do |d|
188
+ d.pages.add.canvas.line_width(5)
189
+ tf = d.acro_form(create: true).create_text_field('text')
190
+ widget = tf.create_widget(d.pages[0], Rect: [10, 10, 30, 30])
191
+ widget.border_style(color: "black")
192
+ widget = tf.create_widget(d.pages[0], Rect: [40, 10, 70, 30])
193
+ widget.border_style(color: "red")
194
+ tf.field_value = 't'
195
+ widget.unflag(:print)
196
+ end.write(io)
197
+ @ref[:F].embed(io, name: 'Test')
198
+ @ref[:Page] = 0
199
+ assert_equal(" q Q q 5 w\n Q q q\n1.0 0 0 1.0 10.0 10.0 cm\n/XO1 Do\nQ\n Q ",
200
+ @form.referenced_content.contents)
201
+ end
202
+
203
+ it "returns nil if the form is not a reference XObject" do
204
+ @form.delete(:Ref)
205
+ assert_nil(@form.referenced_content)
206
+ end
207
+
208
+ it "returns nil if the file is not embedded and not found" do
209
+ @ref[:F].path = '/tmp/non_existing_path'
210
+ assert_nil(@form.referenced_content)
211
+ end
212
+
213
+ it "returns nil if the page referenced by page number is not found" do
214
+ io = StringIO.new
215
+ HexaPDF::Document.new.tap {|d| d.pages.add; d.pages.add }.write(io)
216
+ @ref[:F].embed(io, name: 'test.pdf')
217
+ @ref[:Page] = 5
218
+ assert_nil(@form.referenced_content)
219
+ end
220
+
221
+ it "returns nil if the page referenced by page label is not found" do
222
+ io = StringIO.new
223
+ HexaPDF::Document.new.tap do |d|
224
+ d.pages.add
225
+ d.pages.add
226
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test')
227
+ end.write(io)
228
+ @ref[:F].embed(io, name: 'test.pdf')
229
+ @ref[:Page] = 'Test5'
230
+ assert_nil(@form.referenced_content)
231
+ end
232
+
233
+ it "returns nil if an error happens during processing" do
234
+ @ref[:F].embed(StringIO.new('temp'), name: 'test.pdf')
235
+ assert_nil(@form.referenced_content)
236
+ end
237
+ end
119
238
  end
@@ -0,0 +1,112 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/optional_content_configuration'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Type::OptionalContentConfiguration do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @oc_config = @doc.optional_content.default_configuration
11
+ @ocg = @doc.optional_content.ocg('Test')
12
+ end
13
+
14
+ describe "ocg_state" do
15
+ it "defaults to the base state if nothing specific is set" do
16
+ assert_equal(:on, @oc_config.ocg_state(@ocg))
17
+ @oc_config[:BaseState] = :OFF
18
+ assert_equal(:off, @oc_config.ocg_state(@ocg))
19
+ @oc_config[:BaseState] = :Unchanged
20
+ assert_nil(@oc_config.ocg_state(@ocg))
21
+ end
22
+
23
+ it "returns :on if the OCG is in the /ON key" do
24
+ @oc_config[:ON] = [@ocg]
25
+ [:ON, :OFF, :Unchanged].each do |base_state|
26
+ @oc_config[:BaseState] = base_state
27
+ assert_equal(:on, @oc_config.ocg_state(@ocg))
28
+ end
29
+ end
30
+
31
+ it "returns :off if the OCG is in the /OFF key" do
32
+ @oc_config[:OFF] = [@ocg]
33
+ [:ON, :OFF, :Unchanged].each do |base_state|
34
+ @oc_config[:BaseState] = base_state
35
+ assert_equal(:off, @oc_config.ocg_state(@ocg))
36
+ end
37
+ end
38
+
39
+ it "adds the OCG to the respective dictionary key if a state is given" do
40
+ [[:ON, :OFF], [:OFF, :ON]].each do |state, other_state|
41
+ @oc_config[other_state] = [@ocg]
42
+ @oc_config.ocg_state(@ocg, state.downcase)
43
+ assert_equal([], @oc_config[other_state].value)
44
+ assert_equal([@ocg], @oc_config[state].value)
45
+ @oc_config.ocg_state(@ocg, state)
46
+ assert_equal([@ocg], @oc_config[state].value)
47
+ end
48
+ end
49
+
50
+ it "fails if an invalid state is given" do
51
+ assert_raises(ArgumentError) { @oc_config.ocg_state(@ocg, :unknwo) }
52
+ end
53
+ end
54
+
55
+ it "returns whether a given ocg is on" do
56
+ assert(@oc_config.ocg_on?(@ocg))
57
+ @oc_config[:OFF] = [@ocg]
58
+ refute(@oc_config.ocg_on?(@ocg))
59
+ end
60
+
61
+ describe "add_ocg_to_ui" do
62
+ it "adds the ocg to the top level" do
63
+ @oc_config.add_ocg_to_ui(@ocg)
64
+ @oc_config.add_ocg_to_ui(@ocg)
65
+ assert_equal([@ocg, @ocg], @oc_config[:Order].value)
66
+ end
67
+
68
+ it "adds the ocg under an existing label" do
69
+ @oc_config[:Order] = [:ocg1, ['Test'], :ocg2]
70
+ @oc_config.add_ocg_to_ui(@ocg, path: 'Test')
71
+ @oc_config.add_ocg_to_ui(@ocg, path: 'Test')
72
+ assert_equal([:ocg1, ['Test', @ocg, @ocg], :ocg2], @oc_config[:Order].value)
73
+ end
74
+
75
+ it "adds the ocg under a new label" do
76
+ @oc_config[:Order] = []
77
+ @oc_config.add_ocg_to_ui(@ocg, path: 'Test')
78
+ @oc_config.add_ocg_to_ui(@ocg, path: 'Test')
79
+ @oc_config.add_ocg_to_ui(@ocg, path: 'Test2')
80
+ assert_equal([['Test', @ocg, @ocg], ['Test2', @ocg]], @oc_config[:Order].value)
81
+ end
82
+
83
+ it "adds the ocg under an existing ocg" do
84
+ @oc_config[:Order] = [:ocg1, :ocg2]
85
+ @oc_config.add_ocg_to_ui(@ocg, path: :ocg2)
86
+ @oc_config.add_ocg_to_ui(@ocg, path: :ocg2)
87
+ assert_equal([:ocg1, :ocg2, [@ocg, @ocg]], @oc_config[:Order].value)
88
+ end
89
+
90
+ it "adds the ocg under a new ocg" do
91
+ @oc_config[:Order] = []
92
+ @oc_config.add_ocg_to_ui(@ocg, path: :ocg1)
93
+ @oc_config.add_ocg_to_ui(@ocg, path: :ocg1)
94
+ @oc_config.add_ocg_to_ui(@ocg, path: :ocg2)
95
+ assert_equal([:ocg1, [@ocg, @ocg], :ocg2, [@ocg]], @oc_config[:Order].value)
96
+ end
97
+
98
+ it "adds the ocg under an existing multi-level path" do
99
+ @oc_config[:Order] = [:ocg1, ['Test', :ocg2, [:ocg4, ['Test2', :ocg5]]], :ocg3]
100
+ @oc_config.add_ocg_to_ui(@ocg, path: ['Test', :ocg2, 'Test2'])
101
+ assert_equal([:ocg1, ['Test', :ocg2, [:ocg4, ['Test2', :ocg5, @ocg]]], :ocg3],
102
+ @oc_config[:Order].value)
103
+ end
104
+
105
+ it "adds the ocg under a new multi-level path" do
106
+ @oc_config[:Order] = [:ocg1, ['Test', :ocg2]]
107
+ @oc_config.add_ocg_to_ui(@ocg, path: ['Test2', :ocg3, 'Test3'])
108
+ assert_equal([:ocg1, ['Test', :ocg2], ['Test2', :ocg3, [['Test3', @ocg]]]],
109
+ @oc_config[:Order].value)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,158 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/optional_content_group'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Type::OptionalContentGroup do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @ocg = @doc.add({Type: :OCG, Name: 'OCG'})
11
+ end
12
+
13
+ it "resolves all referenced type classes" do
14
+ hash = {
15
+ Usage: {
16
+ CreatorInfo: {},
17
+ Language: {},
18
+ Export: {},
19
+ Zoom: {},
20
+ Print: {},
21
+ View: {},
22
+ User: {},
23
+ PageElement: {}
24
+ }
25
+ }
26
+ ocg = @doc.add(hash, type: :OCG)
27
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup, ocg)
28
+ ocu = ocg[:Usage]
29
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage, ocu)
30
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::CreatorInfo,
31
+ ocu[:CreatorInfo])
32
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::Language,
33
+ ocu[:Language])
34
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::Export,
35
+ ocu[:Export])
36
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::Zoom,
37
+ ocu[:Zoom])
38
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::Print,
39
+ ocu[:Print])
40
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::View,
41
+ ocu[:View])
42
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::User,
43
+ ocu[:User])
44
+ assert_kind_of(HexaPDF::Type::OptionalContentGroup::OptionalContentUsage::PageElement,
45
+ ocu[:PageElement])
46
+ end
47
+
48
+ it "must always be an indirect object" do
49
+ assert(@ocg.must_be_indirect?)
50
+ end
51
+
52
+ it "returns the name" do
53
+ assert_equal('OCG', @ocg.name)
54
+ @ocg.name('Other')
55
+ assert_equal('Other', @ocg.name)
56
+ end
57
+
58
+ describe "intent" do
59
+ it "can be ask whether the intent is :View" do
60
+ assert(@ocg.intent_view?)
61
+ @ocg[:Intent] = :Design
62
+ refute(@ocg.intent_view?)
63
+ end
64
+
65
+ it "can be ask whether the intent is :Design" do
66
+ refute(@ocg.intent_design?)
67
+ @ocg[:Intent] = :Design
68
+ assert(@ocg.intent_design?)
69
+ end
70
+
71
+ it "can apply one or more intents" do
72
+ @ocg.apply_intent(:View)
73
+ @ocg.apply_intent(:Design)
74
+ assert(@ocg.intent_view?)
75
+ assert(@ocg.intent_design?)
76
+ end
77
+ end
78
+
79
+ describe "managing the OCG's default configuration" do
80
+ it "can be asked whether it is on by default" do
81
+ assert(@ocg.on?)
82
+ @doc.optional_content.default_configuration[:OFF] = [@ocg]
83
+ refute(@ocg.on?)
84
+ end
85
+
86
+ it "can set its default state to on" do
87
+ @doc.optional_content.default_configuration[:OFF] = [@ocg]
88
+ @ocg.on!
89
+ assert(@ocg.on?)
90
+ end
91
+
92
+ it "can set its default state to off" do
93
+ @ocg.off!
94
+ refute(@ocg.on?)
95
+ end
96
+
97
+ it "can add itself to the UI" do
98
+ @ocg.add_to_ui(path: 'Test')
99
+ assert_equal([['Test', @ocg]], @doc.optional_content.default_configuration[:Order].value)
100
+ end
101
+ end
102
+
103
+ it "can set and return the creator info usage entry" do
104
+ refute(@ocg.creator_info)
105
+ dict = @ocg.creator_info("HexaPDF", :Technical)
106
+ assert_equal({Creator: "HexaPDF", Subtype: :Technical}, dict.value)
107
+ assert_raises(ArgumentError) { @ocg.creator_info("HexaPDF") }
108
+ end
109
+
110
+ it "can set and return the language usage entry" do
111
+ refute(@ocg.language)
112
+ dict = @ocg.language('de')
113
+ assert_equal({Lang: "de", Preferred: :OFF}, dict.value)
114
+ @ocg.language('de', preferred: true)
115
+ assert_equal({Lang: "de", Preferred: :ON}, @ocg.language.value)
116
+ end
117
+
118
+ it "can set and return the export state usage entry" do
119
+ refute(@ocg.export_state)
120
+ assert(@ocg.export_state(true))
121
+ assert(@ocg.export_state)
122
+ end
123
+
124
+ it "can set and return the view state usage entry" do
125
+ refute(@ocg.view_state)
126
+ assert(@ocg.view_state(true))
127
+ assert(@ocg.view_state)
128
+ end
129
+
130
+ it "can set and return the print state usage entry" do
131
+ refute(@ocg.print_state)
132
+ dict = @ocg.print_state(true)
133
+ assert_equal({PrintState: :ON, Subtype: nil}, dict.value)
134
+ @ocg.print_state(true, subtype: :Watermark)
135
+ assert_equal({PrintState: :ON, Subtype: :Watermark}, @ocg.print_state.value)
136
+ end
137
+
138
+ it "can set and return the zoom usage entry" do
139
+ refute(@ocg.zoom)
140
+ dict = @ocg.zoom(min: 2.0)
141
+ assert_equal({min: 2.0, max: nil}, dict.value)
142
+ assert_equal({min: nil, max: 3.0}, @ocg.zoom(max: 3.0).value)
143
+ assert_equal({min: 1.0, max: 3.0}, @ocg.zoom(min: 1.0, max: 3.0).value)
144
+ end
145
+
146
+ it "can set and return the intended user usage entry" do
147
+ refute(@ocg.intended_user)
148
+ dict = @ocg.intended_user(:Ind, 'Me')
149
+ assert_equal({Type: :Ind, Name: "Me"}, dict.value)
150
+ end
151
+
152
+ it "can set and return the page element usage entry" do
153
+ refute(@ocg.page_element)
154
+ assert_equal(:HF, @ocg.page_element(:HF))
155
+ @ocg.page_element(:L)
156
+ assert_equal(:L, @ocg.page_element)
157
+ end
158
+ end
@@ -0,0 +1,109 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/optional_content_properties'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Type::OptionalContentProperties do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @oc = @doc.optional_content
11
+ end
12
+
13
+ describe "add_ocg" do
14
+ it "adds a given OCG object" do
15
+ ocg = @doc.add({Type: :OCG, Name: 'test'})
16
+ assert_same(ocg, @oc.add_ocg(ocg))
17
+ assert_equal([ocg], @oc[:OCGs])
18
+ end
19
+
20
+ it "doesn't add an OCG if it has been added before" do
21
+ ocg = @doc.add({Type: :OCG, Name: 'test'})
22
+ @oc.add_ocg(ocg)
23
+ @oc.add_ocg(ocg)
24
+ assert_equal([ocg], @oc[:OCGs])
25
+ end
26
+
27
+ it "creates a new OCG object with the given name and adds it" do
28
+ ocg = @oc.add_ocg('Test')
29
+ assert_equal([ocg], @oc[:OCGs])
30
+ end
31
+ end
32
+
33
+ describe "ocg" do
34
+ it "returns the first OCG with the given name, regardless of the create argument" do
35
+ ocg1 = @oc.add_ocg('Test')
36
+ _ocg2 = @oc.add_ocg('Test')
37
+ assert_same(ocg1, @oc.ocg('Test', create: false))
38
+ assert_same(ocg1, @oc.ocg('Test', create: true))
39
+ end
40
+
41
+ it "returns nil if no OCG with the given name exists and create is false" do
42
+ assert_nil(@oc.ocg('Other', create: false))
43
+ @oc.add_ocg('Test')
44
+ assert_nil(@oc.ocg('Other', create: false))
45
+ end
46
+
47
+ it "creates an OCG with the given name if none is found and create is true" do
48
+ ocg = @oc.ocg('Test')
49
+ assert_same(ocg, @oc.ocg('Test'))
50
+ assert_equal([ocg], @oc[:OCGs])
51
+ end
52
+ end
53
+
54
+ describe "ocgs" do
55
+ it "returns the list of the known optional content groups, with duplicates removed" do
56
+ ocg1 = @oc.add_ocg(@oc.add_ocg('Test'))
57
+ @oc[:OCGs] << nil
58
+ ocg2 = @oc.add_ocg('Test')
59
+ ocg3 = @oc.add_ocg('Other')
60
+ assert_equal([ocg1, ocg2, ocg3], @oc.ocgs)
61
+ end
62
+ end
63
+
64
+ describe "create_ocmd" do
65
+ it "creates the optional content membership dictionary for the given OCGs" do
66
+ ocmd = @oc.create_ocmd(@oc.ocg('Test'))
67
+ assert_equal({Type: :OCMD, OCGs: [@oc.ocg('Test')], P: :AnyOn}, ocmd.value)
68
+
69
+ ocmd = @oc.create_ocmd([@oc.ocg('Test'), @oc.ocg('Test2')], policy: :any_off)
70
+ assert_equal({Type: :OCMD, OCGs: [@oc.ocg('Test'), @oc.ocg('Test2')], P: :AnyOff}, ocmd.value)
71
+ end
72
+
73
+ it "fails if the policy is invalid" do
74
+ error = assert_raises(ArgumentError) { @oc.create_ocmd(:ocg, policy: :unknown) }
75
+ assert_match(/Invalid OCMD.*unknown/, error.message)
76
+ end
77
+ end
78
+
79
+ describe "default_configuration" do
80
+ it "returns an existing dictionary" do
81
+ dict = @oc.default_configuration
82
+ assert_same(@oc[:D], dict)
83
+ assert_kind_of(HexaPDF::Type::OptionalContentConfiguration, dict)
84
+ end
85
+
86
+ it "sets and returns a default configuration dictionary if none is set" do
87
+ @oc.delete(:D)
88
+ assert_equal({Creator: 'HexaPDF'}, @oc.default_configuration.value)
89
+ end
90
+
91
+ it "sets the default configuration dictionary to the given value" do
92
+ d_before = @oc[:D]
93
+ d_new = @oc.default_configuration(Creator: 'Test')
94
+ refute_same(d_before, d_new)
95
+ assert_same(@oc[:D], d_new)
96
+ assert_equal({Creator: 'Test'}, d_new.value)
97
+ end
98
+ end
99
+
100
+ describe "perform_validation" do
101
+ it "creates the /D entry if it is not set" do
102
+ @oc.delete(:D)
103
+ refute(@oc.validate(auto_correct: false))
104
+ refute(@oc.key?(:D))
105
+ assert(@oc.validate(auto_correct: true))
106
+ assert_equal({Creator: 'HexaPDF'}, @oc[:D].value)
107
+ end
108
+ end
109
+ end
@@ -592,11 +592,12 @@ describe HexaPDF::Type::Page do
592
592
  end
593
593
 
594
594
  it "gracefully handles invalid /Annot key values" do
595
- @page[:Annots] << nil << @doc.add({}, stream: '')
595
+ @page[:Annots] << nil << @doc.add({}, stream: '') << 543
596
596
  result = @page.flatten_annotations
597
597
  assert(result.empty?)
598
598
  assert(@annot1.null?)
599
599
  assert(@annot2.null?)
600
+ assert_equal([], @page[:Annots].value)
600
601
 
601
602
  @page[:Annots] = @doc.add({}, stream: '')
602
603
  result = @page.flatten_annotations
@@ -706,14 +707,14 @@ describe HexaPDF::Type::Page do
706
707
  @appearance[:Matrix] = [0.707106, 0.707106, -0.707106, 0.707106, 10, 30]
707
708
  @page.flatten_annotations
708
709
  assert_operators(@canvas.contents,
709
- [:concatenate_matrix, [0.998269, 0.0, 0.0, 0.415946, 111.21318, 80.60659]],
710
+ [:concatenate_matrix, [0.998269, 0.0, 0.0, 0.415946, 111.193776, 91.933396]],
710
711
  range: 1)
711
712
  end
712
713
 
713
714
  it "scales the appearance to fit into the annotations's rectangle" do
714
715
  @annot1[:Rect] = [100, 100, 130, 150]
715
716
  @page.flatten_annotations
716
- assert_operators(@canvas.contents, [:concatenate_matrix, [0.5, 0, 0, 2, 110, 105]], range: 1)
717
+ assert_operators(@canvas.contents, [:concatenate_matrix, [0.5, 0, 0, 2, 105, 110]], range: 1)
717
718
  end
718
719
  end
719
720
 
@@ -722,12 +723,13 @@ describe HexaPDF::Type::Page do
722
723
  annot1 = @doc.add({Type: :Annot, Subtype: :Text, Rect: [100, 100, 160, 125]})
723
724
  annot2 = @doc.add({Subtype: :Unknown, Rect: [10, 10, 70, 35]})
724
725
  not_an_annot = @doc.add({}, stream: '')
725
- page[:Annots] = [not_an_annot, annot1, nil, annot2]
726
+ page[:Annots] = [not_an_annot, annot1, nil, annot2, {Type: :Annot, Subtype: :Text, Rect: [0, 0, 0, 0]}]
726
727
 
727
728
  annotations = page.each_annotation.to_a
728
- assert_equal(2, annotations.size)
729
+ assert_equal(3, annotations.size)
729
730
  assert_equal([100, 100, 160, 125], annotations[0][:Rect])
730
731
  assert_equal(:Annot, annotations[0].type)
731
732
  assert_equal(:Annot, annotations[1].type)
733
+ assert_equal(:Annot, annotations[2].type)
732
734
  end
733
735
  end
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.33.0
4
+ version: 0.34.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-02 00:00:00.000000000 Z
11
+ date: 2023-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -299,6 +299,8 @@ files:
299
299
  - examples/023-images.rb
300
300
  - examples/024-digital-signatures.rb
301
301
  - examples/025-table_box.rb
302
+ - examples/026-optional_content.rb
303
+ - examples/027-composer_optional_content.rb
302
304
  - examples/emoji-smile.png
303
305
  - examples/emoji-wink.png
304
306
  - examples/machupicchu.jpg
@@ -474,6 +476,7 @@ files:
474
476
  - lib/hexapdf/type/actions/go_to.rb
475
477
  - lib/hexapdf/type/actions/go_to_r.rb
476
478
  - lib/hexapdf/type/actions/launch.rb
479
+ - lib/hexapdf/type/actions/set_ocg_state.rb
477
480
  - lib/hexapdf/type/actions/uri.rb
478
481
  - lib/hexapdf/type/annotation.rb
479
482
  - lib/hexapdf/type/annotations.rb
@@ -500,6 +503,10 @@ files:
500
503
  - lib/hexapdf/type/mark_information.rb
501
504
  - lib/hexapdf/type/names.rb
502
505
  - lib/hexapdf/type/object_stream.rb
506
+ - lib/hexapdf/type/optional_content_configuration.rb
507
+ - lib/hexapdf/type/optional_content_group.rb
508
+ - lib/hexapdf/type/optional_content_membership.rb
509
+ - lib/hexapdf/type/optional_content_properties.rb
503
510
  - lib/hexapdf/type/outline.rb
504
511
  - lib/hexapdf/type/outline_item.rb
505
512
  - lib/hexapdf/type/page.rb
@@ -728,6 +735,7 @@ files:
728
735
  - test/hexapdf/type/acro_form/test_text_field.rb
729
736
  - test/hexapdf/type/acro_form/test_variable_text_field.rb
730
737
  - test/hexapdf/type/actions/test_launch.rb
738
+ - test/hexapdf/type/actions/test_set_ocg_state.rb
731
739
  - test/hexapdf/type/actions/test_uri.rb
732
740
  - test/hexapdf/type/annotations/test_markup_annotation.rb
733
741
  - test/hexapdf/type/annotations/test_text.rb
@@ -748,6 +756,9 @@ files:
748
756
  - test/hexapdf/type/test_info.rb
749
757
  - test/hexapdf/type/test_names.rb
750
758
  - test/hexapdf/type/test_object_stream.rb
759
+ - test/hexapdf/type/test_optional_content_configuration.rb
760
+ - test/hexapdf/type/test_optional_content_group.rb
761
+ - test/hexapdf/type/test_optional_content_properties.rb
751
762
  - test/hexapdf/type/test_outline.rb
752
763
  - test/hexapdf/type/test_outline_item.rb
753
764
  - test/hexapdf/type/test_page.rb
@@ -784,7 +795,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
784
795
  - !ruby/object:Gem::Version
785
796
  version: '0'
786
797
  requirements: []
787
- rubygems_version: 3.5.0.dev
798
+ rubygems_version: 3.4.10
788
799
  signing_key:
789
800
  specification_version: 4
790
801
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby