feat: add adjust colors and scanner effect pages with corresponding types and configurations

This commit is contained in:
alam00000
2026-02-01 12:21:14 +05:30
parent a929524f09
commit 325519b9f7
24 changed files with 2845 additions and 23962 deletions

3
.gitignore vendored
View File

@@ -30,6 +30,9 @@ dist-ssr
coverage/ coverage/
*.lcov *.lcov
# Generated sitemap
public/sitemap.xml
#backup #backup
.seo-backup .seo-backup
libreoffice-wasm-package libreoffice-wasm-package

View File

@@ -98,6 +98,37 @@
"name": "Інвертаваць колеры", "name": "Інвертаваць колеры",
"subtitle": "Стварыць версію PDF у \"цёмнай тэме\"." "subtitle": "Стварыць версію PDF у \"цёмнай тэме\"."
}, },
"scannerEffect": {
"name": "Эфект сканера",
"subtitle": "Зрабіць PDF падобным на адсканіраваны дакумент.",
"scanSettings": "Налады сканіравання",
"colorspace": "Каляровая прастора",
"gray": "Градацыі шэрага",
"border": "Рамка",
"rotate": "Паварот",
"rotateVariance": "Варыяцыя павароту",
"brightness": "Яркасць",
"contrast": "Кантраст",
"blur": "Размыццё",
"noise": "Шум",
"yellowish": "Жаўтаватасць",
"resolution": "Раздзяляльнасць",
"processButton": "Ужыць эфект сканера"
},
"adjustColors": {
"name": "Наладзіць колеры",
"subtitle": "Наладзьце яркасць, кантраст, насычанасць і іншае ў вашым PDF.",
"colorSettings": "Налады колеру",
"brightness": "Яркасць",
"contrast": "Кантраст",
"saturation": "Насычанасць",
"hueShift": "Зрух адцення",
"temperature": "Тэмпература",
"tint": "Адценне",
"gamma": "Гама",
"sepia": "Сэпія",
"processButton": "Прымяніць налады колеру"
},
"backgroundColor": { "backgroundColor": {
"name": "Колер фону", "name": "Колер фону",
"subtitle": "Змяніць колер фону PDF." "subtitle": "Змяніць колер фону PDF."

View File

@@ -98,6 +98,37 @@
"name": "Farben invertieren", "name": "Farben invertieren",
"subtitle": "Eine \"Dunkelmodus\"-Version Ihrer PDF erstellen." "subtitle": "Eine \"Dunkelmodus\"-Version Ihrer PDF erstellen."
}, },
"scannerEffect": {
"name": "Scanner-Effekt",
"subtitle": "Lassen Sie Ihr PDF wie ein gescanntes Dokument aussehen.",
"scanSettings": "Scan-Einstellungen",
"colorspace": "Farbraum",
"gray": "Grau",
"border": "Rand",
"rotate": "Drehen",
"rotateVariance": "Drehvarianz",
"brightness": "Helligkeit",
"contrast": "Kontrast",
"blur": "Unschärfe",
"noise": "Rauschen",
"yellowish": "Gelbstich",
"resolution": "Auflösung",
"processButton": "Scanner-Effekt anwenden"
},
"adjustColors": {
"name": "Farben anpassen",
"subtitle": "Helligkeit, Kontrast, Sättigung und mehr in Ihrem PDF feinabstimmen.",
"colorSettings": "Farbeinstellungen",
"brightness": "Helligkeit",
"contrast": "Kontrast",
"saturation": "Sättigung",
"hueShift": "Farbton",
"temperature": "Temperatur",
"tint": "Tönung",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Farbanpassungen anwenden"
},
"backgroundColor": { "backgroundColor": {
"name": "Hintergrundfarbe", "name": "Hintergrundfarbe",
"subtitle": "Die Hintergrundfarbe Ihrer PDF ändern." "subtitle": "Die Hintergrundfarbe Ihrer PDF ändern."

View File

@@ -98,6 +98,37 @@
"name": "Invert Colors", "name": "Invert Colors",
"subtitle": "Create a \"dark mode\" version of your PDF." "subtitle": "Create a \"dark mode\" version of your PDF."
}, },
"scannerEffect": {
"name": "Scanner Effect",
"subtitle": "Make your PDF look like a scanned document.",
"scanSettings": "Scan Settings",
"colorspace": "Colorspace",
"gray": "Gray",
"border": "Border",
"rotate": "Rotate",
"rotateVariance": "Rotate Variance",
"brightness": "Brightness",
"contrast": "Contrast",
"blur": "Blur",
"noise": "Noise",
"yellowish": "Yellowish",
"resolution": "Resolution",
"processButton": "Apply Scanner Effect"
},
"adjustColors": {
"name": "Adjust Colors",
"subtitle": "Fine-tune brightness, contrast, saturation and more in your PDF.",
"colorSettings": "Color Settings",
"brightness": "Brightness",
"contrast": "Contrast",
"saturation": "Saturation",
"hueShift": "Hue Shift",
"temperature": "Temperature",
"tint": "Tint",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Apply Color Adjustments"
},
"backgroundColor": { "backgroundColor": {
"name": "Background Color", "name": "Background Color",
"subtitle": "Change the background color of your PDF." "subtitle": "Change the background color of your PDF."

View File

@@ -98,6 +98,37 @@
"name": "Invertir Colores", "name": "Invertir Colores",
"subtitle": "Crea una versión en \"modo oscuro\" de tu PDF." "subtitle": "Crea una versión en \"modo oscuro\" de tu PDF."
}, },
"scannerEffect": {
"name": "Efecto escáner",
"subtitle": "Haz que tu PDF parezca un documento escaneado.",
"scanSettings": "Ajustes de escaneo",
"colorspace": "Espacio de color",
"gray": "Gris",
"border": "Borde",
"rotate": "Rotar",
"rotateVariance": "Variación de rotación",
"brightness": "Brillo",
"contrast": "Contraste",
"blur": "Desenfoque",
"noise": "Ruido",
"yellowish": "Amarillento",
"resolution": "Resolución",
"processButton": "Aplicar efecto escáner"
},
"adjustColors": {
"name": "Ajustar colores",
"subtitle": "Ajusta brillo, contraste, saturación y más en tu PDF.",
"colorSettings": "Configuración de color",
"brightness": "Brillo",
"contrast": "Contraste",
"saturation": "Saturación",
"hueShift": "Tono",
"temperature": "Temperatura",
"tint": "Matiz",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Aplicar ajustes de color"
},
"backgroundColor": { "backgroundColor": {
"name": "Color de Fondo", "name": "Color de Fondo",
"subtitle": "Cambia el color de fondo de tu PDF." "subtitle": "Cambia el color de fondo de tu PDF."

View File

@@ -98,6 +98,37 @@
"name": "Inverser les couleurs", "name": "Inverser les couleurs",
"subtitle": "Créer une version « mode sombre » du PDF." "subtitle": "Créer une version « mode sombre » du PDF."
}, },
"scannerEffect": {
"name": "Effet scanner",
"subtitle": "Donnez à votre PDF l'apparence d'un document scanné.",
"scanSettings": "Paramètres de numérisation",
"colorspace": "Espace colorimétrique",
"gray": "Gris",
"border": "Bordure",
"rotate": "Rotation",
"rotateVariance": "Variance de rotation",
"brightness": "Luminosité",
"contrast": "Contraste",
"blur": "Flou",
"noise": "Bruit",
"yellowish": "Jaunissement",
"resolution": "Résolution",
"processButton": "Appliquer l'effet scanner"
},
"adjustColors": {
"name": "Ajuster les couleurs",
"subtitle": "Affinez la luminosité, le contraste, la saturation et plus dans votre PDF.",
"colorSettings": "Paramètres de couleur",
"brightness": "Luminosité",
"contrast": "Contraste",
"saturation": "Saturation",
"hueShift": "Teinte",
"temperature": "Température",
"tint": "Nuance",
"gamma": "Gamma",
"sepia": "Sépia",
"processButton": "Appliquer les ajustements"
},
"backgroundColor": { "backgroundColor": {
"name": "Couleur de fond", "name": "Couleur de fond",
"subtitle": "Modifier la couleur de fond du PDF." "subtitle": "Modifier la couleur de fond du PDF."

View File

@@ -98,6 +98,37 @@
"name": "Balik Warna", "name": "Balik Warna",
"subtitle": "Buat versi \"mode gelap\" dari PDF Anda." "subtitle": "Buat versi \"mode gelap\" dari PDF Anda."
}, },
"scannerEffect": {
"name": "Efek Pemindai",
"subtitle": "Buat PDF Anda terlihat seperti dokumen yang dipindai.",
"scanSettings": "Pengaturan Pemindaian",
"colorspace": "Ruang Warna",
"gray": "Skala Abu-abu",
"border": "Batas",
"rotate": "Putar",
"rotateVariance": "Variasi Putaran",
"brightness": "Kecerahan",
"contrast": "Kontras",
"blur": "Blur",
"noise": "Noise",
"yellowish": "Kekuningan",
"resolution": "Resolusi",
"processButton": "Terapkan Efek Pemindai"
},
"adjustColors": {
"name": "Sesuaikan Warna",
"subtitle": "Sesuaikan kecerahan, kontras, saturasi dan lainnya pada PDF Anda.",
"colorSettings": "Pengaturan Warna",
"brightness": "Kecerahan",
"contrast": "Kontras",
"saturation": "Saturasi",
"hueShift": "Pergeseran Rona",
"temperature": "Suhu",
"tint": "Pewarnaan",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Terapkan Penyesuaian Warna"
},
"backgroundColor": { "backgroundColor": {
"name": "Warna Latar Belakang", "name": "Warna Latar Belakang",
"subtitle": "Ubah warna latar belakang PDF Anda." "subtitle": "Ubah warna latar belakang PDF Anda."

View File

@@ -98,6 +98,37 @@
"name": "Inverti Colori", "name": "Inverti Colori",
"subtitle": "Crea una versione \"modalità scura\" del tuo PDF." "subtitle": "Crea una versione \"modalità scura\" del tuo PDF."
}, },
"scannerEffect": {
"name": "Effetto scanner",
"subtitle": "Fai sembrare il tuo PDF un documento scansionato.",
"scanSettings": "Impostazioni di scansione",
"colorspace": "Spazio colore",
"gray": "Grigio",
"border": "Bordo",
"rotate": "Rotazione",
"rotateVariance": "Variazione rotazione",
"brightness": "Luminosità",
"contrast": "Contrasto",
"blur": "Sfocatura",
"noise": "Rumore",
"yellowish": "Ingiallimento",
"resolution": "Risoluzione",
"processButton": "Applica effetto scanner"
},
"adjustColors": {
"name": "Regola colori",
"subtitle": "Regola luminosità, contrasto, saturazione e altro nel tuo PDF.",
"colorSettings": "Impostazioni colore",
"brightness": "Luminosità",
"contrast": "Contrasto",
"saturation": "Saturazione",
"hueShift": "Tonalità",
"temperature": "Temperatura",
"tint": "Tinta",
"gamma": "Gamma",
"sepia": "Seppia",
"processButton": "Applica regolazioni colore"
},
"backgroundColor": { "backgroundColor": {
"name": "Colore di Sfondo", "name": "Colore di Sfondo",
"subtitle": "Cambia il colore di sfondo del tuo PDF." "subtitle": "Cambia il colore di sfondo del tuo PDF."

View File

@@ -98,6 +98,37 @@
"name": "Kleuren Omkeren", "name": "Kleuren Omkeren",
"subtitle": "Maak een \"donkere modus\"-versie van je PDF." "subtitle": "Maak een \"donkere modus\"-versie van je PDF."
}, },
"scannerEffect": {
"name": "Scannereffect",
"subtitle": "Laat je PDF eruitzien als een gescand document.",
"scanSettings": "Scaninstellingen",
"colorspace": "Kleurruimte",
"gray": "Grijswaarden",
"border": "Rand",
"rotate": "Roteren",
"rotateVariance": "Rotatievariatie",
"brightness": "Helderheid",
"contrast": "Contrast",
"blur": "Vervaging",
"noise": "Ruis",
"yellowish": "Geelheid",
"resolution": "Resolutie",
"processButton": "Scannereffect toepassen"
},
"adjustColors": {
"name": "Kleuren aanpassen",
"subtitle": "Pas helderheid, contrast, verzadiging en meer aan in uw PDF.",
"colorSettings": "Kleurinstellingen",
"brightness": "Helderheid",
"contrast": "Contrast",
"saturation": "Verzadiging",
"hueShift": "Tintrotatie",
"temperature": "Temperatuur",
"tint": "Tint",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Kleuraanpassingen toepassen"
},
"backgroundColor": { "backgroundColor": {
"name": "Achtergrondkleur", "name": "Achtergrondkleur",
"subtitle": "Wijzig de achtergrondkleur van je PDF." "subtitle": "Wijzig de achtergrondkleur van je PDF."

View File

@@ -76,6 +76,37 @@
"name": "Inverter Cores", "name": "Inverter Cores",
"subtitle": "Crie uma versão em \"modo escuro\" do seu PDF." "subtitle": "Crie uma versão em \"modo escuro\" do seu PDF."
}, },
"scannerEffect": {
"name": "Efeito Scanner",
"subtitle": "Faça seu PDF parecer um documento digitalizado.",
"scanSettings": "Configurações de Digitalização",
"colorspace": "Espaço de Cor",
"gray": "Cinza",
"border": "Borda",
"rotate": "Rotação",
"rotateVariance": "Variação de Rotação",
"brightness": "Brilho",
"contrast": "Contraste",
"blur": "Desfoque",
"noise": "Ruído",
"yellowish": "Amarelado",
"resolution": "Resolução",
"processButton": "Aplicar Efeito Scanner"
},
"adjustColors": {
"name": "Ajustar Cores",
"subtitle": "Ajuste brilho, contraste, saturação e mais no seu PDF.",
"colorSettings": "Configurações de Cor",
"brightness": "Brilho",
"contrast": "Contraste",
"saturation": "Saturação",
"hueShift": "Matiz",
"temperature": "Temperatura",
"tint": "Tonalidade",
"gamma": "Gamma",
"sepia": "Sépia",
"processButton": "Aplicar Ajustes de Cor"
},
"backgroundColor": { "backgroundColor": {
"name": "Cor de Fundo", "name": "Cor de Fundo",
"subtitle": "Altere a cor de fundo do seu PDF." "subtitle": "Altere a cor de fundo do seu PDF."

View File

@@ -76,6 +76,37 @@
"name": "Renkleri Ters Çevir", "name": "Renkleri Ters Çevir",
"subtitle": "PDF'niz için \"karanlık mod\" sürümü oluşturun." "subtitle": "PDF'niz için \"karanlık mod\" sürümü oluşturun."
}, },
"scannerEffect": {
"name": "Tarayıcı Efekti",
"subtitle": "PDF'nizi taranmış bir belge gibi gösterin.",
"scanSettings": "Tarama Ayarları",
"colorspace": "Renk Alanı",
"gray": "Gri",
"border": "Kenarlık",
"rotate": "Döndür",
"rotateVariance": "Döndürme Varyansı",
"brightness": "Parlaklık",
"contrast": "Kontrast",
"blur": "Bulanıklık",
"noise": "Gürültü",
"yellowish": "Sarımsı",
"resolution": "Çözünürlük",
"processButton": "Tarayıcı Efekti Uygula"
},
"adjustColors": {
"name": "Renkleri Ayarla",
"subtitle": "PDF'inizde parlaklık, kontrast, doygunluk ve daha fazlasını ayarlayın.",
"colorSettings": "Renk Ayarları",
"brightness": "Parlaklık",
"contrast": "Kontrast",
"saturation": "Doygunluk",
"hueShift": "Ton Kaydırma",
"temperature": "Sıcaklık",
"tint": "Renk Tonu",
"gamma": "Gamma",
"sepia": "Sepya",
"processButton": "Renk Ayarlarını Uygula"
},
"backgroundColor": { "backgroundColor": {
"name": "Arka Plan Rengi", "name": "Arka Plan Rengi",
"subtitle": "PDF'nizin arka plan rengini değiştirin." "subtitle": "PDF'nizin arka plan rengini değiştirin."

View File

@@ -98,6 +98,37 @@
"name": "Đảo ngược màu", "name": "Đảo ngược màu",
"subtitle": "Tạo phiên bản \"chế độ tối\" cho PDF của bạn." "subtitle": "Tạo phiên bản \"chế độ tối\" cho PDF của bạn."
}, },
"scannerEffect": {
"name": "Hiệu ứng máy quét",
"subtitle": "Làm cho PDF trông giống như tài liệu đã quét.",
"scanSettings": "Cài đặt quét",
"colorspace": "Không gian màu",
"gray": "Thang xám",
"border": "Viền",
"rotate": "Xoay",
"rotateVariance": "Biên độ xoay",
"brightness": "Độ sáng",
"contrast": "Độ tương phản",
"blur": "Làm mờ",
"noise": "Nhiễu",
"yellowish": "Ngả vàng",
"resolution": "Độ phân giải",
"processButton": "Áp dụng hiệu ứng máy quét"
},
"adjustColors": {
"name": "Điều chỉnh màu sắc",
"subtitle": "Tinh chỉnh độ sáng, tương phản, độ bão hòa và nhiều hơn nữa.",
"colorSettings": "Cài đặt màu sắc",
"brightness": "Độ sáng",
"contrast": "Tương phản",
"saturation": "Độ bão hòa",
"hueShift": "Dịch chuyển sắc độ",
"temperature": "Nhiệt độ màu",
"tint": "Sắc thái",
"gamma": "Gamma",
"sepia": "Sepia",
"processButton": "Áp dụng điều chỉnh màu"
},
"backgroundColor": { "backgroundColor": {
"name": "Màu nền", "name": "Màu nền",
"subtitle": "Thay đổi màu nền của PDF của bạn." "subtitle": "Thay đổi màu nền của PDF của bạn."

View File

@@ -76,6 +76,37 @@
"name": "反轉顏色", "name": "反轉顏色",
"subtitle": "為你的 PDF 建立深色版本。" "subtitle": "為你的 PDF 建立深色版本。"
}, },
"scannerEffect": {
"name": "掃描效果",
"subtitle": "讓你的 PDF 看起來像掃描文件。",
"scanSettings": "掃描設定",
"colorspace": "色彩空間",
"gray": "灰階",
"border": "邊框",
"rotate": "旋轉",
"rotateVariance": "旋轉變異",
"brightness": "亮度",
"contrast": "對比度",
"blur": "模糊",
"noise": "雜訊",
"yellowish": "泛黃",
"resolution": "解析度",
"processButton": "套用掃描效果"
},
"adjustColors": {
"name": "調整顏色",
"subtitle": "微調 PDF 的亮度、對比度、飽和度等。",
"colorSettings": "顏色設定",
"brightness": "亮度",
"contrast": "對比度",
"saturation": "飽和度",
"hueShift": "色相偏移",
"temperature": "色溫",
"tint": "色調",
"gamma": "Gamma",
"sepia": "復古色",
"processButton": "套用顏色調整"
},
"backgroundColor": { "backgroundColor": {
"name": "背景顏色", "name": "背景顏色",
"subtitle": "更改你的 PDF 的背景顏色。" "subtitle": "更改你的 PDF 的背景顏色。"

View File

@@ -98,6 +98,37 @@
"name": "反转颜色", "name": "反转颜色",
"subtitle": "创建您的 PDF 的“暗黑模式”版本。" "subtitle": "创建您的 PDF 的“暗黑模式”版本。"
}, },
"scannerEffect": {
"name": "扫描效果",
"subtitle": "让您的 PDF 看起来像扫描文件。",
"scanSettings": "扫描设置",
"colorspace": "色彩空间",
"gray": "灰度",
"border": "边框",
"rotate": "旋转",
"rotateVariance": "旋转偏差",
"brightness": "亮度",
"contrast": "对比度",
"blur": "模糊",
"noise": "噪点",
"yellowish": "泛黄",
"resolution": "分辨率",
"processButton": "应用扫描效果"
},
"adjustColors": {
"name": "调整颜色",
"subtitle": "微调 PDF 的亮度、对比度、饱和度等。",
"colorSettings": "颜色设置",
"brightness": "亮度",
"contrast": "对比度",
"saturation": "饱和度",
"hueShift": "色相偏移",
"temperature": "色温",
"tint": "色调",
"gamma": "Gamma",
"sepia": "复古色",
"processButton": "应用颜色调整"
},
"backgroundColor": { "backgroundColor": {
"name": "背景颜色", "name": "背景颜色",
"subtitle": "更改您的 PDF 的背景颜色。" "subtitle": "更改您的 PDF 的背景颜色。"

File diff suppressed because it is too large Load Diff

View File

@@ -119,6 +119,18 @@ export const categories = [
icon: 'ph-circle-half', icon: 'ph-circle-half',
subtitle: 'Create a "dark mode" version of your PDF.', subtitle: 'Create a "dark mode" version of your PDF.',
}, },
{
href: import.meta.env.BASE_URL + 'scanner-effect.html',
name: 'Scanner Effect',
icon: 'ph-scan',
subtitle: 'Make your PDF look like a scanned document.',
},
{
href: import.meta.env.BASE_URL + 'adjust-colors.html',
name: 'Adjust Colors',
icon: 'ph-sliders-horizontal',
subtitle: 'Fine-tune brightness, contrast, saturation and more.',
},
{ {
href: import.meta.env.BASE_URL + 'background-color.html', href: import.meta.env.BASE_URL + 'background-color.html',
name: 'Background Color', name: 'Background Color',

View File

@@ -0,0 +1,553 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import type { AdjustColorsSettings } from '../types/adjust-colors-type.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
let files: File[] = [];
let cachedBaselineData: ImageData | null = null;
let cachedBaselineWidth = 0;
let cachedBaselineHeight = 0;
let pdfjsDoc: pdfjsLib.PDFDocumentProxy | null = null;
function getSettings(): AdjustColorsSettings {
return {
brightness: parseInt(
(document.getElementById('setting-brightness') as HTMLInputElement)
?.value ?? '0'
),
contrast: parseInt(
(document.getElementById('setting-contrast') as HTMLInputElement)
?.value ?? '0'
),
saturation: parseInt(
(document.getElementById('setting-saturation') as HTMLInputElement)
?.value ?? '0'
),
hueShift: parseInt(
(document.getElementById('setting-hue-shift') as HTMLInputElement)
?.value ?? '0'
),
temperature: parseInt(
(document.getElementById('setting-temperature') as HTMLInputElement)
?.value ?? '0'
),
tint: parseInt(
(document.getElementById('setting-tint') as HTMLInputElement)?.value ??
'0'
),
gamma: parseFloat(
(document.getElementById('setting-gamma') as HTMLInputElement)?.value ??
'1.0'
),
sepia: parseInt(
(document.getElementById('setting-sepia') as HTMLInputElement)?.value ??
'0'
),
};
}
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
else if (max === g) h = ((b - r) / d + 2) / 6;
else h = ((r - g) / d + 4) / 6;
}
return [h, s, l];
}
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
if (s === 0) {
const v = Math.round(l * 255);
return [v, v, v];
}
const hue2rgb = (p: number, q: number, t: number): number => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return [
Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
Math.round(hue2rgb(p, q, h) * 255),
Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
];
}
function applyEffects(
sourceData: ImageData,
canvas: HTMLCanvasElement,
settings: AdjustColorsSettings
): void {
const ctx = canvas.getContext('2d')!;
const w = sourceData.width;
const h = sourceData.height;
canvas.width = w;
canvas.height = h;
const imageData = new ImageData(new Uint8ClampedArray(sourceData.data), w, h);
const data = imageData.data;
const contrastFactor =
settings.contrast !== 0
? (259 * (settings.contrast + 255)) / (255 * (259 - settings.contrast))
: 1;
const gammaCorrection = settings.gamma !== 1.0 ? 1 / settings.gamma : 1;
const sepiaAmount = settings.sepia / 100;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Brightness
if (settings.brightness !== 0) {
const adj = settings.brightness * 2.55;
r += adj;
g += adj;
b += adj;
}
// Contrast
if (settings.contrast !== 0) {
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
// Saturation and Hue Shift (via HSL)
if (settings.saturation !== 0 || settings.hueShift !== 0) {
const [hue, sat, lig] = rgbToHsl(
Math.max(0, Math.min(255, r)),
Math.max(0, Math.min(255, g)),
Math.max(0, Math.min(255, b))
);
let newHue = hue;
if (settings.hueShift !== 0) {
newHue = (hue + settings.hueShift / 360) % 1;
if (newHue < 0) newHue += 1;
}
let newSat = sat;
if (settings.saturation !== 0) {
const satAdj = settings.saturation / 100;
newSat = satAdj > 0 ? sat + (1 - sat) * satAdj : sat * (1 + satAdj);
newSat = Math.max(0, Math.min(1, newSat));
}
[r, g, b] = hslToRgb(newHue, newSat, lig);
}
// Temperature (warm/cool shift)
if (settings.temperature !== 0) {
const t = settings.temperature / 50;
r += 30 * t;
b -= 30 * t;
}
// Tint (green-magenta axis)
if (settings.tint !== 0) {
const t = settings.tint / 50;
g += 30 * t;
}
// Gamma
if (settings.gamma !== 1.0) {
r = Math.pow(Math.max(0, Math.min(255, r)) / 255, gammaCorrection) * 255;
g = Math.pow(Math.max(0, Math.min(255, g)) / 255, gammaCorrection) * 255;
b = Math.pow(Math.max(0, Math.min(255, b)) / 255, gammaCorrection) * 255;
}
// Sepia
if (settings.sepia > 0) {
const sr = 0.393 * r + 0.769 * g + 0.189 * b;
const sg = 0.349 * r + 0.686 * g + 0.168 * b;
const sb = 0.272 * r + 0.534 * g + 0.131 * b;
r = r + (sr - r) * sepiaAmount;
g = g + (sg - g) * sepiaAmount;
b = b + (sb - b) * sepiaAmount;
}
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
}
ctx.putImageData(imageData, 0, 0);
}
function updatePreview(): void {
if (!cachedBaselineData) return;
const previewCanvas = document.getElementById(
'preview-canvas'
) as HTMLCanvasElement;
if (!previewCanvas) return;
const settings = getSettings();
const baselineCopy = new ImageData(
new Uint8ClampedArray(cachedBaselineData.data),
cachedBaselineWidth,
cachedBaselineHeight
);
applyEffects(baselineCopy, previewCanvas, settings);
}
async function renderPreview(): Promise<void> {
if (!pdfjsDoc) return;
const page = await pdfjsDoc.getPage(1);
const viewport = page.getViewport({ scale: 1.0 });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
cachedBaselineData = ctx.getImageData(0, 0, canvas.width, canvas.height);
cachedBaselineWidth = canvas.width;
cachedBaselineHeight = canvas.height;
updatePreview();
}
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((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 = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
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 = () => {
files = [];
pdfjsDoc = null;
cachedBaselineData = null;
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
readFileAsArrayBuffer(file)
.then((buffer: ArrayBuffer) => {
return getPDFDocument(buffer).promise;
})
.then((pdf: pdfjsLib.PDFDocumentProxy) => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
})
.catch(() => {
metaSpan.textContent = formatBytes(file.size);
});
});
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
pdfjsDoc = null;
cachedBaselineData = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function processAllPages(): Promise<void> {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Applying color adjustments...');
try {
const settings = getSettings();
const pdfBytes = (await readFileAsArrayBuffer(files[0])) as ArrayBuffer;
const doc = await getPDFDocument({ data: pdfBytes }).promise;
const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= doc.numPages; i++) {
showLoader(`Processing page ${i} of ${doc.numPages}...`);
const page = await doc.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const renderCanvas = document.createElement('canvas');
const renderCtx = renderCanvas.getContext('2d')!;
renderCanvas.width = viewport.width;
renderCanvas.height = viewport.height;
await page.render({
canvasContext: renderCtx,
viewport,
canvas: renderCanvas,
}).promise;
const baseData = renderCtx.getImageData(
0,
0,
renderCanvas.width,
renderCanvas.height
);
const outputCanvas = document.createElement('canvas');
applyEffects(baseData, outputCanvas, settings);
const pngBlob = await new Promise<Blob | null>((resolve) =>
outputCanvas.toBlob(resolve, 'image/png')
);
if (pngBlob) {
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await newPdfDoc.embedPng(pngBytes);
const origViewport = page.getViewport({ scale: 1.0 });
const newPage = newPdfDoc.addPage([
origViewport.width,
origViewport.height,
]);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: origViewport.width,
height: origViewport.height,
});
}
}
const resultBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(resultBytes)], { type: 'application/pdf' }),
'color-adjusted.pdf'
);
showAlert(
'Success',
'Color adjustments applied successfully!',
'success',
() => {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to apply color adjustments. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
const sliderDefaults: {
id: string;
display: string;
suffix: string;
defaultValue: string;
}[] = [
{
id: 'setting-brightness',
display: 'brightness-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-contrast',
display: 'contrast-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-saturation',
display: 'saturation-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-hue-shift',
display: 'hue-shift-value',
suffix: '°',
defaultValue: '0',
},
{
id: 'setting-temperature',
display: 'temperature-value',
suffix: '',
defaultValue: '0',
},
{ id: 'setting-tint', display: 'tint-value', suffix: '', defaultValue: '0' },
{
id: 'setting-gamma',
display: 'gamma-value',
suffix: '',
defaultValue: '1.0',
},
{
id: 'setting-sepia',
display: 'sepia-value',
suffix: '',
defaultValue: '0',
},
];
function resetSettings(): void {
sliderDefaults.forEach(({ id, display, suffix, defaultValue }) => {
const slider = document.getElementById(id) as HTMLInputElement;
const label = document.getElementById(display);
if (slider) slider.value = defaultValue;
if (label) label.textContent = defaultValue + suffix;
});
updatePreview();
}
function setupSettingsListeners(): void {
sliderDefaults.forEach(({ id, display, suffix }) => {
const slider = document.getElementById(id) as HTMLInputElement;
const label = document.getElementById(display);
if (slider && label) {
slider.addEventListener('input', () => {
label.textContent = slider.value + suffix;
updatePreview();
});
}
});
const resetBtn = document.getElementById('reset-settings-btn');
if (resetBtn) {
resetBtn.addEventListener('click', resetSettings);
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = async (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
showLoader('Loading preview...');
try {
const buffer = await readFileAsArrayBuffer(validFiles[0]);
pdfjsDoc = await getPDFDocument({ data: buffer }).promise;
await renderPreview();
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to load PDF for preview.');
} finally {
hideLoader();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', processAllPages);
}
setupSettingsListeners();
});

View File

@@ -0,0 +1,586 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import type { ScanSettings } from '../types/scanner-effect-type.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
let files: File[] = [];
let cachedBaselineData: ImageData | null = null;
let cachedBaselineWidth = 0;
let cachedBaselineHeight = 0;
let pdfjsDoc: pdfjsLib.PDFDocumentProxy | null = null;
function getSettings(): ScanSettings {
return {
grayscale:
(document.getElementById('setting-grayscale') as HTMLInputElement)
?.checked ?? false,
border:
(document.getElementById('setting-border') as HTMLInputElement)
?.checked ?? false,
rotate: parseFloat(
(document.getElementById('setting-rotate') as HTMLInputElement)?.value ??
'0'
),
rotateVariance: parseFloat(
(document.getElementById('setting-rotate-variance') as HTMLInputElement)
?.value ?? '0'
),
brightness: parseInt(
(document.getElementById('setting-brightness') as HTMLInputElement)
?.value ?? '0'
),
contrast: parseInt(
(document.getElementById('setting-contrast') as HTMLInputElement)
?.value ?? '0'
),
blur: parseFloat(
(document.getElementById('setting-blur') as HTMLInputElement)?.value ??
'0'
),
noise: parseInt(
(document.getElementById('setting-noise') as HTMLInputElement)?.value ??
'10'
),
yellowish: parseInt(
(document.getElementById('setting-yellowish') as HTMLInputElement)
?.value ?? '0'
),
resolution: parseInt(
(document.getElementById('setting-resolution') as HTMLInputElement)
?.value ?? '150'
),
};
}
function applyEffects(
sourceData: ImageData,
canvas: HTMLCanvasElement,
settings: ScanSettings,
rotationAngle: number,
scale: number = 1
): void {
const ctx = canvas.getContext('2d')!;
const w = sourceData.width;
const h = sourceData.height;
const scaledBlur = settings.blur * scale;
const scaledNoise = settings.noise * scale;
const workCanvas = document.createElement('canvas');
workCanvas.width = w;
workCanvas.height = h;
const workCtx = workCanvas.getContext('2d')!;
if (scaledBlur > 0) {
workCtx.filter = `blur(${scaledBlur}px)`;
}
workCtx.putImageData(sourceData, 0, 0);
if (scaledBlur > 0) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d')!;
tempCtx.filter = `blur(${scaledBlur}px)`;
tempCtx.drawImage(workCanvas, 0, 0);
workCtx.filter = 'none';
workCtx.clearRect(0, 0, w, h);
workCtx.drawImage(tempCanvas, 0, 0);
}
const imageData = workCtx.getImageData(0, 0, w, h);
const data = imageData.data;
const contrastFactor =
settings.contrast !== 0
? (259 * (settings.contrast + 255)) / (255 * (259 - settings.contrast))
: 1;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
if (settings.grayscale) {
const grey = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
r = grey;
g = grey;
b = grey;
}
if (settings.brightness !== 0) {
r += settings.brightness;
g += settings.brightness;
b += settings.brightness;
}
if (settings.contrast !== 0) {
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
if (settings.yellowish > 0) {
const intensity = settings.yellowish / 50;
r += 20 * intensity;
g += 12 * intensity;
b -= 15 * intensity;
}
if (scaledNoise > 0) {
const n = (Math.random() - 0.5) * scaledNoise;
r += n;
g += n;
b += n;
}
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
}
workCtx.putImageData(imageData, 0, 0);
if (settings.border) {
const borderSize = Math.max(w, h) * 0.02;
const gradient1 = workCtx.createLinearGradient(0, 0, borderSize, 0);
gradient1.addColorStop(0, 'rgba(0,0,0,0.3)');
gradient1.addColorStop(1, 'rgba(0,0,0,0)');
workCtx.fillStyle = gradient1;
workCtx.fillRect(0, 0, borderSize, h);
const gradient2 = workCtx.createLinearGradient(w, 0, w - borderSize, 0);
gradient2.addColorStop(0, 'rgba(0,0,0,0.3)');
gradient2.addColorStop(1, 'rgba(0,0,0,0)');
workCtx.fillStyle = gradient2;
workCtx.fillRect(w - borderSize, 0, borderSize, h);
const gradient3 = workCtx.createLinearGradient(0, 0, 0, borderSize);
gradient3.addColorStop(0, 'rgba(0,0,0,0.3)');
gradient3.addColorStop(1, 'rgba(0,0,0,0)');
workCtx.fillStyle = gradient3;
workCtx.fillRect(0, 0, w, borderSize);
const gradient4 = workCtx.createLinearGradient(0, h, 0, h - borderSize);
gradient4.addColorStop(0, 'rgba(0,0,0,0.3)');
gradient4.addColorStop(1, 'rgba(0,0,0,0)');
workCtx.fillStyle = gradient4;
workCtx.fillRect(0, h - borderSize, w, borderSize);
}
if (rotationAngle !== 0) {
const rad = (rotationAngle * Math.PI) / 180;
const cos = Math.abs(Math.cos(rad));
const sin = Math.abs(Math.sin(rad));
const newW = Math.ceil(w * cos + h * sin);
const newH = Math.ceil(w * sin + h * cos);
canvas.width = newW;
canvas.height = newH;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, newW, newH);
ctx.translate(newW / 2, newH / 2);
ctx.rotate(rad);
ctx.drawImage(workCanvas, -w / 2, -h / 2);
ctx.setTransform(1, 0, 0, 1, 0, 0);
} else {
canvas.width = w;
canvas.height = h;
ctx.drawImage(workCanvas, 0, 0);
}
}
function updatePreview(): void {
if (!cachedBaselineData) return;
const previewCanvas = document.getElementById(
'preview-canvas'
) as HTMLCanvasElement;
if (!previewCanvas) return;
const settings = getSettings();
const baselineCopy = new ImageData(
new Uint8ClampedArray(cachedBaselineData.data),
cachedBaselineWidth,
cachedBaselineHeight
);
applyEffects(baselineCopy, previewCanvas, settings, settings.rotate);
}
async function renderPreview(): Promise<void> {
if (!pdfjsDoc) return;
const page = await pdfjsDoc.getPage(1);
const viewport = page.getViewport({ scale: 1.0 });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
cachedBaselineData = ctx.getImageData(0, 0, canvas.width, canvas.height);
cachedBaselineWidth = canvas.width;
cachedBaselineHeight = canvas.height;
updatePreview();
}
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((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 = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
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 = () => {
files = [];
pdfjsDoc = null;
cachedBaselineData = null;
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
readFileAsArrayBuffer(file)
.then((buffer: ArrayBuffer) => {
return getPDFDocument(buffer).promise;
})
.then((pdf: pdfjsLib.PDFDocumentProxy) => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
})
.catch(() => {
metaSpan.textContent = formatBytes(file.size);
});
});
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
pdfjsDoc = null;
cachedBaselineData = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function processAllPages(): Promise<void> {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Applying scanner effect...');
try {
const settings = getSettings();
const pdfBytes = (await readFileAsArrayBuffer(files[0])) as ArrayBuffer;
const doc = await getPDFDocument({ data: pdfBytes }).promise;
const newPdfDoc = await PDFDocument.create();
const dpiScale = settings.resolution / 72;
for (let i = 1; i <= doc.numPages; i++) {
showLoader(`Processing page ${i} of ${doc.numPages}...`);
const page = await doc.getPage(i);
const viewport = page.getViewport({ scale: dpiScale });
const renderCanvas = document.createElement('canvas');
const renderCtx = renderCanvas.getContext('2d')!;
renderCanvas.width = viewport.width;
renderCanvas.height = viewport.height;
await page.render({
canvasContext: renderCtx,
viewport,
canvas: renderCanvas,
}).promise;
const baseData = renderCtx.getImageData(
0,
0,
renderCanvas.width,
renderCanvas.height
);
const baselineCopy = new ImageData(
new Uint8ClampedArray(baseData.data),
baseData.width,
baseData.height
);
const outputCanvas = document.createElement('canvas');
const pageRotation =
settings.rotate +
(settings.rotateVariance > 0
? (Math.random() - 0.5) * 2 * settings.rotateVariance
: 0);
applyEffects(
baselineCopy,
outputCanvas,
settings,
pageRotation,
dpiScale
);
const jpegBlob = await new Promise<Blob | null>((resolve) =>
outputCanvas.toBlob(resolve, 'image/jpeg', 0.85)
);
if (jpegBlob) {
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([
outputCanvas.width,
outputCanvas.height,
]);
newPage.drawImage(jpegImage, {
x: 0,
y: 0,
width: outputCanvas.width,
height: outputCanvas.height,
});
}
}
const resultBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(resultBytes)], { type: 'application/pdf' }),
'scanned.pdf'
);
showAlert(
'Success',
'Scanner effect applied successfully!',
'success',
() => {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to apply scanner effect. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
const sliderDefaults: {
id: string;
display: string;
suffix: string;
defaultValue: string;
}[] = [
{
id: 'setting-rotate',
display: 'rotate-value',
suffix: '°',
defaultValue: '0',
},
{
id: 'setting-rotate-variance',
display: 'rotate-variance-value',
suffix: '°',
defaultValue: '0',
},
{
id: 'setting-brightness',
display: 'brightness-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-contrast',
display: 'contrast-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-blur',
display: 'blur-value',
suffix: 'px',
defaultValue: '0',
},
{
id: 'setting-noise',
display: 'noise-value',
suffix: '',
defaultValue: '10',
},
{
id: 'setting-yellowish',
display: 'yellowish-value',
suffix: '',
defaultValue: '0',
},
{
id: 'setting-resolution',
display: 'resolution-value',
suffix: ' DPI',
defaultValue: '150',
},
];
function resetSettings(): void {
sliderDefaults.forEach(({ id, display, suffix, defaultValue }) => {
const slider = document.getElementById(id) as HTMLInputElement;
const label = document.getElementById(display);
if (slider) slider.value = defaultValue;
if (label) label.textContent = defaultValue + suffix;
});
const grayscale = document.getElementById(
'setting-grayscale'
) as HTMLInputElement;
const border = document.getElementById('setting-border') as HTMLInputElement;
if (grayscale) grayscale.checked = false;
if (border) border.checked = false;
updatePreview();
}
function setupSettingsListeners(): void {
sliderDefaults.forEach(({ id, display, suffix }) => {
const slider = document.getElementById(id) as HTMLInputElement;
const label = document.getElementById(display);
if (slider && label) {
slider.addEventListener('input', () => {
label.textContent = slider.value + suffix;
if (id !== 'setting-resolution') {
updatePreview();
}
});
}
});
const toggleIds = ['setting-grayscale', 'setting-border'];
toggleIds.forEach((id) => {
const toggle = document.getElementById(id) as HTMLInputElement;
if (toggle) {
toggle.addEventListener('change', updatePreview);
}
});
const resetBtn = document.getElementById('reset-settings-btn');
if (resetBtn) {
resetBtn.addEventListener('click', resetSettings);
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = async (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
showLoader('Loading preview...');
try {
const buffer = await readFileAsArrayBuffer(validFiles[0]);
pdfjsDoc = await getPDFDocument({ data: buffer }).promise;
await renderPreview();
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to load PDF for preview.');
} finally {
hideLoader();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', processAllPages);
}
setupSettingsListeners();
});

View File

@@ -0,0 +1,10 @@
export interface AdjustColorsSettings {
brightness: number;
contrast: number;
saturation: number;
hueShift: number;
temperature: number;
tint: number;
gamma: number;
sepia: number;
}

View File

@@ -47,3 +47,5 @@ export * from './sign-pdf-type.ts';
export * from './add-watermark-type.ts'; export * from './add-watermark-type.ts';
export * from './email-to-pdf-type.ts'; export * from './email-to-pdf-type.ts';
export * from './bookmark-pdf-type.ts'; export * from './bookmark-pdf-type.ts';
export * from './scanner-effect-type.ts';
export * from './adjust-colors-type.ts';

View File

@@ -0,0 +1,16 @@
export interface ScannerEffectState {
file: File | null;
}
export interface ScanSettings {
grayscale: boolean;
border: boolean;
rotate: number;
rotateVariance: number;
brightness: number;
contrast: number;
blur: number;
noise: number;
yellowish: number;
resolution: number;
}

View File

@@ -0,0 +1,599 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
Adjust Colors Online Free - PDF Color Adjustment Tool | BentoPDF
</title>
<meta
name="title"
content="Adjust Colors Online Free - PDF Color Adjustment Tool | BentoPDF"
/>
<meta
name="description"
content="★ Adjust PDF colors online free - Brightness, contrast, saturation, hue, temperature & more ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser ★ Fast & secure"
/>
<meta
name="keywords"
content="adjust pdf colors, pdf brightness, pdf contrast, pdf saturation, pdf hue shift, pdf color correction"
/>
<meta name="author" content="BentoPDF" />
<meta
name="robots"
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
/>
<link rel="canonical" href="https://www.bentopdf.com/adjust-colors.html" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/adjust-colors" />
<meta
property="og:title"
content="Adjust Colors Online Free - PDF Color Adjustment Tool | BentoPDF"
/>
<meta
property="og:description"
content="★ Adjust PDF colors online free - Brightness, contrast, saturation, hue, temperature & more ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser ★ Fast & secure"
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-adjust-colors.png"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="BentoPDF" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://www.bentopdf.com/adjust-colors" />
<meta name="twitter:title" content="Adjust Colors Free" />
<meta
name="twitter:description"
content="★ Adjust PDF colors online free - Brightness, contrast, saturation, hue & more ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser"
/>
<meta
name="twitter:image"
content="https://www.bentopdf.com/images/twitter-adjust-colors.png"
/>
<meta name="twitter:site" content="@BentoPDF" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Adjust Colors" />
<title>Adjust Colors - BentoPDF</title>
<meta
name="description"
content="Adjust brightness, contrast, saturation, hue, temperature, gamma and more in your PDF. Free, secure, and runs entirely in your browser."
/>
<link href="/src/css/styles.css" rel="stylesheet" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/images/favicon-512x512.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/apple-touch-icon.png"
/>
<link rel="icon" href="/favicon.ico" sizes="32x32" />
</head>
<body class="antialiased bg-gray-900">
{{> navbar }}
<div
id="uploader"
class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900"
>
<div
id="tool-uploader"
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-4xl w-full text-gray-200 border border-gray-700"
>
<button
id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
>
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer" data-i18n="tools.backToTools">
Back to Tools
</span>
</button>
<h1
class="text-2xl font-bold text-white mb-2"
data-i18n="tools:adjustColors.name"
>
Adjust Colors Free Online - PDF Color Correction Tool
</h1>
<p class="text-gray-400 mb-6" data-i18n="tools:adjustColors.subtitle">
Fine-tune brightness, contrast, saturation, hue and more in your PDF.
</p>
<div
id="drop-zone"
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300"
>
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i
data-lucide="upload-cloud"
class="w-10 h-10 mb-3 text-gray-400"
></i>
<p class="mb-2 text-sm text-gray-400">
<span class="font-semibold" data-i18n="upload.clickToSelect"
>Click to select a file</span
>
<span data-i18n="upload.orDragAndDrop">or drag and drop</span>
</p>
<p class="text-xs text-gray-500">A single PDF file</p>
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">
Your files never leave your device.
</p>
</div>
<input
id="file-input"
type="file"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
accept="application/pdf"
/>
</div>
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="options-panel" class="hidden mt-6">
<div class="flex flex-col lg:flex-row gap-6">
<div class="flex-1 min-w-0">
<div
class="border border-gray-600 rounded-lg overflow-hidden bg-gray-900"
>
<canvas id="preview-canvas" class="w-full"></canvas>
</div>
</div>
<div class="lg:w-80 flex-shrink-0 space-y-4">
<div class="flex items-center justify-between">
<h3
class="text-lg font-semibold text-white flex items-center gap-2"
>
<i data-lucide="palette" class="w-5 h-5"></i>
<span data-i18n="tools:adjustColors.colorSettings"
>Color Settings</span
>
</h3>
<button
id="reset-settings-btn"
class="text-xs text-gray-300 hover:text-white border border-gray-600 hover:border-gray-500 px-3 py-1 rounded-lg transition-colors"
>
Reset
</button>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.brightness"
>Brightness</span
>
<span id="brightness-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-brightness"
min="-100"
max="100"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.contrast"
>Contrast</span
>
<span id="contrast-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-contrast"
min="-100"
max="100"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.saturation"
>Saturation</span
>
<span id="saturation-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-saturation"
min="-100"
max="100"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.hueShift"
>Hue Shift</span
>
<span id="hue-shift-value" class="text-gray-400"></span>
</div>
<input
type="range"
id="setting-hue-shift"
min="0"
max="360"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.temperature"
>Temperature</span
>
<span id="temperature-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-temperature"
min="-50"
max="50"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.tint"
>Tint</span
>
<span id="tint-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-tint"
min="-50"
max="50"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.gamma"
>Gamma</span
>
<span id="gamma-value" class="text-gray-400">1.0</span>
</div>
<input
type="range"
id="setting-gamma"
min="0.2"
max="5.0"
value="1.0"
step="0.1"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:adjustColors.sepia"
>Sepia</span
>
<span id="sepia-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-sepia"
min="0"
max="100"
value="0"
class="w-full accent-indigo-500"
/>
</div>
</div>
</div>
<button
id="process-btn"
class="btn-gradient w-full mt-6"
data-i18n="tools:adjustColors.processButton"
>
Apply Color Adjustments
</button>
</div>
</div>
</div>
<div
id="loader-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
>
<div
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
>
<div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium">
Processing...
</p>
</div>
</div>
<div
id="alert-modal"
class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden"
>
<div
class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700"
>
<h3
id="alert-title"
class="text-xl font-bold text-white mb-2"
data-i18n="alert.title"
>
Alert
</h3>
<p id="alert-message" class="text-gray-300 mb-6"></p>
<button
id="alert-ok"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
>
OK
</button>
</div>
</div>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-8 text-center">
How It Works
</h2>
<div class="space-y-6">
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
1
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Upload File</h3>
<p class="text-gray-400">
Click or drag and drop your PDF file to begin
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
2
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Adjust Colors</h3>
<p class="text-gray-400">
Fine-tune color settings with real-time preview
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
3
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Download</h3>
<p class="text-gray-400">Save your color-adjusted PDF instantly</p>
</div>
</div>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-6 text-center">
Related PDF Tools
</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<a
href="pdf-to-greyscale.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">PDF to Greyscale</h3>
<p class="text-gray-400 text-sm">Free online greyscale tool</p>
</a>
<a
href="invert-colors.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Invert Colors</h3>
<p class="text-gray-400 text-sm">Free online invert colors tool</p>
</a>
<a
href="scanner-effect.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Scanner Effect</h3>
<p class="text-gray-400 text-sm">Make PDFs look scanned</p>
</a>
<a
href="background-color.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Background Color</h3>
<p class="text-gray-400 text-sm">Change PDF background</p>
</a>
<a
href="change-text-color.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Text Color</h3>
<p class="text-gray-400 text-sm">Change PDF text color</p>
</a>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-6 text-center">
Frequently Asked Questions
</h2>
<div class="space-y-4">
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Is adjust colors really free?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
Yes! BentoPDF is 100% free with no hidden fees, no signup required,
and unlimited file processing.
</p>
</details>
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Are my files private and secure?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
Absolutely! All processing happens in your browser. Your files never
leave your device, ensuring complete privacy.
</p>
</details>
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Is there a file size limit?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
No! Process files of any size, as many times as you want, completely
free.
</p>
</details>
</div>
</section>
{{> footer }}
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
<script type="module" src="/src/js/utils/full-width.ts"></script>
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
<script type="module" src="/src/version.ts"></script>
<script type="module" src="/src/js/logic/adjust-colors-page.ts"></script>
<script type="module" src="/src/js/mobileMenu.ts"></script>
<script type="module" src="/src/js/main.ts"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Adjust Colors - BentoPDF",
"applicationCategory": "PDF Tool",
"operatingSystem": "Any - Web Browser",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"ratingCount": "1856"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to adjust PDF colors online",
"description": "Learn how to adjust PDF colors using BentoPDF",
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Upload File",
"text": "Click or drag and drop your PDF file"
},
{
"@type": "HowToStep",
"position": 2,
"name": "Adjust Colors",
"text": "Fine-tune brightness, contrast, saturation, hue and more with live preview"
},
{
"@type": "HowToStep",
"position": 3,
"name": "Download",
"text": "Download your color-adjusted PDF"
}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.bentopdf.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Adjust Colors",
"item": "https://www.bentopdf.com/adjust-colors"
}
]
}
</script>
</body>
</html>

