Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"homepage": "https://gitlab.com/philips/supernote-typescript",
"devDependencies": {
"@types/color": "^3.0.3",
"@types/color": "^3.0.7",
"@types/fs-extra": "^9.0.13",
"@types/node": "^18.6.5",
"@types/sharp": "^0.30.5",
Expand Down
74 changes: 48 additions & 26 deletions src/conversion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ILayer, ISupernote } from './format';
import { Image, ImageColorModel, decodePng } from "image-js";
import { Image, ImageColorModel, decodePng } from 'image-js';
import Color from 'color';

type ColorType = InstanceType<typeof Color>;
type Pixel = [number, number, number, number]; // Define type for pixel data (alpha, red, green, blue)

function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {
Expand Down Expand Up @@ -29,15 +30,20 @@ async function compositeImages(sourceImage: Image, destinationImage: Image) {
sourceImage.width !== destinationImage.width ||
sourceImage.height !== destinationImage.height
) {
throw new Error('Images must have the same dimensions for compositing.');
throw new Error(
'Images must have the same dimensions for compositing.',
);
}

for (let y = 0; y < destinationImage.height; y++) {
for (let x = 0; x < destinationImage.width; x++) {
const sourcePixel = sourceImage.getPixel(x, y) as Pixel; // Explicitly cast for type safety
const destinationPixel = destinationImage.getPixel(x, y) as Pixel;

const blendedPixel = blendPixelOverlay(sourcePixel, destinationPixel);
const blendedPixel = blendPixelOverlay(
sourcePixel,
destinationPixel,
);
destinationImage.setPixel(x, y, blendedPixel);
}
}
Expand All @@ -50,7 +56,12 @@ function blendPixelOverlay(sourcePixel: Pixel, destinationPixel: Pixel): Pixel {

// HACK: if the pixel is fully transparent just pass the lower pixel through
if (sourceR === 0 && sourceG === 0 && sourceB === 0 && sourceA == 0) {
return [destinationA, destinationR, destinationG, destinationB] as Pixel;
return [
destinationA,
destinationR,
destinationG,
destinationB,
] as Pixel;
}

return sourcePixel;
Expand All @@ -68,8 +79,11 @@ export function toImage(note: ISupernote, pageNumbers?: number[]) {
const decoder = new RattaRLEDecoder();
return Promise.all(
pages.map(async (page, pageIndex) => {
const overlays = page.LAYERSEQ.map((name) => page[name] as ILayer).filter(
(layer) => layer.bitmapBuffer !== null && layer.bitmapBuffer.length,
const overlays = page.LAYERSEQ.map(
(name) => page[name] as ILayer,
).filter(
(layer) =>
layer.bitmapBuffer !== null && layer.bitmapBuffer.length,
);

const promises = overlays.map(async (layer): Promise<Image> => {
Expand All @@ -86,7 +100,10 @@ export function toImage(note: ISupernote, pageNumbers?: number[]) {
note.pageWidth,
note.pageHeight,
);
return new Image(note.pageWidth, note.pageHeight, { colorModel: ImageColorModel.RGBA, data: buffer });
return new Image(note.pageWidth, note.pageHeight, {
colorModel: ImageColorModel.RGBA,
data: buffer,
});
});

let images = await Promise.all(promises);
Expand All @@ -102,25 +119,25 @@ export function toImage(note: ISupernote, pageNumbers?: number[]) {
}

/** Color palette to use as substitutes for the Supernote's colors. */
export interface IColorPalette extends Record<string, Color> {
background: Color;
black: Color;
darkGray: Color;
gray: Color;
white: Color;
markerBlack: Color;
markerDarkGray: Color;
markerGray: Color;

darkGrayX2: Color;
grayX2: Color;
markerDarkGrayX2: Color;
markerGrayX2: Color;
export interface IColorPalette extends Record<string, ColorType> {
background: ColorType;
black: ColorType;
darkGray: ColorType;
gray: ColorType;
white: ColorType;
markerBlack: ColorType;
markerDarkGray: ColorType;
markerGray: ColorType;

darkGrayX2: ColorType;
grayX2: ColorType;
markerDarkGrayX2: ColorType;
markerGrayX2: ColorType;
}

/** Default color palette to use based on named colors in the color library. */
const defaultPalette: IColorPalette = {
background: Color('transparent'),
background: Color('white'),
black: Color('black'),
darkGray: Color('darkgray'),
gray: Color('gray'),
Expand Down Expand Up @@ -191,7 +208,7 @@ export class RattaRLEDecoder {
) {
const pal = palette ?? defaultPalette;
const translation = Object.entries(this.encodedPalette).reduce(
(acc: Record<number, Color>, [key, value]) => {
(acc: Record<number, ColorType>, [key, value]) => {
acc[value] = pal[key] ?? defaultPalette[key];
return acc;
},
Expand Down Expand Up @@ -265,10 +282,12 @@ export class RattaRLEDecoder {
chunks: Uint8Array[],
encodedColor: number,
length: number,
translation: Record<number, Color>,
translation: Record<number, ColorType>,
): Uint8Array[] {
let newColor =
encodedColor === -1 ? Color('transparent') : translation[encodedColor];
encodedColor === -1
? Color('transparent')
: translation[encodedColor];
let chunk: Uint8Array;
if (newColor === undefined) {
// HACK(philips): if we get an unknown color just ignore it and make it black
Expand All @@ -277,7 +296,10 @@ export class RattaRLEDecoder {
chunk = Uint8Array.from(new Uint8Array([0, 0, 0, 0]));
} else {
chunk = Uint8Array.from(
new Uint8Array([...newColor.rgb().array(), ~~(255 * newColor.alpha())]),
new Uint8Array([
...newColor.rgb().array(),
~~(255 * newColor.alpha()),
]),
);
}
for (let index = 0; index < length; index++) {
Expand Down
4 changes: 2 additions & 2 deletions src/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,13 @@ export class SupernoteX {

/** Parse Supernote file signature from buffer. */
_parseSignature(buffer: Uint8Array): string {
const pattern = /^noteSN_FILE_VER_(\d{8})/;
const pattern = /^(mark|note)SN_FILE_VER_(\d{8})/;
const content = uint8ArrayToString(buffer, 'utf8', 0, 24);
const match = content.match(pattern);
if (!match)
throw new Error("Cannot parse this file. Signature doesn't match.");
this.signature = content;
this.version = parseFloat(match[1]);
this.version = parseFloat(match[2]);
return this.signature;
}

Expand Down
Binary file added tests/input/digest_test.mark
Binary file not shown.
81 changes: 50 additions & 31 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ describe("image", () => {
}, { timeout: 30000 })
})

describe('digest_image', () => {
test(
'convert mark file into a png image',
async () => {
let sn = new SupernoteX(await readFileToUint8Array('digest_test.mark'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
await imagejs.writeSync(
`tests/output/digest_image.mark-${index}.png`,
image,
);
}
},
{ timeout: 30000 },
);
});


describe("nomad", () => {
test("convert a note from a nomad Chauvet 3.15.27 to png pages", async () => {
let sn = new SupernoteX(await readFileToUint8Array("nomad-3.15.27-blank-2p.note"))
Expand Down Expand Up @@ -101,40 +120,40 @@ describe("horizontal orientation value detection", () => {
}
}, 30000)

test('convert a horizontal note from a A6X2 Nomad w/ orientation value 1270', async () => {
let sn = new SupernoteX(await readFileToUint8Array('horizontal_1270.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.width).toBeGreaterThan(image.height); // expect a landscape image
await imagejs.writeSync(`tests/output/horizontal_1270.note-${index}.png`, image);
}
}, 30000);
test('convert a horizontal note from a A6X2 Nomad w/ orientation value 1270', async () => {
let sn = new SupernoteX(await readFileToUint8Array('horizontal_1270.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.width).toBeGreaterThan(image.height); // expect a landscape image
await imagejs.writeSync(`tests/output/horizontal_1270.note-${index}.png`, image);
}
}, 30000);

// Ensure vertical orientation values (1000, 1180) are still read as vertical
test('convert a vertical note w/ orientation value 1000', async () => {
let sn = new SupernoteX(await readFileToUint8Array('vertical_1000.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.height).toBeGreaterThan(image.width); // expect a portrait image
await imagejs.writeSync(`tests/output/vertical_1000.note-${index}.png`, image);
}
}, 30000);
test('convert a vertical note w/ orientation value 1000', async () => {
let sn = new SupernoteX(await readFileToUint8Array('vertical_1000.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.height).toBeGreaterThan(image.width); // expect a portrait image
await imagejs.writeSync(`tests/output/vertical_1000.note-${index}.png`, image);
}
}, 30000);

test(
'convert a vertical note from a A6X2 Nomad w/ orientation value 1180',
async () => {
let sn = new SupernoteX(await readFileToUint8Array('vertical_1180.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.height).toBeGreaterThan(image.width); // expect a portrait image
await imagejs.writeSync(`tests/output/vertical_1180.note-${index}.png`, image);
}
},
{ timeout: 30000 },
);
test(
'convert a vertical note from a A6X2 Nomad w/ orientation value 1180',
async () => {
let sn = new SupernoteX(await readFileToUint8Array('vertical_1180.note'));
let images = await toImage(sn);
expect(images).not.toBeUndefined();
for await (const [index, image] of images.entries()) {
expect(image.height).toBeGreaterThan(image.width); // expect a portrait image
await imagejs.writeSync(`tests/output/vertical_1180.note-${index}.png`, image);
}
},
{ timeout: 30000 },
);
})

describe("color", () => {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{
"compilerOptions": {
"target": "es2016",
"target": "es2018",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
Expand Down