Przeglądaj źródła

added "zoom out the page" option (cf #440)

Former-commit-id: c398eaddcf9cdc77607f3990802c62589e28bf6c
Gildas 5 lat temu
rodzic
commit
f0222839a9

+ 4 - 0
_locales/de/messages.json

@@ -235,6 +235,10 @@
 		"message": "maximale Inaktivitätszeit (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "Seite verkleinern",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "Bilder für alternative Bildschirmauflösungen entfernen",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/en/messages.json

@@ -235,6 +235,10 @@
 		"message": "maximum idle time (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "remove images for alternative screen resolutions",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/es/messages.json

@@ -235,6 +235,10 @@
 		"message": "periodo máximo de espera (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "alejar la página",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "eliminar imágenes para resoluciones alternativas de pantalla",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/fr/messages.json

@@ -235,6 +235,10 @@
 		"message": "temps d'inactivité maximal (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "dézoomer la page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "supprimer les images pour des résolutions d'écran alternatives",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/ja/messages.json

@@ -235,6 +235,10 @@
 		"message": "最大アイドル時間(ミリ秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "代替画面解像度用の画像を削除します",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/pl/messages.json

@@ -235,6 +235,10 @@
 		"message": "maksymalny czas bezczynności (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "usuwaj obrazy dla alternatywnych rozdzielczości ekranu",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/ru/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальное время простоя (мс)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "удалить изображения для альтернативных разрешений экрана",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/uk/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальний час простою (мс)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "видалити зображення для альтернативних розширень екрану ",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -235,6 +235,10 @@
 		"message": "最长空闲时间(毫秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "移除用于备选分辨率的图片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -235,6 +235,10 @@
 		"message": "最長空閒時間(毫秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "移除用於備選分辨率的圖片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 1 - 1
extension/core/bg/business.js

@@ -207,7 +207,7 @@ singlefile.extension.core.bg.business = (() => {
 		const tabId = taskInfo.tab.id;
 		const taskId = taskInfo.id;
 		taskInfo.cancelled = true;
-		singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: "content.cancelSave" });
+		singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: "content.cancelSave", resetZoomLevel: taskInfo.options.loadDeferredImagesKeepZoomLevel });
 		if (taskInfo.cancel) {
 			taskInfo.cancel();
 		}

+ 1 - 0
extension/core/bg/config.js

