This commit is contained in:
535
services/archiveService.ts
Normal file
535
services/archiveService.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
import archiver from 'archiver';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const STORAGE_ROOT = path.resolve(__dirname, '../../storage');
|
||||
|
||||
// Aspect Ratio Map for exact print dimensions
|
||||
const ASPECT_RATIO_MAP: Record<string, [number, number]> = {
|
||||
"1:1": [6000, 6000],
|
||||
"3:4": [4500, 6000],
|
||||
"4:3": [6000, 4500],
|
||||
"4:5": [4800, 6000],
|
||||
"5:4": [6000, 4800],
|
||||
"9:16": [3375, 6000],
|
||||
"16:9": [6000, 3375],
|
||||
"2:3": [4000, 6000],
|
||||
"3:2": [6000, 4000],
|
||||
"1:4": [1500, 6000],
|
||||
"4:1": [6000, 1500],
|
||||
};
|
||||
const MIN_PRINT_SIZE = 6000;
|
||||
|
||||
export class ArchiveService {
|
||||
|
||||
// 1. MASTER ZIP: Everything (Source of Truth)
|
||||
async createMasterArchive(projectId: string, res: any) {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
include: { assets: true, seoData: true, user: { include: { etsyShops: true } } }
|
||||
});
|
||||
|
||||
if (!project) throw new Error("Project not found");
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
res.attachment(`MasterBundle_${project.productType}_${projectId.substring(0, 6)}.zip`);
|
||||
archive.pipe(res);
|
||||
|
||||
const qualityLog: string[] = [];
|
||||
qualityLog.push(`QUALITY VALIDATION REPORT`);
|
||||
qualityLog.push(`Generated: ${new Date().toISOString()}`);
|
||||
qualityLog.push(`Project ID: ${projectId}`);
|
||||
qualityLog.push(`Product Type: ${project.productType}`);
|
||||
qualityLog.push(`----------------------------------------`);
|
||||
|
||||
// A. Add All Assets & Validate (with Print-Ready Enforcement)
|
||||
const MIN_PRINT_SIZE = 6000; // Minimum long side for print-ready
|
||||
|
||||
for (const asset of project.assets) {
|
||||
const assetPath = path.join(STORAGE_ROOT, asset.path);
|
||||
if (fs.existsSync(assetPath)) {
|
||||
const internalPath = `assets/${asset.type}/${path.basename(assetPath)}`;
|
||||
|
||||
// VALIDATION & ENFORCEMENT (Only for images)
|
||||
if (assetPath.endsWith('.png') || assetPath.endsWith('.jpg')) {
|
||||
try {
|
||||
const meta = await sharp(assetPath).metadata();
|
||||
const currentWidth = meta.width || 0;
|
||||
const currentHeight = meta.height || 0;
|
||||
const longSide = Math.max(currentWidth, currentHeight);
|
||||
const isPrintReady = longSide >= MIN_PRINT_SIZE && (meta.density || 72) >= 300;
|
||||
const colorSpace = meta.channels === 4 ? "RGB+Alpha" : meta.channels === 3 ? "RGB" : "CMYK/Other";
|
||||
|
||||
// For MASTER and UPSCALED: Enforce 6000px @ 300 DPI
|
||||
if (['master', 'upscaled'].includes(asset.type) && !isPrintReady) {
|
||||
const ratio = currentWidth / currentHeight;
|
||||
let finalWidth: number;
|
||||
let finalHeight: number;
|
||||
|
||||
if (currentWidth >= currentHeight) {
|
||||
finalWidth = MIN_PRINT_SIZE;
|
||||
finalHeight = Math.round(MIN_PRINT_SIZE / ratio);
|
||||
} else {
|
||||
finalHeight = MIN_PRINT_SIZE;
|
||||
finalWidth = Math.round(MIN_PRINT_SIZE * ratio);
|
||||
}
|
||||
|
||||
// Upscale on-the-fly for archive
|
||||
const upscaledBuffer = await sharp(assetPath)
|
||||
.resize(finalWidth, finalHeight, { kernel: sharp.kernel.lanczos3, fit: 'fill' })
|
||||
.withMetadata({ density: 300 })
|
||||
.png({ quality: 100, compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
|
||||
archive.append(upscaledBuffer, { name: `assets/${asset.type}/PrintReady_${finalWidth}x${finalHeight}_${path.basename(assetPath)}` });
|
||||
|
||||
qualityLog.push(`[UPSCALED FOR PRINT] ${internalPath}`);
|
||||
qualityLog.push(` - Original: ${currentWidth}x${currentHeight}px | Density: ${meta.density || 72}dpi`);
|
||||
qualityLog.push(` - Upscaled to: ${finalWidth}x${finalHeight}px @ 300 DPI for print-readiness.`);
|
||||
} else {
|
||||
// Add as-is (already compliant or non-master asset)
|
||||
archive.file(assetPath, { name: internalPath });
|
||||
|
||||
const status = isPrintReady ? "PRINT-READY ✓" : (longSide >= 2000 ? "PASS" : "WARN (Low Res)");
|
||||
qualityLog.push(`[${status}] ${internalPath}`);
|
||||
qualityLog.push(` - Dims: ${currentWidth}x${currentHeight}px | Density: ${meta.density || 72}dpi | Color: ${colorSpace}`);
|
||||
if (!isPrintReady && ['master', 'upscaled'].includes(asset.type)) {
|
||||
qualityLog.push(` - WARNING: Asset may be too small for large format printing.`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
qualityLog.push(`[ERROR] validation failed for ${internalPath}: ${(e as Error).message}`);
|
||||
// Still add the original file on error
|
||||
archive.file(assetPath, { name: internalPath });
|
||||
}
|
||||
} else {
|
||||
// Non-image assets (text files, etc.)
|
||||
archive.file(assetPath, { name: internalPath });
|
||||
}
|
||||
} else {
|
||||
qualityLog.push(`[MISSING] Asset record found but file missing: ${asset.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// B. Add Restoration Manifest (Machine Readable)
|
||||
const manifest = {
|
||||
version: "1.0",
|
||||
timestamp: new Date().toISOString(),
|
||||
project: {
|
||||
id: project.id,
|
||||
niche: project.niche,
|
||||
productType: project.productType,
|
||||
creativity: project.creativity,
|
||||
aspectRatio: project.aspectRatio
|
||||
},
|
||||
seo: project.seoData,
|
||||
assets: project.assets.map(a => ({ type: a.type, path: a.path, meta: a.meta }))
|
||||
};
|
||||
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
||||
|
||||
// C. Add Quality Report
|
||||
archive.append(qualityLog.join('\n'), { name: 'docs/QUALITY_REPORT.txt' });
|
||||
|
||||
// D. Add Strategy / SEO
|
||||
if (project.seoData) {
|
||||
// Construct the State Object exactly how the UI components demand it
|
||||
// CRITICAL FIX: Fallback to seoData if strategy is missing (Recovered Projects Scenario)
|
||||
const resolvedStrategy = (project.seoData as any) || {};
|
||||
const shopName = project.user?.etsyShops?.[0]?.shopName || "The Artist";
|
||||
|
||||
// Format Attributes
|
||||
let attributesStr = "None";
|
||||
try {
|
||||
if ((project.seoData as any).attributes) {
|
||||
const attrs = JSON.parse((project.seoData as any).attributes);
|
||||
attributesStr = Object.entries(attrs).map(([k, v]) => `- ${k.toUpperCase()}: ${v}`).join('\n');
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
const seoContent = `
|
||||
TITLE: ${(project as any).sku ? `[${(project as any).sku}] ` : ''}${project.seoData.title}
|
||||
SKU: ${(project as any).sku || 'Not Assigned'}
|
||||
CATEGORY PATH: ${(project.seoData as any).categoryPath || "Not set"}
|
||||
|
||||
DESCRIPTION:
|
||||
${project.seoData.description}
|
||||
|
||||
KEYWORDS: ${JSON.parse(project.seoData.keywords as string || '[]').join(', ')}
|
||||
|
||||
HIDDEN ATTRIBUTES:
|
||||
${attributesStr}
|
||||
|
||||
--------------------------------------
|
||||
Generated for ${shopName} via DigiCraft
|
||||
`;
|
||||
archive.append(seoContent, { name: 'docs/SEO_METADATA.txt' });
|
||||
|
||||
if (project.seoData.printingGuide) {
|
||||
const guidePath = path.join(STORAGE_ROOT, project.seoData.printingGuide);
|
||||
if (fs.existsSync(guidePath)) {
|
||||
archive.file(guidePath, { name: 'docs/Printing_Guide.txt' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
}
|
||||
|
||||
// 2. CUSTOMER ARCHIVE: Filtering sensitive info, only deliverables
|
||||
async createCustomerArchive(projectId: string, mode: 'rgb' | 'cmyk', res: any) {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
include: { assets: true, seoData: true, user: { include: { etsyShops: true } } }
|
||||
});
|
||||
|
||||
if (!project) throw new Error("Project not found");
|
||||
|
||||
const shopName = (project.user as any)?.etsyShopName || project.user?.etsyShops?.[0]?.shopName || "The Artist";
|
||||
const shopUrl = (project.user as any)?.etsyShopLink || "";
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
res.attachment(`CustomerDownload_${mode.toUpperCase()}_${projectId.substring(0, 6)}.zip`);
|
||||
archive.pipe(res);
|
||||
|
||||
// ----------------------------------------------------
|
||||
// A. Neuro-Marketing Experience Pack (Etsy-Compliant)
|
||||
// ----------------------------------------------------
|
||||
|
||||
const readmeContent = `
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
✨ THANK YOU FOR CHOOSING ${shopName.toUpperCase()}! ✨
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Congratulations on your wonderful choice! 🎨
|
||||
|
||||
We're so excited that you've chosen to bring this artwork into your space.
|
||||
Every pixel has been crafted with care, and we can't wait to see how you use it.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
📥 WHAT'S INSIDE THIS PACKAGE?
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
${mode === 'cmyk' ? `✓ Professional CMYK Print Files (.tif format)
|
||||
→ Optimized for commercial printing
|
||||
→ 300 DPI high-resolution
|
||||
→ Ready for offset, laser, or inkjet printing` : `✓ Digital RGB Files (.png format)
|
||||
→ Perfect for digital displays
|
||||
→ 300 DPI high-resolution
|
||||
→ Vibrant colors for screens and home printers`}
|
||||
|
||||
✓ Multiple Aspect Ratios Included:
|
||||
→ 3:4 (9x12, 18x24 inches)
|
||||
→ 4:5 (8x10, 16x20 inches)
|
||||
→ 2:3 (4x6, 24x36 inches)
|
||||
→ 5:7 / ISO (A1-A5 international)
|
||||
→ 11:14 (Exclusive frame size)
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
🖨️ HOW TO PRINT (3 Easy Steps)
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
1️⃣ Choose your file based on your frame size
|
||||
2️⃣ Upload to a print service (Costco, Walgreens, local print shop)
|
||||
3️⃣ Select "Original Size" or "Fit to Frame" and print!
|
||||
|
||||
💡 Pro Tip: For the BEST quality, use a professional print service
|
||||
with heavyweight matte or luster paper (200gsm+).
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
💬 WE'D LOVE TO HEAR FROM YOU
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Your feedback helps us grow and helps other buyers make informed decisions.
|
||||
If you have a moment, sharing your experience would mean the world to us.
|
||||
|
||||
Whether it's a quick thought or a photo of the art in your space —
|
||||
we genuinely appreciate every message. 💌
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
❓ NEED HELP?
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Having trouble? We're here for you!
|
||||
Simply send us a message through Etsy and we'll respond within 24 hours.
|
||||
|
||||
With gratitude,
|
||||
The ${shopName} Team 🌟
|
||||
${shopUrl ? `\n🔗 Visit our shop: ${shopUrl}` : ''}
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
`;
|
||||
archive.append(readmeContent.trim(), { name: '00_READ_ME_FIRST.txt' });
|
||||
|
||||
const licenseContent = `
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
📜 PERSONAL USE LICENSE AGREEMENT
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Thank you for supporting independent artists! 🙏
|
||||
This license grants you the following rights:
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
✅ WHAT YOU CAN DO
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
• Print and display this artwork in your home or office
|
||||
• Use as a digital wallpaper on your personal devices
|
||||
• Gift a PRINTED copy to friends or family
|
||||
• Print multiple copies for your own personal spaces
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
❌ WHAT'S NOT ALLOWED
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
• Reselling, sharing, or redistributing the digital files
|
||||
• Uploading to print-on-demand sites (Redbubble, Amazon, Etsy, etc.)
|
||||
• Using for commercial products without a commercial license
|
||||
• Claiming this artwork as your own creation
|
||||
• Modifying and reselling as a "new" product
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
🛡️ COPYRIGHT NOTICE
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
This artwork is protected by copyright law.
|
||||
Unauthorized distribution or commercial use is prohibited.
|
||||
|
||||
We put our heart into every design. Thank you for respecting
|
||||
the rights of independent artists and small businesses. ❤️
|
||||
|
||||
© ${new Date().getFullYear()} ${shopName}. All Rights Reserved.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
📧 QUESTIONS ABOUT LICENSING?
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Need a commercial license? Want to use this for a business project?
|
||||
Just send us a message — we're happy to discuss custom licensing options!
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
`;
|
||||
archive.append(licenseContent.trim(), { name: '01_LICENSE.txt' });
|
||||
|
||||
// NEW: Thank You Card with Soft Review Request
|
||||
const thankYouContent = `
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
💌 A PERSONAL NOTE FROM THE ARTIST
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Hey there, art lover! 👋
|
||||
|
||||
I wanted to take a moment to personally thank you for your purchase.
|
||||
As a small business owner, every order feels like a little celebration.
|
||||
|
||||
Your support allows me to keep creating, keep dreaming, and keep
|
||||
bringing beautiful art into the world. That means everything.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
🌟 SHARE YOUR EXPERIENCE
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
If your order arrived safely and you're happy with the quality,
|
||||
I'd be incredibly grateful if you could take a moment to share
|
||||
your experience with other buyers.
|
||||
|
||||
Your honest feedback — whatever it may be — helps others discover
|
||||
our work and helps us understand how we can serve you better.
|
||||
|
||||
(No pressure, no expectations. Just genuine appreciation for
|
||||
any thoughts you're willing to share. 🙏)
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
📸 WE LOVE SEEING YOUR SPACE!
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Hung up your print? Styled it on your desk?
|
||||
We'd love to see how you've decorated your space!
|
||||
|
||||
Tag us or send a photo — we might feature you on our page!
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Until next time,
|
||||
${shopName} 🎨✨
|
||||
${shopUrl ? `\n🔗 ${shopUrl}` : ''}
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
`;
|
||||
archive.append(thankYouContent.trim(), { name: '02_THANK_YOU.txt' });
|
||||
|
||||
// ----------------------------------------------------
|
||||
// B. Add Deliverables (with Print-Ready Enforcement)
|
||||
// ----------------------------------------------------
|
||||
|
||||
// Filter: Only Master and Variants
|
||||
const deliverables = project.assets.filter(a => ['master', 'variant', 'upscaled'].includes(a.type));
|
||||
|
||||
// Get target dimensions from project's aspect ratio
|
||||
const targetRatio = project.aspectRatio || "3:4";
|
||||
let targetWidth: number;
|
||||
let targetHeight: number;
|
||||
|
||||
if (ASPECT_RATIO_MAP[targetRatio]) {
|
||||
[targetWidth, targetHeight] = ASPECT_RATIO_MAP[targetRatio];
|
||||
} else {
|
||||
// Fallback for unknown ratios
|
||||
const [w, h] = targetRatio.split(':').map(Number);
|
||||
if (w > h) {
|
||||
targetWidth = MIN_PRINT_SIZE;
|
||||
targetHeight = Math.round(MIN_PRINT_SIZE * h / w);
|
||||
} else {
|
||||
targetHeight = MIN_PRINT_SIZE;
|
||||
targetWidth = Math.round(MIN_PRINT_SIZE * w / h);
|
||||
}
|
||||
}
|
||||
|
||||
const nameCount: Record<string, number> = {};
|
||||
|
||||
for (const asset of deliverables) {
|
||||
const assetPath = path.join(STORAGE_ROOT, asset.path);
|
||||
if (fs.existsSync(assetPath)) {
|
||||
// filename logic: SEO Title (truncated) + Ratio
|
||||
let safeTitle = "Artwork";
|
||||
if (project.seoData?.title) {
|
||||
safeTitle = project.seoData.title
|
||||
.replace(/[^a-zA-Z0-9\s-_]/g, '') // Remove special chars
|
||||
.trim()
|
||||
.replace(/\s+/g, '_'); // Replace spaces with underscores
|
||||
|
||||
if (safeTitle.length > 40) {
|
||||
safeTitle = safeTitle.substring(0, 40);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const meta = await sharp(assetPath).metadata();
|
||||
const currentWidth = meta.width || 0;
|
||||
const currentHeight = meta.height || 0;
|
||||
|
||||
// Check if image is already at target specifications
|
||||
const isPrintReady = (currentWidth === targetWidth && currentHeight === targetHeight);
|
||||
|
||||
// Determine Ratio String
|
||||
let ratioStr = "Original";
|
||||
// Simple GCD or lookup could be better, but approximation works for filenames
|
||||
const r = currentWidth / currentHeight;
|
||||
if (Math.abs(r - 3 / 4) < 0.05) ratioStr = "3x4";
|
||||
else if (Math.abs(r - 4 / 5) < 0.05) ratioStr = "4x5";
|
||||
else if (Math.abs(r - 2 / 3) < 0.05) ratioStr = "2x3";
|
||||
else if (Math.abs(r - 9 / 16) < 0.05) ratioStr = "9x16";
|
||||
else if (Math.abs(r - 5 / 7) < 0.05) ratioStr = "5x7";
|
||||
else if (Math.abs(r - 11 / 14) < 0.05) ratioStr = "11x14";
|
||||
else if (Math.abs(r - 1) < 0.05) ratioStr = "1x1";
|
||||
else ratioStr = `${currentWidth}x${currentHeight}`;
|
||||
|
||||
// Dynamic Truncation Logic to preserve Suffix/SKU
|
||||
const skuPrefix = (project as any).sku ? `${(project as any).sku}_` : "";
|
||||
const ratioSuffix = `_${ratioStr}`;
|
||||
const targetExt = mode === 'cmyk' ? '.tif' : '.png';
|
||||
|
||||
const MAX_FILENAME_LEN = 65; // Strict limit
|
||||
const reservedLen = skuPrefix.length + ratioSuffix.length + targetExt.length;
|
||||
const availableTitleLen = MAX_FILENAME_LEN - reservedLen;
|
||||
|
||||
let finalTitle = safeTitle;
|
||||
if (finalTitle.length > availableTitleLen) {
|
||||
finalTitle = finalTitle.substring(0, Math.max(10, availableTitleLen)); // Ensure at least 10 chars
|
||||
}
|
||||
|
||||
// Construct Final Name
|
||||
// Handle duplicates by pre-checking against nameCount map?
|
||||
// Actually, simple counter logic needs to happen BEFORE full truncation or AFTER?
|
||||
// Better to rely on unique ratioStr for master assets.
|
||||
// But if multiple assets map to same ratio (e.g. variants), we need index.
|
||||
|
||||
let baseNameCandidate = `${skuPrefix}${finalTitle}${ratioSuffix}`;
|
||||
|
||||
if (nameCount[baseNameCandidate]) {
|
||||
nameCount[baseNameCandidate]++;
|
||||
// Adjust title slightly to accommodate counter?
|
||||
// Or just append counter and risk going over limit slightly?
|
||||
// Let's shorten title more if needed.
|
||||
const countStr = `_${nameCount[baseNameCandidate]}`;
|
||||
if ((baseNameCandidate.length + countStr.length + targetExt.length) > 70) {
|
||||
// Shrink title more
|
||||
const reduceBy = (baseNameCandidate.length + countStr.length + targetExt.length) - 70;
|
||||
finalTitle = finalTitle.substring(0, finalTitle.length - reduceBy);
|
||||
baseNameCandidate = `${skuPrefix}${finalTitle}${ratioSuffix}`;
|
||||
}
|
||||
baseNameCandidate += countStr;
|
||||
} else {
|
||||
nameCount[baseNameCandidate] = 1;
|
||||
}
|
||||
|
||||
const finalFilename = `${baseNameCandidate}${targetExt}`;
|
||||
|
||||
// ... Processing buffers ...
|
||||
|
||||
let sourceBuffer: Buffer;
|
||||
|
||||
// Upscale and enforce exact ratio for master/upscaled types
|
||||
if (!isPrintReady && ['master', 'upscaled'].includes(asset.type)) {
|
||||
sourceBuffer = await sharp(assetPath)
|
||||
.resize(targetWidth, targetHeight, {
|
||||
kernel: sharp.kernel.lanczos3,
|
||||
fit: 'cover',
|
||||
position: 'center'
|
||||
})
|
||||
.withMetadata({ density: 300 })
|
||||
.png({ quality: 100, compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
} else {
|
||||
sourceBuffer = fs.readFileSync(assetPath);
|
||||
}
|
||||
|
||||
if (mode === 'cmyk') {
|
||||
// TRUE CMYK Conversion
|
||||
let cmykBuffer: Buffer;
|
||||
try {
|
||||
cmykBuffer = await sharp(sourceBuffer)
|
||||
.pipelineColourspace('rgb16')
|
||||
.toColourspace('cmyk')
|
||||
.withMetadata({ density: 300 })
|
||||
.tiff({ compression: 'lzw', quality: 100, xres: 300, yres: 300 })
|
||||
.toBuffer();
|
||||
} catch (cmykError: any) {
|
||||
console.warn(`[CMYK] Fallback for ${finalFilename}`);
|
||||
cmykBuffer = await sharp(sourceBuffer)
|
||||
.withMetadata({ density: 300 })
|
||||
.tiff({ compression: 'lzw', quality: 100, xres: 300, yres: 300 })
|
||||
.toBuffer();
|
||||
}
|
||||
archive.append(cmykBuffer, { name: `print_ready/${finalFilename}` });
|
||||
} else {
|
||||
archive.append(sourceBuffer, { name: `digital_files_rgb/${finalFilename}` });
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(`Processing failed for ${assetPath}:`, e);
|
||||
// Fallback to original
|
||||
archive.file(assetPath, { name: `fallback/${path.basename(assetPath)}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add Guide
|
||||
if (project.seoData?.printingGuide) {
|
||||
const guidePath = path.join(STORAGE_ROOT, project.seoData.printingGuide);
|
||||
if (fs.existsSync(guidePath)) {
|
||||
archive.file(guidePath, { name: 'Printing_Instructions.txt' });
|
||||
}
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
export const archiveService = new ArchiveService();
|
||||
Reference in New Issue
Block a user