Skip to content

Commit 7b618c1

Browse files
committed
Add untested version of ImportPages task for devel check
1 parent 05060c2 commit 7b618c1

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

lib/hexapdf/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ def self.font_on_invalid_glyph(codepoint, invalid_glyph)
624624
dereference: 'HexaPDF::Task::Dereference',
625625
pdfa: 'HexaPDF::Task::PDFA',
626626
merge_acro_form: 'HexaPDF::Task::MergeAcroForm',
627+
import_pages: 'HexaPDF::Task::ImportPages',
627628
})
628629

629630
# The global configuration object, providing the following options:

lib/hexapdf/task.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ module Task
6666
autoload(:Dereference, 'hexapdf/task/dereference')
6767
autoload(:PDFA, 'hexapdf/task/pdfa')
6868
autoload(:MergeAcroForm, 'hexapdf/task/merge_acro_form')
69+
autoload(:ImportPages, 'hexapdf/task/import_pages')
6970

7071
end
7172

lib/hexapdf/task/import_pages.rb

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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-2026 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/serializer'
38+
require 'set'
39+
40+
module HexaPDF
41+
module Task
42+
43+
# Task for importing pages from another document.
44+
#
45+
# It takes care of
46+
#
47+
# * importing the specified pages,
48+
# * handling optional content groups,
49+
# * and merging form fields.
50+
#
51+
# Note that the /Order, /AS and /Locked fields of the default optional content configuration
52+
# dictionary are not preserved.
53+
#
54+
# Example:
55+
#
56+
# doc.task(:import_pages, source: source_doc, pages: [4..-2])
57+
module ImportPages
58+
59+
# Performs the necessary steps to import the pages from the +source+ docment into the target
60+
# document +doc+. Returns the imported pages.
61+
#
62+
# +source+::
63+
# Specifies the source PDF document from which the pages should be imported.
64+
#
65+
# +pages+::
66+
# Has to be one of the following:
67+
#
68+
# +:all+:: Imports all pages from the +source+ document.
69+
# Integer value:: Imports the page with the given zero-based index.
70+
# Range value:: Imports the pages from the range.
71+
# Array of Integer or Range values:: Imports all specified pages or page ranges.
72+
#
73+
# +:append+::
74+
# Specifies whether the imported pages should be appended to the target document's page
75+
# tree.
76+
#
77+
# +ocgs+::
78+
# Specifies the handling of optional content groups:
79+
#
80+
# +:preserve+:: Preserve the on/off state for all used OCGs.
81+
# +:ignore+:: Ignore the on/off state.
82+
#
83+
# +:acro_form+::
84+
#
85+
# Specifies whether AcroForm fields should be merged into the target document:
86+
#
87+
# +:merge+:: Merge AcroForm fields.
88+
# +:ignore+:: Ignore AcroForm fields.
89+
def self.call(doc, source:, pages:, append: true, ocgs: :preserve, acro_form: :merge)
90+
# Retrieve all specified source pages
91+
pages = if pages == :all
92+
source.pages.each.to_a
93+
elsif pages.kind_of?(Integer)
94+
[source.pages[pages]]
95+
else
96+
result = Set.new
97+
all_pages = source.pages.each.to_a
98+
pages.each {|selector| result.merge(Array(all_pages[selector])) }
99+
result
100+
end
101+
102+
# Import the source pages and optionally append them to the target page tree
103+
pages = pages.map do |page|
104+
imported_page = doc.import(page)
105+
doc.pages << imported_page if append
106+
imported_page
107+
end
108+
109+
doc.task(:merge_acro_form, source: source, pages: pages) if acro_form == :merge
110+
preserve_ocgs(doc, source, pages) if ocgs == :preserve
111+
112+
pages
113+
end
114+
115+
# Preserves the state of the OCGs found on +pages+ so that the visual appearance in the target
116+
# document +doc+ is the same as in the +source+ document.
117+
def self.preserve_ocgs(doc, source, pages)
118+
# Find all OCGs used on all pages
119+
ocgs = Set.new
120+
process_ocg_or_ocmd = lambda do |obj|
121+
if obj.type == :OCG
122+
ocgs << obj
123+
elsif obj.type == :OCMD
124+
ocgs.merge(ocmd[:OCGs].to_ary)
125+
end
126+
end
127+
seen_resources = {}
128+
pages.each do |page|
129+
unless seen_resources[page.resources] # handle case when pages share the resources dict
130+
page.resources[:Properties]&.each do |name, obj|
131+
next unless obj
132+
process_ocg_or_ocmd.call(obj)
133+
end
134+
135+
page.resources[:XObject]&.each do |name, obj|
136+
process_ocg_or_ocmd.call(obj[:OC]) if obj.key?(:OC)
137+
end
138+
end
139+
140+
page.each_annotation do |annot|
141+
process_ocg_or_ocmd.call(annot[:OC]) if annot.key?(:OC)
142+
end
143+
144+
seen_resources[page.resources] = true
145+
end
146+
147+
return if ocgs.empty?
148+
149+
# Add all found OCGs to the optional content properties dictionary
150+
ocp = doc.optional_content
151+
ocgs.each {|ocg| ocp.add_ocg(ocg) }
152+
153+
# Create a mapping from source OCGs to target OCGs and vice-versa
154+
source_ocg = {}
155+
target_ocg = {}
156+
source.optional_content.ocgs.each do |ocg|
157+
imported_ocg = doc.import(ocg)
158+
next unless ocgs.include?(imported_ocg)
159+
source_ocg[imported_ocg] = ocg
160+
target_ocg[ocg] = imported_ocg
161+
end
162+
163+
# Ensure the initial state of the OCGs is correct
164+
source_config = source.optional_content.default_configuration
165+
target_config = ocp.default_configuration
166+
ocgs.each do |ocg|
167+
target_config.ocg_state(ocg, source_config.ocg_state(source_ocg[ocg]))
168+
end
169+
170+
# Copy "radio button groups" from the source document and remove unknown OCGs from them
171+
source_config[:RBGroups]&.each do |array|
172+
result = array.map {|ocg| target_ocg[ocg] }.compact
173+
next if result.empty?
174+
(target_config[:RBGroups] ||= []) << result
175+
end
176+
end
177+
178+
end
179+
180+
end
181+
end

0 commit comments

Comments
 (0)