@@ -43,6 +43,7 @@ singlefile.extension.core.bg.config = (() => {
 		loadDeferredImagesMaxIdleTime: 1500,
 		loadDeferredImagesBlockCookies: false,
 		loadDeferredImagesBlockStorage: false,
+		loadDeferredImagesKeepZoomLevel: false,
 		filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
 		infobarTemplate: "",
 		includeInfobar: false,

+ 6 - 0
extension/core/content/content-main.js

@@ -56,6 +56,9 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 					ui.onEndPage();
 					browser.runtime.sendMessage({ method: "ui.processCancelled" });
 				}
+				if (message.resetZoomLevel) {
+					singlefile.lib.processors.lazy.content.loader.resetZoomLevel();
+				}
 				return {};
 			}
 			if (message.method == "content.getSelectedLinks") {
@@ -141,6 +144,9 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 			if (!processor.cancelled) {
 				if (event.type == event.RESOURCES_INITIALIZED) {
 					maxIndex = event.detail.max;
+					if (options.loadDeferredImagesKeepZoomLevel) {
+						singlefile.lib.processors.lazy.content.loader.resetZoomLevel();
+					}
 				}
 				if (event.type == event.RESOURCES_INITIALIZED || event.type == event.RESOURCE_LOADED) {
 					if (event.type == event.RESOURCE_LOADED) {

+ 6 - 0
extension/ui/bg/ui-options.js

@@ -42,6 +42,7 @@
 	const compressCSSLabel = document.getElementById("compressCSSLabel");
 	const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 	const loadDeferredImagesMaxIdleTimeLabel = document.getElementById("loadDeferredImagesMaxIdleTimeLabel");
+	const loadDeferredImagesKeepZoomLevelLabel = document.getElementById("loadDeferredImagesKeepZoomLevelLabel");
 	const addMenuEntryLabel = document.getElementById("addMenuEntryLabel");
 	const filenameTemplateLabel = document.getElementById("filenameTemplateLabel");
 	const filenameMaxLengthLabel = document.getElementById("filenameMaxLengthLabel");
@@ -118,6 +119,7 @@
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 	const loadDeferredImagesMaxIdleTimeInput = document.getElementById("loadDeferredImagesMaxIdleTimeInput");
+	const loadDeferredImagesKeepZoomLevelInput = document.getElementById("loadDeferredImagesKeepZoomLevelInput");
 	const contextMenuEnabledInput = document.getElementById("contextMenuEnabledInput");
 	const filenameTemplateInput = document.getElementById("filenameTemplateInput");
 	const filenameMaxLengthInput = document.getElementById("filenameMaxLengthInput");
@@ -437,6 +439,7 @@
 	compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 	loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 	loadDeferredImagesMaxIdleTimeLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesMaxIdleTime");
+	loadDeferredImagesKeepZoomLevelLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesKeepZoomLevel");
 	addMenuEntryLabel.textContent = browser.i18n.getMessage("optionAddMenuEntry");
 	filenameTemplateLabel.textContent = browser.i18n.getMessage("optionFilenameTemplate");
 	filenameMaxLengthLabel.textContent = browser.i18n.getMessage("optionFilenameMaxLength");
@@ -620,6 +623,8 @@
 		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages && !profileOptions.saveRawPage;
 		loadDeferredImagesInput.disabled = profileOptions.saveRawPage;
 		loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
+		loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel && !profileOptions.saveRawPage;
+		loadDeferredImagesKeepZoomLevelInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPape;
 		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPage;
 		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
 		filenameTemplateInput.value = profileOptions.filenameTemplate;
@@ -696,6 +701,7 @@
 				compressCSS: compressCSSInput.checked,
 				loadDeferredImages: loadDeferredImagesInput.checked,
 				loadDeferredImagesMaxIdleTime: Math.max(loadDeferredImagesMaxIdleTimeInput.value, 0),
+				loadDeferredImagesKeepZoomLevel: loadDeferredImagesKeepZoomLevelInput.checked,
 				contextMenuEnabled: contextMenuEnabledInput.checked,
 				filenameTemplate: filenameTemplateInput.value,
 				filenameMaxLength: filenameMaxLengthInput.value,

+ 7 - 0
extension/ui/pages/help.html

@@ -267,6 +267,13 @@
 							for example the network or system conditions are degraded. You can also decrease this value
 							otherwise.</p>
 					</li>
+					<li data-options-label="loadDeferredImagesKeepZoomLevelLabel"> <span class="option">Option: zoom out
+							the page</span>
+						<p>Check this option to save the entire content of a page by zooming out the page. This can help
+							saving pages using infinite virtual lists for example.
+						</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 					<li data-options-label="removeAlternativeImagesLabel"> <span class="option">Option: remove images
 							for alternative screen resolutions</span>
 						<p>Check this option to remove images that are alternatives in lower and/or higher resolutions

+ 4 - 0
extension/ui/pages/options.html

@@ -135,6 +135,10 @@
 				<label for="loadDeferredImagesMaxIdleTimeInput" id="loadDeferredImagesMaxIdleTimeLabel"></label>
 				<input type="number" class="large-input" id="loadDeferredImagesMaxIdleTimeInput" step="100">
 			</div>
+			<div class="option second-level">
+				<label for="loadDeferredImagesKeepZoomLevelInput" id="loadDeferredImagesKeepZoomLevelLabel"></label>
+				<input type="checkbox" id="loadDeferredImagesKeepZoomLevelInput">
+			</div>
 			<div class="option">
 				<label for="removeAlternativeImagesInput" id="removeAlternativeImagesLabel"></label>
 				<input type="checkbox" id="removeAlternativeImagesInput">

+ 95 - 57
lib/single-file/processors/hooks/content/content-hooks-frames-web.js

@@ -27,6 +27,9 @@
 
 	const LOAD_DEFERRED_IMAGES_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT = "single-file-load-deferred-images-keep-zoom-level-start";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-end";
+	const LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
 	const BLOCK_COOKIES_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
@@ -60,36 +63,41 @@
 	const observers = new Map();
 	const observedElements = new Map();
 
-	addEventListener.call(window, LOAD_DEFERRED_IMAGES_START_EVENT, () => {
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_START_EVENT, () => loadDeferredImagesStart());
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT, () => loadDeferredImagesStart(true));
+
+	function loadDeferredImagesStart(keepZoomLevel) {
 		const scrollingElement = document.scrollingElement || document.documentElement;
 		const clientHeight = scrollingElement.clientHeight;
 		const clientWidth = scrollingElement.clientWidth;
 		const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
 		const scrollWidth = Math.max(scrollingElement.scrollWidth - clientWidth, clientWidth);
-		scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
-		scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
-		screen.__defineGetter__("height", () => scrollHeight);
-		screen.__defineGetter__("width", () => scrollWidth);
 		document.querySelectorAll("[loading=lazy]").forEach(element => {
 			element.loading = "eager";
 			element.setAttribute(LAZY_LOAD_ATTRIBUTE, "");
 		});
-		if (!window._singleFile_getBoundingClientRect) {
-			window._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
-			Element.prototype.getBoundingClientRect = function () {
-				const boundingRect = window._singleFile_getBoundingClientRect.call(this);
-				if (this == scrollingElement) {
-					boundingRect.__defineGetter__("height", () => scrollHeight);
-					boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
-					boundingRect.__defineGetter__("width", () => scrollWidth);
-					boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
-				}
-				return boundingRect;
-			};
-			window._singleFile_innerHeight = window.innerHeight;
-			window._singleFile_innerWidth = window.innerWidth;
-			window.__defineGetter__("innerHeight", () => scrollHeight);
-			window.__defineGetter__("innerWidth", () => scrollWidth);
+		if (!keepZoomLevel) {
+			scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
+			scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
+			screen.__defineGetter__("height", () => scrollHeight);
+			screen.__defineGetter__("width", () => scrollWidth);
+			if (!window._singleFile_getBoundingClientRect) {
+				window._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
+				Element.prototype.getBoundingClientRect = function () {
+					const boundingRect = window._singleFile_getBoundingClientRect.call(this);
+					if (this == scrollingElement) {
+						boundingRect.__defineGetter__("height", () => scrollHeight);
+						boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
+						boundingRect.__defineGetter__("width", () => scrollWidth);
+						boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
+					}
+					return boundingRect;
+				};
+				window._singleFile_innerHeight = window.innerHeight;
+				window._singleFile_innerWidth = window.innerWidth;
+				window.__defineGetter__("innerHeight", () => scrollHeight);
+				window.__defineGetter__("innerWidth", () => scrollWidth);
+			}
 		}
 		if (!window._singleFileImage) {
 			const Image = window.Image;
@@ -123,8 +131,14 @@
 				};
 			});
 		}
-		const zoomFactorX = (clientHeight + window.scrollY) / scrollHeight;
-		const zoomFactorY = (clientWidth + window.scrollX) / scrollWidth;
+		let zoomFactorX, zoomFactorY;
+		if (keepZoomLevel) {
+			zoomFactorX = clientHeight / scrollHeight;
+			zoomFactorY = clientWidth / scrollWidth;
+		} else {
+			zoomFactorX = (clientHeight + window.scrollY) / scrollHeight;
+			zoomFactorY = (clientWidth + window.scrollX) / scrollWidth;
+		}
 		const zoomFactor = Math.min(zoomFactorX, zoomFactorY);
 		if (zoomFactor < 1) {
 			const transform = document.documentElement.style.getPropertyValue("transform");
@@ -135,54 +149,78 @@
 			document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
 			dispatchEvent.call(window, new UIEvent("resize"));
 			dispatchEvent.call(window, new UIEvent("scroll"));
-			document.documentElement.style.setProperty("transform", transform, transformPriority);
-			document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
-		}
-		dispatchEvent.call(window, new UIEvent("resize"));
-		dispatchEvent.call(window, new UIEvent("scroll"));
-		const docBoundingRect = scrollingElement.getBoundingClientRect();
-		[...observers].forEach(([intersectionObserver, observer]) => {
-			const rootBoundingRect = observer.options && observer.options.root && observer.options.root.getBoundingClientRect();
-			const targetElements = observedElements.get(intersectionObserver);
-			if (targetElements) {
-				observer.callback(targetElements.map(target => {
-					const boundingClientRect = target.getBoundingClientRect();
-					const isIntersecting = true;
-					const intersectionRatio = 1;
-					const rootBounds = observer.options && observer.options.root ? rootBoundingRect : docBoundingRect;
-					const time = 0;
-					return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
-				}), intersectionObserver);
+			if (keepZoomLevel) {
+				document.documentElement.style.setProperty("-sf-transform", transform, transformPriority);
+				document.documentElement.style.setProperty("-sf-transform-origin", transformOrigin, transformOriginPriority);
+			} else {
+				document.documentElement.style.setProperty("transform", transform, transformPriority);
+				document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
 			}
-		});
+		}
+		if (!keepZoomLevel) {
+			dispatchEvent.call(window, new UIEvent("resize"));
+			dispatchEvent.call(window, new UIEvent("scroll"));
+			const docBoundingRect = scrollingElement.getBoundingClientRect();
+			[...observers].forEach(([intersectionObserver, observer]) => {
+				const rootBoundingRect = observer.options && observer.options.root && observer.options.root.getBoundingClientRect();
+				const targetElements = observedElements.get(intersectionObserver);
+				if (targetElements) {
+					observer.callback(targetElements.map(target => {
+						const boundingClientRect = target.getBoundingClientRect();
+						const isIntersecting = true;
+						const intersectionRatio = 1;
+						const rootBounds = observer.options && observer.options.root ? rootBoundingRect : docBoundingRect;
+						const time = 0;
+						return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
+					}), intersectionObserver);
+				}
+			});
+		}
+	}
+
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_END_EVENT, () => loadDeferredImagesEnd());
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT, () => loadDeferredImagesEnd(true));
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT, () => {
+		const transform = document.documentElement.style.getPropertyValue("-sf-transform");
+		const transformPriority = document.documentElement.style.getPropertyPriority("-sf-transform");
+		const transformOrigin = document.documentElement.style.getPropertyValue("-sf-transform-origin");
+		const transformOriginPriority = document.documentElement.style.getPropertyPriority("-sf-transform-origin");
+		document.documentElement.style.setProperty("transform", transform, transformPriority);
+		document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
+		document.documentElement.style.removeProperty("-sf-transform");
+		document.documentElement.style.removeProperty("-sf-transform-origin");
 	});
 
