chromium/chromeos/utils/pdf_conversion.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/utils/pdf_conversion.h"

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "printing/units.h"
#include "third_party/skia/include/codec/SkCodec.h"
#include "third_party/skia/include/codec/SkJpegDecoder.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkTypes.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"
#include "ui/gfx/image/buffer_w_stream.h"

namespace chromeos {

namespace {

// The number of degrees to rotate a PDF image.
constexpr int kRotationDegrees = 180;

// Creates a new page for the PDF document and adds `image_data` to the page.
// `rotate` indicates whether the page should be rotated 180 degrees.
// Returns whether the page was successfully created.
bool AddPdfPage(sk_sp<SkDocument> pdf_doc,
                const sk_sp<SkData>& jpeg_image_data,
                bool rotate,
                std::optional<int> dpi) {
  if (!SkJpegDecoder::IsJpeg(jpeg_image_data->data(),
                             jpeg_image_data->size())) {
    LOG(ERROR) << "Not a valid JPEG image.";
    return false;
  }
  CHECK(
      SkJpegDecoder::IsJpeg(jpeg_image_data->data(), jpeg_image_data->size()));
  const sk_sp<SkImage> image =
      SkImages::DeferredFromEncodedData(jpeg_image_data);
  if (!image) {
    LOG(ERROR) << "Unable to generate image from encoded image data.";
    return false;
  }

  // Convert from JPG dimensions in pixels (DPI) to PDF dimensions in points
  // (1/72 in).
  int page_width;
  int page_height;
  if (dpi.has_value() && dpi.value() > 0) {
    page_width = printing::ConvertUnit(image->width(), dpi.value(),
                                       printing::kPointsPerInch);
    page_height = printing::ConvertUnit(image->height(), dpi.value(),
                                        printing::kPointsPerInch);
  } else {
    page_width = image->width();
    page_height = image->height();
  }
  SkCanvas* page_canvas = pdf_doc->beginPage(page_width, page_height);
  if (!page_canvas) {
    LOG(ERROR) << "Unable to access PDF page canvas.";
    return false;
  }

  // Rotate pages that were flipped by an ADF scanner.
  if (rotate) {
    page_canvas->rotate(kRotationDegrees);
    page_canvas->translate(-page_width, -page_height);
  }

  SkRect image_bounds = SkRect::MakeIWH(page_width, page_height);
  page_canvas->drawImageRect(image, image_bounds, SkSamplingOptions());
  pdf_doc->endPage();
  return true;
}

}  // namespace

bool ConvertJpgImagesToPdf(const std::vector<std::string>& jpg_images,
                           const base::FilePath& file_path,
                           bool rotate_alternate_pages,
                           std::optional<int> dpi) {
  DCHECK(!file_path.empty());

  // Register Jpeg Decoder for use by DeferredFromEncodedData in AddPdfPage.
  SkCodecs::Register(SkJpegDecoder::Decoder());

  SkFILEWStream pdf_outfile(file_path.value().c_str());
  if (!pdf_outfile.isValid()) {
    LOG(ERROR) << "Unable to open output file.";
    return false;
  }

  sk_sp<SkDocument> pdf_doc = SkPDF::MakeDocument(&pdf_outfile);
  DCHECK(pdf_doc);

  // Never rotate first page of PDF.
  bool rotate_current_page = false;
  for (const auto& jpg_image : jpg_images) {
    SkDynamicMemoryWStream img_stream;
    if (!img_stream.write(jpg_image.c_str(), jpg_image.size())) {
      LOG(ERROR) << "Unable to write image to dynamic memory stream.";
      return false;
    }

    const sk_sp<SkData> img_data = img_stream.detachAsData();
    if (img_data->isEmpty()) {
      LOG(ERROR) << "Stream data is empty.";
      return false;
    }

    if (!AddPdfPage(pdf_doc, img_data, rotate_current_page, dpi)) {
      LOG(ERROR) << "Unable to add new PDF page.";
      return false;
    }

    if (rotate_alternate_pages) {
      rotate_current_page = !rotate_current_page;
    }
  }

  pdf_doc->close();
  return true;
}

bool ConvertJpgImagesToPdf(const std::vector<std::vector<uint8_t>>& jpg_images,
                           std::vector<uint8_t>* output) {
  gfx::BufferWStream output_stream;
  sk_sp<SkDocument> pdf_doc = SkPDF::MakeDocument(&output_stream);
  DCHECK(pdf_doc);

  // Register Jpeg Decoder for use by DeferredFromEncodedData in AddPdfPage.
  SkCodecs::Register(SkJpegDecoder::Decoder());

  for (const auto& jpg_image : jpg_images) {
    SkDynamicMemoryWStream img_stream;
    bool result = img_stream.write(jpg_image.data(), jpg_image.size());
    CHECK(result);

    const sk_sp<SkData> img_data = img_stream.detachAsData();
    if (img_data->isEmpty()) {
      LOG(ERROR) << "Stream data is empty.";
      return false;
    }

    if (!AddPdfPage(pdf_doc, img_data, false, std::nullopt)) {
      LOG(ERROR) << "Unable to add new PDF page.";
      return false;
    }
  }

  pdf_doc->close();
  *output = output_stream.TakeBuffer();
  return true;
}

}  // namespace chromeos