mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-13 09:54:19 +01:00
Refactor color input fields and enhance watermark functionality
- Updated color input fields in various HTML pages to remove unnecessary classes for improved styling consistency. - Modified the watermark node to include options for positioning and flattening watermarks. - Enhanced the addTextWatermark function to support customizable positioning and page selection for watermarks. - Added new controls for text and image watermarks in the UI, allowing users to specify text, font size, color, opacity, angle, and image scaling. - Updated the WASM provider to use the latest version of pymupdf-wasm.
This commit is contained in:
1 parent
2e64e8e8d4
commit
88260c26ab
41 files changed
+1512
-383
No files matched your search
@@ -1,4 +1,4 @@
|
||||
export const PACKAGE_VERSIONS = {
|
||||
ghostscript: '0.1.1',
|
||||
pymupdf: '0.11.14',
|
||||
pymupdf: '0.11.16',
|
||||
} as const;
|
||||
+887
-128
File diff suppressed because it is too large.
Load diff
@@ -253,7 +253,7 @@ placeholder="${field.placeholder || ''}" />
|
||||
)
|
||||
.join('')}
|
||||
</select>
|
||||
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''}
|
||||
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden mt-2" value="#000000" />' : ''}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'destination') {
|
||||
|
||||
@@ -931,7 +931,7 @@ function showProperties(field: FormField): void {
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Text Color</label>
|
||||
<input type="color" id="propTextColor" value="${field.textColor}" class="w-full border border-gray-500 rounded px-2 py-1 h-10">
|
||||
<input type="color" id="propTextColor" value="${field.textColor}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Alignment</label>
|
||||
@@ -1169,7 +1169,7 @@ function showProperties(field: FormField): void {
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Border Color</label>
|
||||
<input type="color" id="propBorderColor" value="${field.borderColor || '#000000'}" class="w-full border border-gray-500 rounded px-2 py-1 h-10">
|
||||
<input type="color" id="propBorderColor" value="${field.borderColor || '#000000'}">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="propHideBorder" ${field.hideBorder ? 'checked' : ''} class="mr-2">
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export interface AddWatermarkState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
pdfBytes: Uint8Array | null;
|
||||
previewCanvas: HTMLCanvasElement | null;
|
||||
watermarkX: number; // 0–1, percentage from left
|
||||
watermarkY: number; // 0–1, percentage from top (flipped to bottom for PDF)
|
||||
}
|
||||
@@ -187,6 +187,9 @@ export interface TextWatermarkOptions {
|
||||
color: { r: number; g: number; b: number };
|
||||
opacity: number;
|
||||
angle: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
pageIndices?: number[];
|
||||
}
|
||||
|
||||
export async function addTextWatermark(
|
||||
@@ -194,19 +197,60 @@ export async function addTextWatermark(
|
||||
options: TextWatermarkOptions
|
||||
): Promise<Uint8Array> {
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to create canvas context');
|
||||
|
||||
const dpr = 2;
|
||||
const colorR = Math.round(options.color.r * 255);
|
||||
const colorG = Math.round(options.color.g * 255);
|
||||
const colorB = Math.round(options.color.b * 255);
|
||||
const fontStr = `bold ${options.fontSize * dpr}px "Noto Sans SC", "Noto Sans JP", "Noto Sans KR", "Noto Sans Arabic", Arial, sans-serif`;
|
||||
|
||||
ctx.font = fontStr;
|
||||
const metrics = ctx.measureText(options.text);
|
||||
|
||||
canvas.width = Math.ceil(metrics.width) + 4;
|
||||
canvas.height = Math.ceil(options.fontSize * dpr * 1.4);
|
||||
|
||||
ctx.font = fontStr;
|
||||
ctx.fillStyle = `rgb(${colorR}, ${colorG}, ${colorB})`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(options.text, 2, canvas.height / 2);
|
||||
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),
|
||||
'image/png'
|
||||
);
|
||||
});
|
||||
const imageBytes = new Uint8Array(await blob.arrayBuffer());
|
||||
|
||||
const image = await pdfDoc.embedPng(imageBytes);
|
||||
const pages = pdfDoc.getPages();
|
||||
const posX = options.x ?? 0.5;
|
||||
const posY = options.y ?? 0.5;
|
||||
const imgWidth = image.width / dpr;
|
||||
const imgHeight = image.height / dpr;
|
||||
|
||||
for (const page of pages) {
|
||||
const rad = (options.angle * Math.PI) / 180;
|
||||
const halfW = imgWidth / 2;
|
||||
const halfH = imgHeight / 2;
|
||||
|
||||
const targetIndices = options.pageIndices ?? pages.map((_, i) => i);
|
||||
for (const idx of targetIndices) {
|
||||
const page = pages[idx];
|
||||
if (!page) continue;
|
||||
const { width, height } = page.getSize();
|
||||
const textWidth = font.widthOfTextAtSize(options.text, options.fontSize);
|
||||
const cx = posX * width;
|
||||
const cy = posY * height;
|
||||
|
||||
page.drawText(options.text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font,
|
||||
size: options.fontSize,
|
||||
color: rgb(options.color.r, options.color.g, options.color.b),
|
||||
page.drawImage(image, {
|
||||
x: cx - Math.cos(rad) * halfW + Math.sin(rad) * halfH,
|
||||
y: cy - Math.sin(rad) * halfW - Math.cos(rad) * halfH,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: options.opacity,
|
||||
rotate: degrees(options.angle),
|
||||
});
|
||||
@@ -221,6 +265,9 @@ export interface ImageWatermarkOptions {
|
||||
opacity: number;
|
||||
angle: number;
|
||||
scale: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
pageIndices?: number[];
|
||||
}
|
||||
|
||||
export async function addImageWatermark(
|
||||
@@ -233,15 +280,26 @@ export async function addImageWatermark(
|
||||
? await pdfDoc.embedPng(options.imageBytes)
|
||||
: await pdfDoc.embedJpg(options.imageBytes);
|
||||
const pages = pdfDoc.getPages();
|
||||
const posX = options.x ?? 0.5;
|
||||
const posY = options.y ?? 0.5;
|
||||
|
||||
for (const page of pages) {
|
||||
const imgWidth = image.width * options.scale;
|
||||
const imgHeight = image.height * options.scale;
|
||||
const rad = (options.angle * Math.PI) / 180;
|
||||
const halfW = imgWidth / 2;
|
||||
const halfH = imgHeight / 2;
|
||||
|
||||
const targetIndices = options.pageIndices ?? pages.map((_, i) => i);
|
||||
for (const idx of targetIndices) {
|
||||
const page = pages[idx];
|
||||
if (!page) continue;
|
||||
const { width, height } = page.getSize();
|
||||
const imgWidth = image.width * options.scale;
|
||||
const imgHeight = image.height * options.scale;
|
||||
const cx = posX * width;
|
||||
const cy = posY * height;
|
||||
|
||||
page.drawImage(image, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
x: cx - Math.cos(rad) * halfW + Math.sin(rad) * halfH,
|
||||
y: cy - Math.sin(rad) * halfW - Math.cos(rad) * halfH,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: options.opacity,
|
||||
|
||||
@@ -9,7 +9,7 @@ interface WasmProviderConfig {
|
||||
const STORAGE_KEY = 'bentopdf:wasm-providers';
|
||||
|
||||
const CDN_DEFAULTS: Record<WasmPackage, string> = {
|
||||
pymupdf: 'https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/',
|
||||
pymupdf: 'https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/',
|
||||
ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/',
|
||||
cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf/dist/',
|
||||
};
|
||||
|
||||
@@ -3,9 +3,10 @@ import { BaseWorkflowNode } from './base-node';
|
||||
import { pdfSocket } from '../sockets';
|
||||
import type { SocketData } from '../types';
|
||||
import { requirePdfInput, processBatch } from '../types';
|
||||
import { addTextWatermark } from '../../utils/pdf-operations';
|
||||
import { addTextWatermark, parsePageRange } from '../../utils/pdf-operations';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { hexToRgb } from '../../utils/helpers.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export class WatermarkNode extends BaseWorkflowNode {
|
||||
readonly category = 'Edit & Annotate' as const;
|
||||
@@ -39,6 +40,18 @@ export class WatermarkNode extends BaseWorkflowNode {
|
||||
'angle',
|
||||
new ClassicPreset.InputControl('number', { initial: -45 })
|
||||
);
|
||||
this.addControl(
|
||||
'position',
|
||||
new ClassicPreset.InputControl('text', { initial: 'center' })
|
||||
);
|
||||
this.addControl(
|
||||
'pages',
|
||||
new ClassicPreset.InputControl('text', { initial: 'all' })
|
||||
);
|
||||
this.addControl(
|
||||
'flatten',
|
||||
new ClassicPreset.InputControl('text', { initial: 'no' })
|
||||
);
|
||||
}
|
||||
|
||||
async data(
|
||||
@@ -67,16 +80,89 @@ export class WatermarkNode extends BaseWorkflowNode {
|
||||
const opacity = getNum('opacity', 30) / 100;
|
||||
const angle = getNum('angle', -45);
|
||||
|
||||
const positionPresets: Record<string, { x: number; y: number }> = {
|
||||
'top-left': { x: 0.15, y: 0.15 },
|
||||
top: { x: 0.5, y: 0.15 },
|
||||
'top-right': { x: 0.85, y: 0.15 },
|
||||
left: { x: 0.15, y: 0.5 },
|
||||
center: { x: 0.5, y: 0.5 },
|
||||
right: { x: 0.85, y: 0.5 },
|
||||
'bottom-left': { x: 0.15, y: 0.85 },
|
||||
bottom: { x: 0.5, y: 0.85 },
|
||||
'bottom-right': { x: 0.85, y: 0.85 },
|
||||
};
|
||||
const posKey = getText('position', 'center').trim().toLowerCase();
|
||||
const { x, y } = positionPresets[posKey] ?? positionPresets['center'];
|
||||
|
||||
const pagesStr = getText('pages', 'all').trim().toLowerCase();
|
||||
const shouldFlatten =
|
||||
getText('flatten', 'no').trim().toLowerCase() === 'yes';
|
||||
|
||||
return {
|
||||
pdf: await processBatch(pdfInputs, async (input) => {
|
||||
const resultBytes = await addTextWatermark(input.bytes, {
|
||||
const srcDoc = await PDFDocument.load(input.bytes);
|
||||
const totalPages = srcDoc.getPageCount();
|
||||
|
||||
const pageIndices =
|
||||
pagesStr === 'all' ? undefined : parsePageRange(pagesStr, totalPages);
|
||||
|
||||
let resultBytes = await addTextWatermark(input.bytes, {
|
||||
text: watermarkText,
|
||||
fontSize,
|
||||
color: { r: c.r, g: c.g, b: c.b },
|
||||
opacity,
|
||||
angle,
|
||||
x,
|
||||
y: 1 - y,
|
||||
pageIndices,
|
||||
});
|
||||
|
||||
if (shouldFlatten) {
|
||||
const watermarkedPdf = await pdfjsLib.getDocument({
|
||||
data: resultBytes.slice(),
|
||||
}).promise;
|
||||
const flattenedDoc = await PDFDocument.create();
|
||||
const renderScale = 2.5;
|
||||
|
||||
for (let i = 1; i <= watermarkedPdf.numPages; i++) {
|
||||
const page = await watermarkedPdf.getPage(i);
|
||||
const unscaledVP = page.getViewport({ scale: 1 });
|
||||
const viewport = page.getViewport({ scale: renderScale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
await page.render({ canvasContext: ctx, canvas, viewport }).promise;
|
||||
|
||||
const jpegBytes = await new Promise<ArrayBuffer>(
|
||||
(resolve, reject) =>
|
||||
canvas.toBlob(
|
||||
(blob) =>
|
||||
blob
|
||||
? blob.arrayBuffer().then(resolve)
|
||||
: reject(new Error(`Failed to rasterize page ${i}`)),
|
||||
'image/jpeg',
|
||||
0.92
|
||||
)
|
||||
);
|
||||
|
||||
const image = await flattenedDoc.embedJpg(jpegBytes);
|
||||
const newPage = flattenedDoc.addPage([
|
||||
unscaledVP.width,
|
||||
unscaledVP.height,
|
||||
]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: unscaledVP.width,
|
||||
height: unscaledVP.height,
|
||||
});
|
||||
}
|
||||
|
||||
resultBytes = new Uint8Array(await flattenedDoc.save());
|
||||
}
|
||||
|
||||
const resultDoc = await PDFDocument.load(resultBytes);
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user