Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support directories as params, format with black, and add type hints #17

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
.idea/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This is a small utility to convert WOFF files to the OTF font format. It uses Py
## Usage
To run the script, simply invoke it from the command line:
```
./woff2otf.py font.woff font.otf
./woff2otf.py font.woff|src-directory [font.otf|dst-directory]
```

The first parameter is the source file (the WOFF) font, and the second parameter is the output file (in OTF format).
The first parameter is the source file (the WOFF) font or a directory containing (other directories with) WOFF files, and the second parameter is the output file (in OTF format) or a directory which will contain the OTF files. When using directories, the subdirectories structure will be kept.
144 changes: 88 additions & 56 deletions woff2otf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,92 +19,124 @@
import struct
import sys
import zlib
from pathlib import Path
from typing import Union, BinaryIO


def convert_streams(infile, outfile):
WOFFHeader = {'signature': struct.unpack(">I", infile.read(4))[0],
'flavor': struct.unpack(">I", infile.read(4))[0],
'length': struct.unpack(">I", infile.read(4))[0],
'numTables': struct.unpack(">H", infile.read(2))[0],
'reserved': struct.unpack(">H", infile.read(2))[0],
'totalSfntSize': struct.unpack(">I", infile.read(4))[0],
'majorVersion': struct.unpack(">H", infile.read(2))[0],
'minorVersion': struct.unpack(">H", infile.read(2))[0],
'metaOffset': struct.unpack(">I", infile.read(4))[0],
'metaLength': struct.unpack(">I", infile.read(4))[0],
'metaOrigLength': struct.unpack(">I", infile.read(4))[0],
'privOffset': struct.unpack(">I", infile.read(4))[0],
'privLength': struct.unpack(">I", infile.read(4))[0]}

outfile.write(struct.pack(">I", WOFFHeader['flavor']));
outfile.write(struct.pack(">H", WOFFHeader['numTables']));
maximum = list(filter(lambda x: x[1] <= WOFFHeader['numTables'], [(n, 2**n) for n in range(64)]))[-1];
def convert_streams(infile: BinaryIO, outfile: BinaryIO):
WOFFHeader = {
"signature": struct.unpack(">I", infile.read(4))[0],
"flavor": struct.unpack(">I", infile.read(4))[0],
"length": struct.unpack(">I", infile.read(4))[0],
"numTables": struct.unpack(">H", infile.read(2))[0],
"reserved": struct.unpack(">H", infile.read(2))[0],
"totalSfntSize": struct.unpack(">I", infile.read(4))[0],
"majorVersion": struct.unpack(">H", infile.read(2))[0],
"minorVersion": struct.unpack(">H", infile.read(2))[0],
"metaOffset": struct.unpack(">I", infile.read(4))[0],
"metaLength": struct.unpack(">I", infile.read(4))[0],
"metaOrigLength": struct.unpack(">I", infile.read(4))[0],
"privOffset": struct.unpack(">I", infile.read(4))[0],
"privLength": struct.unpack(">I", infile.read(4))[0],
}

outfile.write(struct.pack(">I", WOFFHeader["flavor"]))
outfile.write(struct.pack(">H", WOFFHeader["numTables"]))
maximum = list(
filter(
lambda x: x[1] <= WOFFHeader["numTables"], [(n, 2**n) for n in range(64)]
)
)[-1]
searchRange = maximum[1] * 16
outfile.write(struct.pack(">H", searchRange));
outfile.write(struct.pack(">H", searchRange))
entrySelector = maximum[0]
outfile.write(struct.pack(">H", entrySelector));
rangeShift = WOFFHeader['numTables'] * 16 - searchRange;
outfile.write(struct.pack(">H", rangeShift));
outfile.write(struct.pack(">H", entrySelector))
rangeShift = WOFFHeader["numTables"] * 16 - searchRange
outfile.write(struct.pack(">H", rangeShift))

offset = outfile.tell()

TableDirectoryEntries = []
for i in range(0, WOFFHeader['numTables']):
TableDirectoryEntries.append({'tag': struct.unpack(">I", infile.read(4))[0],
'offset': struct.unpack(">I", infile.read(4))[0],
'compLength': struct.unpack(">I", infile.read(4))[0],
'origLength': struct.unpack(">I", infile.read(4))[0],
'origChecksum': struct.unpack(">I", infile.read(4))[0]})
offset += 4*4

