blob: 0119c5c7e1b175e3cd6c8144282323aa756df9fa [file]
// Copyright 2012 The Chromium Authors
// 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 <stdint.h>
#include <algorithm>
#include <memory>
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/numerics/byte_conversions.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "printing/mojom/print.mojom.h"
#include "skia/ext/skia_utils_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/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
namespace printing {
namespace {
// This is less than sizeof(ENHMETARECORD) because ENHMETARECORD contains a
// DWORD field of variable length that can sometimes have a length of 0.
constexpr uint32_t kMinEnhMetaRecordSize = 8;
bool DIBFormatNativelySupported(HDC dc,
uint32_t escape,
base::span<const uint8_t> bits) {
// ExtEscape() takes an int for its data size parameter, so reject large
// input sizes that do not fit in an int.
if (!base::IsValueInRangeForNumericType<int>(bits.size())) {
return false;
}
BOOL supported = FALSE;
if (ExtEscape(dc, QUERYESCSUPPORT, sizeof(escape),
reinterpret_cast<LPCSTR>(&escape), 0, 0) > 0) {
ExtEscape(dc, escape, static_cast<int>(bits.size()),
reinterpret_cast<LPCSTR>(bits.data()), sizeof(supported),
reinterpret_cast<LPSTR>(&supported));
}
return !!supported;
}
const BITMAPINFOHEADER* GetBitmapInfoHeader(
base::span<const uint8_t> record_span) {
if (record_span.size() < sizeof(EMRSTRETCHDIBITS)) {
return nullptr;
}
const auto* sdib_record =
reinterpret_cast<const EMRSTRETCHDIBITS*>(record_span.data());
if (sdib_record->offBmiSrc < sizeof(EMRSTRETCHDIBITS) ||
sdib_record->cbBmiSrc < sizeof(BITMAPINFOHEADER)) {
return nullptr;
}
base::CheckedNumeric<uint32_t> end_bmi = sdib_record->offBmiSrc;
end_bmi += sdib_record->cbBmiSrc;
if (!end_bmi.IsValid() || end_bmi.ValueOrDie() > record_span.size()) {
return nullptr;
}
return reinterpret_cast<const BITMAPINFOHEADER*>(
record_span.subspan(sdib_record->offBmiSrc).data());
}
base::span<const uint8_t> GetBitmapBits(base::span<const uint8_t> record_span,
uint32_t expected_size) {
if (record_span.size() < sizeof(EMRSTRETCHDIBITS)) {
return {};
}
const auto* sdib_record =
reinterpret_cast<const EMRSTRETCHDIBITS*>(record_span.data());
if (sdib_record->offBitsSrc < sizeof(EMRSTRETCHDIBITS) ||
sdib_record->cbBitsSrc != expected_size) {
return {};
}
base::CheckedNumeric<uint32_t> end_bits = sdib_record->offBitsSrc;
end_bits += sdib_record->cbBitsSrc;
if (!end_bits.IsValid() || end_bits.ValueOrDie() > record_span.size()) {
return {};
}
return record_span.subspan(sdib_record->offBitsSrc, sdib_record->cbBitsSrc);
}
base::span<const uint8_t> GetGdiCommentData(
base::span<const uint8_t> record_span) {
if (record_span.size() < sizeof(EMRGDICOMMENT)) {
return {};
}
const auto* comment =
reinterpret_cast<const EMRGDICOMMENT*>(record_span.data());
base::CheckedNumeric<uint32_t> end_data = offsetof(EMRGDICOMMENT, Data);
end_data += comment->cbData;
if (!end_data.IsValid() || end_data.ValueOrDie() > record_span.size()) {
return {};
}
return record_span.subspan(offsetof(EMRGDICOMMENT, Data), comment->cbData);
}
} // namespace
Emf::Emf() = default;
Emf::~Emf() {
Close();
}
void Emf::Close() {
DCHECK(!hdc_);
if (emf_)
DeleteEnhMetaFile(emf_);
emf_ = nullptr;
}
bool Emf::Init() {
DCHECK(!emf_ && !hdc_);
hdc_ = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
DCHECK(hdc_);
return !!hdc_;
}
bool Emf::InitFromData(base::span<const uint8_t> data) {
DCHECK(!emf_ && !hdc_);
if (!base::IsValueInRangeForNumericType<UINT>(data.size()))
return false;
emf_ = SetEnhMetaFileBits(static_cast<UINT>(data.size()), data.data());
return !!emf_;
}
bool Emf::FinishDocument() {
DCHECK(!emf_ && hdc_);
emf_ = CloseEnhMetaFile(hdc_);
DCHECK(emf_);
hdc_ = nullptr;
return !!emf_;
}
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;
CHECK(GetWorldTransform(context, &base_matrix));
Emf::EnumerationContext playback_context(GetDataSize());
playback_context.base_matrix = &base_matrix;
gfx::Rect bound = GetPageBounds(1);
RECT rect = bound.ToRECT();
return bound.IsEmpty() ||
::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;
CHECK_EQ(GetEnhMetaFileHeader(emf_, sizeof(header), &header), sizeof(header));
// 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);
}
unsigned int Emf::GetPageCount() const {
return 1;
}
HDC Emf::context() const {
return hdc_;
}
uint32_t Emf::GetDataSize() const {
DCHECK(emf_ && !hdc_);
return GetEnhMetaFileBits(emf_, 0, nullptr);
}
bool Emf::GetData(void* buffer, uint32_t size) const {
DCHECK(emf_ && !hdc_);
DCHECK(buffer && size);
uint32_t size2 =
GetEnhMetaFileBits(emf_, size, reinterpret_cast<BYTE*>(buffer));
DCHECK(size2 == size);
return size2 == size && size2 != 0;
}
bool Emf::ShouldCopySharedMemoryRegionData() const {
// `InitFromData()` operates directly upon memory provide to it, so any
// caller for cases where this data is shared cross-process should have the
// data copied before it is operated upon.
return true;
}
mojom::MetafileDataType Emf::GetDataType() const {
return mojom::MetafileDataType::kEMF;
}
int CALLBACK Emf::SafePlaybackProc(HDC hdc,
HANDLETABLE* handle_table,
const ENHMETARECORD* record,
int objects_count,
LPARAM param) {
auto* context = reinterpret_cast<Emf::EnumerationContext*>(param);
// The Emf::Record::SafePlayback() call below assumes this check has happened.
if (record->nSize < kMinEnhMetaRecordSize ||
record->nSize > context->remaining_metafile_size || record->nSize % 4) {
return 0;
}
context->remaining_metafile_size -= record->nSize;
context->handle_table = handle_table;
context->objects_count = objects_count;
context->hdc = hdc;
Record record_instance(record);
bool success = record_instance.SafePlayback(context);
return success ? 1 : 0;
}
PostScriptMetaFile::PostScriptMetaFile() = default;
PostScriptMetaFile::~PostScriptMetaFile() = default;
mojom::MetafileDataType PostScriptMetaFile::GetDataType() const {
return mojom::MetafileDataType::kPostScriptEmf;
}
bool PostScriptMetaFile::SafePlayback(HDC hdc) const {
Emf::Enumerator emf_enum(*this, nullptr, nullptr);
for (const Emf::Record& record : emf_enum) {
const ENHMETARECORD* emf_record = record.record();
if (emf_record->iType != EMR_GDICOMMENT) {
continue;
}
// SAFETY: `Emf::Enumerator::EnhMetaFileProc()` verified that `nSize` fits
// within the remaining metafile buffer.
base::span<const uint8_t> record_span = UNSAFE_BUFFERS(base::span(
reinterpret_cast<const uint8_t*>(emf_record), emf_record->nSize));
base::span<const uint8_t> data_span = GetGdiCommentData(record_span);
if (data_span.size() < sizeof(uint16_t)) {
continue;
}
// First uint16_t element in `data` holds the size of the rest of `data`,
// which is the actual PostScript data. Windows requires this payload size +
// PS data structure.
const uint16_t ps_payload_size =
base::U16FromNativeEndian(data_span.first<sizeof(uint16_t)>());
const uint32_t data_size = 2 + ps_payload_size;
// Assume value used in PDFium's core/fxge/win32/cpsoutput.cpp is not going
// to change.
static constexpr uint16_t kExpectedPdfiumMax = 1024 + 2;
if (data_size != data_span.size() || data_size > kExpectedPdfiumMax) {
continue;
}
int ret =
ExtEscape(hdc, PASSTHROUGH, static_cast<int>(data_size),
reinterpret_cast<const char*>(data_span.data()), 0, nullptr);
DCHECK_EQ(ps_payload_size, ret);
}
return true;
}
Emf::EnumerationContext::EnumerationContext(uint32_t metafile_size)
: remaining_metafile_size(metafile_size) {}
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: {
// SAFETY: Verified by this method's caller.
base::span<const uint8_t> record_span = UNSAFE_BUFFERS(base::span(
reinterpret_cast<const uint8_t*>(record()), record()->nSize));
if (record_span.empty()) {
return false;
}
const BITMAPINFOHEADER* bmih = GetBitmapInfoHeader(record_span);
if (!bmih) {
return false;
}
bool play_normally = true;
res = false;
HDC hdc = context->hdc;
SkBitmap bitmap;
// TODO(crbug.com/503801928): Remove `BI_JPEG` and `BI_PNG` handling code
// if this check holds.
DUMP_WILL_BE_CHECK(bmih->biCompression == BI_RGB);
if (bmih->biCompression == BI_JPEG) {
base::span<const uint8_t> bits =
GetBitmapBits(record_span, bmih->biSizeImage);
if (bits.empty()) {
return false;
}
if (!DIBFormatNativelySupported(hdc, CHECKJPEGFORMAT, bits)) {
play_normally = false;
bitmap = gfx::JPEGCodec::Decode(bits);
if (bitmap.isNull()) {
return false;
}
}
} else if (bmih->biCompression == BI_PNG) {
base::span<const uint8_t> bits =
GetBitmapBits(record_span, bmih->biSizeImage);
if (bits.empty()) {
return false;
}
if (!DIBFormatNativelySupported(hdc, CHECKPNGFORMAT, bits)) {
play_normally = false;
bitmap = gfx::PNGCodec::Decode(bits);
if (bitmap.isNull()) {
return false;
}
}
}
if (play_normally) {
res = Play(context);
} else {
const auto* sdib_record =
reinterpret_cast<const EMRSTRETCHDIBITS*>(record());
const uint32_t* pixels =
static_cast<const uint32_t*>(bitmap.getPixels());
CHECK(pixels);
BITMAPINFOHEADER bmi = {0};
skia::CreateBitmapHeaderForN32SkBitmap(bitmap, &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));
}
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: {
CHECK_EQ(record()->nSize, sizeof(EMRMODIFYWORLDTRANSFORM));
const auto* modify_world_transform =
reinterpret_cast<const EMRMODIFYWORLDTRANSFORM*>(record());
HDC hdc = context->hdc;
switch (modify_world_transform->iMode) {
case MWT_IDENTITY:
if (base_matrix) {
res = 0 != ::SetWorldTransform(hdc, base_matrix);
} else {
res = 0 != ::ModifyWorldTransform(
hdc, &modify_world_transform->xform, MWT_IDENTITY);
}
break;
case MWT_LEFTMULTIPLY:
case MWT_RIGHTMULTIPLY:
res = 0 != ::ModifyWorldTransform(hdc, &modify_world_transform->xform,
modify_world_transform->iMode);
break;
case 4: // MWT_SET
if (base_matrix) {
res = 0 != ::SetWorldTransform(hdc, base_matrix) &&
::ModifyWorldTransform(hdc, &modify_world_transform->xform,
MWT_LEFTMULTIPLY);
} else {
res = 0 != ::SetWorldTransform(hdc, &modify_world_transform->xform);
}
break;
default:
res = false;
break;
}
break;
}
case EMR_SETLAYOUT:
// Ignore it.
res = true;
break;
default: {
res = Play(context);
break;
}
}
return res;
}
void Emf::StartPage(const gfx::Size& /*page_size*/,
const gfx::Rect& /*content_area*/,
float /*scale_factor*/,
mojom::PageOrientation /*page_orientation*/) {}
bool Emf::FinishPage() {
return true;
}
Emf::Enumerator::Enumerator(const Emf& emf, HDC context, const RECT* rect)
: context_(emf.GetDataSize()) {
CHECK(::EnumEnhMetaFile(context, emf.emf(), &Emf::Enumerator::EnhMetaFileProc,
this, rect));
DCHECK_EQ(context_.hdc, context);
}
Emf::Enumerator::~Enumerator() = default;
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 (record->nSize < kMinEnhMetaRecordSize ||
record->nSize > emf.context_.remaining_metafile_size ||
record->nSize % 4) {
return 0;
}
emf.context_.remaining_metafile_size -= record->nSize;
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;
}
} // namespace printing