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.
- package/README.md +137 -2
- package/bun.lockb +0 -0
- package/dist/image-exporter.es.js +499 -612
- package/dist/image-exporter.umd.js +506 -619
- package/package.json +18 -15
- package/src/capture/capture-element.ts +58 -0
- package/src/capture/determine-total-elements.ts +40 -0
- package/src/capture/download-images.ts +69 -0
- package/src/capture/get-image-options.ts +196 -0
- package/src/capture/handle-filenames.ts +52 -0
- package/src/capture/index.ts +99 -0
- package/src/capture/remove-hidden-elements.ts +19 -0
- package/src/config.ts +19 -0
- package/src/cors-proxy/cleanup.ts +43 -0
- package/src/cors-proxy/index.ts +6 -21
- package/src/{utils → cors-proxy}/is-valid-url.ts +5 -3
- package/src/cors-proxy/proxy-css.ts +51 -44
- package/src/cors-proxy/proxy-images.ts +21 -77
- package/src/cors-proxy/run.ts +26 -0
- package/src/index.ts +10 -4
- package/src/logger.ts +61 -0
- package/src/types.d.ts +51 -0
- package/vite.config.js +3 -7
- package/example/example.css +0 -122
- package/example/example.html +0 -152
- package/example/github.jpg +0 -0
- package/example/poll-h.svg +0 -1
- package/src/capture-images.ts +0 -129
- package/src/clean-up.ts +0 -50
- package/src/default-options.ts +0 -58
- package/src/download-images.ts +0 -52
- package/src/get-capture-element.test.html +0 -21
- package/src/get-capture-element.test.ts +0 -36
- package/src/get-capture-element.ts +0 -175
- package/src/get-options/get-input-options.test.html +0 -217
- package/src/get-options/get-input-options.test.ts +0 -109
- package/src/get-options/get-input-options.ts +0 -40
- package/src/get-options/get-item-options.ts +0 -46
- package/src/get-options/get-wrapper-options.test.html +0 -33
- package/src/get-options/get-wrapper-options.test.ts +0 -109
- package/src/get-options/get-wrapper-options.ts +0 -84
- package/src/get-options/index.ts +0 -28
- package/src/image-exporter.ts +0 -108
- package/src/types/image.ts +0 -2
- package/src/types/index.ts +0 -2
- package/src/types/options.ts +0 -69
- package/src/utils/convert-to-slug.ts +0 -15
- package/src/utils/get-attribute-values.ts +0 -68
- package/src/utils/get-date-MMDDYY.ts +0 -11
- package/src/utils/ignore-items.ts +0 -11
- package/src/utils/index.ts +0 -18
- package/src/utils/is-visible.ts +0 -12
- package/src/utils/parse-labels.ts +0 -55
- package/src/utils/push-to-window.ts +0 -3
- package/tests/index.html +0 -88
- 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.
|
|
378
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
811
|
+
quality: imageOptions.quality,
|
|
1138
812
|
// Ensure scale is a number
|
|
1139
|
-
pixelRatio:
|
|
1140
|
-
//
|
|
1141
|
-
|
|
813
|
+
pixelRatio: imageOptions.scale,
|
|
814
|
+
// Ignores elements with data-ignore-capture attribute
|
|
815
|
+
filter
|
|
1142
816
|
};
|
|
1143
|
-
switch (
|
|
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
|
-
|
|
823
|
+
break;
|
|
824
|
+
case "svg":
|
|
825
|
+
dataURL = await toSvg(element, htmlToImageOptions);
|
|
1152
826
|
break;
|
|
1153
827
|
}
|
|
1154
|
-
return
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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,
|
|
3287
|
+
async function downloadImages(images, config) {
|
|
3626
3288
|
if (images.length === 1) {
|
|
3627
|
-
const
|
|
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
|
|
3631
|
-
|
|
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
|
-
|
|
3643
|
-
|
|
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
|
|
3646
|
-
|
|
3311
|
+
console.error("Image Exporter - Error generating ZIP:", error);
|
|
3312
|
+
return;
|
|
3647
3313
|
}
|
|
3648
3314
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
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
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
const
|
|
3776
|
-
element,
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
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.
|
|
3690
|
+
window.imageExporter = capture;
|
|
3804
3691
|
}
|
|
3805
3692
|
export {
|
|
3806
|
-
|
|
3693
|
+
capture
|
|
3807
3694
|
};
|