Skip to content

Commit 602fc3a

Browse files
committed
WIP FXL/Divina improvements
1 parent a61bc3e commit 602fc3a

File tree

5 files changed

+146
-83
lines changed

5 files changed

+146
-83
lines changed

navigator/src/epub/frame/FrameBlobBuilder.ts

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MediaType } from "@readium/shared";
1+
import { MediaType, Resource } from "@readium/shared";
22
import { Link, Publication } from "@readium/shared";
33
import { Injector } from "../../injection/Injector";
44

@@ -20,73 +20,126 @@ const csp = (domains: string[]) => {
2020
].join("; ");
2121
};
2222

23-
export default class FrameBlobBuider {
24-
private readonly item: Link;
25-
private readonly burl: string;
26-
private readonly pub: Publication;
23+
export default class FrameBlobBuilder {
2724
private readonly cssProperties?: { [key: string]: string };
2825
private readonly injector: Injector | null = null;
2926

27+
private currentUrl?: string;
28+
private currentResource?: Resource;
29+
3030
constructor(
31-
pub: Publication,
32-
baseURL: string,
33-
item: Link,
31+
private readonly pub: Publication,
32+
private readonly baseURL: string,
33+
private readonly item: Link,
3434
options: {
3535
cssProperties?: { [key: string]: string };
3636
injector?: Injector | null;
3737
}
3838
) {
39-
this.pub = pub;
4039
this.item = item;
41-
this.burl = item.toURL(baseURL) || "";
4240
this.cssProperties = options.cssProperties;
4341
this.injector = options.injector ?? null;
4442
}
4543

44+
public reset() {
45+
this.currentUrl && URL.revokeObjectURL(this.currentUrl);
46+
this.currentUrl = undefined;
47+
this.currentResource?.close();
48+
this.currentResource = undefined;
49+
}
50+
4651
public async build(fxl = false): Promise<string> {
47-
if(!this.item.mediaType.isHTML) {
48-
if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
49-
return this.buildImageFrame();
52+
if(this.currentUrl) return this.currentUrl;
53+
54+
this.currentResource = this.pub.get(this.item);
55+
const link = await this.currentResource.link();
56+
if(!this.currentResource) {
57+
// Reset has occured in the meantime
58+
return "about:blank";
59+
}
60+
if(!link.mediaType.isHTML) {
61+
if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
62+
const blobUrl = await this.buildImageFrame();
63+
this.currentUrl = blobUrl;
64+
return blobUrl;
5065
} else
51-
throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
66+
throw Error("Unsupported frame mediatype " + link.mediaType.string);
5267
} else {
53-
return await this.buildHtmlFrame(fxl);
68+
const blobUrl = await this.buildHtmlFrame(fxl);
69+
this.currentUrl = blobUrl;
70+
return blobUrl;
5471
}
5572
}
5673

5774
private async buildHtmlFrame(fxl = false): Promise<string> {
75+
if(!this.currentResource) throw new Error("No resource loaded");
76+
5877
// Load the HTML resource
59-
const txt = await this.pub.get(this.item).readAsString();
60-
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
78+
const link = await this.currentResource.link();
79+
const txt = await this.currentResource.readAsString();
80+
if(!txt) throw new Error(`Failed reading item ${link.href}`);
6181

6282
const doc = new DOMParser().parseFromString(
6383
txt,
64-
this.item.mediaType.string as DOMParserSupportedType
84+
link.mediaType.string as DOMParserSupportedType
6585
);
66-
86+
6787
const perror = doc.querySelector("parsererror");
6888
if (perror) {
6989
const details = perror.querySelector("div");
70-
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
90+
throw new Error(`Failed parsing item ${link.href}: ${details?.textContent || perror.textContent}`);
7191
}
7292

7393
// Apply resource injections if injection service is provided
7494
if (this.injector) {
75-
await this.injector.injectForDocument(doc, this.item);
95+
await this.injector.injectForDocument(doc, link);
7696
}
7797

78-
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
98+
return this.finalizeDOM(doc, this.pub.baseURL, link.toURL(this.baseURL) || "", link.mediaType, fxl, this.cssProperties);
7999
}
80100

81-
private buildImageFrame(): string {
82-
// Rudimentary image display
83-
const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
101+
private async buildImageFrame(): Promise<string> {
102+
if(!this.currentResource) throw new Error("No resource loaded");
103+
const link = await this.currentResource.link();
104+
const burl = link.toURL(this.baseURL) || ""
105+
106+
// Rudimentary image display in an HTML doc
107+
const doc = document.implementation.createHTMLDocument(link.title || link.href);
108+
109+
// Add viewport if available
110+
if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
111+
const viewportMeta = doc.createElement("meta");
112+
viewportMeta.name = "viewport";
113+
viewportMeta.content = `width=${link.width}, height=${link.height}`;
114+
viewportMeta.dataset.readium = "true";
115+
doc.head.appendChild(viewportMeta);
116+
}
117+
84118
const simg = document.createElement("img");
85-
simg.src = this.burl || "";
86-
simg.alt = this.item.title || "";
119+
simg.src = burl || "";
120+
simg.alt = link.title || "";
87121
simg.decoding = "async";
88122
doc.body.appendChild(simg);
89-
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
123+
124+
// Apply resource injections if injection service is provided
125+
if (this.injector) {
126+
await this.injector.injectForDocument(doc, new Link({
127+
// Temporary solution to address injector only expecting (X)HTML
128+
// documents for injection, which we are technically providing
129+
href: "readium-image-frame.xhtml",
130+
type: MediaType.XHTML.string
131+
}));
132+
}
133+
134+
// Add image style
135+
const sstyle = doc.createElement("style");
136+
sstyle.dataset.readium = "true";
137+
sstyle.textContent = `
138+
html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
139+
img { margin: 0; padding: 0; border: 0; }`;
140+
doc.head.appendChild(sstyle);
141+
142+
return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true);
90143
}
91144

92145
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
@@ -101,7 +154,10 @@ export default class FrameBlobBuider {
101154

102155
// Get allowed domains from injector if it exists
103156
const allowedDomains = this.injector?.getAllowedDomains?.() || [];
104-
157+
158+
// Remove query from root if present, as CSP doesn't allow them
159+
root = root?.split("?")[0];
160+
105161
// Always include the root domain if provided
106162
const domains = [...new Set([
107163
...(root ? [root] : []),
@@ -124,6 +180,7 @@ export default class FrameBlobBuider {
124180
// loaded in parallel, greatly increasing overall speed.
125181
doc.body.querySelectorAll("img").forEach((img) => {
126182
img.setAttribute("fetchpriority", "high");
183+
img.setAttribute("referrerpolicy", "origin");
127184
});
128185

129186
// We need to ensure that lang is set on the root element

navigator/src/epub/frame/FramePoolManager.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ModuleName } from "@readium/navigator-html-injectables";
22
import { Locator, Publication } from "@readium/shared";
3-
import FrameBlobBuider from "./FrameBlobBuilder";
3+
import FrameBlobBuilder from "./FrameBlobBuilder";
44
import { FrameManager } from "./FrameManager";
55
import { Injector } from "../../injection/Injector";
66
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator";
@@ -14,7 +14,7 @@ export class FramePoolManager {
1414
private _currentFrame: FrameManager | undefined;
1515
private currentCssProperties: { [key: string]: string } | undefined;
1616
private readonly pool: Map<string, FrameManager> = new Map();
17-
private readonly blobs: Map<string, string> = new Map();
17+
private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
1818
private readonly inprogress: Map<string, Promise<void>> = new Map();
1919
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
2020
private currentBaseURL: string | undefined;
@@ -62,10 +62,8 @@ export class FramePoolManager {
6262
this.pool.clear();
6363

6464
// Revoke all blobs
65-
this.blobs.forEach(v => {
66-
this.injector?.releaseBlobUrl?.(v);
67-
URL.revokeObjectURL(v);
68-
});
65+
this.blobs.forEach(v => v.reset());
66+
this.blobs.clear();
6967

7068
// Clean up injector if it exists
7169
this.injector?.dispose();
@@ -106,15 +104,18 @@ export class FramePoolManager {
106104
this.pool.delete(href);
107105
if(this.pendingUpdates.has(href))
108106
this.pendingUpdates.set(href, { inPool: false });
107+
// Note that we don't reset the blob here, unlike in the FXL pool.
108+
// This is because FXL tends to have a ton more blobs. Maybe we'll adjust
109+
// this at a later point with a much larger boundary for resets to deal
110+
// with extremely long/large reflowable publications.
111+
// Reflowable publication resources also tend to be much larger documents,
112+
// so they're more expensive to preprocess with the FrameBlobBuilder.
109113
});
110114

111115
// Check if base URL of publication has changed
112116
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
113117
// Revoke all blobs
114-
this.blobs.forEach(v => {
115-
this.injector?.releaseBlobUrl?.(v);
116-
URL.revokeObjectURL(v);
117-
});
118+
this.blobs.forEach(v => v.reset());
118119
this.blobs.clear();
119120
}
120121
this.currentBaseURL = pub.baseURL;
@@ -127,18 +128,14 @@ export class FramePoolManager {
127128
// when navigating backwards, where paginated will go the
128129
// start of the resource instead of the end due to the
129130
// corrupted width ColumnSnapper (injectables) gets on init
130-
this.blobs.forEach(v => {
131-
this.injector?.releaseBlobUrl?.(v);
132-
URL.revokeObjectURL(v);
133-
});
131+
this.blobs.forEach(v => v.reset());
134132
this.blobs.clear();
135133
this.pendingUpdates.clear();
136134
}
137135
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
138-
const url = this.blobs.get(href);
139-
if(url) {
140-
this.injector?.releaseBlobUrl?.(url);
141-
URL.revokeObjectURL(url);
136+
const v = this.blobs.get(href);
137+
if(v) {
138+
v.reset();
142139
this.blobs.delete(href);
143140
this.pendingUpdates.delete(href);
144141
}
@@ -157,21 +154,19 @@ export class FramePoolManager {
157154
const itm = pub.readingOrder.findWithHref(href);
158155
if(!itm) return; // TODO throw?
159156
if(!this.blobs.has(href)) {
160-
const blobBuilder = new FrameBlobBuider(
157+
this.blobs.set(href, new FrameBlobBuilder(
161158
pub,
162159
this.currentBaseURL || "",
163160
itm,
164161
{
165162
cssProperties: this.currentCssProperties,
166163
injector: this.injector
167164
}
168-
);
169-
const blobURL = await blobBuilder.build();
170-
this.blobs.set(href, blobURL);
165+
));
171166
}
172167

173168
// Create <iframe>
174-
const fm = new FrameManager(this.blobs.get(href)!, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
169+
const fm = new FrameManager(await this.blobs.get(href)!.build(), this.contentProtectionConfig, this.keyboardPeripheralsConfig);
175170
if(href !== newHref) await fm.hide(); // Avoid unecessary hide
176171
this.container.appendChild(fm.iframe);
177172
await fm.load(modules);

navigator/src/epub/fxl/FXLFrameManager.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navi
77

88
export class FXLFrameManager {
99
private frame: HTMLIFrameElement;
10+
private frameIsAppended = false;
1011
private loader: Loader | undefined;
1112
public source: string;
1213
private comms: FrameComms | undefined;
@@ -20,6 +21,7 @@ export class FXLFrameManager {
2021
public debugHref: string;
2122
private loadPromise: Promise<Window> | undefined;
2223
private showPromise: Promise<void> | undefined;
24+
private viewportSize: { width: number, height: number } | undefined = undefined;
2325

2426
constructor(
2527
peripherals: FXLPeripherals,
@@ -54,11 +56,13 @@ export class FXLFrameManager {
5456
this.wrapper = document.createElement("div");
5557
this.wrapper.style.position = "relative";
5658
this.wrapper.style.float = this.wrapper.style.cssFloat = direction === ReadingProgression.rtl ? "right" : "left";
57-
58-
this.wrapper.appendChild(this.frame);
5959
}
6060

6161
async load(modules: ModuleName[], source: string): Promise<Window> {
62+
if(!this.frameIsAppended) {
63+
this.wrapper.appendChild(this.frame);
64+
this.frameIsAppended = true;
65+
}
6266
if(this.source === source && this.loadPromise/* && this.loaded*/) {
6367
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
6468
return this.loadPromise;
@@ -104,6 +108,7 @@ export class FXLFrameManager {
104108

105109
// Parses the page size from the viewport meta tag of the loaded resource.
106110
loadPageSize(): { width: number, height: number } {
111+
if(this.viewportSize) return this.viewportSize;
107112
const wnd = this.frame.contentWindow!;
108113

109114
// Try to get the page size from the viewport meta tag
@@ -120,8 +125,10 @@ export class FXLFrameManager {
120125
else if(match[1] === "height")
121126
height = Number.parseFloat(match[2]);
122127
}
123-
if(width > 0 && height > 0)
124-
return { width, height };
128+
if(width > 0 && height > 0) {
129+
this.viewportSize = { width, height };
130+
return this.viewportSize;
131+
}
125132
}
126133

127134
// Otherwise get it from the size of the loaded content

0 commit comments

Comments
 (0)