opentypejs / opentype.js

Read and write OpenType fonts using JavaScript.
https://opentype.js.org/
MIT License
4.48k stars 477 forks source link

Suport for WOFF File Format #43

Closed quasado closed 9 years ago

quasado commented 10 years ago

Are there any plans to support Web-Fonts (WOFF) i.e. to read from Google-Fonts and the such? As far as I understood, on could actually use zlib.js to decompress the woff format?

fdb commented 10 years ago

It would be cool to support WOFF. I haven't looked at the spec yet, but because of the compression it is more bandwith-friendly with minimal changes.

quasado commented 10 years ago

Yes, I guess uncompressing would be easy via the zlib.js. However, I couldn't really understand what the underlying format of WOFF (after decompression) would be, as far as I understood it could be any of TTF, OpenType etc?

Btw., here's an implementation of reading WOFF Headers, might help:

https://github.com/bramstein/opentype

raphaelyancey commented 9 years ago

I'd be happy to see WOFF support too!

timo22345 commented 9 years ago

I made an example of woff to otf conversion: http://jsbin.com/rijuvad.

It uses https://github.com/arty-name/woff2otf (which is based on python version), which uses https://github.com/nodeca/pako/blob/master/dist/pako_inflate.min.js.

The text above is rendered by opentype.js draw() function and the text below is normal fillText:

screen shot 2015-09-25 at 02 26 08

There is also newer woff2, but the support is currently so low that basically using woff (and SVG/EOT/TTF/OTF) are needed at least few years to the future: screen shot 2015-09-25 at 02 12 30

timo22345 commented 9 years ago

opentype.min.js is 79.9 KB.

woff2otf.min.js is 1.13KB.

pako_inflate.min.js. is 22.19KB.

Pako says it is "Almost as fast in modern JS engines as C implementation".

Could this be the method to allow loading WOFF into opentype.js?

If reverse is needed (is it?), then there is need for also Pako deflate which is 26.3 KB.

Other possibility is to make use of browser's native deflate/inflate, if "the lock" is some day removed. So far I have not found a native way, but it surely is there, because browser can decompress on the fly files that are deflated on server and browser can read/write compressed PNG files.

fdb commented 9 years ago

Cool! I'll look at Pako next week.

timo22345 commented 9 years ago

I was a bit worried about adding 22.19KB Pako Inflate to opentype.js, and fortunately I found another library, Imaya zlib.js, which is only 6.82KB and seems to be significantly faster than Pako (at least in this very limited test with one woff font). Devongovett send then a message (see below) and provided even smaller library Tiny-Inflate, which is only 3.74KB. And this provided to be even faster than Imaya.

TINY-INFLATE: Test: http://jsbin.com/qiwase/edit?html,js,output Source: https://raw.githubusercontent.com/devongovett/tiny-inflate/master/index.js Source size minified: 3.74KB Inflate execution time: 6 ms

IMAYA: Test: http://jsbin.com/zororo/edit?html,js,output Source: https://raw.githubusercontent.com/imaya/zlib.js/master/bin/inflate.min.js Source size minified: 6.82KB Inflate execution time: 15 ms

PAKO: Test: http://jsbin.com/wunipa/edit?html,js,output Source: https://github.com/nodeca/pako/blob/master/dist/pako_inflate.min.js Source size minified: 22.19KB Inflate execution time: 37 ms

(All times are measured in Chrome OSX Macbook Pro Mid 2010. Fastest of ten samples.)

If I understand the things right, woff can have multiple compressed/non-compressed tables, which means that every table needs to be decompressed individually. The following confirms that:

The main body of the file consists of the same collection of font data tables as the input sfnt font, stored in the same order, except that each table MAY be compressed, and the sfnt table directory is replaced by the WOFF table directory.

