Skip to content

Commit

Permalink
Change internal glyph representation for font reader
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Apr 2, 2019
1 parent ab610f3 commit 41548a9
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 181 deletions.
48 changes: 41 additions & 7 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const convert = require('./convert');
const parse_range = require('./range_parse');
const canvas = require('canvas');


Expand Down Expand Up @@ -86,6 +85,43 @@ class RawTextHelpFormatter2 extends argparse.RawDescriptionHelpFormatter {
}
}


// parse decimal or hex code in unicode range
function unicode_point(str) {
let m = /^(?:(?:0x([0-9a-f]+))|([0-9]+))$/i.exec(str.trim());

if (!m) throw new Error(str + ' is not a number');

let [ , hex, dec ] = m;

let value = hex ? parseInt(hex, 16) : parseInt(dec, 10);

if (value > 0x10FFFF) throw new Error(str + ' is out of unicode range');

return value;
}


// parse range
function range(str) {
let m = /^(.+?)(?:-(.+?))?(?:=>(.+?))?$/i.exec(str);

let [ , start, end, mapped_start ] = m;

if (!end) end = start;
if (!mapped_start) mapped_start = start;

start = unicode_point(start);
end = unicode_point(end);

if (start > end) throw new Error('Invalid range: ' + str);

mapped_start = unicode_point(mapped_start);

return [ start, end, mapped_start ];
}


// exclude negative numbers and non-numbers
function int(str) {
if (!/^\d+$/.test(str)) throw new Error(`${str} is not a valid number`);
Expand All @@ -98,12 +134,6 @@ function int(str) {
}


// wrap range parser into function to show error text correctly (it uses function name)
function range(str) {
return parse_range(str);
}


module.exports.run = function (argv, debug = false) {

//
Expand Down Expand Up @@ -228,3 +258,7 @@ List of characters to copy, belongs to previously declared "--font". Examples:
}

};


// export for tests
module.exports._range = range;
77 changes: 52 additions & 25 deletions lib/collect_font_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const fontkit = require('fontkit');
const AppError = require('./app_error');
const Ranger = require('./ranger');
const utils = require('./utils');


