<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arif</title>
    <description>The latest articles on DEV Community by Arif (@arif_approvavisa).</description>
    <link>https://dev.to/arif_approvavisa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4164305%2Fb4a103f5-b1aa-437f-98fd-961c5879a99a.png</url>
      <title>DEV Community: Arif</title>
      <link>https://dev.to/arif_approvavisa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arif_approvavisa"/>
    <language>en</language>
    <item>
      <title>Pixels aren't millimetres: building accurate photo print layouts in JavaScript</title>
      <dc:creator>Arif</dc:creator>
      <pubDate>Wed, 21 Oct 2026 10:41:46 +0000</pubDate>
      <link>https://dev.to/arif_approvavisa/pixels-arent-millimetres-building-accurate-photo-print-layouts-in-javascript-305</link>
      <guid>https://dev.to/arif_approvavisa/pixels-arent-millimetres-building-accurate-photo-print-layouts-in-javascript-305</guid>
      <description>&lt;p&gt;I’m Arif Ali, founder of &lt;a href="https://fotocarnetya.com" rel="noopener noreferrer"&gt;Foto Carnet Ya&lt;/a&gt;. It helps people prepare photos for Spanish DNI, passports and TIE cards. Its free tools let users crop photos to the required dimensions and create print sheets without an account. The geometry in this article comes from that project.&lt;/p&gt;

&lt;p&gt;A photo can look correctly sized in a browser and still come out wrong on paper. A preview has pixels; a print layout has physical dimensions. Getting from one to the other requires an explicit resolution, consistent units, and a print workflow that preserves scale.&lt;/p&gt;

&lt;p&gt;This tutorial walks through the geometry behind a fixed-aspect photo crop and a sheet containing multiple copies. The examples use a 26 mm wide by 32 mm high photo as a layout input. That input is not a statement about which documents require a photo or whether a particular photo will be accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the physical model in millimetres
&lt;/h2&gt;

&lt;p&gt;Store paper dimensions, photo dimensions, margins and gaps in the same unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heightMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heightMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marginMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gapMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not substitute a 4 × 6 inch paper preset for a 10 × 15 cm preset. They are close, but different:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paper&lt;/th&gt;
&lt;th&gt;Width&lt;/th&gt;
&lt;th&gt;Height&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 × 15 cm&lt;/td&gt;
&lt;td&gt;100 mm&lt;/td&gt;
&lt;td&gt;150 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 × 6 inches&lt;/td&gt;
&lt;td&gt;101.6 mm&lt;/td&gt;
&lt;td&gt;152.4 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction matters when a photo's printed size is part of the output contract. Give each paper size its own preset and label.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Convert to pixels at the rendering boundary
&lt;/h2&gt;

&lt;p&gt;There are exactly 25.4 millimetres in an inch. For a chosen raster resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mmToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mmToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 307&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mmToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 378&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mmToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 1181&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mmToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 1772&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;dpi&lt;/code&gt; names the intended print resolution; for a raster image, pixels per inch is the more precise term. It does not describe a printer's individual ink dots.&lt;/p&gt;

&lt;p&gt;The resulting dimensions are approximations because pixels are whole numbers. One pixel at 300 pixels per inch represents about 0.085 mm.&lt;/p&gt;

&lt;p&gt;Keep the layout in millimetres until drawing. Repeatedly converting back and forth between rounded pixel measurements adds avoidable rounding error.&lt;/p&gt;

&lt;p&gt;Increasing the output pixel count also does not recover detail missing from a low-resolution source image.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Crop to the target aspect ratio before drawing
&lt;/h2&gt;

&lt;p&gt;Drawing a full camera image directly into a 307 × 378 rectangle can stretch the person's face. Crop the source to the target aspect ratio first.&lt;/p&gt;

