// ==========================================
// OmniConvert: Utilities Processor Modules
// ==========================================
// 1. CHARACTER COUNTER MODULE
const CharCounterHub = {
init() {
const $ = (id) => document.getElementById(id);
if (!$('charInputTextArea')) return;
$('charInputTextArea').addEventListener('input', () => this.updateStats());
// Quick Actions
$('charBtnUpper').addEventListener('click', () => this.transformText('upper'));
$('charBtnLower').addEventListener('click', () => this.transformText('lower'));
$('charBtnTitle').addEventListener('click', () => this.transformText('title'));
$('charBtnStrip').addEventListener('click', () => this.transformText('strip'));
$('charBtnCopy').addEventListener('click', () => this.copyToClipboard());
$('charBtnClear').addEventListener('click', () => this.clear());
},
updateStats() {
const text = document.getElementById('charInputTextArea').value;
// Counts
const charCountWithSpaces = text.length;
const charCountNoSpaces = text.replace(/\s/g, '').length;
// Words
const wordsArray = text.trim().split(/\s+/).filter(w => w.length > 0);
const wordCount = wordsArray.length;
// Lines
const lineCount = text.length > 0 ? text.split('\n').length : 0;
// Sentences
const sentenceCount = text.length > 0 ? (text.split(/[.!?]+/).filter(s => s.trim().length > 0).length) : 0;
// Paragraphs
const paragraphCount = text.length > 0 ? (text.split(/\n\s*\n/).filter(p => p.trim().length > 0).length) : 0;
// Time estimates
const readingTimeSec = Math.ceil((wordCount / 200) * 60); // 200 words per minute
const speakingTimeSec = Math.ceil((wordCount / 150) * 60); // 150 words per minute
// Render Stats
document.getElementById('charCountWithSpaces').textContent = charCountWithSpaces;
document.getElementById('charCountNoSpaces').textContent = charCountNoSpaces;
document.getElementById('charWordCount').textContent = wordCount;
document.getElementById('charLineCount').textContent = lineCount;
document.getElementById('charSentenceCount').textContent = sentenceCount;
document.getElementById('charParagraphCount').textContent = paragraphCount;
document.getElementById('charReadingTime').textContent = `~${readingTimeSec} ${I18n ? I18n.t('sec', 'сек') : 'сек'}`;
document.getElementById('charSpeakingTime').textContent = `~${speakingTimeSec} ${I18n ? I18n.t('sec', 'сек') : 'сек'}`;
// Word Frequency Analysis
this.renderWordFrequency(wordsArray);
},
renderWordFrequency(words) {
const listContainer = document.getElementById('charWordFreqList');
if (words.length === 0) {
listContainer.innerHTML = `${I18n ? I18n.t('placeholder_freq_empty', 'Введите текст для подсчета частоты слов...') : 'Введите текст для подсчета частоты слов...'}`;
return;
}
// Filter, lower case and tally frequencies
const freq = {};
words.forEach(w => {
const cleanWord = w.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()?"'«»]/g, '');
if (cleanWord.length > 2) { // only count words of length > 2
freq[cleanWord] = (freq[cleanWord] || 0) + 1;
}
});
const sortedWords = Object.keys(freq).map(w => ({ word: w, count: freq[w] }))
.sort((a, b) => b.count - a.count)
.slice(0, 5); // top 5
if (sortedWords.length === 0) {
listContainer.innerHTML = `${I18n ? I18n.t('no_popular_words', 'Нет популярных слов (учитываются слова длиннее 2 символов)') : 'Нет популярных слов (учитываются слова длиннее 2 символов)'}`;
return;
}
const maxCount = sortedWords[0].count;
listContainer.innerHTML = '';
sortedWords.forEach(item => {
const pct = Math.round((item.count / maxCount) * 100);
const row = document.createElement('div');
row.className = 'freq-row';
row.innerHTML = `
${item.word}
${item.count}
`;
listContainer.appendChild(row);
});
},
transformText(type) {
const area = document.getElementById('charInputTextArea');
let text = area.value;
if (!text) return;
if (type === 'upper') {
text = text.toUpperCase();
} else if (type === 'lower') {
text = text.toLowerCase();
} else if (type === 'title') {
text = text.replace(/(^\s*|[.!?]\s+|\s+)([a-zа-я])/g, (m, p1, p2) => p1 + p2.toUpperCase());
} else if (type === 'strip') {
text = text.replace(/[ \t]+/g, ' ').replace(/\n\s*\n+/g, '\n\n').trim();
}
area.value = text;
this.updateStats();
App.showToast('Преобразование текста завершено', 'success');
},
copyToClipboard() {
const text = document.getElementById('charInputTextArea').value;
if (!text) return;
navigator.clipboard.writeText(text)
.then(() => App.showToast('Текст скопирован в буфер обмена', 'success'))
.catch(() => App.showToast('Не удалось скопировать', 'error'));
},
clear() {
document.getElementById('charInputTextArea').value = '';
this.updateStats();
}
};
// 2. PDF TOOLS MODULE
const PDFToolsHub = {
mergeQueue: [], // Array of { id, file, name, pageCount }
nextMergeId: 1,
init() {
const $ = (id) => document.getElementById(id);
if (!$('pdfTabMergeBtn')) return;
// Tabs toggler
$('pdfTabMergeBtn').addEventListener('click', () => this.switchTab('merge'));
$('pdfTabSplitBtn').addEventListener('click', () => this.switchTab('split'));
// Merger bindings
App.setupDropzone('pdfMergeDropzone', 'pdfMergeFileInput', (files) => {
this.handleMergeFilesAdded(files);
});
$('pdfMergeSubmitBtn').addEventListener('click', () => this.executeMerge());
// Splitter bindings
App.setupDropzone('pdfSplitDropzone', 'pdfSplitFileInput', (files) => {
if (files.length > 0) this.loadSplitFile(files[0]);
});
$('pdfSplitRangeToggle').addEventListener('change', (e) => {
const row = $('pdfSplitRangeRow');
if (e.target.checked) row.classList.remove('hidden');
else row.classList.add('hidden');
this.validateSplitBtn();
});
$('pdfSplitSubmitBtn').addEventListener('click', () => this.executeSplit());
},
switchTab(tab) {
const $ = (id) => document.getElementById(id);
if (tab === 'merge') {
$('pdfTabMergeBtn').classList.add('active');
$('pdfTabSplitBtn').classList.remove('active');
$('pdfMergeContainer').classList.remove('hidden');
$('pdfSplitContainer').classList.add('hidden');
} else {
$('pdfTabMergeBtn').classList.remove('active');
$('pdfTabSplitBtn').classList.add('active');
$('pdfMergeContainer').classList.add('hidden');
$('pdfSplitContainer').classList.remove('hidden');
}
},
// PDF Merger implementation
async handleMergeFilesAdded(fileList) {
const pdfjsLib = window.pdfjsLib || window['pdfjs-dist/build/pdf'];
if (!pdfjsLib) {
App.showToast('Библиотека PDF.js не найдена.', 'error');
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js';
App.showToast('Загрузка PDF-файлов...', 'info');
for (const file of fileList) {
if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) {
App.showToast(`Файл "${file.name}" не является PDF-документом`, 'warning');
continue;
}
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const pageCount = pdf.numPages;
this.mergeQueue.push({
id: this.nextMergeId++,
file,
name: file.name,
pageCount
});
} catch (err) {
console.error(err);
App.showToast(`Ошибка разбора PDF "${file.name}": ${err.message}`, 'error');
}
}
this.renderMergeList();
},
renderMergeList() {
const list = document.getElementById('pdfMergeList');
const container = document.getElementById('pdfMergeListCard');
const submitBtn = document.getElementById('pdfMergeSubmitBtn');
list.innerHTML = '';
document.getElementById('pdfMergeCount').textContent = this.mergeQueue.length;
if (this.mergeQueue.length === 0) {
container.classList.add('hidden');
submitBtn.disabled = true;
return;
}
container.classList.remove('hidden');
submitBtn.disabled = this.mergeQueue.length < 2;
this.mergeQueue.forEach((item, index) => {
const row = document.createElement('div');
row.className = 'pdf-item';
row.innerHTML = `
${item.name}
${item.pageCount} стр. (${App.formatBytes(item.file.size)})
`;
row.querySelector('.pdf-up-btn').addEventListener('click', () => this.moveMergeItem(index, -1));
row.querySelector('.pdf-down-btn').addEventListener('click', () => this.moveMergeItem(index, 1));
row.querySelector('.pdf-del-btn').addEventListener('click', () => this.deleteMergeItem(index));
list.appendChild(row);
});
},
moveMergeItem(index, direction) {
const targetIdx = index + direction;
if (targetIdx < 0 || targetIdx >= this.mergeQueue.length) return;
// Swap
const temp = this.mergeQueue[index];
this.mergeQueue[index] = this.mergeQueue[targetIdx];
this.mergeQueue[targetIdx] = temp;
this.renderMergeList();
},
deleteMergeItem(index) {
this.mergeQueue.splice(index, 1);
this.renderMergeList();
},
async executeMerge() {
if (this.mergeQueue.length < 2) return;
if (typeof window.PDFLib === 'undefined') {
App.showToast('Библиотека pdf-lib не найдена. Проверьте lib/pdf-lib.min.js', 'error');
return;
}
const btn = document.getElementById('pdfMergeSubmitBtn');
btn.disabled = true;
btn.innerHTML = ' Сшивание...';
App.showToast('Начало сборки PDF документа...', 'info');
try {
const mergedPdf = await window.PDFLib.PDFDocument.create();
for (const item of this.mergeQueue) {
const bytes = await item.file.arrayBuffer();
const pdf = await window.PDFLib.PDFDocument.load(bytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page) => mergedPdf.addPage(page));
}
const mergedPdfBytes = await mergedPdf.save();
const blob = new Blob([mergedPdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `omniconvert_merged_${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 5000);
App.showToast('Документы PDF успешно объединены и скачаны!', 'success');
// Reset queue
this.mergeQueue = [];
this.renderMergeList();
} catch (err) {
console.error(err);
App.showToast('Сбой слияния PDF: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '🔗 Склеить PDF-файлы';
}
},
// PDF Splitter implementation
currentSplitFile: null,
async loadSplitFile(file) {
const pdfjsLib = window.pdfjsLib || window['pdfjs-dist/build/pdf'];
if (!pdfjsLib) {
App.showToast('Библиотека PDF.js не найдена.', 'error');
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js';
App.showToast('Чтение страниц документа...', 'info');
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const totalPages = pdf.numPages;
this.currentSplitFile = file;
// UI setup
document.getElementById('pdfSplitFilename').textContent = `${file.name} (${App.formatBytes(file.size)})`;
document.getElementById('pdfSplitTotalPages').textContent = totalPages;
// Populate Checklist
const checklist = document.getElementById('pdfSplitPageChecklist');
checklist.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const label = document.createElement('label');
label.className = 'page-chk-label';
label.innerHTML = `
Стр. ${i}
`;
label.querySelector('input').addEventListener('change', (e) => {
if (e.target.checked) label.classList.add('selected');
else label.classList.remove('selected');
this.validateSplitBtn();
});
// Initial style
label.classList.add('selected');
checklist.appendChild(label);
}
document.getElementById('pdfSplitDetailsCard').classList.remove('hidden');
this.validateSplitBtn();
App.showToast('PDF успешно загружен в разделитель', 'success');
} catch (err) {
console.error(err);
App.showToast('Ошибка чтения PDF: ' + err.message, 'error');
}
},
validateSplitBtn() {
const isRange = document.getElementById('pdfSplitRangeToggle').checked;
const submitBtn = document.getElementById('pdfSplitSubmitBtn');
if (!this.currentSplitFile) {
submitBtn.disabled = true;
return;
}
if (isRange) {
submitBtn.disabled = false;
} else {
const checkedCount = document.querySelectorAll('#pdfSplitPageChecklist input:checked').length;
submitBtn.disabled = checkedCount === 0;
}
},
async executeSplit() {
if (!this.currentSplitFile) return;
if (typeof window.PDFLib === 'undefined') {
App.showToast('Библиотека pdf-lib не найдена.', 'error');
return;
}
const btn = document.getElementById('pdfSplitSubmitBtn');
btn.disabled = true;
btn.innerHTML = ' Обработка...';
App.showToast('Извлечение страниц...', 'info');
try {
const bytes = await this.currentSplitFile.arrayBuffer();
const sourcePdf = await window.PDFLib.PDFDocument.load(bytes);
const splitPdf = await window.PDFLib.PDFDocument.create();
const totalPages = sourcePdf.getPageCount();
let selectedPages = [];
const isRange = document.getElementById('pdfSplitRangeToggle').checked;
if (isRange) {
// Parse range input (e.g. 1-3, 5)
const rangeText = document.getElementById('pdfSplitRangeInput').value.trim();
if (!rangeText) {
throw new Error('Пожалуйста, введите корректный диапазон страниц.');
}
const parts = rangeText.split(',');
for (const p of parts) {
const range = p.trim().split('-');
if (range.length === 1) {
const val = parseInt(range[0], 10);
if (!isNaN(val) && val >= 1 && val <= totalPages) {
selectedPages.push(val - 1);
}
} else if (range.length === 2) {
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
if (!isNaN(start) && !isNaN(end) && start <= end && start >= 1 && end <= totalPages) {
for (let i = start; i <= end; i++) {
selectedPages.push(i - 1);
}
}
}
}
} else {
// Read checked checkboxes
const checkboxes = document.querySelectorAll('#pdfSplitPageChecklist input:checked');
checkboxes.forEach((cb) => {
selectedPages.push(parseInt(cb.value, 10) - 1);
});
}
// Eliminate duplicates and sort indices
selectedPages = Array.from(new Set(selectedPages)).sort((a, b) => a - b);
if (selectedPages.length === 0) {
throw new Error('Не выбрано ни одной страницы для извлечения');
}
const copiedPages = await splitPdf.copyPages(sourcePdf, selectedPages);
copiedPages.forEach((page) => splitPdf.addPage(page));
const splitBytes = await splitPdf.save();
const blob = new Blob([splitBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const rawName = this.currentSplitFile.name.replace(/\.[^.]+$/, '');
const a = document.createElement('a');
a.href = url;
a.download = `${rawName}_split_${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 5000);
App.showToast('Новый PDF документ успешно скачан!', 'success');
} catch (err) {
console.error(err);
App.showToast('Ошибка разделения PDF: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '⚡ Извлечь страницы';
}
}
};
// 3. CRYPTOGRAPHY HUB MODULE (AES & Checksums)
const CryptoHub = {
init() {
const $ = (id) => document.getElementById(id);
if (!$('hashDropzone')) return;
// File Hash Checksums bindings
App.setupDropzone('hashDropzone', 'hashFileInput', (files) => {
if (files.length > 0) this.calculateFileHashes(files[0]);
});
// AES bindings
$('cryptoEncryptBtn').addEventListener('click', () => this.encryptAction());
$('cryptoDecryptBtn').addEventListener('click', () => this.decryptAction());
$('cryptoCopyOutputBtn').addEventListener('click', () => this.copyCryptoOutput());
},
async calculateFileHashes(file) {
if (typeof window.SparkMD5 === 'undefined') {
App.showToast('Библиотека SparkMD5 не найдена. Проверьте lib/spark-md5.min.js', 'error');
return;
}
const $ = (id) => document.getElementById(id);
$('hashResultsWrap').classList.add('hidden');
$('hashFileDetails').textContent = `Анализ: ${file.name} (${App.formatBytes(file.size)}) ...`;
App.showToast('Вычисление хеш-сумм...', 'info');
try {
const buffer = await file.arrayBuffer();
// Calculate MD5
const spark = new window.SparkMD5.ArrayBuffer();
spark.append(buffer);
const md5Hash = spark.end();
$('hashMD5').value = md5Hash;
// Calculate SHA hashes natively via SubtleCrypto
const sha1Buffer = await window.crypto.subtle.digest('SHA-1', buffer);
$('hashSHA1').value = this.hex(sha1Buffer);
const sha256Buffer = await window.crypto.subtle.digest('SHA-256', buffer);
$('hashSHA256').value = this.hex(sha256Buffer);
const sha512Buffer = await window.crypto.subtle.digest('SHA-512', buffer);
$('hashSHA512').value = this.hex(sha512Buffer);
$('hashFileDetails').textContent = `${file.name} (${App.formatBytes(file.size)})`;
$('hashResultsWrap').classList.remove('hidden');
App.showToast('Контрольные суммы вычислены успешно', 'success');
} catch (e) {
console.error(e);
App.showToast('Ошибка вычисления хеша: ' + e.message, 'error');
}
},
hex(buffer) {
const hexCodes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
const value = view.getUint32(i);
const stringValue = value.toString(16);
const padding = '00000000';
const paddedValue = (padding + stringValue).slice(-8);
hexCodes.push(paddedValue);
}
return hexCodes.join('');
},
// AES Cryptography using PBKDF2 + AES-GCM 256-bit
async encryptAction() {
const text = document.getElementById('cryptoInputText').value.trim();
const password = document.getElementById('cryptoPassword').value;
if (!text || !password) {
App.showToast('Пожалуйста, заполните поле ввода и введите пароль', 'warning');
return;
}
try {
const enc = new TextEncoder();
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const baseKey = await window.crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const ciphertext = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv }, key, enc.encode(text)
);
// Pack salt + iv + ciphertext
const combined = new Uint8Array(salt.byteLength + iv.byteLength + ciphertext.byteLength);
combined.set(salt, 0);
combined.set(iv, salt.byteLength);
combined.set(new Uint8Array(ciphertext), salt.byteLength + iv.byteLength);
// Convert packed array to Base64
let binary = '';
for (let i = 0; i < combined.length; i++) {
binary += String.fromCharCode(combined[i]);
}
const b64Result = btoa(binary);
document.getElementById('cryptoOutputText').value = b64Result;
App.showToast('Текст зашифрован!', 'success');
} catch (e) {
console.error(e);
App.showToast('Сбой шифрования: ' + e.message, 'error');
}
},
async decryptAction() {
const b64Text = document.getElementById('cryptoInputText').value.trim();
const password = document.getElementById('cryptoPassword').value;
if (!b64Text || !password) {
App.showToast('Пожалуйста, вставьте зашифрованный текст и введите пароль', 'warning');
return;
}
try {
const enc = new TextEncoder();
const dec = new TextDecoder();
// Unpack Base64 string
const binaryString = atob(b64Text);
const combined = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
combined[i] = binaryString.charCodeAt(i);
}
if (combined.byteLength < 28) {
throw new Error('Зашифрованная строка имеет некорректный размер');
}
const salt = combined.slice(0, 16);
const iv = combined.slice(16, 28);
const ciphertext = combined.slice(28);
const baseKey = await window.crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const decrypted = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv }, key, ciphertext
);
document.getElementById('cryptoOutputText').value = dec.decode(decrypted);
App.showToast('Текст успешно расшифрован!', 'success');
} catch (e) {
console.error(e);
App.showToast('Ошибка расшифровки: неверный пароль или поврежденная строка', 'error');
}
},
copyCryptoOutput() {
const text = document.getElementById('cryptoOutputText').value;
if (!text) return;
navigator.clipboard.writeText(text)
.then(() => App.showToast('Результат скопирован', 'success'))
.catch(() => App.showToast('Не удалось скопировать', 'error'));
}
};
// 4. QR CODES & SVG MODULE
const QrSvgHub = {
init() {
const $ = (id) => document.getElementById(id);
if (!$('qrTabGenBtn')) return;
// Tabs toggle
$('qrTabGenBtn').addEventListener('click', () => this.switchTab('gen'));
$('qrTabScanBtn').addEventListener('click', () => this.switchTab('scan'));
$('qrTabSvgBtn').addEventListener('click', () => this.switchTab('svg'));
// QR Gen bindings
$('qrGenBtn').addEventListener('click', () => this.generateQRCode());
$('qrGenSizeSlider').addEventListener('input', (e) => {
$('qrGenSizeDisplay').textContent = `${e.target.value} x ${e.target.value} px`;
});
$('qrGenDownloadBtn').addEventListener('click', () => this.downloadQRCode());
// QR Scan bindings
App.setupDropzone('qrScanDropzone', 'qrScanFileInput', (files) => {
if (files.length > 0) this.scanQRImage(files[0]);
});
$('qrScanCopyBtn').addEventListener('click', () => {
const txt = $('qrScanResultText').value;
navigator.clipboard.writeText(txt)
.then(() => App.showToast('Текст QR скопирован', 'success'));
});
// SVG Optimizer bindings
$('svgOptimizeBtn').addEventListener('click', () => this.optimizeSVG());
$('svgOptFileLoader').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) this.loadSVGFile(file);
});
$('svgOptDownloadBtn').addEventListener('click', () => this.downloadSVG());
},
switchTab(tab) {
const $ = (id) => document.getElementById(id);
const tabs = ['qrTabGenBtn', 'qrTabScanBtn', 'qrTabSvgBtn'];
const containers = ['qrGenContainer', 'qrScanContainer', 'svgOptContainer'];
tabs.forEach(t => $(t).classList.remove('active'));
containers.forEach(c => $(c).classList.add('hidden'));
if (tab === 'gen') {
$('qrTabGenBtn').classList.add('active');
$('qrGenContainer').classList.remove('hidden');
} else if (tab === 'scan') {
$('qrTabScanBtn').classList.add('active');
$('qrScanContainer').classList.remove('hidden');
} else {
$('qrTabSvgBtn').classList.add('active');
$('svgOptContainer').classList.remove('hidden');
}
},
// QR Code generator using davidshimjs/qrcodejs
generateQRCode() {
const text = document.getElementById('qrGenInputText').value.trim();
if (!text) {
App.showToast('Введите текст или ссылку для QR-кода', 'warning');
return;
}
if (typeof window.QRCode === 'undefined') {
App.showToast('Библиотека QRCode.js не найдена.', 'error');
return;
}
const container = document.getElementById('qrGenCanvasContainer');
container.innerHTML = '';
const size = parseInt(document.getElementById('qrGenSizeSlider').value, 10);
const colorFore = document.getElementById('qrGenColorFore').value;
const colorBack = document.getElementById('qrGenColorBack').value;
try {
new window.QRCode(container, {
text: text,
width: size,
height: size,
colorDark: colorFore,
colorLight: colorBack,
correctLevel: window.QRCode.CorrectLevel.H
});
document.getElementById('qrGenDownloadBtn').classList.remove('hidden');
App.showToast('QR-Код сгенерирован', 'success');
} catch (e) {
console.error(e);
App.showToast('Сбой генерации: ' + e.message, 'error');
}
},
downloadQRCode() {
// Find canvas rendered inside container
const canvas = document.querySelector('#qrGenCanvasContainer canvas');
if (!canvas) {
// Fallback: search for image rendered
const img = document.querySelector('#qrGenCanvasContainer img');
if (img && img.src) {
const a = document.createElement('a');
a.href = img.src;
a.download = `omniconvert_qr_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
a.remove();
App.showToast('QR-Код скачан', 'success');
} else {
App.showToast('Изображение QR не готово', 'warning');
}
return;
}
const url = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = `omniconvert_qr_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
a.remove();
App.showToast('QR-Код скачан', 'success');
},
// QR Code scanning from image via jsQR
scanQRImage(file) {
if (typeof window.jsQR === 'undefined') {
App.showToast('Библиотека jsQR не найдена. Проверьте lib/jsQR.min.js', 'error');
return;
}
App.showToast('Анализ изображения...', 'info');
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = window.jsQR(imgData.data, imgData.width, imgData.height);
const area = document.getElementById('qrScanResultText');
const copyBtn = document.getElementById('qrScanCopyBtn');
if (code && code.data) {
area.value = code.data;
copyBtn.disabled = false;
App.showToast('QR-Код успешно расшифрован!', 'success');
} else {
area.value = '';
copyBtn.disabled = true;
App.showToast('Не удалось обнаружить QR-Код на изображении', 'warning');
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
},
// SVG Optimizer implementation
originalSVG: '',
optimizedSVG: '',
loadSVGFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('svgOptTextArea').value = e.target.result;
App.showToast('Файл SVG загружен', 'success');
};
reader.readAsText(file);
},
optimizeSVG() {
let svg = document.getElementById('svgOptTextArea').value.trim();
if (!svg) {
App.showToast('Введите или загрузите SVG-код', 'warning');
return;
}
const cleanMeta = document.getElementById('svgOptMeta').checked;
const cleanXml = document.getElementById('svgOptXml').checked;
const roundCoords = document.getElementById('svgOptRound').checked;
const sizeBefore = new Blob([svg], { type: 'image/svg+xml' }).size;
// Run cleanups
try {
if (cleanMeta) {
// Comments
svg = svg.replace(//g, '');
// Adobe / Inkscape metadata
svg = svg.replace(/[\s\S]*?<\/metadata>/gi, '');
svg = svg.replace(/sodipodi:[a-z-]+="[^"]*"/gi, '');
svg = svg.replace(/inkscape:[a-z-]+="[^"]*"/gi, '');
}
if (cleanXml) {
// XML declarations & doctype
svg = svg.replace(/<\?xml[\s\S]*?\?>/i, '');
svg = svg.replace(//i, '');
}
if (roundCoords) {
// Find float numbers in path data and round them to 2 decimals
svg = svg.replace(/(-?\d+\.\d{3,})/g, (match) => {
return parseFloat(parseFloat(match).toFixed(2));
});
}
// Compact spacing
svg = svg.replace(/\s+/g, ' ')
.replace(/>\s+<')
.trim();
const sizeAfter = new Blob([svg], { type: 'image/svg+xml' }).size;
const pct = (((sizeBefore - sizeAfter) / sizeBefore) * 100).toFixed(1);
document.getElementById('svgOptSizeBefore').textContent = App.formatBytes(sizeBefore);
document.getElementById('svgOptSizeAfter').textContent = App.formatBytes(sizeAfter);
document.getElementById('svgOptSavings').textContent = `-${pct}%`;
document.getElementById('svgOptStatsCard').classList.remove('hidden');
document.getElementById('svgOptDownloadBtn').classList.remove('hidden');
this.optimizedSVG = svg;
App.showToast('SVG оптимизирован!', 'success');
} catch (e) {
console.error(e);
App.showToast('Ошибка оптимизации SVG: ' + e.message, 'error');
}
},
downloadSVG() {
if (!this.optimizedSVG) return;
const blob = new Blob([this.optimizedSVG], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `omniconvert_optimized_${Date.now()}.svg`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 5000);
App.showToast('Оптимизированный SVG скачан', 'success');
}
};
// Initialize modules on document ready
document.addEventListener('DOMContentLoaded', () => {
CharCounterHub.init();
PDFToolsHub.init();
CryptoHub.init();
QrSvgHub.init();
});