diff --git a/package.json b/package.json index 068a68e..7dcac97 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/conversion.ts b/src/conversion.ts index ef7856c..ace5799 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -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; type Pixel = [number, number, number, number]; // Define type for pixel data (alpha, red, green, blue) function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array { @@ -29,7 +30,9 @@ 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++) { @@ -37,7 +40,10 @@ async function compositeImages(sourceImage: Image, destinationImage: Image) { 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); } } @@ -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; @@ -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 => { @@ -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); @@ -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 { - 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 { + 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'), @@ -191,7 +208,7 @@ export class RattaRLEDecoder { ) { const pal = palette ?? defaultPalette; const translation = Object.entries(this.encodedPalette).reduce( - (acc: Record, [key, value]) => { + (acc: Record, [key, value]) => { acc[value] = pal[key] ?? defaultPalette[key]; return acc; }, @@ -265,10 +282,12 @@ export class RattaRLEDecoder { chunks: Uint8Array[], encodedColor: number, length: number, - translation: Record, + translation: Record, ): 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 @@ -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++) { diff --git a/src/parsing.ts b/src/parsing.ts index 9067746..0eb5cfc 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -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; } diff --git a/tests/input/digest_test.mark b/tests/input/digest_test.mark new file mode 100644 index 0000000..262fe9f Binary files /dev/null and b/tests/input/digest_test.mark differ diff --git a/tests/main.test.ts b/tests/main.test.ts index 29c94fa..e575308 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -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")) @@ -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", () => { diff --git a/tsconfig.json b/tsconfig.json index 3f67214..5407317 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2018", "module": "ESNext", "moduleResolution": "node", "strict": true,