Spaces:
Running
Running
Download utils-processor.js from smmisha/omniconvert: direct link, hf CLI and curl.
- Browser
- Download file 34.2 kB
-
https://huggingface.co/spaces/smmisha/omniconvert/resolve/main/utils-processor.js
- Command line
-
hf download hf://spaces/smmisha/omniconvert/utils-processor.js
-
curl -L -o utils-processor.js https://huggingface.co/spaces/smmisha/omniconvert/resolve/main/utils-processor.js
34.2 kB
| // ========================================== | |
| // 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 = `<span style="color:var(--text-muted); font-style:italic;">${I18n ? I18n.t('placeholder_freq_empty', 'Введите текст для подсчета частоты слов...') : 'Введите текст для подсчета частоты слов...'}</span>`; | |
| 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 = `<span style="color:var(--text-muted); font-style:italic;">${I18n ? I18n.t('no_popular_words', 'Нет популярных слов (учитываются слова длиннее 2 символов)') : 'Нет популярных слов (учитываются слова длиннее 2 символов)'}</span>`; | |
| 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 = ` | |
| <span class="freq-word" title="${item.word}">${item.word}</span> | |
| <div class="freq-bar-wrap"> | |
| <div class="freq-bar" style="width: ${pct}%"></div> | |
| </div> | |
| <span class="freq-num">${item.count}</span> | |
| `; | |
| 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 = ` | |
| <div class="pdf-item-title" title="${item.name}">${item.name}</div> | |
| <div class="pdf-item-meta">${item.pageCount} стр. (${App.formatBytes(item.file.size)})</div> | |
| <div class="pdf-item-actions"> | |
| <button class="pdf-up-btn" data-idx="${index}">↑</button> | |
| <button class="pdf-down-btn" data-idx="${index}">↓</button> | |
| <button class="pdf-del-btn" style="color:var(--error); border-color:rgba(239,68,68,0.2);" data-idx="${index}">✕</button> | |
| </div> | |
| `; | |
| 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 = '<span class="loading-spinner"></span> Сшивание...'; | |
| 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 = ` | |
| <input type="checkbox" value="${i}" checked> | |
| <span>Стр. ${i}</span> | |
| `; | |
| 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 = '<span class="loading-spinner"></span> Обработка...'; | |
| 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(/<!--[\s\S]*?-->/g, ''); | |
| // Adobe / Inkscape metadata | |
| svg = svg.replace(/<metadata>[\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(/<!DOCTYPE[\s\S]*?>/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+</g, '><') | |
| .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(); | |
| }); | |