Skip to content

Commit

Permalink
feat: add SVG table support
Browse files Browse the repository at this point in the history
  • Loading branch information
kinolaev committed Mar 27, 2024
1 parent 6408975 commit a3e4ea0
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 21 deletions.
32 changes: 21 additions & 11 deletions docs/font-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ <h1>Free Software</h1>
var font = null;
const fontSize = 32;
const textToRender = 'Grumpy wizards make toxic brew for the evil Queen and Jack.';
const drawOptions = {
kerning: true,
features: [
/**
* these 4 features are required to render Arabic text properly
* and shouldn't be turned off when rendering arabic text.
*/
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
{ script: 'latn', tags: ['liga', 'rlig'] }
]
};

function escapeHtml(unsafe) {
return unsafe
Expand Down Expand Up @@ -127,17 +138,7 @@ <h1>Free Software</h1>
var previewCanvas = document.getElementById('preview');
var previewCtx = previewCanvas.getContext("2d");
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
font.draw(previewCtx, textToRender, 0, 32, fontSize, {
kerning: true,
features: [
/**
* these 4 features are required to render Arabic text properly
* and shouldn't be turned off when rendering arabic text.
*/
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
{ script: 'latn', tags: ['liga', 'rlig'] }
]
});
font.draw(previewCtx, textToRender, 0, 32, fontSize, drawOptions);
}

function showErrorMessage(message) {
Expand Down Expand Up @@ -234,9 +235,18 @@ <h1>Free Software</h1>
}

function onFontLoaded(font) {
if (window.font) {
window.font.onGlyphUpdated = null;
}
window.font = font;
renderText(font);
displayFontData(font);
font.onGlyphUpdated = (glyphId) => {
const glyphIds = font.stringToGlyphIndexes(textToRender, drawOptions);
if (glyphIds.includes(glyphId)) {
renderText(font);
}
};
}

var tableHeaders = document.getElementById('font-data').getElementsByTagName('h3');
Expand Down
10 changes: 10 additions & 0 deletions docs/glyph-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ <h1>Free Software</h1>
}

function onFontLoaded(font) {
if (window.font) {
window.font.onGlyphUpdated = null
}
window.font = font;

var w = cellWidth - cellMarginLeftRight * 2,
Expand Down Expand Up @@ -371,6 +374,13 @@ <h1>Free Software</h1>
displayGlyphPage(0);
displayGlyph(-1);
displayGlyphData(-1);

font.onGlyphUpdated = (glyphId) => {
const firstGlyph = pageSelected * cellCount;
if (firstGlyph <= glyphId && glyphId < firstGlyph + cellCount) {
renderGlyphItem(document.getElementById('g'+(glyphId - firstGlyph)), glyphId);
}
};
}

function cellSelect(event) {
Expand Down
42 changes: 34 additions & 8 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,7 @@ <h1>Free Software</h1>
if (!font) return;
const textToRender = form.textField.value;
var previewCtx = document.getElementById('preview').getContext('2d');
var options = {
kerning: form.kerning.checked,
hinting: form.hinting.checked,
features: {
liga: form.ligatures.checked,
rlig: form.ligatures.checked
}
};
var options = getDrawOptions();
previewCtx.clearRect(0, 0, 940, 300);
const fontSize = +form.fontsize.value;
font.draw(previewCtx, textToRender, 0, 200, fontSize, options);
Expand All @@ -134,6 +127,17 @@ <h1>Free Software</h1>
snapPath.draw(snapCtx);
}

function getDrawOptions() {
return {
kerning: form.kerning.checked,
hinting: form.hinting.checked,
features: {
liga: form.ligatures.checked,
rlig: form.ligatures.checked
}
};
}

