image.spec.ts 4.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import sharp from 'sharp'
  2. import { describe, expect, it } from 'vitest'
  3. import { detectImage, probeImage } from '../src/image.ts'
  4. async function raster(format: 'png' | 'jpeg' | 'webp' | 'gif'): Promise<Uint8Array> {
  5. const image = sharp({
  6. create: { width: 3, height: 2, channels: 4, background: { r: 1, g: 2, b: 3, alpha: 1 } },
  7. })
  8. return new Uint8Array(await image.toFormat(format).toBuffer())
  9. }
  10. describe('raster decoding', () => {
  11. it('decodes every supported format and its intrinsic dimensions', async () => {
  12. for (const [format, mediaType] of [
  13. ['png', 'image/png'],
  14. ['jpeg', 'image/jpeg'],
  15. ['webp', 'image/webp'],
  16. ['gif', 'image/gif'],
  17. ] as const) {
  18. await expect(detectImage(await raster(format)))
  19. .resolves.toMatchObject({ mediaType, width: 3, height: 2, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' })
  20. }
  21. })
  22. it('rejects excess decoded pixels before decoding', async () => {
  23. await expect(detectImage(await raster('png'), { maxPixels: 5 }))
  24. .rejects.toMatchObject({ code: 'IMAGE_TOO_MANY_PIXELS' })
  25. })
  26. it('rejects a side above the per-side limit and accepts a side exactly at it', async () => {
  27. await expect(detectImage(await raster('png'), { maxDimension: 2 }))
  28. .rejects.toMatchObject({ code: 'IMAGE_DIMENSION_TOO_LARGE' })
  29. await expect(detectImage(await raster('png'), { maxDimension: 3 }))
  30. .resolves.toMatchObject({ mediaType: 'image/png', width: 3, height: 2, animated: false, carriesMetadata: false, depth: 'uchar', space: 'srgb' })
  31. })
  32. it('rejects malformed bytes and truncated payloads with readable headers', async () => {
  33. await expect(detectImage(Uint8Array.of(1, 2, 3)))
  34. .rejects.toMatchObject({ code: 'INVALID_IMAGE' })
  35. const unsupported = await sharp({
  36. create: { width: 1, height: 1, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 } },
  37. }).tiff().toBuffer()
  38. await expect(detectImage(unsupported)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
  39. const complete = await raster('png')
  40. const truncated = complete.subarray(0, 62)
  41. await expect(sharp(truncated).metadata()).resolves.toMatchObject({ width: 3, height: 2 })
  42. await expect(detectImage(truncated)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
  43. })
  44. it('reports animation from a multi-frame container and perceived axes from EXIF orientation', async () => {
  45. const header = Buffer.from('47494638396101000100800000000000ffffff', 'hex')
  46. const frame = Buffer.from('21f90401000000002c0000000001000100000202440100', 'hex')
  47. const twoFrameGif = Uint8Array.from(Buffer.concat([header, frame, frame, Buffer.from('3b', 'hex')]))
  48. await expect(detectImage(twoFrameGif)).resolves.toMatchObject({ mediaType: 'image/gif', animated: true })
  49. const oriented = new Uint8Array(await sharp({
  50. create: { width: 4, height: 2, channels: 3, background: { r: 1, g: 2, b: 3 } },
  51. }).jpeg().withMetadata({ orientation: 6 }).toBuffer())
  52. await expect(detectImage(oriented)).resolves.toMatchObject({
  53. mediaType: 'image/jpeg', width: 2, height: 4, animated: false, carriesMetadata: true,
  54. })
  55. const flipped = new Uint8Array(await sharp({
  56. create: { width: 4, height: 2, channels: 3, background: { r: 1, g: 2, b: 3 } },
  57. }).jpeg().withMetadata({ orientation: 3 }).toBuffer())
  58. await expect(detectImage(flipped)).resolves.toMatchObject({
  59. mediaType: 'image/jpeg', width: 4, height: 2, animated: false, carriesMetadata: true,
  60. })
  61. })
  62. it('reports color profiles and encoder metadata as metadata', async () => {
  63. const profiled = new Uint8Array(await sharp({
  64. create: { width: 4, height: 2, channels: 3, background: { r: 1, g: 2, b: 3 } },
  65. }).png().withIccProfile('p3').toBuffer())
  66. await expect(detectImage(profiled)).resolves.toMatchObject({ carriesMetadata: true })
  67. const commented = new Uint8Array(await sharp({
  68. create: { width: 4, height: 2, channels: 3, background: { r: 1, g: 2, b: 3 } },
  69. }).png().withMetadata().toBuffer())
  70. await expect(detectImage(commented)).resolves.toMatchObject({ carriesMetadata: true })
  71. })
  72. it('probes malformed bytes and unsupported formats into the same stable error', async () => {
  73. await expect(probeImage(Uint8Array.of(1, 2, 3)))
  74. .rejects.toMatchObject({ code: 'INVALID_IMAGE' })
  75. const unsupported = await sharp({
  76. create: { width: 1, height: 1, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 } },
  77. }).tiff().toBuffer()
  78. await expect(probeImage(unsupported)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
  79. })
  80. })