Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/hooks/useCryptoPrices.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ async function fetchPrices() {
function toWei(usdTarget, tokenPriceUsd) {
if (!tokenPriceUsd || tokenPriceUsd <= 0) return undefined;
const raw = usdTarget / tokenPriceUsd;
if (!isFinite(raw) || isNaN(raw)) return undefined;
if (!isFinite(raw) || Number.isNaN(raw)) return undefined;
try { return parseEther(raw.toFixed(8)); } catch { return undefined; }
}

function toHuman(usdTarget, tokenPriceUsd, decimals = 5) {
if (!tokenPriceUsd || tokenPriceUsd <= 0) return undefined;
const raw = usdTarget / tokenPriceUsd;
if (!isFinite(raw) || isNaN(raw)) return undefined;
if (!isFinite(raw) || Number.isNaN(raw)) return undefined;
return raw.toFixed(decimals);
}

Expand Down
177 changes: 1 addition & 176 deletions src/services/pdf.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,179 +456,4 @@ export const convertToGrayscale = async (file, onProgress) => {

const base64Image = finalCanvas.toDataURL("image/jpeg", 0.9);
const imageBytes = await fetch(base64Image).then((res) =>
res.arrayBuffer(),
);

const embeddedImage = await bwPdf.embedJpg(imageBytes);
const { width, height } = embeddedImage.scale(1);

const newPage = bwPdf.addPage([width, height]);
newPage.drawImage(embeddedImage, { x: 0, y: 0, width, height });

// Release the canvases as we go so large files do not balloon memory use.
tempCanvas.width = 0;
finalCanvas.width = 0;
}

const pdfBytes = await bwPdf.save();
return new Blob([pdfBytes], { type: "application/pdf" });
};

/**
* Password-protect a PDF file.
* @param {File} file - The source PDF
* @param {string} userPassword - Password required to open the document
* @param {string} [ownerPassword] - Owner password (defaults to a random token)
* @returns {Promise<Blob>}
*/
export const lockPdf = async (file, userPassword, ownerPassword) => {
if (!file) throw new Error("No file provided.");
if (!userPassword) throw new Error("A password is required.");

// Load via pdf-lib to ensure the file is a valid, readable PDF.
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const pdfBytes = await pdfDoc.save();

// Owner password defaults to a random unguessable string so the user
// password is the only way to open the document.
const ownerPwd = ownerPassword || crypto.randomUUID();

const encryptedBytes = await encryptPDF(
pdfBytes,
userPassword,
ownerPwd,
);

return new Blob([encryptedBytes], { type: "application/pdf" });
};

// ─────────────────────────────────────────────────────────────────────────────
// applyEdits — bake canvas annotations into a PDF
// annotations: [{type, pageIndex, color, opacity, strokeWidth, ...shape}]
// pages: [{width, height, pdfWidth, pdfHeight}] (rendered at RENDER_SCALE)
// ─────────────────────────────────────────────────────────────────────────────
export const applyEdits = async (file, annotations, pages) => {
if (!file) throw new Error("No file provided.");

const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pdfPages = pdfDoc.getPages();

function hexToRgb(hex) {
const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return r ? rgb(parseInt(r[1],16)/255, parseInt(r[2],16)/255, parseInt(r[3],16)/255) : rgb(0,0,0);
}

for (const ann of annotations) {
const info = pages[ann.pageIndex];
const pdfPage = pdfPages[ann.pageIndex];
if (!info || !pdfPage) continue;

// Scale factors: canvas-px → PDF-points
const sx = info.pdfWidth / info.width;
const sy = info.pdfHeight / info.height;
const toX = (cx) => cx * sx;
const toY = (cy) => info.pdfHeight - cy * sy; // flip Y (PDF origin = bottom-left)
const clr = hexToRgb(ann.color);

switch (ann.type) {
case "draw": {
if (!ann.points || ann.points.length < 2) break;
const step = Math.max(1, Math.floor(ann.points.length / 300));
const pts = ann.points.filter((_, i) => i % step === 0);
if (pts.length < 2) pts.push(ann.points.at(-1));
for (let i = 1; i < pts.length; i++) {
pdfPage.drawLine({
start: { x: toX(pts[i-1].x), y: toY(pts[i-1].y) },
end: { x: toX(pts[i].x), y: toY(pts[i].y) },
thickness: (ann.strokeWidth ?? 2) * sx,
color: clr,
opacity: ann.opacity ?? 1,
});
}
break;
}
case "highlight": {
const lx = Math.min(ann.x, ann.x2), ly = Math.min(ann.y, ann.y2);
const w = Math.abs(ann.x2 - ann.x), h = Math.abs(ann.y2 - ann.y);
pdfPage.drawRectangle({
x: toX(lx), y: toY(ly + h), width: w * sx, height: h * sy,
color: clr, opacity: 0.35,
});
break;
}
case "rect": {
const lx = Math.min(ann.x, ann.x2), ly = Math.min(ann.y, ann.y2);
const w = Math.abs(ann.x2 - ann.x), h = Math.abs(ann.y2 - ann.y);
pdfPage.drawRectangle({
x: toX(lx), y: toY(ly + h), width: w * sx, height: h * sy,
borderColor: clr, borderWidth: (ann.strokeWidth ?? 2) * sx,
borderOpacity: ann.opacity ?? 1, opacity: 0,
});
break;
}
case "text": {
const ptSize = (ann.fontSize ?? 18) * sx;
pdfPage.drawText(ann.text, {
x: toX(ann.x), y: toY(ann.y + (ann.fontSize ?? 18)),
size: Math.max(4, ptSize), font, color: clr, opacity: ann.opacity ?? 1,
});
break;
}
}
}

const pdfBytes = await pdfDoc.save();
return new Blob([pdfBytes], { type: "application/pdf" });
};

// ─────────────────────────────────────────────────────────────────────────────
// editPdfText — replace existing text in a PDF (cover-and-redraw strategy)
// textEdits: { [itemId]: newText }
// textItems: [{id, pageIndex, pdfX, pdfY, pdfW, fontSize, str}]
// ─────────────────────────────────────────────────────────────────────────────
export const editPdfText = async (file, textEdits, textItems) => {
if (!file) throw new Error("No file provided.");

const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pdfPages = pdfDoc.getPages();

for (const [itemId, newText] of Object.entries(textEdits)) {
const item = textItems.find(t => t.id === itemId);
if (!item || newText === item.str) continue;

const page = pdfPages[item.pageIndex];
if (!page) continue;

const fs = Math.min(Math.max(item.fontSize, 4), 144);
const pad = 1;

// 1. Erase original with a white rectangle
page.drawRectangle({
x: item.pdfX - pad,
y: item.pdfY - pad,
width: Math.max(item.pdfW + pad * 2, 6),
height: fs * 1.25 + pad * 2,
color: rgb(1, 1, 1),
opacity: 1,
});

// 2. Draw replacement text
if (newText.trim()) {
try {
page.drawText(String(newText), {
x: item.pdfX, y: item.pdfY,
size: fs, font,
color: rgb(0, 0, 0),
});
} catch { /* skip un-drawable characters */ }
}
}

const bytes = await pdfDoc.save();
return new Blob([bytes], { type: "application/pdf" });
};
.catch(err => console.error(err))
2 changes: 1 addition & 1 deletion src/utils/formatters.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const formatFileSize = (bytes) => {
if (typeof bytes !== 'number' || isNaN(bytes) || bytes <= 0) return '0 Bytes';
if (typeof bytes !== 'number' || Number.isNaN(bytes) || bytes <= 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
Expand Down