hexapdf 0.33.0 → 0.34.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/examples/026-optional_content.rb +55 -0
- data/examples/027-composer_optional_content.rb +83 -0
- data/lib/hexapdf/cli/command.rb +7 -1
- data/lib/hexapdf/cli/fonts.rb +1 -1
- data/lib/hexapdf/cli/inspect.rb +2 -4
- data/lib/hexapdf/composer.rb +2 -1
- data/lib/hexapdf/configuration.rb +21 -1
- data/lib/hexapdf/content/canvas.rb +52 -0
- data/lib/hexapdf/content/operator.rb +2 -0
- data/lib/hexapdf/dictionary.rb +1 -0
- data/lib/hexapdf/dictionary_fields.rb +1 -2
- data/lib/hexapdf/digital_signature/verification_result.rb +1 -2
- data/lib/hexapdf/document/layout.rb +3 -0
- data/lib/hexapdf/document/pages.rb +1 -1
- data/lib/hexapdf/document.rb +7 -0
- data/lib/hexapdf/encryption/ruby_aes.rb +10 -20
- data/lib/hexapdf/layout/box.rb +23 -3
- data/lib/hexapdf/layout/column_box.rb +2 -1
- data/lib/hexapdf/layout/frame.rb +23 -6
- data/lib/hexapdf/layout/inline_box.rb +20 -9
- data/lib/hexapdf/layout/list_box.rb +34 -20
- data/lib/hexapdf/layout/page_style.rb +2 -1
- data/lib/hexapdf/layout/style.rb +46 -6
- data/lib/hexapdf/layout/table_box.rb +9 -7
- data/lib/hexapdf/layout/text_box.rb +9 -2
- data/lib/hexapdf/layout/text_fragment.rb +28 -2
- data/lib/hexapdf/layout/text_layouter.rb +21 -5
- data/lib/hexapdf/stream.rb +1 -2
- data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
- data/lib/hexapdf/type/actions.rb +1 -0
- data/lib/hexapdf/type/annotations/text.rb +1 -2
- data/lib/hexapdf/type/catalog.rb +10 -1
- data/lib/hexapdf/type/cid_font.rb +15 -1
- data/lib/hexapdf/type/form.rb +75 -5
- data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
- data/lib/hexapdf/type/optional_content_group.rb +370 -0
- data/lib/hexapdf/type/optional_content_membership.rb +63 -0
- data/lib/hexapdf/type/optional_content_properties.rb +158 -0
- data/lib/hexapdf/type/page.rb +27 -11
- data/lib/hexapdf/type/page_label.rb +4 -8
- data/lib/hexapdf/type.rb +4 -0
- data/lib/hexapdf/utils/pdf_doc_encoding.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +49 -0
- data/test/hexapdf/document/test_layout.rb +7 -2
- data/test/hexapdf/document/test_pages.rb +6 -6
- data/test/hexapdf/layout/test_box.rb +13 -4
- data/test/hexapdf/layout/test_frame.rb +13 -1
- data/test/hexapdf/layout/test_inline_box.rb +17 -8
- data/test/hexapdf/layout/test_list_box.rb +48 -31
- data/test/hexapdf/layout/test_style.rb +10 -0
- data/test/hexapdf/layout/test_table_box.rb +32 -26
- data/test/hexapdf/layout/test_text_box.rb +8 -0
- data/test/hexapdf/layout/test_text_fragment.rb +33 -0
- data/test/hexapdf/layout/test_text_layouter.rb +32 -5
- data/test/hexapdf/test_composer.rb +10 -0
- data/test/hexapdf/test_dictionary.rb +10 -0
- data/test/hexapdf/test_document.rb +4 -0
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
- data/test/hexapdf/type/test_catalog.rb +11 -0
- data/test/hexapdf/type/test_form.rb +119 -0
- data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
- data/test/hexapdf/type/test_optional_content_group.rb +158 -0
- data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
- data/test/hexapdf/type/test_page.rb +2 -2
- 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
|
data/lib/hexapdf/type/page.rb
CHANGED
@@ -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
|
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
|
-
#
|
268
|
-
#
|
269
|
-
#
|
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.
|
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.
|
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
|
-
|
596
|
-
|
597
|
-
|
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(
|
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])
|