from fontTools import ttLib
from fontTools.ttLib import woff2
from fontTools.ttLib.tables import _g_l_y_f
from fontTools.ttLib.woff2 import (
    WOFF2Reader,
    woff2DirectorySize,
    woff2DirectoryFormat,
    woff2FlagsSize,
    woff2UnknownTagSize,
    woff2Base128MaxSize,
    WOFF2DirectoryEntry,
    getKnownTagIndex,
    packBase128,
    base128Size,
    woff2UnknownTagIndex,
    WOFF2FlavorData,
    woff2TransformedTableTags,
    WOFF2GlyfTable,
    WOFF2LocaTable,
    WOFF2HmtxTable,
    WOFF2Writer,
    unpackBase128,
    unpack255UShort,
    pack255UShort,
)
import unittest
from fontTools.misc import sstruct
from fontTools.misc.textTools import Tag, bytechr, byteord
from fontTools import fontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.recordingPen import RecordingPen
from io import BytesIO
import struct
import os
import random
import copy
from collections import OrderedDict
from functools import partial
import pytest

haveBrotli = False
try:
    try:
        import brotlicffi as brotli
    except ImportError:
        import brotli
    haveBrotli = True
except ImportError:
    pass


# Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires
# deprecation warnings if a program uses the old name.
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp


current_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
data_dir = os.path.join(current_dir, "data")
TTX = os.path.join(data_dir, "TestTTF-Regular.ttx")
OTX = os.path.join(data_dir, "TestOTF-Regular.otx")
METADATA = os.path.join(data_dir, "test_woff2_metadata.xml")

TT_WOFF2 = BytesIO()
CFF_WOFF2 = BytesIO()


def setUpModule():
    if not haveBrotli:
        raise unittest.SkipTest("No module named brotli")
    assert os.path.exists(TTX)
    assert os.path.exists(OTX)
    # import TT-flavoured test font and save it as WOFF2
    ttf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
    ttf.importXML(TTX)
    ttf.flavor = "woff2"
    ttf.save(TT_WOFF2, reorderTables=None)
    # import CFF-flavoured test font and save it as WOFF2
    otf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
    otf.importXML(OTX)
    otf.flavor = "woff2"
    otf.save(CFF_WOFF2, reorderTables=None)


class WOFF2ReaderTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.file = BytesIO(CFF_WOFF2.getvalue())
        cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        cls.font.importXML(OTX)

    def setUp(self):
        self.file.seek(0)

    def test_bad_signature(self):
        with self.assertRaisesRegex(ttLib.TTLibError, "bad signature"):
            WOFF2Reader(BytesIO(b"wOFF"))

    def test_not_enough_data_header(self):
        incomplete_header = self.file.read(woff2DirectorySize - 1)
        with self.assertRaisesRegex(ttLib.TTLibError, "not enough data"):
            WOFF2Reader(BytesIO(incomplete_header))

    def test_incorrect_compressed_size(self):
        data = self.file.read(woff2DirectorySize)
        header = sstruct.unpack(woff2DirectoryFormat, data)
        header["totalCompressedSize"] = 0
        data = sstruct.pack(woff2DirectoryFormat, header)
        with self.assertRaises((brotli.error, ttLib.TTLibError)):
            WOFF2Reader(BytesIO(data + self.file.read()))

    def test_incorrect_uncompressed_size(self):
        decompress_backup = brotli.decompress
        brotli.decompress = lambda data: b""  # return empty byte string
        with self.assertRaisesRegex(
            ttLib.TTLibError, "unexpected size for decompressed"
        ):
            WOFF2Reader(self.file)
        brotli.decompress = decompress_backup

    def test_incorrect_file_size(self):
        data = self.file.read(woff2DirectorySize)
        header = sstruct.unpack(woff2DirectoryFormat, data)
        header["length"] -= 1
        data = sstruct.pack(woff2DirectoryFormat, header)
        with self.assertRaisesRegex(
            ttLib.TTLibError, "doesn't match the actual file size"
        ):
            WOFF2Reader(BytesIO(data + self.file.read()))

    def test_num_tables(self):
        tags = [t for t in self.font.keys() if t not in ("GlyphOrder", "DSIG")]
        data = self.file.read(woff2DirectorySize)
        header = sstruct.unpack(woff2DirectoryFormat, data)
        self.assertEqual(header["numTables"], len(tags))

    def test_table_tags(self):
        tags = set([t for t in self.font.keys() if t not in ("GlyphOrder", "DSIG")])
        reader = WOFF2Reader(self.file)
        self.assertEqual(set(reader.keys()), tags)

    def test_get_normal_tables(self):
        woff2Reader = WOFF2Reader(self.file)
        specialTags = woff2TransformedTableTags + ("head", "GlyphOrder", "DSIG")
        for tag in [t for t in self.font.keys() if t not in specialTags]:
            origData = self.font.getTableData(tag)
            decompressedData = woff2Reader[tag]
            self.assertEqual(origData, decompressedData)

    def test_reconstruct_unknown(self):
        reader = WOFF2Reader(self.file)
        with self.assertRaisesRegex(ttLib.TTLibError, "transform for table .* unknown"):
            reader.reconstructTable("head")


class WOFF2ReaderTTFTest(WOFF2ReaderTest):
    """Tests specific to TT-flavored fonts."""

    @classmethod
    def setUpClass(cls):
        cls.file = BytesIO(TT_WOFF2.getvalue())
        cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        cls.font.importXML(TTX)

    def setUp(self):
        self.file.seek(0)

    def test_reconstruct_glyf(self):
        woff2Reader = WOFF2Reader(self.file)
        reconstructedData = woff2Reader["glyf"]
        self.assertEqual(self.font.getTableData("glyf"), reconstructedData)

    def test_reconstruct_loca(self):
        woff2Reader = WOFF2Reader(self.file)
        reconstructedData = woff2Reader["loca"]
        self.font.getTableData("glyf")  # 'glyf' needs to be compiled before 'loca'
        self.assertEqual(self.font.getTableData("loca"), reconstructedData)
        self.assertTrue(hasattr(woff2Reader.tables["glyf"], "data"))

    def test_reconstruct_loca_not_match_orig_size(self):
        reader = WOFF2Reader(self.file)
        reader.tables["loca"].origLength -= 1
        with self.assertRaisesRegex(
            ttLib.TTLibError, "'loca' table doesn't match original size"
        ):
            reader.reconstructTable("loca")