-	addEventListener.call(window, LOAD_DEFERRED_IMAGES_END_EVENT, () => {
+	function loadDeferredImagesEnd(keepZoomLevel) {
 		const scrollingElement = document.scrollingElement || document.documentElement;
 		document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
 			element.loading = "lazy";
 			element.removeAttribute(LAZY_LOAD_ATTRIBUTE);
 		});
-		delete scrollingElement.clientHeight;
-		delete scrollingElement.clientWidth;
-		delete screen.height;
-		delete screen.width;
-		if (window._singleFile_getBoundingClientRect) {
-			Element.prototype.getBoundingClientRect = window._singleFile_getBoundingClientRect;
-			window.innerHeight = window._singleFile_innerHeight;
-			window.innerWidth = window._singleFile_innerWidth;
-			delete window._singleFile_getBoundingClientRect;
-			delete window._singleFile_innerHeight;
-			delete window._singleFile_innerWidth;
+		if (!keepZoomLevel) {
+			delete scrollingElement.clientHeight;
+			delete scrollingElement.clientWidth;
+			delete screen.height;
+			delete screen.width;
+			if (window._singleFile_getBoundingClientRect) {
+				Element.prototype.getBoundingClientRect = window._singleFile_getBoundingClientRect;
+				window.innerHeight = window._singleFile_innerHeight;
+				window.innerWidth = window._singleFile_innerWidth;
+				delete window._singleFile_getBoundingClientRect;
+				delete window._singleFile_innerHeight;
+				delete window._singleFile_innerWidth;
+			}
 		}
 		if (window._singleFileImage) {
 			delete window.Image;
 			window.Image = window._singleFileImage;
 			delete window._singleFileImage;
 		}
-		dispatchEvent.call(window, new UIEvent("resize"));
-		dispatchEvent.call(window, new UIEvent("scroll"));
-	});
+		if (!keepZoomLevel) {
+			dispatchEvent.call(window, new UIEvent("resize"));
+			dispatchEvent.call(window, new UIEvent("scroll"));
+		}
+	}
 
 	addEventListener.call(window, BLOCK_COOKIES_START_EVENT, () => {
 		try {

+ 16 - 2
lib/single-file/processors/hooks/content/content-hooks-frames.js

@@ -27,6 +27,9 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 
 	const LOAD_DEFERRED_IMAGES_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT = "single-file-load-deferred-images-keep-zoom-level-start";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-end";
+	const LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
 	const BLOCK_COOKIES_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
@@ -80,7 +83,11 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_START_EVENT));
 			}
