CCA: Render the first page of PDF to an image as the thumbnail
The previous PDF image extraction logic is error-prone and hard to
maintain. This CL renders the first page of PDF to an image instead of
extracting the first image in the PDF as the thumbnail.
Bug: 330084715, 329069826
Test: Gallery button can display PDFs from CCA and other sources correctly
Change-Id: I0655cbeaec17ed4e00902ec1f74331e603fb0e5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5392765
Reviewed-by: Ashley Prasad <ashleydp@google.com>
Commit-Queue: Chu-Hsuan Yang <chuhsuan@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Sean Li <seannli@google.com>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1292827}
diff --git a/ash/webui/camera_app_ui/camera_app_helper.mojom b/ash/webui/camera_app_ui/camera_app_helper.mojom
index d7579aa..51132e5 100644
--- a/ash/webui/camera_app_ui/camera_app_helper.mojom
+++ b/ash/webui/camera_app_ui/camera_app_helper.mojom
@@ -311,4 +311,7 @@
// Calling the Update() whenever the screen is locked/unlocked.
SetScreenLockedMonitor(pending_remote<ScreenLockedMonitor> monitor)
=> (bool is_screen_locked);
+
+ // Returns the first page of a PDF as a JPEG.
+ RenderPdfAsJpeg(array<uint8> pdf_data) => (array<uint8> jpeg_data);
};
diff --git a/ash/webui/camera_app_ui/camera_app_helper_impl.cc b/ash/webui/camera_app_ui/camera_app_helper_impl.cc
index b3e30fd..72b4679 100644
--- a/ash/webui/camera_app_ui/camera_app_helper_impl.cc
+++ b/ash/webui/camera_app_ui/camera_app_helper_impl.cc
@@ -628,4 +628,9 @@
ash::SessionManagerClient::Get()->IsScreenLocked());
}
+void CameraAppHelperImpl::RenderPdfAsJpeg(const std::vector<uint8_t>& pdf_data,
+ RenderPdfAsJpegCallback callback) {
+ camera_app_ui_->delegate()->RenderPdfAsJpeg(pdf_data, std::move(callback));
+}
+
} // namespace ash
diff --git a/ash/webui/camera_app_ui/camera_app_helper_impl.h b/ash/webui/camera_app_ui/camera_app_helper_impl.h
index 3c9f4dc..07fa006 100644
--- a/ash/webui/camera_app_ui/camera_app_helper_impl.h
+++ b/ash/webui/camera_app_ui/camera_app_helper_impl.h
@@ -116,6 +116,8 @@
void GetEventsSender(GetEventsSenderCallback callback) override;
void SetScreenLockedMonitor(mojo::PendingRemote<ScreenLockedMonitor> monitor,
SetScreenLockedMonitorCallback callback) override;
+ void RenderPdfAsJpeg(const std::vector<uint8_t>& pdf_data,
+ RenderPdfAsJpegCallback callback) override;
private:
void CheckExternalScreenState();
diff --git a/ash/webui/camera_app_ui/camera_app_ui_delegate.h b/ash/webui/camera_app_ui/camera_app_ui_delegate.h
index 76a328a..62f6f1a2 100644
--- a/ash/webui/camera_app_ui/camera_app_ui_delegate.h
+++ b/ash/webui/camera_app_ui/camera_app_ui_delegate.h
@@ -133,6 +133,11 @@
// Gets the system language of the current profile.
virtual std::string GetSystemLanguage() = 0;
+
+ // Returns the first page of a PDF as a JPEG.
+ virtual void RenderPdfAsJpeg(
+ const std::vector<uint8_t>& pdf,
+ base::OnceCallback<void(const std::vector<uint8_t>&)> callback) = 0;
};
} // namespace ash
diff --git a/ash/webui/camera_app_ui/resources/js/local_dev_overrides.ts b/ash/webui/camera_app_ui/resources/js/local_dev_overrides.ts
index 0d78776..175a65d 100644
--- a/ash/webui/camera_app_ui/resources/js/local_dev_overrides.ts
+++ b/ash/webui/camera_app_ui/resources/js/local_dev_overrides.ts
@@ -178,6 +178,10 @@
return false;
}
+ override async renderPdfAsImage(_pdf: Blob): Promise<Blob> {
+ return new Blob();
+ }
+
/* eslint-enable @typescript-eslint/require-await */
}
diff --git a/ash/webui/camera_app_ui/resources/js/mojo/chrome_helper.ts b/ash/webui/camera_app_ui/resources/js/mojo/chrome_helper.ts
index 45d7eb9..c0832e47 100644
--- a/ash/webui/camera_app_ui/resources/js/mojo/chrome_helper.ts
+++ b/ash/webui/camera_app_ui/resources/js/mojo/chrome_helper.ts
@@ -240,6 +240,8 @@
abstract initScreenLockedMonitor(onChange: (isScreenLocked: boolean) => void):
Promise<boolean>;
+ abstract renderPdfAsImage(pdf: Blob): Promise<Blob>;
+
/**
* Creates a new instance of ChromeHelper if it is not set. Returns the
* existing instance.
@@ -507,4 +509,11 @@
monitorCallbackRouter.$.bindNewPipeAndPassRemote());
return isScreenLocked;
}
+
+ override async renderPdfAsImage(pdf: Blob): Promise<Blob> {
+ const buffer = new Uint8Array(await pdf.arrayBuffer());
+ const numArray = castToNumberArray(buffer);
+ const {jpegData} = await this.remote.renderPdfAsJpeg(numArray);
+ return new Blob([new Uint8Array(jpegData)], {type: MimeType.JPEG});
+ }
}
diff --git a/ash/webui/camera_app_ui/resources/js/thumbnailer.ts b/ash/webui/camera_app_ui/resources/js/thumbnailer.ts
index 1bafc61..e872f31 100644
--- a/ash/webui/camera_app_ui/resources/js/thumbnailer.ts
+++ b/ash/webui/camera_app_ui/resources/js/thumbnailer.ts
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import {assertInstanceof} from './assert.js';
+import {ChromeHelper} from './mojo/chrome_helper.js';
import {
EmptyThumbnailError,
LoadError,
@@ -147,80 +148,6 @@
}
/**
- * Failed to find image in pdf error.
- */
-class NoImageInPdfError extends Error {
- constructor(message = 'Failed to find image in pdf') {
- super(message);
- this.name = this.constructor.name;
- }
-}
-
-/**
- * Gets image embedded in a PDF.
- *
- * @param blob Blob of PDF.
- */
-async function getImageFromPdf(blob: Blob): Promise<Blob> {
- const buf = await blob.arrayBuffer();
- const view = new Uint8Array(buf);
- let i = 0;
- /**
- * Finds |patterns| in view starting from |i| and moves |i| to end of found
- * pattern index.
- *
- * @return Returns begin of found pattern index or -1 for no further pattern
- * is found.
- */
- function findPattern(...patterns: number[]): number {
- for (; i + patterns.length < view.length; i++) {
- if (patterns.every((b, index) => b === view[i + index])) {
- const ret = i;
- i += patterns.length;
- return ret;
- }
- }
- return -1;
- }
- // Parse object contains /Subtype /Image name and field from pdf format:
- // <</Name1 /Field1... \n/Name2... >>...<<...>>
- // The jpeg stream will follow the target object with length in field of
- // /Length.
- while (i < view.length) {
- const start = findPattern(0x3c, 0x3c); // <<
- if (start === -1) {
- throw new NoImageInPdfError();
- }
- const end = findPattern(0x3e, 0x3e); // >>
- if (end === -1) {
- throw new NoImageInPdfError();
- }
- const s = String.fromCharCode(...view.slice(start + 2, end));
- const objs = s.split('\n');
- let isImage = false;
- let length = 0;
- for (const obj of objs) {
- const [name, field] = obj.split(' ');
- switch (name) {
- case '/Subtype':
- isImage = field === '/Image';
- break;
- case '/Length':
- length = Number(field);
- break;
- default:
- // nothing to do.
- }
- }
- if (isImage) {
- i += ' stream\n'.length;
- return new Blob([buf.slice(i, i + length)], {type: 'image/jpeg'});
- }
- }
- throw new NoImageInPdfError();
-}
-
-/**
* Throws when the input blob type is not supported by thumbnailer.
*/
class InvalidBlobTypeError extends Error {
@@ -247,7 +174,7 @@
case MimeType.MP4:
return scaleVideo(blob, VIDEO_COVER_WIDTH);
case MimeType.PDF:
- return getImageFromPdf(blob);
+ return ChromeHelper.getInstance().renderPdfAsImage(blob);
default:
throw new InvalidBlobTypeError(blob.type);
}
diff --git a/chrome/browser/ash/system_web_apps/apps/DEPS b/chrome/browser/ash/system_web_apps/apps/DEPS
index 071e437..0d8e6f0e 100644
--- a/chrome/browser/ash/system_web_apps/apps/DEPS
+++ b/chrome/browser/ash/system_web_apps/apps/DEPS
@@ -59,6 +59,8 @@
"+chrome/browser/media/webrtc",
"+chrome/browser/metrics",
"+chrome/browser/notifications",
+ # TODO(b/335118703): Move out from this list.
+ "+chrome/browser/pdf",
"+chrome/browser/platform_util.h",
"+chrome/browser/policy",
"+chrome/browser/profiles",
@@ -99,6 +101,8 @@
"+chrome/common/url_constants.h",
"+chrome/common/webui_url_constants.h",
"+chrome/grit",
+ # TODO(b/335118703): Move out from this list.
+ "+chrome/services/pdf/public/mojom",
"+chrome/test/base",
]
diff --git a/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.cc b/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.cc
index 2023ace..fb591a9 100644
--- a/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.cc
+++ b/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.cc
@@ -12,7 +12,9 @@
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
+#include "base/functional/bind.h"
#include "base/logging.h"
+#include "base/memory/read_only_shared_memory_region.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
@@ -29,6 +31,7 @@
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_device_salt_service_factory.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
+#include "chrome/browser/pdf/pdf_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
@@ -41,6 +44,8 @@
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/pref_names.h"
+#include "chrome/services/pdf/public/mojom/pdf_service.mojom.h"
+#include "chrome/services/pdf/public/mojom/pdf_thumbnailer.mojom.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/devicetype.h"
#include "chromeos/ui/base/window_properties.h"
@@ -51,8 +56,16 @@
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui_data_source.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkData.h"
+#include "third_party/skia/include/core/SkStream.h"
+#include "third_party/skia/include/encode/SkJpegEncoder.h"
#include "ui/chromeos/styles/cros_styles.h"
+#include "ui/gfx/geometry/size.h"
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"
@@ -78,6 +91,9 @@
const int64_t kStorageLowThreshold = 128 * 1024 * 1024; // 128MB
const int64_t kStorageCriticallyLowThreshold = 32 * 1024 * 1024; // 32MB
+// PDFs saved from CCA are always 72 dpi.
+constexpr int kPdfDpi = 72;
+
} // namespace
// static
@@ -244,6 +260,79 @@
}
}
+ChromeCameraAppUIDelegate::PdfServiceManager::PdfServiceManager() {
+ pdf_thumbnailers_.set_disconnect_handler(
+ base::BindRepeating(&ChromeCameraAppUIDelegate::PdfServiceManager::
+ ConsumeGotThumbnailCallback,
+ weak_factory_.GetWeakPtr(), std::vector<uint8_t>()));
+}
+
+ChromeCameraAppUIDelegate::PdfServiceManager::~PdfServiceManager() = default;
+
+void ChromeCameraAppUIDelegate::PdfServiceManager::GetThumbnail(
+ const std::vector<uint8_t>& pdf,
+ base::OnceCallback<void(const std::vector<uint8_t>&)> callback) {
+ // TODO(b/329069826): To prevent the thumbnailer from adding a white
+ // background to the result, get the actual dimensions and limit them to the
+ // maximum supported dimensions (keeping the aspect ratio), rather than
+ // passing the maximum supported dimensions directly.
+ auto params = pdf::mojom::ThumbParams::New(
+ /*size_px=*/gfx::Size(pdf::mojom::PdfThumbnailer::kMaxWidthPixels,
+ pdf::mojom::PdfThumbnailer::kMaxHeightPixels),
+ /*dpi=*/gfx::Size(kPdfDpi, kPdfDpi),
+ /*stretch_to_bounds=*/false, /*keep_aspect_ratio=*/true);
+ auto pdf_region = base::ReadOnlySharedMemoryRegion::Create(pdf.size());
+ if (!pdf_region.IsValid()) {
+ LOG(ERROR) << "Failed to allocate memory for PDF";
+ std::move(callback).Run({});
+ return;
+ }
+ memcpy(pdf_region.mapping.memory(), pdf.data(), pdf.size());
+
+ mojo::Remote<pdf::mojom::PdfService> pdf_service = LaunchPdfService();
+ mojo::PendingRemote<pdf::mojom::PdfThumbnailer> pdf_thumbnailer;
+ pdf_service->BindPdfThumbnailer(
+ pdf_thumbnailer.InitWithNewPipeAndPassReceiver());
+ mojo::RemoteSetElementId pdf_service_id =
+ pdf_services_.Add(std::move(pdf_service));
+ mojo::RemoteSetElementId pdf_thumbnailer_id =
+ pdf_thumbnailers_.Add(std::move(pdf_thumbnailer));
+ pdf_thumbnailer_callbacks[pdf_thumbnailer_id] = std::move(callback);
+ pdf_thumbnailers_.Get(pdf_thumbnailer_id)
+ ->GetThumbnail(
+ std::move(params), std::move(pdf_region.region),
+ base::BindOnce(
+ &ChromeCameraAppUIDelegate::PdfServiceManager::GotThumbnail,
+ weak_factory_.GetWeakPtr(), pdf_service_id, pdf_thumbnailer_id));
+}
+
+void ChromeCameraAppUIDelegate::PdfServiceManager::GotThumbnail(
+ mojo::RemoteSetElementId pdf_service_id,
+ mojo::RemoteSetElementId pdf_thumbnailer_id,
+ const SkBitmap& bitmap) {
+ SkDynamicMemoryWStream stream;
+ if (SkJpegEncoder::Encode(&stream, bitmap.pixmap(), {}) &&
+ stream.bytesWritten()) {
+ sk_sp<SkData> jpeg_data = stream.detachAsData();
+ ConsumeGotThumbnailCallback(
+ std::vector<uint8_t>(jpeg_data->bytes(),
+ jpeg_data->bytes() + jpeg_data->size()),
+ pdf_thumbnailer_id);
+ } else {
+ LOG(ERROR) << "Failed to encode bitmap to JPEG";
+ ConsumeGotThumbnailCallback({}, pdf_thumbnailer_id);
+ }
+ pdf_thumbnailers_.Remove(pdf_thumbnailer_id);
+ pdf_services_.Remove(pdf_service_id);
+}
+
+void ChromeCameraAppUIDelegate::PdfServiceManager::ConsumeGotThumbnailCallback(
+ const std::vector<uint8_t>& thumbnail,
+ mojo::RemoteSetElementId id) {
+ std::move(pdf_thumbnailer_callbacks[id]).Run(thumbnail);
+ pdf_thumbnailer_callbacks.erase(id);
+}
+
ChromeCameraAppUIDelegate::ChromeCameraAppUIDelegate(content::WebUI* web_ui)
: web_ui_(web_ui),
session_start_time_(base::Time::Now()),
@@ -575,6 +664,12 @@
return accept_languages.substr(0, accept_languages.find(','));
}
+void ChromeCameraAppUIDelegate::RenderPdfAsJpeg(
+ const std::vector<uint8_t>& pdf,
+ base::OnceCallback<void(const std::vector<uint8_t>&)> callback) {
+ pdf_service_manager_.GetThumbnail(pdf, std::move(callback));
+}
+
ash::CameraAppUIDelegate::WifiConfig::WifiConfig() = default;
ash::CameraAppUIDelegate::WifiConfig::WifiConfig(
diff --git a/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h b/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h
index 128d6be..ec4ec75 100644
--- a/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h
+++ b/chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h
@@ -9,6 +9,7 @@
#include "ash/public/cpp/holding_space/holding_space_client.h"
#include "ash/webui/camera_app_ui/camera_app_ui_delegate.h"
+#include "base/containers/flat_map.h"
#include "base/files/file_path_watcher.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
@@ -17,8 +18,12 @@
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/ui/webui/ash/system_web_dialog_delegate.h"
+#include "chrome/services/pdf/public/mojom/pdf_service.mojom.h"
+#include "chrome/services/pdf/public/mojom/pdf_thumbnailer.mojom.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/browser/web_ui.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
namespace content {
struct MediaStreamRequest;
@@ -113,6 +118,32 @@
weak_factory_{this};
};
+ class PdfServiceManager {
+ public:
+ PdfServiceManager();
+ PdfServiceManager(const PdfServiceManager&) = delete;
+ PdfServiceManager& operator=(const PdfServiceManager&) = delete;
+ ~PdfServiceManager();
+
+ void GetThumbnail(
+ const std::vector<uint8_t>& pdf,
+ base::OnceCallback<void(const std::vector<uint8_t>&)> callback);
+
+ private:
+ void GotThumbnail(mojo::RemoteSetElementId pdf_service_id,
+ mojo::RemoteSetElementId pdf_thumbnailer_id,
+ const SkBitmap& bitmap);
+ void ConsumeGotThumbnailCallback(const std::vector<uint8_t>& thumbnail,
+ mojo::RemoteSetElementId id);
+
+ mojo::RemoteSet<pdf::mojom::PdfThumbnailer> pdf_thumbnailers_;
+ base::flat_map<mojo::RemoteSetElementId,
+ base::OnceCallback<void(const std::vector<uint8_t>&)>>
+ pdf_thumbnailer_callbacks;
+ mojo::RemoteSet<pdf::mojom::PdfService> pdf_services_;
+ base::WeakPtrFactory<PdfServiceManager> weak_factory_{this};
+ };
+
explicit ChromeCameraAppUIDelegate(content::WebUI* web_ui);
ChromeCameraAppUIDelegate(const ChromeCameraAppUIDelegate&) = delete;
@@ -142,6 +173,9 @@
content::BrowserContext* context) override;
void OpenWifiDialog(WifiConfig wifi_config) override;
std::string GetSystemLanguage() override;
+ void RenderPdfAsJpeg(
+ const std::vector<uint8_t>& pdf,
+ base::OnceCallback<void(const std::vector<uint8_t>&)> callback) override;
private:
base::FilePath GetMyFilesFolder();
@@ -168,6 +202,8 @@
base::WeakPtr<ChromeCameraAppUIDelegate::StorageMonitor>
storage_monitor_weak_ptr_;
+ PdfServiceManager pdf_service_manager_;
+
// Weak pointer for this class |ChromeCameraAppUIDelegate|, used to run on
// main thread (mojo thread).
base::WeakPtrFactory<ChromeCameraAppUIDelegate> weak_factory_{this};