Flesh out the integration test more

This commit is contained in:
Patrick Walton 2017-11-15 14:36:59 -08:00
parent 8c1b3e9cb5
commit 86df78f939
25 changed files with 894 additions and 256 deletions

View File

@ -36,7 +36,11 @@ body {
font-family: "Inter UI", sans-serif;
}
body.pf-unscrollable {
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
nav {
-moz-user-select: none;
@ -71,6 +75,9 @@ nav {
.pf-toolbar-button {
pointer-events: auto;
}
#pf-test-pane {
pointer-events: auto;
}
.pf-toolbar-button.btn-outline-secondary:not(:hover) {
background: rgba(255, 255, 255, 0.75);
}
@ -120,6 +127,18 @@ button > svg path {
#pf-svg, #pf-svg * {
visibility: hidden !important;
}
#pf-outer-container {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#pf-inner-container {
overflow: scroll;
}
.pf-spinner {
position: absolute;
top: 0;

View File

@ -6,33 +6,119 @@
{{>partials/header.html}}
<script type="text/javascript" src="/js/pathfinder/integration-test.js"></script>
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}}
<div class="container-fluid">
<div class="row py-3">
<div id="pf-test-pane" class="col">
<form>
<div class="form-group">
<label for="pf-select-file">Font</label>
<select id="pf-select-file" class="form-control custom-select">
<option value="open-sans">Open Sans</option>
</select>
</div>
<div class="form-group">
<label for="pf-font-size">Font Size</label>
<div class="input-group">
<input id="pf-font-size" type="number" class="form-control"
value="32">
<div class="input-group-addon">px</div>
<body>
<div id="pf-outer-container" class="w-100">
<div class="w-100">
{{>partials/navbar.html isTool=true}}
</div>
<div id="pf-inner-container" class="container-fluid">
<div class="row">
<div id="pf-integration-test-sidebar" class="col">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link" id="pf-test-suite-tab" data-toggle="tab"
href="#pf-test-suite" role="tab" aria-controls="pf-test-suite"
aria-selected="false">Test Suite</a>
</li>
<li class="nav-item">
<a class="nav-link active" id="pf-custom-test-tab"
data-toggle="tab"
href="#pf-custom-test" role="tab" aria-controls="pf-custom-test"
aria-selected="true">Custom Test</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane p-3" id="pf-test-suite" role="tabpanel"
aria-labelledby="pf-test-suite-tab">
<button id="pf-run-tests-button" class="btn btn-primary m-2">
Run Tests
</button>
<table id="pf-results-table" class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Font</th>
<th scope="col">Char.</th>
<th scope="col">Size</th>
<th scope="col">AA</th>
<th scope="col">Subpixel</th>
<th scope="col">Reference</th>
<th scope="col">Expected SSIM</th>
<th scope="col">Actual SSIM</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="tab-pane show active p-3" id="pf-custom-test"
role="tabpanel" aria-labelledby="pf-custom-test-tab">
<form>
<div class="form-group">
<label for="pf-select-file">Font</label>
<select id="pf-select-file"
class="form-control custom-select">
<option value="open-sans">Open Sans</option>
<option value="eb-garamond">EB Garamond</option>
<option value="nimbus-sans">Nimbus Sans</option>
</select>
</div>
<div class="form-group">
<label for="pf-font-size">Font Size</label>
<div class="input-group">
<input id="pf-font-size" type="number"
class="form-control" value="32">
<div class="input-group-addon">px</div>
</div>
</div>
<div class="form-group">
<label for="pf-character">Character</label>
<input id="pf-character" type="text" maxlength="1" class="form-control"
value="B">
</div>
<div class="form-group">
<label for="pf-aa-level-select">Antialiasing</label>
<select id="pf-aa-level-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="xcaa" selected>XCAA</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa"
title="Subpixel AA"}}
</div>
<div class="form-group">
<label for="pf-reference-renderer">
Reference Renderer
</label>
<select id="pf-reference-renderer"
class="form-control custom-select">
<option value="freetype">FreeType</option>
<option value="core-graphics">Core Graphics</option>
</select>
</div>
<div class="form-group">
<label for="pf-ssim-label">SSIM</label>
<div id="pf-ssim-label">&mdash;</div>
</div>
</form>
</div>
</div>
</form>
</div>
<div class="col">
<div class="row">
<canvas id="pf-reference-canvas" class="pf-draggable border"
width="300" height="400"></canvas>
<canvas id="pf-canvas" class="pf-draggable pf-no-autoresize border"
width="300" height="400"></canvas>
</div>
<div class="row">
<canvas id="pf-difference-canvas" class="pf-draggable border"
width="300" height="400"></canvas>
</div>
</div>
</div>
<canvas id="pf-reference-canvas" class="pf-draggable pf-pane col" width="300"
height="400">
</canvas>
<canvas id="pf-canvas" class="pf-draggable pf-pane col" width="300"
height="400"></canvas>
</div>
</div>
</body>

View File

@ -15,6 +15,7 @@
"@types/lodash": "^4.14.74",
"@types/node": "^8.0.28",
"@types/opentype.js": "0.0.0",
"@types/papaparse": "^4.1.31",
"@types/webgl-ext": "0.0.29",
"base64-js": "^1.2.1",
"bootstrap": "^4.0.0-beta",
@ -23,10 +24,12 @@
"handlebars-loader": "^1.6.0",
"handlebars-webpack-plugin": "^1.2.0",
"html-loader": "^0.5.1",
"image-ssim": "^0.2.0",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"octicons": "^6.0.1",
"opentype.js": "^0.7.3",
"papaparse": "^4.3.6",
"parse-color": "^1.0.0",
"path-data-polyfill.js": "^1.0.2",
"popper.js": "^1.12.5",

View File

@ -393,7 +393,7 @@ class ThreeDRenderer extends Renderer {
}
protected get objectCount(): number {
return this.meshes.length;
return this.meshes == null ? 0 : this.meshes.length;
}
private cubeVertexPositionBuffer: WebGLBuffer;

View File

