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

improved image grouping implementation with global variables

Gildas 7 лет назад
Родитель
Сommit
e1409ebea9

+ 0 - 3
extension/core/bg/script-loader.js

@@ -68,9 +68,6 @@ singlefile.scriptLoader = (() => {
 		],
 		],
 		lazyLoadImages: [
 		lazyLoadImages: [
 			"/lib/single-file/lazy-loader.js"
 			"/lib/single-file/lazy-loader.js"
-		],
-		groupDuplicateImages: [
-			"lib/single-file/html-images-minifier.js"
 		]
 		]
 	};
 	};
 
 

+ 14 - 10
lib/single-file/doc-helper.js

@@ -28,6 +28,7 @@ this.docHelper = this.docHelper || (() => {
 	const RESPONSIVE_IMAGE_ATTRIBUTE_NAME = "data-single-file-responsive-image";
 	const RESPONSIVE_IMAGE_ATTRIBUTE_NAME = "data-single-file-responsive-image";
 	const IMAGE_ATTRIBUTE_NAME = "data-single-file-image";
 	const IMAGE_ATTRIBUTE_NAME = "data-single-file-image";
 	const INPUT_VALUE_ATTRIBUTE_NAME = "data-single-file-value";
 	const INPUT_VALUE_ATTRIBUTE_NAME = "data-single-file-value";
+	const SHEET_ATTRIBUTE_NAME = "data-single-file-sheet";
 
 
 	return {
 	return {
 		preProcessDoc,
 		preProcessDoc,
@@ -38,7 +39,8 @@ this.docHelper = this.docHelper || (() => {
 		removedContentAttributeName,
 		removedContentAttributeName,
 		responsiveImagesAttributeName,
 		responsiveImagesAttributeName,
 		imagesAttributeName,
 		imagesAttributeName,
-		inputValueAttributeName
+		inputValueAttributeName,
+		sheetAttributeName
 	};
 	};
 
 
 	function preProcessDoc(doc, win, options) {
 	function preProcessDoc(doc, win, options) {
@@ -100,23 +102,27 @@ this.docHelper = this.docHelper || (() => {
 	}
 	}
 
 
 	function imagesAttributeName(sessionId) {
 	function imagesAttributeName(sessionId) {
-		return IMAGE_ATTRIBUTE_NAME + (sessionId ? "-" + sessionId : "");
+		return IMAGE_ATTRIBUTE_NAME + (sessionId || "");
 	}
 	}
 
 
 	function preservedSpaceAttributeName(sessionId) {
 	function preservedSpaceAttributeName(sessionId) {
-		return PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME + (sessionId ? "-" + sessionId : "");
+		return PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME + (sessionId || "");
 	}
 	}
 
 
 	function removedContentAttributeName(sessionId) {
 	function removedContentAttributeName(sessionId) {
-		return REMOVED_CONTENT_ATTRIBUTE_NAME + (sessionId ? "-" + sessionId : "");
+		return REMOVED_CONTENT_ATTRIBUTE_NAME + (sessionId || "");
 	}
 	}
 
 
 	function windowIdAttributeName(sessionId) {
 	function windowIdAttributeName(sessionId) {
-		return WIN_ID_ATTRIBUTE_NAME + (sessionId ? "-" + sessionId : "");
+		return WIN_ID_ATTRIBUTE_NAME + (sessionId || "");
 	}
 	}
 
 
 	function inputValueAttributeName(sessionId) {
 	function inputValueAttributeName(sessionId) {
-		return INPUT_VALUE_ATTRIBUTE_NAME + (sessionId ? "-" + sessionId : "");
+		return INPUT_VALUE_ATTRIBUTE_NAME + (sessionId || "");
+	}
+
+	function sheetAttributeName(sessionId) {
+		return SHEET_ATTRIBUTE_NAME + (sessionId || "");
 	}
 	}
 
 
 	function getCanvasData(doc) {
 	function getCanvasData(doc) {
@@ -180,10 +186,8 @@ this.docHelper = this.docHelper || (() => {
 		const height = imageElement.clientHeight;
 		const height = imageElement.clientHeight;
 		if (width >= 0 && height >= 0 && paddingLeft >= 0 && paddingRight >= 0 && paddingTop >= 0 && paddingBottom >= 0) {
 		if (width >= 0 && height >= 0 && paddingLeft >= 0 && paddingRight >= 0 && paddingTop >= 0 && paddingBottom >= 0) {
 			return {
 			return {
-				width,
-				height,
-				contentWidth: width - paddingLeft - paddingRight,
-				contentHeight: height - paddingTop - paddingBottom,
+				width: Math.ceil(width - paddingLeft - paddingRight),
+				height: Math.floor(height - paddingTop - paddingBottom),
 			};
 			};
 		}
 		}
 	}
 	}

+ 0 - 87
lib/single-file/html-images-minifier.js

@@ -1,87 +0,0 @@
-/*
- * Copyright 2018 Gildas Lormeau
- * contact : gildas.lormeau <at> gmail.com
- * 
- * This file is part of SingleFile.
- *
- *   SingleFile is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU Lesser General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   SingleFile is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU Lesser General Public License for more details.
- *
- *   You should have received a copy of the GNU Lesser General Public License
- *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/* global docHelper */
-
-this.imagesMinifier = this.imagesMinifier || (() => {
-
-	return {
-		process: (doc, options) => {
-			const imageGroups = getImageGroups(doc);
-			const duplicates = new Set();
-			imageGroups.forEach((elements, src) => {
-				if (elements.length > 1 && src && src != doc.baseURI) {
-					elements.forEach(element => duplicates.add(element));
-				}
-			});
-			const customProperties = new Map();
-			if (duplicates.size) {
-				processImages(doc, duplicates, options, customProperties);
-			}
-			const styleElement = doc.createElement("style");
-			let sheetContent = "";
-			customProperties.forEach((variableName, src) => {
-				if (sheetContent) {
-					sheetContent += ";";
-				}
-				sheetContent += variableName + ":url(\"" + src + "\")";
-			});
-			styleElement.textContent = ":root{" + sheetContent + "}";
-			doc.head.appendChild(styleElement);
-		},
-		postProcess: () => { }
-	};
-
-	function getImageGroups(doc) {
-		const imageGroups = new Map();
-		doc.querySelectorAll("img[src]:not([srcset])").forEach(imageElement => {
-			if (imageElement.src) {
-				let imageInfo = imageGroups.get(imageElement.src);
-				if (!imageInfo) {
-					imageInfo = [];
-					imageGroups.set(imageElement.src, imageInfo);
-				}
-				imageInfo.push(imageElement);
-			}
-		});
-		return imageGroups;
-	}
-
-	function processImages(doc, duplicates, options, customProperties) {
-		doc.querySelectorAll("img[src]:not([srcset])").forEach((imgElement, imgIndex) => {
-			if (duplicates.has(imgElement) && imgElement.style.getPropertyValue("background-image")) {
-				const src = imgElement.getAttribute("src");
-				const dataAttributeName = docHelper.imagesAttributeName(options.sessionId);
-				const imageData = options.imageData[Number(imgElement.getAttribute(dataAttributeName))];
-				imgElement.setAttribute("src", "data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + imageData.contentWidth + "\" height=\"" + imageData.contentHeight + "\"><rect fill-opacity=\"0\"/></svg>");
-				let variableName = customProperties.get(src);
-				if (!variableName) {
-					variableName = "--single-file-image-" + imgIndex;
-					customProperties.set(src, variableName);
-				}
-				imgElement.style.setProperty("background-image", "var(" + variableName + ")", "important");
-				imgElement.style.setProperty("background-size", imageData.contentWidth + "px " + imageData.contentHeight + "px", "important");
-				imgElement.style.setProperty("background-origin", "content-box", "important");
-				imgElement.style.setProperty("background-repeat", "no-repeat", "important");
-			}
-		});
-	}
-
-})();

+ 5 - 5
lib/single-file/single-file-browser.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
  */
 
 
-/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, lazyLoader, serializer, docHelper, mediasMinifier, TextEncoder, crypto, RulesMatcher, imagesMinifier */
+/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, lazyLoader, serializer, docHelper, mediasMinifier, TextEncoder, crypto, RulesMatcher */
 
 
 this.SingleFile = this.SingleFile || (() => {
 this.SingleFile = this.SingleFile || (() => {
 
 
@@ -208,10 +208,6 @@ this.SingleFile = this.SingleFile || (() => {
 			return rulesMatcher.getMediaAllInfo();
 			return rulesMatcher.getMediaAllInfo();
 		}
 		}
 
 
-		static minifyImages(doc, mediaAllInfo, options) {
-			return imagesMinifier.process(doc, mediaAllInfo, options);
-		}
-
 		static compressCSS(content, options) {
 		static compressCSS(content, options) {
 			return uglifycss.processString(content, options);
 			return uglifycss.processString(content, options);
 		}
 		}
@@ -263,6 +259,10 @@ this.SingleFile = this.SingleFile || (() => {
 		static inputValueAttributeName(sessionId) {
 		static inputValueAttributeName(sessionId) {
 			return docHelper.inputValueAttributeName(sessionId);
 			return docHelper.inputValueAttributeName(sessionId);
 		}
 		}
+
+		static sheetAttributeName(sessionId) {
+			return docHelper.sheetAttributeName(sessionId);
+		}
 	}
 	}
 
 
 	function log(...args) {
 	function log(...args) {

+ 84 - 57
lib/single-file/single-file-core.js

@@ -99,7 +99,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	}, {
 	}, {
 		sequential: [
 		sequential: [
 			{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
 			{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
-			{ option: "groupDuplicateImages", action: "groupDuplicateImages" },
 			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
 			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
 			{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" }
 			{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" }
 		],
 		],
@@ -260,7 +259,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 					const resourceContent = await Download.getContent(resourceURL, { asDataURI, maxResourceSize: options.maxResourceSize, maxResourceSizeEnabled: options.maxResourceSizeEnabled });
 					const resourceContent = await Download.getContent(resourceURL, { asDataURI, maxResourceSize: options.maxResourceSize, maxResourceSizeEnabled: options.maxResourceSizeEnabled });
 					indexResource = indexResource + 1;
 					indexResource = indexResource + 1;
 					onloadListener({ index: indexResource, url: resourceURL });
 					onloadListener({ index: indexResource, url: resourceURL });
-					resourceRequests.forEach(resourceRequest => resourceRequest.resolve(resourceContent));
+					resourceRequests.forEach(resourceRequest => resourceRequest.resolve({ content: resourceContent, indexResource, duplicate: Boolean(resourceRequests.length > 1) }));
 				} catch (error) {
 				} catch (error) {
 					indexResource = indexResource + 1;
 					indexResource = indexResource + 1;
 					onloadListener({ index: indexResource, url: resourceURL });
 					onloadListener({ index: indexResource, url: resourceURL });
@@ -578,12 +577,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 			}
 		}
 		}
 
 
-		groupDuplicateImages() {
-			if (this.options.imageData) {
-				DOM.minifyImages(this.doc, this.options);
-			}
-		}
-
 		removeAlternativeMedias() {
 		removeAlternativeMedias() {
 			const stats = DOM.minifyMedias(this.doc);
 			const stats = DOM.minifyMedias(this.doc);
 			this.stats.set("processed", "medias", stats.processed);
 			this.stats.set("processed", "medias", stats.processed);
@@ -682,26 +675,26 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 
 		async pageResources() {
 		async pageResources() {
 			const resourcePromises = [
 			const resourcePromises = [
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest),
-				DomProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.baseURI, this.batchRequest),
-				DomProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options, true),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options),
+				DomProcessorHelper.processXLinks(this.doc, this.doc.querySelectorAll("use"), this.baseURI, this.batchRequest),
+				DomProcessorHelper.processSrcset(this.doc, this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)
 			];
 			];
 			if (!this.options.removeAudioSrc) {
 			if (!this.options.removeAudioSrc) {
-				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.batchRequest));
+				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.batchRequest, this.options));
 			}
 			}
 			if (!this.options.removeVideoSrc) {
 			if (!this.options.removeVideoSrc) {
-				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.batchRequest));
+				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.batchRequest, this.options));
 			}
 			}
 			if (this.options.lazyLoadImages) {
 			if (this.options.lazyLoadImages) {
 				const imageSelectors = DOM.lazyLoaderImageSelectors();
 				const imageSelectors = DOM.lazyLoaderImageSelectors();
-				Object.keys(imageSelectors.src).forEach(selector => resourcePromises.push(DomProcessorHelper.processAttribute(this.doc.querySelectorAll(selector), imageSelectors.src[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
-				Object.keys(imageSelectors.srcset).forEach(selector => resourcePromises.push(DomProcessorHelper.processSrcset(this.doc.querySelectorAll(selector), imageSelectors.srcset[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
+				Object.keys(imageSelectors.src).forEach(selector => resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll(selector), imageSelectors.src[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest, this.options)));
+				Object.keys(imageSelectors.srcset).forEach(selector => resourcePromises.push(DomProcessorHelper.processSrcset(this.doc, this.doc.querySelectorAll(selector), imageSelectors.srcset[selector], PREFIX_DATA_URI_IMAGE, this.baseURI, this.batchRequest)));
 			}
 			}
 			await resourcePromises;
 			await resourcePromises;
 		}
 		}
@@ -711,9 +704,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 		}
 
 
 		async processStylesheets() {
 		async processStylesheets() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async (styleElement, indexStyle) => {
+			await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
 				this.stats.add("processed", "stylesheets", 1);
 				this.stats.add("processed", "stylesheets", 1);
-				styleElement.textContent = await DomProcessorHelper.processStylesheet(styleElement.textContent, styleElement.sheet.cssRules, this.baseURI, this.options, false, indexStyle, this.batchRequest);
+				styleElement.textContent = await DomProcessorHelper.processStylesheet(this.doc, styleElement.textContent, styleElement.sheet.cssRules, this.baseURI, this.options, this.batchRequest);
 			}));
 			}));
 		}
 		}
 
 
@@ -835,7 +828,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 		}
 
 
 		async processStyleAttributes() {
 		async processStyleAttributes() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => element.setAttribute("style", await DomProcessorHelper.processStylesheet(element.getAttribute("style"), [{ type: CSSRule.STYLE_RULE, cssText: element.getAttribute("style") }], this.baseURI, this.options, true, 0, this.batchRequest))));
+			await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => element.setAttribute("style", await DomProcessorHelper.processStylesheet(this.doc, element.getAttribute("style"), [{ type: CSSRule.STYLE_RULE, cssText: element.getAttribute("style") }], this.baseURI, this.options, this.batchRequest))));
 		}
 		}
 
 
 		async resolveLinkedStylesheetURLs() {
 		async resolveLinkedStylesheetURLs() {
@@ -863,6 +856,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
 	const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
 	const PREFIX_DATA_URI_NO_MIMETYPE = "data:;";
 	const PREFIX_DATA_URI_NO_MIMETYPE = "data:;";
 	const PREFIX_DATA_URI_OCTET_STREAM = "data:application/octet-stream";
 	const PREFIX_DATA_URI_OCTET_STREAM = "data:application/octet-stream";
+	const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
 
 
 	class DomProcessorHelper {
 	class DomProcessorHelper {
 		static async getFilename(options, content) {
 		static async getFilename(options, content) {
@@ -1016,22 +1010,22 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 			}
 		}
 		}
 
 
-		static async processStylesheet(stylesheetContent, cssRules, baseURI, options, inline, indexStyle, batchRequest) {
+		static async processStylesheet(doc, stylesheetContent, cssRules, baseURI, options, batchRequest) {
 			let sheetContent = "", variablesInfo = { index: 0, cssText: "" };
 			let sheetContent = "", variablesInfo = { index: 0, cssText: "" };
 			const urlFunctions = DomUtil.getUrlFunctions(stylesheetContent);
 			const urlFunctions = DomUtil.getUrlFunctions(stylesheetContent);
-			const variableNames = new Map();
 			const urlFunctionData = new Map();
 			const urlFunctionData = new Map();
 			await Promise.all(urlFunctions.map(async urlFunction => {
 			await Promise.all(urlFunctions.map(async urlFunction => {
 				const originalResourceURL = DomUtil.matchURL(urlFunction);
 				const originalResourceURL = DomUtil.matchURL(urlFunction);
 				const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 				const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 				if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && stylesheetContent.includes(urlFunction)) {
 				if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && stylesheetContent.includes(urlFunction)) {
-					const dataURI = await batchRequest.addURL(resourceURL);
+					const { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL);
 					const regExpUrlFunction = DomUtil.getRegExp(urlFunction);
 					const regExpUrlFunction = DomUtil.getRegExp(urlFunction);
-					const functions = stylesheetContent.match(regExpUrlFunction);
-					if (options.groupDuplicateImages && functions.length > 1) {
-						variableNames.set(urlFunction, "--single-file-" + indexStyle + "-" + variableNames.size);
+					if (duplicate && options.groupDuplicateImages) {
+						urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI: content, variableName: "var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")" });
+						DomUtil.insertVariable(doc, indexResource, content, options);
+					} else {
+						urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI: content });
 					}
 					}
-					urlFunctionData.set(urlFunction, { regExpUrlFunction, dataURI });
 				}
 				}
 			}));
 			}));
 			const rulesContent = processRules(cssRules);
 			const rulesContent = processRules(cssRules);