def normalise_table(font, tag, padding=4):
    """Return normalised table data. Keep 'font' instance unmodified."""
    assert tag in ("glyf", "loca", "head")
    assert tag in font
    if tag == "head":
        origHeadFlags = font["head"].flags
        font["head"].flags |= 1 << 11
        tableData = font["head"].compile(font)
    if font.sfntVersion in ("\x00\x01\x00\x00", "true"):
        assert {"glyf", "loca", "head"}.issubset(font.keys())
        origIndexFormat = font["head"].indexToLocFormat
        if hasattr(font["loca"], "locations"):
            origLocations = font["loca"].locations[:]
        else:
            origLocations = []
        glyfTable = ttLib.newTable("glyf")
        glyfTable.decompile(font.getTableData("glyf"), font)
        glyfTable.padding = padding
        if tag == "glyf":
            tableData = glyfTable.compile(font)
        elif tag == "loca":
            glyfTable.compile(font)
            tableData = font["loca"].compile(font)
        if tag == "head":
            glyfTable.compile(font)
            font["loca"].compile(font)
            tableData = font["head"].compile(font)
        font["head"].indexToLocFormat = origIndexFormat
        font["loca"].set(origLocations)
    if tag == "head":
        font["head"].flags = origHeadFlags
    return tableData


def normalise_font(font, padding=4):
    """Return normalised font data. Keep 'font' instance unmodified."""
    # drop DSIG but keep a copy
    DSIG_copy = copy.deepcopy(font["DSIG"])
    del font["DSIG"]
    # override TTFont attributes
    origFlavor = font.flavor
    origRecalcBBoxes = font.recalcBBoxes
    origRecalcTimestamp = font.recalcTimestamp
    origLazy = font.lazy
    font.flavor = None
    font.recalcBBoxes = False
    font.recalcTimestamp = False
    font.lazy = True
    # save font to temporary stream
    infile = BytesIO()
    font.save(infile)
    infile.seek(0)
    # reorder tables alphabetically
    outfile = BytesIO()
    reader = ttLib.sfnt.SFNTReader(infile)
    writer = ttLib.sfnt.SFNTWriter(
        outfile,
        len(reader.tables),
        reader.sfntVersion,
        reader.flavor,
        reader.flavorData,
    )
    for tag in sorted(reader.keys()):
        if tag in woff2TransformedTableTags + ("head",):
            writer[tag] = normalise_table(font, tag, padding)
        else:
            writer[tag] = reader[tag]
    writer.close()
    # restore font attributes
    font["DSIG"] = DSIG_copy
    font.flavor = origFlavor
    font.recalcBBoxes = origRecalcBBoxes
    font.recalcTimestamp = origRecalcTimestamp
    font.lazy = origLazy
    return outfile.getvalue()


class WOFF2DirectoryEntryTest(unittest.TestCase):
    def setUp(self):
        self.entry = WOFF2DirectoryEntry()

    def test_not_enough_data_table_flags(self):
        with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'flags'"):
            self.entry.fromString(b"")

    def test_not_enough_data_table_tag(self):
        incompleteData = bytearray([0x3F, 0, 0, 0])
        with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"):
            self.entry.fromString(bytes(incompleteData))

    def test_loca_zero_transformLength(self):
        data = bytechr(getKnownTagIndex("loca"))  # flags
        data += packBase128(random.randint(1, 100))  # origLength
        data += packBase128(1)  # non-zero transformLength
        with self.assertRaisesRegex(
            ttLib.TTLibError, "transformLength of the 'loca' table must be 0"
        ):
            self.entry.fromString(data)

    def test_fromFile(self):
        unknownTag = Tag("ZZZZ")
        data = bytechr(getKnownTagIndex(unknownTag))
        data += unknownTag.tobytes()
        data += packBase128(random.randint(1, 100))
        expectedPos = len(data)
        f = BytesIO(data + b"\0" * 100)
        self.entry.fromFile(f)
        self.assertEqual(f.tell(), expectedPos)

    def test_transformed_toString(self):
        self.entry.tag = Tag("glyf")
        self.entry.flags = getKnownTagIndex(self.entry.tag)
        self.entry.origLength = random.randint(101, 200)
        self.entry.length = random.randint(1, 100)
        expectedSize = (
            woff2FlagsSize
            + base128Size(self.entry.origLength)
            + base128Size(self.entry.length)
        )
        data = self.entry.toString()
        self.assertEqual(len(data), expectedSize)

    def test_known_toString(self):
        self.entry.tag = Tag("head")
        self.entry.flags = getKnownTagIndex(self.entry.tag)
        self.entry.origLength = 54
        expectedSize = woff2FlagsSize + base128Size(self.entry.origLength)
        data = self.entry.toString()
        self.assertEqual(len(data), expectedSize)

    def test_unknown_toString(self):
        self.entry.tag = Tag("ZZZZ")
        self.entry.flags = woff2UnknownTagIndex
        self.entry.origLength = random.randint(1, 100)
        expectedSize = (
            woff2FlagsSize + woff2UnknownTagSize + base128Size(self.entry.origLength)
        )
        data = self.entry.toString()
        self.assertEqual(len(data), expectedSize)

    def test_glyf_loca_transform_flags(self):
        for tag in ("glyf", "loca"):
            entry = WOFF2DirectoryEntry()
            entry.tag = Tag(tag)
            entry.flags = getKnownTagIndex(entry.tag)

            self.assertEqual(entry.transformVersion, 0)
            self.assertTrue(entry.transformed)

            entry.transformed = False

            self.assertEqual(entry.transformVersion, 3)
            self.assertEqual(entry.flags & 0b11000000, (3 << 6))
            self.assertFalse(entry.transformed)

    def test_other_transform_flags(self):
        entry = WOFF2DirectoryEntry()
        entry.tag = Tag("ZZZZ")
        entry.flags = woff2UnknownTagIndex

        self.assertEqual(entry.transformVersion, 0)
        self.assertFalse(entry.transformed)

        entry.transformed = True

        self.assertEqual(entry.transformVersion, 1)
        self.assertEqual(entry.flags & 0b11000000, (1 << 6))
        self.assertTrue(entry.transformed)


class DummyReader(WOFF2Reader):
    def __init__(self, file, checkChecksums=1, fontNumber=-1):
        self.file = file
        for attr in (
            "majorVersion",
            "minorVersion",
            "metaOffset",
            "metaLength",
            "metaOrigLength",
            "privLength",
            "privOffset",
        ):
            setattr(self, attr, 0)
        self.tables = {}