@ -19,18 +19,21 @@ const GAMMA_LUT_URI: string = "/textures/gamma-lut.png";
const SWITCHES: SwitchMap = {
gammaCorrection: {
defaultValue: 'on',
id: 'pf-gamma-correction',
offValue: 'off',
onValue: 'on',
radioButtonName: 'gammaCorrectionRadioButton',
},
stemDarkening: {
defaultValue: 'dark',
id: 'pf-stem-darkening',
offValue: 'none',
onValue: 'dark',
radioButtonName: 'stemDarkeningRadioButton',
},
subpixelAA: {
defaultValue: 'none',
id: 'pf-subpixel-aa',
offValue: 'none',
onValue: 'medium',
@ -43,6 +46,7 @@ interface SwitchDescriptor {
radioButtonName: keyof Switches;
onValue: string;
offValue: string;
defaultValue: string;
}
interface SwitchMap {
@ -66,25 +70,33 @@ interface Switches {
export abstract class AppController {
protected canvas: HTMLCanvasElement;
protected selectFileElement: HTMLSelectElement | null;
protected screenshotButton: HTMLButtonElement | null;
start(): void {}
start(): void {
this.selectFileElement = document.getElementById('pf-select-file') as HTMLSelectElement |
null;
}
protected loadInitialFile(builtinFileURI: string) {
const selectFileElement = document.getElementById('pf-select-file') as
(HTMLSelectElement | null);
if (selectFileElement != null) {
const selectedOption = selectFileElement.selectedOptions[0] as HTMLOptionElement;
protected loadInitialFile(builtinFileURI: string): void {
if (this.selectFileElement != null) {
const selectedOption = this.selectFileElement.selectedOptions[0] as HTMLOptionElement;
this.fetchFile(selectedOption.value, builtinFileURI);
} else {
this.fetchFile(this.defaultFile, builtinFileURI);
}
}
protected fetchFile(file: string, builtinFileURI: string) {
window.fetch(`${builtinFileURI}/${file}`)
.then(response => response.arrayBuffer())
.then(data => this.fileLoaded(data, file));
protected fetchFile(file: string, builtinFileURI: string): Promise<void> {
return new Promise(resolve => {
window.fetch(`${builtinFileURI}/${file}`)
.then(response => response.arrayBuffer())
.then(data => {
this.fileLoaded(data, file);
resolve();
});
});
}
protected abstract fileLoaded(data: ArrayBuffer, builtinName: string | null): void;
@ -108,7 +120,8 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
protected shaderSources: ShaderMap<ShaderProgramSource> | null;
protected gammaLUT: HTMLImageElement;
private aaLevelSelect: HTMLSelectElement | null;
protected aaLevelSelect: HTMLSelectElement | null;
private fpsLabel: HTMLElement | null;
constructor() {
@ -185,12 +198,9 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
this.filePickerView.onFileLoaded = fileData => this.fileLoaded(fileData, null);
}
const selectFileElement = document.getElementById('pf-select-file') as
(HTMLSelectElement | null);
if (selectFileElement != null) {
selectFileElement.addEventListener('click',
event => this.fileSelectionChanged(event),
false);
if (this.selectFileElement != null) {
this.selectFileElement
.addEventListener('click', event => this.fileSelectionChanged(event), false);
}
this.fpsLabel = document.getElementById('pf-fps-label');
@ -261,17 +271,7 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
protected abstract createView(): View;
private loadGammaLUT(): Promise<HTMLImageElement> {
return window.fetch(GAMMA_LUT_URI)
.then(response => response.blob())
.then(blob => {
const imgElement = document.createElement('img');
imgElement.src = URL.createObjectURL(blob);
return imgElement;
});
}
private updateAALevel() {
protected updateAALevel(): Promise<void> {
let aaType: AntialiasingStrategyName, aaLevel: number;
if (this.aaLevelSelect != null) {
const selectedOption = this.aaLevelSelect.selectedOptions[0];
@ -288,17 +288,29 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
const switchDescriptor = SWITCHES[switchName];
const radioButtonName = switchDescriptor.radioButtonName;
const radioButton = this[radioButtonName];
if (radioButton != null && radioButton.checked)
if (radioButton == null)
aaOptions[switchName] = switchDescriptor.defaultValue as any;
else if (radioButton.checked)
aaOptions[switchName] = switchDescriptor.onValue as any;
else
aaOptions[switchName] = switchDescriptor.offValue as any;
}
this.view.then(view => {
return this.view.then(view => {
view.setAntialiasingOptions(aaType, aaLevel, aaOptions as AAOptions);
});
}
private loadGammaLUT(): Promise<HTMLImageElement> {
return window.fetch(GAMMA_LUT_URI)
.then(response => response.blob())
.then(blob => {
const imgElement = document.createElement('img');
imgElement.src = URL.createObjectURL(blob);
return imgElement;
});
}
private fileSelectionChanged(event: Event): void {
const selectFileElement = event.currentTarget as HTMLSelectElement;
const selectedOption = selectFileElement.selectedOptions[0] as HTMLOptionElement;

View File

@ -328,7 +328,7 @@ class BenchmarkRenderer extends Renderer {
}
protected get objectCount(): number {
return this.meshes.length;
return this.meshes == null ? 0 : this.meshes.length;
}
private _pixelsPerEm: number = 32.0;

View File

@ -9,10 +9,14 @@
// except according to those terms.
import * as glmatrix from 'gl-matrix';
import * as imageSSIM from 'image-ssim';
import * as _ from 'lodash';
import * as papaparse from 'papaparse';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
import {SubpixelAAType} from './aa-strategy';
import {DemoAppController} from "./app-controller";
import {SUBPIXEL_GRANULARITY} from './atlas';
import {OrthographicCamera} from './camera';
import {UniformMap} from './gl-utils';
import {PathfinderMeshData} from './meshes';
@ -35,9 +39,27 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
xcaa: AdaptiveMonochromeXCAAStrategy,
};
const STRING: string = "A";
const RENDER_REFERENCE_URI: string = "/render-reference";
const TEST_DATA_URI: string = "/test-data/integration-test-text.csv";
const SSIM_TOLERANCE: number = 0.01;
const SSIM_WINDOW_SIZE: number = 8;
interface IntegrationTestGroup {
font: string;
tests: IntegrationTestCase[];
}
interface IntegrationTestCase {
size: number;
character: string;
aaMode: keyof AntialiasingStrategyTable;
subpixel: boolean;
referenceRenderer: ReferenceRenderer;
expectedSSIM: number;
}
type ReferenceRenderer = 'core-graphics' | 'freetype';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
@ -49,6 +71,10 @@ class IntegrationTestAppController extends DemoAppController<IntegrationTestView
font: PathfinderFont | null;
textRun: TextRun | null;
referenceCanvas: HTMLCanvasElement;
tests: Promise<IntegrationTestGroup[]>;
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
@ -56,17 +82,155 @@ class IntegrationTestAppController extends DemoAppController<IntegrationTestView
private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData;
private referenceCanvas: HTMLCanvasElement;
private fontSizeInput: HTMLInputElement;
private characterInput: HTMLInputElement;
private referenceRendererSelect: HTMLSelectElement;
private ssimLabel: HTMLElement;
private runTestsButton: HTMLButtonElement;
private resultsTable: HTMLTableElement;
private differenceCanvas: HTMLCanvasElement;
private currentTestGroupIndex: number | null;
private currentTestCaseIndex: number | null;
private currentGlobalTestCaseIndex: number | null;
get currentFontSize(): number {
return parseInt(this.fontSizeInput.value, 10);
}
set currentFontSize(newFontSize: number) {
this.fontSizeInput.value = "" + newFontSize;
}
get currentCharacter(): string {
return this.characterInput.value;
}
set currentCharacter(newCharacter: string) {
this.characterInput.value = newCharacter;
}
get currentReferenceRenderer(): ReferenceRenderer {
return this.referenceRendererSelect.value as ReferenceRenderer;
}
set currentReferenceRenderer(newReferenceRenderer: ReferenceRenderer) {
this.referenceRendererSelect.value = newReferenceRenderer;
}
start(): void {
this.referenceRendererSelect =
unwrapNull(document.getElementById('pf-reference-renderer')) as HTMLSelectElement;
this.referenceRendererSelect.addEventListener('change', () => {
this.view.then(view => this.runSingleTest());
}, false);
super.start();
this.currentTestGroupIndex = null;
this.currentTestCaseIndex = null;
this.currentGlobalTestCaseIndex = null;
this.referenceCanvas = unwrapNull(document.getElementById('pf-reference-canvas')) as
HTMLCanvasElement;
this.fontSizeInput = unwrapNull(document.getElementById('pf-font-size')) as
HTMLInputElement;
this.fontSizeInput.addEventListener('change', () => {
this.view.then(view => this.runSingleTest());
}, false);
this.characterInput = unwrapNull(document.getElementById('pf-character')) as
HTMLInputElement;
this.characterInput.addEventListener('change', () => {
this.view.then(view => this.runSingleTest());
}, false);
this.ssimLabel = unwrapNull(document.getElementById('pf-ssim-label'));
this.resultsTable = unwrapNull(document.getElementById('pf-results-table')) as
HTMLTableElement;
this.runTestsButton = unwrapNull(document.getElementById('pf-run-tests-button')) as
HTMLButtonElement;
this.runTestsButton.addEventListener('click', () => {
this.view.then(view => this.runTests());
}, false);
this.differenceCanvas = unwrapNull(document.getElementById('pf-difference-canvas')) as
HTMLCanvasElement;
this.loadTestData();
this.populateResultsTable();
this.loadInitialFile(this.builtinFileURI);
}
runNextTestIfNecessary(tests: IntegrationTestGroup[]): void {
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null ||
this.currentGlobalTestCaseIndex == null) {
return;
}
this.currentTestCaseIndex++;
this.currentGlobalTestCaseIndex++;
if (this.currentTestCaseIndex === tests[this.currentTestGroupIndex].tests.length) {
this.currentTestCaseIndex = 0;
this.currentTestGroupIndex++;
if (this.currentTestGroupIndex === tests.length) {
// Done running tests.
this.currentTestCaseIndex = null;
this.currentTestGroupIndex = null;
this.currentGlobalTestCaseIndex = null;
this.view.then(view => view.suppressAutomaticRedraw = false);
return;
}
}
this.loadFontForTestGroupIfNecessary(tests).then(() => {
this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest());
});
}
recordSSIMResult(tests: IntegrationTestGroup[], ssimResult: imageSSIM.IResult): void {
const formattedSSIM: string = "" + (Math.round(ssimResult.ssim * 1000.0) / 1000.0);
this.ssimLabel.textContent = formattedSSIM;
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null ||
this.currentGlobalTestCaseIndex == null) {
return;
}
const testGroup = tests[this.currentTestGroupIndex];
const expectedSSIM = testGroup.tests[this.currentTestCaseIndex].expectedSSIM;
const passed = Math.abs(expectedSSIM - ssimResult.ssim) <= SSIM_TOLERANCE;
const resultsBody: Element = unwrapNull(this.resultsTable.lastElementChild);
let resultsRow = unwrapNull(resultsBody.firstElementChild);
for (let rowIndex = 0; rowIndex < this.currentGlobalTestCaseIndex; rowIndex++)
resultsRow = unwrapNull(resultsRow.nextElementSibling);
const passCell = unwrapNull(resultsRow.firstElementChild);
const resultsCell = unwrapNull(resultsRow.lastElementChild);
resultsCell.textContent = formattedSSIM;
passCell.textContent = passed ? "✓" : "✗";
resultsRow.classList.remove('table-success', 'table-danger');
resultsRow.classList.add(passed ? 'table-success' : 'table-danger');
}
drawDifferenceImage(differenceImage: imageSSIM.IImage): void {
const canvas = this.differenceCanvas;
const context = unwrapNull(canvas.getContext('2d'));
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
const data = new Uint8ClampedArray(differenceImage.data);
const imageData = new ImageData(data, differenceImage.width, differenceImage.height);
context.putImageData(imageData, 0, 0);
}
protected createView(): IntegrationTestView {
return new IntegrationTestView(this,
unwrapNull(this.gammaLUT),
@ -78,38 +242,158 @@ class IntegrationTestAppController extends DemoAppController<IntegrationTestView
const font = new PathfinderFont(fileData, builtinName);
this.font = font;
const textRun = new TextRun(STRING, [0, 0], font);
textRun.layout();
this.textRun = textRun;
const textFrame = new TextFrame([textRun], font);
const glyphIDs = textFrame.allGlyphIDs;
glyphIDs.sort((a, b) => a - b);
this.glyphStore = new GlyphStore(font, glyphIDs);
this.glyphStore.partition().then(result => {
this.baseMeshes = result.meshes;
const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, glyphIDs);
this.expandedMeshes = expandedMeshes;
this.view.then(view => view.attachMeshes([expandedMeshes.meshes]));
});
this.loadInitialReference();
// Don't automatically run the test unless this is a custom test.
if (this.currentGlobalTestCaseIndex == null)
this.runSingleTest();
}
private loadInitialReference(): void {
private loadTestData(): void {
this.tests = window.fetch(TEST_DATA_URI)
.then(response => response.text())
.then(testDataText => {
const fontNames = [];
const groups: {[font: string]: IntegrationTestCase[]} = {};
const testData = papaparse.parse(testDataText, {
comments: "#",
header: true,
skipEmptyLines: true,
});
for (const row of testData.data) {
if (!groups.hasOwnProperty(row.Font)) {
fontNames.push(row.Font);
groups[row.Font] = [];
}
groups[row.Font].push({
aaMode: row['AA Mode'] as keyof AntialiasingStrategyTable,
character: row.Character,
expectedSSIM: parseFloat(row['Expected SSIM']),
referenceRenderer: row['Reference Renderer'],
size: parseInt(row.Size, 10),
subpixel: !!row.Subpixel,
});
}
return fontNames.map(fontName => {
return {
font: fontName,
tests: groups[fontName],
};
});
});
}
private populateResultsTable(): void {
this.tests.then(tests => {
const resultsBody: Element = unwrapNull(this.resultsTable.lastElementChild);
for (const testGroup of tests) {
for (const test of testGroup.tests) {
const row = document.createElement('tr');
addCell(row, "");
addCell(row, testGroup.font);
addCell(row, test.character);
addCell(row, "" + test.size);
addCell(row, "" + test.aaMode);
addCell(row, test.subpixel ? "Y" : "N");
addCell(row, test.referenceRenderer);
addCell(row, "" + test.expectedSSIM);
addCell(row, "");
resultsBody.appendChild(row);
}
}
});
}
private runSingleTest(): void {
this.setUpTextRun();
this.loadReference().then(() => this.loadRendering());
}
private runTests(): void {
this.view.then(view => {
view.suppressAutomaticRedraw = true;
this.tests.then(tests => {
this.currentTestGroupIndex = 0;
this.currentTestCaseIndex = 0;
this.currentGlobalTestCaseIndex = 0;
this.loadFontForTestGroupIfNecessary(tests).then(() => {
this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest());
});
});
});
}
private loadFontForTestGroupIfNecessary(tests: IntegrationTestGroup[]): Promise<void> {
return new Promise(resolve => {
if (this.currentTestGroupIndex == null) {
resolve();
return;
}
this.fetchFile(tests[this.currentTestGroupIndex].font, BUILTIN_FONT_URI).then(() => {
resolve();
});
});
}
private setOptionsForCurrentTest(tests: IntegrationTestGroup[]): Promise<void> {
if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null)
return new Promise(resolve => resolve());
const currentTestCase = tests[this.currentTestGroupIndex].tests[this.currentTestCaseIndex];
this.currentFontSize = currentTestCase.size;
this.currentCharacter = currentTestCase.character;
this.currentReferenceRenderer = currentTestCase.referenceRenderer;
const aaLevelSelect = unwrapNull(this.aaLevelSelect);
aaLevelSelect.selectedIndex = _.findIndex(aaLevelSelect.options, option => {
return option.value.startsWith(currentTestCase.aaMode);
});
unwrapNull(this.subpixelAARadioButton).checked = currentTestCase.subpixel;
return this.updateAALevel();
}
private setUpTextRun(): void {
const font = unwrapNull(this.font);
const textRun = new TextRun(this.currentCharacter, [0, 0], font);
textRun.layout();
this.textRun = textRun;
this.glyphStore = new GlyphStore(font, [textRun.glyphIDs[0]]);
}
private loadRendering(): void {
this.glyphStore.partition().then(result => {
const textRun = unwrapNull(this.textRun);
this.baseMeshes = result.meshes;
const textFrame = new TextFrame([textRun], unwrapNull(this.font));
const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, [textRun.glyphIDs[0]]);
this.expandedMeshes = expandedMeshes;
this.view.then(view => {
view.attachMeshes([expandedMeshes.meshes]);
view.redraw();
});
});
}
private loadReference(): Promise<void> {
const request = {
face: {
Builtin: unwrapNull(this.font).builtinFontName,
},
fontIndex: 0,
glyph: this.glyphStore.glyphIDs[0],
pointSize: 32.0,
pointSize: this.currentFontSize,
renderer: this.currentReferenceRenderer,
};
window.fetch(RENDER_REFERENCE_URI, {
return window.fetch(RENDER_REFERENCE_URI, {
body: JSON.stringify(request),
headers: {'Content-Type': 'application/json'} as any,
method: 'POST',
@ -117,7 +401,10 @@ class IntegrationTestAppController extends DemoAppController<IntegrationTestView
const imgElement = document.createElement('img');
imgElement.src = URL.createObjectURL(blob);
imgElement.addEventListener('load', () => {
const context = unwrapNull(this.referenceCanvas.getContext('2d'));
const canvas = this.referenceCanvas;
const context = unwrapNull(canvas.getContext('2d'));
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(imgElement, 0, 0);
}, false);
});
@ -143,14 +430,45 @@ class IntegrationTestView extends DemoView {
this.resizeToFit(true);
}
protected renderingFinished(): void {
const gl = this.renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const pixelRect = this.renderer.getPixelRectForGlyphAt(0);
const canvasHeight = this.canvas.height;
const width = pixelRect[2] - pixelRect[0], height = pixelRect[3] - pixelRect[1];
const originY = Math.max(canvasHeight - height, 0);
const flippedBuffer = new Uint8Array(width * height * 4);
gl.readPixels(0, originY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, flippedBuffer);
const buffer = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
const destRowStart = y * width * 4;
const srcRowStart = (height - y - 1) * width * 4;
buffer.set(flippedBuffer.slice(srcRowStart, srcRowStart + width * 4),
destRowStart);
}
const renderedImage = createSSIMImage(buffer, pixelRect);
this.appController.tests.then(tests => {
const referenceImage = createSSIMImage(this.appController.referenceCanvas,
pixelRect);
const ssimResult = imageSSIM.compare(referenceImage, renderedImage, SSIM_WINDOW_SIZE);
const differenceImage = generateDifferenceImage(referenceImage, renderedImage);
this.appController.recordSSIMResult(tests, ssimResult);
this.appController.drawDifferenceImage(differenceImage);
this.appController.runNextTestIfNecessary(tests);
});
}
}
class IntegrationTestRenderer extends Renderer {
renderContext: IntegrationTestView;
camera: OrthographicCamera;
private _pixelsPerEm: number = 32.0;
get destFramebuffer(): WebGLFramebuffer | null {
return null;
}
@ -177,7 +495,7 @@ class IntegrationTestRenderer extends Renderer {
}
protected get objectCount(): number {
return this.meshes.length;
return this.meshes == null ? 0 : this.meshes.length;
}
protected get usedSizeFactor(): glmatrix.vec2 {
@ -201,12 +519,14 @@ class IntegrationTestRenderer extends Renderer {
}
private get pixelsPerUnit(): number {
const font = unwrapNull(this.renderContext.appController.font);
return this._pixelsPerEm / font.opentypeFont.unitsPerEm;
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
return appController.currentFontSize / font.opentypeFont.unitsPerEm;
}
private get stemDarkeningAmount(): glmatrix.vec2 {
return computeStemDarkeningAmount(this._pixelsPerEm, this.pixelsPerUnit);
const appController = this.renderContext.appController;
return computeStemDarkeningAmount(appController.currentFontSize, this.pixelsPerUnit);
}
constructor(renderContext: IntegrationTestView) {
@ -225,27 +545,23 @@ class IntegrationTestRenderer extends Renderer {
}
pathCountForObject(objectIndex: number): number {
return STRING.length;
return 1;
}
pathBoundingRects(objectIndex: number): Float32Array {
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
const boundingRects = new Float32Array((STRING.length + 1) * 4);
const boundingRects = new Float32Array(2 * 4);
for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) {
const glyphID = unwrapNull(appController.textRun).glyphIDs[glyphIndex];
const glyphID = unwrapNull(appController.textRun).glyphIDs[0];
const metrics = font.metricsForGlyph(glyphID);
if (metrics == null)
continue;
const metrics = unwrapNull(font.metricsForGlyph(glyphID));
boundingRects[(glyphIndex + 1) * 4 + 0] = metrics.xMin;
boundingRects[(glyphIndex + 1) * 4 + 1] = metrics.yMin;
boundingRects[(glyphIndex + 1) * 4 + 2] = metrics.xMax;
boundingRects[(glyphIndex + 1) * 4 + 3] = metrics.yMax;
}
boundingRects[4 + 0] = metrics.xMin;
boundingRects[4 + 1] = metrics.yMin;
boundingRects[4 + 2] = metrics.xMax;
boundingRects[4 + 3] = metrics.yMax;
return boundingRects;
}
@ -254,21 +570,33 @@ class IntegrationTestRenderer extends Renderer {
this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0);
}
getPixelRectForGlyphAt(glyphIndex: number): glmatrix.vec4 {
const appController = this.renderContext.appController;
const font = unwrapNull(appController.font);
const hint = new Hint(font, this.pixelsPerUnit, true);
const textRun = unwrapNull(appController.textRun);
const glyphID = textRun.glyphIDs[glyphIndex];
return textRun.pixelRectForGlyphAt(glyphIndex,
this.pixelsPerUnit,
this.pixelsPerUnit,
hint,
this.stemDarkeningAmount,
SUBPIXEL_GRANULARITY);
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType):
AntialiasingStrategy {
// FIXME(pcwalton)
// return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
return new ANTIALIASING_STRATEGIES.xcaa(aaLevel, 'medium');
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected compositeIfNecessary(): void {}
protected pathColorsForObject(objectIndex: number): Uint8Array {
const pathColors = new Uint8Array(4 * (STRING.length + 1));
for (let pathIndex = 0; pathIndex < STRING.length; pathIndex++)
pathColors.set(TEXT_COLOR, (pathIndex + 1) * 4);
const pathColors = new Uint8Array(4 * 2);
pathColors.set(TEXT_COLOR, 1 * 4);
return pathColors;
}
@ -277,29 +605,22 @@ class IntegrationTestRenderer extends Renderer {
const canvas = this.renderContext.canvas;
const font = unwrapNull(appController.font);
const hint = new Hint(font, this.pixelsPerUnit, true);
const canvasHeight = canvas.height;
const pathTransforms = new Float32Array(4 * (STRING.length + 1));
const pathTransforms = new Float32Array(4 * 2);
let currentX = 0;
const availableWidth = canvas.width / this.pixelsPerUnit;
const lineHeight = font.opentypeFont.lineHeight();
const textRun = unwrapNull(appController.textRun);
const glyphID = textRun.glyphIDs[0];
const pixelRect = textRun.pixelRectForGlyphAt(0,
this.pixelsPerUnit,
this.pixelsPerUnit,
hint,
glmatrix.vec2.create(),
SUBPIXEL_GRANULARITY);
for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) {
const textRun = unwrapNull(appController.textRun);
const glyphID = textRun.glyphIDs[glyphIndex];
const pixelRect = textRun.pixelRectForGlyphAt(glyphIndex,
this.pixelsPerUnit,
this.pixelsPerUnit,
hint,
this.stemDarkeningAmount,
0);
const x = -pixelRect[0] / this.pixelsPerUnit;
const y = (canvas.height - (pixelRect[3] - pixelRect[1])) / this.pixelsPerUnit;
const y = (canvasHeight - pixelRect[3]) / this.pixelsPerUnit;
pathTransforms.set([1, 1, currentX, y], (glyphIndex + 1) * 4);
currentX += font.opentypeFont.glyphs.get(glyphID).advanceWidth;
}
pathTransforms.set([1, 1, x, y], 1 * 4);
return pathTransforms;
}
@ -313,6 +634,71 @@ class IntegrationTestRenderer extends Renderer {
}
}
function createSSIMImage(image: HTMLCanvasElement | Uint8Array, rect: glmatrix.vec4):
imageSSIM.IImage {
const size = glmatrix.vec2.clone([rect[2] - rect[0], rect[3] - rect[1]]);
let data;
if (image instanceof HTMLCanvasElement) {
const context = unwrapNull(image.getContext('2d'));
data = new Uint8Array(context.getImageData(0, 0, size[0], size[1]).data);
} else {
data = image;
}
return {
channels: imageSSIM.Channels.RGBAlpha,
data: data,
height: size[1],
width: size[0],
};
}
function generateDifferenceImage(referenceImage: imageSSIM.IImage,
renderedImage: imageSSIM.IImage):
imageSSIM.IImage {
const differenceImage = new Uint8Array(referenceImage.width * referenceImage.height * 4);
for (let y = 0; y < referenceImage.height; y++) {
const rowStart = y * referenceImage.width * 4;
for (let x = 0; x < referenceImage.width; x++) {
const pixelStart = rowStart + x * 4;
let differenceSum = 0;
for (let channel = 0; channel < 3; channel++) {
differenceSum += Math.abs(referenceImage.data[pixelStart + channel] -
renderedImage.data[pixelStart + channel]);
}
if (differenceSum === 0) {
// Lighten to indicate no difference.
for (let channel = 0; channel < 4; channel++) {
differenceImage[pixelStart + channel] =
Math.floor(referenceImage.data[pixelStart + channel] / 2) + 128;
}
continue;
}
// Draw differences in red.
const differenceMean = differenceSum / 3;
differenceImage[pixelStart + 0] = 127 + Math.round(differenceMean / 2);
differenceImage[pixelStart + 1] = differenceImage[pixelStart + 2] = 0;
differenceImage[pixelStart + 3] = 255;
}
}
return {
channels: referenceImage.channels,
data: differenceImage,
height: referenceImage.height,
width: referenceImage.width,
};
}
function addCell(row: HTMLTableRowElement, text: string): void {
const tableCell = document.createElement('td');
tableCell.textContent = text;
row.appendChild(tableCell);
}
function main() {
const controller = new IntegrationTestAppController;
window.addEventListener('load', () => controller.start(), false);

View File

@ -36,8 +36,8 @@ export abstract class Renderer {
readonly pathTransformBufferTextures: PathfinderBufferTexture[];
meshes: PathfinderMeshBuffers[];
meshData: PathfinderMeshData[];
meshes: PathfinderMeshBuffers[] | null;
meshData: PathfinderMeshData[] | null;
get emboldenAmount(): glmatrix.vec2 {
return glmatrix.vec2.create();
@ -52,13 +52,17 @@ export abstract class Renderer {
}
get backgroundColor(): glmatrix.vec4 {
return glmatrix.vec4.create();
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get usesIntermediateRenderTargets(): boolean {
return false;
}
get meshesAttached(): boolean {
return this.meshes != null && this.meshData != null;
}
abstract get destFramebuffer(): WebGLFramebuffer | null;
abstract get destAllocatedSize(): glmatrix.vec2;
abstract get destUsedSize(): glmatrix.vec2;
@ -88,6 +92,9 @@ export abstract class Renderer {
constructor(renderContext: RenderContext) {
this.renderContext = renderContext;
this.meshData = null;
this.meshes = null;
this.lastTimings = { rendering: 0, compositing: 0 };
this.gammaCorrectionMode = 'on';
@ -304,12 +311,14 @@ export abstract class Renderer {
}
pathRangeForObject(objectIndex: number): Range {
if (this.meshes == null)
return new Range(0, 0);
const bVertexPathRanges = this.meshes[objectIndex].bVertexPathRanges;
return new Range(1, bVertexPathRanges.length + 1);
}
protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null {
return glmatrix.vec4.create();
return null;
}
protected bindGammaLUT(bgColor: glmatrix.vec3, textureUnit: number, uniforms: UniformMap):
@ -344,9 +353,11 @@ export abstract class Renderer {
const clearColor = this.backgroundColor;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.destFramebuffer);
gl.depthMask(true);
gl.viewport(0, 0, this.destAllocatedSize[0], this.destAllocatedSize[1]);
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected clearForDirectRendering(objectIndex: number): void {
@ -388,6 +399,9 @@ export abstract class Renderer {
protected newTimingsReceived(): void {}
private directlyRenderObject(objectIndex: number): void {
if (this.meshes == null || this.meshData == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;
@ -552,6 +566,9 @@ export abstract class Renderer {
}
private initImplicitCoverCurveVAO(objectIndex: number, instanceRange: Range): void {
if (this.meshes == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;
@ -608,6 +625,9 @@ export abstract class Renderer {
}
private initImplicitCoverInteriorVAO(objectIndex: number, instanceRange: Range): void {
if (this.meshes == null)
return;
const renderContext = this.renderContext;
const gl = renderContext.gl;

View File

@ -127,7 +127,7 @@ const SHADER_URLS: ShaderMap<ShaderProgramURLs> = {
},
ssaaSubpixelResolve: {
fragment: "/glsl/gles2/ssaa-subpixel-resolve.fs.glsl",
vertex: "/glsl/gles2/ssaa-subpixel-resolve.vs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
xcaaMonoResolve: {
fragment: "/glsl/gles2/xcaa-mono-resolve.fs.glsl",

View File

@ -102,7 +102,8 @@ export default class SSAAStrategy extends AntialiasingStrategy {
const clearColor = renderer.backgroundColor;
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
prepareForDirectRendering(renderer: Renderer): void {}

View File

@ -208,6 +208,10 @@ class SVGDemoRenderer extends Renderer {
return transform;
}
protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null {
return glmatrix.vec4.create();
}
protected directCurveProgramName(): keyof ShaderMap<void> {
if (this.antialiasingStrategy instanceof XCAAStrategy)
return 'xcaaMultiDirectCurve';

View File

@ -359,11 +359,15 @@ class TextDemoRenderer extends TextRenderer {
glyphTexCoordsBuffer: WebGLBuffer;
glyphElementsBuffer: WebGLBuffer;
private glyphBounds: Float32Array;
get layout(): SimpleTextLayout {
return this.renderContext.appController.layout;
}
private glyphBounds: Float32Array;
get backgroundColor(): glmatrix.vec4 {
return glmatrix.vec4.create();
}
prepareToAttachText(): void {
if (this.atlasFramebuffer == null)

View File

@ -122,7 +122,7 @@ export abstract class TextRenderer extends Renderer {
}
protected get objectCount(): number {
return this.meshes.length;
return this.meshes == null ? 0 : this.meshes.length;
}
private stemDarkening: StemDarkeningMode;
@ -193,14 +193,7 @@ export abstract class TextRenderer extends Renderer {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected clearForDirectRendering(): void {
const gl = this.renderContext.gl;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(0.0);
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
protected clearForDirectRendering(): void {}
protected buildAtlasGlyphs(atlasGlyphs: AtlasGlyph[]): void {
const font = this.renderContext.font;

View File

@ -349,10 +349,10 @@ export class UnitMetrics {
descent: number;
constructor(metrics: Metrics, stemDarkeningAmount: glmatrix.vec2) {
this.left = metrics.xMin - stemDarkeningAmount[0];
this.right = metrics.xMax + stemDarkeningAmount[0];
this.ascent = metrics.yMax + stemDarkeningAmount[1];
this.descent = metrics.yMin - stemDarkeningAmount[1];
this.left = metrics.xMin;
this.right = metrics.xMax + stemDarkeningAmount[0] * 2;
this.ascent = metrics.yMax + stemDarkeningAmount[1] * 2;
this.descent = metrics.yMin;
}
}

View File

@ -51,6 +51,8 @@ declare class WebGLQuery {}
export abstract class PathfinderView {
canvas: HTMLCanvasElement;
suppressAutomaticRedraw: boolean;
protected abstract get camera(): Camera;
private dirty: boolean;
@ -59,12 +61,13 @@ export abstract class PathfinderView {
constructor() {
this.dirty = false;
this.suppressAutomaticRedraw = false;
this.canvas = unwrapNull(document.getElementById('pf-canvas')) as HTMLCanvasElement;
window.addEventListener('resize', () => this.resizeToFit(false), false);
}
setDirty(): void {
if (this.dirty)
if (this.dirty || this.suppressAutomaticRedraw)
return;
this.dirty = true;
window.requestAnimationFrame(() => this.redraw());
@ -97,16 +100,16 @@ export abstract class PathfinderView {
this.pulseHandle = window.requestAnimationFrame(tick);
}
redraw(): void {
this.dirty = false;
}
protected resized(): void {
this.setDirty();
}
protected redraw(): void {
this.dirty = false;
}
protected resizeToFit(initialSize: boolean): void {
if (!this.canvas.classList.contains('pf-pane')) {
if (!this.canvas.classList.contains('pf-no-autoresize')) {
const windowWidth = window.innerWidth;
const canvasTop = this.canvas.getBoundingClientRect().top;
let height = window.scrollY + window.innerHeight - canvasTop;
@ -212,6 +215,46 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
this.renderer.setAntialiasingOptions(aaType, aaLevel, aaOptions);
}
snapshot(rect: glmatrix.vec4): Uint8Array {
const gl = this.renderContext.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const canvasHeight = this.canvas.height;
const width = rect[2] - rect[0], height = rect[3] - rect[1];
const originX = Math.max(rect[0], 0);
const originY = Math.max(canvasHeight - height, 0);
const flippedBuffer = new Uint8Array(width * height * 4);
gl.readPixels(originX, originY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, flippedBuffer);
const buffer = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
const destRowStart = y * width * 4;
const srcRowStart = (height - y - 1) * width * 4;
buffer.set(flippedBuffer.slice(srcRowStart, srcRowStart + width * 4),
destRowStart);
}
return buffer;
}
redraw(): void {
super.redraw();
if (!this.renderer.meshesAttached)
return;
this.renderer.redraw();
// Invoke the post-render hook.
this.renderingFinished();
// Take a screenshot if desired.
if (this.wantsScreenshot) {
this.wantsScreenshot = false;
this.takeScreenshot();
}
}
protected resized(): void {
super.resized();
this.renderer.canvasResized();
@ -247,21 +290,6 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
this.compositingTimerQuery = this.timerQueryExt.createQueryEXT();
}
protected redraw(): void {
super.redraw();
this.renderer.redraw();
// Invoke the post-render hook.
this.renderingFinished();
// Take a screenshot if desired.
if (this.wantsScreenshot) {
this.wantsScreenshot = false;
this.takeScreenshot();
}
}
protected renderingFinished(): void {}
private compileShaders(commonSource: string, shaderSources: ShaderMap<ShaderProgramSource>):

View File

@ -385,6 +385,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
objectIndex: number,
lineProgram: PathfinderShaderProgram):
void {
if (renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -421,6 +424,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
objectIndex: number,
curveProgram: PathfinderShaderProgram):
void {
if (renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -458,6 +464,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
}
private initCoverVAOForObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshes == null || renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -518,6 +527,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
}
private initLineVAOsForObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshes == null || renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -597,6 +609,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
}
private initCurveVAOsForObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshes == null || renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -675,6 +690,9 @@ export abstract class MCAAStrategy extends XCAAStrategy {
}
private coverObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshes == null || renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -796,6 +814,9 @@ export class ECAAStrategy extends XCAAStrategy {
}
private createLineVAO(renderer: Renderer): void {
if (renderer.meshes == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -857,6 +878,9 @@ export class ECAAStrategy extends XCAAStrategy {
}
private createCurveVAO(renderer: Renderer): void {
if (renderer.meshes == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -914,6 +938,9 @@ export class ECAAStrategy extends XCAAStrategy {
}
private antialiasLinesOfObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;
@ -942,6 +969,9 @@ export class ECAAStrategy extends XCAAStrategy {
}
private antialiasCurvesOfObject(renderer: Renderer, objectIndex: number): void {
if (renderer.meshData == null)
return;
const renderContext = renderer.renderContext;
const gl = renderContext.gl;

View File

@ -25,6 +25,7 @@ git = "https://github.com/servo/fontsan.git"
[dependencies.pathfinder_font_renderer]
path = "../../font-renderer"
features = ["freetype"]
[dependencies.pathfinder_partitioner]
path = "../../partitioner"

View File

@ -33,7 +33,8 @@ use app_units::Au;
use euclid::{Point2D, Transform2D};
use image::{DynamicImage, ImageBuffer, ImageFormat, ImageRgba8};
use lru_cache::LruCache;
use pathfinder_font_renderer::{FontContext, FontInstance, FontKey, GlyphKey, SubpixelOffset};
use pathfinder_font_renderer::{FontContext, FontInstance, FontKey, GlyphImage};
use pathfinder_font_renderer::{GlyphKey, SubpixelOffset};
use pathfinder_partitioner::mesh_library::MeshLibrary;
use pathfinder_partitioner::partitioner::Partitioner;
use pathfinder_path_utils::cubic::CubicCurve;
@ -52,6 +53,9 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::u32;
#[cfg(target_os = "macos")]
use pathfinder_font_renderer::core_graphics;
const SUGGESTED_JSON_SIZE_LIMIT: u64 = 32 * 1024 * 1024;
const CUBIC_ERROR_TOLERANCE: f32 = 0.1;
@ -83,6 +87,7 @@ static STATIC_SVG_OCTICONS_PATH: &'static str = "../client/node_modules/octicons
static STATIC_WOFF2_INTER_UI_PATH: &'static str = "../../resources/fonts/inter-ui";
static STATIC_GLSL_PATH: &'static str = "../../shaders";
static STATIC_DATA_PATH: &'static str = "../../resources/data";
static STATIC_TEST_DATA_PATH: &'static str = "../../resources/tests";
static STATIC_TEXTURES_PATH: &'static str = "../../resources/textures";
static STATIC_DOC_API_INDEX_URI: &'static str = "/doc/api/pathfinder_font_renderer/index.html";
@ -122,6 +127,14 @@ enum FontRequestFace {
Custom(String),
}
#[derive(Clone, Copy, Serialize, Deserialize)]
enum ReferenceRenderer {
#[serde(rename = "freetype")]
FreeType,
#[serde(rename = "core-graphics")]
CoreGraphics,
}
#[derive(Clone, Serialize, Deserialize)]
struct RenderReferenceRequest {
face: FontRequestFace,
@ -131,6 +144,7 @@ struct RenderReferenceRequest {
#[serde(rename = "pointSize")]
point_size: f64,
renderer: ReferenceRenderer,
}
#[derive(Clone, Copy, Serialize, Deserialize)]
@ -152,6 +166,7 @@ enum FontError {
FontSanitizationFailed,
FontLoadingFailed,
RasterizationFailed,
ReferenceRasterizerUnavailable,
Unimplemented,
}
@ -254,10 +269,9 @@ impl<'r> Responder<'r> for ReferenceImage {
}
}
fn add_font_from_request(font_context: &mut FontContext, face: &FontRequestFace, font_index: u32)
-> Result<FontKey, FontError> {
// Fetch the OTF data.
let otf_data = match *face {
// Fetches the OTF data.
fn otf_data_from_request(face: &FontRequestFace) -> Result<Arc<Vec<u8>>, FontError> {
match *face {
FontRequestFace::Builtin(ref builtin_font_name) => {
// Read in the builtin font.
match BUILTIN_FONTS.iter().filter(|& &(name, _)| name == builtin_font_name).next() {
@ -266,7 +280,7 @@ fn add_font_from_request(font_context: &mut FontContext, face: &FontRequestFace,
File::open(path).expect("Couldn't find builtin font!")
.read_to_end(&mut data)
.expect("Couldn't read builtin font!");
Arc::new(data)
Ok(Arc::new(data))
}
None => return Err(FontError::UnknownBuiltinFont),
}
@ -280,20 +294,36 @@ fn add_font_from_request(font_context: &mut FontContext, face: &FontRequestFace,
// Sanitize.
match fontsan::process(&unsafe_otf_data) {
Ok(otf_data) => Arc::new(otf_data),
Ok(otf_data) => Ok(Arc::new(otf_data)),
Err(_) => return Err(FontError::FontSanitizationFailed),
}
}
};
// Parse glyph data.
let font_key = FontKey::new();
if font_context.add_font_from_memory(&font_key, otf_data, font_index).is_err() {
println!("Failed to add font from memory!");
return Err(FontError::FontLoadingFailed)
}
}
Ok(font_key)
#[cfg(target_os = "macos")]
fn rasterize_glyph_with_core_graphics(font_key: &FontKey,
font_index: u32,
otf_data: Arc<Vec<u8>>,
font_instance: &FontInstance,
glyph_key: &GlyphKey)
-> Result<GlyphImage, FontError> {
let mut font_context =
try!(core_graphics::FontContext::new().map_err(|_| FontError::FontLoadingFailed));
try!(font_context.add_font_from_memory(font_key, otf_data, font_index)
.map_err(|_| FontError::FontLoadingFailed));
font_context.rasterize_glyph_with_native_rasterizer(&font_instance, &glyph_key, true)
.map_err(|_| FontError::RasterizationFailed)
}
#[cfg(not(target_os = "macos"))]
fn rasterize_glyph_with_core_graphics(_: &FontKey,
_: u32,
_: Arc<Vec<u8>>,
_: &FontInstance,
_: &GlyphKey)
-> Result<GlyphImage, FontError> {
Err(FontError::ReferenceRasterizerUnavailable)
}
#[post("/partition-font", format = "application/json", data = "<request>")]
@ -325,9 +355,13 @@ fn partition_font(request: Json<PartitionFontRequest>) -> Result<PartitionRespon
return Err(FontError::FontLoadingFailed)
}
};
let font_key = try!(add_font_from_request(&mut font_context,
&request.face,
request.font_index));
let font_key = FontKey::new();
let otf_data = try!(otf_data_from_request(&request.face));
if font_context.add_font_from_memory(&font_key, otf_data, request.font_index).is_err() {
return Err(FontError::FontLoadingFailed)
}
let font_instance = FontInstance {
font_key: font_key,
size: Au::from_f64_px(request.point_size),
@ -463,27 +497,35 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
#[post("/render-reference", format = "application/json", data = "<request>")]
fn render_reference(request: Json<RenderReferenceRequest>)
-> Result<ReferenceImage, FontError> {
// Load the font.
let mut font_context = match FontContext::new() {
Ok(font_context) => font_context,
Err(_) => {
println!("Failed to create a font context!");
return Err(FontError::FontLoadingFailed)
}
};
let font_key = try!(add_font_from_request(&mut font_context,
&request.face,
request.font_index));
let font_key = FontKey::new();
let otf_data = try!(otf_data_from_request(&request.face));
let font_instance = FontInstance {
font_key: font_key,
size: Au::from_f64_px(request.point_size),
};
// Render the image.
let glyph_key = GlyphKey::new(request.glyph, SubpixelOffset(0));
let glyph_image = try!(font_context.rasterize_glyph_with_native_rasterizer(&font_instance,
&glyph_key)
.map_err(|_| FontError::RasterizationFailed));
// Rasterize the glyph using the right rasterizer.
let glyph_image = match request.renderer {
ReferenceRenderer::FreeType => {
let mut font_context =
try!(FontContext::new().map_err(|_| FontError::FontLoadingFailed));
try!(font_context.add_font_from_memory(&font_key, otf_data, request.font_index)
.map_err(|_| FontError::FontLoadingFailed));
try!(font_context.rasterize_glyph_with_native_rasterizer(&font_instance,
&glyph_key,
true)
.map_err(|_| FontError::RasterizationFailed))
}
ReferenceRenderer::CoreGraphics => {
try!(rasterize_glyph_with_core_graphics(&font_key,
request.font_index,
otf_data,
&font_instance,
&glyph_key))
}
};
let dimensions = &glyph_image.dimensions;
let image_buffer = ImageBuffer::from_raw(dimensions.size.width,
dimensions.size.height,
@ -590,6 +632,10 @@ fn static_svg_demo(svg_name: String) -> Option<NamedFile> {
fn static_data(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new(STATIC_DATA_PATH).join(file)).ok()
}
#[get("/test-data/<file..>")]
fn static_test_data(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new(STATIC_TEST_DATA_PATH).join(file)).ok()
}
#[get("/textures/<file..>")]
fn static_textures(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new(STATIC_TEXTURES_PATH).join(file)).ok()
@ -654,6 +700,7 @@ fn main() {
static_otf_demo,
static_svg_demo,
static_data,
static_test_data,
static_textures,
]).launch();
}

View File

@ -101,7 +101,7 @@ impl FontContext {
}
}
pub fn glyph_dimensions(&self, font_instance: &FontInstance, glyph_key: &GlyphKey)
pub fn glyph_dimensions(&self, font_instance: &FontInstance, glyph_key: &GlyphKey, exact: bool)
-> Result<GlyphDimensions, ()> {
let core_graphics_font = match self.core_graphics_fonts.get(&font_instance.font_key) {
None => return Err(()),
@ -138,10 +138,12 @@ impl FontContext {
// the values seem to be 1.21% in the X direction and 1.5125% in the Y direction. Make sure
// that there's enough room to account for this. We round the values up to 2% to account
// for the possibility that Apple might tweak this later.
let font_dilation_radius = (font_instance.size.to_f32_px() * FONT_DILATION_AMOUNT *
0.5).ceil() as i32;
lower_left += Vector2D::new(-font_dilation_radius, -font_dilation_radius);
upper_right += Vector2D::new(font_dilation_radius, font_dilation_radius);
if !exact {
let font_dilation_radius = (font_instance.size.to_f32_px() *
FONT_DILATION_AMOUNT * 0.5).ceil() as i32;
lower_left += Vector2D::new(-font_dilation_radius, -font_dilation_radius);
upper_right += Vector2D::new(font_dilation_radius, font_dilation_radius);
}
Ok(GlyphDimensions {
origin: lower_left,
@ -194,14 +196,15 @@ impl FontContext {
/// Uses the native Core Graphics library to rasterize a glyph on CPU.
pub fn rasterize_glyph_with_native_rasterizer(&self,
font_instance: &FontInstance,
glyph_key: &GlyphKey)
glyph_key: &GlyphKey,
exact: bool)
-> Result<GlyphImage, ()> {
let core_graphics_font = match self.core_graphics_fonts.get(&font_instance.font_key) {
None => return Err(()),
Some(core_graphics_font) => core_graphics_font,
};
let dimensions = try!(self.glyph_dimensions(font_instance, glyph_key));
let dimensions = try!(self.glyph_dimensions(font_instance, glyph_key, exact));
// TODO(pcwalton): Add support for non-subpixel render modes.
let bitmap_context_flags = kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst;
@ -260,7 +263,14 @@ impl FontContext {
// Draw the glyph, and extract the pixels.
core_graphics_context.show_glyphs_at_positions(&[glyph_key.glyph_index as CGGlyph],
&[origin]);
let pixels = core_graphics_context.data().to_vec();
let mut pixels = core_graphics_context.data().to_vec();
// Swap BGRA to RGBA.
for pixel in pixels.chunks_mut(4) {
let (b, r) = (pixel[0], pixel[2]);
pixel[0] = r;
pixel[2] = b;
}
// Return the image.
Ok(GlyphImage {

View File

@ -103,7 +103,7 @@ impl FontContext {
self.load_glyph(font_instance, glyph_key).ok_or(()).map(|glyph_slot| {
unsafe {
GlyphOutline {
stream: OutlineStream::new(&(*glyph_slot).outline, 72.0),
stream: OutlineStream::new(&(*glyph_slot).outline),
phantom: PhantomData,
}
}
@ -113,7 +113,8 @@ impl FontContext {
/// Uses the FreeType library to rasterize a glyph on CPU.
pub fn rasterize_glyph_with_native_rasterizer(&self,
font_instance: &FontInstance,
glyph_key: &GlyphKey)
glyph_key: &GlyphKey,
_: bool)
-> Result<GlyphImage, ()> {
// Load the glyph.
let slot = match self.load_glyph(font_instance, glyph_key) {
@ -165,19 +166,21 @@ impl FontContext {
let pixel_origin = Point2D::new((*slot).bitmap_left, (*slot).bitmap_top);
// Allocate the RGBA8 buffer.
let area = pixel_size.area() as usize;
let mut dest_pixels: Vec<u32> = vec![0; area];
let src_pixels = slice::from_raw_parts((*bitmap).buffer, area * 3);
let src_stride = (*bitmap).pitch as usize;
let dest_stride = pixel_size.width as usize;
let src_area = src_stride * ((*bitmap).rows as usize);
let dest_area = pixel_size.area() as usize;
let mut dest_pixels: Vec<u32> = vec![0; dest_area];
let src_pixels = slice::from_raw_parts((*bitmap).buffer, src_area);
// Convert to RGBA8.
let pixel_width = pixel_size.width as usize;
for y in 0..(pixel_size.height as usize) {
let dest_row = &mut dest_pixels[(y * pixel_width)..((y + 1) * pixel_width)];
let src_row = &src_pixels[(y * pixel_width * 3)..((y + 1) * pixel_width * 3)];
let dest_row = &mut dest_pixels[(y * dest_stride)..((y + 1) * dest_stride)];
let src_row = &src_pixels[(y * src_stride)..((y + 1) * src_stride)];
for (x, dest) in dest_row.iter_mut().enumerate() {
*dest = (src_row[x * 3 + 2] as u32) |
((src_row[x * 3 + 1] as u32) << 8) |
((src_row[x * 3 + 0] as u32) << 16) |
*dest = ((255 - src_row[x * 3 + 2]) as u32) |
(((255 - src_row[x * 3 + 1]) as u32) << 8) |
(((255 - src_row[x * 3 + 0]) as u32) << 16) |
(0xff << 24)
}
}
@ -202,7 +205,7 @@ impl FontContext {
};
unsafe {
let point_size = (font_instance.size.to_f64_px() / (DPI as f64)).to_ft_f26dot6();
let point_size = font_instance.size.to_ft_f26dot6();
FT_Set_Char_Size(face.face, point_size, 0, DPI, 0);
if FT_Load_Glyph(face.face, glyph_key.glyph_index as FT_UInt, GLYPH_LOAD_FLAGS) != 0 {

View File

@ -20,19 +20,17 @@ pub struct OutlineStream<'a> {
contour_index: u16,
first_position_of_subpath: Point2D<f32>,
first_point_index_of_contour: bool,
dpi: f32,
}
impl<'a> OutlineStream<'a> {
#[inline]
pub unsafe fn new(outline: &FT_Outline, dpi: f32) -> OutlineStream {
pub unsafe fn new(outline: &FT_Outline) -> OutlineStream {
OutlineStream {
outline: outline,
point_index: 0,
contour_index: 0,
first_position_of_subpath: Point2D::zero(),
first_point_index_of_contour: true,
dpi: dpi,
}
}
@ -42,7 +40,7 @@ impl<'a> OutlineStream<'a> {
let point_offset = self.point_index as isize;
let position = ft_vector_to_f32(*self.outline.points.offset(point_offset));
let tag = *self.outline.tags.offset(point_offset);
(position * self.dpi, tag)
(position, tag)
}
}
}

View File

@ -24,10 +24,10 @@ extern crate serde_derive;
#[cfg(test)]
extern crate env_logger;
#[cfg(all(target_os = "macos", not(feature = "freetype")))]
#[cfg(target_os = "macos")]
extern crate core_graphics as core_graphics_sys;
#[cfg(all(target_os = "macos", not(feature = "freetype")))]
#[cfg(target_os = "macos")]
extern crate core_text;
#[cfg(any(target_os = "linux", feature = "freetype"))]
@ -57,8 +57,8 @@ pub use directwrite::FontContext;
#[cfg(any(target_os = "linux", feature = "freetype"))]
pub use freetype::FontContext;
#[cfg(all(target_os = "macos", not(feature = "freetype")))]
mod core_graphics;
#[cfg(target_os = "macos")]
pub mod core_graphics;
#[cfg(all(target_os = "windows", not(feature = "freetype")))]
mod directwrite;
#[cfg(any(target_os = "linux", feature = "freetype"))]

View File

@ -0,0 +1,17 @@
Font,Size,Character,AA Mode,Subpixel,Reference Renderer,Expected SSIM
open-sans,32,A,xcaa,1,core-graphics,0.766
open-sans,32,A,xcaa,1,freetype,0.595
open-sans,48,A,xcaa,1,core-graphics,0.952
open-sans,48,A,xcaa,1,freetype,0.774
open-sans,48,P,ssaa,0,core-graphics,0.969
open-sans,48,P,none,0,freetype,0.841
open-sans,48,P,ssaa,0,freetype,0.847
open-sans,48,P,none,0,core-graphics,0.926
eb-garamond,64,G,xcaa,1,core-graphics,0.705
eb-garamond,36,J,xcaa,0,core-graphics,0.462
eb-garamond,40,J,ssaa,0,core-graphics,0.399
eb-garamond,40,J,ssaa,0,freetype,0.321
eb-garamond,40,J,none,0,freetype,0.318
eb-garamond,24,J,xcaa,1,core-graphics,0.523
nimbus-sans,48,X,xcaa,1,freetype,0.725
nimbus-sans,48,X,xcaa,1,core-graphics,0.968
1 Font Size Character AA Mode Subpixel Reference Renderer Expected SSIM
2 open-sans 32 A xcaa 1 core-graphics 0.766
3 open-sans 32 A xcaa 1 freetype 0.595
4 open-sans 48 A xcaa 1 core-graphics 0.952
5 open-sans 48 A xcaa 1 freetype 0.774
6 open-sans 48 P ssaa 0 core-graphics 0.969
7 open-sans 48 P none 0 freetype 0.841
8 open-sans 48 P ssaa 0 freetype 0.847
9 open-sans 48 P none 0 core-graphics 0.926
10 eb-garamond 64 G xcaa 1 core-graphics 0.705
11 eb-garamond 36 J xcaa 0 core-graphics 0.462
12 eb-garamond 40 J ssaa 0 core-graphics 0.399
13 eb-garamond 40 J ssaa 0 freetype 0.321
14 eb-garamond 40 J none 0 freetype 0.318
15 eb-garamond 24 J xcaa 1 core-graphics 0.523
16 nimbus-sans 48 X xcaa 1 freetype 0.725
17 nimbus-sans 48 X xcaa 1 core-graphics 0.968

View File

@ -16,7 +16,7 @@ uniform ivec2 uSourceDimensions;
varying vec2 vTexCoord;
float sampleSource(float deltaX) {
return texture2D(uSource, vec2(vTexCoord.s + deltaX, vTexCoord.y)).r;
return texture2D(uSource, vec2(vTexCoord.x + deltaX, vTexCoord.y)).r;
}
void main() {
@ -34,5 +34,5 @@ void main() {
lcdFilter(shadeL.y, shadeL.x, shade0, shadeR.x, shadeR.y),
lcdFilter(shadeL.x, shade0, shadeR.x, shadeR.y, shadeR.z));
gl_FragColor = vec4(color, any(greaterThan(color, vec3(0.0))) ? 1.0 : 0.0);
gl_FragColor = vec4(color, 1.0);
}

View File

@ -1,24 +0,0 @@
// pathfinder/shaders/gles2/ssaa-subpixel-resolve.vs.glsl
//
// Copyright (c) 2017 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
precision mediump float;
uniform mat4 uTransform;
uniform vec2 uTexScale;
attribute vec2 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = uTransform * vec4(aPosition, 0.0, 1.0);
vTexCoord = aTexCoord * uTexScale;
}