diff --git a/navigator-html-injectables/package.json b/navigator-html-injectables/package.json
index b6332de2..c09ca27d 100644
--- a/navigator-html-injectables/package.json
+++ b/navigator-html-injectables/package.json
@@ -1,6 +1,6 @@
{
"name": "@readium/navigator-html-injectables",
- "version": "2.4.2",
+ "version": "2.3.1-alpha.2",
"type": "module",
"description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
"author": "readium",
diff --git a/navigator-html-injectables/src/helpers/css.ts b/navigator-html-injectables/src/helpers/css.ts
index c609bd4e..cbd2cdce 100644
--- a/navigator-html-injectables/src/helpers/css.ts
+++ b/navigator-html-injectables/src/helpers/css.ts
@@ -1,5 +1,7 @@
import { ReadiumWindow } from "./dom.ts";
+export const isTypedOMSupported = () => typeof window.CSSTransformValue !== 'undefined';
+
export function getProperties(wnd: ReadiumWindow) {
const cssProperties: { [key: string]: string } = {};
diff --git a/navigator-html-injectables/src/modules/snapper/ColumnSnapper.ts b/navigator-html-injectables/src/modules/snapper/ColumnSnapper.ts
index 62a10fe0..f392a95e 100644
--- a/navigator-html-injectables/src/modules/snapper/ColumnSnapper.ts
+++ b/navigator-html-injectables/src/modules/snapper/ColumnSnapper.ts
@@ -8,6 +8,7 @@ import { rangeFromLocator } from "../../helpers/locator.ts";
import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom.ts";
import { PatternAnalyzer } from "../../protection/PatternAnalyzer.ts";
import { BaseSuspiciousActivityEvent } from "../Peripherals.ts";
+import { isTypedOMSupported } from "../../helpers/css.ts";
const COLUMN_SNAPPER_STYLE_ID = "readium-column-snapper-style";
@@ -166,7 +167,13 @@ export class ColumnSnapper extends Snapper {
const spos = position(currentScrollLeft, so, elapsed, period);
doc.scrollLeft = spos;
if(this.overscroll !== 0)
- doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
+ if(isTypedOMSupported()) {
+ doc.attributeStyleMap.set("transform", new CSSTransformValue([
+ new CSSTranslate(CSS.px(-lpos), CSS.px(0), CSS.px(0))
+ ]));
+ } else {
+ doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
+ }
if (elapsed < period)
this.wnd.requestAnimationFrame(step);
@@ -282,10 +289,22 @@ export class ColumnSnapper extends Snapper {
if(newpos < minScrollLeft) {
this.overscroll = newpos;
- this.doc().style.transform = `translate3d(${-this.overscroll}px, 0px, 0px)`;
+ if(isTypedOMSupported()) {
+ this.doc().attributeStyleMap.set("transform", new CSSTransformValue([
+ new CSSTranslate(CSS.px(-newpos), CSS.px(0), CSS.px(0))
+ ]));
+ } else {
+ this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
+ }
} else if(newpos > maxScrollLeft) {
this.overscroll = newpos;
- this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
+ if(isTypedOMSupported()) {
+ this.doc().attributeStyleMap.set("transform", new CSSTransformValue([
+ new CSSTranslate(CSS.px(-newpos), CSS.px(0), CSS.px(0))
+ ]));
+ } else {
+ this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
+ }
} else {
this.overscroll = 0;
this.doc().style.removeProperty("transform");
diff --git a/navigator/package.json b/navigator/package.json
index e6fc399b..9fb8a5e0 100644
--- a/navigator/package.json
+++ b/navigator/package.json
@@ -1,6 +1,6 @@
{
"name": "@readium/navigator",
- "version": "2.5.4",
+ "version": "2.4.0-alpha.17",
"type": "module",
"description": "Next generation SDK for publications in Web Apps",
"author": "readium",
diff --git a/navigator/src/epub/EpubNavigator.ts b/navigator/src/epub/EpubNavigator.ts
index 97c92768..3423e059 100644
--- a/navigator/src/epub/EpubNavigator.ts
+++ b/navigator/src/epub/EpubNavigator.ts
@@ -238,11 +238,12 @@ export class EpubNavigator extends VisualNavigator implements Configurable {
+ await this.go(this.currentLocation, false, (s) => {
+ res(s);
+ });
+ });
}
public get settings(): Readonly {
@@ -885,7 +892,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable 0;
if(hasProgression)
done = await new Promise((res, _) => {
@@ -896,7 +903,42 @@ export class EpubNavigator extends VisualNavigator implements Configurable p.locations.position === locator.locations.position);
+ if (match) {
+ locator = match.copyWithLocations(locator.locations);
+ fellback = true;
+ }
+ }
+ if(!fellback && typeof locator.locations?.totalProgression === "number") {
+ // If locator has no href, but it does have a totalProgression,
+ // we can attempt to find the right resource from the positions list.
+ // This is here to help with conversion from OPDS locators which only
+ // require the total progression in the publication.
+ const targetProgression = locator.locations.totalProgression;
+ let closestIdx = 0;
+ let closestDist = Infinity;
+ for (let i = 0; i < this.positions.length; i++) {
+ const pos = this.positions[i];
+ // Use totalProgression if available, otherwise estimate from index
+ const posProg = pos.locations.totalProgression ?? (i / this.positions.length);
+ const dist = Math.abs(posProg - targetProgression);
+ if (dist < closestDist) {
+ closestDist = dist;
+ closestIdx = i;
+ }
+ }
+ locator = this.positions[closestIdx].copyWithLocations(locator.locations);
+ }
+ }
+ return locator;
+ }
+
public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
+ locator = this.completeLocator(locator);
const href = locator.href.split("#")[0];
let link = this.pub.readingOrder.findWithHref(href);
if(!link) {
diff --git a/navigator/src/epub/frame/FrameBlobBuilder.ts b/navigator/src/epub/frame/FrameBlobBuilder.ts
index e05b49cd..704ada34 100644
--- a/navigator/src/epub/frame/FrameBlobBuilder.ts
+++ b/navigator/src/epub/frame/FrameBlobBuilder.ts
@@ -1,4 +1,4 @@
-import { Link, MediaType, Publication, ReadingProgression } from "@readium/shared";
+import { Link, MediaType, Publication, ReadingProgression, Resource } from "@readium/shared";
import { Injector } from "../../injection/Injector.ts";
import { getScriptMode } from "../../helpers/scriptMode.ts";
@@ -20,73 +20,121 @@ const csp = (domains: string[]) => {
].join("; ");
};
-export default class FrameBlobBuider {
- private readonly item: Link;
- private readonly burl: string;
- private readonly pub: Publication;
+export default class FrameBlobBuilder {
private readonly cssProperties?: { [key: string]: string };
private readonly injector: Injector | null = null;
+ private currentUrl?: string;
+ private currentResource?: Resource;
+
constructor(
- pub: Publication,
- baseURL: string,
- item: Link,
+ private readonly pub: Publication,
+ private readonly baseURL: string,
+ private readonly item: Link,
options: {
cssProperties?: { [key: string]: string };
injector?: Injector | null;
}
) {
- this.pub = pub;
this.item = item;
- this.burl = item.toURL(baseURL) || "";
this.cssProperties = options.cssProperties;
this.injector = options.injector ?? null;
}
+ public reset() {
+ this.currentUrl && URL.revokeObjectURL(this.currentUrl);
+ this.currentUrl = undefined;
+ this.currentResource?.close();
+ this.currentResource = undefined;
+ }
+
public async build(fxl = false): Promise {
- if(!this.item.mediaType.isHTML) {
- if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
- return this.buildImageFrame();
+ if(this.currentUrl) return this.currentUrl;
+
+ this.currentResource = this.pub.get(this.item);
+ const link = await this.currentResource.link();
+ if(!this.currentResource) {
+ // Reset has occured in the meantime
+ return "about:blank";
+ }
+ if(!link.mediaType.isHTML) {
+ if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
+ const blobUrl = await this.buildImageFrame();
+ this.currentUrl = blobUrl;
+ return blobUrl;
} else
- throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
+ throw Error("Unsupported frame mediatype " + link.mediaType.string);
} else {
- return await this.buildHtmlFrame(fxl);
+ const blobUrl = await this.buildHtmlFrame(fxl);
+ this.currentUrl = blobUrl;
+ return blobUrl;
}
}
private async buildHtmlFrame(fxl = false): Promise {
- // Load the HTML resource
- const txt = await this.pub.get(this.item).readAsString();
- if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
+ if(!this.currentResource) throw new Error("No resource loaded");
- const doc = new DOMParser().parseFromString(
- txt,
- this.item.mediaType.string as DOMParserSupportedType
- );
+ // Load the HTML resource
+ const link = await this.currentResource.link();
+ const doc = await this.currentResource.readAsXML() as HTMLDocument;
+ if(!doc) throw new Error(`Failed reading item ${link.href}`);
const perror = doc.querySelector("parsererror");
if (perror) {
const details = perror.querySelector("div");
- throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
+ throw new Error(`Failed parsing item ${link.href}: ${details?.textContent || perror.textContent}`);
}
// Apply resource injections if injection service is provided
if (this.injector) {
- await this.injector.injectForDocument(doc, this.item);
+ await this.injector.injectForDocument(doc, link);
}
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
+ return this.finalizeDOM(doc, this.pub.baseURL, link.toURL(this.baseURL) || "", link.mediaType, fxl, this.cssProperties);
}
- private buildImageFrame(): string {
- // Rudimentary image display
- const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
+ private async buildImageFrame(): Promise {
+ if(!this.currentResource) throw new Error("No resource loaded");
+ const link = await this.currentResource.link();
+ const burl = link.toURL(this.baseURL) || ""
+
+ // Rudimentary image display in an HTML doc
+ const doc = document.implementation.createHTMLDocument(link.title || link.href);
+
+ // Add viewport if available
+ if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
+ const viewportMeta = doc.createElement("meta");
+ viewportMeta.name = "viewport";
+ viewportMeta.content = `width=${link.width}, height=${link.height}`;
+ viewportMeta.dataset.readium = "true";
+ doc.head.appendChild(viewportMeta);
+ }
+
const simg = document.createElement("img");
- simg.src = this.burl || "";
- simg.alt = this.item.title || "";
- simg.decoding = "async";
+ simg.src = burl || "";
+ simg.alt = link.title || "";
+ await simg.decode(); // Reduce repaints
doc.body.appendChild(simg);
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
+
+ // Apply resource injections if injection service is provided
+ if (this.injector) {
+ await this.injector.injectForDocument(doc, new Link({
+ // Temporary solution to address injector only expecting (X)HTML
+ // documents for injection, which we are technically providing
+ href: "readium-image-frame.xhtml",
+ type: MediaType.XHTML.string
+ }));
+ }
+
+ // Add image style
+ const sstyle = doc.createElement("style");
+ sstyle.dataset.readium = "true";
+ sstyle.textContent = `
+ html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
+ img { margin: 0; padding: 0; border: 0; }`;
+ doc.head.appendChild(sstyle);
+
+ return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true);
}
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
@@ -102,6 +150,10 @@ export default class FrameBlobBuider {
// Get allowed domains from injector if it exists
const allowedDomains = this.injector?.getAllowedDomains?.() || [];
+ // Remove query from root if present, as CSP doesn't allow them
+ root = root?.split("?")[0];
+
+
// Always include the root domain if provided
const domains = [...new Set([
...(root ? [root] : []),
@@ -124,6 +176,7 @@ export default class FrameBlobBuider {
// loaded in parallel, greatly increasing overall speed.
doc.body.querySelectorAll("img").forEach((img) => {
img.setAttribute("fetchpriority", "high");
+ img.setAttribute("referrerpolicy", "origin");
});
// We need to ensure that lang is set on the root element
diff --git a/navigator/src/epub/frame/FrameManager.ts b/navigator/src/epub/frame/FrameManager.ts
index 34856344..bceb4dd9 100644
--- a/navigator/src/epub/frame/FrameManager.ts
+++ b/navigator/src/epub/frame/FrameManager.ts
@@ -27,7 +27,7 @@ export class FrameManager {
this.frame.sandbox.value = "allow-same-origin allow-scripts";
this.frame.classList.add("readium-navigator-iframe");
this.frame.style.visibility = "hidden";
- this.frame.style.setProperty("aria-hidden", "true");
+ this.frame.ariaHidden = "true";
this.frame.style.opacity = "0";
this.frame.style.position = "absolute";
this.frame.style.pointerEvents = "none";
@@ -111,7 +111,7 @@ export class FrameManager {
async hide(): Promise {
if(this.destroyed) return;
this.frame.style.visibility = "hidden";
- this.frame.style.setProperty("aria-hidden", "true");
+ this.frame.ariaHidden = "true";
this.frame.style.opacity = "0";
this.frame.style.pointerEvents = "none";
this.hidden = true;
@@ -140,9 +140,9 @@ export class FrameManager {
const remove = () => {
this.frame.style.removeProperty("visibility");
- this.frame.style.removeProperty("aria-hidden");
this.frame.style.removeProperty("opacity");
this.frame.style.removeProperty("pointer-events");
+ this.frame.ariaHidden = null;
this.hidden = false;
if (sML.UA.WebKit) {
diff --git a/navigator/src/epub/frame/FramePoolManager.ts b/navigator/src/epub/frame/FramePoolManager.ts
index 6b869f3c..7ca428a1 100644
--- a/navigator/src/epub/frame/FramePoolManager.ts
+++ b/navigator/src/epub/frame/FramePoolManager.ts
@@ -1,12 +1,12 @@
import { ModuleName } from "@readium/navigator-html-injectables";
import { Locator, Publication } from "@readium/shared";
-import FrameBlobBuider from "./FrameBlobBuilder.ts";
+import FrameBlobBuilder from "./FrameBlobBuilder.ts";
import { FrameManager } from "./FrameManager.ts";
import { Injector } from "../../injection/Injector.ts";
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
-const UPPER_BOUNDARY = 5;
-const LOWER_BOUNDARY = 3;
+const UPPER_BOUNDARY = 10;
+const LOWER_BOUNDARY = 5;
export class FramePoolManager {
private readonly container: HTMLElement;
@@ -14,8 +14,8 @@ export class FramePoolManager {
private _currentFrame: FrameManager | undefined;
private currentCssProperties: { [key: string]: string } | undefined;
private readonly pool: Map = new Map();
- private readonly blobs: Map = new Map();
- private readonly inprogress: Map> = new Map();
+ private readonly blobs: Map = new Map();
+ private readonly inprogress: Map> = new Map();
private pendingUpdates: Map = new Map();
private currentBaseURL: string | undefined;
private readonly injector: Injector | null = null;
@@ -42,7 +42,7 @@ export class FramePoolManager {
// Wait for all in-progress loads to complete
let iit = this.inprogress.values();
let inp = iit.next();
- const inprogressPromises: Promise[] = [];
+ const inprogressPromises: Promise[] = [];
while(inp.value) {
inprogressPromises.push(inp.value);
inp = iit.next();
@@ -62,10 +62,8 @@ export class FramePoolManager {
this.pool.clear();
// Revoke all blobs
- this.blobs.forEach(v => {
- this.injector?.releaseBlobUrl?.(v);
- URL.revokeObjectURL(v);
- });
+ this.blobs.forEach(v => v.reset());
+ this.blobs.clear();
// Clean up injector if it exists
this.injector?.dispose();
@@ -106,15 +104,18 @@ export class FramePoolManager {
this.pool.delete(href);
if(this.pendingUpdates.has(href))
this.pendingUpdates.set(href, { inPool: false });
+ // Note that we don't reset the blob here, unlike in the FXL pool.
+ // This is because FXL tends to have a ton more blobs. Maybe we'll adjust
+ // this at a later point with a much larger boundary for resets to deal
+ // with extremely long/large reflowable publications.
+ // Reflowable publication resources also tend to be much larger documents,
+ // so they're more expensive to preprocess with the FrameBlobBuilder.
});
// Check if base URL of publication has changed
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
// Revoke all blobs
- this.blobs.forEach(v => {
- this.injector?.releaseBlobUrl?.(v);
- URL.revokeObjectURL(v);
- });
+ this.blobs.forEach(v => v.reset());
this.blobs.clear();
}
this.currentBaseURL = pub.baseURL;
@@ -127,18 +128,14 @@ export class FramePoolManager {
// when navigating backwards, where paginated will go the
// start of the resource instead of the end due to the
// corrupted width ColumnSnapper (injectables) gets on init
- this.blobs.forEach(v => {
- this.injector?.releaseBlobUrl?.(v);
- URL.revokeObjectURL(v);
- });
+ this.blobs.forEach(v => v.reset());
this.blobs.clear();
this.pendingUpdates.clear();
}
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
- const url = this.blobs.get(href);
- if(url) {
- this.injector?.releaseBlobUrl?.(url);
- URL.revokeObjectURL(url);
+ const v = this.blobs.get(href);
+ if(v) {
+ v.reset();
this.blobs.delete(href);
this.pendingUpdates.delete(href);
}
@@ -157,7 +154,7 @@ export class FramePoolManager {
const itm = pub.readingOrder.findWithHref(href);
if(!itm) return; // TODO throw?
if(!this.blobs.has(href)) {
- const blobBuilder = new FrameBlobBuider(
+ this.blobs.set(href, new FrameBlobBuilder(
pub,
this.currentBaseURL || "",
itm,
@@ -165,24 +162,33 @@ export class FramePoolManager {
cssProperties: this.currentCssProperties,
injector: this.injector
}
- );
- const blobURL = await blobBuilder.build();
- this.blobs.set(href, blobURL);
+ ));
}
// Create