class WOFF2FlavorDataTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        assert os.path.exists(METADATA)
        with open(METADATA, "rb") as f:
            cls.xml_metadata = f.read()
        cls.compressed_metadata = brotli.compress(
            cls.xml_metadata, mode=brotli.MODE_TEXT
        )
        # make random byte strings; font data must be 4-byte aligned
        cls.fontdata = bytes(bytearray(random.sample(range(0, 256), 80)))
        cls.privData = bytes(bytearray(random.sample(range(0, 256), 20)))

    def setUp(self):
        self.file = BytesIO(self.fontdata)
        self.file.seek(0, 2)

    def test_get_metaData_no_privData(self):
        self.file.write(self.compressed_metadata)
        reader = DummyReader(self.file)
        reader.metaOffset = len(self.fontdata)
        reader.metaLength = len(self.compressed_metadata)
        reader.metaOrigLength = len(self.xml_metadata)
        flavorData = WOFF2FlavorData(reader)
        self.assertEqual(self.xml_metadata, flavorData.metaData)

    def test_get_privData_no_metaData(self):
        self.file.write(self.privData)
        reader = DummyReader(self.file)
        reader.privOffset = len(self.fontdata)
        reader.privLength = len(self.privData)
        flavorData = WOFF2FlavorData(reader)
        self.assertEqual(self.privData, flavorData.privData)

    def test_get_metaData_and_privData(self):
        self.file.write(self.compressed_metadata + self.privData)
        reader = DummyReader(self.file)
        reader.metaOffset = len(self.fontdata)
        reader.metaLength = len(self.compressed_metadata)
        reader.metaOrigLength = len(self.xml_metadata)
        reader.privOffset = reader.metaOffset + reader.metaLength
        reader.privLength = len(self.privData)
        flavorData = WOFF2FlavorData(reader)
        self.assertEqual(self.xml_metadata, flavorData.metaData)
        self.assertEqual(self.privData, flavorData.privData)

    def test_get_major_minorVersion(self):
        reader = DummyReader(self.file)
        reader.majorVersion = reader.minorVersion = 1
        flavorData = WOFF2FlavorData(reader)
        self.assertEqual(flavorData.majorVersion, 1)
        self.assertEqual(flavorData.minorVersion, 1)

    def test_mutually_exclusive_args(self):
        msg = "arguments are mutually exclusive"
        reader = DummyReader(self.file)
        with self.assertRaisesRegex(TypeError, msg):
            WOFF2FlavorData(reader, transformedTables={"hmtx"})
        with self.assertRaisesRegex(TypeError, msg):
            WOFF2FlavorData(reader, data=WOFF2FlavorData())

    def test_transformedTables_default(self):
        flavorData = WOFF2FlavorData()
        self.assertEqual(flavorData.transformedTables, set(woff2TransformedTableTags))

    def test_transformedTables_invalid(self):
        msg = r"'glyf' and 'loca' must be transformed \(or not\) together"

        with self.assertRaisesRegex(ValueError, msg):
            WOFF2FlavorData(transformedTables={"glyf"})

        with self.assertRaisesRegex(ValueError, msg):
            WOFF2FlavorData(transformedTables={"loca"})


class WOFF2WriterTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.font = ttLib.TTFont(
            recalcBBoxes=False, recalcTimestamp=False, flavor="woff2"
        )
        cls.font.importXML(OTX)
        cls.tags = sorted(t for t in cls.font.keys() if t != "GlyphOrder")
        cls.numTables = len(cls.tags)
        cls.file = BytesIO(CFF_WOFF2.getvalue())
        cls.file.seek(0, 2)
        cls.length = (cls.file.tell() + 3) & ~3
        cls.setUpFlavorData()

    @classmethod
    def setUpFlavorData(cls):
        assert os.path.exists(METADATA)
        with open(METADATA, "rb") as f:
            cls.xml_metadata = f.read()
        cls.compressed_metadata = brotli.compress(
            cls.xml_metadata, mode=brotli.MODE_TEXT
        )
        cls.privData = bytes(bytearray(random.sample(range(0, 256), 20)))

    def setUp(self):
        self.file.seek(0)
        self.writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)

    def test_DSIG_dropped(self):
        self.writer["DSIG"] = b"\0"
        self.assertEqual(len(self.writer.tables), 0)
        self.assertEqual(self.writer.numTables, self.numTables - 1)

    def test_no_rewrite_table(self):
        self.writer["ZZZZ"] = b"\0"
        with self.assertRaisesRegex(ttLib.TTLibError, "cannot rewrite"):
            self.writer["ZZZZ"] = b"\0"

    def test_num_tables(self):
        self.writer["ABCD"] = b"\0"
        with self.assertRaisesRegex(ttLib.TTLibError, "wrong number of tables"):
            self.writer.close()

    def test_required_tables(self):
        font = ttLib.TTFont(flavor="woff2")
        with self.assertRaisesRegex(ttLib.TTLibError, "missing required table"):
            font.save(BytesIO())

    def test_head_transform_flag(self):
        headData = self.font.getTableData("head")
        origFlags = byteord(headData[16])
        woff2font = ttLib.TTFont(self.file)
        newHeadData = woff2font.getTableData("head")
        modifiedFlags = byteord(newHeadData[16])
        self.assertNotEqual(origFlags, modifiedFlags)
        restoredFlags = modifiedFlags & ~0x08  # turn off bit 11
        self.assertEqual(origFlags, restoredFlags)

    def test_tables_sorted_alphabetically(self):
        expected = sorted([t for t in self.tags if t != "DSIG"])
        woff2font = ttLib.TTFont(self.file)
        self.assertEqual(expected, list(woff2font.reader.keys()))

    def test_checksums(self):
        normFile = BytesIO(normalise_font(self.font, padding=4))
        normFile.seek(0)
        normFont = ttLib.TTFont(normFile, checkChecksums=2)
        w2font = ttLib.TTFont(self.file)
        # force reconstructing glyf table using 4-byte padding
        w2font.reader.padding = 4
        for tag in [t for t in self.tags if t != "DSIG"]:
            w2data = w2font.reader[tag]
            normData = normFont.reader[tag]
            if tag == "head":
                w2data = w2data[:8] + b"\0\0\0\0" + w2data[12:]
                normData = normData[:8] + b"\0\0\0\0" + normData[12:]
            w2CheckSum = ttLib.sfnt.calcChecksum(w2data)
            normCheckSum = ttLib.sfnt.calcChecksum(normData)
            self.assertEqual(w2CheckSum, normCheckSum)
        normCheckSumAdjustment = normFont["head"].checkSumAdjustment
        self.assertEqual(normCheckSumAdjustment, w2font["head"].checkSumAdjustment)

    def test_calcSFNTChecksumsLengthsAndOffsets(self):
        normFont = ttLib.TTFont(BytesIO(normalise_font(self.font, padding=4)))
        for tag in self.tags:
            self.writer[tag] = self.font.getTableData(tag)
        self.writer._normaliseGlyfAndLoca(padding=4)
        self.writer._setHeadTransformFlag()
        self.writer.tables = OrderedDict(sorted(self.writer.tables.items()))
        self.writer._calcSFNTChecksumsLengthsAndOffsets()
        for tag, entry in normFont.reader.tables.items():
            self.assertEqual(entry.offset, self.writer.tables[tag].origOffset)
            self.assertEqual(entry.length, self.writer.tables[tag].origLength)
            self.assertEqual(entry.checkSum, self.writer.tables[tag].checkSum)

    def test_bad_sfntVersion(self):
        for i in range(self.numTables):
            self.writer[bytechr(65 + i) * 4] = b"\0"
        self.writer.sfntVersion = "ZZZZ"
        with self.assertRaisesRegex(ttLib.TTLibError, "bad sfntVersion"):
            self.writer.close()

    def test_calcTotalSize_no_flavorData(self):
        expected = self.length
        self.writer.file = BytesIO()
        for tag in self.tags:
            self.writer[tag] = self.font.getTableData(tag)
        self.writer.close()
        self.assertEqual(expected, self.writer.length)
        self.assertEqual(expected, self.writer.file.tell())

    def test_calcTotalSize_with_metaData(self):
        expected = self.length + len(self.compressed_metadata)
        flavorData = self.writer.flavorData = WOFF2FlavorData()
        flavorData.metaData = self.xml_metadata
        self.writer.file = BytesIO()
        for tag in self.tags:
            self.writer[tag] = self.font.getTableData(tag)
        self.writer.close()
        self.assertEqual(expected, self.writer.length)
        self.assertEqual(expected, self.writer.file.tell())

    def test_calcTotalSize_with_privData(self):
        expected = self.length + len(self.privData)
        flavorData = self.writer.flavorData = WOFF2FlavorData()
        flavorData.privData = self.privData
        self.writer.file = BytesIO()
        for tag in self.tags:
            self.writer[tag] = self.font.getTableData(tag)
        self.writer.close()
        self.assertEqual(expected, self.writer.length)
        self.assertEqual(expected, self.writer.file.tell())

    def test_calcTotalSize_with_metaData_and_privData(self):
        metaDataLength = (len(self.compressed_metadata) + 3) & ~3
        expected = self.length + metaDataLength + len(self.privData)
        flavorData = self.writer.flavorData = WOFF2FlavorData()
        flavorData.metaData = self.xml_metadata
        flavorData.privData = self.privData
        self.writer.file = BytesIO()
        for tag in self.tags:
            self.writer[tag] = self.font.getTableData(tag)
        self.writer.close()
        self.assertEqual(expected, self.writer.length)
        self.assertEqual(expected, self.writer.file.tell())

    def test_getVersion(self):
        # no version
        self.assertEqual((0, 0), self.writer._getVersion())
        # version from head.fontRevision
        fontRevision = self.font["head"].fontRevision
        versionTuple = tuple(int(i) for i in str(fontRevision).split("."))
        entry = self.writer.tables["head"] = ttLib.newTable("head")
        entry.data = self.font.getTableData("head")
        self.assertEqual(versionTuple, self.writer._getVersion())
        # version from writer.flavorData
        flavorData = self.writer.flavorData = WOFF2FlavorData()
        flavorData.majorVersion, flavorData.minorVersion = (10, 11)
        self.assertEqual((10, 11), self.writer._getVersion())

    def test_hmtx_trasform(self):
        tableTransforms = {"glyf", "loca", "hmtx"}

        writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
        writer.flavorData = WOFF2FlavorData(transformedTables=tableTransforms)

        for tag in self.tags:
            writer[tag] = self.font.getTableData(tag)
        writer.close()

        # enabling hmtx transform has no effect when font has no glyf table
        self.assertEqual(writer.file.getvalue(), CFF_WOFF2.getvalue())

    def test_no_transforms(self):
        writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
        writer.flavorData = WOFF2FlavorData(transformedTables=())

        for tag in self.tags:
            writer[tag] = self.font.getTableData(tag)
        writer.close()

        # transforms settings have no effect when font is CFF-flavored, since
        # all the current transforms only apply to TrueType-flavored fonts.
        self.assertEqual(writer.file.getvalue(), CFF_WOFF2.getvalue())


