Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions src/odr/internal/pdf/pdf_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <odr/internal/crypto/crypto_util.hpp>
#include <odr/internal/util/hash_util.hpp>
#include <odr/internal/util/number_util.hpp>

#include <optional>
#include <ostream>
Expand All @@ -12,9 +13,44 @@

namespace odr::internal::pdf {

namespace {

/// A name's regular characters (ISO 32000-1 7.3.5): anything else is written
/// as `#` and two hex digits, `#` itself included.
bool name_char_is_regular(const unsigned char c) {
return c >= 0x21 && c <= 0x7e && c != '#' && c != '/' && c != '%' &&
c != '(' && c != ')' && c != '<' && c != '>' && c != '[' && c != ']' &&
c != '{' && c != '}';
}

} // namespace

void StandardString::to_stream(std::ostream &out) const {
// TODO escape
out << "(" << string << ")";
// 7.3.4.2: only the reverse solidus and unbalanced parentheses need
// escaping, but balance is a property of the whole string, so escape every
// parenthesis rather than track it. A literal carriage return is read back
// as an end-of-line marker, i.e. as `\n`, so it has to be escaped too.
out << "(";
for (const char c : string) {
switch (c) {
case '\\':
out << "\\\\";
break;
case '(':
out << "\\(";
break;
case ')':
out << "\\)";
break;
case '\r':
out << "\\r";
break;
default:
out << c;
break;
}
}
out << ")";
}

std::string StandardString::to_string() const {
Expand All @@ -33,7 +69,16 @@ std::string HexString::to_string() const {
return ss.str();
}

void Name::to_stream(std::ostream &out) const { out << "/" << string; }
void Name::to_stream(std::ostream &out) const {
out << "/";
for (const char c : string) {
if (name_char_is_regular(static_cast<unsigned char>(c))) {
out << c;
} else {
out << fmt::format("#{:02X}", static_cast<unsigned char>(c));
}
}
}

std::string Name::to_string() const {
std::ostringstream ss;
Expand Down Expand Up @@ -136,8 +181,8 @@ void Object::to_stream(std::ostream &out) const {
} else if (is_integer()) {
out << as_integer();
} else if (is_real()) {
// not `setprecision`, which would stick to the stream
out << fmt::format("{:.4g}", as_real());
// 7.3.3 has no exponent form, and a stream would carry the host locale
out << util::number::to_string_significant(as_real(), 10);
} else if (is_standard_string()) {
as<StandardString>().to_stream(out);
} else if (is_hex_string()) {
Expand All @@ -164,12 +209,16 @@ std::string Object::to_string() const {
void Array::to_stream(std::ostream &out) const {
out << "[";

bool first = true;
for (const auto &item : *this) {
if (!first) {
out << " ";
}
first = false;
item.to_stream(out);
out << " ";
}

out << " ]";
out << "]";
}

std::string Array::to_string() const {
Expand All @@ -182,13 +231,13 @@ void Dictionary::to_stream(std::ostream &out) const {
out << "<<";

for (const auto &[key, value] : *this) {
out << "/" << key;
Name(key).to_stream(out);
out << " ";
value.to_stream(out);
out << " ";
}

out << " >>";
out << ">>";
}

std::string Dictionary::to_string() const {
Expand Down
18 changes: 18 additions & 0 deletions src/odr/internal/util/math_util.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <array>
#include <optional>

namespace odr::internal::util::math {

Expand Down Expand Up @@ -61,6 +62,23 @@ struct Transform2D {
apply(const double x, const double y) const noexcept {
return {a * x + c * y + e, b * x + d * y + f};
}

/// The transform undoing this one, or `nullopt` where the linear part is
/// singular.
[[nodiscard]] constexpr std::optional<Transform2D> inverse() const noexcept {
const double determinant = a * d - b * c;
if (determinant == 0) {
return std::nullopt;
}
return Transform2D{
d / determinant,
-b / determinant,
-c / determinant,
a / determinant,
(c * f - d * e) / determinant,
(b * e - a * f) / determinant,
};
}
};

} // namespace odr::internal::util::math
58 changes: 58 additions & 0 deletions test/src/internal/pdf/pdf_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,61 @@ TEST(PdfObject, to_string) {
EXPECT_EQ(Object(Name{"Type"}).to_string(), "/Type");
EXPECT_EQ(Object(ObjectReference(12, 0)).to_string(), "12 0 R");
}

// 7.3.3 knows no exponent form, and the host locale must not reach the output.
TEST(PdfObject, real_to_string_is_plain_decimal) {
EXPECT_EQ(Object(Real{1.5}).to_string(), "1.5");
EXPECT_EQ(Object(Real{0.0}).to_string(), "0");
EXPECT_EQ(Object(Real{-72.25}).to_string(), "-72.25");
// `{:g}` would have written these as `1e-05` and `1.44e+04`
EXPECT_EQ(Object(Real{0.00001}).to_string(), "0.00001");
EXPECT_EQ(Object(Real{14400.0}).to_string(), "14400");
// and would have rounded this one to four significant digits
EXPECT_EQ(Object(Real{612.345}).to_string(), "612.345");
}

TEST(PdfObject, standard_string_escapes_delimiters) {
EXPECT_EQ(Object(StandardString{"plain"}).to_string(), "(plain)");
EXPECT_EQ(Object(StandardString{"a(b)c"}).to_string(), R"((a\(b\)c))");
EXPECT_EQ(Object(StandardString{R"(back\slash)"}).to_string(),
R"((back\\slash))");
EXPECT_EQ(Object(StandardString{"cr\rlf"}).to_string(), R"((cr\rlf))");
// a line feed stands for itself (7.3.4.2), so it is written raw
EXPECT_EQ(Object(StandardString{"a\nb"}).to_string(), "(a\nb)");
}

TEST(PdfObject, name_escapes_irregular_characters) {
EXPECT_EQ(Object(Name{"Type"}).to_string(), "/Type");
EXPECT_EQ(Object(Name{"A;Name_With-Various***Chars?"}).to_string(),
"/A;Name_With-Various***Chars?");
EXPECT_EQ(Object(Name{"Adobe Green"}).to_string(), "/Adobe#20Green");
EXPECT_EQ(Object(Name{"paired()parentheses"}).to_string(),
"/paired#28#29parentheses");
EXPECT_EQ(Object(Name{"The_Key_of_F#_Minor"}).to_string(),
"/The_Key_of_F#23_Minor");
EXPECT_EQ(Object(Name{"1.2/3"}).to_string(), "/1.2#2F3");
}

TEST(PdfObject, container_to_string) {
Array array;
array.holder().emplace_back(Integer{1});
array.holder().emplace_back(Real{2.5});
array.holder().emplace_back(Name{"Three"});
EXPECT_EQ(Object(std::move(array)).to_string(), "[1 2.5 /Three]");

EXPECT_EQ(Object(Array{}).to_string(), "[]");

Dictionary dictionary;
dictionary["Type"] = Object(Name{"Catalog"});
EXPECT_EQ(Object(std::move(dictionary)).to_string(), "<</Type /Catalog >>");

EXPECT_EQ(Object(Dictionary{}).to_string(), "<<>>");
}

// A key is a name and is escaped as one, or a space in it would split the
// dictionary in two on the way back in.
TEST(PdfObject, dictionary_key_is_escaped) {
Dictionary dictionary;
dictionary["Odd Key"] = Object(Integer{1});
EXPECT_EQ(Object(std::move(dictionary)).to_string(), "<</Odd#20Key 1 >>");
}
35 changes: 35 additions & 0 deletions test/src/internal/pdf/pdf_object_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,38 @@ TEST(PdfObjectParser, hex_string_pads_odd_digit) {
// whitespace before the terminator must not be mistaken for a digit
EXPECT_EQ(read_hex_string("<41 >"), "A");
}

// What `to_stream` writes, `read_object` reads back unchanged.
TEST(PdfObjectParser, serialization_round_trips) {
const auto round_trip = [](const Object &object) {
return read_object(object.to_string()).first;
};

EXPECT_EQ(round_trip(Object(StandardString{"a(b)c\\d\re"})).as_string(),
"a(b)c\\d\re");
EXPECT_EQ(round_trip(Object(Name{"Odd #Name/With Spaces"})).as_string(),
"Odd #Name/With Spaces");
EXPECT_DOUBLE_EQ(round_trip(Object(Real{0.00001})).as_real(), 0.00001);
EXPECT_DOUBLE_EQ(round_trip(Object(Real{-612.345})).as_real(), -612.345);

Array array;
array.holder().emplace_back(Integer{1});
array.holder().emplace_back(Real{2.5});
array.holder().emplace_back(Name{"Three"});
const Object read_array = round_trip(Object(std::move(array)));
ASSERT_TRUE(read_array.is_array());
ASSERT_EQ(read_array.as_array().size(), 3u);
EXPECT_EQ(read_array.as_array()[0].as_integer(), 1);
EXPECT_DOUBLE_EQ(read_array.as_array()[1].as_real(), 2.5);
EXPECT_EQ(read_array.as_array()[2].as_string(), "Three");

Dictionary dictionary;
dictionary["Type"] = Object(Name{"Annot"});
dictionary["Odd Key"] = Object(Integer{1});
dictionary["Rect"] = Object(Real{72.5});
const Object read_dictionary = round_trip(Object(std::move(dictionary)));
ASSERT_TRUE(read_dictionary.is_dictionary());
EXPECT_EQ(read_dictionary.as_dictionary().get("Type").as_string(), "Annot");
EXPECT_EQ(read_dictionary.as_dictionary().get("Odd Key").as_integer(), 1);
EXPECT_DOUBLE_EQ(read_dictionary.as_dictionary().get("Rect").as_real(), 72.5);
}
30 changes: 30 additions & 0 deletions test/src/internal/util/math_util_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ TEST(Transform2D, compose_is_ordered) {
EXPECT_DOUBLE_EQ(q[1], 10);
}

// Applying a transform then its inverse returns the original point, whatever
// the transform is made of.
TEST(Transform2D, inverse_undoes_apply) {
const Transform2D m = Transform2D::translation(-30, -40) *
Transform2D::scaling_translation(1, -1, 0, 800) *
Transform2D{0, 1, -1, 0, 600, 0};

const auto inverse = m.inverse();
ASSERT_TRUE(inverse.has_value());

const auto forward = m.apply(123, 456);
const auto back = inverse->apply(forward[0], forward[1]);
EXPECT_DOUBLE_EQ(back[0], 123);
EXPECT_DOUBLE_EQ(back[1], 456);
}

TEST(Transform2D, inverse_of_identity_is_identity) {
const auto inverse = Transform2D().inverse();
ASSERT_TRUE(inverse.has_value());
const auto p = inverse->apply(3, 4);
EXPECT_DOUBLE_EQ(p[0], 3);
EXPECT_DOUBLE_EQ(p[1], 4);
}

// A singular linear part collapses the plane, so nothing undoes it.
TEST(Transform2D, inverse_of_singular_is_nullopt) {
EXPECT_FALSE(Transform2D::scaling(0, 1).inverse().has_value());
EXPECT_FALSE((Transform2D{1, 2, 2, 4, 5, 6}).inverse().has_value());
}

// Composing then applying equals applying each factor in sequence.
TEST(Transform2D, compose_matches_sequential_apply) {
const Transform2D a{1, 2, 3, 4, 5, 6};
Expand Down
Loading