Commit 8fb89e17 authored by Kyra Moed's avatar Kyra Moed Committed by Chromium LUCI CQ

scanning: Support multipage PDF in ScanService

Implement multipage PDF file format saving capabilities atop PNG to PDF
image format conversion.

Bug: b:161237869
Change-Id: Ib74311185a3e4a6535b88042f203db956a77669a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2580363
Commit-Queue: Kyra Moed <kmoed@google.com>
Reviewed-by: default avatarJesse Schettler <jschettler@chromium.org>
Cr-Commit-Position: refs/heads/master@{#836719}
parent faed3dcd
......@@ -58,7 +58,23 @@ struct PngImageData {
// |file_ext|.
std::string CreateFilename(const base::Time::Exploded& start_time,
uint32_t page_number,
const std::string& file_ext) {
const mojo_ipc::FileType file_type) {
std::string file_ext;
switch (file_type) {
case mojo_ipc::FileType::kPng:
file_ext = "png";
break;
case mojo_ipc::FileType::kJpg:
file_ext = "jpg";
break;
case mojo_ipc::FileType::kPdf:
// The filename of a PDF doesn't include the page number.
return base::StringPrintf("scan_%02d%02d%02d-%02d%02d%02d.pdf",
start_time.year, start_time.month,
start_time.day_of_month, start_time.hour,
start_time.minute, start_time.second);
}
return base::StringPrintf(
"scan_%02d%02d%02d-%02d%02d%02d_%d.%s", start_time.year, start_time.month,
start_time.day_of_month, start_time.hour, start_time.minute,
......@@ -142,27 +158,10 @@ base::Optional<PngImageData> GetPngData(sk_sp<SkData> img_data) {
return acquired_data;
}
// Converts |png_img| to PDF and writes the PDF to |file_path|.
// Returns whether the converted image was successfully saved.
bool SaveAsPdf(const std::string& png_img, const base::FilePath& file_path) {
SkDynamicMemoryWStream img_stream;
if (!img_stream.write(png_img.c_str(), png_img.size())) {
LOG(ERROR) << "Unable to write image to dynamic memory stream.";
return false;
}
sk_sp<SkData> img_data = img_stream.detachAsData();
if (img_data->isEmpty()) {
LOG(ERROR) << "Stream data is empty.";
return false;
}
base::Optional<PngImageData> acquired_data = GetPngData(img_data);
if (!acquired_data.has_value()) {
LOG(ERROR) << "Unable to process image data.";
return false;
}
// Converts |png_images| into a single PDF and writes the PDF to |file_path|.
// Returns whether the PDF was successfully saved.
bool SaveAsPdf(const std::vector<std::string>& png_images,
const base::FilePath& file_path) {
SkFILEWStream pdf_outfile(file_path.value().c_str());
if (!pdf_outfile.isValid()) {
LOG(ERROR) << "Unable to open output file.";
......@@ -171,12 +170,31 @@ bool SaveAsPdf(const std::string& png_img, const base::FilePath& file_path) {
sk_sp<SkDocument> pdf_doc = SkPDF::MakeDocument(&pdf_outfile);
SkASSERT(pdf_doc);
if (!AddPdfPage(pdf_doc, acquired_data.value())) {
LOG(ERROR) << "Unable to add new PDF page.";
return false;
for (const auto& png_img : png_images) {
SkDynamicMemoryWStream img_stream;
if (!img_stream.write(png_img.c_str(), png_img.size())) {
LOG(ERROR) << "Unable to write image to dynamic memory stream.";
return false;
}
sk_sp<SkData> img_data = img_stream.detachAsData();
if (img_data->isEmpty()) {
LOG(ERROR) << "Stream data is empty.";
return false;
}
base::Optional<PngImageData> acquired_data = GetPngData(img_data);
if (!acquired_data.has_value()) {
LOG(ERROR) << "Unable to process image data.";
return false;
}
if (!AddPdfPage(pdf_doc, acquired_data.value())) {
LOG(ERROR) << "Unable to add new PDF page.";
return false;
}
}
// TODO(kmoed): Add multipage scan functionality.
pdf_doc->close();
return true;
}
......@@ -188,28 +206,16 @@ base::FilePath SavePage(const base::FilePath& scan_to_path,
std::string scanned_image,
uint32_t page_number,
const base::Time::Exploded& start_time) {
std::string filename;
switch (file_type) {
case mojo_ipc::FileType::kPng:
filename = CreateFilename(start_time, page_number, "png");
if (!WriteImage(scan_to_path.Append(filename), scanned_image))
return base::FilePath();
break;
case mojo_ipc::FileType::kJpg:
filename = CreateFilename(start_time, page_number, "jpg");
scanned_image = PngToJpg(scanned_image);
if (scanned_image.empty() ||
!WriteImage(scan_to_path.Append(filename), scanned_image)) {
return base::FilePath();
}
break;
case mojo_ipc::FileType::kPdf:
filename = CreateFilename(start_time, page_number, "pdf");
if (!SaveAsPdf(scanned_image, scan_to_path.Append(filename)))
return base::FilePath();
break;
std::string filename = CreateFilename(start_time, page_number, file_type);
if (file_type == mojo_ipc::FileType::kPng) {
if (!WriteImage(scan_to_path.Append(filename), scanned_image))
return base::FilePath();
} else if (file_type == mojo_ipc::FileType::kJpg) {
scanned_image = PngToJpg(scanned_image);
if (scanned_image.empty() ||
!WriteImage(scan_to_path.Append(filename), scanned_image)) {
return base::FilePath();
}
}
return scan_to_path.Append(filename);
......@@ -279,6 +285,7 @@ void ScanService::StartScan(
base::Time::Now().LocalExplode(&start_time_);
save_failed_ = false;
last_scanned_file_path_.clear();
scanned_images_.clear();
lorgnette_scanner_manager_->Scan(
scanner_name, mojo::ConvertTo<lorgnette::ScanSettings>(settings),
base::BindRepeating(&ScanService::OnProgressPercentReceived,
......@@ -364,6 +371,17 @@ void ScanService::OnPageReceived(const base::FilePath& scan_to_path,
scan_job_observer_->OnPageComplete(
std::vector<uint8_t>(scanned_image.begin(), scanned_image.end()));
// If the selected file type is PDF, the PDF will be created after all the
// scanned images are received.
if (file_type == mojo_ipc::FileType::kPdf) {
scanned_images_.push_back(std::move(scanned_image));
if (last_scanned_file_path_.empty()) {
last_scanned_file_path_ = scan_to_path.Append(CreateFilename(
start_time_, /*not used*/ 0, mojo_ipc::FileType::kPdf));
}
return;
}
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(&SavePage, scan_to_path, file_type,
......@@ -373,6 +391,15 @@ void ScanService::OnPageReceived(const base::FilePath& scan_to_path,
}
void ScanService::OnScanCompleted(bool success) {
if (!scanned_images_.empty()) {
base::PostTaskAndReplyWithResult(
task_runner_.get(), FROM_HERE,
base::BindOnce(&SaveAsPdf, scanned_images_, last_scanned_file_path_),
base::BindOnce(&ScanService::OnAllPagesSaved,
weak_ptr_factory_.GetWeakPtr()));
return;
}
// Post a task to the task runner to ensure all the pages have been saved
// before reporting the scan job as complete.
base::PostTaskAndReplyWithResult(
......@@ -386,6 +413,7 @@ void ScanService::OnCancelCompleted(bool success) {
if (success) {
save_failed_ = false;
last_scanned_file_path_.clear();
scanned_images_.clear();
}
scan_job_observer_->OnCancelComplete(success);
}
......
......@@ -139,6 +139,9 @@ class ScanService : public scanning::mojom::ScanService, public KeyedService {
// Indicates whether there was a failure to save scanned images.
bool save_failed_;
// The scanned images used to create a multipage PDF.
std::vector<std::string> scanned_images_;
// The time a scan was started. Used in filenames when saving scanned images.
base::Time::Exploded start_time_;
......
......@@ -85,6 +85,15 @@ std::vector<base::FilePath> CreateSavedScanPaths(
return file_paths;
}
// Returns single FilePath to mimic saved PDF format scan.
base::FilePath CreateSavedPdfScanPath(const base::FilePath& dir,
const base::Time::Exploded& scan_time) {
return dir.Append(base::StringPrintf("scan_%02d%02d%02d-%02d%02d%02d.pdf",
scan_time.year, scan_time.month,
scan_time.day_of_month, scan_time.hour,
scan_time.minute, scan_time.second));
}
// Returns a manually generated PNG image.
std::string CreatePng() {
SkBitmap bitmap;
......@@ -340,19 +349,16 @@ TEST_F(ScanServiceTest, Scan) {
base::Time::Now().LocalExplode(&scan_time);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPng);
std::map<std::string, mojo_ipc::FileType> file_types = {
{"png", mojo_ipc::FileType::kPng},
{"jpg", mojo_ipc::FileType::kJpg},
{"pdf", mojo_ipc::FileType::kPdf}};
{"png", mojo_ipc::FileType::kPng}, {"jpg", mojo_ipc::FileType::kJpg}};
for (const auto& type : file_types) {
const std::vector<base::FilePath> saved_scan_paths = CreateSavedScanPaths(
temp_dir_.GetPath(), scan_time, type.first, scan_data.size());
for (const auto& saved_scan_path : saved_scan_paths)
EXPECT_FALSE(base::PathExists(saved_scan_path));
settings.file_type = type.second;
mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), type.second);
EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone()));
for (const auto& saved_scan_path : saved_scan_paths)
EXPECT_TRUE(base::PathExists(saved_scan_path));
......@@ -363,6 +369,32 @@ TEST_F(ScanServiceTest, Scan) {
}
}
// Test that a scan with PDF file format can be perfomed successfully.
TEST_F(ScanServiceTest, PdfScan) {
fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse(
{kFirstTestScannerName});
const std::vector<std::string> scan_data = {CreatePng(), CreatePng(),
CreatePng()};
fake_lorgnette_scanner_manager_.SetScanResponse(scan_data);
auto scanners = GetScanners();
ASSERT_EQ(scanners.size(), 1u);
base::Time::Exploded scan_time;
// Since we're using mock time, this is deterministic.
base::Time::Now().LocalExplode(&scan_time);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPdf);
const base::FilePath saved_scan_path =
CreateSavedPdfScanPath(temp_dir_.GetPath(), scan_time);
EXPECT_FALSE(base::PathExists(saved_scan_path));
EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone()));
EXPECT_TRUE(base::PathExists(saved_scan_path));
EXPECT_TRUE(fake_scan_job_observer_.scan_success());
EXPECT_EQ(saved_scan_path, fake_scan_job_observer_.last_scanned_file_path());
}
// Test that when a scan fails, the scan job is marked as failed.
TEST_F(ScanServiceTest, ScanFails) {
// Skip setting the scan data in FakeLorgnetteScannerManager so the scan will
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment