docs: add common tools and components

This commit is contained in:
2026-08-23 08:20:14 +05:00
parent b48a8adfa7
commit 3b18d0c6f8
8 changed files with 322 additions and 20 deletions
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { encodeBmpBytes } from './bmp';
import { makeImage } from './test-helpers';
describe('encodeBmpBytes', () => {
it('пишет заголовки BM и размеры для 2x1 без паддинга-неопределённости', () => {
const bytes = encodeBmpBytes(
makeImage(2, 1, [
[255, 0, 0, 255],
[0, 255, 0, 255]
])
);
expect([bytes[0], bytes[1]]).toEqual([0x42, 0x4d]);
const view = new DataView(bytes.buffer);
expect(view.getUint32(2, true)).toBe(54 + 8);
expect(view.getInt32(18, true)).toBe(2);
expect(view.getInt32(22, true)).toBe(1);
expect(view.getUint16(28, true)).toBe(24);
});
it('хранит пиксели в BGR снизу-вверх с паддингом строки', () => {
const bytes = encodeBmpBytes(
makeImage(2, 1, [
[255, 0, 0, 255],
[0, 255, 0, 255]
])
);
expect([...bytes.slice(54, 60)]).toEqual([
0, 0, 255,
0, 255, 0
]);
expect(bytes[60]).toBe(0);
expect(bytes[61]).toBe(0);
});
it('нижняя строка изображения идёт первой в файле', () => {
const bytes = encodeBmpBytes(
makeImage(4, 2, [
[10, 10, 10, 255],
[20, 20, 20, 255],
[30, 30, 30, 255],
[40, 40, 40, 255],
[50, 50, 50, 255],
[60, 60, 60, 255],
[70, 70, 70, 255],
[80, 80, 80, 255]
])
);
expect(bytes[54]).toBe(50);
expect(bytes[54 + 12]).toBe(10);
});
});
+37
View File
@@ -0,0 +1,37 @@
import type { PixelImage } from './types';
export function encodeBmpBytes(img: PixelImage): Uint8Array<ArrayBuffer> {
const rowSize = Math.ceil((img.width * 3) / 4) * 4;
const pixelArraySize = rowSize * img.height;
const fileSize = 54 + pixelArraySize;
const buf = new Uint8Array(fileSize);
const view = new DataView(buf.buffer);
buf[0] = 0x42;
buf[1] = 0x4d;
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true);
view.setUint32(14, 40, true);
view.setInt32(18, img.width, true);
view.setInt32(22, img.height, true);
view.setUint16(26, 1, true);
view.setUint16(28, 24, true);
view.setUint32(30, 0, true);
view.setUint32(34, pixelArraySize, true);
view.setInt32(38, 2835, true);
view.setInt32(42, 2835, true);
for (let y = 0; y < img.height; y++) {
const srcY = img.height - 1 - y;
let off = 54 + y * rowSize;
for (let x = 0; x < img.width; x++) {
const i = (srcY * img.width + x) * 4;
buf[off++] = img.data[i + 2];
buf[off++] = img.data[i + 1];
buf[off++] = img.data[i];
}
}
return buf;
}
+5 -1
View File
@@ -1,6 +1,7 @@
import { encodeBmpBytes } from './bmp';
import { type PixelImage } from './types';
export type OutputMime = 'image/png' | 'image/jpeg' | 'image/webp';
export type OutputMime = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/bmp';
export const ACCEPTED_IMAGE_TYPES =
'image/png,image/jpeg,image/webp,image/gif,image/bmp,image/x-icon';
@@ -38,6 +39,9 @@ export async function encode(
mime: OutputMime = 'image/png',
quality?: number
): Promise<Blob> {
if (mime === 'image/bmp') {
return new Blob([encodeBmpBytes(img)], { type: mime });
}
if (mime === 'image/jpeg' || mime === 'image/webp') {
if (quality !== undefined && (quality < 0 || quality > 1)) {
throw new RangeError('quality должен быть в диапазоне 0..1');