@@ -1047,7 +1041,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 						const mediaRulesContent = processRules(cssRule.cssRules);
 						const mediaRulesContent = processRules(cssRule.cssRules);
 						rulesContent += "@media " + Array.from(cssRule.media).join(",") + "{" + mediaRulesContent + "}";
 						rulesContent += "@media " + Array.from(cssRule.media).join(",") + "{" + mediaRulesContent + "}";
 					} else if (cssRule.type == CSSRule.STYLE_RULE) {
 					} else if (cssRule.type == CSSRule.STYLE_RULE) {
-						rulesContent += processURLFunctions(cssRule.cssText, !inline && options.compressCSS);
+						rulesContent += processURLFunctions(cssRule.cssText);
 					} else {
 					} else {
 						rulesContent += processURLFunctions(cssRule.cssText);
 						rulesContent += processURLFunctions(cssRule.cssText);
 					}
 					}
@@ -1055,28 +1049,17 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				return rulesContent;
 				return rulesContent;
 			}
 			}
 
 
-			function processURLFunctions(cssText, useCustomProperties) {
+			function processURLFunctions(cssText) {
 				const urlFunctions = DomUtil.getUrlFunctions(cssText);
 				const urlFunctions = DomUtil.getUrlFunctions(cssText);
 				urlFunctions.forEach(urlFunction => {
 				urlFunctions.forEach(urlFunction => {
 					const originalResourceURL = DomUtil.matchURL(urlFunction);
 					const originalResourceURL = DomUtil.matchURL(urlFunction);
 					const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 					const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && cssText.includes(urlFunction)) {
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL) && cssText.includes(urlFunction)) {
 						const resourceInfo = urlFunctionData.get(urlFunction);
 						const resourceInfo = urlFunctionData.get(urlFunction);
-						const dataURI = resourceInfo.dataURI;
-						if (useCustomProperties) {
-							const variableName = variableNames.get(urlFunction);
-							if (variableName) {
-								cssText = cssText.replace(resourceInfo.regExpUrlFunction, "var(" + variableName + ")");
-								if (variablesInfo.cssText) {
-									variablesInfo.cssText += ";";
-								}
-								variablesInfo.cssText += variableName + ":url(\"" + dataURI + "\")";
-								variablesInfo.index++;
-							} else {
-								cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, dataURI));
-							}
+						if (options.groupDuplicateImages && resourceInfo.variableName) {
+							cssText = cssText.replace(resourceInfo.regExpUrlFunction, resourceInfo.variableName);
 						} else {
 						} else {
-							cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, dataURI));
+							cssText = cssText.replace(resourceInfo.regExpUrlFunction, urlFunction.replace(originalResourceURL, resourceInfo.dataURI));
 						}
 						}
 					}
 					}
 				});
 				});
