// 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<bool*>(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<HDC*>(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<const EMRALPHABLEND*>(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<HBRUSH>(::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<LPCSTR>(&escape), 0, 0) > 0) {
ExtEscape(dc, escape, size, reinterpret_cast<LPCSTR>(bits),
sizeof(supported), reinterpret_cast<LPSTR>(&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<const BYTE*>(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<void*>(&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<BYTE*>(buffer));
DCHECK(size2 == size);
return size2 == size && size2 != 0;
}
bool Emf::GetDataAsVector(std::vector<uint8>* 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<uint8> buffer;
if (GetDataAsVector(&buffer)) {
DWORD written = 0;
if (WriteFile(file, &*buffer.begin(), static_cast<DWORD>(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<Emf::EnumerationContext*>(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<const EMRSTRETCHDIBITS*>(record());
const BYTE* record_start = reinterpret_cast<const BYTE *>(record());
const BITMAPINFOHEADER *bmih =
reinterpret_cast<const BITMAPINFOHEADER *>(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<SkBitmap> 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<const uint32_t*>(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<const BITMAPINFO *>(&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<const XFORM*>(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<const XFORM*>(record()->dParm);
const DWORD* option = reinterpret_cast<const DWORD*>(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<const EMRGDICOMMENT*>(record());
if (comment_record->cbData == sizeof(PageBreakRecord)) {
const PageBreakRecord* page_break_record =
reinterpret_cast<const PageBreakRecord*>(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<const BYTE *>(&record));
}
bool Emf::FinishPage() {
DCHECK(hdc_);
if (!hdc_)
return false;
PageBreakRecord record(PageBreakRecord::END_PAGE);
return !!GdiComment(hdc_, sizeof(record),
reinterpret_cast<const BYTE *>(&record));
}
Emf::Enumerator::Enumerator(const Emf& emf, HDC context, const RECT* rect) {
items_.clear();
if (!EnumEnhMetaFile(context,
emf.emf(),
&Emf::Enumerator::EnhMetaFileProc,
reinterpret_cast<void*>(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<Enumerator*>(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<int>(1, page_size.width() * scale));
page_size.set_height(std::max<int>(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<Emf> 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<Emf> 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