buninu-browser 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -1
- package/lib/cdp-server.js +47 -14
- package/lib/clipboard.js +22 -0
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +40 -11
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +77 -16
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/raw-frames.js +47 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +34 -40
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +133 -98
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +52 -5
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +287 -99
- package/lib/renderer/page-renderer.js +663 -239
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/clipboard.test.js +35 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/raw-capture.test.js +152 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* screenshots and scrolling on its own channel, so the page stays visible and
|
|
7
7
|
* scrollable while the main thread works.
|
|
8
8
|
*
|
|
9
|
-
* Channel to the controller:
|
|
10
|
-
*
|
|
9
|
+
* Channel to the controller: authenticated loopback socket, with length-prefixed
|
|
10
|
+
* JSON and optional binary pixels (does not go through main-thread IPC):
|
|
11
11
|
* controller -> compositor { id, method: "screenshot" | "scroll" | "scrollOffset", params }
|
|
12
12
|
* (screenshot params: [{ format?: "png" | "jpeg" | "webp", quality? }])
|
|
13
13
|
* compositor -> controller { id, result } | { id, error }
|
|
@@ -22,36 +22,45 @@
|
|
|
22
22
|
* a fragment link, a key press or a new document.
|
|
23
23
|
*/
|
|
24
24
|
import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
25
|
-
import {
|
|
25
|
+
import { rawFrame, captureViewport, encodeImage, encodeImageBase64 } from "../paint/frame.js";
|
|
26
|
+
import { FrameCaptureCache } from "../paint/frame-cache.js";
|
|
27
|
+
import { FrameLeaseTracker } from "../paint/shared-frame-pool.js";
|
|
26
28
|
import wasmpeg from "../../vendor/wasmpeg/src/js/index.js";
|
|
27
29
|
import { VideoPresentation, compositeVideo } from "./video-presentation.js";
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
const MAX_LINE_BYTES = 1024 * 1024;
|
|
31
|
+
import {connectCompositor} from './compositor-channel.js';
|
|
31
32
|
|
|
32
33
|
const viewport = new ScrollViewport(800, 600);
|
|
33
34
|
let tile = null;
|
|
34
35
|
let scrollEpoch = -1;
|
|
35
36
|
let frameSerial = 0;
|
|
36
|
-
|
|
37
|
+
const captures = new FrameCaptureCache();
|
|
38
|
+
const frameLeases = new FrameLeaseTracker(serial => postMessage({ type: "frame-release", serial }));
|
|
37
39
|
const video = new VideoPresentation(wasmpeg, () => {
|
|
38
40
|
frameSerial++;
|
|
39
41
|
send({ type: "frame" });
|
|
40
42
|
}, report => postMessage({ type: "video-report", ...report }));
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
let channel;
|
|
44
45
|
function send(message) {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const result = message.result;
|
|
47
|
+
if (result?.pixels instanceof Uint8Array) {
|
|
48
|
+
const {pixels,...metadata} = result;
|
|
49
|
+
channel.send({...message,result:metadata},pixels);
|
|
50
|
+
} else channel.send(message);
|
|
47
51
|
}
|
|
52
|
+
const endpoint = await new Promise(resolve => {
|
|
53
|
+
self.onmessage = ({data}) => { if (data?.type === 'connect') resolve(data.endpoint); };
|
|
54
|
+
});
|
|
55
|
+
channel = await connectCompositor(endpoint, request => { void handleRequest(request); }, () => process.exit(1));
|
|
48
56
|
|
|
49
57
|
self.onmessage = ({ data }) => {
|
|
50
58
|
if (data?.type === "video-open") { void video.open(data); return; }
|
|
51
59
|
if (data?.type === "video-stop") { video.stop(data.id); frameSerial++; return; }
|
|
52
|
-
if (data?.type === "video-bind") { video.bind(data.id, data.audioId, data.audioStart, data.time); return; }
|
|
60
|
+
if (data?.type === "video-bind") { video.bind(data.id, data.audioId, data.audioStart, data.time, data.endTime); return; }
|
|
53
61
|
if (data?.type !== "frame") return;
|
|
54
62
|
tile = data.tile;
|
|
63
|
+
frameLeases.replace(data.serial, tile);
|
|
55
64
|
frameSerial++;
|
|
56
65
|
viewport.resize(data.viewport.width, data.viewport.height);
|
|
57
66
|
viewport.setContentSize(data.content.width, data.content.height);
|
|
@@ -73,16 +82,21 @@ self.onmessage = ({ data }) => {
|
|
|
73
82
|
};
|
|
74
83
|
|
|
75
84
|
const methods = {
|
|
76
|
-
async screenshot({ format = "png", quality = 80 } = {}) {
|
|
85
|
+
async screenshot({ format = "png", quality = 80, optimizeForSpeed = false, clip } = {}) {
|
|
77
86
|
if (!tile) return "";
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
if (format === 'rgba' || format === 'bgra') {
|
|
88
|
+
return frameLeases.capture(frame => {
|
|
89
|
+
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame) : frame;
|
|
90
|
+
const {pixels,width,height} = captureViewport(composited,viewport.x,viewport.y,viewport.width,viewport.height,clip);
|
|
91
|
+
return rawFrame(pixels,width,height,format);
|
|
92
|
+
});
|
|
84
93
|
}
|
|
85
|
-
|
|
94
|
+
const key = `${frameSerial}:${viewport.x}:${viewport.y}:${format}:${quality}:${optimizeForSpeed}:${JSON.stringify(clip)}`;
|
|
95
|
+
return captures.get(key, () => frameLeases.capture(frame => {
|
|
96
|
+
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame) : frame;
|
|
97
|
+
const { pixels, width, height } = captureViewport(composited, viewport.x, viewport.y, viewport.width, viewport.height,clip);
|
|
98
|
+
return encodeImageBase64(pixels, width, height, { format, quality, optimizeForSpeed });
|
|
99
|
+
}));
|
|
86
100
|
},
|
|
87
101
|
scroll(deltaX, deltaY) {
|
|
88
102
|
if (viewport.scrollBy(deltaX, deltaY)) {
|
|
@@ -96,13 +110,7 @@ const methods = {
|
|
|
96
110
|
},
|
|
97
111
|
};
|
|
98
112
|
|
|
99
|
-
async function
|
|
100
|
-
let request;
|
|
101
|
-
try {
|
|
102
|
-
request = JSON.parse(line);
|
|
103
|
-
} catch {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
113
|
+
async function handleRequest(request) {
|
|
106
114
|
if (request?.type === "audio-timing" && Number.isInteger(request.audioId)) {
|
|
107
115
|
video.timing(request.audioId, request.seconds);
|
|
108
116
|
return;
|
|
@@ -116,20 +124,6 @@ async function handleLine(line) {
|
|
|
116
124
|
}
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
(async () => {
|
|
120
|
-
const decoder = new TextDecoder();
|
|
121
|
-
let pending = "";
|
|
122
|
-
for await (const chunk of Bun.file(CHANNEL_FD).stream()) {
|
|
123
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
124
|
-
let newline;
|
|
125
|
-
while ((newline = pending.indexOf("\n")) >= 0) {
|
|
126
|
-
handleLine(pending.slice(0, newline));
|
|
127
|
-
pending = pending.slice(newline + 1);
|
|
128
|
-
}
|
|
129
|
-
if (pending.length > MAX_LINE_BYTES) pending = "";
|
|
130
|
-
}
|
|
131
|
-
})();
|
|
132
|
-
|
|
133
127
|
// Bun.Image initialises its codecs on first use; do that before the
|
|
134
128
|
// renderer installs seccomp, which forbids opening files.
|
|
135
129
|
await encodeImage(new Uint8Array(4), 1, 1);
|
|
@@ -5,14 +5,38 @@ export function installAttributeReactions(Adapter, constructor, realm, callPage)
|
|
|
5
5
|
const callback = constructor.prototype.attributeChangedCallback;
|
|
6
6
|
if (typeof callback !== "function") return;
|
|
7
7
|
const observed = constructor.observedAttributes;
|
|
8
|
+
const pending = new WeakMap();
|
|
8
9
|
Object.defineProperty(Adapter, "observedAttributes", {
|
|
9
10
|
value: observed === undefined ? [] : Array.from(observed, String),
|
|
10
11
|
});
|
|
11
12
|
Object.defineProperty(Adapter.prototype, "attributeChangedCallback", {
|
|
12
13
|
value(name, oldValue, newValue, namespace = null) {
|
|
13
|
-
|
|
14
|
+
const args = [name, oldValue, newValue, namespace];
|
|
15
|
+
const reactions = pending.get(this);
|
|
16
|
+
if (reactions) reactions.push(args);
|
|
17
|
+
else callPage(callback, realm.wrap(this), args);
|
|
14
18
|
},
|
|
15
19
|
configurable: true,
|
|
16
20
|
writable: true,
|
|
17
21
|
});
|
|
22
|
+
const observedNames = new Set(Adapter.observedAttributes);
|
|
23
|
+
// The host DOM upgrades existing elements in place, but does not enqueue
|
|
24
|
+
// their initial attribute callbacks. Capture values before construction:
|
|
25
|
+
// those reactions run after the constructor and before connectedCallback.
|
|
26
|
+
return (element) => {
|
|
27
|
+
const reactions = Array.from(element.attributes)
|
|
28
|
+
.filter(attribute => observedNames.has(attribute.localName))
|
|
29
|
+
.map(attribute => [attribute.localName, null, attribute.value, attribute.namespaceURI]);
|
|
30
|
+
pending.set(element, reactions);
|
|
31
|
+
return {
|
|
32
|
+
finish() {
|
|
33
|
+
try {
|
|
34
|
+
// Attribute writes by the constructor or earlier callbacks append
|
|
35
|
+
// to this element's queue, after the captured initial reactions.
|
|
36
|
+
for (let index = 0; index < reactions.length; index++) callPage(callback, realm.wrap(element), reactions[index]);
|
|
37
|
+
} finally { pending.delete(element); }
|
|
38
|
+
},
|
|
39
|
+
cancel() { pending.delete(element); },
|
|
40
|
+
};
|
|
41
|
+
};
|
|
18
42
|
}
|
package/lib/renderer/flags.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Command-line flags the controller passes to renderer processes. */
|
|
2
2
|
import { CHROMIUM_VERSION } from "../user-agent.js";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Start the compositor Worker with its pre-sandbox loopback channel. */
|
|
5
5
|
export const COMPOSITOR_FLAG = "--compositor";
|
|
6
6
|
|
|
7
7
|
/** BUNINU_VERIFY_STYLE: check every incremental restyle against a full one (development). */
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Web fonts belong to the document. Built-in typefaces are shared native
|
|
2
|
+
// references: registering their byte buffers again would copy tens of MB for
|
|
3
|
+
// every document and iframe.
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export function registerTypeface(CanvasKit, provider, typeface, family) {
|
|
8
|
+
const bytes = encoder.encode(`${family}\0`);
|
|
9
|
+
const memory = CanvasKit.Malloc(Uint8Array, bytes.length);
|
|
10
|
+
try {
|
|
11
|
+
memory.toTypedArray().set(bytes);
|
|
12
|
+
// This is the entry point the bundled registerFont(bytes, family) uses
|
|
13
|
+
// after creating a Typeface. Skia retains the typeface and copies the name.
|
|
14
|
+
provider._registerFont(typeface, memory.byteOffset);
|
|
15
|
+
} finally {
|
|
16
|
+
CanvasKit.Free(memory);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function styleRank(face, wanted) {
|
|
21
|
+
const order = wanted === "italic" ? ["italic", "oblique", "normal"]
|
|
22
|
+
: wanted === "oblique" ? ["oblique", "italic", "normal"] : ["normal", "oblique", "italic"];
|
|
23
|
+
return order.indexOf(face.style ?? "normal");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** CSS Fonts weight fallback order, including the special 400..500 interval. */
|
|
27
|
+
function weightRank(face, wanted) {
|
|
28
|
+
const [low, high] = face.weight ?? [400, 400];
|
|
29
|
+
if (low <= wanted && wanted <= high) return 0;
|
|
30
|
+
if (wanted >= 400 && wanted <= 500) {
|
|
31
|
+
if (low > wanted && low <= 500) return low - wanted;
|
|
32
|
+
if (high < wanted) return 1000 + wanted - high;
|
|
33
|
+
return 2000 + low - wanted;
|
|
34
|
+
}
|
|
35
|
+
if (wanted < 400) return high < wanted ? wanted - high : 1000 + low - wanted;
|
|
36
|
+
return low > wanted ? low - wanted : 1000 + wanted - high;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function covers(face, point) {
|
|
40
|
+
return (face.unicodeRanges ?? [[0, 0x10ffff]]).some(([from, to]) => from <= point && point <= to);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stretchRank(face, wanted) {
|
|
44
|
+
const [low, high] = face.stretch ?? [100, 100];
|
|
45
|
+
if (low <= wanted && wanted <= high) return 0;
|
|
46
|
+
if (wanted <= 100) return high < wanted ? wanted - high : 1000 + low - wanted;
|
|
47
|
+
return low > wanted ? low - wanted : 1000 + wanted - high;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ranked(faces, weight, style, stretch = 100) {
|
|
51
|
+
return faces.toReversed().sort((left, right) => stretchRank(left, stretch) - stretchRank(right, stretch)
|
|
52
|
+
|| styleRank(left, style) - styleRank(right, style)
|
|
53
|
+
|| weightRank(left, weight) - weightRank(right, weight));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Only request faces needed by text which actually participates in layout. */
|
|
57
|
+
export function neededFontFaces(faces, textRuns, availableFamilies = new Set()) {
|
|
58
|
+
const families = new Map();
|
|
59
|
+
for (const face of faces) {
|
|
60
|
+
const key = face.family.toLowerCase();
|
|
61
|
+
if (!families.has(key)) families.set(key, []);
|
|
62
|
+
families.get(key).push(face);
|
|
63
|
+
}
|
|
64
|
+
const needed = new Set();
|
|
65
|
+
const choices = new Map();
|
|
66
|
+
for (const { text, style } of textRuns) {
|
|
67
|
+
if (!text || !style) continue;
|
|
68
|
+
const points = new Set([...text].map(character => character.codePointAt(0)));
|
|
69
|
+
for (const family of style.fontFamily ?? []) {
|
|
70
|
+
const definitions = families.get(family.toLowerCase());
|
|
71
|
+
if (!definitions) {
|
|
72
|
+
if (availableFamilies.has(family.toLowerCase())) break;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const key = `${family.toLowerCase()}\n${style.fontWeight ?? 400}/${style.fontStyle ?? "normal"}/${style.fontStretch ?? 100}`;
|
|
76
|
+
let ordered = choices.get(key);
|
|
77
|
+
if (!ordered) choices.set(key, ordered = ranked(definitions, style.fontWeight ?? 400, style.fontStyle ?? "normal", style.fontStretch ?? 100));
|
|
78
|
+
for (const point of points) {
|
|
79
|
+
const face = ordered.find(face => covers(face, point));
|
|
80
|
+
if (face) { needed.add(face); points.delete(point); }
|
|
81
|
+
}
|
|
82
|
+
if (!points.size) break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return needed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class DocumentFonts {
|
|
89
|
+
#CanvasKit;
|
|
90
|
+
#builtins;
|
|
91
|
+
#defaultCollection;
|
|
92
|
+
#fetch;
|
|
93
|
+
#changed;
|
|
94
|
+
#error;
|
|
95
|
+
#provider = null;
|
|
96
|
+
#collection = null;
|
|
97
|
+
#faces = [];
|
|
98
|
+
#records = new Map();
|
|
99
|
+
#sources = new Map();
|
|
100
|
+
#pending = new Set();
|
|
101
|
+
#requests = new Set();
|
|
102
|
+
#closed = false;
|
|
103
|
+
#nextAlias = 1;
|
|
104
|
+
#availableFamilies;
|
|
105
|
+
#hasUnicodeRanges = false;
|
|
106
|
+
|
|
107
|
+
constructor({ CanvasKit, builtins, defaultCollection, fetch, availableFamilies = new Set(), changed = () => {}, error = () => {} }) {
|
|
108
|
+
this.#CanvasKit = CanvasKit;
|
|
109
|
+
this.#builtins = builtins;
|
|
110
|
+
this.#defaultCollection = defaultCollection;
|
|
111
|
+
this.#fetch = fetch;
|
|
112
|
+
this.#changed = changed;
|
|
113
|
+
this.#error = error;
|
|
114
|
+
this.#availableFamilies = availableFamilies;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get collection() { return this.#collection ?? this.#defaultCollection; }
|
|
118
|
+
|
|
119
|
+
get hasUnicodeRanges() { return this.#hasUnicodeRanges; }
|
|
120
|
+
|
|
121
|
+
setFaces(faces) {
|
|
122
|
+
this.#faces = faces;
|
|
123
|
+
this.#hasUnicodeRanges = faces.some(face => face.unicodeRanges?.length
|
|
124
|
+
&& !(face.unicodeRanges.length === 1 && face.unicodeRanges[0][0] === 0 && face.unicodeRanges[0][1] === 0x10ffff));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
update(faces, textRuns) {
|
|
128
|
+
if (this.#closed) return;
|
|
129
|
+
this.setFaces(faces);
|
|
130
|
+
for (const face of neededFontFaces(faces, textRuns, this.#availableFamilies)) {
|
|
131
|
+
const key = JSON.stringify(face);
|
|
132
|
+
if (this.#records.has(key)) continue;
|
|
133
|
+
const record = { face, alias: null };
|
|
134
|
+
this.#records.set(key, record);
|
|
135
|
+
const pending = this.#load(record).catch(error => {
|
|
136
|
+
if (!this.#closed) this.#error(error);
|
|
137
|
+
}).finally(() => this.#pending.delete(pending));
|
|
138
|
+
this.#pending.add(pending);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Available faces in CSS fallback order; unicode-subset files share a family. */
|
|
143
|
+
resolve(family, weight = 400, style = "normal", point = null, stretch = 100) {
|
|
144
|
+
const definitions = this.#faces.filter(face => face.family.toLowerCase() === family.toLowerCase()
|
|
145
|
+
&& (point === null || covers(face, point)));
|
|
146
|
+
return ranked(definitions, weight, style, stretch).map(face => this.#records.get(JSON.stringify(face))?.alias).filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async ready() {
|
|
150
|
+
while (!this.#closed && this.#pending.size) await Promise.all([...this.#pending]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#ensureProvider() {
|
|
154
|
+
if (this.#provider) return;
|
|
155
|
+
const kit = this.#CanvasKit;
|
|
156
|
+
this.#provider = kit.TypefaceFontProvider.Make();
|
|
157
|
+
for (const { family, typeface } of this.#builtins) registerTypeface(kit, this.#provider, typeface, family);
|
|
158
|
+
this.#collection = kit.FontCollection.Make();
|
|
159
|
+
this.#collection.setDefaultFontManager(this.#provider);
|
|
160
|
+
this.#collection.enableFontFallback();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async #load(record) {
|
|
164
|
+
let failure;
|
|
165
|
+
for (const source of record.face.sources) {
|
|
166
|
+
if (this.#closed) return;
|
|
167
|
+
try {
|
|
168
|
+
let typeface;
|
|
169
|
+
if (source.local) {
|
|
170
|
+
typeface = this.#builtins.find(entry => entry.family.toLowerCase() === source.local.toLowerCase())?.typeface;
|
|
171
|
+
if (!typeface) continue;
|
|
172
|
+
} else {
|
|
173
|
+
if (!source.url || (source.format && !/^(?:woff2?|truetype|opentype)(?:-variations)?$/i.test(source.format))) continue;
|
|
174
|
+
let pending = this.#sources.get(source.url);
|
|
175
|
+
if (!pending) {
|
|
176
|
+
pending = this.#loadSource(source.url);
|
|
177
|
+
this.#sources.set(source.url, pending);
|
|
178
|
+
}
|
|
179
|
+
typeface = await pending;
|
|
180
|
+
if (this.#closed) return;
|
|
181
|
+
}
|
|
182
|
+
this.#ensureProvider();
|
|
183
|
+
const alias = `Buninu Web Font ${this.#nextAlias++}`;
|
|
184
|
+
registerTypeface(this.#CanvasKit, this.#provider, typeface, alias);
|
|
185
|
+
record.alias = alias;
|
|
186
|
+
this.#changed();
|
|
187
|
+
return;
|
|
188
|
+
} catch (error) { failure = error; }
|
|
189
|
+
}
|
|
190
|
+
if (failure) throw failure;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async #loadSource(url) {
|
|
194
|
+
// Fonts use CORS even though images and classic stylesheets normally do
|
|
195
|
+
// not. This goes through the controller's existing page-fetch policy.
|
|
196
|
+
const request = this.#fetch({ url, method: "GET", headers: [], body: null,
|
|
197
|
+
mode: "cors", credentials: "same-origin", redirect: "follow" });
|
|
198
|
+
this.#requests.add(request);
|
|
199
|
+
try {
|
|
200
|
+
const response = await request;
|
|
201
|
+
if (this.#closed) return null;
|
|
202
|
+
if (response.status < 200 || response.status >= 300) throw new Error(`Font HTTP ${response.status}`);
|
|
203
|
+
const bytes = response.body instanceof Uint8Array ? response.body : await Bun.readableStreamToBytes(response.body);
|
|
204
|
+
if (this.#closed) return null;
|
|
205
|
+
if (!bytes.length || bytes.length > MAX_FONT_BYTES) throw new Error("Font size exceeds the renderer limit");
|
|
206
|
+
const typeface = this.#CanvasKit.Typeface.MakeTypefaceFromData(bytes);
|
|
207
|
+
if (!typeface) throw new Error("Font decoding failed");
|
|
208
|
+
return typeface;
|
|
209
|
+
} finally {
|
|
210
|
+
this.#requests.delete(request);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
dispose() {
|
|
215
|
+
if (this.#closed) return;
|
|
216
|
+
this.#closed = true;
|
|
217
|
+
for (const request of this.#requests) request.abort?.();
|
|
218
|
+
this.#requests.clear();
|
|
219
|
+
this.#collection?.delete();
|
|
220
|
+
this.#provider?.delete();
|
|
221
|
+
this.#collection = this.#provider = null;
|
|
222
|
+
for (const source of this.#sources.values()) source.then(typeface => typeface?.delete(), () => {});
|
|
223
|
+
this.#sources.clear();
|
|
224
|
+
this.#records.clear();
|
|
225
|
+
this.#faces = [];
|
|
226
|
+
}
|
|
227
|
+
}
|