@@ -1084,16 +1067,25 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 			}
 		}
 		}
 
 
-		static async processAttribute(resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
+		static async processAttribute(doc, resourceElements, attributeName, prefixDataURI, baseURI, batchRequest, options, processDuplicates) {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 				let resourceURL = resourceElement.getAttribute(attributeName);
 				let resourceURL = resourceElement.getAttribute(attributeName);
 				if (resourceURL) {
 				if (resourceURL) {
 					resourceURL = DomUtil.normalizeURL(resourceURL);
 					resourceURL = DomUtil.normalizeURL(resourceURL);
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 						try {
 						try {
-							const dataURI = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
-							if (dataURI.startsWith(prefixDataURI) || dataURI.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) || dataURI.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
-								resourceElement.setAttribute(attributeName, dataURI);
+							const { content, indexResource, duplicate } = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
+							if (content.startsWith(prefixDataURI) || content.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) || content.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
+								if (processDuplicates && duplicate && options.groupDuplicateImages) {
+									if (resourceElement.style.getPropertyValue("background-image")) {
+										resourceElement.setAttribute(attributeName, content);
+									} else {
+										DomUtil.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options);
+										DomUtil.insertVariable(doc, indexResource, content, options);
+									}
+								} else {
+									resourceElement.setAttribute(attributeName, content);
+								}
 							} else {
 							} else {
 								resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 								resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 							}
 							}
@@ -1105,7 +1097,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}));
 			}));
 		}
 		}
 
 
