mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(passport-photo): add custom dimensions, DPI control, fix crop mismatch
- Added "Custom Dimensions" option in country dropdown with width/height inputs - Added DPI control (72-600, default 300) for all specs - Backend now uses actual bg-removed image dimensions for crop computation, scaling landmark coordinates when dimensions differ from the original - Dropdown background uses explicit bg-white/dark:bg-zinc-900 classes - Backend supports customWidthMm, customHeightMm, and dpi in generate request
This commit is contained in:
@@ -34,6 +34,9 @@ const generateSettingsSchema = z.object({
|
|||||||
bgColor: z.string().default("#FFFFFF"),
|
bgColor: z.string().default("#FFFFFF"),
|
||||||
printLayout: z.string().default("none"),
|
printLayout: z.string().default("none"),
|
||||||
maxFileSizeKb: z.number().default(0),
|
maxFileSizeKb: z.number().default(0),
|
||||||
|
dpi: z.number().min(72).max(600).default(300),
|
||||||
|
customWidthMm: z.number().optional(),
|
||||||
|
customHeightMm: z.number().optional(),
|
||||||
adjustX: z.number().default(0),
|
adjustX: z.number().default(0),
|
||||||
adjustY: z.number().default(0),
|
adjustY: z.number().default(0),
|
||||||
landmarks: landmarksSchema,
|
landmarks: landmarksSchema,
|
||||||
@@ -289,6 +292,9 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
bgColor,
|
bgColor,
|
||||||
printLayout,
|
printLayout,
|
||||||
maxFileSizeKb,
|
maxFileSizeKb,
|
||||||
|
dpi: userDpi,
|
||||||
|
customWidthMm,
|
||||||
|
customHeightMm,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
landmarks: rawLandmarks,
|
landmarks: rawLandmarks,
|
||||||
@@ -298,31 +304,51 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
|
|
||||||
// Look up country spec
|
// Look up country spec
|
||||||
const countrySpec = PASSPORT_SPECS.find((s) => s.code === countryCode);
|
const countrySpec = PASSPORT_SPECS.find((s) => s.code === countryCode);
|
||||||
if (!countrySpec) {
|
if (!countrySpec && !customWidthMm) {
|
||||||
return reply.status(400).send({ error: `Unknown country code: ${countryCode}` });
|
return reply.status(400).send({ error: `Unknown country code: ${countryCode}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const docSpec = countrySpec.documents.find((d) => d.type === documentType);
|
const baseDoc =
|
||||||
if (!docSpec) {
|
countrySpec?.documents.find((d) => d.type === documentType) ?? countrySpec?.documents[0];
|
||||||
return reply.status(400).send({
|
// Build effective doc spec: custom dimensions/DPI override country defaults
|
||||||
error: `No ${documentType} spec found for ${countryCode}`,
|
const docSpec = {
|
||||||
});
|
...(baseDoc ?? {
|
||||||
}
|
headHeightMin: 0.7,
|
||||||
|
headHeightMax: 0.8,
|
||||||
|
eyeLineFromBottom: 0.63,
|
||||||
|
bgColor: "#FFFFFF",
|
||||||
|
bgColors: ["#FFFFFF"],
|
||||||
|
label: "Custom",
|
||||||
|
type: "passport" as const,
|
||||||
|
dpi: 300,
|
||||||
|
width: 35,
|
||||||
|
height: 45,
|
||||||
|
}),
|
||||||
|
width: customWidthMm ?? baseDoc?.width ?? 35,
|
||||||
|
height: customHeightMm ?? baseDoc?.height ?? 45,
|
||||||
|
dpi: userDpi,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspacePath = getWorkspacePath(jobId);
|
const workspacePath = getWorkspacePath(jobId);
|
||||||
const bgRemovedFilename = `${filename.replace(/\.[^.]+$/, "")}_nobg.png`;
|
const bgRemovedFilename = `${filename.replace(/\.[^.]+$/, "")}_nobg.png`;
|
||||||
|
|
||||||
const [bgRemovedBuffer, originalBuffer] = await Promise.all([
|
const bgRemovedBuffer = await readFile(join(workspacePath, "output", bgRemovedFilename));
|
||||||
readFile(join(workspacePath, "output", bgRemovedFilename)),
|
|
||||||
readFile(join(workspacePath, "input", filename)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Convert normalized landmarks (0-1) to pixel coordinates
|
// Use actual bg-removed image dimensions for crop (may differ from
|
||||||
const crownYPx = (rawLandmarks.crown.y + adjustY) * imgH;
|
// the original image dimensions reported by the analyze endpoint).
|
||||||
const chinYPx = (rawLandmarks.chin.y + adjustY) * imgH;
|
const bgMeta = await sharp(bgRemovedBuffer).metadata();
|
||||||
const eyeYPx = (rawLandmarks.eyeCenter.y + adjustY) * imgH;
|
const actualW = bgMeta.width ?? imgW;
|
||||||
const faceCenterXPx = (rawLandmarks.faceCenterX + adjustX) * imgW;
|
const actualH = bgMeta.height ?? imgH;
|
||||||
|
|
||||||
|
// Convert normalized landmarks (0-1) to pixel coordinates in the
|
||||||
|
// bg-removed image space (scale if dimensions differ from original).
|
||||||
|
const scaleX = actualW / imgW;
|
||||||
|
const scaleY = actualH / imgH;
|
||||||
|
const crownYPx = (rawLandmarks.crown.y + adjustY) * imgH * scaleY;
|
||||||
|
const chinYPx = (rawLandmarks.chin.y + adjustY) * imgH * scaleY;
|
||||||
|
const eyeYPx = (rawLandmarks.eyeCenter.y + adjustY) * imgH * scaleY;
|
||||||
|
const faceCenterXPx = (rawLandmarks.faceCenterX + adjustX) * imgW * scaleX;
|
||||||
|
|
||||||
// Compute crop region from landmarks
|
// Compute crop region from landmarks
|
||||||
const targetHeadRatio = (docSpec.headHeightMin + docSpec.headHeightMax) / 2;
|
const targetHeadRatio = (docSpec.headHeightMin + docSpec.headHeightMax) / 2;
|
||||||
@@ -343,11 +369,8 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
const bgRgb = { r: bgR, g: bgG, b: bgB, alpha: 1 };
|
const bgRgb = { r: bgR, g: bgG, b: bgB, alpha: 1 };
|
||||||
|
|
||||||
// Composite bg-removed subject onto colored background
|
// Composite bg-removed subject onto colored background
|
||||||
const bgRemovedMeta = await sharp(bgRemovedBuffer).metadata();
|
|
||||||
const srcW = bgRemovedMeta.width ?? imgW;
|
|
||||||
const srcH = bgRemovedMeta.height ?? imgH;
|
|
||||||
const bgLayer = await sharp({
|
const bgLayer = await sharp({
|
||||||
create: { width: srcW, height: srcH, channels: 4, background: bgRgb },
|
create: { width: actualW, height: actualH, channels: 4, background: bgRgb },
|
||||||
})
|
})
|
||||||
.composite([{ input: bgRemovedBuffer, blend: "over" }])
|
.composite([{ input: bgRemovedBuffer, blend: "over" }])
|
||||||
.png()
|
.png()
|
||||||
@@ -363,8 +386,8 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
|
|
||||||
const padLeft = Math.max(0, -rawLeft);
|
const padLeft = Math.max(0, -rawLeft);
|
||||||
const padTop = Math.max(0, -rawTop);
|
const padTop = Math.max(0, -rawTop);
|
||||||
const padRight = Math.max(0, rawLeft + rawW - srcW);
|
const padRight = Math.max(0, rawLeft + rawW - actualW);
|
||||||
const padBottom = Math.max(0, rawTop + rawH - srcH);
|
const padBottom = Math.max(0, rawTop + rawH - actualH);
|
||||||
|
|
||||||
let sourceForCrop = bgLayer;
|
let sourceForCrop = bgLayer;
|
||||||
if (padLeft > 0 || padTop > 0 || padRight > 0 || padBottom > 0) {
|
if (padLeft > 0 || padTop > 0 || padRight > 0 || padBottom > 0) {
|
||||||
@@ -425,8 +448,8 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
dpi: docSpec.dpi,
|
dpi: docSpec.dpi,
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
country: countrySpec.name,
|
country: countrySpec?.name ?? "Custom",
|
||||||
countryCode: countrySpec.code,
|
countryCode: countrySpec?.code ?? "CUSTOM",
|
||||||
documentType: docSpec.type,
|
documentType: docSpec.type,
|
||||||
documentLabel: docSpec.label,
|
documentLabel: docSpec.label,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ interface PassportPhotoStore {
|
|||||||
setBgColor: (c: string) => void;
|
setBgColor: (c: string) => void;
|
||||||
maxFileSizeKb: number;
|
maxFileSizeKb: number;
|
||||||
setMaxFileSizeKb: (s: number) => void;
|
setMaxFileSizeKb: (s: number) => void;
|
||||||
|
dpi: number;
|
||||||
|
setDpi: (d: number) => void;
|
||||||
|
customWidthMm: number | null;
|
||||||
|
customHeightMm: number | null;
|
||||||
|
setCustomDimensions: (w: number | null, h: number | null) => void;
|
||||||
adjustX: number;
|
adjustX: number;
|
||||||
adjustY: number;
|
adjustY: number;
|
||||||
setAdjustX: (x: number) => void;
|
setAdjustX: (x: number) => void;
|
||||||
@@ -88,13 +93,20 @@ const usePassportPhotoStore = create<PassportPhotoStore>((set) => ({
|
|||||||
analyzeResult: null,
|
analyzeResult: null,
|
||||||
setAnalyzeResult: (analyzeResult) => set({ analyzeResult, generateResult: null }),
|
setAnalyzeResult: (analyzeResult) => set({ analyzeResult, generateResult: null }),
|
||||||
countryCode: "US",
|
countryCode: "US",
|
||||||
setCountryCode: (countryCode) => set({ countryCode, generateResult: null }),
|
setCountryCode: (countryCode) =>
|
||||||
|
set({ countryCode, generateResult: null, customWidthMm: null, customHeightMm: null }),
|
||||||
documentType: "passport",
|
documentType: "passport",
|
||||||
setDocumentType: (documentType) => set({ documentType, generateResult: null }),
|
setDocumentType: (documentType) => set({ documentType, generateResult: null }),
|
||||||
bgColor: "#FFFFFF",
|
bgColor: "#FFFFFF",
|
||||||
setBgColor: (bgColor) => set({ bgColor, generateResult: null }),
|
setBgColor: (bgColor) => set({ bgColor, generateResult: null }),
|
||||||
maxFileSizeKb: 0,
|
maxFileSizeKb: 0,
|
||||||
setMaxFileSizeKb: (maxFileSizeKb) => set({ maxFileSizeKb }),
|
setMaxFileSizeKb: (maxFileSizeKb) => set({ maxFileSizeKb }),
|
||||||
|
dpi: 300,
|
||||||
|
setDpi: (dpi) => set({ dpi, generateResult: null }),
|
||||||
|
customWidthMm: null,
|
||||||
|
customHeightMm: null,
|
||||||
|
setCustomDimensions: (customWidthMm, customHeightMm) =>
|
||||||
|
set({ customWidthMm, customHeightMm, countryCode: "CUSTOM", generateResult: null }),
|
||||||
adjustX: 0,
|
adjustX: 0,
|
||||||
adjustY: 0,
|
adjustY: 0,
|
||||||
setAdjustX: (adjustX) => set({ adjustX, generateResult: null }),
|
setAdjustX: (adjustX) => set({ adjustX, generateResult: null }),
|
||||||
@@ -149,21 +161,51 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDocSpec(countryCode: string, documentType: string): PassportDocumentSpec {
|
const CUSTOM_SPEC: PassportSpec = {
|
||||||
|
code: "CUSTOM",
|
||||||
|
name: "Custom",
|
||||||
|
flag: "\u2699\uFE0F",
|
||||||
|
region: "americas",
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
type: "passport",
|
||||||
|
label: "Custom",
|
||||||
|
width: 35,
|
||||||
|
height: 45,
|
||||||
|
dpi: 300,
|
||||||
|
headHeightMin: 0.7,
|
||||||
|
headHeightMax: 0.8,
|
||||||
|
eyeLineFromBottom: 0.63,
|
||||||
|
bgColor: "#FFFFFF",
|
||||||
|
bgColors: ["#FFFFFF"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDocSpec(
|
||||||
|
countryCode: string,
|
||||||
|
documentType: string,
|
||||||
|
customW: number | null,
|
||||||
|
customH: number | null,
|
||||||
|
dpi: number,
|
||||||
|
): PassportDocumentSpec {
|
||||||
|
if (countryCode === "CUSTOM" && customW && customH) {
|
||||||
|
return { ...CUSTOM_SPEC.documents[0], width: customW, height: customH, dpi };
|
||||||
|
}
|
||||||
const spec = PASSPORT_SPECS.find((s) => s.code === countryCode) ?? PASSPORT_SPECS[0];
|
const spec = PASSPORT_SPECS.find((s) => s.code === countryCode) ?? PASSPORT_SPECS[0];
|
||||||
return spec.documents.find((d) => d.type === documentType) ?? spec.documents[0];
|
const doc = spec.documents.find((d) => d.type === documentType) ?? spec.documents[0];
|
||||||
|
return { ...doc, dpi };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountrySpec(countryCode: string): PassportSpec {
|
function getCountrySpec(countryCode: string): PassportSpec {
|
||||||
|
if (countryCode === "CUSTOM") return CUSTOM_SPEC;
|
||||||
return PASSPORT_SPECS.find((s) => s.code === countryCode) ?? PASSPORT_SPECS[0];
|
return PASSPORT_SPECS.find((s) => s.code === countryCode) ?? PASSPORT_SPECS[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format dimensions as "35x45mm" */
|
|
||||||
function formatDimensions(doc: PassportDocumentSpec): string {
|
function formatDimensions(doc: PassportDocumentSpec): string {
|
||||||
return `${doc.width}x${doc.height}mm`;
|
return `${doc.width}x${doc.height}mm`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get pixel dimensions for a spec */
|
|
||||||
function getPixelDimensions(doc: PassportDocumentSpec): { w: number; h: number } {
|
function getPixelDimensions(doc: PassportDocumentSpec): { w: number; h: number } {
|
||||||
const MM_PER_INCH = 25.4;
|
const MM_PER_INCH = 25.4;
|
||||||
return {
|
return {
|
||||||
@@ -296,6 +338,11 @@ export function PassportPhotoSettings() {
|
|||||||
setBgColor,
|
setBgColor,
|
||||||
maxFileSizeKb,
|
maxFileSizeKb,
|
||||||
setMaxFileSizeKb,
|
setMaxFileSizeKb,
|
||||||
|
dpi,
|
||||||
|
setDpi,
|
||||||
|
customWidthMm,
|
||||||
|
customHeightMm,
|
||||||
|
setCustomDimensions,
|
||||||
analyzeResult,
|
analyzeResult,
|
||||||
setAnalyzeResult,
|
setAnalyzeResult,
|
||||||
generateResult,
|
generateResult,
|
||||||
@@ -319,8 +366,10 @@ export function PassportPhotoSettings() {
|
|||||||
width: 0,
|
width: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom file size input
|
// Custom inputs
|
||||||
const [customSizeInput, setCustomSizeInput] = useState("");
|
const [customSizeInput, setCustomSizeInput] = useState("");
|
||||||
|
const [customWInput, setCustomWInput] = useState("");
|
||||||
|
const [customHInput, setCustomHInput] = useState("");
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
const [analyzeError, setAnalyzeError] = useState<string | null>(null);
|
const [analyzeError, setAnalyzeError] = useState<string | null>(null);
|
||||||
@@ -328,7 +377,8 @@ export function PassportPhotoSettings() {
|
|||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
const selectedSpec = getCountrySpec(countryCode);
|
const selectedSpec = getCountrySpec(countryCode);
|
||||||
const docSpec = getDocSpec(countryCode, documentType);
|
const isCustom = countryCode === "CUSTOM";
|
||||||
|
const docSpec = getDocSpec(countryCode, documentType, customWidthMm, customHeightMm, dpi);
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
const uniqueDocTypes = [...new Set(selectedSpec.documents.map((d) => d.type))];
|
const uniqueDocTypes = [...new Set(selectedSpec.documents.map((d) => d.type))];
|
||||||
const pxDims = getPixelDimensions(docSpec);
|
const pxDims = getPixelDimensions(docSpec);
|
||||||
@@ -417,16 +467,19 @@ export function PassportPhotoSettings() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const headers = formatHeaders({ "Content-Type": "application/json" });
|
const headers = formatHeaders({ "Content-Type": "application/json" });
|
||||||
const body = {
|
const body: Record<string, unknown> = {
|
||||||
jobId: analyzeResult.jobId,
|
jobId: analyzeResult.jobId,
|
||||||
filename: analyzeResult.filename,
|
filename: analyzeResult.filename,
|
||||||
countryCode,
|
countryCode: isCustom ? "US" : countryCode,
|
||||||
documentType,
|
documentType,
|
||||||
bgColor,
|
bgColor,
|
||||||
maxFileSizeKb,
|
maxFileSizeKb,
|
||||||
|
dpi,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
landmarks: analyzeResult.landmarks,
|
landmarks: analyzeResult.landmarks,
|
||||||
|
...(isCustom && customWidthMm ? { customWidthMm } : {}),
|
||||||
|
...(isCustom && customHeightMm ? { customHeightMm } : {}),
|
||||||
imageWidth: analyzeResult.imageWidth,
|
imageWidth: analyzeResult.imageWidth,
|
||||||
imageHeight: analyzeResult.imageHeight,
|
imageHeight: analyzeResult.imageHeight,
|
||||||
};
|
};
|
||||||
@@ -530,6 +583,25 @@ export function PassportPhotoSettings() {
|
|||||||
|
|
||||||
{/* Country list */}
|
{/* Country list */}
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
|
{/* Custom option */}
|
||||||
|
{!filteredSpecs && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setCustomDimensions(customWidthMm ?? 35, customHeightMm ?? 45);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors border-b border-border ${
|
||||||
|
isCustom ? "bg-primary/10 text-primary" : "text-foreground hover:bg-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{"\u2699\uFE0F"}</span>
|
||||||
|
<span className="flex-1 text-left">Custom Dimensions</span>
|
||||||
|
{isCustom && <Check className="h-3 w-3 text-primary shrink-0" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{filteredSpecs ? (
|
{filteredSpecs ? (
|
||||||
filteredSpecs.length > 0 ? (
|
filteredSpecs.length > 0 ? (
|
||||||
filteredSpecs.map((spec) => (
|
filteredSpecs.map((spec) => (
|
||||||
@@ -600,6 +672,60 @@ export function PassportPhotoSettings() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Custom dimensions input */}
|
||||||
|
{isCustom && (
|
||||||
|
<>
|
||||||
|
<SectionLabel>Dimensions (mm)</SectionLabel>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={customWInput || String(customWidthMm ?? 35)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCustomWInput(e.target.value);
|
||||||
|
const v = Number.parseInt(e.target.value, 10);
|
||||||
|
if (v > 0) setCustomDimensions(v, customHeightMm ?? 45);
|
||||||
|
}}
|
||||||
|
placeholder="Width"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-xs text-foreground"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs">{"\u00D7"}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={customHInput || String(customHeightMm ?? 45)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCustomHInput(e.target.value);
|
||||||
|
const v = Number.parseInt(e.target.value, 10);
|
||||||
|
if (v > 0) setCustomDimensions(customWidthMm ?? 35, v);
|
||||||
|
}}
|
||||||
|
placeholder="Height"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-xs text-foreground"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs">mm</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* DPI */}
|
||||||
|
<SectionLabel>DPI</SectionLabel>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={dpi}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = Number.parseInt(e.target.value, 10);
|
||||||
|
if (v >= 72 && v <= 600) setDpi(v);
|
||||||
|
}}
|
||||||
|
min="72"
|
||||||
|
max="600"
|
||||||
|
className="w-20 px-2 py-1.5 rounded border border-border bg-background text-xs text-foreground"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">pixels per inch</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Background color */}
|
{/* Background color */}
|
||||||
<SectionLabel>Background Color</SectionLabel>
|
<SectionLabel>Background Color</SectionLabel>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -743,6 +869,9 @@ export function PassportPhotoPreview() {
|
|||||||
countryCode,
|
countryCode,
|
||||||
documentType,
|
documentType,
|
||||||
bgColor,
|
bgColor,
|
||||||
|
dpi,
|
||||||
|
customWidthMm,
|
||||||
|
customHeightMm,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
setAdjustX,
|
setAdjustX,
|
||||||
@@ -760,7 +889,7 @@ export function PassportPhotoPreview() {
|
|||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const dragStartRef = useRef<{ x: number; y: number; ax: number; ay: number } | null>(null);
|
const dragStartRef = useRef<{ x: number; y: number; ax: number; ay: number } | null>(null);
|
||||||
|
|
||||||
const docSpec = getDocSpec(countryCode, documentType);
|
const docSpec = getDocSpec(countryCode, documentType, customWidthMm, customHeightMm, dpi);
|
||||||
const pxDims = getPixelDimensions(docSpec);
|
const pxDims = getPixelDimensions(docSpec);
|
||||||
|
|
||||||
// Compliance checks
|
// Compliance checks
|
||||||
|
|||||||
Reference in New Issue
Block a user