antiwork/gumroad · warning · ValidationError

Image must be square.

Error message

Image must be square.

What it means

Fourth check in validateFile: after a successful decode, height !== width throws ValidationError('Image must be square.') — product thumbnails are required to be perfectly square, independently of the 600px minimum side enforced on the next line. The check runs on actual decoded pixel dimensions, not metadata.

Source

Thrown at app/javascript/components/ProductEdit/ProductTab/ThumbnailEditor.tsx:39

const MIN_SIDE_DIMENSION = 600;
const MEGABYTE = 1024 * 1024;
const MAX_FILE_SIZE = 5 * MEGABYTE;
export class ValidationError extends Error {
  constructor(message = "Invalid file type.") {
    super(message);
  }
}

const validateFile = async (file: File) => {
  if (!FileUtils.isFileNameExtensionAllowed(file.name, ALLOWED_EXTENSIONS)) throw new ValidationError();

  if (file.size > MAX_FILE_SIZE)
    throw new ValidationError("Could not process your thumbnail, please upload an image with size smaller than 5 MB.");

  const dimensions = await getImageDimensionsFromFile(file).catch(() => null);
  if (!dimensions) throw new ValidationError();
  if (dimensions.height !== dimensions.width) throw new ValidationError("Image must be square.");

  if (dimensions.height < MIN_SIDE_DIMENSION) throw new ValidationError("Image must be at least 600x600px.");
};

export const coverUrlForThumbnail = (covers: AssetPreview[]) =>
  covers.find((cover) => cover.type === "image" || cover.type === "unsplash")?.url ?? null;

export const ThumbnailEditor = ({
  covers,
  thumbnail,
  setThumbnail,
  permalink,
  nativeType,
}: {
  covers: AssetPreview[];
  thumbnail: Thumbnail | null;
  setThumbnail: (thumbnail: Thumbnail | null) => void;
  permalink: string;

View on GitHub (pinned to afeacbd394)

Solutions

  1. Crop (not stretch) to square in any editor — center-crop is usually right for product shots.
  2. Automate: `magick input.jpg -gravity center -crop 1:1 +repage square.jpg`.
  3. Crop to at least 600x600 while squaring, or the next check (MIN_SIDE_DIMENSION) will reject it.
  4. Some editors' export presets have a 1:1 option — use it once and reuse the preset.

Example fix

// before — user uploads 1600x900 and only learns after upload
// after — offer an automatic center-crop instead of rejecting
const dims = await getImageDimensionsFromFile(file);
if (dims && dims.height !== dims.width) {
  const side = Math.min(dims.width, dims.height);
  file = await cropToSquare(file, side); // center-crop via canvas
}
const dimensions = await getImageDimensionsFromFile(file).catch(() => null);
Defensive patterns

Strategy: validation

Validate before calling

const isSquare = async (file: File): Promise<boolean> => {
  const dims = await getImageDimensionsFromFile(file).catch(() => null);
  return dims !== null && dims.width === dims.height;
};

if (!(await isSquare(file))) {
  const proceed = await confirm('Image is not square — center-crop it automatically?');
  if (proceed) file = await cropToSquare(file);
  else return;
}

Type guard

const isValidationError = (e: unknown): e is ValidationError => e instanceof ValidationError;

Try / catch

try {
  await validateFile(file);
} catch (e) {
  if (e instanceof ValidationError) {
    const actionable = e.message; // 'Image must be square.' / '...at least 600x600px.' etc.
    showAlert(actionable, 'error');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Uploading any non-square image — typical 3:2 or 16:9 camera/screenshot output, a 1080x1350 portrait, a banner-style 1500x500 crop — anything where height and width differ by even one pixel.

Common situations: Product photos straight off a phone camera; marketing banners repurposed as thumbnails; artwork cropped to the product's aspect ratio rather than 1:1; social images exported at 1200x630.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/e14299fc6bfd8119. Report an issue: GitHub.