Files
digicraft-be/services/stickerSheetService.ts
Fahri Can Seçer 80dcf4d04a
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
main
2026-02-05 01:29:22 +03:00

193 lines
7.0 KiB
TypeScript

import sharp from 'sharp';
import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
// Paper Dimensions (at 300 DPI)
const PAPER_SIZES: Record<string, { width: number, height: number }> = {
'A4': { width: 2480, height: 3508 }, // 210mm x 297mm
'Letter': { width: 2550, height: 3300 }, // 8.5in x 11in
'A5': { width: 1748, height: 2480 } // 148mm x 210mm
};
export const stickerSheetService = {
/**
* Add a white border and optional cut line to an image.
* @param inputPath Path to the source image (sticker).
* @param borderWidth Width of the white contour in pixels (default 40).
*/
async addCutContour(inputPath: string, borderWidth: number = 40): Promise<Buffer> {
// 1. Load Image
const image = sharp(inputPath);
const metadata = await image.metadata();
if (!metadata.width || !metadata.height) throw new Error("Invalid image metadata");
// 2. Create Mask (Alpha Channel)
const alpha = await image.clone()
.toColourspace('b-w')
.ensureAlpha()
.extractChannel(3) // Get alpha channel
.raw()
.toBuffer();
// 3. Dilate Mask to create Border Shape
// Proper dilation requires morphological operations which Sharp doesn't support natively well for complex shapes without external libs.
// TRICK: We will blur the alpha channel and threshold it to simulate dilation/expansion.
const expandedMask = await sharp(alpha, { raw: { width: metadata.width, height: metadata.height, channels: 1 } })
.blur(borderWidth) // Spread the alpha
.threshold(10) // Binarize to create hard edge
.toBuffer();
// 4. Create White Silhouette using the expanded mask
const whiteBackground = await sharp({
create: {
width: metadata.width,
height: metadata.height,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 }
}
})
.joinChannel(expandedMask) // Apply mask
.png()
.toBuffer();
// 5. Composite Original Image ON TOP of White Silhouette
return sharp(whiteBackground)
.composite([{ input: inputPath }])
.withMetadata({ density: 300 }) // Enforce 300 DPI
.png()
.toBuffer();
},
/**
* Generate a BLACKOUT mask for Cricut Cut Lines.
* Returns a binary verification image (Black shape on transparent background).
*/
async generateCutMask(inputPath: string, borderWidth: number = 40): Promise<Buffer> {
// 1. Load Image
const image = sharp(inputPath);
const metadata = await image.metadata();
if (!metadata.width || !metadata.height) throw new Error("Invalid image metadata");
// 2. Extract Alpha
const alpha = await image.clone()
.toColourspace('b-w')
.ensureAlpha()
.extractChannel(3)
.raw()
.toBuffer();
// 3. Dilate (Same logic as addCutContour to match perfectly)
const expandedMask = await sharp(alpha, { raw: { width: metadata.width, height: metadata.height, channels: 1 } })
.blur(borderWidth)
.threshold(10)
.toBuffer();
// 4. Create BLACK Silhouette
return sharp({
create: {
width: metadata.width,
height: metadata.height,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 1 } // BLACK
}
})
.joinChannel(expandedMask) // Apply same mask
.withMetadata({ density: 300 }) // Enforce 300 DPI
.png()
.toBuffer();
},
/**
* Arrange multiple stickers onto a single sheet.
* @param stickerPaths Array of absolute paths to sticker images.
* @param options Configuration for the sheet (target dimensions, paper size name)
*/
async generateSheet(stickerPaths: string[], options: { width: number, height: number, paperSizeName?: string }): Promise<Buffer> {
const { width, height } = options;
// Target 4-5 columns for A4/Letter at 2500px width.
// If width is ~5000px (High Res A4), we still want ~4-5 columns?
// Or do we want more stickers if we have them?
// Usually a sticker sheet has a fixed number of stickers.
// Let's assume we want a standard grid.
// Let's base sticker size on WIDTH.
// 4 cols + 5 gaps = width.
// stickerW * 4 + gap * 5 = width.
// let gap = stickerW / 10.
// stickerW * 4 + stickerW/2 = width => 4.5 stickerW = width.
const COLS = 4;
// Calculation:
// Width = COLS * StickerW + (COLS + 1) * GAP
// Let GAP = StickerW * 0.1
// Width = 4 * S + 5 * 0.1 * S = 4.5 S
const stickerWidth = Math.floor(width / (COLS + (COLS + 1) * 0.1));
const gap = Math.floor(stickerWidth * 0.1);
console.log(`[StickerService] Generating Sheet ${width}x${height}. Sticker W: ${stickerWidth}, Gap: ${gap}`);
const composites: sharp.OverlayOptions[] = [];
let currentX = gap;
let currentY = gap;
let maxHeightInRow = 0;
for (const stickerPath of stickerPaths) {
let stickerBuffer;
try {
stickerBuffer = await this.addCutContour(stickerPath, Math.floor(stickerWidth * 0.05)); // Scale border too
} catch (e) {
console.error(`Failed to process sticker ${stickerPath}`, e);
continue;
}
// Resize to target width
const resizedSticker = await sharp(stickerBuffer)
.resize({ width: stickerWidth })
.toBuffer();
const meta = await sharp(resizedSticker).metadata();
const w = meta.width || stickerWidth;
const h = meta.height || stickerWidth;
// Check if fits in current row
if (currentX + w + gap > width) {
// Next Row
currentX = gap;
currentY += maxHeightInRow + gap;
maxHeightInRow = 0;
}
// Check if fits in page (vertical)
if (currentY + h + gap > height) {
console.warn("Sheet full, skipping remaining stickers");
break;
}
composites.push({
input: resizedSticker,
top: currentY,
left: currentX
});
currentX += w + gap;
if (h > maxHeightInRow) maxHeightInRow = h;
}
// Create Sheet Canvas
return sharp({
create: {
width: width,
height: height,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 } // Transparent
}
})
.composite(composites)
.png()
.toBuffer();
}
};