// Copyright 2024 The PDFium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com #include "core/fpdfapi/edit/cpdf_npagetooneexporter.h" #include #include #include #include #include "constants/page_object.h" #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" #include "core/fpdfapi/page/cpdf_page.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fxcrt/check.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/span.h" namespace { // Calculates the N-up parameters. When importing multiple pages into one page. // The space of output page is evenly divided along the X axis and Y axis based // on the input `pages_on_x_axis` and `pages_on_y_axis`. class NupState { public: NupState(const CFX_SizeF& pagesize, size_t pages_on_x_axis, size_t pages_on_y_axis); // Calculate sub page origin and scale with the source page of `pagesize` and // new page of `sub_page_size_`. CPDF_NPageToOneExporter::NupPageSettings CalculateNewPagePosition( const CFX_SizeF& pagesize); private: // Helper function to get the `sub_x`, `sub_y` pair based on // `sub_page_index_`. The space of output page is evenly divided into slots // along x and y axis. `sub_x` and `sub_y` are 0-based indices that indicate // which allocation slot to use. std::pair ConvertPageOrder() const; // Given the `sub_x` and `sub_y` subpage position within a page, and a source // page with dimensions of `pagesize`, calculate the sub page's origin and // scale. CPDF_NPageToOneExporter::NupPageSettings CalculatePageEdit( size_t sub_x, size_t sub_y, const CFX_SizeF& pagesize) const; const CFX_SizeF dest_page_size_; const size_t pages_on_x_axis_; const size_t pages_on_y_axis_; const size_t pages_per_sheet_; CFX_SizeF sub_page_size_; // A 0-based index, in range of [0, pages_per_sheet_ - 1). size_t sub_page_index_ = 0; }; NupState::NupState(const CFX_SizeF& pagesize, size_t pages_on_x_axis, size_t pages_on_y_axis) : dest_page_size_(pagesize), pages_on_x_axis_(pages_on_x_axis), pages_on_y_axis_(pages_on_y_axis), pages_per_sheet_(pages_on_x_axis * pages_on_y_axis) { DCHECK(pages_on_x_axis_ > 0); DCHECK(pages_on_y_axis_ > 0); DCHECK(dest_page_size_.width > 0); DCHECK(dest_page_size_.height > 0); sub_page_size_.width = dest_page_size_.width / pages_on_x_axis_; sub_page_size_.height = dest_page_size_.height / pages_on_y_axis_; } std::pair NupState::ConvertPageOrder() const { size_t sub_x = sub_page_index_ % pages_on_x_axis_; size_t sub_y = sub_page_index_ / pages_on_x_axis_; // Y Axis, pages start from the top of the output page. sub_y = pages_on_y_axis_ - sub_y - 1; return {sub_x, sub_y}; } CPDF_NPageToOneExporter::NupPageSettings NupState::CalculatePageEdit( size_t sub_x, size_t sub_y, const CFX_SizeF& pagesize) const { CPDF_NPageToOneExporter::NupPageSettings settings; settings.sub_page_start_point.x = sub_x * sub_page_size_.width; settings.sub_page_start_point.y = sub_y * sub_page_size_.height; const float x_scale = sub_page_size_.width / pagesize.width; const float y_scale = sub_page_size_.height / pagesize.height; settings.scale = std::min(x_scale, y_scale); float sub_width = pagesize.width * settings.scale; float sub_height = pagesize.height * settings.scale; if (x_scale > y_scale) { settings.sub_page_start_point.x += (sub_page_size_.width - sub_width) / 2; } else { settings.sub_page_start_point.y += (sub_page_size_.height - sub_height) / 2; } return settings; } CPDF_NPageToOneExporter::NupPageSettings NupState::CalculateNewPagePosition( const CFX_SizeF& pagesize) { if (sub_page_index_ >= pages_per_sheet_) { sub_page_index_ = 0; } auto [sub_x, sub_y] = ConvertPageOrder(); ++sub_page_index_; return CalculatePageEdit(sub_x, sub_y, pagesize); } // Helper that generates the content stream for a sub-page. ByteString GenerateSubPageContentStream( const ByteString& xobject_name, const CPDF_NPageToOneExporter::NupPageSettings& settings) { CFX_Matrix matrix; matrix.Scale(settings.scale, settings.scale); matrix.Translate(settings.sub_page_start_point.x, settings.sub_page_start_point.y); fxcrt::ostringstream content_stream; content_stream << "q\n"; WriteMatrix(content_stream, matrix) << " cm\n" << "/" << xobject_name << " Do Q\n"; return ByteString(content_stream); } } // namespace CPDF_NPageToOneExporter::CPDF_NPageToOneExporter(CPDF_Document* dest_doc, CPDF_Document* src_doc) : CPDF_PageOrganizer(dest_doc, src_doc) {} CPDF_NPageToOneExporter::~CPDF_NPageToOneExporter() = default; bool CPDF_NPageToOneExporter::ExportNPagesToOne( pdfium::span page_indices, const CFX_SizeF& dest_page_size, size_t pages_on_x_axis, size_t pages_on_y_axis) { if (!Init()) { return false; } FX_SAFE_SIZE_T safe_pages_per_sheet = pages_on_x_axis; safe_pages_per_sheet *= pages_on_y_axis; if (!safe_pages_per_sheet.IsValid()) { return false; } ClearObjectNumberMap(); src_page_xobject_map_.clear(); size_t pages_per_sheet = safe_pages_per_sheet.ValueOrDie(); NupState n_up_state(dest_page_size, pages_on_x_axis, pages_on_y_axis); FX_SAFE_INT32 curpage = 0; const CFX_FloatRect dest_page_rect(0, 0, dest_page_size.width, dest_page_size.height); for (size_t outer_page_index = 0; outer_page_index < page_indices.size(); outer_page_index += pages_per_sheet) { xobject_name_to_number_map_.clear(); RetainPtr dest_page_dict = dest()->CreateNewPage(curpage.ValueOrDie()); if (!dest_page_dict) { return false; } dest_page_dict->SetRectFor(pdfium::page_object::kMediaBox, dest_page_rect); ByteString content; size_t inner_page_max = std::min(outer_page_index + pages_per_sheet, page_indices.size()); for (size_t i = outer_page_index; i < inner_page_max; ++i) { RetainPtr src_page_dict = src()->GetMutablePageDictionary(page_indices[i]); if (!src_page_dict) { return false; } auto src_page = pdfium::MakeRetain(src(), src_page_dict); src_page->AddPageImageCache(); NupPageSettings settings = n_up_state.CalculateNewPagePosition(src_page->GetPageSize()); content += AddSubPage(src_page, settings); } FinishPage(dest_page_dict, content); ++curpage; } return true; } // static ByteString CPDF_NPageToOneExporter::GenerateSubPageContentStreamForTesting( const ByteString& xobject_name, const NupPageSettings& settings) { return GenerateSubPageContentStream(xobject_name, settings); } ByteString CPDF_NPageToOneExporter::AddSubPage( const RetainPtr& src_page, const NupPageSettings& settings) { uint32_t src_page_obj_num = src_page->GetDict()->GetObjNum(); const auto it = src_page_xobject_map_.find(src_page_obj_num); ByteString xobject_name = it != src_page_xobject_map_.end() ? it->second : MakeXObjectFromPage(src_page); return GenerateSubPageContentStream(xobject_name, settings); } RetainPtr CPDF_NPageToOneExporter::MakeXObjectFromPageRaw( RetainPtr src_page) { RetainPtr src_page_dict = src_page->GetDict(); RetainPtr src_contents = src_page_dict->GetDirectObjectFor(pdfium::page_object::kContents); auto new_xobject = dest()->NewIndirect(dest()->New()); RetainPtr new_xobject_dict = new_xobject->GetMutableDict(); static const char kResourceString[] = "Resources"; if (!CPDF_PageOrganizer::CopyInheritable(new_xobject_dict, src_page_dict, kResourceString)) { // Use a default empty resources if it does not exist. new_xobject_dict->SetNewFor(kResourceString); } uint32_t src_page_obj_num = src_page_dict->GetObjNum(); uint32_t new_xobject_obj_num = new_xobject_dict->GetObjNum(); AddObjectMapping(src_page_obj_num, new_xobject_obj_num); UpdateReference(new_xobject_dict); new_xobject_dict->SetNewFor("Type", "XObject"); new_xobject_dict->SetNewFor("Subtype", "Form"); new_xobject_dict->SetNewFor("FormType", 1); new_xobject_dict->SetRectFor("BBox", src_page->GetBBox()); new_xobject_dict->SetMatrixFor("Matrix", src_page->GetPageMatrix()); if (!src_contents) { return new_xobject; } const CPDF_Array* src_contents_array = src_contents->AsArray(); if (!src_contents_array) { RetainPtr stream(src_contents->AsStream()); auto acc = pdfium::MakeRetain(std::move(stream)); acc->LoadAllDataFiltered(); new_xobject->SetDataAndRemoveFilter(acc->GetSpan()); return new_xobject; } ByteString src_content_stream; for (size_t i = 0; i < src_contents_array->size(); ++i) { RetainPtr stream = src_contents_array->GetStreamAt(i); auto acc = pdfium::MakeRetain(std::move(stream)); acc->LoadAllDataFiltered(); src_content_stream += ByteStringView(acc->GetSpan()); src_content_stream += "\n"; } new_xobject->SetDataAndRemoveFilter(src_content_stream.unsigned_span()); return new_xobject; } ByteString CPDF_NPageToOneExporter::MakeXObjectFromPage( RetainPtr src_page) { RetainPtr new_xobject = MakeXObjectFromPageRaw(src_page); // TODO(xlou): A better name schema to avoid possible object name collision. ByteString xobject_name = ByteString::Format("X%d", ++object_number_); xobject_name_to_number_map_[xobject_name] = new_xobject->GetObjNum(); src_page_xobject_map_[src_page->GetDict()->GetObjNum()] = xobject_name; return xobject_name; } std::unique_ptr CPDF_NPageToOneExporter::CreateXObjectContextFromPage(int src_page_index) { RetainPtr src_page_dict = src()->GetMutablePageDictionary(src_page_index); if (!src_page_dict) { return nullptr; } auto src_page = pdfium::MakeRetain(src(), src_page_dict); auto xobject = std::make_unique(); xobject->dest_doc = dest(); xobject->xobject.Reset(MakeXObjectFromPageRaw(src_page)); return xobject; } void CPDF_NPageToOneExporter::FinishPage( RetainPtr dest_page_dict, const ByteString& content) { RetainPtr resources = dest_page_dict->GetOrCreateDictFor(pdfium::page_object::kResources); RetainPtr xobject = resources->GetOrCreateDictFor("XObject"); for (auto& it : xobject_name_to_number_map_) { xobject->SetNewFor(it.first, dest(), it.second); } auto stream = dest()->NewIndirect(dest()->New()); stream->SetData(content.unsigned_span()); dest_page_dict->SetNewFor(pdfium::page_object::kContents, dest(), stream->GetObjNum()); } XObjectContext::XObjectContext() = default; XObjectContext::~XObjectContext() = default;