&lt;p&gt;For positive source dimensions and a positive aspect ratio, the largest matching rectangle inside the source is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fitCrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aspect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;aspect&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;aspect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fitCrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { x: 0, y: approximately 153.85, width: 3000,&lt;/span&gt;
&lt;span class="c1"&gt;//   height: approximately 3692.31 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a centred geometric crop. A portrait editor may let the user move it to position their head properly. Keep those crop coordinates in source-image pixels, and clamp the rectangle inside the image after each movement.&lt;/p&gt;

&lt;p&gt;The calculation preserves shape. It does not check head position, expression, lighting or document-specific acceptance rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Calculate how many copies fit
&lt;/h2&gt;

&lt;p&gt;For one axis, the capacity is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;floor((paper size - 2 × margin + gap) / (photo size + gap))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why add the gap in the numerator? Because &lt;code&gt;n&lt;/code&gt; photos need only &lt;code&gt;n - 1&lt;/code&gt; gaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n × photo size + (n - 1) × gap &amp;lt;= paper size - 2 × margin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rearranging gives the formula above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gridCapacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;paperWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paperHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;photoWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;photoHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;paperWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photoWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;paperHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photoHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;gridCapacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { columns: 3, rows: 4, capacity: 12 }&lt;/span&gt;

&lt;span class="nf"&gt;gridCapacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { columns: 5, rows: 2, capacity: 10 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evaluate both paper orientations. Landscape does not necessarily fit more copies. These examples assume positive finite dimensions and non-negative margins and gaps; validate user input before calling the function.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Centre the grid independently of the canvas
&lt;/h2&gt;

&lt;p&gt;For a full grid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usedWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;photoWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usedHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;photoHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paperWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;usedWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paperHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;usedHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;columnIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photoWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rowIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photoHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only convert these positions to pixels when calling &lt;code&gt;drawImage&lt;/code&gt;. For a partial grid, explicitly decide whether to centre the grid's bounding box or centre each row separately. Those are different layouts, especially when the last row is incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. An exported image still needs a print-size contract
&lt;/h2&gt;

&lt;p&gt;A canvas with 1181 × 1772 pixels does not force every print application to use 100 × 150 mm paper at the intended scale.&lt;/p&gt;

&lt;p&gt;The export and printing steps need to agree on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The intended paper dimensions.&lt;/li&gt;
&lt;li&gt;The raster resolution, including density metadata where the format supports it.&lt;/li&gt;
&lt;li&gt;Whether the print application applies scaling, cropping or borderless enlargement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this project's PNG download path, the renderer creates the canvas at 300 pixels per inch, then a separate export helper writes a PNG &lt;code&gt;pHYs&lt;/code&gt; chunk declaring its physical density. Metadata can communicate intent, but a print application may still override it.&lt;/p&gt;

&lt;p&gt;For verification, select the matching paper size, avoid automatic enlargement, and measure a printed copy with a ruler. The on-screen preview cannot prove the final physical dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test the geometry without a browser
&lt;/h2&gt;

&lt;p&gt;The useful boundary is between a pure layout function and a renderer. The layout function returns dimensions and positions; the renderer consumes them.&lt;/p&gt;

&lt;p&gt;That makes these properties testable without React, canvas or a printer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every crop stays inside the source image and retains the target aspect ratio.&lt;/li&gt;
&lt;li&gt;Every photo stays within the paper's configured margins.&lt;/li&gt;
&lt;li&gt;No two photos overlap.&lt;/li&gt;
&lt;li&gt;Impossible copy counts are rejected.&lt;/li&gt;
&lt;li&gt;Exact metric paper sizes remain distinct from inch-based presets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, for every returned position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;photoWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;paperWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;photoHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;paperHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a small numerical tolerance when calculations involve floating-point values. These checks protect layout correctness, while a separate physical print check verifies the complete output workflow.&lt;/p&gt;

&lt;p&gt;The same separation is useful for labels, badges, tickets and other browser-generated assets that must have a predictable size on paper.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was drafted with AI assistance from the author's existing project code. The examples focus on geometry and printing; they do not establish compliance with any identity-document requirements.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A passport photo tool taught me that “processing complete” isn’t enough</title>
      <dc:creator>Arif</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:49:35 +0000</pubDate>
      <link>https://dev.to/arif_approvavisa/a-passport-photo-tool-taught-me-that-processing-complete-isnt-enough-4l5f</link>
      <guid>https://dev.to/arif_approvavisa/a-passport-photo-tool-taught-me-that-processing-complete-isnt-enough-4l5f</guid>
      <description>&lt;p&gt;A passport photo tool sounds like a fairly small project.&lt;/p&gt;

&lt;p&gt;Upload an image. Crop it. Prepare the background. Export the right dimensions.&lt;/p&gt;

&lt;p&gt;Then someone uploads a perfectly reasonable photo, another country’s workflow handles it, and the India flow keeps asking them to retake it.&lt;/p&gt;

&lt;p&gt;That’s a problem I’ve been working through while building &lt;a href="https://approvavisa.com" rel="noopener noreferrer"&gt;ApprovaVisa&lt;/a&gt;, a tool for preparing passport and visa photos online.&lt;/p&gt;

&lt;p&gt;Getting the image to process was only part of the work. The other part was making sure the person using the tool could understand what had happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Please retake your photo” doesn’t explain much
&lt;/h2&gt;

&lt;p&gt;From the user’s side, an unchanged preview and a repeated retake message look like a broken upload.&lt;/p&gt;

&lt;p&gt;That’s a reasonable conclusion.&lt;/p&gt;

&lt;p&gt;If processing stops, the interface needs to explain why. Is the source photo unsuitable? Is there a requirement the tool cannot meet? Did something fail during processing?&lt;/p&gt;

&lt;p&gt;Those situations need different responses. Asking someone to keep uploading another photo won’t help if the problem is in the application.&lt;/p&gt;

&lt;p&gt;This experience made me pay closer attention to the information we show when a workflow cannot continue. An error message should give someone a useful next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  A specification change touches more than the backend
&lt;/h2&gt;

&lt;p&gt;Updating a photo requirement sounds simple until you follow it through the product.&lt;/p&gt;

&lt;p&gt;The details page explains the requirements. The processing flow applies them. The preview shows the result. The downloaded file needs to match what was promised.&lt;/p&gt;

&lt;p&gt;If those parts disagree, the user has no easy way to know which one is correct.&lt;/p&gt;

&lt;p&gt;While updating the India workflow, I had to think about those pieces together. Changing a dimension in one place is only useful if the rest of the experience follows it.&lt;/p&gt;

&lt;p&gt;It also matters how we describe the result. A tool can prepare an image and check certain properties, but the receiving authority makes the final acceptance decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A working backend can still look broken
&lt;/h2&gt;

&lt;p&gt;After the processing issue, we had a smaller but very visible problem.&lt;/p&gt;

&lt;p&gt;The scanning interface had stage numbers, progress percentages, and preview controls overlapping on the image.&lt;/p&gt;

&lt;p&gt;Each element had a purpose. Together, they were difficult to read.&lt;/p&gt;

&lt;p&gt;This was happening at the exact moment someone wanted to inspect their photo and understand whether processing was going well.&lt;/p&gt;

&lt;p&gt;The layout needed clearer spacing and predictable positions. The overlays could stay on the image, but they needed room to do their jobs.&lt;/p&gt;

&lt;p&gt;It was a useful reminder: a successful backend response doesn’t automatically produce a clear user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transitions deserve more attention
&lt;/h2&gt;

&lt;p&gt;I’m now looking more carefully at the moments between steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload to processing.&lt;/li&gt;
&lt;li&gt;Processing to preview.&lt;/li&gt;
&lt;li&gt;Preview to download.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At each transition, the user should be able to answer a few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did my photo actually change?&lt;/li&gt;
&lt;li&gt;What did the tool do?&lt;/li&gt;
&lt;li&gt;Is there anything I still need to check?&lt;/li&gt;
&lt;li&gt;What will I receive if I continue?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions sound obvious when written down. They’re surprisingly easy to miss while concentrating on the processing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project is now
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://approvavisa.com" rel="noopener noreferrer"&gt;ApprovaVisa&lt;/a&gt; lets users select their country and document type, upload a photo, and preview the prepared result before payment. Downloads include a digital photo and an A4 print sheet.&lt;/p&gt;

&lt;p&gt;I’m continuing to improve the experience around that workflow, especially the explanations and previews.&lt;/p&gt;

&lt;p&gt;Building it has reminded me that even a focused tool can carry complicated expectations. Someone isn’t just testing an image processor—they’re trying to finish an application and move on with their day.&lt;/p&gt;

&lt;p&gt;Have you built something where the backend worked, but the interface made it look broken? What helped you spot the problem?&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobpi46b9idbe6zm2id3v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobpi46b9idbe6zm2id3v.jpg" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k03aa0dwrw2uijsx4q3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k03aa0dwrw2uijsx4q3.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>ux</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
