| // Copyright (c) 2012 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 "chrome/browser/printing/print_job.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/run_loop.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/printing/print_job_worker.h" |
| #include "content/public/browser/notification_service.h" |
| #include "printing/printed_document.h" |
| |
| #if defined(OS_WIN) |
| #include "chrome/browser/printing/pdf_to_emf_converter.h" |
| #include "printing/pdf_render_settings.h" |
| #include "printing/printed_page_win.h" |
| #endif |
| |
| using base::TimeDelta; |
| |
| namespace printing { |
| |
| // Helper function to ensure |owner| is valid until at least |callback| returns. |
| void HoldRefCallback(const scoped_refptr<PrintJobWorkerOwner>& owner, |
| const base::Closure& callback) { |
| callback.Run(); |
| } |
| |
| PrintJob::PrintJob() |
| : is_job_pending_(false), is_canceling_(false), quit_factory_(this) { |
| // This is normally a UI message loop, but in unit tests, the message loop is |
| // of the 'default' type. |
| DCHECK(base::MessageLoopForUI::IsCurrent() || |
| base::MessageLoop::current()->type() == |
| base::MessageLoop::TYPE_DEFAULT); |
| } |
| |
| PrintJob::~PrintJob() { |
| // The job should be finished (or at least canceled) when it is destroyed. |
| DCHECK(!is_job_pending_); |
| DCHECK(!is_canceling_); |
| DCHECK(!worker_ || !worker_->IsRunning()); |
| DCHECK(RunsTasksInCurrentSequence()); |
| } |
| |
| void PrintJob::Initialize(PrintJobWorkerOwner* job, |
| const base::string16& name, |
| int page_count) { |
| DCHECK(!worker_); |
| DCHECK(!is_job_pending_); |
| DCHECK(!is_canceling_); |
| DCHECK(!document_.get()); |
| worker_ = job->DetachWorker(this); |
| settings_ = job->settings(); |
| |
| PrintedDocument* new_doc = |
| new PrintedDocument(settings_, name, job->cookie()); |
| |
| new_doc->set_page_count(page_count); |
| UpdatePrintedDocument(new_doc); |
| |
| #if defined(OS_WIN) |
| pdf_page_mapping_ = PageRange::GetPages(settings_.ranges()); |
| if (pdf_page_mapping_.empty()) { |
| for (int i = 0; i < page_count; i++) |
| pdf_page_mapping_.push_back(i); |
| } |
| #endif |
| |
| // Don't forget to register to our own messages. |
| registrar_.Add(this, chrome::NOTIFICATION_PRINT_JOB_EVENT, |
| content::Source<PrintJob>(this)); |
| } |
| |
| void PrintJob::Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| DCHECK(RunsTasksInCurrentSequence()); |
| DCHECK_EQ(chrome::NOTIFICATION_PRINT_JOB_EVENT, type); |
| |
| OnNotifyPrintJobEvent(*content::Details<JobEventDetails>(details).ptr()); |
| } |
| |
| void PrintJob::GetSettingsDone(const PrintSettings& new_settings, |
| PrintingContext::Result result) { |
| NOTREACHED(); |
| } |
| |
| std::unique_ptr<PrintJobWorker> PrintJob::DetachWorker( |
| PrintJobWorkerOwner* new_owner) { |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| const PrintSettings& PrintJob::settings() const { |
| return settings_; |
| } |
| |
| int PrintJob::cookie() const { |
| // Always use an invalid cookie in this case. |
| if (!document_.get()) |
| return 0; |
| return document_->cookie(); |
| } |
| |
| void PrintJob::StartPrinting() { |
| DCHECK(RunsTasksInCurrentSequence()); |
| if (!worker_->IsRunning() || is_job_pending_) { |
| NOTREACHED(); |
| return; |
| } |
| |
| // Real work is done in PrintJobWorker::StartPrinting(). |
| worker_->PostTask(FROM_HERE, |
| base::Bind(&HoldRefCallback, base::WrapRefCounted(this), |
| base::Bind(&PrintJobWorker::StartPrinting, |
| base::Unretained(worker_.get()), |
| base::RetainedRef(document_)))); |
| // Set the flag right now. |
| is_job_pending_ = true; |
| |
| // Tell everyone! |
| scoped_refptr<JobEventDetails> details( |
| new JobEventDetails(JobEventDetails::NEW_DOC, 0, document_.get())); |
| content::NotificationService::current()->Notify( |
| chrome::NOTIFICATION_PRINT_JOB_EVENT, |
| content::Source<PrintJob>(this), |
| content::Details<JobEventDetails>(details.get())); |
| } |
| |
| void PrintJob::Stop() { |
| DCHECK(RunsTasksInCurrentSequence()); |
| |
| if (quit_factory_.HasWeakPtrs()) { |
| // In case we're running a nested run loop to wait for a job to finish, |
| // and we finished before the timeout, quit the nested loop right away. |
| Quit(); |
| quit_factory_.InvalidateWeakPtrs(); |
| } |
| |
| // Be sure to live long enough. |
| scoped_refptr<PrintJob> handle(this); |
| |
| if (worker_->IsRunning()) { |
| ControlledWorkerShutdown(); |
| } else { |
| // Flush the cached document. |
| is_job_pending_ = false; |
| UpdatePrintedDocument(nullptr); |
| } |
| } |
| |
| void PrintJob::Cancel() { |
| if (is_canceling_) |
| return; |
| is_canceling_ = true; |
| |
| DCHECK(RunsTasksInCurrentSequence()); |
| if (worker_ && worker_->IsRunning()) { |
| // Call this right now so it renders the context invalid. Do not use |
| // InvokeLater since it would take too much time. |
| worker_->Cancel(); |
| } |
| // Make sure a Cancel() is broadcast. |
| scoped_refptr<JobEventDetails> details( |
| new JobEventDetails(JobEventDetails::FAILED, 0, nullptr)); |
| content::NotificationService::current()->Notify( |
| chrome::NOTIFICATION_PRINT_JOB_EVENT, |
| content::Source<PrintJob>(this), |
| content::Details<JobEventDetails>(details.get())); |
| Stop(); |
| is_canceling_ = false; |
| } |
| |
| bool PrintJob::FlushJob(base::TimeDelta timeout) { |
| // Make sure the object outlive this message loop. |
| scoped_refptr<PrintJob> handle(this); |
| |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::BindOnce(&PrintJob::Quit, quit_factory_.GetWeakPtr()), |
| timeout); |
| |
| base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).Run(); |
| |
| return true; |
| } |
| |
| bool PrintJob::is_job_pending() const { |
| return is_job_pending_; |
| } |
| |
| PrintedDocument* PrintJob::document() const { |
| return document_.get(); |
| } |
| |
| #if defined(OS_WIN) |
| class PrintJob::PdfConversionState { |
| public: |
| PdfConversionState(const gfx::Size& page_size, const gfx::Rect& content_area) |
| : page_count_(0), |
| current_page_(0), |
| pages_in_progress_(0), |
| page_size_(page_size), |
| content_area_(content_area) {} |
| |
| void Start(const scoped_refptr<base::RefCountedMemory>& data, |
| const PdfRenderSettings& conversion_settings, |
| PdfConverter::StartCallback start_callback) { |
| converter_ = PdfConverter::StartPdfConverter(data, conversion_settings, |
| std::move(start_callback)); |
| } |
| |
| void GetMorePages(const PdfConverter::GetPageCallback& get_page_callback) { |
| const int kMaxNumberOfTempFilesPerDocument = 3; |
| while (pages_in_progress_ < kMaxNumberOfTempFilesPerDocument && |
| current_page_ < page_count_) { |
| ++pages_in_progress_; |
| converter_->GetPage(current_page_++, get_page_callback); |
| } |
| } |
| |
| void OnPageProcessed(const PdfConverter::GetPageCallback& get_page_callback) { |
| --pages_in_progress_; |
| GetMorePages(get_page_callback); |
| // Release converter if we don't need this any more. |
| if (!pages_in_progress_ && current_page_ >= page_count_) |
| converter_.reset(); |
| } |
| |
| void set_page_count(int page_count) { page_count_ = page_count; } |
| const gfx::Size& page_size() const { return page_size_; } |
| const gfx::Rect& content_area() const { return content_area_; } |
| |
| private: |
| int page_count_; |
| int current_page_; |
| int pages_in_progress_; |
| gfx::Size page_size_; |
| gfx::Rect content_area_; |
| std::unique_ptr<PdfConverter> converter_; |
| }; |
| |
| void PrintJob::StartPdfToEmfConversion( |
| const scoped_refptr<base::RefCountedMemory>& bytes, |
| const gfx::Size& page_size, |
| const gfx::Rect& content_area, |
| bool print_text_with_gdi) { |
| DCHECK(!pdf_conversion_state_); |
| pdf_conversion_state_ = |
| base::MakeUnique<PdfConversionState>(page_size, content_area); |
| PdfRenderSettings render_settings( |
| content_area, gfx::Point(0, 0), settings().dpi_size(), |
| /*autorotate=*/true, |
| print_text_with_gdi ? PdfRenderSettings::Mode::GDI_TEXT |
| : PdfRenderSettings::Mode::NORMAL); |
| pdf_conversion_state_->Start( |
| bytes, render_settings, |
| base::BindOnce(&PrintJob::OnPdfConversionStarted, this)); |
| } |
| |
| void PrintJob::OnPdfConversionStarted(int page_count) { |
| if (page_count <= 0) { |
| // Be sure to live long enough. |
| scoped_refptr<PrintJob> handle(this); |
| pdf_conversion_state_.reset(); |
| Cancel(); |
| return; |
| } |
| pdf_conversion_state_->set_page_count(page_count); |
| pdf_conversion_state_->GetMorePages( |
| base::BindRepeating(&PrintJob::OnPdfPageConverted, this)); |
| } |
| |
| void PrintJob::OnPdfPageConverted(int page_number, |
| float scale_factor, |
| std::unique_ptr<MetafilePlayer> metafile) { |
| DCHECK(pdf_conversion_state_); |
| if (!document_.get() || !metafile || page_number < 0 || |
| static_cast<size_t>(page_number) >= pdf_page_mapping_.size()) { |
| // Be sure to live long enough. |
| scoped_refptr<PrintJob> handle(this); |
| pdf_conversion_state_.reset(); |
| Cancel(); |
| return; |
| } |
| |
| // Update the rendered document. It will send notifications to the listener. |
| document_->SetPage(pdf_page_mapping_[page_number], std::move(metafile), |
| scale_factor, pdf_conversion_state_->page_size(), |
| pdf_conversion_state_->content_area()); |
| |
| pdf_conversion_state_->GetMorePages( |
| base::Bind(&PrintJob::OnPdfPageConverted, this)); |
| } |
| |
| void PrintJob::StartPdfToTextConversion( |
| const scoped_refptr<base::RefCountedMemory>& bytes, |
| const gfx::Size& page_size) { |
| DCHECK(!pdf_conversion_state_); |
| pdf_conversion_state_ = |
| base::MakeUnique<PdfConversionState>(gfx::Size(), gfx::Rect()); |
| gfx::Rect page_area = gfx::Rect(0, 0, page_size.width(), page_size.height()); |
| PdfRenderSettings render_settings( |
| page_area, gfx::Point(0, 0), settings().dpi_size(), |
| /*autorotate=*/true, PdfRenderSettings::Mode::TEXTONLY); |
| pdf_conversion_state_->Start( |
| bytes, render_settings, |
| base::BindOnce(&PrintJob::OnPdfConversionStarted, this)); |
| } |
| |
| void PrintJob::StartPdfToPostScriptConversion( |
| const scoped_refptr<base::RefCountedMemory>& bytes, |
| const gfx::Rect& content_area, |
| const gfx::Point& physical_offsets, |
| bool ps_level2) { |
| DCHECK(!pdf_conversion_state_); |
| pdf_conversion_state_ = base::MakeUnique<PdfConversionState>( |
| gfx::Size(), gfx::Rect()); |
| PdfRenderSettings render_settings( |
| content_area, physical_offsets, settings().dpi_size(), |
| /*autorotate=*/true, |
| ps_level2 ? PdfRenderSettings::Mode::POSTSCRIPT_LEVEL2 |
| : PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3); |
| pdf_conversion_state_->Start( |
| bytes, render_settings, |
| base::BindOnce(&PrintJob::OnPdfConversionStarted, this)); |
| } |
| #endif // defined(OS_WIN) |
| |
| void PrintJob::UpdatePrintedDocument(PrintedDocument* new_document) { |
| if (document_.get() == new_document) |
| return; |
| |
| document_ = new_document; |
| |
| if (document_.get()) |
| settings_ = document_->settings(); |
| |
| if (worker_) { |
| DCHECK(!is_job_pending_); |
| // Sync the document with the worker. |
| worker_->PostTask(FROM_HERE, |
| base::Bind(&HoldRefCallback, base::WrapRefCounted(this), |
| base::Bind(&PrintJobWorker::OnDocumentChanged, |
| base::Unretained(worker_.get()), |
| base::RetainedRef(document_)))); |
| } |
| } |
| |
| void PrintJob::OnNotifyPrintJobEvent(const JobEventDetails& event_details) { |
| switch (event_details.type()) { |
| case JobEventDetails::FAILED: { |
| settings_.Clear(); |
| // No need to cancel since the worker already canceled itself. |
| Stop(); |
| break; |
| } |
| case JobEventDetails::USER_INIT_DONE: |
| case JobEventDetails::DEFAULT_INIT_DONE: |
| case JobEventDetails::USER_INIT_CANCELED: { |
| DCHECK_EQ(event_details.document(), document_.get()); |
| break; |
| } |
| case JobEventDetails::NEW_DOC: |
| case JobEventDetails::JOB_DONE: |
| case JobEventDetails::ALL_PAGES_REQUESTED: { |
| // Don't care. |
| break; |
| } |
| case JobEventDetails::DOC_DONE: { |
| // This will call Stop() and broadcast a JOB_DONE message. |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&PrintJob::OnDocumentDone, this)); |
| break; |
| } |
| #if defined(OS_WIN) |
| case JobEventDetails::PAGE_DONE: |
| if (pdf_conversion_state_) { |
| pdf_conversion_state_->OnPageProcessed( |
| base::Bind(&PrintJob::OnPdfPageConverted, this)); |
| } |
| break; |
| #endif // defined(OS_WIN) |
| default: { |
| NOTREACHED(); |
| break; |
| } |
| } |
| } |
| |
| void PrintJob::OnDocumentDone() { |
| // Be sure to live long enough. The instance could be destroyed by the |
| // JOB_DONE broadcast. |
| scoped_refptr<PrintJob> handle(this); |
| |
| // Stop the worker thread. |
| Stop(); |
| |
| scoped_refptr<JobEventDetails> details( |
| new JobEventDetails(JobEventDetails::JOB_DONE, 0, document_.get())); |
| content::NotificationService::current()->Notify( |
| chrome::NOTIFICATION_PRINT_JOB_EVENT, |
| content::Source<PrintJob>(this), |
| content::Details<JobEventDetails>(details.get())); |
| } |
| |
| void PrintJob::ControlledWorkerShutdown() { |
| DCHECK(RunsTasksInCurrentSequence()); |
| |
| // The deadlock this code works around is specific to window messaging on |
| // Windows, so we aren't likely to need it on any other platforms. |
| #if defined(OS_WIN) |
| // We could easily get into a deadlock case if worker_->Stop() is used; the |
| // printer driver created a window as a child of the browser window. By |
| // canceling the job, the printer driver initiated dialog box is destroyed, |
| // which sends a blocking message to its parent window. If the browser window |
| // thread is not processing messages, a deadlock occurs. |
| // |
| // This function ensures that the dialog box will be destroyed in a timely |
| // manner by the mere fact that the thread will terminate. So the potential |
| // deadlock is eliminated. |
| worker_->StopSoon(); |
| |
| // Delay shutdown until the worker terminates. We want this code path |
| // to wait on the thread to quit before continuing. |
| if (worker_->IsRunning()) { |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::Bind(&PrintJob::ControlledWorkerShutdown, this), |
| base::TimeDelta::FromMilliseconds(100)); |
| return; |
| } |
| #endif |
| |
| // Now make sure the thread object is cleaned up. Do this on a worker |
| // thread because it may block. |
| base::PostTaskWithTraitsAndReply( |
| FROM_HERE, |
| {base::MayBlock(), base::WithBaseSyncPrimitives(), |
| base::TaskPriority::BACKGROUND, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, |
| base::BindOnce(&PrintJobWorker::Stop, base::Unretained(worker_.get())), |
| base::BindOnce(&PrintJob::HoldUntilStopIsCalled, this)); |
| |
| is_job_pending_ = false; |
| registrar_.RemoveAll(); |
| UpdatePrintedDocument(nullptr); |
| } |
| |
| void PrintJob::HoldUntilStopIsCalled() { |
| } |
| |
| void PrintJob::Quit() { |
| base::RunLoop::QuitCurrentWhenIdleDeprecated(); |
| } |
| |
| #if defined(OS_WIN) |
| JobEventDetails::JobEventDetails(Type type, |
| int job_id, |
| PrintedDocument* document, |
| PrintedPage* page) |
| : document_(document), page_(page), type_(type), job_id_(job_id) {} |
| #endif |
| |
| JobEventDetails::JobEventDetails(Type type, |
| int job_id, |
| PrintedDocument* document) |
| : document_(document), type_(type), job_id_(job_id) {} |
| |
| JobEventDetails::~JobEventDetails() { |
| } |
| |
| PrintedDocument* JobEventDetails::document() const { return document_.get(); } |
| |
| #if defined(OS_WIN) |
| PrintedPage* JobEventDetails::page() const { return page_.get(); } |
| #endif |
| } // namespace printing |