-		static async processXLinks(resourceElements, baseURI, batchRequest) {
+		static async processXLinks(doc, resourceElements, baseURI, batchRequest) {
 			const attributeName = "xlink:href";
 			const attributeName = "xlink:href";
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 				const originalResourceURL = resourceElement.getAttribute(attributeName);
 				const originalResourceURL = resourceElement.getAttribute(attributeName);
@@ -1113,7 +1105,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 					const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 					const resourceURL = DomUtil.normalizeURL(originalResourceURL);
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 						try {
 						try {
-							const content = await batchRequest.addURL(new URL(resourceURL, baseURI).href, false);
+							const { content } = await batchRequest.addURL(new URL(resourceURL, baseURI).href, false);
 							const DOMParser = DOM.getParser();
 							const DOMParser = DOM.getParser();
 							if (DOMParser) {
 							if (DOMParser) {
 								const svgDoc = new DOMParser().parseFromString(content, "image/svg+xml");
 								const svgDoc = new DOMParser().parseFromString(content, "image/svg+xml");
@@ -1138,18 +1130,18 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}));
 			}));
 		}
 		}
 
 
-		static async processSrcset(resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
+		static async processSrcset(doc, resourceElements, attributeName, prefixDataURI, baseURI, batchRequest) {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 				const srcset = DOM.parseSrcset(resourceElement.getAttribute(attributeName));
 				const srcset = DOM.parseSrcset(resourceElement.getAttribute(attributeName));
 				const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
 				const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
 					const resourceURL = DomUtil.normalizeURL(srcsetValue.url);
 					const resourceURL = DomUtil.normalizeURL(srcsetValue.url);
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 					if (resourceURL && resourceURL != baseURI && DomUtil.testValidPath(resourceURL)) {
 						try {
 						try {
-							let dataURI = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
-							if (!dataURI.startsWith(prefixDataURI) && !dataURI.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) && !dataURI.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
+							const { content } = await batchRequest.addURL(new URL(resourceURL, baseURI).href);
+							if (!content.startsWith(prefixDataURI) && !content.startsWith(PREFIX_DATA_URI_NO_MIMETYPE) && !content.startsWith(PREFIX_DATA_URI_OCTET_STREAM)) {
 								resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 								resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 							}
 							}
-							return dataURI + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
+							return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
 						} catch (error) {
 						} catch (error) {
 							resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 							resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
 						}
 						}
@@ -1277,6 +1269,41 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 			}
 		}
 		}
 
 
