Skip to content

Commit

Permalink
docs: added JSDoc to all classes
Browse files Browse the repository at this point in the history
  • Loading branch information
cereallarceny committed Sep 11, 2024
1 parent 3b8f0f4 commit a604c2d
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 72 deletions.
13 changes: 13 additions & 0 deletions packages/reader/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { createIndexTag, getIndex, isHead } from '@flipbookqr/shared';

/**
* Sorts frames based on their index. Ensures the head frame is always first.
*
* @param {string} a - The first frame to compare.
* @param {string} b - The second frame to compare.
* @returns {number} - Returns -1 if `a` should come before `b`, 1 if `a` should come after `b`, and 0 if they are equal.
*/
export const sortFrames = (a: string, b: string): number => {
// Make sure the head frame is first
if (isHead(a)) return -1;
Expand All @@ -14,6 +21,12 @@ export const sortFrames = (a: string, b: string): number => {
return 0;
};

/**
* Slices a frame string to remove everything before the index tag.
*
* @param {string} frame - The frame string to slice.
* @returns {string} - The sliced frame string.
*/
export const sliceFrames = (frame: string): string => {
// Get the index
const idx = getIndex(frame);
Expand Down
77 changes: 53 additions & 24 deletions packages/reader/src/processors/file-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ export class FileProcessor extends FrameProcessor {
* @param {File} file - The file to be processed (e.g., an image file or a GIF).
*/
constructor(file: File) {
// Initialize the processor
super();

// Set up logger
this._log = getLogger();

// Set the file to process
this._file = file;

// Create canvas element
const canvas = document.createElement('canvas');

// Set default canvas dimensions
this._width = 1920;
this._height = 1080;

this._canvas = canvas;
this._ctx = canvas.getContext('2d');
}
Expand All @@ -47,21 +48,24 @@ export class FileProcessor extends FrameProcessor {
* @throws Will throw an error if an unsupported frame type is provided.
*/
protected setFrame(frame: HTMLImageElement | ImageBitmap): void {
// Store the frame dimensions
let width, height;

// Get the frame dimensions
if (frame instanceof HTMLImageElement || frame instanceof ImageBitmap) {
width = frame.width;
height = frame.height;
} else {
throw new Error('Unsupported frame type');
}

// Update the canvas dimensions
this._width = width;
this._height = height;

this._canvas.width = width;
this._canvas.height = height;

// Draw the frame on the canvas
this._ctx?.drawImage(frame, 0, 0, this._width, this._height);
}

Expand All @@ -71,20 +75,24 @@ export class FileProcessor extends FrameProcessor {
* @returns {QRCode | null} The decoded QR code data, or null if no data is found.
*/
protected getFrameData(): QRCode | null {
// Get the image data from the canvas
const imageData = this._ctx?.getImageData(0, 0, this._width, this._height);

// Decode the image data to extract QR code data
let decodedData: null | QRCode = null;
if (imageData && 'data' in imageData) {
decodedData = jsQR(imageData.data, this._width, this._height);
}

// Return the decoded data
return decodedData;
}

/**
* Cleans up by removing the canvas element from the DOM.
*/
protected destroy(): void {
// Remove the canvas element
this._canvas.remove();
}

Expand All @@ -93,17 +101,24 @@ export class FileProcessor extends FrameProcessor {
*
* @returns {Promise<string>} A promise that resolves to the extracted QR code data, or an empty string if no data is found.
*/
protected processSingleFrame(): Promise<string> {
protected processSingleFrame(file: File): Promise<string> {
return new Promise((resolve, reject) => {
try {
// Create a new image element
const img = new Image();

// When the image is loaded, extract the QR code data
img.onload = () => {
this.setFrame(img);
const frameData = this.getFrameData();
resolve(frameData?.data || '');
};

// When an error occurs, reject the promise
img.onerror = reject;
img.src = URL.createObjectURL(this._file);

// Set the image source to the file URL
img.src = URL.createObjectURL(file);
} catch (error) {
reject(new Error('Failed to process frame'));
}
Expand All @@ -116,10 +131,16 @@ export class FileProcessor extends FrameProcessor {
* @returns {Promise<string[]>} A promise that resolves to an array of QR code data from each frame.
*/
protected async processAllFrames(): Promise<string[]> {
// Convert the GIF file to an ArrayBuffer
const buffer = await this.convertFileToBuffer(this._file);

// Parse the GIF file
const parsed = parseGIF(buffer);

// Decompress the frames
const frames = decompressFrames(parsed, true);

// Convert each frame to a File object
const files: File[] = frames.map((frame) =>
this.convertUnit8ClampedArrayToFile(
frame.patch,
Expand All @@ -129,24 +150,12 @@ export class FileProcessor extends FrameProcessor {
)
);

const dataPromises: Promise<string>[] = files.map(
(file) =>
new Promise((resolve, reject) => {
try {
const img = new Image();
img.onload = () => {
this.setFrame(img);
const frameData = this.getFrameData();
resolve(frameData?.data || '');
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
} catch (error) {
reject(new Error('Failed to extract data from file'));
}
})
// Process each frame
const dataPromises: Promise<string>[] = files.map((file) =>
this.processSingleFrame(file)
);

// Return the QR code data from each frame
return await Promise.all(dataPromises);
}

Expand All @@ -157,6 +166,7 @@ export class FileProcessor extends FrameProcessor {
* @returns {boolean} True if the file is a GIF, false otherwise.
*/
protected isGIF(file: File): boolean {
// Check the file extension and MIME type
const extension = file.name.split('.').pop()?.toLowerCase();
return extension === 'gif' || file.type.startsWith('image/gif');
}
Expand All @@ -169,16 +179,21 @@ export class FileProcessor extends FrameProcessor {
*/
protected convertFileToBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
// Create a new FileReader instance
const reader = new FileReader();

// When the file is read, resolve the promise with the ArrayBuffer
reader.onload = (event) => {
const arrayBuffer = event.target?.result;
if (arrayBuffer) {
resolve(arrayBuffer as ArrayBuffer);
}
};

// When an error occurs, reject the promise
reader.onerror = () => reject(new Error('Failed to read file'));

// Read the file as an ArrayBuffer
reader.readAsArrayBuffer(file);
});
}
Expand All @@ -198,21 +213,28 @@ export class FileProcessor extends FrameProcessor {
height: number,
fileName: string
): File {
// Create a canvas element
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;

// Get the 2D context
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('2D context not supported');
}

// Create an ImageData object and put it on the canvas
const imageDataObj = new ImageData(imageData, width, height);
ctx.putImageData(imageDataObj, 0, 0);

// Convert the canvas to a data URL
const dataURL = canvas.toDataURL();

// Convert the data URL to a Blob object
const blob = this.dataURLtoBlob(dataURL);

// Create a new File object
return new File([blob], fileName, { type: blob.type });
}

Expand All @@ -223,16 +245,19 @@ export class FileProcessor extends FrameProcessor {
* @returns {Blob} The Blob object created from the data URL.
*/
protected dataURLtoBlob(dataURL: string): Blob {
// Split the data URL into parts
const parts = dataURL.split(';base64,');
const contentType = parts[0]?.split(':')[1];
const raw = window.atob(parts[1]!);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);

// Create a Uint8Array from the raw data
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}

// Return a new Blob object
return new Blob([uInt8Array], { type: contentType });
}

Expand All @@ -242,11 +267,15 @@ export class FileProcessor extends FrameProcessor {
* @returns {Promise<string>} A promise that resolves to the processed QR code data as a string.
*/
async read(): Promise<string> {
// If the file is a GIF, process all frames
if (this.isGIF(this._file)) {
const allFrames = await this.processAllFrames();
return allFrames.sort(sortFrames).map(sliceFrames).join('');
} else {
return await this.processSingleFrame();
}

// Otherwise, process a single frame
else {
return await this.processSingleFrame(this._file);
}
}
}
Loading

0 comments on commit a604c2d

Please sign in to comment.