( http://www.w3.org/TR/2012/REC-WOFF-20121213/#OverallStructure )

And arty-name:s woff2otf does just this table "separation" and calls inflate every time when compressed table is found. I assume that usually webfont glyph amount and other tabledata are tried to keep as minimal as possible, so such inflate library that can decompress fast many small data blocks is the winner.

Of course it is wise to test with many different woff:s before making a decision which inflate library to choose.

devongovett commented 9 years ago

If you're worried about file size, https://github.com/devongovett/tiny-inflate is only 1.3KB min+gzipped. Probably not the fastest library, but it's definitely one of the smallest.

FWIW, not sure how you're measuring performance, but pako has been the fastest library in my tests, and according to their own benchmarks.

timo22345 commented 9 years ago

I got also Tiny-Inflate working. It gave Data error, but it was some 2-byte header which had to be stripped:

http://jsbin.com/qiwase/edit?html,js,output

This is how I measured the performance:

TINY-INFLATE:

window.time_cumul = 0;
function zlib_decompress(buffer, decompressedSize) {
  var time = performance.now();
        buffer = new Uint8Array(buffer);
        buffer = buffer.subarray(2,buffer.length); // strip header
        var plain = new Uint8Array(decompressedSize);
        window.inflate(buffer, plain);
  time = performance.now()-time;
  window.time_cumul += time;
  window.time_div.innerHTML = 'Decompress time (ms): ' + window.time_cumul;
  return plain;
}

IMAYA:

window.time_cumul = 0;
function zlib_decompress(buffer) {
  var time = performance.now();
        buffer = new Uint8Array(buffer);
        var inflate = new Zlib.Inflate(buffer);
        var plain = inflate.decompress();
  time = performance.now()-time;
  window.time_cumul += time;
  window.time_div.innerHTML = 'Decompress time (ms): ' + window.time_cumul;
  return plain;
}

PAKO:

window.time_cumul = 0;
function zlib_decompress(buffer) {
  var time = performance.now();
        var inflate = new pako.Inflate();
        inflate.push(new Uint8Array(buffer), true);
  time = performance.now()-time;
  window.time_cumul += time;
  window.time_div.innerHTML = 'Decompress time (ms): ' + window.time_cumul;
  return inflate.result;
}

If you find any flaws in performance measuring, please report.

EDIT: Maybe those new pako.Inflate() and new Zlib.Inflate(buffer) could be moved outside of the function zlib_decompress() so that they are executed at the page load, but not sure about this because this is first time I use them.

timo22345 commented 9 years ago

I made a SO question about the native DEFLATE/INFLATE support: http://stackoverflow.com/questions/32838795/native-deflate-inflate-in-browsers.

Hopefully this is coming to browsers some day. Until it we have to make use of libraries.

timo22345 commented 9 years ago

I got also Tiny-Inflate working by stripping a 2-byte header from compressed buffers before inflation, and in addition to that it is tiny, it is also fast: only 6 ms in my test.

Pako advertises itself to be the fastest, and devongovett says that tiny-inflate is slower than Pako, but this test shows opposite. Maybe this is due to that the buffer lengths are rather small:

Compressed  Uncompressed
1264        3076
42          48
84          96
342         530
314         444
9639        16496
47          54
30          36
435         580
272         292
438         926
329         499
243         412
(in bytes)

I updated the above messages of mine to include also Tiny-Inflate.

puzrin commented 9 years ago

@timo22345 try to do couple of dummy passes before measurement. JS JIT needs to collect stat to warm up and reoptimize underlying code.

timo22345 commented 9 years ago

@puzrin Thanks for the tip! I just tried that, but can't get it to go below 33ms in OSX Chrome.

timo22345 commented 9 years ago

@puzrin Yes, you were right. It was my measurement. I was pressing always the JSBIN run button.

When I added an own Execute button, the code runs in minimum of 1.5 ms. The first run is always about 35ms, but then the next is 2 - 20 ms and finally when warmed up the final higher speed is reached. Funny!

This is the code with Execute-button: http://jsbin.com/cevotu/1/edit?html,js,output

timo22345 commented 9 years ago

This "warming up" means that those other libs also run faster after warmup.

The performer in unwarmed test (Tiny-Inflate) runs also in 1.2 - 1.5 ms when warmed up (using Execute-button): http://jsbin.com/daliba/1/edit?html,js,output

I have to test these libs more with larger fonts and report here the results in various font sizes.

puzrin commented 9 years ago

You can do estimate without any additional measurements :) . Inflate speed is usually 30-100 mb/sec, depending on implementation. Font size is << 10mb. Unpacking will take 0.3 sec in worst case (that will never happen in real world).

fpirsch commented 9 years ago

@timo22345 Using your examples, tiny-inflate runs always faster here (Firefox: ≈6ms before warm-up, ≈1ms after). With its tiny size, it could be a winner.

fdb commented 9 years ago

That looks awesome! I'll look at tiny-inflate on Monday.

fdb commented 9 years ago

Okay, I tried with both Pako and tiny-inflate. For tiny-inflate, I had to strip the two-byte header as @timo22345 suggested (Thanks Timo!). Both seem to be working fine, but since tiny-inflate is smaller, I prefert it over Pako.

The code is in the woff-support branch, if anyone wants to check it out.

I'll test some more with different fonts, then release later this week.

puzrin commented 9 years ago

@fdb, FYI there is raw deflate stream, and wrapped deflate data. Wrapped data has 2-bytes header and 4-bytes Adler checksum tail. I guess, tiny deflate supports raw streams only, but that's enougth for your needs.