| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "pdf/pdfium/pdfium_print.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/strings/string_number_conversions.h" |
| #include "pdf/pdf_transform.h" |
| #include "pdf/pdfium/pdfium_engine.h" |
| #include "pdf/pdfium/pdfium_mem_buffer_file_read.h" |
| #include "pdf/pdfium/pdfium_mem_buffer_file_write.h" |
| #include "ppapi/c/dev/ppp_printing_dev.h" |
| #include "ppapi/c/private/ppp_pdf.h" |
| #include "printing/nup_parameters.h" |
| #include "printing/units.h" |
| #include "third_party/pdfium/public/cpp/fpdf_scopers.h" |
| #include "third_party/pdfium/public/fpdf_flatten.h" |
| #include "third_party/pdfium/public/fpdf_ppo.h" |
| #include "third_party/pdfium/public/fpdf_transformpage.h" |
| #include "ui/gfx/codec/jpeg_codec.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| using printing::ConvertUnit; |
| using printing::ConvertUnitDouble; |
| using printing::kPointsPerInch; |
| |
| namespace chrome_pdf { |
| |
| namespace { |
| |
| // UI should have done parameter sanity check, when execution |
| // reaches here, |pages_per_sheet| should be a positive integer. |
| bool ShouldDoNup(int pages_per_sheet) { |
| return pages_per_sheet > 1; |
| } |
| |
| // Check the source doc orientation. Returns true if the doc is landscape. |
| // For now the orientation of the doc is determined by its first page's |
| // orientation. Improvement can be added in the future to better determine the |
| // orientation of the source docs that have mixed orientation. |
| // TODO(xlou): rotate pages if the source doc has mixed orientation. So that |
| // the orientation of all pages of the doc are uniform. Pages of square size |
| // will not be rotated. |
| bool IsSourcePdfLandscape(FPDF_DOCUMENT doc) { |
| DCHECK(doc); |
| |
| ScopedFPDFPage pdf_page(FPDF_LoadPage(doc, 0)); |
| DCHECK(pdf_page); |
| |
| bool is_source_landscape = |
| FPDF_GetPageWidth(pdf_page.get()) > FPDF_GetPageHeight(pdf_page.get()); |
| return is_source_landscape; |
| } |
| |
| // Set the destination page size and content area in points based on source |
| // page rotation and orientation. |
| // |
| // |rotated| True if source page is rotated 90 degree or 270 degree. |
| // |is_src_page_landscape| is true if the source page orientation is landscape. |
| // |page_size| has the actual destination page size in points. |
| // |content_rect| has the actual destination page printable area values in |
| // points. |
| void SetPageSizeAndContentRect(bool rotated, |
| bool is_src_page_landscape, |
| pp::Size* page_size, |
| pp::Rect* content_rect) { |
| bool is_dst_page_landscape = page_size->width() > page_size->height(); |
| bool page_orientation_mismatched = |
| is_src_page_landscape != is_dst_page_landscape; |
| bool rotate_dst_page = rotated ^ page_orientation_mismatched; |
| if (rotate_dst_page) { |
| page_size->SetSize(page_size->height(), page_size->width()); |
| content_rect->SetRect(content_rect->y(), content_rect->x(), |
| content_rect->height(), content_rect->width()); |
| } |
| } |
| |
| // Transform |page| contents to fit in the selected printer paper size. |
| void TransformPDFPageForPrinting(FPDF_PAGE page, |
| double scale_factor, |
| const PP_PrintSettings_Dev& print_settings) { |
| // Get the source page width and height in points. |
| const double src_page_width = FPDF_GetPageWidth(page); |
| const double src_page_height = FPDF_GetPageHeight(page); |
| |
| const int src_page_rotation = FPDFPage_GetRotation(page); |
| const bool fit_to_page = print_settings.print_scaling_option == |
| PP_PRINTSCALINGOPTION_FIT_TO_PRINTABLE_AREA; |
| |
| pp::Size page_size(print_settings.paper_size); |
| pp::Rect content_rect(print_settings.printable_area); |
| const bool rotated = (src_page_rotation % 2 == 1); |
| SetPageSizeAndContentRect(rotated, src_page_width > src_page_height, |
| &page_size, &content_rect); |
| |
| // Compute the screen page width and height in points. |
| const int actual_page_width = |
| rotated ? page_size.height() : page_size.width(); |
| const int actual_page_height = |
| rotated ? page_size.width() : page_size.height(); |
| |
| const gfx::Rect gfx_content_rect(content_rect.x(), content_rect.y(), |
| content_rect.width(), content_rect.height()); |
| if (fit_to_page) { |
| scale_factor = CalculateScaleFactor(gfx_content_rect, src_page_width, |
| src_page_height, rotated); |
| } |
| |
| // Calculate positions for the clip box. |
| PdfRectangle media_box; |
| PdfRectangle crop_box; |
| bool has_media_box = |
| !!FPDFPage_GetMediaBox(page, &media_box.left, &media_box.bottom, |
| &media_box.right, &media_box.top); |
| bool has_crop_box = !!FPDFPage_GetCropBox( |
| page, &crop_box.left, &crop_box.bottom, &crop_box.right, &crop_box.top); |
| CalculateMediaBoxAndCropBox(rotated, has_media_box, has_crop_box, &media_box, |
| &crop_box); |
| PdfRectangle source_clip_box = CalculateClipBoxBoundary(media_box, crop_box); |
| ScalePdfRectangle(scale_factor, &source_clip_box); |
| |
| // Calculate the translation offset values. |
| double offset_x = 0; |
| double offset_y = 0; |
| if (fit_to_page) { |
| CalculateScaledClipBoxOffset(gfx_content_rect, source_clip_box, &offset_x, |
| &offset_y); |
| } else { |
| CalculateNonScaledClipBoxOffset(gfx_content_rect, src_page_rotation, |
| actual_page_width, actual_page_height, |
| source_clip_box, &offset_x, &offset_y); |
| } |
| |
| // Reset the media box and crop box. When the page has crop box and media box, |
| // the plugin will display the crop box contents and not the entire media box. |
| // If the pages have different crop box values, the plugin will display a |
| // document of multiple page sizes. To give better user experience, we |
| // decided to have same crop box and media box values. Hence, the user will |
| // see a list of uniform pages. |
| FPDFPage_SetMediaBox(page, 0, 0, page_size.width(), page_size.height()); |
| FPDFPage_SetCropBox(page, 0, 0, page_size.width(), page_size.height()); |
| |
| // Transformation is not required, return. Do this check only after updating |
| // the media box and crop box. For more detailed information, please refer to |
| // the comment block right before FPDF_SetMediaBox and FPDF_GetMediaBox calls. |
| if (scale_factor == 1.0 && offset_x == 0 && offset_y == 0) |
| return; |
| |
| // All the positions have been calculated, now manipulate the PDF. |
| FS_MATRIX matrix = {static_cast<float>(scale_factor), |
| 0, |
| 0, |
| static_cast<float>(scale_factor), |
| static_cast<float>(offset_x), |
| static_cast<float>(offset_y)}; |
| FS_RECTF cliprect = {static_cast<float>(source_clip_box.left + offset_x), |
| static_cast<float>(source_clip_box.top + offset_y), |
| static_cast<float>(source_clip_box.right + offset_x), |
| static_cast<float>(source_clip_box.bottom + offset_y)}; |
| FPDFPage_TransFormWithClip(page, &matrix, &cliprect); |
| FPDFPage_TransformAnnots(page, scale_factor, 0, 0, scale_factor, offset_x, |
| offset_y); |
| } |
| |
| void FitContentsToPrintableAreaIfRequired( |
| FPDF_DOCUMENT doc, |
| double scale_factor, |
| const PP_PrintSettings_Dev& print_settings) { |
| // Check to see if we need to fit pdf contents to printer paper size. |
| if (print_settings.print_scaling_option == PP_PRINTSCALINGOPTION_SOURCE_SIZE) |
| return; |
| |
| int num_pages = FPDF_GetPageCount(doc); |
| // In-place transformation is more efficient than creating a new |
| // transformed document from the source document. Therefore, transform |
| // every page to fit the contents in the selected printer paper. |
| for (int i = 0; i < num_pages; ++i) { |
| ScopedFPDFPage page(FPDF_LoadPage(doc, i)); |
| TransformPDFPageForPrinting(page.get(), scale_factor, print_settings); |
| } |
| } |
| |
| int GetBlockForJpeg(void* param, |
| unsigned long pos, |
| unsigned char* buf, |
| unsigned long size) { |
| std::vector<uint8_t>* data_vector = static_cast<std::vector<uint8_t>*>(param); |
| if (pos + size < pos || pos + size > data_vector->size()) |
| return 0; |
| memcpy(buf, data_vector->data() + pos, size); |
| return 1; |
| } |
| |
| std::string GetPageRangeStringFromRange( |
| const PP_PrintPageNumberRange_Dev* page_ranges, |
| uint32_t page_range_count) { |
| DCHECK(page_range_count); |
| |
| std::string page_number_str; |
| for (uint32_t i = 0; i < page_range_count; ++i) { |
| if (!page_number_str.empty()) |
| page_number_str.push_back(','); |
| const PP_PrintPageNumberRange_Dev& range = page_ranges[i]; |
| page_number_str.append(base::UintToString(range.first_page_number + 1)); |
| if (range.first_page_number != range.last_page_number) { |
| page_number_str.push_back('-'); |
| page_number_str.append(base::UintToString(range.last_page_number + 1)); |
| } |
| } |
| return page_number_str; |
| } |
| |
| } // namespace |
| |
| PDFiumPrint::PDFiumPrint(PDFiumEngine* engine) : engine_(engine) {} |
| |
| PDFiumPrint::~PDFiumPrint() = default; |
| |
| // static |
| std::vector<uint32_t> PDFiumPrint::GetPageNumbersFromPrintPageNumberRange( |
| const PP_PrintPageNumberRange_Dev* page_ranges, |
| uint32_t page_range_count) { |
| DCHECK(page_range_count); |
| |
| std::vector<uint32_t> page_numbers; |
| for (uint32_t i = 0; i < page_range_count; ++i) { |
| for (uint32_t page_number = page_ranges[i].first_page_number; |
| page_number <= page_ranges[i].last_page_number; ++page_number) { |
| page_numbers.push_back(page_number); |
| } |
| } |
| return page_numbers; |
| } |
| |
| pp::Buffer_Dev PDFiumPrint::PrintPagesAsRasterPDF( |
| const PP_PrintPageNumberRange_Dev* page_ranges, |
| uint32_t page_range_count, |
| const PP_PrintSettings_Dev& print_settings, |
| const PP_PdfPrintSettings_Dev& pdf_print_settings) { |
| std::vector<PDFiumPage> pages_to_print; |
| // width and height of source PDF pages. |
| std::vector<std::pair<double, double>> source_page_sizes; |
| // Collect pages to print and sizes of source pages. |
| std::vector<uint32_t> page_numbers = |
| PDFiumPrint::GetPageNumbersFromPrintPageNumberRange(page_ranges, |
| page_range_count); |
| for (uint32_t page_number : page_numbers) { |
| ScopedFPDFPage pdf_page(FPDF_LoadPage(engine_->doc(), page_number)); |
| double source_page_width = FPDF_GetPageWidth(pdf_page.get()); |
| double source_page_height = FPDF_GetPageHeight(pdf_page.get()); |
| source_page_sizes.push_back( |
| std::make_pair(source_page_width, source_page_height)); |
| // For computing size in pixels, use a square dpi since the source PDF page |
| // has square DPI. |
| int width_in_pixels = |
| ConvertUnit(source_page_width, kPointsPerInch, print_settings.dpi); |
| int height_in_pixels = |
| ConvertUnit(source_page_height, kPointsPerInch, print_settings.dpi); |
| |
| pp::Rect rect(width_in_pixels, height_in_pixels); |
| pages_to_print.push_back(PDFiumPage(engine_, page_number, rect, true)); |
| } |
| |
| ScopedFPDFDocument output_doc(FPDF_CreateNewDocument()); |
| DCHECK(output_doc); |
| |
| size_t i = 0; |
| for (; i < pages_to_print.size(); ++i) { |
| double source_page_width = source_page_sizes[i].first; |
| double source_page_height = source_page_sizes[i].second; |
| |
| // Use |temp_doc| to compress image by saving PDF to |buffer|. |
| pp::Buffer_Dev buffer; |
| { |
| ScopedFPDFDocument temp_doc( |
| CreateSinglePageRasterPdf(source_page_width, source_page_height, |
| print_settings, &pages_to_print[i])); |
| |
| if (!temp_doc) |
| break; |
| |
| buffer = GetFlattenedPrintData(temp_doc.get()); |
| } |
| |
| PDFiumMemBufferFileRead file_read(buffer.data(), buffer.size()); |
| ScopedFPDFDocument temp_doc(FPDF_LoadCustomDocument(&file_read, nullptr)); |
| if (!FPDF_ImportPages(output_doc.get(), temp_doc.get(), "1", i)) |
| break; |
| } |
| |
| pp::Buffer_Dev buffer; |
| if (i == pages_to_print.size()) { |
| FPDF_CopyViewerPreferences(output_doc.get(), engine_->doc()); |
| uint32_t pages_per_sheet = pdf_print_settings.pages_per_sheet; |
| uint32_t scale_factor = pdf_print_settings.scale_factor; |
| if (ShouldDoNup(pages_per_sheet)) { |
| buffer = NupPdfToPdf(output_doc.get(), pages_per_sheet, print_settings); |
| } else { |
| FitContentsToPrintableAreaIfRequired( |
| output_doc.get(), scale_factor / 100.0f, print_settings); |
| buffer = GetPrintData(output_doc.get()); |
| } |
| } |
| |
| return buffer; |
| } |
| |
| pp::Buffer_Dev PDFiumPrint::PrintPagesAsPDF( |
| const PP_PrintPageNumberRange_Dev* page_ranges, |
| uint32_t page_range_count, |
| const PP_PrintSettings_Dev& print_settings, |
| const PP_PdfPrintSettings_Dev& pdf_print_settings) { |
| ScopedFPDFDocument output_doc(FPDF_CreateNewDocument()); |
| DCHECK(output_doc); |
| FPDF_CopyViewerPreferences(output_doc.get(), engine_->doc()); |
| |
| std::string page_number_str = |
| GetPageRangeStringFromRange(page_ranges, page_range_count); |
| if (!FPDF_ImportPages(output_doc.get(), engine_->doc(), |
| page_number_str.c_str(), 0)) { |
| return pp::Buffer_Dev(); |
| } |
| |
| // Now flatten all the output pages. |
| if (!FlattenPrintData(output_doc.get())) |
| return pp::Buffer_Dev(); |
| |
| pp::Buffer_Dev buffer; |
| uint32_t pages_per_sheet = pdf_print_settings.pages_per_sheet; |
| uint32_t scale_factor = pdf_print_settings.scale_factor; |
| if (ShouldDoNup(pages_per_sheet)) { |
| buffer = NupPdfToPdf(output_doc.get(), pages_per_sheet, print_settings); |
| } else { |
| FitContentsToPrintableAreaIfRequired(output_doc.get(), |
| scale_factor / 100.0f, print_settings); |
| buffer = GetPrintData(output_doc.get()); |
| } |
| |
| return buffer; |
| } |
| |
| FPDF_DOCUMENT PDFiumPrint::CreateSinglePageRasterPdf( |
| double source_page_width, |
| double source_page_height, |
| const PP_PrintSettings_Dev& print_settings, |
| PDFiumPage* page_to_print) { |
| FPDF_DOCUMENT temp_doc = FPDF_CreateNewDocument(); |
| DCHECK(temp_doc); |
| |
| const pp::Size& bitmap_size(page_to_print->rect().size()); |
| |
| pp::ImageData image = |
| pp::ImageData(engine_->GetPluginInstance(), |
| PP_IMAGEDATAFORMAT_BGRA_PREMUL, bitmap_size, false); |
| |
| ScopedFPDFBitmap bitmap( |
| FPDFBitmap_CreateEx(bitmap_size.width(), bitmap_size.height(), |
| FPDFBitmap_BGRx, image.data(), image.stride())); |
| |
| // Clear the bitmap |
| FPDFBitmap_FillRect(bitmap.get(), 0, 0, bitmap_size.width(), |
| bitmap_size.height(), 0xFFFFFFFF); |
| |
| pp::Rect page_rect = page_to_print->rect(); |
| FPDF_RenderPageBitmap(bitmap.get(), page_to_print->GetPrintPage(), |
| page_rect.x(), page_rect.y(), page_rect.width(), |
| page_rect.height(), print_settings.orientation, |
| FPDF_PRINTING | FPDF_NO_CATCH); |
| |
| // Draw the forms. |
| FPDF_FFLDraw(engine_->form(), bitmap.get(), page_to_print->GetPrintPage(), |
| page_rect.x(), page_rect.y(), page_rect.width(), |
| page_rect.height(), print_settings.orientation, |
| FPDF_ANNOT | FPDF_PRINTING | FPDF_NO_CATCH); |
| |
| unsigned char* bitmap_data = |
| static_cast<unsigned char*>(FPDFBitmap_GetBuffer(bitmap.get())); |
| double ratio_x = ConvertUnitDouble(bitmap_size.width(), print_settings.dpi, |
| kPointsPerInch); |
| double ratio_y = ConvertUnitDouble(bitmap_size.height(), print_settings.dpi, |
| kPointsPerInch); |
| |
| // Add the bitmap to an image object and add the image object to the output |
| // page. |
| FPDF_PAGEOBJECT temp_img = FPDFPageObj_NewImageObj(temp_doc); |
| |
| bool encoded = false; |
| std::vector<uint8_t> compressed_bitmap_data; |
| if (!(print_settings.format & PP_PRINTOUTPUTFORMAT_PDF)) { |
| // Use quality = 40 as this does not significantly degrade the printed |
| // document relative to a normal bitmap and provides better compression than |
| // a higher quality setting. |
| const int kQuality = 40; |
| SkImageInfo info = SkImageInfo::Make( |
| FPDFBitmap_GetWidth(bitmap.get()), FPDFBitmap_GetHeight(bitmap.get()), |
| kBGRA_8888_SkColorType, kOpaque_SkAlphaType); |
| SkPixmap src(info, bitmap_data, FPDFBitmap_GetStride(bitmap.get())); |
| encoded = gfx::JPEGCodec::Encode(src, kQuality, &compressed_bitmap_data); |
| } |
| |
| { |
| ScopedFPDFPage temp_page_holder( |
| FPDFPage_New(temp_doc, 0, source_page_width, source_page_height)); |
| FPDF_PAGE temp_page = temp_page_holder.get(); |
| if (encoded) { |
| FPDF_FILEACCESS file_access = {}; |
| file_access.m_FileLen = |
| static_cast<unsigned long>(compressed_bitmap_data.size()); |
| file_access.m_GetBlock = &GetBlockForJpeg; |
| file_access.m_Param = &compressed_bitmap_data; |
| |
| FPDFImageObj_LoadJpegFileInline(&temp_page, 1, temp_img, &file_access); |
| } else { |
| FPDFImageObj_SetBitmap(&temp_page, 1, temp_img, bitmap.get()); |
| } |
| |
| FPDFImageObj_SetMatrix(temp_img, ratio_x, 0, 0, ratio_y, 0, 0); |
| FPDFPage_InsertObject(temp_page, temp_img); |
| FPDFPage_GenerateContent(temp_page); |
| } |
| |
| page_to_print->ClosePrintPage(); |
| return temp_doc; |
| } |
| |
| pp::Buffer_Dev PDFiumPrint::NupPdfToPdf( |
| FPDF_DOCUMENT doc, |
| uint32_t pages_per_sheet, |
| const PP_PrintSettings_Dev& print_settings) { |
| DCHECK(doc); |
| DCHECK(ShouldDoNup(pages_per_sheet)); |
| |
| PP_Size page_size = print_settings.paper_size; |
| |
| printing::NupParameters nup_params; |
| bool is_landscape = IsSourcePdfLandscape(doc); |
| nup_params.SetParameters(pages_per_sheet, is_landscape); |
| |
| // Import n pages to one. |
| bool paper_is_landscape = page_size.width > page_size.height; |
| if (nup_params.landscape() != paper_is_landscape) |
| std::swap(page_size.width, page_size.height); |
| |
| ScopedFPDFDocument output_doc_nup(FPDF_ImportNPagesToOne( |
| doc, page_size.width, page_size.height, nup_params.num_pages_on_x_axis(), |
| nup_params.num_pages_on_y_axis())); |
| if (!output_doc_nup) |
| return pp::Buffer_Dev(); |
| |
| FitContentsToPrintableAreaIfRequired(output_doc_nup.get(), 1.0f, |
| print_settings); |
| return GetPrintData(output_doc_nup.get()); |
| } |
| |
| bool PDFiumPrint::FlattenPrintData(FPDF_DOCUMENT doc) { |
| DCHECK(doc); |
| |
| ScopedSubstFont scoped_subst_font(engine_); |
| int page_count = FPDF_GetPageCount(doc); |
| for (int i = 0; i < page_count; ++i) { |
| ScopedFPDFPage page(FPDF_LoadPage(doc, i)); |
| DCHECK(page); |
| if (FPDFPage_Flatten(page.get(), FLAT_PRINT) == FLATTEN_FAIL) |
| return false; |
| } |
| return true; |
| } |
| |
| pp::Buffer_Dev PDFiumPrint::GetPrintData(FPDF_DOCUMENT doc) { |
| DCHECK(doc); |
| |
| pp::Buffer_Dev buffer; |
| PDFiumMemBufferFileWrite output_file_write; |
| if (FPDF_SaveAsCopy(doc, &output_file_write, 0)) { |
| size_t size = output_file_write.size(); |
| buffer = pp::Buffer_Dev(engine_->GetPluginInstance(), size); |
| if (!buffer.is_null()) |
| memcpy(buffer.data(), output_file_write.buffer().c_str(), size); |
| } |
| return buffer; |
| } |
| |
| pp::Buffer_Dev PDFiumPrint::GetFlattenedPrintData(FPDF_DOCUMENT doc) { |
| DCHECK(doc); |
| |
| pp::Buffer_Dev buffer; |
| if (FlattenPrintData(doc)) |
| buffer = GetPrintData(doc); |
| return buffer; |
| } |
| |
| } // namespace chrome_pdf |