Flesh out the integration test more
This commit is contained in:
parent
8c1b3e9cb5
commit
86df78f939
|
@ -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;
|
||||
|
|
|
@ -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×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">—</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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>):
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue