image-exporter 0.0.1 → 1.0.0

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.
Files changed (56) hide show
  1. package/README.md +137 -2
  2. package/bun.lockb +0 -0
  3. package/dist/image-exporter.es.js +499 -612
  4. package/dist/image-exporter.umd.js +506 -619
  5. package/package.json +18 -15
  6. package/src/capture/capture-element.ts +58 -0
  7. package/src/capture/determine-total-elements.ts +40 -0
  8. package/src/capture/download-images.ts +69 -0
  9. package/src/capture/get-image-options.ts +196 -0
  10. package/src/capture/handle-filenames.ts +52 -0
  11. package/src/capture/index.ts +99 -0
  12. package/src/capture/remove-hidden-elements.ts +19 -0
  13. package/src/config.ts +19 -0
  14. package/src/cors-proxy/cleanup.ts +43 -0
  15. package/src/cors-proxy/index.ts +6 -21
  16. package/src/{utils → cors-proxy}/is-valid-url.ts +5 -3
  17. package/src/cors-proxy/proxy-css.ts +51 -44
  18. package/src/cors-proxy/proxy-images.ts +21 -77
  19. package/src/cors-proxy/run.ts +26 -0
  20. package/src/index.ts +10 -4
  21. package/src/logger.ts +61 -0
  22. package/src/types.d.ts +51 -0
  23. package/vite.config.js +3 -7
  24. package/example/example.css +0 -122
  25. package/example/example.html +0 -152
  26. package/example/github.jpg +0 -0
  27. package/example/poll-h.svg +0 -1
  28. package/src/capture-images.ts +0 -129
  29. package/src/clean-up.ts +0 -50
  30. package/src/default-options.ts +0 -58
  31. package/src/download-images.ts +0 -52
  32. package/src/get-capture-element.test.html +0 -21
  33. package/src/get-capture-element.test.ts +0 -36
  34. package/src/get-capture-element.ts +0 -175
  35. package/src/get-options/get-input-options.test.html +0 -217
  36. package/src/get-options/get-input-options.test.ts +0 -109
  37. package/src/get-options/get-input-options.ts +0 -40
  38. package/src/get-options/get-item-options.ts +0 -46
  39. package/src/get-options/get-wrapper-options.test.html +0 -33
  40. package/src/get-options/get-wrapper-options.test.ts +0 -109
  41. package/src/get-options/get-wrapper-options.ts +0 -84
  42. package/src/get-options/index.ts +0 -28
  43. package/src/image-exporter.ts +0 -108
  44. package/src/types/image.ts +0 -2
  45. package/src/types/index.ts +0 -2
  46. package/src/types/options.ts +0 -69
  47. package/src/utils/convert-to-slug.ts +0 -15
  48. package/src/utils/get-attribute-values.ts +0 -68
  49. package/src/utils/get-date-MMDDYY.ts +0 -11
  50. package/src/utils/ignore-items.ts +0 -11
  51. package/src/utils/index.ts +0 -18
  52. package/src/utils/is-visible.ts +0 -12
  53. package/src/utils/parse-labels.ts +0 -55
  54. package/src/utils/push-to-window.ts +0 -3
  55. package/tests/index.html +0 -88
  56. package/tests/input-tests.html +0 -169
