// 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/emf_win.h" #include "base/files/file_path.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/win/scoped_gdi_object.h" #include "base/win/scoped_hdc.h" #include "base/win/scoped_select_object.h" #include "skia/ext/vector_platform_device_emf_win.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/gfx/codec/jpeg_codec.h" #include "ui/gfx/codec/png_codec.h" #include "ui/gfx/gdi_util.h" #include "ui/gfx/rect.h" #include "ui/gfx/size.h" namespace { const int kCustomGdiCommentSignature = 0xdeadbabe; struct PageBreakRecord { int signature; enum PageBreakType { START_PAGE, END_PAGE, } type; explicit PageBreakRecord(PageBreakType type_in) : signature(kCustomGdiCommentSignature), type(type_in) { } bool IsValid() const { return (signature == kCustomGdiCommentSignature) && (type >= START_PAGE) && (type <= END_PAGE); } }; int CALLBACK IsAlphaBlendUsedEnumProc(HDC, HANDLETABLE*, const ENHMETARECORD *record, int, LPARAM data) { bool* result = reinterpret_cast(data); if (!result) return 0; switch (record->iType) { case EMR_ALPHABLEND: { *result = true; return 0; break; } } return 1; } int CALLBACK RasterizeAlphaBlendProc(HDC metafile_dc, HANDLETABLE* handle_table, const ENHMETARECORD *record, int num_objects, LPARAM data) { HDC bitmap_dc = *reinterpret_cast(data); // Play this command to the bitmap DC. ::PlayEnhMetaFileRecord(bitmap_dc, handle_table, record, num_objects); switch (record->iType) { case EMR_ALPHABLEND: { const EMRALPHABLEND* alpha_blend = reinterpret_cast(record); // Don't modify transformation here. // Old implementation did reset transformations for DC to identity matrix. // That was not correct and cause some bugs, like unexpected cropping. // EMRALPHABLEND is rendered into bitmap and metafile contexts with // current transformation. If we don't touch them here BitBlt will copy // same areas. ::BitBlt(metafile_dc, alpha_blend->xDest, alpha_blend->yDest, alpha_blend->cxDest, alpha_blend->cyDest, bitmap_dc, alpha_blend->xDest, alpha_blend->yDest, SRCCOPY); break; } case EMR_CREATEBRUSHINDIRECT: case EMR_CREATECOLORSPACE: case EMR_CREATECOLORSPACEW: case EMR_CREATEDIBPATTERNBRUSHPT: case EMR_CREATEMONOBRUSH: case EMR_CREATEPALETTE: case EMR_CREATEPEN: case EMR_DELETECOLORSPACE: case EMR_DELETEOBJECT: case EMR_EXTCREATEFONTINDIRECTW: // Play object creation command only once. break; default: // Play this command to the metafile DC. ::PlayEnhMetaFileRecord(metafile_dc, handle_table, record, num_objects); break; } return 1; // Continue enumeration } // Bitmapt for rasterization. class RasterBitmap { public: explicit RasterBitmap(const gfx::Size& raster_size) : saved_object_(NULL) { context_.Set(::CreateCompatibleDC(NULL)); if (!context_) { NOTREACHED() << "Bitmap DC creation failed"; return; } ::SetGraphicsMode(context_, GM_ADVANCED); void* bits = NULL; gfx::Rect bitmap_rect(raster_size); gfx::CreateBitmapHeader(raster_size.width(), raster_size.height(), &header_.bmiHeader); bitmap_.Set(::CreateDIBSection(context_, &header_, DIB_RGB_COLORS, &bits, NULL, 0)); if (!bitmap_) NOTREACHED() << "Raster bitmap creation for printing failed"; saved_object_ = ::SelectObject(context_, bitmap_); RECT rect = bitmap_rect.ToRECT(); ::FillRect(context_, &rect, static_cast(::GetStockObject(WHITE_BRUSH))); } ~RasterBitmap() { ::SelectObject(context_, saved_object_); } HDC context() const { return context_; } base::win::ScopedCreateDC context_; BITMAPINFO header_; base::win::ScopedBitmap bitmap_; HGDIOBJ saved_object_; private: DISALLOW_COPY_AND_ASSIGN(RasterBitmap); }; } // namespace namespace printing { bool DIBFormatNativelySupported(HDC dc, uint32 escape, const BYTE* bits, int size) { BOOL supported = FALSE; if (ExtEscape(dc, QUERYESCSUPPORT, sizeof(escape), reinterpret_cast(&escape), 0, 0) > 0) { ExtEscape(dc, escape, size, reinterpret_cast(bits), sizeof(supported), reinterpret_cast(&supported)); } return !!supported; } Emf::Emf() : emf_(NULL), hdc_(NULL), page_count_(0) { } Emf::~Emf() { DCHECK(!hdc_); if (emf_) DeleteEnhMetaFile(emf_); } bool Emf::InitToFile(const base::FilePath& metafile_path) { DCHECK(!emf_ && !hdc_); hdc_ = CreateEnhMetaFile(NULL, metafile_path.value().c_str(), NULL, NULL); DCHECK(hdc_); return hdc_ != NULL; } bool Emf::InitFromFile(const base::FilePath& metafile_path) { DCHECK(!emf_ && !hdc_); emf_ = GetEnhMetaFile(metafile_path.value().c_str()); DCHECK(emf_); return emf_ != NULL; } bool Emf::Init() { DCHECK(!emf_ && !hdc_); hdc_ = CreateEnhMetaFile(NULL, NULL, NULL, NULL); DCHECK(hdc_); return hdc_ != NULL; } bool Emf::InitFromData(const void* src_buffer, uint32 src_buffer_size) { DCHECK(!emf_ && !hdc_); emf_ = SetEnhMetaFileBits(src_buffer_size, reinterpret_cast(src_buffer)); return emf_ != NULL; } bool Emf::FinishDocument() { DCHECK(!emf_ && hdc_); emf_ = CloseEnhMetaFile(hdc_); DCHECK(emf_); hdc_ = NULL; return emf_ != NULL; } bool Emf::Playback(HDC hdc, const RECT* rect) const { DCHECK(emf_ && !hdc_); RECT bounds; if (!rect) { // Get the natural bounds of the EMF buffer. bounds = GetPageBounds(1).ToRECT(); rect = &bounds; } return PlayEnhMetaFile(hdc, emf_, rect) != 0; } bool Emf::SafePlayback(HDC context) const { DCHECK(emf_ && !hdc_); XFORM base_matrix; if (!GetWorldTransform(context, &base_matrix)) { NOTREACHED(); return false; } Emf::EnumerationContext playback_context; playback_context.base_matrix = &base_matrix; RECT rect = GetPageBounds(1).ToRECT(); return EnumEnhMetaFile(context, emf_, &Emf::SafePlaybackProc, reinterpret_cast(&playback_context), &rect) != 0; } gfx::Rect Emf::GetPageBounds(unsigned int page_number) const { DCHECK(emf_ && !hdc_); DCHECK_EQ(1U, page_number); ENHMETAHEADER header; if (GetEnhMetaFileHeader(emf_, sizeof(header), &header) != sizeof(header)) { NOTREACHED(); return gfx::Rect(); } // Add 1 to right and bottom because it's inclusive rectangle. // See ENHMETAHEADER. return gfx::Rect(header.rclBounds.left, header.rclBounds.top, header.rclBounds.right - header.rclBounds.left + 1, header.rclBounds.bottom - header.rclBounds.top + 1); } uint32 Emf::GetDataSize() const { DCHECK(emf_ && !hdc_); return GetEnhMetaFileBits(emf_, 0, NULL); } bool Emf::GetData(void* buffer, uint32 size) const { DCHECK(emf_ && !hdc_); DCHECK(buffer && size); uint32 size2 = GetEnhMetaFileBits(emf_, size, reinterpret_cast(buffer)); DCHECK(size2 == size); return size2 == size && size2 != 0; } bool Emf::GetDataAsVector(std::vector* buffer) const { uint32 size = GetDataSize(); if (!size) return false; buffer->resize(size); if (!GetData(&buffer->front(), size)) return false; return true; } bool Emf::SaveTo(const base::FilePath& file_path) const { HANDLE file = CreateFile(file_path.value().c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL); if (file == INVALID_HANDLE_VALUE) return false; bool success = false; std::vector buffer; if (GetDataAsVector(&buffer)) { DWORD written = 0; if (WriteFile(file, &*buffer.begin(), static_cast(buffer.size()), &written, NULL) && written == buffer.size()) { success = true; } } CloseHandle(file); return success; } int CALLBACK Emf::SafePlaybackProc(HDC hdc, HANDLETABLE* handle_table, const ENHMETARECORD* record, int objects_count, LPARAM param) { Emf::EnumerationContext* context = reinterpret_cast(param); context->handle_table = handle_table; context->objects_count = objects_count; context->hdc = hdc; Record record_instance(record); bool success = record_instance.SafePlayback(context); DCHECK(success); return 1; } Emf::EnumerationContext::EnumerationContext() { memset(this, 0, sizeof(*this)); } Emf::Record::Record(const ENHMETARECORD* record) : record_(record) { DCHECK(record_); } bool Emf::Record::Play(Emf::EnumerationContext* context) const { return 0 != PlayEnhMetaFileRecord(context->hdc, context->handle_table, record_, context->objects_count); } bool Emf::Record::SafePlayback(Emf::EnumerationContext* context) const { // For EMF field description, see [MS-EMF] Enhanced Metafile Format // Specification. // // This is the second major EMF breakage I get; the first one being // SetDCBrushColor/SetDCPenColor/DC_PEN/DC_BRUSH being silently ignored. // // This function is the guts of the fix for bug 1186598. Some printer drivers // somehow choke on certain EMF records, but calling the corresponding // function directly on the printer HDC is fine. Still, playing the EMF record // fails. Go figure. // // The main issue is that SetLayout is totally unsupported on these printers // (HP 4500/4700). I used to call SetLayout and I stopped. I found out this is // not sufficient because GDI32!PlayEnhMetaFile internally calls SetLayout(!) // Damn. // // So I resorted to manually parse the EMF records and play them one by one. // The issue with this method compared to using PlayEnhMetaFile to play back // an EMF buffer is that the later silently fixes the matrix to take in // account the matrix currently loaded at the time of the call. // The matrix magic is done transparently when using PlayEnhMetaFile but since // I'm processing one field at a time, I need to do the fixup myself. Note // that PlayEnhMetaFileRecord doesn't fix the matrix correctly even when // called inside an EnumEnhMetaFile loop. Go figure (bis). // // So when I see a EMR_SETWORLDTRANSFORM and EMR_MODIFYWORLDTRANSFORM, I need // to fix the matrix according to the matrix previously loaded before playing // back the buffer. Otherwise, the previously loaded matrix would be ignored // and the EMF buffer would always be played back at its native resolution. // Duh. // // I also use this opportunity to skip over eventual EMR_SETLAYOUT record that // could remain. // // Another tweak we make is for JPEGs/PNGs in calls to StretchDIBits. // (Our Pepper plugin code uses a JPEG). If the printer does not support // JPEGs/PNGs natively we decompress the JPEG/PNG and then set it to the // device. // TODO(sanjeevr): We should also add JPEG/PNG support for SetSIBitsToDevice // // We also process any custom EMR_GDICOMMENT records which are our // placeholders for StartPage and EndPage. // Note: I should probably care about view ports and clipping, eventually. bool res = false; const XFORM* base_matrix = context->base_matrix; switch (record()->iType) { case EMR_STRETCHDIBITS: { const EMRSTRETCHDIBITS * sdib_record = reinterpret_cast(record()); const BYTE* record_start = reinterpret_cast(record()); const BITMAPINFOHEADER *bmih = reinterpret_cast(record_start + sdib_record->offBmiSrc); const BYTE* bits = record_start + sdib_record->offBitsSrc; bool play_normally = true; res = false; HDC hdc = context->hdc; scoped_ptr bitmap; if (bmih->biCompression == BI_JPEG) { if (!DIBFormatNativelySupported(hdc, CHECKJPEGFORMAT, bits, bmih->biSizeImage)) { play_normally = false; bitmap.reset(gfx::JPEGCodec::Decode(bits, bmih->biSizeImage)); } } else if (bmih->biCompression == BI_PNG) { if (!DIBFormatNativelySupported(hdc, CHECKPNGFORMAT, bits, bmih->biSizeImage)) { play_normally = false; bitmap.reset(new SkBitmap()); gfx::PNGCodec::Decode(bits, bmih->biSizeImage, bitmap.get()); } } if (!play_normally) { DCHECK(bitmap.get()); if (bitmap.get()) { SkAutoLockPixels lock(*bitmap.get()); DCHECK_EQ(bitmap->config(), SkBitmap::kARGB_8888_Config); const uint32_t* pixels = static_cast(bitmap->getPixels()); if (pixels == NULL) { NOTREACHED(); return false; } BITMAPINFOHEADER bmi = {0}; gfx::CreateBitmapHeader(bitmap->width(), bitmap->height(), &bmi); res = (0 != StretchDIBits(hdc, sdib_record->xDest, sdib_record->yDest, sdib_record->cxDest, sdib_record->cyDest, sdib_record->xSrc, sdib_record->ySrc, sdib_record->cxSrc, sdib_record->cySrc, pixels, reinterpret_cast(&bmi), sdib_record->iUsageSrc, sdib_record->dwRop)); } } else { res = Play(context); } break; } case EMR_SETWORLDTRANSFORM: { DCHECK_EQ(record()->nSize, sizeof(DWORD) * 2 + sizeof(XFORM)); const XFORM* xform = reinterpret_cast(record()->dParm); HDC hdc = context->hdc; if (base_matrix) { res = 0 != SetWorldTransform(hdc, base_matrix) && ModifyWorldTransform(hdc, xform, MWT_LEFTMULTIPLY); } else { res = 0 != SetWorldTransform(hdc, xform); } break; } case EMR_MODIFYWORLDTRANSFORM: { DCHECK_EQ(record()->nSize, sizeof(DWORD) * 2 + sizeof(XFORM) + sizeof(DWORD)); const XFORM* xform = reinterpret_cast(record()->dParm); const DWORD* option = reinterpret_cast(xform + 1); HDC hdc = context->hdc; switch (*option) { case MWT_IDENTITY: if (base_matrix) { res = 0 != SetWorldTransform(hdc, base_matrix); } else { res = 0 != ModifyWorldTransform(hdc, xform, MWT_IDENTITY); } break; case MWT_LEFTMULTIPLY: case MWT_RIGHTMULTIPLY: res = 0 != ModifyWorldTransform(hdc, xform, *option); break; case 4: // MWT_SET if (base_matrix) { res = 0 != SetWorldTransform(hdc, base_matrix) && ModifyWorldTransform(hdc, xform, MWT_LEFTMULTIPLY); } else { res = 0 != SetWorldTransform(hdc, xform); } break; default: res = false; break; } break; } case EMR_SETLAYOUT: // Ignore it. res = true; break; case EMR_GDICOMMENT: { const EMRGDICOMMENT* comment_record = reinterpret_cast(record()); if (comment_record->cbData == sizeof(PageBreakRecord)) { const PageBreakRecord* page_break_record = reinterpret_cast(comment_record->Data); if (page_break_record && page_break_record->IsValid()) { if (page_break_record->type == PageBreakRecord::START_PAGE) { res = !!::StartPage(context->hdc); DCHECK_EQ(0, context->dc_on_page_start); context->dc_on_page_start = ::SaveDC(context->hdc); } else if (page_break_record->type == PageBreakRecord::END_PAGE) { DCHECK_NE(0, context->dc_on_page_start); ::RestoreDC(context->hdc, context->dc_on_page_start); context->dc_on_page_start = 0; res = !!::EndPage(context->hdc); } else { res = false; NOTREACHED(); } } else { res = Play(context); } } else { res = true; } break; } default: { res = Play(context); break; } } return res; } SkBaseDevice* Emf::StartPageForVectorCanvas( const gfx::Size& page_size, const gfx::Rect& content_area, const float& scale_factor) { if (!StartPage(page_size, content_area, scale_factor)) return NULL; return skia::VectorPlatformDeviceEmf::CreateDevice(page_size.width(), page_size.height(), true, hdc_); } bool Emf::StartPage(const gfx::Size& /*page_size*/, const gfx::Rect& /*content_area*/, const float& /*scale_factor*/) { DCHECK(hdc_); if (!hdc_) return false; page_count_++; PageBreakRecord record(PageBreakRecord::START_PAGE); return !!GdiComment(hdc_, sizeof(record), reinterpret_cast(&record)); } bool Emf::FinishPage() { DCHECK(hdc_); if (!hdc_) return false; PageBreakRecord record(PageBreakRecord::END_PAGE); return !!GdiComment(hdc_, sizeof(record), reinterpret_cast(&record)); } Emf::Enumerator::Enumerator(const Emf& emf, HDC context, const RECT* rect) { items_.clear(); if (!EnumEnhMetaFile(context, emf.emf(), &Emf::Enumerator::EnhMetaFileProc, reinterpret_cast(this), rect)) { NOTREACHED(); items_.clear(); } DCHECK_EQ(context_.hdc, context); } Emf::Enumerator::const_iterator Emf::Enumerator::begin() const { return items_.begin(); } Emf::Enumerator::const_iterator Emf::Enumerator::end() const { return items_.end(); } int CALLBACK Emf::Enumerator::EnhMetaFileProc(HDC hdc, HANDLETABLE* handle_table, const ENHMETARECORD* record, int objects_count, LPARAM param) { Enumerator& emf = *reinterpret_cast(param); if (!emf.context_.handle_table) { DCHECK(!emf.context_.handle_table); DCHECK(!emf.context_.objects_count); emf.context_.handle_table = handle_table; emf.context_.objects_count = objects_count; emf.context_.hdc = hdc; } else { DCHECK_EQ(emf.context_.handle_table, handle_table); DCHECK_EQ(emf.context_.objects_count, objects_count); DCHECK_EQ(emf.context_.hdc, hdc); } emf.items_.push_back(Record(record)); return 1; } bool Emf::IsAlphaBlendUsed() const { bool result = false; ::EnumEnhMetaFile(NULL, emf(), &IsAlphaBlendUsedEnumProc, &result, NULL); return result; } Emf* Emf::RasterizeMetafile(int raster_area_in_pixels) const { gfx::Rect page_bounds = GetPageBounds(1); gfx::Size page_size(page_bounds.size()); if (page_size.GetArea() <= 0) { NOTREACHED() << "Metafile is empty"; page_bounds = gfx::Rect(1, 1); } float scale = sqrt(float(raster_area_in_pixels) / page_size.GetArea()); page_size.set_width(std::max(1, page_size.width() * scale)); page_size.set_height(std::max(1, page_size.height() * scale)); RasterBitmap bitmap(page_size); gfx::Rect bitmap_rect(page_size); RECT rect = bitmap_rect.ToRECT(); Playback(bitmap.context(), &rect); scoped_ptr result(new Emf); result->Init(); HDC hdc = result->context(); DCHECK(hdc); skia::InitializeDC(hdc); // Params are ignored. result->StartPage(page_bounds.size(), page_bounds, 1); ::ModifyWorldTransform(hdc, NULL, MWT_IDENTITY); XFORM xform = { float(page_bounds.width()) / bitmap_rect.width(), 0, 0, float(page_bounds.height()) / bitmap_rect.height(), page_bounds.x(), page_bounds.y(), }; ::SetWorldTransform(hdc, &xform); ::BitBlt(hdc, 0, 0, bitmap_rect.width(), bitmap_rect.height(), bitmap.context(), bitmap_rect.x(), bitmap_rect.y(), SRCCOPY); result->FinishPage(); result->FinishDocument(); return result.release(); } Emf* Emf::RasterizeAlphaBlend() const { gfx::Rect page_bounds = GetPageBounds(1); if (page_bounds.size().GetArea() <= 0) { NOTREACHED() << "Metafile is empty"; page_bounds = gfx::Rect(1, 1); } RasterBitmap bitmap(page_bounds.size()); // Map metafile page_bounds.x(), page_bounds.y() to bitmap 0, 0. XFORM xform = { 1, 0, 0, 1, -page_bounds.x(), -page_bounds.y()}; ::SetWorldTransform(bitmap.context(), &xform); scoped_ptr result(new Emf); result->Init(); HDC hdc = result->context(); DCHECK(hdc); skia::InitializeDC(hdc); HDC bitmap_dc = bitmap.context(); RECT rect = page_bounds.ToRECT(); ::EnumEnhMetaFile(hdc, emf(), &RasterizeAlphaBlendProc, &bitmap_dc, &rect); result->FinishDocument(); return result.release(); } } // namespace printing