DEV Community

Jason T.
Jason T.

Posted on

Convert photo sizes from millimetres to pixels without mixing up upload rules

China's 33 × 48 mm paper photo converts to 390 × 567 px at 300 dpi. Its separate digital upload rule is 354 × 472 to 420 × 560 px. Use Math.round(mm / 25.4 * dpi) for print conversions. Validate upload dimensions and head height against their own rules.

The official print and pixel sizes for all 21 documents are in pixtidy's requirements table (disclosure: I built it).

How do you convert millimetres to pixels in JavaScript?

  1. Pass the physical measurement and dpi explicitly.
function mmToPixels(mm, dpi) {
  return Math.round(mm / 25.4 * dpi);
}

mmToPixels(33, 300); // 390
mmToPixels(48, 300); // 567
Enter fullscreen mode Exit fullscreen mode

Apply the function independently to width and height. Round at the end, not during the division.

Keep this function narrow. Giving it file-format or head-height logic would hide which requirement you are actually testing.

Calculate each resolution from the original millimetres. Schengen's 35 mm width rounds to 413 px at 300 dpi, but doubling that rounded result gives 826 px. Converting 35 mm directly at 600 dpi gives 827 px.

That one-pixel difference is a useful regression test for code that generates multiple print resolutions.

What sizes should your conversion tests expect?

  1. Keep the physical input beside each expected result.
Print photo Physical size Dpi Converted size
Schengen visa 35 × 45 mm 300 413 × 531 px
Schengen visa 35 × 45 mm 600 827 × 1063 px
US passport 2 × 2 in 300 600 × 600 px
US passport 2 × 2 in 600 1200 × 1200 px
Chinese visa 33 × 48 mm 300 390 × 567 px

Use the millimetre rows as fixtures for mmToPixels. The US passport print size is specified in inches: 2 × 2 in.

The DS-160 digital rule also uses 600 × 600 to 1200 × 1200 px. Store those as direct pixel constraints, independently of the US passport print calculation.

How should you model print, upload, and head-height requirements?

  1. Give each requirement its own scope and unit.

A compact model can preserve the original measurements without turning every rule into pixels up front:

