single-file-core 1.5.110 → 1.5.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/helper.js CHANGED
@@ -75,6 +75,8 @@ const COMMENT_HEADER_LEGACY = "Archive processed by SingleFile";
75
75
  const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
76
76
  const INFOBAR_TAGNAME = infobar.INFOBAR_TAGNAME;
77
77
  const EMPTY_RESOURCE = "data:,";
78
+ const POSTER_CONTENT_TYPES = ["image/webp", "image/jpeg"];
79
+ const POSTER_QUALITY = 0.8;
78
80
  const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", "\"", "<", ">", "\\\\", "\x00-\x1f", "\x7F"];
79
81
  const DEFAULT_REPLACEMENT_CHARACTER = "_";
80
82
  const DEFAULT_REPLACEMENT_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", """, "<", ">", "\"];
@@ -143,9 +145,24 @@ export {
143
145
  WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
144
146
  MESSAGE_PREFIX,
145
147
  NO_SCRIPT_PROPERTY_NAME,
146
- NESTING_TRACK_ID_ATTRIBUTE_NAME
148
+ NESTING_TRACK_ID_ATTRIBUTE_NAME,
149
+ getPosterDataURI
147
150
  };
148
151
 
152
+ // a poster is a still of a video the reader cannot play, so it is stored in a
153
+ // lossy format: the same frame costs an order of magnitude less than as a PNG.
154
+ // toDataURL falls back to PNG without reporting it when it does not support the
155
+ // format, so the type it returned is tested instead of assumed
156
+ function getPosterDataURI(canvasElement) {
157
+ for (const contentType of POSTER_CONTENT_TYPES) {
158
+ const dataURI = canvasElement.toDataURL(contentType, POSTER_QUALITY);
159
+ if (dataURI.startsWith("data:" + contentType)) {
160
+ return dataURI;
161
+ }
162
+ }
163
+ return canvasElement.toDataURL("image/png");
164
+ }
165
+
149
166
  let userScriptHandlerObserver;
150
167
 
151
168
  function initUserScriptHandler() {
@@ -534,14 +551,16 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
534
551
  });
535
552
  element.setAttribute(VIDEO_ATTRIBUTE_NAME, data.videos.length - 1);
536
553
  }
537
- if (!element.getAttribute("poster")) {
554
+ // the posters are only inserted when the videos are blocked, capturing them
555
+ // otherwise encodes a frame per video for nothing
556
+ if (options.blockVideos && !element.getAttribute("poster")) {
538
557
  const canvasElement = doc.createElement("canvas");
539
558
  const context = canvasElement.getContext("2d");
540
559
  canvasElement.width = element.videoWidth;
541
560
  canvasElement.height = element.videoHeight;
542
561
  try {
543
562
  context.drawImage(element, 0, 0, canvasElement.width, canvasElement.height);
544
- data.posters.push(canvasElement.toDataURL("image/png"));
563
+ data.posters.push(getPosterDataURI(canvasElement));
545
564
  element.setAttribute(POSTER_ATTRIBUTE_NAME, data.posters.length - 1);
546
565
  data.markedElements.push(element);
547
566
  // eslint-disable-next-line no-unused-vars
package/core/index.js CHANGED
@@ -155,6 +155,7 @@ const STAGES = [{
155
155
  { action: "replaceStylesheets" },
156
156
  { action: "replaceStyleAttributes" },
157
157
  { action: "insertVariables" },
158
+ { action: "removeEmptyMediaAutoplay" },
158
159
  { option: "compressHTML", action: "compressHTML" },
159
160
  { action: "cleanupPage" }
160
161
  ],
@@ -824,6 +825,25 @@ class Processor {
824
825
  });
825
826
  }
826
827
 
828
+ // a media element left without any source can never start, and the attribute
829
+ // would keep it announcing a playback that never happens. The sources are
830
+ // dropped whenever they could not be stored, so this is not specific to
831
+ // blocked videos, and it runs on the final state to leave alone an element
832
+ // that kept one of several sources
833
+ removeEmptyMediaAutoplay() {
834
+ this.doc.querySelectorAll("video[autoplay], audio[autoplay]").forEach(element => {
835
+ const sourceElements = Array.from(element.querySelectorAll("source"));
836
+ if (!hasSource(element) && !sourceElements.some(hasSource)) {
837
+ element.removeAttribute("autoplay");
838
+ }
839
+ });
840
+
841
+ function hasSource(element) {
842
+ const src = element.getAttribute("src");
843
+ return Boolean(src) && src != util.EMPTY_RESOURCE;
844
+ }
845
+ }
846
+
827
847
  removeFrames() {
828
848
  const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
829
849
  this.stats.set("discarded", "frames", frameElements.length);
@@ -1195,7 +1215,7 @@ class Processor {
1195
1215
  canvasElement.height = videoData.size.videoHeight;
1196
1216
  context.drawImage(temporaryVideoElement, 0, 0, canvasElement.width, canvasElement.height);
1197
1217
  try {
1198
- videoElement.poster = canvasElement.toDataURL("image/png");
1218
+ videoElement.poster = util.getPosterDataURI(canvasElement);
1199
1219
  // eslint-disable-next-line no-unused-vars
1200
1220
  } catch (error) {
1201
1221
  videoElement.poster = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='" + videoData.size.videoWidth + "' height='" + videoData.size.videoHeight + "'%3E%3C/svg%3E";
package/core/util.js CHANGED
@@ -225,7 +225,8 @@ function getInstance(utilOptions) {
225
225
  INFOBAR_TAGNAME: helper.INFOBAR_TAGNAME,
226
226
  WAIT_FOR_USERSCRIPT_PROPERTY_NAME: helper.WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
227
227
  NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME,
228
- NESTING_TRACK_ID_ATTRIBUTE_NAME: helper.NESTING_TRACK_ID_ATTRIBUTE_NAME
228
+ NESTING_TRACK_ID_ATTRIBUTE_NAME: helper.NESTING_TRACK_ID_ATTRIBUTE_NAME,
229
+ getPosterDataURI: helper.getPosterDataURI
229
230
  };
230
231
 
231
232
  async function getContent(resourceURL, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.110",
3
+ "version": "1.5.112",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -750,7 +750,7 @@ async function addPageResources(zipWriter, pageData, options, prefixName, url) {
750
750
  ]);
751
751
  }
752
752
 
753
- async function addFile(zipWriter, prefixName, data, disableCompresson) {
753
+ async function addFile(zipWriter, prefixName, data, disableCompression) {
754
754
  const dataReader = typeof data.content == "string" ? new TextReader(data.content) : new BlobReader(new Blob([new Uint8Array(data.content)]));
755
755
  const options = { password: data.password, bufferedWrite: true };
756
756
  if (!data.password) {
@@ -758,7 +758,7 @@ async function addFile(zipWriter, prefixName, data, disableCompresson) {
758
758
  // password the resource URLs would be readable while the same map in manifest.json is not
759
759
  options.comment = data.url && data.url.startsWith("data:") ? "data:" : data.url;
760
760
  }
761
- if (NO_COMPRESSION_EXTENSIONS.includes(data.extension) || disableCompresson) {
761
+ if (NO_COMPRESSION_EXTENSIONS.includes(data.extension) || disableCompression) {
762
762
  options.level = 0;
763
763
  }
764
764
  await zipWriter.add(prefixName + data.name, dataReader, options);