@@ -1,281 +1,3 @@
1
- function pushToWindow(propertyName, value) {
2
- window[propertyName] = value;
3
- }
4
- function getWrapperOptions(options) {
5
- try {
6
- const wrapper = document.querySelector(options.selectors.wrapper);
7
- if (!wrapper) {
8
- new Error("Wrapper element not found");
9
- return options;
10
- }
11
- Object.keys(options.image).forEach((key) => {
12
- const attrValue = wrapper.getAttribute(options.image[key].attributeSelector);
13
- if (attrValue !== null) {
14
- const safeValue = optionSafetyCheck(key, attrValue);
15
- if (safeValue === null) return;
16
- options.image[key].value = safeValue;
17
- }
18
- });
19
- Object.keys(options.zip).forEach((key) => {
20
- const attrValue = wrapper.getAttribute(options.zip[key].attributeSelector);
21
- if (attrValue !== null) {
22
- const safeValue = optionSafetyCheck(key, attrValue);
23
- if (safeValue === null) return;
24
- options.zip[key].value = safeValue;
25
- }
26
- });
27
- if (options.debug) pushToWindow("getWrapperOptionsDebug", options);
28
- return options;
29
- } catch (e) {
30
- console.error("ImageExporter: Error in getWrapperOptions", e);
31
- return options;
32
- }
33
- }
34
- function optionSafetyCheck(key, value) {
35
- if (value === "on") return true;
36
- if (value === "off") return false;
37
- if (key === "scale" || key === "quality") {
38
- if (typeof value === "number") return value;
39
- value = value.trim();
40
- value = parseFloat(value);
41
- if (isNaN(value)) return null;
42
- return value;
43
- }
44
- if (key === "dateInLabel" || key === "scaleInLabel") {
45
- if (typeof value === "boolean") return value;
46
- value = value.trim();
47
- return value === "true";
48
- }
49
- if (key === "format") {
50
- value = value.trim();
51
- if (value === "jpg" || value === "png") return value;
52
- return null;
53
- }
54
- return value;
55
- }
56
- async function handleOptions(optionsType, key) {
57
- const selector = optionsType[key].inputSelector;
58
- const inputElement = document.querySelector(`[${selector}]`);
59
- if (!inputElement) return;
60
- if (inputElement.getAttribute("type") === "checkbox") {
61
- optionsType[key].value = inputElement.checked;
62
- return;
63
- }
64
- if (inputElement.value) {
65
- const safeValue = optionSafetyCheck(key, inputElement.value);
66
- if (safeValue === null) return;
67
- optionsType[key].value = safeValue;
68
- }
69
- }
70
- function getInputOptions(options) {
71
- try {
72
- Promise.all(
73
- Object.keys(options.image).map((key) => handleOptions(options.image, key))
74
- );
75
- Promise.all(Object.keys(options.zip).map((key) => handleOptions(options.zip, key)));
76
- if (options.debug) pushToWindow("getInputOptionsDebug", options);
77
- return options;
78
- } catch (e) {
79
- console.error("ImageExporter: Error in getUserOptions", e);
80
- return options;
81
- }
82
- }
83
- function convertToSlug(input) {
84
- if (!input) {
85
- return null;
86
- }
87
- input = input.toLowerCase();
88
- input = input.replace(/[^a-z0-9_@ -]/g, "");
89
- input = input.replace(/\s+/g, "-");
90
- return input;
91
- }
92
- function isVisible(element) {
93
- if (!(element instanceof HTMLElement)) return false;
94
- if (element.offsetParent === null) return false;
95
- const rect = element.getBoundingClientRect();
96
- return rect.width > 0 && rect.height > 0;
97
- }
98
- function getDateMMDDYY() {
99
- return String((/* @__PURE__ */ new Date()).getMonth() + 1).padStart(2, "0") + String((/* @__PURE__ */ new Date()).getDate()).padStart(2, "0") + (/* @__PURE__ */ new Date()).getFullYear().toString().slice(-2);
100
- }
101
- function parseZipLabel(options) {
102
- const date = getDateMMDDYY();
103
- const zipScale = convertStringToBoolean(options.zip.scaleInLabel.value) ? `_@${options.image.scale.value}x` : "";
104
- const zipDate = convertStringToBoolean(options.zip.dateInLabel.value) ? `_${date}` : "";
105
- const zipNameSlug = convertToSlug(options.zip.label.value);
106
- const zipLabel = `${zipNameSlug}${zipDate}${zipScale}.zip`;
107
- return zipLabel;
108
- }
109
- function parseImageLabel(itemOptions) {
110
- const date = getDateMMDDYY();
111
- const imgScale = itemOptions.image.scaleInLabel.value ? `_@${itemOptions.image.scale.value}x` : "";
112
- const imgDate = itemOptions.image.dateInLabel.value ? `_${date}` : "";
113
- const imgNameSlug = convertToSlug(itemOptions.userSlug) || `img-${itemOptions.id}`;
114
- const imgLabel = `${imgNameSlug}${imgDate}${imgScale}`;
115
- return imgLabel;
116
- }
117
- function convertStringToBoolean(str) {
118
- if (str === "true") {
119
- return true;
120
- } else if (str === "false") {
121
- return false;
122
- }
123
- return str;
124
- }
125
- function isValidUrl(string) {
126
- try {
127
- const url = new URL(string);
128
- if (url.protocol === "data:") {
129
- return false;
130
- }
131
- if (url.protocol !== "http:" && url.protocol !== "https:") {
132
- return false;
133
- }
134
- return true;
135
- } catch (_) {
136
- return false;
137
- }
138
- }
139
- function getItemOptions(element, options, index) {
140
- var _a;
141
- let itemOptions = {
142
- ...options,
143
- id: index,
144
- userSlug: "",
145
- slug: "",
146
- fileName: ""
147
- };
148
- Object.keys(options.image).forEach((key) => {
149
- const attributeValue = element.getAttribute(options.image[key].attributeSelector);
150
- if (attributeValue !== null) {
151
- itemOptions.image[key].value = attributeValue;
152
- }
153
- });
154
- itemOptions.id = index;
155
- itemOptions.userSlug = ((_a = element.querySelector(options.selectors.slug)) == null ? void 0 : _a.textContent) || "";
156
- itemOptions.slug = parseImageLabel(itemOptions);
157
- return itemOptions;
158
- }
159
- function determineOptions(options) {
160
- options = getWrapperOptions(options);
161
- options = getInputOptions(options);
162
- return options;
163
- }
164
- function getCaptureElements(options) {
165
- try {
166
- if (!document.querySelector(options.selectors.capture)) {
167
- console.error("ImageExporter: No capture items found in the wrapper.");
168
- return [];
169
- }
170
- findMultiScaleElements(options);
171
- const elements = Array.from(
172
- document.querySelectorAll(
173
- `${options.selectors.wrapper} ${options.selectors.capture}`
174
- )
175
- );
176
- const visibleElements = elements.filter((element) => isVisible(element));
177
- return visibleElements;
178
- } catch (e) {
179
- console.error("ImageExporter: Error in getCaptureElements", e);
180
- return [];
181
- }
182
- }
183
- function findMultiScaleElements(options) {
184
- try {
185
- const elements = Array.from(
186
- document.querySelectorAll(
187
- `${options.selectors.wrapper} ${options.selectors.capture}`
188
- )
189
- );
190
- if (elements) {
191
- const elementsWithScaleAttribute = elements.filter(
192
- (element) => element.hasAttribute(options.image.scale.attributeSelector)
193
- );
194
- elementsWithScaleAttribute.forEach((element) => {
195
- const scaleValue = element.getAttribute(
196
- options.image.scale.attributeSelector
197
- );
198
- if (scaleValue.includes(",")) {
199
- const scaleArray = scaleValue.split(",").map(Number);
200
- encapsulateMultiScaleElements(options, element, scaleArray, scaleValue);
201
- if (options.debug)
202
- pushToWindow("findMultiScaleElementsTest", element.outerHTML);
203
- }
204
- });
205
- return true;
206
- } else {
207
- return false;
208
- }
209
- } catch (e) {
210
- console.error("ImageExporter: Error in findMultiScaleElements", e);
211
- return;
212
- }
213
- }
214
- function encapsulateMultiScaleElements(options, element, scaleArray, scaleValue) {
215
- try {
216
- element.setAttribute(options.image.scale.attributeSelector, scaleArray[0].toString());
217
- element.setAttribute(options.image.scaleInLabel.attributeSelector, "true");
218
- element.setAttribute("ie-clone-source", scaleValue);
219
- for (let i = 1; i < scaleArray.length; i++) {
220
- const newElement = cloneElementAttributes(options, element);
221
- newElement.setAttribute(
222
- options.image.scale.attributeSelector,
223
- scaleArray[i].toString()
224
- );
225
- newElement.setAttribute(options.image.scaleInLabel.attributeSelector, "true");
226
- if (element.parentNode) {
227
- element.parentNode.insertBefore(newElement, element);
228
- newElement.appendChild(element);
229
- }
230
- }
231
- } catch (e) {
232
- console.error("ImageExporter: Error in encapsulateMultiScaleElements", e);
233
- return;
234
- }
235
- }
236
- function cloneElementAttributes(options, originalElement) {
237
- try {
238
- const clonedElement = document.createElement("div");
239
- const arrayOfPossibleAttributes = Object.keys(options.image).map(
240
- (key) => options.image[key].attributeSelector
241
- );
242
- const { prefix, value } = parseStringAttribute(options.selectors.capture);
243
- clonedElement.setAttribute(prefix, value);
244
- clonedElement.setAttribute("ie-clone", "true");
245
- setExplicitDimensions(originalElement, clonedElement);
246
- Array.from(originalElement.attributes).forEach((attr) => {
247
- if (attr.name in arrayOfPossibleAttributes) {
248
- clonedElement.setAttribute(attr.name, attr.value);
249
- }
250
- });
251
- return clonedElement;
252
- } catch (e) {
253
- console.error("ImageExporter: Error in cloneElementAttributes", e);
254
- return originalElement;
255
- }
256
- }
257
- function parseStringAttribute(attributeValue) {
258
- if (!attributeValue.includes("=")) {
259
- throw new Error("Invalid attribute format. Expected format: [prefix=value]");
260
- }
261
- const attributeArray = attributeValue.split("=");
262
- if (attributeArray.length !== 2) {
263
- throw new Error("Invalid attribute format. Expected format: [prefix=value]");
264
- }
265
- const prefix = attributeArray[0].trim().replace(/^\[|\]$/g, "");
266
- const value = attributeArray[1].trim().replace(/^\[|\]$/g, "").replace(/^'|'$/g, "");
267
- if (!prefix || !value) {
268
- throw new Error("Invalid attribute format. Prefix or value is missing.");
269
- }
270
- return { prefix, value };
271
- }
272
- function setExplicitDimensions(originalElement, clonedElement) {
273
- const originalElementStyle = window.getComputedStyle(originalElement);
274
- const originalElementWidth = originalElementStyle.getPropertyValue("width");
275
- const originalElementHeight = originalElementStyle.getPropertyValue("height");
276
- clonedElement.style.width = originalElementWidth;
277
- clonedElement.style.height = originalElementHeight;
278
- }
279
1
  function resolveUrl(url, baseUrl) {
280
2
  if (url.match(/^[a-z]+:\/\//i)) {
281
3
  return url;
@@ -315,6 +37,18 @@ function toArray(arrayLike) {
315
37
  }
316
38
  return arr;
317
39
  }
40
+ let styleProps = null;
41
+ function getStyleProperties(options = {}) {
42
+ if (styleProps) {
43
+ return styleProps;
44
+ }
45
+ if (options.includeStyleProperties) {
46
+ styleProps = options.includeStyleProperties;
47
+ return styleProps;
48
+ }
49
+ styleProps = toArray(window.getComputedStyle(document.documentElement));
50
+ return styleProps;
51
+ }
318
52
  function px(node, styleProperty) {
319
53
  const win = node.ownerDocument.defaultView || window;
320
54
  const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
@@ -374,8 +108,11 @@ function checkCanvasDimensions(canvas) {
374
108
  function createImage(url) {
375
109
  return new Promise((resolve, reject) => {
376
110
  const img = new Image();
377
- img.decode = () => resolve(img);
378
- img.onload = () => resolve(img);
111
+ img.onload = () => {
112
+ img.decode().then(() => {
113
+ requestAnimationFrame(() => resolve(img));
114
+ });
115
+ };
379
116
  img.onerror = reject;
380
117
  img.crossOrigin = "anonymous";
381
118
  img.decoding = "async";
@@ -413,19 +150,19 @@ function formatCSSText(style) {
413
150
  const content = style.getPropertyValue("content");
414
151
  return `${style.cssText} content: '${content.replace(/'|"/g, "")}';`;
415
152
  }
416
- function formatCSSProperties(style) {
417
- return toArray(style).map((name) => {
153
+ function formatCSSProperties(style, options) {
154
+ return getStyleProperties(options).map((name) => {
418
155
  const value = style.getPropertyValue(name);
419
156
  const priority = style.getPropertyPriority(name);
420
157
  return `${name}: ${value}${priority ? " !important" : ""};`;
421
158
  }).join(" ");
422
159
  }
423
- function getPseudoElementStyle(className, pseudo, style) {
160
+ function getPseudoElementStyle(className, pseudo, style, options) {
424
161
  const selector = `.${className}:${pseudo}`;
425
- const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style);
162
+ const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style, options);
426
163
  return document.createTextNode(`${selector}{${cssText}}`);
427
164
  }
428
- function clonePseudoElement(nativeNode, clonedNode, pseudo) {
165
+ function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
429
166
  const style = window.getComputedStyle(nativeNode, pseudo);
430
167
  const content = style.getPropertyValue("content");
431
168
  if (content === "" || content === "none") {
@@ -438,12 +175,12 @@ function clonePseudoElement(nativeNode, clonedNode, pseudo) {
438
175
  return;
439
176
  }
440
177
  const styleElement = document.createElement("style");
441
- styleElement.appendChild(getPseudoElementStyle(className, pseudo, style));
178
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
442
179
  clonedNode.appendChild(styleElement);
443
180
  }
444
- function clonePseudoElements(nativeNode, clonedNode) {
445
- clonePseudoElement(nativeNode, clonedNode, ":before");
446
- clonePseudoElement(nativeNode, clonedNode, ":after");
181
+ function clonePseudoElements(nativeNode, clonedNode, options) {
182
+ clonePseudoElement(nativeNode, clonedNode, ":before", options);
183
+ clonePseudoElement(nativeNode, clonedNode, ":after", options);
447
184
  }
448
185
  const WOFF = "application/font-woff";
449
186
  const JPEG = "image/jpeg";
@@ -559,11 +296,11 @@ async function cloneVideoElement(video, options) {
559
296
  const dataURL = await resourceToDataURL(poster, contentType, options);
560
297
  return createImage(dataURL);
561
298
  }
562
- async function cloneIFrameElement(iframe) {
299
+ async function cloneIFrameElement(iframe, options) {
563
300
  var _a;
564
301
  try {
565
302
  if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
566
- return await cloneNode(iframe.contentDocument.body, {}, true);
303
+ return await cloneNode(iframe.contentDocument.body, options, true);
567
304
  }
568
305
  } catch (_b) {
569
306
  }
@@ -577,13 +314,17 @@ async function cloneSingleNode(node, options) {
577
314
  return cloneVideoElement(node, options);
578
315
  }
579
316
  if (isInstanceOfElement(node, HTMLIFrameElement)) {
580
- return cloneIFrameElement(node);
317
+ return cloneIFrameElement(node, options);
581
318
  }
582
- return node.cloneNode(false);
319
+ return node.cloneNode(isSVGElement(node));
583
320
  }
584
321
  const isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SLOT";
322
+ const isSVGElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SVG";
585
323
  async function cloneChildren(nativeNode, clonedNode, options) {
586
324
  var _a, _b;
325
+ if (isSVGElement(clonedNode)) {
326
+ return clonedNode;
327
+ }
587
328
  let children = [];
588
329
  if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
589
330
  children = toArray(nativeNode.assignedNodes());
@@ -602,7 +343,7 @@ async function cloneChildren(nativeNode, clonedNode, options) {
602
343
  }), Promise.resolve());
603
344
  return clonedNode;
604
345
  }
605
- function cloneCSSStyle(nativeNode, clonedNode) {
346
+ function cloneCSSStyle(nativeNode, clonedNode, options) {
606
347
  const targetStyle = clonedNode.style;
607
348
  if (!targetStyle) {
608
349
  return;
@@ -612,7 +353,7 @@ function cloneCSSStyle(nativeNode, clonedNode) {
612
353
  targetStyle.cssText = sourceStyle.cssText;
613
354
  targetStyle.transformOrigin = sourceStyle.transformOrigin;
614
355
  } else {
615
- toArray(sourceStyle).forEach((name) => {
356
+ getStyleProperties(options).forEach((name) => {
616
357
  let value = sourceStyle.getPropertyValue(name);
617
358
  if (name === "font-size" && value.endsWith("px")) {
618
359
  const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1;
@@ -645,10 +386,10 @@ function cloneSelectValue(nativeNode, clonedNode) {
645
386
  }
646
387
  }
647
388
  }
648
- function decorate(nativeNode, clonedNode) {
389
+ function decorate(nativeNode, clonedNode, options) {
649
390
  if (isInstanceOfElement(clonedNode, Element)) {
650
- cloneCSSStyle(nativeNode, clonedNode);
651
- clonePseudoElements(nativeNode, clonedNode);
391
+ cloneCSSStyle(nativeNode, clonedNode, options);
392
+ clonePseudoElements(nativeNode, clonedNode, options);
652
393
  cloneInputValue(nativeNode, clonedNode);
653
394
  cloneSelectValue(nativeNode, clonedNode);
654
395
  }
@@ -694,7 +435,7 @@ async function cloneNode(node, options, isRoot) {
694
435
  if (!isRoot && options.filter && !options.filter(node)) {
695
436
  return null;
696
437
  }
697
- return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode)).then((clonedNode) => ensureSVGSymbols(clonedNode, options));
438
+ return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode, options)).then((clonedNode) => ensureSVGSymbols(clonedNode, options));
698
439
  }
699
440
  const URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g;
700
441
  const URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
@@ -760,12 +501,8 @@ async function embedProp(propName, node, options) {
760
501
  return false;
761
502
  }
762
503
  async function embedBackground(clonedNode, options) {
763
- if (!await embedProp("background", clonedNode, options)) {
764
- await embedProp("background-image", clonedNode, options);
765
- }
766
- if (!await embedProp("mask", clonedNode, options)) {
767
- await embedProp("mask-image", clonedNode, options);
768
- }
504
+ await embedProp("background", clonedNode, options) || await embedProp("background-image", clonedNode, options);
505
+ await embedProp("mask", clonedNode, options) || await embedProp("-webkit-mask", clonedNode, options) || await embedProp("mask-image", clonedNode, options) || await embedProp("-webkit-mask-image", clonedNode, options);
769
506
  }
770
507
  async function embedImageNode(clonedNode, options) {
771
508
  const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);
@@ -776,7 +513,13 @@ async function embedImageNode(clonedNode, options) {
776
513
  const dataURL = await resourceToDataURL(url, getMimeType(url), options);
777
514
  await new Promise((resolve, reject) => {
778
515
  clonedNode.onload = resolve;
779
- clonedNode.onerror = reject;
516
+ clonedNode.onerror = options.onImageErrorHandler ? (...attributes) => {
517
+ try {
518
+ resolve(options.onImageErrorHandler(...attributes));
519
+ } catch (error) {
520
+ reject(error);
521
+ }
522
+ } : reject;
780
523
  const image = clonedNode;
781
524
  if (image.decode) {
782
525
  image.decode = resolve;
@@ -915,7 +658,7 @@ async function getCSSRules(styleSheets, options) {
915
658
  const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
916
659
  if (sheet.href != null) {
917
660
  deferreds.push(fetchCSS(sheet.href).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
918
- inline.insertRule(rule, sheet.cssRules.length);
661
+ inline.insertRule(rule, inline.cssRules.length);
919
662
  })).catch((err) => {
920
663
  console.error("Error loading remote stylesheet", err);
921
664
  }));
@@ -950,9 +693,29 @@ async function parseWebFontRules(node, options) {
950
693
  const cssRules = await getCSSRules(styleSheets, options);
951
694
  return getWebFontRules(cssRules);
952
695
  }
696
+ function normalizeFontFamily(font) {
697
+ return font.trim().replace(/["']/g, "");
698
+ }
699
+ function getUsedFonts(node) {
700
+ const fonts = /* @__PURE__ */ new Set();
701
+ function traverse(node2) {
702
+ const fontFamily = node2.style.fontFamily || getComputedStyle(node2).fontFamily;
703
+ fontFamily.split(",").forEach((font) => {
704
+ fonts.add(normalizeFontFamily(font));
705
+ });
706
+ Array.from(node2.children).forEach((child) => {
707
+ if (child instanceof HTMLElement) {
708
+ traverse(child);
709
+ }
710
+ });
711
+ }
712
+ traverse(node);
713
+ return fonts;
714
+ }
953
715
  async function getWebFontCSS(node, options) {
954
716
  const rules = await parseWebFontRules(node, options);
955
- const cssTexts = await Promise.all(rules.map((rule) => {
717
+ const usedFonts = getUsedFonts(node);
718
+ const cssTexts = await Promise.all(rules.filter((rule) => usedFonts.has(normalizeFontFamily(rule.style.fontFamily))).map((rule) => {
956
719
  const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;
957
720
  return embedResources(rule.cssText, baseUrl, options);
958
721
  }));
@@ -1011,173 +774,72 @@ async function toJpeg(node, options = {}) {
1011
774
  const canvas = await toCanvas(node, options);
1012
775
  return canvas.toDataURL("image/jpeg", options.quality || 1);
1013
776
  }
1014
- async function proxyCSS(options) {
1015
- try {
1016
- const css = document.querySelectorAll('link[rel="stylesheet"]');
1017
- for (let stylesheetElement of css) {
1018
- let stylesheetURL = stylesheetElement.getAttribute("href");
1019
- if (stylesheetURL && !stylesheetURL.startsWith("data:") && isValidUrl(stylesheetURL) && !stylesheetURL.startsWith(options.corsProxyBaseUrl)) {
1020
- const url = options.corsProxyBaseUrl + encodeURIComponent(stylesheetURL);
1021
- try {
1022
- const response = await fetch(url);
1023
- const css2 = await response.text();
1024
- const styleEl = document.createElement("style");
1025
- styleEl.textContent = css2;
1026
- document.head.appendChild(styleEl);
1027
- stylesheetElement.remove();
1028
- } catch (error) {
1029
- console.error("Error fetching CSS:", error);
1030
- }
1031
- }
1032
- }
1033
- } catch (e) {
1034
- console.error("ImageExporter: Error in proxyCSS", e);
1035
- }
1036
- }
1037
- async function proxyImages(options) {
1038
- try {
1039
- const links = document.querySelectorAll("link");
1040
- links.forEach((link) => {
1041
- link.setAttribute("crossorigin", "anonymous");
1042
- });
1043
- const wrapper = document.querySelector(options.selectors.wrapper);
1044
- if (!wrapper) {
1045
- console.error("ImageExporter: Wrapper element not found.");
1046
- return;
777
+ function handleFileNames(imageOptions, filenames) {
778
+ let proposedFilename = imageOptions.label;
779
+ if (imageOptions.includeScaleInLabel) proposedFilename += `_@${imageOptions.scale}x`;
780
+ proposedFilename += `.${imageOptions.format}`;
781
+ if (!filenames.includes(proposedFilename)) {
782
+ filenames.push(proposedFilename);
783
+ return proposedFilename;
784
+ }
785
+ const numberPattern = /-(\d+)$/;
786
+ const match = proposedFilename.match(numberPattern);
787
+ if (match) {
788
+ const baseFilename = proposedFilename.replace(numberPattern, "");
789
+ let counter = parseInt(match[1], 10);
790
+ while (filenames.includes(`${baseFilename}-${counter}`)) {
791
+ counter++;
1047
792
  }
1048
- const images = Array.from(wrapper.querySelectorAll("img"));
1049
- const srcMap = /* @__PURE__ */ new Map();
1050
- images.forEach((img) => {
1051
- const srcs = srcMap.get(img.src) || [];
1052
- srcs.push(img);
1053
- srcMap.set(img.src, srcs);
1054
- });
1055
- for (const [src, duplicates] of srcMap) {
1056
- if (!isValidUrl(src) || options.corsProxyBaseUrl && src.startsWith(options.corsProxyBaseUrl)) {
1057
- continue;
1058
- }
1059
- if (duplicates.length > 1) {
1060
- try {
1061
- const response = await fetch(
1062
- options.corsProxyBaseUrl + encodeURIComponent(src)
1063
- );
1064
- const blob = await response.blob();
1065
- const dataURL = await blobToDataURL(blob);
1066
- duplicates.forEach((dupImg) => {
1067
- if (dupImg.src === src) {
1068
- dupImg.src = dataURL;
1069
- }
1070
- });
1071
- } catch (error) {
1072
- console.error("Error fetching image:", error);
1073
- }
1074
- } else {
1075
- images.forEach((img) => {
1076
- if (img.src === src) {
1077
- img.src = options.corsProxyBaseUrl + encodeURIComponent(src);
1078
- }
1079
- });
1080
- }
1081
- }
1082
- } catch (e) {
1083
- console.error("ImageExporter: Error in proxyImages", e);
1084
- return;
1085
- }
1086
- }
1087
- function blobToDataURL(blob) {
1088
- return new Promise((resolve, reject) => {
1089
- const reader = new FileReader();
1090
- reader.onloadend = () => resolve(reader.result);
1091
- reader.onerror = reject;
1092
- reader.readAsDataURL(blob);
1093
- });
1094
- }
1095
- async function runCorsProxy(options) {
1096
- try {
1097
- if (!options.corsProxyBaseUrl || !isValidUrl(options.corsProxyBaseUrl)) return;
1098
- await proxyCSS(options);
1099
- await proxyImages(options);
1100
- return;
1101
- } catch (e) {
1102
- console.error("ImageExporter: Error in runCorsProxy", e);
1103
- }
1104
- }
1105
- function ignoreFilter(options) {
1106
- return (node) => {
1107
- if (!(node instanceof HTMLElement)) {
1108
- throw new Error("The provided node is not an HTMLElement");
793
+ const newFilename = `${baseFilename}-${counter}`;
794
+ filenames.push(newFilename);
795
+ return newFilename;
796
+ } else {
797
+ let counter = 2;
798
+ while (filenames.includes(`${proposedFilename}-${counter}`)) {
799
+ counter++;
1109
800
  }
1110
- return !node.matches(options.selectors.ignore);
1111
- };
1112
- }
1113
- async function captureImages(options, captureElements) {
1114
- try {
1115
- await runCorsProxy(options);
1116
- const images = await Promise.all(
1117
- captureElements.map(
1118
- (element, index) => captureImage(
1119
- element,
1120
- getItemOptions(element, options, index + 1),
1121
- ignoreFilter(options)
1122
- )
1123
- )
1124
- );
1125
- return images;
1126
- } catch (e) {
1127
- console.error("ImageExporter: Error in captureImages", e);
1128
- return [];
801
+ const newFilename = `${proposedFilename}-${counter}`;
802
+ filenames.push(newFilename);
803
+ return newFilename;
1129
804
  }
1130
805
  }
1131
- async function captureImage(element, itemOptions, ignoreFilter2) {
806
+ async function captureElement(element, imageOptions, filenames) {
1132
807
  try {
1133
- itemOptions.slug = ensureUniqueSlug(itemOptions.slug);
1134
808
  let dataURL = "";
1135
809
  let htmlToImageOptions = {
1136
810
  // Ensure quality is a number
1137
- quality: itemOptions.image.quality.value,
811
+ quality: imageOptions.quality,
1138
812
  // Ensure scale is a number
1139
- pixelRatio: itemOptions.image.scale.value
1140
- // Function that returns false if the element should be ignored
1141
- // filter: ignoreFilter,
813
+ pixelRatio: imageOptions.scale,
814
+ // Ignores elements with data-ignore-capture attribute
815
+ filter
1142
816
  };
1143
- switch (itemOptions.image.format.value.toLowerCase()) {
817
+ switch (imageOptions.format) {
1144
818
  case "jpg":
1145
819
  dataURL = await toJpeg(element, htmlToImageOptions);
1146
- itemOptions.fileName = `${itemOptions.slug}.jpg`;
1147
820
  break;
1148
821
  case "png":
1149
- default:
1150
822
  dataURL = await toPng(element, htmlToImageOptions);
1151
- itemOptions.fileName = `${itemOptions.slug}.png`;
823
+ break;
824
+ case "svg":
825
+ dataURL = await toSvg(element, htmlToImageOptions);
1152
826
  break;
1153
827
  }
1154
- return [dataURL, itemOptions.fileName];
1155
- } catch (e) {
1156
- console.error("ImageExporter: Error in captureImage", e);
1157
- return ["", ""];
828
+ return {
829
+ dataURL,
830
+ fileName: handleFileNames(imageOptions, filenames)
831
+ };
832
+ } catch (error) {
833
+ console.error("ImageExporter: Error in captureImage", error);
834
+ return { dataURL: "", fileName: "" };
1158
835
  }
1159
836
  }
1160
- let usedSlugs = [];
1161
- function ensureUniqueSlug(slug) {
1162
- try {
1163
- if (usedSlugs.includes(slug)) {
1164
- let counter = 1;
1165
- let newSlug = `${slug}-${counter}`;
1166
- while (usedSlugs.includes(newSlug)) {
1167
- counter++;
1168
- newSlug = `${slug}-${counter}`;
1169
- }
1170
- usedSlugs.push(newSlug);
1171
- return newSlug;
1172
- } else {
1173
- usedSlugs.push(slug);
1174
- return slug;
1175
- }
1176
- } catch (e) {
1177
- console.error("ImageExporter: Error in ensureUniqueSlug", e);
1178
- return slug;
837
+ const filter = (node) => {
838
+ if (node instanceof HTMLElement) {
839
+ return !node.hasAttribute("data-ignore-capture");
1179
840
  }
1180
- }
841
+ return true;
842
+ };
1181
843
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
1182
844
  function getDefaultExportFromCjs(x) {
1183
845
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
@@ -3622,186 +3284,411 @@ https://github.com/nodeca/pako/blob/main/LICENSE
3622
3284
  })(jszip_min);
3623
3285
  var jszip_minExports = jszip_min.exports;
3624
3286
  const JSZip = /* @__PURE__ */ getDefaultExportFromCjs(jszip_minExports);
3625
- async function downloadImages(images, options) {
3287
+ async function downloadImages(images, config) {
3626
3288
  if (images.length === 1) {
3627
- const [dataURL, fileName] = images[0];
3628
- await download(dataURL, fileName);
3289
+ const image = images[0];
3290
+ await download(image.dataURL, image.fileName);
3629
3291
  } else if (images.length > 1) {
3630
- const zipName = parseZipLabel(options);
3631
- const zipBlob = await zipUpImages(images);
3632
- await download(zipBlob, zipName);
3292
+ const imagesBlob = await zipUpImages(images);
3293
+ if (imagesBlob) await download(imagesBlob, parseLabel(config));
3633
3294
  }
3634
3295
  }
3635
3296
  async function zipUpImages(images) {
3636
3297
  const zip = new JSZip();
3637
- images.forEach(([dataURL, filename]) => {
3638
- const content = dataURL.split(",")[1];
3639
- zip.file(filename, content, { base64: true });
3640
- });
3641
3298
  try {
3642
- const zipBlob = await zip.generateAsync({ type: "blob" });
3643
- return zipBlob;
3299
+ images.forEach((image) => {
3300
+ const content = image.dataURL.split(",")[1];
3301
+ zip.file(image.fileName, content, { base64: true });
3302
+ });
3303
+ } catch (error) {
3304
+ console.error("Image Exporter - Error adding images to ZIP:", error);
3305
+ return;
3306
+ }
3307
+ try {
3308
+ const imagesBlob = await zip.generateAsync({ type: "blob" });
3309
+ return imagesBlob;
3644
3310
  } catch (error) {
3645
- console.error("Error creating ZIP:", error);
3646
- throw error;
3311
+ console.error("Image Exporter - Error generating ZIP:", error);
3312
+ return;
3647
3313
  }
3648
3314
  }
3649
- const defaultOptions = {
3650
- corsProxyBaseUrl: "",
3651
- downloadImages: true,
3652
- selectors: {
3653
- wrapper: "[ie='wrapper']",
3654
- capture: "[ie='capture']",
3655
- trigger: "[ie='trigger']",
3656
- slug: "[ie='slug']",
3657
- ignore: "[ie='ignore']"
3658
- },
3659
- image: {
3660
- scale: {
3661
- value: 1,
3662
- attributeSelector: "ie-scale",
3663
- inputSelector: "ie-scale-input"
3664
- },
3665
- quality: {
3666
- value: 1,
3667
- attributeSelector: "ie-quality",
3668
- inputSelector: "ie-quality-input"
3669
- },
3670
- format: {
3671
- value: "jpg",
3672
- attributeSelector: "ie-format",
3673
- inputSelector: "ie-format-input"
3674
- },
3675
- dateInLabel: {
3676
- value: true,
3677
- attributeSelector: "ie-img-label-date",
3678
- inputSelector: "ie-img-label-date-input"
3679
- },
3680
- scaleInLabel: {
3681
- value: true,
3682
- attributeSelector: "ie-img-label-scale",
3683
- inputSelector: "ie-img-label-scale-input"
3315
+ function parseLabel(config) {
3316
+ try {
3317
+ let label = config.zipLabel;
3318
+ label = label.replace(/\s+/g, "-");
3319
+ return label.replace(/[^a-zA-Z0-9-_]/g, "");
3320
+ } catch (error) {
3321
+ console.error(error);
3322
+ return "images";
3323
+ }
3324
+ }
3325
+ const log = {
3326
+ info: (...messages) => logAction(messages, "info"),
3327
+ error: (...messages) => logAction(messages, "error"),
3328
+ verbose: (...messages) => logAction(messages, "verbose"),
3329
+ progress: (progress, total) => logProgress(progress, total)
3330
+ };
3331
+ async function logAction(messages, type = "info") {
3332
+ const combinedMessage = messages.map((msg) => typeof msg === "object" ? JSON.stringify(msg) : msg).join(" ");
3333
+ switch (type) {
3334
+ case "info":
3335
+ if (loggingLevel === "info" || loggingLevel === "verbose") {
3336
+ console.log(...messages);
3337
+ }
3338
+ break;
3339
+ case "error":
3340
+ if (loggingLevel === "error" || loggingLevel === "verbose") {
3341
+ console.error(...messages);
3342
+ }
3343
+ break;
3344
+ case "verbose":
3345
+ if (loggingLevel === "verbose") {
3346
+ console.log(...messages);
3347
+ }
3348
+ break;
3349
+ }
3350
+ if (windowLogging) window.imageExporterLogs.push({ message: combinedMessage, type });
3351
+ }
3352
+ async function logProgress(progress, total) {
3353
+ if (windowLogging) {
3354
+ window.imageExporterProgress.push([progress, total]);
3355
+ }
3356
+ }
3357
+ window.imageExporterLogs = [];
3358
+ window.imageExporterProgress = [];
3359
+ function isValidUrl(string) {
3360
+ try {
3361
+ const url = new URL(string);
3362
+ if (url.protocol === "data:") {
3363
+ return false;
3684
3364
  }
3685
- },
3686
- zip: {
3687
- label: {
3688
- value: "images",
3689
- attributeSelector: "ie-zip-label",
3690
- inputSelector: "ie-zip-label-input"
3691
- },
3692
- dateInLabel: {
3693
- value: true,
3694
- attributeSelector: "ie-zip-label-date",
3695
- inputSelector: "ie-zip-label-date-input"
3696
- },
3697
- scaleInLabel: {
3698
- value: true,
3699
- attributeSelector: "ie-zip-label-scale",
3700
- inputSelector: "ie-zip-label-scale-input"
3365
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
3366
+ return false;
3701
3367
  }
3702
- },
3703
- debug: false
3368
+ return true;
3369
+ } catch (_) {
3370
+ return false;
3371
+ }
3372
+ }
3373
+ async function proxyCSS(config) {
3374
+ const stylesheetElements = document.querySelectorAll('link[rel="stylesheet"]');
3375
+ log.verbose("stylesheet elements to proxy", stylesheetElements.length);
3376
+ for (let stylesheetElement of stylesheetElements) {
3377
+ const stylesheetURL = stylesheetElement.getAttribute("href");
3378
+ if (!stylesheetURL) continue;
3379
+ if (stylesheetURL.startsWith("data:")) continue;
3380
+ if (stylesheetURL.startsWith(config.corsProxyBaseUrl)) continue;
3381
+ if (!isValidUrl(stylesheetURL)) continue;
3382
+ stylesheetElement.setAttribute("crossorigin", "anonymous");
3383
+ const proxiedURL = config.corsProxyBaseUrl + encodeURIComponent(stylesheetURL);
3384
+ try {
3385
+ const response = await fetch(proxiedURL);
3386
+ let cssContent = await response.text();
3387
+ cssContent = cssContent.replace(
3388
+ /url\(['"]?(https?:\/\/[^'")\s]+)['"]?\)/g,
3389
+ (match, url) => {
3390
+ if (url.startsWith(config.corsProxyBaseUrl)) return match;
3391
+ return `url("${config.corsProxyBaseUrl}${encodeURIComponent(url)}")`;
3392
+ }
3393
+ );
3394
+ const styleElement = document.createElement("style");
3395
+ styleElement.textContent = cssContent;
3396
+ styleElement.setAttribute(
3397
+ "original-link-element",
3398
+ encodeURIComponent(stylesheetElement.outerHTML)
3399
+ );
3400
+ stylesheetElement.insertAdjacentElement("afterend", styleElement);
3401
+ stylesheetElement.remove();
3402
+ log.verbose("Proxied: ", stylesheetURL);
3403
+ } catch (error) {
3404
+ console.error("Error fetching CSS:", error);
3405
+ }
3406
+ }
3407
+ }
3408
+ async function proxyImages(config, elements) {
3409
+ try {
3410
+ const elementArray = Array.from(elements);
3411
+ if (!elementArray.length) return;
3412
+ log.verbose("images to proxy", elementArray.length);
3413
+ for (const element of elementArray) {
3414
+ const images = Array.from(element.querySelectorAll("img"));
3415
+ for (const img of images) {
3416
+ if (isValidUrl(img.src) && !img.src.startsWith(config.corsProxyBaseUrl)) {
3417
+ img.setAttribute("original-src", img.src);
3418
+ img.src = config.corsProxyBaseUrl + encodeURIComponent(img.src);
3419
+ log.verbose("Proxied: ", img.src);
3420
+ }
3421
+ }
3422
+ }
3423
+ } catch (e) {
3424
+ console.error("ImageExporter: Error in proxyImages", e);
3425
+ return;
3426
+ }
3427
+ }
3428
+ async function runCorsProxy(config, elements) {
3429
+ try {
3430
+ log.verbose("running CORS proxy");
3431
+ if (!config.corsProxyBaseUrl || !isValidUrl(config.corsProxyBaseUrl)) return;
3432
+ await proxyCSS(config);
3433
+ await proxyImages(config, elements);
3434
+ } catch (e) {
3435
+ console.error("ImageExporter: Error in runCorsProxy", e);
3436
+ }
3437
+ }
3438
+ async function cleanUpCorsProxy() {
3439
+ await restoreCSS();
3440
+ await restoreImages();
3441
+ }
3442
+ async function restoreCSS() {
3443
+ const styleElements = document.querySelectorAll("style[original-link-element]");
3444
+ for (let styleElement of styleElements) {
3445
+ const originalLinkElementHTML = decodeURIComponent(
3446
+ styleElement.getAttribute("original-link-element")
3447
+ );
3448
+ const tempContainer = document.createElement("div");
3449
+ tempContainer.innerHTML = originalLinkElementHTML;
3450
+ styleElement.parentNode.insertBefore(tempContainer.firstChild, styleElement);
3451
+ styleElement.remove();
3452
+ log.verbose("Restored: ", originalLinkElementHTML);
3453
+ }
3454
+ }
3455
+ async function restoreImages() {
3456
+ const imageElements = document.querySelectorAll(
3457
+ "img[original-src]"
3458
+ );
3459
+ for (let imageElement of imageElements) {
3460
+ const originalSrc = imageElement.getAttribute("original-src");
3461
+ imageElement.src = originalSrc;
3462
+ imageElement.removeAttribute("original-src");
3463
+ log.verbose("Restored: ", originalSrc);
3464
+ }
3465
+ }
3466
+ const corsProxy = {
3467
+ run: runCorsProxy,
3468
+ cleanUp: cleanUpCorsProxy
3704
3469
  };
3705
- function cleanUp(options, captureElements) {
3706
- const sourceElements = document.querySelectorAll("[ie-clone-source]");
3707
- if (sourceElements) {
3708
- sourceElements.forEach((sourceElement) => {
3709
- const attributeValue = sourceElement.getAttribute("ie-clone-source");
3710
- sourceElement.removeAttribute("ie-clone-source");
3711
- if (attributeValue) {
3712
- sourceElement.setAttribute(options.image.scale.attributeSelector, attributeValue);
3713
- }
3714
- let parentElement = sourceElement.parentElement;
3715
- let lastCloneParent = null;
3716
- while (parentElement) {
3717
- if (parentElement.hasAttribute("ie-clone")) {
3718
- lastCloneParent = parentElement;
3719
- }
3720
- parentElement = parentElement.parentElement;
3721
- }
3722
- if (lastCloneParent) {
3723
- const nextSibling = lastCloneParent.nextElementSibling;
3724
- if (nextSibling) {
3725
- nextSibling.parentNode.insertBefore(sourceElement, nextSibling);
3726
- } else {
3727
- lastCloneParent.parentNode.appendChild(sourceElement);
3470
+ async function getImageOptions(element, config) {
3471
+ return {
3472
+ label: parseLabel2(),
3473
+ format: parseFormat(),
3474
+ scale: parseScale(),
3475
+ quality: parseQuality(),
3476
+ includeScaleInLabel: parseIncludeScaleInLabel()
3477
+ };
3478
+ function parseLabel2() {
3479
+ try {
3480
+ const label = element.dataset.label || config.defaultImageLabel;
3481
+ if (label === "") return config.defaultImageLabel;
3482
+ const regex = /[^a-zA-Z0-9-_]/g;
3483
+ return label.replace(regex, "");
3484
+ } catch (error) {
3485
+ console.error(error);
3486
+ return config.defaultImageLabel;
3487
+ }
3488
+ }
3489
+ function parseFormat() {
3490
+ try {
3491
+ let format = element.dataset.format || config.format;
3492
+ format = format.trim().toLowerCase();
3493
+ if (format === "jpg" || format === "png" || format === "svg") {
3494
+ return format;
3495
+ } else {
3496
+ throw new Error(
3497
+ `ImageExporter: provided format is not valid.
3498
+ Provided: ${format}
3499
+ Element: ${element}
3500
+ Accepted values: jpg, png, svg
3501
+ Defaulting to: ${config.format}`
3502
+ );
3503
+ }
3504
+ } catch (error) {
3505
+ console.error(error);
3506
+ return config.format;
3507
+ }
3508
+ }
3509
+ function parseScale() {
3510
+ try {
3511
+ const scaleAsString = element.dataset.scale;
3512
+ if (!scaleAsString) return config.scale;
3513
+ if (scaleAsString.includes(",")) {
3514
+ const scales = scaleAsString.trim().split(",").map((scale) => parseFloat(scale));
3515
+ if (scales.some((scale) => isNaN(scale))) {
3516
+ throw new Error(
3517
+ `ImageExporter: provided scale is not valid.
3518
+ Provided: ${scaleAsString}
3519
+ Element: ${element}
3520
+ Accepted values: number or csv numbers e.g. (1,2)
3521
+ Defaulting to ${config.scale}`
3522
+ );
3728
3523
  }
3729
- lastCloneParent.remove();
3524
+ return scales;
3525
+ } else {
3526
+ const scaleAsNumber = parseFloat(scaleAsString.trim());
3527
+ if (isNaN(scaleAsNumber)) {
3528
+ throw new Error(
3529
+ `ImageExporter: provided scale is not valid.
3530
+ Provided: ${scaleAsString}
3531
+ Element: ${element}
3532
+ Accepted values: number or csv numbers e.g. (1,2)
3533
+ Defaulting to: ${config.scale}`
3534
+ );
3535
+ }
3536
+ return scaleAsNumber;
3730
3537
  }
3731
- });
3538
+ } catch (error) {
3539
+ console.error(error);
3540
+ return config.scale;
3541
+ }
3542
+ }
3543
+ function parseQuality() {
3544
+ try {
3545
+ const qualityAsString = element.dataset.quality;
3546
+ if (!qualityAsString) return config.quality;
3547
+ const qualityAsNumber = parseFloat(qualityAsString.trim());
3548
+ if (isNaN(qualityAsNumber)) {
3549
+ throw new Error(
3550
+ `ImageExporter: provided quality is not valid.
3551
+ Provided: ${qualityAsString}
3552
+ Element: ${element}
3553
+ Accepted values: number
3554
+ Defaulting to: ${config.quality}`
3555
+ );
3556
+ }
3557
+ return qualityAsNumber;
3558
+ } catch (error) {
3559
+ console.error(error);
3560
+ return config.quality;
3561
+ }
3562
+ }
3563
+ function parseIncludeScaleInLabel() {
3564
+ try {
3565
+ let includeScaleInLabel = element.dataset.includeScaleInLabel;
3566
+ if (!includeScaleInLabel) return config.includeScaleInLabel;
3567
+ includeScaleInLabel = includeScaleInLabel.trim();
3568
+ if (includeScaleInLabel === "true" || includeScaleInLabel === "false") {
3569
+ return includeScaleInLabel === "true";
3570
+ } else {
3571
+ throw new Error(
3572
+ `ImageExporter: provided includeScaleInLabel is not valid.
3573
+ Provided: ${includeScaleInLabel}
3574
+ Element: ${element}
3575
+ Accepted values: true or false
3576
+ Defaulting to: ${config.includeScaleInLabel}`
3577
+ );
3578
+ }
3579
+ } catch (error) {
3580
+ console.error(error);
3581
+ return config.includeScaleInLabel;
3582
+ }
3732
3583
  }
3733
3584
  }
3734
- class ImageExporter {
3735
- constructor(userOptions = {}) {
3736
- this.options = { ...defaultOptions, ...userOptions };
3737
- }
3738
- /**
3739
- * Captures images from all elements specified in the options.
3740
- * If downloadImages is set to true, the images will be downloaded.
3741
- *
3742
- * @returns {types.Image[]} An array of captured images.
3743
- */
3744
- async captureAll() {
3745
- this.options = determineOptions(this.options);
3746
- const captureElements = getCaptureElements(this.options);
3747
- const images = await captureImages(this.options, captureElements);
3748
- if (this.options.downloadImages) downloadImages(images, this.options);
3749
- cleanUp(this.options);
3750
- return images;
3585
+ const defaultImageOptions = {
3586
+ label: "image",
3587
+ format: "jpg",
3588
+ scale: 1,
3589
+ quality: 1,
3590
+ includeScaleInLabel: false
3591
+ };
3592
+ const defaultConfig = {
3593
+ ...defaultImageOptions,
3594
+ downloadImages: true,
3595
+ defaultImageLabel: "image",
3596
+ zipLabel: "images",
3597
+ corsProxyBaseUrl: "",
3598
+ enableWindowLogging: true,
3599
+ loggingLevel: "none"
3600
+ };
3601
+ function removeHiddenElements(elements) {
3602
+ elements = Array.from(elements);
3603
+ return elements.filter((element) => isVisible(element));
3604
+ }
3605
+ function isVisible(element) {
3606
+ const computedStyle = window.getComputedStyle(element);
3607
+ return element.offsetParent !== null && element.style.display !== "none" && computedStyle.visibility === "visible" && computedStyle.opacity !== "0" && computedStyle.width !== "0" && computedStyle.height !== "0";
3608
+ }
3609
+ async function determineTotalElements(elements) {
3610
+ try {
3611
+ let totalElements = 0;
3612
+ for (const element of elements) {
3613
+ const scaleAsString = element.dataset.scale;
3614
+ if (!scaleAsString) continue;
3615
+ if (scaleAsString.includes(",")) {
3616
+ const scales = scaleAsString.trim().split(",").map((scale) => parseFloat(scale));
3617
+ if (scales.some((scale) => isNaN(scale))) continue;
3618
+ totalElements += scales.length;
3619
+ } else {
3620
+ const scaleAsNumber = parseFloat(scaleAsString.trim());
3621
+ if (isNaN(scaleAsNumber)) {
3622
+ continue;
3623
+ } else {
3624
+ totalElements++;
3625
+ }
3626
+ }
3627
+ }
3628
+ return totalElements;
3629
+ } catch (error) {
3630
+ return 1;
3751
3631
  }
3752
- /**
3753
- * Captures an image from a single element.
3754
- * If downloadImages is set to true, the image will be downloaded.
3755
- *
3756
- * If multiscale elements are found,
3757
- * the element will be cloned and captured at each scale.
3758
- */
3759
- async captureElement(element) {
3760
- this.options = determineOptions(this.options);
3761
- await runCorsProxy(this.options);
3762
- ignoreFilter(this.options);
3763
- const multiScale = findMultiScaleElements(this.options);
3764
- if (multiScale) {
3765
- const elements = Array.from(
3766
- document.querySelectorAll(
3767
- "[ie-clone], [ie-clone-source]"
3768
- )
3632
+ }
3633
+ let windowLogging = true;
3634
+ let loggingLevel = "none";
3635
+ async function capture(elements, userConfig) {
3636
+ try {
3637
+ const config = { ...defaultConfig, ...userConfig };
3638
+ windowLogging = config.enableWindowLogging;
3639
+ loggingLevel = config.loggingLevel;
3640
+ log.verbose("config", config);
3641
+ if (elements instanceof HTMLElement) elements = [elements];
3642
+ const originalLength = elements.length;
3643
+ elements = removeHiddenElements(elements);
3644
+ const totalElements = await determineTotalElements(elements);
3645
+ if (originalLength !== elements.length)
3646
+ log.verbose(
3647
+ "skipping capture of hidden elements: ",
3648
+ originalLength - elements.length
3769
3649
  );
3770
- const images = await captureImages(this.options, elements);
3771
- if (this.options.downloadImages) downloadImages(images, this.options);
3772
- cleanUp(this.options);
3773
- return images;
3774
- }
3775
- const image = await captureImage(
3776
- element,
3777
- getItemOptions(element, this.options, 1)
3778
- );
3779
- if (this.options.downloadImages) downloadImages([image], this.options);
3780
- cleanUp(this.options);
3781
- return image;
3782
- }
3783
- /**
3784
- * Adds a click event listener to the trigger element.
3785
- * If no element is provided, the captureAll method will be run on click.
3786
- * If an element is provided, provided element will be captured on click.
3787
- */
3788
- addTrigger(triggerSelector, element = null) {
3789
- const triggerElement = document.querySelector(triggerSelector);
3790
- if (!triggerElement) throw new Error("Trigger element not found");
3791
- if (!element) {
3792
- triggerElement.addEventListener("click", () => {
3793
- this.captureAll();
3794
- });
3795
- } else {
3796
- triggerElement.addEventListener("click", () => {
3797
- this.captureElement(element);
3798
- });
3650
+ log.verbose("element to capture", elements.length);
3651
+ if (userConfig.corsProxyBaseUrl) await corsProxy.run(config, elements);
3652
+ let images = [];
3653
+ let filenames = [];
3654
+ let imageNumber = 1;
3655
+ for (const element of elements) {
3656
+ const imageOptions = await getImageOptions(element, config);
3657
+ log.verbose("image options", imageOptions);
3658
+ if (imageOptions.scale instanceof Array) {
3659
+ log.verbose("multi-scale capture");
3660
+ imageOptions.includeScaleInLabel = true;
3661
+ for (const scale of imageOptions.scale) {
3662
+ log.progress(imageNumber++, totalElements);
3663
+ const image = await captureElement(
3664
+ element,
3665
+ { ...imageOptions, scale },
3666
+ filenames
3667
+ );
3668
+ images.push(image);
3669
+ }
3670
+ } else if (typeof imageOptions.scale === "number") {
3671
+ log.progress(imageNumber++, totalElements);
3672
+ log.verbose("single-scale capture");
3673
+ const image = await captureElement(
3674
+ element,
3675
+ imageOptions,
3676
+ filenames
3677
+ );
3678
+ images.push(image);
3679
+ }
3799
3680
  }
3681
+ if (userConfig.downloadImages) downloadImages(images, config);
3682
+ if (userConfig.corsProxyBaseUrl) await corsProxy.cleanUp();
3683
+ return images;
3684
+ } catch (error) {
3685
+ log.error(error);
3686
+ return null;
3800
3687
  }
3801
3688
  }
3802
3689
  if (typeof window !== "undefined") {
3803
- window.ImageExporter = ImageExporter;
3690
+ window.imageExporter = capture;
3804
3691
  }
3805
3692
  export {
3806
- ImageExporter
3693
+ capture
3807
3694
  };