class WOFF2WriterTTFTest(WOFF2WriterTest):
    @classmethod
    def setUpClass(cls):
        cls.font = ttLib.TTFont(
            recalcBBoxes=False, recalcTimestamp=False, flavor="woff2"
        )
        cls.font.importXML(TTX)
        cls.tags = sorted(t for t in cls.font.keys() if t != "GlyphOrder")
        cls.numTables = len(cls.tags)
        cls.file = BytesIO(TT_WOFF2.getvalue())
        cls.file.seek(0, 2)
        cls.length = (cls.file.tell() + 3) & ~3
        cls.setUpFlavorData()

    def test_normaliseGlyfAndLoca(self):
        normTables = {}
        for tag in ("head", "loca", "glyf"):
            normTables[tag] = normalise_table(self.font, tag, padding=4)
        for tag in self.tags:
            tableData = self.font.getTableData(tag)
            self.writer[tag] = tableData
            if tag in normTables:
                self.assertNotEqual(tableData, normTables[tag])
        self.writer._normaliseGlyfAndLoca(padding=4)
        self.writer._setHeadTransformFlag()
        for tag in normTables:
            self.assertEqual(self.writer.tables[tag].data, normTables[tag])

    def test_hmtx_trasform(self):
        def compile_hmtx(compressed):
            tableTransforms = woff2TransformedTableTags
            if compressed:
                tableTransforms += ("hmtx",)
            writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
            writer.flavorData = WOFF2FlavorData(transformedTables=tableTransforms)
            for tag in self.tags:
                writer[tag] = self.font.getTableData(tag)
            writer.close()
            return writer.tables["hmtx"].length

        uncompressed_length = compile_hmtx(compressed=False)
        compressed_length = compile_hmtx(compressed=True)

        # enabling optional hmtx transform shaves off a few bytes
        self.assertLess(compressed_length, uncompressed_length)

    def test_no_transforms(self):
        writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
        writer.flavorData = WOFF2FlavorData(transformedTables=())

        for tag in self.tags:
            writer[tag] = self.font.getTableData(tag)
        writer.close()

        self.assertNotEqual(writer.file.getvalue(), TT_WOFF2.getvalue())

        writer.file.seek(0)
        reader = WOFF2Reader(writer.file)
        self.assertEqual(len(reader.flavorData.transformedTables), 0)