-			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
+			if (options.loadDeferredImagesKeepZoomLevel) {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT));
+			} else {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
+			}
 		},
 		loadDeferredImagesEnd: options => {
 			if (options.loadDeferredImagesBlockCookies) {
@@ -89,7 +96,14 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_END_EVENT));
 			}
-			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
+			if (options.loadDeferredImagesKeepZoomLevel) {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT));
+			} else {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
+			}
+		},
+		loadDeferredImagesResetZoomLevel: () => {
+			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT));
 		},
 		LOAD_IMAGE_EVENT,
 		IMAGE_LOADED_EVENT

+ 6 - 0
lib/single-file/processors/lazy/content/content-lazy-loader.js

@@ -44,6 +44,12 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 			if (scrollY <= maxScrollY && scrollX <= maxScrollX) {
 				return process(options);
 			}
+		},
+		resetZoomLevel: () => {
+			const frames = singlefile.lib.processors.hooks.content.frames;
+			if (frames) {
+				frames.loadDeferredImagesResetZoomLevel();
+			}
 		}
 	};
 

+ 12 - 0
lib/single-file/single-file-core.js

@@ -93,6 +93,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 	const STAGES = [{
 		sequential: [
 			{ action: "preProcessPage" },
+			{ option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
 			{ action: "replaceStyleContents" },
 			{ action: "resetCharsetMeta" },
 			{ option: "saveFavicon", action: "saveFavicon" },
@@ -1351,6 +1352,17 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 			}
 		}
 
+		resetZoomLevel() {
+			const transform = this.doc.documentElement.style.getPropertyValue("-sf-transform");
+			const transformPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform");
+			const transformOrigin = this.doc.documentElement.style.getPropertyValue("-sf-transform-origin");
+			const transformOriginPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform-origin");
+			this.doc.documentElement.style.setProperty("transform", transform, transformPriority);
+			this.doc.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
+			this.doc.documentElement.style.removeProperty("-sf-transform");
+			this.doc.documentElement.style.removeProperty("-sf-transform-origin");
+		}
+
 		async insertMAFFMetaData() {
 			const maffMetaData = await this.maffMetaDataPromise;
 			if (maffMetaData && maffMetaData.content) {