2045 lines
81 KiB
HTML
2045 lines
81 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>pdf2scan — сделать скан из PDF онлайн бесплатно</title>
|
||
<meta name="description" content="Сделайте скан из PDF онлайн — бесплатно, без регистрации. Добавьте эффект сканирования, сшивку (нитка, ленточка, пружинка, уголок, дырокол), нумерацию страниц и лист «прошито-пронумеровано».">
|
||
<meta name="keywords" content="сделать скан из pdf онлайн, pdf в скан онлайн, сканирование pdf бесплатно, pdf с эффектом сканирования, прошить документ онлайн, сшивка документов онлайн, растровый pdf онлайн, pdf2scan">
|
||
<link rel="canonical" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="ru" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="en" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="de" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="fr" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="it" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="es" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="pt" href="https://pdf2scan.online/">
|
||
<link rel="alternate" hreflang="x-default" href="https://pdf2scan.online/">
|
||
<meta property="og:title" content="pdf2scan — сделать скан из PDF онлайн бесплатно">
|
||
<meta property="og:description" content="Сделайте скан из PDF онлайн — бесплатно, без регистрации. Сшивка, нумерация, эффект сканирования в один клик.">
|
||
<meta property="og:url" content="https://pdf2scan.online/">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:image" content="https://pdf2scan.online/static/logo.png">
|
||
<meta property="og:image:width" content="640">
|
||
<meta property="og:image:height" content="640">
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="pdf2scan — сделать скан из PDF онлайн бесплатно">
|
||
<meta name="twitter:description" content="Сделайте скан из PDF онлайн — бесплатно, без регистрации. Сшивка, нумерация, эффект сканирования в один клик.">
|
||
<meta name="twitter:image" content="https://pdf2scan.online/static/logo.png">
|
||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||
<link rel="shortcut icon" href="/favicon.ico">
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "WebApplication",
|
||
"name": "pdf2scan",
|
||
"url": "https://pdf2scan.online",
|
||
"description": "Бесплатный онлайн-инструмент для создания PDF с эффектом сканирования. Поддерживает сшивку нитками, ленточкой, уголком, пружинкой и дыроколом. Нумерация страниц, титульный лист, PDF/A.",
|
||
"applicationCategory": "UtilitiesApplication",
|
||
"operatingSystem": "Any",
|
||
"browserRequirements": "Requires JavaScript",
|
||
"inLanguage": ["ru", "en", "de", "fr", "it", "es", "pt"],
|
||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "RUB" },
|
||
"featureList": [
|
||
"Эффект сканирования PDF онлайн",
|
||
"Сшивка нитками, ленточкой, уголком, пружинкой, дыроколом",
|
||
"Нумерация страниц",
|
||
"Титульный лист",
|
||
"Лист прошито и пронумеровано",
|
||
"Формат PDF/A",
|
||
"Цветной и чёрно-белый режим",
|
||
"Конвертация Word в PDF со сканированием"
|
||
],
|
||
"creator": { "@type": "Organization", "name": "pdf2scan", "url": "https://pdf2scan.online" }
|
||
}
|
||
</script>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--blue: #007AFF;
|
||
--blue-hover: #0066D6;
|
||
--blue-light: #E8F2FF;
|
||
--gray: #8E8E93;
|
||
--gray-light: #F2F2F7;
|
||
--border: #D1D1D6;
|
||
--text: #1C1C1E;
|
||
--text-secondary: #636366;
|
||
--radius: 14px;
|
||
--radius-sm: 10px;
|
||
--shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
|
||
background: #F2F2F7;
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
/* ── Header ── */
|
||
header {
|
||
width: 100%;
|
||
background: rgba(255,255,255,0.85);
|
||
backdrop-filter: saturate(180%) blur(20px);
|
||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
padding: 8px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* ── Language switcher ── */
|
||
#lang-switcher {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
#lang-btn {
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 5px 8px;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
#lang-btn:hover { background: var(--gray-light); }
|
||
|
||
#lang-btn .lang-arrow {
|
||
font-size: 9px;
|
||
color: var(--gray);
|
||
line-height: 1;
|
||
}
|
||
|
||
.flag-img {
|
||
width: 20px;
|
||
height: 15px;
|
||
border-radius: 2px;
|
||
display: block;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.lang-option .flag-img {
|
||
width: 22px;
|
||
height: 16px;
|
||
}
|
||
|
||
#lang-dropdown {
|
||
display: none;
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
left: 0;
|
||
background: white;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow);
|
||
overflow: hidden;
|
||
min-width: 150px;
|
||
z-index: 200;
|
||
}
|
||
|
||
#lang-dropdown.open { display: block; }
|
||
|
||
.lang-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 9px 14px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background 0.1s;
|
||
border: none;
|
||
background: none;
|
||
width: 100%;
|
||
text-align: left;
|
||
font-family: inherit;
|
||
color: var(--text);
|
||
}
|
||
|
||
.lang-option:hover { background: var(--gray-light); }
|
||
.lang-option.active { background: var(--blue-light); color: var(--blue); font-weight: 600; }
|
||
|
||
.lang-option .lang-flag { font-size: 18px; }
|
||
|
||
.logo-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--blue);
|
||
letter-spacing: -0.3px;
|
||
}
|
||
|
||
.logo-subtitle {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
margin-top: 1px;
|
||
}
|
||
|
||
/* ── Telegram auth in header ── */
|
||
#header-auth {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
#tg-user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: var(--text);
|
||
}
|
||
|
||
#tg-user-info img {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
#tg-logout-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 12px;
|
||
color: var(--gray);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
padding: 0;
|
||
}
|
||
|
||
#tg-login-header-btn {
|
||
padding: 7px 14px;
|
||
background: var(--blue);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
#tg-login-header-btn:hover { background: var(--blue-hover); }
|
||
|
||
/* ── Main card ── */
|
||
main {
|
||
width: 100%;
|
||
max-width: 640px;
|
||
padding: 14px 16px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-sm);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 9px 20px 7px;
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text-secondary);
|
||
letter-spacing: 0.2px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* ── Drop zone ── */
|
||
.drop-zone {
|
||
border: 2px dashed var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 18px 20px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s, background 0.2s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
position: relative;
|
||
}
|
||
|
||
.drop-zone:hover, .drop-zone.drag-over {
|
||
border-color: var(--blue);
|
||
background: var(--blue-light);
|
||
}
|
||
|
||
.drop-zone input[type=file] {
|
||
display: none;
|
||
}
|
||
|
||
.drop-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
background: var(--blue-light);
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.drop-icon svg { fill: var(--blue); }
|
||
|
||
.drop-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.drop-hint {
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.file-badge {
|
||
background: var(--blue-light);
|
||
color: var(--blue);
|
||
border-radius: 8px;
|
||
padding: 6px 14px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
display: none;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ── Settings ── */
|
||
.setting-row {
|
||
padding: 9px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.setting-row:last-child { border-bottom: none; }
|
||
|
||
.setting-label {
|
||
font-size: 15px;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Segmented control */
|
||
.seg {
|
||
display: flex;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.seg input[type=radio] { display: none; }
|
||
|
||
.seg label {
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
transition: background 0.15s, color 0.15s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.seg label:not(:last-of-type) { border-right: 1px solid var(--border); }
|
||
|
||
.seg input[type=radio]:checked + label {
|
||
background: var(--blue);
|
||
color: white;
|
||
}
|
||
|
||
/* Stitch grid */
|
||
.stitch-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
}
|
||
|
||
.stitch-grid input[type=radio] { display: none; }
|
||
|
||
.stitch-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 6px 4px;
|
||
border: 2px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
transition: border-color 0.15s, background 0.15s;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stitch-btn .icon { font-size: 20px; }
|
||
|
||
.stitch-grid input[type=radio]:checked + .stitch-btn {
|
||
border-color: var(--blue);
|
||
background: var(--blue-light);
|
||
color: var(--blue);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Toggle switch */
|
||
.toggle {
|
||
position: relative;
|
||
width: 51px;
|
||
height: 31px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.toggle input { display: none; }
|
||
|
||
.toggle-track {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 100px;
|
||
background: var(--border);
|
||
transition: background 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.toggle-track::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 2px;
|
||
width: 27px;
|
||
height: 27px;
|
||
border-radius: 50%;
|
||
background: white;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.toggle input:checked + .toggle-track {
|
||
background: var(--blue);
|
||
}
|
||
|
||
.toggle input:checked + .toggle-track::after {
|
||
transform: translateX(20px);
|
||
}
|
||
|
||
/* ── Auth banner ── */
|
||
#auth-banner {
|
||
display: none;
|
||
background: linear-gradient(135deg, var(--blue) 0%, #5AC8FA 100%);
|
||
color: white;
|
||
border-radius: var(--radius);
|
||
padding: 24px 20px;
|
||
text-align: center;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
#auth-banner h3 { font-size: 18px; font-weight: 700; }
|
||
#auth-banner p { font-size: 14px; opacity: 0.9; line-height: 1.4; }
|
||
|
||
/* ── Submit button ── */
|
||
#submit-btn {
|
||
width: 100%;
|
||
padding: 16px;
|
||
background: var(--blue);
|
||
color: white;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s, transform 0.1s;
|
||
font-family: inherit;
|
||
}
|
||
|
||
#submit-btn:hover { background: var(--blue-hover); }
|
||
#submit-btn:active { transform: scale(0.98); }
|
||
#submit-btn:disabled {
|
||
background: var(--gray);
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
/* ── Progress ── */
|
||
#progress-card { display: none; }
|
||
|
||
.progress-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 16px 20px 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.progress-label span:last-child { color: var(--blue); font-weight: 600; }
|
||
|
||
.progress-bar-track {
|
||
height: 8px;
|
||
background: var(--gray-light);
|
||
border-radius: 100px;
|
||
margin: 0 20px 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar-fill {
|
||
height: 100%;
|
||
background: var(--blue);
|
||
border-radius: 100px;
|
||
width: 0%;
|
||
transition: width 0.4s ease;
|
||
}
|
||
|
||
.progress-speed {
|
||
padding: 0 20px 14px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
min-height: 18px;
|
||
}
|
||
|
||
/* ── Result ── */
|
||
#result-card { display: none; }
|
||
|
||
.result-content {
|
||
padding: 14px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.success-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
background: #34C759;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.success-icon svg { fill: white; }
|
||
|
||
.result-title { font-size: 20px; font-weight: 700; }
|
||
.result-sub { font-size: 14px; color: var(--text-secondary); }
|
||
|
||
#download-btn {
|
||
padding: 14px 40px;
|
||
background: #34C759;
|
||
color: white;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: opacity 0.15s;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
}
|
||
|
||
#download-btn:hover { opacity: 0.85; }
|
||
|
||
#another-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--blue);
|
||
font-size: 15px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
}
|
||
|
||
/* ── Error ── */
|
||
.error-msg {
|
||
background: #FFF0F0;
|
||
border: 1px solid #FFD6D6;
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px 16px;
|
||
color: #C00;
|
||
font-size: 14px;
|
||
display: none;
|
||
}
|
||
|
||
/* ── Info ── */
|
||
.info-msg {
|
||
background: var(--blue-light);
|
||
border: 1px solid #B3D4FF;
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px 16px;
|
||
color: var(--blue);
|
||
font-size: 14px;
|
||
display: none;
|
||
}
|
||
|
||
/* ── Donate card ── */
|
||
#donate-card {
|
||
display: none;
|
||
background: linear-gradient(135deg, #fffbf0 0%, #fff8e6 100%);
|
||
border: 1px solid #ffe0a0;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.donate-inner {
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18px;
|
||
}
|
||
|
||
.donate-text { flex: 1; min-width: 0; }
|
||
|
||
.donate-icon { font-size: 22px; margin-bottom: 2px; }
|
||
|
||
.donate-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #b45309;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.donate-desc {
|
||
font-size: 13px;
|
||
color: #92400e;
|
||
line-height: 1.4;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.donate-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.donate-btn {
|
||
padding: 7px 14px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
transition: opacity 0.15s;
|
||
}
|
||
|
||
.donate-btn:hover { opacity: 0.85; }
|
||
|
||
.donate-btn-sbp {
|
||
background: #1a56db;
|
||
color: white;
|
||
}
|
||
|
||
.donate-qr {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.donate-qr img {
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 8px;
|
||
border: 1px solid #ffe0a0;
|
||
background: white;
|
||
padding: 4px;
|
||
display: block;
|
||
}
|
||
|
||
.donate-qr-hint {
|
||
font-size: 10px;
|
||
color: #92400e;
|
||
text-align: center;
|
||
max-width: 100px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
@media (max-width: 420px) {
|
||
.donate-inner { flex-direction: column; }
|
||
.donate-qr { flex-direction: row; align-items: center; gap: 12px; width: 100%; }
|
||
.donate-qr img { width: 80px; height: 80px; }
|
||
.donate-qr-hint { max-width: none; }
|
||
}
|
||
|
||
/* ── SEO section ── */
|
||
#seo-section {
|
||
width: 100%;
|
||
max-width: 640px;
|
||
padding: 0 16px 8px;
|
||
}
|
||
|
||
.seo-heading {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin-bottom: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.seo-steps {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.seo-step {
|
||
background: white;
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px 10px;
|
||
text-align: center;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.seo-step-num {
|
||
width: 28px;
|
||
height: 28px;
|
||
background: var(--blue);
|
||
color: white;
|
||
border-radius: 50%;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto 6px;
|
||
}
|
||
|
||
.seo-step-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.seo-step-desc {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.seo-about {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.55;
|
||
text-align: center;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
/* ── Footer ── */
|
||
footer {
|
||
padding: 10px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<!-- Language switcher -->
|
||
<div id="lang-switcher">
|
||
<button id="lang-btn" onclick="toggleLangDropdown(event)" aria-label="Select language">
|
||
<img id="lang-flag-img" class="flag-img" src="/static/flags/ru.png" alt="RU">
|
||
<span class="lang-arrow">▼</span>
|
||
</button>
|
||
<div id="lang-dropdown">
|
||
<button class="lang-option" onclick="setLang('ru')"><img class="flag-img" src="/static/flags/ru.png" alt="RU"> Русский</button>
|
||
<button class="lang-option" onclick="setLang('en')"><img class="flag-img" src="/static/flags/gb.png" alt="GB"> English</button>
|
||
<button class="lang-option" onclick="setLang('de')"><img class="flag-img" src="/static/flags/de.png" alt="DE"> Deutsch</button>
|
||
<button class="lang-option" onclick="setLang('fr')"><img class="flag-img" src="/static/flags/fr.png" alt="FR"> Français</button>
|
||
<button class="lang-option" onclick="setLang('it')"><img class="flag-img" src="/static/flags/it.png" alt="IT"> Italiano</button>
|
||
<button class="lang-option" onclick="setLang('es')"><img class="flag-img" src="/static/flags/es.png" alt="ES"> Español</button>
|
||
<button class="lang-option" onclick="setLang('pt')"><img class="flag-img" src="/static/flags/pt.png" alt="PT"> Português</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="logo-icon">
|
||
<img src="/static/logo.png" width="40" height="40" alt="pdf2scan logo" style="display:block;">
|
||
</div>
|
||
<div>
|
||
<div class="logo-text">pdf2scan</div>
|
||
<div class="logo-subtitle" data-i18n="logo_subtitle">Создание растровых pdf с эффектом сканирования из электронных pdf документов</div>
|
||
</div>
|
||
|
||
<div id="header-auth">
|
||
<button id="tg-login-header-btn" onclick="startTgAuth()" style="display:none;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221l-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.447 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.333-.373-.12L8.32 13.617l-2.96-.924c-.643-.204-.657-.643.136-.953l11.57-4.461c.537-.194 1.006.131.828.942z"/></svg>
|
||
<span data-i18n="login_btn">Войти</span>
|
||
</button>
|
||
<div id="tg-user-info" style="display:none;">
|
||
<img id="tg-user-photo" src="" alt="" onerror="this.style.display='none'">
|
||
<span id="tg-user-name"></span>
|
||
<button id="tg-logout-btn" onclick="logoutTg()" data-i18n="logout_btn">Выйти</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
|
||
<!-- Description -->
|
||
<p data-i18n="page_description" style="font-size:13px; line-height:1.45; color:var(--text-secondary); padding: 0 4px;">
|
||
🧷 Выберите тип сшивки и при необходимости дополнительные настройки, затем загрузите файл — получите готовый скан.
|
||
</p>
|
||
|
||
<!-- Upload -->
|
||
<div class="card">
|
||
<div class="card-title" data-i18n="card_file">Файл</div>
|
||
<div style="padding: 12px;">
|
||
<div class="drop-zone" id="drop-zone">
|
||
<input type="file" id="file-input" accept=".pdf,.doc,.docx" style="display:none" />
|
||
<div class="drop-icon">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
||
</svg>
|
||
</div>
|
||
<div class="drop-title" data-i18n="drop_title">Выберите PDF или Word</div>
|
||
<div class="drop-hint" data-i18n="drop_hint">или перетащите файл сюда</div>
|
||
<div class="file-badge" id="file-badge">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stitching -->
|
||
<div class="card">
|
||
<div class="card-title" data-i18n="card_stitch">Тип сшивки</div>
|
||
<div class="stitch-grid">
|
||
|
||
<input type="radio" name="stitching" id="s-thread" value="нитка">
|
||
<label class="stitch-btn" for="s-thread"><span class="icon">🧵</span><span data-i18n="stitch_thread">Нитка</span></label>
|
||
|
||
<input type="radio" name="stitching" id="s-ribbon" value="ленточка">
|
||
<label class="stitch-btn" for="s-ribbon"><span class="icon">🎀</span><span data-i18n="stitch_ribbon">Ленточка</span></label>
|
||
|
||
<input type="radio" name="stitching" id="s-corner" value="уголок">
|
||
<label class="stitch-btn" for="s-corner"><span class="icon">📐</span><span data-i18n="stitch_corner">Уголок</span></label>
|
||
|
||
<input type="radio" name="stitching" id="s-spiral" value="пружинка">
|
||
<label class="stitch-btn" for="s-spiral"><span class="icon">🌀</span><span data-i18n="stitch_spiral">Пружинка</span></label>
|
||
|
||
<input type="radio" name="stitching" id="s-punch" value="дырокол">
|
||
<label class="stitch-btn" for="s-punch"><span class="icon">⭕</span><span data-i18n="stitch_punch">Дырокол</span></label>
|
||
|
||
<input type="radio" name="stitching" id="s-none" value="без сшивки" checked>
|
||
<label class="stitch-btn" for="s-none"><span class="icon">📄</span><span data-i18n="stitch_none">Без сшивки</span></label>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Extra settings -->
|
||
<div class="card">
|
||
<div class="card-title" data-i18n="card_extra">Дополнительно</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="color_mode">Режим цвета</span>
|
||
<div class="seg">
|
||
<input type="radio" name="color" id="c-color" value="color" checked>
|
||
<label for="c-color" data-i18n="color_color">Цветной</label>
|
||
<input type="radio" name="color" id="c-bw" value="bw">
|
||
<label for="c-bw" data-i18n="color_bw">Ч/Б</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="quality">Качество</span>
|
||
<div class="seg">
|
||
<input type="radio" name="quality" id="q-high" value="high" checked>
|
||
<label for="q-high" data-i18n="quality_high">Высокое</label>
|
||
<input type="radio" name="quality" id="q-medium" value="medium">
|
||
<label for="q-medium" data-i18n="quality_medium">Среднее</label>
|
||
<input type="radio" name="quality" id="q-low" value="low">
|
||
<label for="q-low" data-i18n="quality_low">Низкое</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="title_page">Титульный лист</span>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="title-check">
|
||
<span class="toggle-track"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="page_numbers">Нумерация страниц</span>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="num-check">
|
||
<span class="toggle-track"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="certified_page">Лист «прошито-пронумеровано»</span>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="flip-check">
|
||
<span class="toggle-track"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="setting-row">
|
||
<span class="setting-label" data-i18n="pdfa_format">Формат PDF/A</span>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="pdfa-check">
|
||
<span class="toggle-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auth banner -->
|
||
<div id="auth-banner">
|
||
<h3 data-i18n="auth_required">🔐 Требуется авторизация</h3>
|
||
<p data-i18n="auth_required_desc">Для файлов больше 100 страниц или 50 МБ необходим вход через Telegram.</p>
|
||
<button onclick="startTgAuth()" style="padding:10px 24px;background:white;color:#007AFF;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;" data-i18n="login_tg">
|
||
Войти через Telegram
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Info -->
|
||
<div class="info-msg" id="info-msg"></div>
|
||
|
||
<!-- Error -->
|
||
<div class="error-msg" id="error-msg"></div>
|
||
|
||
<!-- Submit -->
|
||
<button id="submit-btn" disabled data-i18n="btn_select_file">Выберите файл</button>
|
||
|
||
<!-- Progress -->
|
||
<div class="card" id="progress-card">
|
||
<div class="progress-label">
|
||
<span id="progress-status" data-i18n="progress_processing">Обработка...</span>
|
||
<span id="progress-pct"></span>
|
||
</div>
|
||
<div class="progress-bar-track">
|
||
<div class="progress-bar-fill" id="progress-fill"></div>
|
||
</div>
|
||
<div class="progress-speed" id="progress-speed"></div>
|
||
</div>
|
||
|
||
<!-- Result -->
|
||
<div class="card" id="result-card">
|
||
<div class="result-content">
|
||
<div class="success-icon">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||
<polyline points="20 6 9 17 4 12" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||
</svg>
|
||
</div>
|
||
<div class="result-title" data-i18n="result_title">Готово!</div>
|
||
<div class="result-sub" id="result-sub" data-i18n="result_sub">Ваш PDF обработан и готов к скачиванию.</div>
|
||
<div style="font-size:13px;color:#FF9500;background:#FFF8EC;border-radius:8px;padding:8px 14px;" data-i18n="result_timer">
|
||
⏱ Ссылка активна 30 минут — скачайте файл сейчас
|
||
</div>
|
||
<a id="download-btn" href="#" download data-i18n="download_btn">⬇ Скачать результат</a>
|
||
<button id="another-btn" onclick="resetApp()" data-i18n="another_btn">Обработать ещё один файл</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Donate -->
|
||
<div id="donate-card">
|
||
<div class="donate-inner">
|
||
<div class="donate-text">
|
||
<div class="donate-icon">🍩</div>
|
||
<div class="donate-title" data-i18n="donate_title">Поддержите проект</div>
|
||
<div class="donate-desc" data-i18n="donate_desc">Если сервис полезен — поддержите его работу донатом. Это помогает оплачивать сервер и развивать проект.</div>
|
||
<div class="donate-buttons">
|
||
<a class="donate-btn donate-btn-sbp" href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019cbe28-6b54-7eb8-8a52-189633e608ea" target="_blank" rel="noopener" data-i18n="donate_sbp">СБП / Ozon Pay</a>
|
||
</div>
|
||
</div>
|
||
<div class="donate-qr">
|
||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https%3A%2F%2Ffinance.ozon.ru%2Fapps%2Fsbp%2Fozonbankpay%2F019cbe28-6b54-7eb8-8a52-189633e608ea" alt="QR donate" loading="lazy">
|
||
<div class="donate-qr-hint" data-i18n="donate_qr_hint">Сканируйте для оплаты</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<section id="seo-section">
|
||
<h2 class="seo-heading" data-i18n="seo_title">Как работает pdf2scan</h2>
|
||
<div class="seo-steps">
|
||
<div class="seo-step">
|
||
<div class="seo-step-num">1</div>
|
||
<div class="seo-step-title" data-i18n="seo_step1_title">Загрузите файл</div>
|
||
<div class="seo-step-desc" data-i18n="seo_step1_desc">PDF или Word — перетащите или выберите с устройства</div>
|
||
</div>
|
||
<div class="seo-step">
|
||
<div class="seo-step-num">2</div>
|
||
<div class="seo-step-title" data-i18n="seo_step2_title">Настройте параметры</div>
|
||
<div class="seo-step-desc" data-i18n="seo_step2_desc">Тип сшивки, цвет, качество, нумерация и другие опции</div>
|
||
</div>
|
||
<div class="seo-step">
|
||
<div class="seo-step-num">3</div>
|
||
<div class="seo-step-title" data-i18n="seo_step3_title">Скачайте результат</div>
|
||
<div class="seo-step-desc" data-i18n="seo_step3_desc">Готовый PDF со сканом доступен для скачивания 30 минут</div>
|
||
</div>
|
||
</div>
|
||
<p class="seo-about" data-i18n="seo_about">pdf2scan — бесплатный онлайн-инструмент для создания растрового PDF с эффектом сканирования. Поддерживает сшивку нитками, ленточкой, уголком, пружинкой и дыроколом. Идеально для подготовки документов к сдаче в архив, нотариусу или государственные органы. Без регистрации, без установки программ.</p>
|
||
</section>
|
||
|
||
<footer>
|
||
pdf2scan — <span data-i18n="footer_text">превращаем PDF в скан</span> | <a href="https://t.me/pdf2scan_bot" style="color:var(--blue); text-decoration:none;" data-i18n="footer_tg">Telegram-бот</a>
|
||
</footer>
|
||
|
||
<script>
|
||
// ── Translations ──────────────────────────────────────────────────────────
|
||
const LANGS = {
|
||
ru: { img: '/static/flags/ru.png', alt: 'RU', name: 'Русский' },
|
||
en: { img: '/static/flags/gb.png', alt: 'GB', name: 'English' },
|
||
de: { img: '/static/flags/de.png', alt: 'DE', name: 'Deutsch' },
|
||
fr: { img: '/static/flags/fr.png', alt: 'FR', name: 'Français' },
|
||
it: { img: '/static/flags/it.png', alt: 'IT', name: 'Italiano' },
|
||
es: { img: '/static/flags/es.png', alt: 'ES', name: 'Español' },
|
||
pt: { img: '/static/flags/pt.png', alt: 'PT', name: 'Português' },
|
||
};
|
||
|
||
const TR = {
|
||
ru: {
|
||
page_title: 'pdf2scan — онлайн сканирование PDF',
|
||
logo_subtitle: 'Создание растровых pdf с эффектом сканирования из электронных pdf документов',
|
||
login_btn: 'Войти',
|
||
logout_btn: 'Выйти',
|
||
page_description: '🧷 Выберите тип сшивки и при необходимости дополнительные настройки, затем загрузите файл — получите готовый скан.',
|
||
card_file: 'Файл',
|
||
drop_title: 'Выберите PDF или Word',
|
||
drop_hint: 'или перетащите файл сюда',
|
||
file_selected: 'Файл выбран',
|
||
card_stitch: 'Тип сшивки',
|
||
stitch_thread: 'Нитка',
|
||
stitch_ribbon: 'Ленточка',
|
||
stitch_corner: 'Уголок',
|
||
stitch_spiral: 'Пружинка',
|
||
stitch_punch: 'Дырокол',
|
||
stitch_none: 'Без сшивки',
|
||
card_extra: 'Дополнительно',
|
||
color_mode: 'Режим цвета',
|
||
color_color: 'Цветной',
|
||
color_bw: 'Ч/Б',
|
||
quality: 'Качество',
|
||
quality_high: 'Высокое',
|
||
quality_medium: 'Среднее',
|
||
quality_low: 'Низкое',
|
||
title_page: 'Титульный лист',
|
||
page_numbers: 'Нумерация страниц',
|
||
certified_page: 'Лист «прошито-пронумеровано»',
|
||
pdfa_format: 'Формат PDF/A',
|
||
auth_required: '🔐 Требуется авторизация',
|
||
auth_required_desc:'Для файлов больше 100 страниц или 50 МБ необходим вход через Telegram.',
|
||
login_tg: 'Войти через Telegram',
|
||
btn_select_file: 'Выберите файл',
|
||
btn_process: 'Обработать PDF',
|
||
btn_sending: 'Отправка...',
|
||
btn_processing: 'Обработка...',
|
||
btn_auth_required: 'Необходима авторизация',
|
||
progress_loading: 'Загрузка файла...',
|
||
progress_uploaded: 'Файл загружен, ожидание обработки...',
|
||
progress_processing:'Обработка...',
|
||
result_title: 'Готово!',
|
||
result_sub: 'Ваш PDF обработан и готов к скачиванию.',
|
||
result_timer: '⏱ Ссылка активна 30 минут — скачайте файл сейчас',
|
||
download_btn: '⬇ Скачать результат',
|
||
another_btn: 'Обработать ещё один файл',
|
||
footer_text: 'превращаем PDF в скан',
|
||
footer_tg: 'Telegram-бот',
|
||
err_no_file: 'Файл не выбран',
|
||
err_task_not_found:'Задача не найдена. Попробуйте отправить файл снова.',
|
||
err_network: 'Ошибка сети при загрузке файла',
|
||
err_timeout: 'Превышено время ожидания',
|
||
err_auth_start: 'Не удалось начать авторизацию.',
|
||
err_auth_expired: 'Время авторизации истекло. Попробуйте снова.',
|
||
err_auth_launch: 'Ошибка при запуске авторизации.',
|
||
err_processing: 'Произошла ошибка обработки.',
|
||
info_auth_waiting: 'Откройте Telegram-бота и нажмите Start — авторизация произойдёт автоматически...',
|
||
queue_text: (pos) => `В очереди (${pos} ${pluralizeRu(pos, 'задание', 'задания', 'заданий')})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} МБ · ${speed} МБ/с`,
|
||
err_server: (code) => `Ошибка ${code}`,
|
||
donate_title: 'Поддержите проект',
|
||
donate_desc: 'Если сервис полезен — поддержите его работу донатом. Это помогает оплачивать сервер и развивать проект.',
|
||
donate_desc_heavy: '⚠️ Большие файлы сильно нагружают сервер. Если сервис полезен — поддержите проект донатом.',
|
||
donate_sbp: 'СБП / Ozon Pay',
|
||
donate_qr_hint: 'Сканируйте для оплаты',
|
||
seo_title: 'Как работает pdf2scan',
|
||
seo_step1_title: 'Загрузите файл',
|
||
seo_step1_desc: 'PDF или Word — перетащите или выберите с устройства',
|
||
seo_step2_title: 'Настройте параметры',
|
||
seo_step2_desc: 'Тип сшивки, цвет, качество, нумерация и другие опции',
|
||
seo_step3_title: 'Скачайте результат',
|
||
seo_step3_desc: 'Готовый PDF со сканом доступен для скачивания 30 минут',
|
||
seo_about: 'pdf2scan — бесплатный онлайн-инструмент для создания растрового PDF с эффектом сканирования. Поддерживает сшивку нитками, ленточкой, уголком, пружинкой и дыроколом. Идеально для подготовки документов к сдаче в архив, нотариусу или государственные органы. Без регистрации, без установки программ.',
|
||
},
|
||
en: {
|
||
page_title: 'pdf2scan — online PDF scan',
|
||
logo_subtitle: 'Convert PDF to scanned document online',
|
||
login_btn: 'Login',
|
||
logout_btn: 'Logout',
|
||
page_description: '🧷 Select binding type and optional settings, then upload your file — get a ready scan.',
|
||
card_file: 'File',
|
||
drop_title: 'Select PDF or Word',
|
||
drop_hint: 'or drag and drop file here',
|
||
file_selected: 'File selected',
|
||
card_stitch: 'Binding type',
|
||
stitch_thread: 'Thread',
|
||
stitch_ribbon: 'Ribbon',
|
||
stitch_corner: 'Corner',
|
||
stitch_spiral: 'Spiral',
|
||
stitch_punch: 'Hole punch',
|
||
stitch_none: 'No binding',
|
||
card_extra: 'Options',
|
||
color_mode: 'Color mode',
|
||
color_color: 'Color',
|
||
color_bw: 'B&W',
|
||
quality: 'Quality',
|
||
quality_high: 'High',
|
||
quality_medium: 'Medium',
|
||
quality_low: 'Low',
|
||
title_page: 'Title page',
|
||
page_numbers: 'Page numbers',
|
||
certified_page: 'Certification page',
|
||
pdfa_format: 'PDF/A format',
|
||
auth_required: '🔐 Authorization required',
|
||
auth_required_desc:'Files with more than 100 pages or 50 MB require Telegram login.',
|
||
login_tg: 'Login with Telegram',
|
||
btn_select_file: 'Select file',
|
||
btn_process: 'Process PDF',
|
||
btn_sending: 'Uploading...',
|
||
btn_processing: 'Processing...',
|
||
btn_auth_required: 'Authorization required',
|
||
progress_loading: 'Uploading file...',
|
||
progress_uploaded: 'File uploaded, waiting for processing...',
|
||
progress_processing:'Processing...',
|
||
result_title: 'Done!',
|
||
result_sub: 'Your PDF has been processed and is ready to download.',
|
||
result_timer: '⏱ Link active for 30 minutes — download now',
|
||
download_btn: '⬇ Download result',
|
||
another_btn: 'Process another file',
|
||
footer_text: 'convert PDF to scan',
|
||
footer_tg: 'Telegram bot',
|
||
err_no_file: 'No file selected',
|
||
err_task_not_found:'Task not found. Please try submitting the file again.',
|
||
err_network: 'Network error while uploading',
|
||
err_timeout: 'Request timed out',
|
||
err_auth_start: 'Failed to start authorization.',
|
||
err_auth_expired: 'Authorization expired. Please try again.',
|
||
err_auth_launch: 'Error launching authorization.',
|
||
err_processing: 'An error occurred during processing.',
|
||
info_auth_waiting: 'Open the Telegram bot and press Start — authorization will happen automatically...',
|
||
queue_text: (pos) => `In queue (position ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} MB · ${speed} MB/s`,
|
||
err_server: (code) => `Error ${code}`,
|
||
donate_title: 'Support the project',
|
||
donate_desc: 'If this service is useful to you, please consider supporting it with a donation.',
|
||
donate_desc_heavy: '⚠️ Large files put heavy load on the server. If the service is useful, please support the project.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Scan to pay',
|
||
seo_title: 'How pdf2scan works',
|
||
seo_step1_title: 'Upload your file',
|
||
seo_step1_desc: 'PDF or Word — drag & drop or choose from device',
|
||
seo_step2_title: 'Configure options',
|
||
seo_step2_desc: 'Binding type, color, quality, page numbers and more',
|
||
seo_step3_title: 'Download the result',
|
||
seo_step3_desc: 'Your scanned PDF is ready to download for 30 minutes',
|
||
seo_about: 'pdf2scan is a free online tool to create raster PDFs with a scan effect. Supports thread, ribbon, corner, spiral and hole-punch binding. Perfect for preparing documents for archives, notaries or government agencies. No registration, no software required.',
|
||
},
|
||
de: {
|
||
page_title: 'pdf2scan — Online-PDF-Scan',
|
||
logo_subtitle: 'PDF online in gescannte Dokumente umwandeln',
|
||
login_btn: 'Anmelden',
|
||
logout_btn: 'Abmelden',
|
||
page_description: '🧷 Wählen Sie den Bindungstyp und optionale Einstellungen, laden Sie dann Ihre Datei hoch — erhalten Sie einen fertigen Scan.',
|
||
card_file: 'Datei',
|
||
drop_title: 'PDF oder Word auswählen',
|
||
drop_hint: 'oder Datei hier ablegen',
|
||
file_selected: 'Datei ausgewählt',
|
||
card_stitch: 'Bindungstyp',
|
||
stitch_thread: 'Faden',
|
||
stitch_ribbon: 'Band',
|
||
stitch_corner: 'Ecke',
|
||
stitch_spiral: 'Spirale',
|
||
stitch_punch: 'Lochstanzer',
|
||
stitch_none: 'Ohne Bindung',
|
||
card_extra: 'Optionen',
|
||
color_mode: 'Farbmodus',
|
||
color_color: 'Farbe',
|
||
color_bw: 'S/W',
|
||
quality: 'Qualität',
|
||
quality_high: 'Hoch',
|
||
quality_medium: 'Mittel',
|
||
quality_low: 'Niedrig',
|
||
title_page: 'Titelseite',
|
||
page_numbers: 'Seitennummerierung',
|
||
certified_page: 'Zertifizierungsseite',
|
||
pdfa_format: 'PDF/A-Format',
|
||
auth_required: '🔐 Anmeldung erforderlich',
|
||
auth_required_desc:'Dateien mit mehr als 100 Seiten oder 50 MB erfordern eine Telegram-Anmeldung.',
|
||
login_tg: 'Mit Telegram anmelden',
|
||
btn_select_file: 'Datei auswählen',
|
||
btn_process: 'PDF verarbeiten',
|
||
btn_sending: 'Hochladen...',
|
||
btn_processing: 'Verarbeitung...',
|
||
btn_auth_required: 'Anmeldung erforderlich',
|
||
progress_loading: 'Datei wird hochgeladen...',
|
||
progress_uploaded: 'Datei hochgeladen, warte auf Verarbeitung...',
|
||
progress_processing:'Verarbeitung...',
|
||
result_title: 'Fertig!',
|
||
result_sub: 'Ihr PDF wurde verarbeitet und ist zum Download bereit.',
|
||
result_timer: '⏱ Link 30 Minuten aktiv — jetzt herunterladen',
|
||
download_btn: '⬇ Ergebnis herunterladen',
|
||
another_btn: 'Eine weitere Datei verarbeiten',
|
||
footer_text: 'PDF in Scan umwandeln',
|
||
footer_tg: 'Telegram-Bot',
|
||
err_no_file: 'Keine Datei ausgewählt',
|
||
err_task_not_found:'Aufgabe nicht gefunden. Bitte versuchen Sie, die Datei erneut zu senden.',
|
||
err_network: 'Netzwerkfehler beim Hochladen',
|
||
err_timeout: 'Zeitüberschreitung',
|
||
err_auth_start: 'Autorisierung konnte nicht gestartet werden.',
|
||
err_auth_expired: 'Autorisierung abgelaufen. Bitte erneut versuchen.',
|
||
err_auth_launch: 'Fehler beim Starten der Autorisierung.',
|
||
err_processing: 'Bei der Verarbeitung ist ein Fehler aufgetreten.',
|
||
info_auth_waiting: 'Öffnen Sie den Telegram-Bot und drücken Sie Start — die Autorisierung erfolgt automatisch...',
|
||
queue_text: (pos) => `In der Warteschlange (Position ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} MB · ${speed} MB/s`,
|
||
err_server: (code) => `Fehler ${code}`,
|
||
donate_title: 'Projekt unterstützen',
|
||
donate_desc: 'Wenn dieser Dienst nützlich ist, unterstützen Sie ihn bitte mit einer Spende.',
|
||
donate_desc_heavy: '⚠️ Große Dateien belasten den Server stark. Wenn der Dienst nützlich ist, unterstützen Sie das Projekt.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Zum Bezahlen scannen',
|
||
seo_title: 'So funktioniert pdf2scan',
|
||
seo_step1_title: 'Datei hochladen',
|
||
seo_step1_desc: 'PDF oder Word — per Drag & Drop oder vom Gerät',
|
||
seo_step2_title: 'Optionen einstellen',
|
||
seo_step2_desc: 'Bindungstyp, Farbe, Qualität, Seitennummern und mehr',
|
||
seo_step3_title: 'Ergebnis herunterladen',
|
||
seo_step3_desc: 'Ihr gescanntes PDF steht 30 Minuten zum Download bereit',
|
||
seo_about: 'pdf2scan ist ein kostenloses Online-Tool zur Erstellung von Raster-PDFs mit Scan-Effekt. Unterstützt Faden-, Band-, Ecken-, Spiral- und Lochbindung. Perfekt für die Vorbereitung von Dokumenten für Archive, Notare oder Behörden. Keine Registrierung, keine Software erforderlich.',
|
||
},
|
||
fr: {
|
||
page_title: 'pdf2scan — scan PDF en ligne',
|
||
logo_subtitle: 'Convertir des PDF en documents scannés en ligne',
|
||
login_btn: 'Connexion',
|
||
logout_btn: 'Déconnexion',
|
||
page_description: '🧷 Sélectionnez le type de reliure et les paramètres optionnels, puis téléchargez votre fichier — obtenez un scan prêt à l\'emploi.',
|
||
card_file: 'Fichier',
|
||
drop_title: 'Sélectionner PDF ou Word',
|
||
drop_hint: 'ou déposez le fichier ici',
|
||
file_selected: 'Fichier sélectionné',
|
||
card_stitch: 'Type de reliure',
|
||
stitch_thread: 'Fil',
|
||
stitch_ribbon: 'Ruban',
|
||
stitch_corner: 'Coin',
|
||
stitch_spiral: 'Spirale',
|
||
stitch_punch: 'Perforateur',
|
||
stitch_none: 'Sans reliure',
|
||
card_extra: 'Options',
|
||
color_mode: 'Mode couleur',
|
||
color_color: 'Couleur',
|
||
color_bw: 'N/B',
|
||
quality: 'Qualité',
|
||
quality_high: 'Haute',
|
||
quality_medium: 'Moyenne',
|
||
quality_low: 'Basse',
|
||
title_page: 'Page de titre',
|
||
page_numbers: 'Numérotation des pages',
|
||
certified_page: 'Page de certification',
|
||
pdfa_format: 'Format PDF/A',
|
||
auth_required: '🔐 Autorisation requise',
|
||
auth_required_desc:'Les fichiers de plus de 100 pages ou 50 Mo nécessitent une connexion Telegram.',
|
||
login_tg: 'Se connecter via Telegram',
|
||
btn_select_file: 'Sélectionner un fichier',
|
||
btn_process: 'Traiter le PDF',
|
||
btn_sending: 'Envoi...',
|
||
btn_processing: 'Traitement...',
|
||
btn_auth_required: 'Autorisation requise',
|
||
progress_loading: 'Téléchargement du fichier...',
|
||
progress_uploaded: 'Fichier téléchargé, en attente de traitement...',
|
||
progress_processing:'Traitement...',
|
||
result_title: 'Terminé!',
|
||
result_sub: 'Votre PDF a été traité et est prêt à être téléchargé.',
|
||
result_timer: '⏱ Lien actif pendant 30 minutes — téléchargez maintenant',
|
||
download_btn: '⬇ Télécharger le résultat',
|
||
another_btn: 'Traiter un autre fichier',
|
||
footer_text: 'convertir PDF en scan',
|
||
footer_tg: 'Bot Telegram',
|
||
err_no_file: 'Aucun fichier sélectionné',
|
||
err_task_not_found:'Tâche introuvable. Veuillez soumettre le fichier à nouveau.',
|
||
err_network: 'Erreur réseau lors du téléchargement',
|
||
err_timeout: 'Délai d\'attente dépassé',
|
||
err_auth_start: 'Impossible de démarrer l\'autorisation.',
|
||
err_auth_expired: 'Autorisation expirée. Veuillez réessayer.',
|
||
err_auth_launch: 'Erreur lors du lancement de l\'autorisation.',
|
||
err_processing: 'Une erreur s\'est produite lors du traitement.',
|
||
info_auth_waiting: 'Ouvrez le bot Telegram et appuyez sur Démarrer — l\'autorisation se fera automatiquement...',
|
||
queue_text: (pos) => `En attente (position ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} Mo · ${speed} Mo/s`,
|
||
err_server: (code) => `Erreur ${code}`,
|
||
donate_title: 'Soutenir le projet',
|
||
donate_desc: 'Si ce service vous est utile, soutenez-le avec un don.',
|
||
donate_desc_heavy: '⚠️ Les gros fichiers sollicitent fortement le serveur. Si le service est utile, soutenez le projet.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Scannez pour payer',
|
||
seo_title: 'Comment fonctionne pdf2scan',
|
||
seo_step1_title: 'Téléchargez votre fichier',
|
||
seo_step1_desc: 'PDF ou Word — glisser-déposer ou choisir depuis l\'appareil',
|
||
seo_step2_title: 'Configurez les options',
|
||
seo_step2_desc: 'Type de reliure, couleur, qualité, numérotation et plus',
|
||
seo_step3_title: 'Téléchargez le résultat',
|
||
seo_step3_desc: 'Votre PDF scanné est disponible 30 minutes au téléchargement',
|
||
seo_about: 'pdf2scan est un outil en ligne gratuit pour créer des PDF raster avec effet de scan. Supporte la reliure par fil, ruban, coin, spirale et perforateur. Idéal pour préparer des documents pour les archives, notaires ou administrations. Sans inscription, sans logiciel.',
|
||
},
|
||
it: {
|
||
page_title: 'pdf2scan — scansione PDF online',
|
||
logo_subtitle: 'Converti PDF in documenti scansionati online',
|
||
login_btn: 'Accedi',
|
||
logout_btn: 'Esci',
|
||
page_description: '🧷 Seleziona il tipo di rilegatura e le impostazioni opzionali, poi carica il file — ottieni una scansione pronta.',
|
||
card_file: 'File',
|
||
drop_title: 'Seleziona PDF o Word',
|
||
drop_hint: 'o trascina il file qui',
|
||
file_selected: 'File selezionato',
|
||
card_stitch: 'Tipo di rilegatura',
|
||
stitch_thread: 'Filo',
|
||
stitch_ribbon: 'Nastro',
|
||
stitch_corner: 'Angolo',
|
||
stitch_spiral: 'Spirale',
|
||
stitch_punch: 'Perforatore',
|
||
stitch_none: 'Senza rilegatura',
|
||
card_extra: 'Opzioni',
|
||
color_mode: 'Modalità colore',
|
||
color_color: 'Colore',
|
||
color_bw: 'B/N',
|
||
quality: 'Qualità',
|
||
quality_high: 'Alta',
|
||
quality_medium: 'Media',
|
||
quality_low: 'Bassa',
|
||
title_page: 'Pagina del titolo',
|
||
page_numbers: 'Numerazione pagine',
|
||
certified_page: 'Pagina di certificazione',
|
||
pdfa_format: 'Formato PDF/A',
|
||
auth_required: '🔐 Autorizzazione richiesta',
|
||
auth_required_desc:'I file con più di 100 pagine o 50 MB richiedono il login Telegram.',
|
||
login_tg: 'Accedi con Telegram',
|
||
btn_select_file: 'Seleziona file',
|
||
btn_process: 'Elabora PDF',
|
||
btn_sending: 'Caricamento...',
|
||
btn_processing: 'Elaborazione...',
|
||
btn_auth_required: 'Autorizzazione richiesta',
|
||
progress_loading: 'Caricamento file...',
|
||
progress_uploaded: 'File caricato, in attesa di elaborazione...',
|
||
progress_processing:'Elaborazione...',
|
||
result_title: 'Pronto!',
|
||
result_sub: 'Il tuo PDF è stato elaborato ed è pronto per il download.',
|
||
result_timer: '⏱ Link attivo per 30 minuti — scarica ora',
|
||
download_btn: '⬇ Scarica il risultato',
|
||
another_btn: 'Elabora un altro file',
|
||
footer_text: 'converti PDF in scansione',
|
||
footer_tg: 'Bot Telegram',
|
||
err_no_file: 'Nessun file selezionato',
|
||
err_task_not_found:'Attività non trovata. Prova a inviare nuovamente il file.',
|
||
err_network: 'Errore di rete durante il caricamento',
|
||
err_timeout: 'Timeout della richiesta',
|
||
err_auth_start: 'Impossibile avviare l\'autorizzazione.',
|
||
err_auth_expired: 'Autorizzazione scaduta. Riprova.',
|
||
err_auth_launch: 'Errore nell\'avvio dell\'autorizzazione.',
|
||
err_processing: 'Si è verificato un errore durante l\'elaborazione.',
|
||
info_auth_waiting: 'Apri il bot Telegram e premi Start — l\'autorizzazione avverrà automaticamente...',
|
||
queue_text: (pos) => `In coda (posizione ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} MB · ${speed} MB/s`,
|
||
err_server: (code) => `Errore ${code}`,
|
||
donate_title: 'Sostieni il progetto',
|
||
donate_desc: 'Se questo servizio ti è utile, considera di supportarlo con una donazione.',
|
||
donate_desc_heavy: '⚠️ I file grandi caricano molto il server. Se il servizio è utile, sostieni il progetto.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Scansiona per pagare',
|
||
seo_title: 'Come funziona pdf2scan',
|
||
seo_step1_title: 'Carica il file',
|
||
seo_step1_desc: 'PDF o Word — trascina o scegli dal dispositivo',
|
||
seo_step2_title: 'Configura le opzioni',
|
||
seo_step2_desc: 'Tipo di rilegatura, colore, qualità, numerazione e altro',
|
||
seo_step3_title: 'Scarica il risultato',
|
||
seo_step3_desc: 'Il tuo PDF scansionato è disponibile per 30 minuti',
|
||
seo_about: 'pdf2scan è uno strumento online gratuito per creare PDF raster con effetto scansione. Supporta rilegatura con filo, nastro, angolo, spirale e perforatore. Ideale per preparare documenti per archivi, notai o enti pubblici. Senza registrazione, senza software.',
|
||
},
|
||
es: {
|
||
page_title: 'pdf2scan — escaneo PDF en línea',
|
||
logo_subtitle: 'Convierte PDF en documentos escaneados en línea',
|
||
login_btn: 'Iniciar sesión',
|
||
logout_btn: 'Cerrar sesión',
|
||
page_description: '🧷 Selecciona el tipo de encuadernación y configuraciones opcionales, luego sube tu archivo — obtén un escaneo listo.',
|
||
card_file: 'Archivo',
|
||
drop_title: 'Seleccionar PDF o Word',
|
||
drop_hint: 'o arrastra el archivo aquí',
|
||
file_selected: 'Archivo seleccionado',
|
||
card_stitch: 'Tipo de encuadernación',
|
||
stitch_thread: 'Hilo',
|
||
stitch_ribbon: 'Cinta',
|
||
stitch_corner: 'Esquina',
|
||
stitch_spiral: 'Espiral',
|
||
stitch_punch: 'Perforadora',
|
||
stitch_none: 'Sin encuadernación',
|
||
card_extra: 'Opciones',
|
||
color_mode: 'Modo de color',
|
||
color_color: 'Color',
|
||
color_bw: 'B/N',
|
||
quality: 'Calidad',
|
||
quality_high: 'Alta',
|
||
quality_medium: 'Media',
|
||
quality_low: 'Baja',
|
||
title_page: 'Página de título',
|
||
page_numbers: 'Numeración de páginas',
|
||
certified_page: 'Página de certificación',
|
||
pdfa_format: 'Formato PDF/A',
|
||
auth_required: '🔐 Autorización requerida',
|
||
auth_required_desc:'Los archivos con más de 100 páginas o 50 MB requieren inicio de sesión en Telegram.',
|
||
login_tg: 'Iniciar sesión con Telegram',
|
||
btn_select_file: 'Seleccionar archivo',
|
||
btn_process: 'Procesar PDF',
|
||
btn_sending: 'Enviando...',
|
||
btn_processing: 'Procesando...',
|
||
btn_auth_required: 'Autorización requerida',
|
||
progress_loading: 'Subiendo archivo...',
|
||
progress_uploaded: 'Archivo subido, esperando procesamiento...',
|
||
progress_processing:'Procesando...',
|
||
result_title: '¡Listo!',
|
||
result_sub: 'Tu PDF ha sido procesado y está listo para descargar.',
|
||
result_timer: '⏱ Enlace activo por 30 minutos — descarga ahora',
|
||
download_btn: '⬇ Descargar resultado',
|
||
another_btn: 'Procesar otro archivo',
|
||
footer_text: 'convierte PDF en escaneo',
|
||
footer_tg: 'Bot de Telegram',
|
||
err_no_file: 'Ningún archivo seleccionado',
|
||
err_task_not_found:'Tarea no encontrada. Intenta enviar el archivo de nuevo.',
|
||
err_network: 'Error de red al subir',
|
||
err_timeout: 'Tiempo de espera agotado',
|
||
err_auth_start: 'No se pudo iniciar la autorización.',
|
||
err_auth_expired: 'Autorización expirada. Inténtalo de nuevo.',
|
||
err_auth_launch: 'Error al iniciar la autorización.',
|
||
err_processing: 'Se produjo un error durante el procesamiento.',
|
||
info_auth_waiting: 'Abre el bot de Telegram y pulsa Iniciar — la autorización ocurrirá automáticamente...',
|
||
queue_text: (pos) => `En cola (posición ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} MB · ${speed} MB/s`,
|
||
err_server: (code) => `Error ${code}`,
|
||
donate_title: 'Apoya el proyecto',
|
||
donate_desc: 'Si este servicio te es útil, considera apoyarlo con una donación.',
|
||
donate_desc_heavy: '⚠️ Los archivos grandes sobrecargan el servidor. Si el servicio es útil, apoya el proyecto.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Escanea para pagar',
|
||
seo_title: 'Cómo funciona pdf2scan',
|
||
seo_step1_title: 'Sube tu archivo',
|
||
seo_step1_desc: 'PDF o Word — arrastra o elige desde el dispositivo',
|
||
seo_step2_title: 'Configura las opciones',
|
||
seo_step2_desc: 'Tipo de encuadernación, color, calidad, numeración y más',
|
||
seo_step3_title: 'Descarga el resultado',
|
||
seo_step3_desc: 'Tu PDF escaneado está disponible 30 minutos para descargar',
|
||
seo_about: 'pdf2scan es una herramienta online gratuita para crear PDFs rasterizados con efecto de escáner. Soporta encuadernación con hilo, cinta, esquina, espiral y perforadora. Ideal para preparar documentos para archivos, notarios o administraciones. Sin registro, sin software.',
|
||
},
|
||
pt: {
|
||
page_title: 'pdf2scan — digitalização PDF online',
|
||
logo_subtitle: 'Converta PDF em documentos digitalizados online',
|
||
login_btn: 'Entrar',
|
||
logout_btn: 'Sair',
|
||
page_description: '🧷 Selecione o tipo de encadernação e configurações opcionais, depois faça upload do arquivo — obtenha uma digitalização pronta.',
|
||
card_file: 'Arquivo',
|
||
drop_title: 'Selecionar PDF ou Word',
|
||
drop_hint: 'ou arraste o arquivo aqui',
|
||
file_selected: 'Arquivo selecionado',
|
||
card_stitch: 'Tipo de encadernação',
|
||
stitch_thread: 'Fio',
|
||
stitch_ribbon: 'Fita',
|
||
stitch_corner: 'Canto',
|
||
stitch_spiral: 'Espiral',
|
||
stitch_punch: 'Furador',
|
||
stitch_none: 'Sem encadernação',
|
||
card_extra: 'Opções',
|
||
color_mode: 'Modo de cor',
|
||
color_color: 'Colorido',
|
||
color_bw: 'P/B',
|
||
quality: 'Qualidade',
|
||
quality_high: 'Alta',
|
||
quality_medium: 'Média',
|
||
quality_low: 'Baixa',
|
||
title_page: 'Página de título',
|
||
page_numbers: 'Numeração de páginas',
|
||
certified_page: 'Página de certificação',
|
||
pdfa_format: 'Formato PDF/A',
|
||
auth_required: '🔐 Autorização necessária',
|
||
auth_required_desc:'Arquivos com mais de 100 páginas ou 50 MB requerem login no Telegram.',
|
||
login_tg: 'Entrar com Telegram',
|
||
btn_select_file: 'Selecionar arquivo',
|
||
btn_process: 'Processar PDF',
|
||
btn_sending: 'Enviando...',
|
||
btn_processing: 'Processando...',
|
||
btn_auth_required: 'Autorização necessária',
|
||
progress_loading: 'Fazendo upload...',
|
||
progress_uploaded: 'Arquivo enviado, aguardando processamento...',
|
||
progress_processing:'Processando...',
|
||
result_title: 'Pronto!',
|
||
result_sub: 'Seu PDF foi processado e está pronto para download.',
|
||
result_timer: '⏱ Link ativo por 30 minutos — baixe agora',
|
||
download_btn: '⬇ Baixar resultado',
|
||
another_btn: 'Processar outro arquivo',
|
||
footer_text: 'converta PDF em digitalização',
|
||
footer_tg: 'Bot do Telegram',
|
||
err_no_file: 'Nenhum arquivo selecionado',
|
||
err_task_not_found:'Tarefa não encontrada. Tente enviar o arquivo novamente.',
|
||
err_network: 'Erro de rede ao fazer upload',
|
||
err_timeout: 'Tempo limite esgotado',
|
||
err_auth_start: 'Não foi possível iniciar a autorização.',
|
||
err_auth_expired: 'Autorização expirada. Tente novamente.',
|
||
err_auth_launch: 'Erro ao iniciar a autorização.',
|
||
err_processing: 'Ocorreu um erro durante o processamento.',
|
||
info_auth_waiting: 'Abra o bot do Telegram e pressione Iniciar — a autorização ocorrerá automaticamente...',
|
||
queue_text: (pos) => `Na fila (posição ${pos})`,
|
||
upload_speed: (loaded, total, speed) => `${loaded} / ${total} MB · ${speed} MB/s`,
|
||
err_server: (code) => `Erro ${code}`,
|
||
donate_title: 'Apoie o projeto',
|
||
donate_desc: 'Se este serviço é útil para você, considere apoiá-lo com uma doação.',
|
||
donate_desc_heavy: '⚠️ Arquivos grandes sobrecarregam o servidor. Se o serviço é útil, apoie o projeto.',
|
||
donate_sbp: 'SBP / Ozon Pay',
|
||
donate_yoo: 'YooMoney',
|
||
donate_qr_hint: 'Escaneie para pagar',
|
||
seo_title: 'Como funciona o pdf2scan',
|
||
seo_step1_title: 'Envie seu arquivo',
|
||
seo_step1_desc: 'PDF ou Word — arraste ou escolha do dispositivo',
|
||
seo_step2_title: 'Configure as opções',
|
||
seo_step2_desc: 'Tipo de encadernação, cor, qualidade, numeração e mais',
|
||
seo_step3_title: 'Baixe o resultado',
|
||
seo_step3_desc: 'Seu PDF digitalizado fica disponível por 30 minutos',
|
||
seo_about: 'pdf2scan é uma ferramenta online gratuita para criar PDFs rasterizados com efeito de digitalização. Suporta encadernação com fio, fita, canto, espiral e furador. Ideal para preparar documentos para arquivos, cartórios ou órgãos públicos. Sem cadastro, sem software.',
|
||
},
|
||
};
|
||
|
||
function pluralizeRu(n, f1, f2, f5) {
|
||
const n10 = n % 10, n100 = n % 100;
|
||
if (n10 === 1 && n100 !== 11) return f1;
|
||
if (n10 >= 2 && n10 <= 4 && (n100 < 10 || n100 >= 20)) return f2;
|
||
return f5;
|
||
}
|
||
|
||
// ── Language management ───────────────────────────────────────────────────
|
||
let currentLang = localStorage.getItem('pdf2scan_lang') || detectLang();
|
||
|
||
function detectLang() {
|
||
const nav = navigator.language || navigator.userLanguage || 'ru';
|
||
const code = nav.slice(0, 2).toLowerCase();
|
||
return TR[code] ? code : 'ru';
|
||
}
|
||
|
||
function t(key) {
|
||
const tr = TR[currentLang] || TR['ru'];
|
||
return tr[key] !== undefined ? tr[key] : (TR['ru'][key] || key);
|
||
}
|
||
|
||
function applyTranslations() {
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
const val = t(key);
|
||
if (typeof val === 'string') el.textContent = val;
|
||
});
|
||
document.title = t('page_title');
|
||
document.documentElement.lang = currentLang;
|
||
// Update flag button
|
||
const flagEl = document.getElementById('lang-flag-img');
|
||
flagEl.src = LANGS[currentLang].img;
|
||
flagEl.alt = LANGS[currentLang].alt;
|
||
// Re-render donate desc if visible
|
||
const donateCard = document.getElementById('donate-card');
|
||
if (donateCard && donateCard.style.display !== 'none') {
|
||
const isHeavy = filePages > DONATE_PAGE_THRESHOLD || fileSizeMb > 10;
|
||
donateCard.querySelector('.donate-desc').textContent = t(isHeavy ? 'donate_desc_heavy' : 'donate_desc');
|
||
}
|
||
// Highlight active language in dropdown
|
||
document.querySelectorAll('.lang-option').forEach(btn => {
|
||
const lang = btn.getAttribute('onclick').match(/setLang\('(\w+)'\)/)[1];
|
||
btn.classList.toggle('active', lang === currentLang);
|
||
});
|
||
// Re-render dynamic button state
|
||
updateSubmitBtn();
|
||
}
|
||
|
||
function setLang(lang) {
|
||
if (!TR[lang]) return;
|
||
currentLang = lang;
|
||
localStorage.setItem('pdf2scan_lang', lang);
|
||
closeLangDropdown();
|
||
applyTranslations();
|
||
}
|
||
|
||
function toggleLangDropdown(e) {
|
||
e.stopPropagation();
|
||
document.getElementById('lang-dropdown').classList.toggle('open');
|
||
}
|
||
|
||
function closeLangDropdown() {
|
||
document.getElementById('lang-dropdown').classList.remove('open');
|
||
}
|
||
|
||
document.addEventListener('click', closeLangDropdown);
|
||
|
||
// ── Donate ────────────────────────────────────────────────────────────────
|
||
const DONATE_PAGE_THRESHOLD = 20;
|
||
|
||
function showDonateCard(heavy) {
|
||
const card = document.getElementById('donate-card');
|
||
const desc = card.querySelector('.donate-desc');
|
||
desc.textContent = t(heavy ? 'donate_desc_heavy' : 'donate_desc');
|
||
card.style.display = 'block';
|
||
}
|
||
|
||
function hideDonateCard() {
|
||
document.getElementById('donate-card').style.display = 'none';
|
||
}
|
||
|
||
// ── App state ─────────────────────────────────────────────────────────────
|
||
const TG_AUTH_KEY = 'pdf2scan_tg_auth';
|
||
|
||
let selectedFile = null;
|
||
let currentJobId = null;
|
||
let pollInterval = null;
|
||
let progressInterval = null;
|
||
let tgUser = null;
|
||
let authRequired = false;
|
||
let processingStartTime = null;
|
||
let filePages = 0;
|
||
let fileSizeMb = 0;
|
||
|
||
// ── Подсчёт страниц PDF на клиенте ───────────────────────────────────────
|
||
async function countPdfPages(file) {
|
||
try {
|
||
const buf = await file.arrayBuffer();
|
||
const text = new TextDecoder('latin1').decode(new Uint8Array(buf));
|
||
const matches = text.match(/\/Type\s*\/Page[^s]/g);
|
||
return matches ? matches.length : 0;
|
||
} catch(e) { return 0; }
|
||
}
|
||
|
||
// ── localStorage сессия ───────────────────────────────────────────────────
|
||
function loadSavedAuth() {
|
||
try {
|
||
const raw = localStorage.getItem(TG_AUTH_KEY);
|
||
if (!raw) return null;
|
||
const data = JSON.parse(raw);
|
||
if (Date.now() / 1000 - (data.saved_at || 0) > 86400) {
|
||
localStorage.removeItem(TG_AUTH_KEY);
|
||
return null;
|
||
}
|
||
return data;
|
||
} catch(e) { return null; }
|
||
}
|
||
|
||
function saveAuth(user) {
|
||
try {
|
||
localStorage.setItem(TG_AUTH_KEY, JSON.stringify({ ...user, saved_at: Date.now() / 1000 }));
|
||
} catch(e) {}
|
||
}
|
||
|
||
function logoutTg() {
|
||
localStorage.removeItem(TG_AUTH_KEY);
|
||
tgUser = null;
|
||
renderHeaderAuth();
|
||
updateSubmitBtn();
|
||
renderAuthBanner();
|
||
}
|
||
|
||
function renderHeaderAuth() {
|
||
const loginBtn = document.getElementById('tg-login-header-btn');
|
||
const userInfo = document.getElementById('tg-user-info');
|
||
if (tgUser) {
|
||
loginBtn.style.display = 'none';
|
||
userInfo.style.display = 'flex';
|
||
document.getElementById('tg-user-name').textContent =
|
||
tgUser.first_name + (tgUser.last_name ? ' ' + tgUser.last_name : '');
|
||
const photo = document.getElementById('tg-user-photo');
|
||
photo.style.display = 'none';
|
||
} else {
|
||
loginBtn.style.display = 'flex';
|
||
userInfo.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ── Авторизация через бота ────────────────────────────────────────────────
|
||
let _authPollInterval = null;
|
||
let _authToken = null;
|
||
let _authWindow = null;
|
||
|
||
async function startTgAuth() {
|
||
try {
|
||
const r = await fetch('/api/auth/token', { method: 'POST' });
|
||
if (!r.ok) { showError(t('err_auth_start')); return; }
|
||
const data = await r.json();
|
||
_authToken = data.token;
|
||
_authWindow = window.open(data.url, '_blank');
|
||
showInfo(t('info_auth_waiting'));
|
||
if (_authPollInterval) clearInterval(_authPollInterval);
|
||
_authPollInterval = setInterval(_pollAuthToken, 2000);
|
||
} catch(e) {
|
||
showError(t('err_auth_launch'));
|
||
}
|
||
}
|
||
|
||
async function _pollAuthToken() {
|
||
if (!_authToken) return;
|
||
try {
|
||
const r = await fetch(`/api/auth/poll/${_authToken}`);
|
||
if (!r.ok) { _stopAuthPoll(); return; }
|
||
const data = await r.json();
|
||
if (data.status === 'verified') {
|
||
_stopAuthPoll();
|
||
tgUser = { user_id: data.user_id, first_name: data.first_name, last_name: data.last_name, username: data.username };
|
||
saveAuth(tgUser);
|
||
renderHeaderAuth();
|
||
renderAuthBanner();
|
||
updateSubmitBtn();
|
||
hideInfo();
|
||
if (_authWindow) { try { _authWindow.close(); } catch(_) {} }
|
||
} else if (data.status === 'expired') {
|
||
_stopAuthPoll();
|
||
showError(t('err_auth_expired'));
|
||
}
|
||
} catch(e) { /* сетевая ошибка, попробуем снова */ }
|
||
}
|
||
|
||
function _stopAuthPoll() {
|
||
if (_authPollInterval) { clearInterval(_authPollInterval); _authPollInterval = null; }
|
||
_authToken = null;
|
||
}
|
||
|
||
// ── Инициализация ─────────────────────────────────────────────────────────
|
||
tgUser = loadSavedAuth();
|
||
renderHeaderAuth();
|
||
|
||
// ── Drop zone ─────────────────────────────────────────────────────────────
|
||
const dropZone = document.getElementById('drop-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
const fileBadge = document.getElementById('file-badge');
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
|
||
applyTranslations(); // после объявления submitBtn — иначе TDZ ReferenceError
|
||
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
dropZone.addEventListener('dragenter', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
||
dropZone.addEventListener('dragover', e => { e.preventDefault(); });
|
||
dropZone.addEventListener('dragleave', e => { if (!dropZone.contains(e.relatedTarget)) dropZone.classList.remove('drag-over'); });
|
||
dropZone.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('drag-over');
|
||
const f = e.dataTransfer.files[0];
|
||
const _allowed = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||
if (f && (_allowed.includes(f.type) || /\.(pdf|doc|docx)$/i.test(f.name))) handleFile(f);
|
||
});
|
||
|
||
fileInput.addEventListener('change', () => {
|
||
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
||
});
|
||
|
||
async function handleFile(f) {
|
||
selectedFile = f;
|
||
fileBadge.textContent = f.name;
|
||
fileBadge.style.display = 'block';
|
||
document.querySelector('.drop-title').textContent = t('file_selected');
|
||
hideError();
|
||
hideDonateCard();
|
||
|
||
fileSizeMb = f.size / (1024 * 1024);
|
||
filePages = await countPdfPages(f);
|
||
await checkAuth(filePages, fileSizeMb);
|
||
}
|
||
|
||
async function checkAuth(pages, sizeMb) {
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('pages', pages);
|
||
fd.append('size_mb', sizeMb);
|
||
const r = await fetch('/api/check', { method: 'POST', body: fd });
|
||
const data = await r.json();
|
||
authRequired = data.requires_auth;
|
||
renderAuthBanner();
|
||
} catch (e) {
|
||
authRequired = false;
|
||
renderAuthBanner();
|
||
}
|
||
updateSubmitBtn();
|
||
}
|
||
|
||
function renderAuthBanner() {
|
||
const banner = document.getElementById('auth-banner');
|
||
if (authRequired && !tgUser) {
|
||
banner.style.display = 'flex';
|
||
} else {
|
||
banner.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function updateSubmitBtn() {
|
||
if (!selectedFile) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = t('btn_select_file');
|
||
} else if (authRequired && !tgUser) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = t('btn_auth_required');
|
||
} else {
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = t('btn_process');
|
||
}
|
||
}
|
||
|
||
// ── Submit ────────────────────────────────────────────────────────────────
|
||
submitBtn.addEventListener('click', submitJob);
|
||
|
||
function submitJob() {
|
||
if (!selectedFile) { showError(t('err_no_file')); return; }
|
||
hideError();
|
||
hideInfo();
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = t('btn_sending');
|
||
|
||
document.getElementById('progress-card').style.display = 'block';
|
||
document.getElementById('result-card').style.display = 'none';
|
||
setProgressPct(0);
|
||
document.getElementById('progress-status').textContent = t('progress_loading');
|
||
document.getElementById('progress-speed').textContent = '';
|
||
|
||
const fd = new FormData();
|
||
fd.append('file', selectedFile);
|
||
fd.append('stitching', getRadio('stitching'));
|
||
fd.append('color', getRadio('color'));
|
||
fd.append('quality', getRadio('quality'));
|
||
fd.append('title', document.getElementById('title-check').checked ? 1 : 0);
|
||
fd.append('numeration', document.getElementById('num-check').checked ? 1 : 0);
|
||
fd.append('flip_side', document.getElementById('flip-check').checked ? 1 : 0);
|
||
fd.append('pdfa', document.getElementById('pdfa-check').checked ? 1 : 0);
|
||
if (tgUser) fd.append('user_id', tgUser.user_id || tgUser.id || 0);
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
const uploadStart = Date.now();
|
||
let lastLoaded = 0, lastTime = uploadStart;
|
||
|
||
xhr.upload.onprogress = function(e) {
|
||
if (!e.lengthComputable) return;
|
||
const pct = Math.round(e.loaded / e.total * 100);
|
||
setProgressPct(pct);
|
||
|
||
const now = Date.now();
|
||
const dt = (now - lastTime) / 1000;
|
||
if (dt >= 0.3) {
|
||
const speedMb = (e.loaded - lastLoaded) / (1024 * 1024) / dt;
|
||
lastLoaded = e.loaded; lastTime = now;
|
||
const loaded = (e.loaded / (1024*1024)).toFixed(1);
|
||
const total = (e.total / (1024*1024)).toFixed(1);
|
||
const speedFn = (TR[currentLang] || TR['ru']).upload_speed;
|
||
document.getElementById('progress-speed').textContent =
|
||
speedFn(loaded, total, speedMb.toFixed(1));
|
||
}
|
||
document.getElementById('progress-status').textContent =
|
||
pct < 100 ? t('progress_loading') : t('progress_uploaded');
|
||
};
|
||
|
||
xhr.onload = function() {
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
try {
|
||
const data = JSON.parse(xhr.responseText);
|
||
currentJobId = data.job_id;
|
||
document.getElementById('progress-speed').textContent = '';
|
||
setUiState('processing');
|
||
startPolling();
|
||
if (filePages > DONATE_PAGE_THRESHOLD || fileSizeMb > 10) {
|
||
showDonateCard(true);
|
||
}
|
||
} catch(e) {
|
||
onUploadError(t('err_server')('?'));
|
||
}
|
||
} else {
|
||
try {
|
||
const d = JSON.parse(xhr.responseText);
|
||
const detail = d.detail;
|
||
const msg = typeof detail === 'string' ? detail : t('err_server')(xhr.status);
|
||
onUploadError(msg);
|
||
} catch(_) { onUploadError(t('err_server')(xhr.status)); }
|
||
}
|
||
};
|
||
|
||
xhr.onerror = function() { onUploadError(t('err_network')); };
|
||
xhr.ontimeout = function() { onUploadError(t('err_timeout')); };
|
||
xhr.timeout = 300000;
|
||
|
||
xhr.open('POST', '/api/submit');
|
||
xhr.send(fd);
|
||
}
|
||
|
||
function onUploadError(msg) {
|
||
showError('⚠ ' + msg);
|
||
document.getElementById('progress-card').style.display = 'none';
|
||
submitBtn.disabled = !selectedFile || (authRequired && !tgUser);
|
||
submitBtn.textContent = t('btn_process');
|
||
}
|
||
|
||
function getRadio(name) {
|
||
const el = document.querySelector(`input[name="${name}"]:checked`);
|
||
return el ? el.value : '';
|
||
}
|
||
|
||
// ── Прогрессбар ───────────────────────────────────────────────────────────
|
||
function startProgressSim() {
|
||
processingStartTime = Date.now();
|
||
if (progressInterval) clearInterval(progressInterval);
|
||
progressInterval = setInterval(() => {
|
||
const elapsed = (Date.now() - processingStartTime) / 1000;
|
||
const pct = Math.round(90 * (1 - Math.exp(-elapsed / 45)));
|
||
setProgressPct(pct);
|
||
}, 500);
|
||
}
|
||
|
||
function stopProgressSim() {
|
||
if (progressInterval) { clearInterval(progressInterval); progressInterval = null; }
|
||
}
|
||
|
||
function setProgressPct(pct) {
|
||
document.getElementById('progress-fill').style.width = pct + '%';
|
||
document.getElementById('progress-pct').textContent = pct + '%';
|
||
}
|
||
|
||
// ── Polling ───────────────────────────────────────────────────────────────
|
||
function startPolling() {
|
||
pollInterval = setInterval(poll, 2000);
|
||
poll();
|
||
}
|
||
|
||
async function poll() {
|
||
if (!currentJobId) return;
|
||
try {
|
||
const r = await fetch(`/api/status/${currentJobId}`);
|
||
if (r.status === 404) {
|
||
clearInterval(pollInterval);
|
||
stopProgressSim();
|
||
showError(t('err_task_not_found'));
|
||
setUiState('idle');
|
||
return;
|
||
}
|
||
if (!r.ok) return;
|
||
const data = await r.json();
|
||
|
||
if (data.status === 'done') {
|
||
clearInterval(pollInterval);
|
||
stopProgressSim();
|
||
setProgressPct(100);
|
||
setTimeout(() => setUiState('done'), 300);
|
||
} else if (data.status === 'error') {
|
||
clearInterval(pollInterval);
|
||
stopProgressSim();
|
||
showError(data.error || t('err_processing'));
|
||
setUiState('idle');
|
||
} else if (data.status === 'queued') {
|
||
const pos = data.queue_pos || '...';
|
||
const queueFn = (TR[currentLang] || TR['ru']).queue_text;
|
||
document.getElementById('progress-status').textContent = queueFn(pos);
|
||
} else {
|
||
document.getElementById('progress-status').textContent = t('progress_processing');
|
||
}
|
||
} catch(e) { /* network glitch */ }
|
||
}
|
||
|
||
// ── UI state ──────────────────────────────────────────────────────────────
|
||
function setUiState(state) {
|
||
const progressCard = document.getElementById('progress-card');
|
||
const resultCard = document.getElementById('result-card');
|
||
|
||
if (state === 'processing') {
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = t('btn_processing');
|
||
progressCard.style.display = 'block';
|
||
resultCard.style.display = 'none';
|
||
document.getElementById('progress-status').textContent = t('progress_processing');
|
||
document.getElementById('progress-speed').textContent = '';
|
||
startProgressSim();
|
||
} else if (state === 'done') {
|
||
progressCard.style.display = 'none';
|
||
resultCard.style.display = 'block';
|
||
const btn = document.getElementById('download-btn');
|
||
btn.href = `/api/download/${currentJobId}`;
|
||
btn.download = (selectedFile ? selectedFile.name.replace(/\.pdf$/i,'') : 'document') + '_scan.pdf';
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = t('btn_process');
|
||
showDonateCard(filePages > DONATE_PAGE_THRESHOLD || fileSizeMb > 10);
|
||
} else {
|
||
submitBtn.disabled = !selectedFile || (authRequired && !tgUser);
|
||
submitBtn.textContent = selectedFile ? t('btn_process') : t('btn_select_file');
|
||
progressCard.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function showError(msg) {
|
||
const el = document.getElementById('error-msg');
|
||
el.textContent = msg.startsWith('⚠') ? msg : '⚠ ' + msg;
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function hideError() {
|
||
document.getElementById('error-msg').style.display = 'none';
|
||
}
|
||
|
||
function showInfo(msg) {
|
||
const el = document.getElementById('info-msg');
|
||
el.textContent = msg;
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function hideInfo() {
|
||
document.getElementById('info-msg').style.display = 'none';
|
||
}
|
||
|
||
function resetApp() {
|
||
currentJobId = null;
|
||
selectedFile = null;
|
||
authRequired = false;
|
||
filePages = 0;
|
||
fileSizeMb = 0;
|
||
fileInput.value = '';
|
||
fileBadge.style.display = 'none';
|
||
document.querySelector('.drop-title').textContent = t('drop_title');
|
||
document.getElementById('result-card').style.display = 'none';
|
||
document.getElementById('progress-card').style.display = 'none';
|
||
document.getElementById('auth-banner').style.display = 'none';
|
||
hideDonateCard();
|
||
hideError();
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = t('btn_select_file');
|
||
if (pollInterval) clearInterval(pollInterval);
|
||
stopProgressSim();
|
||
hideInfo();
|
||
document.getElementById('progress-speed').textContent = '';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|