-
Notifications
You must be signed in to change notification settings - Fork 475
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
387 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { Parser } from '../parse.js'; | ||
import table from '../table.js'; | ||
import { isGzip, unGzip } from '../util.js'; | ||
|
||
// https://learn.microsoft.com/en-us/typography/opentype/spec/svg | ||
|
||
/** | ||
* @typedef {Object} SVGTable | ||
* @prop {SVGDocumentRecord[]} documentRecords | ||
* @prop {Uint8Array[]} documents | ||
* @prop {Map<number, SVGImage>} images | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} SVGDocumentRecord | ||
* @prop {number} startGlyphID | ||
* @prop {number} endGlyphID | ||
* @prop {number} svgDocIndex | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} SVGImage | ||
* @prop {number} leftSideBearing | ||
* @prop {number} baseline | ||
* @prop {HTMLImageElement} image | ||
*/ | ||
|
||
/** | ||
* @param {DataView} data | ||
* @param {number} offset | ||
* @returns {SVGTable} | ||
*/ | ||
function parseSvgTable(data, offset) { | ||
const buf = data.buffer; | ||
const p = new Parser(data, offset); | ||
const documents = []; | ||
const version = p.parseUShort(); | ||
if (version !== 0) return { documentRecords: [], documents }; | ||
|
||
p.relativeOffset = p.parseOffset32(); | ||
const svgDocumentListOffset = data.byteOffset + offset + p.relativeOffset; | ||
const numEntries = p.parseUShort(); | ||
const documentRecords = new Array(numEntries); | ||
const documentIndices = new Map(); | ||
for (let i = 0; i < numEntries; i++) { | ||
const startGlyphID = p.parseUShort(); | ||
const endGlyphID = p.parseUShort(); | ||
const svgDocOffset = svgDocumentListOffset + p.parseOffset32(); | ||
const svgDocLength = p.parseULong(); | ||
let svgDocIndex = documentIndices.get(svgDocOffset); | ||
if (svgDocIndex === undefined) { | ||
svgDocIndex = documents.length; | ||
documents.push(new Uint8Array(buf, svgDocOffset, svgDocLength)); | ||
documentIndices.set(svgDocOffset, svgDocIndex); | ||
} | ||
documentRecords[i] = { startGlyphID, endGlyphID, svgDocIndex }; | ||
} | ||
return { documentRecords, documents, images: new Map() }; | ||
} | ||
|
||
/** | ||
* @param {SVGTable} svgTable | ||
* @returns {opentype.Table} | ||
*/ | ||
function makeSvgTable({ documentRecords, documents }) { | ||
const numEntries = documentRecords.length; | ||
const numDocuments = documents.length; | ||
const documentsFieldOffset = 3 + 1 + numEntries * 4; | ||
const fields = new Array(documentsFieldOffset + numDocuments); | ||
const documentLocations = new Array(numDocuments); | ||
|
||
// SVG Documents | ||
let svgDocOffset = 2 + numEntries * (2 + 2 + 4 + 4); | ||
let fieldIndex = documentsFieldOffset; | ||
for (let i = 0; i < numDocuments; i++) { | ||
const name = 'svgDoc_' + i; | ||
const value = documents[i]; | ||
const svgDocLength = value.length; | ||
fields[fieldIndex++] = { name, type: 'LITERAL', value }; | ||
documentLocations[i] = { svgDocOffset, svgDocLength }; | ||
svgDocOffset += svgDocLength; | ||
} | ||
|
||
// SVG Table Header | ||
fields[0] = { name: 'version', type: 'USHORT', value: 0 }; | ||
fields[1] = { name: 'svgDocumentListOffset', type: 'ULONG', value: 2 + 4 + 4 }; | ||
fields[2] = { name: 'reserved', type: 'ULONG', value: 0 }; | ||
|
||
// SVG Document List | ||
fields[3] = { name: 'numEntries', type: 'USHORT', value: numEntries }; | ||
fieldIndex = 4; | ||
for (let i = 0; i < numEntries; i++) { | ||
const namePrefix = 'documentRecord_' + i; | ||
const { startGlyphID, endGlyphID, svgDocIndex } = documentRecords[i]; | ||
const { svgDocOffset, svgDocLength } = documentLocations[svgDocIndex]; | ||
fields[fieldIndex++] = { name: namePrefix + '_startGlyphID', type: 'USHORT', value: startGlyphID }; | ||
fields[fieldIndex++] = { name: namePrefix + '_endGlyphID', type: 'USHORT', value: endGlyphID }; | ||
fields[fieldIndex++] = { name: namePrefix + '_svgDocOffset', type: 'ULONG', value: svgDocOffset }; | ||
fields[fieldIndex++] = { name: namePrefix + '_svgDocLength', type: 'ULONG', value: svgDocLength }; | ||
} | ||
|
||
return new table.Table('SVG ', fields); | ||
} | ||
|
||
/** | ||
* @param {Uint8Array} buf | ||
* @returns {string} | ||
*/ | ||
function decodeSvgDocument(buf) { | ||
return new TextDecoder().decode(isGzip(buf) ? unGzip(buf) : buf); | ||
} | ||
|
||
/** | ||
* @param {SVGTable} svgTable | ||
* @param {Array<SVGDocumentRecord>} [documentRecords] | ||
* @returns {Promise<void>} | ||
*/ | ||
function prepareSvgImages(svgTable, documentRecords) { | ||
const records = documentRecords || svgTable.documentRecords; | ||
return Promise.all(records.map(r => prepareSvgRecordImages(svgTable, r))); | ||
} | ||
|
||
/** | ||
* @param {SVGTable} svgTable | ||
* @param {SVGDocumentRecord} record | ||
* @returns {Promise<void>} | ||
*/ | ||
function prepareSvgRecordImages({ documents, images }, record) { | ||
const promises = []; | ||
const docSvgText = decodeSvgDocument(documents[record.svgDocIndex]); | ||
if (record.startGlyphID === record.endGlyphID) { | ||
const glyphSvgImage = makeGlyphSvgImage(docSvgText); | ||
images.set(record.startGlyphID, glyphSvgImage); | ||
promises.push(glyphSvgImage.image.decode()); | ||
} else { | ||
const glyphSvgTemplate = makeGlyphSvgTemplate(docSvgText); | ||
for (let i = record.startGlyphID, l = record.endGlyphID; i <= l; i++) { | ||
const glyphSvgImage = makeGlyphSvgImage(glyphSvgTemplate(i)); | ||
images.set(i, glyphSvgImage); | ||
promises.push(glyphSvgImage.image.decode()); | ||
} | ||
} | ||
return Promise.all(promises); | ||
} | ||
|
||
/** | ||
* https://learn.microsoft.com/en-us/typography/opentype/spec/svg#glyph-identifiers | ||
* @param {string} text | ||
* @returns {(glyphId: number) => string} | ||
*/ | ||
function makeGlyphSvgTemplate(text) { | ||
const contentStart = text.indexOf('>', text.indexOf('<svg') + 4) + 1; | ||
const contentEnd = text.lastIndexOf('</svg>'); | ||
const parts = [ | ||
text.substring(0, contentStart), | ||
'<defs>', | ||
text.substring(contentStart, contentEnd), | ||
'</defs><use href="#glyph', '', '"/>', | ||
text.substring(contentEnd), | ||
]; | ||
return (glyphId) => { | ||
parts[4] = glyphId.toString(); | ||
return parts.join(''); | ||
}; | ||
} | ||
|
||
/** | ||
* @param {string} text | ||
* @returns {SVGImage} | ||
*/ | ||
function makeGlyphSvgImage(text) { | ||
const svgDocument = new DOMParser().parseFromString(text, 'image/svg+xml'); | ||
/** @type {SVGSVGElement} */ | ||
const svg = svgDocument.documentElement; | ||
const viewBoxVal = svg.viewBox.baseVal; | ||
const widthVal = svg.width.baseVal; | ||
const heightVal = svg.height.baseVal; | ||
let xScale = 1; | ||
let yScale = 1; | ||
if (viewBoxVal.width > 0 && viewBoxVal.height > 0) { | ||
if (widthVal.unitType === 1) { | ||
xScale = widthVal.valueInSpecifiedUnits / viewBoxVal.width; | ||
yScale = heightVal.unitType === 1 ? heightVal.valueInSpecifiedUnits / viewBoxVal.height : xScale; | ||
} else if (heightVal.unitType === 1) { | ||
yScale = heightVal.valueInSpecifiedUnits / viewBoxVal.height; | ||
xScale = yScale; | ||
} | ||
} | ||
|
||
const div = document.createElement('div'); | ||
div.style.position = 'fixed'; | ||
div.style.visibility = 'hidden'; | ||
div.appendChild(svg); | ||
document.body.appendChild(div); | ||
const bbox = svg.getBBox(); | ||
document.body.removeChild(div); | ||
|
||
const leftSideBearing = (bbox.x - viewBoxVal.x) * xScale; | ||
const baseline = (viewBoxVal.y - bbox.y) * yScale; | ||
const width = bbox.width * xScale; | ||
const height = bbox.height * yScale; | ||
|
||
svg.setAttribute('viewBox', [bbox.x, bbox.y, bbox.width, bbox.height].join(' ')); | ||
if (xScale !== 1) svg.setAttribute('width', width); | ||
if (yScale !== 1) svg.setAttribute('height', height); | ||
|
||
const image = new Image(width, height); | ||
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg.outerHTML); | ||
return { leftSideBearing, baseline, image }; | ||
} | ||
|
||
export default { | ||
make: makeSvgTable, | ||
parse: parseSvgTable, | ||
decodeDocument: decodeSvgDocument, | ||
prepareImages: prepareSvgImages, | ||
}; |
Oops, something went wrong.