1 // Copyright 2024 The PDFium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
6
7 #include "core/fpdfapi/edit/cpdf_npagetooneexporter.h"
8
9 #include <algorithm>
10 #include <memory>
11 #include <sstream>
12 #include <utility>
13
14 #include "constants/page_object.h"
15 #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h"
16 #include "core/fpdfapi/page/cpdf_page.h"
17 #include "core/fpdfapi/parser/cpdf_array.h"
18 #include "core/fpdfapi/parser/cpdf_dictionary.h"
19 #include "core/fpdfapi/parser/cpdf_document.h"
20 #include "core/fpdfapi/parser/cpdf_name.h"
21 #include "core/fpdfapi/parser/cpdf_number.h"
22 #include "core/fpdfapi/parser/cpdf_object.h"
23 #include "core/fpdfapi/parser/cpdf_reference.h"
24 #include "core/fpdfapi/parser/cpdf_stream.h"
25 #include "core/fpdfapi/parser/cpdf_stream_acc.h"
26 #include "core/fxcrt/check.h"
27 #include "core/fxcrt/fx_safe_types.h"
28 #include "core/fxcrt/fx_string_wrappers.h"
29 #include "core/fxcrt/span.h"
30
31 namespace {
32
33 // Calculates the N-up parameters. When importing multiple pages into one page.
34 // The space of output page is evenly divided along the X axis and Y axis based
35 // on the input `pages_on_x_axis` and `pages_on_y_axis`.
36 class NupState {
37 public:
38 NupState(const CFX_SizeF& pagesize,
39 size_t pages_on_x_axis,
40 size_t pages_on_y_axis);
41
42 // Calculate sub page origin and scale with the source page of `pagesize` and
43 // new page of `sub_page_size_`.
44 CPDF_NPageToOneExporter::NupPageSettings CalculateNewPagePosition(
45 const CFX_SizeF& pagesize);
46
47 private:
48 // Helper function to get the `sub_x`, `sub_y` pair based on
49 // `sub_page_index_`. The space of output page is evenly divided into slots
50 // along x and y axis. `sub_x` and `sub_y` are 0-based indices that indicate
51 // which allocation slot to use.
52 std::pair<size_t, size_t> ConvertPageOrder() const;
53
54 // Given the `sub_x` and `sub_y` subpage position within a page, and a source
55 // page with dimensions of `pagesize`, calculate the sub page's origin and
56 // scale.
57 CPDF_NPageToOneExporter::NupPageSettings CalculatePageEdit(
58 size_t sub_x,
59 size_t sub_y,
60 const CFX_SizeF& pagesize) const;
61
62 const CFX_SizeF dest_page_size_;
63 const size_t pages_on_x_axis_;
64 const size_t pages_on_y_axis_;
65 const size_t pages_per_sheet_;
66 CFX_SizeF sub_page_size_;
67
68 // A 0-based index, in range of [0, pages_per_sheet_ - 1).
69 size_t sub_page_index_ = 0;
70 };
71
NupState(const CFX_SizeF & pagesize,size_t pages_on_x_axis,size_t pages_on_y_axis)72 NupState::NupState(const CFX_SizeF& pagesize,
73 size_t pages_on_x_axis,
74 size_t pages_on_y_axis)
75 : dest_page_size_(pagesize),
76 pages_on_x_axis_(pages_on_x_axis),
77 pages_on_y_axis_(pages_on_y_axis),
78 pages_per_sheet_(pages_on_x_axis * pages_on_y_axis) {
79 DCHECK(pages_on_x_axis_ > 0);
80 DCHECK(pages_on_y_axis_ > 0);
81 DCHECK(dest_page_size_.width > 0);
82 DCHECK(dest_page_size_.height > 0);
83
84 sub_page_size_.width = dest_page_size_.width / pages_on_x_axis_;
85 sub_page_size_.height = dest_page_size_.height / pages_on_y_axis_;
86 }
87
ConvertPageOrder() const88 std::pair<size_t, size_t> NupState::ConvertPageOrder() const {
89 size_t sub_x = sub_page_index_ % pages_on_x_axis_;
90 size_t sub_y = sub_page_index_ / pages_on_x_axis_;
91
92 // Y Axis, pages start from the top of the output page.
93 sub_y = pages_on_y_axis_ - sub_y - 1;
94
95 return {sub_x, sub_y};
96 }
97
CalculatePageEdit(size_t sub_x,size_t sub_y,const CFX_SizeF & pagesize) const98 CPDF_NPageToOneExporter::NupPageSettings NupState::CalculatePageEdit(
99 size_t sub_x,
100 size_t sub_y,
101 const CFX_SizeF& pagesize) const {
102 CPDF_NPageToOneExporter::NupPageSettings settings;
103 settings.sub_page_start_point.x = sub_x * sub_page_size_.width;
104 settings.sub_page_start_point.y = sub_y * sub_page_size_.height;
105
106 const float x_scale = sub_page_size_.width / pagesize.width;
107 const float y_scale = sub_page_size_.height / pagesize.height;
108 settings.scale = std::min(x_scale, y_scale);
109
110 float sub_width = pagesize.width * settings.scale;
111 float sub_height = pagesize.height * settings.scale;
112 if (x_scale > y_scale) {
113 settings.sub_page_start_point.x += (sub_page_size_.width - sub_width) / 2;
114 } else {
115 settings.sub_page_start_point.y += (sub_page_size_.height - sub_height) / 2;
116 }
117 return settings;
118 }
119
CalculateNewPagePosition(const CFX_SizeF & pagesize)120 CPDF_NPageToOneExporter::NupPageSettings NupState::CalculateNewPagePosition(
121 const CFX_SizeF& pagesize) {
122 if (sub_page_index_ >= pages_per_sheet_) {
123 sub_page_index_ = 0;
124 }
125
126 auto [sub_x, sub_y] = ConvertPageOrder();
127 ++sub_page_index_;
128 return CalculatePageEdit(sub_x, sub_y, pagesize);
129 }
130
131 // Helper that generates the content stream for a sub-page.
GenerateSubPageContentStream(const ByteString & xobject_name,const CPDF_NPageToOneExporter::NupPageSettings & settings)132 ByteString GenerateSubPageContentStream(
133 const ByteString& xobject_name,
134 const CPDF_NPageToOneExporter::NupPageSettings& settings) {
135 CFX_Matrix matrix;
136 matrix.Scale(settings.scale, settings.scale);
137 matrix.Translate(settings.sub_page_start_point.x,
138 settings.sub_page_start_point.y);
139
140 fxcrt::ostringstream content_stream;
141 content_stream << "q\n";
142 WriteMatrix(content_stream, matrix) << " cm\n"
143 << "/" << xobject_name << " Do Q\n";
144 return ByteString(content_stream);
145 }
146
147 } // namespace
148
CPDF_NPageToOneExporter(CPDF_Document * dest_doc,CPDF_Document * src_doc)149 CPDF_NPageToOneExporter::CPDF_NPageToOneExporter(CPDF_Document* dest_doc,
150 CPDF_Document* src_doc)
151 : CPDF_PageOrganizer(dest_doc, src_doc) {}
152
153 CPDF_NPageToOneExporter::~CPDF_NPageToOneExporter() = default;
154
ExportNPagesToOne(pdfium::span<const uint32_t> page_indices,const CFX_SizeF & dest_page_size,size_t pages_on_x_axis,size_t pages_on_y_axis)155 bool CPDF_NPageToOneExporter::ExportNPagesToOne(
156 pdfium::span<const uint32_t> page_indices,
157 const CFX_SizeF& dest_page_size,
158 size_t pages_on_x_axis,
159 size_t pages_on_y_axis) {
160 if (!Init()) {
161 return false;
162 }
163
164 FX_SAFE_SIZE_T safe_pages_per_sheet = pages_on_x_axis;
165 safe_pages_per_sheet *= pages_on_y_axis;
166 if (!safe_pages_per_sheet.IsValid()) {
167 return false;
168 }
169
170 ClearObjectNumberMap();
171 src_page_xobject_map_.clear();
172 size_t pages_per_sheet = safe_pages_per_sheet.ValueOrDie();
173 NupState n_up_state(dest_page_size, pages_on_x_axis, pages_on_y_axis);
174
175 FX_SAFE_INT32 curpage = 0;
176 const CFX_FloatRect dest_page_rect(0, 0, dest_page_size.width,
177 dest_page_size.height);
178 for (size_t outer_page_index = 0; outer_page_index < page_indices.size();
179 outer_page_index += pages_per_sheet) {
180 xobject_name_to_number_map_.clear();
181
182 RetainPtr<CPDF_Dictionary> dest_page_dict =
183 dest()->CreateNewPage(curpage.ValueOrDie());
184 if (!dest_page_dict) {
185 return false;
186 }
187
188 dest_page_dict->SetRectFor(pdfium::page_object::kMediaBox, dest_page_rect);
189 ByteString content;
190 size_t inner_page_max =
191 std::min(outer_page_index + pages_per_sheet, page_indices.size());
192 for (size_t i = outer_page_index; i < inner_page_max; ++i) {
193 RetainPtr<CPDF_Dictionary> src_page_dict =
194 src()->GetMutablePageDictionary(page_indices[i]);
195 if (!src_page_dict) {
196 return false;
197 }
198
199 auto src_page = pdfium::MakeRetain<CPDF_Page>(src(), src_page_dict);
200 src_page->AddPageImageCache();
201 NupPageSettings settings =
202 n_up_state.CalculateNewPagePosition(src_page->GetPageSize());
203 content += AddSubPage(src_page, settings);
204 }
205
206 FinishPage(dest_page_dict, content);
207 ++curpage;
208 }
209
210 return true;
211 }
212
213 // static
GenerateSubPageContentStreamForTesting(const ByteString & xobject_name,const NupPageSettings & settings)214 ByteString CPDF_NPageToOneExporter::GenerateSubPageContentStreamForTesting(
215 const ByteString& xobject_name,
216 const NupPageSettings& settings) {
217 return GenerateSubPageContentStream(xobject_name, settings);
218 }
219
AddSubPage(const RetainPtr<CPDF_Page> & src_page,const NupPageSettings & settings)220 ByteString CPDF_NPageToOneExporter::AddSubPage(
221 const RetainPtr<CPDF_Page>& src_page,
222 const NupPageSettings& settings) {
223 uint32_t src_page_obj_num = src_page->GetDict()->GetObjNum();
224 const auto it = src_page_xobject_map_.find(src_page_obj_num);
225 ByteString xobject_name = it != src_page_xobject_map_.end()
226 ? it->second
227 : MakeXObjectFromPage(src_page);
228 return GenerateSubPageContentStream(xobject_name, settings);
229 }
230
MakeXObjectFromPageRaw(RetainPtr<CPDF_Page> src_page)231 RetainPtr<CPDF_Stream> CPDF_NPageToOneExporter::MakeXObjectFromPageRaw(
232 RetainPtr<CPDF_Page> src_page) {
233 RetainPtr<const CPDF_Dictionary> src_page_dict = src_page->GetDict();
234 RetainPtr<const CPDF_Object> src_contents =
235 src_page_dict->GetDirectObjectFor(pdfium::page_object::kContents);
236
237 auto new_xobject =
238 dest()->NewIndirect<CPDF_Stream>(dest()->New<CPDF_Dictionary>());
239 RetainPtr<CPDF_Dictionary> new_xobject_dict = new_xobject->GetMutableDict();
240 static const char kResourceString[] = "Resources";
241 if (!CPDF_PageOrganizer::CopyInheritable(new_xobject_dict, src_page_dict,
242 kResourceString)) {
243 // Use a default empty resources if it does not exist.
244 new_xobject_dict->SetNewFor<CPDF_Dictionary>(kResourceString);
245 }
246 uint32_t src_page_obj_num = src_page_dict->GetObjNum();
247 uint32_t new_xobject_obj_num = new_xobject_dict->GetObjNum();
248 AddObjectMapping(src_page_obj_num, new_xobject_obj_num);
249 UpdateReference(new_xobject_dict);
250 new_xobject_dict->SetNewFor<CPDF_Name>("Type", "XObject");
251 new_xobject_dict->SetNewFor<CPDF_Name>("Subtype", "Form");
252 new_xobject_dict->SetNewFor<CPDF_Number>("FormType", 1);
253 new_xobject_dict->SetRectFor("BBox", src_page->GetBBox());
254 new_xobject_dict->SetMatrixFor("Matrix", src_page->GetPageMatrix());
255 if (!src_contents) {
256 return new_xobject;
257 }
258 const CPDF_Array* src_contents_array = src_contents->AsArray();
259 if (!src_contents_array) {
260 RetainPtr<const CPDF_Stream> stream(src_contents->AsStream());
261 auto acc = pdfium::MakeRetain<CPDF_StreamAcc>(std::move(stream));
262 acc->LoadAllDataFiltered();
263 new_xobject->SetDataAndRemoveFilter(acc->GetSpan());
264 return new_xobject;
265 }
266 ByteString src_content_stream;
267 for (size_t i = 0; i < src_contents_array->size(); ++i) {
268 RetainPtr<const CPDF_Stream> stream = src_contents_array->GetStreamAt(i);
269 auto acc = pdfium::MakeRetain<CPDF_StreamAcc>(std::move(stream));
270 acc->LoadAllDataFiltered();
271 src_content_stream += ByteStringView(acc->GetSpan());
272 src_content_stream += "\n";
273 }
274 new_xobject->SetDataAndRemoveFilter(src_content_stream.unsigned_span());
275 return new_xobject;
276 }
277
MakeXObjectFromPage(RetainPtr<CPDF_Page> src_page)278 ByteString CPDF_NPageToOneExporter::MakeXObjectFromPage(
279 RetainPtr<CPDF_Page> src_page) {
280 RetainPtr<CPDF_Stream> new_xobject = MakeXObjectFromPageRaw(src_page);
281
282 // TODO(xlou): A better name schema to avoid possible object name collision.
283 ByteString xobject_name = ByteString::Format("X%d", ++object_number_);
284 xobject_name_to_number_map_[xobject_name] = new_xobject->GetObjNum();
285 src_page_xobject_map_[src_page->GetDict()->GetObjNum()] = xobject_name;
286 return xobject_name;
287 }
288
289 std::unique_ptr<XObjectContext>
CreateXObjectContextFromPage(int src_page_index)290 CPDF_NPageToOneExporter::CreateXObjectContextFromPage(int src_page_index) {
291 RetainPtr<CPDF_Dictionary> src_page_dict =
292 src()->GetMutablePageDictionary(src_page_index);
293 if (!src_page_dict) {
294 return nullptr;
295 }
296
297 auto src_page = pdfium::MakeRetain<CPDF_Page>(src(), src_page_dict);
298 auto xobject = std::make_unique<XObjectContext>();
299 xobject->dest_doc = dest();
300 xobject->xobject.Reset(MakeXObjectFromPageRaw(src_page));
301 return xobject;
302 }
303
FinishPage(RetainPtr<CPDF_Dictionary> dest_page_dict,const ByteString & content)304 void CPDF_NPageToOneExporter::FinishPage(
305 RetainPtr<CPDF_Dictionary> dest_page_dict,
306 const ByteString& content) {
307 RetainPtr<CPDF_Dictionary> resources =
308 dest_page_dict->GetOrCreateDictFor(pdfium::page_object::kResources);
309 RetainPtr<CPDF_Dictionary> xobject = resources->GetOrCreateDictFor("XObject");
310 for (auto& it : xobject_name_to_number_map_) {
311 xobject->SetNewFor<CPDF_Reference>(it.first, dest(), it.second);
312 }
313
314 auto stream =
315 dest()->NewIndirect<CPDF_Stream>(dest()->New<CPDF_Dictionary>());
316 stream->SetData(content.unsigned_span());
317 dest_page_dict->SetNewFor<CPDF_Reference>(pdfium::page_object::kContents,
318 dest(), stream->GetObjNum());
319 }
320
321 XObjectContext::XObjectContext() = default;
322
323 XObjectContext::~XObjectContext() = default;
324