Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion navigator-html-injectables/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions navigator-html-injectables/src/helpers/css.ts
Original file line number Diff line number Diff line change
@@ -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 } = {};

Expand Down
25 changes: 22 additions & 3 deletions navigator-html-injectables/src/modules/snapper/ColumnSnapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion navigator/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
48 changes: 45 additions & 3 deletions navigator/src/epub/EpubNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,12 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
});
}

if(this._layout === Layout.fixed) {
if(this._layout === Layout.fixed || this._layout === Layout.scrolled) {
this.framePool = new FXLFramePoolManager(
this.container,
this.positions,
this.pub,
this._layout,
this._injector,
this._contentProtection,
this._keyboardPeripherals
Expand All @@ -265,9 +266,15 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi

if(this.currentLocation === undefined)
this.currentLocation = this.positions[0];
else
this.currentLocation = this.completeLocator(this.currentLocation);

await this.resizeHandler();
await this.apply();
return new Promise(async res => {
await this.go(this.currentLocation, false, (s) => {
res(s);
});
});
}

public get settings(): Readonly<EpubSettings> {
Expand Down Expand Up @@ -885,7 +892,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
return;
}

const progression = locator?.locations?.progression;
const progression = locator.locations?.progression;
const hasProgression = progression && progression > 0;
if(hasProgression)
done = await new Promise<boolean>((res, _) => {
Expand All @@ -896,7 +903,42 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
cb(done);
}

private completeLocator(locator: Locator): Locator {
if(!locator.href) {
let fellback = false;
if(typeof locator.locations.position === "number") {
const match = this.positions.find(p => 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) {
Expand Down
117 changes: 85 additions & 32 deletions navigator/src/epub/frame/FrameBlobBuilder.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<string> {
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<string> {
// 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<string> {
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) {
Expand All @@ -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] : []),
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions navigator/src/epub/frame/FrameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -111,7 +111,7 @@ export class FrameManager {
async hide(): Promise<void> {
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;
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading