omniconvert / data-processor.js
smmisha's picture
feat: complete omni-converter app with trilingual support (UA/RU/EN), offline libraries, image/data/audio tools, pdf tools, crypto & qr/svg optimizer
befbf55
Raw History Blame Contribute Delete
29.7 kB
// ==========================================
// OmniConvert: Data Converter Module
// ==========================================
const DataHub = {
// Bind UI Controls
init() {
this.bindEvents();
},
bindEvents() {
const $ = (id) => document.getElementById(id);
// Text area counters
$('dataInputTextArea').addEventListener('input', () => this.updateCounters());
// Paste button
$('pasteInputBtn').addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
$('dataInputTextArea').value = text;
this.updateCounters();
App.showToast('Текст вставлен из буфера', 'success');
} catch (err) {
App.showToast('Не удалось получить доступ к буферу', 'error');
}
});
// File loader trigger
$('loadInputFileBtn').addEventListener('click', () => {
$('dataFileLoader').click();
});
// File loader event
$('dataFileLoader').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (ext === 'pdf') {
App.showToast(`Чтение PDF "${file.name}"...`, 'info');
try {
const extractedText = await this.extractTextFromPDF(file);
$('dataInputTextArea').value = extractedText;
this.updateCounters();
this.autoDetectFormat(file.name);
App.showToast(`Текст из PDF "${file.name}" успешно извлечен`, 'success');
} catch (err) {
console.error(err);
App.showToast(`Ошибка чтения PDF: ${err.message}`, 'error');
}
} else {
const reader = new FileReader();
reader.onload = (evt) => {
$('dataInputTextArea').value = evt.target.result;
this.updateCounters();
this.autoDetectFormat(file.name);
App.showToast(`Файл "${file.name}" загружен`, 'success');
};
reader.onerror = () => {
App.showToast('Ошибка при чтении текстового файла', 'error');
};
reader.readAsText(file);
}
});
// Copy output
$('copyOutputBtn').addEventListener('click', async () => {
const outText = $('dataOutputTextArea').value;
if (!outText) {
App.showToast('Нет данных для копирования', 'warning');
return;
}
try {
await navigator.clipboard.writeText(outText);
App.showToast('Сконвертированный текст скопирован!', 'success');
} catch (e) {
App.showToast('Ошибка копирования', 'error');
}
});
// Download output file
$('downloadOutputBtn').addEventListener('click', async () => {
const outText = document.getElementById('dataOutputTextArea').value;
if (!outText) {
App.showToast('Нет данных для скачивания', 'warning');
return;
}
const fmt = document.getElementById('targetFormatSelect').value;
const ext = this.getFormatExtension(fmt);
let blob;
if (fmt === 'pdf') {
App.showToast('Генерация PDF документа...', 'info');
try {
blob = await this.textToPDF(outText);
} catch (e) {
App.showToast('Ошибка создания PDF: ' + e.message, 'error');
return;
}
} else {
blob = new Blob([outText], { type: 'text/plain;charset=utf-8' });
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `omniconvert_export_${Date.now()}.${ext}`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 4000);
App.showToast('Файл скачан', 'success');
});
// Format & Minify utilities
$('formatOutputBtn').addEventListener('click', () => this.beautifyOutput());
$('minifyOutputBtn').addEventListener('click', () => this.minifyOutput());
// Core conversion trigger button
$('convertDataBtn').addEventListener('click', () => this.convert());
},
// Line count counters update
updateCounters() {
const input = document.getElementById('dataInputTextArea').value;
const output = document.getElementById('dataOutputTextArea').value;
const countLines = (str) => str ? str.split('\n').length : 0;
const label = I18n ? I18n.t('lines_count', 'Строк:') : 'Строк:';
document.getElementById('inputLinesCount').textContent = `${label} ${countLines(input)}`;
document.getElementById('outputLinesCount').textContent = `${label} ${countLines(output)}`;
},
// Detect format based on file extension
autoDetectFormat(filename) {
const ext = filename.split('.').pop().toLowerCase();
const select = document.getElementById('sourceFormatSelect');
if (ext === 'json') select.value = 'json';
else if (ext === 'csv') select.value = 'csv';
else if (ext === 'xml') select.value = 'xml';
else if (['yaml', 'yml'].includes(ext)) select.value = 'yaml';
else if (ext === 'ini') select.value = 'ini';
else if (ext === 'md') select.value = 'md_text';
else if (ext === 'html') select.value = 'html';
else if (ext === 'pdf') select.value = 'pdf';
else select.value = 'auto';
},
// Main converter coordinator
convert() {
const inputTextArea = document.getElementById('dataInputTextArea');
const outputTextArea = document.getElementById('dataOutputTextArea');
const inputData = inputTextArea.value.trim();
if (!inputData) {
App.showToast('Пожалуйста, введите исходные данные', 'warning');
return;
}
let srcFmt = document.getElementById('sourceFormatSelect').value;
const dstFmt = document.getElementById('targetFormatSelect').value;
if (srcFmt === 'auto') {
srcFmt = this.detectFormatFromContent(inputData);
App.showToast(`Автоопределение формата: ${srcFmt.toUpperCase()}`, 'info');
}
try {
// 1. Parse Source Format -> Intermediate JS Object
const parsedObject = this.parseData(inputData, srcFmt);
// 2. Stringify Intermediate JS Object -> Destination Format
const outputText = this.stringifyData(parsedObject, dstFmt);
outputTextArea.value = outputText;
this.updateCounters();
App.showToast('Конвертация завершена успешно!', 'success');
} catch (err) {
console.error(err);
outputTextArea.value = `[ОШИБКА КОНВЕРТАЦИИ]:\n${err.message}`;
this.updateCounters();
App.showToast('Ошибка конвертации. Подробности в окне вывода.', 'error');
}
},
// Heuristic content-based format checker
detectFormatFromContent(str) {
if (str.startsWith('{') || str.startsWith('[')) return 'json';
if (str.startsWith('<?xml') || str.startsWith('<')) {
// Check if it looks more like HTML code or XML document
if (str.toLowerCase().includes('<!doctype html>') || str.toLowerCase().includes('<html')) {
return 'html';
}
return 'xml';
}
if (str.includes('[') && str.includes(']') && str.includes('=')) return 'ini';
if (str.includes('|') && str.includes('---')) return 'md_table';
if (str.includes('\n') && (str.includes(',') || str.includes(';'))) {
return 'csv';
}
// Check if it has markdown formatting
if (str.includes('# ') || str.includes('**') || str.includes('* ')) {
return 'md_text';
}
if (str.includes('--- Страница ')) {
return 'pdf';
}
return 'yaml';
},
// Parsing Router
parseData(str, format) {
switch (format) {
case 'json':
return JSON.parse(str);
case 'csv':
return this.parseCSV(str);
case 'xml':
return this.parseXML(str);
case 'yaml':
return this.parseYAML(str);
case 'ini':
return this.parseINI(str);
case 'md_table':
return this.parseMarkdownTable(str);
case 'md_text':
case 'html':
case 'pdf':
return str; // Plain string representations for HTML / Markdown / PDF texts
default:
throw new Error(`Неподдерживаемый формат разбора: ${format}`);
}
},
// Serializing Router
stringifyData(obj, format) {
switch (format) {
case 'json':
// If obj is a plain text from HTML/Markdown parser, wrap it in JSON string
if (typeof obj === 'string') return JSON.stringify({ text: obj }, null, 2);
return JSON.stringify(obj, null, 2);
case 'csv':
if (typeof obj === 'string') throw new Error('Невозможно преобразовать текстовый документ в плоскую CSV таблицу');
return this.stringifyCSV(obj);
case 'xml':
if (typeof obj === 'string') return `<?xml version="1.0" encoding="UTF-8"?>\n<text>${this.escapeXML(obj)}</text>`;
return this.stringifyXML(obj);
case 'yaml':
if (typeof obj === 'string') return window.jsyaml ? window.jsyaml.dump({ text: obj }) : `text: ${obj}`;
return this.stringifyYAML(obj);
case 'ini':
if (typeof obj === 'string') return `text = ${obj}`;
return this.stringifyINI(obj);
case 'md_table':
if (typeof obj === 'string') throw new Error('Невозможно преобразовать текстовый документ в Markdown-таблицу');
return this.stringifyMarkdownTable(obj);
case 'md_text':
if (typeof obj === 'string') {
// If it's HTML, convert to MD
if (obj.trim().startsWith('<') || obj.includes('</')) {
return this.convertHtmlToMarkdown(obj);
}
return obj;
}
return '```yaml\n' + this.stringifyYAML(obj) + '\n```';
case 'html':
if (typeof obj === 'string') {
// Convert MD to HTML
return this.convertMarkdownToHtml(obj);
}
return '<pre><code>' + JSON.stringify(obj, null, 2) + '</code></pre>';
case 'pdf':
if (typeof obj === 'string') {
return obj;
}
try {
return JSON.stringify(obj, null, 2);
} catch (e) {
return String(obj);
}
default:
throw new Error(`Неподдерживаемый формат сборки: ${format}`);
}
},
// ==========================================
// PARSERS IMPLEMENTATION
// ==========================================
// CSV -> Array of Objects parser
parseCSV(str) {
const lines = str.split(/\r?\n/).filter(line => line.trim() !== '');
if (lines.length === 0) return [];
// Auto-detect separator: comma or semicolon
const headerLine = lines[0];
const separator = headerLine.includes(';') ? ';' : ',';
const parseCSVLine = (line) => {
const result = [];
let cell = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === separator && !inQuotes) {
result.push(cell.trim().replace(/^"|"$/g, ''));
cell = '';
} else {
cell += char;
}
}
result.push(cell.trim().replace(/^"|"$/g, ''));
return result;
};
const headers = parseCSVLine(lines[0]);
const items = [];
for (let i = 1; i < lines.length; i++) {
const cells = parseCSVLine(lines[i]);
// Pad or trim columns to match headers size
const item = {};
headers.forEach((h, index) => {
item[h] = cells[index] !== undefined ? cells[index] : '';
});
items.push(item);
}
return items;
},
// XML Parser converting nodes to JS structures
parseXML(str) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(str, 'text/xml');
// Check for parser errors
const parserErrorNode = xmlDoc.querySelector('parsererror');
if (parserErrorNode) {
throw new Error(`XML Parser Error: ${parserErrorNode.textContent}`);
}
const xmlNodeToObject = (node) => {
// If node is a text node
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue;
}
const obj = {};
// Node attributes
if (node.attributes && node.attributes.length > 0) {
obj['@attributes'] = {};
for (let j = 0; j < node.attributes.length; j++) {
const attribute = node.attributes.item(j);
obj['@attributes'][attribute.nodeName] = attribute.nodeValue;
}
}
// Child nodes
if (node.childNodes && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
const item = node.childNodes.item(i);
if (item.nodeType === Node.ELEMENT_NODE) {
const nodeName = item.nodeName;
const nodeVal = xmlNodeToObject(item);
if (obj[nodeName] === undefined) {
obj[nodeName] = nodeVal;
} else {
// Convert duplicate elements to array
if (!Array.isArray(obj[nodeName])) {
obj[nodeName] = [obj[nodeName]];
}
obj[nodeName].push(nodeVal);
}
} else if (item.nodeType === Node.TEXT_NODE && item.nodeValue.trim() !== '') {
if (node.childNodes.length === 1) {
return item.nodeValue.trim();
} else {
obj['#text'] = item.nodeValue.trim();
}
}
}
}
return Object.keys(obj).length === 0 ? "" : obj;
};
const rootNode = xmlDoc.documentElement;
const result = {};
result[rootNode.nodeName] = xmlNodeToObject(rootNode);
return result;
},
// YAML parser using local library
parseYAML(str) {
if (!window.jsyaml) {
throw new Error('Модуль js-yaml не загружен. Проверьте lib/js-yaml.min.js');
}
return window.jsyaml.load(str);
},
// INI Parser
parseINI(str) {
const result = {};
let currentSection = result;
const lines = str.split(/\r?\n/);
lines.forEach(line => {
line = line.trim();
if (!line || line.startsWith(';') || line.startsWith('#')) return; // comment / blank
if (line.startsWith('[') && line.endsWith(']')) {
const sectionName = line.slice(1, -1).trim();
result[sectionName] = {};
currentSection = result[sectionName];
} else {
const parts = line.split('=');
if (parts.length >= 2) {
const key = parts[0].trim();
const val = parts.slice(1).join('=').trim();
currentSection[key] = val;
}
}
});
return result;
},
// ==========================================
// SERIALIZERS IMPLEMENTATION
// ==========================================
// Convert array of objects to CSV sheet
stringifyCSV(obj) {
let items = Array.isArray(obj) ? obj : [obj];
// Resolve nested arrays if XML parsed directly
if (items.length === 1 && typeof items[0] === 'object' && !Array.isArray(items[0])) {
const keys = Object.keys(items[0]);
if (keys.length === 1 && Array.isArray(items[0][keys[0]])) {
items = items[0][keys[0]];
}
}
if (items.length === 0 || typeof items[0] !== 'object') {
return 'Header\nEmpty dataset or invalid format';
}
// Extract headers (flatten fields slightly if object)
const headersSet = new Set();
items.forEach(item => {
if (item && typeof item === 'object') {
Object.keys(item).forEach(k => headersSet.add(k));
}
});
const headers = Array.from(headersSet);
const escapeCSV = (val) => {
if (val === null || val === undefined) return '';
let str = typeof val === 'object' ? JSON.stringify(val) : String(val);
str = str.replace(/"/g, '""');
if (str.includes(',') || str.includes(';') || str.includes('\n') || str.includes('"')) {
return `"${str}"`;
}
return str;
};
const separator = ',';
const csvLines = [headers.join(separator)];
items.forEach(item => {
const row = headers.map(h => escapeCSV(item[h]));
csvLines.push(row.join(separator));
});
return csvLines.join('\n');
},
// Object to XML converter
stringifyXML(obj) {
let rootName = 'root';
let contentObj = obj;
// XML needs exactly one root node
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
const keys = Object.keys(obj);
if (keys.length === 1) {
rootName = keys[0];
contentObj = obj[keys[0]];
}
}
const objectToXmlStr = (nodeName, val, indent = '') => {
if (val === null || val === undefined) {
return `${indent}<${nodeName} />`;
}
if (typeof val !== 'object') {
return `${indent}<${nodeName}>${this.escapeXML(String(val))}</${nodeName}>`;
}
if (Array.isArray(val)) {
return val.map(item => objectToXmlStr(nodeName, item, indent)).join('\n');
}
let attributesStr = '';
if (val['@attributes']) {
attributesStr = Object.keys(val['@attributes'])
.map(k => ` ${k}="${this.escapeXML(String(val['@attributes'][k]))}"`)
.join('');
}
const childNodesStr = Object.keys(val)
.filter(k => k !== '@attributes' && k !== '#text')
.map(k => objectToXmlStr(k, val[k], indent + ' '))
.join('\n');
if (childNodesStr) {
return `${indent}<${nodeName}${attributesStr}>\n${childNodesStr}\n${indent}</${nodeName}>`;
} else if (val['#text']) {
return `${indent}<${nodeName}${attributesStr}>${this.escapeXML(String(val['#text']))}</${nodeName}>`;
} else {
return `${indent}<${nodeName}${attributesStr} />`;
}
};
return `<?xml version="1.0" encoding="UTF-8"?>\n${objectToXmlStr(rootName, contentObj)}`;
},
escapeXML(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
},
// YAML Stringifier using local library
stringifyYAML(obj) {
if (!window.jsyaml) {
throw new Error('Модуль js-yaml не загружен. Проверьте lib/js-yaml.min.js');
}
return window.jsyaml.dump(obj, { indent: 2 });
},
// Object to INI serializer
stringifyINI(obj) {
if (typeof obj !== 'object' || Array.isArray(obj)) {
throw new Error('INI формат поддерживает только ассоциативные словари (key-value)');
}
const lines = [];
const globalKeys = [];
const sections = [];
// Categorize top-level keys
Object.keys(obj).forEach(k => {
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) {
sections.push(k);
} else {
globalKeys.push(k);
}
});
// Write global keys
globalKeys.forEach(k => {
lines.push(`${k} = ${obj[k]}`);
});
if (globalKeys.length > 0 && sections.length > 0) lines.push('');
// Write sections
sections.forEach(secName => {
lines.push(`[${secName}]`);
const sectionObj = obj[secName];
Object.keys(sectionObj).forEach(k => {
lines.push(`${k} = ${sectionObj[k]}`);
});
lines.push('');
});
return lines.join('\n').trim();
},
// XML / JSON beautifiers
beautifyOutput() {
const area = document.getElementById('dataOutputTextArea');
const val = area.value.trim();
if (!val) return;
const fmt = document.getElementById('targetFormatSelect').value;
try {
if (fmt === 'json') {
const obj = JSON.parse(val);
area.value = JSON.stringify(obj, null, 2);
} else if (fmt === 'xml') {
// XML is already formatted nicely in stringifyXML, but we can do a DOMParser format
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(val, 'application/xml');
const serializer = new XMLSerializer();
// Since we don't have standard browser formatters, simple XML formatting:
area.value = this.formatXMLString(serializer.serializeToString(xmlDoc));
}
this.updateCounters();
App.showToast('Текст отформатирован', 'success');
} catch (e) {
App.showToast('Невозможно отформатировать: неверная структура', 'warning');
}
},
minifyOutput() {
const area = document.getElementById('dataOutputTextArea');
const val = area.value.trim();
if (!val) return;
const fmt = document.getElementById('targetFormatSelect').value;
try {
if (fmt === 'json') {
const obj = JSON.parse(val);
area.value = JSON.stringify(obj);
} else if (fmt === 'xml') {
// strip spaces between tags
area.value = val.replace(/>\s+</g, '><');
}
this.updateCounters();
App.showToast('Текст минифицирован (сжат)', 'success');
} catch (e) {
App.showToast('Невозможно минифицировать: неверная структура', 'warning');
}
},
formatXMLString(xml) {
let formatted = '';
let indent = '';
const reg = /(>)(<)(\/*)/g;
xml = xml.replace(reg, '$1\r\n$2$3');
let pad = 0;
xml.split('\r\n').forEach(node => {
let indent = 0;
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0;
} else if (node.match(/^<\/\w/)) {
if (pad !== 0) {
pad -= 1;
}
} else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
indent = 1;
} else {
indent = 0;
}
let padding = '';
for (let i = 0; i < pad; i++) {
padding += ' ';
}
formatted += padding + node + '\r\n';
pad += indent;
});
return formatted.trim();
},
getFormatExtension(fmt) {
const mapping = {
json: 'json',
csv: 'csv',
xml: 'xml',
yaml: 'yaml',
ini: 'ini',
md_table: 'md',
md_text: 'md',
html: 'html',
pdf: 'pdf'
};
return mapping[fmt] || 'txt';
},
// ==========================================
// MARKDOWN & HTML PROCESSING IMPLEMENTATION
// ==========================================
// Parser: Markdown table -> JS Array of Objects
parseMarkdownTable(str) {
const lines = str.split(/\r?\n/).map(l => l.trim()).filter(l => l !== '');
if (lines.length === 0) return [];
// Find lines with table pipes
const rowLines = lines.filter(line => line.includes('|'));
if (rowLines.length < 2) {
throw new Error('Markdown-таблица должна содержать заголовок и разделитель (например: | header | |---|)');
}
const parseRow = (line) => {
let clean = line;
if (clean.startsWith('|')) clean = clean.slice(1);
if (clean.endsWith('|')) clean = clean.slice(0, -1);
return clean.split('|').map(cell => cell.trim());
};
const headers = parseRow(rowLines[0]);
const rows = [];
// Line index 1 is separator (e.g. |---|---|)
const separator = rowLines[1];
if (!separator.includes('-')) {
throw new Error('Вторая строка Markdown-таблицы должна содержать разделители "---"');
}
for (let i = 2; i < rowLines.length; i++) {
const cells = parseRow(rowLines[i]);
const rowObj = {};
headers.forEach((h, idx) => {
rowObj[h] = cells[idx] !== undefined ? cells[idx] : '';
});
rows.push(rowObj);
}
return rows;
},
// Serializer: JS Array of Objects -> Markdown table
stringifyMarkdownTable(obj) {
let items = Array.isArray(obj) ? obj : [obj];
// Resolve nested arrays from XML nodes
if (items.length === 1 && typeof items[0] === 'object' && !Array.isArray(items[0])) {
const keys = Object.keys(items[0]);
if (keys.length === 1 && Array.isArray(items[0][keys[0]])) {
items = items[0][keys[0]];
}
}
if (items.length === 0 || typeof items[0] !== 'object') {
return '| Сообщение |\n| --- |\n| Пустой набор данных |';
}
const headersSet = new Set();
items.forEach(item => {
if (item && typeof item === 'object') {
Object.keys(item).forEach(k => headersSet.add(k));
}
});
const headers = Array.from(headersSet);
if (headers.length === 0) {
return '| Сообщение |\n| --- |\n| Пустой набор данных |';
}
const headerRow = '| ' + headers.join(' | ') + ' |';
const separatorRow = '| ' + headers.map(() => '---').join(' | ') + ' |';
const valueRows = items.map(item => {
const rowValues = headers.map(h => {
const val = item[h];
if (val === null || val === undefined) return '';
return typeof val === 'object' ? JSON.stringify(val) : String(val);
});
return '| ' + rowValues.join(' | ') + ' |';
});
return [headerRow, separatorRow, ...valueRows].join('\n');
},
// Offline Markdown to HTML conversion
convertMarkdownToHtml(str) {
if (typeof window.marked === 'undefined') {
throw new Error('Библиотека marked.js не загружена. Проверьте lib/marked.min.js');
}
// Support marked parsing APIs
if (typeof window.marked.parse === 'function') {
return window.marked.parse(str);
} else if (typeof window.marked === 'function') {
return window.marked(str);
}
throw new Error('Сбой инициализации парсера marked.js');
},
// Offline HTML to Markdown conversion
convertHtmlToMarkdown(str) {
if (typeof window.TurndownService === 'undefined') {
throw new Error('Библиотека turndown.js не загружена. Проверьте lib/turndown.js');
}
const turndownService = new window.TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*'
});
return turndownService.turndown(str);
},
// Compile Russian and multilingual text into PDF natively offline
async textToPDF(text) {
if (typeof window.jspdf === 'undefined' || !window.jspdf.jsPDF) {
throw new Error('Библиотека jsPDF не загружена. Проверьте lib/jspdf.umd.min.js');
}
const lines = text.split('\n');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const padding = 40;
const fontSize = 14;
const lineHeight = 22;
const width = 800;
// Calculate required height based on line numbers (min page format aspect ratio height)
const height = Math.max(1130, padding * 2 + lines.length * lineHeight);
canvas.width = width;
canvas.height = height;
// Fill background with white
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// Draw text
ctx.fillStyle = '#111827';
ctx.font = `${fontSize}px Arial, Helvetica, sans-serif`;
let y = padding + fontSize;
lines.forEach(line => {
ctx.fillText(line, padding, y);
y += lineHeight;
});
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'p',
unit: 'px',
format: [width, height]
});
const imgData = canvas.toDataURL('image/jpeg', 0.95);
doc.addImage(imgData, 'JPEG', 0, 0, width, height);
return doc.output('blob');
},
// Extract text content from PDF pages natively offline
async extractTextFromPDF(file) {
const pdfjsLib = window.pdfjsLib || window['pdfjs-dist/build/pdf'];
if (!pdfjsLib) {
throw new Error('Библиотека PDF.js не загружена. Проверьте lib/pdf.min.js');
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js';
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const numPages = pdf.numPages;
App.showToast(`Извлечение текста из PDF: ${numPages} стр.`, 'info');
let fullText = '';
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
const textItems = textContent.items;
let pageText = '';
let lastY = null;
for (const item of textItems) {
// If Y-coordinate changes by more than 10px, start a new line
if (lastY !== null && Math.abs(item.transform[5] - lastY) > 10) {
pageText += '\n';
}
pageText += item.str;
lastY = item.transform[5];
}
if (pageText.trim()) {
fullText += `--- Страница ${pageNum} ---\n${pageText.trim()}\n\n`;
}
}
return fullText.trim();
}
};
// Initialize module
document.addEventListener('DOMContentLoaded', () => {
DataHub.init();
});