const requirements = {
  chinaVisa: {
    print: { unit: "mm", width: 33, height: 48 },
    upload: {
      widthPx: [354, 420],
      heightPx: [472, 560],
      format: "JPEG",
      sizeKB: [40, 120]
    },
    head: { scope: "print", unit: "mm", range: [28, 33] }
  },
  schengenVisa: {
    print: { unit: "mm", width: 35, height: 45 },
    head: {
      scope: "print",
      unit: "mm",
      range: [32, 36],
      measurement: "chin-to-crown"
    }
  },
  usPassport: {
    print: { unit: "in", width: 2, height: 2 }
  },
  ds160: {
    upload: {
      widthPx: [600, 1200],
      heightPx: [600, 1200],
      square: true,
      sizeKB: [null, 240]
    },
    head: {
      scope: "digital",
      unit: "percent",
      range: [50, 69]
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

The DS-160 file-size rule is 240 KB or less. China's digital file-size rule is 40–120 KB.

The conversion formula takes dpi as an input. That lets you generate Schengen output at 300 or 600 dpi from the same physical dimensions.

The scope field matters for China's head-height requirement. Its 28–33 mm measurement belongs to the 48 mm paper photo.

How can you validate digital dimensions and file limits?

  1. Return individual failures instead of a single unexplained boolean.

This validator takes normalized metadata. sizeKB is already expressed in KB, and format uses the model's "JPEG" label.

function validateUpload(rule, photo) {
  const errors = [];
  const within = (value, [min, max]) =>
    (min === null || value >= min) && value <= max;

  if (!within(photo.widthPx, rule.widthPx)) {
    errors.push("width");
  }
  if (!within(photo.heightPx, rule.heightPx)) {
    errors.push("height");
  }
  if (rule.square && photo.widthPx !== photo.heightPx) {
    errors.push("square");
  }
  if (rule.format && photo.format !== rule.format) {
    errors.push("format");
  }
  if (!within(photo.sizeKB, rule.sizeKB)) {
    errors.push("file-size");
  }

  return errors;
}
Enter fullscreen mode Exit fullscreen mode

China's upload limits are 354 × 472 to 420 × 560 px, JPEG, and 40–120 KB. The 300 dpi print size is separate from the digital upload size.

DS-160 has a 240 KB ceiling.

Treat these results as metadata checks, not a promise of acceptance. Head height still needs its own measurement.

Which boundary tests catch the China mismatch?

  1. Test rejected inputs as well as the exact allowed boundaries.

Run this after the function and model above. Each test throws if the validator returns an unexpected result.

function expectErrors(name, rule, photo, expected) {
  const actual = validateUpload(rule, photo);
  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
    throw new Error(`${name}: got ${JSON.stringify(actual)}`);
  }
}

const china = requirements.chinaVisa.upload;
const base = {
  widthPx: 390,
  heightPx: 560,
  format: "JPEG",
  sizeKB: 40
};

const cases = [
  ["minimum dimensions", { widthPx: 354, heightPx: 472 }, []],
  ["maximum dimensions", { widthPx: 420, heightPx: 560 }, []],
  ["width below minimum", { widthPx: 353 }, ["width"]],
  ["width above maximum", { widthPx: 421 }, ["width"]],
  ["height below minimum", { heightPx: 471 }, ["height"]],
  ["height above maximum", { heightPx: 561 }, ["height"]],
  ["print conversion", {
    widthPx: mmToPixels(33, 300),
    heightPx: mmToPixels(48, 300)
  }, ["height"]],
  ["below 40 KB", { sizeKB: 39 }, ["file-size"]],
  ["at 40 KB", { sizeKB: 40 }, []],
  ["at 120 KB", { sizeKB: 120 }, []],
  ["above 120 KB", { sizeKB: 121 }, ["file-size"]],
  ["wrong format", { format: "PNG" }, ["format"]]
];

for (const [name, patch, expected] of cases) {
  expectErrors(name, china, { ...base, ...patch }, expected);
}

const ds160 = requirements.ds160.upload;
for (const [sizeKB, expected] of [
  [240, []],
  [241, ["file-size"]]
]) {
  expectErrors(`DS-160 ${sizeKB} KB`, ds160, {
    widthPx: 600,
    heightPx: 600,
    sizeKB
  }, expected);
}
Enter fullscreen mode Exit fullscreen mode

These tests make the inclusive boundaries visible.

Keep the print-conversion case even if you already test 561 px. It catches a different implementation mistake: populating upload dimensions from a correct print calculation.

How do you test head height without mixing units?

  1. Schengen specifies head height in millimetres; DS-160 specifies a percentage of image height.

For Schengen, chin-to-crown height is 32–36 mm. At 300 dpi, the rounded range is 378–425 px.

DS-160 instead requires 50–69% of image height:

const schengenHead300 =
  requirements.schengenVisa.head.range.map(
    mm => mmToPixels(mm, 300)
  ); // [378, 425]

function ds160HeadPasses(headPx, imageHeightPx) {
  const percent = headPx / imageHeightPx * 100;
  const [min, max] = requirements.ds160.head.range;
  return percent >= min && percent <= max;
}
Enter fullscreen mode Exit fullscreen mode

China's JPEG requirement and both file-size bounds remain separate.

Head-height numbers need units and scope. Schengen's range is 32–36 mm, while DS-160's range is 50–69% of image height.

Where can you find document-specific official sources?

  1. Review the document's source before updating its fixture.

pixtidy's requirements table lists print sizes, digital file rules, head height, and official-source links for 21 documents.

Use the relevant document's official-source link when reviewing a change. Then update the matching model field and its boundary tests.

Related: Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks · Why a DS-160 photo can be too small, and how I test it in the browser

Disclosure: I built pixtidy.

Top comments (0)