{"record":{"id":"444ed354c5157629","repo":"paperclipai/paperclip","slug":"decoded-image-exceeds-the-pixel-limit","errorCode":null,"errorMessage":"Decoded image exceeds the pixel limit","messagePattern":"Decoded image exceeds the pixel limit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/photon/media.ts","lineNumber":88,"sourceCode":"    throw new Error(\"HEIF dimensions could not be verified\");\n}\n\nexport async function validatePhotonImage(\n  body: Buffer,\n  contentType: string,\n): Promise<void> {\n  if (!contentType.startsWith(\"image/\")) return;\n  if (HEIF_CONTENT_TYPES.has(contentType)) return validateHeifDimensions(body);\n  const metadata = await sharp(body, {\n    limitInputPixels: MAX_PIXELS,\n    failOn: \"error\",\n  }).metadata();\n  if (\n    !metadata.width ||\n    !metadata.height ||\n    metadata.width * metadata.height * (metadata.pages ?? 1) > MAX_PIXELS\n  )\n    throw new Error(\"Decoded image exceeds the pixel limit\");\n  const formats: Record<string, string[]> = {\n    \"image/jpeg\": [\"jpeg\"],\n    \"image/jpg\": [\"jpeg\"],\n    \"image/png\": [\"png\"],\n    \"image/webp\": [\"webp\"],\n    \"image/gif\": [\"gif\"],\n  };\n  if (!formats[contentType]?.includes(metadata.format ?? \"\"))\n    throw new Error(\"Image bytes do not match the declared content type\");\n}\n\n/** Isolate the native converter with a deadline and bounded input/output.\n * Native packages exist for macOS, Windows and Linux glibc x64/arm64.\n * Unsupported hosts retain the original and report an unavailable preview. */\nexport async function photonHeifPreview(body: Buffer): Promise<Buffer> {\n  validateHeifDimensions(body);\n  const modulePath = require.resolve(\"heif2jpeg\");\n  const script = `const {heifToJpeg}=require(process.argv[1]);const chunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',async()=>{try{const jpeg=await heifToJpeg(Buffer.concat(chunks),{quality:80});process.stdout.end(jpeg);}catch{process.exitCode=1;}});`;","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/media.ts#L70-L106","documentation":"validatePhotonImage decodes metadata via sharp (with limitInputPixels: MAX_PIXELS and failOn: 'error') and throws when width/height are missing or width*height*pages exceeds MAX_PIXELS (50,000,000). This rejects decoded images that are too large — including animated/multi-page formats where total pixels scale by page count.","triggerScenarios":"Calling validatePhotonImage (from ingestAttachments or photonHeifPreview's re-validation of converted JPEG) with a non-HEIF image/* buffer whose decoded dimensions exceed 50MP, or an animated GIF/WebP whose frame count times frame size exceeds 50MP; also zero-dimension metadata from corrupt images.","commonSituations":"Users uploading 60MP+ DSLR panoramas; large animated GIFs with hundreds of frames; corrupt images where sharp returns no width/height; screenshots at extreme resolutions.","solutions":["Downscale client-side before upload (e.g. browser canvas or sharp resize to max 8192px) to stay under 50MP.","If it's an animated GIF/WebP, trim frames or convert to a video/shorter animation so width*height*pages fits the budget.","If larger images are a legitimate product need, raise MAX_PIXELS in media.ts (and note sharp's memory implications).","Catch this error in ingestAttachments and return a 413/422 with a user-facing 'image resolution too high' message rather than failing the whole request."],"exampleFix":"// before\nawait validatePhotonImage(rawBuffer, 'image/png'); // 80MP panorama -> throws\n// after\nconst resized = await sharp(rawBuffer).resize({ width: 8000, withoutEnlargement: true }).png().toBuffer();\nawait validatePhotonImage(resized, 'image/png');","handlingStrategy":"validation","validationCode":"import sharp from 'sharp';\nexport async function isWithinPixelLimit(body: Buffer): Promise<boolean> {\n  const meta = await sharp(body, { limitInputPixels: 50_000_000 }).metadata().catch(() => null);\n  if (!meta?.width || !meta?.height) return false;\n  return meta.width * meta.height * (meta.pages ?? 1) <= 50_000_000;\n}","typeGuard":"function hasBoundedDecodedSize(m: { width?: number; height?: number; pages?: number }): boolean {\n  return typeof m.width === 'number' && typeof m.height === 'number' &&\n    m.width > 0 && m.height > 0 &&\n    m.width * m.height * (m.pages ?? 1) <= 50_000_000;\n}","tryCatchPattern":"try {\n  await validatePhotonImage(body, contentType);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Decoded image exceeds the pixel limit') {\n    const downscaled = await sharp(body).resize({ width: 8192, withoutEnlargement: true }).toBuffer();\n    return validatePhotonImage(downscaled, contentType);\n  }\n  throw err;\n}","preventionTips":["Downscale large uploads client-side before sending","Remember animated formats count pages: cap frame count for GIF/WebP","Check dimensions in the browser via createImageBitmap before upload","Keep sharp's limitInputPixels and the MAX_PIXELS check consistent"],"tags":["image","sharp","pixel-limit","validation"],"backgroundTag":"file-size-limit-exceeded","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}