class WOFF2LocaTableTest(unittest.TestCase):
    def setUp(self):
        self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        font["head"] = ttLib.newTable("head")
        font["loca"] = WOFF2LocaTable()
        font["glyf"] = WOFF2GlyfTable()

    def test_compile_short_loca(self):
        locaTable = self.font["loca"]
        locaTable.set(list(range(0, 0x20000, 2)))
        self.font["glyf"].indexFormat = 0
        locaData = locaTable.compile(self.font)
        self.assertEqual(len(locaData), 0x20000)

    def test_compile_short_loca_overflow(self):
        locaTable = self.font["loca"]
        locaTable.set(list(range(0x20000 + 1)))
        self.font["glyf"].indexFormat = 0
        with self.assertRaisesRegex(
            ttLib.TTLibError, "indexFormat is 0 but local offsets > 0x20000"
        ):
            locaTable.compile(self.font)

    def test_compile_short_loca_not_multiples_of_2(self):
        locaTable = self.font["loca"]
        locaTable.set([1, 3, 5, 7])
        self.font["glyf"].indexFormat = 0
        with self.assertRaisesRegex(ttLib.TTLibError, "offsets not multiples of 2"):
            locaTable.compile(self.font)

    def test_compile_long_loca(self):
        locaTable = self.font["loca"]
        locaTable.set(list(range(0x20001)))
        self.font["glyf"].indexFormat = 1
        locaData = locaTable.compile(self.font)
        self.assertEqual(len(locaData), 0x20001 * 4)

    def test_compile_set_indexToLocFormat_0(self):
        locaTable = self.font["loca"]
        # offsets are all multiples of 2 and max length is < 0x10000
        locaTable.set(list(range(0, 0x20000, 2)))
        locaTable.compile(self.font)
        newIndexFormat = self.font["head"].indexToLocFormat
        self.assertEqual(0, newIndexFormat)

    def test_compile_set_indexToLocFormat_1(self):
        locaTable = self.font["loca"]
        # offsets are not multiples of 2
        locaTable.set(list(range(10)))
        locaTable.compile(self.font)
        newIndexFormat = self.font["head"].indexToLocFormat
        self.assertEqual(1, newIndexFormat)
        # max length is >= 0x10000
        locaTable.set(list(range(0, 0x20000 + 1, 2)))
        locaTable.compile(self.font)
        newIndexFormat = self.font["head"].indexToLocFormat
        self.assertEqual(1, newIndexFormat)


class WOFF2GlyfTableTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        font.importXML(TTX)
        cls.tables = {}
        cls.transformedTags = ("maxp", "head", "loca", "glyf")
        for tag in reversed(cls.transformedTags):  # compile in inverse order
            cls.tables[tag] = font.getTableData(tag)
        infile = BytesIO(TT_WOFF2.getvalue())
        reader = WOFF2Reader(infile)
        cls.transformedGlyfData = reader.tables["glyf"].loadData(reader.transformBuffer)
        cls.glyphOrder = [".notdef"] + [
            "glyph%.5d" % i for i in range(1, font["maxp"].numGlyphs)
        ]

    def setUp(self):
        self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        font.setGlyphOrder(self.glyphOrder)
        font["head"] = ttLib.newTable("head")
        font["maxp"] = ttLib.newTable("maxp")
        font["loca"] = WOFF2LocaTable()
        font["glyf"] = WOFF2GlyfTable()
        for tag in self.transformedTags:
            font[tag].decompile(self.tables[tag], font)

    def test_reconstruct_glyf_padded_4(self):
        glyfTable = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        glyfTable.padding = 4
        data = glyfTable.compile(self.font)
        normGlyfData = normalise_table(self.font, "glyf", glyfTable.padding)
        self.assertEqual(normGlyfData, data)

    def test_reconstruct_glyf_padded_2(self):
        glyfTable = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        glyfTable.padding = 2
        data = glyfTable.compile(self.font)
        normGlyfData = normalise_table(self.font, "glyf", glyfTable.padding)
        self.assertEqual(normGlyfData, data)

    def test_reconstruct_glyf_unpadded(self):
        glyfTable = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        data = glyfTable.compile(self.font)
        self.assertEqual(self.tables["glyf"], data)

    def test_reconstruct_glyf_incorrect_glyphOrder(self):
        glyfTable = WOFF2GlyfTable()
        badGlyphOrder = self.font.getGlyphOrder()[:-1]
        self.font.setGlyphOrder(badGlyphOrder)
        with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
            glyfTable.reconstruct(self.transformedGlyfData, self.font)

    def test_reconstruct_glyf_missing_glyphOrder(self):
        glyfTable = WOFF2GlyfTable()
        del self.font.glyphOrder
        numGlyphs = self.font["maxp"].numGlyphs
        del self.font["maxp"]
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        expected = [".notdef"]
        expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)])
        self.assertEqual(expected, glyfTable.glyphOrder)

    def test_reconstruct_loca_padded_4(self):
        locaTable = self.font["loca"] = WOFF2LocaTable()
        glyfTable = self.font["glyf"] = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        glyfTable.padding = 4
        glyfTable.compile(self.font)
        data = locaTable.compile(self.font)
        normLocaData = normalise_table(self.font, "loca", glyfTable.padding)
        self.assertEqual(normLocaData, data)

    def test_reconstruct_loca_padded_2(self):
        locaTable = self.font["loca"] = WOFF2LocaTable()
        glyfTable = self.font["glyf"] = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        glyfTable.padding = 2
        glyfTable.compile(self.font)
        data = locaTable.compile(self.font)
        normLocaData = normalise_table(self.font, "loca", glyfTable.padding)
        self.assertEqual(normLocaData, data)

    def test_reconstruct_loca_unpadded(self):
        locaTable = self.font["loca"] = WOFF2LocaTable()
        glyfTable = self.font["glyf"] = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        glyfTable.compile(self.font)
        data = locaTable.compile(self.font)
        self.assertEqual(self.tables["loca"], data)

    def test_reconstruct_glyf_header_not_enough_data(self):
        with self.assertRaisesRegex(ttLib.TTLibError, "not enough 'glyf' data"):
            WOFF2GlyfTable().reconstruct(b"", self.font)

    def test_reconstruct_glyf_table_incorrect_size(self):
        msg = "incorrect size of transformed 'glyf'"
        with self.assertRaisesRegex(ttLib.TTLibError, msg):
            WOFF2GlyfTable().reconstruct(self.transformedGlyfData + b"\x00", self.font)
        with self.assertRaisesRegex(ttLib.TTLibError, msg):
            WOFF2GlyfTable().reconstruct(self.transformedGlyfData[:-1], self.font)

    def test_transform_glyf(self):
        glyfTable = self.font["glyf"]
        data = glyfTable.transform(self.font)
        self.assertEqual(self.transformedGlyfData, data)

    def test_roundtrip_glyf_reconstruct_and_transform(self):
        glyfTable = WOFF2GlyfTable()
        glyfTable.reconstruct(self.transformedGlyfData, self.font)
        data = glyfTable.transform(self.font)
        self.assertEqual(self.transformedGlyfData, data)

    def test_roundtrip_glyf_transform_and_reconstruct(self):
        glyfTable = self.font["glyf"]
        transformedData = glyfTable.transform(self.font)
        newGlyfTable = WOFF2GlyfTable()
        newGlyfTable.reconstruct(transformedData, self.font)
        newGlyfTable.padding = 4
        reconstructedData = newGlyfTable.compile(self.font)
        normGlyfData = normalise_table(self.font, "glyf", newGlyfTable.padding)
        self.assertEqual(normGlyfData, reconstructedData)