+		static insertVariable(doc, indexResource, content, options) {
+			const sheetAttributeName = DOM.sheetAttributeName(options.sessionId);
+			let styleElement = doc.querySelector("style[" + sheetAttributeName + "]"), insertedVariables;
+			if (!styleElement) {
+				styleElement = doc.createElement("style");
+				if (doc.head.firstChild) {
+					doc.head.insertBefore(styleElement, doc.head.firstChild);
+				} else {
+					doc.head.appendChild(styleElement);
+				}
+				styleElement.setAttribute(sheetAttributeName, "[]");
+				insertedVariables = [];
+			} else {
+				insertedVariables = JSON.parse(styleElement.getAttribute(sheetAttributeName));
+			}
+			if (!insertedVariables.includes(indexResource)) {
+				insertedVariables.push(indexResource);
+				styleElement.textContent = styleElement.textContent + `:root{${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")}`;
+				styleElement.setAttribute(sheetAttributeName, JSON.stringify(insertedVariables));
+			}
+		}
+
+		static replaceImageSource(imgElement, variableName, options) {
+			const dataAttributeName = DOM.imagesAttributeName(options.sessionId);
+			if (imgElement.getAttribute(dataAttributeName) != null) {
+				const imgData = options.imageData[Number(imgElement.getAttribute(dataAttributeName))];
+				imgElement.setAttribute("src", `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="${imgData.width}" height="${imgData.height}"><rect fill-opacity="0"/></svg>`);
+				imgElement.style.setProperty("background-image", "var(" + variableName + ")", "important");
+				imgElement.style.setProperty("background-size", `${imgData.width}px ${imgData.height}px`, "important");
+				imgElement.style.setProperty("background-origin", "content-box", "important");
+				imgElement.style.setProperty("background-repeat", "no-repeat", "important");
+				imgElement.removeAttribute(dataAttributeName);
+			}
+		}
+
 	}
 	}
 
 
 	function log(...args) {
 	function log(...args) {

+ 0 - 1
manifest.json

@@ -68,7 +68,6 @@
 			"lib/single-file/css-rules-minifier.js",
 			"lib/single-file/css-rules-minifier.js",
 			"lib/single-file/css-srcset-parser.js",
 			"lib/single-file/css-srcset-parser.js",
 			"lib/single-file/html-minifier.js",
 			"lib/single-file/html-minifier.js",
-			"lib/single-file/html-images-minifier.js",
 			"lib/single-file/html-serializer.js",
 			"lib/single-file/html-serializer.js",
 			"lib/single-file/lazy-loader.js",
 			"lib/single-file/lazy-loader.js",
 			"lib/single-file/single-file-core.js",
 			"lib/single-file/single-file-core.js",