feat: Add VitePress docs, EPUB to PDF tool, Phosphor icons, and licensing updates

- Set up VitePress documentation site (docs:dev, docs:build, docs:preview)
- Added Getting Started, Tools Reference, Contributing, and Commercial License pages
- Created self-hosting guides for Docker, Vercel, Netlify, Cloudflare, AWS, Hostinger, Nginx, Apache
- Updated README with documentation link, sponsors section, and docs contribution guide

- Added EPUB to PDF converter using LibreOffice WASM

- Migrated to Phosphor Icons for consistent iconography

- Added donation ribbon banner on landing page
- Removed 'Like My Work?' section (replaced by ribbon)
- Updated licensing.html with delivery model, AGPL notice, invoicing, and no-refund policy

- Added Commercial License documentation page
- Updated translations table (Chinese added, marked non-English as In Progress)

- Added sponsors.yml workflow for auto-generating sponsor avatars
This commit is contained in:
abdullahalam123
2025-12-27 19:30:31 +05:30
parent 0e888743d3
commit f30a084fce
189 changed files with 59872 additions and 3300 deletions

View File

@@ -0,0 +1,517 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
interface BookletState {
file: File | null;
pdfDoc: PDFLibDocument | null;
pdfBytes: Uint8Array | null;
pdfjsDoc: pdfjsLib.PDFDocumentProxy | null;
}
const pageState: BookletState = {
file: null,
pdfDoc: null,
pdfBytes: null,
pdfjsDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.pdfBytes = null;
pageState.pdfjsDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const previewArea = document.getElementById('booklet-preview');
if (previewArea) previewArea.innerHTML = '<p class="text-gray-400 text-center py-8">Upload a PDF and click "Generate Preview" to see the booklet layout</p>';
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
if (downloadBtn) downloadBtn.disabled = true;
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfBytes = new Uint8Array(arrayBuffer);
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.pdfjsDoc = await pdfjsLib.getDocument({ data: pageState.pdfBytes.slice() }).promise;
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
const previewBtn = document.getElementById('preview-btn') as HTMLButtonElement;
if (previewBtn) previewBtn.disabled = false;
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function getGridDimensions(): { rows: number; cols: number } {
const gridMode = (document.querySelector('input[name="grid-mode"]:checked') as HTMLInputElement)?.value || '1x2';
switch (gridMode) {
case '1x2': return { rows: 1, cols: 2 };
case '2x2': return { rows: 2, cols: 2 };
case '2x4': return { rows: 2, cols: 4 };
case '4x4': return { rows: 4, cols: 4 };
default: return { rows: 1, cols: 2 };
}
}
function getOrientation(isBookletMode: boolean): 'portrait' | 'landscape' {
const orientationValue = (document.querySelector('input[name="orientation"]:checked') as HTMLInputElement)?.value || 'auto';
if (orientationValue === 'portrait') return 'portrait';
if (orientationValue === 'landscape') return 'landscape';
return isBookletMode ? 'landscape' : 'portrait';
}
function getSheetDimensions(isBookletMode: boolean): { width: number; height: number } {
const paperSizeKey = (document.getElementById('paper-size') as HTMLSelectElement).value as keyof typeof PageSizes;
const pageDims = PageSizes[paperSizeKey] || PageSizes.Letter;
const orientation = getOrientation(isBookletMode);
if (orientation === 'landscape') {
return { width: pageDims[1], height: pageDims[0] };
}
return { width: pageDims[0], height: pageDims[1] };
}
async function generatePreview() {
if (!pageState.pdfDoc || !pageState.pdfjsDoc) {
showAlert('Error', 'Please load a PDF first.');
return;
}
const previewArea = document.getElementById('booklet-preview')!;
const totalPages = pageState.pdfDoc.getPageCount();
const { rows, cols } = getGridDimensions();
const pagesPerSheet = rows * cols;
const isBookletMode = rows === 1 && cols === 2;
let numSheets: number;
if (isBookletMode) {
const sheetsNeeded = Math.ceil(totalPages / 4);
numSheets = sheetsNeeded * 2;
} else {
numSheets = Math.ceil(totalPages / pagesPerSheet);
}
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
// Get container width to make canvas fill it
const previewContainer = document.getElementById('booklet-preview')!;
const containerWidth = previewContainer.clientWidth - 32; // account for padding
const aspectRatio = sheetWidth / sheetHeight;
const canvasWidth = containerWidth;
const canvasHeight = containerWidth / aspectRatio;
previewArea.innerHTML = '<p class="text-gray-400 text-center py-4">Generating preview...</p>';
const totalRounded = isBookletMode ? Math.ceil(totalPages / 4) * 4 : totalPages;
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
const pageThumbnails: Map<number, ImageBitmap> = new Map();
const thumbnailScale = 0.3;
for (let i = 1; i <= totalPages; i++) {
try {
const page = await pageState.pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: thumbnailScale });
const offscreen = new OffscreenCanvas(viewport.width, viewport.height);
const ctx = offscreen.getContext('2d')!;
await page.render({
canvasContext: ctx as any,
viewport: viewport,
canvas: offscreen as any,
}).promise;
const bitmap = await createImageBitmap(offscreen);
pageThumbnails.set(i, bitmap);
} catch (e) {
console.error(`Failed to render page ${i}:`, e);
}
}
previewArea.innerHTML = `<p class="text-indigo-400 text-sm mb-4 text-center">${totalPages} pages → ${numSheets} output sheets</p>`;
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvas.className = 'border border-gray-600 rounded-lg mb-4';
const ctx = canvas.getContext('2d')!;
const isFront = sheetIndex % 2 === 0;
ctx.fillStyle = isFront ? '#1f2937' : '#1a2e1a';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = '#4b5563';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, canvasWidth, canvasHeight);
const cellWidth = canvasWidth / cols;
const cellHeight = canvasHeight / rows;
const padding = 4;
ctx.strokeStyle = '#374151';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
for (let c = 1; c < cols; c++) {
ctx.beginPath();
ctx.moveTo(c * cellWidth, 0);
ctx.lineTo(c * cellWidth, canvasHeight);
ctx.stroke();
}
for (let r = 1; r < rows; r++) {
ctx.beginPath();
ctx.moveTo(0, r * cellHeight);
ctx.lineTo(canvasWidth, r * cellHeight);
ctx.stroke();
}
ctx.setLineDash([]);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const slotIndex = r * cols + c;
let pageNumber: number;
if (isBookletMode) {
const physicalSheet = Math.floor(sheetIndex / 2);
const isFrontSide = sheetIndex % 2 === 0;
if (isFrontSide) {
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
} else {
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
}
} else {
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
}
const x = c * cellWidth + padding;
const y = r * cellHeight + padding;
const slotWidth = cellWidth - padding * 2;
const slotHeight = cellHeight - padding * 2;
const exists = pageNumber >= 1 && pageNumber <= totalPages;
if (exists) {
const thumbnail = pageThumbnails.get(pageNumber);
if (thumbnail) {
let rotation = 0;
if (rotationMode === '90cw') rotation = 90;
else if (rotationMode === '90ccw') rotation = -90;
else if (rotationMode === 'alternate') rotation = (pageNumber % 2 === 1) ? 90 : -90;
const isRotated = rotation !== 0;
const srcWidth = isRotated ? thumbnail.height : thumbnail.width;
const srcHeight = isRotated ? thumbnail.width : thumbnail.height;
const scale = Math.min(slotWidth / srcWidth, slotHeight / srcHeight);
const drawWidth = srcWidth * scale;
const drawHeight = srcHeight * scale;
const drawX = x + (slotWidth - drawWidth) / 2;
const drawY = y + (slotHeight - drawHeight) / 2;
ctx.save();
if (rotation !== 0) {
const centerX = drawX + drawWidth / 2;
const centerY = drawY + drawHeight / 2;
ctx.translate(centerX, centerY);
ctx.rotate((rotation * Math.PI) / 180);
ctx.drawImage(thumbnail, -drawHeight / 2, -drawWidth / 2, drawHeight, drawWidth);
} else {
ctx.drawImage(thumbnail, drawX, drawY, drawWidth, drawHeight);
}
ctx.restore();
ctx.strokeStyle = '#6b7280';
ctx.lineWidth = 1;
ctx.strokeRect(drawX, drawY, drawWidth, drawHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${pageNumber}`, x + slotWidth / 2, y + slotHeight - 4);
}
} else {
ctx.fillStyle = '#374151';
ctx.fillRect(x, y, slotWidth, slotHeight);
ctx.strokeStyle = '#4b5563';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, slotWidth, slotHeight);
ctx.fillStyle = '#6b7280';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('(blank)', x + slotWidth / 2, y + slotHeight / 2);
}
}
}
ctx.fillStyle = '#9ca3af';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : '';
ctx.fillText(`Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`, canvasWidth - 6, 4);
previewArea.appendChild(canvas);
}
pageThumbnails.forEach(bitmap => bitmap.close());
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
downloadBtn.disabled = false;
}
function applyRotation(doc: PDFLibDocument, mode: string) {
const pages = doc.getPages();
pages.forEach((page, index) => {
let rotation = 0;
switch (mode) {
case '90cw': rotation = 90; break;
case '90ccw': rotation = -90; break;
case 'alternate': rotation = (index % 2 === 0) ? 90 : -90; break;
default: rotation = 0;
}
if (rotation !== 0) {
page.setRotation(degrees(page.getRotation().angle + rotation));
}
});
}
async function createBooklet() {
if (!pageState.pdfBytes) {
showAlert('Error', 'Please load a PDF first.');
return;
}
showLoader('Creating Booklet...');
try {
const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice());
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
applyRotation(sourceDoc, rotationMode);
const totalPages = sourceDoc.getPageCount();
const { rows, cols } = getGridDimensions();
const pagesPerSheet = rows * cols;
const isBookletMode = rows === 1 && cols === 2;
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
const outputDoc = await PDFLibDocument.create();
let numSheets: number;
let totalRounded: number;
if (isBookletMode) {
totalRounded = Math.ceil(totalPages / 4) * 4;
numSheets = Math.ceil(totalPages / 4) * 2;
} else {
totalRounded = totalPages;
numSheets = Math.ceil(totalPages / pagesPerSheet);
}
const cellWidth = sheetWidth / cols;
const cellHeight = sheetHeight / rows;
const padding = 10;
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
const outputPage = outputDoc.addPage([sheetWidth, sheetHeight]);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const slotIndex = r * cols + c;
let pageNumber: number;
if (isBookletMode) {
const physicalSheet = Math.floor(sheetIndex / 2);
const isFrontSide = sheetIndex % 2 === 0;
if (isFrontSide) {
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
} else {
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
}
} else {
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
}
if (pageNumber >= 1 && pageNumber <= totalPages) {
const [embeddedPage] = await outputDoc.embedPdf(sourceDoc, [pageNumber - 1]);
const { width: srcW, height: srcH } = embeddedPage;
const availableWidth = cellWidth - padding * 2;
const availableHeight = cellHeight - padding * 2;
const scale = Math.min(availableWidth / srcW, availableHeight / srcH);
const scaledWidth = srcW * scale;
const scaledHeight = srcH * scale;
const x = c * cellWidth + padding + (availableWidth - scaledWidth) / 2;
const y = sheetHeight - (r + 1) * cellHeight + padding + (availableHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
}
const pdfBytes = await outputDoc.save();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
`${originalName}_booklet.pdf`
);
showAlert('Success', `Booklet created with ${numSheets} sheets!`, 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the booklet.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const previewBtn = document.getElementById('preview-btn');
const downloadBtn = document.getElementById('download-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (previewBtn) {
previewBtn.addEventListener('click', generatePreview);
}
if (downloadBtn) {
downloadBtn.addEventListener('click', createBooklet);
}
});