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:
@@ -8,7 +8,7 @@ VITE_CORS_PROXY_SECRET=
|
|||||||
# WASM Module URLs
|
# WASM Module URLs
|
||||||
# Pre-configured defaults enable advanced PDF features out of the box.
|
# Pre-configured defaults enable advanced PDF features out of the box.
|
||||||
# For air-gapped / offline deployments, point these to your internal server (e.g., /wasm/pymupdf/).
|
# For air-gapped / offline deployments, point these to your internal server (e.g., /wasm/pymupdf/).
|
||||||
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/
|
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
|
||||||
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
||||||
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
||||||
|
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ Advanced PDF features (PyMuPDF, Ghostscript, CoherentPDF) are pre-configured to
|
|||||||
The default URLs are set in `.env.production`:
|
The default URLs are set in `.env.production`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/
|
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
|
||||||
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
||||||
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ npx wrangler secret put CPDF_SOURCE -c wasm-wrangler.toml
|
|||||||
|
|
||||||
**Recommended Source URLs:**
|
**Recommended Source URLs:**
|
||||||
|
|
||||||
- PYMUPDF_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/`
|
- PYMUPDF_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`
|
||||||
- GS_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/`
|
- GS_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/`
|
||||||
- CPDF_SOURCE: `https://cdn.jsdelivr.net/npm/coherentpdf/dist/`
|
- CPDF_SOURCE: `https://cdn.jsdelivr.net/npm/coherentpdf/dist/`
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ docker run -d -p 3000:8080 bentopdf:custom
|
|||||||
| ----------------------- | ------------------------------- | -------------------------------------------------------------- |
|
| ----------------------- | ------------------------------- | -------------------------------------------------------------- |
|
||||||
| `SIMPLE_MODE` | Build without LibreOffice tools | `false` |
|
| `SIMPLE_MODE` | Build without LibreOffice tools | `false` |
|
||||||
| `BASE_URL` | Deploy to subdirectory | `/` |
|
| `BASE_URL` | Deploy to subdirectory | `/` |
|
||||||
| `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/` |
|
| `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
|
||||||
| `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/` |
|
| `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/` |
|
||||||
| `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf/dist/` |
|
| `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf/dist/` |
|
||||||
| `VITE_DEFAULT_LANGUAGE` | Default UI language | `en` |
|
| `VITE_DEFAULT_LANGUAGE` | Default UI language | `en` |
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ As of v2.0.0, WASM modules are pre-configured to load from jsDelivr CDN via envi
|
|||||||
These are set in `.env.production` and baked into the build:
|
These are set in `.env.production` and baked into the build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/
|
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
|
||||||
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
|
||||||
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "إضافة علامة مائية",
|
"name": "إضافة علامة مائية",
|
||||||
"subtitle": "ختم نص أو صورة على صفحات PDF الخاصة بك."
|
"subtitle": "ختم نص أو صورة على صفحات PDF الخاصة بك.",
|
||||||
|
"applyToAllPages": "تطبيق على جميع الصفحات"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "رأس وتذييل",
|
"name": "رأس وتذييل",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Дадаць вадзяны знак",
|
"name": "Дадаць вадзяны знак",
|
||||||
"subtitle": "Накласці на старонкі PDF тэкст або відарыс."
|
"subtitle": "Накласці на старонкі PDF тэкст або відарыс.",
|
||||||
|
"applyToAllPages": "Прымяніць да ўсіх старонак"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Верхні і ніжні калантытул",
|
"name": "Верхні і ніжні калантытул",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Tilføj vandmærke",
|
"name": "Tilføj vandmærke",
|
||||||
"subtitle": "Placer tekst eller et billede oven på dine PDF-sider."
|
"subtitle": "Placer tekst eller et billede oven på dine PDF-sider.",
|
||||||
|
"applyToAllPages": "Anvend på alle sider"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Sidehoved og sidefod",
|
"name": "Sidehoved og sidefod",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Wasserzeichen hinzufügen",
|
"name": "Wasserzeichen hinzufügen",
|
||||||
"subtitle": "Text oder ein Bild über Ihre PDF-Seiten stempeln."
|
"subtitle": "Text oder ein Bild über Ihre PDF-Seiten stempeln.",
|
||||||
|
"applyToAllPages": "Auf alle Seiten anwenden"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Kopf- & Fußzeile",
|
"name": "Kopf- & Fußzeile",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Add Watermark",
|
"name": "Add Watermark",
|
||||||
"subtitle": "Stamp text or an image over your PDF pages."
|
"subtitle": "Stamp text or an image over your PDF pages.",
|
||||||
|
"applyToAllPages": "Apply to all pages"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Header & Footer",
|
"name": "Header & Footer",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Agregar Marca de Agua",
|
"name": "Agregar Marca de Agua",
|
||||||
"subtitle": "Estampa texto o una imagen sobre tus páginas PDF."
|
"subtitle": "Estampa texto o una imagen sobre tus páginas PDF.",
|
||||||
|
"applyToAllPages": "Aplicar a todas las páginas"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Encabezado y Pie de Página",
|
"name": "Encabezado y Pie de Página",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Ajouter un filigrane",
|
"name": "Ajouter un filigrane",
|
||||||
"subtitle": "Apposer un texte ou une image sur les pages du PDF."
|
"subtitle": "Apposer un texte ou une image sur les pages du PDF.",
|
||||||
|
"applyToAllPages": "Appliquer à toutes les pages"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "En-tête et pied de page",
|
"name": "En-tête et pied de page",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Tambah Watermark",
|
"name": "Tambah Watermark",
|
||||||
"subtitle": "Cap teks atau gambar di atas halaman PDF Anda."
|
"subtitle": "Cap teks atau gambar di atas halaman PDF Anda.",
|
||||||
|
"applyToAllPages": "Terapkan ke semua halaman"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Header & Footer",
|
"name": "Header & Footer",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Aggiungi Filigrana",
|
"name": "Aggiungi Filigrana",
|
||||||
"subtitle": "Applica testo o un'immagine sulle pagine del tuo PDF."
|
"subtitle": "Applica testo o un'immagine sulle pagine del tuo PDF.",
|
||||||
|
"applyToAllPages": "Applica a tutte le pagine"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Intestazione e Piè di Pagina",
|
"name": "Intestazione e Piè di Pagina",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Watermerk toevoegen",
|
"name": "Watermerk toevoegen",
|
||||||
"subtitle": "Tekst of een afbeelding over de pagina's van je PDF stempelen."
|
"subtitle": "Tekst of een afbeelding over de pagina's van je PDF stempelen.",
|
||||||
|
"applyToAllPages": "Toepassen op alle pagina's"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Koptekst & Voettekst",
|
"name": "Koptekst & Voettekst",
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Adicionar Marca d'Água",
|
"name": "Adicionar Marca d'Água",
|
||||||
"subtitle": "Carimbe texto ou uma imagem sobre as páginas do seu PDF."
|
"subtitle": "Carimbe texto ou uma imagem sobre as páginas do seu PDF.",
|
||||||
|
"applyToAllPages": "Aplicar a todas as páginas"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Cabeçalho e Rodapé",
|
"name": "Cabeçalho e Rodapé",
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Filigran Ekle",
|
"name": "Filigran Ekle",
|
||||||
"subtitle": "PDF sayfalarınızın üzerine metin veya görsel damgası ekleyin."
|
"subtitle": "PDF sayfalarınızın üzerine metin veya görsel damgası ekleyin.",
|
||||||
|
"applyToAllPages": "Tüm sayfalara uygula"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Üst Bilgi & Alt Bilgi",
|
"name": "Üst Bilgi & Alt Bilgi",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "Thêm Watermark",
|
"name": "Thêm Watermark",
|
||||||
"subtitle": "Đóng dấu văn bản hoặc hình ảnh lên các trang PDF của bạn."
|
"subtitle": "Đóng dấu văn bản hoặc hình ảnh lên các trang PDF của bạn.",
|
||||||
|
"applyToAllPages": "Áp dụng cho tất cả các trang"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "Đầu trang & Chân trang",
|
"name": "Đầu trang & Chân trang",
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "添加浮水印",
|
"name": "添加浮水印",
|
||||||
"subtitle": "在你的 PDF 頁面上壓印文字或圖片。"
|
"subtitle": "在你的 PDF 頁面上壓印文字或圖片。",
|
||||||
|
"applyToAllPages": "套用至所有頁面"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "頁首與頁尾",
|
"name": "頁首與頁尾",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
},
|
},
|
||||||
"addWatermark": {
|
"addWatermark": {
|
||||||
"name": "添加水印",
|
"name": "添加水印",
|
||||||
"subtitle": "在您的 PDF 页面上添加文字或图片水印。"
|
"subtitle": "在您的 PDF 页面上添加文字或图片水印。",
|
||||||
|
"applyToAllPages": "应用到所有页面"
|
||||||
},
|
},
|
||||||
"headerFooter": {
|
"headerFooter": {
|
||||||
"name": "页眉和页脚",
|
"name": "页眉和页脚",
|
||||||
|
|||||||
@@ -701,6 +701,33 @@ button:disabled,
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Color picker */
|
||||||
|
input[type='color'] {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid #4b5563;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color']::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color']::-webkit-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color']::-moz-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide spin buttons for number inputs */
|
/* Hide spin buttons for number inputs */
|
||||||
input[type='number'] {
|
input[type='number'] {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const PACKAGE_VERSIONS = {
|
export const PACKAGE_VERSIONS = {
|
||||||
ghostscript: '0.1.1',
|
ghostscript: '0.1.1',
|
||||||
pymupdf: '0.11.14',
|
pymupdf: '0.11.16',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -253,7 +253,7 @@ placeholder="${field.placeholder || ''}" />
|
|||||||
)
|
)
|
||||||
.join('')}
|
.join('')}
|
||||||
</select>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (field.type === 'destination') {
|
} else if (field.type === 'destination') {
|
||||||
|
|||||||
@@ -931,7 +931,7 @@ function showProperties(field: FormField): void {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Text Color</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Alignment</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Border Color</label>
|
<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>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input type="checkbox" id="propHideBorder" ${field.hideBorder ? 'checked' : ''} class="mr-2">
|
<input type="checkbox" id="propHideBorder" ${field.hideBorder ? 'checked' : ''} class="mr-2">
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export interface AddWatermarkState {
|
export interface AddWatermarkState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: PDFLibDocument | 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 };
|
color: { r: number; g: number; b: number };
|
||||||
opacity: number;
|
opacity: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
pageIndices?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addTextWatermark(
|
export async function addTextWatermark(
|
||||||
@@ -194,19 +197,60 @@ export async function addTextWatermark(
|
|||||||
options: TextWatermarkOptions
|
options: TextWatermarkOptions
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
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 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 { width, height } = page.getSize();
|
||||||
const textWidth = font.widthOfTextAtSize(options.text, options.fontSize);
|
const cx = posX * width;
|
||||||
|
const cy = posY * height;
|
||||||
|
|
||||||
page.drawText(options.text, {
|
page.drawImage(image, {
|
||||||
x: (width - textWidth) / 2,
|
x: cx - Math.cos(rad) * halfW + Math.sin(rad) * halfH,
|
||||||
y: height / 2,
|
y: cy - Math.sin(rad) * halfW - Math.cos(rad) * halfH,
|
||||||
font,
|
width: imgWidth,
|
||||||
size: options.fontSize,
|
height: imgHeight,
|
||||||
color: rgb(options.color.r, options.color.g, options.color.b),
|
|
||||||
opacity: options.opacity,
|
opacity: options.opacity,
|
||||||
rotate: degrees(options.angle),
|
rotate: degrees(options.angle),
|
||||||
});
|
});
|
||||||
@@ -221,6 +265,9 @@ export interface ImageWatermarkOptions {
|
|||||||
opacity: number;
|
opacity: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
pageIndices?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addImageWatermark(
|
export async function addImageWatermark(
|
||||||
@@ -233,15 +280,26 @@ export async function addImageWatermark(
|
|||||||
? await pdfDoc.embedPng(options.imageBytes)
|
? await pdfDoc.embedPng(options.imageBytes)
|
||||||
: await pdfDoc.embedJpg(options.imageBytes);
|
: await pdfDoc.embedJpg(options.imageBytes);
|
||||||
const pages = pdfDoc.getPages();
|
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 { width, height } = page.getSize();
|
||||||
const imgWidth = image.width * options.scale;
|
const cx = posX * width;
|
||||||
const imgHeight = image.height * options.scale;
|
const cy = posY * height;
|
||||||
|
|
||||||
page.drawImage(image, {
|
page.drawImage(image, {
|
||||||
x: (width - imgWidth) / 2,
|
x: cx - Math.cos(rad) * halfW + Math.sin(rad) * halfH,
|
||||||
y: (height - imgHeight) / 2,
|
y: cy - Math.sin(rad) * halfW - Math.cos(rad) * halfH,
|
||||||
width: imgWidth,
|
width: imgWidth,
|
||||||
height: imgHeight,
|
height: imgHeight,
|
||||||
opacity: options.opacity,
|
opacity: options.opacity,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface WasmProviderConfig {
|
|||||||
const STORAGE_KEY = 'bentopdf:wasm-providers';
|
const STORAGE_KEY = 'bentopdf:wasm-providers';
|
||||||
|
|
||||||
const CDN_DEFAULTS: Record<WasmPackage, string> = {
|
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/',
|
ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/',
|
||||||
cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf/dist/',
|
cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf/dist/',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { BaseWorkflowNode } from './base-node';
|
|||||||
import { pdfSocket } from '../sockets';
|
import { pdfSocket } from '../sockets';
|
||||||
import type { SocketData } from '../types';
|
import type { SocketData } from '../types';
|
||||||
import { requirePdfInput, processBatch } 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 { PDFDocument } from 'pdf-lib';
|
||||||
import { hexToRgb } from '../../utils/helpers.js';
|
import { hexToRgb } from '../../utils/helpers.js';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
export class WatermarkNode extends BaseWorkflowNode {
|
export class WatermarkNode extends BaseWorkflowNode {
|
||||||
readonly category = 'Edit & Annotate' as const;
|
readonly category = 'Edit & Annotate' as const;
|
||||||
@@ -39,6 +40,18 @@ export class WatermarkNode extends BaseWorkflowNode {
|
|||||||
'angle',
|
'angle',
|
||||||
new ClassicPreset.InputControl('number', { initial: -45 })
|
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(
|
async data(
|
||||||
@@ -67,16 +80,89 @@ export class WatermarkNode extends BaseWorkflowNode {
|
|||||||
const opacity = getNum('opacity', 30) / 100;
|
const opacity = getNum('opacity', 30) / 100;
|
||||||
const angle = getNum('angle', -45);
|
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 {
|
return {
|
||||||
pdf: await processBatch(pdfInputs, async (input) => {
|
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,
|
text: watermarkText,
|
||||||
fontSize,
|
fontSize,
|
||||||
color: { r: c.r, g: c.g, b: c.b },
|
color: { r: c.r, g: c.g, b: c.b },
|
||||||
opacity,
|
opacity,
|
||||||
angle,
|
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);
|
const resultDoc = await PDFDocument.load(resultBytes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -158,171 +158,406 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="options-panel" class="hidden mt-6 space-y-4">
|
<div id="editor-panel" class="hidden bg-gray-900 px-2 sm:px-4 py-4 sm:py-8">
|
||||||
<div class="flex gap-4 mb-4">
|
<div class="max-w-7xl mx-auto">
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<div
|
||||||
<input
|
class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-0 sm:justify-between mb-4 sm:mb-6"
|
||||||
type="radio"
|
>
|
||||||
name="watermark-type"
|
<button
|
||||||
value="text"
|
id="editor-back-btn"
|
||||||
checked
|
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 font-semibold text-sm"
|
||||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500"
|
>
|
||||||
/>
|
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
||||||
<span class="text-sm font-medium text-gray-300"
|
<span>Change File</span>
|
||||||
>Text Watermark</span
|
</button>
|
||||||
|
<div
|
||||||
|
id="editor-file-info"
|
||||||
|
class="text-xs sm:text-sm text-gray-400 truncate max-w-full sm:max-w-[60%]"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4 sm:gap-6">
|
||||||
|
<div class="flex-1 min-w-0 flex flex-col">
|
||||||
|
<div
|
||||||
|
class="bg-gray-800 rounded-xl border border-gray-700 p-2 sm:p-4 flex-1 flex flex-col"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2 sm:mb-3">
|
||||||
|
<span class="text-xs sm:text-sm font-medium text-gray-300"
|
||||||
|
>Preview</span
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
id="prev-page-btn"
|
||||||
|
disabled
|
||||||
|
class="p-1 rounded hover:bg-gray-700 text-gray-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<i data-lucide="chevron-left" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-1 text-xs text-gray-300">
|
||||||
|
<input
|
||||||
|
id="page-num-input"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value="1"
|
||||||
|
class="w-10 bg-gray-700 border border-gray-600 text-white rounded text-center text-xs py-0.5"
|
||||||
|
/>
|
||||||
|
<span>/ <span id="total-pages">1</span></span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="next-page-btn"
|
||||||
|
disabled
|
||||||
|
class="p-1 rounded hover:bg-gray-700 text-gray-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500 hidden sm:inline"
|
||||||
|
>Drag watermark to position</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="preview-wrapper"
|
||||||
|
class="flex-1 min-h-[200px] lg:min-h-0 flex items-center justify-center bg-gray-900 rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
</label>
|
<div
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
id="preview-container"
|
||||||
<input
|
class="relative overflow-hidden select-none"
|
||||||
type="radio"
|
style="touch-action: none"
|
||||||
name="watermark-type"
|
>
|
||||||
value="image"
|
<canvas id="preview-canvas"></canvas>
|
||||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500"
|
<div
|
||||||
/>
|
id="watermark-box"
|
||||||
<span class="text-sm font-medium text-gray-300"
|
class="absolute pointer-events-auto"
|
||||||
>Image Watermark</span
|
style="
|
||||||
>
|
left: 50%;
|
||||||
</label>
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative border border-dashed border-indigo-400/60 cursor-grab active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<div id="watermark-overlay" class="whitespace-nowrap">
|
||||||
|
CONFIDENTIAL
|
||||||
|
</div>
|
||||||
|
<img id="image-watermark-overlay" class="hidden" alt="" />
|
||||||
|
<div
|
||||||
|
data-handle="nw"
|
||||||
|
class="resize-handle absolute -top-1.5 -left-1.5 w-3 h-3 bg-white border-2 border-indigo-500 rounded-sm cursor-nw-resize pointer-events-auto z-10"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
data-handle="ne"
|
||||||
|
class="resize-handle absolute -top-1.5 -right-1.5 w-3 h-3 bg-white border-2 border-indigo-500 rounded-sm cursor-ne-resize pointer-events-auto z-10"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
data-handle="sw"
|
||||||
|
class="resize-handle absolute -bottom-1.5 -left-1.5 w-3 h-3 bg-white border-2 border-indigo-500 rounded-sm cursor-sw-resize pointer-events-auto z-10"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
data-handle="se"
|
||||||
|
class="resize-handle absolute -bottom-1.5 -right-1.5 w-3 h-3 bg-white border-2 border-indigo-500 rounded-sm cursor-se-resize pointer-events-auto z-10"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="text-watermark-options">
|
<div class="w-full lg:w-80 flex-shrink-0">
|
||||||
<div class="space-y-4">
|
<div
|
||||||
<div>
|
class="bg-gray-800 rounded-xl border border-gray-700 p-3 sm:p-5 space-y-3 sm:space-y-5"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
id="type-text-btn"
|
||||||
|
class="flex-1 py-2 px-3 text-sm font-medium rounded-lg bg-indigo-600 text-white transition-colors"
|
||||||
|
>
|
||||||
|
Text
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="type-image-btn"
|
||||||
|
class="flex-1 py-2 px-3 text-sm font-medium rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 bg-gray-900 rounded-lg px-3 py-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="apply-all-pages"
|
||||||
|
checked
|
||||||
|
class="w-4 h-4 accent-indigo-500 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
<label
|
<label
|
||||||
for="watermark-text"
|
for="apply-all-pages"
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="text-sm text-gray-300 cursor-pointer"
|
||||||
>Watermark Text</label
|
data-i18n="tools:addWatermark.applyToAllPages"
|
||||||
|
>Apply to all pages</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="page-range-section" class="space-y-1.5">
|
||||||
|
<label
|
||||||
|
for="page-range-input"
|
||||||
|
class="block text-sm font-medium text-gray-300"
|
||||||
|
>Page Range</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="watermark-text"
|
id="page-range-input"
|
||||||
placeholder="CONFIDENTIAL"
|
value="all"
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5"
|
placeholder="all or 1-3, 5, 7-9"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm placeholder-gray-500"
|
||||||
/>
|
/>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Use "all" or specify pages, e.g. 1-3, 5, 7-9
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="text-watermark-options">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="watermark-text"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>Text</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="watermark-text"
|
||||||
|
placeholder="CONFIDENTIAL"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="font-size"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>Font Size</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="font-size"
|
||||||
|
value="72"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="text-color"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>Color</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="text-color"
|
||||||
|
value="#888888"
|
||||||
|
class=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="opacity-text"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>
|
||||||
|
Opacity: <span id="opacity-value-text">0.3</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="opacity-text"
|
||||||
|
min="0.05"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value="0.3"
|
||||||
|
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer accent-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="angle-text"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>
|
||||||
|
Angle: <span id="angle-value-text">-45</span>°
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="angle-text"
|
||||||
|
min="-90"
|
||||||
|
max="90"
|
||||||
|
step="1"
|
||||||
|
value="-45"
|
||||||
|
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer accent-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="image-watermark-options" class="hidden">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="image-watermark-input"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>Watermark Image</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="image-watermark-input"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-indigo-600 file:text-white hover:file:bg-indigo-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="image-scale"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>
|
||||||
|
Scale: <span id="image-scale-value">100</span>%
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="image-scale"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
step="5"
|
||||||
|
value="100"
|
||||||
|
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer accent-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="opacity-image"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>
|
||||||
|
Opacity: <span id="opacity-value-image">0.3</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="opacity-image"
|
||||||
|
min="0.05"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value="0.3"
|
||||||
|
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer accent-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="angle-image"
|
||||||
|
class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
|
>
|
||||||
|
Angle: <span id="angle-value-image">0</span>°
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="angle-image"
|
||||||
|
min="-90"
|
||||||
|
max="90"
|
||||||
|
step="1"
|
||||||
|
value="0"
|
||||||
|
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer accent-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="font-size"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Font Size</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="font-size"
|
|
||||||
value="72"
|
|
||||||
min="10"
|
|
||||||
max="200"
|
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="text-color"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Color</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
id="text-color"
|
|
||||||
value="#888888"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="opacity-text"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Opacity: <span id="opacity-value-text">0.3</span></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="opacity-text"
|
|
||||||
min="0.1"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value="0.3"
|
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="angle-text"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Angle: <span id="angle-value-text">-45</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="angle-text"
|
|
||||||
min="-90"
|
|
||||||
max="90"
|
|
||||||
step="5"
|
|
||||||
value="-45"
|
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="image-watermark-options" class="hidden">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label class="block mb-1.5 text-sm font-medium text-gray-300"
|
||||||
for="image-watermark-input"
|
>Position</label
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Watermark Image (PNG/JPG)</label
|
|
||||||
>
|
>
|
||||||
|
<div class="grid grid-cols-3 gap-1.5">
|
||||||
|
<button
|
||||||
|
data-pos="0.15,0.15"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Top Left
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.5,0.15"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Top
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.85,0.15"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Top Right
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.15,0.5"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Left
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.5,0.5"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-indigo-600 hover:bg-indigo-500 text-white rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Center
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.85,0.5"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Right
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.15,0.85"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Bottom Left
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.5,0.85"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Bottom
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-pos="0.85,0.85"
|
||||||
|
class="pos-preset-btn py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Bottom Right
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-3 bg-gray-900 rounded-lg p-3">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="checkbox"
|
||||||
id="image-watermark-input"
|
id="flatten-watermark"
|
||||||
accept="image/png, image/jpeg"
|
class="mt-0.5 w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500"
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-600 file:text-white hover:file:bg-indigo-700"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
for="opacity-image"
|
for="flatten-watermark"
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="text-sm font-medium text-gray-300 cursor-pointer"
|
||||||
>Opacity: <span id="opacity-value-image">0.3</span></label
|
>Flatten watermark</label
|
||||||
>
|
>
|
||||||
<input
|
<p class="text-xs text-gray-500 mt-0.5">
|
||||||
type="range"
|
Bakes the watermark into page pixels, making it
|
||||||
id="opacity-image"
|
tamper-resistant. Text will no longer be selectable.
|
||||||
min="0.1"
|
</p>
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value="0.3"
|
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="angle-image"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
|
||||||
>Angle: <span id="angle-value-image">0</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="angle-image"
|
|
||||||
min="-90"
|
|
||||||
max="90"
|
|
||||||
step="5"
|
|
||||||
value="0"
|
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button id="process-btn" class="btn-gradient w-full">
|
||||||
|
Add Watermark
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
|
||||||
Add Watermark
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -182,12 +182,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Background Color</label
|
>Background Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="background-color" value="#FFFFCC" />
|
||||||
type="color"
|
|
||||||
id="background-color"
|
|
||||||
value="#FFFFCC"
|
|
||||||
class="w-full h-12 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
<button id="process-btn" class="btn-gradient w-full mt-4">
|
||||||
Change Background Color
|
Change Background Color
|
||||||
|
|||||||
@@ -320,12 +320,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Color</label
|
>Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="text-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="text-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -217,12 +217,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Background Color</label
|
>Background Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="background-color" value="#FFFFFF" />
|
||||||
type="color"
|
|
||||||
id="background-color"
|
|
||||||
value="#FFFFFF"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
@@ -261,12 +256,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Separator Color</label
|
>Separator Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="separator-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="separator-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -477,12 +477,7 @@
|
|||||||
class="block text-sm font-medium text-gray-300 mb-2"
|
class="block text-sm font-medium text-gray-300 mb-2"
|
||||||
>Text Color</label
|
>Text Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="sig-text-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="sig-text-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -296,12 +296,7 @@
|
|||||||
>Background Color</label
|
>Background Color</label
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<input
|
<input type="color" id="background-color" value="#ffffff" />
|
||||||
type="color"
|
|
||||||
id="background-color"
|
|
||||||
value="#ffffff"
|
|
||||||
class="h-10 w-20 rounded border border-gray-600 bg-gray-700 cursor-pointer"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-400"
|
<span class="text-sm text-gray-400"
|
||||||
>Color for margins/padding</span
|
>Color for margins/padding</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -202,12 +202,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Font Color</label
|
>Font Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="font-color" value="#000000" class="" />
|
||||||
type="color"
|
|
||||||
id="font-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -253,12 +253,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Border Color</label
|
>Border Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="border-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="border-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -219,12 +219,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Color</label
|
>Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="text-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="text-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-10 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -169,12 +169,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>New Text Color</label
|
>New Text Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="text-color-input" value="#0000FF" />
|
||||||
type="color"
|
|
||||||
id="text-color-input"
|
|
||||||
value="#0000FF"
|
|
||||||
class="w-full h-12 bg-gray-700 border border-gray-600 rounded-lg cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
<button id="process-btn" class="btn-gradient w-full mt-4">
|
||||||
Change Text Color
|
Change Text Color
|
||||||
|
|||||||
@@ -250,12 +250,7 @@
|
|||||||
class="block mb-2 text-sm font-medium text-gray-300"
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
>Text Color</label
|
>Text Color</label
|
||||||
>
|
>
|
||||||
<input
|
<input type="color" id="text-color" value="#000000" />
|
||||||
type="color"
|
|
||||||
id="text-color"
|
|
||||||
value="#000000"
|
|
||||||
class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -144,10 +144,10 @@
|
|||||||
<span class="text-xs text-gray-500">Recommended:</span>
|
<span class="text-xs text-gray-500">Recommended:</span>
|
||||||
<code
|
<code
|
||||||
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||||
>https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/</code
|
>https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/</code
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/"
|
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/"
|
||||||
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user