module.exports = function collect_font_data(args, createCanvas) {
Expand Down Expand Up @@ -68,7 +69,7 @@ module.exports = function collect_font_data(args, createCanvas) {
}

let mapping = ranger.get();
let output = [];
let glyphs = [];

for (let dst_code of Object.keys(mapping).sort((a, b) => a - b)) {
let src_code = mapping[dst_code].code;
Expand All @@ -77,46 +78,72 @@ module.exports = function collect_font_data(args, createCanvas) {

if (!font.hasGlyphForCodePoint(src_code)) continue;

let output_char = { code: Number(dst_code), pixels: [] };
let width_before = Math.ceil(-font.bbox.minX * args.size / font.unitsPerEm);
let width_after = Math.ceil(font.bbox.maxX * args.size / font.unitsPerEm);
let height_before = Math.ceil(-font.bbox.minY * args.size / font.unitsPerEm);
let height_after = Math.ceil(font.bbox.maxY * args.size / font.unitsPerEm);

let glyph = font.glyphForCodePoint(src_code);
let scale = font.unitsPerEm / (font.ascent - font.descent);
let canvas = createCanvas(width_before + width_after, height_before + height_after);

let ctx = canvas.getContext('2d');
let width = canvas.width;
let height = canvas.height;

ctx.save();

let height = args.size;
let width = Math.round(args.size * glyph.advanceWidth / (font.ascent - font.descent));
ctx.beginPath();

// skip combining characters with advanceWidth=0:
// U+300, U+301, U+303, U+309, U+30f, U+323, U+483, U+484, U+485, U+486, U+488, U+489
if (width <= 0) continue;
ctx.clearRect(0, 0, width, height);

let cv = createCanvas(width, height);
let ctx = cv.getContext('2d');
ctx.translate(width_before, height_before);
ctx.scale(args.size / font.unitsPerEm, args.size / font.unitsPerEm);

// flip character and move baseline, so character fits in its box
ctx.translate(0, args.size * font.ascent / (font.ascent - font.descent));
ctx.scale(1, -1);
let glyph = font.glyphForCodePoint(src_code);
glyph.path.toFunction()(ctx);
ctx.fill();

// count font height as ascender+descender, not as em size
// (we have fonts that are bigger than em box)
glyph.render(ctx, args.size * scale);
ctx.restore();

let image_data = ctx.getImageData(0, 0, width, height).data;
let pixels = [];

for (let y = 0; y < height; y++) {
// extract pixels array
for (let pos = 3, y = 0; y < height; y++) {
let line = [];

for (let x = 0; x < width; x++) {
let offset = (y * width + x) * 4;
for (let x = 0; x < width; x++, pos += 4) {
let alpha = image_data[pos];

// use alpha, ignore rgb (they're 0 0 0 for non-colored fonts)
line.push(image_data[offset + 3]);
line.push(alpha);
}

output_char.pixels.push(line);
pixels.push(line);
}

output.push(output_char);
let glyph_bbox = {
x: -width_before,
y: -height_before,
width,
height
};

glyphs.push({
code: Number(dst_code),
advanceWidth: Math.round(glyph.advanceWidth * args.size / font.unitsPerEm),
bbox: glyph_bbox,
pixels
});
}

return output;
let first_font = fonts[args.font[0].source_path];

glyphs = glyphs.map(utils.autocrop);

return {
ascent: Math.round(first_font.ascent * args.size / first_font.unitsPerEm),
descent: Math.round(first_font.descent * args.size / first_font.unitsPerEm),
size: args.size,
bboxMax: utils.get_max_bbox(glyphs),
glyphs
};
};
9 changes: 6 additions & 3 deletions lib/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
'use strict';

const collect_font_data = require('./collect_font_data');
const create_font = require('./create_font');

let writers = {
dump: require('./writers/dump')
};


//
Expand All @@ -18,9 +21,9 @@ const create_font = require('./create_font');
//
module.exports = function convert(args, createCanvas) {
let font_data = collect_font_data(args, createCanvas);
let files = create_font(args, font_data);
let files = writers[args.format](args, font_data);

return files;
};

module.exports.formats = create_font.formats;
module.exports.formats = Object.keys(writers);
15 changes: 0 additions & 15 deletions lib/create_font.js

This file was deleted.

38 changes: 0 additions & 38 deletions lib/range_parse.js

This file was deleted.

103 changes: 103 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

'use strict';


module.exports.autocrop = function autocrop(glyph) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;

// scan the entire image trying to find bbox
for (let y = 0; y < glyph.bbox.height; y++) {
for (let x = 0; x < glyph.bbox.width; x++) {
if (glyph.pixels[y][x]) {
minX = x > minX ? minX : x;
minY = y > minY ? minY : y;
maxX = x < maxX ? maxX : x;
maxY = y < maxY ? maxY : y;
}
}
}

// create new pixels array
let pixels = [];

for (let y = minY; y <= maxY; y++) {
let line = [];

for (let x = minX; x <= maxX; x++) {
line.push(glyph.pixels[y][x]);
}

pixels.push(line);
}

let bbox;

if (minX <= maxX && minY <= maxY) {
bbox = {
x: minX + glyph.bbox.x,
y: minY + glyph.bbox.y,
width: maxX - minX + 1,
height: maxY - minY + 1
};
} else {
// bounding box doesn't exist, e.g. whitespace
bbox = {
x: 0,
y: 0,
width: 0,
height: 0
};
}

return Object.assign({}, glyph, { pixels, bbox });
};


function set_byte_depth(depth) {
return function (byte) {
// calculate significant bits, e.g. for depth=2 it's 0, 1, 2 or 3
let value = ~~(byte / (256 >> depth));

// spread those bits around 0..255 range, e.g. for depth=2 it's 0, 85, 170 or 255
let scale = (2 << (depth - 1)) - 1;

return (value * 0xFFFF / scale) >> 8;
};
}


module.exports.set_depth = function set_depth(glyph, depth) {
let pixels = [];
let fn = set_byte_depth(depth);

for (let y = 0; y < glyph.bbox.height; y++) {
pixels.push(glyph.pixels[y].map(fn));
}

return Object.assign({}, glyph, { pixels });
};


module.exports.get_max_bbox = function get_max_bbox(glyphs) {
let bboxMax = { x: 0, y: 0, width: 0, height: 0 };

for (let glyph of glyphs) {
if (bboxMax.width === 0 && bboxMax.height === 0) {
bboxMax = glyph.bbox;
} else {
let minX = Math.min(glyph.bbox.x, bboxMax.x);
let minY = Math.min(glyph.bbox.y, bboxMax.y);
let maxX = Math.max(glyph.bbox.x + glyph.bbox.width, bboxMax.x + bboxMax.width) - 1;
let maxY = Math.max(glyph.bbox.y + glyph.bbox.height, bboxMax.y + bboxMax.height) - 1;

bboxMax = {
x: minX,
y: minY,
width: maxX - minX + 1,
height: maxY - minY + 1
};
}
}

return bboxMax;
};
Loading

0 comments on commit 41548a9

Please sign in to comment.