Skip to content

Commit

Permalink
image click zoom/pan improved (PR #83)
Browse files Browse the repository at this point in the history
  • Loading branch information
panaC authored Feb 22, 2025
1 parent faee556 commit 2fde6b4
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 38 deletions.
18 changes: 16 additions & 2 deletions src/electron/common/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,22 @@ import { ISelectionInfo } from "./selection";
export const R2_EVENT_IMAGE_CLICK = "R2_EVENT_IMAGE_CLICK";
// tslint:disable-next-line:class-name
export interface IEventPayload_R2_EVENT_IMAGE_CLICK {
href: string;
imageCssSelector: string;
isSVGFragment: boolean;
isSVGImage: boolean;
HTMLImgSrc_SVGImageHref_SVGFragmentMarkup: string;
cssSelectorOf_HTMLImg_SVGImage_SVGFragment: string;
languageOf_HTMLImg_SVGImage_SVGFragment: string | undefined;
directionOf_HTMLImg_SVGImage_SVGFragment: string | undefined;
naturalWidthOf_HTMLImg_SVGImage: number | undefined; // undefined with isSVGFragment and normally undefined with isSVGImage (HTMLImageElement.naturalWidth/Height exists, not SVGImageElement.naturalWidth/Height) ... but, we load the image href into an HTML image in order to extract the natural dimensions
naturalHeightOf_HTMLImg_SVGImage: number | undefined; // (same comment as above)
altAttributeOf_HTMLImg_SVGImage_SVGFragment: string | null;
titleAttributeOf_HTMLImg_SVGImage_SVGFragment: string | null;
ariaLabelAttributeOf_HTMLImg_SVGImage_SVGFragment: string | null;
// isFigure: boolean;
// figureCssSelector: string | undefined;
// figcaptionCssSelector: string | undefined;
// ariaDescribedbyAttribute: string | undefined;
// ariaDetailsAttribute: string | undefined;
}

// in RENDERER: webview.send()
Expand Down
12 changes: 6 additions & 6 deletions src/electron/renderer/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,8 @@ export function readiumCssUpdate(rcss: IEventPayload_R2_EVENT_READIUMCSS) {
let _webview1: IReadiumElectronWebview | undefined;
let _webview2: IReadiumElectronWebview | undefined;

let _imageClickHandler: ((href: string) => void) | undefined;
export function setImageClickHandler(cb: (href: string) => void) {
let _imageClickHandler: ((payload: IEventPayload_R2_EVENT_IMAGE_CLICK) => void) | undefined;
export function setImageClickHandler(cb: (payload: IEventPayload_R2_EVENT_IMAGE_CLICK) => void) {
_imageClickHandler = cb;
};

Expand Down Expand Up @@ -493,13 +493,13 @@ function createWebViewInternal(preloadScriptPath: string): IReadiumElectronWebvi
} else if (event.channel === R2_EVENT_IMAGE_CLICK) {
const payload = event.args[0] as IEventPayload_R2_EVENT_IMAGE_CLICK;
if (_imageClickHandler) {
debug("R2_EVENT_IMAGE_CLICK (ipc-message) href [_imageClickHandler]: " + payload.href + " ___ " + payload.imageCssSelector);
_imageClickHandler(payload.href);
debug("R2_EVENT_IMAGE_CLICK (ipc-message) href [_imageClickHandler]: " + JSON.stringify(payload, null, 4));
_imageClickHandler({...payload});
} else {
debug("R2_EVENT_IMAGE_CLICK (ipc-message) href [NOT _imageClickHandler => webview.send(R2_EVENT_IMAGE_CLICK]: " + payload.href + " ___ " + payload.imageCssSelector);
debug("R2_EVENT_IMAGE_CLICK (ipc-message) href [NOT _imageClickHandler => webview.send(R2_EVENT_IMAGE_CLICK]: " + JSON.stringify(payload, null, 4));
// webview === event.currentTarget as IReadiumElectronWebview
// webview === wv
webview.send(R2_EVENT_IMAGE_CLICK, payload.href, payload.imageCssSelector);
webview.send(R2_EVENT_IMAGE_CLICK, {...payload});
}
} else if (!highlightsHandleIpcMessage(event.channel, event.args, webview) &&
!ttsHandleIpcMessage(event.channel, event.args, webview) &&
Expand Down
14 changes: 10 additions & 4 deletions src/electron/renderer/webview/popoutImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import { ReadiumElectronWebviewWindow } from "./state";

export function popoutImage(
win: ReadiumElectronWebviewWindow,
element: HTMLImageElement | SVGElement,
href_src: string,
_cssSelectorOf_HTMLImg_SVGImage_SVGFragment: string,
HTMLImg_SVGImage_SVGFragment: HTMLImageElement | SVGElement,
HTMLImgSrc_SVGImageHref_SVGFragmentMarkup: string,
isSVGFragment: boolean,
_isSVGImage: boolean,
focusScrollRaw:
(el: HTMLOrSVGElement, doFocus: boolean, animate: boolean, domRect: DOMRect | undefined) => void,
ensureTwoPageSpreadWithOddColumnsIsOffsetTempDisable: () => number,
ensureTwoPageSpreadWithOddColumnsIsOffsetReEnable: (val: number) => void,
) {
const element = HTMLImg_SVGImage_SVGFragment;
let href_src = HTMLImgSrc_SVGImageHref_SVGFragmentMarkup;

// https://github.com/jackmoore/wheelzoom/blob/05224659740eea775a779faa62cef0ec0126082/wheelzoom.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(win as any).wheelzoom = (img: HTMLImageElement) => {
Expand Down Expand Up @@ -420,8 +426,8 @@ export function popoutImage(
// return false;
// }

const isSVG = href_src.startsWith("<svg");
if (isSVG) {
// const isSVG = href_src.startsWith("<svg");
if (isSVGFragment) {
// href_src = href_src.replace(/[\r\n]/g, " ").replace(/\s\s+/g, " ").trim();
href_src = "data:image/svg+xml;base64," + Buffer.from(href_src).toString("base64");
}
Expand Down
101 changes: 75 additions & 26 deletions src/electron/renderer/webview/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import {
import { IPropertyAnimationState, animateProperty } from "../common/animateProperty";
import { uniqueCssSelector } from "../common/cssselector3";

import { normalizeText } from "../common/dom-text-utils";
import { getDirection, getLanguage, normalizeText } from "../common/dom-text-utils";
import { easings } from "../common/easings";
import { closePopupDialogs, isPopupDialogOpen } from "../common/popup-dialog";
import { getURLQueryParams } from "../common/querystring";
Expand Down Expand Up @@ -2734,27 +2734,29 @@ function loaded(forced: boolean) {
// });

let linkElement: Element | undefined;
let imageElement: Element | undefined;
let HTMLImg_SVGImage_SVGFragment: Element | undefined;

let href_src: string | SVGAnimatedString | undefined;
let href_src_image_nested_in_link: string | SVGAnimatedString | undefined;
let isSVG = false;
let isSVGFragment = false;
let isSVGImage = false;
let globalSVGDefs: NodeListOf<Element> | undefined;
let currentElement: Element | undefined = ev.target as Element;
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
const tagName = currentElement.tagName.toLowerCase();
if ((tagName === "img" || tagName === "image" || tagName === "svg")
&& !currentElement.classList.contains(POPOUTIMAGE_CONTAINER_ID)) {

isSVG = false;
isSVGFragment = false;
if (tagName === "svg") {
if (imageElement) {
if (HTMLImg_SVGImage_SVGFragment) {
// image inside SVG
currentElement = currentElement.parentNode as Element;
isSVGImage = true;
continue;
}

isSVG = true;
isSVGFragment = true;
href_src = currentElement.outerHTML;

const defs = currentElement.querySelectorAll("defs > *[id]");
Expand Down Expand Up @@ -2844,7 +2846,7 @@ function loaded(forced: boolean) {
}
debug(`IMG CLICK: ${href_src} (${href_src_})`);
}
imageElement = currentElement;
HTMLImg_SVGImage_SVGFragment = currentElement;

// DOM parent / ancestor could be link a@href, so let's continue walking up the tree
// break;
Expand Down Expand Up @@ -2873,7 +2875,7 @@ function loaded(forced: boolean) {

// at that point, can be both an image and a link! ("img" element descendant of "a" ... clickable image link)

if (!href_src || (!imageElement && !linkElement)) {
if (!href_src || (!HTMLImg_SVGImage_SVGFragment && !linkElement)) {
clearImageZoomOutline();
return;
}
Expand Down Expand Up @@ -2909,21 +2911,21 @@ function loaded(forced: boolean) {

debug(`HREF SRC: ${href_src} ${href_src_image_nested_in_link} (${win.location.href})`);

const has = imageElement?.hasAttribute(`data-${POPOUTIMAGE_CONTAINER_ID}`);
if (imageElement && href_src && (has ||
((!linkElement && !win.READIUM2.isFixedLayout && !isSVG) || ev.shiftKey)
const has = HTMLImg_SVGImage_SVGFragment?.hasAttribute(`data-${POPOUTIMAGE_CONTAINER_ID}`);
if (HTMLImg_SVGImage_SVGFragment && href_src && (has ||
((!linkElement && !win.READIUM2.isFixedLayout && !isSVGFragment) || ev.shiftKey)
)) {
if (linkElement && href_src_image_nested_in_link) {
href_src = href_src_image_nested_in_link;
}

clearImageZoomOutline(); // removes imageElement `data-${POPOUTIMAGE_CONTAINER_ID}`
clearImageZoomOutline(); // removes HTMLImg_SVGImage_SVGFragment `data-${POPOUTIMAGE_CONTAINER_ID}`

ev.preventDefault();
ev.stopPropagation();

if (has) {
if (!isSVG &&
if (!isSVGFragment &&
!/^(https?|thoriumhttps):\/\//.test(href_src) &&
!href_src.startsWith((READIUM2_ELECTRON_HTTP_PROTOCOL + "://"))) {

Expand All @@ -2932,17 +2934,61 @@ function loaded(forced: boolean) {
debug(`IMG CLICK ABSOLUTE-ized: ${href_src}`);
}

const imageCssSelector = getCssSelector(imageElement);
const cssSelectorOf_HTMLImg_SVGImage_SVGFragment = getCssSelector(HTMLImg_SVGImage_SVGFragment);

// const isFigure = (parentElement: Element | null): parentElement is HTMLElement =>
// parentElement?.tagName === "FIGURE" || parentElement?.tagName === "figure";

debug("R2_EVENT_IMAGE_CLICK (ipcRenderer.sendToHost) href: " + href_src + " ___ " + cssSelectorOf_HTMLImg_SVGImage_SVGFragment);

debug("R2_EVENT_IMAGE_CLICK (ipcRenderer.sendToHost) href: " + href_src + " ___ " + imageCssSelector);
const payload: IEventPayload_R2_EVENT_IMAGE_CLICK = {
href: href_src,
imageCssSelector,
isSVGFragment,
isSVGImage,
HTMLImgSrc_SVGImageHref_SVGFragmentMarkup: href_src,
cssSelectorOf_HTMLImg_SVGImage_SVGFragment,
languageOf_HTMLImg_SVGImage_SVGFragment: getLanguage(HTMLImg_SVGImage_SVGFragment),
directionOf_HTMLImg_SVGImage_SVGFragment: getDirection(HTMLImg_SVGImage_SVGFragment), // TODO: really, only useful for child title element of SVG, not in the general case
altAttributeOf_HTMLImg_SVGImage_SVGFragment: HTMLImg_SVGImage_SVGFragment.getAttribute("alt"),
titleAttributeOf_HTMLImg_SVGImage_SVGFragment: HTMLImg_SVGImage_SVGFragment.getAttribute("title"),
ariaLabelAttributeOf_HTMLImg_SVGImage_SVGFragment: HTMLImg_SVGImage_SVGFragment.getAttribute("aria-label"),
naturalWidthOf_HTMLImg_SVGImage: isSVGFragment ? undefined :
isSVGImage ? (((HTMLImg_SVGImage_SVGFragment as SVGImageElement) as unknown as HTMLImageElement).naturalWidth || undefined) :
(HTMLImg_SVGImage_SVGFragment as HTMLImageElement).naturalWidth,
naturalHeightOf_HTMLImg_SVGImage: isSVGFragment ? undefined :
isSVGImage ? (((HTMLImg_SVGImage_SVGFragment as SVGImageElement) as unknown as HTMLImageElement).naturalHeight || undefined) :
(HTMLImg_SVGImage_SVGFragment as HTMLImageElement).naturalHeight,
// isFigure: isFigure(HTMLImg_SVGImage_SVGFragment.parentElement),
// figureCssSelector: isFigure(HTMLImg_SVGImage_SVGFragment.parentElement)
// ? getCssSelector(HTMLImg_SVGImage_SVGFragment.parentElement)
// : undefined,
// figcaptionCssSelector: isFigure(HTMLImg_SVGImage_SVGFragment.parentElement)
// ? getCssSelector(HTMLImg_SVGImage_SVGFragment.parentElement.getElementsByTagName("figcaption")[0]) // return "" if undefined
// : undefined,
// ariaDescribedbyAttribute: (HTMLImg_SVGImage_SVGFragment as HTMLImageElement).getAttribute("aria-describedby") || undefined,
// ariaDetailsAttribute: (HTMLImg_SVGImage_SVGFragment as HTMLImageElement).getAttribute("aria-details") || undefined, // aria-details from the image is removed in navigator why ?
};
ipcRenderer.sendToHost(R2_EVENT_IMAGE_CLICK, payload);
payload.naturalWidthOf_HTMLImg_SVGImage = payload.naturalWidthOf_HTMLImg_SVGImage || undefined;
payload.naturalHeightOf_HTMLImg_SVGImage = payload.naturalHeightOf_HTMLImg_SVGImage || undefined;
if (!isSVGFragment && // isSVGImage or just HTML image
!payload.naturalWidthOf_HTMLImg_SVGImage || !payload.naturalHeightOf_HTMLImg_SVGImage) {

const imageObject = new Image();
imageObject.onload = function() {
payload.naturalWidthOf_HTMLImg_SVGImage = imageObject.naturalWidth || undefined;
payload.naturalHeightOf_HTMLImg_SVGImage = imageObject.naturalHeight || undefined;
ipcRenderer.sendToHost(R2_EVENT_IMAGE_CLICK, payload);
};
imageObject.onerror = function() {
ipcRenderer.sendToHost(R2_EVENT_IMAGE_CLICK, payload);
};
imageObject.src = href_src; // HTMLImgSrc_SVGImageHref_SVGFragmentMarkup
} else {
ipcRenderer.sendToHost(R2_EVENT_IMAGE_CLICK, payload);
}

} else {
// removed by clearImageZoomOutline();
imageElement.setAttribute(`data-${POPOUTIMAGE_CONTAINER_ID}`, "1");
HTMLImg_SVGImage_SVGFragment.setAttribute(`data-${POPOUTIMAGE_CONTAINER_ID}`, "1");
}

return;
Expand Down Expand Up @@ -2995,18 +3041,21 @@ function loaded(forced: boolean) {
}, true);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcRenderer.on("R2_EVENT_IMAGE_CLICK", (_event: any, href_src: string, imageCssSelector: string) => {
debug("R2_EVENT_IMAGE_CLICK (ipcRenderer.on) href: " + href_src + " ___ " + imageCssSelector);
ipcRenderer.on("R2_EVENT_IMAGE_CLICK", (_event: any, payload: IEventPayload_R2_EVENT_IMAGE_CLICK) => {
debug("R2_EVENT_IMAGE_CLICK (ipcRenderer.on) href: " + JSON.stringify(payload, null, 4));
// win.document.querySelectorAll(`img[data-${POPOUTIMAGE_CONTAINER_ID}]`);
// win.document.querySelectorAll(`image[data-${POPOUTIMAGE_CONTAINER_ID}]`);
// win.document.querySelectorAll(`svg[data-${POPOUTIMAGE_CONTAINER_ID}]`);
// const imageElement = win.document.querySelector(`[data-${POPOUTIMAGE_CONTAINER_ID}]`);
const imageElement = win.document.querySelector(imageCssSelector);
if (imageElement) {
// const HTMLImg_SVGImage_SVGFragment = win.document.querySelector(`[data-${POPOUTIMAGE_CONTAINER_ID}]`);
const HTMLImg_SVGImage_SVGFragment = win.document.querySelector(payload.cssSelectorOf_HTMLImg_SVGImage_SVGFragment);
if (HTMLImg_SVGImage_SVGFragment) {
popoutImage(
win,
imageElement as HTMLImageElement | SVGElement,
href_src,
payload.cssSelectorOf_HTMLImg_SVGImage_SVGFragment,
HTMLImg_SVGImage_SVGFragment as HTMLImageElement | SVGElement,
payload.HTMLImgSrc_SVGImageHref_SVGFragmentMarkup,
payload.isSVGFragment,
payload.isSVGImage,
focusScrollRaw,
ensureTwoPageSpreadWithOddColumnsIsOffsetTempDisable,
ensureTwoPageSpreadWithOddColumnsIsOffsetReEnable);
Expand Down

0 comments on commit 2fde6b4

Please sign in to comment.