@pytest.fixture(scope="module")
def fontfile():
    class Glyph(object):
        def __init__(self, empty=False, **kwargs):
            if not empty:
                self.draw = partial(self.drawRect, **kwargs)
            else:
                self.draw = lambda pen: None

        @staticmethod
        def drawRect(pen, xMin, xMax):
            pen.moveTo((xMin, 0))
            pen.lineTo((xMin, 1000))
            pen.lineTo((xMax, 1000))
            pen.lineTo((xMax, 0))
            pen.closePath()

    class CompositeGlyph(object):
        def __init__(self, components):
            self.components = components

        def draw(self, pen):
            for baseGlyph, (offsetX, offsetY) in self.components:
                pen.addComponent(baseGlyph, (1, 0, 0, 1, offsetX, offsetY))

    fb = fontBuilder.FontBuilder(unitsPerEm=1000, isTTF=True)
    fb.setupGlyphOrder(
        [".notdef", "space", "A", "acutecomb", "Aacute", "zero", "one", "two"]
    )
    fb.setupCharacterMap(
        {
            0x20: "space",
            0x41: "A",
            0x0301: "acutecomb",
            0xC1: "Aacute",
            0x30: "zero",
            0x31: "one",
            0x32: "two",
        }
    )
    fb.setupHorizontalMetrics(
        {
            ".notdef": (500, 50),
            "space": (600, 0),
            "A": (550, 40),
            "acutecomb": (0, -40),
            "Aacute": (550, 40),
            "zero": (500, 30),
            "one": (500, 50),
            "two": (500, 40),
        }
    )
    fb.setupHorizontalHeader(ascent=1000, descent=-200)

    srcGlyphs = {
        ".notdef": Glyph(xMin=50, xMax=450),
        "space": Glyph(empty=True),
        "A": Glyph(xMin=40, xMax=510),
        "acutecomb": Glyph(xMin=-40, xMax=60),
        "Aacute": CompositeGlyph([("A", (0, 0)), ("acutecomb", (200, 0))]),
        "zero": Glyph(xMin=30, xMax=470),
        "one": Glyph(xMin=50, xMax=450),
        "two": Glyph(xMin=40, xMax=460),
    }
    pen = TTGlyphPen(srcGlyphs)
    glyphSet = {}
    for glyphName, glyph in srcGlyphs.items():
        glyph.draw(pen)
        glyphSet[glyphName] = pen.glyph()
    fb.setupGlyf(glyphSet)

    fb.setupNameTable(
        {
            "familyName": "TestWOFF2",
            "styleName": "Regular",
            "uniqueFontIdentifier": "TestWOFF2 Regular; Version 1.000; ABCD",
            "fullName": "TestWOFF2 Regular",
            "version": "Version 1.000",
            "psName": "TestWOFF2-Regular",
        }
    )
    fb.setupOS2()
    fb.setupPost()

    buf = BytesIO()
    fb.save(buf)
    buf.seek(0)

    assert fb.font["maxp"].numGlyphs == 8
    assert fb.font["hhea"].numberOfHMetrics == 6
    for glyphName in fb.font.getGlyphOrder():
        xMin = getattr(fb.font["glyf"][glyphName], "xMin", 0)
        assert xMin == fb.font["hmtx"][glyphName][1]

    return buf


@pytest.fixture
def ttFont(fontfile):
    return ttLib.TTFont(fontfile, recalcBBoxes=False, recalcTimestamp=False)


class WOFF2HmtxTableTest(object):
    def test_transform_no_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()
        hmtxTable.metrics = ttFont["hmtx"].metrics

        data = hmtxTable.transform(ttFont)

        assert data == (
            b"\x03"  # 00000011 | bits 0 and 1 are set (no sidebearings arrays)
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
        )

    def test_transform_proportional_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()
        metrics = ttFont["hmtx"].metrics
        # force one of the proportional glyphs to have its left sidebearing be
        # different from its xMin (40)
        metrics["A"] = (550, 39)
        hmtxTable.metrics = metrics

        assert ttFont["glyf"]["A"].xMin != metrics["A"][1]

        data = hmtxTable.transform(ttFont)

        assert data == (
            b"\x02"  # 00000010 | bits 0 unset: explicit proportional sidebearings
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
            # lsbArray
            b"\x002"  # .notdef: 50
            b"\x00\x00"  # space: 0
            b"\x00'"  # A: 39 (xMin: 40)
            b"\xff\xd8"  # acutecomb: -40
            b"\x00("  # Aacute: 40
            b"\x00\x1e"  # zero: 30
        )

    def test_transform_monospaced_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()
        metrics = ttFont["hmtx"].metrics
        hmtxTable.metrics = metrics

        # force one of the monospaced glyphs at the end of hmtx table to have
        # its xMin different from its left sidebearing (50)
        ttFont["glyf"]["one"].xMin = metrics["one"][1] + 1

        data = hmtxTable.transform(ttFont)

        assert data == (
            b"\x01"  # 00000001 | bits 1 unset: explicit monospaced sidebearings
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
            # leftSideBearingArray
            b"\x002"  # one: 50 (xMin: 51)
            b"\x00("  # two: 40
        )

    def test_transform_not_applicable(self, ttFont):
        hmtxTable = WOFF2HmtxTable()
        metrics = ttFont["hmtx"].metrics
        # force both a proportional and monospaced glyph to have sidebearings
        # different from the respective xMin coordinates
        metrics["A"] = (550, 39)
        metrics["one"] = (500, 51)
        hmtxTable.metrics = metrics

        # 'None' signals to fall back using untransformed hmtx table data
        assert hmtxTable.transform(ttFont) is None

    def test_reconstruct_no_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()

        data = (
            b"\x03"  # 00000011 | bits 0 and 1 are set (no sidebearings arrays)
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
        )

        hmtxTable.reconstruct(data, ttFont)

        assert hmtxTable.metrics == {
            ".notdef": (500, 50),
            "space": (600, 0),
            "A": (550, 40),
            "acutecomb": (0, -40),
            "Aacute": (550, 40),
            "zero": (500, 30),
            "one": (500, 50),
            "two": (500, 40),
        }

    def test_reconstruct_proportional_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()

        data = (
            b"\x02"  # 00000010 | bits 0 unset: explicit proportional sidebearings
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
            # lsbArray
            b"\x002"  # .notdef: 50
            b"\x00\x00"  # space: 0
            b"\x00'"  # A: 39 (xMin: 40)
            b"\xff\xd8"  # acutecomb: -40
            b"\x00("  # Aacute: 40
            b"\x00\x1e"  # zero: 30
        )

        hmtxTable.reconstruct(data, ttFont)

        assert hmtxTable.metrics == {
            ".notdef": (500, 50),
            "space": (600, 0),
            "A": (550, 39),
            "acutecomb": (0, -40),
            "Aacute": (550, 40),
            "zero": (500, 30),
            "one": (500, 50),
            "two": (500, 40),
        }

        assert ttFont["glyf"]["A"].xMin == 40

    def test_reconstruct_monospaced_sidebearings(self, ttFont):
        hmtxTable = WOFF2HmtxTable()

        data = (
            b"\x01"  # 00000001 | bits 1 unset: explicit monospaced sidebearings
            # advanceWidthArray
            b"\x01\xf4"  # .notdef: 500
            b"\x02X"  # space: 600
            b"\x02&"  # A: 550
            b"\x00\x00"  # acutecomb: 0
            b"\x02&"  # Aacute: 550
            b"\x01\xf4"  # zero: 500
            # leftSideBearingArray
            b"\x003"  # one: 51 (xMin: 50)
            b"\x00("  # two: 40
        )

        hmtxTable.reconstruct(data, ttFont)

        assert hmtxTable.metrics == {
            ".notdef": (500, 50),
            "space": (600, 0),
            "A": (550, 40),
            "acutecomb": (0, -40),
            "Aacute": (550, 40),
            "zero": (500, 30),
            "one": (500, 51),
            "two": (500, 40),
        }

        assert ttFont["glyf"]["one"].xMin == 50

    def test_reconstruct_flags_reserved_bits(self):
        hmtxTable = WOFF2HmtxTable()

        with pytest.raises(
            ttLib.TTLibError, match="Bits 2-7 of 'hmtx' flags are reserved"
        ):
            hmtxTable.reconstruct(b"\xFF", ttFont=None)

    def test_reconstruct_flags_required_bits(self):
        hmtxTable = WOFF2HmtxTable()

        with pytest.raises(ttLib.TTLibError, match="either bits 0 or 1 .* must set"):
            hmtxTable.reconstruct(b"\x00", ttFont=None)

    def test_reconstruct_too_much_data(self, ttFont):
        ttFont["hhea"].numberOfHMetrics = 2
        data = b"\x03\x01\xf4\x02X\x02&"
        hmtxTable = WOFF2HmtxTable()

        with pytest.raises(ttLib.TTLibError, match="too much 'hmtx' table data"):
            hmtxTable.reconstruct(data, ttFont)


