From 1e1af7aa41c116b923768de5149c5c01bbe9cc88 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Fri, 4 Oct 2024 17:54:30 +0100 Subject: [PATCH 1/2] [glyf] Attempt to drop empty components from composite glyphs. This is because such components cause DirectWrite to fail to render the composite at all, even though an empty component should (presumably) have no visible effect. Reported in the context of pdf.js at https://github.com/mozilla/pdf.js/issues/18848, but experimentation indicates that Windows/DWrite fails to paint such glyphs in other contexts as well -- e.g. when using Character Map to view an installed font. (I don't think there's any real use case for such components, and their presence is probably an anomaly/artifact of a particular font creation/subsetting/embedding workflow. Still, if OTS can fix them up, this will best serve end users.) The current patch discards empty components from composite glyphs *unless* the empty glyph is the last component. In that case, we can't just skip it, because the preceding component (which will now be last) has the MORE_COMPONENTS flag, and that's no longer true. I have not seen cases in the wild where this issue affects the final component, but it's presumably possible (and testing indicates that DirectWrite would fail in that case too), so this would be a good followup enhancement. Currently the code just issues a warning if such a case occurs. --- src/glyf.cc | 54 ++++++++++++++++++++++++++++++++++++++++++++++++----- src/glyf.h | 10 ++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/glyf.cc b/src/glyf.cc index 12c0537f..ad6de53d 100644 --- a/src/glyf.cc +++ b/src/glyf.cc @@ -15,6 +15,8 @@ // glyf - Glyph Data // http://www.microsoft.com/typography/otspec/glyf.htm +#define TABLE_NAME "glyf" + namespace ots { bool OpenTypeGLYF::ParseFlagsForSimpleGlyph(Buffer &glyph, @@ -259,10 +261,15 @@ bool OpenTypeGLYF::ParseSimpleGlyph(Buffer &glyph, bool OpenTypeGLYF::ParseCompositeGlyph( Buffer &glyph, - ComponentPointCount* component_point_count) { + unsigned glyph_id, + ComponentPointCount* component_point_count, + unsigned* skip_count) { uint16_t flags = 0; uint16_t gid = 0; + std::vector> skip_ranges; do { + unsigned int start = glyph.offset(); + if (!glyph.ReadU16(&flags) || !glyph.ReadU16(&gid)) { return Error("Can't read composite glyph flags or glyphIndex"); } @@ -309,6 +316,23 @@ bool OpenTypeGLYF::ParseCompositeGlyph( } } + if (this->loca->offsets[gid] == this->loca->offsets[gid + 1]) { + // DirectWrite chokes on composite glyphs that have a completely empty glyph + // as a component; see https://github.com/mozilla/pdf.js/issues/18848. + // To work around this, we attempt to drop empty components. + Font* font = this->GetFont(); + if (flags & MORE_COMPONENTS) { + OTS_WARNING("skipping empty gid %u used as component in composite glyph %u", gid, glyph_id); + skip_ranges.push_back(std::make_pair(start, glyph.offset())); + } else { + // We can't drop the empty glyph if it is the last component, because that would + // invalidate the previous component's flags. + // TODO: Handle this case? Not currently observed in the wild, so maybe not a priority. + OTS_WARNING("empty gid %u used as last component in glyph %u; " + "may fail to render in Windows", gid, glyph_id); + } + } + // Push inital components on stack at level 1 // to traverse them in parent function. component_point_count->gid_stack.push_back({gid, 1}); @@ -333,7 +357,21 @@ bool OpenTypeGLYF::ParseCompositeGlyph( } } - this->iov.push_back(std::make_pair(glyph.buffer(), glyph.offset())); + // Append the glyph data to this->iov, skipping any ranges recorded in skip_ranges. + *skip_count = 0; + unsigned offset = 0; + while (!skip_ranges.empty()) { + auto& range = skip_ranges.front(); + if (range.first > offset) { + this->iov.push_back(std::make_pair(glyph.buffer() + offset, range.first - offset)); + } + offset = range.second; + *skip_count += range.second - range.first; + skip_ranges.erase(skip_ranges.begin()); + } + if (glyph.offset() > offset) { + this->iov.push_back(std::make_pair(glyph.buffer() + offset, glyph.offset() - offset)); + } return true; } @@ -353,6 +391,7 @@ bool OpenTypeGLYF::Parse(const uint8_t *data, size_t length) { GetFont()->GetTypedTable(OTS_TAG_NAME)); bool is_tricky = name->IsTrickyFont(); + this->loca = loca; this->maxp = maxp; const unsigned num_glyphs = maxp->num_glyphs; @@ -366,6 +405,9 @@ bool OpenTypeGLYF::Parse(const uint8_t *data, size_t length) { uint32_t current_offset = 0; for (unsigned i = 0; i < num_glyphs; ++i) { + // Used by ParseCompositeGlyph to return the number of bytes being skipped + // in the glyph description, so we can adjust offsets properly. + unsigned skip_count = 0; Buffer glyph(GetGlyphBufferSection(data, length, offsets, i)); if (!glyph.buffer()) @@ -414,7 +456,7 @@ bool OpenTypeGLYF::Parse(const uint8_t *data, size_t length) { } else { ComponentPointCount component_point_count; - if (!ParseCompositeGlyph(glyph, &component_point_count)) { + if (!ParseCompositeGlyph(glyph, i, &component_point_count, &skip_count)) { return Error("Failed to parse glyph %d", i); } @@ -465,7 +507,7 @@ bool OpenTypeGLYF::Parse(const uint8_t *data, size_t length) { } } - size_t new_size = glyph.offset(); + size_t new_size = glyph.offset() - skip_count; resulting_offsets[i] = current_offset; // glyphs must be four byte aligned // TODO(yusukes): investigate whether this padding is really necessary. @@ -622,7 +664,7 @@ Buffer OpenTypeGLYF::GetGlyphBufferSection( bool OpenTypeGLYF::Serialize(OTSStream *out) { for (unsigned i = 0; i < this->iov.size(); ++i) { if (!out->Write(this->iov[i].first, this->iov[i].second)) { - return Error("Falied to write glyph %d", i); + return Error("Failed to write glyph %d", i); } } @@ -630,3 +672,5 @@ bool OpenTypeGLYF::Serialize(OTSStream *out) { } } // namespace ots + +#undef TABLE_NAME diff --git a/src/glyf.h b/src/glyf.h index c0d5871e..74ca87bc 100644 --- a/src/glyf.h +++ b/src/glyf.h @@ -12,6 +12,7 @@ #include "ots.h" namespace ots { +class OpenTypeLOCA; class OpenTypeMAXP; class OpenTypeGLYF : public Table { @@ -53,10 +54,14 @@ class OpenTypeGLYF : public Table { int16_t xmax, int16_t ymax, bool is_tricky_font); + + // The skip_count outparam returns the number of bytes from the original + // glyph description that are being skipped on output (normally zero). bool ParseCompositeGlyph( Buffer &glyph, - ComponentPointCount* component_point_count); - + unsigned glyph_id, + ComponentPointCount* component_point_count, + unsigned* skip_count); bool TraverseComponentsCountingPoints( Buffer& glyph, @@ -70,6 +75,7 @@ class OpenTypeGLYF : public Table { const std::vector& loca_offsets, unsigned glyph_id); + OpenTypeLOCA* loca; OpenTypeMAXP* maxp; std::vector > iov; From 5eaf8b28d8ede8665fe94200095205e1f48efdba Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Mon, 7 Oct 2024 13:37:14 +0100 Subject: [PATCH 2/2] [glyf] More complete fixup for composite glyphs with empty components. This attempts to handle composite-glyph fixup more thoroughly, including the ability to drop the last component of a composite. Includes a couple of test fonts (derived from the pdf.js issue) that hit the new codepath. --- src/glyf.cc | 125 ++++++++++++++---- src/glyf.h | 6 +- ...3cceb6013b960021d7779081ee4d707d7b80f5.ttf | Bin 0 -> 11176 bytes ...ce5d954a1696217ac99318e7deba01236eca95.ttf | Bin 0 -> 10760 bytes tests/meson.build | 2 + 5 files changed, 108 insertions(+), 25 deletions(-) create mode 100755 tests/fonts/good/113cceb6013b960021d7779081ee4d707d7b80f5.ttf create mode 100755 tests/fonts/good/1cce5d954a1696217ac99318e7deba01236eca95.ttf diff --git a/src/glyf.cc b/src/glyf.cc index ad6de53d..14eca719 100644 --- a/src/glyf.cc +++ b/src/glyf.cc @@ -231,7 +231,7 @@ bool OpenTypeGLYF::ParseSimpleGlyph(Buffer &glyph, this->iov.push_back(std::make_pair(glyph.buffer(), 2)); // output a fixed-up version of the bounding box uint8_t* fixed_bbox = new uint8_t[8]; - fixed_bboxes.push_back(fixed_bbox); + replacements.push_back(fixed_bbox); xmin = ots_htons(xmin); std::memcpy(fixed_bbox, &xmin, 2); ymin = ots_htons(ymin); @@ -266,9 +266,19 @@ bool OpenTypeGLYF::ParseCompositeGlyph( unsigned* skip_count) { uint16_t flags = 0; uint16_t gid = 0; - std::vector> skip_ranges; + enum class edit_t : uint8_t { + skip_bytes, // param is number of bytes to skip from offset + set_flag, // param is flag to be set (in the 16-bit field at offset) + clear_flag, // param is flag to be cleared + }; + // List of glyph data edits to be applied: first value is offset in the data, + // second is a pair of . + typedef std::pair> edit_rec; + std::vector edits; + unsigned prev_start = 0; + bool we_have_instructions = false; do { - unsigned int start = glyph.offset(); + unsigned start = glyph.offset(); if (!glyph.ReadU16(&flags) || !glyph.ReadU16(&gid)) { return Error("Can't read composite glyph flags or glyphIndex"); @@ -317,28 +327,49 @@ bool OpenTypeGLYF::ParseCompositeGlyph( } if (this->loca->offsets[gid] == this->loca->offsets[gid + 1]) { + Warning("empty gid %u used as component in glyph %u", gid, glyph_id); // DirectWrite chokes on composite glyphs that have a completely empty glyph // as a component; see https://github.com/mozilla/pdf.js/issues/18848. // To work around this, we attempt to drop empty components. - Font* font = this->GetFont(); - if (flags & MORE_COMPONENTS) { - OTS_WARNING("skipping empty gid %u used as component in composite glyph %u", gid, glyph_id); - skip_ranges.push_back(std::make_pair(start, glyph.offset())); - } else { - // We can't drop the empty glyph if it is the last component, because that would - // invalidate the previous component's flags. - // TODO: Handle this case? Not currently observed in the wild, so maybe not a priority. - OTS_WARNING("empty gid %u used as last component in glyph %u; " - "may fail to render in Windows", gid, glyph_id); + // But we don't drop the component if it's the only (remaining) one in the composite. + if (prev_start > 0 || (flags & MORE_COMPONENTS)) { + if (!(flags & MORE_COMPONENTS)) { + // We're dropping the last component, so we need to clear the MORE_COMPONENTS flag + // on the previous one. + edits.push_back(edit_rec{prev_start, std::make_pair(edit_t::clear_flag, MORE_COMPONENTS)}); + } + // If this component was the first to provide WE_HAVE_INSTRUCTIONS, set it on the previous (if any). + if ((flags & WE_HAVE_INSTRUCTIONS) && !we_have_instructions && prev_start > 0) { + edits.push_back(edit_rec{prev_start, std::make_pair(edit_t::set_flag, WE_HAVE_INSTRUCTIONS)}); + } + // Finally, skip the actual bytes of this component. + edits.push_back(edit_rec{start, std::make_pair(edit_t::skip_bytes, glyph.offset() - start)}); } + } else { + // If this is the first component we're keeping, but we already saw WE_HAVE_INSTRUCTIONS + // (on a dropped component), we need to ensure that flag is set here. + if (prev_start == 0 && we_have_instructions && !(flags & WE_HAVE_INSTRUCTIONS)) { + edits.push_back(edit_rec{start, std::make_pair(edit_t::set_flag, WE_HAVE_INSTRUCTIONS)}); + } + prev_start = start; } - // Push inital components on stack at level 1 + we_have_instructions = we_have_instructions || (flags & WE_HAVE_INSTRUCTIONS); + + // Push initial components on stack at level 1 // to traverse them in parent function. component_point_count->gid_stack.push_back({gid, 1}); } while (flags & MORE_COMPONENTS); - if (flags & WE_HAVE_INSTRUCTIONS) { + // Sort any required edits by offset in the glyph data. + struct { + bool operator() (const edit_rec& a, const edit_rec& b) const { + return a.first < b.first; + } + } cmp; + std::sort(edits.begin(), edits.end(), cmp); + + if (we_have_instructions) { uint16_t bytecode_length; if (!glyph.ReadU16(&bytecode_length)) { return Error("Can't read instructions size"); @@ -357,18 +388,66 @@ bool OpenTypeGLYF::ParseCompositeGlyph( } } - // Append the glyph data to this->iov, skipping any ranges recorded in skip_ranges. + // Record the glyph data in this->iov, accounting for any required edits. *skip_count = 0; unsigned offset = 0; - while (!skip_ranges.empty()) { - auto& range = skip_ranges.front(); - if (range.first > offset) { - this->iov.push_back(std::make_pair(glyph.buffer() + offset, range.first - offset)); + while (!edits.empty()) { + auto& edit = edits.front(); + // Handle any glyph data between current offset and the next edit position. + if (edit.first > offset) { + this->iov.push_back(std::make_pair(glyph.buffer() + offset, edit.first - offset)); + offset = edit.first; + } + + // Handle the edit. Note that there may be multiple set_flag/clear_flag edits + // at the same offset, but skip_bytes will never coincide with another edit. + auto& action = edit.second; + switch (action.first) { + case edit_t::set_flag: + case edit_t::clear_flag: { + // Read the existing flags word. + uint16_t flags; + std::memcpy(&flags, glyph.buffer() + offset, 2); + flags = ots_ntohs(flags); + // Apply all flag changes for the current offset. + while (!edits.empty() && edits.front().first == offset) { + auto& e = edits.front(); + switch (e.second.first) { + case edit_t::set_flag: + flags |= e.second.second; + break; + case edit_t::clear_flag: + flags &= ~e.second.second; + break; + default: + assert(false); + break; + } + edits.erase(edits.begin()); + } + // Record the modified flags word. + flags = ots_htons(flags); + uint8_t* flags_data = new uint8_t[2]; + std::memcpy(flags_data, &flags, 2); + replacements.push_back(flags_data); + this->iov.push_back(std::make_pair(flags_data, 2)); + offset += 2; + break; + } + + case edit_t::skip_bytes: + offset = edit.first + action.second; + *skip_count += action.second; + edits.erase(edits.begin()); + break; + + default: + assert(false); + break; } - offset = range.second; - *skip_count += range.second - range.first; - skip_ranges.erase(skip_ranges.begin()); } + + // Handle any remaining glyph data after the last edit. if (glyph.offset() > offset) { this->iov.push_back(std::make_pair(glyph.buffer() + offset, glyph.offset() - offset)); } diff --git a/src/glyf.h b/src/glyf.h index 74ca87bc..d9d102d3 100644 --- a/src/glyf.h +++ b/src/glyf.h @@ -21,7 +21,7 @@ class OpenTypeGLYF : public Table { : Table(font, tag, tag), maxp(NULL) { } ~OpenTypeGLYF() { - for (auto* p : fixed_bboxes) { + for (auto* p : replacements) { delete[] p; } } @@ -80,7 +80,9 @@ class OpenTypeGLYF : public Table { std::vector > iov; - std::vector fixed_bboxes; + // Any blocks of replacement data created during parsing are stored here + // to be available during serialization. + std::vector replacements; }; } // namespace ots diff --git a/tests/fonts/good/113cceb6013b960021d7779081ee4d707d7b80f5.ttf b/tests/fonts/good/113cceb6013b960021d7779081ee4d707d7b80f5.ttf new file mode 100755 index 0000000000000000000000000000000000000000..6cbd25e0cb7cd344664206a5b5e44b729139f237 GIT binary patch literal 11176 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 AB>(^b literal 0 HcmV?d00001 diff --git a/tests/meson.build b/tests/meson.build index 0b3bdd14..15b500ef 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -7,6 +7,7 @@ good_fonts = [ 'fonts/good/0c4afb23b983bbab65c39869b81ca1dfb90c0258.ttf', 'fonts/good/0e16ec5ab94d3992bba42a9177b159113ede1485.ttf', 'fonts/good/0fc088827bbe36bfb3fb6c3d8b59f66cb234dfb3.ttf', + 'fonts/good/113cceb6013b960021d7779081ee4d707d7b80f5.ttf', 'fonts/good/1232d0423fe3bb731faa3da008281ca030d3fe0a.woff', 'fonts/good/126e13890b4c36319166a07bb5f4301132e6dcee.ttf', 'fonts/good/14b84df95987d7ba699d058686e9163eb4ca5e75.ttf', @@ -15,6 +16,7 @@ good_fonts = [ 'fonts/good/164f99832db39451f53858175d6c2d251feb028c.ttf', 'fonts/good/171ec9ef597e59a0f33cdeae1d4cf43af1d255ce.otf', 'fonts/good/19d60aa144bb703f0c7535e3b34e926e0cae954d.ttf', + 'fonts/good/1cce5d954a1696217ac99318e7deba01236eca95.ttf', 'fonts/good/224f3a28601603e869da5ab0650148ae8cbadd2d.ttf', 'fonts/good/24834cb0e118b8a80c05209d996963cf05121f43.ttf', 'fonts/good/27312d3d8d62bf7f3d2dec4afd90ac5549c05958.otf',