hexapdf 0.33.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -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 +2 -1
  9. data/lib/hexapdf/configuration.rb +21 -1
  10. data/lib/hexapdf/content/canvas.rb +52 -0
  11. data/lib/hexapdf/content/operator.rb +2 -0
  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 +27 -11
  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/document/test_layout.rb +7 -2
  48. data/test/hexapdf/document/test_pages.rb +6 -6
  49. data/test/hexapdf/layout/test_box.rb +13 -4
  50. data/test/hexapdf/layout/test_frame.rb +13 -1
  51. data/test/hexapdf/layout/test_inline_box.rb +17 -8
  52. data/test/hexapdf/layout/test_list_box.rb +48 -31
  53. data/test/hexapdf/layout/test_style.rb +10 -0
  54. data/test/hexapdf/layout/test_table_box.rb +32 -26
  55. data/test/hexapdf/layout/test_text_box.rb +8 -0
  56. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  57. data/test/hexapdf/layout/test_text_layouter.rb +32 -5
  58. data/test/hexapdf/test_composer.rb +10 -0
  59. data/test/hexapdf/test_dictionary.rb +10 -0
  60. data/test/hexapdf/test_document.rb +4 -0
  61. data/test/hexapdf/test_writer.rb +3 -3
  62. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  63. data/test/hexapdf/type/test_catalog.rb +11 -0
  64. data/test/hexapdf/type/test_form.rb +119 -0
  65. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  66. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  67. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  68. data/test/hexapdf/type/test_page.rb +2 -2
  69. metadata +14 -3
