frontbacked-svg 5.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/LICENSE +191 -0
- package/README.md +41 -0
- package/dist/main.cjs +3785 -0
- package/dist/main.d.cts +316 -0
- package/dist/main.d.ts +316 -0
- package/dist/main.mjs +3681 -0
- package/package.json +103 -0
- package/src/base64Image.ts +101 -0
- package/src/cssParser.ts +59 -0
- package/src/filters.ts +226 -0
- package/src/font-utils.ts +204 -0
- package/src/get-default-fields-value.ts +64 -0
- package/src/getSvg.ts +284 -0
- package/src/imageHelper.ts +293 -0
- package/src/imagePassportUtils.ts +718 -0
- package/src/images-processor.ts +186 -0
- package/src/main.ts +1354 -0
- package/src/polyfills/DOMParser.ts +28 -0
- package/src/polyfills/File.ts +20 -0
- package/src/polyfills/Image.ts +90 -0
- package/src/svgScaler.ts +179 -0
- package/src/textGenCodeParser.ts +319 -0
- package/src/time.ts +147 -0
- package/src/toolsFunc.ts +79 -0
- package/src/types.ts +177 -0
- package/src/utils.ts +758 -0
- package/src/watermaker.ts +190 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { getCanvas } from "./imageHelper.ts";
|
|
2
|
+
import { ImageWrapper } from "./polyfills/Image.ts";
|
|
3
|
+
|
|
4
|
+
interface Point {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PointVariants {
|
|
10
|
+
x: number[],
|
|
11
|
+
y: number[]
|
|
12
|
+
}
|
|
13
|
+
interface Quadrilateral {
|
|
14
|
+
topLeft: Point;
|
|
15
|
+
topRight: Point;
|
|
16
|
+
bottomRight: Point;
|
|
17
|
+
bottomLeft: Point;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AnalyzeResult {
|
|
21
|
+
rotationAngle: number;
|
|
22
|
+
scaleX: number;
|
|
23
|
+
scaleY: number;
|
|
24
|
+
shearX: number;
|
|
25
|
+
shearY: number;
|
|
26
|
+
center: Point;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
corners: Quadrilateral;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DebugColors {
|
|
33
|
+
border?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isNode = typeof window === 'undefined';
|
|
37
|
+
|
|
38
|
+
const COLOR_THRESH = 10
|
|
39
|
+
const COLOR_THRESH_LOW = 4
|
|
40
|
+
|
|
41
|
+
const COLOR_MAIN_MARGIN = 16
|
|
42
|
+
const COLOR_OTHERS_DIFF = 1
|
|
43
|
+
|
|
44
|
+
const isValidPixel = (targetChannel: number, secondPixel: number, thirdPixel: number): boolean => {
|
|
45
|
+
//return targetChannel > COLOR_THRESH && secondPixel < targetChannel && thirdPixel < targetChannel
|
|
46
|
+
//return targetChannel > COLOR_THRESH && secondPixel < COLOR_THRESH_LOW && thirdPixel < COLOR_THRESH_LOW
|
|
47
|
+
const max = Math.max(secondPixel, thirdPixel)
|
|
48
|
+
return targetChannel > max && Math.abs(targetChannel - max) >= COLOR_MAIN_MARGIN && Math.abs(secondPixel - thirdPixel) <= COLOR_OTHERS_DIFF
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function base64ToImage(base64: string): Promise<any> {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const img = new Image();
|
|
54
|
+
img.onload = () => {
|
|
55
|
+
const realImage = ((img as any) as ImageWrapper).realImage? ((img as any) as ImageWrapper).realImage : img
|
|
56
|
+
resolve(realImage)
|
|
57
|
+
};
|
|
58
|
+
//img.crossOrigin='anonymous'
|
|
59
|
+
img.src = base64;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function imageToBase64(image: HTMLCanvasElement): string {
|
|
64
|
+
return image.toDataURL();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resizeImage(
|
|
68
|
+
image: any,
|
|
69
|
+
targetWidth: number,
|
|
70
|
+
targetHeight: number,
|
|
71
|
+
widthCropSizePctFallback: number = 30
|
|
72
|
+
): any {
|
|
73
|
+
const canvas = getCanvas(image.width, image.height);
|
|
74
|
+
//const canvas = document.createElement('canvas');
|
|
75
|
+
const ctx = canvas.getContext('2d') as any;
|
|
76
|
+
if (ctx) {
|
|
77
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
78
|
+
const aspectRatio = image.naturalWidth / image.naturalHeight;
|
|
79
|
+
var newWidth = targetWidth;
|
|
80
|
+
var newHeight = targetWidth / aspectRatio;
|
|
81
|
+
|
|
82
|
+
canvas.width = targetWidth;
|
|
83
|
+
canvas.height = targetHeight;
|
|
84
|
+
|
|
85
|
+
if(targetHeight > newHeight) {
|
|
86
|
+
// Calculate how much width should be cropped off (fall off) based on fallOffPercentage
|
|
87
|
+
const heightDiff = targetHeight - newHeight
|
|
88
|
+
const widthCropSizePctCalc = (heightDiff * 100) / targetHeight
|
|
89
|
+
const widthCropSizePct = Math.min(widthCropSizePctFallback, widthCropSizePctCalc)
|
|
90
|
+
const widthCropSize = (widthCropSizePct / 100) * image.naturalWidth;
|
|
91
|
+
|
|
92
|
+
////console.log("resizeImage::", widthCropSizePctCalc, widthCropSizePctFallback)
|
|
93
|
+
|
|
94
|
+
const widthCropSizeHalf = widthCropSize / 2;
|
|
95
|
+
|
|
96
|
+
newHeight += (widthCropSizePct / 100) * newHeight;
|
|
97
|
+
|
|
98
|
+
ctx.drawImage(
|
|
99
|
+
image,
|
|
100
|
+
|
|
101
|
+
widthCropSizeHalf, // start the cropping at this position
|
|
102
|
+
0, // No vertical cropping
|
|
103
|
+
|
|
104
|
+
// Reduce the source image width by cropWidth * 2 so we can evenly crop from left(cropWidth) and(+) right(cropWidth)
|
|
105
|
+
image.naturalWidth - widthCropSize,
|
|
106
|
+
image.naturalHeight, // Full height
|
|
107
|
+
|
|
108
|
+
0,
|
|
109
|
+
targetHeight - newHeight, // Draw at the top-left corner of the canvas
|
|
110
|
+
|
|
111
|
+
newWidth, // Fit to the target width
|
|
112
|
+
newHeight// Fit to the target height
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
ctx.drawImage(
|
|
117
|
+
image,
|
|
118
|
+
|
|
119
|
+
0,
|
|
120
|
+
0,
|
|
121
|
+
|
|
122
|
+
image.width,
|
|
123
|
+
image.height,
|
|
124
|
+
|
|
125
|
+
0,
|
|
126
|
+
targetHeight - newHeight,
|
|
127
|
+
|
|
128
|
+
newWidth,
|
|
129
|
+
newHeight
|
|
130
|
+
);*/
|
|
131
|
+
|
|
132
|
+
} else {
|
|
133
|
+
ctx.drawImage(
|
|
134
|
+
image,
|
|
135
|
+
0, 0,
|
|
136
|
+
image.width, image.height,
|
|
137
|
+
0, 0,
|
|
138
|
+
newWidth, newHeight
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return canvas;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function calculateAngleBetweenPoints(center: Point, p1: Point, p2: Point): number {
|
|
146
|
+
const { x: x0, y: y0 } = center;
|
|
147
|
+
const { x: x1, y: y1 } = p1;
|
|
148
|
+
const { x: x2, y: y2 } = p2;
|
|
149
|
+
|
|
150
|
+
function getAngle(x: number, y: number): number {
|
|
151
|
+
return Math.atan2(y - y0, x - x0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function radiansToDegrees(radians: number): number {
|
|
155
|
+
return radians * (180 / Math.PI);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const theta1 = getAngle(x1, y1);
|
|
159
|
+
const theta2 = getAngle(x2, y2);
|
|
160
|
+
|
|
161
|
+
let deltaTheta = theta2 - theta1;
|
|
162
|
+
|
|
163
|
+
if (deltaTheta < 0) {
|
|
164
|
+
deltaTheta += 2 * Math.PI;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const antiClockwiseAngle = radiansToDegrees(deltaTheta);
|
|
168
|
+
const clockwiseAngle = 360 - antiClockwiseAngle;
|
|
169
|
+
|
|
170
|
+
////console.log("annnnnngssss: ", clockwiseAngle, antiClockwiseAngle);
|
|
171
|
+
return antiClockwiseAngle;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function calculateDistance(pointA: Point, pointB: Point): number {
|
|
175
|
+
return Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function calculateMidpoint(pointA: Point, pointB: Point): Point {
|
|
179
|
+
return {
|
|
180
|
+
x: Math.round((pointA.x + pointB.x) / 2),
|
|
181
|
+
y: Math.round((pointA.y + pointB.y) / 2)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ensureTopSpace(image: any, space: number): Promise<any> {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const canvas = getCanvas(image.naturalWidth || image.width, image.naturalHeight || image.height);
|
|
188
|
+
//const canvas = document.createElement('canvas');
|
|
189
|
+
canvas.width = image.naturalWidth;
|
|
190
|
+
canvas.height = image.naturalHeight;
|
|
191
|
+
const ctx = canvas.getContext('2d') as any;
|
|
192
|
+
if (!ctx) return reject(new Error("Could not get canvas context"));
|
|
193
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
194
|
+
|
|
195
|
+
ctx.drawImage(image, 0, 0);
|
|
196
|
+
|
|
197
|
+
const imgData = ctx.getImageData(0, 0, image.width, image.height);
|
|
198
|
+
const pixels = imgData.data;
|
|
199
|
+
|
|
200
|
+
const isRowTransparent = (row: number): boolean => {
|
|
201
|
+
for (let col = 0; col < image.width; col++) {
|
|
202
|
+
const index = (row * image.width + col) * 4;
|
|
203
|
+
const alpha = pixels[index + 3];
|
|
204
|
+
if (alpha !== 0) return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
let transparentRowsAtTop = 0;
|
|
210
|
+
for (let row = 0; row < image.height; row++) {
|
|
211
|
+
if (isRowTransparent(row)) {
|
|
212
|
+
transparentRowsAtTop++;
|
|
213
|
+
} else {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (transparentRowsAtTop >= space) {
|
|
219
|
+
return resolve(image);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rowsToAdd = space - transparentRowsAtTop;
|
|
223
|
+
|
|
224
|
+
const newCanvas = getCanvas(image.width, image.height);
|
|
225
|
+
//const newCanvas = document.createElement('canvas');
|
|
226
|
+
newCanvas.width = image.width;
|
|
227
|
+
newCanvas.height = image.height;
|
|
228
|
+
const newCtx = newCanvas.getContext('2d') as any;
|
|
229
|
+
if (!newCtx) return reject(new Error("Could not get canvas context"));
|
|
230
|
+
|
|
231
|
+
newCtx.clearRect(0, 0, newCanvas.width, newCanvas.height);
|
|
232
|
+
|
|
233
|
+
newCtx.drawImage(
|
|
234
|
+
image,
|
|
235
|
+
0, 0,
|
|
236
|
+
image.width, image.height - rowsToAdd,
|
|
237
|
+
0, rowsToAdd,
|
|
238
|
+
image.width, image.height - rowsToAdd
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
resolve(newCanvas);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findBlueQuadrilateralCorners(imageA: any): Promise<Quadrilateral | null> {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const canvas = getCanvas(imageA.naturalWidth, imageA.naturalHeight);
|
|
248
|
+
canvas.width = imageA.naturalWidth;
|
|
249
|
+
canvas.height = imageA.naturalHeight;
|
|
250
|
+
const ctx = canvas.getContext('2d') as any;
|
|
251
|
+
if (!ctx) return reject(new Error("Could not get canvas context"));
|
|
252
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
253
|
+
|
|
254
|
+
ctx.drawImage(imageA, 0, 0);
|
|
255
|
+
|
|
256
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
257
|
+
const data = imageData.data;
|
|
258
|
+
|
|
259
|
+
const cornerPoints: Point[] = [];
|
|
260
|
+
|
|
261
|
+
for (let y = 0; y < imageData.height; y++) {
|
|
262
|
+
for (let x = 0; x < imageData.width; x++) {
|
|
263
|
+
const index = (y * imageData.width + x) * 4;
|
|
264
|
+
|
|
265
|
+
const r = data[index];
|
|
266
|
+
const g = data[index + 1];
|
|
267
|
+
const b = data[index + 2];
|
|
268
|
+
|
|
269
|
+
if (isValidPixel(b, r, g)) {
|
|
270
|
+
cornerPoints.push({ x, y });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (cornerPoints.length === 0) {
|
|
276
|
+
return resolve(null);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const topLeft: Point = { x: Infinity, y: Infinity };
|
|
280
|
+
const topRight: Point = { x: -Infinity, y: Infinity };
|
|
281
|
+
const bottomLeft: Point = { x: Infinity, y: -Infinity };
|
|
282
|
+
const bottomRight: Point = { x: -Infinity, y: -Infinity };
|
|
283
|
+
|
|
284
|
+
cornerPoints.forEach(point => {
|
|
285
|
+
if (point.x + point.y < topLeft.x + topLeft.y) {
|
|
286
|
+
topLeft.x = point.x;
|
|
287
|
+
topLeft.y = point.y;
|
|
288
|
+
}
|
|
289
|
+
if (point.x - point.y > topRight.x - topRight.y) {
|
|
290
|
+
topRight.x = point.x;
|
|
291
|
+
topRight.y = point.y;
|
|
292
|
+
}
|
|
293
|
+
if (point.x - point.y < bottomLeft.x - bottomLeft.y) {
|
|
294
|
+
bottomLeft.x = point.x;
|
|
295
|
+
bottomLeft.y = point.y;
|
|
296
|
+
}
|
|
297
|
+
if (point.x + point.y > bottomRight.x + bottomRight.y) {
|
|
298
|
+
bottomRight.x = point.x;
|
|
299
|
+
bottomRight.y = point.y;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
resolve({ topLeft, topRight, bottomRight, bottomLeft });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function detectGreenLine(imageA: any): Promise<{ startPoint: Point, endPoint: Point }> {
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
const canvas = getCanvas(imageA.naturalWidth, imageA.naturalHeight);
|
|
310
|
+
//const canvas = document.createElement('canvas');
|
|
311
|
+
canvas.width = imageA.naturalWidth;
|
|
312
|
+
canvas.height = imageA.naturalHeight;
|
|
313
|
+
const ctx = canvas.getContext('2d') as any;
|
|
314
|
+
if (!ctx) return reject(new Error("Could not get canvas context"));
|
|
315
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
316
|
+
|
|
317
|
+
ctx.drawImage(imageA, 0, 0);
|
|
318
|
+
|
|
319
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
320
|
+
const data = imageData.data;
|
|
321
|
+
|
|
322
|
+
let smallestX: Point = { x: Infinity, y: 0 };
|
|
323
|
+
let smallestY: Point = { x: 0, y: Infinity };
|
|
324
|
+
let biggestX: Point = { x: 0, y: 0 };
|
|
325
|
+
let biggestY: Point = { x: 0, y: 0 };
|
|
326
|
+
|
|
327
|
+
for (let y = 0; y < canvas.height; y++) {
|
|
328
|
+
for (let x = 0; x < canvas.width; x++) {
|
|
329
|
+
const index = (y * canvas.width + x) * 4;
|
|
330
|
+
|
|
331
|
+
const r = data[index];
|
|
332
|
+
const g = data[index + 1];
|
|
333
|
+
const b = data[index + 2];
|
|
334
|
+
|
|
335
|
+
if (isValidPixel(g, r, b)) {
|
|
336
|
+
if (x < smallestX.x) smallestX = { x, y };
|
|
337
|
+
if (x > biggestX.x) biggestX = { x, y };
|
|
338
|
+
if (y < smallestY.y) smallestY = { x, y };
|
|
339
|
+
if (y > biggestY.y) biggestY = { x, y };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const isHorizontal = imageA.height > imageA.width;
|
|
345
|
+
const startPoint = isHorizontal ? smallestX : smallestY;
|
|
346
|
+
const endPoint = isHorizontal ? biggestX : biggestY;
|
|
347
|
+
|
|
348
|
+
if (!startPoint || !endPoint) {
|
|
349
|
+
return reject(new Error("No green line found"));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
resolve({ startPoint, endPoint });
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getRedDots(imageA: any): Promise<PointVariants> {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const canvas = getCanvas(imageA.naturalWidth, imageA.naturalHeight);
|
|
359
|
+
//const canvas = document.createElement('canvas');
|
|
360
|
+
canvas.width = imageA.naturalWidth;
|
|
361
|
+
canvas.height = imageA.naturalHeight;
|
|
362
|
+
const ctx = canvas.getContext('2d') as any;
|
|
363
|
+
if (!ctx) return reject(new Error("Could not get canvas context"));
|
|
364
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
365
|
+
|
|
366
|
+
ctx.drawImage(imageA, 0, 0);
|
|
367
|
+
|
|
368
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
369
|
+
const data = imageData.data;
|
|
370
|
+
|
|
371
|
+
let redDots: PointVariants = { x: [], y: [] };
|
|
372
|
+
|
|
373
|
+
for (let y = 0; y < canvas.height; y++) {
|
|
374
|
+
for (let x = 0; x < canvas.width; x++) {
|
|
375
|
+
const index = (y * canvas.width + x) * 4;
|
|
376
|
+
|
|
377
|
+
const r = data[index];
|
|
378
|
+
const g = data[index + 1];
|
|
379
|
+
const b = data[index + 2];
|
|
380
|
+
|
|
381
|
+
// Identify the red dot
|
|
382
|
+
if (isValidPixel(r, g, b)) {
|
|
383
|
+
redDots.x.push(x);
|
|
384
|
+
redDots.y.push(y);
|
|
385
|
+
////console.log("redDot:", "x:", x, "y:", y, "r:", r, "g:", g, "b:", b)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (redDots.x.length == 0) {
|
|
391
|
+
return reject(new Error("No red dot found"));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
resolve(redDots);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function chipOffImage(image: any, quad: Quadrilateral, borderColor: string | null = null): any {
|
|
399
|
+
// Create a canvas element and set its size to match the image
|
|
400
|
+
const canvas = getCanvas(image.naturalWidth || image.width, image.naturalHeight || image.height);
|
|
401
|
+
//const canvas = document.createElement('canvas');
|
|
402
|
+
canvas.width = image.naturalWidth || image.width;
|
|
403
|
+
canvas.height = image.naturalHeight || image.height;
|
|
404
|
+
const ctx = canvas.getContext('2d') as any;
|
|
405
|
+
if (!ctx) throw new Error("Could not get canvas context");
|
|
406
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
407
|
+
|
|
408
|
+
// Clear the entire canvas to make everything transparent by default
|
|
409
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
410
|
+
|
|
411
|
+
// Extract quad points
|
|
412
|
+
const { topLeft, topRight, bottomRight, bottomLeft } = quad;
|
|
413
|
+
|
|
414
|
+
// Create a path for the quadrilateral area
|
|
415
|
+
ctx.beginPath();
|
|
416
|
+
ctx.moveTo(topLeft.x, topLeft.y);
|
|
417
|
+
ctx.lineTo(topRight.x, topRight.y);
|
|
418
|
+
ctx.lineTo(bottomRight.x, bottomRight.y);
|
|
419
|
+
ctx.lineTo(bottomLeft.x, bottomLeft.y);
|
|
420
|
+
ctx.closePath();
|
|
421
|
+
|
|
422
|
+
// Clip to the quadrilateral area
|
|
423
|
+
ctx.save();
|
|
424
|
+
ctx.clip();
|
|
425
|
+
|
|
426
|
+
// Draw the image only within the clipped area
|
|
427
|
+
ctx.drawImage(image, 0, 0);
|
|
428
|
+
|
|
429
|
+
// Restore context to stop clipping
|
|
430
|
+
ctx.restore();
|
|
431
|
+
|
|
432
|
+
// If borderColor is provided, draw the border around the quad
|
|
433
|
+
if (borderColor) {
|
|
434
|
+
ctx.strokeStyle = borderColor;
|
|
435
|
+
ctx.lineWidth = 2;
|
|
436
|
+
ctx.beginPath();
|
|
437
|
+
ctx.moveTo(topLeft.x, topLeft.y);
|
|
438
|
+
ctx.lineTo(topRight.x, topRight.y);
|
|
439
|
+
ctx.lineTo(bottomRight.x, bottomRight.y);
|
|
440
|
+
ctx.lineTo(bottomLeft.x, bottomLeft.y);
|
|
441
|
+
ctx.closePath();
|
|
442
|
+
ctx.stroke();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Return the canvas as an image source (or append the canvas directly)
|
|
446
|
+
return canvas;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function applyTransformation(
|
|
450
|
+
imageB: any,
|
|
451
|
+
rotationAngle: number,
|
|
452
|
+
scaleX: number,
|
|
453
|
+
scaleY: number,
|
|
454
|
+
shearX: number,
|
|
455
|
+
shearY: number,
|
|
456
|
+
center: Point,
|
|
457
|
+
targetWidth: number,
|
|
458
|
+
targetHeight: number,
|
|
459
|
+
corners: Quadrilateral,
|
|
460
|
+
debugColors?: DebugColors
|
|
461
|
+
): any {
|
|
462
|
+
const canvas = getCanvas(targetWidth, targetHeight);
|
|
463
|
+
//const canvas = document.createElement('canvas');
|
|
464
|
+
const ctx = canvas.getContext('2d') as any;
|
|
465
|
+
if (!ctx) throw new Error("Could not get canvas context");
|
|
466
|
+
ctx.imageSmoothingEnabled=true; ctx.imageSmoothingQuality='high'
|
|
467
|
+
|
|
468
|
+
canvas.width = targetWidth;
|
|
469
|
+
canvas.height = targetHeight;
|
|
470
|
+
|
|
471
|
+
ctx.translate(targetWidth / 2, targetHeight / 2);
|
|
472
|
+
ctx.rotate((rotationAngle * Math.PI) / 180);
|
|
473
|
+
ctx.transform(scaleX, shearX, shearY, scaleY, 0, 0);
|
|
474
|
+
|
|
475
|
+
if (center) {
|
|
476
|
+
const translateX = center.x - targetWidth / 2;
|
|
477
|
+
const translateY = center.y - targetHeight / 2;
|
|
478
|
+
ctx.translate(translateX, translateY);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
ctx.drawImage(imageB, -imageB.width / 2, -imageB.height / 2);
|
|
482
|
+
|
|
483
|
+
const chippedCanvas = chipOffImage(canvas, corners, debugColors?.border);
|
|
484
|
+
|
|
485
|
+
return chippedCanvas;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function hashImageSrc(imageSrc: string) {
|
|
489
|
+
// Fetch the image as a blob (binary large object)
|
|
490
|
+
const response = await fetch(imageSrc);
|
|
491
|
+
const blob = await response.blob();
|
|
492
|
+
|
|
493
|
+
// Convert the blob to an array buffer
|
|
494
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
495
|
+
|
|
496
|
+
// Hash the array buffer using SHA-256
|
|
497
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
|
498
|
+
|
|
499
|
+
// Convert the hash buffer to a hex string
|
|
500
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
501
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
502
|
+
|
|
503
|
+
return hashHex;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function analyzeImage(imageA: any, debug?: boolean): Promise<AnalyzeResult> {
|
|
507
|
+
const corners = await findBlueQuadrilateralCorners(imageA)
|
|
508
|
+
|
|
509
|
+
if (!corners) throw new Error("Blue border box not found!");
|
|
510
|
+
|
|
511
|
+
const { topLeft, topRight, bottomLeft, bottomRight } = corners
|
|
512
|
+
|
|
513
|
+
let redDots: PointVariants = await getRedDots(imageA)
|
|
514
|
+
|
|
515
|
+
if (!topLeft || !bottomRight) throw new Error("Bounding box corners not found");
|
|
516
|
+
|
|
517
|
+
const greenLineInfo = await detectGreenLine(imageA)
|
|
518
|
+
const { startPoint, endPoint } = greenLineInfo
|
|
519
|
+
|
|
520
|
+
const greenLineCurrentPosition = calculateMidpoint(startPoint, endPoint)
|
|
521
|
+
|
|
522
|
+
const center = {
|
|
523
|
+
x: redDots.x[redDots.x.length - 1],//Math.round((topRight.x - topLeft.x) / 2),//
|
|
524
|
+
y: redDots.y[redDots.y.length - 1],//Math.round((bottomLeft.y - topLeft.y) / 2)//
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const greenLineCircleRadius = Math.round(calculateDistance(center, greenLineCurrentPosition))
|
|
528
|
+
|
|
529
|
+
const greenLinePreviousPosition = {
|
|
530
|
+
x: center.x,
|
|
531
|
+
y: center.y + greenLineCircleRadius
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const angleFill = 2 * (imageA.height > imageA.width? -1 : 1)
|
|
535
|
+
const rotationAngle = angleFill + Math.round(calculateAngleBetweenPoints(center, greenLinePreviousPosition, greenLineCurrentPosition))
|
|
536
|
+
////console.log("detectGreenLineAngle: ", greenLineInfo, "greenLineCurrentPosition: ", greenLineCurrentPosition, "greenLinePreviousPosition: ", greenLinePreviousPosition);
|
|
537
|
+
|
|
538
|
+
let scaleX = parseFloat((calculateDistance(topLeft, topRight) / imageA.naturalWidth).toPrecision(12));
|
|
539
|
+
let scaleY = parseFloat((calculateDistance(topLeft, bottomLeft) / imageA.naturalHeight).toPrecision(12));
|
|
540
|
+
|
|
541
|
+
const approximationErrorFill = 0.04
|
|
542
|
+
scaleX = scaleX + approximationErrorFill;
|
|
543
|
+
scaleY = scaleY + approximationErrorFill;
|
|
544
|
+
|
|
545
|
+
const shearX = 0//(topRight.y - topLeft.y) / imageA.naturalWidth;
|
|
546
|
+
const shearY = 0//(bottomLeft.x - topLeft.x) / imageA.naturalHeight;
|
|
547
|
+
|
|
548
|
+
if(debug) {/*
|
|
549
|
+
console.log(
|
|
550
|
+
"analyzeImageV6:imageA", await hashImageSrc(imageA.src),
|
|
551
|
+
"imageA.width", imageA.width,
|
|
552
|
+
"imageA.height", imageA.height,
|
|
553
|
+
"corners", corners,
|
|
554
|
+
"greenLineInfo", greenLineInfo,
|
|
555
|
+
"redDots", redDots,
|
|
556
|
+
"center", center,
|
|
557
|
+
"greenLineCircleRadius", greenLineCircleRadius,
|
|
558
|
+
"greenLinePreviousPosition", greenLinePreviousPosition,
|
|
559
|
+
"angleFill", angleFill,
|
|
560
|
+
"rotationAngle", rotationAngle,
|
|
561
|
+
"shearX", shearX,
|
|
562
|
+
"shearY", shearY
|
|
563
|
+
)*/
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
rotationAngle,
|
|
568
|
+
scaleX,
|
|
569
|
+
scaleY,
|
|
570
|
+
shearX,
|
|
571
|
+
shearY,
|
|
572
|
+
center,
|
|
573
|
+
width: imageA.width,
|
|
574
|
+
height: imageA.height,
|
|
575
|
+
corners
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function imageOrientation(angle: number): "landscape" | "portrait" | "diagonal" {
|
|
580
|
+
// Normalize the angle to be between 0 and 360
|
|
581
|
+
const normalizedAngle = angle % 360;
|
|
582
|
+
|
|
583
|
+
// Define a tolerance for angles close to 90, 180, 270, 0
|
|
584
|
+
const tolerance = 30;//A bit less than 45deg
|
|
585
|
+
|
|
586
|
+
// Check if the angle is near 90° or 270° (horizontal)
|
|
587
|
+
if (
|
|
588
|
+
Math.abs(normalizedAngle - 90) <= tolerance ||
|
|
589
|
+
Math.abs(normalizedAngle - 270) <= tolerance
|
|
590
|
+
) {
|
|
591
|
+
return "landscape"; // The canvas is horizontal (landscape)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check if the angle is near 0° or 180° (vertical)
|
|
595
|
+
if (
|
|
596
|
+
Math.abs(normalizedAngle - 0) <= tolerance ||
|
|
597
|
+
Math.abs(normalizedAngle - 180) <= tolerance ||
|
|
598
|
+
Math.abs(normalizedAngle - 360) <= tolerance
|
|
599
|
+
) {
|
|
600
|
+
return "portrait"; // The canvas is vertical (portrait)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return "diagonal"; // For other angles (e.g., 45°, 135°, etc.), we cannot decide
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const alignedImageSize = (imageB: any, width: number, height: number, angle: number): any => {
|
|
607
|
+
/*
|
|
608
|
+
if (height > width) {
|
|
609
|
+
return (resizeImage(imageB, width, height) as any) as HTMLImageElement;
|
|
610
|
+
|
|
611
|
+
} else {
|
|
612
|
+
return (resizeImage(imageB, height, width) as any) as HTMLImageElement;
|
|
613
|
+
}*/
|
|
614
|
+
|
|
615
|
+
const orientation = imageOrientation(angle)
|
|
616
|
+
////console.log("alignedImageSize: ", angle, angle - 360, orientation)
|
|
617
|
+
if (orientation == "portrait" || orientation == "diagonal") {
|
|
618
|
+
return (resizeImage(imageB, width, height) as any);
|
|
619
|
+
|
|
620
|
+
} else {
|
|
621
|
+
return (resizeImage(imageB, height, width) as any);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function transformImage(
|
|
626
|
+
base64ImageA: string,
|
|
627
|
+
base64ImageB: string,
|
|
628
|
+
topSpacePct: number,
|
|
629
|
+
debugColors?: DebugColors
|
|
630
|
+
): Promise<string> {
|
|
631
|
+
const imageA = await base64ToImage(base64ImageA);
|
|
632
|
+
let imageB = await base64ToImage(base64ImageB);
|
|
633
|
+
|
|
634
|
+
const analyzeResult = await analyzeImage(imageA);
|
|
635
|
+
|
|
636
|
+
if (!analyzeResult) {
|
|
637
|
+
throw new Error("Failed to analyze image");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
////console.log("analyzeResult:transformImage ", analyzeResult);
|
|
641
|
+
|
|
642
|
+
const { rotationAngle, scaleX, scaleY, shearX, shearY, center, width, height, corners } = analyzeResult;
|
|
643
|
+
|
|
644
|
+
const w = width//Math.max(corners.topRight.x - corners.topLeft.x, corners.bottomRight.x - corners.bottomLeft.x)
|
|
645
|
+
const h = height//Math.max(corners.bottomLeft.y - corners.topLeft.y, corners.bottomRight.y - corners.topRight.y)
|
|
646
|
+
|
|
647
|
+
// Resize imageB to match the dimensions of imageA
|
|
648
|
+
if (imageB.width !== w || imageB.height !== h) {
|
|
649
|
+
imageB = alignedImageSize(imageB, w, h, rotationAngle)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const space = Math.round((topSpacePct * imageB.height) / 100);
|
|
653
|
+
imageB = await ensureTopSpace(imageB, space);
|
|
654
|
+
|
|
655
|
+
const transformedImageB = applyTransformation(
|
|
656
|
+
imageB,
|
|
657
|
+
rotationAngle,
|
|
658
|
+
scaleX,
|
|
659
|
+
scaleY,
|
|
660
|
+
shearX,
|
|
661
|
+
shearY,
|
|
662
|
+
center,
|
|
663
|
+
w,
|
|
664
|
+
h,
|
|
665
|
+
corners,
|
|
666
|
+
debugColors
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
return imageToBase64(transformedImageB);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export async function transformImageByTemplate(
|
|
673
|
+
analyzeResult: AnalyzeResult,
|
|
674
|
+
base64ImageB: string,
|
|
675
|
+
topSpacePct: number = 5,
|
|
676
|
+
debugColors?: DebugColors
|
|
677
|
+
): Promise<string> {
|
|
678
|
+
|
|
679
|
+
////console.log("transformImageByTemplate:1", topSpacePct)
|
|
680
|
+
//return base64ImageB
|
|
681
|
+
let imageB = await base64ToImage(base64ImageB);
|
|
682
|
+
|
|
683
|
+
//console.log("imageB.1", imageB)
|
|
684
|
+
|
|
685
|
+
const { rotationAngle, scaleX, scaleY, shearX, shearY, center, width, height, corners } = analyzeResult;
|
|
686
|
+
|
|
687
|
+
const w = width//Math.max(corners.topRight.x - corners.topLeft.x, corners.bottomRight.x - corners.bottomLeft.x)
|
|
688
|
+
const h = height//Math.max(corners.bottomLeft.y - corners.topLeft.y, corners.bottomRight.y - corners.topRight.y)
|
|
689
|
+
|
|
690
|
+
// Resize imageB to match the dimensions of imageA
|
|
691
|
+
if (imageB.width !== w || imageB.height !== h) {
|
|
692
|
+
imageB = alignedImageSize(imageB, w, h, rotationAngle)
|
|
693
|
+
//console.log("imageB.2", imageB)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const space = Math.round((topSpacePct * imageB.height) / 100);
|
|
697
|
+
imageB = await ensureTopSpace(imageB, space)as any;
|
|
698
|
+
|
|
699
|
+
//console.log("imageB.3", imageB)
|
|
700
|
+
|
|
701
|
+
////console.log("transformImageByTemplate:", w, h, width, height)
|
|
702
|
+
|
|
703
|
+
const transformedImageB = applyTransformation(
|
|
704
|
+
imageB,
|
|
705
|
+
rotationAngle - 360,
|
|
706
|
+
scaleX,
|
|
707
|
+
scaleY,
|
|
708
|
+
shearX,
|
|
709
|
+
shearY,
|
|
710
|
+
center,
|
|
711
|
+
w,
|
|
712
|
+
h,
|
|
713
|
+
corners,
|
|
714
|
+
debugColors
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
return imageToBase64(transformedImageB);
|
|
718
|
+
}
|