// ========================================== // 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(); });