# Validate Portrait Export Ratios with Integer Cross-Products in Node.js

A portrait export can have the right filename and still use the wrong canvas shape. If your application stores an intended ratio beside export dimensions, a small metadata check can catch a square file sent to a vertical slot before the next handoff.

## 1\. Keep the export target explicit

Use width and height from the final, orientation-corrected export. Keep the target ratio as a pair of positive integers, such as \[1, 1\], \[4, 5\] or \[3, 4\]. These are illustrative portrait layout choices, not document-photo specifications or rules for any particular service.

Do not infer the intended destination from the measured ratio. A square image can serve many purposes. The caller supplies both the intended target and the measured dimensions; this checker only compares them.

## 2\. Compare integer cross-products

For a target a:b, an exact match means width × b equals height × a. This avoids deciding exact equality from a rounded decimal such as 0.8. Convert validated safe integers to BigInt before multiplying so the products remain exact even when their size exceeds Number.MAX_SAFE_INTEGER.

For optional tolerance, define the relative error as abs(width × b − height × a) / (height × a). A tolerance in parts per million accepts the result when the numerator times 1,000,000 is no greater than the denominator times the tolerance. Zero tolerance requires an exact match. This definition is relative to the target ratio; it is not an error percentage for width and height independently.

Save this dependency-free module as ratio-check.mjs. Its returned fraction uses strings so JSON.stringify can serialize the result without a BigInt replacer.

```javascript
export function checkRatio(width, height, ratio, tolerancePpm = 0) {
  const positive = n => Number.isSafeInteger(n) && n > 0;
  if (!positive(width) || !positive(height) ||
      !Array.isArray(ratio) || ratio.length !== 2 ||
      !ratio.every(positive)) {
    throw new TypeError('Use positive safe integers for dimensions and ratio');
  }
  if (!Number.isSafeInteger(tolerancePpm) ||
      tolerancePpm < 0 || tolerancePpm > 1_000_000) {
    throw new TypeError('tolerancePpm must be an integer from 0 to 1000000');
  }
  const [a, b] = ratio.map(BigInt);
  const actual = BigInt(width) * b;
  const expected = BigInt(height) * a;
  const delta = actual - expected;
  const absolute = delta < 0n ? -delta : delta;
  return {
    width, height, target: ratio.join(':'),
    exact: delta === 0n,
    withinTolerance: absolute * 1_000_000n <=
      BigInt(tolerancePpm) * expected,
    direction: delta === 0n ? 'exact' : delta > 0n ? 'too-wide' : 'too-tall',
    relativeError: {
      numerator: absolute.toString(), denominator: expected.toString()
    }
  };
}
```

## 3\. Describe three synthetic exports

Save the following as jobs.json. The first export is exactly square. The second is close to 4:5, with an application-chosen tolerance of 1000 ppm, equivalent to 0.1 percent. The third is a wide export compared against a vertical 3:4 target.

```json
[
  {
    "id": "square-avatar",
    "width": 1200,
    "height": 1200,
    "ratio": [
      1,
      1
    ]
  },
  {
    "id": "portrait-preview",
    "width": 800,
    "height": 1001,
    "ratio": [
      4,
      5
    ],
    "tolerancePpm": 1000
  },
  {
    "id": "wide-export",
    "width": 1600,
    "height": 900,
    "ratio": [
      3,
      4
    ]
  }
]
```

For the second row, 800 × 5 = 4000 and 1001 × 4 = 4004. Its relative error is 4/4004. At 1000 ppm, 4,000,000 ≤ 4,004,000, so withinTolerance is true while exact remains false. At 999 ppm the same row fails because 4,000,000 is greater than 3,999,996.

## 4\. Add a command-line check

Save this as ratio-cli.mjs beside the module. The script reads a nonempty JSON array, evaluates each record and prints the complete report. Exit 0 means every record meets its configured ratio tolerance, exit 1 means at least one valid record misses it, and exit 2 means the command or input is invalid.

```javascript
import { readFile } from 'node:fs/promises';
import { checkRatio } from './ratio-check.mjs';

try {
  if (process.argv.length !== 3) throw new Error('Usage: node ratio-cli.mjs jobs.json');
  const jobs = JSON.parse(await readFile(process.argv[2], 'utf8'));
  if (!Array.isArray(jobs) || jobs.length === 0) {
    throw new Error('jobs must be a nonempty array');
  }
  const results = jobs.map(job => {
    if (!job || typeof job !== 'object' ||
        typeof job.id !== 'string' || !job.id.trim()) {
      throw new Error('Each job needs a nonempty string id');
    }
    return { id: job.id, ...checkRatio(job.width, job.height,
      job.ratio, job.tolerancePpm ?? 0) };
  });
  console.log(JSON.stringify(results, null, 2));
  process.exitCode = results.every(r => r.withinTolerance) ? 0 : 1;
} catch (error) {
  console.error(error.message);
  process.exitCode = 2;
}
```

All records are validated before a report is printed. An invalid record produces exit 2 rather than a partial success report. The nonempty id connects each result to the caller’s manifest; unique-id enforcement can be added if the surrounding application requires it.

## 5\. Read exact and tolerated results separately

```bash
node ratio-cli.mjs jobs.json
```

The synthetic fixture returns withinTolerance values true, true and false, and exits 1. The portrait-preview row remains an inexact result. Keep both flags in any review UI so a tolerated mismatch is not relabeled as an exact export.

A separate boundary fixture of 1001 × 1000 against 1:1 lands exactly at 1000 ppm: it passes at 1000 and fails at 999. Also check zero, fractional and unsafe-integer dimensions, invalid ratio terms and negative tolerance. Do not silently round or swap dimensions to make an invalid input pass.

The code was run with Node.js 24.20.0 against 22 synthetic checks, including the examples, large integer products, input preservation and independent command-line exits 0, 1 and 2. These checks use local JSON and numbers; no portrait file was decoded, cropped, generated or visually inspected.

## 6\. Put the check at the export boundary

Run this after your image pipeline has applied orientation and produced its final dimensions. Record those dimensions with the target ratio and tolerance. The checker trusts the supplied numbers, so reading an unrotated source header while handing off a rotated export would check the wrong geometry.

A matching ratio does not establish pixel resolution, headroom, background, likeness or suitability for a destination. Keep those separate from this arithmetic result. If a receiver specifies exact width and height, compare those dimensions explicitly as well; 400 × 500 and 800 × 1000 share 4:5 but are different exports.

Drafted with AI assistance. The working code and synthetic checks were executed locally. No actual ID-photo or professional-headshot workflow was tested.

PixMind source: [ID photo and professional headshot purposes](https://www.pixmind.io/posts/id-photo-vs-professional-headshot). This developer adaptation focuses on ratio metadata rather than choosing a photo for an application.

JavaScript reference: [MDN BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).