View File

@@ -0,0 +1,659 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scanner Effect Online Free - Scanner Effect Tool | BentoPDF</title>
<meta
name="title"
content="Scanner Effect Online Free - Scanner Effect Tool | BentoPDF"
/>
<meta
name="description"
content="★ Scanner Effect online free - Make PDFs look scanned ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser ★ Fast & secure"
/>
<meta
name="keywords"
content="scanner effect, scan pdf, make pdf look scanned, pdf scanner"
/>
<meta name="author" content="BentoPDF" />
<meta
name="robots"
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
/>
<link rel="canonical" href="https://www.bentopdf.com/scanner-effect.html" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/scanner-effect" />
<meta
property="og:title"
content="Scanner Effect Online Free - Scanner Effect Tool | BentoPDF"
/>
<meta
property="og:description"
content="★ Scanner Effect online free - Make PDFs look scanned ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser ★ Fast & secure"
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-scanner-effect.png"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="BentoPDF" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:url"
content="https://www.bentopdf.com/scanner-effect"
/>
<meta name="twitter:title" content="Scanner Effect Free" />
<meta
name="twitter:description"
content="★ Scanner Effect online free - Make PDFs look scanned ★ No signup ★ Unlimited files ★ Privacy-first ★ Works in browser ★ Fast & secu"
/>
<meta
name="twitter:image"
content="https://www.bentopdf.com/images/twitter-scanner-effect.png"
/>
<meta name="twitter:site" content="@BentoPDF" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Scanner Effect" />
<title>Scanner Effect - BentoPDF</title>
<meta
name="description"
content="Make your PDF look like a scanned document. Adjust noise, brightness, contrast, blur, and more. Free, secure, and runs entirely in your browser."
/>
<link href="/src/css/styles.css" rel="stylesheet" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/images/favicon-512x512.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/apple-touch-icon.png"
/>
<link rel="icon" href="/favicon.ico" sizes="32x32" />
</head>
<body class="antialiased bg-gray-900">
{{> navbar }}
<div
id="uploader"
class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900"
>
<div
id="tool-uploader"
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-4xl w-full text-gray-200 border border-gray-700"
>
<button
id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
>
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer" data-i18n="tools.backToTools">
Back to Tools
</span>
</button>
<h1
class="text-2xl font-bold text-white mb-2"
data-i18n="tools:scannerEffect.name"
>
Scanner Effect Free Online - Make PDFs Look Scanned
</h1>
<p class="text-gray-400 mb-6" data-i18n="tools:scannerEffect.subtitle">
Make your PDF look like a scanned document.
</p>
<div
id="drop-zone"
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300"
>
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i
data-lucide="upload-cloud"
class="w-10 h-10 mb-3 text-gray-400"
></i>
<p class="mb-2 text-sm text-gray-400">
<span class="font-semibold" data-i18n="upload.clickToSelect"
>Click to select a file</span
>
<span data-i18n="upload.orDragAndDrop">or drag and drop</span>
</p>
<p class="text-xs text-gray-500">A single PDF file</p>
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">
Your files never leave your device.
</p>
</div>
<input
id="file-input"
type="file"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
accept="application/pdf"
/>
</div>
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="options-panel" class="hidden mt-6">
<div class="flex flex-col lg:flex-row gap-6">
<div class="flex-1 min-w-0">
<div
class="border border-gray-600 rounded-lg overflow-hidden bg-gray-900"
>
<canvas id="preview-canvas" class="w-full"></canvas>
</div>
</div>
<div class="lg:w-80 flex-shrink-0 space-y-4">
<div class="flex items-center justify-between">
<h3
class="text-lg font-semibold text-white flex items-center gap-2"
>
<i data-lucide="scan" class="w-5 h-5"></i>
<span data-i18n="tools:scannerEffect.scanSettings"
>Scan Settings</span
>
</h3>
<button
id="reset-settings-btn"
class="text-xs text-gray-300 hover:text-white border border-gray-600 hover:border-gray-500 px-3 py-1 rounded-lg transition-colors"
>
Reset
</button>
</div>
<div class="flex items-center justify-between">
<span
class="text-sm text-gray-300"
data-i18n="tools:scannerEffect.colorspace"
>Colorspace</span
>
<label class="flex items-center gap-2 cursor-pointer">
<span
class="text-xs text-gray-400"
data-i18n="tools:scannerEffect.gray"
>Gray</span
>
<div class="relative">
<input
type="checkbox"
id="setting-grayscale"
class="sr-only peer"
/>
<div
class="w-10 h-5 bg-gray-600 peer-checked:bg-indigo-600 rounded-full transition-colors"
></div>
<div
class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"
></div>
</div>
</label>
</div>
<div class="flex items-center justify-between">
<span
class="text-sm text-gray-300"
data-i18n="tools:scannerEffect.border"
>Border</span
>
<label class="flex items-center gap-2 cursor-pointer">
<div class="relative">
<input
type="checkbox"
id="setting-border"
class="sr-only peer"
/>
<div
class="w-10 h-5 bg-gray-600 peer-checked:bg-indigo-600 rounded-full transition-colors"
></div>
<div
class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"
></div>
</div>
</label>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.rotate"
>Rotate</span
>
<span id="rotate-value" class="text-gray-400"></span>
</div>
<input
type="range"
id="setting-rotate"
min="-5"
max="5"
value="0"
step="0.1"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.rotateVariance"
>Rotate Variance</span
>
<span id="rotate-variance-value" class="text-gray-400"
></span
>
</div>
<input
type="range"
id="setting-rotate-variance"
min="0"
max="3"
value="0"
step="0.1"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.brightness"
>Brightness</span
>
<span id="brightness-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-brightness"
min="-50"
max="50"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.contrast"
>Contrast</span
>
<span id="contrast-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-contrast"
min="-50"
max="50"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.blur"
>Blur</span
>
<span id="blur-value" class="text-gray-400">0px</span>
</div>
<input
type="range"
id="setting-blur"
min="0"
max="3"
value="0"
step="0.1"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.noise"
>Noise</span
>
<span id="noise-value" class="text-gray-400">10</span>
</div>
<input
type="range"
id="setting-noise"
min="0"
max="50"
value="10"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.yellowish"
>Yellowish</span
>
<span id="yellowish-value" class="text-gray-400">0</span>
</div>
<input
type="range"
id="setting-yellowish"
min="0"
max="50"
value="0"
class="w-full accent-indigo-500"
/>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span
class="text-gray-300"
data-i18n="tools:scannerEffect.resolution"
>Resolution</span
>
<span id="resolution-value" class="text-gray-400"
>150 DPI</span
>
</div>
<input
type="range"
id="setting-resolution"
min="72"
max="300"
value="150"
class="w-full accent-indigo-500"
/>
</div>
</div>
</div>
<button
id="process-btn"
class="btn-gradient w-full mt-6"
data-i18n="tools:scannerEffect.processButton"
>
Apply Scanner Effect
</button>
</div>
</div>
</div>
<div
id="loader-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
>
<div
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
>
<div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium">
Processing...
</p>
</div>
</div>
<div
id="alert-modal"
class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden"
>
<div
class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700"
>
<h3
id="alert-title"
class="text-xl font-bold text-white mb-2"
data-i18n="alert.title"
>
Alert
</h3>
<p id="alert-message" class="text-gray-300 mb-6"></p>
<button
id="alert-ok"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
>
OK
</button>
</div>
</div>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-8 text-center">
How It Works
</h2>
<div class="space-y-6">
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
1
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Upload File</h3>
<p class="text-gray-400">
Click or drag and drop your PDF file to begin
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
2
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">
Adjust Settings
</h3>
<p class="text-gray-400">
Customize the scanner effect with real-time preview
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
>
3
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Download</h3>
<p class="text-gray-400">Save your scanned-looking PDF instantly</p>
</div>
</div>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-6 text-center">
Related PDF Tools
</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<a
href="pdf-to-greyscale.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">PDF to Greyscale</h3>
<p class="text-gray-400 text-sm">Free online greyscale tool</p>
</a>
<a
href="invert-colors.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Invert Colors</h3>
<p class="text-gray-400 text-sm">Free online invert colors tool</p>
</a>
<a
href="compress-pdf.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Compress PDF</h3>
<p class="text-gray-400 text-sm">Free online compress pdf tool</p>
</a>
<a
href="rotate-pdf.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Rotate PDF</h3>
<p class="text-gray-400 text-sm">Free online rotate pdf tool</p>
</a>
<a
href="edit-pdf.html"
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
>
<h3 class="text-white font-semibold mb-1">Edit PDF</h3>
<p class="text-gray-400 text-sm">Free online edit pdf tool</p>
</a>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-6 text-center">
Frequently Asked Questions
</h2>
<div class="space-y-4">
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Is scanner effect really free?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
Yes! BentoPDF is 100% free with no hidden fees, no signup required,
and unlimited file processing.
</p>
</details>
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Are my files private and secure?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
Absolutely! All processing happens in your browser. Your files never
leave your device, ensuring complete privacy.
</p>
</details>
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
<summary
class="cursor-pointer font-semibold text-white flex items-center justify-between"
>
Is there a file size limit?
<i data-lucide="chevron-down" class="w-5 h-5"></i>
</summary>
<p class="mt-3 text-gray-400">
No! Process files of any size, as many times as you want, completely
free.
</p>
</details>
</div>
</section>
{{> footer }}
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
<script type="module" src="/src/js/utils/full-width.ts"></script>
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
<script type="module" src="/src/version.ts"></script>
<script type="module" src="/src/js/logic/scanner-effect-page.ts"></script>
<script type="module" src="/src/js/mobileMenu.ts"></script>
<script type="module" src="/src/js/main.ts"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Scanner Effect - BentoPDF",
"applicationCategory": "PDF Tool",
"operatingSystem": "Any - Web Browser",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"ratingCount": "2143"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to apply scanner effect online",
"description": "Learn how to make a PDF look scanned using BentoPDF",
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Upload File",
"text": "Click or drag and drop your PDF file"
},
{
"@type": "HowToStep",
"position": 2,
"name": "Adjust Settings",
"text": "Customize noise, brightness, contrast and more with live preview"
},
{
"@type": "HowToStep",
"position": 3,
"name": "Download",
"text": "Download your scanned-looking PDF"
}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.bentopdf.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Scanner Effect",
"item": "https://www.bentopdf.com/scanner-effect"
}
]
}
</script>
</body>
</html>

View File

@@ -404,6 +404,8 @@ export default defineConfig(() => {
'add-watermark': resolve(__dirname, 'src/pages/add-watermark.html'), 'add-watermark': resolve(__dirname, 'src/pages/add-watermark.html'),
'header-footer': resolve(__dirname, 'src/pages/header-footer.html'), 'header-footer': resolve(__dirname, 'src/pages/header-footer.html'),
'invert-colors': resolve(__dirname, 'src/pages/invert-colors.html'), 'invert-colors': resolve(__dirname, 'src/pages/invert-colors.html'),
'scanner-effect': resolve(__dirname, 'src/pages/scanner-effect.html'),
'adjust-colors': resolve(__dirname, 'src/pages/adjust-colors.html'),
'background-color': resolve( 'background-color': resolve(
__dirname, __dirname,
'src/pages/background-color.html' 'src/pages/background-color.html'