class WOFF2RoundtripTest(object):
    @staticmethod
    def roundtrip(infile):
        infile.seek(0)
        ttFont = ttLib.TTFont(infile, recalcBBoxes=False, recalcTimestamp=False)
        outfile = BytesIO()
        ttFont.save(outfile)
        return outfile, ttFont

    def test_roundtrip_default_transforms(self, ttFont):
        ttFont.flavor = "woff2"
        # ttFont.flavorData = None
        tmp = BytesIO()
        ttFont.save(tmp)

        tmp2, ttFont2 = self.roundtrip(tmp)

        assert tmp.getvalue() == tmp2.getvalue()
        assert ttFont2.reader.flavorData.transformedTables == {"glyf", "loca"}

    def test_roundtrip_no_transforms(self, ttFont):
        ttFont.flavor = "woff2"
        ttFont.flavorData = WOFF2FlavorData(transformedTables=[])
        tmp = BytesIO()
        ttFont.save(tmp)

        tmp2, ttFont2 = self.roundtrip(tmp)

        assert tmp.getvalue() == tmp2.getvalue()
        assert not ttFont2.reader.flavorData.transformedTables

    def test_roundtrip_all_transforms(self, ttFont):
        ttFont.flavor = "woff2"
        ttFont.flavorData = WOFF2FlavorData(transformedTables=["glyf", "loca", "hmtx"])
        tmp = BytesIO()
        ttFont.save(tmp)

        tmp2, ttFont2 = self.roundtrip(tmp)

        assert tmp.getvalue() == tmp2.getvalue()
        assert ttFont2.reader.flavorData.transformedTables == {"glyf", "loca", "hmtx"}

    def test_roundtrip_only_hmtx_no_glyf_transform(self, ttFont):
        ttFont.flavor = "woff2"
        ttFont.flavorData = WOFF2FlavorData(transformedTables=["hmtx"])
        tmp = BytesIO()
        ttFont.save(tmp)

        tmp2, ttFont2 = self.roundtrip(tmp)

        assert tmp.getvalue() == tmp2.getvalue()
        assert ttFont2.reader.flavorData.transformedTables == {"hmtx"}

    def test_roundtrip_no_glyf_and_loca_tables(self):
        ttx = os.path.join(
            os.path.dirname(current_dir), "subset", "data", "google_color.ttx"
        )
        ttFont = ttLib.TTFont()
        ttFont.importXML(ttx)

        assert "glyf" not in ttFont
        assert "loca" not in ttFont

        ttFont.flavor = "woff2"
        tmp = BytesIO()
        ttFont.save(tmp)

        tmp2, ttFont2 = self.roundtrip(tmp)
        assert tmp.getvalue() == tmp2.getvalue()
        assert ttFont.flavor == "woff2"

    def test_roundtrip_off_curve_despite_overlap_bit(self):
        ttx = os.path.join(data_dir, "woff2_overlap_offcurve_in.ttx")
        ttFont = ttLib.TTFont()
        ttFont.importXML(ttx)

        assert ttFont["glyf"]["A"].flags[0] == _g_l_y_f.flagOverlapSimple

        ttFont.flavor = "woff2"
        tmp = BytesIO()
        ttFont.save(tmp)

        _, ttFont2 = self.roundtrip(tmp)
        assert ttFont2.flavor == "woff2"
        # check that the off-curve point is still there
        assert ttFont2["glyf"]["A"].flags[0] & _g_l_y_f.flagOnCurve == 0
        # check that the overlap bit is still there
        assert ttFont2["glyf"]["A"].flags[0] & _g_l_y_f.flagOverlapSimple != 0