for TableDirectoryEntry in TableDirectoryEntries:
outfile.write(struct.pack(">I", TableDirectoryEntry['tag']))
outfile.write(struct.pack(">I", TableDirectoryEntry['origChecksum']))
for i in range(0, WOFFHeader["numTables"]):
TableDirectoryEntries.append(
{
"tag": struct.unpack(">I", infile.read(4))[0],
"offset": struct.unpack(">I", infile.read(4))[0],
"compLength": struct.unpack(">I", infile.read(4))[0],
"origLength": struct.unpack(">I", infile.read(4))[0],
"origChecksum": struct.unpack(">I", infile.read(4))[0],
}
)
offset += 4 * 4

for TableDirectoryEntry in TableDirectoryEntries:
outfile.write(struct.pack(">I", TableDirectoryEntry["tag"]))
outfile.write(struct.pack(">I", TableDirectoryEntry["origChecksum"]))
outfile.write(struct.pack(">I", offset))
outfile.write(struct.pack(">I", TableDirectoryEntry['origLength']))
TableDirectoryEntry['outOffset'] = offset
offset += TableDirectoryEntry['origLength']
outfile.write(struct.pack(">I", TableDirectoryEntry["origLength"]))
TableDirectoryEntry["outOffset"] = offset
offset += TableDirectoryEntry["origLength"]
if (offset % 4) != 0:
offset += 4 - (offset % 4)

for TableDirectoryEntry in TableDirectoryEntries:
infile.seek(TableDirectoryEntry['offset'])
compressedData = infile.read(TableDirectoryEntry['compLength'])
if TableDirectoryEntry['compLength'] != TableDirectoryEntry['origLength']:
infile.seek(TableDirectoryEntry["offset"])
compressedData = infile.read(TableDirectoryEntry["compLength"])
if TableDirectoryEntry["compLength"] != TableDirectoryEntry["origLength"]:
uncompressedData = zlib.decompress(compressedData)
else:
uncompressedData = compressedData
outfile.seek(TableDirectoryEntry['outOffset'])
outfile.seek(TableDirectoryEntry["outOffset"])
outfile.write(uncompressedData)
offset = TableDirectoryEntry['outOffset'] + TableDirectoryEntry['origLength'];
offset = TableDirectoryEntry["outOffset"] + TableDirectoryEntry["origLength"]
padding = 0
if (offset % 4) != 0:
padding = 4 - (offset % 4)
outfile.write(bytearray(padding));
outfile.write(bytearray(padding))


def convert(infilename, outfilename):
with open(infilename , mode='rb') as infile:
with open(outfilename, mode='wb') as outfile:
def convert(in_filename: Union[str, Path], out_filename: Union[str, Path]):
with open(in_filename, mode="rb") as infile:
with open(out_filename, mode="wb") as outfile:
convert_streams(infile, outfile)


def main(argv):
def discover_and_convert(source_dir: Path, target_dir: Path):
for file in source_dir.glob("*.woff"):
convert(file, target_dir / file.name.replace(".woff", ".otf"))
for subdir in source_dir.glob("**/"): # first subdir is the dir itself
if subdir == source_dir:
continue
discover_and_convert(subdir, target_dir / subdir.name)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the recursion and not just use source.dir.glob("**/*.woff") in the first place?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your question makes a lot of sense.
The only reason I opted for recursion is because I could keep the same directory structure in the output directory with no effort.

If the source directory looks like:

input_dir
    subdir1
        subdir11
            subdir111
                font.woff

With your approach and with no effort, the output dir would look like:

output_dir
    font.otf

If you have a clean way of keeping the subdir structure, I'd happily apply it.



def main(argv: list[str]):
if len(argv) == 1 or len(argv) > 3:
print('I convert *.woff files to *.otf files. (one at a time :)\n'
'Usage: woff2otf.py web_font.woff [converted_filename.otf]\n'
'If the target file name is omitted, it will be guessed. Have fun!\n')
print(
"I convert *.woff files to *.otf files. (one at a time :)\n"
"Usage: woff2otf.py web_font.woff|directory [converted_filename.otf|directory]\n"
"If the target file name is omitted, it will be guessed. Have fun!\n"
)
return

source_file_name = argv[1]
if len(argv) == 3:
target_file_name = argv[2]
source = Path(argv[1]).resolve()
target = Path(argv[2]) if len(argv) == 3 else None

if source.is_dir():
if target is None:
target = source
assert target.is_dir()
else:
target_file_name = source_file_name.rsplit('.', 1)[0] + '.otf'
if target is None:
target = source.parent / source.name.replace(".woff", ".otf")
assert target.is_file()

convert(source_file_name, target_file_name)
if source.is_file():
convert(source, target)
else:
discover_and_convert(source, target)
return 0


if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main(sys.argv))