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.
@@ -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
+ }