import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { ImageDimensions } from 'src/types'; import { applyToPoint, compose, flipX, flipY, identity, Matrix, rotate, scale, translate } from 'transformation-matrix'; export const getOutputDimensions = ( edits: AssetEditActionItem[], startingDimensions: ImageDimensions, ): ImageDimensions => { let { width, height } = startingDimensions; const crop = edits.find((edit) => edit.action === AssetEditAction.Crop); if (crop) { width = crop.parameters.width; height = crop.parameters.height; } for (const edit of edits) { if (edit.action === AssetEditAction.Rotate) { const angleDegrees = edit.parameters.angle; if (angleDegrees === 90 || angleDegrees === 270) { [width, height] = [height, width]; } } } return { width, height }; }; export const createAffineMatrix = ( edits: AssetEditActionItem[], scalingParameters?: { pointSpace: ImageDimensions; targetSpace: ImageDimensions; }, ): Matrix => { let scalingMatrix: Matrix = identity(); if (scalingParameters) { const { pointSpace, targetSpace } = scalingParameters; const scaleX = targetSpace.width / pointSpace.width; scalingMatrix = scale(scaleX); } return compose( scalingMatrix, ...edits.map((edit) => { switch (edit.action) { case 'rotate': { const angleInRadians = (-edit.parameters.angle * Math.PI) / 180; return rotate(angleInRadians); } case 'mirror': { return edit.parameters.axis === 'horizontal' ? flipY() : flipX(); } default: { return identity(); } } }), ); }; export type Point = { x: number; y: number }; type TransformState = { points: Point[]; currentWidth: number; currentHeight: number; }; /** * Transforms an array of points through a series of edit operations (crop, rotate, mirror). * Points should be in absolute pixel coordinates relative to the starting dimensions. */ export const transformPoints = ( points: Point[], edits: AssetEditActionItem[], startingDimensions: ImageDimensions, { inverse = false } = {}, ): TransformState => { let currentWidth = startingDimensions.width; let currentHeight = startingDimensions.height; let transformedPoints = [...points]; // Handle crop first if not inverting if (!inverse) { const crop = edits.find((edit) => edit.action === 'crop'); if (crop) { const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = crop.parameters; transformedPoints = transformedPoints.map((p) => ({ x: p.x - cropX, y: p.y - cropY, })); currentWidth = cropWidth; currentHeight = cropHeight; } } // Apply rotate and mirror transforms const editSequence = inverse ? edits.toReversed() : edits; for (const edit of editSequence) { let matrix: Matrix = identity(); if (edit.action === 'rotate') { const angleDegrees = edit.parameters.angle; const angleRadians = (angleDegrees * Math.PI) / 180; const newWidth = angleDegrees === 90 || angleDegrees === 270 ? currentHeight : currentWidth; const newHeight = angleDegrees === 90 || angleDegrees === 270 ? currentWidth : currentHeight; matrix = compose( translate(newWidth / 2, newHeight / 2), rotate(inverse ? -angleRadians : angleRadians), translate(-currentWidth / 2, -currentHeight / 2), ); currentWidth = newWidth; currentHeight = newHeight; } else if (edit.action === 'mirror') { matrix = compose( translate(currentWidth / 2, currentHeight / 2), edit.parameters.axis === 'horizontal' ? flipY() : flipX(), translate(-currentWidth / 2, -currentHeight / 2), ); } else { // Skip non-affine transformations continue; } transformedPoints = transformedPoints.map((p) => applyToPoint(matrix, p)); } // Handle crop last if inverting if (inverse) { const crop = edits.find((edit) => edit.action === 'crop'); if (crop) { const { x: cropX, y: cropY } = crop.parameters; transformedPoints = transformedPoints.map((p) => ({ x: p.x + cropX, y: p.y + cropY, })); } } return { points: transformedPoints, currentWidth, currentHeight, }; }; type FaceBoundingBox = { boundingBoxX1: number; boundingBoxX2: number; boundingBoxY1: number; boundingBoxY2: number; imageWidth: number; imageHeight: number; }; export const transformFaceBoundingBox = ( box: FaceBoundingBox, edits: AssetEditActionItem[], imageDimensions: ImageDimensions, ): FaceBoundingBox => { if (edits.length === 0) { return box; } const scaleX = imageDimensions.width / box.imageWidth; const scaleY = imageDimensions.height / box.imageHeight; const points: Point[] = [ { x: box.boundingBoxX1 * scaleX, y: box.boundingBoxY1 * scaleY }, { x: box.boundingBoxX2 * scaleX, y: box.boundingBoxY2 * scaleY }, ]; const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); // Ensure x1,y1 is top-left and x2,y2 is bottom-right const [p1, p2] = transformedPoints; return { boundingBoxX1: Math.min(p1.x, p2.x), boundingBoxY1: Math.min(p1.y, p2.y), boundingBoxX2: Math.max(p1.x, p2.x), boundingBoxY2: Math.max(p1.y, p2.y), imageWidth: currentWidth, imageHeight: currentHeight, }; }; const reorderQuadPointsForRotation = (points: Point[], rotationDegrees: number): Point[] => { const [p1, p2, p3, p4] = points; switch (rotationDegrees) { case 90: { return [p4, p1, p2, p3]; } case 180: { return [p3, p4, p1, p2]; } case 270: { return [p2, p3, p4, p1]; } default: { return points; } } }; export const transformOcrBoundingBox = ( box: AssetOcrResponseDto, edits: AssetEditActionItem[], imageDimensions: ImageDimensions, ): AssetOcrResponseDto => { if (edits.length === 0) { return box; } const points: Point[] = [ { x: box.x1 * imageDimensions.width, y: box.y1 * imageDimensions.height }, { x: box.x2 * imageDimensions.width, y: box.y2 * imageDimensions.height }, { x: box.x3 * imageDimensions.width, y: box.y3 * imageDimensions.height }, { x: box.x4 * imageDimensions.width, y: box.y4 * imageDimensions.height }, ]; const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); // Reorder points to maintain semantic ordering (topLeft, topRight, bottomRight, bottomLeft) const netRotation = edits.find((e) => e.action == AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; const reorderedPoints = reorderQuadPointsForRotation(transformedPoints, netRotation); const [p1, p2, p3, p4] = reorderedPoints; return { ...box, x1: p1.x / currentWidth, y1: p1.y / currentHeight, x2: p2.x / currentWidth, y2: p2.y / currentHeight, x3: p3.x / currentWidth, y3: p3.y / currentHeight, x4: p4.x / currentWidth, y4: p4.y / currentHeight, }; };