class MainTest(object):
    @staticmethod
    def make_ttf(tmpdir):
        ttFont = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        ttFont.importXML(TTX)
        filename = str(tmpdir / "TestTTF-Regular.ttf")
        ttFont.save(filename)
        return filename

    def test_compress_ttf(self, tmpdir):
        input_file = self.make_ttf(tmpdir)

        assert woff2.main(["compress", input_file]) is None

        assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)

    def test_compress_ttf_no_glyf_transform(self, tmpdir):
        input_file = self.make_ttf(tmpdir)

        assert woff2.main(["compress", "--no-glyf-transform", input_file]) is None

        assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)

    def test_compress_ttf_hmtx_transform(self, tmpdir):
        input_file = self.make_ttf(tmpdir)

        assert woff2.main(["compress", "--hmtx-transform", input_file]) is None

        assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)

    def test_compress_ttf_no_glyf_transform_hmtx_transform(self, tmpdir):
        input_file = self.make_ttf(tmpdir)

        assert (
            woff2.main(
                ["compress", "--no-glyf-transform", "--hmtx-transform", input_file]
            )
            is None
        )

        assert (tmpdir / "TestTTF-Regular.woff2").check(file=True)

    def test_compress_output_file(self, tmpdir):
        input_file = self.make_ttf(tmpdir)
        output_file = tmpdir / "TestTTF.woff2"

        assert woff2.main(["compress", "-o", str(output_file), str(input_file)]) is None

        assert output_file.check(file=True)

    def test_compress_otf(self, tmpdir):
        ttFont = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
        ttFont.importXML(OTX)
        input_file = str(tmpdir / "TestOTF-Regular.otf")
        ttFont.save(input_file)

        assert woff2.main(["compress", input_file]) is None

        assert (tmpdir / "TestOTF-Regular.woff2").check(file=True)

    def test_recompress_woff2_keeps_flavorData(self, tmpdir):
        woff2_font = ttLib.TTFont(BytesIO(TT_WOFF2.getvalue()))
        woff2_font.flavorData.privData = b"FOOBAR"
        woff2_file = tmpdir / "TestTTF-Regular.woff2"
        woff2_font.save(str(woff2_file))

        assert woff2_font.flavorData.transformedTables == {"glyf", "loca"}

        woff2.main(["compress", "--hmtx-transform", str(woff2_file)])

        output_file = tmpdir / "TestTTF-Regular#1.woff2"
        assert output_file.check(file=True)

        new_woff2_font = ttLib.TTFont(str(output_file))

        assert new_woff2_font.flavorData.transformedTables == {"glyf", "loca", "hmtx"}
        assert new_woff2_font.flavorData.privData == b"FOOBAR"

    def test_decompress_ttf(self, tmpdir):
        input_file = tmpdir / "TestTTF-Regular.woff2"
        input_file.write_binary(TT_WOFF2.getvalue())

        assert woff2.main(["decompress", str(input_file)]) is None

        assert (tmpdir / "TestTTF-Regular.ttf").check(file=True)

    def test_decompress_otf(self, tmpdir):
        input_file = tmpdir / "TestTTF-Regular.woff2"
        input_file.write_binary(CFF_WOFF2.getvalue())

        assert woff2.main(["decompress", str(input_file)]) is None

        assert (tmpdir / "TestTTF-Regular.otf").check(file=True)

    def test_decompress_output_file(self, tmpdir):
        input_file = tmpdir / "TestTTF-Regular.woff2"
        input_file.write_binary(TT_WOFF2.getvalue())
        output_file = tmpdir / "TestTTF.ttf"

        assert (
            woff2.main(["decompress", "-o", str(output_file), str(input_file)]) is None
        )

        assert output_file.check(file=True)

    def test_no_subcommand_show_help(self, capsys):
        with pytest.raises(SystemExit):
            woff2.main(["--help"])

        captured = capsys.readouterr()
        assert "usage: fonttools ttLib.woff2" in captured.out


class Base128Test(unittest.TestCase):
    def test_unpackBase128(self):
        self.assertEqual(unpackBase128(b"\x3f\x00\x00"), (63, b"\x00\x00"))
        self.assertEqual(unpackBase128(b"\x8f\xff\xff\xff\x7f")[0], 4294967295)

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "UIntBase128 value must not start with leading zeros",
            unpackBase128,
            b"\x80\x80\x3f",
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "UIntBase128-encoded sequence is longer than 5 bytes",
            unpackBase128,
            b"\x8f\xff\xff\xff\xff\x7f",
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            r"UIntBase128 value exceeds 2\*\*32-1",
            unpackBase128,
            b"\x90\x80\x80\x80\x00",
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "not enough data to unpack UIntBase128",
            unpackBase128,
            b"",
        )

    def test_base128Size(self):
        self.assertEqual(base128Size(0), 1)
        self.assertEqual(base128Size(24567), 3)
        self.assertEqual(base128Size(2**32 - 1), 5)

    def test_packBase128(self):
        self.assertEqual(packBase128(63), b"\x3f")
        self.assertEqual(packBase128(2**32 - 1), b"\x8f\xff\xff\xff\x7f")
        self.assertRaisesRegex(
            ttLib.TTLibError,
            r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
            packBase128,
            2**32 + 1,
        )
        self.assertRaisesRegex(
            ttLib.TTLibError,
            r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
            packBase128,
            -1,
        )


class UShort255Test(unittest.TestCase):
    def test_unpack255UShort(self):
        self.assertEqual(unpack255UShort(bytechr(252))[0], 252)
        # some numbers (e.g. 506) can have multiple encodings
        self.assertEqual(unpack255UShort(struct.pack(b"BB", 254, 0))[0], 506)
        self.assertEqual(unpack255UShort(struct.pack(b"BB", 255, 253))[0], 506)
        self.assertEqual(unpack255UShort(struct.pack(b"BBB", 253, 1, 250))[0], 506)

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "not enough data to unpack 255UInt16",
            unpack255UShort,
            struct.pack(b"BB", 253, 0),
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "not enough data to unpack 255UInt16",
            unpack255UShort,
            struct.pack(b"B", 254),
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "not enough data to unpack 255UInt16",
            unpack255UShort,
            struct.pack(b"B", 255),
        )

    def test_pack255UShort(self):
        self.assertEqual(pack255UShort(252), b"\xfc")
        self.assertEqual(pack255UShort(505), b"\xff\xfc")
        self.assertEqual(pack255UShort(506), b"\xfe\x00")
        self.assertEqual(pack255UShort(762), b"\xfd\x02\xfa")

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "255UInt16 format requires 0 <= integer <= 65535",
            pack255UShort,
            -1,
        )

        self.assertRaisesRegex(
            ttLib.TTLibError,
            "255UInt16 format requires 0 <= integer <= 65535",
            pack255UShort,
            0xFFFF + 1,
        )


class VarCompositeTest(unittest.TestCase):
    def test_var_composite(self):
        input_path = os.path.join(data_dir, "varc-ac00-ac01.ttf")
        ttf = ttLib.TTFont(input_path)
        ttf.flavor = "woff2"
        out = BytesIO()
        ttf.save(out)

        ttf = ttLib.TTFont(out)
        ttf.flavor = None
        out = BytesIO()
        ttf.save(out)


class CubicTest(unittest.TestCase):
    def test_cubic(self):
        input_path = os.path.join(
            data_dir, "..", "tables", "data", "NotoSans-VF-cubic.subset.ttf"
        )
        ttf = ttLib.TTFont(input_path)
        pen1 = RecordingPen()
        ttf.getGlyphSet()["a"].draw(pen1)
        ttf.flavor = "woff2"
        out = BytesIO()
        ttf.save(out)

        ttf = ttLib.TTFont(out)
        ttf.flavor = None
        pen2 = RecordingPen()
        ttf.getGlyphSet()["a"].draw(pen2)
        out = BytesIO()
        ttf.save(out)

        assert pen1.value == pen2.value


if __name__ == "__main__":
    import sys

    sys.exit(unittest.main())
