html-images-minifier.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * Copyright 2018 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * SingleFile is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Lesser General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SingleFile is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /* global CSSRule, docHelper, cssWhat, lazyLoader */
  21. this.imagesMinifier = this.imagesMinifier || (() => {
  22. const DEBUG = false;
  23. const SVG_NS = "http://www.w3.org/2000/svg";
  24. const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
  25. const TRANSFORMED_IMAGE_ATTRIBUTE = "data-single-file-image-transform";
  26. const IGNORED_ATTRIBUTES = ["src", "width", "height", "viewBox", "preserveAspectRatio", "xlink:href"];
  27. return {
  28. process: (doc, mediaAllInfo, options) => {
  29. const imageGroups = getImageGroups(doc);
  30. let duplicates = new Set();
  31. const duplicateURLs = [];
  32. imageGroups.forEach((elements, src) => {
  33. if (elements.length > 1 && src && src != doc.baseURI) {
  34. elements.forEach(element => duplicates.add(element));
  35. duplicateURLs.push(src);
  36. }
  37. });
  38. if (duplicateURLs.length) {
  39. processStyleSheets(doc, duplicates, mediaAllInfo);
  40. processImages(doc, duplicates, duplicateURLs, options);
  41. }
  42. },
  43. postProcess(doc) {
  44. doc.querySelectorAll("svg[" + TRANSFORMED_IMAGE_ATTRIBUTE + "]").forEach(svgElement => {
  45. const useElement = svgElement.childNodes[0];
  46. if (useElement) {
  47. const refImageId = useElement.getAttribute("xlink:href").substring(1);
  48. if (refImageId) {
  49. const refImageElement = doc.getElementById(refImageId);
  50. if (refImageElement && refImageElement.getAttribute("xlink:href").startsWith(PREFIX_DATA_URI_IMAGE_SVG)) {
  51. svgElement.removeAttributeNS(SVG_NS, "preserveAspectRatio");
  52. svgElement.removeAttributeNS(SVG_NS, TRANSFORMED_IMAGE_ATTRIBUTE);
  53. }
  54. }
  55. }
  56. });
  57. }
  58. };
  59. function getImageGroups(doc) {
  60. const imageGroups = new Map();
  61. doc.querySelectorAll("img[src]:not([srcset])").forEach(imageElement => {
  62. if (imageElement.src) {
  63. let imageInfo = imageGroups.get(imageElement.src);
  64. if (!imageInfo) {
  65. imageInfo = [];
  66. imageGroups.set(imageElement.src, imageInfo);
  67. }
  68. imageInfo.push(imageElement);
  69. }
  70. });
  71. return imageGroups;
  72. }
  73. function processStyleSheets(doc, duplicates, mediaAllInfo) {
  74. const matchedSelectors = getMatchedSelectors(duplicates, mediaAllInfo);
  75. doc.querySelectorAll("style").forEach((styleElement, sheetIndex) => {
  76. if (styleElement.sheet) {
  77. const cssRules = styleElement.sheet.cssRules;
  78. let mediaInfo;
  79. if (styleElement.media && styleElement.media != "all") {
  80. mediaInfo = mediaAllInfo.medias.get("style-" + sheetIndex + "-" + styleElement.media);
  81. } else {
  82. mediaInfo = mediaAllInfo;
  83. }
  84. styleElement.textContent = processRules(doc, cssRules, sheetIndex, mediaInfo, matchedSelectors);
  85. }
  86. });
  87. }
  88. function processImages(doc, duplicates, duplicateURLs, options) {
  89. const svgElement = doc.createElementNS(SVG_NS, "svg");
  90. const defsElement = doc.createElementNS(SVG_NS, "defs");
  91. svgElement.setAttributeNS(SVG_NS, "width", 0);
  92. svgElement.setAttributeNS(SVG_NS, "height", 0);
  93. svgElement.setAttributeNS(SVG_NS, "style", "display:none!important");
  94. svgElement.appendChild(defsElement);
  95. duplicateURLs.forEach((src, srcIndex) => {
  96. const imageElement = doc.createElementNS(SVG_NS, "image");
  97. imageElement.setAttribute("xlink:href", src);
  98. imageElement.id = "single-file-" + srcIndex;
  99. defsElement.appendChild(imageElement);
  100. });
  101. doc.body.appendChild(svgElement);
  102. const ignoredAttributeNames = [];
  103. if (options.lazyLoadImages) {
  104. const imageSelectors = lazyLoader.imageSelectors;
  105. Object.keys(imageSelectors.src).forEach(selector => ignoredAttributeNames.push(imageSelectors.src[selector]));
  106. Object.keys(imageSelectors.srcset).forEach(selector => ignoredAttributeNames.push(imageSelectors.srcset[selector]));
  107. }
  108. doc.querySelectorAll("img[src]:not([srcset])").forEach(imgElement => {
  109. let replaceImage = !options.lazyLoadImages;
  110. if (!replaceImage) {
  111. replaceImage = !Object.keys(ignoredAttributeNames).map(key => ignoredAttributeNames[key]).find(attributeName => imgElement.getAttribute(attributeName));
  112. }
  113. if (replaceImage && duplicates.has(imgElement)) {
  114. const urlIndex = duplicateURLs.indexOf(imgElement.src);
  115. if (urlIndex != -1) {
  116. const dataAttributeName = docHelper.imagesAttributeName(options.sessionId);
  117. const imageData = options.imageData[Number(imgElement.getAttribute(dataAttributeName))];
  118. const width = (imageData.naturalWidth > 1 && imageData.naturalWidth) || imageData.width;
  119. const height = (imageData.naturalHeight > 1 && imageData.naturalHeight) || imageData.height;
  120. if (width > 1 && height > 1) {
  121. const svgElement = doc.createElementNS(SVG_NS, "svg");
  122. const useElement = doc.createElementNS(SVG_NS, "use");
  123. svgElement.appendChild(useElement);
  124. imgElement.getAttributeNames().forEach(attributeName => {
  125. try {
  126. if (!IGNORED_ATTRIBUTES.includes(attributeName)) {
  127. svgElement.setAttribute(attributeName, imgElement.getAttribute(attributeName));
  128. }
  129. } catch (error) {
  130. if (!IGNORED_ATTRIBUTES.includes(attributeName)) {
  131. try {
  132. svgElement.setAttributeNS(SVG_NS, attributeName, imgElement.getAttribute(attributeName));
  133. } catch (error) {
  134. /* ignored */
  135. }
  136. }
  137. }
  138. });
  139. svgElement.setAttribute(TRANSFORMED_IMAGE_ATTRIBUTE, "");
  140. svgElement.setAttributeNS(SVG_NS, "viewBox", "0 0 " + width + " " + height);
  141. svgElement.setAttributeNS(SVG_NS, "width", imageData.clientWidth);
  142. svgElement.setAttributeNS(SVG_NS, "height", imageData.clientHeight);
  143. svgElement.setAttributeNS(SVG_NS, "preserveAspectRatio", "none");
  144. useElement.setAttributeNS(SVG_NS, "xlink:href", "#single-file-" + urlIndex);
  145. const imageElement = doc.getElementById("single-file-" + urlIndex);
  146. if (!imageElement.getAttributeNS(SVG_NS, "width") && !imageElement.getAttributeNS(SVG_NS, "height")) {
  147. imageElement.setAttributeNS(SVG_NS, "viewBox", "0 0 " + width + " " + height);
  148. imageElement.setAttributeNS(SVG_NS, "width", width);
  149. imageElement.setAttributeNS(SVG_NS, "height", height);
  150. }
  151. imgElement.parentElement.replaceChild(svgElement, imgElement);
  152. }
  153. }
  154. }
  155. });
  156. }
  157. function getMatchedSelectors(duplicates, parentMediaInfo, matchedRules = new Map()) {
  158. duplicates.forEach(imageElement => {
  159. let elementInfos = parentMediaInfo.elements.get(imageElement);
  160. if (!elementInfos) {
  161. elementInfos = parentMediaInfo.pseudos.get(imageElement);
  162. }
  163. if (elementInfos) {
  164. elementInfos.forEach(elementInfo => {
  165. if (elementInfo.cssRule) {
  166. let selectorInfo = matchedRules.get(elementInfo.cssRule.selectorText);
  167. if (!selectorInfo) {
  168. matchedRules.set(elementInfo.cssRule.selectorText, elementInfo.selectors);
  169. }
  170. }
  171. });
  172. }
  173. });
  174. parentMediaInfo.medias.forEach(mediaInfo => getMatchedSelectors(duplicates, mediaInfo, matchedRules));
  175. return matchedRules;
  176. }
  177. function processRules(doc, cssRules, sheetIndex, mediaInfo, matchedSelectors) {
  178. let sheetContent = "", mediaRuleIndex = 0;
  179. let startTime;
  180. if (DEBUG && cssRules.length > 1) {
  181. startTime = Date.now();
  182. log(" -- STARTED processRules", "rules.length =", cssRules.length);
  183. }
  184. Array.from(cssRules).forEach(cssRule => {
  185. if (cssRule.type == CSSRule.MEDIA_RULE) {
  186. sheetContent += "@media " + Array.from(cssRule.media).join(",") + "{";
  187. sheetContent += processRules(doc, cssRule.cssRules, sheetIndex, mediaInfo.medias.get("rule-" + sheetIndex + "-" + mediaRuleIndex + "-" + cssRule.media.mediaText), matchedSelectors);
  188. mediaRuleIndex++;
  189. sheetContent += "}";
  190. } else if (cssRule.type == CSSRule.STYLE_RULE) {
  191. const selectors = matchedSelectors.get(cssRule.selectorText);
  192. if (selectors) {
  193. selectors.forEach(selector => {
  194. const newSelector = transformSelector(selector);
  195. if (newSelector) {
  196. selectors.push(newSelector);
  197. }
  198. });
  199. const selectorText = selectors.map(selector => cssWhat.stringify([selector])).join(",");
  200. cssRule.selectorText = selectorText;
  201. }
  202. sheetContent += cssRule.cssText;
  203. } else {
  204. sheetContent += cssRule.cssText;
  205. }
  206. });
  207. if (DEBUG && cssRules.length > 1) {
  208. log(" -- ENDED processRules delay =", Date.now() - startTime);
  209. }
  210. return sheetContent;
  211. }
  212. function transformSelector(selector) {
  213. selector = JSON.parse(JSON.stringify(selector));
  214. let simpleSelector, selectorIndex = selector.length - 1, imageTagFound;
  215. while (selectorIndex >= 0 && !imageTagFound) {
  216. simpleSelector = selector[selectorIndex];
  217. imageTagFound = simpleSelector.type == "tag" && simpleSelector.name == "img";
  218. if (!imageTagFound) {
  219. selectorIndex--;
  220. }
  221. }
  222. if (imageTagFound) {
  223. simpleSelector.name = "svg";
  224. return selector;
  225. }
  226. }
  227. function log(...args) {
  228. console.log("S-File <img-min>", ...args); // eslint-disable-line no-console
  229. }
  230. })();