Просмотр исходного кода

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

Former-commit-id: c398eaddcf9cdc77607f3990802c62589e28bf6c
Gildas 5 лет назад
Родитель
Сommit
f0222839a9

+ 4 - 0
_locales/de/messages.json

@@ -235,6 +235,10 @@
 		"message": "maximale Inaktivitätszeit (ms)",
 		"message": "maximale Inaktivitätszeit (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "Seite verkleinern",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 	"optionRemoveAlternativeImages": {
 		"message": "Bilder für alternative Bildschirmauflösungen entfernen",
 		"message": "Bilder für alternative Bildschirmauflösungen entfernen",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"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)",
 		"message": "maximum idle time (ms)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "remove images for alternative screen resolutions",
 		"message": "remove images for alternative screen resolutions",
 		"description": "Options page label: '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)",
 		"message": "periodo máximo de espera (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "alejar la página",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 	"optionRemoveAlternativeImages": {
 		"message": "eliminar imágenes para resoluciones alternativas de pantalla",
 		"message": "eliminar imágenes para resoluciones alternativas de pantalla",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"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)",
 		"message": "temps d'inactivité maximal (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "dézoomer la page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 	"optionRemoveAlternativeImages": {
 		"message": "supprimer les images pour des résolutions d'écran alternatives",
 		"message": "supprimer les images pour des résolutions d'écran alternatives",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/ja/messages.json

@@ -235,6 +235,10 @@
 		"message": "最大アイドル時間(ミリ秒)",
 		"message": "最大アイドル時間(ミリ秒)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "代替画面解像度用の画像を削除します",
 		"message": "代替画面解像度用の画像を削除します",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"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)",
 		"message": "maksymalny czas bezczynności (ms)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "usuwaj obrazy dla alternatywnych rozdzielczości ekranu",
 		"message": "usuwaj obrazy dla alternatywnych rozdzielczości ekranu",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/ru/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальное время простоя (мс)",
 		"message": "максимальное время простоя (мс)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "удалить изображения для альтернативных разрешений экрана",
 		"message": "удалить изображения для альтернативных разрешений экрана",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/uk/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальний час простою (мс)",
 		"message": "максимальний час простою (мс)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "видалити зображення для альтернативних розширень екрану ",
 		"message": "видалити зображення для альтернативних розширень екрану ",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -235,6 +235,10 @@
 		"message": "最长空闲时间(毫秒)",
 		"message": "最长空闲时间(毫秒)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "移除用于备选分辨率的图片",
 		"message": "移除用于备选分辨率的图片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -235,6 +235,10 @@
 		"message": "最長空閒時間(毫秒)",
 		"message": "最長空閒時間(毫秒)",
 		"description": "Options page label: '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": {
 	"optionRemoveAlternativeImages": {
 		"message": "移除用於備選分辨率的圖片",
 		"message": "移除用於備選分辨率的圖片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
 		"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 tabId = taskInfo.tab.id;
 		const taskId = taskInfo.id;
 		const taskId = taskInfo.id;
 		taskInfo.cancelled = true;
 		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) {
 		if (taskInfo.cancel) {
 			taskInfo.cancel();
 			taskInfo.cancel();
 		}
 		}

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

@@ -43,6 +43,7 @@ singlefile.extension.core.bg.config = (() => {
 		loadDeferredImagesMaxIdleTime: 1500,
 		loadDeferredImagesMaxIdleTime: 1500,
 		loadDeferredImagesBlockCookies: false,
 		loadDeferredImagesBlockCookies: false,
 		loadDeferredImagesBlockStorage: false,
 		loadDeferredImagesBlockStorage: false,
+		loadDeferredImagesKeepZoomLevel: false,
 		filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
 		filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
 		infobarTemplate: "",
 		infobarTemplate: "",
 		includeInfobar: false,
 		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();
 					ui.onEndPage();
 					browser.runtime.sendMessage({ method: "ui.processCancelled" });
 					browser.runtime.sendMessage({ method: "ui.processCancelled" });
 				}
 				}
+				if (message.resetZoomLevel) {
+					singlefile.lib.processors.lazy.content.loader.resetZoomLevel();
+				}
 				return {};
 				return {};
 			}
 			}
 			if (message.method == "content.getSelectedLinks") {
 			if (message.method == "content.getSelectedLinks") {
@@ -141,6 +144,9 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 			if (!processor.cancelled) {
 			if (!processor.cancelled) {
 				if (event.type == event.RESOURCES_INITIALIZED) {
 				if (event.type == event.RESOURCES_INITIALIZED) {
 					maxIndex = event.detail.max;
 					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.RESOURCES_INITIALIZED || event.type == event.RESOURCE_LOADED) {
 					if (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 compressCSSLabel = document.getElementById("compressCSSLabel");
 	const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 	const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 	const loadDeferredImagesMaxIdleTimeLabel = document.getElementById("loadDeferredImagesMaxIdleTimeLabel");
 	const loadDeferredImagesMaxIdleTimeLabel = document.getElementById("loadDeferredImagesMaxIdleTimeLabel");
+	const loadDeferredImagesKeepZoomLevelLabel = document.getElementById("loadDeferredImagesKeepZoomLevelLabel");
 	const addMenuEntryLabel = document.getElementById("addMenuEntryLabel");
 	const addMenuEntryLabel = document.getElementById("addMenuEntryLabel");
 	const filenameTemplateLabel = document.getElementById("filenameTemplateLabel");
 	const filenameTemplateLabel = document.getElementById("filenameTemplateLabel");
 	const filenameMaxLengthLabel = document.getElementById("filenameMaxLengthLabel");
 	const filenameMaxLengthLabel = document.getElementById("filenameMaxLengthLabel");
@@ -118,6 +119,7 @@
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 	const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 	const loadDeferredImagesMaxIdleTimeInput = document.getElementById("loadDeferredImagesMaxIdleTimeInput");
 	const loadDeferredImagesMaxIdleTimeInput = document.getElementById("loadDeferredImagesMaxIdleTimeInput");
+	const loadDeferredImagesKeepZoomLevelInput = document.getElementById("loadDeferredImagesKeepZoomLevelInput");
 	const contextMenuEnabledInput = document.getElementById("contextMenuEnabledInput");
 	const contextMenuEnabledInput = document.getElementById("contextMenuEnabledInput");
 	const filenameTemplateInput = document.getElementById("filenameTemplateInput");
 	const filenameTemplateInput = document.getElementById("filenameTemplateInput");
 	const filenameMaxLengthInput = document.getElementById("filenameMaxLengthInput");
 	const filenameMaxLengthInput = document.getElementById("filenameMaxLengthInput");
@@ -437,6 +439,7 @@
 	compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 	compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 	loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 	loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 	loadDeferredImagesMaxIdleTimeLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesMaxIdleTime");
 	loadDeferredImagesMaxIdleTimeLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesMaxIdleTime");
+	loadDeferredImagesKeepZoomLevelLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesKeepZoomLevel");
 	addMenuEntryLabel.textContent = browser.i18n.getMessage("optionAddMenuEntry");
 	addMenuEntryLabel.textContent = browser.i18n.getMessage("optionAddMenuEntry");
 	filenameTemplateLabel.textContent = browser.i18n.getMessage("optionFilenameTemplate");
 	filenameTemplateLabel.textContent = browser.i18n.getMessage("optionFilenameTemplate");
 	filenameMaxLengthLabel.textContent = browser.i18n.getMessage("optionFilenameMaxLength");
 	filenameMaxLengthLabel.textContent = browser.i18n.getMessage("optionFilenameMaxLength");
@@ -620,6 +623,8 @@
 		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages && !profileOptions.saveRawPage;
 		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages && !profileOptions.saveRawPage;
 		loadDeferredImagesInput.disabled = profileOptions.saveRawPage;
 		loadDeferredImagesInput.disabled = profileOptions.saveRawPage;
 		loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
 		loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
+		loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel && !profileOptions.saveRawPage;
+		loadDeferredImagesKeepZoomLevelInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPape;
 		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPage;
 		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPage;
 		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
 		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
 		filenameTemplateInput.value = profileOptions.filenameTemplate;
 		filenameTemplateInput.value = profileOptions.filenameTemplate;
@@ -696,6 +701,7 @@
 				compressCSS: compressCSSInput.checked,
 				compressCSS: compressCSSInput.checked,
 				loadDeferredImages: loadDeferredImagesInput.checked,
 				loadDeferredImages: loadDeferredImagesInput.checked,
 				loadDeferredImagesMaxIdleTime: Math.max(loadDeferredImagesMaxIdleTimeInput.value, 0),
 				loadDeferredImagesMaxIdleTime: Math.max(loadDeferredImagesMaxIdleTimeInput.value, 0),
+				loadDeferredImagesKeepZoomLevel: loadDeferredImagesKeepZoomLevelInput.checked,
 				contextMenuEnabled: contextMenuEnabledInput.checked,
 				contextMenuEnabled: contextMenuEnabledInput.checked,
 				filenameTemplate: filenameTemplateInput.value,
 				filenameTemplate: filenameTemplateInput.value,
 				filenameMaxLength: filenameMaxLengthInput.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
 							for example the network or system conditions are degraded. You can also decrease this value
 							otherwise.</p>
 							otherwise.</p>
 					</li>
 					</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
 					<li data-options-label="removeAlternativeImagesLabel"> <span class="option">Option: remove images
 							for alternative screen resolutions</span>
 							for alternative screen resolutions</span>
 						<p>Check this option to remove images that are alternatives in lower and/or higher resolutions
 						<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>
 				<label for="loadDeferredImagesMaxIdleTimeInput" id="loadDeferredImagesMaxIdleTimeLabel"></label>
 				<input type="number" class="large-input" id="loadDeferredImagesMaxIdleTimeInput" step="100">
 				<input type="number" class="large-input" id="loadDeferredImagesMaxIdleTimeInput" step="100">
 			</div>
 			</div>
+			<div class="option second-level">
+				<label for="loadDeferredImagesKeepZoomLevelInput" id="loadDeferredImagesKeepZoomLevelLabel"></label>
+				<input type="checkbox" id="loadDeferredImagesKeepZoomLevelInput">
+			</div>
 			<div class="option">
 			<div class="option">
 				<label for="removeAlternativeImagesInput" id="removeAlternativeImagesLabel"></label>
 				<label for="removeAlternativeImagesInput" id="removeAlternativeImagesLabel"></label>
 				<input type="checkbox" id="removeAlternativeImagesInput">
 				<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_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
 	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_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
@@ -60,36 +63,41 @@
 	const observers = new Map();
 	const observers = new Map();
 	const observedElements = 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 scrollingElement = document.scrollingElement || document.documentElement;
 		const clientHeight = scrollingElement.clientHeight;
 		const clientHeight = scrollingElement.clientHeight;
 		const clientWidth = scrollingElement.clientWidth;
 		const clientWidth = scrollingElement.clientWidth;
 		const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
 		const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
 		const scrollWidth = Math.max(scrollingElement.scrollWidth - clientWidth, clientWidth);
 		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 => {
 		document.querySelectorAll("[loading=lazy]").forEach(element => {
 			element.loading = "eager";
 			element.loading = "eager";
 			element.setAttribute(LAZY_LOAD_ATTRIBUTE, "");
 			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) {
 		if (!window._singleFileImage) {
 			const Image = window.Image;
 			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);
 		const zoomFactor = Math.min(zoomFactorX, zoomFactorY);
 		if (zoomFactor < 1) {
 		if (zoomFactor < 1) {
 			const transform = document.documentElement.style.getPropertyValue("transform");
 			const transform = document.documentElement.style.getPropertyValue("transform");
@@ -135,54 +149,78 @@
 			document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
 			document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
 			dispatchEvent.call(window, new UIEvent("resize"));
 			dispatchEvent.call(window, new UIEvent("resize"));
 			dispatchEvent.call(window, new UIEvent("scroll"));
 			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;
 		const scrollingElement = document.scrollingElement || document.documentElement;
 		document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
 		document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
 			element.loading = "lazy";
 			element.loading = "lazy";
 			element.removeAttribute(LAZY_LOAD_ATTRIBUTE);
 			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) {
 		if (window._singleFileImage) {
 			delete window.Image;
 			delete window.Image;
 			window.Image = window._singleFileImage;
 			window.Image = window._singleFileImage;
 			delete 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, () => {
 	addEventListener.call(window, BLOCK_COOKIES_START_EVENT, () => {
 		try {
 		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_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
 	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_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
 	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) {
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_START_EVENT));
 				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 => {
 		loadDeferredImagesEnd: options => {
 			if (options.loadDeferredImagesBlockCookies) {
 			if (options.loadDeferredImagesBlockCookies) {
@@ -89,7 +96,14 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 			if (options.loadDeferredImagesBlockStorage) {
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_END_EVENT));
 				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,
 		LOAD_IMAGE_EVENT,
 		IMAGE_LOADED_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) {
 			if (scrollY <= maxScrollY && scrollX <= maxScrollX) {
 				return process(options);
 				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 = [{
 	const STAGES = [{
 		sequential: [
 		sequential: [
 			{ action: "preProcessPage" },
 			{ action: "preProcessPage" },
+			{ option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
 			{ action: "replaceStyleContents" },
 			{ action: "replaceStyleContents" },
 			{ action: "resetCharsetMeta" },
 			{ action: "resetCharsetMeta" },
 			{ option: "saveFavicon", action: "saveFavicon" },
 			{ 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() {
 		async insertMAFFMetaData() {
 			const maffMetaData = await this.maffMetaDataPromise;
 			const maffMetaData = await this.maffMetaDataPromise;
 			if (maffMetaData && maffMetaData.content) {
 			if (maffMetaData && maffMetaData.content) {