{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "square-image",
  "type": "registry:lib",
  "title": "Square image",
  "description": "Square crops of a picked picture (centre and zoom in source pixels) and their PNG encoding in the browser, for avatars and logos.",
  "files": [
    {
      "path": "src/lib/square-image.ts",
      "type": "registry:lib",
      "content": "/*\n * Square crops of an uploaded picture (a person's avatar, a tenant's logo),\n * done in the browser: the server then receives a small PNG of a known size\n * and never decodes an arbitrary image. The crop is described in source\n * pixels by its centre and a zoom (1 = the largest square that fits), so the\n * crop editor and the encoder agree whatever size the preview is drawn at.\n */\n\n/** Picture types the picker accepts; everything is re-encoded as PNG. */\nexport const SQUARE_IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"];\n\n/** Side of the PNG the encoder produces by default. */\nexport const SQUARE_IMAGE_SIZE = 256;\n\n/** Largest zoom the editor offers. */\nexport const MAX_SQUARE_ZOOM = 4;\n\nexport interface SquareCrop {\n  /** Centre of the square, in source pixels. */\n  centerX: number;\n  centerY: number;\n  /** 1 shows the largest square that fits; 2 shows half its side. */\n  zoom: number;\n}\n\nexport interface SquareRect {\n  x: number;\n  y: number;\n  side: number;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\n/** The centred crop of the largest square: what an upload without editing gives. */\nexport function centredCrop(width: number, height: number): SquareCrop {\n  return { centerX: width / 2, centerY: height / 2, zoom: 1 };\n}\n\n/** The source rectangle of a crop, kept inside the picture. */\nexport function cropRect(width: number, height: number, crop: SquareCrop): SquareRect {\n  const zoom = clamp(crop.zoom, 1, MAX_SQUARE_ZOOM);\n  const side = Math.min(width, height) / zoom;\n  const x = clamp(crop.centerX - side / 2, 0, width - side);\n  const y = clamp(crop.centerY - side / 2, 0, height - side);\n  return { x, y, side };\n}\n\n/** The same crop with its centre moved back inside the picture (after a drag or a zoom out). */\nexport function clampCrop(width: number, height: number, crop: SquareCrop): SquareCrop {\n  const rect = cropRect(width, height, crop);\n  return { centerX: rect.x + rect.side / 2, centerY: rect.y + rect.side / 2, zoom: clamp(crop.zoom, 1, MAX_SQUARE_ZOOM) };\n}\n\n/** Moves a crop by a drag of `dx`, `dy` screen pixels over a preview `viewport` pixels wide. */\nexport function panCrop(width: number, height: number, crop: SquareCrop, dx: number, dy: number, viewport: number): SquareCrop {\n  const { side } = cropRect(width, height, crop);\n  const scale = side / viewport;\n  return clampCrop(width, height, { ...crop, centerX: crop.centerX - dx * scale, centerY: crop.centerY - dy * scale });\n}\n\n/** Encodes the crop of a decoded picture as a square PNG of `size` pixels. */\nexport async function encodeSquarePng(image: CanvasImageSource & { width: number; height: number }, crop: SquareCrop, size = SQUARE_IMAGE_SIZE): Promise<Blob> {\n  const rect = cropRect(image.width, image.height, crop);\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = size;\n  canvas.height = size;\n  const context = canvas.getContext(\"2d\");\n  if (!context) throw new Error(\"Canvas not available\");\n  context.imageSmoothingQuality = \"high\";\n  context.drawImage(image, rect.x, rect.y, rect.side, rect.side, 0, 0, size, size);\n  return new Promise((resolve, reject) => canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error(\"Encoding failed\"))), \"image/png\"));\n}\n\n/** Decodes a picked file and encodes its centred square as a PNG, without an editor. */\nexport async function squarePng(file: Blob, size = SQUARE_IMAGE_SIZE): Promise<Blob> {\n  const bitmap = await createImageBitmap(file);\n  try {\n    return await encodeSquarePng(bitmap, centredCrop(bitmap.width, bitmap.height), size);\n  } finally {\n    bitmap.close();\n  }\n}\n"
    }
  ]
}
