Skip to content

Commit

Permalink
implement reading, writing and drawing of CFF PaintType and StrokeWid…
Browse files Browse the repository at this point in the history
…th (#651)

* implement reading, writing and drawing of CFF PaintType and StrokeWidth

* clarify
  • Loading branch information
Connum authored Nov 30, 2023
1 parent fb886bb commit 6c55400
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 5 deletions.
6 changes: 6 additions & 0 deletions src/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Substitution from './substitution.js';
import { isBrowser, checkArgument } from './util.js';
import HintingTrueType from './hintingtt.js';
import Bidi from './bidi.js';
import { applyPaintType } from './tables/cff.js';

function createDefaultNamesInfo(options) {
return {
Expand Down Expand Up @@ -397,6 +398,11 @@ Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback)
*/
Font.prototype.getPath = function(text, x, y, fontSize, options) {
const fullPath = new Path();
applyPaintType(this, fullPath, fontSize);
if (fullPath.stroke) {
const scale = 1 / (fullPath.unitsPerEm || 1000) * fontSize;
fullPath.strokeWidth *= scale;
}
this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
fullPath.extend(glyphPath);
Expand Down
5 changes: 4 additions & 1 deletion src/glyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
if (!options) options = { };
let xScale = options.xScale;
let yScale = options.yScale;
const scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;

if (options.hinting && font && font.hinting) {
// in case of hinting, the hinting engine takes care
Expand All @@ -163,12 +164,14 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
xScale = yScale = 1;
} else {
commands = this.path.commands;
const scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;
if (xScale === undefined) xScale = scale;
if (yScale === undefined) yScale = scale;
}

const p = new Path();
p.fill = this.path.fill;
p.stroke = this.path.stroke;
p.strokeWidth = this.path.strokeWidth * scale;
for (let i = 0; i < commands.length; i += 1) {
const cmd = commands[i];
if (cmd.type === 'M') {
Expand Down
31 changes: 28 additions & 3 deletions src/tables/cff.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,22 @@ function parseBlend(operands) {
}
}

/**
* Applies path styles according to a CFF font's PaintType
* @param {Font} font
* @param {Path} path
* @returns {Number} paintType
*/
function applyPaintType(font, path) {
const paintType = font.tables.cff && font.tables.cff.topDict && font.tables.cff.topDict.paintType || 0;
if (paintType === 2) {
path.fill = null;
path.stroke = 'black';
path.strokeWidth = font.tables.cff.topDict.strokeWidth || 0;
}
return paintType;
}

// Take in charstring code and return a Glyph object.
// The encoding is described in the Type 2 Charstring Format
// https://www.microsoft.com/typography/OTSPEC/charstr2.htm
Expand Down Expand Up @@ -618,10 +634,11 @@ function parseCFFCharstring(font, glyph, code, version) {
subrsBias = cffTable.topDict._subrsBias;
}

const paintType = applyPaintType(font, p);
let width = defaultWidthX;

function newContour(x, y) {
if (open) {
if (open && paintType !== 2) {
p.closePath();
}

Expand Down Expand Up @@ -874,7 +891,7 @@ function parseCFFCharstring(font, glyph, code, version) {
haveWidth = true;
}

if (open) {
if (open && paintType !== 2) {
p.closePath();
open = false;
}
Expand Down Expand Up @@ -1488,7 +1505,7 @@ function makePrivateDict(attrs, strings, version) {
return t;
}

function makeCFFTable(glyphs, options,) {
function makeCFFTable(glyphs, options) {
// @TODO: make it configurable to use CFF or CFF2 for output
// right now, CFF2 fonts can be parsed, but will be saved as CFF
const cffVersion = 1;
Expand Down Expand Up @@ -1521,6 +1538,13 @@ function makeCFFTable(glyphs, options,) {
private: [0, 999]
};

const topDictOptions = options && options.topDict || {};

if(cffVersion < 2 && topDictOptions.paintType) {
attrs.paintType = topDictOptions.paintType;
attrs.strokeWidth = topDictOptions.strokeWidth || 0;
}

const privateAttrs = {};

const glyphNames = [];
Expand Down Expand Up @@ -1566,3 +1590,4 @@ function makeCFFTable(glyphs, options,) {
}

export default { parse: parseCFFTable, make: makeCFFTable };
export { applyPaintType };
3 changes: 2 additions & 1 deletion src/tables/sfnt.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ function fontToSfntTable(font) {
weightName: englishStyleName,
postScriptName: postScriptName,
unitsPerEm: font.unitsPerEm,
fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax]
fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax],
topDict: font.tables.cff && font.tables.cff.topDict || {}
});

const metaTable = (font.metas && Object.keys(font.metas).length > 0) ? meta.make(font.metas) : undefined;
Expand Down
Binary file added test/fonts/CFF1SingleLinePaintTypeTEST.otf
Binary file not shown.
5 changes: 5 additions & 0 deletions test/fonts/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ AbrilFatface-Regular.otf
SIL Open Font License, Version 1.1.
https://www.fontsquirrel.com/license/abril-fatface

CFF1SingleLinePaintTypeTEST.otf
Copyright (c) 2023, Constantin Groß, https://www.48design.com
SIL Open Font License, Version 1.1
https://opensource.org/licenses/OFL-1.1

Changa-Regular.ttf
Changa-VariableFont_wght.ttf
Copyright 2011 Eduardo Tunni (www.tipo.net.ar)
Expand Down
24 changes: 24 additions & 0 deletions test/tables/cff.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,28 @@ describe('tables/cff.js', function () {
assert.deepEqual(commands[13], { type: 'C', x: 36, y: 407, x1: 66, y1: 495, x2: 36, y2: 456 });
assert.deepEqual(commands[14], { type: 'Z' });
});

it('handles PaintType and StrokeWidth', function() {
const font = loadSync('./test/fonts/CFF1SingleLinePaintTypeTEST.otf', { lowMemory: true });
assert.equal(font.tables.cff.topDict.paintType, 2);
assert.equal(font.tables.cff.topDict.strokeWidth, 50);
let path;
const redraw = () => path = font.getPath('10', 0, 0, 12);
redraw();
assert.equal(path.commands.filter(c => c.type === 'Z').length, 0);
assert.equal(path.fill, null);
assert.equal(path.stroke, 'black');
assert.equal(path.strokeWidth, 0.6);
const svg1 = '<path d="M5.44-9.45C4.61-8.12 2.05-9.23 2.05-9.23M4.01-8.80C3.50-3.57 7.36 2.11 5.24-0.27C3.32-2.43 0.34-3.38 0.34-3.38M7.58-2.39L6.47-6.41L10.21-9.33L14.60-7.54L15.25-2.84L11.98-0.60L7.58-2.39" fill="none" stroke="black" stroke-width="0.6"/>';
assert.equal(path.toSVG(),svg1);
font.tables.cff.topDict.paintType = 0;
// redraw
redraw();
path = font.getPath('10', 0, 0, 12);
assert.equal(path.fill, 'black');
assert.equal(path.stroke, null);
assert.equal(path.strokeWidth, 1);
const svg2 = '<path d="M5.44-9.45C4.61-8.12 2.05-9.23 2.05-9.23M4.01-8.80C3.50-3.57 7.36 2.11 5.24-0.27C3.32-2.43 0.34-3.38 0.34-3.38M7.58-2.39L6.47-6.41L10.21-9.33L14.60-7.54L15.25-2.84L11.98-0.60L7.58-2.39"/>';
assert.equal(path.toSVG(), svg2);
});
});

0 comments on commit 6c55400

Please sign in to comment.