@@ -0,0 +1,370 @@
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-2023 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents an optional content group (OCG).
43
+ #
44
+ # An optional content group represents graphics that can be made visible or invisible
45
+ # dynamically by the PDF processor. These graphics may reside in any content stream and don't
46
+ # need to be consecutive with respect to the drawing order.
47
+ #
48
+ # Most PDF viewers call this feature "layers" since it is often used to show/hide parts of
49
+ # drawings or maps.
50
+ #
51
+ # == Intent and Usage
52
+ #
53
+ # An OCG may be assigned an intent (defaults to :View) and usage information. This allows one to
54
+ # specify in more detail how an OCG may be used (e.g. to only show the content when a certain
55
+ # zoom level is active).
56
+ #
57
+ # See: PDF2.0 s8.11.2
58
+ class OptionalContentGroup < Dictionary
59
+
60
+ # Represents an optional content group's usage dictionary which describes how the content
61
+ # controlled by the group should be used.
62
+ #
63
+ # See: PDF2.0 s8.11.4.4
64
+ class OptionalContentUsage < Dictionary
65
+
66
+ # The dictionary used as value for the /CreatorInfo key.
67
+ #
68
+ # See: PDF2.0 s8.11.4.4
69
+ class CreatorInfo < Dictionary
70
+ define_type :XXOCUsageCreatorInfo
71
+ define_field :Creator, type: String, required: true
72
+ define_field :Subtype, type: Symbol, required: true
73
+ end
74
+
75
+ # The dictionary used as value for the /Language key.
76
+ #
77
+ # See: PDF2.0 s8.11.4.4
78
+ class Language < Dictionary
79
+ define_type :XXOCUsageLanguage
80
+ define_field :Lang, type: String, required: true
81
+ define_field :Preferred, type: Symbol, default: :OFF, allowed_values: [:ON, :OFF]
82
+ end
83
+
84
+ # The dictionary used as value for the /Export key.
85
+ #
86
+ # See: PDF2.0 s8.11.4.4
87
+ class Export < Dictionary
88
+ define_type :XXOCUsageExport
89
+ define_field :ExportState, type: Symbol, required: true, allowed_values: [:ON, :OFF]
90
+ end
91
+
92
+ # The dictionary used as value for the /Zoom key.
93
+ #
94
+ # See: PDF2.0 s8.11.4.4
95
+ class Zoom < Dictionary
96
+ define_type :XXOCUsageZoom
97
+ define_field :min, type: Numeric, default: 0
98
+ define_field :max, type: Numeric
99
+ end
100
+
101
+ # The dictionary used as value for the /Print key.
102
+ #
103
+ # See: PDF2.0 s8.11.4.4
104
+ class Print < Dictionary
105
+ define_type :XXOCUsagePrint
106
+ define_field :Subtype, type: Symbol
107
+ define_field :PrintState, type: Symbol, allowed_values: [:ON, :OFF]
108
+ end
109
+
110
+ # The dictionary used as value for the /View key.
111
+ #
112
+ # See: PDF2.0 s8.11.4.4
113
+ class View < Dictionary
114
+ define_type :XXOCUsageView
115
+ define_field :ViewState, type: Symbol, required: true, allowed_values: [:ON, :OFF]
116
+ end
117
+
118
+ # The dictionary used as value for the /User key.
119
+ #
120
+ # See: PDF2.0 s8.11.4.4
121
+ class User < Dictionary
122
+ define_type :XXOCUsageUser
123
+ define_field :Type, type: Symbol, required: true, allowed_values: [:Ind, :Ttl, :Org]
124
+ define_field :Name, type: [String, PDFArray], required: true
125
+ end
126
+
127
+ # The dictionary used as value for the /PageElement key.
128
+ #
129
+ # See: PDF2.0 s8.11.4.4
130
+ class PageElement < Dictionary
131
+ define_type :XXOCUsagePageElement
132
+ define_field :Subtype, type: Symbol, required: true, allowed_values: [:HF, :FG, :BG, :L]
133
+ end
134
+
135
+ define_type :XXOCUsage
136
+
137
+ define_field :CreatorInfo, type: :XXOCUsageCreatorInfo
138
+ define_field :Language, type: :XXOCUsageLanguage
139
+ define_field :Export, type: :XXOCUsageExport
140
+ define_field :Zoom, type: :XXOCUsageZoom
141
+ define_field :Print, type: :XXOCUsagePrint
142
+ define_field :View, type: :XXOCUsageView
143
+ define_field :User, type: :XXOCUsageUser
144
+ define_field :PageElement, type: :XXOCUsagePageElement
145
+
146
+ end
147
+
148
+ define_type :OCG
149
+
150
+ define_field :Type, type: Symbol, required: true, default: type
151
+ define_field :Name, type: String, required: true
152
+ define_field :Intent, type: [Symbol, PDFArray], default: :View
153
+ define_field :Usage, type: :XXOCUsage
154
+
155
+ # Returns +true+ since optional content group dictionaries objects must always be indirect.
156
+ def must_be_indirect?
157
+ true
158
+ end
159
+
160
+ # :call-seq:
161
+ # ocg.name -> name
162
+ # ocg.name(value) -> value
163
+ #
164
+ # Returns the name of the OCG if no argument is given. Otherwise sets the name to the given
165
+ # value.
166
+ def name(value = nil)
167
+ if value
168
+ self[:Name] = value
169
+ else
170
+ self[:Name]
171
+ end
172
+ end
173
+
174
+ # Applies the given intent (:View, :Design or a custom intent) to the OCG.
175
+ def apply_intent(intent)
176
+ self[:Intent] = key?(:Intent) ? Array(self[:Intent]) : []
177
+ self[:Intent] << intent
178
+ end
179
+
180
+ # Returns +true+ if this OCG has an intent of :View.
181
+ def intent_view?
182
+ Array(self[:Intent]).include?(:View)
183
+ end
184
+
185
+ # Returns +true+ if this OCG has an intent of :Design.
186
+ def intent_design?
187
+ Array(self[:Intent]).include?(:Design)
188
+ end
189
+
190
+ # Returns +true+ if the OCG is set to on in the default configuration (see
191
+ # OptionalContentProperties#default_configuration).
192
+ def on?
193
+ document.optional_content.default_configuration.ocg_on?(self)
194
+ end
195
+
196
+ # Sets the state of the OCG to on in the default configuration (see
197
+ # OptionalContentProperties#default_configuration).
198
+ def on!
199
+ document.optional_content.default_configuration.ocg_state(self, :on)
200
+ end
201
+
202
+ # Sets the state of the OCG to off in the default configuration (see
203
+ # OptionalContentProperties#default_configuration).
204
+ def off!
205
+ document.optional_content.default_configuration.ocg_state(self, :off)
206
+ end
207
+
208
+ # Adds the OCG to the PDF processor's user interface in the default configuration (see
209
+ # OptionalContentProperties#default_configuration), either at the top-level or under the given
210
+ # hierarchical +path+ but always as the last item.
211
+ def add_to_ui(path: nil)
212
+ document.optional_content.default_configuration.add_ocg_to_ui(self, path: path)
213
+ end
214
+
215
+ # :call-seq:
216
+ # ocg.creator_info -> creator_info or nil
217
+ # ocg.creator_info(creator, subtype) -> creator_info
218
+ #
219
+ # Returns the creator info dictionary (see OptionalContentUsage::CreatorInfo) or +nil+ if no
220
+ # argument is given. Otherwise sets the creator info using the given values.
221
+ #
222
+ # The creator info dictionary is used to store application-specific data. The string +creator+
223
+ # specifies the application that created the group and the symbol +subtype+ defines the type
224
+ # of content controlled by the OCG (for example :Artwork for graphic design applications or
225
+ # :Technical for technical designs such as plans).
226
+ def creator_info(creator = nil, subtype = nil)
227
+ if creator && subtype
228
+ self[:Usage] ||= {}
229
+ self[:Usage][:CreatorInfo] = {Creator: creator, Subtype: subtype}
230
+ elsif creator || subtype
231
+ raise ArgumentError, "Missing argument, both creator and subtype are needed"
232
+ end
233
+ self[:Usage]&.[](:CreatorInfo)
234
+ end
235
+
236
+ # :call-seq:
237
+ # ocg.language -> language_info or nil
238
+ # ocg.language(lang, preferred: false) -> language_info
239
+ #
240
+ # Returns the language dictionary (see OptionalContentUsage::Language) or +nil+ if no argument
241
+ # is given. Otherwise sets the langauge using the given values.
242
+ #
243
+ # The language dictionary describes the language of the content controlled by the OCG. The
244
+ # string +lang+ needs to be a language tag as defined in BCP 47 (e.g. 'en' or 'de-AT'). If
245
+ # +preferred+ is +true+, this dictionary is preferred if there is only a partial match
246
+ def language(lang = nil, preferred: false)
247
+ if lang
248
+ self[:Usage] ||= {}
249
+ self[:Usage][:Language] = {Lang: lang, Preferred: (preferred ? :ON : :OFF)}
250
+ end
251
+ self[:Usage]&.[](:Language)
252
+ end
253
+
254
+ # :call-seq:
255
+ # ocg.export_state -> true or false
256
+ # ocg.export_state(state) -> state
257
+ #
258
+ # Returns the export state if no argument is given. Otherwise sets the export state using the
259
+ # given value.
260
+ #
261
+ # The export state indicates the recommended state of the content when the PDF document is
262
+ # saved to a format that does not support optional content (e.g. a raster image format). If
263
+ # +state+ is +true+, the content controlled by the OCG will be visible.
264
+ def export_state(state = nil)
265
+ if state
266
+ self[:Usage] ||= {}
267
+ self[:Usage][:Export] = {ExportState: (state ? :ON : :OFF)}
268
+ end
269
+ self[:Usage]&.[](:Export)&.[](:ExportState) == :ON
270
+ end
271
+
272
+ # :call-seq:
273
+ # ocg.view_state -> true or false
274
+ # ocg.view_state(state) -> state
275
+ #
276
+ # Returns the view state if no argument is given. Otherwise sets the view state using the
277
+ # given value.
278
+ #
279
+ # The view state indicates the state of the content when the PDF document is first opened. If
280
+ # +state+ is +true+, the content controlled by the OCG will be visible.
281
+ def view_state(state = nil)
282
+ if state
283
+ self[:Usage] ||= {}
284
+ self[:Usage][:View] = {ViewState: (state ? :ON : :OFF)}
285
+ end
286
+ self[:Usage]&.[](:View)&.[](:ViewState) == :ON
287
+ end
288
+
289
+ # :call-seq:
290
+ # ocg.print_state -> print_state or nil
291
+ # ocg.print_state(state, subtype: nil) -> print_state
292
+ #
293
+ # Returns the print state (see OptionalContentUsage::Print) or +nil+ if no argument is given.
294
+ # Otherwise sets the print state using the given values.
295
+ #
296
+ # The print state indicates the state of the content when the PDF document is printed. If
297
+ # +state+ is +true+, the content controlled by the OCG will be printed. The symbol +subtype+
298
+ # may optionally specify the kind of content controlled by the OCG (e.g. :Trapping or
299
+ # :Watermark).
300
+ def print_state(state = nil, subtype: nil)
301
+ if state
302
+ self[:Usage] ||= {}
303
+ self[:Usage][:Print] = {PrintState: (state ? :ON : :OFF), Subtype: subtype}
304
+ end
305
+ self[:Usage]&.[](:Print)
306
+ end
307
+
308
+ # :call-seq:
309
+ # ocg.zoom -> zoom_dict or nil
310
+ # ocg.zoom(min: nil, max: nil) -> zoom_dict
311
+ #
312
+ # Returns the zoom dictionary (see OptionalContentUsage::Zoom) or +nil+ if no argument is
313
+ # given. Otherwise sets the zoom range using the given values.
314
+ #
315
+ # The zoom range specifies the magnifications at which the content in the OCG is visible.
316
+ # Either +min+ or +max+ or both can be specified as magnification factors (i.e. 1.0 means
317
+ # viewing at 100%):
318
+ #
319
+ # * If +min+ is specified but +max+ isn't, the maximum possible magnification factor of the
320
+ # PDF processor is used for +max+.
321
+ #
322
+ # * If +max+ is specified but +min+ isn't, the default value of 0 for +min+ is used.
323
+ def zoom(min: nil, max: nil)
324
+ if min || max
325
+ self[:Usage] ||= {}
326
+ self[:Usage][:Zoom] = {min: min, max: max}
327
+ end
328
+ self[:Usage]&.[](:Zoom)
329
+ end
330
+
331
+ # :call-seq:
332
+ # ocg.intended_user -> user_dict or nil
333
+ # ocg.intended_user(type, name) -> user_dict
334
+ #
335
+ # Returns the user dictionary (see OptionalContentUsage::User) or +nil+ if no argument is
336
+ # given. Otherwise sets the user information using the given values.
337
+ #
338
+ # The information specifies one or more users for whom this OCG is primarily intended. The
339
+ # symbol +type+ can either be :Ind (individual), :Ttl (title or position) or :Org
340
+ # (organisation). The argument +name+ can either be a single name or an array of names.
341
+ def intended_user(type = nil, name = nil)
342
+ if type && name
343
+ self[:Usage] ||= {}
344
+ self[:Usage][:User] = {Type: type, Name: name}
345
+ end
346
+ self[:Usage]&.[](:User)
347
+ end
348
+
349
+ # :call-seq:
350
+ # ocg.page_element -> element_type or nil
351
+ # ocg.page_element(subtype) -> element_type
352
+ #
353
+ # Returns the page element type if no argument is given. Otherwise sets the page element type
354
+ # using the given value.
355
+ #
356
+ # When set, the page element declares that the OCG contains a pagination artificat. The symbol
357
+ # argument +subtype+ can either be :HF (header/footer), :FG (foreground image or graphics),
358
+ # :BG (background image or graphics), or :L (logo).
359
+ def page_element(subtype = nil)
360
+ if subtype
361
+ self[:Usage] ||= {}
362
+ self[:Usage][:PageElement] = {Subtype: subtype}
363
+ end
364
+ self[:Usage]&.[](:PageElement)&.[](:Subtype)
365
+ end
366
+
367
+ end
368
+
369
+ end
370
+ end
@@ -0,0 +1,63 @@
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-2023 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents an optional content membership dictionary.
43
+ #
44
+ # A membership dictionary allows more complex visibility policies, like:
45
+ #
46
+ # * Content that should be visible when a certain optional content group is off instead of on.
47
+ # * Content that should be visible when all of a number of OCGs are on.
48
+ #
49
+ # See: PDF2.0 s8.11.2.2
50
+ class OptionalContentMembership < Dictionary
51
+
52
+ define_type :OCMD
53
+
54
+ define_field :Type, type: Symbol, required: true, default: type
55
+ define_field :OCGs, type: [:OCG, PDFArray]
56
+ define_field :P, type: Symbol, default: :AnyOn,
57
+ allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
58
+ define_field :VE, type: PDFArray
59
+
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,158 @@
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-2023 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents an optional content properties dictionary.
43
+ #
44
+ # This dictionary is the value of the /OCProperties key in the document catalog and needs to
45
+ # exist for optional content to be usable by a PDF processor.
46
+ #
47
+ # In HexaPDF it provides the main entry point for working with optional content.
48
+ #
49
+ # See: PDF2.0 s8.11.4.2
50
+ class OptionalContentProperties < Dictionary
51
+
52
+ define_type :XXOCProperties
53
+
54
+ define_field :OCGs, type: PDFArray, default: [], required: true
55
+ define_field :D, type: :XXOCConfiguration, required: true
56
+ define_field :Configs, type: PDFArray
57
+
58
+ # :call-seq:
59
+ # optional_content.add_ocg(name) -> ocg
60
+ # optional_content.add_ocg(ocg) -> ocg
61
+ #
62
+ # Adds the given optional content group to the list of known OCGs and returns it. If a string
63
+ # is provided, an optional content group with that name is created before adding it.
64
+ #
65
+ # See: #ocg, OptionalContentGroup
66
+ def add_ocg(name_or_dict)
67
+ ocg = if name_or_dict.kind_of?(Dictionary)
68
+ name_or_dict
69
+ else
70
+ document.add({Type: :OCG, Name: name_or_dict})
71
+ end
72
+ self[:OCGs] << ocg unless self[:OCGs].include?(ocg)
73
+ ocg
74
+ end
75
+
76
+ # :call-seq:
77
+ # optional_content.ocg(name, create: true) -> ocg or +nil+
78
+ #
79
+ # Returns the first found optional content group with the given +name+.
80
+ #
81
+ # If no optional content group with the given +name+ exists but the optional argument +create+
82
+ # is +true+, a new OCG with the given +name+ is created and returned. Otherwise +nil+ is
83
+ # returned.
84
+ #
85
+ # See: #add_ocg
86
+ def ocg(name, create: true)
87
+ self[:OCGs].find {|ocg| ocg.name == name } || (create && add_ocg(name) || nil)
88
+ end
89
+
90
+ # Returns the list of known optional content group objects, with duplicates removed.
91
+ def ocgs
92
+ self[:OCGs].uniq.compact
93
+ end
94
+
95
+
96
+ OCMD_POLICY_MAPPING = {any_on: :AnyOn, AnyOn: :AnyOn, any_off: :AnyOff, # :nodoc:
97
+ AnyOff: :AnyOff, all_off: :AllOff, AllOff: :AllOff}
98
+
99
+ # Creates an optional content membership dictionary containing the given optional content
100
+ # group(s).
101
+ #
102
+ # The optional argument +policy+ specifies the visibility policy:
103
+ #
104
+ # :any_on/:AnyOn:: Content is visible if any of the OCGs are on.
105
+ # :any_off/:AnyOff:: Content is visible if any of the OCGs are off.
106
+ # :all_on/:AllOn:: Content is only visible if all OCGs are on.
107
+ # :all_off/:AllOff:: Content is only visible if all OCGs are off.
108
+ #
109
+ # See: OptionalContentMembership
110
+ def create_ocmd(ocgs, policy: :any_on)
111
+ policy = OCMD_POLICY_MAPPING.fetch(policy) do
112
+ raise ArgumentError, "Invalid OCMD policy #{policy} specified"
113
+ end
114
+ document.wrap({Type: :OCMD, OCGs: Array(ocgs), P: policy})
115
+ end
116
+
117
+ # :call-seq:
118
+ # optional_content.default_configuration -> config_dict
119
+ # optional_content.default_configuration(hash) -> config_dict
120
+ #
121
+ # Returns the default optional content configuration dictionary if no argument is given.
122
+ # Otherwise sets the the default optional content configuration to the given hash value.
123
+ #
124
+ # The default configuration defines the initial state of the optional content groups and how
125
+ # those states may be changed by a PDF processor.
126
+ #
127
+ # Example:
128
+ #
129
+ # optional_content.default_configuration(
130
+ # Name: 'My Configuration',
131
+ # OFF: [ocg1],
132
+ # Order: [ocg_all, [ocg1, ocg2, ocg3]]
133
+ # )
134
+ #
135
+ # See: OptionalContentConfiguration
136
+ def default_configuration(hash = nil)
137
+ if hash
138
+ self[:D] = hash
139
+ else
140
+ self[:D] ||= {Creator: 'HexaPDF'}
141
+ end
142
+ self[:D]
143
+ end
144
+
145
+ private
146
+
147
+ def perform_validation(&block) # :nodoc:
148
+ unless key?(:D)
149
+ yield('The OptionalContentProperties dictionary needs a default configuration', true)
150
+ self[:D] = {Creator: 'HexaPDF'}
151
+ end
152
+ super
153
+ end
154
+
155
+ end
156
+
157
+ end
158
+ end
@@ -261,12 +261,23 @@ module HexaPDF
261
261
  # Rotates the page +angle+ degrees counterclockwise where +angle+ has to be a multiple of 90.
