diff --git a/engine/src/flutter/include/minikin/CmapCoverage.h b/engine/src/flutter/include/minikin/CmapCoverage.h index 19b43f38de..5136d8692a 100644 --- a/engine/src/flutter/include/minikin/CmapCoverage.h +++ b/engine/src/flutter/include/minikin/CmapCoverage.h @@ -23,7 +23,7 @@ namespace minikin { class CmapCoverage { public: - static bool getCoverage(SparseBitSet &coverage, const uint8_t* cmap_data, size_t cmap_size, + static SparseBitSet getCoverage(const uint8_t* cmap_data, size_t cmap_size, bool* has_cmap_format14_subtable); }; diff --git a/engine/src/flutter/include/minikin/SparseBitSet.h b/engine/src/flutter/include/minikin/SparseBitSet.h index ba9d7797fb..91288988ac 100644 --- a/engine/src/flutter/include/minikin/SparseBitSet.h +++ b/engine/src/flutter/include/minikin/SparseBitSet.h @@ -31,19 +31,20 @@ namespace minikin { // of thousands to millions. It is particularly efficient when there are // large gaps. The motivating example is Unicode coverage of a font, but // the abstraction itself is fully general. - class SparseBitSet { public: - SparseBitSet(): mMaxVal(0), mOwnIndicesAndBitmaps(false) { - } - - // Clear the set - void clear(); + // Create an empty bit set. + SparseBitSet() : mMaxVal(0) {} // Initialize the set to a new value, represented by ranges. For // simplicity, these ranges are arranged as pairs of values, // inclusive of start, exclusive of end, laid out in a uint32 array. - void initFromRanges(const uint32_t* ranges, size_t nRanges); + SparseBitSet(const uint32_t* ranges, size_t nRanges) : SparseBitSet() { + initFromRanges(ranges, nRanges); + } + + SparseBitSet(SparseBitSet&&) = default; + SparseBitSet& operator=(SparseBitSet&&) = default; // Determine whether the value is included in the set bool get(uint32_t ch) const { @@ -65,6 +66,8 @@ public: static const uint32_t kNotFound = ~0u; private: + void initFromRanges(const uint32_t* ranges, size_t nRanges); + static const int kLogValuesPerPage = 8; static const int kPageMask = (1 << kLogValuesPerPage) - 1; static const int kLogBytesPerEl = 2; @@ -81,18 +84,14 @@ private: uint32_t mMaxVal; - // True if this SparseBitSet is responsible for freeing mIndices and mBitamps. - bool mOwnIndicesAndBitmaps; - - uint32_t mIndexSize; - const uint32_t* mIndices; - uint32_t mBitmapSize; - const element* mBitmaps; + std::unique_ptr mIndices; + std::unique_ptr mBitmaps; uint32_t mZeroPageIndex; -}; -// Note: this thing cannot be used in vectors yet. If that were important, we'd need to -// make the copy constructor work, and probably set up move traits as well. + // Forbid copy and assign. + SparseBitSet(const SparseBitSet&) = delete; + void operator=(const SparseBitSet&) = delete; +}; } // namespace minikin diff --git a/engine/src/flutter/libs/minikin/CmapCoverage.cpp b/engine/src/flutter/libs/minikin/CmapCoverage.cpp index 6fa67155b4..ed053da788 100644 --- a/engine/src/flutter/libs/minikin/CmapCoverage.cpp +++ b/engine/src/flutter/libs/minikin/CmapCoverage.cpp @@ -132,76 +132,146 @@ static bool getCoverageFormat12(vector& coverage, const uint8_t* data, return true; } -bool CmapCoverage::getCoverage(SparseBitSet& coverage, const uint8_t* cmap_data, size_t cmap_size, +// Lower value has higher priority. 0 for the highest priority table. +// kLowestPriority for unsupported tables. +// This order comes from HarfBuzz's hb-ot-font.cc and needs to be kept in sync with it. +constexpr uint8_t kLowestPriority = 255; +uint8_t getTablePriority(uint16_t platformId, uint16_t encodingId) { + if (platformId == 3 && encodingId == 10) { + return 0; + } + if (platformId == 0 && encodingId == 6) { + return 1; + } + if (platformId == 0 && encodingId == 4) { + return 2; + } + if (platformId == 3 && encodingId == 1) { + return 3; + } + if (platformId == 0 && encodingId == 3) { + return 4; + } + if (platformId == 0 && encodingId == 2) { + return 5; + } + if (platformId == 0 && encodingId == 1) { + return 6; + } + if (platformId == 0 && encodingId == 0) { + return 7; + } + // Tables other than above are not supported. + return kLowestPriority; +} + +SparseBitSet CmapCoverage::getCoverage(const uint8_t* cmap_data, size_t cmap_size, bool* has_cmap_format14_subtable) { - vector coverageVec; - const size_t kHeaderSize = 4; - const size_t kNumTablesOffset = 2; - const size_t kTableSize = 8; - const size_t kPlatformIdOffset = 0; - const size_t kEncodingIdOffset = 2; - const size_t kOffsetOffset = 4; - const uint16_t kUnicodePlatformId = 0; - const uint16_t kMicrosoftPlatformId = 3; - const uint16_t kUnicodeBmpEncodingId = 1; - const uint16_t kVariationSequencesEncodingId = 5; - const uint16_t kUnicodeUcs4EncodingId = 10; - const uint32_t kNoTable = UINT32_MAX; + constexpr size_t kHeaderSize = 4; + constexpr size_t kNumTablesOffset = 2; + constexpr size_t kTableSize = 8; + constexpr size_t kPlatformIdOffset = 0; + constexpr size_t kEncodingIdOffset = 2; + constexpr size_t kOffsetOffset = 4; + constexpr size_t kFormatOffset = 0; + constexpr uint32_t kInvalidOffset = UINT32_MAX; + if (kHeaderSize > cmap_size) { - return false; + return SparseBitSet(); } uint32_t numTables = readU16(cmap_data, kNumTablesOffset); if (kHeaderSize + numTables * kTableSize > cmap_size) { - return false; + return SparseBitSet(); } - uint32_t bestTable = kNoTable; - bool hasCmapFormat14Subtable = false; - for (uint32_t i = 0; i < numTables; i++) { - uint16_t platformId = readU16(cmap_data, kHeaderSize + i * kTableSize + kPlatformIdOffset); - uint16_t encodingId = readU16(cmap_data, kHeaderSize + i * kTableSize + kEncodingIdOffset); - if (platformId == kMicrosoftPlatformId && encodingId == kUnicodeUcs4EncodingId) { - bestTable = i; - break; - } else if (platformId == kMicrosoftPlatformId && encodingId == kUnicodeBmpEncodingId) { - bestTable = i; - } else if (platformId == kUnicodePlatformId && - encodingId == kVariationSequencesEncodingId) { - uint32_t offset = readU32(cmap_data, kHeaderSize + i * kTableSize + kOffsetOffset); - if (offset <= cmap_size - 2 && readU16(cmap_data, offset) == 14) { - hasCmapFormat14Subtable = true; + + uint32_t bestTableOffset = kInvalidOffset; + uint16_t bestTableFormat = 0; + uint8_t bestTablePriority = kLowestPriority; + *has_cmap_format14_subtable = false; + for (uint32_t i = 0; i < numTables; ++i) { + const uint32_t tableHeadOffset = kHeaderSize + i * kTableSize; + const uint16_t platformId = readU16(cmap_data, tableHeadOffset + kPlatformIdOffset); + const uint16_t encodingId = readU16(cmap_data, tableHeadOffset + kEncodingIdOffset); + const uint32_t offset = readU32(cmap_data, tableHeadOffset + kOffsetOffset); + + if (offset > cmap_size - 2) { + continue; // Invalid table: not enough space to read. + } + const uint16_t format = readU16(cmap_data, offset + kFormatOffset); + + if (platformId == 0 /* Unicode */ && encodingId == 5 /* Variation Sequences */) { + if (!(*has_cmap_format14_subtable) && format == 14) { + *has_cmap_format14_subtable = true; + } else { + // Ignore the (0, 5) table if we have already seen another valid one or it's in a + // format we don't understand. + } + } else { + uint32_t length; + uint32_t language; + + if (format == 4) { + constexpr size_t lengthOffset = 2; + constexpr size_t languageOffset = 4; + constexpr size_t minTableSize = languageOffset + 2; + if (offset > cmap_size - minTableSize) { + continue; // Invalid table: not enough space to read. + } + length = readU16(cmap_data, offset + lengthOffset); + language = readU16(cmap_data, offset + languageOffset); + } else if (format == 12) { + constexpr size_t lengthOffset = 4; + constexpr size_t languageOffset = 8; + constexpr size_t minTableSize = languageOffset + 4; + if (offset > cmap_size - minTableSize) { + continue; // Invalid table: not enough space to read. + } + length = readU32(cmap_data, offset + lengthOffset); + language = readU32(cmap_data, offset + languageOffset); + } else { + continue; + } + + if (length > cmap_size - offset) { + continue; // Invalid table: table length is larger than whole cmap data size. + } + if (language != 0) { + // Unsupported or invalid table: this is either a subtable for the Macintosh + // platform (which we don't support), or an invalid subtable since language field + // should be zero for non-Macintosh subtables. + continue; + } + const uint8_t priority = getTablePriority(platformId, encodingId); + if (priority < bestTablePriority) { + bestTableOffset = offset; + bestTablePriority = priority; + bestTableFormat = format; } } + if (*has_cmap_format14_subtable && bestTablePriority == 0 /* highest priority */) { + // Already found the highest priority table and variation sequences table. No need to + // look at remaining tables. + break; + } } - *has_cmap_format14_subtable = hasCmapFormat14Subtable; -#ifdef VERBOSE_DEBUG - ALOGD("best table = %d\n", bestTable); -#endif - if (bestTable == kNoTable) { - return false; + if (bestTableOffset == kInvalidOffset) { + return SparseBitSet(); } - uint32_t offset = readU32(cmap_data, kHeaderSize + bestTable * kTableSize + kOffsetOffset); - if (offset > cmap_size - 2) { - return false; - } - uint16_t format = readU16(cmap_data, offset); - bool success = false; - const uint8_t* tableData = cmap_data + offset; - const size_t tableSize = cmap_size - offset; - if (format == 4) { + const uint8_t* tableData = cmap_data + bestTableOffset; + const size_t tableSize = cmap_size - bestTableOffset; + vector coverageVec; + bool success; + if (bestTableFormat == 4) { success = getCoverageFormat4(coverageVec, tableData, tableSize); - } else if (format == 12) { + } else { success = getCoverageFormat12(coverageVec, tableData, tableSize); } if (success) { - coverage.initFromRanges(&coverageVec.front(), coverageVec.size() >> 1); + return SparseBitSet(&coverageVec.front(), coverageVec.size() >> 1); + } else { + return SparseBitSet(); } -#ifdef VERBOSE_DEBUG - for (size_t i = 0; i < coverageVec.size(); i += 2) { - ALOGD("%x:%x\n", coverageVec[i], coverageVec[i + 1]); - } - ALOGD("success = %d", success); -#endif - return success; + } } // namespace minikin diff --git a/engine/src/flutter/libs/minikin/FontFamily.cpp b/engine/src/flutter/libs/minikin/FontFamily.cpp index 492db1e4f4..39d374b99d 100644 --- a/engine/src/flutter/libs/minikin/FontFamily.cpp +++ b/engine/src/flutter/libs/minikin/FontFamily.cpp @@ -175,8 +175,7 @@ void FontFamily::computeCoverage() { ALOGE("Could not get cmap table size!\n"); return; } - // TODO: Error check? - CmapCoverage::getCoverage(mCoverage, cmapTable.get(), cmapTable.size(), &mHasVSTable); + mCoverage = CmapCoverage::getCoverage(cmapTable.get(), cmapTable.size(), &mHasVSTable); for (size_t i = 0; i < mFonts.size(); ++i) { std::unordered_set supportedAxes = mFonts[i].getSupportedAxesLocked(); diff --git a/engine/src/flutter/libs/minikin/SparseBitSet.cpp b/engine/src/flutter/libs/minikin/SparseBitSet.cpp index 51557df9af..28ae710c18 100644 --- a/engine/src/flutter/libs/minikin/SparseBitSet.cpp +++ b/engine/src/flutter/libs/minikin/SparseBitSet.cpp @@ -27,17 +27,6 @@ namespace minikin { const uint32_t SparseBitSet::kNotFound; -void SparseBitSet::clear() { - mMaxVal = 0; - if (mOwnIndicesAndBitmaps) { - delete[] mIndices; - delete[] mBitmaps; - mIndexSize = 0; - mBitmapSize = 0; - mOwnIndicesAndBitmaps = false; - } -} - uint32_t SparseBitSet::calcNumPages(const uint32_t* ranges, size_t nRanges) { bool haveZeroPage = false; uint32_t nonzeroPageEnd = 0; @@ -64,17 +53,12 @@ uint32_t SparseBitSet::calcNumPages(const uint32_t* ranges, size_t nRanges) { void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) { if (nRanges == 0) { - clear(); return; } mMaxVal = ranges[nRanges * 2 - 1]; - mIndexSize = (mMaxVal + kPageMask) >> kLogValuesPerPage; - uint32_t* indices = new uint32_t[mIndexSize]; + mIndices.reset(new uint32_t[(mMaxVal + kPageMask) >> kLogValuesPerPage]); uint32_t nPages = calcNumPages(ranges, nRanges); - mBitmapSize = nPages << (kLogValuesPerPage - kLogBitsPerEl); - element* bitmaps = new element[mBitmapSize]; - mOwnIndicesAndBitmaps = true; - memset(bitmaps, 0, nPages << (kLogValuesPerPage - 3)); + mBitmaps.reset(new element[nPages << (kLogValuesPerPage - kLogBitsPerEl)]()); mZeroPageIndex = noZeroPage; uint32_t nonzeroPageEnd = 0; uint32_t currentPage = 0; @@ -90,32 +74,30 @@ void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) { mZeroPageIndex = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl); } for (uint32_t j = nonzeroPageEnd; j < startPage; j++) { - indices[j] = mZeroPageIndex; + mIndices[j] = mZeroPageIndex; } } - indices[startPage] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl); + mIndices[startPage] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl); } size_t index = ((currentPage - 1) << (kLogValuesPerPage - kLogBitsPerEl)) + ((start & kPageMask) >> kLogBitsPerEl); size_t nElements = (end - (start & ~kElMask) + kElMask) >> kLogBitsPerEl; if (nElements == 1) { - bitmaps[index] |= (kElAllOnes >> (start & kElMask)) & + mBitmaps[index] |= (kElAllOnes >> (start & kElMask)) & (kElAllOnes << ((~end + 1) & kElMask)); } else { - bitmaps[index] |= kElAllOnes >> (start & kElMask); + mBitmaps[index] |= kElAllOnes >> (start & kElMask); for (size_t j = 1; j < nElements - 1; j++) { - bitmaps[index + j] = kElAllOnes; + mBitmaps[index + j] = kElAllOnes; } - bitmaps[index + nElements - 1] |= kElAllOnes << ((~end + 1) & kElMask); + mBitmaps[index + nElements - 1] |= kElAllOnes << ((~end + 1) & kElMask); } for (size_t j = startPage + 1; j < endPage + 1; j++) { - indices[j] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl); + mIndices[j] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl); } nonzeroPageEnd = endPage + 1; } - mBitmaps = bitmaps; - mIndices = indices; } int SparseBitSet::CountLeadingZeros(element x) { diff --git a/engine/src/flutter/tests/unittest/Android.mk b/engine/src/flutter/tests/unittest/Android.mk index d30c1062ce..b817c46961 100644 --- a/engine/src/flutter/tests/unittest/Android.mk +++ b/engine/src/flutter/tests/unittest/Android.mk @@ -19,8 +19,8 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_TEST_DATA := \ - data/BoldItalic.ttf \ data/Bold.ttf \ + data/BoldItalic.ttf \ data/ColorEmojiFont.ttf \ data/ColorTextMixedEmojiFont.ttf \ data/Emoji.ttf \ @@ -32,11 +32,14 @@ LOCAL_TEST_DATA := \ data/NoGlyphFont.ttf \ data/Regular.ttf \ data/TextEmojiFont.ttf \ + data/UnicodeBMPOnly.ttf \ + data/UnicodeBMPOnly2.ttf \ + data/UnicodeUCS4.ttf \ data/VariationSelectorTest-Regular.ttf \ data/ZhHans.ttf \ data/ZhHant.ttf \ + data/emoji.xml \ data/itemize.xml \ - data/emoji.xml LOCAL_TEST_DATA := $(foreach f,$(LOCAL_TEST_DATA),frameworks/minikin/tests:$(f)) @@ -66,6 +69,7 @@ LOCAL_SRC_FILES += \ ../util/FontTestUtils.cpp \ ../util/MinikinFontForTest.cpp \ ../util/UnicodeUtils.cpp \ + CmapCoverageTest.cpp \ EmojiTest.cpp \ FontCollectionTest.cpp \ FontCollectionItemizeTest.cpp \ diff --git a/engine/src/flutter/tests/unittest/CmapCoverageTest.cpp b/engine/src/flutter/tests/unittest/CmapCoverageTest.cpp new file mode 100644 index 0000000000..cbcc557a34 --- /dev/null +++ b/engine/src/flutter/tests/unittest/CmapCoverageTest.cpp @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +namespace minikin { + +size_t writeU16(uint16_t x, uint8_t* out, size_t offset) { + out[offset] = x >> 8; + out[offset + 1] = x; + return offset + 2; +} + +size_t writeI16(int16_t sx, uint8_t* out, size_t offset) { + return writeU16(static_cast(sx), out, offset); +} + +size_t writeU32(uint32_t x, uint8_t* out, size_t offset) { + out[offset] = x >> 24; + out[offset + 1] = x >> 16; + out[offset + 2] = x >> 8; + out[offset + 3] = x; + return offset + 4; +} + +// Returns valid cmap format 4 table contents. All glyph ID is same value as code point. (e.g. +// 'a' (U+0061) is mapped to Glyph ID = 0x0061). +// 'range' should be specified with inclusive-inclusive values. +static std::vector buildCmapFormat4Table(const std::vector& ranges) { + uint16_t segmentCount = ranges.size() / 2 + 1 /* +1 for end marker */; + + const size_t numOfUint16 = + 8 /* format, length, languages, segCountX2, searchRange, entrySelector, rangeShift, pad */ + + segmentCount * 4 /* endCount, startCount, idRange, idRangeOffset */; + const size_t finalLength = sizeof(uint16_t) * numOfUint16; + + std::vector out(finalLength); + size_t head = 0; + head = writeU16(4, out.data(), head); // format + head = writeU16(finalLength, out.data(), head); // length + head = writeU16(0, out.data(), head); // langauge + + const uint16_t searchRange = 2 * (1 << static_cast(floor(log2(segmentCount)))); + + head = writeU16(segmentCount * 2, out.data(), head); // segCountX2 + head = writeU16(searchRange, out.data(), head); // searchRange + head = writeU16(__builtin_ctz(searchRange) - 1, out.data(), head); // entrySelector + head = writeU16(segmentCount * 2 - searchRange, out.data(), head); // rangeShift + + size_t endCountHead = head; + size_t startCountHead = head + segmentCount * sizeof(uint16_t) + 2 /* padding */; + size_t idDeltaHead = startCountHead + segmentCount * sizeof(uint16_t); + size_t idRangeOffsetHead = idDeltaHead + segmentCount * sizeof(uint16_t); + + for (size_t i = 0; i < ranges.size() / 2; ++i) { + const uint16_t begin = ranges[i * 2]; + const uint16_t end = ranges[i * 2 + 1]; + startCountHead = writeU16(begin, out.data(), startCountHead); + endCountHead = writeU16(end, out.data(), endCountHead); + // map glyph ID as the same value of the code point. + idDeltaHead = writeU16(0, out.data(), idDeltaHead); + idRangeOffsetHead = writeU16(0 /* we don't use this */, out.data(), idRangeOffsetHead); + } + + // fill end marker + endCountHead = writeU16(0xFFFF, out.data(), endCountHead); + startCountHead = writeU16(0xFFFF, out.data(), startCountHead); + idDeltaHead = writeU16(1, out.data(), idDeltaHead); + idRangeOffsetHead = writeU16(0, out.data(), idRangeOffsetHead); + LOG_ALWAYS_FATAL_IF(endCountHead > finalLength); + LOG_ALWAYS_FATAL_IF(startCountHead > finalLength); + LOG_ALWAYS_FATAL_IF(idDeltaHead > finalLength); + LOG_ALWAYS_FATAL_IF(idRangeOffsetHead != finalLength); + return out; +} + +// Returns valid cmap format 4 table contents. All glyph ID is same value as code point. (e.g. +// 'a' (U+0061) is mapped to Glyph ID = 0x0061). +// 'range' should be specified with inclusive-inclusive values. +static std::vector buildCmapFormat12Table(const std::vector& ranges) { + uint32_t numGroups = ranges.size() / 2; + + const size_t finalLength = 2 /* format */ + 2 /* reserved */ + 4 /* length */ + + 4 /* languages */ + 4 /* numGroups */ + 12 /* size of a group */ * numGroups; + + std::vector out(finalLength); + size_t head = 0; + head = writeU16(12, out.data(), head); // format + head = writeU16(0, out.data(), head); // reserved + head = writeU32(finalLength, out.data(), head); // length + head = writeU32(0, out.data(), head); // langauge + head = writeU32(numGroups, out.data(), head); // numGroups + + for (uint32_t i = 0; i < numGroups; ++i) { + const uint32_t start = ranges[2 * i]; + const uint32_t end = ranges[2 * i + 1]; + head = writeU32(start, out.data(), head); + head = writeU32(end, out.data(), head); + // map glyph ID as the same value of the code point. + // TODO: Use glyph IDs lower than 65535. + // Cmap can store 32 bit glyph ID but due to the size of numGlyph, a font file can contain + // up to 65535 glyphs in a file. + head = writeU32(start, out.data(), head); + } + + LOG_ALWAYS_FATAL_IF(head != finalLength); + return out; +} + +class CmapBuilder { +public: + static constexpr size_t kEncodingTableHead = 4; + static constexpr size_t kEncodingTableSize = 8; + + CmapBuilder(int numTables) : mNumTables(numTables), mCurrentTableIndex(0) { + const size_t headerSize = + 2 /* version */ + 2 /* numTables */ + kEncodingTableSize * numTables; + out.resize(headerSize); + writeU16(0, out.data(), 0); + writeU16(numTables, out.data(), 2); + } + + void appendTable(uint16_t platformId, uint16_t encodingId, + const std::vector& table) { + appendEncodingTable(platformId, encodingId, out.size()); + out.insert(out.end(), table.begin(), table.end()); + } + + // TODO: Introduce Format 14 table builder. + + std::vector build() { + LOG_ALWAYS_FATAL_IF(mCurrentTableIndex != mNumTables); + return out; + } + + // Helper functions. + static std::vector buildSingleFormat4Cmap(uint16_t platformId, uint16_t encodingId, + const std::vector& ranges) { + CmapBuilder builder(1); + builder.appendTable(platformId, encodingId, buildCmapFormat4Table(ranges)); + return builder.build(); + } + + static std::vector buildSingleFormat12Cmap(uint16_t platformId, uint16_t encodingId, + const std::vector& ranges) { + CmapBuilder builder(1); + builder.appendTable(platformId, encodingId, buildCmapFormat12Table(ranges)); + return builder.build(); + } + +private: + void appendEncodingTable(uint16_t platformId, uint16_t encodingId, uint32_t offset) { + LOG_ALWAYS_FATAL_IF(mCurrentTableIndex == mNumTables); + + const size_t currentEncodingTableHead = + kEncodingTableHead + mCurrentTableIndex * kEncodingTableSize; + size_t head = writeU16(platformId, out.data(), currentEncodingTableHead); + head = writeU16(encodingId, out.data(), head); + head = writeU32(offset, out.data(), head); + LOG_ALWAYS_FATAL_IF((head - currentEncodingTableHead) != kEncodingTableSize); + mCurrentTableIndex++; + } + + int mNumTables; + int mCurrentTableIndex; + std::vector out; +}; + +TEST(CmapCoverageTest, SingleFormat4_brokenCmap) { + bool has_cmap_format_14_subtable = false; + { + SCOPED_TRACE("Reading beyond buffer size - Too small cmap size"); + std::vector cmap = + CmapBuilder::buildSingleFormat4Cmap(0, 0, std::vector({'a', 'a'})); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), 3 /* too small */, &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Reading beyond buffer size - space needed for tables goes beyond cmap size"); + std::vector cmap = + CmapBuilder::buildSingleFormat4Cmap(0, 0, std::vector({'a', 'a'})); + + writeU16(1000, cmap.data(), 2 /* offset of num tables in cmap header */); + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Reading beyond buffer size - Invalid offset in encoding table"); + std::vector cmap = + CmapBuilder::buildSingleFormat4Cmap(0, 0, std::vector({'a', 'a'})); + + writeU16(1000, cmap.data(), 8 /* offset of the offset in the first encoding record */); + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, SingleFormat4) { + bool has_cmap_format_14_subtable = false; + struct TestCast { + std::string testTitle; + uint16_t platformId; + uint16_t encodingId; + } TEST_CASES[] = { + { "Platform 0, Encoding 0", 0, 0 }, + { "Platform 0, Encoding 1", 0, 1 }, + { "Platform 0, Encoding 2", 0, 2 }, + { "Platform 0, Encoding 3", 0, 3 }, + { "Platform 3, Encoding 1", 3, 1 }, + }; + + for (const auto& testCase : TEST_CASES) { + SCOPED_TRACE(testCase.testTitle.c_str()); + std::vector cmap = CmapBuilder::buildSingleFormat4Cmap( + testCase.platformId, testCase.encodingId, std::vector({'a', 'a'})); + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); + EXPECT_FALSE(coverage.get('b')); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, SingleFormat12) { + bool has_cmap_format_14_subtable = false; + + struct TestCast { + std::string testTitle; + uint16_t platformId; + uint16_t encodingId; + } TEST_CASES[] = { + { "Platform 0, Encoding 4", 0, 4 }, + { "Platform 0, Encoding 6", 0, 6 }, + { "Platform 3, Encoding 10", 3, 10 }, + }; + + for (const auto& testCase : TEST_CASES) { + SCOPED_TRACE(testCase.testTitle.c_str()); + std::vector cmap = CmapBuilder::buildSingleFormat12Cmap( + testCase.platformId, testCase.encodingId, std::vector({'a', 'a'})); + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); + EXPECT_FALSE(coverage.get('b')); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, notSupportedEncodings) { + bool has_cmap_format_14_subtable = false; + + struct TestCast { + std::string testTitle; + uint16_t platformId; + uint16_t encodingId; + } TEST_CASES[] = { + // Any encodings with platform 2 is not supported. + { "Platform 2, Encoding 0", 2, 0 }, + { "Platform 2, Encoding 1", 2, 1 }, + { "Platform 2, Encoding 2", 2, 2 }, + { "Platform 2, Encoding 3", 2, 3 }, + // UCS-2 or UCS-4 are supported on Platform == 3. Others are not supported. + { "Platform 3, Encoding 0", 3, 0 }, // Symbol + { "Platform 3, Encoding 2", 3, 2 }, // ShiftJIS + { "Platform 3, Encoding 3", 3, 3 }, // RPC + { "Platform 3, Encoding 4", 3, 4 }, // Big5 + { "Platform 3, Encoding 5", 3, 5 }, // Wansung + { "Platform 3, Encoding 6", 3, 6 }, // Johab + { "Platform 3, Encoding 7", 3, 7 }, // Reserved + { "Platform 3, Encoding 8", 3, 8 }, // Reserved + { "Platform 3, Encoding 9", 3, 9 }, // Reserved + // Uknown platforms + { "Platform 4, Encoding 0", 4, 0 }, + { "Platform 5, Encoding 1", 5, 1 }, + { "Platform 6, Encoding 0", 6, 0 }, + { "Platform 7, Encoding 1", 7, 1 }, + }; + + for (const auto& testCase : TEST_CASES) { + SCOPED_TRACE(testCase.testTitle.c_str()); + CmapBuilder builder(1); + std::vector cmap = CmapBuilder::buildSingleFormat4Cmap( + testCase.platformId, testCase.encodingId, std::vector({'a', 'a'})); + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, brokenFormat4Table) { + bool has_cmap_format_14_subtable = false; + { + SCOPED_TRACE("Too small table cmap size"); + std::vector table = buildCmapFormat4Table(std::vector({'a', 'a'})); + table.resize(2); // Remove trailing data. + + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Too many segments"); + std::vector table = buildCmapFormat4Table(std::vector({'a', 'a'})); + writeU16(5000, table.data(), 6 /* segment count offset */); // 5000 segments. + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Inversed range"); + std::vector table = buildCmapFormat4Table(std::vector({'b', 'b'})); + // Put smaller end code point to inverse the range. + writeU16('a', table.data(), 14 /* the first element of endCount offset */); + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, brokenFormat12Table) { + bool has_cmap_format_14_subtable = false; + { + SCOPED_TRACE("Too small cmap size"); + std::vector table = buildCmapFormat12Table(std::vector({'a', 'a'})); + table.resize(2); // Remove trailing data. + + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Too many groups"); + std::vector table = buildCmapFormat12Table(std::vector({'a', 'a'})); + writeU32(5000, table.data(), 12 /* num group offset */); // 5000 groups. + + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Inversed range."); + std::vector table = buildCmapFormat12Table(std::vector({'a', 'a'})); + // Put larger start code point to inverse the range. + writeU32('b', table.data(), 16 /* start code point offset in the first group */); + + CmapBuilder builder(1); + builder.appendTable(0, 0, table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_EQ(0U, coverage.length()); + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, TableSelection_Priority) { + bool has_cmap_format_14_subtable = false; + std::vector highestFormat12Table = + buildCmapFormat12Table(std::vector({'a', 'a'})); + std::vector highestFormat4Table = + buildCmapFormat4Table(std::vector({'a', 'a'})); + std::vector format4 = buildCmapFormat4Table(std::vector({'b', 'b'})); + std::vector format12 = buildCmapFormat12Table(std::vector({'b', 'b'})); + + { + SCOPED_TRACE("(platform, encoding) = (3, 10) is the highest priority."); + + struct LowerPriorityTable { + uint16_t platformId; + uint16_t encodingId; + const std::vector& table; + } LOWER_PRIORITY_TABLES[] = { + { 0, 0, format4 }, + { 0, 1, format4 }, + { 0, 2, format4 }, + { 0, 3, format4 }, + { 0, 4, format12 }, + { 0, 6, format12 }, + { 3, 1, format4 }, + }; + + for (const auto& table : LOWER_PRIORITY_TABLES) { + CmapBuilder builder(2); + builder.appendTable(table.platformId, table.encodingId, table.table); + builder.appendTable(3, 10, highestFormat12Table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from highest table + EXPECT_FALSE(coverage.get('b')); // should not use other table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + } + { + SCOPED_TRACE("(platform, encoding) = (3, 1) case"); + + struct LowerPriorityTable { + uint16_t platformId; + uint16_t encodingId; + const std::vector& table; + } LOWER_PRIORITY_TABLES[] = { + { 0, 0, format4 }, + { 0, 1, format4 }, + { 0, 2, format4 }, + { 0, 3, format4 }, + }; + + for (const auto& table : LOWER_PRIORITY_TABLES) { + CmapBuilder builder(2); + builder.appendTable(table.platformId, table.encodingId, table.table); + builder.appendTable(3, 1, highestFormat4Table); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from highest table + EXPECT_FALSE(coverage.get('b')); // should not use other table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + } +} + +TEST(CmapCoverageTest, TableSelection_SkipBrokenFormat4Table) { + SparseBitSet coverage; + bool has_cmap_format_14_subtable = false; + std::vector validTable = + buildCmapFormat4Table(std::vector({'a', 'a'})); + { + SCOPED_TRACE("Unsupported format"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat4Table(std::vector({'b', 'b'})); + writeU16(0, table.data(), 0 /* format offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Invalid language"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat4Table(std::vector({'b', 'b'})); + writeU16(1, table.data(), 4 /* language offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Invalid length"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat4Table(std::vector({'b', 'b'})); + writeU16(5000, table.data(), 2 /* length offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +TEST(CmapCoverageTest, TableSelection_SkipBrokenFormat12Table) { + SparseBitSet coverage; + bool has_cmap_format_14_subtable = false; + std::vector validTable = + buildCmapFormat12Table(std::vector({'a', 'a'})); + { + SCOPED_TRACE("Unsupported format"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat12Table(std::vector({'b', 'b'})); + writeU16(0, table.data(), 0 /* format offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Invalid language"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat12Table(std::vector({'b', 'b'})); + writeU32(1, table.data(), 8 /* language offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } + { + SCOPED_TRACE("Invalid length"); + CmapBuilder builder(2); + std::vector table = + buildCmapFormat12Table(std::vector({'b', 'b'})); + writeU32(5000, table.data(), 4 /* length offset */); + builder.appendTable(3, 1, table); + builder.appendTable(0, 0, validTable); + std::vector cmap = builder.build(); + + SparseBitSet coverage = CmapCoverage::getCoverage( + cmap.data(), cmap.size(), &has_cmap_format_14_subtable); + EXPECT_TRUE(coverage.get('a')); // comes from valid table + EXPECT_FALSE(coverage.get('b')); // should not use invalid table. + EXPECT_FALSE(has_cmap_format_14_subtable); + } +} + +} // namespace minikin diff --git a/engine/src/flutter/tests/unittest/FontFamilyTest.cpp b/engine/src/flutter/tests/unittest/FontFamilyTest.cpp index 90cc794083..5a775a3587 100644 --- a/engine/src/flutter/tests/unittest/FontFamilyTest.cpp +++ b/engine/src/flutter/tests/unittest/FontFamilyTest.cpp @@ -45,6 +45,12 @@ static FontLanguage createFontLanguageWithoutICUSanitization(const std::string& return FontLanguage(input.c_str(), input.size()); } +std::shared_ptr makeFamily(const std::string& fontPath) { + std::shared_ptr font(new MinikinFontForTest(fontPath)); + return std::make_shared( + std::vector({Font(font, FontStyle())})); +} + TEST_F(FontLanguageTest, basicTests) { FontLanguage defaultLang; FontLanguage emptyLang("", 0); @@ -596,15 +602,8 @@ TEST_F(FontFamilyTest, createFamilyWithVariationTest) { const char kMultiAxisFont[] = kTestFontDir "/MultiAxis.ttf"; const char kNoAxisFont[] = kTestFontDir "/Regular.ttf"; - std::shared_ptr multiAxisFont(new MinikinFontForTest(kMultiAxisFont)); - std::shared_ptr multiAxisFamily( - std::shared_ptr(new FontFamily( - std::vector({Font(multiAxisFont, FontStyle())})))); - - std::shared_ptr noAxisFont(new MinikinFontForTest(kNoAxisFont)); - std::shared_ptr noAxisFamily( - std::shared_ptr(new FontFamily( - std::vector({Font(noAxisFont, FontStyle())})))); + std::shared_ptr multiAxisFamily = makeFamily(kMultiAxisFont); + std::shared_ptr noAxisFamily = makeFamily(kNoAxisFont); { // Do not ceate new instance if none of variations are specified. @@ -656,4 +655,31 @@ TEST_F(FontFamilyTest, createFamilyWithVariationTest) { } } +TEST_F(FontFamilyTest, coverageTableSelectionTest) { + // This font supports U+0061. The cmap subtable is format 4 and its platform ID is 0 and + // encoding ID is 1. + const char kUnicodeEncoding1Font[] = kTestFontDir "UnicodeBMPOnly.ttf"; + + // This font supports U+0061. The cmap subtable is format 4 and its platform ID is 0 and + // encoding ID is 3. + const char kUnicodeEncoding3Font[] = kTestFontDir "UnicodeBMPOnly2.ttf"; + + // This font has both cmap format 4 subtable which platform ID is 0 and encoding ID is 1 + // and cmap format 14 subtable which platform ID is 0 and encoding ID is 10. + // U+0061 is listed in both subtable but U+1F926 is only listed in latter. + const char kUnicodeEncoding4Font[] = kTestFontDir "UnicodeUCS4.ttf"; + + std::shared_ptr unicodeEnc1Font = makeFamily(kUnicodeEncoding1Font); + std::shared_ptr unicodeEnc3Font = makeFamily(kUnicodeEncoding3Font); + std::shared_ptr unicodeEnc4Font = makeFamily(kUnicodeEncoding4Font); + + android::AutoMutex _l(gMinikinLock); + + EXPECT_TRUE(unicodeEnc1Font->hasGlyph(0x0061, 0)); + EXPECT_TRUE(unicodeEnc3Font->hasGlyph(0x0061, 0)); + EXPECT_TRUE(unicodeEnc4Font->hasGlyph(0x0061, 0)); + + EXPECT_TRUE(unicodeEnc4Font->hasGlyph(0x1F926, 0)); +} + } // namespace minikin diff --git a/engine/src/flutter/tests/unittest/SparseBitSetTest.cpp b/engine/src/flutter/tests/unittest/SparseBitSetTest.cpp index cfb437f3f5..39c9e1b912 100644 --- a/engine/src/flutter/tests/unittest/SparseBitSetTest.cpp +++ b/engine/src/flutter/tests/unittest/SparseBitSetTest.cpp @@ -32,8 +32,7 @@ TEST(SparseBitSetTest, randomTest) { range.push_back((range.back() - 1) + distribution(mt)); } - SparseBitSet bitset; - bitset.initFromRanges(range.data(), range.size() / 2); + SparseBitSet bitset(range.data(), range.size() / 2); uint32_t ch = 0; for (size_t i = 0; i < range.size() / 2; ++i) {