image-exporter 0.0.1
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/.gitattributes +2 -0
- package/.prettierrc +5 -0
- package/LICENSE.md +201 -0
- package/README.md +3 -0
- package/dist/image-exporter.es.js +3807 -0
- package/dist/image-exporter.umd.js +3813 -0
- package/example/example.css +122 -0
- package/example/example.html +152 -0
- package/example/github.jpg +0 -0
- package/example/poll-h.svg +1 -0
- package/package.json +50 -0
- package/src/capture-images.ts +129 -0
- package/src/clean-up.ts +50 -0
- package/src/cors-proxy/index.ts +22 -0
- package/src/cors-proxy/proxy-css.ts +52 -0
- package/src/cors-proxy/proxy-images.ts +90 -0
- package/src/default-options.ts +58 -0
- package/src/download-images.ts +52 -0
- package/src/get-capture-element.test.html +21 -0
- package/src/get-capture-element.test.ts +36 -0
- package/src/get-capture-element.ts +175 -0
- package/src/get-options/get-input-options.test.html +217 -0
- package/src/get-options/get-input-options.test.ts +109 -0
- package/src/get-options/get-input-options.ts +40 -0
- package/src/get-options/get-item-options.ts +46 -0
- package/src/get-options/get-wrapper-options.test.html +33 -0
- package/src/get-options/get-wrapper-options.test.ts +109 -0
- package/src/get-options/get-wrapper-options.ts +84 -0
- package/src/get-options/index.ts +28 -0
- package/src/image-exporter.ts +108 -0
- package/src/index.ts +8 -0
- package/src/types/image.ts +2 -0
- package/src/types/index.ts +2 -0
- package/src/types/options.ts +69 -0
- package/src/utils/convert-to-slug.ts +15 -0
- package/src/utils/get-attribute-values.ts +68 -0
- package/src/utils/get-date-MMDDYY.ts +11 -0
- package/src/utils/ignore-items.ts +11 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/is-valid-url.ts +20 -0
- package/src/utils/is-visible.ts +12 -0
- package/src/utils/parse-labels.ts +55 -0
- package/src/utils/push-to-window.ts +3 -0
- package/tests/index.html +88 -0
- package/tests/input-tests.html +169 -0
- package/vite.config.js +39 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
body {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: center;
|
|
4
|
+
margin: 0;
|
|
5
|
+
background-color: #151224;
|
|
6
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
7
|
+
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
|
|
8
|
+
sans-serif;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.wrapper {
|
|
12
|
+
display: grid;
|
|
13
|
+
width: 800px;
|
|
14
|
+
height: 500px;
|
|
15
|
+
grid-auto-columns: 1fr;
|
|
16
|
+
grid-column-gap: 16px;
|
|
17
|
+
grid-row-gap: 16px;
|
|
18
|
+
grid-template-columns: 1fr 2fr;
|
|
19
|
+
grid-template-rows: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.input-wrapper {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
align-items: flex-start;
|
|
26
|
+
color: #fff;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.input-label {
|
|
30
|
+
margin-bottom: 0px;
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.example-input {
|
|
35
|
+
margin-bottom: 1rem;
|
|
36
|
+
padding: 6px;
|
|
37
|
+
-webkit-align-self: stretch;
|
|
38
|
+
-ms-flex-item-align: stretch;
|
|
39
|
+
-ms-grid-row-align: stretch;
|
|
40
|
+
align-self: stretch;
|
|
41
|
+
border: 1px solid #7000ff;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
color: #000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.example-button {
|
|
47
|
+
display: -webkit-box;
|
|
48
|
+
display: -webkit-flex;
|
|
49
|
+
display: -ms-flexbox;
|
|
50
|
+
display: flex;
|
|
51
|
+
padding: 11px 22px;
|
|
52
|
+
background-color: #7000ff;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.example-wrapper {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
align-items: center;
|
|
60
|
+
grid-column-gap: 16px;
|
|
61
|
+
grid-row-gap: 16px;
|
|
62
|
+
border-style: dashed;
|
|
63
|
+
border-width: 2px;
|
|
64
|
+
border-color: #7000ff;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.example-2 {
|
|
69
|
+
display: flex;
|
|
70
|
+
width: 200px;
|
|
71
|
+
height: 200px;
|
|
72
|
+
padding: 22px;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
align-items: center;
|
|
75
|
+
background-color: #7000ff;
|
|
76
|
+
font-size: 24px;
|
|
77
|
+
line-height: 1;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
text-align: center;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.example-1 {
|
|
83
|
+
display: flex;
|
|
84
|
+
width: 400px;
|
|
85
|
+
height: 200px;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
align-items: center;
|
|
88
|
+
background-color: #fff;
|
|
89
|
+
font-size: 24px;
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.example-text-2 {
|
|
95
|
+
color: #fff;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.example-text-1 {
|
|
99
|
+
color: #7000ff;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.gf_loader {
|
|
103
|
+
display: none;
|
|
104
|
+
opacity: 0;
|
|
105
|
+
position: absolute;
|
|
106
|
+
width: 100dvw;
|
|
107
|
+
height: 100dvh;
|
|
108
|
+
z-index: 99;
|
|
109
|
+
background-color: rgba(25, 25, 25, 0.9);
|
|
110
|
+
color: #fff;
|
|
111
|
+
}
|
|
112
|
+
.gf_loader-message {
|
|
113
|
+
display: flex;
|
|
114
|
+
width: 100dvw;
|
|
115
|
+
height: 100dvh;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
}
|
|
119
|
+
.img {
|
|
120
|
+
max-width: 100%;
|
|
121
|
+
height: 100%;
|
|
122
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>image-exporter Example</title>
|
|
7
|
+
<link rel="stylesheet" href="example.css" />
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div class="gf_loader">
|
|
12
|
+
<div class="gf_loader-message">Loading...</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="wrapper">
|
|
15
|
+
<div>
|
|
16
|
+
<form action="" autocomplete="off" class="input-wrapper">
|
|
17
|
+
<!--You can set settings via user input like this-->
|
|
18
|
+
<label for="format" class="input-label">File Format</label>
|
|
19
|
+
<select title="format" gf-format-input class="example-input">
|
|
20
|
+
<option value="png">PNG</option>
|
|
21
|
+
<option value="jpg">JPG</option>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
<label for="scale" class="input-label">Scale</label>
|
|
25
|
+
<select title="scale" gf-scale-input class="example-input">
|
|
26
|
+
<option value="1">@1x</option>
|
|
27
|
+
<option value="2">@2x</option>
|
|
28
|
+
<option value="3">@3x</option>
|
|
29
|
+
<option value="4">@4x</option>
|
|
30
|
+
</select>
|
|
31
|
+
|
|
32
|
+
<label for="zipname" class="input-label">Zip Name</label>
|
|
33
|
+
<input
|
|
34
|
+
title="zipname"
|
|
35
|
+
type="text"
|
|
36
|
+
value="images"
|
|
37
|
+
gf-zip-label-input
|
|
38
|
+
class="example-input"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<label for="zip-label-date" class="input-label">Zip Label Date</label>
|
|
42
|
+
<select title="zip-label-date" gf-zip-label-date-input class="example-input">
|
|
43
|
+
<option value="true">Enabled</option>
|
|
44
|
+
<option value="false">Disabled</option>
|
|
45
|
+
</select>
|
|
46
|
+
|
|
47
|
+
<label for="zip-label-scale" class="input-label">Zip Label Scale</label>
|
|
48
|
+
<select title="zip-label-scale" gf-zip-label-scale-input class="example-input">
|
|
49
|
+
<option value="true">Enabled</option>
|
|
50
|
+
<option value="false">Disabled</option>
|
|
51
|
+
</select>
|
|
52
|
+
|
|
53
|
+
<div gf="trigger" class="example-button">Generate</div>
|
|
54
|
+
</form>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="example-wrapper" gf="wrapper" gf-zip-label="test">
|
|
57
|
+
<div class="example-1" gf="capture" gf-scale="1,4">
|
|
58
|
+
<div class="example-text-1" gf="slug">Example image 1</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="example-2" gf="capture">
|
|
61
|
+
<div class="example-text-2" gf="slug">Example image 2</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<script src="../dist/image-exporter.umd.js" type="text/javascript"></script>
|
|
66
|
+
<script>
|
|
67
|
+
let options = {
|
|
68
|
+
corsProxyBaseUrl: "http://localhost:8010/",
|
|
69
|
+
selectors: {
|
|
70
|
+
wrapper: "[gf='wrapper']",
|
|
71
|
+
capture: "[gf='capture']",
|
|
72
|
+
trigger: "[gf='trigger']",
|
|
73
|
+
slug: "[gf='slug']",
|
|
74
|
+
ignore: "[gf='ignore']",
|
|
75
|
+
},
|
|
76
|
+
image: {
|
|
77
|
+
scale: {
|
|
78
|
+
value: 1,
|
|
79
|
+
attributeSelector: "gf-scale",
|
|
80
|
+
inputSelector: "gf-scale-input",
|
|
81
|
+
},
|
|
82
|
+
quality: {
|
|
83
|
+
value: 1,
|
|
84
|
+
attributeSelector: "gf-quality",
|
|
85
|
+
inputSelector: "gf-quality-input",
|
|
86
|
+
},
|
|
87
|
+
format: {
|
|
88
|
+
value: "jpg",
|
|
89
|
+
attributeSelector: "gf-format",
|
|
90
|
+
inputSelector: "gf-format-input",
|
|
91
|
+
},
|
|
92
|
+
dateInLabel: {
|
|
93
|
+
value: true,
|
|
94
|
+
attributeSelector: "gf-img-label-date",
|
|
95
|
+
inputSelector: "gf-img-label-date-input",
|
|
96
|
+
},
|
|
97
|
+
scaleInLabel: {
|
|
98
|
+
value: true,
|
|
99
|
+
attributeSelector: "gf-img-label-scale",
|
|
100
|
+
inputSelector: "gf-img-label-scale-input",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
zip: {
|
|
104
|
+
label: {
|
|
105
|
+
value: "images",
|
|
106
|
+
attributeSelector: "gf-zip-label",
|
|
107
|
+
inputSelector: "gf-zip-label-input",
|
|
108
|
+
},
|
|
109
|
+
dateInLabel: {
|
|
110
|
+
value: true,
|
|
111
|
+
attributeSelector: "gf-zip-label-date",
|
|
112
|
+
inputSelector: "gf-zip-label-date-input",
|
|
113
|
+
},
|
|
114
|
+
scaleInLabel: {
|
|
115
|
+
value: true,
|
|
116
|
+
attributeSelector: "gf-zip-label-scale",
|
|
117
|
+
inputSelector: "gf-zip-label-scale-input",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let imageExporter = new ImageExporter({ options });
|
|
123
|
+
imageExporter.addTrigger(document.querySelector("[gf=trigger]"));
|
|
124
|
+
// imageExporter.addTrigger(
|
|
125
|
+
// document.querySelector("[gf=test-trigger]"),
|
|
126
|
+
// document.querySelector("[test=test]")
|
|
127
|
+
// );
|
|
128
|
+
</script>
|
|
129
|
+
<script>
|
|
130
|
+
let optionsx = {
|
|
131
|
+
inputPrefix: "gf",
|
|
132
|
+
attributes: {
|
|
133
|
+
slugSelector: "[gf='slug']",
|
|
134
|
+
wrapperSelector: '[gf="wrapper"]',
|
|
135
|
+
captureSelector: '[gf="capture"]',
|
|
136
|
+
triggerSelector: '[gf="trigger"]',
|
|
137
|
+
slugSelector: '[gf="slug"]',
|
|
138
|
+
ignoreSelector: '[gf="ignore"]',
|
|
139
|
+
scale: "gf-scale",
|
|
140
|
+
quality: "gf-quality",
|
|
141
|
+
format: "gf-format",
|
|
142
|
+
zipLabel: "gf-zip-label",
|
|
143
|
+
zipLabelDate: "gf-zip-label-date",
|
|
144
|
+
zipLabelScale: "gf-zip-label-scale",
|
|
145
|
+
imgLabelDate: "gf-img-label-date",
|
|
146
|
+
imgLabelScale: "gf-img-label-scale",
|
|
147
|
+
corsProxyBaseUrl: "gf-cors-proxy-base-url",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
</script>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M448 432V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48zM112 192c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h128c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h224c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h64c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16h-64z"/></svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-exporter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"directories": {
|
|
7
|
+
"dist": "dist",
|
|
8
|
+
"src": "src",
|
|
9
|
+
"example": "example"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/image-exporter.es.js",
|
|
14
|
+
"require": "./dist/image-exporter.umd.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "vite",
|
|
19
|
+
"watch": "NODE_ENV=development vite build --watch",
|
|
20
|
+
"build": "NODE_ENV=production vite build",
|
|
21
|
+
"test": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/briantuckerdesign/image-exporter.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"image exporter"
|
|
29
|
+
],
|
|
30
|
+
"author": "Brian Tucker",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/briantuckerdesign/image-exporter/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/briantuckerdesign/image-exporter#readme",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"downloadjs": "^1.4.7",
|
|
38
|
+
"html-to-image": "^1.11.11",
|
|
39
|
+
"jszip": "^3.10.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@rollup/plugin-strip": "^3.0.4",
|
|
43
|
+
"@types/jest": "^29.5.12",
|
|
44
|
+
"@vitest/ui": "^2.0.4",
|
|
45
|
+
"puppeteer": "^22.14.0",
|
|
46
|
+
"typescript": "^5.3.3",
|
|
47
|
+
"vite": "^5.2.4",
|
|
48
|
+
"vitest": "^2.0.4"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as types from "./types";
|
|
2
|
+
import * as htmlToImage from "html-to-image";
|
|
3
|
+
import { runCorsProxy } from "./cors-proxy";
|
|
4
|
+
import { ignoreFilter } from "./utils/ignore-items";
|
|
5
|
+
import { getItemOptions } from "./get-options";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Asynchronously captures images from a set of DOM elements using specified options.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* This function is designed to capture images from a collection of elements. It first initializes
|
|
12
|
+
* a CORS proxy and prepares nodes that should be ignored during the image capture process. Then, for
|
|
13
|
+
* each element in the `captureElements` array, it captures an image using the `captureImage` function
|
|
14
|
+
* with options tailored to each element. The function handles these operations asynchronously and
|
|
15
|
+
* collects all the captured images in an array.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} options - An object containing global options for image capture.
|
|
18
|
+
* @param {Array<HTMLElement>} captureElements - An array of DOM elements from which images will be captured.
|
|
19
|
+
* @returns {Promise<Array>} A promise that resolves to an array of captured images.
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
export async function captureImages(
|
|
23
|
+
options: types.Options,
|
|
24
|
+
captureElements: HTMLElement[]
|
|
25
|
+
): Promise<types.Image[]> {
|
|
26
|
+
try {
|
|
27
|
+
// When enabled, replaces urls with proxied ones to bypass CORS errors.
|
|
28
|
+
await runCorsProxy(options);
|
|
29
|
+
|
|
30
|
+
// Gets array of tuples representing images, see captureImage() documentation for more info
|
|
31
|
+
const images = await Promise.all(
|
|
32
|
+
captureElements.map((element, index) =>
|
|
33
|
+
captureImage(
|
|
34
|
+
element,
|
|
35
|
+
getItemOptions(element, options, index + 1),
|
|
36
|
+
ignoreFilter(options)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
console.log(images);
|
|
41
|
+
return images;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error("ImageExporter: Error in captureImages", e);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Asynchronously captures an image from a DOM element with specific options.
|
|
50
|
+
*
|
|
51
|
+
* This function is responsible for capturing an image of the provided DOM element. It updates a
|
|
52
|
+
* loading message based on the 'slug' property in the options. The image capture is then performed
|
|
53
|
+
* using the 'htmlToImage' library, with settings tailored according to the provided options. The function
|
|
54
|
+
* supports different image formats and handles quality and scaling. The result is an image encoded
|
|
55
|
+
* as a data URL along with its filename.
|
|
56
|
+
*
|
|
57
|
+
* @param {HTMLElement} element - The DOM element from which the image is to be captured.
|
|
58
|
+
* @param {Object} itemOptions - An object containing options for the capture process. It includes properties
|
|
59
|
+
* like 'slug', 'format', 'quality', 'scale', and 'loaderEnabled'.
|
|
60
|
+
* @returns {Promise<[string, string]>} A promise that resolves to a tuple: [dataURL, fileName].
|
|
61
|
+
* 'dataURL' is the base64 encoded image, and 'fileName' is the name of the image file.
|
|
62
|
+
*
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
export async function captureImage(
|
|
66
|
+
element,
|
|
67
|
+
itemOptions: types.ItemOptions,
|
|
68
|
+
ignoreFilter
|
|
69
|
+
): Promise<types.Image> {
|
|
70
|
+
try {
|
|
71
|
+
itemOptions.slug = ensureUniqueSlug(itemOptions.slug);
|
|
72
|
+
|
|
73
|
+
let dataURL = "";
|
|
74
|
+
// Final settings for capturing images.
|
|
75
|
+
let htmlToImageOptions = {
|
|
76
|
+
// Ensure quality is a number
|
|
77
|
+
quality: itemOptions.image.quality.value,
|
|
78
|
+
// Ensure scale is a number
|
|
79
|
+
pixelRatio: itemOptions.image.scale.value,
|
|
80
|
+
// Function that returns false if the element should be ignored
|
|
81
|
+
// filter: ignoreFilter,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Captures image based on format
|
|
85
|
+
switch (itemOptions.image.format.value.toLowerCase()) {
|
|
86
|
+
case "jpg":
|
|
87
|
+
dataURL = await htmlToImage.toJpeg(element, htmlToImageOptions);
|
|
88
|
+
itemOptions.fileName = `${itemOptions.slug}.jpg`;
|
|
89
|
+
console.log("Captured image as jpg", itemOptions.fileName);
|
|
90
|
+
break;
|
|
91
|
+
case "png":
|
|
92
|
+
default:
|
|
93
|
+
dataURL = await htmlToImage.toPng(element, htmlToImageOptions);
|
|
94
|
+
itemOptions.fileName = `${itemOptions.slug}.png`;
|
|
95
|
+
console.log("Captured image as png", itemOptions.fileName);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
// const image: types.Image = [dataURL, itemOptions.fileName];
|
|
99
|
+
|
|
100
|
+
// returns image stored in tuple. [dataURL, fileName]
|
|
101
|
+
return [dataURL, itemOptions.fileName];
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error("ImageExporter: Error in captureImage", e);
|
|
104
|
+
return ["", ""];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let usedSlugs: any = [];
|
|
109
|
+
|
|
110
|
+
function ensureUniqueSlug(slug: string): string {
|
|
111
|
+
try {
|
|
112
|
+
if (usedSlugs.includes(slug)) {
|
|
113
|
+
let counter = 1;
|
|
114
|
+
let newSlug = `${slug}-${counter}`;
|
|
115
|
+
while (usedSlugs.includes(newSlug)) {
|
|
116
|
+
counter++;
|
|
117
|
+
newSlug = `${slug}-${counter}`;
|
|
118
|
+
}
|
|
119
|
+
usedSlugs.push(newSlug);
|
|
120
|
+
return newSlug;
|
|
121
|
+
} else {
|
|
122
|
+
usedSlugs.push(slug);
|
|
123
|
+
return slug;
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error("ImageExporter: Error in ensureUniqueSlug", e);
|
|
127
|
+
return slug;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/clean-up.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as types from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function cleans up after the `findMultiScaleElements` feature.
|
|
5
|
+
* It removes cloned elements and sets the `ie-scale` attribute back to the csv value.
|
|
6
|
+
*
|
|
7
|
+
* Eventually this may become a subfunction, with this function having more tasks.
|
|
8
|
+
*
|
|
9
|
+
* @param options
|
|
10
|
+
* @param captureElements
|
|
11
|
+
*/
|
|
12
|
+
export function cleanUp(options: types.Options, captureElements: Element[]) {
|
|
13
|
+
// Find 'ie-clone-source', if exists...
|
|
14
|
+
const sourceElements = document.querySelectorAll("[ie-clone-source]");
|
|
15
|
+
if (sourceElements) {
|
|
16
|
+
console.log("ping");
|
|
17
|
+
sourceElements.forEach((sourceElement) => {
|
|
18
|
+
// Set attribute options.attributes.scale.attributeSelector to the value of 'ie-clone-source'
|
|
19
|
+
const attributeValue = sourceElement.getAttribute("ie-clone-source");
|
|
20
|
+
sourceElement.removeAttribute("ie-clone-source");
|
|
21
|
+
if (attributeValue) {
|
|
22
|
+
console.log("pong");
|
|
23
|
+
sourceElement.setAttribute(options.image.scale.attributeSelector, attributeValue);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Climb DOM to find last parent with 'ie-clone' attribute
|
|
27
|
+
let parentElement = sourceElement.parentElement;
|
|
28
|
+
let lastCloneParent: any = null;
|
|
29
|
+
while (parentElement) {
|
|
30
|
+
if (parentElement.hasAttribute("ie-clone")) {
|
|
31
|
+
lastCloneParent = parentElement;
|
|
32
|
+
}
|
|
33
|
+
parentElement = parentElement.parentElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (lastCloneParent) {
|
|
37
|
+
// Move sourceElement to next sibling of the last parent with 'ie-clone' attribute
|
|
38
|
+
const nextSibling = lastCloneParent.nextElementSibling;
|
|
39
|
+
if (nextSibling) {
|
|
40
|
+
nextSibling.parentNode.insertBefore(sourceElement, nextSibling);
|
|
41
|
+
} else {
|
|
42
|
+
lastCloneParent.parentNode.appendChild(sourceElement);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Remove the last parent with 'ie-clone' attribute
|
|
46
|
+
lastCloneParent.remove();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isValidUrl } from "../utils";
|
|
2
|
+
import { proxyCSS } from "./proxy-css";
|
|
3
|
+
import { proxyImages } from "./proxy-images";
|
|
4
|
+
import { Options } from "../types/options";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* runCorsProxy - Initializes a CORS proxy by processing image and CSS resources on a web page.
|
|
8
|
+
* Logs the total number of calls made to the proxy server.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export async function runCorsProxy(options: Options): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
if (!options.corsProxyBaseUrl || !isValidUrl(options.corsProxyBaseUrl)) return;
|
|
14
|
+
|
|
15
|
+
await proxyCSS(options);
|
|
16
|
+
await proxyImages(options);
|
|
17
|
+
|
|
18
|
+
return;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error("ImageExporter: Error in runCorsProxy", e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isValidUrl } from "../utils";
|
|
2
|
+
import * as types from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* proxyCSS - Processes CSS stylesheets linked in the document to use the CORS proxy.
|
|
5
|
+
* Each valid and non-data URL stylesheet's href attribute is updated with the proxy URL.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} options - Configuration settings, including the CORS proxy base URL.
|
|
8
|
+
* Expected properties:
|
|
9
|
+
* - corsProxyBaseURL: String - The base URL of the CORS proxy server.
|
|
10
|
+
* @param {number} proxyPings - Initial count of proxy server pings.
|
|
11
|
+
* @returns {Promise<number>} - Returns the updated count of proxy server pings after processing stylesheets.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export async function proxyCSS(options: types.Options) {
|
|
15
|
+
try {
|
|
16
|
+
const css = document.querySelectorAll('link[rel="stylesheet"]');
|
|
17
|
+
|
|
18
|
+
for (let stylesheetElement of css) {
|
|
19
|
+
let stylesheetURL = stylesheetElement.getAttribute("href");
|
|
20
|
+
|
|
21
|
+
// Check if the URL is valid and not a base64 encoded string
|
|
22
|
+
if (
|
|
23
|
+
stylesheetURL &&
|
|
24
|
+
!stylesheetURL.startsWith("data:") &&
|
|
25
|
+
isValidUrl(stylesheetURL) &&
|
|
26
|
+
!stylesheetURL.startsWith(options.corsProxyBaseUrl)
|
|
27
|
+
) {
|
|
28
|
+
const url = options.corsProxyBaseUrl + encodeURIComponent(stylesheetURL);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Fetch the CSS content
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
const css = await response.text();
|
|
34
|
+
|
|
35
|
+
// Create a <style> element and set its content
|
|
36
|
+
const styleEl = document.createElement("style");
|
|
37
|
+
styleEl.textContent = css;
|
|
38
|
+
|
|
39
|
+
// Append the <style> element to the document's <head>
|
|
40
|
+
document.head.appendChild(styleEl);
|
|
41
|
+
|
|
42
|
+
// Remove the original <link> element
|
|
43
|
+
stylesheetElement.remove();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error fetching CSS:", error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("ImageExporter: Error in proxyCSS", e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { blobToDataURL, isValidUrl } from "../utils";
|
|
2
|
+
import * as types from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* proxyImages - Processes images within a specified wrapper element to use the CORS proxy.
|
|
5
|
+
* Groups images by their source, fetches and replaces the src with a data URL for duplicates,
|
|
6
|
+
* and prefixes the proxy URL for unique images.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Configuration settings, including the selector for the wrapper and CORS proxy base URL.
|
|
9
|
+
* Expected properties:
|
|
10
|
+
* - wrapperSelector: String - The CSS selector for the wrapper element containing images.
|
|
11
|
+
* - corsProxyBaseURL: String - The base URL of the CORS proxy server.
|
|
12
|
+
* @returns {Promise<number>} - Returns the number of times the proxy server was pinged.
|
|
13
|
+
*/
|
|
14
|
+
export async function proxyImages(options: types.Options) {
|
|
15
|
+
try {
|
|
16
|
+
// find all link tags in head and add crossorigin="anonymous"
|
|
17
|
+
const links = document.querySelectorAll("link");
|
|
18
|
+
links.forEach((link) => {
|
|
19
|
+
link.setAttribute("crossorigin", "anonymous");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const wrapper = document.querySelector(options.selectors.wrapper);
|
|
23
|
+
if (!wrapper) {
|
|
24
|
+
console.error("ImageExporter: Wrapper element not found.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const images = Array.from(wrapper.querySelectorAll("img")) as HTMLImageElement[];
|
|
28
|
+
|
|
29
|
+
const srcMap = new Map<string, HTMLImageElement[]>();
|
|
30
|
+
|
|
31
|
+
// Group images by src
|
|
32
|
+
images.forEach((img) => {
|
|
33
|
+
const srcs = srcMap.get(img.src) || [];
|
|
34
|
+
srcs.push(img);
|
|
35
|
+
srcMap.set(img.src, srcs);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
for (const [src, duplicates] of srcMap) {
|
|
39
|
+
if (
|
|
40
|
+
!isValidUrl(src) ||
|
|
41
|
+
(options.corsProxyBaseUrl && src.startsWith(options.corsProxyBaseUrl))
|
|
42
|
+
) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (duplicates.length > 1) {
|
|
46
|
+
// Fetch and replace src for duplicate images
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(
|
|
49
|
+
options.corsProxyBaseUrl + encodeURIComponent(src)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const blob = await response.blob();
|
|
53
|
+
const dataURL = await blobToDataURL(blob);
|
|
54
|
+
duplicates.forEach((dupImg) => {
|
|
55
|
+
if (dupImg.src === src) {
|
|
56
|
+
dupImg.src = dataURL;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Error fetching image:", error);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Prefix src for unique images
|
|
64
|
+
images.forEach((img) => {
|
|
65
|
+
if (img.src === src) {
|
|
66
|
+
img.src = options.corsProxyBaseUrl + encodeURIComponent(src);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error("ImageExporter: Error in proxyImages", e);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* blobToDataURL - Converts a Blob object to a data URL.
|
|
79
|
+
*
|
|
80
|
+
* @param {Blob} blob - The Blob object to be converted.
|
|
81
|
+
* @returns {Promise<string>} - Returns a Promise that resolves to a data URL string.
|
|
82
|
+
*/
|
|
83
|
+
function blobToDataURL(blob: Blob): Promise<string> {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const reader = new FileReader();
|
|
86
|
+
reader.onloadend = () => resolve(reader.result as string);
|
|
87
|
+
reader.onerror = reject;
|
|
88
|
+
reader.readAsDataURL(blob);
|
|
89
|
+
});
|
|
90
|
+
}
|