From 12275e82ea9754701a0643a303dedb82e5785be2 Mon Sep 17 00:00:00 2001 From: FedorSarafanov Date: Sun, 9 Nov 2025 23:51:28 +0300 Subject: [PATCH] Initial commit --- index.html | 99 ++++++++++ npz-reader.js | 498 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 597 insertions(+) create mode 100644 index.html create mode 100644 npz-reader.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..2de8aac --- /dev/null +++ b/index.html @@ -0,0 +1,99 @@ + + + + + Чтение .npz в браузере + + + + +

Чтение NPZ файлов в браузере

+

Выберите .npz файл для чтения (без использования внешних библиотек)

+ + +
Ожидание файла...
+ + + + + diff --git a/npz-reader.js b/npz-reader.js new file mode 100644 index 0000000..f8c3cad --- /dev/null +++ b/npz-reader.js @@ -0,0 +1,498 @@ +/** + * Чтение NPZ файлов в браузере без внешних библиотек + * NPZ файл - это ZIP архив, содержащий .npy файлы + */ + +// Проверка поддержки форматов сжатия +const SUPPORTED_DEFLATE_FORMATS = []; +if (typeof DecompressionStream !== 'undefined') { + // Проверяем поддержку форматов + // Примечание: создание DecompressionStream может не выбрасывать ошибку сразу, + // поэтому проверяем при использовании + // Приоритет: 'deflate-raw' для ZIP (raw deflate), затем 'deflate' (zlib формат) + SUPPORTED_DEFLATE_FORMATS.push('deflate-raw', 'deflate'); +} + +class NPZReader { + /** + * Читает NPZ файл из ArrayBuffer + * @param {ArrayBuffer} buffer - ArrayBuffer с содержимым NPZ файла + * @returns {Promise} Объект с ключами - именами массивов, значениями - массивами данных + */ + static async readNPZ(buffer) { + const zipFiles = await this.parseZIP(buffer); + console.log(`Найдено файлов в ZIP: ${Object.keys(zipFiles).length}`); + console.log('Файлы:', Object.keys(zipFiles)); + + const result = {}; + + for (const [filename, fileData] of Object.entries(zipFiles)) { + if (filename.endsWith('.npy')) { + const arrayName = filename.replace('.npy', ''); + console.log(`Парсинг NPY файла: ${filename} -> ${arrayName}`); + try { + result[arrayName] = this.parseNPY(fileData); + console.log(`NPY файл ${filename} успешно распарсен`); + } catch (err) { + console.error(`Ошибка парсинга NPY файла ${filename}:`, err); + throw err; + } + } else { + console.log(`Пропущен файл (не .npy): ${filename}`); + } + } + + console.log(`Итого распарсено массивов: ${Object.keys(result).length}`); + return result; + } + + /** + * Парсит ZIP архив (упрощённая реализация для NPZ файлов) + * @param {ArrayBuffer} buffer - ArrayBuffer с ZIP архивом + * @returns {Promise} Объект с именами файлов и их содержимым + */ + static async parseZIP(buffer) { + const view = new DataView(buffer); + const files = {}; + let offset = 0; + + // Ищем центральный каталог в конце файла + // ZIP файл имеет структуру: [локальные файлы] [центральный каталог] [конец центрального каталога] + + // Ищем сигнатуру конца центрального каталога (0x06054b50) + let eocdOffset = -1; + for (let i = buffer.byteLength - 22; i >= 0; i--) { + if (view.getUint32(i, true) === 0x06054b50) { + eocdOffset = i; + break; + } + } + + if (eocdOffset === -1) { + throw new Error('Неверный формат ZIP файла'); + } + + // Читаем информацию о центральном каталоге + const cdOffset = view.getUint32(eocdOffset + 16, true); + const cdSize = view.getUint32(eocdOffset + 12, true); + const totalEntries = view.getUint16(eocdOffset + 10, true); + + // Читаем записи центрального каталога + let currentOffset = cdOffset; + for (let i = 0; i < totalEntries; i++) { + // Сигнатура центрального файла (0x02014b50) + if (view.getUint32(currentOffset, true) !== 0x02014b50) { + throw new Error('Ошибка чтения центрального каталога'); + } + + // Флаги сжатия + const flags = view.getUint16(currentOffset + 8, true); + const method = view.getUint16(currentOffset + 10, true); + + // Размеры + const compressedSize = view.getUint32(currentOffset + 20, true); + const uncompressedSize = view.getUint32(currentOffset + 24, true); + + // Длина имени файла + const filenameLength = view.getUint16(currentOffset + 28, true); + const extraLength = view.getUint16(currentOffset + 30, true); + const commentLength = view.getUint16(currentOffset + 32, true); + + // Смещение локального заголовка + const localHeaderOffset = view.getUint32(currentOffset + 42, true); + + // Читаем имя файла + const filenameBytes = new Uint8Array(buffer, currentOffset + 46, filenameLength); + const filename = new TextDecoder('utf-8').decode(filenameBytes); + + // Отладочная информация + console.log(`Файл: ${filename}, метод сжатия: ${method}, размер: ${compressedSize}/${uncompressedSize}`); + + // Переходим к следующей записи + currentOffset += 46 + filenameLength + extraLength + commentLength; + + // Читаем локальный заголовок файла + if (view.getUint32(localHeaderOffset, true) !== 0x04034b50) { + throw new Error('Ошибка чтения локального заголовка'); + } + + const localFilenameLength = view.getUint16(localHeaderOffset + 26, true); + const localExtraLength = view.getUint16(localHeaderOffset + 28, true); + const dataOffset = localHeaderOffset + 30 + localFilenameLength + localExtraLength; + + // Читаем данные файла + if (method === 0) { + // Без сжатия (store) + console.log(`Чтение несжатого файла ${filename} (размер: ${(uncompressedSize / 1024).toFixed(2)} КБ)...`); + const fileData = new Uint8Array(buffer, dataOffset, uncompressedSize); + // Создаём копию данных, чтобы иметь независимый ArrayBuffer + const fileDataCopy = new Uint8Array(fileData); + files[filename] = fileDataCopy.buffer; + console.log(`Файл ${filename} прочитан (${fileDataCopy.length} байт)`); + } else if (method === 8) { + // Deflate сжатие - используем встроенный API браузера + // ZIP использует "raw deflate" (без zlib заголовка) + console.log(`Начало распаковки файла ${filename} (${(compressedSize / 1024 / 1024).toFixed(2)} МБ -> ${(uncompressedSize / 1024 / 1024).toFixed(2)} МБ)...`); + const compressedData = new Uint8Array(buffer, dataOffset, compressedSize); + try { + const startTime = Date.now(); + const decompressedData = await this.inflate(compressedData); + const endTime = Date.now(); + console.log(`Файл ${filename} распакован за ${((endTime - startTime) / 1000).toFixed(2)} секунд`); + files[filename] = decompressedData.buffer; + } catch (err) { + throw new Error(`Ошибка распаковки файла ${filename}: ${err.message}`); + } + } else { + throw new Error(`Неподдерживаемый метод сжатия: ${method} для файла ${filename}`); + } + } + + return files; + } + + /** + * Распаковывает данные, сжатые методом Deflate + * @param {Uint8Array} data - Сжатые данные + * @returns {Promise} Распакованные данные + */ + static async inflate(data) { + // Используем встроенный API DecompressionStream (доступен в современных браузерах) + if (typeof DecompressionStream === 'undefined') { + throw new Error('DecompressionStream не поддерживается. Используйте современный браузер (Chrome 80+, Firefox 113+, Safari 16.4+).'); + } + + // ZIP использует "raw deflate" (без zlib заголовка и контрольной суммы) + // Используем поддерживаемые форматы + const formats = SUPPORTED_DEFLATE_FORMATS.length > 0 + ? SUPPORTED_DEFLATE_FORMATS + : ['deflate-raw', 'deflate']; // Fallback на стандартные форматы + + let lastError = null; + + for (const format of formats) { + try { + console.log(`Попытка распаковки с форматом: ${format}`); + // Создаём новый stream для каждой попытки + const stream = new DecompressionStream(format); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + + // Запускаем чтение параллельно с записью + const readPromise = (async () => { + const chunks = []; + let done = false; + let totalRead = 0; + while (!done) { + const { done: readerDone, value } = await reader.read(); + done = readerDone; + if (value) { + chunks.push(value); + totalRead += value.length; + // Логируем прогресс каждые 10 МБ + if (totalRead % (10 * 1024 * 1024) < value.length) { + console.log(`Распаковано: ${(totalRead / 1024 / 1024).toFixed(2)} МБ`); + } + } + } + console.log(`Распаковка завершена, всего: ${(totalRead / 1024 / 1024).toFixed(2)} МБ`); + return { chunks, totalRead }; + })(); + + // Записываем данные частями, чтобы не блокировать поток + console.log('Запись данных в stream...'); + const chunkSize = 1024 * 1024; // 1 МБ за раз + let written = 0; + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = data.slice(i, Math.min(i + chunkSize, data.length)); + await writer.write(chunk); + written += chunk.length; + if (written % (1024 * 1024 * 5) < chunkSize) { // Логируем каждые 5 МБ + console.log(`Записано: ${(written / 1024 / 1024).toFixed(2)} МБ из ${(data.length / 1024 / 1024).toFixed(2)} МБ`); + } + } + await writer.close(); + console.log('Данные записаны, ожидание завершения распаковки...'); + + // Ждём завершения чтения + const { chunks, totalRead } = await readPromise; + + // Объединяем все чанки в один массив + if (chunks.length === 0) { + throw new Error('Нет данных после распаковки'); + } + + const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; + } catch (err) { + lastError = err; + // Пробуем следующий формат + console.log(`Формат ${format} не сработал:`, err.message); + continue; + } + } + + // Если ни один формат не сработал + throw new Error(`Ошибка распаковки: ${lastError?.message || 'неизвестная ошибка'}. Попробованы форматы: ${formats.join(', ')}. ZIP использует raw deflate, который может не поддерживаться в вашем браузере.`); + } + + /** + * Парсит NPY файл (формат NumPy) + * @param {ArrayBuffer} buffer - ArrayBuffer с содержимым NPY файла + * @returns {Object} Объект с данными массива {data, shape, dtype} + */ + static parseNPY(buffer) { + const view = new DataView(buffer); + let offset = 0; + + // Проверяем магический заголовок (0x93 'NUMPY') + if (view.getUint8(offset) !== 0x93) { + throw new Error('Неверный формат NPY файла: неверный магический байт'); + } + offset++; + + const magic = String.fromCharCode( + view.getUint8(offset++), + view.getUint8(offset++), + view.getUint8(offset++), + view.getUint8(offset++), + view.getUint8(offset++) + ); + + if (magic !== 'NUMPY') { + throw new Error('Неверный формат NPY файла: неверная магическая строка'); + } + + // Читаем версию формата + const majorVersion = view.getUint8(offset++); + const minorVersion = view.getUint8(offset++); + + // Читаем длину заголовка + let headerLength; + if (majorVersion === 1) { + headerLength = view.getUint16(offset, true); + offset += 2; + } else if (majorVersion === 2 || majorVersion === 3) { + headerLength = view.getUint32(offset, true); + offset += 4; + } else { + throw new Error(`Неподдерживаемая версия NPY формата: ${majorVersion}.${minorVersion}`); + } + + // Читаем заголовок (Python dict в виде строки) + const headerBytes = new Uint8Array(buffer, offset, headerLength); + const headerStr = new TextDecoder('utf-8').decode(headerBytes); + offset += headerLength; + + // Парсим заголовок (упрощённый парсинг Python dict) + console.log('Заголовок NPY:', headerStr); + const header = this.parseHeader(headerStr); + console.log('Распарсенный заголовок:', header); + + // Читаем данные + const dataOffset = offset; + const dataLength = buffer.byteLength - dataOffset; + const data = new Uint8Array(buffer, dataOffset, dataLength); + + console.log(`Данные NPY: offset=${dataOffset}, length=${dataLength}, shape=${JSON.stringify(header.shape)}, dtype=${header.descr}`); + + // Проверяем, что shape и descr найдены + if (!header.descr) { + throw new Error('Не найден dtype (descr) в заголовке NPY'); + } + if (!header.shape || header.shape.length === 0) { + throw new Error('Не найден shape в заголовке NPY'); + } + + // Конвертируем данные в нужный тип + const typedArray = this.convertToTypedArray(data, header.descr, header.shape); + console.log(`Конвертировано: ${typedArray.length} элементов`); + + return { + data: typedArray, + shape: header.shape, + dtype: header.descr, + fortran_order: header.fortran_order || false + }; + } + + /** + * Парсит заголовок NPY файла (Python dict) + * @param {string} headerStr - Строка заголовка + * @returns {Object} Распарсенный заголовок + */ + static parseHeader(headerStr) { + // Упрощённый парсер Python dict + // Формат: {'descr': ' { + const trimmed = s.trim(); + return trimmed ? parseInt(trimmed, 10) : null; + }).filter(x => x !== null); // Убираем null значения + } else { + result.shape = []; // Скаляр + } + } else { + console.warn('Не найден shape в заголовке:', headerStr); + result.shape = []; + } + + return result; + } + + /** + * Конвертирует байты в типизированный массив + * @param {Uint8Array} data - Сырые данные + * @param {string} dtype - Описание типа данных (например, '} shape - Форма массива + * @returns {TypedArray} Типизированный массив + */ + static convertToTypedArray(data, dtype, shape) { + // Парсим dtype: ' little-endian, float64 + // Формат: [endian][type][size] + // endian: '<' (little), '>' (big), '=' (native), '|' (not applicable, single byte) + // type: 'f' (float), 'i' (int), 'u' (uint), 'c' (complex), 'b' (bool) + // size: количество байт (1, 2, 4, 8) + + console.log(`convertToTypedArray: dtype=${dtype}, shape=${JSON.stringify(shape)}, data.length=${data.length}`); + + const endian = dtype[0]; + const type = dtype[1]; + const size = parseInt(dtype.substring(2), 10); + + // Проверяем, что size валидный + if (isNaN(size) || size <= 0) { + throw new Error(`Неверный размер dtype: ${dtype}`); + } + + const isLittleEndian = endian === '<' || (endian === '=' && this.isLittleEndian()); + + // Вычисляем общее количество элементов + const totalElements = shape.length === 0 ? 1 : shape.reduce((a, b) => a * b, 1); + + let typedArray; + + switch (type) { + case 'f': // float + if (size === 4) { + typedArray = new Float32Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 8) { + typedArray = new Float64Array(data.buffer, data.byteOffset, totalElements); + } else { + throw new Error(`Неподдерживаемый размер float: ${size}`); + } + break; + + case 'i': // int (signed) + if (size === 1) { + typedArray = new Int8Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 2) { + typedArray = new Int16Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 4) { + typedArray = new Int32Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 8) { + // Int64Array не существует, используем BigInt64Array + typedArray = new BigInt64Array(data.buffer, data.byteOffset, totalElements); + } else { + throw new Error(`Неподдерживаемый размер int: ${size}`); + } + break; + + case 'u': // uint (unsigned) + if (size === 1) { + typedArray = new Uint8Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 2) { + typedArray = new Uint16Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 4) { + typedArray = new Uint32Array(data.buffer, data.byteOffset, totalElements); + } else if (size === 8) { + // Uint64Array не существует, используем BigUint64Array + typedArray = new BigUint64Array(data.buffer, data.byteOffset, totalElements); + } else { + throw new Error(`Неподдерживаемый размер uint: ${size}`); + } + break; + + case 'b': // bool + typedArray = new Uint8Array(data.buffer, data.byteOffset, totalElements); + break; + + case 'c': // complex + // Комплексные числа занимают в 2 раза больше места + if (size === 8) { + // complex64 = 2 * float32 + typedArray = new Float32Array(data.buffer, data.byteOffset, totalElements * 2); + } else if (size === 16) { + // complex128 = 2 * float64 + typedArray = new Float64Array(data.buffer, data.byteOffset, totalElements * 2); + } else { + throw new Error(`Неподдерживаемый размер complex: ${size}`); + } + break; + + default: + throw new Error(`Неподдерживаемый тип данных: ${type}`); + } + + // Если порядок байт не совпадает, нужно переставить байты + if (!isLittleEndian && size > 1) { + const view = new DataView(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); + for (let i = 0; i < typedArray.length; i++) { + const offset = i * size; + // Переставляем байты для каждого элемента + const bytes = new Uint8Array(size); + for (let j = 0; j < size; j++) { + bytes[j] = view.getUint8(offset + j); + } + bytes.reverse(); + for (let j = 0; j < size; j++) { + view.setUint8(offset + j, bytes[j]); + } + } + } + + return typedArray; + } + + /** + * Проверяет, является ли система little-endian + * @returns {boolean} + */ + static isLittleEndian() { + const buffer = new ArrayBuffer(2); + const view = new DataView(buffer); + view.setUint16(0, 0x0001, true); + return view.getUint8(0) === 1; + } +} +