From 8615b962096f8f16852e398f9c504984908ce554 Mon Sep 17 00:00:00 2001 From: "tommy(Chung-il)" Date: Tue, 19 Sep 2023 02:58:59 +0900 Subject: [PATCH] feat: utility for MIME type detection (detect func) (#24) ## Summary - Integrated a MIME type detection utility in the `fepack/image` package. This enhancement will allow users to easily identify and work with various image formats. - Being able to reliably determine the MIME type of an image ensures better compatibility and error handling. potentially eliminating a number of runtime errors. ## Checks Please check the following: - [x] I have written documents and tests, if needed. --- .changeset/stale-bulldogs-report.md | 5 ++ .changeset/sweet-gifts-grow.md | 5 ++ packages/image/package.json | 1 + packages/image/src/__test__/detect.test.ts | 33 +++++++++++++ packages/image/src/detect.ts | 57 ++++++++++++++++++++++ packages/image/src/index.ts | 1 + pnpm-lock.yaml | 51 ++++++++++--------- 7 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 .changeset/stale-bulldogs-report.md create mode 100644 .changeset/sweet-gifts-grow.md create mode 100644 packages/image/src/__test__/detect.test.ts create mode 100644 packages/image/src/detect.ts diff --git a/.changeset/stale-bulldogs-report.md b/.changeset/stale-bulldogs-report.md new file mode 100644 index 0000000..332d2dc --- /dev/null +++ b/.changeset/stale-bulldogs-report.md @@ -0,0 +1,5 @@ +--- +"@fepack/image": patch +--- + +feat: utility for MIME type detection (detect func) diff --git a/.changeset/sweet-gifts-grow.md b/.changeset/sweet-gifts-grow.md new file mode 100644 index 0000000..332d2dc --- /dev/null +++ b/.changeset/sweet-gifts-grow.md @@ -0,0 +1,5 @@ +--- +"@fepack/image": patch +--- + +feat: utility for MIME type detection (detect func) diff --git a/packages/image/package.json b/packages/image/package.json index 80f37e7..cadacdc 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -49,6 +49,7 @@ "@fepack/eslint-config-js": "workspace:*", "@fepack/eslint-config-ts": "workspace:*", "@fepack/tsconfig": "workspace:*", + "@types/node": "^20.6.2", "@vitest/browser": "^0.34.4", "@vitest/coverage-istanbul": "^0.34.4", "esbuild": "^0.18.11", diff --git a/packages/image/src/__test__/detect.test.ts b/packages/image/src/__test__/detect.test.ts new file mode 100644 index 0000000..53ff15d --- /dev/null +++ b/packages/image/src/__test__/detect.test.ts @@ -0,0 +1,33 @@ +import { Buffer } from "buffer"; +import { describe, expect, test } from "vitest"; +import { detect } from ".."; +import { FILE_TYPES } from "../detect"; + +describe("MIME type detection utility", () => { + test("should return null for unknown signatures", () => { + const unknownSignature = Buffer.from([0x00, 0x00, 0x00, 0x00]); + expect(detect(unknownSignature)).toBeNull(); + }); + + Object.keys(FILE_TYPES).forEach((key) => { + const { mime, signature } = FILE_TYPES[key]; + + test(`should detect ${key} files`, () => { + const signatureBuffer = Buffer.from(signature); + expect(detect(signatureBuffer)).toEqual(mime); + }); + }); + + test("should return null for partial signatures", () => { + const partialJPEGSignature = Buffer.from([0xff, 0xd8]); + expect(detect(partialJPEGSignature)).toBeNull(); + }); + + test("should detect MIME type even with extra data", () => { + const jpegWithExtraData = Buffer.concat([ + Buffer.from(FILE_TYPES.JPEG.signature), + Buffer.from([0x00, 0x01, 0x02]), + ]); + expect(detect(jpegWithExtraData)).toEqual(FILE_TYPES.JPEG.mime); + }); +}); diff --git a/packages/image/src/detect.ts b/packages/image/src/detect.ts new file mode 100644 index 0000000..90b6144 --- /dev/null +++ b/packages/image/src/detect.ts @@ -0,0 +1,57 @@ +/** + * Represents the file signatures for various MIME types. + * @see https://en.wikipedia.org/wiki/List_of_file_signatures + */ +export const FILE_TYPES = { + JPEG: { + mime: "image/jpeg", + signature: [0xff, 0xd8, 0xff], + }, + PNG: { + mime: "image/png", + signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], + }, + GIF: { + mime: "image/gif", + signature: [0x47, 0x49, 0x46, 0x38], + }, + WEBP: { + mime: "image/webp", + signature: [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50], + }, + SVG: { + mime: "image/svg+xml", + signature: [0x3c, 0x3f, 0x78, 0x6d, 0x6c], + }, + AVIF: { + mime: "image/avif", + signature: [0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66], + }, + ICO: { + mime: "image/x-icon", + signature: [0x00, 0x00, 0x01, 0x00], + }, +}; + +const FILE_TYPE_KEYS = Object.keys( + FILE_TYPES, +) as readonly (keyof typeof FILE_TYPES)[]; + +/** + * Inspects the first few bytes of a buffer to determine if + * it matches a known file signature. + * + * @param {Buffer} buffer - The buffer containing the file's first few bytes. + * @returns The detected MIME type or null if no known signature is matched. + */ +export function detect(buffer: Buffer) { + for (const key of FILE_TYPE_KEYS) { + const { mime, signature } = FILE_TYPES[key]; + + if (signature.every((byte, index) => byte === buffer[index])) { + return mime; + } + } + + return null; +} diff --git a/packages/image/src/index.ts b/packages/image/src/index.ts index c50a05d..6e14b09 100644 --- a/packages/image/src/index.ts +++ b/packages/image/src/index.ts @@ -1,2 +1,3 @@ export { default as checkWebPSupport } from "./checkWebPSupport"; +export { detect } from "./detect"; export { load, type ImageSource } from "./load"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb692f3..de6f5ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 5.0.1 turbo: specifier: latest - version: 1.10.13 + version: 1.10.14 typescript: specifier: 5.1.6 version: 5.1.6 @@ -108,6 +108,9 @@ importers: '@fepack/tsconfig': specifier: workspace:* version: link:../../configs/tsconfig + '@types/node': + specifier: ^20.6.2 + version: 20.6.2 '@vitest/browser': specifier: ^0.34.4 version: 0.34.4(rollup@2.79.1)(vitest@0.34.4) @@ -3791,6 +3794,10 @@ packages: /@types/node@18.17.16: resolution: {integrity: sha512-e0zgs7qe1XH/X3KEPnldfkD07LH9O1B9T31U8qoO7lqGSjj3/IrBuvqMeJ1aYejXRK3KOphIUDw6pLIplEW17A==} + /@types/node@20.6.2: + resolution: {integrity: sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==} + dev: true + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -3865,7 +3872,7 @@ packages: /@types/sax@1.2.4: resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==} dependencies: - '@types/node': 17.0.45 + '@types/node': 18.17.16 dev: false /@types/scheduler@0.16.3: @@ -11614,64 +11621,64 @@ packages: yargs: 17.7.2 dev: true - /turbo-darwin-64@1.10.13: - resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} + /turbo-darwin-64@1.10.14: + resolution: {integrity: sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.13: - resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} + /turbo-darwin-arm64@1.10.14: + resolution: {integrity: sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.13: - resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} + /turbo-linux-64@1.10.14: + resolution: {integrity: sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.13: - resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} + /turbo-linux-arm64@1.10.14: + resolution: {integrity: sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.13: - resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} + /turbo-windows-64@1.10.14: + resolution: {integrity: sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.13: - resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} + /turbo-windows-arm64@1.10.14: + resolution: {integrity: sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.13: - resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} + /turbo@1.10.14: + resolution: {integrity: sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.10.14 + turbo-darwin-arm64: 1.10.14 + turbo-linux-64: 1.10.14 + turbo-linux-arm64: 1.10.14 + turbo-windows-64: 1.10.14 + turbo-windows-arm64: 1.10.14 dev: true /typanion@3.14.0: