| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/services/print_compositor/print_compositor_impl.h" |
| |
| #include <algorithm> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/logging.h" |
| #include "base/memory/discardable_memory.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/crash/core/common/crash_key.h" |
| #include "components/discardable_memory/client/client_discardable_shared_memory_manager.h" |
| #include "components/enterprise/buildflags/buildflags.h" |
| #include "components/services/print_compositor/public/cpp/print_service_mojo_types.h" |
| #include "content/public/utility/utility_thread.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/system/platform_handle.h" |
| #include "printing/common/metafile_utils.h" |
| #include "printing/mojom/print.mojom.h" |
| #include "skia/ext/font_utils.h" |
| #include "third_party/blink/public/platform/web_image_generator.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkDocument.h" |
| #include "third_party/skia/include/core/SkGraphics.h" |
| #include "third_party/skia/include/core/SkSerialProcs.h" |
| #include "third_party/skia/include/docs/SkMultiPictureDocument.h" |
| #include "ui/accessibility/ax_tree_update.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "content/public/child/dwrite_font_proxy_init_win.h" |
| #include "printing/backend/win_helper.h" // nogncheck |
| #elif BUILDFLAG(IS_APPLE) |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/skia/include/core/SkFontMgr.h" |
| #elif BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID) |
| #include "third_party/blink/public/platform/platform.h" |
| #endif |
| |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| #include "components/enterprise/watermarking/mojom/watermark.mojom.h" // nogncheck |
| #include "components/enterprise/watermarking/watermark.h" // nogncheck |
| #endif |
| |
| using MojoDiscardableSharedMemoryManager = |
| discardable_memory::mojom::DiscardableSharedMemoryManager; |
| |
| namespace printing { |
| |
| namespace { |
| |
| sk_sp<SkDocument> MakeDocument( |
| const std::string& creator, |
| const std::string& title, |
| ui::AXTreeUpdate* accessibility_tree, |
| mojom::GenerateDocumentOutline generate_document_outline, |
| mojom::PrintCompositor::DocumentType document_type, |
| SkWStream& stream) { |
| #if BUILDFLAG(IS_WIN) |
| if (document_type == mojom::PrintCompositor::DocumentType::kXPS) { |
| return MakeXpsDocument(&stream); |
| } |
| #endif |
| CHECK_EQ(document_type, mojom::PrintCompositor::DocumentType::kPDF); |
| return MakePdfDocument( |
| creator, title, |
| accessibility_tree ? *accessibility_tree : ui::AXTreeUpdate(), |
| generate_document_outline, &stream); |
| } |
| |
| } // namespace |
| |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| |
| void DrawEnterpriseWatermark( |
| SkCanvas* canvas, |
| SkSize size, |
| const watermark::mojom::WatermarkBlockPtr& watermark_block) { |
| if (!watermark_block) { |
| return; |
| } |
| base::ReadOnlySharedMemoryMapping mapping = |
| watermark_block->serialized_skpicture.Map(); |
| if (!mapping.IsValid()) { |
| LOG(ERROR) |
| << "Error serializing the watermark block received from the browser"; |
| return; |
| } |
| auto skpicture_span = mapping.GetMemoryAsSpan<uint8_t>(); |
| SkMemoryStream stream(skpicture_span.data(), skpicture_span.size()); |
| sk_sp<SkPicture> picture = SkPicture::MakeFromStream(&stream); |
| |
| enterprise_watermark::DrawWatermark(canvas, picture.get(), |
| watermark_block->width, |
| watermark_block->height, size); |
| } |
| |
| #endif |
| |
| PrintCompositorImpl::PrintCompositorImpl( |
| mojo::PendingReceiver<mojom::PrintCompositor> receiver, |
| bool initialize_environment, |
| scoped_refptr<base::SingleThreadTaskRunner> io_task_runner) |
| : io_task_runner_(std::move(io_task_runner)) { |
| if (receiver) { |
| receiver_.Bind(std::move(receiver)); |
| |
| mojo::PendingRemote<MojoDiscardableSharedMemoryManager> manager_remote; |
| content::ChildThread::Get()->BindHostReceiver( |
| manager_remote.InitWithNewPipeAndPassReceiver()); |
| DCHECK(io_task_runner_); |
| discardable_shared_memory_manager_ = base::MakeRefCounted< |
| discardable_memory::ClientDiscardableSharedMemoryManager>( |
| std::move(manager_remote), io_task_runner_); |
| base::DiscardableMemoryAllocator::SetInstance( |
| discardable_shared_memory_manager_.get()); |
| } |
| |
| if (!initialize_environment) |
| return; |
| |
| #if BUILDFLAG(IS_WIN) |
| // Initialize direct write font proxy so skia can use it. |
| content::InitializeDWriteFontProxy(); |
| #endif |
| |
| // Hook up blink's codecs so skia can call them. |
| SkGraphics::SetImageGeneratorFromEncodedDataFactory( |
| blink::WebImageGenerator::CreateAsSkImageGenerator); |
| |
| #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID) |
| content::UtilityThread::Get()->EnsureBlinkInitializedWithSandboxSupport(); |
| // Check that we have sandbox support on this platform. |
| DCHECK(blink::Platform::Current()->GetSandboxSupport()); |
| #else |
| content::UtilityThread::Get()->EnsureBlinkInitialized(); |
| #endif |
| |
| #if BUILDFLAG(IS_APPLE) |
| // Check that font access is granted. |
| // This doesn't do comprehensive tests to make sure fonts can work properly. |
| // It is just a quick and simple check to catch things like improper sandbox |
| // policy setup. |
| DCHECK(skia::DefaultFontMgr()->countFamilies()); |
| #endif |
| } |
| |
| PrintCompositorImpl::~PrintCompositorImpl() { |
| #if BUILDFLAG(IS_WIN) |
| content::UninitializeDWriteFontProxy(); |
| #endif |
| } |
| |
| void PrintCompositorImpl::NotifyUnavailableSubframe(uint64_t frame_guid) { |
| // Add this frame into the map. |
| DCHECK(!base::Contains(frame_info_map_, frame_guid)); |
| auto& frame_info = |
| frame_info_map_.emplace(frame_guid, std::make_unique<FrameInfo>()) |
| .first->second; |
| frame_info->composited = true; |
| // Set content to be nullptr so it will be replaced by an empty picture during |
| // deserialization of its parent. |
| frame_info->content = nullptr; |
| |
| // Update the requests in case any of them might be waiting for this frame. |
| UpdateRequestsWithSubframeInfo(frame_guid, std::vector<uint64_t>()); |
| } |
| |
| void PrintCompositorImpl::AddSubframeContent( |
| uint64_t frame_guid, |
| base::ReadOnlySharedMemoryRegion serialized_content, |
| const ContentToFrameMap& subframe_content_map) { |
| base::ReadOnlySharedMemoryMapping mapping = serialized_content.Map(); |
| if (!mapping.IsValid()) { |
| NotifyUnavailableSubframe(frame_guid); |
| return; |
| } |
| |
| // Add this frame and its serialized content. |
| DCHECK(!base::Contains(frame_info_map_, frame_guid)); |
| frame_info_map_.emplace(frame_guid, std::make_unique<FrameInfo>( |
| mapping.GetMemoryAsSpan<uint8_t>(), |
| subframe_content_map)); |
| |
| // If there is no request, we do nothing more. |
| // Otherwise, we need to check whether any request actually waits on this |
| // frame content. |
| if (requests_.empty()) |
| return; |
| |
| // Get the pending list which is a list of subframes this frame needs |
| // but are still unavailable. |
| std::vector<uint64_t> pending_subframes; |
| for (auto& subframe_content : subframe_content_map) { |
| auto subframe_guid = subframe_content.second; |
| if (!base::Contains(frame_info_map_, subframe_guid)) |
| pending_subframes.push_back(subframe_guid); |
| } |
| |
| // Update the requests in case any of them is waiting for this frame. |
| UpdateRequestsWithSubframeInfo(frame_guid, pending_subframes); |
| } |
| |
| void PrintCompositorImpl::SetAccessibilityTree( |
| const ui::AXTreeUpdate& accessibility_tree) { |
| accessibility_tree_ = accessibility_tree; |
| } |
| |
| void PrintCompositorImpl::CompositePage( |
| uint64_t frame_guid, |
| base::ReadOnlySharedMemoryRegion serialized_content, |
| const ContentToFrameMap& subframe_content_map, |
| mojom::PrintCompositor::CompositePageCallback callback) { |
| TRACE_EVENT0("print", "PrintCompositorImpl::CompositePage"); |
| // This function is always called to composite a page to PDF. |
| HandleCompositionRequest( |
| frame_guid, std::move(serialized_content), subframe_content_map, |
| mojom::PrintCompositor::DocumentType::kPDF, std::move(callback)); |
| } |
| |
| void PrintCompositorImpl::CompositeDocument( |
| uint64_t frame_guid, |
| base::ReadOnlySharedMemoryRegion serialized_content, |
| const ContentToFrameMap& subframe_content_map, |
| mojom::PrintCompositor::DocumentType document_type, |
| mojom::PrintCompositor::CompositeDocumentCallback callback) { |
| TRACE_EVENT0("print", "PrintCompositorImpl::CompositeDocument"); |
| CHECK(!doc_info_); |
| HandleCompositionRequest(frame_guid, std::move(serialized_content), |
| subframe_content_map, document_type, |
| std::move(callback)); |
| } |
| |
| void PrintCompositorImpl::PrepareToCompositeDocument( |
| mojom::PrintCompositor::DocumentType document_type, |
| mojom::PrintCompositor::PrepareToCompositeDocumentCallback callback) { |
| CHECK(!doc_info_); |
| #if BUILDFLAG(IS_WIN) |
| if (document_type == mojom::PrintCompositor::DocumentType::kXPS) { |
| xps_initializer_ = std::make_unique<ScopedXPSInitializer>(); |
| } |
| #endif |
| doc_info_ = std::make_unique<DocumentInfo>(document_type); |
| std::move(callback).Run(mojom::PrintCompositor::Status::kSuccess); |
| } |
| |
| void PrintCompositorImpl::FinishDocumentComposition( |
| uint32_t page_count, |
| mojom::PrintCompositor::FinishDocumentCompositionCallback callback) { |
| CHECK(doc_info_); |
| DCHECK_GT(page_count, 0U); |
| doc_info_->page_count = page_count; |
| doc_info_->callback = std::move(callback); |
| |
| if (!doc_info_->doc) { |
| doc_info_->doc = MakeDocument( |
| creator_, title_, &accessibility_tree_, generate_document_outline_, |
| doc_info_->document_type, doc_info_->compositor_stream); |
| } |
| |
| HandleDocumentCompletionRequest(); |
| } |
| |
| void PrintCompositorImpl::SetWebContentsURL(const GURL& url) { |
| // Record the most recent url we tried to print. This should be sufficient |
| // for users using print preview by default. |
| static crash_reporter::CrashKeyString<1024> crash_key("main-frame-url"); |
| crash_key.Set(url.spec()); |
| } |
| |
| void PrintCompositorImpl::SetUserAgent(const std::string& user_agent) { |
| if (!user_agent.empty()) |
| creator_ = user_agent; |
| } |
| |
| void PrintCompositorImpl::UpdateRequestsWithSubframeInfo( |
| uint64_t frame_guid, |
| const std::vector<uint64_t>& pending_subframes) { |
| // Check for each request's pending list. |
| for (auto it = requests_.begin(); it != requests_.end();) { |
| auto& request = *it; |
| // If the request needs this frame, we can remove the dependency, but |
| // update with this frame's pending list. |
| auto& pending_list = request->pending_subframes; |
| if (pending_list.erase(frame_guid)) { |
| std::ranges::copy(pending_subframes, |
| std::inserter(pending_list, pending_list.end())); |
| } |
| |
| // If the request still has pending frames, or isn't at the front of the |
| // request queue (and thus could be dependent upon content from their |
| // data stream), then keep waiting. |
| const bool fulfill_request = |
| it == requests_.begin() && pending_list.empty(); |
| if (!fulfill_request) { |
| ++it; |
| continue; |
| } |
| |
| // Fulfill the request now. |
| FulfillRequest(request->serialized_content, request->subframe_content_map, |
| request->document_type, std::move(request->callback)); |
| |
| // Check for a collected print preview document that was waiting on |
| // this page to finish. |
| if (doc_info_) { |
| if (doc_info_->page_count && |
| (doc_info_->pages_written == doc_info_->page_count)) { |
| FinishDocumentRequest(std::move(doc_info_->callback)); |
| } |
| } |
| it = requests_.erase(it); |
| } |
| } |
| |
| bool PrintCompositorImpl::IsReadyToComposite( |
| uint64_t frame_guid, |
| const ContentToFrameMap& subframe_content_map, |
| base::flat_set<uint64_t>* pending_subframes) const { |
| pending_subframes->clear(); |
| base::flat_set<uint64_t> visited_frames = {frame_guid}; |
| CheckFramesForReadiness(subframe_content_map, pending_subframes, |
| &visited_frames); |
| return pending_subframes->empty(); |
| } |
| |
| void PrintCompositorImpl::CheckFramesForReadiness( |
| const ContentToFrameMap& subframe_content_map, |
| base::flat_set<uint64_t>* pending_subframes, |
| base::flat_set<uint64_t>* visited) const { |
| for (auto& subframe_content : subframe_content_map) { |
| auto subframe_guid = subframe_content.second; |
| // If this frame has been checked, skip it. |
| if (!visited->insert(subframe_guid).second) |
| continue; |
| |
| auto iter = frame_info_map_.find(subframe_guid); |
| if (iter == frame_info_map_.end()) { |
| pending_subframes->insert(subframe_guid); |
| } else { |
| CheckFramesForReadiness(iter->second->subframe_content_map, |
| pending_subframes, visited); |
| } |
| } |
| } |
| |
| void PrintCompositorImpl::HandleCompositionRequest( |
| uint64_t frame_guid, |
| base::ReadOnlySharedMemoryRegion serialized_content, |
| const ContentToFrameMap& subframe_content_map, |
| mojom::PrintCompositor::DocumentType document_type, |
| CompositePagesCallback callback) { |
| base::ReadOnlySharedMemoryMapping mapping = serialized_content.Map(); |
| if (!mapping.IsValid()) { |
| LOG(ERROR) << "HandleCompositionRequest: Cannot map input."; |
| std::move(callback).Run(mojom::PrintCompositor::Status::kHandleMapError, |
| base::ReadOnlySharedMemoryRegion()); |
| return; |
| } |
| |
| base::flat_set<uint64_t> pending_subframes; |
| if (IsReadyToComposite(frame_guid, subframe_content_map, |
| &pending_subframes)) { |
| // This request has all the necessary subframes. |
| // Due to typeface serialization caching, need to ensure that previous |
| // requests have already been processed, otherwise this request could |
| // fail by trying to use a typeface which hasn't been deserialized yet. |
| if (requests_.empty()) { |
| FulfillRequest(mapping.GetMemoryAsSpan<uint8_t>(), subframe_content_map, |
| document_type, std::move(callback)); |
| return; |
| } |
| } |
| |
| // When it is not ready yet, keep its information and |
| // wait until all dependent subframes are ready. |
| auto iter = frame_info_map_.find(frame_guid); |
| if (iter == frame_info_map_.end()) |
| frame_info_map_[frame_guid] = std::make_unique<FrameInfo>(); |
| |
| requests_.push_back(std::make_unique<RequestInfo>( |
| mapping.GetMemoryAsSpan<uint8_t>(), subframe_content_map, |
| std::move(pending_subframes), document_type, std::move(callback))); |
| } |
| |
| void PrintCompositorImpl::HandleDocumentCompletionRequest() { |
| if (doc_info_->pages_written == doc_info_->page_count) { |
| FinishDocumentRequest(std::move(doc_info_->callback)); |
| return; |
| } |
| // Just need to wait on pages to percolate through processing, callback will |
| // be handled from UpdateRequestsWithSubframeInfo() once the pending requests |
| // have finished. |
| } |
| |
| mojom::PrintCompositor::Status PrintCompositorImpl::CompositePages( |
| base::span<const uint8_t> serialized_content, |
| const ContentToFrameMap& subframe_content_map, |
| base::ReadOnlySharedMemoryRegion* region, |
| mojom::PrintCompositor::DocumentType document_type) { |
| TRACE_EVENT0("print", "PrintCompositorImpl::CompositePages"); |
| |
| PictureDeserializationContext subframes = |
| GetPictureDeserializationContext(subframe_content_map); |
| |
| // Read in content and convert it into pdf. |
| SkMemoryStream stream(serialized_content.data(), serialized_content.size()); |
| int page_count = SkMultiPictureDocument::ReadPageCount(&stream); |
| if (!page_count) { |
| LOG(ERROR) << "CompositePages: No page is read."; |
| return mojom::PrintCompositor::Status::kContentFormatError; |
| } |
| |
| std::vector<SkDocumentPage> pages(page_count); |
| SkDeserialProcs procs = |
| DeserializationProcs(&subframes, &typefaces_, &images_); |
| if (!SkMultiPictureDocument::Read(&stream, pages.data(), page_count, |
| &procs)) { |
| LOG(ERROR) << "CompositePages: Page reading failed."; |
| return mojom::PrintCompositor::Status::kContentFormatError; |
| } |
| |
| // Create PDF document providing accessibility data early if concurrent |
| // document composition is not in effect, i.e. when handling |
| // CompositeDocumentToPdf() call. |
| SkDynamicMemoryWStream wstream; |
| sk_sp<SkDocument> doc = |
| MakeDocument(creator_, title_, doc_info_ ? nullptr : &accessibility_tree_, |
| generate_document_outline_, document_type, wstream); |
| |
| if (doc_info_) { |
| // Create full document if needed. |
| if (!doc_info_->doc) { |
| doc_info_->doc = MakeDocument( |
| creator_, title_, &accessibility_tree_, generate_document_outline_, |
| doc_info_->document_type, doc_info_->compositor_stream); |
| } |
| } |
| |
| for (const auto& page : pages) { |
| TRACE_EVENT0("print", "PrintCompositorImpl::CompositePages draw page"); |
| DrawPage(doc.get(), page); |
| |
| if (doc_info_) { |
| // Optionally draw this page into the full document in `doc_info_` as |
| // well. |
| DrawPage(doc_info_->doc.get(), page); |
| doc_info_->pages_written++; |
| } |
| } |
| doc->close(); |
| |
| base::MappedReadOnlyRegion region_mapping = |
| base::ReadOnlySharedMemoryRegion::Create(wstream.bytesWritten()); |
| if (!region_mapping.IsValid()) { |
| LOG(ERROR) << "CompositePages: Cannot create new shared memory region."; |
| return mojom::PrintCompositor::Status::kHandleMapError; |
| } |
| |
| wstream.copyToAndReset(region_mapping.mapping.memory()); |
| *region = std::move(region_mapping.region); |
| return mojom::PrintCompositor::Status::kSuccess; |
| } |
| |
| void PrintCompositorImpl::CompositeSubframe(FrameInfo* frame_info) { |
| frame_info->composited = true; |
| |
| // Composite subframes first. |
| PictureDeserializationContext subframes = |
| GetPictureDeserializationContext(frame_info->subframe_content_map); |
| |
| // Composite the entire frame. |
| SkMemoryStream stream(frame_info->serialized_content.data(), |
| frame_info->serialized_content.size()); |
| SkDeserialProcs procs = DeserializationProcs( |
| &subframes, &frame_info->typefaces, &frame_info->images); |
| frame_info->content = SkPicture::MakeFromStream(&stream, &procs); |
| } |
| |
| PrintCompositorImpl::PictureDeserializationContext |
| PrintCompositorImpl::GetPictureDeserializationContext( |
| const ContentToFrameMap& subframe_content_map) { |
| PictureDeserializationContext subframes; |
| for (auto& content_info : subframe_content_map) { |
| uint32_t content_id = content_info.first; |
| uint64_t frame_guid = content_info.second; |
| auto iter = frame_info_map_.find(frame_guid); |
| if (iter == frame_info_map_.end()) |
| continue; |
| |
| FrameInfo* frame_info = iter->second.get(); |
| if (!frame_info->composited) |
| CompositeSubframe(frame_info); |
| subframes[content_id] = frame_info->content; |
| } |
| return subframes; |
| } |
| |
| void PrintCompositorImpl::DrawPage(SkDocument* doc, |
| const SkDocumentPage& page) { |
| SkCanvas* canvas = doc->beginPage(page.fSize.width(), page.fSize.height()); |
| canvas->drawPicture(page.fPicture); |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| DrawEnterpriseWatermark(canvas, page.fSize, watermark_block_); |
| #endif |
| doc->endPage(); |
| } |
| |
| void PrintCompositorImpl::FulfillRequest( |
| base::span<const uint8_t> serialized_content, |
| const ContentToFrameMap& subframe_content_map, |
| mojom::PrintCompositor::DocumentType document_type, |
| CompositePagesCallback callback) { |
| base::ReadOnlySharedMemoryRegion region; |
| auto status = CompositePages(serialized_content, subframe_content_map, |
| ®ion, document_type); |
| std::move(callback).Run(status, std::move(region)); |
| } |
| |
| void PrintCompositorImpl::FinishDocumentRequest( |
| FinishDocumentCompositionCallback callback) { |
| mojom::PrintCompositor::Status status; |
| base::ReadOnlySharedMemoryRegion region; |
| |
| doc_info_->doc->close(); |
| |
| base::MappedReadOnlyRegion region_mapping = |
| base::ReadOnlySharedMemoryRegion::Create( |
| doc_info_->compositor_stream.bytesWritten()); |
| if (region_mapping.IsValid()) { |
| doc_info_->compositor_stream.copyToAndReset( |
| region_mapping.mapping.memory()); |
| region = std::move(region_mapping.region); |
| status = mojom::PrintCompositor::Status::kSuccess; |
| } else { |
| LOG(ERROR) << "FinishDocumentRequest: " |
| << "Cannot create new shared memory region."; |
| status = mojom::PrintCompositor::Status::kHandleMapError; |
| } |
| |
| std::move(callback).Run(status, std::move(region)); |
| } |
| |
| PrintCompositorImpl::FrameContentInfo::FrameContentInfo( |
| base::span<const uint8_t> content, |
| const ContentToFrameMap& map) |
| : serialized_content(content.begin(), content.end()), |
| subframe_content_map(map) {} |
| |
| PrintCompositorImpl::FrameContentInfo::FrameContentInfo() = default; |
| |
| PrintCompositorImpl::FrameContentInfo::~FrameContentInfo() = default; |
| |
| // TODO(crbug.com/40100562) Make use of `document_type` parameter once |
| // `MakeXpsDocument()` is available. |
| PrintCompositorImpl::DocumentInfo::DocumentInfo( |
| mojom::PrintCompositor::DocumentType document_type) |
| : document_type(document_type) {} |
| |
| PrintCompositorImpl::DocumentInfo::~DocumentInfo() = default; |
| |
| PrintCompositorImpl::RequestInfo::RequestInfo( |
| base::span<const uint8_t> content, |
| const ContentToFrameMap& content_info, |
| const base::flat_set<uint64_t>& pending_subframes, |
| mojom::PrintCompositor::DocumentType document_type, |
| mojom::PrintCompositor::CompositePageCallback callback) |
| : FrameContentInfo(content, content_info), |
| pending_subframes(pending_subframes), |
| document_type(document_type), |
| callback(std::move(callback)) {} |
| |
| PrintCompositorImpl::RequestInfo::~RequestInfo() = default; |
| |
| void PrintCompositorImpl::SetGenerateDocumentOutline( |
| mojom::GenerateDocumentOutline generate_document_outline) { |
| generate_document_outline_ = generate_document_outline; |
| } |
| |
| void PrintCompositorImpl::SetTitle(const std::string& title) { |
| title_ = title; |
| } |
| |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| void PrintCompositorImpl::SetWatermarkBlock( |
| watermark::mojom::WatermarkBlockPtr watermark_block) { |
| watermark_block_ = std::move(watermark_block); |
| } |
| |
| const watermark::mojom::WatermarkBlockPtr& |
| PrintCompositorImpl::watermark_block_for_testing() const { |
| return watermark_block_; |
| } |
| |
| #endif |
| |
| } // namespace printing |