function enableHighDPICanvas(canvas) {
if (typeof canvas === 'string') {
canvas = document.getElementById(canvas);
Expand Down Expand Up @@ -176,6 +180,9 @@ <h1>Free Software</h1>
}

function onFontLoaded(font) {
if (window.font) {
window.font.onGlyphUpdated = null
}
window.font = font;

// Show the first 100 glyphs.
Expand All @@ -186,9 +193,11 @@ <h1>Free Software</h1>
const x = 50;
const y = 120;
const fontSize = 72;
const ctxs = new Array(amount);
for (let i = 0; i < amount; i++) {
const glyph = font.glyphs.get(i);
const ctx = createGlyphCanvas(glyph, 150);
ctxs[i] = ctx;
glyph.draw(ctx, x, y, fontSize);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
Expand All @@ -206,6 +215,23 @@ <h1>Free Software</h1>
}

renderText();

font.onGlyphUpdated = (glyphId) => {
if (0 <= glyphId && glyphId < amount) {
const glyph = font.glyphs.get(glyphId);
const ctx = ctxs[glyphId];
glyph.draw(ctx, x, y, fontSize);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
}

const textToRender = form.textField.value;
const drawOptions = getDrawOptions();
const glyphIds = font.stringToGlyphIndexes(textToRender, drawOptions);
if (glyphIds.includes(glyphId)) {
renderText();
}
};
}
const loadScript = (src) => new Promise((onload) => document.head.append(Object.assign(document.createElement('script'), {src, onload})));
async function display(file, name) {
Expand Down
14 changes: 14 additions & 0 deletions src/glyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function getPathDefinition(glyph, path) {
* @property {number} [yMax]
* @property {number} [advanceWidth]
* @property {number} [leftSideBearing]
* @property {import('./tables/svg.js').SVGImage | Promise<import('./tables/svg.js').SVGImage> | Error} [svgImage]
*/

// A Glyph is an individual mark that often corresponds to a character.
Expand Down Expand Up @@ -147,6 +148,19 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
let yScale = options.yScale;
const scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;

const svgImage = this.svgImage;
if (svgImage && svgImage.image) {
const path = new Path();
path.drawImage(
svgImage.image,
x + svgImage.leftSideBearing * scale,
y - svgImage.baseline * scale,
svgImage.image.width * scale,
svgImage.image.height * scale,
);
return path;
}

if (options.hinting && font && font.hinting) {
// in case of hinting, the hinting engine takes care
// of scaling the points (not the path) before hinting.
Expand Down
38 changes: 38 additions & 0 deletions src/glyphset.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// The GlyphSet object

import Glyph from './glyph.js';
import svg from './tables/svg.js';

// Define a property on the glyph that depends on the path being loaded.
function defineDependentProperty(glyph, externalName, internalName) {
Expand Down Expand Up @@ -103,6 +104,39 @@ GlyphSet.prototype.push = function(index, loader) {
this.length++;
};

function defineSvgImageLoader(glyph, font) {
glyph._svgImage = function () {
return font.tables.svg
? svg.imageLoader(font.tables.svg, font.unitsPerEm, glyph.index)
: Promise.resolve();
};

Object.defineProperty(glyph, 'svgImage', {
get: function() {
if ( typeof glyph._svgImage === 'function' ) {
glyph._svgImage = glyph._svgImage()
.then(svgImage => {
glyph._svgImage = svgImage;
if (svgImage && font.onGlyphUpdated) {
font.onGlyphUpdated(glyph.index);
}
return svgImage;
})
.catch(error => {
glyph._svgImage = error;
throw error;
});
}
return glyph._svgImage;
},
set: function(newValue) {
glyph._svgImage = newValue;
},
enumerable: true,
configurable: true
});
}

/**
* @alias opentype.glyphLoader
* @param {opentype.Font} font
Expand Down Expand Up @@ -142,6 +176,8 @@ function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
defineDependentProperty(glyph, 'yMin', '_yMin');
defineDependentProperty(glyph, 'yMax', '_yMax');

defineSvgImageLoader(glyph, font);

return glyph;
};
}
Expand All @@ -163,6 +199,8 @@ function cffGlyphLoader(font, index, parseCFFCharstring, charstring, version) {
return path;
};

defineSvgImageLoader(glyph, font);

return glyph;
};
}
Expand Down
5 changes: 5 additions & 0 deletions src/opentype.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import os2 from './tables/os2.js';
import post from './tables/post.js';
import meta from './tables/meta.js';
import gasp from './tables/gasp.js';
import svg from './tables/svg.js';
/**
* The opentype library.
* @namespace opentype
Expand Down Expand Up @@ -390,6 +391,10 @@ function parseBuffer(buffer, opt={}) {
table = uncompressTable(data, tableEntry);
font.tables.gasp = gasp.parse(table.data, table.offset);
break;
case 'SVG ':
table = uncompressTable(data, tableEntry);
font.tables.svg = svg.parse(table.data, table.offset);
break;
}
}

Expand Down
25 changes: 25 additions & 0 deletions src/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,21 @@ Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y)
});
};

/**
* @function drawImage
* @memberof opentype.Path.prototype
*/
Path.prototype.drawImage = function(image, x, y, width, height) {
this.commands.push({
type: 'I',
image,
x,
y,
width,
height
});
};

/**
* Closes the path
* @function closePath
Expand Down Expand Up @@ -488,6 +503,14 @@ Path.prototype.getBoundingBox = function() {
prevX = startX;
prevY = startY;
break;
case 'I':
box.addPoint(cmd.x, cmd.y);
box.addPoint(cmd.x + cmd.width, cmd.y);
box.addPoint(cmd.x + cmd.width, cmd.y + cmd.height);
box.addPoint(cmd.x, cmd.y + cmd.height);
startX = prevX = cmd.x;
startY = prevY = cmd.y;
break;
default:
throw new Error('Unexpected path command ' + cmd.type);
}
Expand Down Expand Up @@ -516,6 +539,8 @@ Path.prototype.draw = function(ctx) {
ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
} else if (cmd.type === 'Z') {
ctx.closePath();
} else if (cmd.type === 'I') {
ctx.drawImage(cmd.image, cmd.x, cmd.y, cmd.width, cmd.height);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/tables/sfnt.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import fvar from './fvar.js';
import stat from './stat.js';
import avar from './avar.js';
import gasp from './gasp.js';
import svg from './svg.js';

function log2(v) {
return Math.log(v) / Math.log(2) | 0;
Expand Down Expand Up @@ -361,7 +362,8 @@ function fontToSfntTable(font) {
cpal,
colr,
stat,
avar
avar,
svg,
};

const optionalTableArgs = {
Expand Down
Loading

0 comments on commit a3e4ea0

Please sign in to comment.