262
262
  #
263
263
  # Positive values rotate the page to the left, negative values to the right. If +flatten+ is
264
- # +true+, the rotation is not done via the page's meta data but by "rotating" the canvas
265
- # itself.
264
+ # +true+, the rotation is not done via the page's meta (i.e. the /Rotate key) data but by
265
+ # rotating the canvas itself and all other necessary objects like the various page boxes and
266
+ # annotations.
266
267
  #
267
- # Note that the :Rotate key of a page object describes the angle in a clockwise orientation
268
- # but this method uses counterclockwise rotation to be consistent with other rotation methods
269
- # (e.g. HexaPDF::Content::Canvas#rotate).
268
+ # Notes:
269
+ #
270
+ # * The given +angle+ is applied in addition to a possibly already existing rotation
271
+ # (specified via the /Rotate key) and does not replace it.
272
+ #
273
+ # * Specifying 0 for +angle+ is valid and means that no additional rotation should be applied.
274
+ # The only meaningful usage of 0 for +angle+ is when +flatten+ is set to +true+ (so that the
275
+ # /Rotate key is removed and the existing rotation information incorporated into the canvas,
276
+ # page boxes and annotations).
277
+ #
278
+ # * The /Rotate key of a page object describes the angle in a clockwise orientation but this
279
+ # method uses counterclockwise rotation to be consistent with other rotation methods (e.g.
280
+ # HexaPDF::Content::Canvas#rotate).
270
281
  def rotate(angle, flatten: false)
