// 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 "printing/pdf_metafile_cg_mac.h" #include #include #include "base/files/file_path.h" #include "base/lazy_instance.h" #include "base/logging.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_cftyperef.h" #include "base/strings/sys_string_conversions.h" #include "base/threading/thread_local.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/size.h" using base::ScopedCFTypeRef; namespace { // What is up with this ugly hack? , that's what. // The bug: Printing certain PDFs crashes. The cause: When printing, the // renderer process assembles pages one at a time, in PDF format, to send to the // browser process. When printing a PDF, the PDF plugin returns output in PDF // format. There is a bug in 10.5 and 10.6 (, // ) where reference counting is broken when // drawing certain PDFs into PDF contexts. So at the high-level, a PdfMetafileCg // is used to hold the destination context, and then about five layers down on // the callstack, a PdfMetafileCg is used to hold the source PDF. If the source // PDF is drawn into the destination PDF context and then released, accessing // the destination PDF context will crash. So the outermost instantiation of // PdfMetafileCg creates a pool for deeper instantiations to dump their used // PDFs into rather than releasing them. When the top-level PDF is closed, then // it's safe to clear the pool. A thread local is used to allow this to work in // single-process mode. TODO(avi): This Apple bug appears fixed in 10.7; when // 10.7 is the minimum required version for Chromium, remove this hack. base::LazyInstance >::Leaky thread_pdf_docs = LAZY_INSTANCE_INITIALIZER; // Rotate a page by |num_rotations| * 90 degrees, counter-clockwise. void RotatePage(CGContextRef context, const CGRect rect, int num_rotations) { switch (num_rotations) { case 0: break; case 1: // After rotating by 90 degrees with the axis at the origin, the page // content is now "off screen". Shift it right to move it back on screen. CGContextTranslateCTM(context, rect.size.width, 0); // Rotates counter-clockwise by 90 degrees. CGContextRotateCTM(context, M_PI_2); break; case 2: // After rotating by 180 degrees with the axis at the origin, the page // content is now "off screen". Shift it right and up to move it back on // screen. CGContextTranslateCTM(context, rect.size.width, rect.size.height); // Rotates counter-clockwise by 90 degrees. CGContextRotateCTM(context, M_PI); break; case 3: // After rotating by 270 degrees with the axis at the origin, the page // content is now "off screen". Shift it right to move it back on screen. CGContextTranslateCTM(context, 0, rect.size.height); // Rotates counter-clockwise by 90 degrees. CGContextRotateCTM(context, -M_PI_2); break; default: NOTREACHED(); break; } } } // namespace namespace printing { PdfMetafileCg::PdfMetafileCg() : page_is_open_(false), thread_pdf_docs_owned_(false) { if (!thread_pdf_docs.Pointer()->Get() && base::mac::IsOSSnowLeopard()) { thread_pdf_docs_owned_ = true; thread_pdf_docs.Pointer()->Set( CFSetCreateMutable(kCFAllocatorDefault, 0, &kCFTypeSetCallBacks)); } } PdfMetafileCg::~PdfMetafileCg() { DCHECK(thread_checker_.CalledOnValidThread()); if (pdf_doc_ && thread_pdf_docs.Pointer()->Get()) { // Transfer ownership to the pool. CFSetAddValue(thread_pdf_docs.Pointer()->Get(), pdf_doc_); } if (thread_pdf_docs_owned_) { CFRelease(thread_pdf_docs.Pointer()->Get()); thread_pdf_docs.Pointer()->Set(NULL); } } bool PdfMetafileCg::Init() { // Ensure that Init hasn't already been called. DCHECK(!context_.get()); DCHECK(!pdf_data_.get()); pdf_data_.reset(CFDataCreateMutable(kCFAllocatorDefault, 0)); if (!pdf_data_.get()) { LOG(ERROR) << "Failed to create pdf data for metafile"; return false; } ScopedCFTypeRef pdf_consumer( CGDataConsumerCreateWithCFData(pdf_data_)); if (!pdf_consumer.get()) { LOG(ERROR) << "Failed to create data consumer for metafile"; pdf_data_.reset(NULL); return false; } context_.reset(CGPDFContextCreate(pdf_consumer, NULL, NULL)); if (!context_.get()) { LOG(ERROR) << "Failed to create pdf context for metafile"; pdf_data_.reset(NULL); } return true; } bool PdfMetafileCg::InitFromData(const void* src_buffer, uint32_t src_buffer_size) { DCHECK(!context_.get()); DCHECK(!pdf_data_.get()); if (!src_buffer || src_buffer_size == 0) { return false; } pdf_data_.reset(CFDataCreateMutable(kCFAllocatorDefault, src_buffer_size)); CFDataAppendBytes(pdf_data_, static_cast(src_buffer), src_buffer_size); return true; } bool PdfMetafileCg::StartPage(const gfx::Size& page_size, const gfx::Rect& content_area, const float& scale_factor) { DCHECK(context_.get()); DCHECK(!page_is_open_); double height = page_size.height(); double width = page_size.width(); CGRect bounds = CGRectMake(0, 0, width, height); CGContextBeginPage(context_, &bounds); page_is_open_ = true; CGContextSaveGState(context_); // Move to the context origin. CGContextTranslateCTM(context_, content_area.x(), -content_area.y()); // Flip the context. CGContextTranslateCTM(context_, 0, height); CGContextScaleCTM(context_, scale_factor, -scale_factor); return context_.get() != NULL; } bool PdfMetafileCg::FinishPage() { DCHECK(context_.get()); DCHECK(page_is_open_); CGContextRestoreGState(context_); CGContextEndPage(context_); page_is_open_ = false; return true; } bool PdfMetafileCg::FinishDocument() { DCHECK(context_.get()); DCHECK(!page_is_open_); #ifndef NDEBUG // Check that the context will be torn down properly; if it's not, pdf_data_ // will be incomplete and generate invalid PDF files/documents. if (context_.get()) { CFIndex extra_retain_count = CFGetRetainCount(context_.get()) - 1; if (extra_retain_count > 0) { LOG(ERROR) << "Metafile context has " << extra_retain_count << " extra retain(s) on Close"; } } #endif CGPDFContextClose(context_.get()); context_.reset(NULL); return true; } bool PdfMetafileCg::RenderPage(unsigned int page_number, CGContextRef context, const CGRect rect, const MacRenderPageParams& params) const { CGPDFDocumentRef pdf_doc = GetPDFDocument(); if (!pdf_doc) { LOG(ERROR) << "Unable to create PDF document from data"; return false; } CGPDFPageRef pdf_page = CGPDFDocumentGetPage(pdf_doc, page_number); CGRect source_rect = CGPDFPageGetBoxRect(pdf_page, kCGPDFCropBox); int pdf_src_rotation = CGPDFPageGetRotationAngle(pdf_page); float scaling_factor = 1.0; const bool source_is_landscape = (source_rect.size.width > source_rect.size.height); const bool dest_is_landscape = (rect.size.width > rect.size.height); const bool rotate = params.autorotate ? (source_is_landscape != dest_is_landscape) : false; const float source_width = rotate ? source_rect.size.height : source_rect.size.width; const float source_height = rotate ? source_rect.size.width : source_rect.size.height; // See if we need to scale the output. const bool scaling_needed = (params.shrink_to_fit && ((source_width > rect.size.width) || (source_height > rect.size.height))) || (params.stretch_to_fit && ((source_width < rect.size.width) && (source_height < rect.size.height))); if (scaling_needed) { float x_scaling_factor = rect.size.width / source_width; float y_scaling_factor = rect.size.height / source_height; scaling_factor = std::min(x_scaling_factor, y_scaling_factor); } // Some PDFs have a non-zero origin. Need to take that into account and align // the PDF to the origin. const float x_origin_offset = -1 * source_rect.origin.x; const float y_origin_offset = -1 * source_rect.origin.y; // If the PDF needs to be centered, calculate the offsets here. float x_offset = params.center_horizontally ? ((rect.size.width - (source_width * scaling_factor)) / 2) : 0; if (rotate) x_offset = -x_offset; float y_offset = params.center_vertically ? ((rect.size.height - (source_height * scaling_factor)) / 2) : 0; CGContextSaveGState(context); // The transform operations specified here gets applied in reverse order. // i.e. the origin offset translation happens first. // Origin is at bottom-left. CGContextTranslateCTM(context, x_offset, y_offset); int num_rotations = 0; if (rotate) { if (pdf_src_rotation == 0 || pdf_src_rotation == 270) { num_rotations = 1; } else { num_rotations = 3; } } else { if (pdf_src_rotation == 180 || pdf_src_rotation == 270) { num_rotations = 2; } } RotatePage(context, rect, num_rotations); CGContextScaleCTM(context, scaling_factor, scaling_factor); CGContextTranslateCTM(context, x_origin_offset, y_origin_offset); CGContextDrawPDFPage(context, pdf_page); CGContextRestoreGState(context); return true; } unsigned int PdfMetafileCg::GetPageCount() const { CGPDFDocumentRef pdf_doc = GetPDFDocument(); return pdf_doc ? CGPDFDocumentGetNumberOfPages(pdf_doc) : 0; } gfx::Rect PdfMetafileCg::GetPageBounds(unsigned int page_number) const { CGPDFDocumentRef pdf_doc = GetPDFDocument(); if (!pdf_doc) { LOG(ERROR) << "Unable to create PDF document from data"; return gfx::Rect(); } if (page_number > GetPageCount()) { LOG(ERROR) << "Invalid page number: " << page_number; return gfx::Rect(); } CGPDFPageRef pdf_page = CGPDFDocumentGetPage(pdf_doc, page_number); CGRect page_rect = CGPDFPageGetBoxRect(pdf_page, kCGPDFMediaBox); return gfx::Rect(page_rect); } uint32_t PdfMetafileCg::GetDataSize() const { // PDF data is only valid/complete once the context is released. DCHECK(!context_); if (!pdf_data_) return 0; return static_cast(CFDataGetLength(pdf_data_)); } bool PdfMetafileCg::GetData(void* dst_buffer, uint32_t dst_buffer_size) const { // PDF data is only valid/complete once the context is released. DCHECK(!context_); DCHECK(pdf_data_); DCHECK(dst_buffer); DCHECK_GT(dst_buffer_size, 0U); uint32_t data_size = GetDataSize(); if (dst_buffer_size > data_size) { return false; } CFDataGetBytes(pdf_data_, CFRangeMake(0, dst_buffer_size), static_cast(dst_buffer)); return true; } CGContextRef PdfMetafileCg::context() const { return context_.get(); } CGPDFDocumentRef PdfMetafileCg::GetPDFDocument() const { // Make sure that we have data, and that it's not being modified any more. DCHECK(pdf_data_.get()); DCHECK(!context_.get()); if (!pdf_doc_.get()) { ScopedCFTypeRef pdf_data_provider( CGDataProviderCreateWithCFData(pdf_data_)); pdf_doc_.reset(CGPDFDocumentCreateWithProvider(pdf_data_provider)); } return pdf_doc_.get(); } } // namespace printing