Skip to content

Commit

Permalink
feat: add SVG table support (#672)
Browse files Browse the repository at this point in the history
add SVG table parsing, writing and drawing
  • Loading branch information
kinolaev authored Apr 12, 2024
1 parent 976c723 commit d3d3078
Show file tree
Hide file tree
Showing 15 changed files with 560 additions and 25 deletions.
34 changes: 22 additions & 12 deletions docs/font-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,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 @@ -134,18 +145,7 @@ <h1>Free Software</h1>
var previewCanvas = document.getElementById('preview');
var previewCtx = previewCanvas.getContext("2d");
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
var options = {
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'] }
]
};
options = Object.assign({}, font.defaultRenderOptions, options);
var options = Object.assign({}, font.defaultRenderOptions, drawOptions);
font.draw(previewCtx, textToRender, 0, 32, fontSize, options);
}

Expand Down Expand Up @@ -261,9 +261,19 @@ <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 options = Object.assign({}, font.defaultRenderOptions, drawOptions);
const glyphIds = font.stringToGlyphIndexes(textToRender, options);
if (glyphIds.includes(glyphId)) {
renderText(font);
}
};
}

document.getElementById('update').addEventListener('click', () => displayFontData(window.font));
Expand Down
15 changes: 15 additions & 0 deletions docs/glyph-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ <h1>Free Software</h1>
}
return;
}
const image = path._image;
if ( image ) {
ctx.drawImage(image.image, image.x, image.y, image.width, image.height);
return;
}
var i, cmd, x1, y1, x2, y2;
var arrows = [];
ctx.beginPath();
Expand Down Expand Up @@ -490,6 +495,9 @@ <h1>Free Software</h1>
};

function onFontLoaded(font) {
if (window.font) {
window.font.onGlyphUpdated = null
}
window.font = font;
options = Object.assign({}, window.font.defaultRenderOptions);
window.fontOptions = options;
Expand Down Expand Up @@ -525,6 +533,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
43 changes: 34 additions & 9 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,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
}
};
options = Object.assign({}, font.defaultRenderOptions, options);
var options = Object.assign({}, font.defaultRenderOptions, getDrawOptions());
previewCtx.clearRect(0, 0, 940, 300);
const fontSize = +form.fontsize.value;
font.draw(previewCtx, textToRender, 0, 200, fontSize, options);
Expand All @@ -142,6 +134,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 @@ -184,6 +187,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 @@ -194,9 +200,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, {}, font);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
Expand All @@ -214,6 +222,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, {}, font);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
}

const textToRender = form.textField.value;
const options = Object.assign({}, font.defaultRenderOptions, getDrawOptions());
const glyphIds = font.stringToGlyphIndexes(textToRender, options);
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
8 changes: 6 additions & 2 deletions src/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Position from './position.js';
import Substitution from './substitution.js';
import { PaletteManager } from './palettes.js';
import { LayerManager } from './layers.js';
import { SVGImageManager } from './svgimages.js';
import { isBrowser, checkArgument } from './util.js';
import HintingTrueType from './hintingtt.js';
import Bidi from './bidi.js';
Expand Down Expand Up @@ -142,6 +143,7 @@ function Font(options) {
this.tables = this.tables || {};
this.palettes = new PaletteManager(this);
this.layers = new LayerManager(this);
this.svgImages = new SVGImageManager(this);

// needed for low memory mode only.
this._push = null;
Expand Down Expand Up @@ -332,7 +334,8 @@ Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {
* See https://www.microsoft.com/typography/otspec/featuretags.htm
* @property {boolean} [hinting=false] - whether to apply font hinting to the outlines
* @property {integer} [usePalette=0] For COLR/CPAL fonts, the zero-based index of the color palette to use. (Use `Font.palettes.get()` to get the available palettes)
* @property {integer} [drawLayers=true] For COLR/CPAL fonts, this can be turned to false in order to draw the fallback glyphs instead
* @property {boolean} [drawLayers=true] For COLR/CPAL fonts, this can be turned to false in order to draw the fallback glyphs instead
* @property {boolean} [drawSVG=true] For SVG fonts, this can be turned to false in order to draw the fallback glyphs instead
*/
Font.prototype.defaultRenderOptions = {
kerning: true,
Expand All @@ -348,6 +351,7 @@ Font.prototype.defaultRenderOptions = {
hinting: false,
usePalette: 0,
drawLayers: true,
drawSVG: true,
};

/**
Expand Down Expand Up @@ -417,7 +421,7 @@ Font.prototype.getPath = function(text, x, y, fontSize, options) {
}
this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
if ( options.drawLayers ) {
if ( options.drawSVG || options.drawLayers ) {
const layers = glyphPath._layers;
if ( layers && layers.length ) {
for(let l = 0; l < layers.length; l++) {
Expand Down
26 changes: 26 additions & 0 deletions src/glyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
}

const p = new Path();
if ( options.drawSVG ) {
const svgImage = this.getSvgImage(font);
if ( svgImage ) {
const layer = new Path();
layer._image = {
image: svgImage.image,
x: x + svgImage.leftSideBearing * scale,
y: y - svgImage.baseline * scale,
width: svgImage.image.width * scale,
height: svgImage.image.height * scale,
};
p._layers = [layer];
return p;
}
}
if ( options.drawLayers ) {
const layers = this.getLayers(font);
if ( layers && layers.length ) {
Expand Down Expand Up @@ -226,6 +241,17 @@ Glyph.prototype.getLayers = function(font) {
return font.layers.get(this.index);
};

/**
* @param {opentype.Font} font
* @returns {import('./svgimages.js').SVGImage | undefined}
*/
Glyph.prototype.getSvgImage = function(font) {
if(!font) {
throw Error('The font object is required to read the svg table in order to get the image.');
}
return font.svgImages.get(this.index);
};

/**
* Split the glyph into contours.
* This function is here for backwards compatibility, and to
Expand Down
5 changes: 5 additions & 0 deletions src/opentype.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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';
import { PaletteManager } from './palettes.js';
/**
* The opentype library.
Expand Down Expand Up @@ -396,6 +397,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
16 changes: 15 additions & 1 deletion src/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,13 @@ Path.prototype.draw = function(ctx) {
}
return;
}


const image = this._image;
if ( image ) {
ctx.drawImage(image.image, image.x, image.y, image.width, image.height);
return;
}

ctx.beginPath();
for (let i = 0; i < this.commands.length; i += 1) {
const cmd = this.commands[i];
Expand Down Expand Up @@ -640,6 +646,14 @@ Path.prototype.toSVG = function(options, pathData) {
*/
console.warn('toSVG() does not support colr font layers yet');
}
if (this._image) {
/**
* @TODO: implement SVG output for SVG glyphs
* We can't simply output the whole SVG document as it is, we should first sanitize it
* to make sure it doesn't contain any malicious scripts or features not supported in SVG fonts.
*/
console.warn('toSVG() does not support SVG glyphs yet');
}
if (!pathData) {
pathData = this.toPathData(options);
}
Expand Down
Loading

0 comments on commit d3d3078

Please sign in to comment.