ladybird/Userland/Libraries/LibPDF/Fonts/PS1FontProgram.cpp
Julian Offenhäuser 7c4f5b58be LibPDF: Use Gfx::PathRasterizer for Adobe Type 1 font rendering
This gives much better visual results than painting the path directly.
It also has the nice side effect that Type 1 fonts will now look much
more similar to TrueType fonts, which use the same class :^)

In addition, we can now cache glyph bitmaps for repeated use.
2022-11-19 11:04:34 +01:00

641 lines
19 KiB
C++

/*
* Copyright (c) 2022, Julian Offenhäuser <offenhaeuser@protonmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Font/PathRasterizer.h>
#include <LibPDF/CommonNames.h>
#include <LibPDF/Encoding.h>
#include <LibPDF/Fonts/PS1FontProgram.h>
#include <LibPDF/Reader.h>
#include <ctype.h>
#include <math.h>
namespace PDF {
enum Command {
HStem = 1,
VStem = 3,
VMoveTo,
RLineTo,
HLineTo,
VLineTo,
RRCurveTo,
ClosePath,
CallSubr,
Return,
Extended,
HSbW,
EndChar,
RMoveTo = 21,
HMoveTo,
VHCurveTo = 30,
HVCurveTo
};
enum ExtendedCommand {
DotSection,
VStem3,
HStem3,
Div = 12,
CallOtherSubr = 16,
Pop,
SetCurrentPoint = 33,
};
PDFErrorOr<void> PS1FontProgram::parse(ReadonlyBytes const& bytes, size_t cleartext_length, size_t encrypted_length)
{
Reader reader(bytes);
if (reader.remaining() == 0)
return error("Empty font program");
reader.move_to(0);
if (reader.remaining() < 2 || !reader.matches("%!"))
return error("Not a font program");
if (!seek_name(reader, CommonNames::Encoding))
return error("Missing encoding array");
if (TRY(parse_word(reader)) == "StandardEncoding") {
m_encoding = Encoding::standard_encoding();
} else {
HashMap<u16, CharDescriptor> descriptors;
while (reader.remaining()) {
auto word = TRY(parse_word(reader));
if (word == "readonly") {
break;
} else if (word == "dup") {
u32 code_point = TRY(parse_int(reader));
auto name = TRY(parse_word(reader));
descriptors.set(code_point, { name.starts_with('/') ? name.substring_view(1) : name.view(), code_point });
}
}
m_encoding = TRY(Encoding::create(descriptors));
}
bool found_font_matrix = seek_name(reader, "FontMatrix");
if (found_font_matrix) {
auto array = TRY(parse_number_array(reader, 6));
m_font_matrix = { array[0], array[1], array[2], array[3], array[4], array[5] };
} else {
m_font_matrix = { 0.001f, 0.0f, 0.0f, 0.001f, 0.0f, 0.0f };
}
auto decrypted = TRY(decrypt(reader.bytes().slice(cleartext_length, encrypted_length), 55665, 4));
return parse_encrypted_portion(decrypted);
}
RefPtr<Gfx::Bitmap> PS1FontProgram::rasterize_glyph(u32 code_point, float width)
{
auto path = build_char(code_point, width);
auto bounding_box = path.bounding_box().size();
u32 w = (u32)ceilf(bounding_box.width()) + 1;
u32 h = (u32)ceilf(bounding_box.height()) + 1;
Gfx::PathRasterizer rasterizer(Gfx::IntSize(w, h));
rasterizer.draw_path(path);
return rasterizer.accumulate();
}
Gfx::Path PS1FontProgram::build_char(u32 code_point, float width)
{
auto maybe_glyph = m_glyph_map.get(code_point);
if (!maybe_glyph.has_value())
return {};
auto& glyph = maybe_glyph.value();
auto transform = glyph_transform_to_device_space(glyph, width);
// Translate such that the top-left point is at [0, 0].
auto bounding_box = glyph.path.bounding_box();
Gfx::FloatPoint translation(-bounding_box.x(), -(bounding_box.y() + bounding_box.height()));
transform.translate(translation);
return glyph.path.copy_transformed(transform);
}
Gfx::FloatPoint PS1FontProgram::glyph_translation(u32 code_point, float width) const
{
auto maybe_glyph = m_glyph_map.get(code_point);
if (!maybe_glyph.has_value())
return {};
auto& glyph = maybe_glyph.value();
auto transform = glyph_transform_to_device_space(glyph, width);
// Undo the translation we applied earlier.
auto bounding_box = glyph.path.bounding_box();
Gfx::FloatPoint translation(bounding_box.x(), bounding_box.y() + bounding_box.height());
return transform.map(translation);
}
Gfx::AffineTransform PS1FontProgram::glyph_transform_to_device_space(Glyph const& glyph, float width) const
{
auto scale = width / (m_font_matrix.a() * glyph.width + m_font_matrix.e());
auto transform = m_font_matrix;
// Convert character space to device space.
transform.scale(scale, -scale);
return transform;
}
PDFErrorOr<PS1FontProgram::Glyph> PS1FontProgram::parse_glyph(ReadonlyBytes const& data, GlyphParserState& state)
{
auto push = [&](float value) -> PDFErrorOr<void> {
if (state.sp >= state.stack.size())
return error("Operand stack overflow");
state.stack[state.sp++] = value;
return {};
};
auto pop = [&]() -> float {
return state.sp ? state.stack[--state.sp] : 0.0f;
};
auto& path = state.glyph.path;
// Parse the stream of parameters and commands that make up a glyph outline.
for (size_t i = 0; i < data.size(); ++i) {
auto require = [&](unsigned num) -> PDFErrorOr<void> {
if (i + num >= data.size())
return error("Malformed glyph outline definition");
return {};
};
int v = data[i];
if (v == 255) {
TRY(require(4));
int a = data[++i];
int b = data[++i];
int c = data[++i];
int d = data[++i];
TRY(push((a << 24) + (b << 16) + (c << 8) + d));
} else if (v >= 251) {
TRY(require(1));
auto w = data[++i];
TRY(push(-((v - 251) * 256) - w - 108));
} else if (v >= 247) {
TRY(require(1));
auto w = data[++i];
TRY(push(((v - 247) * 256) + w + 108));
} else if (v >= 32) {
TRY(push(v - 139));
} else {
// Not a parameter but a command byte.
switch (v) {
case HStem:
case VStem:
state.sp = 0;
break;
case VMoveTo: {
auto dy = pop();
state.point.translate_by(0.0f, dy);
if (state.flex_feature) {
state.flex_sequence[state.flex_index++] = state.point.x();
state.flex_sequence[state.flex_index++] = state.point.y();
} else {
path.move_to(state.point);
}
state.sp = 0;
break;
}
case RLineTo: {
auto dy = pop();
auto dx = pop();
state.point.translate_by(dx, dy);
path.line_to(state.point);
state.sp = 0;
break;
}
case HLineTo: {
auto dx = pop();
state.point.translate_by(dx, 0.0f);
path.line_to(state.point);
state.sp = 0;
break;
}
case VLineTo: {
auto dy = pop();
state.point.translate_by(0.0f, dy);
path.line_to(state.point);
state.sp = 0;
break;
}
case RRCurveTo: {
auto dy3 = pop();
auto dx3 = pop();
auto dy2 = pop();
auto dx2 = pop();
auto dy1 = pop();
auto dx1 = pop();
auto& point = state.point;
path.cubic_bezier_curve_to(
point + Gfx::FloatPoint(dx1, dy1),
point + Gfx::FloatPoint(dx1 + dx2, dy1 + dy2),
point + Gfx::FloatPoint(dx1 + dx2 + dx3, dy1 + dy2 + dy3));
point.translate_by(dx1 + dx2 + dx3, dy1 + dy2 + dy3);
state.sp = 0;
break;
}
case ClosePath:
path.close();
state.sp = 0;
break;
case CallSubr: {
auto subr_number = pop();
if (static_cast<size_t>(subr_number) >= m_subroutines.size())
return error("Subroutine index out of range");
// Subroutines 0-2 handle the flex feature.
if (subr_number == 0) {
if (state.flex_index != 14)
break;
auto& flex = state.flex_sequence;
path.cubic_bezier_curve_to(
{ flex[2], flex[3] },
{ flex[4], flex[5] },
{ flex[6], flex[7] });
path.cubic_bezier_curve_to(
{ flex[8], flex[9] },
{ flex[10], flex[11] },
{ flex[12], flex[13] });
state.flex_feature = false;
state.sp = 0;
} else if (subr_number == 1) {
state.flex_feature = true;
state.flex_index = 0;
state.sp = 0;
} else if (subr_number == 2) {
state.sp = 0;
} else {
auto subr = m_subroutines[subr_number];
if (subr.is_empty())
return error("Empty subroutine");
TRY(parse_glyph(subr, state));
}
break;
}
case Return:
break;
case Extended: {
TRY(require(1));
switch (data[++i]) {
case DotSection:
case VStem3:
case HStem3:
// FIXME: Do something with these?
state.sp = 0;
break;
case Div: {
auto num2 = pop();
auto num1 = pop();
TRY(push(num2 ? num1 / num2 : 0.0f));
break;
}
case CallOtherSubr: {
auto othersubr_number = pop();
auto n = static_cast<int>(pop());
if (othersubr_number == 0) {
state.postscript_stack[state.postscript_sp++] = pop();
state.postscript_stack[state.postscript_sp++] = pop();
pop();
} else if (othersubr_number == 3) {
state.postscript_stack[state.postscript_sp++] = 3;
} else {
for (int i = 0; i < n; ++i)
state.postscript_stack[state.postscript_sp++] = pop();
}
(void)othersubr_number;
break;
}
case Pop:
TRY(push(state.postscript_stack[--state.postscript_sp]));
break;
case SetCurrentPoint: {
auto y = pop();
auto x = pop();
state.point = { x, y };
path.move_to(state.point);
state.sp = 0;
break;
}
default:
return error(String::formatted("Unhandled command: 12 {}", data[i]));
}
break;
}
case HSbW: {
auto wx = pop();
auto sbx = pop();
state.glyph.width = wx;
state.point = { sbx, 0.0f };
state.sp = 0;
break;
}
case EndChar:
break;
case RMoveTo: {
auto dy = pop();
auto dx = pop();
state.point.translate_by(dx, dy);
if (state.flex_feature) {
state.flex_sequence[state.flex_index++] = state.point.x();
state.flex_sequence[state.flex_index++] = state.point.y();
} else {
path.move_to(state.point);
}
state.sp = 0;
break;
}
case HMoveTo: {
auto dx = pop();
state.point.translate_by(dx, 0.0f);
if (state.flex_feature) {
state.flex_sequence[state.flex_index++] = state.point.x();
state.flex_sequence[state.flex_index++] = state.point.y();
} else {
path.move_to(state.point);
}
state.sp = 0;
break;
}
case VHCurveTo: {
auto dx3 = pop();
auto dy2 = pop();
auto dx2 = pop();
auto dy1 = pop();
auto& point = state.point;
path.cubic_bezier_curve_to(
point + Gfx::FloatPoint(0.0f, dy1),
point + Gfx::FloatPoint(dx2, dy1 + dy2),
point + Gfx::FloatPoint(dx2 + dx3, dy1 + dy2));
point.translate_by(dx2 + dx3, dy1 + dy2);
state.sp = 0;
break;
}
case HVCurveTo: {
auto dy3 = pop();
auto dy2 = pop();
auto dx2 = pop();
auto dx1 = pop();
auto& point = state.point;
path.cubic_bezier_curve_to(
point + Gfx::FloatPoint(dx1, 0.0f),
point + Gfx::FloatPoint(dx1 + dx2, dy2),
point + Gfx::FloatPoint(dx1 + dx2, dy2 + dy3));
point.translate_by(dx1 + dx2, dy2 + dy3);
state.sp = 0;
break;
}
default:
return error(String::formatted("Unhandled command: {}", v));
}
}
}
return state.glyph;
}
PDFErrorOr<void> PS1FontProgram::parse_encrypted_portion(ByteBuffer const& buffer)
{
Reader reader(buffer);
if (seek_name(reader, "lenIV"))
m_lenIV = TRY(parse_int(reader));
if (!seek_name(reader, "Subrs"))
return error("Missing subroutine array");
m_subroutines = TRY(parse_subroutines(reader));
if (!seek_name(reader, "CharStrings"))
return error("Missing char strings array");
while (reader.remaining()) {
auto word = TRY(parse_word(reader));
VERIFY(!word.is_empty());
if (word == "end")
break;
if (word[0] == '/') {
auto encrypted_size = TRY(parse_int(reader));
auto rd = TRY(parse_word(reader));
if (rd == "-|" || rd == "RD") {
auto line = TRY(decrypt(reader.bytes().slice(reader.offset(), encrypted_size), m_encryption_key, m_lenIV));
reader.move_by(encrypted_size);
auto name_mapping = m_encoding->name_mapping();
auto code_point = name_mapping.ensure(word.substring_view(1));
GlyphParserState state;
m_glyph_map.set(code_point, TRY(parse_glyph(line, state)));
}
}
}
return {};
}
PDFErrorOr<Vector<ByteBuffer>> PS1FontProgram::parse_subroutines(Reader& reader)
{
if (!reader.matches_number())
return error("Expected array length");
auto length = TRY(parse_int(reader));
VERIFY(length <= 1024);
Vector<ByteBuffer> array;
TRY(array.try_resize(length));
while (reader.remaining()) {
auto word = TRY(parse_word(reader));
if (word.is_empty())
VERIFY(0);
if (word == "dup") {
auto index = TRY(parse_int(reader));
auto entry = TRY(parse_word(reader));
if (entry.is_empty())
return error("Empty array entry");
if (index >= length)
return error("Array index out of bounds");
if (isdigit(entry[0])) {
auto maybe_encrypted_size = entry.to_int();
if (!maybe_encrypted_size.has_value())
return error("Malformed array");
auto rd = TRY(parse_word(reader));
if (rd == "-|" || rd == "RD") {
array[index] = TRY(decrypt(reader.bytes().slice(reader.offset(), maybe_encrypted_size.value()), m_encryption_key, m_lenIV));
reader.move_by(maybe_encrypted_size.value());
}
} else {
array[index] = TRY(ByteBuffer::copy(entry.bytes()));
}
} else if (word == "index") {
break;
}
}
return array;
}
PDFErrorOr<Vector<float>> PS1FontProgram::parse_number_array(Reader& reader, size_t length)
{
Vector<float> array;
TRY(array.try_resize(length));
reader.consume_whitespace();
if (!reader.consume('['))
return error("Expected array to start with '['");
reader.consume_whitespace();
for (size_t i = 0; i < length; ++i)
array.at(i) = TRY(parse_float(reader));
if (!reader.consume(']'))
return error("Expected array to end with ']'");
return array;
}
PDFErrorOr<String> PS1FontProgram::parse_word(Reader& reader)
{
reader.consume_whitespace();
auto start = reader.offset();
reader.move_while([&](char c) {
return !reader.matches_whitespace() && c != '[' && c != ']';
});
auto end = reader.offset();
if (reader.matches_whitespace())
reader.consume();
return StringView(reader.bytes().data() + start, end - start);
}
PDFErrorOr<float> PS1FontProgram::parse_float(Reader& reader)
{
auto word = TRY(parse_word(reader));
return strtof(String(word).characters(), nullptr);
}
PDFErrorOr<int> PS1FontProgram::parse_int(Reader& reader)
{
auto maybe_int = TRY(parse_word(reader)).to_int();
if (!maybe_int.has_value())
return error("Invalid int");
return maybe_int.value();
}
PDFErrorOr<ByteBuffer> PS1FontProgram::decrypt(ReadonlyBytes const& encrypted, u16 key, size_t skip)
{
auto decrypted = TRY(ByteBuffer::create_uninitialized(encrypted.size() - skip));
u16 R = key;
u16 c1 = 52845;
u16 c2 = 22719;
for (size_t i = 0; i < encrypted.size(); ++i) {
u8 C = encrypted[i];
u8 P = C ^ (R >> 8);
R = (C + R) * c1 + c2;
if (i >= skip)
decrypted[i - skip] = P;
}
return decrypted;
}
bool PS1FontProgram::seek_name(Reader& reader, String const& name)
{
auto start = reader.offset();
reader.move_to(0);
while (reader.remaining()) {
if (reader.consume('/') && reader.matches(name.characters())) {
// Skip name
reader.move_while([&](char) {
return reader.matches_regular_character();
});
reader.consume_whitespace();
return true;
}
}
// Jump back to where we started
reader.move_to(start);
return false;
}
Error PS1FontProgram::error(
String const& message
#ifdef PDF_DEBUG
,
SourceLocation loc
#endif
)
{
#ifdef PDF_DEBUG
dbgln("\033[31m{} Type 1 font error: {}\033[0m", loc, message);
#endif
return Error { Error::Type::MalformedPDF, message };
}
}