docs: add common tools and components
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user