From 6c55400867fc843256eb1436ae89b8d975b537d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Constantin=20Gro=C3=9F?= Date: Fri, 1 Dec 2023 00:01:19 +0100 Subject: [PATCH] implement reading, writing and drawing of CFF PaintType and StrokeWidth (#651) * implement reading, writing and drawing of CFF PaintType and StrokeWidth * clarify --- src/font.js | 6 ++++ src/glyph.js | 5 +++- src/tables/cff.js | 31 +++++++++++++++++++-- src/tables/sfnt.js | 3 +- test/fonts/CFF1SingleLinePaintTypeTEST.otf | Bin 0 -> 2028 bytes test/fonts/LICENSE | 5 ++++ test/tables/cff.js | 24 ++++++++++++++++ 7 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 test/fonts/CFF1SingleLinePaintTypeTEST.otf diff --git a/src/font.js b/src/font.js index 09d2dddc..b232ff19 100644 --- a/src/font.js +++ b/src/font.js @@ -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 { @@ -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); diff --git a/src/glyph.js b/src/glyph.js index f87e0400..ff38f6b3 100644 --- a/src/glyph.js +++ b/src/glyph.js @@ -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 @@ -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') { diff --git a/src/tables/cff.js b/src/tables/cff.js index 51a82986..3fe5b7b4 100755 --- a/src/tables/cff.js +++ b/src/tables/cff.js @@ -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 @@ -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(); } @@ -874,7 +891,7 @@ function parseCFFCharstring(font, glyph, code, version) { haveWidth = true; } - if (open) { + if (open && paintType !== 2) { p.closePath(); open = false; } @@ -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; @@ -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 = []; @@ -1566,3 +1590,4 @@ function makeCFFTable(glyphs, options,) { } export default { parse: parseCFFTable, make: makeCFFTable }; +export { applyPaintType }; diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js index 71ef148a..8abdb844 100644 --- a/src/tables/sfnt.js +++ b/src/tables/sfnt.js @@ -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; diff --git a/test/fonts/CFF1SingleLinePaintTypeTEST.otf b/test/fonts/CFF1SingleLinePaintTypeTEST.otf new file mode 100644 index 0000000000000000000000000000000000000000..64125eb6493f7e6ed6dd274962f875973978d22f GIT binary patch literal 2028 zcmbVNPiP!f9RA+S&L$1pG${m;Y~_I!vzY9^#+Zb7G0CRI&<))+(3DV@&2+PnomqD# zO`{;pWaQ}WmzII>RX|K+bBT2Bw=*-ueE?oARtiQg85Pm_sMtqy^(bbB3p(}w)K-V_)$eLRynXe;6{YUDDD}@VH=nI6R zn?u+tRLt+U!y5G}5c;laJ8tW9q&Y<$5<*ddci*45arV&EU(oeCwBVPw3qS44XzPyt znEo+!9gO5Z^wI9NuG4e|(bjAFvwoyt(ib|wIyfSvT@zPf(BmEL3+k8KQv8gR*o(L4 zsp{E(7S7GCa2=CK>9RGBCG9Sr=j1;e3F(ugVv&-oz*Vn{UDyfu>kktX`BskUkAV6 z&!t0z4zQGnIKVCx7Jkibtob#!v5u;CI^cvlI0mHlZD4Qr-sVS;)_xD{2eByZ0Qcgg z_$I*Z^=b3{^_jM-o3(Oj)s;iVm`o?r$A;y!Z8>hia?6&StJ#h-EJw#zOs8D3;zhgC zlRGglvsKfQ8QXH@e7R^^jw#QZHK%M_G8IqBp=@S;gdobg~DGLf*!ckK0A(Tv-* zQev&`wn}T}eAzM=3T4a9Z&uCxY%YJwEUm8xCeP~ zHa#|yN{y&)7K5^q$y7Xz&!a`<}!WHj2};Bn?5&kAio{o;7gUM1LxC5LJ1hJnl;)nm#J{>4{ s#~VL*mF7%ysk=GaJl?!A#hG4}{ZFL%4>u`pA*xiq>Jhy8HPZtA29EcXxc~qF literal 0 HcmV?d00001 diff --git a/test/fonts/LICENSE b/test/fonts/LICENSE index aaadbb14..1aae1d60 100644 --- a/test/fonts/LICENSE +++ b/test/fonts/LICENSE @@ -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) diff --git a/test/tables/cff.js b/test/tables/cff.js index ab706f70..b55f0f01 100644 --- a/test/tables/cff.js +++ b/test/tables/cff.js @@ -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 = ''; + 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 = ''; + assert.equal(path.toSVG(), svg2); + }); });