271
282
  if angle % 90 != 0
272
283
  raise ArgumentError, "Page rotation has to be multiple of 90 degrees"
@@ -423,7 +434,7 @@ module HexaPDF
423
434
  #
424
435
  # To check whether the origin has been translated or not, use
425
436
  #
426
- # canvas.graphics_state.ctm.evaluate(0, 0)
437
+ # canvas.pos(0, 0)
427
438
  #
428
439
  # and check whether the result is [0, 0]. If it is, then the origin has not been
429
440
  # translated.
@@ -550,7 +561,7 @@ module HexaPDF
550
561
  return not_flattened if annotations.empty?
551
562
 
552
563
  canvas = self.canvas(type: :overlay)
553
- if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
564
+ if (pos = canvas.pos(0, 0)) != [0, 0]
554
565
  canvas.save_graphics_state
555
566
  canvas.translate(-pos[0], -pos[1])
556
567
  end
@@ -592,13 +603,18 @@ module HexaPDF
592
603
  end
593
604
 
594
605
  # Step 2) Fit calculated rectangle to annotation rectangle by translating/scaling
595
- a = HexaPDF::Content::TransformationMatrix.new
596
- a.translate(rect.left - left, rect.bottom - bottom)
597
- a.scale(rect.width.fdiv(right - left), rect.height.fdiv(top - bottom))
606
+
607
+ # The final matrix is composed by translating the bottom-left corner of the transformed
608
+ # bounding box to the bottom-left corner of the annotation rectangle and scaling from the
609
+ # bottom-left corner of the transformed bounding box.
610
+ sx = rect.width.fdiv(right - left)
611
+ sy = rect.height.fdiv(top - bottom)
612
+ tx = rect.left - left + left - left * sx
613
+ ty = rect.bottom - bottom + bottom - bottom * sy
598
614
 
599
615
  # Step 3) Premultiply form matrix - done implicitly when drawing the XObject
600
616
 
601
- canvas.transform(*a) do
617
+ canvas.transform(sx, 0, 0, sy, tx, ty) do
602
618
  # Use [box.left, box.bottom] to counter default translation in #xobject since that
603
619
  # is already taken care of in matrix a
604
620
  canvas.xobject(appearance, at: [box.left, box.bottom])