forked from deriv-com/deriv-utils
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request deriv-com#15 from heorhi-deriv/FEQ-1990-Transfer-f…
…ile-utils George / FEQ-1990 / Transfer file utils
- Loading branch information
Showing
5 changed files
with
311 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const supportedDocumentFormats = ["PNG", "JPG", "JPEG", "GIF", "PDF"] as const; | ||
|
||
export type DocumentFormats = (typeof supportedDocumentFormats)[number]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,15 @@ | ||
import * as AppIDConstants from "./app-id.constants"; | ||
import * as CurrencyConstants from "./currency.constants"; | ||
import * as DocumentConstants from "./document.constants"; | ||
import * as LocalStorageConstants from "./localstorage.constants"; | ||
import * as URLConstants from "./url.constants"; | ||
import * as ValidationConstants from "./validation.constants"; | ||
|
||
export { AppIDConstants, CurrencyConstants, LocalStorageConstants, URLConstants, ValidationConstants }; | ||
export { | ||
AppIDConstants, | ||
CurrencyConstants, | ||
DocumentConstants, | ||
LocalStorageConstants, | ||
URLConstants, | ||
ValidationConstants, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { convertToBase64, isSupportedImageFormat } from "../image.utils"; | ||
import { describe, test, expect } from "vitest"; | ||
|
||
describe("convertToBase64()", () => { | ||
const mockImageFile = new File(["image-data-contents"], "image1.jpg", { type: "image/jpeg" }); | ||
|
||
test("should convert a File to base64", async () => { | ||
const base64Image = await convertToBase64(mockImageFile); | ||
|
||
expect(base64Image).toEqual( | ||
expect.objectContaining({ | ||
src: expect.any(String), | ||
filename: "image1.jpg", | ||
}), | ||
); | ||
|
||
expect(base64Image.src).toMatch(/^data:image\/jpeg;base64,/); | ||
}); | ||
|
||
test("should handle empty files", async () => { | ||
const mockFile = new File([], "empty.txt", { type: "text/plain" }); | ||
const base64Image = await convertToBase64(mockFile); | ||
|
||
expect(base64Image).toEqual( | ||
expect.objectContaining({ | ||
src: "data:text/plain;base64,", | ||
filename: "empty.txt", | ||
}), | ||
); | ||
}); | ||
|
||
test("should handle files with special characters in their names", async () => { | ||
const mockFile = new File(["file contents"], "special&chars.jpg", { type: "image/jpeg" }); | ||
const base64Image = await convertToBase64(mockFile); | ||
|
||
expect(base64Image).toEqual( | ||
expect.objectContaining({ | ||
src: expect.any(String), | ||
filename: "special&chars.jpg", | ||
}), | ||
); | ||
|
||
expect(base64Image.src).toMatch(/^data:image\/jpeg;base64,/); | ||
}); | ||
|
||
test("should handle non-image files", async () => { | ||
const mockFile = new File(["file contents"], "document.pdf", { type: "application/pdf" }); | ||
const base64Image = await convertToBase64(mockFile); | ||
|
||
expect(base64Image).toEqual( | ||
expect.objectContaining({ | ||
src: expect.any(String), | ||
filename: "document.pdf", | ||
}), | ||
); | ||
|
||
expect(base64Image.src).toMatch(/^data:application\/pdf;base64,/); | ||
}); | ||
|
||
test("should handle files with no type information", async () => { | ||
const mockFile = new File(["file contents"], "unknown", { type: "" }); | ||
const base64Image = await convertToBase64(mockFile); | ||
|
||
expect(base64Image).toEqual( | ||
expect.objectContaining({ | ||
src: expect.any(String), | ||
filename: "unknown", | ||
}), | ||
); | ||
}); | ||
}); | ||
|
||
describe("isSupportedImageFormat()", () => { | ||
test("should return true for supported image formats", () => { | ||
expect(isSupportedImageFormat("image1.png")).toBe(true); | ||
expect(isSupportedImageFormat("image1.jpg")).toBe(true); | ||
expect(isSupportedImageFormat("image1.jpeg")).toBe(true); | ||
expect(isSupportedImageFormat("image1.gif")).toBe(true); | ||
expect(isSupportedImageFormat("document.pdf")).toBe(true); | ||
expect(isSupportedImageFormat("mixed.CaSe.JPeG")).toBe(true); | ||
}); | ||
|
||
test("should return false for unsupported image formats", () => { | ||
expect(isSupportedImageFormat("file.txt")).toBe(false); | ||
expect(isSupportedImageFormat("document.docx")).toBe(false); | ||
expect(isSupportedImageFormat("data.xml")).toBe(false); | ||
expect(isSupportedImageFormat("fake-image.jpg.txt")).toBe(false); | ||
expect(isSupportedImageFormat("compressed.jpg.rar")).toBe(false); | ||
}); | ||
|
||
test("should handle edge cases", () => { | ||
// @ts-expect-error - test case to simulate passing null | ||
expect(isSupportedImageFormat(null)).toBe(false); | ||
// @ts-expect-error - test case to simulate passing undefined | ||
expect(isSupportedImageFormat(undefined)).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import { DocumentConstants } from "../constants"; | ||
|
||
const DEFAULT_IMAGE_WIDTH = 2560; | ||
const DEFAULT_IMAGE_QUALITY = 0.9; | ||
const WORD_SIZE = 4; | ||
|
||
interface IExtendedBlob extends Blob { | ||
lastModifiedDate?: number; | ||
name?: string; | ||
} | ||
|
||
type TCompressImageOption = { | ||
maxWidth?: number; | ||
quality?: number; | ||
}; | ||
|
||
type TBase64Image = { | ||
filename: string; | ||
src: string; | ||
}; | ||
|
||
type TCompressImage = TBase64Image & { | ||
options?: TCompressImageOption; | ||
}; | ||
|
||
export type TFileObject = { | ||
filename?: File["name"]; | ||
buffer: FileReader["result"]; | ||
fileSize: File["size"]; | ||
}; | ||
|
||
/** | ||
* Compress an image and return it as a Blob. | ||
* @param {TCompressImage} params - The parameters for image compression. | ||
* @param {string} params.src - The source image URL or data URI. | ||
* @param {string} params.filename - The desired filename for the compressed image. | ||
* @param {Object} [params.options] - Options for image compression. | ||
* @param {number} [params.options.maxWidth=DEFAULT_IMAGE_WIDTH] - The maximum width for the compressed image. | ||
* @param {number} [params.options.quality=DEFAULT_IMAGE_QUALITY] - The image quality (0 to 1) for compression. | ||
* @returns {Promise<IExtendedBlob>} A Promise that resolves with the compressed image as a Blob. | ||
*/ | ||
export const compressImage = ({ src, filename, options }: TCompressImage): Promise<IExtendedBlob> => { | ||
const { maxWidth = DEFAULT_IMAGE_WIDTH, quality = DEFAULT_IMAGE_QUALITY } = options || {}; | ||
|
||
return new Promise((resolve, reject) => { | ||
const image = new Image(); | ||
image.src = src; | ||
image.onload = () => { | ||
const canvas = document.createElement("canvas"); | ||
const canvas_context = canvas.getContext("2d"); | ||
if (!canvas_context || !(canvas_context instanceof CanvasRenderingContext2D)) { | ||
return reject(new Error("Failed to get 2D context")); | ||
} | ||
|
||
if (image.naturalWidth > maxWidth) { | ||
const width = DEFAULT_IMAGE_WIDTH; | ||
const scaleFactor = width / image.naturalWidth; | ||
canvas.width = width; | ||
canvas.height = image.naturalHeight * scaleFactor; | ||
} else { | ||
canvas.width = image.naturalWidth; | ||
canvas.height = image.naturalHeight; | ||
} | ||
|
||
canvas_context.fillStyle = "transparent"; | ||
canvas_context.fillRect(0, 0, canvas.width, canvas.height); | ||
canvas_context.save(); | ||
canvas_context.drawImage(image, 0, 0, canvas.width, canvas.height); | ||
|
||
canvas.toBlob( | ||
(blob) => { | ||
if (!blob) return; | ||
const modified_filename = filename.replace(/\.[^/.]+$/, ".jpg"); | ||
const file: IExtendedBlob = new Blob([blob], { type: "image/jpeg" }); | ||
file.lastModifiedDate = Date.now(); | ||
file.name = modified_filename; | ||
resolve(file); | ||
}, | ||
"image/jpeg", | ||
quality, | ||
); | ||
}; | ||
}); | ||
}; | ||
|
||
/** | ||
* Convert a File to a Base64 encoded image representation. | ||
* @param {File} file - The File object to convert to Base64. | ||
* @returns {Promise<TBase64Image>} A Promise that resolves with an object containing the Base64 image data and the filename. | ||
*/ | ||
export const convertToBase64 = (file: File): Promise<TBase64Image> => { | ||
return new Promise((resolve) => { | ||
const reader = new FileReader(); | ||
reader.readAsDataURL(file); | ||
reader.onloadend = () => { | ||
resolve({ | ||
src: reader.result?.toString() || "", | ||
filename: file.name, | ||
}); | ||
}; | ||
}); | ||
}; | ||
|
||
/** | ||
* Check if a given filename has a supported image format extension. | ||
* | ||
* @param {string} filename - The filename to check for a supported image format. | ||
* @returns {boolean} True if the filename has a supported image format extension, false otherwise. | ||
*/ | ||
export const isSupportedImageFormat = (filename: string) => { | ||
if (!filename) return false; | ||
|
||
return DocumentConstants.supportedDocumentFormats.some((documentFormat) => | ||
filename.toUpperCase().endsWith(documentFormat), | ||
); | ||
}; | ||
|
||
/** | ||
* Convert image to base64 and compress an image file if it is a supported image format. | ||
* | ||
* @param {File} file - The File object to compress. | ||
* @returns {Promise<Blob>} A Promise that resolves with the compressed image as a Blob. | ||
*/ | ||
export const compressImageFile = (file: File) => { | ||
return new Promise<Blob>((resolve) => { | ||
if (isSupportedImageFormat(file.name)) { | ||
convertToBase64(file).then((img) => { | ||
compressImage(img).then(resolve); | ||
}); | ||
} else { | ||
resolve(file); | ||
} | ||
}); | ||
}; | ||
|
||
/** | ||
* Get Uint8Array from number | ||
* | ||
* @param {num} number - The number to convert to Uint8Array. | ||
* @returns {Uint8Array} Uint8Array | ||
*/ | ||
export function numToUint8Array(num: number, arraySize = WORD_SIZE) { | ||
const typedArray = new Uint8Array(arraySize); | ||
const dv = new DataView(typedArray.buffer); | ||
dv.setUint32(0, num); | ||
return typedArray; | ||
} | ||
|
||
/** | ||
* Turn binary into array of chunks | ||
* | ||
* @param {binary} Uint8Array - Uint8Array to be chunked. | ||
* @returns {Uint8Array[]} Array of Uint8Array chunks | ||
*/ | ||
export const generateChunks = (binary: Uint8Array, { chunkSize = 16384 /* 16KB */ }) => { | ||
const chunks = []; | ||
for (let i = 0; i < binary.length; i++) { | ||
const item = binary[i]; | ||
if (i % chunkSize === 0) { | ||
chunks.push([item]); | ||
} else { | ||
chunks[chunks.length - 1].push(item); | ||
} | ||
} | ||
return chunks.map((b) => new Uint8Array(b)).concat(new Uint8Array([])); | ||
}; | ||
|
||
/** | ||
* Read a file and return it as modified object with a buffer of the file contents. | ||
* @param {IExtendedBlob} file - The file to read. | ||
* @returns {Promise<TFileObject>} A Promise that resolves with the file as a TFileObject. | ||
* | ||
*/ | ||
export const readFile = (file: IExtendedBlob) => { | ||
const fr = new FileReader(); | ||
return new Promise< | ||
| TFileObject | ||
| { | ||
message: string; | ||
} | ||
>((resolve) => { | ||
fr.onload = () => { | ||
const fileMetadata = { | ||
filename: file.name, | ||
buffer: fr.result, | ||
fileSize: file.size, | ||
}; | ||
resolve(fileMetadata); | ||
}; | ||
|
||
fr.onerror = () => { | ||
resolve({ | ||
message: `Unable to read file ${file.name}`, | ||
}); | ||
}; | ||
|
||
// Reading file | ||
fr.readAsArrayBuffer(file); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import * as FormatUtils from "./format.utils"; | ||
import * as ImageUtils from "./image.utils"; | ||
import * as LocalStorageUtils from "./localstorage.utils"; | ||
import * as ObjectUtils from "./object.utils"; | ||
import * as PromiseUtils from "./promise.utils"; | ||
import * as URLUtils from "./url.utils"; | ||
import * as WebSocketUtils from "./websocket.utils"; | ||
|
||
export { FormatUtils, LocalStorageUtils, ObjectUtils, PromiseUtils, URLUtils, WebSocketUtils }; | ||
export { ImageUtils, FormatUtils, LocalStorageUtils, ObjectUtils, PromiseUtils, URLUtils, WebSocketUtils }; |